Skip to content

Commit

Permalink
✨ (whatsapp) Add custom session expiration (#842)
Browse files Browse the repository at this point in the history
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
### Summary by CodeRabbit

- New Feature: Introduced session expiry timeout for WhatsApp
integration, allowing users to set the duration after which a session
expires.
- New Feature: Added an option to enable/disable the start bot condition
in WhatsApp integration settings.
- Refactor: Enhanced error handling by throwing specific errors when
necessary conditions are not met.
- Refactor: Improved UI components like `NumberInput` and
`SwitchWithLabel` for better usability.
- Bug Fix: Fixed issues related to session resumption and message
sending in expired sessions. Now, if a session is expired, a new one
will be started instead of attempting to resume the old one.
- Chore: Updated various schemas to reflect changes in session
management and WhatsApp settings.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
baptisteArno authored Sep 22, 2023
1 parent 4cfb45e commit 4f953ac
Show file tree
Hide file tree
Showing 11 changed files with 175 additions and 62 deletions.
26 changes: 16 additions & 10 deletions apps/builder/src/components/inputs/NumberInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
FormControl,
FormLabel,
Stack,
Text,
} from '@chakra-ui/react'
import { Variable, VariableString } from '@typebot.io/schemas'
import { useEffect, useState } from 'react'
Expand All @@ -29,6 +30,7 @@ type Props<HasVariable extends boolean> = {
moreInfoTooltip?: string
isRequired?: boolean
direction?: 'row' | 'column'
suffix?: string
onValueChange: (value?: Value<HasVariable>) => void
} & Omit<NumberInputProps, 'defaultValue' | 'value' | 'onChange' | 'isRequired'>

