⚡ 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
|
<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} />
|
||||||
|
@ -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:
|
||||||
|
<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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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({
|
||||||
|
@ -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",
|
||||||
|
@ -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 ?? '')
|
||||||
|
|
||||||
|
@ -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",
|
"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",
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user