diff --git a/apps/builder/src/components/SwitchWithRelatedSettings.tsx b/apps/builder/src/components/SwitchWithRelatedSettings.tsx index f1b4faf42..9e26394a1 100644 --- a/apps/builder/src/components/SwitchWithRelatedSettings.tsx +++ b/apps/builder/src/components/SwitchWithRelatedSettings.tsx @@ -8,7 +8,7 @@ export const SwitchWithRelatedSettings = ({ children, ...props }: Props) => ( diff --git a/apps/builder/src/features/settings/components/GeneralSettingsForm.tsx b/apps/builder/src/features/settings/components/GeneralSettingsForm.tsx index 5b6139ad2..055bf7a30 100644 --- a/apps/builder/src/features/settings/components/GeneralSettingsForm.tsx +++ b/apps/builder/src/features/settings/components/GeneralSettingsForm.tsx @@ -1,7 +1,17 @@ -import { Flex, FormLabel, Stack, Switch, useDisclosure } from '@chakra-ui/react' +import { + Flex, + FormControl, + FormLabel, + HStack, + Stack, + Switch, + Tag, + useDisclosure, + Text, +} from '@chakra-ui/react' import { useWorkspace } from '@/features/workspace/WorkspaceProvider' import { Plan } from '@typebot.io/prisma' -import { GeneralSettings } from '@typebot.io/schemas' +import { GeneralSettings, rememberUserStorages } from '@typebot.io/schemas' import React from 'react' import { isDefined } from '@typebot.io/lib' import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel' @@ -9,6 +19,9 @@ import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal' import { LockTag } from '@/features/billing/components/LockTag' import { isFreePlan } from '@/features/billing/helpers/isFreePlan' import { useI18n } from '@/locales' +import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings' +import { DropdownList } from '@/components/DropdownList' +import { MoreInfoTooltip } from '@/components/MoreInfoTooltip' type Props = { generalSettings: GeneralSettings @@ -31,10 +44,13 @@ export const GeneralSettingsForm = ({ }) } - const handleNewResultOnRefreshChange = (isRememberSessionChecked: boolean) => + const toggleRememberUser = (isEnabled: boolean) => onGeneralSettingsChange({ ...generalSettings, - isNewResultOnRefreshEnabled: !isRememberSessionChecked, + rememberUser: { + ...generalSettings.rememberUser, + isEnabled, + }, }) const handleInputPrefillChange = (isInputPrefillEnabled: boolean) => @@ -49,6 +65,17 @@ export const GeneralSettingsForm = ({ isHideQueryParamsEnabled, }) + const updateRememberUserStorage = ( + storage: NonNullable['storage'] + ) => + onGeneralSettingsChange({ + ...generalSettings, + rememberUser: { + ...generalSettings.rememberUser, + storage, + }, + }) + return ( - + + + + Storage:  + + + + Choose session to remember the user as + long as he does not closes the tab or the browser. + + + Choose local to remember the user + forever. + + + + + + + ) } diff --git a/apps/docs/docs/editor/settings.mdx b/apps/docs/docs/editor/settings.mdx index c1de74166..260643e8d 100644 --- a/apps/docs/docs/editor/settings.mdx +++ b/apps/docs/docs/editor/settings.mdx @@ -8,8 +8,8 @@ The general settings represent the general behaviors of your typebot. - **Typebot.io branding**: If enabled, will show a "Made with Typebot" badge displayed at the bottom of your bot. - **Prefill input**: If enabled, the inputs will be automatically pre-filled whenever their associated variable has a value. -- **Remember session**: If enabled, when a user refreshes the page, its existing answers will be overwritten. - **Hide query params on bot start**: If enabled, the query params will be hidden when the bot starts. +- **Remember user**: If enabled, user previous variables will be prefilled and his new answers will override the previous ones. ## Typing emulation diff --git a/apps/viewer/src/features/chat/api/sendMessage.ts b/apps/viewer/src/features/chat/api/sendMessage.ts index f7ac28ce6..93b39e52d 100644 --- a/apps/viewer/src/features/chat/api/sendMessage.ts +++ b/apps/viewer/src/features/chat/api/sendMessage.ts @@ -124,8 +124,11 @@ const startSession = async (startParams?: StartParams, userId?: string) => { isPreview: startParams.isPreview || typeof startParams.typebot !== 'string', typebotId: typebot.id, prefilledVariables, - isNewResultOnRefreshEnabled: - typebot.settings.general.isNewResultOnRefreshEnabled ?? false, + isRememberUserEnabled: + typebot.settings.general.rememberUser?.isEnabled ?? + (isDefined(typebot.settings.general.isNewResultOnRefreshEnabled) + ? !typebot.settings.general.isNewResultOnRefreshEnabled + : false), }) const startVariables = @@ -291,11 +294,11 @@ const getResult = async ({ isPreview, resultId, prefilledVariables, - isNewResultOnRefreshEnabled, + isRememberUserEnabled, }: Pick & { typebotId: string prefilledVariables: Variable[] - isNewResultOnRefreshEnabled: boolean + isRememberUserEnabled: boolean }) => { if (isPreview) return const select = { @@ -305,7 +308,7 @@ const getResult = async ({ } satisfies Prisma.ResultSelect const existingResult = - resultId && !isNewResultOnRefreshEnabled + resultId && isRememberUserEnabled ? ((await prisma.result.findFirst({ where: { id: resultId }, select, diff --git a/apps/viewer/src/features/settings/settings.spec.ts b/apps/viewer/src/features/settings/settings.spec.ts index 05cd8c1c7..aaf0d886f 100644 --- a/apps/viewer/src/features/settings/settings.spec.ts +++ b/apps/viewer/src/features/settings/settings.spec.ts @@ -21,7 +21,10 @@ test('Result should be overwritten on page refresh', async ({ page }) => { ...defaultSettings, general: { ...defaultSettings.general, - isNewResultOnRefreshEnabled: false, + rememberUser: { + isEnabled: true, + storage: 'session', + }, }, }, ...parseDefaultGroupWithBlock({ diff --git a/packages/embeds/js/package.json b/packages/embeds/js/package.json index d9832447b..832af42a1 100644 --- a/packages/embeds/js/package.json +++ b/packages/embeds/js/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/js", - "version": "0.0.51", + "version": "0.0.52", "description": "Javascript library to display typebots on your website", "type": "module", "main": "dist/index.js", diff --git a/packages/embeds/js/src/components/Bot.tsx b/packages/embeds/js/src/components/Bot.tsx index 4bd1a56ab..6d0e71210 100644 --- a/packages/embeds/js/src/components/Bot.tsx +++ b/packages/embeds/js/src/components/Bot.tsx @@ -7,9 +7,9 @@ import { setIsMobile } from '@/utils/isMobileSignal' import { BotContext, InitialChatReply, OutgoingLog } from '@/types' import { ErrorMessage } from './ErrorMessage' import { - getExistingResultIdFromSession, - setResultInSession, -} from '@/utils/sessionStorage' + getExistingResultIdFromStorage, + setResultInStorage, +} from '@/utils/storage' import { setCssVariablesValue } from '@/utils/setCssVariablesValue' import immutableCss from '../assets/immutable.css' @@ -52,7 +52,7 @@ export const Bot = (props: BotProps & { class?: string }) => { isPreview: props.isPreview ?? false, resultId: isNotEmpty(props.resultId) ? props.resultId - : getExistingResultIdFromSession(typebotIdFromProps), + : getExistingResultIdFromStorage(typebotIdFromProps), startGroupId: props.startGroupId, prefilledVariables: { ...prefilledVariables, @@ -76,7 +76,10 @@ export const Bot = (props: BotProps & { class?: string }) => { if (!data) return setError(new Error("Error! Couldn't initiate the chat.")) if (data.resultId && typebotIdFromProps) - setResultInSession(typebotIdFromProps, data.resultId) + setResultInStorage(data.typebot.settings.general.rememberUser?.storage)( + typebotIdFromProps, + data.resultId + ) setInitialChatReply(data) setCustomCss(data.typebot.theme.customCss ?? '') diff --git a/packages/embeds/js/src/utils/sessionStorage.ts b/packages/embeds/js/src/utils/sessionStorage.ts deleted file mode 100644 index 6da1ad923..000000000 --- a/packages/embeds/js/src/utils/sessionStorage.ts +++ /dev/null @@ -1,20 +0,0 @@ -const sessionStorageKey = 'resultId' - -export const getExistingResultIdFromSession = (typebotId?: string) => { - if (!typebotId) return - try { - return ( - sessionStorage.getItem(`${sessionStorageKey}-${typebotId}`) ?? undefined - ) - } catch { - /* empty */ - } -} - -export const setResultInSession = (typebotId: string, resultId: string) => { - try { - return sessionStorage.setItem(`${sessionStorageKey}-${typebotId}`, resultId) - } catch { - /* empty */ - } -} diff --git a/packages/embeds/js/src/utils/storage.ts b/packages/embeds/js/src/utils/storage.ts new file mode 100644 index 000000000..e276f18e2 --- /dev/null +++ b/packages/embeds/js/src/utils/storage.ts @@ -0,0 +1,29 @@ +const sessionStorageKey = 'resultId' + +export const getExistingResultIdFromStorage = (typebotId?: string) => { + if (!typebotId) return + try { + return ( + sessionStorage.getItem(`${sessionStorageKey}-${typebotId}`) ?? + localStorage.getItem(`${sessionStorageKey}-${typebotId}`) ?? + undefined + ) + } catch { + /* empty */ + } +} + +export const setResultInStorage = + (storageType: 'local' | 'session' = 'session') => + (typebotId: string, resultId: string) => { + try { + ;(storageType === 'session' ? localStorage : sessionStorage).removeItem( + `${sessionStorageKey}-${typebotId}` + ) + return ( + storageType === 'session' ? sessionStorage : localStorage + ).setItem(`${sessionStorageKey}-${typebotId}`, resultId) + } catch { + /* empty */ + } + } diff --git a/packages/embeds/react/package.json b/packages/embeds/react/package.json index 2f63d59a9..d0c911345 100644 --- a/packages/embeds/react/package.json +++ b/packages/embeds/react/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/react", - "version": "0.0.51", + "version": "0.0.52", "description": "React library to display typebots on your website", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/schemas/features/typebot/settings.ts b/packages/schemas/features/typebot/settings.ts index e39cc3825..4585ec580 100644 --- a/packages/schemas/features/typebot/settings.ts +++ b/packages/schemas/features/typebot/settings.ts @@ -1,11 +1,19 @@ import { z } from 'zod' +export const rememberUserStorages = ['session', 'local'] as const + const generalSettings = z.object({ isBrandingEnabled: z.boolean(), isTypingEmulationEnabled: z.boolean().optional(), isInputPrefillEnabled: z.boolean().optional(), isHideQueryParamsEnabled: z.boolean().optional(), isNewResultOnRefreshEnabled: z.boolean().optional(), + rememberUser: z + .object({ + isEnabled: z.boolean().optional(), + storage: z.enum(rememberUserStorages).optional(), + }) + .optional(), }) const typingEmulation = z.object({ @@ -32,7 +40,9 @@ export const settingsSchema = z.object({ export const defaultSettings: Settings = { general: { isBrandingEnabled: true, - isNewResultOnRefreshEnabled: true, + rememberUser: { + isEnabled: false, + }, isInputPrefillEnabled: true, isHideQueryParamsEnabled: true, },