Expand All @@ -41,6 +43,7 @@ export const NumberInput = <HasVariable extends boolean>({
moreInfoTooltip,
isRequired,
direction,
suffix,
...props
}: Props<HasVariable>) => {
const [value, setValue] = useState(defaultValue?.toString() ?? '')
Expand Down Expand Up @@ -99,24 +102,27 @@ export const NumberInput = <HasVariable extends boolean>({
isRequired={isRequired}
justifyContent="space-between"
width={label ? 'full' : 'auto'}
spacing={0}
spacing={direction === 'column' ? 2 : 3}
>
{label && (
<FormLabel mb="2" flexShrink={0}>
<FormLabel mb="0" mr="0" flexShrink={0}>
{label}{' '}
{moreInfoTooltip && (
<MoreInfoTooltip>{moreInfoTooltip}</MoreInfoTooltip>
)}
</FormLabel>
)}
{withVariableButton ?? true ? (
<HStack spacing={0}>
{Input}
<VariablesButton onSelectVariable={handleVariableSelected} />
</HStack>
) : (
Input
)}
<HStack>
{withVariableButton ?? true ? (
<HStack spacing="0">
{Input}
<VariablesButton onSelectVariable={handleVariableSelected} />
</HStack>
) : (
Input
)}
{suffix ? <Text>{suffix}</Text> : null}
</HStack>
</FormControl>
)
}
4 changes: 2 additions & 2 deletions apps/builder/src/components/inputs/SwitchWithLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export type SwitchWithLabelProps = {
label: string
initialValue: boolean
moreInfoContent?: string
onCheckChange: (isChecked: boolean) => void
onCheckChange?: (isChecked: boolean) => void
justifyContent?: FormControlProps['justifyContent']
} & Omit<SwitchProps, 'value' | 'justifyContent'>

Expand All @@ -29,7 +29,7 @@ export const SwitchWithLabel = ({

const handleChange = () => {
setIsChecked(!isChecked)
onCheckChange(!isChecked)
if (onCheckChange) onCheckChange(!isChecked)
}

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TextInput, NumberInput } from '@/components/inputs'
import { HStack, Stack, Text } from '@chakra-ui/react'
import { Stack, Text } from '@chakra-ui/react'
import { EmbedBubbleContent } from '@typebot.io/schemas'
import { sanitizeUrl } from '@typebot.io/lib'
import { useScopedI18n } from '@/locales'
Expand Down Expand Up @@ -34,14 +34,13 @@ export const EmbedUploadContent = ({ content, onSubmit }: Props) => {
</Text>
</Stack>

<HStack>
<NumberInput
label="Height:"
defaultValue={content?.height}
onValueChange={handleHeightChange}
/>
<Text>{scopedT('numberInput.unit')}</Text>
</HStack>
<NumberInput
label="Height:"
defaultValue={content?.height}
onValueChange={handleHeightChange}
suffix={scopedT('numberInput.unit')}
width="150px"
/>
</Stack>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ import { Comparison, LogicalOperator } from '@typebot.io/schemas'
import { DropdownList } from '@/components/DropdownList'
import { WhatsAppComparisonItem } from './WhatsAppComparisonItem'
import { AlertInfo } from '@/components/AlertInfo'
import { NumberInput } from '@/components/inputs'
import { defaultSessionExpiryTimeout } from '@typebot.io/schemas/features/whatsapp'
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
import { isDefined } from '@typebot.io/lib/utils'

export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => {
const { typebot, updateTypebot, isPublished } = useTypebot()
Expand Down Expand Up @@ -122,6 +126,46 @@ export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => {
})
}

const updateIsStartConditionEnabled = (isEnabled: boolean) => {
if (!typebot) return
updateTypebot({
updates: {
settings: {
...typebot.settings,
whatsApp: {
...typebot.settings.whatsApp,
startCondition: !isEnabled
? undefined
: {
comparisons: [],
logicalOperator: LogicalOperator.AND,
},
},
},
},
})
}

const updateSessionExpiryTimeout = (sessionExpiryTimeout?: number) => {
if (
!typebot ||
(sessionExpiryTimeout &&
(sessionExpiryTimeout <= 0 || sessionExpiryTimeout > 48))
)
return
updateTypebot({
updates: {
settings: {
...typebot.settings,
whatsApp: {
...typebot.settings.whatsApp,
sessionExpiryTimeout,
},
},
},
})
}

return (
<Modal isOpen={isOpen} onClose={onClose} size="xl">
<ModalOverlay />
Expand Down Expand Up @@ -166,33 +210,58 @@ export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => {
<Accordion allowToggle>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Start flow only if
Configure integration
<AccordionIcon />
</AccordionButton>
<AccordionPanel as={Stack} spacing="4" pt="4">
<TableList<Comparison>
initialItems={
whatsAppSettings?.startCondition?.comparisons ?? []
}
onItemsChange={updateStartConditionComparisons}
Item={WhatsAppComparisonItem}
ComponentBetweenItems={() => (
<Flex justify="center">
<DropdownList
currentItem={
whatsAppSettings?.startCondition
?.logicalOperator
}
onItemSelect={
updateStartConditionLogicalOperator
}
items={Object.values(LogicalOperator)}
size="sm"
/>
</Flex>
<HStack>
<NumberInput
max={48}
min={0}
width="100px"
label="Session expire timeout:"
defaultValue={
whatsAppSettings?.sessionExpiryTimeout
}
placeholder={defaultSessionExpiryTimeout.toString()}
moreInfoTooltip="A number between 0 and 48 that represents the time in hours after which the session will expire if the user does not interact with the bot. The conversation restarts if the user sends a message after that expiration time."
onValueChange={updateSessionExpiryTimeout}
withVariableButton={false}
suffix="hours"
/>
</HStack>
<SwitchWithRelatedSettings
label={'Start bot condition'}
initialValue={isDefined(
whatsAppSettings?.startCondition
)}
addLabel="Add a comparison"
/>
onCheckChange={updateIsStartConditionEnabled}
>
<TableList<Comparison>
initialItems={
whatsAppSettings?.startCondition?.comparisons ??
[]
}
onItemsChange={updateStartConditionComparisons}
Item={WhatsAppComparisonItem}
ComponentBetweenItems={() => (
<Flex justify="center">
<DropdownList
currentItem={
whatsAppSettings?.startCondition
?.logicalOperator
}
onItemSelect={
updateStartConditionLogicalOperator
}
items={Object.values(LogicalOperator)}
size="sm"
/>
</Flex>
)}
addLabel="Add a comparison"
/>
</SwitchWithRelatedSettings>
</AccordionPanel>
</AccordionItem>
</Accordion>
Expand Down
12 changes: 12 additions & 0 deletions apps/viewer/src/features/chat/api/sendMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { saveStateToDatabase } from '@typebot.io/bot-engine/saveStateToDatabase'
import { restartSession } from '@typebot.io/bot-engine/queries/restartSession'
import { continueBotFlow } from '@typebot.io/bot-engine/continueBotFlow'
import { parseDynamicTheme } from '@typebot.io/bot-engine/parseDynamicTheme'
import { isDefined } from '@typebot.io/lib/utils'

export const sendMessage = publicProcedure
.meta({
Expand All @@ -30,6 +31,17 @@ export const sendMessage = publicProcedure
}) => {
const session = sessionId ? await getSession(sessionId) : null

const isSessionExpired =
session &&
isDefined(session.state.expiryTimeout) &&
session.updatedAt.getTime() + session.state.expiryTimeout < Date.now()

if (isSessionExpired)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Session expired. You need to start a new session.',
})

if (!session) {
if (!startParams)
throw new TRPCError({
Expand Down
7 changes: 6 additions & 1 deletion packages/bot-engine/continueBotFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { validateRatingReply } from './blocks/inputs/rating/validateRatingReply'
import { parsePictureChoicesReply } from './blocks/inputs/pictureChoice/parsePictureChoicesReply'
import { parseVariables } from './variables/parseVariables'
import { updateVariablesInSession } from './variables/updateVariablesInSession'
import { TRPCError } from '@trpc/server'

export const continueBotFlow =
(state: SessionState) =>
Expand All @@ -46,7 +47,11 @@ export const continueBotFlow =

const block = blockIndex >= 0 ? group?.blocks[blockIndex ?? 0] : null

if (!block || !group) return startBotFlow(state)
if (!block || !group)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Group / block not found',
})

if (block.type === LogicBlockType.SET_VARIABLE) {
const existingVariable = state.typebotsQueue[0].typebot.variables.find(
Expand Down
6 changes: 2 additions & 4 deletions packages/bot-engine/queries/getSession.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import prisma from '@typebot.io/lib/prisma'
import { ChatSession, sessionStateSchema } from '@typebot.io/schemas'

export const getSession = async (
sessionId: string
): Promise<Pick<ChatSession, 'state' | 'id'> | null> => {
export const getSession = async (sessionId: string) => {
const session = await prisma.chatSession.findUnique({
where: { id: sessionId },
select: { id: true, state: true },
select: { id: true, state: true, updatedAt: true },
})
if (!session) return null
return { ...session, state: sessionStateSchema.parse(session.state) }
Expand Down
29 changes: 18 additions & 11 deletions packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { continueBotFlow } from '../continueBotFlow'
import { decrypt } from '@typebot.io/lib/api'
import { saveStateToDatabase } from '../saveStateToDatabase'
import prisma from '@typebot.io/lib/prisma'
import { isDefined } from '@typebot.io/lib/utils'

export const resumeWhatsAppFlow = async ({
receivedMessage,
Expand Down Expand Up @@ -64,17 +65,23 @@ export const resumeWhatsAppFlow = async ({
}
}

const resumeResponse = sessionState
? await continueBotFlow(sessionState)(messageContent)
: workspaceId
? await startWhatsAppSession({
message: receivedMessage,
sessionId,
workspaceId,
credentials: { ...credentials, id: credentialsId as string },
contact,
})
: undefined
const isSessionExpired =
session &&
isDefined(session.state.expiryTimeout) &&
session?.updatedAt.getTime() + session.state.expiryTimeout < Date.now()

const resumeResponse =
sessionState && !isSessionExpired
? await continueBotFlow(sessionState)(messageContent)
: workspaceId
? await startWhatsAppSession({
message: receivedMessage,
sessionId,
workspaceId,
credentials: { ...credentials, id: credentialsId as string },
contact,
})
: undefined

if (!resumeResponse) {
console.error('Could not find or create session', sessionId)
Expand Down
7 changes: 6 additions & 1 deletion packages/bot-engine/whatsapp/startWhatsAppSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import {
WhatsAppCredentials,
WhatsAppIncomingMessage,
defaultSessionExpiryTimeout,
} from '@typebot.io/schemas/features/whatsapp'
import { isNotDefined } from '@typebot.io/lib/utils'
import { startSession } from '../startSession'
Expand Down Expand Up @@ -76,14 +77,18 @@ export const startWhatsAppSession = async ({
userId: undefined,
})

const sessionExpiryTimeoutHours =
publicTypebot.settings.whatsApp?.sessionExpiryTimeout ??
defaultSessionExpiryTimeout

return {
...session,
newSessionState: {
...session.newSessionState,
whatsApp: {
contact,
credentialsId: credentials.id,
},
expiryTimeout: sessionExpiryTimeoutHours * 60 * 60 * 1000,
},
}
}
Expand Down
6 changes: 5 additions & 1 deletion packages/schemas/features/chat/sessionState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,13 @@ const sessionStateSchemaV2 = z.object({
name: z.string(),
phoneNumber: z.string(),
}),
credentialsId: z.string().optional(),
})
.optional(),
expiryTimeout: z
.number()
.min(1)
.optional()
.describe('Expiry timeout in milliseconds'),
typingEmulation: settingsSchema.shape.typingEmulation.optional(),
})

Expand Down
Loading

3 comments on commit 4f953ac

@vercel
Copy link

@vercel vercel bot commented on 4f953ac Sep 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

viewer-v2 – ./apps/viewer

cinecorn.com
ezbooking.ai
bot.reviewzer.com
bot.uluhub.com.br
cares.urlabout.me
chat.ezbooking.ai
chat.gaswadern.de
chat.gniorder.com
chat.leadmagic.io
chat.onrentme.com
chat.rojie.online
chatdocidadao.com
chatwebonline.com
fmm.wpwakanda.com
footballmeetup.ie
gentleman-shop.fr
island.wakanda.is
k1.kandabrand.com
kp.pedroknoll.com
lb.ticketfute.com
mariwelash.com.br
metodoelev.com.br
nutriandreia.shop
order.chatjer.com
ov1.wpwakanda.com
ov2.wpwakanda.com
ov3.wpwakanda.com
pcb.drapamela.com
softwarelucra.com
support.triplo.ai
survey.collab.day
test.eqfeqfeq.com
viewer.typebot.io
welcome.triplo.ai
www.thegymgame.it
zeropendencia.com
1988.bouclidom.com
a.onewebcenter.com
amancarseat.online
amostra-safe.click
andreimayer.com.br
bebesemcolicas.com
bot.innovacion.fun
bot.jogodospix.com
bot.jogomilion.com
bot.lucide.contact
bot.neferlopez.com
bot.photonative.de
bot.rajatanjak.com
bot.samplehunt.com
bot.sinalcerto.com
bot.wphelpchat.com
viewer-v2-typebot-io.vercel.app
preagendamento.sergiolimajr.com.br
prenotazione.ristorantekintsugi.it
download.thailandmicespecialist.com
mdb.assessoria.aloisio.progenbr.com
mdb.assessoria.girotto.progenbr.com
mdb.assessoria.marinho.progenbr.com
mdb.assessoria.rodrigo.progenbr.com
register.thailandmicespecialist.com
mdb.assessoria.desideri.progenbr.com
mdb.assessoria.fernanda.progenbr.com
mdb.assessoria.jbatista.progenbr.com
mdb.assessoria.mauricio.progenbr.com
mdb.evento.autocadastro.progenbr.com
form.shopmercedesbenzsouthorlando.com
mdb.evento.equipeinterna.progenbr.com
bot.studiotecnicoimmobiliaremerelli.it
mdb.assessoria.boaventura.progenbr.com
mdb.assessoria.jtrebesqui.progenbr.com
pesquisa.escolamodacomproposito.com.br
anamnese.clinicaramosodontologia.com.br
gabinete.baleia.formulario.progenbr.com
mdb.assessoria.carreirinha.progenbr.com
chrome-os-inquiry-system.itschromeos.com
mdb.assessoria.paulomarques.progenbr.com
viewer-v2-git-main-typebot-io.vercel.app
main-menu-for-itschromeos.itschromeos.com
mdb.assessoria.qrcode.ademir.progenbr.com
mdb.assessoria.qrcode.arthur.progenbr.com
mdb.assessoria.qrcode.danilo.progenbr.com
mdb.assessoria.qrcode.marcao.progenbr.com
mdb.assessoria.qrcode.marcio.progenbr.com
mdb.assessoria.qrcode.aloisio.progenbr.com
mdb.assessoria.qrcode.girotto.progenbr.com
mdb.assessoria.qrcode.marinho.progenbr.com
mdb.assessoria.qrcode.rodrigo.progenbr.com
mdb.assessoria.carlosalexandre.progenbr.com
mdb.assessoria.qrcode.desideri.progenbr.com
mdb.assessoria.qrcode.fernanda.progenbr.com
mdb.assessoria.qrcode.jbatista.progenbr.com
mdb.assessoria.qrcode.mauricio.progenbr.com
mdb.assessoria.fernanda.regional.progenbr.com
mdb.assessoria.qrcode.boaventura.progenbr.com
mdb.assessoria.qrcode.jtrebesqui.progenbr.com
mdb.assessoria.qrcode.carreirinha.progenbr.com
mdb.assessoria.qrcode.paulomarques.progenbr.com
mdb.assessoria.qrcode.carlosalexandre.progenbr.com
mdb.assessoria.qrcode.fernanda.regional.progenbr.com

@vercel
Copy link

@vercel vercel bot commented on 4f953ac Sep 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

builder-v2 – ./apps/builder

builder-v2-typebot-io.vercel.app
app.typebot.io
builder-v2-git-main-typebot-io.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 4f953ac Sep 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.