2
0

🐛 (js) Improve session remember behavior

Make sure it correctly retrieves saved variables and doesn't clash with other embedded typebots
This commit is contained in:
Baptiste Arnaud
2023-03-02 10:55:03 +01:00
parent c172a44566
commit ba253cf3e9
16 changed files with 122 additions and 42 deletions

View File

@ -441,7 +441,6 @@
"hostAvatar": { "hostAvatar": {
"isEnabled": true "isEnabled": true
}, },
"guestAvatar": { "isEnabled": false },
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" }, "hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" } "guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
}, },
@ -451,7 +450,13 @@
} }
}, },
"settings": { "settings": {
"general": { "isBrandingEnabled": true }, "general": {
"isBrandingEnabled": true,
"isInputPrefillEnabled": true,
"isResultSavingEnabled": true,
"isHideQueryParamsEnabled": true,
"isNewResultOnRefreshEnabled": true
},
"metadata": { "metadata": {
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form." "description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
}, },

View File

@ -467,7 +467,6 @@
"hostAvatar": { "hostAvatar": {
"isEnabled": true "isEnabled": true
}, },
"guestAvatar": { "isEnabled": false },
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" }, "hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" } "guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
}, },
@ -480,6 +479,7 @@
"general": { "general": {
"isBrandingEnabled": true, "isBrandingEnabled": true,
"isInputPrefillEnabled": true, "isInputPrefillEnabled": true,
"isResultSavingEnabled": true,
"isHideQueryParamsEnabled": true, "isHideQueryParamsEnabled": true,
"isNewResultOnRefreshEnabled": true "isNewResultOnRefreshEnabled": true
}, },

View File

@ -558,6 +558,7 @@
"general": { "general": {
"isBrandingEnabled": true, "isBrandingEnabled": true,
"isInputPrefillEnabled": true, "isInputPrefillEnabled": true,
"isResultSavingEnabled": true,
"isHideQueryParamsEnabled": true, "isHideQueryParamsEnabled": true,
"isNewResultOnRefreshEnabled": true "isNewResultOnRefreshEnabled": true
}, },

View File

