🐛 (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:
@ -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."
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
|
@ -558,6 +558,7 @@
|
|||||||
"general": {
|
"general": {
|
||||||
"isBrandingEnabled": true,
|
"isBrandingEnabled": true,
|
||||||
"isInputPrefillEnabled": true,
|
"isInputPrefillEnabled": true,
|
||||||
|
"isResultSavingEnabled": true,
|
||||||
"isHideQueryParamsEnabled": true,
|
"isHideQueryParamsEnabled": true,
|
||||||
"isNewResultOnRefreshEnabled": true
|
"isNewResultOnRefreshEnabled": true
|
||||||
},
|
},
|
||||||
|
@ -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."
|
||||||
},
|
},
|
||||||
|
@ -790,6 +790,8 @@
|
|||||||
"general": {
|
"general": {
|
||||||
"isBrandingEnabled": true,
|
"isBrandingEnabled": true,
|
||||||
"isInputPrefillEnabled": true,
|
"isInputPrefillEnabled": true,
|
||||||
|
"isResultSavingEnabled": true,
|
||||||
|
"isHideQueryParamsEnabled": true,
|
||||||
"isNewResultOnRefreshEnabled": true
|
"isNewResultOnRefreshEnabled": true
|
||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
|
@ -465,6 +465,8 @@
|
|||||||
"general": {
|
"general": {
|
||||||
"isBrandingEnabled": true,
|
"isBrandingEnabled": true,
|
||||||
"isInputPrefillEnabled": true,
|
"isInputPrefillEnabled": true,
|
||||||
|
"isResultSavingEnabled": true,
|
||||||
|
"isHideQueryParamsEnabled": true,
|
||||||
"isNewResultOnRefreshEnabled": true
|
"isNewResultOnRefreshEnabled": true
|
||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
|
@ -959,6 +959,8 @@
|
|||||||
"general": {
|
"general": {
|
||||||
"isBrandingEnabled": true,
|
"isBrandingEnabled": true,
|
||||||
"isInputPrefillEnabled": true,
|
"isInputPrefillEnabled": true,
|
||||||
|
"isResultSavingEnabled": true,
|
||||||
|
"isHideQueryParamsEnabled": true,
|
||||||
"isNewResultOnRefreshEnabled": true
|
"isNewResultOnRefreshEnabled": true
|
||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
|
@ -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)')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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])
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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) => {
|
||||||
|
@ -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> => ({
|
||||||
|
@ -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",
|
||||||
|
@ -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 ?? '')
|
||||||
|
|
||||||
|
@ -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 */
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
Reference in New Issue
Block a user