2
0

(whatsapp) Add custom session expiration (#842)

<!-- 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 -->
This commit is contained in:
Baptiste Arnaud
2023-09-22 17:12:15 +02:00
committed by GitHub
parent 4cfb45e2a3
commit 4f953ac272
11 changed files with 175 additions and 62 deletions

View File

@ -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'
@ -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'>
@ -41,6 +43,7 @@ export const NumberInput = <HasVariable extends boolean>({
moreInfoTooltip,
isRequired,
direction,
suffix,
...props
}: Props<HasVariable>) => {
const [value, setValue] = useState(defaultValue?.toString() ?? '')
@ -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>
)
}

View File

@ -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'>
@ -29,7 +29,7 @@ export const SwitchWithLabel = ({
const handleChange = () => {
setIsChecked(!isChecked)
onCheckChange(!isChecked)
if (onCheckChange) onCheckChange(!isChecked)
}
return (

View File

@ -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'
@ -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>
)
}

View File

@ -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()
@ -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 />
@ -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>

View File

@ -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({
@ -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({