2
0

Remember result in either local or session storage (#514)

Closes #513
This commit is contained in:
Baptiste Arnaud
2023-05-16 14:58:56 +02:00
committed by GitHub
parent 0ae2a4ba8b
commit 27b009dd76
11 changed files with 129 additions and 50 deletions

View File

@ -8,7 +8,7 @@ export const SwitchWithRelatedSettings = ({ children, ...props }: Props) => (
<Stack <Stack
borderWidth={props.initialValue ? 1 : undefined} borderWidth={props.initialValue ? 1 : undefined}
rounded="md" rounded="md"
p={props.initialValue ? '4' : undefined} p={props.initialValue ? '3' : undefined}
spacing={4} spacing={4}
> >
<SwitchWithLabel {...props} /> <SwitchWithLabel {...props} />

View File

@ -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 { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { Plan } from '@typebot.io/prisma' import { Plan } from '@typebot.io/prisma'
import { GeneralSettings } from '@typebot.io/schemas' import { GeneralSettings, rememberUserStorages } from '@typebot.io/schemas'
import React from 'react' import React from 'react'
import { isDefined } from '@typebot.io/lib' import { isDefined } from '@typebot.io/lib'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel' 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 { LockTag } from '@/features/billing/components/LockTag'
import { isFreePlan } from '@/features/billing/helpers/isFreePlan' import { isFreePlan } from '@/features/billing/helpers/isFreePlan'
import { useI18n } from '@/locales' import { useI18n } from '@/locales'
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
import { DropdownList } from '@/components/DropdownList'
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
type Props = { type Props = {
generalSettings: GeneralSettings generalSettings: GeneralSettings
@ -31,10 +44,13 @@ export const GeneralSettingsForm = ({
}) })
} }
const handleNewResultOnRefreshChange = (isRememberSessionChecked: boolean) => const toggleRememberUser = (isEnabled: boolean) =>
onGeneralSettingsChange({ onGeneralSettingsChange({
...generalSettings, ...generalSettings,
isNewResultOnRefreshEnabled: !isRememberSessionChecked, rememberUser: {
...generalSettings.rememberUser,
isEnabled,
},
}) })
const handleInputPrefillChange = (isInputPrefillEnabled: boolean) => const handleInputPrefillChange = (isInputPrefillEnabled: boolean) =>
@ -49,6 +65,17 @@ export const GeneralSettingsForm = ({
isHideQueryParamsEnabled, isHideQueryParamsEnabled,
}) })
const updateRememberUserStorage = (
storage: NonNullable<GeneralSettings['rememberUser']>['storage']
) =>
onGeneralSettingsChange({
...generalSettings,
rememberUser: {
...generalSettings.rememberUser,
storage,
},
})
return ( return (
<Stack spacing={6}> <Stack spacing={6}>
<ChangePlanModal <ChangePlanModal
@ -77,22 +104,46 @@ export const GeneralSettingsForm = ({
onCheckChange={handleInputPrefillChange} onCheckChange={handleInputPrefillChange}
moreInfoContent="Inputs are automatically pre-filled whenever their associated variable has a value" 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 <SwitchWithLabel
label="Hide query params on bot start" label="Hide query params on bot start"
initialValue={generalSettings.isHideQueryParamsEnabled ?? true} initialValue={generalSettings.isHideQueryParamsEnabled ?? true}
onCheckChange={handleHideQueryParamsChange} onCheckChange={handleHideQueryParamsChange}
moreInfoContent="If your URL contains query params, they will be automatically hidden when the bot starts." 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:&nbsp;
<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> </Stack>
) )
} }

View File

@ -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. - **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. - **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. - **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 ## Typing emulation

View File

