From ea2a87dd5094bd8a206ef38d8640d821f4b8f012 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Mon, 15 Dec 2025 10:02:01 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20WhatsApp=20typing=20indicator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../results/[resultId]/executeWebhook.ts | 1 + packages/whatsapp/src/resumeWhatsAppFlow.ts | 10 ++++ packages/whatsapp/src/schemas.ts | 1 + .../src/sendWhatsAppTypingIndicator.ts | 55 +++++++++++++++++++ 4 files changed, 67 insertions(+) create mode 100644 packages/whatsapp/src/sendWhatsAppTypingIndicator.ts diff --git a/apps/viewer/src/pages/api/v1/typebots/[typebotId]/blocks/[blockId]/results/[resultId]/executeWebhook.ts b/apps/viewer/src/pages/api/v1/typebots/[typebotId]/blocks/[blockId]/results/[resultId]/executeWebhook.ts index cb522dcc8..2c0055b9f 100644 --- a/apps/viewer/src/pages/api/v1/typebots/[typebotId]/blocks/[blockId]/results/[resultId]/executeWebhook.ts +++ b/apps/viewer/src/pages/api/v1/typebots/[typebotId]/blocks/[blockId]/results/[resultId]/executeWebhook.ts @@ -8,6 +8,7 @@ import { methodNotAllowed, notFound, } from "@typebot.io/lib/api/utils"; +import { createId } from "@typebot.io/lib/createId"; import { byId } from "@typebot.io/lib/utils"; import prisma from "@typebot.io/prisma"; import { isTypebotVersionAtLeastV6 } from "@typebot.io/schemas/helpers/isTypebotVersionAtLeastV6"; diff --git a/packages/whatsapp/src/resumeWhatsAppFlow.ts b/packages/whatsapp/src/resumeWhatsAppFlow.ts index 779053b9b..017836933 100644 --- a/packages/whatsapp/src/resumeWhatsAppFlow.ts +++ b/packages/whatsapp/src/resumeWhatsAppFlow.ts @@ -26,6 +26,7 @@ import type { WhatsAppMessageReferral, } from "./schemas"; import { sendChatReplyToWhatsApp } from "./sendChatReplyToWhatsApp"; +import { sendWhatsAppTypingIndicator } from "./sendWhatsAppTypingIndicator"; import { startWhatsAppSession } from "./startWhatsAppSession"; import { WhatsAppError } from "./WhatsAppError"; @@ -137,6 +138,7 @@ export const resumeWhatsAppFlow = async ({ isWaitingForWebhook, } = await resumeFlowAndSendWhatsAppMessages({ to: receivedMessages[0].from, + messageId: receivedMessages[0].id, credentials, isSessionExpired, reply, @@ -430,6 +432,7 @@ const aggregateParallelMediaMessagesIfRedisEnabled = async ({ const resumeFlowAndSendWhatsAppMessages = async (props: { to: string; + messageId: string | undefined; state: SessionState | null | undefined; sessionStore: SessionStore; reply: Message | undefined; @@ -440,6 +443,13 @@ const resumeFlowAndSendWhatsAppMessages = async (props: { credentialsId?: string; workspaceId?: string; }) => { + if (props.messageId) { + sendWhatsAppTypingIndicator({ + messageId: props.messageId, + credentials: props.credentials, + }); + } + const resumeResponse = await resumeFlow(props); const { diff --git a/packages/whatsapp/src/schemas.ts b/packages/whatsapp/src/schemas.ts index 742ac9a41..c627b37e7 100644 --- a/packages/whatsapp/src/schemas.ts +++ b/packages/whatsapp/src/schemas.ts @@ -103,6 +103,7 @@ const incomingMessageReferral = z.object({ export type WhatsAppMessageReferral = z.infer; const sharedIncomingMessageFieldsSchema = z.object({ + id: z.string().optional(), from: z.string(), timestamp: z.string(), referral: incomingMessageReferral.optional(), diff --git a/packages/whatsapp/src/sendWhatsAppTypingIndicator.ts b/packages/whatsapp/src/sendWhatsAppTypingIndicator.ts new file mode 100644 index 000000000..ecbad9054 --- /dev/null +++ b/packages/whatsapp/src/sendWhatsAppTypingIndicator.ts @@ -0,0 +1,55 @@ +import * as Sentry from "@sentry/nextjs"; +import type { WhatsAppCredentials } from "@typebot.io/credentials/schemas"; +import { env } from "@typebot.io/env"; +import ky from "ky"; +import { dialog360AuthHeaderName, dialog360BaseUrl } from "./constants"; + +type Props = { + messageId: string; + credentials: WhatsAppCredentials["data"]; +}; + +export const sendWhatsAppTypingIndicator = async ({ + messageId, + credentials, +}: Props) => { + try { + const json = { + messaging_product: "whatsapp", + status: "read", + message_id: messageId, + typing_indicator: { + type: "text", + }, + }; + + if (credentials.provider === "360dialog") { + await ky.post(`${dialog360BaseUrl}/messages`, { + headers: { + [dialog360AuthHeaderName]: credentials.apiKey, + }, + json, + }); + } else { + await ky.post( + `${env.WHATSAPP_CLOUD_API_URL}/v21.0/${credentials.phoneNumberId}/messages`, + { + headers: { + Authorization: `Bearer ${credentials.systemUserAccessToken}`, + }, + json, + }, + ); + } + } catch (err) { + // Typing indicators are non-critical, log the error but don't throw + Sentry.captureException(err, { + tags: { + context: "whatsapp-typing-indicator", + }, + extra: { + messageId, + }, + }); + } +};