Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .ncurc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"removeRange": true
}
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
save-exact=true
1 change: 1 addition & 0 deletions apps/playground/.npmrc
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node-linker=hoisted
save-exact=true
18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,28 @@
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"lint": "node scripts/check-exact-deps.mjs && turbo run lint",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"check-types": "turbo run check-types",
"test": "turbo run test --no-cache",
"test:coverage": "turbo run test:coverage --no-cache"
},
"devDependencies": {
"prettier": "^3.8.1",
"turbo": "^2.8.8",
"prettier": "3.8.1",
"turbo": "2.8.20",
"typescript": "5.9.3",
"rimraf": "6.1.2"
"rimraf": "6.1.3"
},
"packageManager": "pnpm@10.29.3",
"packageManager": "pnpm@10.32.1",
"engines": {
"node": ">=20"
},
"pnpm": {
"overrides": {
"on-headers": ">=1.1.0",
"glob": ">=11.1.0",
"node-forge": ">=1.3.2",
"js-yaml": ">=4.1.1",
"on-headers": "1.1.0",
"glob": "13.0.4",
"node-forge": "1.3.3",
"js-yaml": "4.1.1",
"tar": "7.5.12"
}
}
Expand Down
4 changes: 3 additions & 1 deletion packages/react-native/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
const project = "tsconfig.json";

