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

View File

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

View File

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

View File

@ -27,7 +27,9 @@ export const startWhatsAppPreview = authenticatedProcedure
to: z
.string()
.min(1)
.transform((value) => value.replace(/\s/g, '').replace(/\+/g, '')),
.transform((value) =>
value.replace(/\s/g, '').replace(/\+/g, '').replace(/-/g, '')
),
typebotId: z.string(),
startGroupId: z.string().optional(),
})
@ -70,7 +72,7 @@ export const startWhatsAppPreview = authenticatedProcedure
)
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({
where: {
@ -130,7 +132,7 @@ export const startWhatsAppPreview = authenticatedProcedure
whatsApp: (existingSession?.state as SessionState | undefined)
?.whatsApp,
},
id: `wa-${to}-preview`,
id: sessionId,
})
try {
await sendWhatsAppMessage({

View File

@ -17,3 +17,5 @@ It is possible that Meta automatically restricts your newly created Business acc
4. Select `Business` type
5. Give it any name and select your newly created Business Account
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
- 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
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" />
## 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": {
"type": "object",
"properties": {
"metadata": {
"type": "object",
"properties": {
"phone_number_id": {
"type": "string"
}
},
"required": [
"phone_number_id"
],
"additionalProperties": false
},
"contacts": {
"type": "array",
"items": {
@ -33388,6 +33400,9 @@
}
}
},
"required": [
"metadata"
],
"additionalProperties": false
}
},

View File

@ -5746,7 +5746,14 @@
"type": "string"
},
"pixelId": {
"type": "string"
"type": "string",
"description": "Deprecated"
},
"pixelIds": {
"type": "array",
"items": {
"type": "string"
}
},
"gtmId": {
"type": "string"
@ -6396,28 +6403,48 @@
"type": "object",
"properties": {
"filePathProps": {
"type": "object",
"properties": {
"typebotId": {
"type": "string"
"anyOf": [
{
"type": "object",
"properties": {
"typebotId": {
"type": "string"
},
"blockId": {
"type": "string"
},
"resultId": {
"type": "string"
},
"fileName": {
"type": "string"
}
},
"required": [
"typebotId",
"blockId",
"resultId",
"fileName"
],
"additionalProperties": false
},
"blockId": {
"type": "string"
},
"resultId": {
"type": "string"
},
"fileName": {
"type": "string"
{
"type": "object",
"properties": {
"sessionId": {
"type": "string"
},
"fileName": {
"type": "string"
}
},
"required": [
"sessionId",
"fileName"
],
"additionalProperties": false
}
},
"required": [
"typebotId",
"blockId",
"resultId",
"fileName"
],
"additionalProperties": false
]
},
"fileType": {
"type": "string"
@ -6604,6 +6631,18 @@
"value": {
"type": "object",
"properties": {
"metadata": {
"type": "object",
"properties": {
"phone_number_id": {
"type": "string"
}
},
"required": [
"phone_number_id"
],
"additionalProperties": false
},
"contacts": {
"type": "array",
"items": {
@ -6905,6 +6944,9 @@
}
}
},
"required": [
"metadata"
],
"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(),
})
)
.mutation(async ({ input: { entry, workspaceId, credentialsId } }) => {
.mutation(async ({ input: { entry, credentialsId, workspaceId } }) => {
const receivedMessage = entry.at(0)?.changes.at(0)?.value.messages?.at(0)
if (isNotDefined(receivedMessage)) return { message: 'No message found' }
const contactName =
entry.at(0)?.changes.at(0)?.value?.contacts?.at(0)?.profile?.name ?? ''
const contactPhoneNumber =
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({
receivedMessage,
sessionId: `wa-${credentialsId}-${receivedMessage.from}`,
sessionId: `wa-${phoneNumberId}-${receivedMessage.from}`,
credentialsId,
workspaceId,
contact: {

View File

@ -1,5 +1,5 @@
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) => {
const session = await prisma.chatSession.findUnique({

View File

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

View File

@ -20,7 +20,6 @@ import { upsertResult } from '../queries/upsertResult'
type Props = {
incomingMessage?: string
sessionId: string
workspaceId?: string
credentials: WhatsAppCredentials['data'] & Pick<WhatsAppCredentials, 'id'>
contact: NonNullable<SessionState['whatsApp']>['contact']
@ -76,7 +75,7 @@ export const startWhatsAppSession = async ({
publicTypebot.settings.whatsApp?.sessionExpiryTimeout ??
defaultSessionExpiryTimeout
const session = await startSession({
let chatReply = await startSession({
startParams: {
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
const firstEdgeId =
newSessionState.typebotsQueue[0].typebot.groups[0].blocks[0].outgoingEdgeId
const nextGroup = await getNextGroup(newSessionState)(firstEdgeId)
sessionState.typebotsQueue[0].typebot.groups[0].blocks[0].outgoingEdgeId
const nextGroup = await getNextGroup(sessionState)(firstEdgeId)
const firstBlock = nextGroup.group?.blocks.at(0)
if (firstBlock && isInputBlock(firstBlock)) {
const resultId = newSessionState.typebotsQueue[0].resultId
const resultId = sessionState.typebotsQueue[0].resultId
if (resultId)
await upsertResult({
hasStarted: true,
isCompleted: false,
resultId,
typebot: newSessionState.typebotsQueue[0].typebot,
typebot: sessionState.typebotsQueue[0].typebot,
})
newSessionState = (
await continueBotFlow({
...newSessionState,
currentBlock: { groupId: firstBlock.groupId, blockId: firstBlock.id },
})(incomingMessage)
).newSessionState
chatReply = await continueBotFlow({
...sessionState,
currentBlock: { groupId: firstBlock.groupId, blockId: firstBlock.id },
})(incomingMessage)
}
return {
...session,
newSessionState,
}
return chatReply
}
export const messageMatchStartCondition = (

View File

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