From 93e9ec867cc641e961d0dcc07feff3f527c23b4d Mon Sep 17 00:00:00 2001 From: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com> Date: Mon, 30 Dec 2024 19:30:37 +0530 Subject: [PATCH] fix: adds vercel style guide to `@formbricks/js-core` (#4520) --- .../[environmentId]/storage/local/route.ts | 20 ++-- packages/api/src/api/client/attribute.ts | 7 +- packages/api/src/api/client/display.ts | 4 +- packages/api/src/api/client/response.ts | 6 +- packages/api/src/api/client/storage.ts | 30 +++-- packages/api/src/types/index.ts | 16 +-- packages/api/src/utils/make-request.ts | 11 +- packages/js-core/.eslintrc.cjs | 7 +- packages/js-core/src/index.ts | 21 ++-- packages/js-core/src/lib/actions.ts | 10 +- packages/js-core/src/lib/attributes.ts | 76 ++++++------- packages/js-core/src/lib/command-queue.ts | 91 ++++++++++++++++ packages/js-core/src/lib/commandQueue.ts | 80 -------------- packages/js-core/src/lib/config.ts | 40 ++++--- ...vironmentState.ts => environment-state.ts} | 32 +++--- packages/js-core/src/lib/errors.ts | 61 ++++++----- .../{eventListeners.ts => event-listeners.ts} | 6 +- packages/js-core/src/lib/initialize.ts | 103 +++++++++++------- packages/js-core/src/lib/logger.ts | 5 +- .../{noCodeActions.ts => no-code-actions.ts} | 81 ++++++++------ .../lib/{personState.ts => person-state.ts} | 20 ++-- packages/js-core/src/lib/person.ts | 7 +- packages/js-core/src/lib/timeout-stack.ts | 5 +- packages/js-core/src/lib/utils.ts | 94 ++++++++-------- packages/js-core/src/lib/widget.ts | 43 ++++---- packages/js/package.json | 2 +- packages/js/src/index.ts | 12 +- packages/react-native/package.json | 2 +- packages/react-native/src/lib/attributes.ts | 21 ++-- packages/react-native/src/lib/storage.ts | 30 +++-- packages/types/errors.ts | 23 +++- turbo.json | 3 + 32 files changed, 519 insertions(+), 450 deletions(-) create mode 100644 packages/js-core/src/lib/command-queue.ts delete mode 100644 packages/js-core/src/lib/commandQueue.ts rename packages/js-core/src/lib/{environmentState.ts => environment-state.ts} (74%) rename packages/js-core/src/lib/{eventListeners.ts => event-listeners.ts} (94%) rename packages/js-core/src/lib/{noCodeActions.ts => no-code-actions.ts} (72%) rename packages/js-core/src/lib/{personState.ts => person-state.ts} (84%) diff --git a/apps/web/app/api/v1/client/[environmentId]/storage/local/route.ts b/apps/web/app/api/v1/client/[environmentId]/storage/local/route.ts index e49b403859..bdc0af48ea 100644 --- a/apps/web/app/api/v1/client/[environmentId]/storage/local/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/storage/local/route.ts @@ -3,7 +3,6 @@ // method -> PUT (to be the same as the signedUrl method) import { responses } from "@/app/lib/api/response"; import { getBiggerUploadFileSizePermission } from "@/modules/ee/license-check/lib/utils"; -import { headers } from "next/headers"; import { NextRequest } from "next/server"; import { ENCRYPTION_KEY, UPLOADS_DIR } from "@formbricks/lib/constants"; import { validateLocalSignedUrl } from "@formbricks/lib/crypto"; @@ -24,8 +23,7 @@ export const OPTIONS = async (): Promise => { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", - "Access-Control-Allow-Headers": - "Content-Type, Authorization, X-File-Name, X-File-Type, X-Survey-ID, X-Signature, X-Timestamp, X-UUID", + "Access-Control-Allow-Headers": "Content-Type, Authorization", }, } ); @@ -36,15 +34,14 @@ export const POST = async (req: NextRequest, context: Context): Promise ): Promise< - Result< - { changed: boolean; message: string; details?: Record }, - NetworkError | Error | ForbiddenError - > + Result<{ changed: boolean; message: string; details?: Record }, ApiErrorResponse> > { // transform all attributes to string if attributes are present into a new attributes copy const attributes: Record = {}; diff --git a/packages/api/src/api/client/display.ts b/packages/api/src/api/client/display.ts index 4f3113ec19..0e35c8ab19 100644 --- a/packages/api/src/api/client/display.ts +++ b/packages/api/src/api/client/display.ts @@ -1,6 +1,6 @@ import { type TDisplayCreateInput } from "@formbricks/types/displays"; import { type Result } from "@formbricks/types/error-handlers"; -import { type ForbiddenError, type NetworkError } from "@formbricks/types/errors"; +import { type ApiErrorResponse } from "@formbricks/types/errors"; import { makeRequest } from "../../utils/make-request"; export class DisplayAPI { @@ -14,7 +14,7 @@ export class DisplayAPI { async create( displayInput: Omit - ): Promise> { + ): Promise> { return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/displays`, "POST", displayInput); } } diff --git a/packages/api/src/api/client/response.ts b/packages/api/src/api/client/response.ts index ccd385ea0e..1117a40387 100644 --- a/packages/api/src/api/client/response.ts +++ b/packages/api/src/api/client/response.ts @@ -1,5 +1,5 @@ import { type Result } from "@formbricks/types/error-handlers"; -import { type ForbiddenError, type NetworkError } from "@formbricks/types/errors"; +import { type ApiErrorResponse } from "@formbricks/types/errors"; import { type TResponseInput, type TResponseUpdateInput } from "@formbricks/types/responses"; import { makeRequest } from "../../utils/make-request"; @@ -16,7 +16,7 @@ export class ResponseAPI { async create( responseInput: Omit - ): Promise> { + ): Promise> { return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/responses`, "POST", responseInput); } @@ -28,7 +28,7 @@ export class ResponseAPI { ttc, variables, language, - }: TResponseUpdateInputWithResponseId): Promise> { + }: TResponseUpdateInputWithResponseId): Promise> { return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/responses/${responseId}`, "PUT", { finished, endingId, diff --git a/packages/api/src/api/client/storage.ts b/packages/api/src/api/client/storage.ts index c660123e38..a0d4442fb9 100644 --- a/packages/api/src/api/client/storage.ts +++ b/packages/api/src/api/client/storage.ts @@ -47,18 +47,18 @@ export class StorageAPI { const { signedUrl, fileUrl, signingData, presignedFields, updatedFileName } = data; - let requestHeaders: Record = {}; + let localUploadDetails: Record = {}; if (signingData) { const { signature, timestamp, uuid } = signingData; - requestHeaders = { - "X-File-Type": file.type, - "X-File-Name": encodeURIComponent(updatedFileName), - "X-Survey-ID": surveyId ?? "", - "X-Signature": signature, - "X-Timestamp": String(timestamp), - "X-UUID": uuid, + localUploadDetails = { + fileType: file.type, + fileName: encodeURIComponent(updatedFileName), + surveyId: surveyId ?? "", + signature, + timestamp: String(timestamp), + uuid, }; } @@ -91,14 +91,12 @@ export class StorageAPI { try { uploadResponse = await fetch(signedUrlCopy, { method: "POST", - ...(signingData - ? { - headers: { - ...requestHeaders, - }, - } - : {}), - body: presignedFields ? formDataForS3 : JSON.stringify(formData), + body: presignedFields + ? formDataForS3 + : JSON.stringify({ + ...formData, + ...localUploadDetails, + }), }); } catch (err) { console.error("Error uploading file", err); diff --git a/packages/api/src/types/index.ts b/packages/api/src/types/index.ts index 365a31839c..ee4e6f2c2c 100644 --- a/packages/api/src/types/index.ts +++ b/packages/api/src/types/index.ts @@ -1,3 +1,5 @@ +import { type ApiErrorResponse } from "@formbricks/types/errors"; + export interface ApiConfig { environmentId: string; apiHost: string; @@ -8,17 +10,3 @@ export type ApiResponse = ApiSuccessResponse | ApiErrorResponse; export interface ApiSuccessResponse> { data: T; } - -export interface ApiErrorResponse { - code: - | "not_found" - | "gone" - | "bad_request" - | "internal_server_error" - | "unauthorized" - | "method_not_allowed" - | "not_authenticated" - | "forbidden"; - message: string; - details: Record; -} diff --git a/packages/api/src/utils/make-request.ts b/packages/api/src/utils/make-request.ts index 33ed9349eb..245aaa3822 100644 --- a/packages/api/src/utils/make-request.ts +++ b/packages/api/src/utils/make-request.ts @@ -1,13 +1,13 @@ import { type Result, err, ok, wrapThrowsAsync } from "@formbricks/types/error-handlers"; -import { type ForbiddenError, type NetworkError } from "@formbricks/types/errors"; -import type { ApiErrorResponse, ApiResponse, ApiSuccessResponse } from "../types"; +import { type ApiErrorResponse } from "@formbricks/types/errors"; +import { type ApiResponse, type ApiSuccessResponse } from "../types"; export const makeRequest = async ( apiHost: string, endpoint: string, method: "GET" | "POST" | "PUT" | "DELETE", data?: unknown -): Promise> => { +): Promise> => { const url = new URL(apiHost + endpoint); const body = data ? JSON.stringify(data) : undefined; @@ -19,7 +19,8 @@ export const makeRequest = async ( body, }); - if (!res.ok) return err(res.error); + // TODO: Only return api error response relevant keys + if (!res.ok) return err(res.error as unknown as ApiErrorResponse); const response = res.data; const json = (await response.json()) as ApiResponse; @@ -31,7 +32,7 @@ export const makeRequest = async ( status: response.status, message: errorResponse.message || "Something went wrong", url, - ...(Object.keys(errorResponse.details).length > 0 && { details: errorResponse.details }), + ...(Object.keys(errorResponse.details ?? {}).length > 0 && { details: errorResponse.details }), }); } diff --git a/packages/js-core/.eslintrc.cjs b/packages/js-core/.eslintrc.cjs index ed6059c25b..6459e6fb42 100644 --- a/packages/js-core/.eslintrc.cjs +++ b/packages/js-core/.eslintrc.cjs @@ -1,4 +1,7 @@ module.exports = { - extends: ["@formbricks/eslint-config/legacy-library.js"], - parser: "@typescript-eslint/parser", + extends: ["@formbricks/eslint-config/library.js"], + parserOptions: { + project: "tsconfig.json", + tsconfigRootDir: __dirname, + }, }; diff --git a/packages/js-core/src/index.ts b/packages/js-core/src/index.ts index 9a7811d774..6df24370b4 100644 --- a/packages/js-core/src/index.ts +++ b/packages/js-core/src/index.ts @@ -1,12 +1,13 @@ -import { TJsConfigInput, TJsTrackProperties } from "@formbricks/types/js"; +/* eslint-disable import/no-default-export -- We need default exports for the js sdk */ +import { type TJsConfigInput, type TJsTrackProperties } from "@formbricks/types/js"; import { trackCodeAction } from "./lib/actions"; import { getApi } from "./lib/api"; import { setAttributeInApp } from "./lib/attributes"; -import { CommandQueue } from "./lib/commandQueue"; +import { CommandQueue } from "./lib/command-queue"; import { ErrorHandler } from "./lib/errors"; import { initialize } from "./lib/initialize"; import { Logger } from "./lib/logger"; -import { checkPageUrl } from "./lib/noCodeActions"; +import { checkPageUrl } from "./lib/no-code-actions"; import { logoutPerson, resetPerson } from "./lib/person"; const logger = Logger.getInstance(); @@ -14,18 +15,18 @@ const logger = Logger.getInstance(); logger.debug("Create command queue"); const queue = new CommandQueue(); -const init = async (initConfig: TJsConfigInput) => { +const init = async (initConfig: TJsConfigInput): Promise => { ErrorHandler.init(initConfig.errorHandler); queue.add(false, initialize, initConfig); await queue.wait(); }; const setEmail = async (email: string): Promise => { - setAttribute("email", email); + await setAttribute("email", email); await queue.wait(); }; -const setAttribute = async (key: string, value: any): Promise => { +const setAttribute = async (key: string, value: string): Promise => { queue.add(true, setAttributeInApp, key, value); await queue.wait(); }; @@ -40,8 +41,8 @@ const reset = async (): Promise => { await queue.wait(); }; -const track = async (name: string, properties?: TJsTrackProperties): Promise => { - queue.add(true, trackCodeAction, name, properties); +const track = async (code: string, properties?: TJsTrackProperties): Promise => { + queue.add(true, trackCodeAction, code, properties); await queue.wait(); }; @@ -61,5 +62,5 @@ const formbricks = { getApi, }; -export type TFormbricksApp = typeof formbricks; -export default formbricks as TFormbricksApp; +export type TFormbricks = typeof formbricks; +export default formbricks; diff --git a/packages/js-core/src/lib/actions.ts b/packages/js-core/src/lib/actions.ts index 1148bf3a3e..d4d117ffe0 100644 --- a/packages/js-core/src/lib/actions.ts +++ b/packages/js-core/src/lib/actions.ts @@ -1,6 +1,6 @@ -import { TJsTrackProperties } from "@formbricks/types/js"; +import { type TJsTrackProperties } from "@formbricks/types/js"; import { Config } from "./config"; -import { InvalidCodeError, NetworkError, Result, err, okVoid } from "./errors"; +import { type InvalidCodeError, type NetworkError, type Result, err, okVoid } from "./errors"; import { Logger } from "./logger"; import { triggerSurvey } from "./widget"; @@ -12,14 +12,14 @@ export const trackAction = async ( alias?: string, properties?: TJsTrackProperties ): Promise> => { - const aliasName = alias || name; + const aliasName = alias ?? name; logger.debug(`Formbricks: Action "${aliasName}" tracked`); // get a list of surveys that are collecting insights const activeSurveys = config.get().filteredSurveys; - if (!!activeSurveys && activeSurveys.length > 0) { + if (Boolean(activeSurveys) && activeSurveys.length > 0) { for (const survey of activeSurveys) { for (const trigger of survey.triggers) { if (trigger.actionClass.name === name) { @@ -41,7 +41,7 @@ export const trackCodeAction = ( const actionClasses = config.get().environmentState.data.actionClasses; const codeActionClasses = actionClasses.filter((action) => action.type === "code"); - const action = codeActionClasses.find((action) => action.key === code); + const action = codeActionClasses.find((codeActionClass) => codeActionClass.key === code); if (!action) { return err({ diff --git a/packages/js-core/src/lib/attributes.ts b/packages/js-core/src/lib/attributes.ts index 0db882563a..fbd2c27924 100644 --- a/packages/js-core/src/lib/attributes.ts +++ b/packages/js-core/src/lib/attributes.ts @@ -1,10 +1,10 @@ import { FormbricksAPI } from "@formbricks/api"; -import { TAttributes } from "@formbricks/types/attributes"; -import { ForbiddenError } from "@formbricks/types/errors"; +import { type TAttributes } from "@formbricks/types/attributes"; +import { type ApiErrorResponse } from "@formbricks/types/errors"; import { Config } from "./config"; -import { MissingPersonError, NetworkError, Result, err, ok, okVoid } from "./errors"; +import { type Result, err, ok, okVoid } from "./errors"; import { Logger } from "./logger"; -import { fetchPersonState } from "./personState"; +import { fetchPersonState } from "./person-state"; import { filterSurveys } from "./utils"; const config = Config.getInstance(); @@ -20,7 +20,7 @@ export const updateAttribute = async ( message: string; details?: Record; }, - NetworkError | ForbiddenError + ApiErrorResponse > > => { const { apiHost, environmentId } = config.get(); @@ -31,7 +31,7 @@ export const updateAttribute = async ( code: "network_error", status: 500, message: "Missing userId", - url: `${apiHost}/api/v1/client/${environmentId}/contacts/${userId}/attributes`, + url: new URL(`${apiHost}/api/v1/client/${environmentId}/contacts/${userId ?? ""}/attributes`), responseMessage: "Missing userId", }); } @@ -44,9 +44,9 @@ export const updateAttribute = async ( const res = await api.client.attribute.update({ userId, attributes: { [key]: value } }); if (!res.ok) { - // @ts-expect-error - if (res.error.details?.ignore) { - logger.error(res.error.message ?? `Error updating person with userId ${userId}`); + const responseError = res.error; + if (responseError.details?.ignore) { + logger.error(responseError.message); return { ok: true, value: { @@ -57,8 +57,8 @@ export const updateAttribute = async ( } return err({ - code: (res.error as ForbiddenError).code ?? "network_error", - status: (res.error as NetworkError | ForbiddenError).status ?? 500, + code: res.error.code, + status: res.error.status, message: `Error updating person with userId ${userId}`, url: new URL(`${apiHost}/api/v1/client/${environmentId}/contacts/${userId}/attributes`), responseMessage: res.error.message, @@ -66,8 +66,8 @@ export const updateAttribute = async ( } if (res.data.details) { - Object.entries(res.data.details).forEach(([key, value]) => { - logger.error(`${key}: ${value}`); + Object.entries(res.data.details).forEach(([detailsKey, detailsValue]) => { + logger.error(`${detailsKey}: ${detailsValue}`); }); } @@ -103,7 +103,7 @@ export const updateAttributes = async ( environmentId: string, userId: string, attributes: TAttributes -): Promise> => { +): Promise> => { // clean attributes and remove existing attributes if config already exists const updatedAttributes = { ...attributes }; @@ -113,7 +113,7 @@ export const updateAttributes = async ( return ok(updatedAttributes); } - logger.debug("Updating attributes: " + JSON.stringify(updatedAttributes)); + logger.debug(`Updating attributes: ${JSON.stringify(updatedAttributes)}`); const api = new FormbricksAPI({ apiHost, @@ -130,27 +130,28 @@ export const updateAttributes = async ( } return ok(updatedAttributes); - } else { - // @ts-expect-error - if (res.error.details?.ignore) { - logger.error(res.error.message ?? `Error updating person with userId ${userId}`); - return ok(updatedAttributes); - } - - return err({ - code: (res.error as ForbiddenError).code ?? "network_error", - status: (res.error as NetworkError | ForbiddenError).status ?? 500, - message: `Error updating person with userId ${userId}`, - url: new URL(`${apiHost}/api/v1/client/${environmentId}/people/${userId}/attributes`), - responseMessage: res.error.message, - }); } + + const responseError = res.error; + + if (responseError.details?.ignore) { + logger.error(responseError.message); + return ok(updatedAttributes); + } + + return err({ + code: responseError.code, + status: responseError.status, + message: `Error updating person with userId ${userId}`, + url: new URL(`${apiHost}/api/v1/client/${environmentId}/people/${userId}/attributes`), + responseMessage: responseError.responseMessage, + }); }; export const setAttributeInApp = async ( key: string, - value: any -): Promise> => { + value: string +): Promise> => { if (key === "userId") { logger.error("Setting userId is no longer supported. Please set the userId in the init call instead."); return okVoid(); @@ -158,7 +159,7 @@ export const setAttributeInApp = async ( const userId = config.get().personState.data.userId; - logger.debug("Setting attribute: " + key + " to value: " + value); + logger.debug(`Setting attribute: ${key} to value: ${value}`); if (!userId) { logger.error( @@ -194,12 +195,11 @@ export const setAttributeInApp = async ( } return okVoid(); - } else { - const error = result.error; - if (error && error.code === "forbidden") { - logger.error(`Authorization error: ${error.responseMessage}`); - } + } + const error = result.error; + if (error.code === "forbidden") { + logger.error(`Authorization error: ${error.responseMessage ?? ""}`); } - return err(result.error as NetworkError); + return err(result.error); }; diff --git a/packages/js-core/src/lib/command-queue.ts b/packages/js-core/src/lib/command-queue.ts new file mode 100644 index 0000000000..161d1791f1 --- /dev/null +++ b/packages/js-core/src/lib/command-queue.ts @@ -0,0 +1,91 @@ +import { wrapThrowsAsync } from "@formbricks/types/error-handlers"; +import { ErrorHandler, type Result } from "./errors"; +import { checkInitialized } from "./initialize"; + +// Define a base type for acceptable return types +type CommandReturnType = Promise> | Result | Promise; + +// Define a type for functions that return our accepted return types +type CommandFunction = (...args: Args) => CommandReturnType; + +type TArgs = unknown[]; + +// Define a queue item type that's generic over the function and its arguments +interface QueueItem { + command: F; + checkInitialized: boolean; + commandArgs: Parameters; +} + +export class CommandQueue { + private queue: QueueItem[] = []; + private running = false; + private resolvePromise: (() => void) | null = null; + private commandPromise: Promise | null = null; + + // Make add generic over the function type and its parameters + public add( + checkInitializedArg: boolean, + command: CommandFunction, + ...args: Args + ): void { + this.queue.push({ + command: command as CommandFunction, + checkInitialized: checkInitializedArg, + commandArgs: args, + }); + + if (!this.running) { + this.commandPromise = new Promise((resolve) => { + this.resolvePromise = resolve; + void this.run(); + }); + } + } + + public async wait(): Promise { + if (this.running) { + await this.commandPromise; + } + } + + private async run(): Promise { + this.running = true; + + while (this.queue.length > 0) { + const errorHandler = ErrorHandler.getInstance(); + const currentItem = this.queue.shift(); + if (!currentItem) continue; + + if (currentItem.checkInitialized) { + const initResult = checkInitialized(); + if (!initResult.ok) { + errorHandler.handle(initResult.error); + continue; + } + } + + const executeCommand = async (): Promise> => { + return (await currentItem.command.apply(null, currentItem.commandArgs)) as Result; + }; + + const result = await wrapThrowsAsync(executeCommand)(); + + if (result.ok) { + if (!result.data.ok) { + errorHandler.handle(result.data.error); + } + } + if (!result.ok) { + errorHandler.handle(result.error); + } + } + + this.running = false; + if (this.resolvePromise) { + this.resolvePromise(); + this.resolvePromise = null; + this.commandPromise = null; + } + } +} diff --git a/packages/js-core/src/lib/commandQueue.ts b/packages/js-core/src/lib/commandQueue.ts deleted file mode 100644 index c372a569e7..0000000000 --- a/packages/js-core/src/lib/commandQueue.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { wrapThrowsAsync } from "@formbricks/types/error-handlers"; -import { ErrorHandler, Result } from "./errors"; -import { checkInitialized } from "./initialize"; - -export class CommandQueue { - private queue: { - command: (args: any) => Promise> | Result | Promise; - checkInitialized: boolean; - commandArgs: any[any]; - }[] = []; - private running: boolean = false; - private resolvePromise: (() => void) | null = null; - private commandPromise: Promise | null = null; - - public add( - checkInitialized: boolean = true, - command: (...args: A[]) => Promise> | Result | Promise, - ...args: A[] - ) { - this.queue.push({ command, checkInitialized, commandArgs: args }); - - if (!this.running) { - this.commandPromise = new Promise((resolve) => { - this.resolvePromise = resolve; - this.run(); - }); - } - } - - public async wait() { - if (this.running) { - await this.commandPromise; - } - } - - private async run() { - this.running = true; - while (this.queue.length > 0) { - const errorHandler = ErrorHandler.getInstance(); - const currentItem = this.queue.shift(); - - if (!currentItem) continue; - - // make sure formbricks is initialized - if (currentItem.checkInitialized) { - // call different function based on package type - const initResult = checkInitialized(); - - if (initResult && initResult.ok !== true) { - errorHandler.handle(initResult.error); - continue; - } - } - - const executeCommand = async () => { - return (await currentItem?.command.apply(null, currentItem?.commandArgs)) as Result; - }; - - const result = await wrapThrowsAsync(executeCommand)(); - - if (!result) continue; - - if (result.ok) { - if (result.data && !result.data.ok) { - errorHandler.handle(result.data.error); - } - } - - if (result.ok !== true) { - errorHandler.handle(result.error); - } - } - this.running = false; - if (this.resolvePromise) { - this.resolvePromise(); - this.resolvePromise = null; - this.commandPromise = null; - } - } -} diff --git a/packages/js-core/src/lib/config.ts b/packages/js-core/src/lib/config.ts index ca9cfe6bfd..ffb5132cec 100644 --- a/packages/js-core/src/lib/config.ts +++ b/packages/js-core/src/lib/config.ts @@ -1,6 +1,6 @@ -import { TJsConfig, TJsConfigUpdateInput } from "@formbricks/types/js"; +import { type TJsConfig, type TJsConfigUpdateInput } from "@formbricks/types/js"; import { JS_LOCAL_STORAGE_KEY } from "./constants"; -import { Result, err, ok, wrapThrows } from "./errors"; +import { type Result, err, ok, wrapThrows } from "./errors"; export class Config { private static instance: Config | undefined; @@ -22,18 +22,16 @@ export class Config { } public update(newConfig: TJsConfigUpdateInput): void { - if (newConfig) { - this.config = { - ...this.config, - ...newConfig, - status: { - value: newConfig.status?.value || "success", - expiresAt: newConfig.status?.expiresAt || null, - }, - }; + this.config = { + ...this.config, + ...newConfig, + status: { + value: newConfig.status?.value ?? "success", + expiresAt: newConfig.status?.expiresAt ?? null, + }, + }; - this.saveToStorage(); - } + void this.saveToStorage(); } public get(): TJsConfig { @@ -43,7 +41,7 @@ export class Config { return this.config; } - public loadFromLocalStorage(): Result { + public loadFromLocalStorage(): Result { if (typeof window !== "undefined") { const savedConfig = localStorage.getItem(JS_LOCAL_STORAGE_KEY); if (savedConfig) { @@ -54,8 +52,8 @@ export class Config { // check if the config has expired if ( - parsedConfig.environmentState?.expiresAt && - new Date(parsedConfig.environmentState.expiresAt) <= new Date() + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- In case of an error, we don't have environmentState + new Date(parsedConfig.environmentState?.expiresAt) <= new Date() ) { return err(new Error("Config in local storage has expired")); } @@ -67,18 +65,18 @@ export class Config { return err(new Error("No or invalid config in local storage")); } - private async saveToStorage(): Promise, Error>> { - return wrapThrows(async () => { - await localStorage.setItem(JS_LOCAL_STORAGE_KEY, JSON.stringify(this.config)); + private saveToStorage(): Result { + return wrapThrows(() => { + localStorage.setItem(JS_LOCAL_STORAGE_KEY, JSON.stringify(this.config)); })(); } // reset the config - public async resetConfig(): Promise, Error>> { + public resetConfig(): Result { this.config = null; - return wrapThrows(async () => { + return wrapThrows(() => { localStorage.removeItem(JS_LOCAL_STORAGE_KEY); })(); } diff --git a/packages/js-core/src/lib/environmentState.ts b/packages/js-core/src/lib/environment-state.ts similarity index 74% rename from packages/js-core/src/lib/environmentState.ts rename to packages/js-core/src/lib/environment-state.ts index 02fd220782..bf013b2a4f 100644 --- a/packages/js-core/src/lib/environmentState.ts +++ b/packages/js-core/src/lib/environment-state.ts @@ -1,5 +1,6 @@ // shared functions for environment and person state(s) -import { TJsEnvironmentState, TJsEnvironmentSyncParams } from "@formbricks/types/js"; +import { type ApiErrorResponse } from "@formbricks/types/errors"; +import { type TJsEnvironmentState, type TJsEnvironmentSyncParams } from "@formbricks/types/js"; import { Config } from "./config"; import { err } from "./errors"; import { Logger } from "./logger"; @@ -19,9 +20,9 @@ let environmentStateSyncIntervalId: number | null = null; */ export const fetchEnvironmentState = async ( { apiHost, environmentId }: TJsEnvironmentSyncParams, - noCache: boolean = false + noCache = false ): Promise => { - let fetchOptions: RequestInit = {}; + const fetchOptions: RequestInit = {}; if (noCache || getIsDebug()) { fetchOptions.cache = "no-cache"; @@ -33,9 +34,9 @@ export const fetchEnvironmentState = async ( const response = await fetch(url, fetchOptions); if (!response.ok) { - const jsonRes = await response.json(); + const jsonRes = (await response.json()) as { message: string }; - const error = err({ + const error = err({ code: "network_error", status: response.status, message: "Error syncing with backend", @@ -43,14 +44,15 @@ export const fetchEnvironmentState = async ( responseMessage: jsonRes.message, }); - throw error; + // eslint-disable-next-line @typescript-eslint/only-throw-error -- error.error + throw error.error; } - const data = await response.json(); + const data = (await response.json()) as { data: TJsEnvironmentState["data"] }; const { data: state } = data; return { - data: { ...(state as TJsEnvironmentState["data"]) }, + data: { ...state }, expiresAt: new Date(new Date().getTime() + 1000 * 60 * 30), // 30 minutes }; }; @@ -59,14 +61,14 @@ export const fetchEnvironmentState = async ( * Add a listener to check if the environment state has expired with a certain interval */ export const addEnvironmentStateExpiryCheckListener = (): void => { - let updateInterval = 1000 * 60; // every minute + const updateInterval = 1000 * 60; // every minute if (typeof window !== "undefined" && environmentStateSyncIntervalId === null) { - environmentStateSyncIntervalId = window.setInterval(async () => { + const intervalHandler = async (): Promise => { const expiresAt = config.get().environmentState.expiresAt; try { // check if the environmentState has not expired yet - if (expiresAt && new Date(expiresAt) >= new Date()) { + if (new Date(expiresAt) >= new Date()) { return; } @@ -88,13 +90,15 @@ export const addEnvironmentStateExpiryCheckListener = (): void => { environmentState, filteredSurveys, }); - } catch (e) { - console.error(`Error during expiry check: ${e}`); + } catch (e: unknown) { + logger.error(`Error during expiry check: ${e as string}`); logger.debug("Extending config and try again later."); const existingConfig = config.get(); config.update(existingConfig); } - }, updateInterval); + }; + + environmentStateSyncIntervalId = window.setInterval(() => void intervalHandler(), updateInterval); } }; diff --git a/packages/js-core/src/lib/errors.ts b/packages/js-core/src/lib/errors.ts index b81ca2783e..0320dccb8e 100644 --- a/packages/js-core/src/lib/errors.ts +++ b/packages/js-core/src/lib/errors.ts @@ -1,10 +1,17 @@ +/* eslint-disable no-console -- Required for logging a warning here */ import { Logger } from "./logger"; export type { ZErrorHandler } from "@formbricks/types/errors"; -export type ResultError = { ok: false; error: T }; +export interface ResultError { + ok: false; + error: T; +} -export type ResultOk = { ok: true; value: T }; +export interface ResultOk { + ok: true; + value: T; +} export type Result = ResultOk | ResultError; @@ -20,14 +27,14 @@ export const err = (error: E): ResultError => ({ export const wrap = (fn: (value: T) => R) => (result: Result): Result => - result.ok === true ? { ok: true, value: fn(result.value) } : result; + result.ok ? { ok: true, value: fn(result.value) } : result; export function match( result: Result, onSuccess: (value: TSuccess) => TReturn, onError: (error: TError) => TReturn -) { - if (result.ok === true) { +): TReturn { + if (result.ok) { return onSuccess(result.value); } @@ -48,7 +55,7 @@ if (result.ok === true) { } */ export const wrapThrows = - (fn: (...args: A) => T) => + (fn: (...args: A) => T) => (...args: A): Result => { try { return { @@ -63,58 +70,60 @@ export const wrapThrows = } }; -export type NetworkError = { +export interface NetworkError { code: "network_error"; status: number; message: string; url: string; responseMessage: string; -}; +} -export type MissingFieldError = { +export interface MissingFieldError { code: "missing_field"; field: string; -}; +} -export type InvalidMatchTypeError = { +export interface InvalidMatchTypeError { code: "invalid_match_type"; message: string; -}; +} -export type MissingPersonError = { +export interface MissingPersonError { code: "missing_person"; message: string; -}; +} -export type NotInitializedError = { +export interface NotInitializedError { code: "not_initialized"; message: string; -}; +} -export type AttributeAlreadyExistsError = { +export interface AttributeAlreadyExistsError { code: "attribute_already_exists"; message: string; -}; +} -export type InvalidCodeError = { +export interface InvalidCodeError { code: "invalid_code"; message: string; -}; +} const logger = Logger.getInstance(); export class ErrorHandler { private static instance: ErrorHandler | null; - private handleError: (error: any) => void; - public customized: boolean = false; + private handleError: (error: unknown) => void; + public customized = false; public static initialized = false; - private constructor(errorHandler?: (error: any) => void) { + private constructor(errorHandler?: (error: unknown) => void) { if (errorHandler) { this.handleError = errorHandler; this.customized = true; } else { - this.handleError = (err) => Logger.getInstance().error(JSON.stringify(err)); + this.handleError = (error) => { + Logger.getInstance().error(JSON.stringify(error)); + }; } } @@ -126,7 +135,7 @@ export class ErrorHandler { return ErrorHandler.instance; } - static init(errorHandler?: (error: any) => void): void { + static init(errorHandler?: (error: unknown) => void): void { this.initialized = true; ErrorHandler.instance = new ErrorHandler(errorHandler); @@ -136,7 +145,7 @@ export class ErrorHandler { logger.debug(`Custom error handler: ${this.customized ? "yes" : "no"}`); } - public handle(error: any): void { + public handle(error: unknown): void { console.warn("🧱 Formbricks - Global error: ", error); this.handleError(error); } diff --git a/packages/js-core/src/lib/eventListeners.ts b/packages/js-core/src/lib/event-listeners.ts similarity index 94% rename from packages/js-core/src/lib/eventListeners.ts rename to packages/js-core/src/lib/event-listeners.ts index 4ee436a2f9..18166bbad9 100644 --- a/packages/js-core/src/lib/eventListeners.ts +++ b/packages/js-core/src/lib/event-listeners.ts @@ -1,7 +1,7 @@ import { addEnvironmentStateExpiryCheckListener, clearEnvironmentStateExpiryCheckListener, -} from "./environmentState"; +} from "./environment-state"; import { addClickEventListener, addExitIntentListener, @@ -11,8 +11,8 @@ import { removeExitIntentListener, removePageUrlEventListeners, removeScrollDepthListener, -} from "./noCodeActions"; -import { addPersonStateExpiryCheckListener, clearPersonStateExpiryCheckListener } from "./personState"; +} from "./no-code-actions"; +import { addPersonStateExpiryCheckListener, clearPersonStateExpiryCheckListener } from "./person-state"; let areRemoveEventListenersAdded = false; diff --git a/packages/js-core/src/lib/initialize.ts b/packages/js-core/src/lib/initialize.ts index 8231083e36..ec2ed22500 100644 --- a/packages/js-core/src/lib/initialize.ts +++ b/packages/js-core/src/lib/initialize.ts @@ -1,5 +1,6 @@ -import { TAttributes } from "@formbricks/types/attributes"; -import { type ForbiddenError } from "@formbricks/types/errors"; +/* eslint-disable no-console -- required for logging */ +import { type TAttributes } from "@formbricks/types/attributes"; +import { type ApiErrorResponse } from "@formbricks/types/errors"; import { type TJsConfig, type TJsConfigInput } from "@formbricks/types/js"; import { trackNoCodeAction } from "./actions"; import { updateAttributes } from "./attributes"; @@ -9,22 +10,20 @@ import { LEGACY_JS_APP_LOCAL_STORAGE_KEY, LEGACY_JS_WEBSITE_LOCAL_STORAGE_KEY, } from "./constants"; -import { fetchEnvironmentState } from "./environmentState"; +import { fetchEnvironmentState } from "./environment-state"; import { ErrorHandler, - MissingFieldError, - MissingPersonError, - NetworkError, - NotInitializedError, - Result, + type MissingFieldError, + type NotInitializedError, + type Result, err, okVoid, wrapThrows, } from "./errors"; -import { addCleanupEventListeners, addEventListeners, removeAllEventListeners } from "./eventListeners"; +import { addCleanupEventListeners, addEventListeners, removeAllEventListeners } from "./event-listeners"; import { Logger } from "./logger"; -import { checkPageUrl } from "./noCodeActions"; -import { DEFAULT_PERSON_STATE_NO_USER_ID, fetchPersonState } from "./personState"; +import { checkPageUrl } from "./no-code-actions"; +import { DEFAULT_PERSON_STATE_NO_USER_ID, fetchPersonState } from "./person-state"; import { filterSurveys, getIsDebug } from "./utils"; import { addWidgetContainer, removeWidgetContainer, setIsSurveyRunning } from "./widget"; @@ -32,7 +31,7 @@ const logger = Logger.getInstance(); let isInitialized = false; -export const setIsInitialized = (value: boolean) => { +export const setIsInitialized = (value: boolean): void => { isInitialized = value; }; @@ -42,7 +41,13 @@ const migrateLocalStorage = (): { changed: boolean; newState?: TJsConfig } => { if (oldWebsiteConfig) { localStorage.removeItem(LEGACY_JS_WEBSITE_LOCAL_STORAGE_KEY); - const parsedOldConfig = JSON.parse(oldWebsiteConfig) as TJsConfig; + const parsedOldConfig = JSON.parse(oldWebsiteConfig) as Partial<{ + environmentId: string; + apiHost: string; + environmentState: TJsConfig["environmentState"]; + personState: TJsConfig["personState"]; + filteredSurveys: TJsConfig["filteredSurveys"]; + }>; if ( parsedOldConfig.environmentId && @@ -55,14 +60,20 @@ const migrateLocalStorage = (): { changed: boolean; newState?: TJsConfig } => { return { changed: true, - newState: newLocalStorageConfig, + newState: newLocalStorageConfig as TJsConfig, }; } } if (oldAppConfig) { localStorage.removeItem(LEGACY_JS_APP_LOCAL_STORAGE_KEY); - const parsedOldConfig = JSON.parse(oldAppConfig) as TJsConfig; + const parsedOldConfig = JSON.parse(oldAppConfig) as Partial<{ + environmentId: string; + apiHost: string; + environmentState: TJsConfig["environmentState"]; + personState: TJsConfig["personState"]; + filteredSurveys: TJsConfig["filteredSurveys"]; + }>; if ( parsedOldConfig.environmentId && @@ -86,20 +97,24 @@ const migrateProductToProject = (): { changed: boolean; newState?: TJsConfig } = const existingConfig = localStorage.getItem(JS_LOCAL_STORAGE_KEY); if (existingConfig) { - const parsedConfig = JSON.parse(existingConfig); + const parsedConfig = JSON.parse(existingConfig) as TJsConfig; - if (parsedConfig.environmentState.data.product) { - const { environmentState, filteredSurveys, ...restConfig } = parsedConfig; + // @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; - // @ts-expect-error 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 = { @@ -108,6 +123,7 @@ const migrateProductToProject = (): { changed: boolean; newState?: TJsConfig } = ...parsedConfig.environmentState, data: { ...rest, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- product is not in the type project: product, }, }, @@ -126,11 +142,12 @@ const migrateProductToProject = (): { changed: boolean; newState?: TJsConfig } = export const initialize = async ( configInput: TJsConfigInput -): Promise> => { +): Promise> => { const isDebug = getIsDebug(); if (isDebug) { logger.configure({ logLevel: "debug" }); } + let config = Config.getInstance(); const { changed, newState } = migrateLocalStorage(); @@ -166,12 +183,12 @@ export const initialize = async ( try { existingConfig = config.get(); logger.debug("Found existing configuration."); - } catch (e) { + } catch { logger.debug("No existing configuration found."); } // formbricks is in error state, skip initialization - if (existingConfig?.status?.value === "error") { + if (existingConfig?.status.value === "error") { if (isDebug) { logger.debug( "Formbricks is in error state, but debug mode is active. Resetting config and continuing." @@ -180,16 +197,15 @@ export const initialize = async ( return okVoid(); } - logger.debug("Formbricks was set to an error state."); + console.error("🧱 Formbricks - Formbricks was set to an error state."); - const expiresAt = existingConfig?.status?.expiresAt; + const expiresAt = existingConfig.status.expiresAt; if (expiresAt && new Date(expiresAt) > new Date()) { - logger.debug("Error state is not expired, skipping initialization"); + console.error("🧱 Formbricks - Error state is not expired, skipping initialization"); return okVoid(); - } else { - logger.debug("Error state is expired. Continue with initialization."); } + console.error("🧱 Formbricks - Error state is expired. Continuing with initialization."); } ErrorHandler.getInstance().printStatus(); @@ -217,8 +233,7 @@ export const initialize = async ( addWidgetContainer(); if ( - existingConfig && - existingConfig.environmentState && + existingConfig?.environmentState && existingConfig.environmentId === configInput.environmentId && existingConfig.apiHost === configInput.apiHost ) { @@ -233,6 +248,7 @@ export const initialize = async ( if ( configInput.userId && + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- personState could be null (existingConfig.personState === null || (existingConfig.personState.expiresAt && new Date(existingConfig.personState.expiresAt) < new Date())) ) { @@ -278,8 +294,8 @@ export const initialize = async ( }); const surveyNames = filteredSurveys.map((s) => s.name); - logger.debug("Fetched " + surveyNames.length + " surveys during sync: " + surveyNames.join(", ")); - } catch (e) { + logger.debug(`Fetched ${surveyNames.length.toString()} surveys during sync: ${surveyNames.join(", ")}`); + } catch { putFormbricksInErrorState(config); } } else { @@ -319,9 +335,9 @@ export const initialize = async ( configInput.attributes ); - if (res.ok !== true) { + if (!res.ok) { if (res.error.code === "forbidden") { - logger.error(`Authorization error: ${res.error.responseMessage}`); + logger.error(`Authorization error: ${res.error.responseMessage ?? ""}`); } return err(res.error); } @@ -341,7 +357,7 @@ export const initialize = async ( attributes: updatedAttributes ?? {}, }); } catch (e) { - handleErrorOnFirstInit(e as Error); + handleErrorOnFirstInit(e); } // and track the new session event @@ -357,13 +373,14 @@ export const initialize = async ( // check page url if initialized after page load - checkPageUrl(); + void checkPageUrl(); return okVoid(); }; -export const handleErrorOnFirstInit = (e: any) => { - if (e.error.code === "forbidden") { - logger.error(`Authorization error: ${e.error.responseMessage}`); +export const handleErrorOnFirstInit = (e: unknown): void => { + const error = e as ApiErrorResponse; + if (error.code === "forbidden") { + logger.error(`Authorization error: ${error.responseMessage ?? ""}`); } if (getIsDebug()) { @@ -380,7 +397,9 @@ export const handleErrorOnFirstInit = (e: any) => { }; // can't use config.update here because the config is not yet initialized - wrapThrows(() => localStorage.setItem(JS_LOCAL_STORAGE_KEY, JSON.stringify(initialErrorConfig)))(); + wrapThrows(() => { + localStorage.setItem(JS_LOCAL_STORAGE_KEY, JSON.stringify(initialErrorConfig)); + })(); throw new Error("Could not initialize formbricks"); }; @@ -404,7 +423,7 @@ export const deinitalize = (): void => { setIsInitialized(false); }; -export const putFormbricksInErrorState = (config: Config): void => { +export const putFormbricksInErrorState = (formbricksConfig: Config): void => { if (getIsDebug()) { logger.debug("Not putting formbricks in error state because debug mode is active (no error state)"); return; @@ -412,8 +431,8 @@ export const putFormbricksInErrorState = (config: Config): void => { logger.debug("Putting formbricks in error state"); // change formbricks status to error - config.update({ - ...config.get(), + formbricksConfig.update({ + ...formbricksConfig.get(), status: { value: "error", expiresAt: new Date(new Date().getTime() + 10 * 60000), // 10 minutes in the future diff --git a/packages/js-core/src/lib/logger.ts b/packages/js-core/src/lib/logger.ts index 9fa7fa92de..23e693011a 100644 --- a/packages/js-core/src/lib/logger.ts +++ b/packages/js-core/src/lib/logger.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console -- Required for logging */ type LogLevel = "debug" | "error"; interface LoggerConfig { @@ -8,8 +9,6 @@ export class Logger { private static instance: Logger | undefined; private logLevel: LogLevel = "error"; - private constructor() {} - static getInstance(): Logger { if (!Logger.instance) { Logger.instance = new Logger(); @@ -18,7 +17,7 @@ export class Logger { } configure(config: LoggerConfig): void { - if (config && config.logLevel !== undefined) { + if (config.logLevel !== undefined) { this.logLevel = config.logLevel; } } diff --git a/packages/js-core/src/lib/noCodeActions.ts b/packages/js-core/src/lib/no-code-actions.ts similarity index 72% rename from packages/js-core/src/lib/noCodeActions.ts rename to packages/js-core/src/lib/no-code-actions.ts index 7905cfb335..8e0080000a 100644 --- a/packages/js-core/src/lib/noCodeActions.ts +++ b/packages/js-core/src/lib/no-code-actions.ts @@ -1,7 +1,7 @@ -import { TJsEnvironmentStateActionClass } from "@formbricks/types/js"; +import { type TJsEnvironmentStateActionClass } from "@formbricks/types/js"; import { trackNoCodeAction } from "./actions"; import { Config } from "./config"; -import { ErrorHandler, NetworkError, Result, err, match, okVoid } from "./errors"; +import { ErrorHandler, type NetworkError, type Result, type ResultError, err, match, okVoid } from "./errors"; import { Logger } from "./logger"; import { TimeoutStack } from "./timeout-stack"; import { evaluateNoCodeConfigClick, handleUrlFilters } from "./utils"; @@ -18,6 +18,9 @@ const events = ["hashchange", "popstate", "pushstate", "replacestate", "load"]; // Page URL Event Handlers let arePageUrlEventListenersAdded = false; let isHistoryPatched = false; +export const setIsHistoryPatched = (value: boolean): void => { + isHistoryPatched = value; +}; export const checkPageUrl = async (): Promise> => { logger.debug(`Checking page url: ${window.location.href}`); @@ -34,7 +37,7 @@ export const checkPageUrl = async (): Promise> => { if (isValidUrl) { const trackResult = await trackNoCodeAction(event.name); - if (trackResult.ok !== true) { + if (!trackResult.ok) { return err(trackResult.error); } } else { @@ -52,43 +55,45 @@ export const checkPageUrl = async (): Promise> => { return okVoid(); }; -const checkPageUrlWrapper = () => { - checkPageUrl(); -}; +const checkPageUrlWrapper = (): ReturnType => checkPageUrl(); export const addPageUrlEventListeners = (): void => { if (typeof window === "undefined" || arePageUrlEventListenersAdded) return; // Monkey patch history methods if not already done if (!isHistoryPatched) { + // eslint-disable-next-line @typescript-eslint/unbound-method -- We need to access the original method const originalPushState = history.pushState; + // eslint-disable-next-line func-names -- We need an anonymous function here history.pushState = function (...args) { - const returnValue = originalPushState.apply(this, args); + originalPushState.apply(this, args); const event = new Event("pushstate"); window.dispatchEvent(event); - return returnValue; }; isHistoryPatched = true; } - events.forEach((event) => window.addEventListener(event, checkPageUrlWrapper)); + events.forEach((event) => { + window.addEventListener(event, checkPageUrlWrapper as EventListener); + }); arePageUrlEventListenersAdded = true; }; export const removePageUrlEventListeners = (): void => { if (typeof window === "undefined" || !arePageUrlEventListenersAdded) return; - events.forEach((event) => window.removeEventListener(event, checkPageUrlWrapper)); + events.forEach((event) => { + window.removeEventListener(event, checkPageUrlWrapper as EventListener); + }); arePageUrlEventListenersAdded = false; }; // Click Event Handlers let isClickEventListenerAdded = false; -const checkClickMatch = (event: MouseEvent) => { +const checkClickMatch = (event: MouseEvent): void => { const { environmentState } = appConfig.get(); - if (!environmentState) return; const { actionClasses = [] } = environmentState.data; @@ -100,18 +105,26 @@ const checkClickMatch = (event: MouseEvent) => { noCodeClickActionClasses.forEach((action: TJsEnvironmentStateActionClass) => { if (evaluateNoCodeConfigClick(targetElement, action)) { - trackNoCodeAction(action.name).then((res) => { - match( - res, - (_value: unknown) => {}, - (err: any) => errorHandler.handle(err) - ); - }); + trackNoCodeAction(action.name) + .then((res) => { + match( + res, + (_value: unknown) => undefined, + (actionError: unknown) => { + errorHandler.handle(actionError); + } + ); + }) + .catch((error: unknown) => { + errorHandler.handle(error); + }); } }); }; -const checkClickMatchWrapper = (e: MouseEvent) => checkClickMatch(e); +const checkClickMatchWrapper = (e: MouseEvent): void => { + checkClickMatch(e); +}; export const addClickEventListener = (): void => { if (typeof window === "undefined" || isClickEventListenerAdded) return; @@ -128,9 +141,9 @@ export const removeClickEventListener = (): void => { // Exit Intent Handlers let isExitIntentListenerAdded = false; -const checkExitIntent = async (e: MouseEvent) => { +const checkExitIntent = async (e: MouseEvent): Promise | undefined> => { const { environmentState } = appConfig.get(); - const { actionClasses = [] } = environmentState.data ?? {}; + const { actionClasses = [] } = environmentState.data; const noCodeExitIntentActionClasses = actionClasses.filter( (action) => action.type === "noCode" && action.noCodeConfig?.type === "exitIntent" @@ -144,23 +157,25 @@ const checkExitIntent = async (e: MouseEvent) => { if (!isValidUrl) continue; const trackResult = await trackNoCodeAction(event.name); - if (trackResult.ok !== true) return err(trackResult.error); + if (!trackResult.ok) return err(trackResult.error); } } }; -const checkExitIntentWrapper = (e: MouseEvent) => checkExitIntent(e); +const checkExitIntentWrapper = (e: MouseEvent): ReturnType => checkExitIntent(e); export const addExitIntentListener = (): void => { if (typeof document !== "undefined" && !isExitIntentListenerAdded) { - document.querySelector("body")!.addEventListener("mouseleave", checkExitIntentWrapper); + document + .querySelector("body") + ?.addEventListener("mouseleave", checkExitIntentWrapper as unknown as EventListener); isExitIntentListenerAdded = true; } }; export const removeExitIntentListener = (): void => { if (isExitIntentListenerAdded) { - document.removeEventListener("mouseleave", checkExitIntentWrapper); + document.removeEventListener("mouseleave", checkExitIntentWrapper as unknown as EventListener); isExitIntentListenerAdded = false; } }; @@ -169,7 +184,7 @@ export const removeExitIntentListener = (): void => { let scrollDepthListenerAdded = false; let scrollDepthTriggered = false; -const checkScrollDepth = async () => { +const checkScrollDepth = async (): Promise> => { const scrollPosition = window.scrollY; const windowSize = window.innerHeight; const bodyHeight = document.documentElement.scrollHeight; @@ -182,7 +197,7 @@ const checkScrollDepth = async () => { scrollDepthTriggered = true; const { environmentState } = appConfig.get(); - const { actionClasses = [] } = environmentState.data ?? {}; + const { actionClasses = [] } = environmentState.data; const noCodefiftyPercentScrollActionClasses = actionClasses.filter( (action) => action.type === "noCode" && action.noCodeConfig?.type === "fiftyPercentScroll" @@ -195,22 +210,22 @@ const checkScrollDepth = async () => { if (!isValidUrl) continue; const trackResult = await trackNoCodeAction(event.name); - if (trackResult.ok !== true) return err(trackResult.error); + if (!trackResult.ok) return err(trackResult.error); } } return okVoid(); }; -const checkScrollDepthWrapper = () => checkScrollDepth(); +const checkScrollDepthWrapper = (): ReturnType => checkScrollDepth(); export const addScrollDepthListener = (): void => { if (typeof window !== "undefined" && !scrollDepthListenerAdded) { if (document.readyState === "complete") { - window.addEventListener("scroll", checkScrollDepthWrapper); + window.addEventListener("scroll", checkScrollDepthWrapper as EventListener); } else { window.addEventListener("load", () => { - window.addEventListener("scroll", checkScrollDepthWrapper); + window.addEventListener("scroll", checkScrollDepthWrapper as EventListener); }); } scrollDepthListenerAdded = true; @@ -219,7 +234,7 @@ export const addScrollDepthListener = (): void => { export const removeScrollDepthListener = (): void => { if (scrollDepthListenerAdded) { - window.removeEventListener("scroll", checkScrollDepthWrapper); + window.removeEventListener("scroll", checkScrollDepthWrapper as EventListener); scrollDepthListenerAdded = false; } }; diff --git a/packages/js-core/src/lib/personState.ts b/packages/js-core/src/lib/person-state.ts similarity index 84% rename from packages/js-core/src/lib/personState.ts rename to packages/js-core/src/lib/person-state.ts index 3248667152..c0556ccb5e 100644 --- a/packages/js-core/src/lib/personState.ts +++ b/packages/js-core/src/lib/person-state.ts @@ -1,4 +1,4 @@ -import { TJsPersonState, TJsPersonSyncParams } from "@formbricks/types/js"; +import { type TJsPersonState, type TJsPersonSyncParams } from "@formbricks/types/js"; import { Config } from "./config"; import { err } from "./errors"; import { Logger } from "./logger"; @@ -30,9 +30,9 @@ export const DEFAULT_PERSON_STATE_NO_USER_ID: TJsPersonState = { */ export const fetchPersonState = async ( { apiHost, environmentId, userId }: TJsPersonSyncParams, - noCache: boolean = false + noCache = false ): Promise => { - let fetchOptions: RequestInit = {}; + const fetchOptions: RequestInit = {}; if (noCache || getIsDebug()) { fetchOptions.cache = "no-cache"; @@ -44,7 +44,7 @@ export const fetchPersonState = async ( const response = await fetch(url, fetchOptions); if (!response.ok) { - const jsonRes = await response.json(); + const jsonRes = (await response.json()) as { code: string; message: string }; const error = err({ code: jsonRes.code === "forbidden" ? "forbidden" : "network_error", @@ -54,10 +54,10 @@ export const fetchPersonState = async ( responseMessage: jsonRes.message, }); - throw error; + throw new Error(error.error.message); } - const data = await response.json(); + const data = (await response.json()) as { data: TJsPersonState["data"] }; const { data: state } = data; const defaultPersonState: TJsPersonState = { @@ -76,7 +76,7 @@ export const fetchPersonState = async ( } return { - data: { ...(state as TJsPersonState["data"]) }, + data: { ...state }, expiresAt: new Date(new Date().getTime() + 1000 * 60 * 30), // 30 minutes }; }; @@ -88,7 +88,7 @@ export const addPersonStateExpiryCheckListener = (): void => { const updateInterval = 1000 * 60; // every 60 seconds if (typeof window !== "undefined" && personStateSyncIntervalId === null) { - personStateSyncIntervalId = window.setInterval(async () => { + const intervalHandler = (): void => { const userId = config.get().personState.data.userId; if (!userId) { @@ -103,7 +103,9 @@ export const addPersonStateExpiryCheckListener = (): void => { expiresAt: new Date(new Date().getTime() + 1000 * 60 * 30), // 30 minutes }, }); - }, updateInterval); + }; + + personStateSyncIntervalId = window.setInterval(intervalHandler, updateInterval); } }; diff --git a/packages/js-core/src/lib/person.ts b/packages/js-core/src/lib/person.ts index 3be233ae60..6e2af4a7fa 100644 --- a/packages/js-core/src/lib/person.ts +++ b/packages/js-core/src/lib/person.ts @@ -1,12 +1,14 @@ import { Config } from "./config"; -import { NetworkError, Result, err, okVoid } from "./errors"; +import { type NetworkError, type Result, err, okVoid } from "./errors"; import { deinitalize, initialize } from "./initialize"; import { Logger } from "./logger"; +import { setIsHistoryPatched } from "./no-code-actions"; import { closeSurvey } from "./widget"; const config = Config.getInstance(); const logger = Logger.getInstance(); +// eslint-disable-next-line @typescript-eslint/require-await -- There are no promises but our proxy makes the functions async export const logoutPerson = async (): Promise => { deinitalize(); config.resetConfig(); @@ -24,7 +26,10 @@ export const resetPerson = async (): Promise> => { ...(userId && { userId }), attributes: config.get().attributes, }; + await logoutPerson(); + setIsHistoryPatched(false); + try { await initialize(syncParams); return okVoid(); diff --git a/packages/js-core/src/lib/timeout-stack.ts b/packages/js-core/src/lib/timeout-stack.ts index f08b338162..85d9ad47f0 100644 --- a/packages/js-core/src/lib/timeout-stack.ts +++ b/packages/js-core/src/lib/timeout-stack.ts @@ -1,9 +1,8 @@ export class TimeoutStack { - private static instance: TimeoutStack; - // private timeouts: number[] = []; + private static instance: TimeoutStack | null = null; private timeouts: { event: string; timeoutId: number }[] = []; - // Private constructor to prevent direct instantiation + // eslint-disable-next-line @typescript-eslint/no-empty-function -- Empty constructor is intentional private constructor() {} // Retrieve the singleton instance of TimeoutStack diff --git a/packages/js-core/src/lib/utils.ts b/packages/js-core/src/lib/utils.ts index 81f709b7eb..4a72de610f 100644 --- a/packages/js-core/src/lib/utils.ts +++ b/packages/js-core/src/lib/utils.ts @@ -1,14 +1,17 @@ import { diffInDays } from "@formbricks/lib/utils/datetime"; -import { TActionClassNoCodeConfig, TActionClassPageUrlRule } from "@formbricks/types/action-classes"; -import { TAttributes } from "@formbricks/types/attributes"; import { - TJsEnvironmentState, - TJsEnvironmentStateActionClass, - TJsEnvironmentStateSurvey, - TJsPersonState, - TJsTrackProperties, + type TActionClassNoCodeConfig, + type TActionClassPageUrlRule, +} from "@formbricks/types/action-classes"; +import { type TAttributes } from "@formbricks/types/attributes"; +import { + type TJsEnvironmentState, + type TJsEnvironmentStateActionClass, + type TJsEnvironmentStateSurvey, + type TJsPersonState, + type TJsTrackProperties, } from "@formbricks/types/js"; -import { TResponseHiddenFieldValue } from "@formbricks/types/responses"; +import { type TResponseHiddenFieldValue } from "@formbricks/types/responses"; import { Logger } from "./logger"; const logger = Logger.getInstance(); @@ -37,7 +40,7 @@ export const checkUrlMatch = ( }; export const handleUrlFilters = (urlFilters: TActionClassNoCodeConfig["urlFilters"]): boolean => { - if (!urlFilters || urlFilters.length === 0) { + if (urlFilters.length === 0) { return true; } @@ -68,7 +71,7 @@ export const evaluateNoCodeConfigClick = ( if (cssSelector) { // Split selectors that start with a . or # including the . or # const individualSelectors = cssSelector.split(/\s*(?=[.#])/); - for (let selector of individualSelectors) { + for (const selector of individualSelectors) { if (!targetElement.matches(selector)) { return false; } @@ -86,7 +89,7 @@ export const handleHiddenFields = ( hiddenFieldsConfig: TJsEnvironmentStateSurvey["hiddenFields"], hiddenFields: TJsTrackProperties["hiddenFields"] ): TResponseHiddenFieldValue => { - const { enabled: enabledHiddenFields, fieldIds: hiddenFieldIds } = hiddenFieldsConfig || {}; + const { enabled: enabledHiddenFields, fieldIds: hiddenFieldIds } = hiddenFieldsConfig; let hiddenFieldsObject: TResponseHiddenFieldValue = {}; @@ -94,14 +97,14 @@ export const handleHiddenFields = ( logger.error("Hidden fields are not enabled for this survey"); } else if (hiddenFieldIds && hiddenFields) { const unknownHiddenFields: string[] = []; - hiddenFieldsObject = Object.keys(hiddenFields).reduce((acc, key) => { - if (hiddenFieldIds?.includes(key)) { - acc[key] = hiddenFields?.[key]; + hiddenFieldsObject = Object.keys(hiddenFields).reduce((acc, key) => { + if (hiddenFieldIds.includes(key)) { + acc[key] = hiddenFields[key]; } else { unknownHiddenFields.push(key); } return acc; - }, {} as TResponseHiddenFieldValue); + }, {}); if (unknownHiddenFields.length > 0) { logger.error( @@ -113,7 +116,7 @@ export const handleHiddenFields = ( return hiddenFieldsObject; }; -export const shouldDisplayBasedOnPercentage = (displayPercentage: number) => { +export const shouldDisplayBasedOnPercentage = (displayPercentage: number): boolean => { const randomNum = Math.floor(Math.random() * 10000) / 100; return randomNum <= displayPercentage; }; @@ -125,40 +128,39 @@ export const getLanguageCode = ( const language = attributes.language; const availableLanguageCodes = survey.languages.map((surveyLanguage) => surveyLanguage.language.code); if (!language) return "default"; - else { - const selectedLanguage = survey.languages.find((surveyLanguage) => { - return ( - surveyLanguage.language.code === language.toLowerCase() || - surveyLanguage.language.alias?.toLowerCase() === language.toLowerCase() - ); - }); - if (selectedLanguage?.default) { - return "default"; - } - if ( - !selectedLanguage || - !selectedLanguage?.enabled || - !availableLanguageCodes.includes(selectedLanguage.language.code) - ) { - return undefined; - } - return selectedLanguage.language.code; + + const selectedLanguage = survey.languages.find((surveyLanguage) => { + return ( + surveyLanguage.language.code === language.toLowerCase() || + surveyLanguage.language.alias?.toLowerCase() === language.toLowerCase() + ); + }); + if (selectedLanguage?.default) { + return "default"; } + if ( + !selectedLanguage || + !selectedLanguage.enabled || + !availableLanguageCodes.includes(selectedLanguage.language.code) + ) { + return undefined; + } + return selectedLanguage.language.code; }; -export const getDefaultLanguageCode = (survey: TJsEnvironmentStateSurvey) => { - const defaultSurveyLanguage = survey.languages?.find((surveyLanguage) => { - return surveyLanguage.default === true; +export const getDefaultLanguageCode = (survey: TJsEnvironmentStateSurvey): string | undefined => { + const defaultSurveyLanguage = survey.languages.find((surveyLanguage) => { + return surveyLanguage.default; }); if (defaultSurveyLanguage) return defaultSurveyLanguage.language.code; }; -export const getIsDebug = () => window.location.search.includes("formbricksDebug=true"); +export const getIsDebug = (): boolean => window.location.search.includes("formbricksDebug=true"); /** * Filters surveys based on the displayOption, recontactDays, and segments - * @param environmentSate The environment state - * @param personState The person state + * @param environmentSate - The environment state + * @param personState - The person state * @returns The filtered surveys */ @@ -170,10 +172,6 @@ export const filterSurveys = ( const { project, surveys } = environmentState.data; const { displays, responses, lastDisplayAt, segments, userId } = personState.data; - if (!displays) { - return []; - } - // Function to filter surveys based on displayOption criteria let filteredSurveys = surveys.filter((survey: TJsEnvironmentStateSurvey) => { switch (survey.displayOption) { @@ -211,19 +209,19 @@ export const filterSurveys = ( // if survey has recontactDays, check if the last display was more than recontactDays ago else if (survey.recontactDays !== null) { const lastDisplaySurvey = displays.filter((display) => display.surveyId === survey.id)[0]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- lastDisplaySurvey could be falsy if (!lastDisplaySurvey) { return true; } return diffInDays(new Date(), new Date(lastDisplaySurvey.createdAt)) >= survey.recontactDays; } // use recontactDays of the project if survey does not have recontactDays - else if (project.recontactDays !== null) { + else if (project.recontactDays) { return diffInDays(new Date(), new Date(lastDisplayAt)) >= project.recontactDays; } // if no recontactDays is set, show the survey - else { - return true; - } + + return true; }); if (!userId) { diff --git a/packages/js-core/src/lib/widget.ts b/packages/js-core/src/lib/widget.ts index db027c4b2b..18e1bba2ba 100644 --- a/packages/js-core/src/lib/widget.ts +++ b/packages/js-core/src/lib/widget.ts @@ -1,15 +1,17 @@ +/* eslint-disable no-console -- Required for error logging */ +/* eslint-disable @typescript-eslint/no-empty-function -- There are some empty functions here that we need */ import { FormbricksAPI } from "@formbricks/api"; import { ResponseQueue } from "@formbricks/lib/responseQueue"; import { SurveyState } from "@formbricks/lib/surveyState"; import { getStyling } from "@formbricks/lib/utils/styling"; import { - TJsEnvironmentStateSurvey, - TJsFileUploadParams, - TJsPersonState, - TJsTrackProperties, + type TJsEnvironmentStateSurvey, + type TJsFileUploadParams, + type TJsPersonState, + type TJsTrackProperties, } from "@formbricks/types/js"; -import { TResponseHiddenFieldValue, TResponseUpdate } from "@formbricks/types/responses"; -import { TUploadFileConfig } from "@formbricks/types/storage"; +import { type TResponseHiddenFieldValue, type TResponseUpdate } from "@formbricks/types/responses"; +import { type TUploadFileConfig } from "@formbricks/types/storage"; import { Config } from "./config"; import { CONTAINER_ID } from "./constants"; import { Logger } from "./logger"; @@ -27,10 +29,10 @@ const logger = Logger.getInstance(); const timeoutStack = TimeoutStack.getInstance(); let isSurveyRunning = false; -let setIsError = (_: boolean) => {}; -let setIsResponseSendingFinished = (_: boolean) => {}; +let setIsError = (_: boolean): void => {}; +let setIsResponseSendingFinished = (_: boolean): void => {}; -export const setIsSurveyRunning = (value: boolean) => { +export const setIsSurveyRunning = (value: boolean): void => { isSurveyRunning = value; }; @@ -60,7 +62,7 @@ const renderWidget = async ( survey: TJsEnvironmentStateSurvey, action?: string, hiddenFields: TResponseHiddenFieldValue = {} -) => { +): Promise => { if (isSurveyRunning) { logger.debug("A survey is already running. Skipping."); return; @@ -69,11 +71,11 @@ const renderWidget = async ( setIsSurveyRunning(true); if (survey.delay) { - logger.debug(`Delaying survey "${survey.name}" by ${survey.delay} seconds.`); + logger.debug(`Delaying survey "${survey.name}" by ${survey.delay.toString()} seconds.`); } - const { project } = config.get().environmentState.data ?? {}; - const { attributes } = config.get() ?? {}; + const { project } = config.get().environmentState.data; + const { attributes } = config.get(); const isMultiLanguageSurvey = survey.languages.length > 1; let languageCode = "default"; @@ -151,7 +153,7 @@ const renderWidget = async ( const existingDisplays = config.get().personState.data.displays; const newDisplay = { surveyId: survey.id, createdAt: new Date() }; - const displays = existingDisplays ? [...existingDisplays, newDisplay] : [newDisplay]; + const displays = existingDisplays.length ? [...existingDisplays, newDisplay] : [newDisplay]; const previousConfig = config.get(); const updatedPersonState: TJsPersonState = { @@ -203,7 +205,7 @@ const renderWidget = async ( ...config.get().personState, data: { ...config.get().personState.data, - responses: [...responses, surveyState.surveyId], + responses: responses.length ? [...responses, surveyState.surveyId] : [surveyState.surveyId], }, }; @@ -235,7 +237,7 @@ const renderWidget = async ( }, onRetry: () => { setIsError(false); - responseQueue.processQueue(); + void responseQueue.processQueue(); }, hiddenFieldsRecord: hiddenFields, }); @@ -246,7 +248,7 @@ const renderWidget = async ( } }; -export const closeSurvey = async (): Promise => { +export const closeSurvey = (): void => { // remove container element from DOM removeWidgetContainer(); addWidgetContainer(); @@ -276,16 +278,19 @@ export const removeWidgetContainer = (): void => { const loadFormbricksSurveysExternally = (): Promise => { return new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- We need to check if the formbricksSurveys object exists if (window.formbricksSurveys) { resolve(window.formbricksSurveys); } else { const script = document.createElement("script"); script.src = `${config.get().apiHost}/js/surveys.umd.cjs`; script.async = true; - script.onload = () => resolve(window.formbricksSurveys); + script.onload = () => { + resolve(window.formbricksSurveys); + }; script.onerror = (error) => { console.error("Failed to load Formbricks Surveys library:", error); - reject(error); + reject(new Error(`Failed to load Formbricks Surveys library: ${error as string}`)); }; document.head.appendChild(script); } diff --git a/packages/js/package.json b/packages/js/package.json index 73c43d653f..1ac4d07200 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -1,7 +1,7 @@ { "name": "@formbricks/js", "license": "MIT", - "version": "3.0.1", + "version": "3.0.2", "description": "Formbricks-js allows you to connect your index to Formbricks, display surveys and trigger events.", "homepage": "https://formbricks.com", "repository": { diff --git a/packages/js/src/index.ts b/packages/js/src/index.ts index a880bf2200..0d927a07df 100644 --- a/packages/js/src/index.ts +++ b/packages/js/src/index.ts @@ -1,20 +1,20 @@ -import type FormbricksApp from "@formbricks/js-core"; +import type Formbricks from "@formbricks/js-core"; import { loadFormbricksToProxy } from "./lib/load-formbricks"; -type TFormbricksApp = typeof FormbricksApp; +type TFormbricks = typeof Formbricks; declare global { interface Window { - formbricks: TFormbricksApp | undefined; + formbricks: TFormbricks | undefined; } } -const formbricksProxyHandler: ProxyHandler = { +const formbricksProxyHandler: ProxyHandler = { get(_target, prop, _receiver) { return (...args: unknown[]) => loadFormbricksToProxy(prop as string, ...args); }, }; -const formbricksApp: TFormbricksApp = new Proxy({} as TFormbricksApp, formbricksProxyHandler); +const formbricks: TFormbricks = new Proxy({} as TFormbricks, formbricksProxyHandler); // eslint-disable-next-line import/no-default-export -- Required for UMD -export default formbricksApp; +export default formbricks; diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 5ed1e712ea..37b077c647 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -1,6 +1,6 @@ { "name": "@formbricks/react-native", - "version": "1.3.0", + "version": "1.3.1", "license": "MIT", "description": "Formbricks React Native SDK allows you to connect your app to Formbricks, display surveys and trigger events.", "homepage": "https://formbricks.com", diff --git a/packages/react-native/src/lib/attributes.ts b/packages/react-native/src/lib/attributes.ts index d135043b12..2f861a8bf2 100644 --- a/packages/react-native/src/lib/attributes.ts +++ b/packages/react-native/src/lib/attributes.ts @@ -1,7 +1,6 @@ -/* eslint-disable @typescript-eslint/no-unnecessary-condition -- could be undefined */ import { FormbricksAPI } from "@formbricks/api"; import type { TAttributes } from "@formbricks/types/attributes"; -import type { ForbiddenError, NetworkError } from "@formbricks/types/errors"; +import type { ApiErrorResponse } from "@formbricks/types/errors"; import { type Result, err, ok } from "../../../js-core/src/lib/errors"; import { Logger } from "../../../js-core/src/lib/logger"; @@ -12,7 +11,7 @@ export const updateAttributes = async ( environmentId: string, userId: string, attributes: TAttributes -): Promise> => { +): Promise> => { // clean attributes and remove existing attributes if config already exists const updatedAttributes = { ...attributes }; @@ -22,7 +21,7 @@ export const updateAttributes = async ( return ok(updatedAttributes); } - logger.debug("Updating attributes: " + JSON.stringify(updatedAttributes)); + logger.debug(`Updating attributes: ${JSON.stringify(updatedAttributes)}`); const api = new FormbricksAPI({ apiHost, @@ -41,18 +40,18 @@ export const updateAttributes = async ( return ok(updatedAttributes); } - // @ts-expect-error -- details could be defined and present - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- details could be defined and present - if (res.error.details?.ignore) { - logger.error(res.error.message ?? `Error updating person with userId ${userId}`); + const responseError = res.error; + + if (responseError.details?.ignore) { + logger.error(responseError.message); return ok(updatedAttributes); } return err({ - code: (res.error as ForbiddenError).code ?? "network_error", - status: (res.error as NetworkError | ForbiddenError).status ?? 500, + code: responseError.code, + status: responseError.status, message: `Error updating person with userId ${userId}`, url: new URL(`${apiHost}/api/v1/client/${environmentId}/people/${userId}/attributes`), - responseMessage: res.error.message, + responseMessage: responseError.responseMessage, }); }; diff --git a/packages/react-native/src/lib/storage.ts b/packages/react-native/src/lib/storage.ts index a5039dd6c6..ff956c72b7 100644 --- a/packages/react-native/src/lib/storage.ts +++ b/packages/react-native/src/lib/storage.ts @@ -47,18 +47,18 @@ export class StorageAPI { const { signedUrl, fileUrl, signingData, presignedFields, updatedFileName } = data; - let requestHeaders: Record = {}; + let localUploadDetails: Record = {}; if (signingData) { const { signature, timestamp, uuid } = signingData; - requestHeaders = { - "X-File-Type": file.type, - "X-File-Name": encodeURIComponent(updatedFileName), - "X-Survey-ID": surveyId ?? "", - "X-Signature": signature, - "X-Timestamp": String(timestamp), - "X-UUID": uuid, + localUploadDetails = { + fileType: file.type, + fileName: encodeURIComponent(updatedFileName), + surveyId: surveyId ?? "", + signature, + timestamp: String(timestamp), + uuid, }; } @@ -91,14 +91,12 @@ export class StorageAPI { try { uploadResponse = await fetch(signedUrlCopy, { method: "POST", - ...(signingData - ? { - headers: { - ...requestHeaders, - }, - } - : {}), - body: presignedFields ? formDataForS3 : JSON.stringify(formData), + body: presignedFields + ? formDataForS3 + : JSON.stringify({ + ...formData, + ...localUploadDetails, + }), }); } catch (err) { console.error("Error uploading file", err); diff --git a/packages/types/errors.ts b/packages/types/errors.ts index 89e950d9f5..2d4d2575cd 100644 --- a/packages/types/errors.ts +++ b/packages/types/errors.ts @@ -90,14 +90,17 @@ interface NetworkError { message: string; status: number; url: URL; + responseMessage?: string; + details?: Record; } interface ForbiddenError { code: "forbidden"; message: string; - responseMessage?: string; status: number; url: URL; + responseMessage?: string; + details?: Record; } export const ZErrorHandler = z.function().args(z.any()).returns(z.void()); @@ -115,3 +118,21 @@ export { AuthorizationError, }; export type { NetworkError, ForbiddenError }; + +export interface ApiErrorResponse { + code: + | "not_found" + | "gone" + | "bad_request" + | "internal_server_error" + | "unauthorized" + | "method_not_allowed" + | "not_authenticated" + | "forbidden" + | "network_error"; + message: string; + status: number; + url: URL; + details?: Record; + responseMessage?: string; +} diff --git a/turbo.json b/turbo.json index e53a608acd..8329a9bcda 100644 --- a/turbo.json +++ b/turbo.json @@ -31,6 +31,9 @@ "@formbricks/js#lint": { "dependsOn": ["@formbricks/js-core#build"] }, + "@formbricks/js-core#lint": { + "dependsOn": ["@formbricks/api#build"] + }, "@formbricks/react-native#build": { "dependsOn": ["^build"], "outputs": ["dist/**"]