From ba253cf3e9574ba53ec6d142e87a8ef44da69f10 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Thu, 2 Mar 2023 10:55:03 +0100 Subject: [PATCH] :bug: (js) Improve session remember behavior Make sure it correctly retrieves saved variables and doesn't clash with other embedded typebots --- .../public/templates/customer-support.json | 9 ++- .../templates/digital-product-payment.json | 2 +- apps/builder/public/templates/faq.json | 1 + apps/builder/public/templates/lead-gen.json | 14 +++- .../public/templates/lead-scoring.json | 2 + apps/builder/public/templates/onboarding.json | 2 + apps/builder/public/templates/quiz.json | 2 + .../integrations/webhook/webhook.spec.ts | 2 +- .../TypebotProvider/TypebotProvider.tsx | 7 +- .../components/GeneralSettingsForm.tsx | 2 +- .../api/procedures/sendMessageProcedure.ts | 80 ++++++++++++++----- apps/viewer/src/features/variables/utils.ts | 19 ++++- packages/js/package.json | 2 +- packages/js/src/components/Bot.tsx | 7 +- packages/js/src/utils/sessionStorage.ts | 11 ++- packages/react/package.json | 2 +- 16 files changed, 122 insertions(+), 42 deletions(-) diff --git a/apps/builder/public/templates/customer-support.json b/apps/builder/public/templates/customer-support.json index 67980d859..eaebf4ca7 100644 --- a/apps/builder/public/templates/customer-support.json +++ b/apps/builder/public/templates/customer-support.json @@ -441,7 +441,6 @@ "hostAvatar": { "isEnabled": true }, - "guestAvatar": { "isEnabled": false }, "hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" }, "guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" } }, @@ -451,7 +450,13 @@ } }, "settings": { - "general": { "isBrandingEnabled": true }, + "general": { + "isBrandingEnabled": true, + "isInputPrefillEnabled": true, + "isResultSavingEnabled": true, + "isHideQueryParamsEnabled": true, + "isNewResultOnRefreshEnabled": true + }, "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." }, diff --git a/apps/builder/public/templates/digital-product-payment.json b/apps/builder/public/templates/digital-product-payment.json index d86011eb6..d243fcf88 100644 --- a/apps/builder/public/templates/digital-product-payment.json +++ b/apps/builder/public/templates/digital-product-payment.json @@ -467,7 +467,6 @@ "hostAvatar": { "isEnabled": true }, - "guestAvatar": { "isEnabled": false }, "hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" }, "guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" } }, @@ -480,6 +479,7 @@ "general": { "isBrandingEnabled": true, "isInputPrefillEnabled": true, + "isResultSavingEnabled": true, "isHideQueryParamsEnabled": true, "isNewResultOnRefreshEnabled": true }, diff --git a/apps/builder/public/templates/faq.json b/apps/builder/public/templates/faq.json index a49c92f73..1443836ca 100644 --- a/apps/builder/public/templates/faq.json +++ b/apps/builder/public/templates/faq.json @@ -558,6 +558,7 @@ "general": { "isBrandingEnabled": true, "isInputPrefillEnabled": true, + "isResultSavingEnabled": true, "isHideQueryParamsEnabled": true, "isNewResultOnRefreshEnabled": true }, diff --git a/apps/builder/public/templates/lead-gen.json b/apps/builder/public/templates/lead-gen.json index 82993dd34..730d48127 100644 --- a/apps/builder/public/templates/lead-gen.json +++ b/apps/builder/public/templates/lead-gen.json @@ -336,11 +336,11 @@ "placeholderColor": "#9095A0" }, "buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" }, - "hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" }, - "guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }, "hostAvatar": { "isEnabled": true - } + }, + "hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" }, + "guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" } }, "general": { "font": "Open Sans", @@ -348,7 +348,13 @@ } }, "settings": { - "general": { "isBrandingEnabled": true }, + "general": { + "isBrandingEnabled": true, + "isInputPrefillEnabled": true, + "isResultSavingEnabled": true, + "isHideQueryParamsEnabled": true, + "isNewResultOnRefreshEnabled": true + }, "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." }, diff --git a/apps/builder/public/templates/lead-scoring.json b/apps/builder/public/templates/lead-scoring.json index 5472168b0..9ecc55e65 100644 --- a/apps/builder/public/templates/lead-scoring.json +++ b/apps/builder/public/templates/lead-scoring.json @@ -790,6 +790,8 @@ "general": { "isBrandingEnabled": true, "isInputPrefillEnabled": true, + "isResultSavingEnabled": true, + "isHideQueryParamsEnabled": true, "isNewResultOnRefreshEnabled": true }, "metadata": { diff --git a/apps/builder/public/templates/onboarding.json b/apps/builder/public/templates/onboarding.json index ca0f7d7cf..891ab4465 100644 --- a/apps/builder/public/templates/onboarding.json +++ b/apps/builder/public/templates/onboarding.json @@ -465,6 +465,8 @@ "general": { "isBrandingEnabled": true, "isInputPrefillEnabled": true, + "isResultSavingEnabled": true, + "isHideQueryParamsEnabled": true, "isNewResultOnRefreshEnabled": true }, "metadata": { diff --git a/apps/builder/public/templates/quiz.json b/apps/builder/public/templates/quiz.json index 7ecdda1a3..b4d1850a9 100644 --- a/apps/builder/public/templates/quiz.json +++ b/apps/builder/public/templates/quiz.json @@ -959,6 +959,8 @@ "general": { "isBrandingEnabled": true, "isInputPrefillEnabled": true, + "isResultSavingEnabled": true, + "isHideQueryParamsEnabled": true, "isNewResultOnRefreshEnabled": true }, "metadata": { diff --git a/apps/builder/src/features/blocks/integrations/webhook/webhook.spec.ts b/apps/builder/src/features/blocks/integrations/webhook/webhook.spec.ts index 30a9bea1e..a6effe157 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/webhook.spec.ts +++ b/apps/builder/src/features/blocks/integrations/webhook/webhook.spec.ts @@ -89,7 +89,7 @@ test.describe('Builder', () => { await page.click('text=Save in variables') await page.click('text=Add an entry >> nth=-1') 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)') }) }) diff --git a/apps/builder/src/features/editor/providers/TypebotProvider/TypebotProvider.tsx b/apps/builder/src/features/editor/providers/TypebotProvider/TypebotProvider.tsx index 390fa4ccb..14fd57b41 100644 --- a/apps/builder/src/features/editor/providers/TypebotProvider/TypebotProvider.tsx +++ b/apps/builder/src/features/editor/providers/TypebotProvider/TypebotProvider.tsx @@ -235,9 +235,12 @@ export const TypebotProvider = ({ ) useEffect(() => { - Router.events.on('routeChangeStart', () => saveTypebot()) + const handleSaveTypebot = () => { + saveTypebot() + } + Router.events.on('routeChangeStart', handleSaveTypebot) return () => { - Router.events.off('routeChangeStart', () => saveTypebot()) + Router.events.off('routeChangeStart', handleSaveTypebot) } }, [saveTypebot]) diff --git a/apps/builder/src/features/settings/components/GeneralSettingsForm.tsx b/apps/builder/src/features/settings/components/GeneralSettingsForm.tsx index 3a9dd7ec8..423a76cdf 100644 --- a/apps/builder/src/features/settings/components/GeneralSettingsForm.tsx +++ b/apps/builder/src/features/settings/components/GeneralSettingsForm.tsx @@ -88,7 +88,7 @@ export const GeneralSettingsForm = ({ : true } 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." /> { const typebot = await getTypebot(startParams, userId) - const startVariables = startParams.prefilledVariables - ? parsePrefilledVariables(typebot.variables, startParams.prefilledVariables) + const prefilledVariables = startParams.prefilledVariables + ? prefillVariables(typebot.variables, startParams.prefilledVariables) : typebot.variables const result = await getResult({ ...startParams, isPreview, typebot: typebot.id, - startVariables, + prefilledVariables, isNewResultOnRefreshEnabled: typebot.settings.general.isNewResultOnRefreshEnabled ?? false, }) + const startVariables = + result && result.variables.length > 0 + ? injectVariablesFromExistingResult(prefilledVariables, result.variables) + : prefilledVariables + const initialState: SessionState = { typebot: { id: typebot.id, @@ -293,35 +300,64 @@ const getResult = async ({ typebot, isPreview, resultId, - startVariables, + prefilledVariables, isNewResultOnRefreshEnabled, }: Pick & { - startVariables: Variable[] + prefilledVariables: Variable[] isNewResultOnRefreshEnabled: boolean }) => { - if (isPreview || typeof typebot !== 'string') return undefined - const data = { - isCompleted: false, - typebotId: typebot, - variables: startVariables.filter((variable) => variable.value), - } satisfies Prisma.ResultUncheckedCreateInput + if (isPreview || typeof typebot !== 'string') return const select = { id: true, variables: true, hasStarted: true, } satisfies Prisma.ResultSelect - return ( + + const existingResult = resultId && !isNewResultOnRefreshEnabled - ? await prisma.result.update({ + ? ((await prisma.result.findFirst({ where: { id: resultId }, - data, select, - }) - : await prisma.result.create({ - data, - select, - }) - ) as Pick + })) as Pick) + : undefined + + if (existingResult) { + const prefilledVariableWithValue = prefilledVariables.filter( + (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 + } } const parseDynamicThemeInState = (theme: Theme) => { diff --git a/apps/viewer/src/features/variables/utils.ts b/apps/viewer/src/features/variables/utils.ts index 63177e61d..8363f066f 100644 --- a/apps/viewer/src/features/variables/utils.ts +++ b/apps/viewer/src/features/variables/utils.ts @@ -1,5 +1,6 @@ import prisma from '@/lib/prisma' import { + Result, SessionState, StartParams, Typebot, @@ -119,7 +120,7 @@ export const deepParseVariable = return { ...newObj, [key]: currentValue } }, {} as T) -export const parsePrefilledVariables = ( +export const prefillVariables = ( variables: Typebot['variables'], prefilledVariables: NonNullable ): 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 = (state: SessionState) => async (newVariables: VariableWithUnknowValue[]): Promise => ({ diff --git a/packages/js/package.json b/packages/js/package.json index 82bea3018..60ff554a4 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/js", - "version": "0.0.17", + "version": "0.0.18", "description": "Javascript library to display typebots on your website", "type": "module", "main": "dist/index.js", diff --git a/packages/js/src/components/Bot.tsx b/packages/js/src/components/Bot.tsx index c550b6cf1..1d3b7e31b 100644 --- a/packages/js/src/components/Bot.tsx +++ b/packages/js/src/components/Bot.tsx @@ -43,13 +43,15 @@ export const Bot = (props: BotProps & { class?: string }) => { urlParams.forEach((value, key) => { prefilledVariables[key] = value }) + const typebotIdFromProps = + typeof props.typebot === 'string' ? props.typebot : undefined const { data, error } = await getInitialChatReplyQuery({ typebot: props.typebot, apiHost: props.apiHost, isPreview: props.isPreview ?? false, resultId: isNotEmpty(props.resultId) ? props.resultId - : getExistingResultIdFromSession(), + : getExistingResultIdFromSession(typebotIdFromProps), startGroupId: props.startGroupId, 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.resultId) setResultInSession(data.resultId) + if (data.resultId && typebotIdFromProps) + setResultInSession(typebotIdFromProps, data.resultId) setInitialChatReply(data) setCustomCss(data.typebot.theme.customCss ?? '') diff --git a/packages/js/src/utils/sessionStorage.ts b/packages/js/src/utils/sessionStorage.ts index 063e8c021..6da1ad923 100644 --- a/packages/js/src/utils/sessionStorage.ts +++ b/packages/js/src/utils/sessionStorage.ts @@ -1,16 +1,19 @@ const sessionStorageKey = 'resultId' -export const getExistingResultIdFromSession = () => { +export const getExistingResultIdFromSession = (typebotId?: string) => { + if (!typebotId) return try { - return sessionStorage.getItem(sessionStorageKey) ?? undefined + return ( + sessionStorage.getItem(`${sessionStorageKey}-${typebotId}`) ?? undefined + ) } catch { /* empty */ } } -export const setResultInSession = (resultId: string) => { +export const setResultInSession = (typebotId: string, resultId: string) => { try { - return sessionStorage.setItem(sessionStorageKey, resultId) + return sessionStorage.setItem(`${sessionStorageKey}-${typebotId}`, resultId) } catch { /* empty */ } diff --git a/packages/react/package.json b/packages/react/package.json index 517fb9c3b..258f142f0 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/react", - "version": "0.0.17", + "version": "0.0.18", "description": "React library to display typebots on your website", "main": "dist/index.js", "types": "dist/index.d.ts",