2
0

🐛 (whatsapp) Fix auto start input where it didn't display next bu… (#869)

…bbles
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
### Summary by CodeRabbit

**Release Notes**

- New Feature: Enhanced WhatsApp integration with improved phone number
formatting and session ID generation.
- Refactor: Updated the `startWhatsAppPreview` and
`receiveMessagePreview` functions for better consistency and
readability.
- Bug Fix: Added a check for `phoneNumberId` in the `receiveMessage`
function to prevent errors when it's undefined.
- Documentation: Expanded the WhatsApp integration guide and FAQs in the
docs, providing more detailed instructions and addressing common
queries.
- Chore: Introduced a new `metadata` field in the
`whatsAppWebhookRequestBodySchema` to store the `phone_number_id`.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Baptiste Arnaud
2023-09-29 09:59:38 +02:00
committed by GitHub
parent 76f4954540
commit f9a14c0685
14 changed files with 153 additions and 59 deletions

View File

@@ -6,8 +6,8 @@ import {
Alert, Alert,
AlertIcon, AlertIcon,
Button, Button,
Flex,
HStack, HStack,
Link,
SlideFade, SlideFade,
Stack, Stack,
StackProps, StackProps,
@@ -84,28 +84,38 @@ export const WhatsAppPreviewInstructions = (props: StackProps) => {
defaultValue={phoneNumber} defaultValue={phoneNumber}
onChange={setPhoneNumber} onChange={setPhoneNumber}
/> />
<Button {!isMessageSent && (
isDisabled={isEmpty(phoneNumber) || isMessageSent} <Button
isLoading={isSendingMessage} isDisabled={isEmpty(phoneNumber) || isMessageSent}
type="submit" isLoading={isSendingMessage}
> type="submit"
{hasMessageBeenSent ? 'Restart' : 'Start'} the chat >
</Button> {hasMessageBeenSent ? 'Restart' : 'Start'} the chat
</Button>
)}
<SlideFade offsetY="20px" in={isMessageSent} unmountOnExit> <SlideFade offsetY="20px" in={isMessageSent} unmountOnExit>
<Flex> <Stack>
<Alert status="success" w="100%"> <Alert status="success" w="100%">
<HStack> <HStack>
<AlertIcon /> <AlertIcon />
<Stack spacing={1}> <Stack spacing={1}>
<Text fontWeight="semibold">Chat started!</Text> <Text fontWeight="semibold">Chat started!</Text>
<Text fontSize="sm"> <Text fontSize="sm">
Open WhatsApp to test your bot. The first message can take up The first message can take up to 2 min to be delivered.
to 2 min to be delivered.
</Text> </Text>
</Stack> </Stack>
</HStack> </HStack>
</Alert> </Alert>
</Flex> <Button
as={Link}
href={`https://web.whatsapp.com/`}
isExternal
size="sm"
colorScheme="blue"
>
Open WhatsApp Web
</Button>
</Stack>
</SlideFade> </SlideFade>
</Stack> </Stack>
) )

View File

