Dentolize · Send Template in Expired Conversation Walkthrough
On this pageBusiness viewTechnical view

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:

  1. Search for one of the clinic's existing messages (the same messages used for appointment reminders, invoices,

etc.).

  1. See a live preview of what the patient will actually receive, with their name already filled in.
  2. 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.

  1. 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. ConversationMessagesScreen passes conversation and contactName into WhatsAppChatHeader

(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.js renders SendTemplateButton next 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:

  1. A SearchField/SearchField component queries searchMessages with passToQuery: { 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.

  1. Selecting a message computes unfillableVariables(selected.details) — any embedded @VARIABLE in 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).

  1. fillNameVariables(selected.details, name) renders the live preview, substituting @PATIENT_NAME and

@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):

  1. Confirms the company has a connected officialWhatsApp config with a phoneId (line 23-30) — this is the check

that fails in the sandbox used for this walkthrough's screenshots.

  1. Loads the chosen Message and its attached ConversationTemplate, and rejects if it belongs to another company

or its template isn't APPROVED (line 32-48).

  1. 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.

  1. Loads the OnlineConversation and confirms it belongs to the same company (line 58-73).
  2. 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.

  1. Writes a Communication row 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.

  1. On success, increments the conversation's messagesCount, updates lastActivity, 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).