Skip to content
Merged
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: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"devDependencies": {
"prettier": "^3.5.3",
"turbo": "^2.5.2",
"typescript": "5.8.2"
"typescript": "5.8.2",
"rimraf": "6.0.1"
},
"packageManager": "pnpm@9.0.0",
"engines": {
Expand Down
32 changes: 25 additions & 7 deletions packages/react-native/src/components/survey-web-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ 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 appConfig = RNConfig.getInstance();
const logger = Logger.getInstance();
logger.configure({ logLevel: "debug" });

Expand All @@ -25,16 +24,27 @@ export function SurveyWebView(
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");

const project = appConfig.get().environment.data.project;
const language = appConfig.get().user.data.language;
useEffect(() => {
const fetchConfig = async () => {
const config = await RNConfig.getInstance();
setAppConfig(config);
};

void fetchConfig();
}, []);

const styling = getStyling(project, props.survey);
const isBrandingEnabled = project.inAppSurveyBranding;
const isMultiLanguageSurvey = props.survey.languages.length > 1;
const [languageCode, setLanguageCode] = useState("default");

useEffect(() => {
if (!appConfig) {
return;
}

const language = appConfig.get().user.data.language;

if (isMultiLanguageSurvey) {
const displayLanguage = getLanguageCode(props.survey, language);
if (!displayLanguage) {
Expand All @@ -51,7 +61,7 @@ export function SurveyWebView(
} else {
setIsSurveyRunning(true);
}
}, [isMultiLanguageSurvey, language, props.survey]);
}, [isMultiLanguageSurvey, props.survey, appConfig]);

useEffect(() => {
if (!isSurveyRunning) {
Expand All @@ -75,6 +85,14 @@ export function SurveyWebView(
setShowSurvey(true);
}, [props.survey.delay, isSurveyRunning, props.survey.name]);

if (!appConfig) {
return;
}

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

const onCloseSurvey = (): void => {
const { environment: environmentState, user: personState } =
appConfig.get();
Expand Down
20 changes: 14 additions & 6 deletions packages/react-native/src/lib/common/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { wrapThrowsAsync } from "@/lib/common/utils";
import { ApiResponse, ApiSuccessResponse, CreateOrUpdateUserResponse } from "@/types/api";
import {
ApiResponse,
ApiSuccessResponse,
CreateOrUpdateUserResponse,
} from "@/types/api";
import { TEnvironmentState } from "@/types/config";
import { ApiErrorResponse, Result, err, ok } from "@/types/error";

Expand Down Expand Up @@ -40,7 +44,9 @@ export const makeRequest = async <T>(
status: response.status,
message: errorResponse.message || "Something went wrong",
url,
...(Object.keys(errorResponse.details ?? {}).length > 0 && { details: errorResponse.details }),
...(Object.keys(errorResponse.details ?? {}).length > 0 && {
details: errorResponse.details,
}),
});
}

Expand All @@ -50,9 +56,9 @@ export const makeRequest = async <T>(

// Simple API client using fetch
export class ApiClient {
private appUrl: string;
private environmentId: string;
private isDebug: boolean;
private readonly appUrl: string;
private readonly environmentId: string;
private readonly isDebug: boolean;

constructor({
appUrl,
Expand Down Expand Up @@ -90,7 +96,9 @@ export class ApiClient {
);
}

async getEnvironmentState(): Promise<Result<TEnvironmentState, ApiErrorResponse>> {
async getEnvironmentState(): Promise<
Result<TEnvironmentState, ApiErrorResponse>
> {
return makeRequest(
this.appUrl,
`/api/v1/client/${this.environmentId}/environment`,
Expand Down
21 changes: 16 additions & 5 deletions packages/react-native/src/lib/common/command-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { wrapThrowsAsync } from "@/lib/common/utils";
import type { Result } from "@/types/error";

export class CommandQueue {
private queue: {
command: (...args: any[]) => Promise<Result<void, unknown>> | Result<void, unknown> | Promise<void>;
private readonly queue: {
command: (
...args: any[]
) => Promise<Result<void, unknown>> | Result<void, unknown> | Promise<void>;
checkSetup: boolean;
commandArgs: any[];
}[] = [];
Expand All @@ -14,11 +16,17 @@ export class CommandQueue {
private commandPromise: Promise<void> | null = null;

public add<A>(
command: (...args: A[]) => Promise<Result<void, unknown>> | Result<void, unknown> | Promise<void>,
command: (
...args: A[]
) => Promise<Result<void, unknown>> | Result<void, unknown> | Promise<void>,
shouldCheckSetup = true,
...args: A[]
): void {
this.queue.push({ command, checkSetup: shouldCheckSetup, commandArgs: args });
this.queue.push({
command,
checkSetup: shouldCheckSetup,
commandArgs: args,
});

if (!this.running) {
this.commandPromise = new Promise((resolve) => {
Expand Down Expand Up @@ -52,7 +60,10 @@ export class CommandQueue {
}

const executeCommand = async (): Promise<Result<void, unknown>> => {
return (await currentItem.command.apply(null, currentItem.commandArgs)) as Result<void, unknown>;
return (await currentItem.command.apply(
null,
currentItem.commandArgs
)) as Result<void, unknown>;
};

const result = await wrapThrowsAsync(executeCommand)();
Expand Down
36 changes: 20 additions & 16 deletions packages/react-native/src/lib/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,22 @@ export class RNConfig {

private config: TConfig | null = null;

private constructor() {
this.loadFromStorage()
.then((localConfig) => {
if (localConfig.ok) {
this.config = localConfig.data;
}
})
.catch((e: unknown) => {
console.error("Error loading config from storage", e);
});
}
private constructor() {}

static getInstance(): RNConfig {
if (!RNConfig.instance) {
RNConfig.instance = new RNConfig();
public async init(): Promise<void> {
try {
const localConfig = await this.loadFromStorage();
if (localConfig.ok) {
this.config = localConfig.data;
}
} catch (e: unknown) {
console.error("Error loading config from storage", e);
}
}

static async getInstance(): Promise<RNConfig> {
RNConfig.instance ??= new RNConfig();
await RNConfig.instance.init();
return RNConfig.instance;
}

Expand All @@ -46,7 +45,9 @@ export class RNConfig {

public get(): TConfig {
if (!this.config) {
throw new Error("config is null, maybe the init function was not called?");
throw new Error(
"config is null, maybe the init function was not called?"
);
}
return this.config;
}
Expand Down Expand Up @@ -77,7 +78,10 @@ export class RNConfig {

private async saveToStorage(): Promise<Result<void>> {
return wrapThrowsAsync(async () => {
await AsyncStorage.setItem(RN_ASYNC_STORAGE_KEY, JSON.stringify(this.config));
await AsyncStorage.setItem(
RN_ASYNC_STORAGE_KEY,
JSON.stringify(this.config)
);
})();
}

Expand Down
47 changes: 33 additions & 14 deletions packages/react-native/src/lib/common/file-upload.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
/* eslint-disable no-console -- used for error logging */
import { type TUploadFileConfig, type TUploadFileResponse } from "@/types/storage";
import {
type TUploadFileConfig,
type TUploadFileResponse,
} from "@/types/storage";

export class StorageAPI {
private appUrl: string;
private environmentId: string;
private readonly appUrl: string;
private readonly environmentId: string;

constructor(appUrl: string, environmentId: string) {
this.appUrl = appUrl;
Expand All @@ -29,13 +32,16 @@ export class StorageAPI {
surveyId,
};

const response = await fetch(`${this.appUrl}/api/v1/client/${this.environmentId}/storage`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
const response = await fetch(
`${this.appUrl}/api/v1/client/${this.environmentId}/storage`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
}
);

if (!response.ok) {
throw new Error(`Upload failed with status: ${String(response.status)}`);
Expand All @@ -45,7 +51,13 @@ export class StorageAPI {

const { data } = json;

const { signedUrl, fileUrl, signingData, presignedFields, updatedFileName } = data;
const {
signedUrl,
fileUrl,
signingData,
presignedFields,
updatedFileName,
} = data;

let localUploadDetails: Record<string, string> = {};

Expand Down Expand Up @@ -86,7 +98,10 @@ export class StorageAPI {

let uploadResponse: Response = {} as Response;

const signedUrlCopy = signedUrl.replace("http://localhost:3000", this.appUrl);
const signedUrlCopy = signedUrl.replace(
"http://localhost:3000",
this.appUrl
);

try {
uploadResponse = await fetch(signedUrlCopy, {
Expand Down Expand Up @@ -114,12 +129,16 @@ export class StorageAPI {
// if s3 is used, we'll use the text response:
const errorText = await uploadResponse.text();
if (presignedFields && errorText.includes("EntityTooLarge")) {
const error = new Error("File size exceeds the size limit for your plan");
const error = new Error(
"File size exceeds the size limit for your plan"
);
error.name = "FileTooLargeError";
throw error;
}

throw new Error(`Upload failed with status: ${String(uploadResponse.status)}`);
throw new Error(
`Upload failed with status: ${String(uploadResponse.status)}`
);
}

return fileUrl;
Expand Down
4 changes: 1 addition & 3 deletions packages/react-native/src/lib/common/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ export class Logger {
private logLevel: LogLevel = "error";

static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
Logger.instance ??= new Logger();
return Logger.instance;
}

Expand Down
Loading