module.exports = {
extends: [
"@vercel/style-guide/eslint/browser",
"@vercel/style-guide/eslint/typescript",
"@vercel/style-guide/eslint/react",
].map(require.resolve),
parserOptions: {
project: "tsconfig.json",
project,
tsconfigRootDir: __dirname,
},
globals: {
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"devDependencies": {
"@types/react": "19.2.14",
"@vercel/style-guide": "6.0.0",
"@vitest/eslint-plugin": "1.6.12",
"@vitest/coverage-v8": "4.0.18",
"react": "19.2.4",
"react-native": "0.84.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/react-native/src/components/formbricks.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, { useCallback, useEffect, useSyncExternalStore } from "react";
import { View } from "react-native";
import { SurveyWebView } from "@/components/survey-web-view";
import { Logger } from "@/lib/common/logger";
import { setup } from "@/lib/common/setup";
import { SurveyStore } from "@/lib/survey/store";
import React, { useCallback, useEffect, useSyncExternalStore } from "react";
import { View } from "react-native";

interface FormbricksProps {
appUrl: string;
Expand Down
12 changes: 6 additions & 6 deletions packages/react-native/src/components/survey-web-view.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/* eslint-disable no-console -- debugging*/
import React, { type JSX, useEffect, useRef, useState } from "react";
import { KeyboardAvoidingView, Modal, View, StyleSheet } from "react-native";
import { WebView, type WebViewMessageEvent } from "react-native-webview";
import { RNConfig } from "@/lib/common/config";
import { Logger } from "@/lib/common/logger";
import { filterSurveys, getLanguageCode, getStyling } from "@/lib/common/utils";
import { SurveyStore } from "@/lib/survey/store";
import { type TUserState, ZJsRNWebViewOnMessageData } from "@/types/config";
import type { TSurvey, SurveyContainerProps } from "@/types/survey";
import React, { type JSX, useEffect, useRef, useState } from "react";
import { KeyboardAvoidingView, Modal, View, StyleSheet } from "react-native";
import { WebView, type WebViewMessageEvent } from "react-native-webview";

const logger = Logger.getInstance();
logger.configure({ logLevel: "debug" });
Expand All @@ -21,15 +21,15 @@ interface SurveyWebViewProps {

export function SurveyWebView(
props: SurveyWebViewProps
): JSX.Element | undefined {
): JSX.Element | null {
const webViewRef = useRef(null);
const [isSurveyRunning, setIsSurveyRunning] = useState(false);
const [showSurvey, setShowSurvey] = useState(false);
const [appConfig, setAppConfig] = useState<RNConfig | null>(null);
const [languageCode, setLanguageCode] = useState("default");

useEffect(() => {
const fetchConfig = async () => {
const fetchConfig = async (): Promise<void> => {
const config = await RNConfig.getInstance();
setAppConfig(config);
};
Expand Down Expand Up @@ -87,7 +87,7 @@ export function SurveyWebView(
}, [props.survey.delay, isSurveyRunning, props.survey.name]);

if (!appConfig) {
return;
return null;
}

const project = appConfig.get().environment.data.project;
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ export const logout = async (): Promise<void> => {
await queue.wait();
};

export { Formbricks as default } from "@/components/formbricks";
export { Formbricks } from "@/components/formbricks";
10 changes: 5 additions & 5 deletions packages/react-native/src/lib/common/api.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { wrapThrowsAsync } from "@/lib/common/utils";
import {
ApiResponse,
ApiSuccessResponse,
CreateOrUpdateUserResponse,
type ApiResponse,
type ApiSuccessResponse,
type CreateOrUpdateUserResponse,
} from "@/types/api";
import { TEnvironmentState } from "@/types/config";
import { ApiErrorResponse, Result, err, ok } from "@/types/error";
import { type TEnvironmentState } from "@/types/config";
import { type ApiErrorResponse, type Result, err, ok } from "@/types/error";

export const makeRequest = async <T>(
appUrl: string,
Expand Down
3 changes: 2 additions & 1 deletion packages/react-native/src/lib/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class RNConfig {

private config: TConfig | null = null;

// eslint-disable-next-line @typescript-eslint/no-empty-function -- singleton constructor
private constructor() {}

public async init(): Promise<void> {
Expand All @@ -24,7 +25,7 @@ export class RNConfig {
}
}

static async getInstance(): Promise<RNConfig> {
public static async getInstance(): Promise<RNConfig> {
RNConfig.instance ??= new RNConfig();
await RNConfig.instance.init();
return RNConfig.instance;
Expand Down
4 changes: 2 additions & 2 deletions packages/react-native/src/lib/common/event-listeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { addUserStateExpiryCheckListener, clearUserStateExpiryCheckListener } fr
let areRemoveEventListenersAdded = false;

export const addEventListeners = (): void => {
addEnvironmentStateExpiryCheckListener();
addUserStateExpiryCheckListener();
void addEnvironmentStateExpiryCheckListener();
void addUserStateExpiryCheckListener();
};

export const addCleanupEventListeners = (): void => {
Expand Down
141 changes: 89 additions & 52 deletions packages/react-native/src/lib/common/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ export const migrateUserStateAddContactId = async (): Promise<{
return { changed: false };
}

// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- data could be undefined
if (
!existingConfig.user?.data?.contactId &&
existingConfig.user?.data?.userId
!existingConfig.user?.data.contactId &&
existingConfig.user?.data.userId
) {
return { changed: true };
}
Expand All @@ -63,7 +63,9 @@ export const migrateUserStateAddContactId = async (): Promise<{
};

// Helper: Handle missing field error
function handleMissingField(field: string) {
function handleMissingField(
field: string
): Result<void, MissingFieldError> {
const logger = Logger.getInstance();
logger.debug(`No ${field} provided`);
return err({
Expand Down Expand Up @@ -91,21 +93,21 @@ async function syncEnvironmentStateIfExpired(

if (environmentStateResponse.ok) {
return ok(environmentStateResponse.data);
} else {
logger.error(
`Error fetching environment state: ${environmentStateResponse.error.code} - ${environmentStateResponse.error.responseMessage ?? ""}`
);

return err({
code: "network_error",
message: "Error fetching environment state",
status: 500,
url: new URL(
`${configInput.appUrl}/api/v1/client/${configInput.environmentId}/environment`
),
responseMessage: environmentStateResponse.error.message,
});
}

logger.error(
`Error fetching environment state: ${environmentStateResponse.error.code} - ${environmentStateResponse.error.responseMessage ?? ""}`
);

return err({
code: "network_error",
message: "Error fetching environment state",
status: 500,
url: new URL(
`${configInput.appUrl}/api/v1/client/${configInput.environmentId}/environment`
),
responseMessage: environmentStateResponse.error.message,
});
}

// Helper: Sync user state if expired
Expand All @@ -125,7 +127,7 @@ async function syncUserStateIfExpired(

logger.debug("Person state expired. Syncing.");

if (userState?.data?.userId) {
if (userState?.data.userId) {
const updatesResponse = await sendUpdatesToBackend({
appUrl: configInput.appUrl,
environmentId: configInput.environmentId,
Expand All @@ -135,23 +137,23 @@ async function syncUserStateIfExpired(
});
if (updatesResponse.ok) {
return ok(updatesResponse.data.state);
} else {
logger.error(
`Error updating user state: ${updatesResponse.error.code} - ${updatesResponse.error.responseMessage ?? ""}`
);
return err({
code: "network_error",
message: "Error updating user state",
status: 500,
url: new URL(
`${configInput.appUrl}/api/v1/client/${configInput.environmentId}/update/contacts/${userState.data.userId}`
),
responseMessage: "Unknown error",
} as const);
}
} else {
return ok(DEFAULT_USER_STATE_NO_USER_ID);

logger.error(
`Error updating user state: ${updatesResponse.error.code} - ${updatesResponse.error.responseMessage ?? ""}`
);
return err({
code: "network_error",
message: "Error updating user state",
status: 500,
url: new URL(
`${configInput.appUrl}/api/v1/client/${configInput.environmentId}/update/contacts/${userState.data.userId}`
),
responseMessage: "Unknown error",
} as const);
}

return ok(DEFAULT_USER_STATE_NO_USER_ID);
}

// Helper: Update app config with synced states
Expand Down Expand Up @@ -199,22 +201,35 @@ const createNewConfigAndSync = async (
appUrl: configInput.appUrl,
environmentId: configInput.environmentId,
});
if (!environmentStateResponse.ok) {
throw environmentStateResponse.error;

if (environmentStateResponse.ok) {
const personState = DEFAULT_USER_STATE_NO_USER_ID;
const environmentState = environmentStateResponse.data;
const filteredSurveys = filterSurveys(environmentState, personState);
appConfig.update({
appUrl: configInput.appUrl,
environmentId: configInput.environmentId,
user: personState,
environment: environmentState,
filteredSurveys,
});
return;
}
const personState = DEFAULT_USER_STATE_NO_USER_ID;
const environmentState = environmentStateResponse.data;
const filteredSurveys = filterSurveys(environmentState, personState);
appConfig.update({
appUrl: configInput.appUrl,
environmentId: configInput.environmentId,
user: personState,
environment: environmentState,
filteredSurveys,

await handleErrorOnFirstSetup({
code: environmentStateResponse.error.code,
responseMessage:
environmentStateResponse.error.responseMessage ??
environmentStateResponse.error.message,
});
} catch (e) {
} catch (e: unknown) {
const setupError = normalizeSetupError(e);
await handleErrorOnFirstSetup(
e as { code: string; responseMessage: string }
{
code: setupError.code ?? "network_error",
responseMessage:
setupError.responseMessage ?? setupError.message ?? "Unknown error",
}
);
}
};
Expand Down Expand Up @@ -260,10 +275,10 @@ const finalizeSetup = (): void => {
};

// Helper: Load existing config
const loadExistingConfig = async (
const loadExistingConfig = (
appConfig: RNConfig,
logger: ReturnType<typeof Logger.getInstance>
): Promise<TConfig | undefined> => {
): TConfig | undefined => {
let existingConfig: TConfig | undefined;
try {
existingConfig = appConfig.get();
Expand Down Expand Up @@ -294,7 +309,7 @@ export const setup = async (
return okVoid();
}

const existingConfig = await loadExistingConfig(appConfig, logger);
const existingConfig = loadExistingConfig(appConfig, logger);
if (shouldReturnEarlyForErrorState(existingConfig, logger)) {
return okVoid();
}
Expand Down Expand Up @@ -369,8 +384,6 @@ export const checkSetup = (): Result<void, NotSetupError> => {

return okVoid();
};

// eslint-disable-next-line @typescript-eslint/require-await -- disabled for now
export const tearDown = async (): Promise<void> => {
const logger = Logger.getInstance();
const appConfig = await RNConfig.getInstance();
Expand Down Expand Up @@ -425,3 +438,27 @@ export const handleErrorOnFirstSetup = async (e: {

throw new Error("Could not set up formbricks");
};

const normalizeSetupError = (
error: unknown
): Partial<{
code: string;
responseMessage: string;
message: string;
}> => {
if (typeof error !== "object" || error === null) {
return {};
}

const candidate = error as Record<string, unknown>;

return {
code: typeof candidate.code === "string" ? candidate.code : undefined,
responseMessage:
typeof candidate.responseMessage === "string"
? candidate.responseMessage
: undefined,
message:
typeof candidate.message === "string" ? candidate.message : undefined,
};
};
Loading
Loading