From f099a46f833fa69cca5ca5fc960a82152b5528ba Mon Sep 17 00:00:00 2001 From: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com> Date: Fri, 7 Mar 2025 18:11:14 +0530 Subject: [PATCH] feat: surveys package integration with v2 apis (#4882) Co-authored-by: Piyush Gupta --- .../[environmentId]/displays/lib/contact.ts | 26 ++++ .../[environmentId]/displays/lib/display.ts | 54 ++++++++ .../client/[environmentId]/displays/route.ts | 56 +++++++- .../[environmentId]/displays/types/display.ts | 9 ++ packages/api/src/api/client/user.ts | 3 +- packages/js-core/src/lib/initialize.ts | 131 ++---------------- packages/js-core/src/lib/widget.ts | 2 +- .../src/components/survey-web-view.tsx | 2 +- packages/react-native/src/lib/common/setup.ts | 29 +++- .../src/lib/common/tests/utils.test.ts | 3 +- packages/react-native/src/lib/user/state.ts | 1 + packages/react-native/src/types/config.ts | 1 + packages/react-native/src/types/survey.ts | 14 +- .../surveys/src/components/general/survey.tsx | 30 ++-- packages/surveys/src/lib/api-client.ts | 22 ++- packages/surveys/src/lib/response-queue.ts | 1 + packages/surveys/src/lib/survey-state.ts | 16 ++- packages/types/formbricks-surveys.ts | 1 + 18 files changed, 254 insertions(+), 147 deletions(-) create mode 100644 apps/web/app/api/v2/client/[environmentId]/displays/lib/contact.ts create mode 100644 apps/web/app/api/v2/client/[environmentId]/displays/lib/display.ts create mode 100644 apps/web/app/api/v2/client/[environmentId]/displays/types/display.ts diff --git a/apps/web/app/api/v2/client/[environmentId]/displays/lib/contact.ts b/apps/web/app/api/v2/client/[environmentId]/displays/lib/contact.ts new file mode 100644 index 0000000000..a7c02dad94 --- /dev/null +++ b/apps/web/app/api/v2/client/[environmentId]/displays/lib/contact.ts @@ -0,0 +1,26 @@ +import { contactCache } from "@/lib/cache/contact"; +import { cache as reactCache } from "react"; +import { prisma } from "@formbricks/database"; +import { cache } from "@formbricks/lib/cache"; + +export const doesContactExist = reactCache( + (id: string): Promise => + cache( + async () => { + const contact = await prisma.contact.findFirst({ + where: { + id, + }, + select: { + id: true, + }, + }); + + return !!contact; + }, + [`doesContactExistDisplaysApiV2-${id}`], + { + tags: [contactCache.tag.byId(id)], + } + )() +); diff --git a/apps/web/app/api/v2/client/[environmentId]/displays/lib/display.ts b/apps/web/app/api/v2/client/[environmentId]/displays/lib/display.ts new file mode 100644 index 0000000000..c6ddd6479f --- /dev/null +++ b/apps/web/app/api/v2/client/[environmentId]/displays/lib/display.ts @@ -0,0 +1,54 @@ +import { + TDisplayCreateInputV2, + ZDisplayCreateInputV2, +} from "@/app/api/v2/client/[environmentId]/displays/types/display"; +import { Prisma } from "@prisma/client"; +import { prisma } from "@formbricks/database"; +import { displayCache } from "@formbricks/lib/display/cache"; +import { validateInputs } from "@formbricks/lib/utils/validate"; +import { DatabaseError } from "@formbricks/types/errors"; +import { doesContactExist } from "./contact"; + +export const createDisplay = async (displayInput: TDisplayCreateInputV2): Promise<{ id: string }> => { + validateInputs([displayInput, ZDisplayCreateInputV2]); + + const { environmentId, contactId, surveyId } = displayInput; + + try { + const contactExists = contactId ? await doesContactExist(contactId) : false; + + const display = await prisma.display.create({ + data: { + survey: { + connect: { + id: surveyId, + }, + }, + + ...(contactExists && { + contact: { + connect: { + id: contactId, + }, + }, + }), + }, + select: { id: true, contactId: true, surveyId: true }, + }); + + displayCache.revalidate({ + id: display.id, + contactId: display.contactId, + surveyId: display.surveyId, + environmentId, + }); + + return display; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; + } +}; diff --git a/apps/web/app/api/v2/client/[environmentId]/displays/route.ts b/apps/web/app/api/v2/client/[environmentId]/displays/route.ts index 10df87540a..bddc7cb7de 100644 --- a/apps/web/app/api/v2/client/[environmentId]/displays/route.ts +++ b/apps/web/app/api/v2/client/[environmentId]/displays/route.ts @@ -1,3 +1,55 @@ -import { OPTIONS, POST } from "@/app/api/v1/client/[environmentId]/displays/route"; +import { ZDisplayCreateInputV2 } from "@/app/api/v2/client/[environmentId]/displays/types/display"; +import { responses } from "@/app/lib/api/response"; +import { transformErrorToDetails } from "@/app/lib/api/validator"; +import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; +import { capturePosthogEnvironmentEvent } from "@formbricks/lib/posthogServer"; +import { InvalidInputError } from "@formbricks/types/errors"; +import { createDisplay } from "./lib/display"; -export { OPTIONS, POST }; +interface Context { + params: Promise<{ + environmentId: string; + }>; +} + +export const OPTIONS = async (): Promise => { + return responses.successResponse({}, true); +}; + +export const POST = async (request: Request, context: Context): Promise => { + const params = await context.params; + const jsonInput = await request.json(); + const inputValidation = ZDisplayCreateInputV2.safeParse({ + ...jsonInput, + environmentId: params.environmentId, + }); + + if (!inputValidation.success) { + return responses.badRequestResponse( + "Fields are missing or incorrectly formatted", + transformErrorToDetails(inputValidation.error), + true + ); + } + + if (inputValidation.data.contactId) { + const isContactsEnabled = await getIsContactsEnabled(); + if (!isContactsEnabled) { + return responses.forbiddenResponse("User identification is only available for enterprise users.", true); + } + } + + try { + const response = await createDisplay(inputValidation.data); + + await capturePosthogEnvironmentEvent(inputValidation.data.environmentId, "display created"); + return responses.successResponse(response, true); + } catch (error) { + if (error instanceof InvalidInputError) { + return responses.badRequestResponse(error.message); + } else { + console.error(error); + return responses.internalServerErrorResponse(error.message); + } + } +}; diff --git a/apps/web/app/api/v2/client/[environmentId]/displays/types/display.ts b/apps/web/app/api/v2/client/[environmentId]/displays/types/display.ts new file mode 100644 index 0000000000..55df1a3392 --- /dev/null +++ b/apps/web/app/api/v2/client/[environmentId]/displays/types/display.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; +import { ZId } from "@formbricks/types/common"; +import { ZDisplayCreateInput } from "@formbricks/types/displays"; + +export const ZDisplayCreateInputV2 = ZDisplayCreateInput.omit({ userId: true }).extend({ + contactId: ZId.optional(), +}); + +export type TDisplayCreateInputV2 = z.infer; diff --git a/packages/api/src/api/client/user.ts b/packages/api/src/api/client/user.ts index c86378903e..0261af4fa8 100644 --- a/packages/api/src/api/client/user.ts +++ b/packages/api/src/api/client/user.ts @@ -18,6 +18,7 @@ export class UserAPI { expiresAt: Date | null; data: { userId: string | null; + contactId: string | null; segments: string[]; displays: { surveyId: string; createdAt: Date }[]; responses: string[]; @@ -36,7 +37,7 @@ export class UserAPI { attributes[key] = String(userUpdateInput.attributes[key]); } - return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/user`, "POST", { + return makeRequest(this.apiHost, `/api/v2/client/${this.environmentId}/user`, "POST", { userId: userUpdateInput.userId, attributes, }); diff --git a/packages/js-core/src/lib/initialize.ts b/packages/js-core/src/lib/initialize.ts index 453c8feece..b51f106f3a 100644 --- a/packages/js-core/src/lib/initialize.ts +++ b/packages/js-core/src/lib/initialize.ts @@ -4,11 +4,7 @@ import { type ApiErrorResponse } from "@formbricks/types/errors"; import { type TJsConfig, type TJsConfigInput } from "@formbricks/types/js"; import { updateAttributes } from "./attributes"; import { Config } from "./config"; -import { - JS_LOCAL_STORAGE_KEY, - LEGACY_JS_APP_LOCAL_STORAGE_KEY, - LEGACY_JS_WEBSITE_LOCAL_STORAGE_KEY, -} from "./constants"; +import { JS_LOCAL_STORAGE_KEY } from "./constants"; import { fetchEnvironmentState } from "./environment-state"; import { ErrorHandler, @@ -34,105 +30,21 @@ export const setIsInitialized = (value: boolean): void => { isInitialized = value; }; -const migrateLocalStorage = (): { changed: boolean; newState?: TJsConfig } => { - const oldWebsiteConfig = localStorage.getItem(LEGACY_JS_WEBSITE_LOCAL_STORAGE_KEY); - const oldAppConfig = localStorage.getItem(LEGACY_JS_APP_LOCAL_STORAGE_KEY); +// If the js sdk is being used with user identification but there is no contactId, we can just resync +export const migrateUserStateAddContactId = (): { changed: boolean } => { + const existingConfigString = localStorage.getItem(JS_LOCAL_STORAGE_KEY); - if (oldWebsiteConfig) { - localStorage.removeItem(LEGACY_JS_WEBSITE_LOCAL_STORAGE_KEY); - const parsedOldConfig = JSON.parse(oldWebsiteConfig) as Partial<{ - environmentId: string; - apiHost: string; - environmentState: TJsConfig["environmentState"]; - personState: TJsConfig["personState"]; - filteredSurveys: TJsConfig["filteredSurveys"]; - }>; + if (existingConfigString) { + const existingConfig = JSON.parse(existingConfigString) as Partial; - if ( - parsedOldConfig.environmentId && - parsedOldConfig.apiHost && - parsedOldConfig.environmentState && - parsedOldConfig.personState && - parsedOldConfig.filteredSurveys - ) { - const newLocalStorageConfig = { ...parsedOldConfig }; - - return { - changed: true, - newState: newLocalStorageConfig as TJsConfig, - }; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- data could be undefined + if (existingConfig.personState?.data?.contactId) { + return { changed: false }; } - } - if (oldAppConfig) { - localStorage.removeItem(LEGACY_JS_APP_LOCAL_STORAGE_KEY); - const parsedOldConfig = JSON.parse(oldAppConfig) as Partial<{ - environmentId: string; - apiHost: string; - environmentState: TJsConfig["environmentState"]; - personState: TJsConfig["personState"]; - filteredSurveys: TJsConfig["filteredSurveys"]; - }>; - - if ( - parsedOldConfig.environmentId && - parsedOldConfig.apiHost && - parsedOldConfig.environmentState && - parsedOldConfig.personState && - parsedOldConfig.filteredSurveys - ) { - return { - changed: true, - }; - } - } - - return { - changed: false, - }; -}; - -const migrateProductToProject = (): { changed: boolean; newState?: TJsConfig } => { - const existingConfig = localStorage.getItem(JS_LOCAL_STORAGE_KEY); - - if (existingConfig) { - const parsedConfig = JSON.parse(existingConfig) as TJsConfig; - - // @ts-expect-error - product is not in the type - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- environmentState could be undefined in an error state - if (parsedConfig.environmentState?.data?.product) { - const { environmentState: _, filteredSurveys, ...restConfig } = parsedConfig; - - const fixedFilteredSurveys = filteredSurveys.map((survey) => { - // @ts-expect-error - productOverwrites is not in the type - const { productOverwrites, ...rest } = survey; - return { - ...rest, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- productOverwrites is not in the type - projectOverwrites: productOverwrites, - }; - }); - - // @ts-expect-error - product is not in the type - const { product, ...rest } = parsedConfig.environmentState.data; - - const newLocalStorageConfig = { - ...restConfig, - environmentState: { - ...parsedConfig.environmentState, - data: { - ...rest, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- product is not in the type - project: product, - }, - }, - filteredSurveys: fixedFilteredSurveys, - }; - - return { - changed: true, - newState: newLocalStorageConfig, - }; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- data could be undefined + if (!existingConfig.personState?.data?.contactId && existingConfig.personState?.data?.userId) { + return { changed: true }; } } @@ -149,28 +61,11 @@ export const initialize = async ( let config = Config.getInstance(); - const { changed, newState } = migrateLocalStorage(); + const { changed } = migrateUserStateAddContactId(); if (changed) { config.resetConfig(); config = Config.getInstance(); - - // If the js sdk is being used for non identified users, and we have a new state to update to after migrating, we update the state - // otherwise, we just sync again! - if (!configInput.userId && newState) { - config.update(newState); - } - } - - const { changed: migrated, newState: updatedLocalState } = migrateProductToProject(); - - if (migrated) { - config.resetConfig(); - config = Config.getInstance(); - - if (updatedLocalState) { - config.update(updatedLocalState); - } } if (isInitialized) { diff --git a/packages/js-core/src/lib/widget.ts b/packages/js-core/src/lib/widget.ts index 2fbebc6b4c..c272a17682 100644 --- a/packages/js-core/src/lib/widget.ts +++ b/packages/js-core/src/lib/widget.ts @@ -88,7 +88,7 @@ const renderWidget = async ( formbricksSurveys.renderSurvey({ apiHost: config.get().apiHost, environmentId: config.get().environmentId, - userId: config.get().personState.data.userId ?? undefined, + contactId: config.get().personState.data.contactId ?? undefined, action, survey, isBrandingEnabled, diff --git a/packages/react-native/src/components/survey-web-view.tsx b/packages/react-native/src/components/survey-web-view.tsx index a7e81bdc01..e286b7a8b5 100644 --- a/packages/react-native/src/components/survey-web-view.tsx +++ b/packages/react-native/src/components/survey-web-view.tsx @@ -105,7 +105,7 @@ export function SurveyWebView({ survey }: SurveyWebViewProps): JSX.Element | und html: renderHtml({ apiHost: appConfig.get().appUrl, environmentId: appConfig.get().environmentId, - userId: appConfig.get().user.data.userId ?? undefined, + contactId: appConfig.get().user.data.contactId ?? undefined, survey, isBrandingEnabled, styling, diff --git a/packages/react-native/src/lib/common/setup.ts b/packages/react-native/src/lib/common/setup.ts index 45cd25b78d..5779624e21 100644 --- a/packages/react-native/src/lib/common/setup.ts +++ b/packages/react-native/src/lib/common/setup.ts @@ -27,12 +27,39 @@ export const setIsSetup = (state: boolean): void => { isSetup = state; }; +export const migrateUserStateAddContactId = async (): Promise<{ changed: boolean }> => { + const existingConfigString = await AsyncStorage.getItem(RN_ASYNC_STORAGE_KEY); + + if (existingConfigString) { + const existingConfig = JSON.parse(existingConfigString) as Partial; + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- data could be undefined + if (existingConfig.user?.data?.contactId) { + return { changed: false }; + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- data could be undefined + if (!existingConfig.user?.data?.contactId && existingConfig.user?.data?.userId) { + return { changed: true }; + } + } + + return { changed: false }; +}; + export const setup = async ( configInput: TConfigInput ): Promise> => { - const appConfig = RNConfig.getInstance(); + let appConfig = RNConfig.getInstance(); const logger = Logger.getInstance(); + const { changed } = await migrateUserStateAddContactId(); + + if (changed) { + await appConfig.resetConfig(); + appConfig = RNConfig.getInstance(); + } + if (isSetup) { logger.debug("Already set up, skipping setup."); return okVoid(); diff --git a/packages/react-native/src/lib/common/tests/utils.test.ts b/packages/react-native/src/lib/common/tests/utils.test.ts index 36f87d37a7..7985d84f19 100644 --- a/packages/react-native/src/lib/common/tests/utils.test.ts +++ b/packages/react-native/src/lib/common/tests/utils.test.ts @@ -1,4 +1,5 @@ // utils.test.ts +import { beforeEach, describe, expect, test, vi } from "vitest"; import { mockProjectId, mockSurveyId } from "@/lib/common/tests/__mocks__/config.mock"; import { diffInDays, @@ -16,7 +17,6 @@ import type { TSurveyStyling, TUserState, } from "@/types/config"; -import { beforeEach, describe, expect, test, vi } from "vitest"; const mockSurveyId1 = "e3kxlpnzmdp84op9qzxl9olj"; const mockSurveyId2 = "qo9rwjmms42hoy3k85fp8vgu"; @@ -120,6 +120,7 @@ describe("utils.ts", () => { expiresAt: null, data: { userId: null, + contactId: null, segments: [], displays: [], responses: [], diff --git a/packages/react-native/src/lib/user/state.ts b/packages/react-native/src/lib/user/state.ts index 99497f0ad2..388d3e8ccc 100644 --- a/packages/react-native/src/lib/user/state.ts +++ b/packages/react-native/src/lib/user/state.ts @@ -7,6 +7,7 @@ export const DEFAULT_USER_STATE_NO_USER_ID: TUserState = { expiresAt: null, data: { userId: null, + contactId: null, segments: [], displays: [], responses: [], diff --git a/packages/react-native/src/types/config.ts b/packages/react-native/src/types/config.ts index bd3a91c6a7..20b5c3b678 100644 --- a/packages/react-native/src/types/config.ts +++ b/packages/react-native/src/types/config.ts @@ -53,6 +53,7 @@ export interface TUserState { expiresAt: Date | null; data: { userId: string | null; + contactId: string | null; segments: string[]; displays: { surveyId: string; createdAt: Date }[]; responses: string[]; diff --git a/packages/react-native/src/types/survey.ts b/packages/react-native/src/types/survey.ts index 4c75916295..2a7cec0997 100644 --- a/packages/react-native/src/types/survey.ts +++ b/packages/react-native/src/types/survey.ts @@ -1,3 +1,4 @@ +import { type TJsFileUploadParams } from "../../../types/js"; import type { TEnvironmentStateSurvey, TProjectStyling, TSurveyStyling } from "@/types/config"; import type { TResponseData, TResponseUpdate } from "@/types/response"; import type { TFileUploadParams, TUploadFileConfig } from "@/types/storage"; @@ -37,15 +38,20 @@ export interface SurveyInlineProps extends SurveyBaseProps { } export interface SurveyContainerProps extends Omit { - apiHost: string; - environmentId: string; + apiHost?: string; + environmentId?: string; userId?: string; - onDisplayCreated?: () => void; - onResponseCreated?: () => void; + contactId?: string; + onDisplayCreated?: () => void | Promise; + onResponseCreated?: () => void | Promise; + onFileUpload?: (file: TJsFileUploadParams["file"], config?: TUploadFileConfig) => Promise; + onOpenExternalURL?: (url: string) => void | Promise; mode?: "modal" | "inline"; containerId?: string; clickOutside?: boolean; darkOverlay?: boolean; placement?: "bottomLeft" | "bottomRight" | "topLeft" | "topRight" | "center"; action?: string; + singleUseId?: string; + singleUseResponseId?: string; } diff --git a/packages/surveys/src/components/general/survey.tsx b/packages/surveys/src/components/general/survey.tsx index 2bc7fd9f7b..273403bfb1 100644 --- a/packages/surveys/src/components/general/survey.tsx +++ b/packages/surveys/src/components/general/survey.tsx @@ -34,6 +34,11 @@ interface VariableStackEntry { } export function Survey({ + apiHost, + environmentId, + userId, + contactId, + mode, survey, styling, isBrandingEnabled, @@ -43,6 +48,9 @@ export function Survey({ onClose, onFinished, onRetry, + onDisplayCreated, + onResponseCreated, + onOpenExternalURL, isRedirectDisabled = false, prefillResponseData, skipPrefilled, @@ -58,16 +66,9 @@ export function Survey({ shouldResetQuestionId, fullSizeCards = false, autoFocus, - apiHost, - environmentId, - userId, action, - onDisplayCreated, - onResponseCreated, singleUseId, singleUseResponseId, - mode, - onOpenExternalURL, }: SurveyContainerProps) { let apiClient: ApiClient | null = null; @@ -81,13 +82,13 @@ export function Survey({ const surveyState = useMemo(() => { if (apiHost && environmentId) { if (mode === "inline") { - return new SurveyState(survey.id, singleUseId, singleUseResponseId, userId); + return new SurveyState(survey.id, singleUseId, singleUseResponseId, userId, contactId); } - return new SurveyState(survey.id, null, null, userId); + return new SurveyState(survey.id, null, null, userId, contactId); } return null; - }, [survey.id, userId, apiHost, environmentId, singleUseId, singleUseResponseId, mode]); + }, [apiHost, environmentId, mode, survey.id, userId, singleUseId, singleUseResponseId, contactId]); // Update the responseQueue to use the stored responseId const responseQueue = useMemo(() => { @@ -208,6 +209,7 @@ export function Survey({ const display = await apiClient.createDisplay({ surveyId: survey.id, ...(userId && { userId }), + ...(contactId && { contactId }), }); if (!display.ok) { @@ -225,7 +227,7 @@ export function Survey({ console.error("error creating display: ", err); } } - }, [apiClient, survey, userId, onDisplayCreated, surveyState, responseQueue]); + }, [apiClient, surveyState, responseQueue, survey.id, userId, contactId, onDisplayCreated]); useEffect(() => { // call onDisplay when component is mounted @@ -382,6 +384,10 @@ export function Survey({ const onResponseCreateOrUpdate = useCallback( (responseUpdate: TResponseUpdate) => { if (surveyState && responseQueue) { + if (contactId) { + surveyState.updateContactId(contactId); + } + if (userId) { surveyState.updateUserId(userId); } @@ -407,7 +413,7 @@ export function Survey({ } } }, - [surveyState, responseQueue, userId, survey, action, hiddenFieldsRecord, onResponseCreated] + [surveyState, responseQueue, contactId, userId, survey, action, hiddenFieldsRecord, onResponseCreated] ); useEffect(() => { diff --git a/packages/surveys/src/lib/api-client.ts b/packages/surveys/src/lib/api-client.ts index 2f839bb963..5827d5f4f9 100644 --- a/packages/surveys/src/lib/api-client.ts +++ b/packages/surveys/src/lib/api-client.ts @@ -16,15 +16,29 @@ export class ApiClient { } async createDisplay( - displayInput: Omit + displayInput: Omit & { contactId?: string } ): Promise> { - return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/displays`, "POST", displayInput); + const fromV1 = !!displayInput.userId; + + return makeRequest( + this.apiHost, + `/api/${fromV1 ? "v1" : "v2"}/client/${this.environmentId}/displays`, + "POST", + displayInput + ); } async createResponse( - responseInput: Omit + responseInput: Omit & { contactId: string | null } ): Promise> { - return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/responses`, "POST", responseInput); + const fromV1 = !!responseInput.userId; + + return makeRequest( + this.apiHost, + `/api/${fromV1 ? "v1" : "v2"}/client/${this.environmentId}/responses`, + "POST", + responseInput + ); } async updateResponse({ diff --git a/packages/surveys/src/lib/response-queue.ts b/packages/surveys/src/lib/response-queue.ts index 5f4fb6a22a..c3cfcf6a1a 100644 --- a/packages/surveys/src/lib/response-queue.ts +++ b/packages/surveys/src/lib/response-queue.ts @@ -89,6 +89,7 @@ export class ResponseQueue { const response = await this.api.createResponse({ ...responseUpdate, surveyId: this.surveyState.surveyId, + contactId: this.surveyState.contactId || null, userId: this.surveyState.userId || null, singleUseId: this.surveyState.singleUseId || null, data: { ...responseUpdate.data, ...responseUpdate.hiddenFields }, diff --git a/packages/surveys/src/lib/survey-state.ts b/packages/surveys/src/lib/survey-state.ts index b904e2c330..7f751940d6 100644 --- a/packages/surveys/src/lib/survey-state.ts +++ b/packages/surveys/src/lib/survey-state.ts @@ -4,6 +4,7 @@ export class SurveyState { responseId: string | null = null; displayId: string | null = null; userId: string | null = null; + contactId: string | null = null; surveyId: string; responseAcc: TResponseUpdate = { finished: false, data: {}, ttc: {}, variables: {} }; singleUseId: string | null; @@ -12,12 +13,14 @@ export class SurveyState { surveyId: string, singleUseId?: string | null, responseId?: string | null, - userId?: string | null + userId?: string | null, + contactId?: string | null ) { this.surveyId = surveyId; this.userId = userId ?? null; this.singleUseId = singleUseId ?? null; this.responseId = responseId ?? null; + this.contactId = contactId ?? null; } /** @@ -36,7 +39,8 @@ export class SurveyState { this.surveyId, this.singleUseId ?? undefined, this.responseId ?? undefined, - this.userId ?? undefined + this.userId ?? undefined, + this.contactId ?? undefined ); copyInstance.responseId = this.responseId; copyInstance.responseAcc = this.responseAcc; @@ -67,6 +71,14 @@ export class SurveyState { this.userId = id; } + /** + * Update the contact ID + * @param id - The contact ID + */ + updateContactId(id: string) { + this.contactId = id; + } + /** * Accumulate the responses * @param responseUpdate - The new response data to add diff --git a/packages/types/formbricks-surveys.ts b/packages/types/formbricks-surveys.ts index 652f90ed2c..26f7af4dfe 100644 --- a/packages/types/formbricks-surveys.ts +++ b/packages/types/formbricks-surveys.ts @@ -46,6 +46,7 @@ export interface SurveyContainerProps extends Omit void | Promise; onResponseCreated?: () => void | Promise; onFileUpload?: (file: TJsFileUploadParams["file"], config?: TUploadFileConfig) => Promise;