@@ -47,7 +47,7 @@ export const getPhoneNumber = authenticatedProcedure
const formattedPhoneNumber = `${ const formattedPhoneNumber = `${
display_phone_number.startsWith('+') ? '' : '+' display_phone_number.startsWith('+') ? '' : '+'
}${display_phone_number.replace(/\s-/g, '')}` }${display_phone_number.replace(/[\s-]/g, '')}`
return { return {
id: credentials.phoneNumberId, id: credentials.phoneNumberId,

View File

@@ -34,7 +34,7 @@ export const receiveMessagePreview = publicProcedure
const contactPhoneNumber = '+' + receivedMessage.from const contactPhoneNumber = '+' + receivedMessage.from
return resumeWhatsAppFlow({ return resumeWhatsAppFlow({
receivedMessage, receivedMessage,
sessionId: `wa-${receivedMessage.from}-preview`, sessionId: `wa-preview-${receivedMessage.from}`,
contact: { contact: {
name: contactName, name: contactName,
phoneNumber: contactPhoneNumber, phoneNumber: contactPhoneNumber,

View File

@@ -27,7 +27,9 @@ export const startWhatsAppPreview = authenticatedProcedure
to: z to: z
.string() .string()
.min(1) .min(1)
.transform((value) => value.replace(/\s/g, '').replace(/\+/g, '')), .transform((value) =>
value.replace(/\s/g, '').replace(/\+/g, '').replace(/-/g, '')
),
typebotId: z.string(), typebotId: z.string(),
startGroupId: z.string().optional(), startGroupId: z.string().optional(),
}) })
@@ -70,7 +72,7 @@ export const startWhatsAppPreview = authenticatedProcedure
) )
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' }) throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
const sessionId = `wa-${to}-preview` const sessionId = `wa-preview-${to}`
const existingSession = await prisma.chatSession.findFirst({ const existingSession = await prisma.chatSession.findFirst({
where: { where: {
@@ -130,7 +132,7 @@ export const startWhatsAppPreview = authenticatedProcedure
whatsApp: (existingSession?.state as SessionState | undefined) whatsApp: (existingSession?.state as SessionState | undefined)
?.whatsApp, ?.whatsApp,
}, },
id: `wa-${to}-preview`, id: sessionId,
}) })
try { try {
await sendWhatsAppMessage({ await sendWhatsAppMessage({

View File

@@ -17,3 +17,5 @@ It is possible that Meta automatically restricts your newly created Business acc
4. Select `Business` type 4. Select `Business` type
5. Give it any name and select your newly created Business Account 5. Give it any name and select your newly created Business Account
6. On the app page, look for `WhatsApp` product and enable it 6. On the app page, look for `WhatsApp` product and enable it
You can then follow the instructions in the Share tab of your bot to connect your Meta app to Typebot.

View File

@@ -29,8 +29,32 @@ WhatsApp environment have some limitations that you need to keep in mind when bu
- Google Analytics block - Google Analytics block
- Meta Pixel blocks - Meta Pixel blocks
## Configuration
You can customize how your bot behaves on WhatsApp in the `Configure integration` section
<img src="/img/whatsapp/configure-integration.png" alt="WhatsApp configure integration" />
**Session expiration timeout**: A number from 0 to 48 which is the number of hours after which the session will expire. If the user doesn't interact with the bot for more than the timeout, the session will expire and if user sends a new message, it will start a new chat.
**Start bot condition**: A condition that will be evaluated when a user starts a conversation with your bot. If the condition is not met, the bot will not be triggered.
## Contact information ## Contact information
You can automatically assign contact name and phone number to a variable in your bot using a Set variable block with the dedicated system values: You can automatically assign contact name and phone number to a variable in your bot using a Set variable block with the dedicated system values:
<img src="/img/whatsapp/contact-var.png" alt="WhatsApp contact system variables" /> <img src="/img/whatsapp/contact-var.png" alt="WhatsApp contact system variables" />
## FAQ
### How many WhatsApp numbers can I use?
You can integrate as many numbers as you'd like. Keep in mind that Typebot does not provide those numbers. We work as a "Bring your own Meta application" and we give you clear instructions on [how to set up your Meta app](./whatsapp/create-meta-app).
### Can I link multiple bots to the same WhatsApp number?
Yes, you can. You will have to add a "Start bot condition" to each of your bots to make sure that the right bot is triggered when a user starts a conversation.
### Does the integration with WhatsApp requires any paid API?
You integrate your typebots with your own WhatsApp Business Platform which is the official service from Meta. At the moment, the first 1,000 Service conversations each month are free. For more information, refer to [their documentation](https://developers.facebook.com/docs/whatsapp/cloud-api/get-started#pricing---payment-methods)

View File

@@ -33087,6 +33087,18 @@
"value": { "value": {
"type": "object", "type": "object",
"properties": { "properties": {
"metadata": {
"type": "object",
"properties": {
"phone_number_id": {
"type": "string"
}
},
"required": [
"phone_number_id"
],
"additionalProperties": false
},
"contacts": { "contacts": {
"type": "array", "type": "array",
"items": { "items": {
@@ -33388,6 +33400,9 @@
} }
} }
}, },
"required": [
"metadata"
],
"additionalProperties": false "additionalProperties": false
} }
}, },

View File

@@ -5746,7 +5746,14 @@
"type": "string" "type": "string"
}, },
"pixelId": { "pixelId": {
"type": "string" "type": "string",
"description": "Deprecated"
},
"pixelIds": {
"type": "array",
"items": {
"type": "string"
}
}, },
"gtmId": { "gtmId": {
"type": "string" "type": "string"
@@ -6396,28 +6403,48 @@
"type": "object", "type": "object",
"properties": { "properties": {
"filePathProps": { "filePathProps": {
"type": "object", "anyOf": [
"properties": { {
"typebotId": { "type": "object",
"type": "string" "properties": {
"typebotId": {
"type": "string"
},
"blockId": {
"type": "string"
},
"resultId": {
"type": "string"
},
"fileName": {
"type": "string"
}
},
"required": [
"typebotId",
"blockId",
"resultId",
"fileName"
],
"additionalProperties": false
}, },
"blockId": { {
"type": "string" "type": "object",
}, "properties": {
"resultId": { "sessionId": {
"type": "string" "type": "string"
}, },
"fileName": { "fileName": {
"type": "string" "type": "string"
}
},
"required": [
"sessionId",
"fileName"
],
"additionalProperties": false
} }
}, ]
"required": [
"typebotId",
"blockId",
"resultId",
"fileName"
],
"additionalProperties": false
}, },
"fileType": { "fileType": {
"type": "string" "type": "string"
@@ -6604,6 +6631,18 @@
"value": { "value": {
"type": "object", "type": "object",
"properties": { "properties": {
"metadata": {
"type": "object",
"properties": {
"phone_number_id": {
"type": "string"
}
},
"required": [
"phone_number_id"
],
"additionalProperties": false
},
"contacts": { "contacts": {
"type": "array", "type": "array",
"items": { "items": {
@@ -6905,6 +6944,9 @@
} }
} }
}, },
"required": [
"metadata"
],
"additionalProperties": false "additionalProperties": false
} }
}, },

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View File

@@ -23,16 +23,19 @@ export const receiveMessage = publicProcedure
message: z.string(), message: z.string(),
}) })
) )
.mutation(async ({ input: { entry, workspaceId, credentialsId } }) => { .mutation(async ({ input: { entry, credentialsId, workspaceId } }) => {
const receivedMessage = entry.at(0)?.changes.at(0)?.value.messages?.at(0) const receivedMessage = entry.at(0)?.changes.at(0)?.value.messages?.at(0)
if (isNotDefined(receivedMessage)) return { message: 'No message found' } if (isNotDefined(receivedMessage)) return { message: 'No message found' }
const contactName = const contactName =
entry.at(0)?.changes.at(0)?.value?.contacts?.at(0)?.profile?.name ?? '' entry.at(0)?.changes.at(0)?.value?.contacts?.at(0)?.profile?.name ?? ''
const contactPhoneNumber = const contactPhoneNumber =
entry.at(0)?.changes.at(0)?.value?.messages?.at(0)?.from ?? '' entry.at(0)?.changes.at(0)?.value?.messages?.at(0)?.from ?? ''
const phoneNumberId = entry.at(0)?.changes.at(0)?.value
.metadata.phone_number_id
if (!phoneNumberId) return { message: 'No phone number id found' }
return resumeWhatsAppFlow({ return resumeWhatsAppFlow({
receivedMessage, receivedMessage,
sessionId: `wa-${credentialsId}-${receivedMessage.from}`, sessionId: `wa-${phoneNumberId}-${receivedMessage.from}`,
credentialsId, credentialsId,
workspaceId, workspaceId,
contact: { contact: {

View File

@@ -1,5 +1,5 @@
import prisma from '@typebot.io/lib/prisma' import prisma from '@typebot.io/lib/prisma'
import { ChatSession, sessionStateSchema } from '@typebot.io/schemas' import { sessionStateSchema } from '@typebot.io/schemas'
export const getSession = async (sessionId: string) => { export const getSession = async (sessionId: string) => {
const session = await prisma.chatSession.findUnique({ const session = await prisma.chatSession.findUnique({

View File

@@ -71,7 +71,6 @@ export const resumeWhatsAppFlow = async ({
: workspaceId : workspaceId
? await startWhatsAppSession({ ? await startWhatsAppSession({
incomingMessage: messageContent, incomingMessage: messageContent,
sessionId,
workspaceId, workspaceId,
credentials: { ...credentials, id: credentialsId as string }, credentials: { ...credentials, id: credentialsId as string },
contact, contact,

View File

@@ -20,7 +20,6 @@ import { upsertResult } from '../queries/upsertResult'
type Props = { type Props = {
incomingMessage?: string incomingMessage?: string
sessionId: string
workspaceId?: string workspaceId?: string
credentials: WhatsAppCredentials['data'] & Pick<WhatsAppCredentials, 'id'> credentials: WhatsAppCredentials['data'] & Pick<WhatsAppCredentials, 'id'>
contact: NonNullable<SessionState['whatsApp']>['contact'] contact: NonNullable<SessionState['whatsApp']>['contact']
@@ -76,7 +75,7 @@ export const startWhatsAppSession = async ({
publicTypebot.settings.whatsApp?.sessionExpiryTimeout ?? publicTypebot.settings.whatsApp?.sessionExpiryTimeout ??
defaultSessionExpiryTimeout defaultSessionExpiryTimeout
const session = await startSession({ let chatReply = await startSession({
startParams: { startParams: {
typebot: publicTypebot.typebot.publicId as string, typebot: publicTypebot.typebot.publicId as string,
}, },
@@ -89,34 +88,29 @@ export const startWhatsAppSession = async ({
}, },
}) })
let newSessionState: SessionState = session.newSessionState const sessionState: SessionState = chatReply.newSessionState
// If first block is an input block, we can directly continue the bot flow // If first block is an input block, we can directly continue the bot flow
const firstEdgeId = const firstEdgeId =
newSessionState.typebotsQueue[0].typebot.groups[0].blocks[0].outgoingEdgeId sessionState.typebotsQueue[0].typebot.groups[0].blocks[0].outgoingEdgeId
const nextGroup = await getNextGroup(newSessionState)(firstEdgeId) const nextGroup = await getNextGroup(sessionState)(firstEdgeId)
const firstBlock = nextGroup.group?.blocks.at(0) const firstBlock = nextGroup.group?.blocks.at(0)
if (firstBlock && isInputBlock(firstBlock)) { if (firstBlock && isInputBlock(firstBlock)) {
const resultId = newSessionState.typebotsQueue[0].resultId const resultId = sessionState.typebotsQueue[0].resultId
if (resultId) if (resultId)
await upsertResult({ await upsertResult({
hasStarted: true, hasStarted: true,
isCompleted: false, isCompleted: false,
resultId, resultId,
typebot: newSessionState.typebotsQueue[0].typebot, typebot: sessionState.typebotsQueue[0].typebot,
}) })
newSessionState = ( chatReply = await continueBotFlow({
await continueBotFlow({ ...sessionState,
...newSessionState, currentBlock: { groupId: firstBlock.groupId, blockId: firstBlock.id },
currentBlock: { groupId: firstBlock.groupId, blockId: firstBlock.id }, })(incomingMessage)
})(incomingMessage)
).newSessionState
} }
return { return chatReply
...session,
newSessionState,
}
} }
export const messageMatchStartCondition = ( export const messageMatchStartCondition = (

View File

@@ -142,6 +142,9 @@ export const whatsAppWebhookRequestBodySchema = z.object({
changes: z.array( changes: z.array(
z.object({ z.object({
value: z.object({ value: z.object({
metadata: z.object({
phone_number_id: z.string(),
}),
contacts: z contacts: z
.array( .array(
z.object({ z.object({