Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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",
Comment thread
mattinannt marked this conversation as resolved.
"@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";
Comment thread
pandeymangg marked this conversation as resolved.
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