mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-25 15:09:03 -05:00
fix: merges the app and website js sdk into @formbricks/js (#3314)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
+75
@@ -0,0 +1,75 @@
|
||||
/* eslint-disable no-console -- logging is allowed in migration scripts */
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const TRANSACTION_TIMEOUT = 30 * 60 * 1000; // 30 minutes in milliseconds
|
||||
|
||||
async function runMigration(): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
console.log("Starting data migration...");
|
||||
|
||||
await prisma.$transaction(
|
||||
async (transactionPrisma) => {
|
||||
const websiteSurveys = await transactionPrisma.survey.findMany({
|
||||
where: { type: "website" },
|
||||
});
|
||||
|
||||
const updationPromises = [];
|
||||
|
||||
for (const websiteSurvey of websiteSurveys) {
|
||||
updationPromises.push(
|
||||
transactionPrisma.survey.update({
|
||||
where: { id: websiteSurvey.id },
|
||||
data: {
|
||||
type: "app",
|
||||
segment: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
environmentId_title: {
|
||||
environmentId: websiteSurvey.environmentId,
|
||||
title: websiteSurvey.id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
title: websiteSurvey.id,
|
||||
isPrivate: true,
|
||||
environmentId: websiteSurvey.environmentId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(updationPromises);
|
||||
console.log(`Updated ${websiteSurveys.length.toString()} website surveys to app surveys`);
|
||||
},
|
||||
{
|
||||
timeout: TRANSACTION_TIMEOUT,
|
||||
}
|
||||
);
|
||||
|
||||
const endTime = Date.now();
|
||||
console.log(`Data migration completed. Total time: ${((endTime - startTime) / 1000).toFixed(2)}s`);
|
||||
}
|
||||
|
||||
function handleError(error: unknown): void {
|
||||
console.error("An error occurred during migration:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function handleDisconnectError(): void {
|
||||
console.error("Failed to disconnect Prisma client");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
runMigration()
|
||||
.catch(handleError)
|
||||
.finally(() => {
|
||||
prisma.$disconnect().catch(handleDisconnectError);
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `websiteSetupCompleted` on the `Environment` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Environment" DROP COLUMN "websiteSetupCompleted";
|
||||
@@ -51,7 +51,8 @@
|
||||
"data-migration:add-display-id-to-response": "ts-node ./data-migrations/20240905120500_refactor_display_response_relationship/data-migration.ts",
|
||||
"data-migration:address-question": "ts-node ./data-migrations/20240924123456_migrate_address_question/data-migration.ts",
|
||||
"data-migration:advanced-logic": "ts-node ./data-migrations/20240828122408_advanced_logic_editor/data-migration.ts",
|
||||
"data-migration:segments-actions-cleanup": "ts-node ./data-migrations/20240904091113_removed_actions_table/data-migration.ts"
|
||||
"data-migration:segments-actions-cleanup": "ts-node ./data-migrations/20240904091113_removed_actions_table/data-migration.ts",
|
||||
"data-migration:migrate-survey-types": "ts-node ./data-migrations/20241002123456_migrate_survey_types/data-migration.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.18.0",
|
||||
|
||||
@@ -386,24 +386,23 @@ model Integration {
|
||||
}
|
||||
|
||||
model Environment {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
type EnvironmentType
|
||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||
productId String
|
||||
widgetSetupCompleted Boolean @default(false)
|
||||
appSetupCompleted Boolean @default(false)
|
||||
websiteSetupCompleted Boolean @default(false)
|
||||
surveys Survey[]
|
||||
people Person[]
|
||||
actionClasses ActionClass[]
|
||||
attributeClasses AttributeClass[]
|
||||
apiKeys ApiKey[]
|
||||
webhooks Webhook[]
|
||||
tags Tag[]
|
||||
segments Segment[]
|
||||
integration Integration[]
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
type EnvironmentType
|
||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||
productId String
|
||||
widgetSetupCompleted Boolean @default(false)
|
||||
appSetupCompleted Boolean @default(false)
|
||||
surveys Survey[]
|
||||
people Person[]
|
||||
actionClasses ActionClass[]
|
||||
attributeClasses AttributeClass[]
|
||||
apiKeys ApiKey[]
|
||||
webhooks Webhook[]
|
||||
tags Tag[]
|
||||
segments Segment[]
|
||||
integration Integration[]
|
||||
|
||||
@@index([productId])
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
TSegmentUpdateInput,
|
||||
} from "@formbricks/types/segment";
|
||||
import type { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { Alert, AlertDescription } from "@formbricks/ui/components/Alert";
|
||||
import { AlertDialog } from "@formbricks/ui/components/AlertDialog";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { LoadSegmentModal } from "@formbricks/ui/components/LoadSegmentModal";
|
||||
@@ -161,7 +162,7 @@ export function AdvancedTargetingCard({
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
className="w-full rounded-lg border border-slate-300 bg-white"
|
||||
className="w-full overflow-hidden rounded-lg border border-slate-300 bg-white"
|
||||
onOpenChange={setOpen}
|
||||
open={open}>
|
||||
<Collapsible.CollapsibleTrigger
|
||||
@@ -415,6 +416,23 @@ export function AdvancedTargetingCard({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Alert className="flex items-center rounded-none bg-slate-50">
|
||||
<AlertDescription className="ml-2">
|
||||
<span className="mr-1 text-slate-600">
|
||||
User targeting is currently only available when{" "}
|
||||
<Link
|
||||
href="https://formbricks.com//docs/app-surveys/user-identification"
|
||||
target="blank"
|
||||
className="underline">
|
||||
identifying users
|
||||
</Link>{" "}
|
||||
with the Formbricks SDK.
|
||||
</span>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
|
||||
@@ -20,33 +20,20 @@
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
"./app": {
|
||||
"import": "./dist/app.js",
|
||||
"require": "./dist/app.umd.cjs",
|
||||
"types": "./dist/app.d.ts"
|
||||
},
|
||||
"./website": {
|
||||
"import": "./dist/website.js",
|
||||
"require": "./dist/website.umd.cjs",
|
||||
"types": "./dist/website.d.ts"
|
||||
},
|
||||
"./*": "./dist/*"
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.umd.cjs",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"app": [
|
||||
"./dist/app.d.ts"
|
||||
],
|
||||
"website": [
|
||||
"./dist/website.d.ts"
|
||||
"*": [
|
||||
"./dist/index.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite build --watch --mode dev",
|
||||
"build:app": "tsc && vite build --config app.vite.config.ts",
|
||||
"build:website": "tsc && vite build --config website.vite.config.ts",
|
||||
"build": "pnpm build:app && pnpm build:website",
|
||||
"build": "tsc && vite build",
|
||||
"build:dev": "tsc && vite build --mode dev",
|
||||
"go": "vite build --watch --mode dev",
|
||||
"lint": "eslint . --ext .ts,.js,.tsx,.jsx",
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { NetworkError, Result, err, okVoid } from "../../shared/errors";
|
||||
import { Logger } from "../../shared/logger";
|
||||
import { AppConfig } from "./config";
|
||||
import { deinitalize, initialize } from "./initialize";
|
||||
import { closeSurvey } from "./widget";
|
||||
|
||||
const appConfig = AppConfig.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
|
||||
export const logoutPerson = async (): Promise<void> => {
|
||||
deinitalize();
|
||||
appConfig.resetConfig();
|
||||
};
|
||||
|
||||
export const resetPerson = async (): Promise<Result<void, NetworkError>> => {
|
||||
logger.debug("Resetting state & getting new state from backend");
|
||||
closeSurvey();
|
||||
|
||||
const userId = appConfig.get().personState.data.userId;
|
||||
if (!userId) {
|
||||
return err({
|
||||
code: "network_error",
|
||||
status: 500,
|
||||
message: "Missing userId",
|
||||
url: `${appConfig.get().apiHost}/api/v1/client/${appConfig.get().environmentId}/people/${userId}/attributes`,
|
||||
responseMessage: "Missing userId",
|
||||
});
|
||||
}
|
||||
|
||||
const syncParams = {
|
||||
environmentId: appConfig.get().environmentId,
|
||||
apiHost: appConfig.get().apiHost,
|
||||
userId,
|
||||
attributes: appConfig.get().personState.data.attributes,
|
||||
};
|
||||
await logoutPerson();
|
||||
try {
|
||||
await initialize(syncParams);
|
||||
return okVoid();
|
||||
} catch (e) {
|
||||
return err(e as NetworkError);
|
||||
}
|
||||
};
|
||||
@@ -1,11 +1,11 @@
|
||||
import { TJsAppConfigInput, TJsTrackProperties } from "@formbricks/types/js";
|
||||
import { CommandQueue } from "../shared/commandQueue";
|
||||
import { ErrorHandler } from "../shared/errors";
|
||||
import { Logger } from "../shared/logger";
|
||||
import { TJsConfigInput, 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 { ErrorHandler } from "./lib/errors";
|
||||
import { initialize } from "./lib/initialize";
|
||||
import { Logger } from "./lib/logger";
|
||||
import { checkPageUrl } from "./lib/noCodeActions";
|
||||
import { logoutPerson, resetPerson } from "./lib/person";
|
||||
|
||||
@@ -14,9 +14,9 @@ const logger = Logger.getInstance();
|
||||
logger.debug("Create command queue");
|
||||
const queue = new CommandQueue();
|
||||
|
||||
const init = async (initConfig: TJsAppConfigInput) => {
|
||||
const init = async (initConfig: TJsConfigInput) => {
|
||||
ErrorHandler.init(initConfig.errorHandler);
|
||||
queue.add(false, "app", initialize, initConfig);
|
||||
queue.add(false, initialize, initConfig);
|
||||
await queue.wait();
|
||||
};
|
||||
|
||||
@@ -26,27 +26,27 @@ const setEmail = async (email: string): Promise<void> => {
|
||||
};
|
||||
|
||||
const setAttribute = async (key: string, value: any): Promise<void> => {
|
||||
queue.add(true, "app", setAttributeInApp, key, value);
|
||||
queue.add(true, setAttributeInApp, key, value);
|
||||
await queue.wait();
|
||||
};
|
||||
|
||||
const logout = async (): Promise<void> => {
|
||||
queue.add(true, "app", logoutPerson);
|
||||
queue.add(true, logoutPerson);
|
||||
await queue.wait();
|
||||
};
|
||||
|
||||
const reset = async (): Promise<void> => {
|
||||
queue.add(true, "app", resetPerson);
|
||||
queue.add(true, resetPerson);
|
||||
await queue.wait();
|
||||
};
|
||||
|
||||
const track = async (name: string, properties?: TJsTrackProperties): Promise<void> => {
|
||||
queue.add<any>(true, "app", trackCodeAction, name, properties);
|
||||
queue.add<any>(true, trackCodeAction, name, properties);
|
||||
await queue.wait();
|
||||
};
|
||||
|
||||
const registerRouteChange = async (): Promise<void> => {
|
||||
queue.add(true, "app", checkPageUrl);
|
||||
queue.add(true, checkPageUrl);
|
||||
await queue.wait();
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { TJsTrackProperties } from "@formbricks/types/js";
|
||||
import { InvalidCodeError, NetworkError, Result, err, okVoid } from "../../shared/errors";
|
||||
import { Logger } from "../../shared/logger";
|
||||
import { AppConfig } from "./config";
|
||||
import { Config } from "./config";
|
||||
import { InvalidCodeError, NetworkError, Result, err, okVoid } from "./errors";
|
||||
import { Logger } from "./logger";
|
||||
import { triggerSurvey } from "./widget";
|
||||
|
||||
const logger = Logger.getInstance();
|
||||
const appConfig = AppConfig.getInstance();
|
||||
const config = Config.getInstance();
|
||||
|
||||
export const trackAction = async (
|
||||
name: string,
|
||||
@@ -17,7 +17,7 @@ export const trackAction = async (
|
||||
logger.debug(`Formbricks: Action "${aliasName}" tracked`);
|
||||
|
||||
// get a list of surveys that are collecting insights
|
||||
const activeSurveys = appConfig.get().filteredSurveys;
|
||||
const activeSurveys = config.get().filteredSurveys;
|
||||
|
||||
if (!!activeSurveys && activeSurveys.length > 0) {
|
||||
for (const survey of activeSurveys) {
|
||||
@@ -38,7 +38,7 @@ export const trackCodeAction = (
|
||||
code: string,
|
||||
properties?: TJsTrackProperties
|
||||
): Promise<Result<void, NetworkError>> | Result<void, InvalidCodeError> => {
|
||||
const actionClasses = appConfig.get().environmentState.data.actionClasses;
|
||||
const actionClasses = config.get().environmentState.data.actionClasses;
|
||||
|
||||
const codeActionClasses = actionClasses.filter((action) => action.type === "code");
|
||||
const action = codeActionClasses.find((action) => action.key === code);
|
||||
@@ -1,9 +1,9 @@
|
||||
import { FormbricksAPI } from "@formbricks/api";
|
||||
import { AppConfig } from "./config";
|
||||
import { Config } from "./config";
|
||||
|
||||
export const getApi = (): FormbricksAPI => {
|
||||
const inAppConfig = AppConfig.getInstance();
|
||||
const { environmentId, apiHost } = inAppConfig.get();
|
||||
const config = Config.getInstance();
|
||||
const { environmentId, apiHost } = config.get();
|
||||
|
||||
if (!environmentId || !apiHost) {
|
||||
throw new Error("formbricks.init() must be called before getApi()");
|
||||
+24
-24
@@ -1,12 +1,12 @@
|
||||
import { FormbricksAPI } from "@formbricks/api";
|
||||
import { TAttributes } from "@formbricks/types/attributes";
|
||||
import { MissingPersonError, NetworkError, Result, err, ok, okVoid } from "../../shared/errors";
|
||||
import { Logger } from "../../shared/logger";
|
||||
import { fetchPersonState } from "../../shared/personState";
|
||||
import { filterSurveys } from "../../shared/utils";
|
||||
import { AppConfig } from "./config";
|
||||
import { Config } from "./config";
|
||||
import { MissingPersonError, NetworkError, Result, err, ok, okVoid } from "./errors";
|
||||
import { Logger } from "./logger";
|
||||
import { fetchPersonState } from "./personState";
|
||||
import { filterSurveys } from "./utils";
|
||||
|
||||
const appConfig = AppConfig.getInstance();
|
||||
const config = Config.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
|
||||
export const updateAttribute = async (
|
||||
@@ -21,8 +21,8 @@ export const updateAttribute = async (
|
||||
Error | NetworkError
|
||||
>
|
||||
> => {
|
||||
const { apiHost, environmentId } = appConfig.get();
|
||||
const userId = appConfig.get().personState.data.userId;
|
||||
const { apiHost, environmentId } = config.get();
|
||||
const userId = config.get().personState.data.userId;
|
||||
|
||||
if (!userId) {
|
||||
return err({
|
||||
@@ -58,7 +58,7 @@ export const updateAttribute = async (
|
||||
// @ts-expect-error
|
||||
status: res.error.status ?? 500,
|
||||
message: res.error.message ?? `Error updating person with userId ${userId}`,
|
||||
url: `${appConfig.get().apiHost}/api/v1/client/${environmentId}/people/${userId}/attributes`,
|
||||
url: `${config.get().apiHost}/api/v1/client/${environmentId}/people/${userId}/attributes`,
|
||||
responseMessage: res.error.message,
|
||||
});
|
||||
}
|
||||
@@ -93,7 +93,7 @@ export const updateAttributes = async (
|
||||
const updatedAttributes = { ...attributes };
|
||||
|
||||
try {
|
||||
const existingAttributes = appConfig.get().personState.data.attributes;
|
||||
const existingAttributes = config.get().personState.data.attributes;
|
||||
if (existingAttributes) {
|
||||
for (const [key, value] of Object.entries(existingAttributes)) {
|
||||
if (updatedAttributes[key] === value) {
|
||||
@@ -140,7 +140,7 @@ export const updateAttributes = async (
|
||||
};
|
||||
|
||||
export const isExistingAttribute = (key: string, value: string): boolean => {
|
||||
if (appConfig.get().personState.data.attributes[key] === value) {
|
||||
if (config.get().personState.data.attributes[key] === value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -156,14 +156,7 @@ export const setAttributeInApp = async (
|
||||
return okVoid();
|
||||
}
|
||||
|
||||
const userId = appConfig.get().personState.data.userId;
|
||||
|
||||
if (!userId) {
|
||||
return err({
|
||||
code: "missing_person",
|
||||
message: "Missing userId",
|
||||
});
|
||||
}
|
||||
const userId = config.get().personState.data.userId;
|
||||
|
||||
logger.debug("Setting attribute: " + key + " to value: " + value);
|
||||
// check if attribute already exists with this value
|
||||
@@ -172,23 +165,30 @@ export const setAttributeInApp = async (
|
||||
return okVoid();
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
logger.error(
|
||||
"UserId not provided, please provide a userId in the init method before setting attributes."
|
||||
);
|
||||
return okVoid();
|
||||
}
|
||||
|
||||
const result = await updateAttribute(key, value.toString());
|
||||
|
||||
if (result.ok) {
|
||||
if (result.value.changed) {
|
||||
const personState = await fetchPersonState(
|
||||
{
|
||||
apiHost: appConfig.get().apiHost,
|
||||
environmentId: appConfig.get().environmentId,
|
||||
apiHost: config.get().apiHost,
|
||||
environmentId: config.get().environmentId,
|
||||
userId,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
const filteredSurveys = filterSurveys(appConfig.get().environmentState, personState);
|
||||
const filteredSurveys = filterSurveys(config.get().environmentState, personState);
|
||||
|
||||
appConfig.update({
|
||||
...appConfig.get(),
|
||||
config.update({
|
||||
...config.get(),
|
||||
personState,
|
||||
filteredSurveys,
|
||||
});
|
||||
+4
-9
@@ -1,13 +1,10 @@
|
||||
import { wrapThrowsAsync } from "@formbricks/types/error-handlers";
|
||||
import { TJsPackageType } from "@formbricks/types/js";
|
||||
import { checkInitialized as checkInitializedInApp } from "../app/lib/initialize";
|
||||
import { ErrorHandler, Result } from "../shared/errors";
|
||||
import { checkInitialized as checkInitializedWebsite } from "../website/lib/initialize";
|
||||
import { ErrorHandler, Result } from "./errors";
|
||||
import { checkInitialized } from "./initialize";
|
||||
|
||||
export class CommandQueue {
|
||||
private queue: {
|
||||
command: (args: any) => Promise<Result<void, any>> | Result<void, any> | Promise<void>;
|
||||
packageType: TJsPackageType;
|
||||
checkInitialized: boolean;
|
||||
commandArgs: any[any];
|
||||
}[] = [];
|
||||
@@ -17,11 +14,10 @@ export class CommandQueue {
|
||||
|
||||
public add<A>(
|
||||
checkInitialized: boolean = true,
|
||||
packageType: TJsPackageType,
|
||||
command: (...args: A[]) => Promise<Result<void, any>> | Result<void, any> | Promise<void>,
|
||||
...args: A[]
|
||||
) {
|
||||
this.queue.push({ command, checkInitialized, commandArgs: args, packageType });
|
||||
this.queue.push({ command, checkInitialized, commandArgs: args });
|
||||
|
||||
if (!this.running) {
|
||||
this.commandPromise = new Promise((resolve) => {
|
||||
@@ -48,8 +44,7 @@ export class CommandQueue {
|
||||
// make sure formbricks is initialized
|
||||
if (currentItem.checkInitialized) {
|
||||
// call different function based on package type
|
||||
const initResult =
|
||||
currentItem.packageType === "website" ? checkInitializedWebsite() : checkInitializedInApp();
|
||||
const initResult = checkInitialized();
|
||||
|
||||
if (initResult && initResult.ok !== true) {
|
||||
errorHandler.handle(initResult.error);
|
||||
@@ -1,9 +1,9 @@
|
||||
import { TJsConfig, TJsConfigUpdateInput } from "@formbricks/types/js";
|
||||
import { APP_SURVEYS_LOCAL_STORAGE_KEY } from "../../shared/constants";
|
||||
import { Result, err, ok, wrapThrows } from "../../shared/errors";
|
||||
import { JS_LOCAL_STORAGE_KEY } from "./constants";
|
||||
import { Result, err, ok, wrapThrows } from "./errors";
|
||||
|
||||
export class AppConfig {
|
||||
private static instance: AppConfig | undefined;
|
||||
export class Config {
|
||||
private static instance: Config | undefined;
|
||||
private config: TJsConfig | null = null;
|
||||
|
||||
private constructor() {
|
||||
@@ -14,11 +14,11 @@ export class AppConfig {
|
||||
}
|
||||
}
|
||||
|
||||
static getInstance(): AppConfig {
|
||||
if (!AppConfig.instance) {
|
||||
AppConfig.instance = new AppConfig();
|
||||
static getInstance(): Config {
|
||||
if (!Config.instance) {
|
||||
Config.instance = new Config();
|
||||
}
|
||||
return AppConfig.instance;
|
||||
return Config.instance;
|
||||
}
|
||||
|
||||
public update(newConfig: TJsConfigUpdateInput): void {
|
||||
@@ -45,7 +45,7 @@ export class AppConfig {
|
||||
|
||||
public loadFromLocalStorage(): Result<TJsConfig, Error> {
|
||||
if (typeof window !== "undefined") {
|
||||
const savedConfig = localStorage.getItem(APP_SURVEYS_LOCAL_STORAGE_KEY);
|
||||
const savedConfig = localStorage.getItem(JS_LOCAL_STORAGE_KEY);
|
||||
if (savedConfig) {
|
||||
// TODO: validate config
|
||||
// This is a hack to get around the fact that we don't have a proper
|
||||
@@ -69,7 +69,7 @@ export class AppConfig {
|
||||
|
||||
private async saveToStorage(): Promise<Result<Promise<void>, Error>> {
|
||||
return wrapThrows(async () => {
|
||||
await localStorage.setItem(APP_SURVEYS_LOCAL_STORAGE_KEY, JSON.stringify(this.config));
|
||||
await localStorage.setItem(JS_LOCAL_STORAGE_KEY, JSON.stringify(this.config));
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export class AppConfig {
|
||||
this.config = null;
|
||||
|
||||
return wrapThrows(async () => {
|
||||
localStorage.removeItem(APP_SURVEYS_LOCAL_STORAGE_KEY);
|
||||
localStorage.removeItem(JS_LOCAL_STORAGE_KEY);
|
||||
})();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const RN_ASYNC_STORAGE_KEY = "formbricks-react-native";
|
||||
export const JS_LOCAL_STORAGE_KEY = "formbricks-js";
|
||||
export const LEGACY_JS_WEBSITE_LOCAL_STORAGE_KEY = "formbricks-js-website";
|
||||
export const LEGACY_JS_APP_LOCAL_STORAGE_KEY = "formbricks-js-app";
|
||||
export const CONTAINER_ID = "formbricks-app-container";
|
||||
+4
-9
@@ -1,11 +1,11 @@
|
||||
// shared functions for environment and person state(s)
|
||||
import { TJsEnvironmentState, TJsEnvironmentSyncParams } from "@formbricks/types/js";
|
||||
import { AppConfig } from "../app/lib/config";
|
||||
import { WebsiteConfig } from "../website/lib/config";
|
||||
import { Config } from "./config";
|
||||
import { err } from "./errors";
|
||||
import { Logger } from "./logger";
|
||||
import { filterSurveys, getIsDebug } from "./utils";
|
||||
|
||||
const config = Config.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
let environmentStateSyncIntervalId: number | null = null;
|
||||
|
||||
@@ -19,7 +19,6 @@ let environmentStateSyncIntervalId: number | null = null;
|
||||
*/
|
||||
export const fetchEnvironmentState = async (
|
||||
{ apiHost, environmentId }: TJsEnvironmentSyncParams,
|
||||
sdkType: "app" | "website",
|
||||
noCache: boolean = false
|
||||
): Promise<TJsEnvironmentState> => {
|
||||
let fetchOptions: RequestInit = {};
|
||||
@@ -29,7 +28,7 @@ export const fetchEnvironmentState = async (
|
||||
logger.debug("No cache option set for sync");
|
||||
}
|
||||
|
||||
const url = `${apiHost}/api/v1/client/${environmentId}/${sdkType}/environment`;
|
||||
const url = `${apiHost}/api/v1/client/${environmentId}/environment`;
|
||||
|
||||
const response = await fetch(url, fetchOptions);
|
||||
|
||||
@@ -56,10 +55,7 @@ export const fetchEnvironmentState = async (
|
||||
};
|
||||
};
|
||||
|
||||
export const addEnvironmentStateExpiryCheckListener = (
|
||||
sdkType: "app" | "website",
|
||||
config: AppConfig | WebsiteConfig
|
||||
): void => {
|
||||
export const addEnvironmentStateExpiryCheckListener = (): void => {
|
||||
let updateInterval = 1000 * 60; // every minute
|
||||
if (typeof window !== "undefined" && environmentStateSyncIntervalId === null) {
|
||||
environmentStateSyncIntervalId = window.setInterval(async () => {
|
||||
@@ -79,7 +75,6 @@ export const addEnvironmentStateExpiryCheckListener = (
|
||||
apiHost: config.get().apiHost,
|
||||
environmentId: config.get().environmentId,
|
||||
},
|
||||
sdkType,
|
||||
true
|
||||
);
|
||||
|
||||
+6
-10
@@ -1,11 +1,7 @@
|
||||
import {
|
||||
addEnvironmentStateExpiryCheckListener,
|
||||
clearEnvironmentStateExpiryCheckListener,
|
||||
} from "../../shared/environmentState";
|
||||
import {
|
||||
addPersonStateExpiryCheckListener,
|
||||
clearPersonStateExpiryCheckListener,
|
||||
} from "../../shared/personState";
|
||||
} from "./environmentState";
|
||||
import {
|
||||
addClickEventListener,
|
||||
addExitIntentListener,
|
||||
@@ -15,14 +11,14 @@ import {
|
||||
removeExitIntentListener,
|
||||
removePageUrlEventListeners,
|
||||
removeScrollDepthListener,
|
||||
} from "../lib/noCodeActions";
|
||||
import { AppConfig } from "./config";
|
||||
} from "./noCodeActions";
|
||||
import { addPersonStateExpiryCheckListener, clearPersonStateExpiryCheckListener } from "./personState";
|
||||
|
||||
let areRemoveEventListenersAdded = false;
|
||||
|
||||
export const addEventListeners = (config: AppConfig): void => {
|
||||
addEnvironmentStateExpiryCheckListener("app", config);
|
||||
addPersonStateExpiryCheckListener(config);
|
||||
export const addEventListeners = (): void => {
|
||||
addEnvironmentStateExpiryCheckListener();
|
||||
addPersonStateExpiryCheckListener();
|
||||
addPageUrlEventListeners();
|
||||
addClickEventListener();
|
||||
addExitIntentListener();
|
||||
+126
-85
@@ -1,7 +1,14 @@
|
||||
import { TAttributes } from "@formbricks/types/attributes";
|
||||
import type { TJsAppConfigInput, TJsConfig } from "@formbricks/types/js";
|
||||
import { APP_SURVEYS_LOCAL_STORAGE_KEY } from "../../shared/constants";
|
||||
import { fetchEnvironmentState } from "../../shared/environmentState";
|
||||
import { type TJsConfig, type TJsConfigInput } from "@formbricks/types/js";
|
||||
import { trackNoCodeAction } from "./actions";
|
||||
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 { fetchEnvironmentState } from "./environmentState";
|
||||
import {
|
||||
ErrorHandler,
|
||||
MissingFieldError,
|
||||
@@ -12,18 +19,14 @@ import {
|
||||
err,
|
||||
okVoid,
|
||||
wrapThrows,
|
||||
} from "../../shared/errors";
|
||||
import { Logger } from "../../shared/logger";
|
||||
import { fetchPersonState } from "../../shared/personState";
|
||||
import { filterSurveys, getIsDebug } from "../../shared/utils";
|
||||
import { trackNoCodeAction } from "./actions";
|
||||
import { updateAttributes } from "./attributes";
|
||||
import { AppConfig } from "./config";
|
||||
} from "./errors";
|
||||
import { addCleanupEventListeners, addEventListeners, removeAllEventListeners } from "./eventListeners";
|
||||
import { Logger } from "./logger";
|
||||
import { checkPageUrl } from "./noCodeActions";
|
||||
import { DEFAULT_PERSON_STATE_NO_USER_ID, fetchPersonState } from "./personState";
|
||||
import { filterSurveys, getIsDebug } from "./utils";
|
||||
import { addWidgetContainer, removeWidgetContainer, setIsSurveyRunning } from "./widget";
|
||||
|
||||
const appConfigGlobal = AppConfig.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
|
||||
let isInitialized = false;
|
||||
@@ -32,38 +35,72 @@ export const setIsInitialized = (value: boolean) => {
|
||||
isInitialized = value;
|
||||
};
|
||||
|
||||
const checkForOlderLocalConfig = (): boolean => {
|
||||
const oldConfig = localStorage.getItem(APP_SURVEYS_LOCAL_STORAGE_KEY);
|
||||
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 (oldConfig) {
|
||||
const parsedOldConfig = JSON.parse(oldConfig);
|
||||
if (parsedOldConfig.state || parsedOldConfig.expiresAt) {
|
||||
// local config follows old structure
|
||||
return true;
|
||||
if (oldWebsiteConfig) {
|
||||
localStorage.removeItem(LEGACY_JS_WEBSITE_LOCAL_STORAGE_KEY);
|
||||
const parsedOldConfig = JSON.parse(oldWebsiteConfig) as TJsConfig;
|
||||
|
||||
if (
|
||||
parsedOldConfig.environmentId &&
|
||||
parsedOldConfig.apiHost &&
|
||||
parsedOldConfig.environmentState &&
|
||||
parsedOldConfig.personState &&
|
||||
parsedOldConfig.filteredSurveys
|
||||
) {
|
||||
const newLocalStorageConfig = { ...parsedOldConfig };
|
||||
|
||||
return {
|
||||
changed: true,
|
||||
newState: newLocalStorageConfig,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
if (oldAppConfig) {
|
||||
localStorage.removeItem(LEGACY_JS_APP_LOCAL_STORAGE_KEY);
|
||||
const parsedOldConfig = JSON.parse(oldAppConfig) as TJsConfig;
|
||||
|
||||
if (
|
||||
parsedOldConfig.environmentId &&
|
||||
parsedOldConfig.apiHost &&
|
||||
parsedOldConfig.environmentState &&
|
||||
parsedOldConfig.personState &&
|
||||
parsedOldConfig.filteredSurveys
|
||||
) {
|
||||
return {
|
||||
changed: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
changed: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const initialize = async (
|
||||
configInput: TJsAppConfigInput
|
||||
configInput: TJsConfigInput
|
||||
): Promise<Result<void, MissingFieldError | NetworkError | MissingPersonError>> => {
|
||||
const isDebug = getIsDebug();
|
||||
if (isDebug) {
|
||||
logger.configure({ logLevel: "debug" });
|
||||
}
|
||||
let config = Config.getInstance();
|
||||
|
||||
const isLocalStorageOld = checkForOlderLocalConfig();
|
||||
const { changed, newState } = migrateLocalStorage();
|
||||
|
||||
let appConfig = appConfigGlobal;
|
||||
if (changed) {
|
||||
config.resetConfig();
|
||||
config = Config.getInstance();
|
||||
|
||||
if (isLocalStorageOld) {
|
||||
logger.debug("Local config is of an older version");
|
||||
logger.debug("Resetting config");
|
||||
|
||||
appConfig.resetConfig();
|
||||
appConfig = AppConfig.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);
|
||||
}
|
||||
}
|
||||
|
||||
if (isInitialized) {
|
||||
@@ -73,7 +110,7 @@ export const initialize = async (
|
||||
|
||||
let existingConfig: TJsConfig | undefined;
|
||||
try {
|
||||
existingConfig = appConfigGlobal.get();
|
||||
existingConfig = config.get();
|
||||
logger.debug("Found existing configuration.");
|
||||
} catch (e) {
|
||||
logger.debug("No existing configuration found.");
|
||||
@@ -85,7 +122,7 @@ export const initialize = async (
|
||||
logger.debug(
|
||||
"Formbricks is in error state, but debug mode is active. Resetting config and continuing."
|
||||
);
|
||||
appConfigGlobal.resetConfig();
|
||||
config.resetConfig();
|
||||
return okVoid();
|
||||
}
|
||||
|
||||
@@ -122,38 +159,32 @@ export const initialize = async (
|
||||
});
|
||||
}
|
||||
|
||||
if (!configInput.userId) {
|
||||
logger.debug("No userId provided");
|
||||
|
||||
return err({
|
||||
code: "missing_field",
|
||||
field: "userId",
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug("Adding widget container to DOM");
|
||||
addWidgetContainer();
|
||||
|
||||
let updatedAttributes: TAttributes | null = null;
|
||||
if (configInput.attributes) {
|
||||
const res = await updateAttributes(
|
||||
configInput.apiHost,
|
||||
configInput.environmentId,
|
||||
configInput.userId,
|
||||
configInput.attributes
|
||||
);
|
||||
if (res.ok !== true) {
|
||||
return err(res.error);
|
||||
if (configInput.userId) {
|
||||
const res = await updateAttributes(
|
||||
configInput.apiHost,
|
||||
configInput.environmentId,
|
||||
configInput.userId,
|
||||
configInput.attributes
|
||||
);
|
||||
if (res.ok !== true) {
|
||||
return err(res.error);
|
||||
}
|
||||
updatedAttributes = res.value;
|
||||
} else {
|
||||
updatedAttributes = { ...configInput.attributes };
|
||||
}
|
||||
updatedAttributes = res.value;
|
||||
}
|
||||
|
||||
if (
|
||||
existingConfig &&
|
||||
existingConfig.environmentState &&
|
||||
existingConfig.environmentId === configInput.environmentId &&
|
||||
existingConfig.apiHost === configInput.apiHost &&
|
||||
existingConfig.personState?.data?.userId === configInput.userId
|
||||
existingConfig.apiHost === configInput.apiHost
|
||||
) {
|
||||
logger.debug("Configuration fits init parameters.");
|
||||
let isEnvironmentStateExpired = false;
|
||||
@@ -164,7 +195,12 @@ export const initialize = async (
|
||||
isEnvironmentStateExpired = true;
|
||||
}
|
||||
|
||||
if (existingConfig.personState.expiresAt && new Date(existingConfig.personState.expiresAt) < new Date()) {
|
||||
// if the config has a userId and the person state has expired, we need to sync the person state
|
||||
if (
|
||||
configInput.userId &&
|
||||
existingConfig.personState.expiresAt &&
|
||||
new Date(existingConfig.personState.expiresAt) < new Date()
|
||||
) {
|
||||
logger.debug("Person state expired. Syncing.");
|
||||
isPersonStateExpired = true;
|
||||
}
|
||||
@@ -172,29 +208,33 @@ export const initialize = async (
|
||||
try {
|
||||
// fetch the environment state (if expired)
|
||||
const environmentState = isEnvironmentStateExpired
|
||||
? await fetchEnvironmentState(
|
||||
{
|
||||
apiHost: configInput.apiHost,
|
||||
environmentId: configInput.environmentId,
|
||||
},
|
||||
"app"
|
||||
)
|
||||
? await fetchEnvironmentState({
|
||||
apiHost: configInput.apiHost,
|
||||
environmentId: configInput.environmentId,
|
||||
})
|
||||
: existingConfig.environmentState;
|
||||
|
||||
// fetch the person state (if expired)
|
||||
const personState = isPersonStateExpired
|
||||
? await fetchPersonState({
|
||||
|
||||
let { personState } = existingConfig;
|
||||
|
||||
if (isPersonStateExpired) {
|
||||
if (configInput.userId) {
|
||||
personState = await fetchPersonState({
|
||||
apiHost: configInput.apiHost,
|
||||
environmentId: configInput.environmentId,
|
||||
userId: configInput.userId,
|
||||
})
|
||||
: existingConfig.personState;
|
||||
});
|
||||
} else {
|
||||
personState = DEFAULT_PERSON_STATE_NO_USER_ID;
|
||||
}
|
||||
}
|
||||
|
||||
// filter the environment state wrt the person state
|
||||
const filteredSurveys = filterSurveys(environmentState, personState);
|
||||
|
||||
// update the appConfig with the new filtered surveys
|
||||
appConfigGlobal.update({
|
||||
config.update({
|
||||
...existingConfig,
|
||||
environmentState,
|
||||
personState,
|
||||
@@ -204,13 +244,13 @@ export const initialize = async (
|
||||
const surveyNames = filteredSurveys.map((s) => s.name);
|
||||
logger.debug("Fetched " + surveyNames.length + " surveys during sync: " + surveyNames.join(", "));
|
||||
} catch (e) {
|
||||
putFormbricksInErrorState(appConfig);
|
||||
putFormbricksInErrorState(config);
|
||||
}
|
||||
} else {
|
||||
logger.debug(
|
||||
"No valid configuration found or it has been expired. Resetting config and creating new one."
|
||||
);
|
||||
appConfigGlobal.resetConfig();
|
||||
config.resetConfig();
|
||||
logger.debug("Syncing.");
|
||||
|
||||
try {
|
||||
@@ -219,21 +259,22 @@ export const initialize = async (
|
||||
apiHost: configInput.apiHost,
|
||||
environmentId: configInput.environmentId,
|
||||
},
|
||||
"app",
|
||||
false
|
||||
);
|
||||
const personState = await fetchPersonState(
|
||||
{
|
||||
apiHost: configInput.apiHost,
|
||||
environmentId: configInput.environmentId,
|
||||
userId: configInput.userId,
|
||||
},
|
||||
false
|
||||
);
|
||||
const personState = configInput.userId
|
||||
? await fetchPersonState(
|
||||
{
|
||||
apiHost: configInput.apiHost,
|
||||
environmentId: configInput.environmentId,
|
||||
userId: configInput.userId,
|
||||
},
|
||||
false
|
||||
)
|
||||
: DEFAULT_PERSON_STATE_NO_USER_ID;
|
||||
|
||||
const filteredSurveys = filterSurveys(environmentState, personState);
|
||||
|
||||
appConfigGlobal.update({
|
||||
config.update({
|
||||
apiHost: configInput.apiHost,
|
||||
environmentId: configInput.environmentId,
|
||||
personState,
|
||||
@@ -250,14 +291,14 @@ export const initialize = async (
|
||||
|
||||
// update attributes in config
|
||||
if (updatedAttributes && Object.keys(updatedAttributes).length > 0) {
|
||||
appConfigGlobal.update({
|
||||
...appConfigGlobal.get(),
|
||||
config.update({
|
||||
...config.get(),
|
||||
personState: {
|
||||
...appConfigGlobal.get().personState,
|
||||
...config.get().personState,
|
||||
data: {
|
||||
...appConfigGlobal.get().personState.data,
|
||||
...config.get().personState.data,
|
||||
attributes: {
|
||||
...appConfigGlobal.get().personState.data.attributes,
|
||||
...config.get().personState.data.attributes,
|
||||
...updatedAttributes,
|
||||
},
|
||||
},
|
||||
@@ -266,7 +307,7 @@ export const initialize = async (
|
||||
}
|
||||
|
||||
logger.debug("Adding event listeners");
|
||||
addEventListeners(appConfigGlobal);
|
||||
addEventListeners();
|
||||
addCleanupEventListeners();
|
||||
|
||||
setIsInitialized(true);
|
||||
@@ -293,7 +334,7 @@ export const handleErrorOnFirstInit = () => {
|
||||
};
|
||||
|
||||
// can't use config.update here because the config is not yet initialized
|
||||
wrapThrows(() => localStorage.setItem(APP_SURVEYS_LOCAL_STORAGE_KEY, JSON.stringify(initialErrorConfig)))();
|
||||
wrapThrows(() => localStorage.setItem(JS_LOCAL_STORAGE_KEY, JSON.stringify(initialErrorConfig)))();
|
||||
throw new Error("Could not initialize formbricks");
|
||||
};
|
||||
|
||||
@@ -317,7 +358,7 @@ export const deinitalize = (): void => {
|
||||
setIsInitialized(false);
|
||||
};
|
||||
|
||||
export const putFormbricksInErrorState = (appConfig: AppConfig): void => {
|
||||
export const putFormbricksInErrorState = (config: Config): void => {
|
||||
if (getIsDebug()) {
|
||||
logger.debug("Not putting formbricks in error state because debug mode is active (no error state)");
|
||||
return;
|
||||
@@ -325,8 +366,8 @@ export const putFormbricksInErrorState = (appConfig: AppConfig): void => {
|
||||
|
||||
logger.debug("Putting formbricks in error state");
|
||||
// change formbricks status to error
|
||||
appConfig.update({
|
||||
...appConfigGlobal.get(),
|
||||
config.update({
|
||||
...config.get(),
|
||||
status: {
|
||||
value: "error",
|
||||
expiresAt: new Date(new Date().getTime() + 10 * 60000), // 10 minutes in the future
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import type { TActionClass } from "@formbricks/types/action-classes";
|
||||
import { ErrorHandler, NetworkError, Result, err, match, okVoid } from "../../shared/errors";
|
||||
import { Logger } from "../../shared/logger";
|
||||
import { evaluateNoCodeConfigClick, handleUrlFilters } from "../../shared/utils";
|
||||
import { trackNoCodeAction } from "./actions";
|
||||
import { AppConfig } from "./config";
|
||||
import { Config } from "./config";
|
||||
import { ErrorHandler, NetworkError, Result, err, match, okVoid } from "./errors";
|
||||
import { Logger } from "./logger";
|
||||
import { evaluateNoCodeConfigClick, handleUrlFilters } from "./utils";
|
||||
|
||||
const appConfig = AppConfig.getInstance();
|
||||
const appConfig = Config.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
const errorHandler = ErrorHandler.getInstance();
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Config } from "./config";
|
||||
import { NetworkError, Result, err, okVoid } from "./errors";
|
||||
import { deinitalize, initialize } from "./initialize";
|
||||
import { Logger } from "./logger";
|
||||
import { closeSurvey } from "./widget";
|
||||
|
||||
const config = Config.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
|
||||
export const logoutPerson = async (): Promise<void> => {
|
||||
deinitalize();
|
||||
config.resetConfig();
|
||||
};
|
||||
|
||||
export const resetPerson = async (): Promise<Result<void, NetworkError>> => {
|
||||
logger.debug("Resetting state & getting new state from backend");
|
||||
closeSurvey();
|
||||
|
||||
const userId = config.get().personState.data.userId;
|
||||
|
||||
const syncParams = {
|
||||
environmentId: config.get().environmentId,
|
||||
apiHost: config.get().apiHost,
|
||||
...(userId && { userId }),
|
||||
attributes: config.get().personState.data.attributes,
|
||||
};
|
||||
await logoutPerson();
|
||||
try {
|
||||
await initialize(syncParams);
|
||||
return okVoid();
|
||||
} catch (e) {
|
||||
return err(e as NetworkError);
|
||||
}
|
||||
};
|
||||
+12
-10
@@ -1,13 +1,14 @@
|
||||
import { TJsPersonState, TJsPersonSyncParams } from "@formbricks/types/js";
|
||||
import { AppConfig } from "../app/lib/config";
|
||||
import { Config } from "./config";
|
||||
import { err } from "./errors";
|
||||
import { Logger } from "./logger";
|
||||
import { getIsDebug } from "./utils";
|
||||
|
||||
const config = Config.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
let personStateSyncIntervalId: number | null = null;
|
||||
|
||||
export const DEFAULT_PERSON_STATE_WEBSITE: TJsPersonState = {
|
||||
export const DEFAULT_PERSON_STATE_NO_USER_ID: TJsPersonState = {
|
||||
expiresAt: null,
|
||||
data: {
|
||||
userId: null,
|
||||
@@ -39,7 +40,7 @@ export const fetchPersonState = async (
|
||||
logger.debug("No cache option set for sync");
|
||||
}
|
||||
|
||||
const url = `${apiHost}/api/v1/client/${environmentId}/app/people/${userId}`;
|
||||
const url = `${apiHost}/api/v1/client/${environmentId}/identify/people/${userId}`;
|
||||
|
||||
const response = await fetch(url, fetchOptions);
|
||||
|
||||
@@ -60,6 +61,8 @@ export const fetchPersonState = async (
|
||||
const data = await response.json();
|
||||
const { data: state } = data;
|
||||
|
||||
console.log("Person state fetched", state);
|
||||
|
||||
const defaultPersonState: TJsPersonState = {
|
||||
expiresAt: new Date(new Date().getTime() + 1000 * 60 * 30), // 30 minutes
|
||||
data: {
|
||||
@@ -84,25 +87,24 @@ export const fetchPersonState = async (
|
||||
|
||||
/**
|
||||
* Add a listener to check if the person state has expired with a certain interval
|
||||
* @param appConfig - The app config
|
||||
* @param config - The configuration for the SDK
|
||||
*/
|
||||
export const addPersonStateExpiryCheckListener = (appConfig: AppConfig): void => {
|
||||
export const addPersonStateExpiryCheckListener = (): void => {
|
||||
const updateInterval = 1000 * 60; // every 60 seconds
|
||||
|
||||
if (typeof window !== "undefined" && personStateSyncIntervalId === null) {
|
||||
personStateSyncIntervalId = window.setInterval(async () => {
|
||||
const userId = appConfig.get().personState.data.userId;
|
||||
const userId = config.get().personState.data.userId;
|
||||
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// extend the personState validity by 30 minutes:
|
||||
|
||||
appConfig.update({
|
||||
...appConfig.get(),
|
||||
config.update({
|
||||
...config.get(),
|
||||
personState: {
|
||||
...appConfig.get().personState,
|
||||
...config.get().personState,
|
||||
expiresAt: new Date(new Date().getTime() + 1000 * 60 * 30), // 30 minutes
|
||||
},
|
||||
});
|
||||
@@ -158,11 +158,10 @@ export const getIsDebug = () => window.location.search.includes("formbricksDebug
|
||||
// takes the environment and person state and returns the filtered surveys
|
||||
export const filterSurveys = (
|
||||
environmentState: TJsEnvironmentState,
|
||||
personState: TJsPersonState,
|
||||
sdkType: "app" | "website" = "app"
|
||||
personState: TJsPersonState
|
||||
): TSurvey[] => {
|
||||
const { product, surveys } = environmentState.data;
|
||||
const { displays, responses, lastDisplayAt, segments } = personState.data;
|
||||
const { displays, responses, lastDisplayAt, segments, userId } = personState.data;
|
||||
|
||||
if (!displays) {
|
||||
return [];
|
||||
@@ -220,7 +219,7 @@ export const filterSurveys = (
|
||||
}
|
||||
});
|
||||
|
||||
if (sdkType === "website") {
|
||||
if (!userId) {
|
||||
return filteredSurveys;
|
||||
}
|
||||
|
||||
@@ -6,19 +6,18 @@ import { TJsFileUploadParams, TJsPersonState, TJsTrackProperties } from "@formbr
|
||||
import { TResponseHiddenFieldValue, TResponseUpdate } from "@formbricks/types/responses";
|
||||
import { TUploadFileConfig } from "@formbricks/types/storage";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { Logger } from "../../shared/logger";
|
||||
import { Config } from "./config";
|
||||
import { CONTAINER_ID } from "./constants";
|
||||
import { Logger } from "./logger";
|
||||
import {
|
||||
filterSurveys,
|
||||
getDefaultLanguageCode,
|
||||
getLanguageCode,
|
||||
handleHiddenFields,
|
||||
shouldDisplayBasedOnPercentage,
|
||||
} from "../../shared/utils";
|
||||
import { AppConfig } from "./config";
|
||||
} from "./utils";
|
||||
|
||||
const containerId = "formbricks-app-container";
|
||||
|
||||
const appConfig = AppConfig.getInstance();
|
||||
const config = Config.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
let isSurveyRunning = false;
|
||||
let setIsError = (_: boolean) => {};
|
||||
@@ -65,8 +64,8 @@ const renderWidget = async (
|
||||
logger.debug(`Delaying survey "${survey.name}" by ${survey.delay} seconds.`);
|
||||
}
|
||||
|
||||
const { product } = appConfig.get().environmentState.data ?? {};
|
||||
const { attributes } = appConfig.get().personState.data ?? {};
|
||||
const { product } = config.get().environmentState.data ?? {};
|
||||
const { attributes } = config.get().personState.data ?? {};
|
||||
|
||||
const isMultiLanguageSurvey = survey.languages.length > 1;
|
||||
let languageCode = "default";
|
||||
@@ -82,12 +81,12 @@ const renderWidget = async (
|
||||
languageCode = displayLanguage;
|
||||
}
|
||||
|
||||
const surveyState = new SurveyState(survey.id, null, null, appConfig.get().personState.data.userId);
|
||||
const surveyState = new SurveyState(survey.id, null, null, config.get().personState.data.userId);
|
||||
|
||||
const responseQueue = new ResponseQueue(
|
||||
{
|
||||
apiHost: appConfig.get().apiHost,
|
||||
environmentId: appConfig.get().environmentId,
|
||||
apiHost: config.get().apiHost,
|
||||
environmentId: config.get().environmentId,
|
||||
retryAttempts: 2,
|
||||
onResponseSendingFailed: () => {
|
||||
setIsError(true);
|
||||
@@ -121,21 +120,16 @@ const renderWidget = async (
|
||||
setIsResponseSendingFinished = f;
|
||||
},
|
||||
onDisplay: async () => {
|
||||
const { userId } = appConfig.get().personState.data;
|
||||
|
||||
if (!userId) {
|
||||
logger.debug("User ID not found. Skipping.");
|
||||
return;
|
||||
}
|
||||
const { userId } = config.get().personState.data;
|
||||
|
||||
const api = new FormbricksAPI({
|
||||
apiHost: appConfig.get().apiHost,
|
||||
environmentId: appConfig.get().environmentId,
|
||||
apiHost: config.get().apiHost,
|
||||
environmentId: config.get().environmentId,
|
||||
});
|
||||
|
||||
const res = await api.client.display.create({
|
||||
surveyId: survey.id,
|
||||
userId,
|
||||
...(userId && { userId }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -147,10 +141,10 @@ const renderWidget = async (
|
||||
surveyState.updateDisplayId(id);
|
||||
responseQueue.updateSurveyState(surveyState);
|
||||
|
||||
const existingDisplays = appConfig.get().personState.data.displays;
|
||||
const existingDisplays = config.get().personState.data.displays;
|
||||
const newDisplay = { surveyId: survey.id, createdAt: new Date() };
|
||||
const displays = existingDisplays ? [...existingDisplays, newDisplay] : [newDisplay];
|
||||
const previousConfig = appConfig.get();
|
||||
const previousConfig = config.get();
|
||||
|
||||
const updatedPersonState: TJsPersonState = {
|
||||
...previousConfig.personState,
|
||||
@@ -163,23 +157,21 @@ const renderWidget = async (
|
||||
|
||||
const filteredSurveys = filterSurveys(previousConfig.environmentState, updatedPersonState);
|
||||
|
||||
appConfig.update({
|
||||
config.update({
|
||||
...previousConfig,
|
||||
environmentState: previousConfig.environmentState,
|
||||
personState: updatedPersonState,
|
||||
filteredSurveys,
|
||||
});
|
||||
},
|
||||
onResponse: (responseUpdate: TResponseUpdate) => {
|
||||
const { userId } = appConfig.get().personState.data;
|
||||
|
||||
if (!userId) {
|
||||
logger.debug("User ID not found. Skipping.");
|
||||
return;
|
||||
}
|
||||
const { userId } = config.get().personState.data;
|
||||
|
||||
const isNewResponse = surveyState.responseId === null;
|
||||
|
||||
surveyState.updateUserId(userId);
|
||||
if (userId) {
|
||||
surveyState.updateUserId(userId);
|
||||
}
|
||||
|
||||
responseQueue.updateSurveyState(surveyState);
|
||||
responseQueue.add({
|
||||
@@ -198,20 +190,20 @@ const renderWidget = async (
|
||||
});
|
||||
|
||||
if (isNewResponse) {
|
||||
const responses = appConfig.get().personState.data.responses;
|
||||
const responses = config.get().personState.data.responses;
|
||||
const newPersonState: TJsPersonState = {
|
||||
...appConfig.get().personState,
|
||||
...config.get().personState,
|
||||
data: {
|
||||
...appConfig.get().personState.data,
|
||||
...config.get().personState.data,
|
||||
responses: [...responses, surveyState.surveyId],
|
||||
},
|
||||
};
|
||||
|
||||
const filteredSurveys = filterSurveys(appConfig.get().environmentState, newPersonState);
|
||||
const filteredSurveys = filterSurveys(config.get().environmentState, newPersonState);
|
||||
|
||||
appConfig.update({
|
||||
...appConfig.get(),
|
||||
environmentState: appConfig.get().environmentState,
|
||||
config.update({
|
||||
...config.get(),
|
||||
environmentState: config.get().environmentState,
|
||||
personState: newPersonState,
|
||||
filteredSurveys,
|
||||
});
|
||||
@@ -220,8 +212,8 @@ const renderWidget = async (
|
||||
onClose: closeSurvey,
|
||||
onFileUpload: async (file: TJsFileUploadParams["file"], params: TUploadFileConfig) => {
|
||||
const api = new FormbricksAPI({
|
||||
apiHost: appConfig.get().apiHost,
|
||||
environmentId: appConfig.get().environmentId,
|
||||
apiHost: config.get().apiHost,
|
||||
environmentId: config.get().environmentId,
|
||||
});
|
||||
|
||||
return await api.client.storage.uploadFile(
|
||||
@@ -247,11 +239,11 @@ export const closeSurvey = async (): Promise<void> => {
|
||||
removeWidgetContainer();
|
||||
addWidgetContainer();
|
||||
|
||||
const { environmentState, personState } = appConfig.get();
|
||||
const { environmentState, personState } = config.get();
|
||||
const filteredSurveys = filterSurveys(environmentState, personState);
|
||||
|
||||
appConfig.update({
|
||||
...appConfig.get(),
|
||||
config.update({
|
||||
...config.get(),
|
||||
environmentState,
|
||||
personState,
|
||||
filteredSurveys,
|
||||
@@ -262,12 +254,12 @@ export const closeSurvey = async (): Promise<void> => {
|
||||
|
||||
export const addWidgetContainer = (): void => {
|
||||
const containerElement = document.createElement("div");
|
||||
containerElement.id = containerId;
|
||||
containerElement.id = CONTAINER_ID;
|
||||
document.body.appendChild(containerElement);
|
||||
};
|
||||
|
||||
export const removeWidgetContainer = (): void => {
|
||||
document.getElementById(containerId)?.remove();
|
||||
document.getElementById(CONTAINER_ID)?.remove();
|
||||
};
|
||||
|
||||
const loadFormbricksSurveysExternally = (): Promise<typeof window.formbricksSurveys> => {
|
||||
@@ -276,7 +268,7 @@ const loadFormbricksSurveysExternally = (): Promise<typeof window.formbricksSurv
|
||||
resolve(window.formbricksSurveys);
|
||||
} else {
|
||||
const script = document.createElement("script");
|
||||
script.src = `${appConfig.get().apiHost}/api/packages/surveys`;
|
||||
script.src = `${config.get().apiHost}/api/packages/surveys`;
|
||||
script.async = true;
|
||||
script.onload = () => resolve(window.formbricksSurveys);
|
||||
script.onerror = (error) => {
|
||||
@@ -1,3 +0,0 @@
|
||||
export const APP_SURVEYS_LOCAL_STORAGE_KEY = "formbricks-js-app";
|
||||
export const RN_ASYNC_STORAGE_KEY = "formbricks-react-native";
|
||||
export const WEBSITE_SURVEYS_LOCAL_STORAGE_KEY = "formbricks-js-website";
|
||||
@@ -1,46 +0,0 @@
|
||||
import { TJsTrackProperties, TJsWebsiteConfigInput } from "@formbricks/types/js";
|
||||
// Shared imports
|
||||
import { CommandQueue } from "../shared/commandQueue";
|
||||
import { ErrorHandler } from "../shared/errors";
|
||||
import { Logger } from "../shared/logger";
|
||||
// Website package specific imports
|
||||
import { trackCodeAction } from "./lib/actions";
|
||||
import { resetConfig } from "./lib/common";
|
||||
import { initialize } from "./lib/initialize";
|
||||
import { checkPageUrl } from "./lib/noCodeActions";
|
||||
|
||||
const logger = Logger.getInstance();
|
||||
|
||||
logger.debug("Create command queue");
|
||||
const queue = new CommandQueue();
|
||||
|
||||
const init = async (initConfig: TJsWebsiteConfigInput) => {
|
||||
ErrorHandler.init(initConfig.errorHandler);
|
||||
queue.add(false, "website", initialize, initConfig);
|
||||
await queue.wait();
|
||||
};
|
||||
|
||||
const reset = async (): Promise<void> => {
|
||||
queue.add(true, "website", resetConfig);
|
||||
await queue.wait();
|
||||
};
|
||||
|
||||
const track = async (name: string, properties?: TJsTrackProperties): Promise<void> => {
|
||||
queue.add<any>(true, "website", trackCodeAction, name, properties);
|
||||
await queue.wait();
|
||||
};
|
||||
|
||||
const registerRouteChange = async (): Promise<void> => {
|
||||
queue.add(true, "website", checkPageUrl);
|
||||
await queue.wait();
|
||||
};
|
||||
|
||||
const formbricks = {
|
||||
init,
|
||||
track,
|
||||
reset,
|
||||
registerRouteChange,
|
||||
};
|
||||
|
||||
export type TFormbricksWebsite = typeof formbricks;
|
||||
export default formbricks as TFormbricksWebsite;
|
||||
@@ -1,57 +0,0 @@
|
||||
import { TJsTrackProperties } from "@formbricks/types/js";
|
||||
import { InvalidCodeError, NetworkError, Result, err, okVoid } from "../../shared/errors";
|
||||
import { Logger } from "../../shared/logger";
|
||||
import { WebsiteConfig } from "./config";
|
||||
import { triggerSurvey } from "./widget";
|
||||
|
||||
const logger = Logger.getInstance();
|
||||
const websiteConfig = WebsiteConfig.getInstance();
|
||||
|
||||
export const trackAction = async (
|
||||
name: string,
|
||||
alias?: string,
|
||||
properties?: TJsTrackProperties
|
||||
): Promise<Result<void, NetworkError>> => {
|
||||
const aliasName = alias || name;
|
||||
logger.debug(`Formbricks: Action "${aliasName}" tracked`);
|
||||
|
||||
// get a list of surveys that are collecting insights
|
||||
const activeSurveys = websiteConfig.get().filteredSurveys;
|
||||
|
||||
if (!!activeSurveys && activeSurveys.length > 0) {
|
||||
for (const survey of activeSurveys) {
|
||||
for (const trigger of survey.triggers) {
|
||||
if (trigger.actionClass.name === name) {
|
||||
await triggerSurvey(survey, name, properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.debug("No active surveys to display");
|
||||
}
|
||||
|
||||
return okVoid();
|
||||
};
|
||||
|
||||
export const trackCodeAction = (
|
||||
code: string,
|
||||
properties?: TJsTrackProperties
|
||||
): Promise<Result<void, NetworkError>> | Result<void, InvalidCodeError> => {
|
||||
const actionClasses = websiteConfig.get().environmentState.data.actionClasses;
|
||||
|
||||
const codeActionClasses = actionClasses.filter((action) => action.type === "code");
|
||||
const action = codeActionClasses.find((action) => action.key === code);
|
||||
|
||||
if (!action) {
|
||||
return err({
|
||||
code: "invalid_code",
|
||||
message: `${code} action unknown. Please add this action in Formbricks first in order to use it in your code.`,
|
||||
});
|
||||
}
|
||||
|
||||
return trackAction(action.name, code, properties);
|
||||
};
|
||||
|
||||
export const trackNoCodeAction = (name: string): Promise<Result<void, NetworkError>> => {
|
||||
return trackAction(name);
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
import { NetworkError, Result, err, okVoid } from "../../shared/errors";
|
||||
import { Logger } from "../../shared/logger";
|
||||
import { WebsiteConfig } from "./config";
|
||||
import { deinitalize, initialize } from "./initialize";
|
||||
import { closeSurvey } from "./widget";
|
||||
|
||||
const websiteConfig = WebsiteConfig.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
|
||||
export const resetWebsiteConfig = async (): Promise<void> => {
|
||||
deinitalize();
|
||||
websiteConfig.resetConfig();
|
||||
};
|
||||
|
||||
export const resetConfig = async (): Promise<Result<void, NetworkError>> => {
|
||||
logger.debug("Resetting state & getting new state from backend");
|
||||
closeSurvey();
|
||||
|
||||
const syncParams = {
|
||||
environmentId: websiteConfig.get().environmentId,
|
||||
apiHost: websiteConfig.get().apiHost,
|
||||
};
|
||||
|
||||
await resetWebsiteConfig();
|
||||
try {
|
||||
await initialize(syncParams);
|
||||
return okVoid();
|
||||
} catch (e) {
|
||||
return err(e as NetworkError);
|
||||
}
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
import { TJsConfig, TJsConfigUpdateInput } from "@formbricks/types/js";
|
||||
import { WEBSITE_SURVEYS_LOCAL_STORAGE_KEY } from "../../shared/constants";
|
||||
import { Result, err, ok, wrapThrows } from "../../shared/errors";
|
||||
|
||||
export class WebsiteConfig {
|
||||
private static instance: WebsiteConfig | undefined;
|
||||
private config: TJsConfig | null = null;
|
||||
|
||||
private constructor() {
|
||||
const localConfig = this.loadFromLocalStorage();
|
||||
|
||||
if (localConfig.ok) {
|
||||
this.config = localConfig.value;
|
||||
}
|
||||
}
|
||||
|
||||
static getInstance(): WebsiteConfig {
|
||||
if (!WebsiteConfig.instance) {
|
||||
WebsiteConfig.instance = new WebsiteConfig();
|
||||
}
|
||||
return WebsiteConfig.instance;
|
||||
}
|
||||
|
||||
public update(newConfig: TJsConfigUpdateInput): void {
|
||||
if (newConfig) {
|
||||
this.config = {
|
||||
...this.config,
|
||||
...newConfig,
|
||||
status: {
|
||||
value: newConfig.status?.value || "success",
|
||||
expiresAt: newConfig.status?.expiresAt || null,
|
||||
},
|
||||
};
|
||||
|
||||
this.saveToLocalStorage();
|
||||
}
|
||||
}
|
||||
|
||||
public get(): TJsConfig {
|
||||
if (!this.config) {
|
||||
throw new Error("config is null, maybe the init function was not called?");
|
||||
}
|
||||
return this.config;
|
||||
}
|
||||
|
||||
public loadFromLocalStorage(): Result<TJsConfig, Error> {
|
||||
if (typeof window !== "undefined") {
|
||||
const savedConfig = localStorage.getItem(WEBSITE_SURVEYS_LOCAL_STORAGE_KEY);
|
||||
if (savedConfig) {
|
||||
// TODO: validate config
|
||||
// This is a hack to get around the fact that we don't have a proper
|
||||
// way to validate the config yet.
|
||||
const parsedConfig = JSON.parse(savedConfig) as TJsConfig;
|
||||
|
||||
// check if the config has expired
|
||||
|
||||
// TODO: Figure out the expiration logic
|
||||
if (
|
||||
parsedConfig.environmentState &&
|
||||
parsedConfig.environmentState.expiresAt &&
|
||||
new Date(parsedConfig.environmentState.expiresAt) <= new Date()
|
||||
) {
|
||||
return err(new Error("Config in local storage has expired"));
|
||||
}
|
||||
|
||||
return ok(parsedConfig);
|
||||
}
|
||||
}
|
||||
|
||||
return err(new Error("No or invalid config in local storage"));
|
||||
}
|
||||
|
||||
private saveToLocalStorage(): Result<void, Error> {
|
||||
return wrapThrows(() =>
|
||||
localStorage.setItem(WEBSITE_SURVEYS_LOCAL_STORAGE_KEY, JSON.stringify(this.config))
|
||||
)();
|
||||
}
|
||||
|
||||
// reset the config
|
||||
|
||||
public resetConfig(): Result<void, Error> {
|
||||
this.config = null;
|
||||
|
||||
return wrapThrows(() => localStorage.removeItem(WEBSITE_SURVEYS_LOCAL_STORAGE_KEY))();
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import {
|
||||
addEnvironmentStateExpiryCheckListener,
|
||||
clearEnvironmentStateExpiryCheckListener,
|
||||
} from "../../shared/environmentState";
|
||||
import {
|
||||
addClickEventListener,
|
||||
addExitIntentListener,
|
||||
addPageUrlEventListeners,
|
||||
addScrollDepthListener,
|
||||
removeClickEventListener,
|
||||
removeExitIntentListener,
|
||||
removePageUrlEventListeners,
|
||||
removeScrollDepthListener,
|
||||
} from "../lib/noCodeActions";
|
||||
import { WebsiteConfig } from "./config";
|
||||
|
||||
let areRemoveEventListenersAdded = false;
|
||||
|
||||
export const addEventListeners = (config: WebsiteConfig): void => {
|
||||
addEnvironmentStateExpiryCheckListener("website", config);
|
||||
clearEnvironmentStateExpiryCheckListener();
|
||||
addPageUrlEventListeners();
|
||||
addClickEventListener();
|
||||
addExitIntentListener();
|
||||
addScrollDepthListener();
|
||||
};
|
||||
|
||||
export const addCleanupEventListeners = (): void => {
|
||||
if (areRemoveEventListenersAdded) return;
|
||||
window.addEventListener("beforeunload", () => {
|
||||
clearEnvironmentStateExpiryCheckListener();
|
||||
removePageUrlEventListeners();
|
||||
removeClickEventListener();
|
||||
removeExitIntentListener();
|
||||
removeScrollDepthListener();
|
||||
});
|
||||
areRemoveEventListenersAdded = true;
|
||||
};
|
||||
|
||||
export const removeCleanupEventListeners = (): void => {
|
||||
if (!areRemoveEventListenersAdded) return;
|
||||
window.removeEventListener("beforeunload", () => {
|
||||
clearEnvironmentStateExpiryCheckListener();
|
||||
removePageUrlEventListeners();
|
||||
removeClickEventListener();
|
||||
removeExitIntentListener();
|
||||
removeScrollDepthListener();
|
||||
});
|
||||
areRemoveEventListenersAdded = false;
|
||||
};
|
||||
|
||||
export const removeAllEventListeners = (): void => {
|
||||
clearEnvironmentStateExpiryCheckListener();
|
||||
removePageUrlEventListeners();
|
||||
removeClickEventListener();
|
||||
removeExitIntentListener();
|
||||
removeScrollDepthListener();
|
||||
removeCleanupEventListeners();
|
||||
};
|
||||
@@ -1,343 +0,0 @@
|
||||
import type { TJsConfig, TJsWebsiteConfigInput, TJsWebsiteState } from "@formbricks/types/js";
|
||||
import { WEBSITE_SURVEYS_LOCAL_STORAGE_KEY } from "../../shared/constants";
|
||||
import { fetchEnvironmentState } from "../../shared/environmentState";
|
||||
import {
|
||||
ErrorHandler,
|
||||
MissingFieldError,
|
||||
MissingPersonError,
|
||||
NetworkError,
|
||||
NotInitializedError,
|
||||
Result,
|
||||
err,
|
||||
okVoid,
|
||||
wrapThrows,
|
||||
} from "../../shared/errors";
|
||||
import { Logger } from "../../shared/logger";
|
||||
import { DEFAULT_PERSON_STATE_WEBSITE } from "../../shared/personState";
|
||||
import { getIsDebug } from "../../shared/utils";
|
||||
import { filterSurveys as filterPublicSurveys } from "../../shared/utils";
|
||||
import { trackNoCodeAction } from "./actions";
|
||||
import { WebsiteConfig } from "./config";
|
||||
import { addCleanupEventListeners, addEventListeners, removeAllEventListeners } from "./eventListeners";
|
||||
import { checkPageUrl } from "./noCodeActions";
|
||||
import { addWidgetContainer, removeWidgetContainer, setIsSurveyRunning } from "./widget";
|
||||
|
||||
const logger = Logger.getInstance();
|
||||
|
||||
let isInitialized = false;
|
||||
|
||||
export const setIsInitialized = (value: boolean) => {
|
||||
isInitialized = value;
|
||||
};
|
||||
|
||||
const migrateLocalStorage = (): { changed: boolean; newState?: TJsConfig } => {
|
||||
const oldConfig = localStorage.getItem(WEBSITE_SURVEYS_LOCAL_STORAGE_KEY);
|
||||
|
||||
let newWebsiteConfig: TJsConfig;
|
||||
if (oldConfig) {
|
||||
const parsedOldConfig = JSON.parse(oldConfig);
|
||||
// if the old config follows the older structure, we need to migrate it
|
||||
if (parsedOldConfig.state || parsedOldConfig.expiresAt) {
|
||||
logger.debug("Migrating local storage");
|
||||
const { apiHost, environmentId, state, expiresAt } = parsedOldConfig as {
|
||||
apiHost: string;
|
||||
environmentId: string;
|
||||
state: TJsWebsiteState;
|
||||
expiresAt: Date;
|
||||
};
|
||||
const { displays: displaysState, actionClasses, product, surveys, attributes } = state;
|
||||
|
||||
const responses = displaysState
|
||||
.filter((display) => display.responded)
|
||||
.map((display) => display.surveyId);
|
||||
|
||||
const displays = displaysState.map((display) => ({
|
||||
surveyId: display.surveyId,
|
||||
createdAt: display.createdAt,
|
||||
}));
|
||||
const lastDisplayAt = displaysState
|
||||
? displaysState.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0]
|
||||
.createdAt
|
||||
: null;
|
||||
|
||||
newWebsiteConfig = {
|
||||
apiHost,
|
||||
environmentId,
|
||||
environmentState: {
|
||||
data: {
|
||||
surveys,
|
||||
actionClasses,
|
||||
product,
|
||||
},
|
||||
expiresAt,
|
||||
},
|
||||
personState: {
|
||||
expiresAt,
|
||||
data: {
|
||||
userId: null,
|
||||
segments: [],
|
||||
displays,
|
||||
responses,
|
||||
attributes: attributes ?? {},
|
||||
lastDisplayAt,
|
||||
},
|
||||
},
|
||||
filteredSurveys: surveys,
|
||||
status: {
|
||||
value: "success",
|
||||
expiresAt: null,
|
||||
},
|
||||
};
|
||||
|
||||
logger.debug("Migrated local storage to new format");
|
||||
|
||||
return { changed: true, newState: newWebsiteConfig };
|
||||
}
|
||||
|
||||
return { changed: false };
|
||||
}
|
||||
|
||||
return { changed: false };
|
||||
};
|
||||
|
||||
export const initialize = async (
|
||||
configInput: TJsWebsiteConfigInput
|
||||
): Promise<Result<void, MissingFieldError | NetworkError | MissingPersonError>> => {
|
||||
const isDebug = getIsDebug();
|
||||
if (isDebug) {
|
||||
logger.configure({ logLevel: "debug" });
|
||||
}
|
||||
|
||||
const { changed, newState } = migrateLocalStorage();
|
||||
let websiteConfig = WebsiteConfig.getInstance();
|
||||
|
||||
// If the state was changed due to migration, reset and reinitialize the configuration
|
||||
if (changed && newState) {
|
||||
// The state exists in the local storage, so this should not fail
|
||||
websiteConfig.resetConfig(); // Reset the configuration
|
||||
|
||||
// Re-fetch a new instance of WebsiteConfig after resetting
|
||||
websiteConfig = WebsiteConfig.getInstance();
|
||||
|
||||
// Update the new instance with the migrated state
|
||||
websiteConfig.update(newState);
|
||||
}
|
||||
|
||||
if (isInitialized) {
|
||||
logger.debug("Already initialized, skipping initialization.");
|
||||
return okVoid();
|
||||
}
|
||||
|
||||
let existingConfig: TJsConfig | undefined;
|
||||
try {
|
||||
existingConfig = websiteConfig.get();
|
||||
logger.debug("Found existing configuration.");
|
||||
} catch (e) {
|
||||
logger.debug("No existing configuration found.");
|
||||
}
|
||||
|
||||
// formbricks is in error state, skip initialization
|
||||
if (existingConfig?.status?.value === "error") {
|
||||
if (isDebug) {
|
||||
logger.debug(
|
||||
"Formbricks is in error state, but debug mode is active. Resetting config and continuing."
|
||||
);
|
||||
websiteConfig.resetConfig();
|
||||
return okVoid();
|
||||
}
|
||||
|
||||
logger.debug("Formbricks was set to an error state.");
|
||||
|
||||
if (existingConfig?.status?.expiresAt && new Date(existingConfig?.status?.expiresAt) > new Date()) {
|
||||
logger.debug("Error state is not expired, skipping initialization");
|
||||
return okVoid();
|
||||
} else {
|
||||
logger.debug("Error state is expired. Continue with initialization.");
|
||||
}
|
||||
}
|
||||
|
||||
ErrorHandler.getInstance().printStatus();
|
||||
|
||||
logger.debug("Start initialize");
|
||||
|
||||
if (!configInput.environmentId) {
|
||||
logger.debug("No environmentId provided");
|
||||
return err({
|
||||
code: "missing_field",
|
||||
field: "environmentId",
|
||||
});
|
||||
}
|
||||
|
||||
if (!configInput.apiHost) {
|
||||
logger.debug("No apiHost provided");
|
||||
|
||||
return err({
|
||||
code: "missing_field",
|
||||
field: "apiHost",
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug("Adding widget container to DOM");
|
||||
addWidgetContainer();
|
||||
|
||||
if (
|
||||
existingConfig &&
|
||||
existingConfig.environmentId === configInput.environmentId &&
|
||||
existingConfig.apiHost === configInput.apiHost &&
|
||||
existingConfig.environmentState
|
||||
) {
|
||||
logger.debug("Configuration fits init parameters.");
|
||||
if (existingConfig.environmentState.expiresAt < new Date()) {
|
||||
logger.debug("Configuration expired.");
|
||||
|
||||
try {
|
||||
// fetch the environment state
|
||||
|
||||
const environmentState = await fetchEnvironmentState(
|
||||
{
|
||||
apiHost: configInput.apiHost,
|
||||
environmentId: configInput.environmentId,
|
||||
},
|
||||
"website"
|
||||
);
|
||||
|
||||
// filter the surveys with the default person state
|
||||
|
||||
const filteredSurveys = filterPublicSurveys(
|
||||
environmentState,
|
||||
DEFAULT_PERSON_STATE_WEBSITE,
|
||||
"website"
|
||||
);
|
||||
|
||||
websiteConfig.update({
|
||||
apiHost: configInput.apiHost,
|
||||
environmentId: configInput.environmentId,
|
||||
environmentState,
|
||||
personState: DEFAULT_PERSON_STATE_WEBSITE,
|
||||
filteredSurveys,
|
||||
});
|
||||
} catch (e) {
|
||||
putFormbricksInErrorState(websiteConfig);
|
||||
}
|
||||
} else {
|
||||
logger.debug("Configuration not expired. Extending expiration.");
|
||||
websiteConfig.update(existingConfig);
|
||||
}
|
||||
} else {
|
||||
logger.debug(
|
||||
"No valid configuration found or it has been expired. Resetting config and creating new one."
|
||||
);
|
||||
websiteConfig.resetConfig();
|
||||
logger.debug("Syncing.");
|
||||
|
||||
try {
|
||||
const environmentState = await fetchEnvironmentState(
|
||||
{
|
||||
apiHost: configInput.apiHost,
|
||||
environmentId: configInput.environmentId,
|
||||
},
|
||||
"website"
|
||||
);
|
||||
|
||||
const filteredSurveys = filterPublicSurveys(environmentState, DEFAULT_PERSON_STATE_WEBSITE, "website");
|
||||
|
||||
websiteConfig.update({
|
||||
apiHost: configInput.apiHost,
|
||||
environmentId: configInput.environmentId,
|
||||
environmentState,
|
||||
personState: DEFAULT_PERSON_STATE_WEBSITE,
|
||||
filteredSurveys,
|
||||
});
|
||||
} catch (e) {
|
||||
handleErrorOnFirstInit();
|
||||
}
|
||||
|
||||
if (configInput.attributes) {
|
||||
const currentWebsiteConfig = websiteConfig.get();
|
||||
|
||||
websiteConfig.update({
|
||||
...currentWebsiteConfig,
|
||||
personState: {
|
||||
...currentWebsiteConfig.personState,
|
||||
data: {
|
||||
...currentWebsiteConfig.personState.data,
|
||||
attributes: { ...currentWebsiteConfig.personState.data.attributes, ...configInput.attributes },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// and track the new session event
|
||||
await trackNoCodeAction("New Session");
|
||||
}
|
||||
|
||||
logger.debug("Adding event listeners");
|
||||
addEventListeners(websiteConfig);
|
||||
addCleanupEventListeners();
|
||||
|
||||
setIsInitialized(true);
|
||||
logger.debug("Initialized");
|
||||
|
||||
// check page url if initialized after page load
|
||||
|
||||
checkPageUrl();
|
||||
return okVoid();
|
||||
};
|
||||
|
||||
export const handleErrorOnFirstInit = () => {
|
||||
if (getIsDebug()) {
|
||||
logger.debug("Not putting formbricks in error state because debug mode is active (no error state)");
|
||||
return;
|
||||
}
|
||||
|
||||
const initialErrorConfig: Partial<TJsConfig> = {
|
||||
status: {
|
||||
value: "error",
|
||||
expiresAt: new Date(new Date().getTime() + 10 * 60000), // 10 minutes in the future
|
||||
},
|
||||
};
|
||||
|
||||
// can't use config.update here because the config is not yet initialized
|
||||
wrapThrows(() =>
|
||||
localStorage.setItem(WEBSITE_SURVEYS_LOCAL_STORAGE_KEY, JSON.stringify(initialErrorConfig))
|
||||
)();
|
||||
throw new Error("Could not initialize formbricks");
|
||||
};
|
||||
|
||||
export const checkInitialized = (): Result<void, NotInitializedError> => {
|
||||
logger.debug("Check if initialized");
|
||||
if (!isInitialized || !ErrorHandler.initialized) {
|
||||
return err({
|
||||
code: "not_initialized",
|
||||
message: "Formbricks not initialized. Call initialize() first.",
|
||||
});
|
||||
}
|
||||
|
||||
return okVoid();
|
||||
};
|
||||
|
||||
export const deinitalize = (): void => {
|
||||
logger.debug("Deinitializing");
|
||||
removeWidgetContainer();
|
||||
setIsSurveyRunning(false);
|
||||
removeAllEventListeners();
|
||||
setIsInitialized(false);
|
||||
};
|
||||
|
||||
export const putFormbricksInErrorState = (websiteConfig: WebsiteConfig): void => {
|
||||
if (getIsDebug()) {
|
||||
logger.debug("Not putting formbricks in error state because debug mode is active (no error state)");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Putting formbricks in error state");
|
||||
// change formbricks status to error
|
||||
websiteConfig.update({
|
||||
...websiteConfig.get(),
|
||||
status: {
|
||||
value: "error",
|
||||
expiresAt: new Date(new Date().getTime() + 10 * 60000), // 10 minutes in the future
|
||||
},
|
||||
});
|
||||
deinitalize();
|
||||
};
|
||||
@@ -1,189 +0,0 @@
|
||||
import type { TActionClass } from "@formbricks/types/action-classes";
|
||||
import { ErrorHandler, NetworkError, Result, err, match, okVoid } from "../../shared/errors";
|
||||
import { Logger } from "../../shared/logger";
|
||||
import { evaluateNoCodeConfigClick, handleUrlFilters } from "../../shared/utils";
|
||||
import { trackNoCodeAction } from "./actions";
|
||||
import { WebsiteConfig } from "./config";
|
||||
|
||||
const websiteConfig = WebsiteConfig.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
const errorHandler = ErrorHandler.getInstance();
|
||||
|
||||
// Event types for various listeners
|
||||
const events = ["hashchange", "popstate", "pushstate", "replacestate", "load"];
|
||||
|
||||
// Page URL Event Handlers
|
||||
let arePageUrlEventListenersAdded = false;
|
||||
|
||||
export const checkPageUrl = async (): Promise<Result<void, NetworkError>> => {
|
||||
logger.debug(`Checking page url: ${window.location.href}`);
|
||||
const actionClasses = websiteConfig.get().environmentState.data.actionClasses;
|
||||
|
||||
const noCodePageViewActionClasses = actionClasses.filter(
|
||||
(action) => action.type === "noCode" && action.noCodeConfig?.type === "pageView"
|
||||
);
|
||||
|
||||
for (const event of noCodePageViewActionClasses) {
|
||||
const urlFilters = event.noCodeConfig?.urlFilters ?? [];
|
||||
const isValidUrl = handleUrlFilters(urlFilters);
|
||||
|
||||
if (!isValidUrl) continue;
|
||||
|
||||
const trackResult = await trackNoCodeAction(event.name);
|
||||
if (trackResult.ok !== true) return err(trackResult.error);
|
||||
}
|
||||
|
||||
return okVoid();
|
||||
};
|
||||
|
||||
const checkPageUrlWrapper = () => checkPageUrl();
|
||||
|
||||
export const addPageUrlEventListeners = (): void => {
|
||||
if (typeof window === "undefined" || arePageUrlEventListenersAdded) return;
|
||||
events.forEach((event) => window.addEventListener(event, checkPageUrlWrapper));
|
||||
arePageUrlEventListenersAdded = true;
|
||||
};
|
||||
|
||||
export const removePageUrlEventListeners = (): void => {
|
||||
if (typeof window === "undefined" || !arePageUrlEventListenersAdded) return;
|
||||
events.forEach((event) => window.removeEventListener(event, checkPageUrlWrapper));
|
||||
arePageUrlEventListenersAdded = false;
|
||||
};
|
||||
|
||||
// Click Event Handlers
|
||||
let isClickEventListenerAdded = false;
|
||||
|
||||
const checkClickMatch = (event: MouseEvent) => {
|
||||
const { environmentState } = websiteConfig.get();
|
||||
if (!environmentState) return;
|
||||
|
||||
const { actionClasses = [] } = environmentState.data;
|
||||
const noCodeClickActionClasses = actionClasses.filter(
|
||||
(action) => action.type === "noCode" && action.noCodeConfig?.type === "click"
|
||||
);
|
||||
|
||||
const targetElement = event.target as HTMLElement;
|
||||
|
||||
noCodeClickActionClasses.forEach((action: TActionClass) => {
|
||||
if (evaluateNoCodeConfigClick(targetElement, action)) {
|
||||
trackNoCodeAction(action.name).then((res) => {
|
||||
match(
|
||||
res,
|
||||
(_value: unknown) => {},
|
||||
(err: any) => errorHandler.handle(err)
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const checkClickMatchWrapper = (e: MouseEvent) => checkClickMatch(e);
|
||||
|
||||
export const addClickEventListener = (): void => {
|
||||
if (typeof window === "undefined" || isClickEventListenerAdded) return;
|
||||
document.addEventListener("click", checkClickMatchWrapper);
|
||||
isClickEventListenerAdded = true;
|
||||
};
|
||||
|
||||
export const removeClickEventListener = (): void => {
|
||||
if (!isClickEventListenerAdded) return;
|
||||
document.removeEventListener("click", checkClickMatchWrapper);
|
||||
isClickEventListenerAdded = false;
|
||||
};
|
||||
|
||||
// Exit Intent Handlers
|
||||
let isExitIntentListenerAdded = false;
|
||||
|
||||
const checkExitIntent = async (e: MouseEvent) => {
|
||||
const actionClasses = websiteConfig.get().environmentState.data.actionClasses;
|
||||
|
||||
const noCodeExitIntentActionClasses = actionClasses.filter(
|
||||
(action) => action.type === "noCode" && action.noCodeConfig?.type === "exitIntent"
|
||||
);
|
||||
|
||||
if (e.clientY <= 0 && noCodeExitIntentActionClasses.length > 0) {
|
||||
for (const event of noCodeExitIntentActionClasses) {
|
||||
const urlFilters = event.noCodeConfig?.urlFilters ?? [];
|
||||
const isValidUrl = handleUrlFilters(urlFilters);
|
||||
|
||||
if (!isValidUrl) continue;
|
||||
|
||||
const trackResult = await trackNoCodeAction(event.name);
|
||||
if (trackResult.ok !== true) return err(trackResult.error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const checkExitIntentWrapper = (e: MouseEvent) => checkExitIntent(e);
|
||||
|
||||
export const addExitIntentListener = (): void => {
|
||||
if (typeof document !== "undefined" && !isExitIntentListenerAdded) {
|
||||
document.querySelector("body")!.addEventListener("mouseleave", checkExitIntentWrapper);
|
||||
isExitIntentListenerAdded = true;
|
||||
}
|
||||
};
|
||||
|
||||
export const removeExitIntentListener = (): void => {
|
||||
if (isExitIntentListenerAdded) {
|
||||
document.removeEventListener("mouseleave", checkExitIntentWrapper);
|
||||
isExitIntentListenerAdded = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Scroll Depth Handlers
|
||||
let scrollDepthListenerAdded = false;
|
||||
let scrollDepthTriggered = false;
|
||||
|
||||
const checkScrollDepth = async () => {
|
||||
const scrollPosition = window.scrollY;
|
||||
const windowSize = window.innerHeight;
|
||||
const bodyHeight = document.documentElement.scrollHeight;
|
||||
|
||||
if (scrollPosition === 0) {
|
||||
scrollDepthTriggered = false;
|
||||
}
|
||||
|
||||
if (!scrollDepthTriggered && scrollPosition / (bodyHeight - windowSize) >= 0.5) {
|
||||
scrollDepthTriggered = true;
|
||||
|
||||
const actionClasses = websiteConfig.get().environmentState.data.actionClasses;
|
||||
|
||||
const noCodefiftyPercentScrollActionClasses = actionClasses.filter(
|
||||
(action) => action.type === "noCode" && action.noCodeConfig?.type === "fiftyPercentScroll"
|
||||
);
|
||||
|
||||
for (const event of noCodefiftyPercentScrollActionClasses) {
|
||||
const urlFilters = event.noCodeConfig?.urlFilters ?? [];
|
||||
const isValidUrl = handleUrlFilters(urlFilters);
|
||||
|
||||
if (!isValidUrl) continue;
|
||||
|
||||
const trackResult = await trackNoCodeAction(event.name);
|
||||
if (trackResult.ok !== true) return err(trackResult.error);
|
||||
}
|
||||
}
|
||||
|
||||
return okVoid();
|
||||
};
|
||||
|
||||
const checkScrollDepthWrapper = () => checkScrollDepth();
|
||||
|
||||
export const addScrollDepthListener = (): void => {
|
||||
if (typeof window !== "undefined" && !scrollDepthListenerAdded) {
|
||||
if (document.readyState === "complete") {
|
||||
window.addEventListener("scroll", checkScrollDepthWrapper);
|
||||
} else {
|
||||
window.addEventListener("load", () => {
|
||||
window.addEventListener("scroll", checkScrollDepthWrapper);
|
||||
});
|
||||
}
|
||||
scrollDepthListenerAdded = true;
|
||||
}
|
||||
};
|
||||
|
||||
export const removeScrollDepthListener = (): void => {
|
||||
if (scrollDepthListenerAdded) {
|
||||
window.removeEventListener("scroll", checkScrollDepthWrapper);
|
||||
scrollDepthListenerAdded = false;
|
||||
}
|
||||
};
|
||||
@@ -1,290 +0,0 @@
|
||||
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 { TJsPersonState, TJsTrackProperties } from "@formbricks/types/js";
|
||||
import { TResponseHiddenFieldValue, TResponseUpdate } from "@formbricks/types/responses";
|
||||
import { TUploadFileConfig } from "@formbricks/types/storage";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { Logger } from "../../shared/logger";
|
||||
import { filterSurveys as filterPublicSurveys } from "../../shared/utils";
|
||||
import { getDefaultLanguageCode, getLanguageCode, handleHiddenFields } from "../../shared/utils";
|
||||
import { WebsiteConfig } from "./config";
|
||||
|
||||
const containerId = "formbricks-website-container";
|
||||
|
||||
const websiteConfig = WebsiteConfig.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
|
||||
let isSurveyRunning = false;
|
||||
let setIsError = (_: boolean) => {};
|
||||
let setIsResponseSendingFinished = (_: boolean) => {};
|
||||
|
||||
export const setIsSurveyRunning = (value: boolean) => {
|
||||
isSurveyRunning = value;
|
||||
};
|
||||
|
||||
const shouldDisplayBasedOnPercentage = (displayPercentage: number) => {
|
||||
const randomNum = Math.floor(Math.random() * 10000) / 100;
|
||||
return randomNum <= displayPercentage;
|
||||
};
|
||||
|
||||
export const triggerSurvey = async (
|
||||
survey: TSurvey,
|
||||
action?: string,
|
||||
properties?: TJsTrackProperties
|
||||
): Promise<void> => {
|
||||
// Check if the survey should be displayed based on displayPercentage
|
||||
if (survey.displayPercentage) {
|
||||
const shouldDisplaySurvey = shouldDisplayBasedOnPercentage(survey.displayPercentage);
|
||||
if (!shouldDisplaySurvey) {
|
||||
logger.debug("Survey display skipped based on displayPercentage.");
|
||||
return; // skip displaying the survey
|
||||
}
|
||||
}
|
||||
|
||||
const hiddenFieldsObject: TResponseHiddenFieldValue = handleHiddenFields(
|
||||
survey.hiddenFields,
|
||||
properties?.hiddenFields
|
||||
);
|
||||
|
||||
await renderWidget(survey, action, hiddenFieldsObject);
|
||||
};
|
||||
|
||||
const renderWidget = async (
|
||||
survey: TSurvey,
|
||||
action?: string,
|
||||
hiddenFields: TResponseHiddenFieldValue = {}
|
||||
) => {
|
||||
if (isSurveyRunning) {
|
||||
logger.debug("A survey is already running. Skipping.");
|
||||
return;
|
||||
}
|
||||
setIsSurveyRunning(true);
|
||||
|
||||
if (survey.delay) {
|
||||
logger.debug(`Delaying survey by ${survey.delay} seconds.`);
|
||||
}
|
||||
|
||||
const product = websiteConfig.get().environmentState.data.product;
|
||||
const attributes = websiteConfig.get().personState.data.attributes;
|
||||
|
||||
const isMultiLanguageSurvey = survey.languages.length > 1;
|
||||
let languageCode = "default";
|
||||
|
||||
if (isMultiLanguageSurvey && attributes) {
|
||||
const displayLanguage = getLanguageCode(survey, attributes);
|
||||
//if survey is not available in selected language, survey wont be shown
|
||||
if (!displayLanguage) {
|
||||
logger.debug("Survey not available in specified language.");
|
||||
setIsSurveyRunning(true);
|
||||
return;
|
||||
}
|
||||
languageCode = displayLanguage;
|
||||
}
|
||||
|
||||
const surveyState = new SurveyState(survey.id, null, null);
|
||||
|
||||
const responseQueue = new ResponseQueue(
|
||||
{
|
||||
apiHost: websiteConfig.get().apiHost,
|
||||
environmentId: websiteConfig.get().environmentId,
|
||||
retryAttempts: 2,
|
||||
onResponseSendingFailed: () => {
|
||||
setIsError(true);
|
||||
},
|
||||
onResponseSendingFinished: () => {
|
||||
setIsResponseSendingFinished(true);
|
||||
},
|
||||
},
|
||||
surveyState
|
||||
);
|
||||
const productOverwrites = survey.productOverwrites ?? {};
|
||||
const clickOutside = productOverwrites.clickOutsideClose ?? product.clickOutsideClose;
|
||||
const darkOverlay = productOverwrites.darkOverlay ?? product.darkOverlay;
|
||||
const placement = productOverwrites.placement ?? product.placement;
|
||||
const isBrandingEnabled = product.inAppSurveyBranding;
|
||||
const formbricksSurveys = await loadFormbricksSurveysExternally();
|
||||
|
||||
setTimeout(() => {
|
||||
formbricksSurveys.renderSurveyModal({
|
||||
survey,
|
||||
isBrandingEnabled,
|
||||
clickOutside,
|
||||
darkOverlay,
|
||||
languageCode,
|
||||
placement,
|
||||
styling: getStyling(product, survey),
|
||||
getSetIsError: (f: (value: boolean) => void) => {
|
||||
setIsError = f;
|
||||
},
|
||||
getSetIsResponseSendingFinished: (f: (value: boolean) => void) => {
|
||||
setIsResponseSendingFinished = f;
|
||||
},
|
||||
onDisplay: async () => {
|
||||
const api = new FormbricksAPI({
|
||||
apiHost: websiteConfig.get().apiHost,
|
||||
environmentId: websiteConfig.get().environmentId,
|
||||
});
|
||||
const res = await api.client.display.create({
|
||||
surveyId: survey.id,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Could not create display");
|
||||
}
|
||||
|
||||
const { id } = res.data;
|
||||
|
||||
const existingDisplays = websiteConfig.get().personState.data.displays;
|
||||
const newDisplay = { surveyId: survey.id, createdAt: new Date() };
|
||||
const displays = existingDisplays ? [...existingDisplays, newDisplay] : [newDisplay];
|
||||
const previousConfig = websiteConfig.get();
|
||||
|
||||
const updatedPersonState: TJsPersonState = {
|
||||
...previousConfig.personState,
|
||||
data: {
|
||||
...previousConfig.personState.data,
|
||||
displays,
|
||||
lastDisplayAt: new Date(),
|
||||
},
|
||||
};
|
||||
|
||||
const filteredSurveys = filterPublicSurveys(
|
||||
previousConfig.environmentState,
|
||||
updatedPersonState,
|
||||
"website"
|
||||
);
|
||||
|
||||
websiteConfig.update({
|
||||
...previousConfig,
|
||||
environmentState: previousConfig.environmentState,
|
||||
personState: updatedPersonState,
|
||||
filteredSurveys,
|
||||
});
|
||||
|
||||
surveyState.updateDisplayId(id);
|
||||
responseQueue.updateSurveyState(surveyState);
|
||||
},
|
||||
onResponse: (responseUpdate: TResponseUpdate) => {
|
||||
const displays = websiteConfig.get().personState.data.displays;
|
||||
const lastDisplay = displays && displays[displays.length - 1];
|
||||
if (!lastDisplay) {
|
||||
throw new Error("No lastDisplay found");
|
||||
}
|
||||
|
||||
const isNewResponse = surveyState.responseId === null;
|
||||
|
||||
responseQueue.updateSurveyState(surveyState);
|
||||
|
||||
responseQueue.add({
|
||||
data: responseUpdate.data,
|
||||
ttc: responseUpdate.ttc,
|
||||
finished: responseUpdate.finished,
|
||||
language:
|
||||
responseUpdate.language === "default" ? getDefaultLanguageCode(survey) : responseUpdate.language,
|
||||
meta: {
|
||||
url: window.location.href,
|
||||
action,
|
||||
},
|
||||
variables: responseUpdate.variables,
|
||||
hiddenFields,
|
||||
displayId: surveyState.displayId,
|
||||
});
|
||||
|
||||
if (isNewResponse) {
|
||||
const responses = websiteConfig.get().personState.data.responses;
|
||||
const newPersonState: TJsPersonState = {
|
||||
...websiteConfig.get().personState,
|
||||
data: {
|
||||
...websiteConfig.get().personState.data,
|
||||
responses: [...responses, surveyState.surveyId],
|
||||
},
|
||||
};
|
||||
|
||||
const filteredSurveys = filterPublicSurveys(
|
||||
websiteConfig.get().environmentState,
|
||||
newPersonState,
|
||||
"website"
|
||||
);
|
||||
|
||||
websiteConfig.update({
|
||||
...websiteConfig.get(),
|
||||
environmentState: websiteConfig.get().environmentState,
|
||||
personState: newPersonState,
|
||||
filteredSurveys,
|
||||
});
|
||||
}
|
||||
},
|
||||
onClose: closeSurvey,
|
||||
onFileUpload: async (
|
||||
file: { type: string; name: string; base64: string },
|
||||
params: TUploadFileConfig
|
||||
) => {
|
||||
const api = new FormbricksAPI({
|
||||
apiHost: websiteConfig.get().apiHost,
|
||||
environmentId: websiteConfig.get().environmentId,
|
||||
});
|
||||
|
||||
return await api.client.storage.uploadFile(
|
||||
{
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
base64: file.base64,
|
||||
},
|
||||
params
|
||||
);
|
||||
},
|
||||
onRetry: () => {
|
||||
setIsError(false);
|
||||
responseQueue.processQueue();
|
||||
},
|
||||
hiddenFieldsRecord: hiddenFields,
|
||||
});
|
||||
}, survey.delay * 1000);
|
||||
};
|
||||
|
||||
export const closeSurvey = async (): Promise<void> => {
|
||||
// remove container element from DOM
|
||||
removeWidgetContainer();
|
||||
addWidgetContainer();
|
||||
|
||||
const { environmentState, personState } = websiteConfig.get();
|
||||
const filteredSurveys = filterPublicSurveys(environmentState, personState, "website");
|
||||
websiteConfig.update({
|
||||
...websiteConfig.get(),
|
||||
environmentState,
|
||||
personState,
|
||||
filteredSurveys,
|
||||
});
|
||||
setIsSurveyRunning(false);
|
||||
return;
|
||||
};
|
||||
|
||||
export const addWidgetContainer = (): void => {
|
||||
const containerElement = document.createElement("div");
|
||||
containerElement.id = containerId;
|
||||
document.body.appendChild(containerElement);
|
||||
};
|
||||
|
||||
export const removeWidgetContainer = (): void => {
|
||||
document.getElementById(containerId)?.remove();
|
||||
};
|
||||
|
||||
const loadFormbricksSurveysExternally = (): Promise<typeof window.formbricksSurveys> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (window.formbricksSurveys) {
|
||||
resolve(window.formbricksSurveys);
|
||||
} else {
|
||||
const script = document.createElement("script");
|
||||
script.src = `${websiteConfig.get().apiHost}/api/packages/surveys`;
|
||||
script.async = true;
|
||||
script.onload = () => resolve(window.formbricksSurveys);
|
||||
script.onerror = (error) => {
|
||||
console.error("Failed to load Formbricks Surveys library:", error);
|
||||
reject(error);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -16,10 +16,10 @@ const config = () => {
|
||||
minify: "terser",
|
||||
sourcemap: true,
|
||||
lib: {
|
||||
entry: resolve(__dirname, "src/app/index.ts"),
|
||||
entry: resolve(__dirname, "src/index.ts"),
|
||||
name: "formbricks",
|
||||
formats: ["umd"],
|
||||
fileName: "app",
|
||||
fileName: "index",
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
@@ -1,34 +0,0 @@
|
||||
import { resolve } from "path";
|
||||
import { defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
import webPackageJson from "../../apps/web/package.json";
|
||||
|
||||
const config = () => {
|
||||
return defineConfig({
|
||||
define: {
|
||||
"import.meta.env.VERSION": JSON.stringify(webPackageJson.version),
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: { inlineDynamicImports: true },
|
||||
},
|
||||
emptyOutDir: false, // keep the dist folder to avoid errors with pnpm go when folder is empty during build
|
||||
minify: "terser",
|
||||
sourcemap: true,
|
||||
lib: {
|
||||
entry: resolve(__dirname, "src/website/index.ts"),
|
||||
name: "formbricks",
|
||||
formats: ["umd"],
|
||||
fileName: "website",
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
dts({
|
||||
rollupTypes: true,
|
||||
bundledPackages: ["@formbricks/api", "@formbricks/types"],
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -2,12 +2,12 @@
|
||||
<script type="text/javascript">
|
||||
!(function () {
|
||||
var t = document.createElement("script");
|
||||
(t.type = "text/javascript"), (t.async = !0), (t.src = "http://localhost:3000/api/packages/app");
|
||||
(t.type = "text/javascript"), (t.async = !0), (t.src = "http://localhost:3000/api/packages/js");
|
||||
var e = document.getElementsByTagName("script")[0];
|
||||
e.parentNode.insertBefore(t, e),
|
||||
setTimeout(function () {
|
||||
formbricks.init({
|
||||
environmentId: "cm1qbbvo8000c5ij3dt7qmyn6",
|
||||
environmentId: "cm20dunwt0005a9yhllps6eom",
|
||||
userId: "RANDOM_USER_ID",
|
||||
apiHost: "http://localhost:3000",
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "@formbricks/js",
|
||||
"license": "MIT",
|
||||
"version": "2.2.0",
|
||||
"description": "Formbricks-js allows you to connect your app to Formbricks, display surveys and trigger events.",
|
||||
"version": "3.0.0",
|
||||
"description": "Formbricks-js allows you to connect your index to Formbricks, display surveys and trigger events.",
|
||||
"homepage": "https://formbricks.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -19,25 +19,14 @@
|
||||
],
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./app": {
|
||||
"import": "./dist/app.js",
|
||||
"require": "./dist/app.cjs",
|
||||
"types": "./dist/app.d.ts"
|
||||
},
|
||||
"./website": {
|
||||
"import": "./dist/website.js",
|
||||
"require": "./dist/website.cjs",
|
||||
"types": "./dist/website.d.ts"
|
||||
},
|
||||
"./*": "./dist/*"
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"app": [
|
||||
"./dist/app.d.ts"
|
||||
],
|
||||
"website": [
|
||||
"./dist/website.d.ts"
|
||||
"*": [
|
||||
"./dist/index.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment --
|
||||
* Required because it doesn't work without building otherwise
|
||||
*/
|
||||
import { type TFormbricksApp } from "@formbricks/js-core/app";
|
||||
import { type TFormbricksWebsite } from "@formbricks/js-core/website";
|
||||
import { loadFormbricksToProxy } from "./shared/load-formbricks";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
formbricks: TFormbricksApp | TFormbricksWebsite | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const formbricksProxyHandler: ProxyHandler<TFormbricksApp> = {
|
||||
get(_target, prop, _receiver) {
|
||||
return (...args: unknown[]) => loadFormbricksToProxy(prop as string, "app", ...args);
|
||||
},
|
||||
};
|
||||
|
||||
const formbricksApp: TFormbricksApp = new Proxy({} as TFormbricksApp, formbricksProxyHandler);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export -- Required for UMD
|
||||
export default formbricksApp;
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
import { type TFormbricksApp } from "@formbricks/js-core";
|
||||
import { loadFormbricksToProxy } from "./lib/load-formbricks";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
formbricks: TFormbricksApp | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const formbricksProxyHandler: ProxyHandler<TFormbricksApp> = {
|
||||
get(_target, prop, _receiver) {
|
||||
return (...args: unknown[]) => loadFormbricksToProxy(prop as string, ...args);
|
||||
},
|
||||
};
|
||||
|
||||
const formbricksApp: TFormbricksApp = new Proxy({} as TFormbricksApp, formbricksProxyHandler);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export -- Required for UMD
|
||||
export default formbricksApp;
|
||||
@@ -8,13 +8,13 @@ let isInitializing = false;
|
||||
let isInitialized = false;
|
||||
|
||||
// Load the SDK, return the result
|
||||
const loadFormbricksSDK = async (apiHostParam: string, sdkType: "app" | "website"): Promise<Result<void>> => {
|
||||
const loadFormbricksSDK = async (apiHostParam: string): Promise<Result<void>> => {
|
||||
if (!window.formbricks) {
|
||||
const res = await fetch(`${apiHostParam}/api/packages/${sdkType}`);
|
||||
const res = await fetch(`${apiHostParam}/api/packages/js`);
|
||||
|
||||
// Failed to fetch the app package
|
||||
if (!res.ok) {
|
||||
return { ok: false, error: new Error(`Failed to load Formbricks ${sdkType} SDK`) };
|
||||
return { ok: false, error: new Error(`Failed to load Formbricks SDK`) };
|
||||
}
|
||||
|
||||
const sdkScript = await res.text();
|
||||
@@ -33,7 +33,7 @@ const loadFormbricksSDK = async (apiHostParam: string, sdkType: "app" | "website
|
||||
|
||||
setTimeout(() => {
|
||||
clearInterval(checkInterval);
|
||||
reject(new Error(`Formbricks ${sdkType} SDK loading timed out`));
|
||||
reject(new Error(`Formbricks SDK loading timed out`));
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ const loadFormbricksSDK = async (apiHostParam: string, sdkType: "app" | "website
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
error: new Error(err.message ?? `Failed to load Formbricks ${sdkType} SDK`),
|
||||
error: new Error(err.message ?? `Failed to load Formbricks SDK`),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -55,11 +55,8 @@ const loadFormbricksSDK = async (apiHostParam: string, sdkType: "app" | "website
|
||||
|
||||
const functionsToProcess: { prop: string; args: unknown[] }[] = [];
|
||||
|
||||
export const loadFormbricksToProxy = async (
|
||||
prop: string,
|
||||
sdkType: "app" | "website",
|
||||
...args: unknown[]
|
||||
): Promise<void> => {
|
||||
export const loadFormbricksToProxy = async (prop: string, ...args: unknown[]): Promise<void> => {
|
||||
console.log(args);
|
||||
// all of this should happen when not initialized:
|
||||
if (!isInitialized) {
|
||||
if (prop === "init") {
|
||||
@@ -72,7 +69,7 @@ export const loadFormbricksToProxy = async (
|
||||
isInitializing = true;
|
||||
|
||||
const apiHost = (args[0] as { apiHost: string }).apiHost;
|
||||
const loadSDKResult = await loadFormbricksSDK(apiHost, sdkType);
|
||||
const loadSDKResult = await loadFormbricksSDK(apiHost);
|
||||
|
||||
if (loadSDKResult.ok) {
|
||||
if (window.formbricks) {
|
||||
@@ -1,23 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment --
|
||||
* Required because it doesn't work without building otherwise
|
||||
*/
|
||||
import { type TFormbricksApp } from "@formbricks/js-core/app";
|
||||
import { type TFormbricksWebsite } from "@formbricks/js-core/website";
|
||||
import { loadFormbricksToProxy } from "./shared/load-formbricks";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
formbricks: TFormbricksApp | TFormbricksWebsite | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const formbricksProxyHandler: ProxyHandler<TFormbricksWebsite> = {
|
||||
get(_target, prop, _receiver) {
|
||||
return (...args: unknown[]) => loadFormbricksToProxy(prop as string, "website", ...args);
|
||||
},
|
||||
};
|
||||
|
||||
const formbricksWebsite: TFormbricksWebsite = new Proxy({} as TFormbricksWebsite, formbricksProxyHandler);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export -- Required for UMD
|
||||
export default formbricksWebsite;
|
||||
@@ -10,12 +10,10 @@ const config = () => {
|
||||
sourcemap: true,
|
||||
lib: {
|
||||
// Could also be a dictionary or array of multiple entry points
|
||||
entry: {
|
||||
app: resolve(__dirname, "src/app.ts"),
|
||||
website: resolve(__dirname, "src/website.ts"),
|
||||
},
|
||||
entry: resolve(__dirname, "src/index.ts"),
|
||||
name: "formbricksJsWrapper",
|
||||
formats: ["es", "cjs"],
|
||||
fileName: "index",
|
||||
},
|
||||
},
|
||||
plugins: [dts({ rollupTypes: true, bundledPackages: ["@formbricks/js-core"] })],
|
||||
|
||||
@@ -168,7 +168,6 @@ export const createEnvironment = async (
|
||||
type: environmentInput.type || "development",
|
||||
product: { connect: { id: productId } },
|
||||
appSetupCompleted: environmentInput.appSetupCompleted || false,
|
||||
websiteSetupCompleted: environmentInput.websiteSetupCompleted || false,
|
||||
actionClasses: {
|
||||
create: [
|
||||
{
|
||||
|
||||
@@ -18,5 +18,4 @@ export const mockEnvironment: TEnvironment = {
|
||||
type: "production",
|
||||
productId: mockId,
|
||||
appSetupCompleted: false,
|
||||
websiteSetupCompleted: false,
|
||||
};
|
||||
|
||||
@@ -95,7 +95,6 @@ export const mockEnvironment: TEnvironment = {
|
||||
type: "production",
|
||||
productId: mockId,
|
||||
appSetupCompleted: false,
|
||||
websiteSetupCompleted: false,
|
||||
};
|
||||
|
||||
export const mockUser: TUser = {
|
||||
@@ -237,7 +236,7 @@ export const mockSyncSurveyOutput: SurveyMock = {
|
||||
};
|
||||
|
||||
export const mockSurveyOutput: SurveyMock = {
|
||||
type: "website",
|
||||
type: "link",
|
||||
status: "inProgress",
|
||||
displayOption: "respondMultiple",
|
||||
triggers: [{ actionClass: mockActionClass }],
|
||||
@@ -256,7 +255,6 @@ export const mockSurveyOutput: SurveyMock = {
|
||||
};
|
||||
|
||||
export const createSurveyInput: TSurveyCreateInput = {
|
||||
type: "website",
|
||||
status: "inProgress",
|
||||
displayOption: "respondMultiple",
|
||||
triggers: [{ actionClass: mockActionClass }],
|
||||
@@ -264,7 +262,7 @@ export const createSurveyInput: TSurveyCreateInput = {
|
||||
};
|
||||
|
||||
export const updateSurveyInput: TSurvey = {
|
||||
type: "website",
|
||||
type: "link",
|
||||
status: "inProgress",
|
||||
displayOption: "respondMultiple",
|
||||
triggers: [{ actionClass: mockActionClass }],
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useCallback, useEffect, useSyncExternalStore } from "react";
|
||||
import { type TJsAppConfigInput } from "@formbricks/types/js";
|
||||
import { Logger } from "../../js-core/src/shared/logger";
|
||||
import { type TJsReactNativeConfigInput } from "@formbricks/types/js";
|
||||
import { Logger } from "../../js-core/src/lib/logger";
|
||||
import { init } from "./lib";
|
||||
import { SurveyStore } from "./lib/survey-store";
|
||||
import { SurveyWebView } from "./survey-web-view";
|
||||
|
||||
interface FormbricksProps {
|
||||
initConfig: TJsAppConfigInput;
|
||||
initConfig: TJsReactNativeConfigInput;
|
||||
}
|
||||
const surveyStore = SurveyStore.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
|
||||
@@ -5,9 +5,9 @@ import {
|
||||
type Result,
|
||||
err,
|
||||
okVoid,
|
||||
} from "../../../js-core/src/shared/errors";
|
||||
import { Logger } from "../../../js-core/src/shared/logger";
|
||||
import { shouldDisplayBasedOnPercentage } from "../../../js-core/src/shared/utils";
|
||||
} from "../../../js-core/src/lib/errors";
|
||||
import { Logger } from "../../../js-core/src/lib/logger";
|
||||
import { shouldDisplayBasedOnPercentage } from "../../../js-core/src/lib/utils";
|
||||
import { appConfig } from "./config";
|
||||
import { SurveyStore } from "./survey-store";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { FormbricksAPI } from "@formbricks/api";
|
||||
import type { TAttributes } from "@formbricks/types/attributes";
|
||||
import { type Result, err, ok } from "@formbricks/types/error-handlers";
|
||||
import type { NetworkError } from "@formbricks/types/errors";
|
||||
import { Logger } from "../../../js-core/src/shared/logger";
|
||||
import { Logger } from "../../../js-core/src/lib/logger";
|
||||
import { appConfig } from "./config";
|
||||
|
||||
const logger = Logger.getInstance();
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { wrapThrowsAsync } from "@formbricks/types/error-handlers";
|
||||
import { type TJsPackageType } from "@formbricks/types/js";
|
||||
import { ErrorHandler, type Result } from "../../../js-core/src/shared/errors";
|
||||
import { ErrorHandler, type Result } from "../../../js-core/src/lib/errors";
|
||||
import { checkInitialized } from "./initialize";
|
||||
|
||||
export class CommandQueue {
|
||||
private queue: {
|
||||
command: (...args: any[]) => Promise<Result<void, unknown>> | Result<void, unknown> | Promise<void>;
|
||||
packageType: TJsPackageType;
|
||||
checkInitialized: boolean;
|
||||
commandArgs: any[];
|
||||
}[] = [];
|
||||
@@ -15,12 +13,11 @@ export class CommandQueue {
|
||||
private commandPromise: Promise<void> | null = null;
|
||||
|
||||
public add<A>(
|
||||
packageType: TJsPackageType,
|
||||
command: (...args: A[]) => Promise<Result<void, unknown>> | Result<void, unknown> | Promise<void>,
|
||||
shouldCheckInitialized = true,
|
||||
...args: A[]
|
||||
): void {
|
||||
this.queue.push({ command, checkInitialized: shouldCheckInitialized, commandArgs: args, packageType });
|
||||
this.queue.push({ command, checkInitialized: shouldCheckInitialized, commandArgs: args });
|
||||
|
||||
if (!this.running) {
|
||||
this.commandPromise = new Promise((resolve) => {
|
||||
|
||||
@@ -2,17 +2,13 @@
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { type Result, err, ok, wrapThrowsAsync } from "@formbricks/types/error-handlers";
|
||||
import type { TJsAppConfigUpdateInput, TJsRNConfig } from "@formbricks/types/js";
|
||||
import { RN_ASYNC_STORAGE_KEY } from "../../../js-core/src/shared/constants";
|
||||
|
||||
// LocalStorage implementation - default
|
||||
import { RN_ASYNC_STORAGE_KEY } from "../../../js-core/src/lib/constants";
|
||||
|
||||
export class RNConfig {
|
||||
private static instance: RNConfig | undefined;
|
||||
private config: TJsRNConfig | null = null;
|
||||
|
||||
private constructor() {
|
||||
// const localConfig = this.loadFromStorage();
|
||||
|
||||
this.loadFromStorage()
|
||||
.then((localConfig) => {
|
||||
if (localConfig.ok) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type TJsAppConfigInput } from "@formbricks/types/js";
|
||||
import { ErrorHandler } from "../../../js-core/src/shared/errors";
|
||||
import { Logger } from "../../../js-core/src/shared/logger";
|
||||
import { type TJsReactNativeConfigInput } from "@formbricks/types/js";
|
||||
import { ErrorHandler } from "../../../js-core/src/lib/errors";
|
||||
import { Logger } from "../../../js-core/src/lib/logger";
|
||||
import { trackCodeAction } from "./actions";
|
||||
import { CommandQueue } from "./command-queue";
|
||||
import { initialize } from "./initialize";
|
||||
@@ -9,13 +9,13 @@ const logger = Logger.getInstance();
|
||||
logger.debug("Create command queue");
|
||||
const queue = new CommandQueue();
|
||||
|
||||
export const init = async (initConfig: TJsAppConfigInput): Promise<void> => {
|
||||
export const init = async (initConfig: TJsReactNativeConfigInput): Promise<void> => {
|
||||
ErrorHandler.init(initConfig.errorHandler);
|
||||
queue.add("app", initialize, false, initConfig);
|
||||
queue.add(initialize, false, initConfig);
|
||||
await queue.wait();
|
||||
};
|
||||
|
||||
export const track = async (name: string, properties = {}): Promise<void> => {
|
||||
queue.add<any>("app", trackCodeAction, true, name, properties);
|
||||
queue.add<any>(trackCodeAction, true, name, properties);
|
||||
await queue.wait();
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type TAttributes } from "@formbricks/types/attributes";
|
||||
import { type TJsAppConfigInput, type TJsRNConfig } from "@formbricks/types/js";
|
||||
import { type TJsRNConfig, type TJsReactNativeConfigInput } from "@formbricks/types/js";
|
||||
import {
|
||||
ErrorHandler,
|
||||
type MissingFieldError,
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
type Result,
|
||||
err,
|
||||
okVoid,
|
||||
} from "../../../js-core/src/shared/errors";
|
||||
import { Logger } from "../../../js-core/src/shared/logger";
|
||||
} from "../../../js-core/src/lib/errors";
|
||||
import { Logger } from "../../../js-core/src/lib/logger";
|
||||
import { trackAction } from "./actions";
|
||||
import { updateAttributes } from "./attributes";
|
||||
import { appConfig } from "./config";
|
||||
@@ -24,7 +24,7 @@ export const setIsInitialize = (state: boolean): void => {
|
||||
};
|
||||
|
||||
export const initialize = async (
|
||||
c: TJsAppConfigInput
|
||||
c: TJsReactNativeConfigInput
|
||||
): Promise<Result<void, MissingFieldError | NetworkError | MissingPersonError>> => {
|
||||
if (isInitialized) {
|
||||
logger.debug("Already initialized, skipping initialization.");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type NetworkError, type Result, err, okVoid } from "../../../js-core/src/shared/errors";
|
||||
import { Logger } from "../../../js-core/src/shared/logger";
|
||||
import { type NetworkError, type Result, err, okVoid } from "../../../js-core/src/lib/errors";
|
||||
import { Logger } from "../../../js-core/src/lib/logger";
|
||||
import { appConfig } from "./config";
|
||||
import { deinitalize, initialize } from "./initialize";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { TAttributes } from "@formbricks/types/attributes";
|
||||
import { type Result, err, ok } from "@formbricks/types/error-handlers";
|
||||
import type { NetworkError } from "@formbricks/types/errors";
|
||||
import type { TJsAppState, TJsAppStateSync, TJsRNSyncParams } from "@formbricks/types/js";
|
||||
import { Logger } from "../../../js-core/src/shared/logger";
|
||||
import { Logger } from "../../../js-core/src/lib/logger";
|
||||
import type { RNConfig } from "./config";
|
||||
|
||||
const logger = Logger.getInstance();
|
||||
|
||||
@@ -14,8 +14,8 @@ import type { TJsFileUploadParams } from "@formbricks/types/js";
|
||||
import type { TResponseUpdate } from "@formbricks/types/responses";
|
||||
import type { TUploadFileConfig } from "@formbricks/types/storage";
|
||||
import type { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { Logger } from "../../js-core/src/shared/logger";
|
||||
import { getDefaultLanguageCode, getLanguageCode } from "../../js-core/src/shared/utils";
|
||||
import { Logger } from "../../js-core/src/lib/logger";
|
||||
import { getDefaultLanguageCode, getLanguageCode } from "../../js-core/src/lib/utils";
|
||||
import { appConfig } from "./lib/config";
|
||||
import { SurveyStore } from "./lib/survey-store";
|
||||
import { sync } from "./lib/sync";
|
||||
|
||||
@@ -13,7 +13,7 @@ interface AutoCloseProps {
|
||||
export const AutoCloseWrapper = ({ survey, onClose, children, offset }: AutoCloseProps) => {
|
||||
const [countDownActive, setCountDownActive] = useState(true);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const isAppSurvey = survey.type === "app" || survey.type === "website";
|
||||
const isAppSurvey = survey.type === "app";
|
||||
const showAutoCloseProgressBar = countDownActive && isAppSurvey && offset === 0;
|
||||
|
||||
const startCountdown = () => {
|
||||
|
||||
@@ -7,7 +7,6 @@ export const ZEnvironment = z.object({
|
||||
type: z.enum(["development", "production"]),
|
||||
productId: z.string(),
|
||||
appSetupCompleted: z.boolean(),
|
||||
websiteSetupCompleted: z.boolean(),
|
||||
});
|
||||
|
||||
export type TEnvironment = z.infer<typeof ZEnvironment>;
|
||||
@@ -22,13 +21,11 @@ export const ZEnvironmentUpdateInput = z.object({
|
||||
type: z.enum(["development", "production"]),
|
||||
productId: z.string(),
|
||||
appSetupCompleted: z.boolean(),
|
||||
websiteSetupCompleted: z.boolean(),
|
||||
});
|
||||
|
||||
export const ZEnvironmentCreateInput = z.object({
|
||||
type: z.enum(["development", "production"]).optional(),
|
||||
appSetupCompleted: z.boolean().optional(),
|
||||
websiteSetupCompleted: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type TEnvironmentCreateInput = z.infer<typeof ZEnvironmentCreateInput>;
|
||||
|
||||
@@ -174,15 +174,18 @@ export const ZJsWebsiteConfigInput = z.object({
|
||||
|
||||
export type TJsWebsiteConfigInput = z.infer<typeof ZJsWebsiteConfigInput>;
|
||||
|
||||
export const ZJsAppConfigInput = z.object({
|
||||
export const ZJsConfigInput = z.object({
|
||||
environmentId: z.string().cuid2(),
|
||||
apiHost: z.string(),
|
||||
errorHandler: z.function().args(z.any()).returns(z.void()).optional(),
|
||||
userId: z.string(),
|
||||
userId: z.string().optional(),
|
||||
attributes: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
export type TJsAppConfigInput = z.infer<typeof ZJsAppConfigInput>;
|
||||
export type TJsConfigInput = z.infer<typeof ZJsConfigInput>;
|
||||
|
||||
export const ZJsReactNativeConfigInput = ZJsConfigInput.omit({ userId: true }).extend({ userId: z.string() });
|
||||
export type TJsReactNativeConfigInput = z.infer<typeof ZJsReactNativeConfigInput>;
|
||||
|
||||
export const ZJsPeopleUserIdInput = z.object({
|
||||
environmentId: z.string().cuid2(),
|
||||
@@ -234,10 +237,6 @@ export const ZJsWebsiteSyncParams = ZJsPersonSyncParams.omit({ userId: true });
|
||||
|
||||
export type TJsWebsiteSyncParams = z.infer<typeof ZJsWebsiteSyncParams>;
|
||||
|
||||
export const ZJsPackageType = z.union([z.literal("app"), z.literal("website")]);
|
||||
|
||||
export type TJsPackageType = z.infer<typeof ZJsPackageType>;
|
||||
|
||||
export const ZJsTrackProperties = z.object({
|
||||
hiddenFields: ZResponseHiddenFieldValue.optional(),
|
||||
});
|
||||
|
||||
@@ -683,7 +683,7 @@ export const ZSurveyDisplayOption = z.enum([
|
||||
|
||||
export type TSurveyDisplayOption = z.infer<typeof ZSurveyDisplayOption>;
|
||||
|
||||
export const ZSurveyType = z.enum(["link", "app", "website"]);
|
||||
export const ZSurveyType = z.enum(["link", "app"]);
|
||||
|
||||
export type TSurveyType = z.infer<typeof ZSurveyType>;
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ type EmptySpaceFillerProps = {
|
||||
environment: TEnvironment;
|
||||
noWidgetRequired?: boolean;
|
||||
emptyMessage?: string;
|
||||
widgetSetupCompleted?: boolean;
|
||||
};
|
||||
|
||||
export const EmptySpaceFiller = ({
|
||||
@@ -17,7 +16,6 @@ export const EmptySpaceFiller = ({
|
||||
environment,
|
||||
noWidgetRequired,
|
||||
emptyMessage,
|
||||
widgetSetupCompleted = false,
|
||||
}: EmptySpaceFillerProps) => {
|
||||
if (type === "table") {
|
||||
return (
|
||||
@@ -25,7 +23,7 @@ export const EmptySpaceFiller = ({
|
||||
<div className="w-full space-y-3">
|
||||
<div className="h-16 w-full rounded-lg bg-slate-50"></div>
|
||||
<div className="flex h-16 w-full flex-col items-center justify-center rounded-lg bg-slate-50 text-slate-700 transition-all duration-300 ease-in-out hover:bg-slate-100">
|
||||
{!widgetSetupCompleted && !noWidgetRequired && (
|
||||
{!environment.appSetupCompleted && !noWidgetRequired && (
|
||||
<Link
|
||||
className="flex w-full items-center justify-center"
|
||||
href={`/environments/${environment.id}/product/app-connection`}>
|
||||
@@ -34,7 +32,8 @@ export const EmptySpaceFiller = ({
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
{((widgetSetupCompleted || noWidgetRequired) && emptyMessage) || "Waiting for a response 🧘♂️"}
|
||||
{((environment.appSetupCompleted || noWidgetRequired) && emptyMessage) ||
|
||||
"Waiting for a response 🧘♂️"}
|
||||
</div>
|
||||
|
||||
<div className="h-16 w-full rounded-lg bg-slate-50"></div>
|
||||
@@ -53,7 +52,7 @@ export const EmptySpaceFiller = ({
|
||||
<div className="space-y-4">
|
||||
<div className="h-12 w-full rounded-full bg-slate-100"></div>
|
||||
<div className="flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100">
|
||||
{!widgetSetupCompleted && !noWidgetRequired && (
|
||||
{!environment.appSetupCompleted && !noWidgetRequired && (
|
||||
<Link
|
||||
className="flex h-full w-full items-center justify-center"
|
||||
href={`/environments/${environment.id}/product/app-connection`}>
|
||||
@@ -62,7 +61,7 @@ export const EmptySpaceFiller = ({
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
{(widgetSetupCompleted || noWidgetRequired) && (
|
||||
{(environment.appSetupCompleted || noWidgetRequired) && (
|
||||
<span className="bg-light-background-primary-500 text-center">
|
||||
{emptyMessage ?? "Waiting for a response"} 🧘♂️
|
||||
</span>
|
||||
@@ -84,7 +83,7 @@ export const EmptySpaceFiller = ({
|
||||
<div className="space-y-4">
|
||||
<div className="h-12 w-full rounded-full bg-slate-100"></div>
|
||||
<div className="flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100">
|
||||
{!widgetSetupCompleted && !noWidgetRequired && (
|
||||
{!environment.appSetupCompleted && !noWidgetRequired && (
|
||||
<Link
|
||||
className="flex h-full w-full items-center justify-center"
|
||||
href={`/environments/${environment.id}/product/app-connection`}>
|
||||
@@ -93,7 +92,7 @@ export const EmptySpaceFiller = ({
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
{(widgetSetupCompleted || noWidgetRequired) && (
|
||||
{(environment.appSetupCompleted || noWidgetRequired) && (
|
||||
<span className="text-center">Tag a submission to find your list of tags here.</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -132,7 +131,7 @@ export const EmptySpaceFiller = ({
|
||||
<div className="space-y-4">
|
||||
<div className="h-12 w-full rounded-full bg-slate-100"></div>
|
||||
<div className="flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100">
|
||||
{!widgetSetupCompleted && !noWidgetRequired && (
|
||||
{!environment.appSetupCompleted && !noWidgetRequired && (
|
||||
<Link
|
||||
className="flex h-full w-full items-center justify-center"
|
||||
href={`/environments/${environment.id}/product/app-connection`}>
|
||||
@@ -141,7 +140,7 @@ export const EmptySpaceFiller = ({
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
{(widgetSetupCompleted || noWidgetRequired) && (
|
||||
{(environment.appSetupCompleted || noWidgetRequired) && (
|
||||
<span className="text-center">Waiting for a response 🧘♂️</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -72,7 +72,6 @@ export const PreviewSurvey = ({
|
||||
const [isFullScreenPreview, setIsFullScreenPreview] = useState(false);
|
||||
|
||||
const [appSetupCompleted, setAppSetupCompleted] = useState(false);
|
||||
const [websiteSetupCompleted, setWebsiteSetupCompleted] = useState(false);
|
||||
|
||||
const [previewMode, setPreviewMode] = useState("desktop");
|
||||
const [previewPosition, setPreviewPosition] = useState("relative");
|
||||
@@ -122,7 +121,7 @@ export const PreviewSurvey = ({
|
||||
const darkOverlay = surveyDarkOverlay ?? product.darkOverlay;
|
||||
const clickOutsideClose = surveyClickOutsideClose ?? product.clickOutsideClose;
|
||||
|
||||
const widgetSetupCompleted = appSetupCompleted || websiteSetupCompleted;
|
||||
const widgetSetupCompleted = appSetupCompleted;
|
||||
|
||||
const styling: TSurveyStyling | TProductStyling = useMemo(() => {
|
||||
// allow style overwrite is disabled from the product
|
||||
@@ -167,7 +166,7 @@ export const PreviewSurvey = ({
|
||||
|
||||
const onFinished = () => {
|
||||
// close modal if there are no questions left
|
||||
if ((survey.type === "website" || survey.type === "app") && survey.endings.length === 0) {
|
||||
if (survey.type === "app" && survey.endings.length === 0) {
|
||||
setIsModalOpen(false);
|
||||
setTimeout(() => {
|
||||
setQuestionId(survey.questions[0]?.id);
|
||||
@@ -198,7 +197,6 @@ export const PreviewSurvey = ({
|
||||
useEffect(() => {
|
||||
if (environment) {
|
||||
setAppSetupCompleted(environment.appSetupCompleted);
|
||||
setWebsiteSetupCompleted(environment.websiteSetupCompleted);
|
||||
}
|
||||
}, [environment]);
|
||||
|
||||
|
||||
@@ -176,9 +176,9 @@ export const SingleResponseCardHeader = ({
|
||||
|
||||
{pageType === "people" && (
|
||||
<div className="flex items-center justify-center space-x-2 rounded-full bg-slate-100 p-1 px-2 text-sm text-slate-600">
|
||||
{(survey.type === "link" ||
|
||||
environment.appSetupCompleted ||
|
||||
environment.websiteSetupCompleted) && <SurveyStatusIndicator status={survey.status} />}
|
||||
{(survey.type === "link" || environment.appSetupCompleted) && (
|
||||
<SurveyStatusIndicator status={survey.status} />
|
||||
)}
|
||||
<Link
|
||||
className="hover:underline"
|
||||
href={`/environments/${environmentId}/surveys/${survey.id}/summary`}>
|
||||
|
||||
@@ -66,13 +66,11 @@ export const CopySurveyForm = ({
|
||||
<div className="space-y-8 pb-12">
|
||||
{formFields.fields.map((field, productIndex) => {
|
||||
const product = defaultProducts.find((product) => product.id === field.product);
|
||||
const isDisabled = survey.type !== "link" && product?.config.channel !== survey.type;
|
||||
|
||||
return (
|
||||
<div key={product?.id}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<TooltipRenderer
|
||||
shouldRender={isDisabled}
|
||||
tooltipContent={
|
||||
<span>
|
||||
This product is not compatible with the survey type. Please select a different
|
||||
@@ -80,10 +78,7 @@ export const CopySurveyForm = ({
|
||||
</span>
|
||||
}>
|
||||
<div className="w-fit">
|
||||
<p className="text-base font-semibold text-slate-900">
|
||||
{product?.name}
|
||||
{isDisabled && <span className="ml-2 mr-11 text-sm text-gray-500">(Disabled)</span>}
|
||||
</p>
|
||||
<p className="text-base font-semibold text-slate-900">{product?.name}</p>
|
||||
</div>
|
||||
</TooltipRenderer>
|
||||
|
||||
@@ -103,11 +98,6 @@ export const CopySurveyForm = ({
|
||||
<Checkbox
|
||||
{...field}
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
if (isDisabled) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
onCheckedChange={() => {
|
||||
if (field.value.includes(environment.id)) {
|
||||
field.onChange(
|
||||
@@ -118,7 +108,6 @@ export const CopySurveyForm = ({
|
||||
}
|
||||
}}
|
||||
className="mr-2 h-4 w-4 appearance-none border-gray-300 checked:border-transparent checked:bg-slate-500 checked:after:bg-slate-500 checked:hover:bg-slate-500 focus:ring-2 focus:ring-slate-500 focus:ring-opacity-50"
|
||||
disabled={isDisabled}
|
||||
id={environment.id}
|
||||
/>
|
||||
<Label htmlFor={environment.id}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Code, EarthIcon, Link2Icon } from "lucide-react";
|
||||
import { Code, Link2Icon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
@@ -6,7 +6,7 @@ import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { convertDateString, timeSince } from "@formbricks/lib/time";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyType } from "@formbricks/types/surveys/types";
|
||||
import { SurveyStatusIndicator } from "../../SurveyStatusIndicator";
|
||||
import { generateSingleUseIdAction } from "../actions";
|
||||
import { SurveyDropDownMenu } from "./SurveyDropdownMenu";
|
||||
@@ -70,7 +70,7 @@ export const SurveyCard = ({
|
||||
: `/environments/${environment.id}/surveys/${survey.id}/summary`;
|
||||
}, [survey.status, survey.id, environment.id]);
|
||||
|
||||
const SurveyTypeIndicator = ({ type }: { type: TSurvey["type"] }) => (
|
||||
const SurveyTypeIndicator = ({ type }: { type: TSurveyType }) => (
|
||||
<div className="flex items-center space-x-2 text-sm text-slate-600">
|
||||
{type === "app" && (
|
||||
<>
|
||||
@@ -79,13 +79,6 @@ export const SurveyCard = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === "website" && (
|
||||
<>
|
||||
<EarthIcon className="h-4 w-4" />
|
||||
<span> Website</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === "link" && (
|
||||
<>
|
||||
<Link2Icon className="h-4 w-4" />
|
||||
|
||||
@@ -115,7 +115,7 @@ export const SurveyFilters = ({
|
||||
};
|
||||
|
||||
const handleTypeChange = (value: string) => {
|
||||
if (value === "link" || value === "app" || value === "website") {
|
||||
if (value === "link" || value === "app") {
|
||||
if (type.includes(value)) {
|
||||
setSurveyFilters((prev) => ({ ...prev, type: prev.type.filter((v) => v !== value) }));
|
||||
} else {
|
||||
|
||||
@@ -21,7 +21,9 @@ export const TemplateFilters = ({
|
||||
newFilter[index] = filterValue;
|
||||
setSelectedFilter(newFilter);
|
||||
};
|
||||
|
||||
const allFilters = [channelMapping, industryMapping, roleMapping];
|
||||
|
||||
return (
|
||||
<div className="mb-6 gap-3">
|
||||
{allFilters.map((filters, index) => {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { SplitIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TProductConfigIndustry } from "@formbricks/types/product";
|
||||
import { TSurveyType } from "@formbricks/types/surveys/types";
|
||||
import { TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product";
|
||||
import { TTemplate, TTemplateFilter, TTemplateRole } from "@formbricks/types/templates";
|
||||
import { TooltipRenderer } from "../../Tooltip";
|
||||
import { channelMapping, industryMapping, roleMapping } from "../lib/utils";
|
||||
@@ -12,6 +11,8 @@ interface TemplateTagsProps {
|
||||
selectedFilter: TTemplateFilter[];
|
||||
}
|
||||
|
||||
type NonNullabeChannel = NonNullable<TProductConfigChannel>;
|
||||
|
||||
const getRoleBasedStyling = (role: TTemplateRole | undefined): string => {
|
||||
switch (role) {
|
||||
case "productManager":
|
||||
@@ -27,9 +28,9 @@ const getRoleBasedStyling = (role: TTemplateRole | undefined): string => {
|
||||
}
|
||||
};
|
||||
|
||||
const getChannelTag = (channels: TSurveyType[] | undefined): string | undefined => {
|
||||
const getChannelTag = (channels: NonNullabeChannel[] | undefined): string | undefined => {
|
||||
if (!channels) return undefined;
|
||||
const getLabel = (channelValue: TSurveyType) =>
|
||||
const getLabel = (channelValue: NonNullabeChannel) =>
|
||||
channelMapping.find((channel) => channel.value === channelValue)?.label;
|
||||
const labels = channels.map((channel) => getLabel(channel)).sort();
|
||||
|
||||
@@ -60,7 +61,6 @@ export const TemplateTags = ({ template, selectedFilter }: TemplateTagsProps) =>
|
||||
);
|
||||
|
||||
const channelTag = useMemo(() => getChannelTag(template.channels), [template.channels]);
|
||||
|
||||
const getIndustryTag = (industries: TProductConfigIndustry[] | undefined): string | undefined => {
|
||||
// if user selects an industry e.g. eCommerce than the tag should not say "Multiple industries" anymore but "E-Commerce".
|
||||
if (selectedFilter[1] !== null)
|
||||
|
||||
@@ -6,8 +6,8 @@ import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { templates } from "@formbricks/lib/templates";
|
||||
import type { TEnvironment } from "@formbricks/types/environment";
|
||||
import { type TProduct, ZProductConfigIndustry } from "@formbricks/types/product";
|
||||
import { TSurveyCreateInput, ZSurveyType } from "@formbricks/types/surveys/types";
|
||||
import { type TProduct, ZProductConfigChannel, ZProductConfigIndustry } from "@formbricks/types/product";
|
||||
import { TSurveyCreateInput, TSurveyType } from "@formbricks/types/surveys/types";
|
||||
import { TTemplate, TTemplateFilter, ZTemplateRole } from "@formbricks/types/templates";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { createSurveyAction } from "./actions";
|
||||
@@ -37,9 +37,20 @@ export const TemplateList = ({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedFilter, setSelectedFilter] = useState<TTemplateFilter[]>(prefilledFilters);
|
||||
|
||||
const surveyType: TSurveyType = useMemo(() => {
|
||||
if (product.config.channel) {
|
||||
if (product.config.channel === "website") {
|
||||
return "app";
|
||||
}
|
||||
|
||||
return product.config.channel;
|
||||
}
|
||||
|
||||
return "link";
|
||||
}, [product.config.channel]);
|
||||
|
||||
const createSurvey = async (activeTemplate: TTemplate) => {
|
||||
setLoading(true);
|
||||
const surveyType = product.config.channel ?? "link";
|
||||
const augmentedTemplate: TSurveyCreateInput = {
|
||||
...activeTemplate.preset,
|
||||
type: surveyType,
|
||||
@@ -63,8 +74,9 @@ export const TemplateList = ({
|
||||
if (templateSearch) {
|
||||
return template.name.toLowerCase().includes(templateSearch.toLowerCase());
|
||||
}
|
||||
|
||||
// Parse and validate the filters
|
||||
const channelParseResult = ZSurveyType.nullable().safeParse(selectedFilter[0]);
|
||||
const channelParseResult = ZProductConfigChannel.nullable().safeParse(selectedFilter[0]);
|
||||
const industryParseResult = ZProductConfigIndustry.nullable().safeParse(selectedFilter[1]);
|
||||
const roleParseResult = ZTemplateRole.nullable().safeParse(selectedFilter[2]);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { TProduct, TProductConfigIndustry } from "@formbricks/types/product";
|
||||
import { TSurveyQuestion, TSurveyType } from "@formbricks/types/surveys/types";
|
||||
import { TProduct, TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product";
|
||||
import { TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TTemplate, TTemplateRole } from "@formbricks/types/templates";
|
||||
|
||||
export const replaceQuestionPresetPlaceholders = (
|
||||
@@ -36,7 +36,7 @@ export const replacePresetPlaceholders = (template: TTemplate, product: any) =>
|
||||
return { ...template, preset };
|
||||
};
|
||||
|
||||
export const channelMapping: { value: TSurveyType; label: string }[] = [
|
||||
export const channelMapping: { value: TProductConfigChannel; label: string }[] = [
|
||||
{ value: "website", label: "Website Survey" },
|
||||
{ value: "app", label: "App Survey" },
|
||||
{ value: "link", label: "Link Survey" },
|
||||
|
||||
@@ -33,14 +33,14 @@ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
||||
interface TooltipRendererProps {
|
||||
shouldRender: boolean;
|
||||
tooltipContent: ReactNode;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
triggerClass?: string;
|
||||
shouldRender?: boolean;
|
||||
}
|
||||
export const TooltipRenderer = (props: TooltipRendererProps) => {
|
||||
const { children, shouldRender, tooltipContent, className, triggerClass } = props;
|
||||
const { children, shouldRender = true, tooltipContent, className, triggerClass } = props;
|
||||
if (shouldRender) {
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
|
||||
Reference in New Issue
Block a user