@ -124,8 +124,11 @@ const startSession = async (startParams?: StartParams, userId?: string) => {
isPreview: startParams.isPreview || typeof startParams.typebot !== 'string', isPreview: startParams.isPreview || typeof startParams.typebot !== 'string',
typebotId: typebot.id, typebotId: typebot.id,
prefilledVariables, prefilledVariables,
isNewResultOnRefreshEnabled: isRememberUserEnabled:
typebot.settings.general.isNewResultOnRefreshEnabled ?? false, typebot.settings.general.rememberUser?.isEnabled ??
(isDefined(typebot.settings.general.isNewResultOnRefreshEnabled)
? !typebot.settings.general.isNewResultOnRefreshEnabled
: false),
}) })
const startVariables = const startVariables =
@ -291,11 +294,11 @@ const getResult = async ({
isPreview, isPreview,
resultId, resultId,
prefilledVariables, prefilledVariables,
isNewResultOnRefreshEnabled, isRememberUserEnabled,
}: Pick<StartParams, 'isPreview' | 'resultId'> & { }: Pick<StartParams, 'isPreview' | 'resultId'> & {
typebotId: string typebotId: string
prefilledVariables: Variable[] prefilledVariables: Variable[]
isNewResultOnRefreshEnabled: boolean isRememberUserEnabled: boolean
}) => { }) => {
if (isPreview) return if (isPreview) return
const select = { const select = {
@ -305,7 +308,7 @@ const getResult = async ({
} satisfies Prisma.ResultSelect } satisfies Prisma.ResultSelect
const existingResult = const existingResult =
resultId && !isNewResultOnRefreshEnabled resultId && isRememberUserEnabled
? ((await prisma.result.findFirst({ ? ((await prisma.result.findFirst({
where: { id: resultId }, where: { id: resultId },
select, select,

View File

@ -21,7 +21,10 @@ test('Result should be overwritten on page refresh', async ({ page }) => {
...defaultSettings, ...defaultSettings,
general: { general: {
...defaultSettings.general, ...defaultSettings.general,
isNewResultOnRefreshEnabled: false, rememberUser: {
isEnabled: true,
storage: 'session',
},
}, },
}, },
...parseDefaultGroupWithBlock({ ...parseDefaultGroupWithBlock({

View File

@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/js", "name": "@typebot.io/js",
"version": "0.0.51", "version": "0.0.52",
"description": "Javascript library to display typebots on your website", "description": "Javascript library to display typebots on your website",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@ -7,9 +7,9 @@ import { setIsMobile } from '@/utils/isMobileSignal'
import { BotContext, InitialChatReply, OutgoingLog } from '@/types' import { BotContext, InitialChatReply, OutgoingLog } from '@/types'
import { ErrorMessage } from './ErrorMessage' import { ErrorMessage } from './ErrorMessage'
import { import {
getExistingResultIdFromSession, getExistingResultIdFromStorage,
setResultInSession, setResultInStorage,
} from '@/utils/sessionStorage' } from '@/utils/storage'
import { setCssVariablesValue } from '@/utils/setCssVariablesValue' import { setCssVariablesValue } from '@/utils/setCssVariablesValue'
import immutableCss from '../assets/immutable.css' import immutableCss from '../assets/immutable.css'
@ -52,7 +52,7 @@ export const Bot = (props: BotProps & { class?: string }) => {
isPreview: props.isPreview ?? false, isPreview: props.isPreview ?? false,
resultId: isNotEmpty(props.resultId) resultId: isNotEmpty(props.resultId)
? props.resultId ? props.resultId
: getExistingResultIdFromSession(typebotIdFromProps), : getExistingResultIdFromStorage(typebotIdFromProps),
startGroupId: props.startGroupId, startGroupId: props.startGroupId,
prefilledVariables: { prefilledVariables: {
...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) return setError(new Error("Error! Couldn't initiate the chat."))
if (data.resultId && typebotIdFromProps) if (data.resultId && typebotIdFromProps)
setResultInSession(typebotIdFromProps, data.resultId) setResultInStorage(data.typebot.settings.general.rememberUser?.storage)(
typebotIdFromProps,
data.resultId
)
setInitialChatReply(data) setInitialChatReply(data)
setCustomCss(data.typebot.theme.customCss ?? '') setCustomCss(data.typebot.theme.customCss ?? '')

View File

@ -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 */
}
}

View 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 */
}
}

View File

@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/react", "name": "@typebot.io/react",
"version": "0.0.51", "version": "0.0.52",
"description": "React library to display typebots on your website", "description": "React library to display typebots on your website",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@ -1,11 +1,19 @@
import { z } from 'zod' import { z } from 'zod'
export const rememberUserStorages = ['session', 'local'] as const
const generalSettings = z.object({ const generalSettings = z.object({
isBrandingEnabled: z.boolean(), isBrandingEnabled: z.boolean(),
isTypingEmulationEnabled: z.boolean().optional(), isTypingEmulationEnabled: z.boolean().optional(),
isInputPrefillEnabled: z.boolean().optional(), isInputPrefillEnabled: z.boolean().optional(),
isHideQueryParamsEnabled: z.boolean().optional(), isHideQueryParamsEnabled: z.boolean().optional(),
isNewResultOnRefreshEnabled: z.boolean().optional(), isNewResultOnRefreshEnabled: z.boolean().optional(),
rememberUser: z
.object({
isEnabled: z.boolean().optional(),
storage: z.enum(rememberUserStorages).optional(),
})
.optional(),
}) })
const typingEmulation = z.object({ const typingEmulation = z.object({
@ -32,7 +40,9 @@ export const settingsSchema = z.object({
export const defaultSettings: Settings = { export const defaultSettings: Settings = {
general: { general: {
isBrandingEnabled: true, isBrandingEnabled: true,
isNewResultOnRefreshEnabled: true, rememberUser: {
isEnabled: false,
},
isInputPrefillEnabled: true, isInputPrefillEnabled: true,
isHideQueryParamsEnabled: true, isHideQueryParamsEnabled: true,
}, },