@ -336,11 +336,11 @@
"placeholderColor": "#9095A0" "placeholderColor": "#9095A0"
}, },
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" }, "buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" },
"hostAvatar": { "hostAvatar": {
"isEnabled": true "isEnabled": true
} },
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
}, },
"general": { "general": {
"font": "Open Sans", "font": "Open Sans",
@ -348,7 +348,13 @@
} }
}, },
"settings": { "settings": {
"general": { "isBrandingEnabled": true }, "general": {
"isBrandingEnabled": true,
"isInputPrefillEnabled": true,
"isResultSavingEnabled": true,
"isHideQueryParamsEnabled": true,
"isNewResultOnRefreshEnabled": true
},
"metadata": { "metadata": {
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form." "description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
}, },

View File

@ -790,6 +790,8 @@
"general": { "general": {
"isBrandingEnabled": true, "isBrandingEnabled": true,
"isInputPrefillEnabled": true, "isInputPrefillEnabled": true,
"isResultSavingEnabled": true,
"isHideQueryParamsEnabled": true,
"isNewResultOnRefreshEnabled": true "isNewResultOnRefreshEnabled": true
}, },
"metadata": { "metadata": {

View File

@ -465,6 +465,8 @@
"general": { "general": {
"isBrandingEnabled": true, "isBrandingEnabled": true,
"isInputPrefillEnabled": true, "isInputPrefillEnabled": true,
"isResultSavingEnabled": true,
"isHideQueryParamsEnabled": true,
"isNewResultOnRefreshEnabled": true "isNewResultOnRefreshEnabled": true
}, },
"metadata": { "metadata": {

View File

@ -959,6 +959,8 @@
"general": { "general": {
"isBrandingEnabled": true, "isBrandingEnabled": true,
"isInputPrefillEnabled": true, "isInputPrefillEnabled": true,
"isResultSavingEnabled": true,
"isHideQueryParamsEnabled": true,
"isNewResultOnRefreshEnabled": true "isNewResultOnRefreshEnabled": true
}, },
"metadata": { "metadata": {

View File

@ -89,7 +89,7 @@ test.describe('Builder', () => {
await page.click('text=Save in variables') await page.click('text=Save in variables')
await page.click('text=Add an entry >> nth=-1') await page.click('text=Add an entry >> nth=-1')
await page.click('input[placeholder="Select the data"]') await page.click('input[placeholder="Select the data"]')
await page.click('text=data.map(item => item.name)') await page.click('text=data.flatMap(item => item.name)')
}) })
}) })

View File

@ -235,9 +235,12 @@ export const TypebotProvider = ({
) )
useEffect(() => { useEffect(() => {
Router.events.on('routeChangeStart', () => saveTypebot()) const handleSaveTypebot = () => {
saveTypebot()
}
Router.events.on('routeChangeStart', handleSaveTypebot)
return () => { return () => {
Router.events.off('routeChangeStart', () => saveTypebot()) Router.events.off('routeChangeStart', handleSaveTypebot)
} }
}, [saveTypebot]) }, [saveTypebot])

View File

@ -88,7 +88,7 @@ export const GeneralSettingsForm = ({
: true : true
} }
onCheckChange={handleNewResultOnRefreshChange} onCheckChange={handleNewResultOnRefreshChange}
moreInfoContent="If the user refreshes the page, its existing results will be overwritten. Disable this if you want to create a new results every time the user refreshes the page." 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"

View File

@ -1,8 +1,9 @@
import { checkChatsUsage } from '@/features/usage' import { checkChatsUsage } from '@/features/usage'
import { import {
parsePrefilledVariables, prefillVariables,
deepParseVariable, deepParseVariable,
parseVariables, parseVariables,
injectVariablesFromExistingResult,
} from '@/features/variables' } from '@/features/variables'
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { publicProcedure } from '@/utils/server/trpc' import { publicProcedure } from '@/utils/server/trpc'
@ -20,6 +21,7 @@ import {
Theme, Theme,
Typebot, Typebot,
Variable, Variable,
VariableWithValue,
} from 'models' } from 'models'
import { import {
continueBotFlow, continueBotFlow,
@ -27,7 +29,7 @@ import {
setResultAsCompleted, setResultAsCompleted,
startBotFlow, startBotFlow,
} from '../utils' } from '../utils'
import { env, omit } from 'utils' import { env, isDefined, omit } from 'utils'
export const sendMessageProcedure = publicProcedure export const sendMessageProcedure = publicProcedure
.meta({ .meta({
@ -109,19 +111,24 @@ const startSession = async (startParams?: StartParams, userId?: string) => {
const typebot = await getTypebot(startParams, userId) const typebot = await getTypebot(startParams, userId)
const startVariables = startParams.prefilledVariables const prefilledVariables = startParams.prefilledVariables
? parsePrefilledVariables(typebot.variables, startParams.prefilledVariables) ? prefillVariables(typebot.variables, startParams.prefilledVariables)
: typebot.variables : typebot.variables
const result = await getResult({ const result = await getResult({
...startParams, ...startParams,
isPreview, isPreview,
typebot: typebot.id, typebot: typebot.id,
startVariables, prefilledVariables,
isNewResultOnRefreshEnabled: isNewResultOnRefreshEnabled:
typebot.settings.general.isNewResultOnRefreshEnabled ?? false, typebot.settings.general.isNewResultOnRefreshEnabled ?? false,
}) })
const startVariables =
result && result.variables.length > 0
? injectVariablesFromExistingResult(prefilledVariables, result.variables)
: prefilledVariables
const initialState: SessionState = { const initialState: SessionState = {
typebot: { typebot: {
id: typebot.id, id: typebot.id,
@ -293,35 +300,64 @@ const getResult = async ({
typebot, typebot,
isPreview, isPreview,
resultId, resultId,
startVariables, prefilledVariables,
isNewResultOnRefreshEnabled, isNewResultOnRefreshEnabled,
}: Pick<StartParams, 'isPreview' | 'resultId' | 'typebot'> & { }: Pick<StartParams, 'isPreview' | 'resultId' | 'typebot'> & {
startVariables: Variable[] prefilledVariables: Variable[]
isNewResultOnRefreshEnabled: boolean isNewResultOnRefreshEnabled: boolean
}) => { }) => {
if (isPreview || typeof typebot !== 'string') return undefined if (isPreview || typeof typebot !== 'string') return
const data = {
isCompleted: false,
typebotId: typebot,
variables: startVariables.filter((variable) => variable.value),
} satisfies Prisma.ResultUncheckedCreateInput
const select = { const select = {
id: true, id: true,
variables: true, variables: true,
hasStarted: true, hasStarted: true,
} satisfies Prisma.ResultSelect } satisfies Prisma.ResultSelect
return (
const existingResult =
resultId && !isNewResultOnRefreshEnabled resultId && !isNewResultOnRefreshEnabled
? await prisma.result.update({ ? ((await prisma.result.findFirst({
where: { id: resultId }, where: { id: resultId },
data,
select, select,
}) })) as Pick<Result, 'id' | 'variables' | 'hasStarted'>)
: await prisma.result.create({ : undefined
data,
select, if (existingResult) {
}) const prefilledVariableWithValue = prefilledVariables.filter(
) as Pick<Result, 'id' | 'variables' | 'hasStarted'> (prefilledVariable) => isDefined(prefilledVariable.value)
)
const updatedResult = {
variables: prefilledVariableWithValue.concat(
existingResult.variables.filter(
(resultVariable) =>
isDefined(resultVariable.value) &&
!prefilledVariableWithValue.some(
(prefilledVariable) =>
prefilledVariable.name === resultVariable.name
)
)
) as VariableWithValue[],
}
await prisma.result.updateMany({
where: { id: existingResult.id },
data: updatedResult,
})
return {
id: existingResult.id,
variables: updatedResult.variables,
hasStarted: existingResult.hasStarted,
}
} else {
return (await prisma.result.create({
data: {
isCompleted: false,
typebotId: typebot,
variables: prefilledVariables.filter((variable) =>
isDefined(variable.value)
),
},
select,
})) as Pick<Result, 'id' | 'variables' | 'hasStarted'>
}
} }
const parseDynamicThemeInState = (theme: Theme) => { const parseDynamicThemeInState = (theme: Theme) => {

View File

@ -1,5 +1,6 @@
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { import {
Result,
SessionState, SessionState,
StartParams, StartParams,
Typebot, Typebot,
@ -119,7 +120,7 @@ export const deepParseVariable =
return { ...newObj, [key]: currentValue } return { ...newObj, [key]: currentValue }
}, {} as T) }, {} as T)
export const parsePrefilledVariables = ( export const prefillVariables = (
variables: Typebot['variables'], variables: Typebot['variables'],
prefilledVariables: NonNullable<StartParams['prefilledVariables']> prefilledVariables: NonNullable<StartParams['prefilledVariables']>
): Variable[] => ): Variable[] =>
@ -132,6 +133,22 @@ export const parsePrefilledVariables = (
} }
}) })
export const injectVariablesFromExistingResult = (
variables: Typebot['variables'],
resultVariables: Result['variables']
): Variable[] =>
variables.map((variable) => {
const resultVariable = resultVariables.find(
(resultVariable) =>
resultVariable.name === variable.name && !variable.value
)
if (!resultVariable) return variable
return {
...variable,
value: resultVariable.value,
}
})
export const updateVariables = export const updateVariables =
(state: SessionState) => (state: SessionState) =>
async (newVariables: VariableWithUnknowValue[]): Promise<SessionState> => ({ async (newVariables: VariableWithUnknowValue[]): Promise<SessionState> => ({

View File

@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/js", "name": "@typebot.io/js",
"version": "0.0.17", "version": "0.0.18",
"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

@ -43,13 +43,15 @@ export const Bot = (props: BotProps & { class?: string }) => {
urlParams.forEach((value, key) => { urlParams.forEach((value, key) => {
prefilledVariables[key] = value prefilledVariables[key] = value
}) })
const typebotIdFromProps =
typeof props.typebot === 'string' ? props.typebot : undefined
const { data, error } = await getInitialChatReplyQuery({ const { data, error } = await getInitialChatReplyQuery({
typebot: props.typebot, typebot: props.typebot,
apiHost: props.apiHost, apiHost: props.apiHost,
isPreview: props.isPreview ?? false, isPreview: props.isPreview ?? false,
resultId: isNotEmpty(props.resultId) resultId: isNotEmpty(props.resultId)
? props.resultId ? props.resultId
: getExistingResultIdFromSession(), : getExistingResultIdFromSession(typebotIdFromProps),
startGroupId: props.startGroupId, startGroupId: props.startGroupId,
prefilledVariables: { prefilledVariables: {
...prefilledVariables, ...prefilledVariables,
@ -66,7 +68,8 @@ 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) setResultInSession(data.resultId) if (data.resultId && typebotIdFromProps)
setResultInSession(typebotIdFromProps, data.resultId)
setInitialChatReply(data) setInitialChatReply(data)
setCustomCss(data.typebot.theme.customCss ?? '') setCustomCss(data.typebot.theme.customCss ?? '')

View File

@ -1,16 +1,19 @@
const sessionStorageKey = 'resultId' const sessionStorageKey = 'resultId'
export const getExistingResultIdFromSession = () => { export const getExistingResultIdFromSession = (typebotId?: string) => {
if (!typebotId) return
try { try {
return sessionStorage.getItem(sessionStorageKey) ?? undefined return (
sessionStorage.getItem(`${sessionStorageKey}-${typebotId}`) ?? undefined
)
} catch { } catch {
/* empty */ /* empty */
} }
} }
export const setResultInSession = (resultId: string) => { export const setResultInSession = (typebotId: string, resultId: string) => {
try { try {
return sessionStorage.setItem(sessionStorageKey, resultId) return sessionStorage.setItem(`${sessionStorageKey}-${typebotId}`, resultId)
} catch { } catch {
/* empty */ /* empty */
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/react", "name": "@typebot.io/react",
"version": "0.0.17", "version": "0.0.18",
"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",