⚡ Remember result in either local or session storage (#514)
Closes #513
This commit is contained in:
@ -8,7 +8,7 @@ export const SwitchWithRelatedSettings = ({ children, ...props }: Props) => (
|
||||
<Stack
|
||||
borderWidth={props.initialValue ? 1 : undefined}
|
||||
rounded="md"
|
||||
p={props.initialValue ? '4' : undefined}
|
||||
p={props.initialValue ? '3' : undefined}
|
||||
spacing={4}
|
||||
>
|
||||
<SwitchWithLabel {...props} />
|
||||
|
@ -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<GeneralSettings['rememberUser']>['storage']
|
||||
) =>
|
||||
onGeneralSettingsChange({
|
||||
...generalSettings,
|
||||
rememberUser: {
|
||||
...generalSettings.rememberUser,
|
||||
storage,
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<Stack spacing={6}>
|
||||
<ChangePlanModal
|
||||
@ -77,22 +104,46 @@ export const GeneralSettingsForm = ({
|
||||
onCheckChange={handleInputPrefillChange}
|
||||
moreInfoContent="Inputs are automatically pre-filled whenever their associated variable has a value"
|
||||
/>
|
||||
<SwitchWithLabel
|
||||
label="Remember session"
|
||||
initialValue={
|
||||
isDefined(generalSettings.isNewResultOnRefreshEnabled)
|
||||
? !generalSettings.isNewResultOnRefreshEnabled
|
||||
: true
|
||||
}
|
||||
onCheckChange={handleNewResultOnRefreshChange}
|
||||
moreInfoContent="If the user refreshes the page or opens the typebot again during the same session, his previous variables will be prefilled and his new answers will override the previous ones."
|
||||
/>
|
||||
<SwitchWithLabel
|
||||
label="Hide query params on bot start"
|
||||
initialValue={generalSettings.isHideQueryParamsEnabled ?? true}
|
||||
onCheckChange={handleHideQueryParamsChange}
|
||||
moreInfoContent="If your URL contains query params, they will be automatically hidden when the bot starts."
|
||||
/>
|
||||
<SwitchWithRelatedSettings
|
||||
label={'Remember user'}
|
||||
moreInfoContent="If enabled, user previous variables will be prefilled and his new answers will override the previous ones."
|
||||
initialValue={
|
||||
generalSettings.rememberUser?.isEnabled ??
|
||||
(isDefined(generalSettings.isNewResultOnRefreshEnabled)
|
||||
? !generalSettings.isNewResultOnRefreshEnabled
|
||||
: false)
|
||||
}
|
||||
onCheckChange={toggleRememberUser}
|
||||
>
|
||||
<FormControl as={HStack} justifyContent="space-between">
|
||||
<FormLabel mb="0">
|
||||
Storage:
|
||||
<MoreInfoTooltip>
|
||||
<Stack>
|
||||
<Text>
|
||||
Choose <Tag size="sm">session</Tag> to remember the user as
|
||||
long as he does not closes the tab or the browser.
|
||||
</Text>
|
||||
<Text>
|
||||
Choose <Tag size="sm">local</Tag> to remember the user
|
||||
forever.
|
||||
</Text>
|
||||
</Stack>
|
||||
</MoreInfoTooltip>
|
||||
</FormLabel>
|
||||
<DropdownList
|
||||
currentItem={generalSettings.rememberUser?.storage ?? 'session'}
|
||||
onItemSelect={updateRememberUserStorage}
|
||||
items={rememberUserStorages}
|
||||
></DropdownList>
|
||||
</FormControl>
|
||||
</SwitchWithRelatedSettings>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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<StartParams, 'isPreview' | 'resultId'> & {
|
||||
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,
|
||||
|
@ -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({
|
||||
|
@ -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",
|
||||
|
@ -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 ?? '')
|
||||
|
||||
|
@ -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 */
|
||||
}
|
||||
}
|
29
packages/embeds/js/src/utils/storage.ts
Normal file
29
packages/embeds/js/src/utils/storage.ts
Normal file
@ -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 */
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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,
|
||||
},
|
||||
|
Reference in New Issue
Block a user