✨ (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:
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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 (
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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({
|
||||
|
Reference in New Issue
Block a user