Sending a Template
Business view
When a WhatsApp conversation with a patient goes quiet for 24 hours, WhatsApp locks the clinic out of replying normally. Before this PR, that was the end of the conversation from the clinic's side — the "Conversation Expired" banner just sat there until the patient wrote back.
Now, next to that banner, staff see a Send Template button. Clicking it opens a small picker:
- Search for one of the clinic's existing messages (the same messages used for appointment reminders, invoices,
etc.).
- See a live preview of what the patient will actually receive, with their name already filled in.
- If the message needs more than just a name — a date, a doctor, an amount — the picker shows exactly what's
missing and refuses to let the message send. There is no "send it anyway" for a half-filled message.
- Once an eligible message is selected, Send delivers it and the message appears at the bottom of the
conversation like any other reply.
The patient's reply to that template reopens the normal 24-hour window, so from that point on the conversation goes back to working exactly as it did before.
Technical view
Entry points
Both apps thread the conversation id and the contact's display name down to the button, then hand off to a picker screen.
- Mobile.
ConversationMessagesScreenpassesconversationandcontactNameintoWhatsAppChatHeader
(packages/clinic-mobile/src/components/dashboard/WhatsApp/conversationMessages/ConversationMessagesScreen.js:133-139). When expired is true and a conversation id is present, the header renders a Send Template button that navigates to the new SendWhatsappTemplate route (packages/clinic-mobile/src/components/dashboard/WhatsApp/conversationMessages/WhatsAppChatHeader.js:39-53), registered against SendTemplateScreen in the navigation stack (packages/clinic-mobile/src/components/dashboard/DashboardStack.js:95,512).
- Web.
Chats.jsrendersSendTemplateButtonnext to the expired banner, passing the full
onlineConversationDetails object and a scrollToBottom callback (packages/clinic-web/src/components/dashboard/officialWhatsapp/conversations/Chats.js:143-148). The button is disabled unless the current user has the sendWhatsApp permission flag (packages/clinic-web/src/components/dashboard/officialWhatsapp/conversations/SendTemplateButton.js:84).
The picker
Both screens follow the same shape:
- A
SearchField/SearchFieldcomponent queriessearchMessageswithpassToQuery: { approved: true }
(mobile: packages/clinic-mobile/src/components/dashboard/WhatsApp/conversationMessages/SendTemplateScreen.js:104-112; web: packages/clinic-web/src/components/dashboard/officialWhatsapp/conversations/SendTemplateButton.js:100-109). That approved flag is new — see Which Templates Qualify for what it filters.
- Selecting a message computes
unfillableVariables(selected.details)— any embedded@VARIABLEin the message
that isn't the contact's name. If that list is non-empty, sending is blocked (mobile: SendTemplateScreen.js:39,127; web: SendTemplateButton.js:38,95).
fillNameVariables(selected.details, name)renders the live preview, substituting@PATIENT_NAMEand
@FIRST_NAME with the contact's name (or its first token) — both defined in a shared, duplicated templateHelpers.js (mobile: packages/clinic-mobile/src/components/dashboard/WhatsApp/conversationMessages/templateHelpers.js; web: packages/clinic-web/src/components/dashboard/officialWhatsapp/conversations/templateHelpers.js — byte-identical files, one per app, rather than a shared package).
Sending
Both screens call the same new mutation, sendWhatsappTemplate(conversation, message, generatedID) (packages/server/src/schema.graphql:1255), generating the generatedID client-side with uuidv4() so the sent message can be optimistically written into the Apollo cache in front of the conversation's message list (both updateOnNewMessage implementations: SendTemplateScreen.js:41-59, SendTemplateButton.js:41-56).
On the server, sendWhatsappTemplate (packages/server/src/resolvers/mutations/actions/officialWhatsApp/sendWhatsappTemplate.js):
- Confirms the company has a connected
officialWhatsAppconfig with aphoneId(line 23-30) — this is the check
that fails in the sandbox used for this walkthrough's screenshots.
- Loads the chosen
Messageand its attachedConversationTemplate, and rejects if it belongs to another company
or its template isn't APPROVED (line 32-48).
- Re-derives the fillable variables server-side with
extractValues(reused from the reminder-cron code at
packages/server/src/cronJobs/messages/embeddedValues.js, exported for this purpose) and rejects again if anything but a name variable is present (line 50-56) — the client-side check is a UX convenience, not the authority.
- Loads the
OnlineConversationand confirms it belongs to the same company (line 58-73). - Sends a
type: "template"message to Meta's Graph API (POST /v18.0/{phoneId}/messages), passing the template's
name/language and, if there's a name variable, one body parameter (line 84-106). Note there is deliberately no 24-hour expiry check here — sending into an expired conversation is the whole point.
- Writes a
Communicationrow logging what was sent and whether it succeeded, whether WhatsApp accepted it or not
(line 113-141) — this is the same table the messaging crons write to, so a manually-sent template shows up alongside automated ones in the communications log.
- On success, increments the conversation's
messagesCount, updateslastActivity, and appends a
ConversationMessage row so it renders in the thread (line 147-171).
Permission
sendWhatsappTemplate requires the SEND_WA permission, same as the existing free-form send mutation (packages/server/src/permissions/permissions.js:4051).