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
19 changes: 14 additions & 5 deletions packages/react-native/src/components/formbricks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,32 @@ import { Logger } from "@/lib/common/logger";
import { setup } from "@/lib/common/setup";
import { SurveyStore } from "@/lib/survey/store";

interface FormbricksProps {
appUrl: string;
environmentId: string;
}
type FormbricksProps = { appUrl: string } & (
| {
workspaceId: string;
environmentId?: never;
}
| {
/** @deprecated Use `workspaceId` instead. Still works as a backward-compatible alias. */
environmentId: string;
workspaceId?: never;
}
);

const surveyStore = SurveyStore.getInstance();
const logger = Logger.getInstance();

export function Formbricks({
appUrl,
environmentId,
workspaceId,
}: FormbricksProps): React.JSX.Element | null {
// initializes sdk
useEffect(() => {
const setupFormbricks = async (): Promise<void> => {
try {
await setup({
workspaceId,
environmentId,
appUrl,
});
Expand All @@ -34,7 +43,7 @@ export function Formbricks({
setupFormbricks().catch(() => {
logger.debug("Initialization error");
});
}, [environmentId, appUrl]);
}, [environmentId, workspaceId, appUrl]);

const subscribe = useCallback((callback: () => void) => {
const unsubscribe = surveyStore.subscribe(callback);
Expand Down
39 changes: 19 additions & 20 deletions packages/react-native/src/components/survey-web-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,19 +93,18 @@ export function SurveyWebView(props: SurveyWebViewProps): JSX.Element | null {
return null;
}

const project = appConfig.get().environment.data.project;
const styling = getStyling(project, props.survey);
const isBrandingEnabled = project.inAppSurveyBranding;
const settings = appConfig.get().workspace.data.settings;
const styling = getStyling(settings, props.survey);
const isBrandingEnabled = settings.inAppSurveyBranding;

const onCloseSurvey = (): void => {
const { environment: environmentState, user: personState } =
appConfig.get();
const filteredSurveys = filterSurveys(environmentState, personState);
const { workspace, user: userState } = appConfig.get();
const filteredSurveys = filterSurveys(workspace, userState);

appConfig.update({
...appConfig.get(),
environment: environmentState,
user: personState,
workspace,
user: userState,
filteredSurveys,
});

Expand All @@ -114,11 +113,11 @@ export function SurveyWebView(props: SurveyWebViewProps): JSX.Element | null {
};

const surveyPlacement =
props.survey.projectOverwrites?.placement ?? project.placement;
props.survey.projectOverwrites?.placement ?? settings.placement;
const clickOutside =
props.survey.projectOverwrites?.clickOutsideClose ??
project.clickOutsideClose;
const overlay = props.survey.projectOverwrites?.overlay ?? project.overlay;
settings.clickOutsideClose;
const overlay = props.survey.projectOverwrites?.overlay ?? settings.overlay;
const appUrl = appConfig.get().appUrl;

return (
Expand All @@ -141,7 +140,7 @@ export function SurveyWebView(props: SurveyWebViewProps): JSX.Element | null {
originWhitelist={["https://*", "http://*"]}
source={{
html: renderHtml({
environmentId: appConfig.get().environmentId,
workspaceId: appConfig.get().workspaceId,
contactId: appConfig.get().user.data.contactId ?? undefined,
survey: props.survey,
isBrandingEnabled,
Expand Down Expand Up @@ -214,7 +213,7 @@ export function SurveyWebView(props: SurveyWebViewProps): JSX.Element | null {
const displays = [...existingDisplays, newDisplay];
const previousConfig = appConfig.get();

const updatedPersonState = {
const updatedUserState = {
...previousConfig.user,
data: {
...previousConfig.user.data,
Expand All @@ -224,14 +223,14 @@ export function SurveyWebView(props: SurveyWebViewProps): JSX.Element | null {
};

const filteredSurveys = filterSurveys(
previousConfig.environment,
updatedPersonState,
previousConfig.workspace,
updatedUserState,
);

appConfig.update({
...previousConfig,
environment: previousConfig.environment,
user: updatedPersonState,
workspace: previousConfig.workspace,
user: updatedUserState,
filteredSurveys,
});
}
Expand All @@ -246,13 +245,13 @@ export function SurveyWebView(props: SurveyWebViewProps): JSX.Element | null {
};

const filteredSurveys = filterSurveys(
appConfig.get().environment,
appConfig.get().workspace,
newPersonState,
);

appConfig.update({
...appConfig.get(),
environment: appConfig.get().environment,
workspace: appConfig.get().workspace,
user: newPersonState,
filteredSurveys,
});
Expand Down Expand Up @@ -380,7 +379,7 @@ const renderHtml = (
onResponseCreated,
onClose,
};

window.formbricksSurveys.renderSurvey(surveyProps);
}

Expand Down
18 changes: 9 additions & 9 deletions packages/react-native/src/lib/common/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {
ApiSuccessResponse,
CreateOrUpdateUserResponse,
} from "@/types/api";
import type { TEnvironmentState } from "@/types/config";
import type { TWorkspaceState } from "@/types/config";
import { type ApiErrorResponse, err, ok, type Result } from "@/types/error";

export const makeRequest = async <T>(
Expand Down Expand Up @@ -57,20 +57,20 @@ export const makeRequest = async <T>(
// Simple API client using fetch
export class ApiClient {
private readonly appUrl: string;
private readonly environmentId: string;
private readonly workspaceId: string;
private readonly isDebug: boolean;

constructor({
appUrl,
environmentId,
workspaceId,
isDebug = false,
}: {
appUrl: string;
environmentId: string;
workspaceId: string;
isDebug: boolean;
}) {
this.appUrl = appUrl;
this.environmentId = environmentId;
this.workspaceId = workspaceId;
this.isDebug = isDebug;
}

Expand All @@ -82,7 +82,7 @@ export class ApiClient {
// The backend will use the JS type to determine the attribute data type
return makeRequest(
this.appUrl,
`/api/v2/client/${this.environmentId}/user`,
`/api/v2/client/${this.workspaceId}/user`,
"POST",
{
userId: userUpdateInput.userId,
Expand All @@ -92,12 +92,12 @@ export class ApiClient {
);
}

async getEnvironmentState(): Promise<
Result<TEnvironmentState, ApiErrorResponse>
async getWorkspaceState(): Promise<
Result<TWorkspaceState, ApiErrorResponse>
> {
return makeRequest(
this.appUrl,
`/api/v1/client/${this.environmentId}/environment`,
`/api/v1/client/${this.workspaceId}/environment`,
"GET",
undefined,
this.isDebug,
Expand Down
52 changes: 48 additions & 4 deletions packages/react-native/src/lib/common/config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,53 @@
/* eslint-disable no-console -- Required for error logging */
import { AsyncStorage } from "@/lib/common/storage";
import { wrapThrowsAsync } from "@/lib/common/utils";
import type { TConfig, TConfigUpdateInput } from "@/types/config";
import type {
TConfig,
TConfigUpdateInput,
TLegacyConfig,
} from "@/types/config";
import { err, ok, type Result } from "@/types/error";

export const RN_ASYNC_STORAGE_KEY = "formbricks-react-native";

/**
* Migrate AsyncStorage config from the pre-workspace rename shape:
* - `environmentId` → `workspaceId`
* - `environment` → `workspace`
* - `environment.data.project` (or legacy `workspace`) → `workspace.data.settings`
*/
const migrateLegacyConfig = (parsed: TLegacyConfig): TConfig => {
// Already in the new shape
if (parsed.workspace && parsed.workspaceId) {
return parsed;
}

const legacyEnvironment = parsed.environment;
const migratedWorkspace = legacyEnvironment
? (() => {
const envData = legacyEnvironment.data;
const settings = envData.settings ?? envData.project;
return {
expiresAt: legacyEnvironment.expiresAt,
data: {
surveys: envData.surveys,
actionClasses: envData.actionClasses,
settings: settings as TConfig["workspace"]["data"]["settings"],
},
} as TConfig["workspace"];
})()
: undefined;

const { environmentId, environment, ...rest } = parsed;

return {
...(rest as unknown as TConfig),
workspaceId:
(rest as unknown as TConfig).workspaceId ?? (environmentId as string),
...(migratedWorkspace ? { workspace: migratedWorkspace } : {}),
} as TConfig;
};

export class RNConfig {
private static instance: RNConfig | null = null;

Expand Down Expand Up @@ -57,13 +99,15 @@ export class RNConfig {
try {
const savedConfig = await AsyncStorage.getItem(RN_ASYNC_STORAGE_KEY);
if (savedConfig) {
const parsedConfig = JSON.parse(savedConfig) as TConfig;
const parsedConfig = migrateLegacyConfig(
JSON.parse(savedConfig) as TLegacyConfig,
);

// check if the config has expired
if (
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- need to check if expiresAt is set
parsedConfig.environment.expiresAt &&
new Date(parsedConfig.environment.expiresAt) <= new Date()
parsedConfig.workspace.expiresAt &&
new Date(parsedConfig.workspace.expiresAt) <= new Date()
) {
return err(new Error("Config in local storage has expired"));
}
Expand Down
16 changes: 8 additions & 8 deletions packages/react-native/src/lib/common/event-listeners.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
import {
addEnvironmentStateExpiryCheckListener,
clearEnvironmentStateExpiryCheckListener,
} from "@/lib/environment/state";
import {
addUserStateExpiryCheckListener,
clearUserStateExpiryCheckListener,
} from "@/lib/user/state";
import {
addWorkspaceStateExpiryCheckListener,
clearWorkspaceStateExpiryCheckListener,
} from "@/lib/workspace/state";

let areRemoveEventListenersAdded = false;

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

export const addCleanupEventListeners = (): void => {
if (areRemoveEventListenersAdded) return;
clearEnvironmentStateExpiryCheckListener();
clearWorkspaceStateExpiryCheckListener();
clearUserStateExpiryCheckListener();
areRemoveEventListenersAdded = true;
};

export const removeCleanupEventListeners = (): void => {
if (!areRemoveEventListenersAdded) return;
clearEnvironmentStateExpiryCheckListener();
clearWorkspaceStateExpiryCheckListener();
clearUserStateExpiryCheckListener();
areRemoveEventListenersAdded = false;
};

export const removeAllEventListeners = (): void => {
clearEnvironmentStateExpiryCheckListener();
clearWorkspaceStateExpiryCheckListener();
clearUserStateExpiryCheckListener();
removeCleanupEventListeners();
};
Loading
Loading