From c8dac0c65952904aacae962e418f2c322af85fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Thu, 23 Jul 2026 12:02:06 +0200 Subject: [PATCH 01/39] refactor(sdk): return visitor data as an object from native modules --- .../FingerprintJsProProvider.test.ts | 30 +++++++++++++++---- sdk/__tests__/useVisitorData.test.ts | 24 ++++++++++++--- .../reactnative/RNFingerprintjsProModule.kt | 8 ++++- sdk/ios/RNFingerprintjsPro.swift | 14 ++++----- sdk/src/FingerprintJsProAgent.ts | 4 +-- sdk/src/types.ts | 16 ++++++---- 6 files changed, 71 insertions(+), 25 deletions(-) diff --git a/sdk/__tests__/FingerprintJsProProvider.test.ts b/sdk/__tests__/FingerprintJsProProvider.test.ts index c725ea26..5929d809 100644 --- a/sdk/__tests__/FingerprintJsProProvider.test.ts +++ b/sdk/__tests__/FingerprintJsProProvider.test.ts @@ -153,7 +153,11 @@ describe(`FingerprintJsProProvider`, () => { visitorId: mockedVisitorId, } getVisitorIdWithTimeout.mockReturnValueOnce( - Promise.resolve([mockedRequestId, mockedConfidenceScore, JSON.stringify(mockedJsonAnswer)]) + Promise.resolve({ + requestId: mockedRequestId, + confidenceScore: mockedConfidenceScore, + visitorDataJson: JSON.stringify(mockedJsonAnswer), + }) ) void fingerprintClient.getVisitorId(undefined, undefined, { timeout: getRequestTimeout }) @@ -173,7 +177,11 @@ describe(`FingerprintJsProProvider`, () => { visitorId: mockedVisitorId, } getVisitorData.mockReturnValueOnce( - Promise.resolve([mockedRequestId, mockedConfidenceScore, JSON.stringify(mockedJsonAnswer)]) + Promise.resolve({ + requestId: mockedRequestId, + confidenceScore: mockedConfidenceScore, + visitorDataJson: JSON.stringify(mockedJsonAnswer), + }) ) void fingerprintClient.getVisitorData() @@ -190,7 +198,11 @@ describe(`FingerprintJsProProvider`, () => { visitorId: mockedVisitorId, } getVisitorDataWithTimeout.mockReturnValueOnce( - Promise.resolve([mockedRequestId, mockedConfidenceScore, JSON.stringify(mockedJsonAnswer)]) + Promise.resolve({ + requestId: mockedRequestId, + confidenceScore: mockedConfidenceScore, + visitorDataJson: JSON.stringify(mockedJsonAnswer), + }) ) void fingerprintClient.getVisitorData() @@ -211,7 +223,11 @@ describe(`FingerprintJsProProvider`, () => { visitorId: mockedVisitorId, } getVisitorDataWithTimeout.mockReturnValueOnce( - Promise.resolve([mockedRequestId, mockedConfidenceScore, JSON.stringify(mockedJsonAnswer)]) + Promise.resolve({ + requestId: mockedRequestId, + confidenceScore: mockedConfidenceScore, + visitorDataJson: JSON.stringify(mockedJsonAnswer), + }) ) void fingerprintClient.getVisitorData() @@ -234,7 +250,11 @@ describe(`FingerprintJsProProvider`, () => { visitorId: mockedVisitorId, } getVisitorDataWithTimeout.mockReturnValueOnce( - Promise.resolve([mockedRequestId, mockedConfidenceScore, JSON.stringify(mockedJsonAnswer)]) + Promise.resolve({ + requestId: mockedRequestId, + confidenceScore: mockedConfidenceScore, + visitorDataJson: JSON.stringify(mockedJsonAnswer), + }) ) void fingerprintClient.getVisitorData(undefined, undefined, { timeout: getRequestTimeout }) diff --git a/sdk/__tests__/useVisitorData.test.ts b/sdk/__tests__/useVisitorData.test.ts index 7955f235..87e75d97 100644 --- a/sdk/__tests__/useVisitorData.test.ts +++ b/sdk/__tests__/useVisitorData.test.ts @@ -43,7 +43,11 @@ describe('useVisitorData', () => { } getVisitorData.mockReturnValueOnce( - Promise.resolve([mockedRequestId, mockedConfidenceScore, JSON.stringify(mockedJsonAnswer)]) + Promise.resolve({ + requestId: mockedRequestId, + confidenceScore: mockedConfidenceScore, + visitorDataJson: JSON.stringify(mockedJsonAnswer), + }) ) const wrapper = createWrapper() const { result } = renderHook(() => useVisitorData(), { wrapper }) @@ -119,7 +123,11 @@ describe('useVisitorData', () => { const mockedLinkedId = 'test_id' getVisitorData.mockReturnValueOnce( - Promise.resolve([mockedRequestId, mockedConfidenceScore, JSON.stringify(mockedJsonAnswer)]) + Promise.resolve({ + requestId: mockedRequestId, + confidenceScore: mockedConfidenceScore, + visitorDataJson: JSON.stringify(mockedJsonAnswer), + }) ) const wrapper = createWrapper() @@ -152,7 +160,11 @@ describe('useVisitorData', () => { const options: RequestOptions = { timeout: undefined } getVisitorData.mockReturnValueOnce( - Promise.resolve([mockedRequestId, mockedConfidenceScore, JSON.stringify(mockedJsonAnswer)]) + Promise.resolve({ + requestId: mockedRequestId, + confidenceScore: mockedConfidenceScore, + visitorDataJson: JSON.stringify(mockedJsonAnswer), + }) ) const wrapper = createWrapper() @@ -185,7 +197,11 @@ describe('useVisitorData', () => { const options: RequestOptions = { timeout: 15_000 } getVisitorDataWithTimeout.mockReturnValueOnce( - Promise.resolve([mockedRequestId, mockedConfidenceScore, JSON.stringify(mockedJsonAnswer)]) + Promise.resolve({ + requestId: mockedRequestId, + confidenceScore: mockedConfidenceScore, + visitorDataJson: JSON.stringify(mockedJsonAnswer), + }) ) const wrapper = createWrapper() diff --git a/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProModule.kt b/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProModule.kt index b270c82c..654f8a2a 100644 --- a/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProModule.kt +++ b/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProModule.kt @@ -108,7 +108,13 @@ class RNFingerprintjsProModule(reactContext: ReactApplicationContext) : ReactCon fun getVisitorDataWithTimeout(tags: ReadableMap?, linkedId: String?, timeout: Int?, promise: Promise) { try { val callback = { result: FingerprintJSProResponse -> - promise.resolve(Arguments.fromList(listOf(result.requestId, result.confidenceScore.score, result.asJson, result.sealedResult ?: ""))) + val visitorData = Arguments.createMap().apply { + putString("requestId", result.requestId) + putDouble("confidenceScore", result.confidenceScore.score.toDouble()) + putString("visitorDataJson", result.asJson) + putString("sealedResult", result.sealedResult ?: "") + } + promise.resolve(visitorData) } if (timeout != null) { diff --git a/sdk/ios/RNFingerprintjsPro.swift b/sdk/ios/RNFingerprintjsPro.swift index 12a4e917..23d47f54 100644 --- a/sdk/ios/RNFingerprintjsPro.swift +++ b/sdk/ios/RNFingerprintjsPro.swift @@ -57,13 +57,13 @@ class RNFingerprintjsPro: NSObject { let completionHandler: FingerprintPro.VisitorIdResponseBlock = { visitorIdResponseResult in switch visitorIdResponseResult { case let .success(visitorDataResponse): - let tuple = [ - visitorDataResponse.requestId, - visitorDataResponse.confidence, - visitorDataResponse.asJSON(), - visitorDataResponse.sealedResult, - ] as [Any] - resolve(tuple) + let visitorData: [String: Any] = [ + "requestId": visitorDataResponse.requestId, + "confidenceScore": visitorDataResponse.confidence, + "visitorDataJson": visitorDataResponse.asJSON(), + "sealedResult": visitorDataResponse.sealedResult ?? "", + ] + resolve(visitorData) case .failure(let error): let description = error.reactDescription reject("Error: ", description, error) diff --git a/sdk/src/FingerprintJsProAgent.ts b/sdk/src/FingerprintJsProAgent.ts index e200c66a..9e851f0a 100644 --- a/sdk/src/FingerprintJsProAgent.ts +++ b/sdk/src/FingerprintJsProAgent.ts @@ -95,10 +95,10 @@ export class FingerprintJsProAgent implements ProAgent { } else { visitorData = await NativeModules.RNFingerprintjsPro.getVisitorData(tags, linkedId) } - const [requestId, confidenceScore, visitorDataJsonString, sealedResult] = visitorData + const { requestId, confidenceScore, visitorDataJson, sealedResult } = visitorData // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const result = { - ...JSON.parse(visitorDataJsonString), + ...JSON.parse(visitorDataJson), requestId, confidence: { score: confidenceScore, diff --git a/sdk/src/types.ts b/sdk/src/types.ts index bb7c83ec..9e441b17 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -407,9 +407,13 @@ export interface VisitorQueryContext extends VisitorQueryResult { export type VisitorId = string -export type NativeVisitorData = [ - requestId: string, - confidenceScore: string, - visitorDataJsonString: string, - sealedResult?: string, -] +/** + * Raw visitor data returned by the native module. The visitor payload is delivered as a JSON + * string (`visitorDataJson`) that the SDK parses and merges with the top-level fields. + */ +export interface NativeVisitorData { + requestId: string + confidenceScore: number + visitorDataJson: string + sealedResult?: string +} From 5df5374874301c421a38bd7bd750985b70ec5532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Thu, 23 Jul 2026 12:41:38 +0200 Subject: [PATCH 02/39] refactor: access native module through codegen turbomodule spec --- .../FingerprintJsProProvider.test.ts | 64 ++++++------------- sdk/__tests__/setup.ts | 25 ++++---- sdk/package.json | 9 +++ sdk/src/FingerprintJsProAgent.ts | 28 +++----- sdk/src/react-native.d.ts | 24 ------- sdk/src/specs/NativeRNFingerprintjsPro.ts | 52 +++++++++++++++ sdk/src/types.ts | 11 ---- 7 files changed, 104 insertions(+), 109 deletions(-) delete mode 100644 sdk/src/react-native.d.ts create mode 100644 sdk/src/specs/NativeRNFingerprintjsPro.ts diff --git a/sdk/__tests__/FingerprintJsProProvider.test.ts b/sdk/__tests__/FingerprintJsProProvider.test.ts index 5929d809..1fd8104f 100644 --- a/sdk/__tests__/FingerprintJsProProvider.test.ts +++ b/sdk/__tests__/FingerprintJsProProvider.test.ts @@ -5,7 +5,7 @@ import { FingerprintJsProContext } from '../src/FingerprintJsProContext' import { NativeModules } from 'react-native' import { FingerprintJsProAgent } from '../src' -const { configure, getVisitorData, getVisitorIdWithTimeout, getVisitorDataWithTimeout } = +const { configure, getVisitorId, getVisitorData, getVisitorIdWithTimeout, getVisitorDataWithTimeout } = // eslint-disable-next-line @typescript-eslint/consistent-type-assertions NativeModules.RNFingerprintjsPro as unknown as Record @@ -26,7 +26,7 @@ describe(`FingerprintJsProProvider`, () => { wrapper, }) - expect(NativeModules.RNFingerprintjsPro.configure).toHaveBeenCalledWith( + expect(configure).toHaveBeenCalledWith( options.apiKey, options.region, options.endpointUrl, @@ -49,7 +49,7 @@ describe(`FingerprintJsProProvider`, () => { wrapper, }) - expect(NativeModules.RNFingerprintjsPro.configure).toHaveBeenCalledWith( + expect(configure).toHaveBeenCalledWith( options.apiKey, options.region, options.endpointUrl, @@ -72,7 +72,7 @@ describe(`FingerprintJsProProvider`, () => { wrapper, }) - expect(NativeModules.RNFingerprintjsPro.configure).toHaveBeenCalledWith( + expect(configure).toHaveBeenCalledWith( options.apiKey, options.region, options.endpointUrl, @@ -96,7 +96,7 @@ describe(`FingerprintJsProProvider`, () => { wrapper, }) - expect(NativeModules.RNFingerprintjsPro.configure).toHaveBeenCalledWith( + expect(configure).toHaveBeenCalledWith( options.apiKey, options.region, options.endpointUrl, @@ -113,7 +113,7 @@ describe(`FingerprintJsProProvider`, () => { const fingerprintClient = new FingerprintJsProAgent(options) void fingerprintClient.getVisitorId() - expect(NativeModules.RNFingerprintjsPro.getVisitorId).toHaveBeenCalledWith(undefined, undefined) + expect(getVisitorId).toHaveBeenCalledWith(null, null) }) it('should pass timeout to `getVisitorIdWithTimeout` function', () => { @@ -122,11 +122,7 @@ describe(`FingerprintJsProProvider`, () => { const fingerprintClient = new FingerprintJsProAgent(options) void fingerprintClient.getVisitorId() - expect(NativeModules.RNFingerprintjsPro.getVisitorIdWithTimeout).toHaveBeenCalledWith( - undefined, - undefined, - options.requestOptions.timeout - ) + expect(getVisitorIdWithTimeout).toHaveBeenCalledWith(null, null, options.requestOptions.timeout) }) it('should pass timeout to `getVisitorIdWithTimeout` function when timeout is 0', () => { @@ -135,11 +131,7 @@ describe(`FingerprintJsProProvider`, () => { const fingerprintClient = new FingerprintJsProAgent(options) void fingerprintClient.getVisitorId() - expect(NativeModules.RNFingerprintjsPro.getVisitorIdWithTimeout).toHaveBeenCalledWith( - undefined, - undefined, - options.requestOptions.timeout - ) + expect(getVisitorIdWithTimeout).toHaveBeenCalledWith(null, null, options.requestOptions.timeout) }) it('For `getVisitorId` function timeout from params should be more important than the timeout from client configuration', () => { @@ -162,11 +154,7 @@ describe(`FingerprintJsProProvider`, () => { void fingerprintClient.getVisitorId(undefined, undefined, { timeout: getRequestTimeout }) - expect(NativeModules.RNFingerprintjsPro.getVisitorIdWithTimeout).toHaveBeenCalledWith( - undefined, - undefined, - getRequestTimeout - ) + expect(getVisitorIdWithTimeout).toHaveBeenCalledWith(null, null, getRequestTimeout) }) it('should call `getVisitorData` function when there is no timeout', () => { @@ -186,7 +174,7 @@ describe(`FingerprintJsProProvider`, () => { void fingerprintClient.getVisitorData() - expect(NativeModules.RNFingerprintjsPro.getVisitorData).toHaveBeenCalledWith(undefined, undefined) + expect(getVisitorData).toHaveBeenCalledWith(null, null) }) it('should pass timeout to `getVisitorDataWithTimeout` function', () => { @@ -207,11 +195,7 @@ describe(`FingerprintJsProProvider`, () => { void fingerprintClient.getVisitorData() - expect(NativeModules.RNFingerprintjsPro.getVisitorDataWithTimeout).toHaveBeenCalledWith( - undefined, - undefined, - options.requestOptions.timeout - ) + expect(getVisitorDataWithTimeout).toHaveBeenCalledWith(null, null, options.requestOptions.timeout) }) it('should pass timeout to `getVisitorDataWithTimeout` function when timeout is 0', () => { @@ -232,11 +216,7 @@ describe(`FingerprintJsProProvider`, () => { void fingerprintClient.getVisitorData() - expect(NativeModules.RNFingerprintjsPro.getVisitorDataWithTimeout).toHaveBeenCalledWith( - undefined, - undefined, - options.requestOptions.timeout - ) + expect(getVisitorDataWithTimeout).toHaveBeenCalledWith(null, null, options.requestOptions.timeout) }) it('For `getVisitorData` function timeout from params should be more important than the timeout from client configuration', () => { @@ -259,11 +239,7 @@ describe(`FingerprintJsProProvider`, () => { void fingerprintClient.getVisitorData(undefined, undefined, { timeout: getRequestTimeout }) - expect(NativeModules.RNFingerprintjsPro.getVisitorDataWithTimeout).toHaveBeenCalledWith( - undefined, - undefined, - getRequestTimeout - ) + expect(getVisitorDataWithTimeout).toHaveBeenCalledWith(null, null, getRequestTimeout) }) describe('agent params changes propagation', () => { @@ -277,7 +253,7 @@ describe(`FingerprintJsProProvider`, () => { renderProvider({ apiKey: 'key-1' }) expect(configure).toHaveBeenCalledTimes(1) - expect(configure).toHaveBeenLastCalledWith('key-1', undefined, undefined, [], false, pluginVersion, false, 5000) + expect(configure).toHaveBeenLastCalledWith('key-1', null, null, [], false, pluginVersion, false, 5000) }) it('reconfigures the agent when a param changes by value', () => { @@ -287,12 +263,12 @@ describe(`FingerprintJsProProvider`, () => { rerenderWithParams({ apiKey: 'key-2' }) expect(configure).toHaveBeenCalledTimes(2) - expect(configure).toHaveBeenLastCalledWith('key-2', undefined, undefined, [], false, pluginVersion, false, 5000) + expect(configure).toHaveBeenLastCalledWith('key-2', null, null, [], false, pluginVersion, false, 5000) }) it('propagates changes across every configure argument', () => { const { rerenderWithParams } = renderProvider({ apiKey: 'key' }) - expect(configure).toHaveBeenLastCalledWith('key', undefined, undefined, [], false, pluginVersion, false, 5000) + expect(configure).toHaveBeenLastCalledWith('key', null, null, [], false, pluginVersion, false, 5000) rerenderWithParams({ apiKey: 'key', @@ -320,8 +296,8 @@ describe(`FingerprintJsProProvider`, () => { const { rerenderWithParams } = renderProvider({ apiKey: 'key', fallbackEndpointUrls: ['https://a.example'] }) expect(configure).toHaveBeenLastCalledWith( 'key', - undefined, - undefined, + null, + null, ['https://a.example'], false, pluginVersion, @@ -334,8 +310,8 @@ describe(`FingerprintJsProProvider`, () => { expect(configure).toHaveBeenCalledTimes(2) expect(configure).toHaveBeenLastCalledWith( 'key', - undefined, - undefined, + null, + null, ['https://a.example', 'https://b.example'], false, pluginVersion, diff --git a/sdk/__tests__/setup.ts b/sdk/__tests__/setup.ts index 1337e119..e6dca3fa 100644 --- a/sdk/__tests__/setup.ts +++ b/sdk/__tests__/setup.ts @@ -1,19 +1,20 @@ jest.mock('react-native', () => { - const configure = jest.fn() - const getVisitorId = jest.fn() - const getVisitorData = jest.fn() - const getVisitorDataWithTimeout = jest.fn() - const getVisitorIdWithTimeout = jest.fn() + const RNFingerprintjsPro = { + configure: jest.fn(), + getVisitorId: jest.fn(), + getVisitorData: jest.fn(), + getVisitorDataWithTimeout: jest.fn(), + getVisitorIdWithTimeout: jest.fn(), + } return { + TurboModuleRegistry: { + get: jest.fn(() => RNFingerprintjsPro), + getEnforcing: jest.fn(() => RNFingerprintjsPro), + }, + // Kept so the same mock instances remain reachable via `NativeModules` (legacy access path). NativeModules: { - RNFingerprintjsPro: { - configure, - getVisitorId, - getVisitorData, - getVisitorDataWithTimeout, - getVisitorIdWithTimeout, - }, + RNFingerprintjsPro, }, } }) diff --git a/sdk/package.json b/sdk/package.json index 0bf46f39..3074f042 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -22,6 +22,7 @@ }, "files": [ "dist", + "src/specs", "android", "ios", "RNFingerprintjsPro.podspec", @@ -31,6 +32,14 @@ "LICENSE", "CHANGELOG.md" ], + "codegenConfig": { + "name": "RNFingerprintjsProSpec", + "type": "modules", + "jsSrcsDir": "src/specs", + "android": { + "javaPackageName": "com.fingerprintjs.reactnative" + } + }, "keywords": [ "fraud", "fraud detection", diff --git a/sdk/src/FingerprintJsProAgent.ts b/sdk/src/FingerprintJsProAgent.ts index 9e851f0a..fc1c9eaf 100644 --- a/sdk/src/FingerprintJsProAgent.ts +++ b/sdk/src/FingerprintJsProAgent.ts @@ -1,14 +1,6 @@ -import { NativeModules } from 'react-native' import { UnknownError } from './errors' -import type { - FingerprintJsProAgentParams, - NativeVisitorData, - ProAgent, - RequestOptions, - Tags, - VisitorData, - VisitorId, -} from './types' +import RNFingerprintjsPro, { type NativeVisitorData } from './specs/NativeRNFingerprintjsPro' +import type { FingerprintJsProAgentParams, ProAgent, RequestOptions, Tags, VisitorData, VisitorId } from './types' import { unwrapError } from './unwrapError' import { isDefined, isTruthy } from './utils' @@ -37,10 +29,10 @@ export class FingerprintJsProAgent implements ProAgent { locationTimeoutMillisAndroid = 5000, }: FingerprintJsProAgentParams) { try { - NativeModules.RNFingerprintjsPro.configure( + RNFingerprintjsPro.configure( apiKey, - region, - endpointUrl, + region ?? null, + endpointUrl ?? null, fallbackEndpointUrls, extendedResponseFormat, packageVersion, @@ -64,10 +56,10 @@ export class FingerprintJsProAgent implements ProAgent { try { const timeout = options?.timeout ?? this.requestOptions.timeout if (isDefined(timeout)) { - return await NativeModules.RNFingerprintjsPro.getVisitorIdWithTimeout(tags, linkedId, timeout) + return await RNFingerprintjsPro.getVisitorIdWithTimeout(tags ?? null, linkedId ?? null, timeout) } - return await NativeModules.RNFingerprintjsPro.getVisitorId(tags, linkedId) + return await RNFingerprintjsPro.getVisitorId(tags ?? null, linkedId ?? null) } catch (error) { if (error instanceof Error) { throw unwrapError(error) @@ -89,11 +81,11 @@ export class FingerprintJsProAgent implements ProAgent { public async getVisitorData(tags?: Tags, linkedId?: string, options?: RequestOptions): Promise { try { const timeout = options?.timeout ?? this.requestOptions.timeout - let visitorData: NativeVisitorData | null + let visitorData: NativeVisitorData if (isDefined(timeout)) { - visitorData = await NativeModules.RNFingerprintjsPro.getVisitorDataWithTimeout(tags, linkedId, timeout) + visitorData = await RNFingerprintjsPro.getVisitorDataWithTimeout(tags ?? null, linkedId ?? null, timeout) } else { - visitorData = await NativeModules.RNFingerprintjsPro.getVisitorData(tags, linkedId) + visitorData = await RNFingerprintjsPro.getVisitorData(tags ?? null, linkedId ?? null) } const { requestId, confidenceScore, visitorDataJson, sealedResult } = visitorData // eslint-disable-next-line @typescript-eslint/consistent-type-assertions diff --git a/sdk/src/react-native.d.ts b/sdk/src/react-native.d.ts deleted file mode 100644 index 7a8af599..00000000 --- a/sdk/src/react-native.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Tags, VisitorId, NativeVisitorData } from './types' - -declare module 'react-native' { - interface RNFingerprintjsPro { - configure: ( - apiKey: string, - region?: string, - endpointUrl?: string, - fallbackEndpointUrls?: string[], - extendedResponseFormat: boolean, - packageVersion: string, - allowUseOfLocationData: boolean, - locationTimeoutMillisAndroid: number - ) => void - getVisitorId: (tags?: Tags, linkedId?: string) => Promise - getVisitorIdWithTimeout: (tags?: Tags, linkedId?: string, timeout: number) => Promise - getVisitorData: (tags?: Tags, linkedId?: string) => Promise - getVisitorDataWithTimeout: (tags?: Tags, linkedId?: string, timeout: number) => Promise - } - - export interface NativeModulesStatic { - RNFingerprintjsPro: RNFingerprintjsPro - } -} diff --git a/sdk/src/specs/NativeRNFingerprintjsPro.ts b/sdk/src/specs/NativeRNFingerprintjsPro.ts new file mode 100644 index 00000000..c14b2cb9 --- /dev/null +++ b/sdk/src/specs/NativeRNFingerprintjsPro.ts @@ -0,0 +1,52 @@ +import type { TurboModule } from 'react-native' +import { TurboModuleRegistry } from 'react-native' +import type { Double, UnsafeObject } from 'react-native/Libraries/Types/CodegenTypes' + +/** + * Raw visitor data returned by the native module. The visitor payload is delivered as a JSON + * string (`visitorDataJson`) that the SDK parses and merges with the top-level fields. + * + * Codegen requires the numeric field to be typed as {@link Double} and every field to be + * non-nullable, so `sealedResult` is an empty string (rather than absent) when unavailable. + */ +export interface NativeVisitorData { + requestId: string + confidenceScore: Double + visitorDataJson: string + sealedResult: string +} + +/** + * Codegen TurboModule spec for the `RNFingerprintjsPro` native module. + * + * Optional arguments are modelled as nullable (`| null`) rather than optional (`?`) because + * Codegen relies on positional arguments and some optional values are followed by required ones + * (e.g. `timeout`). Nullability maps onto the native modules, which already treat absent + * `tags`/`linkedId`/`region`/`endpointUrl` as "not provided". + */ +export interface Spec extends TurboModule { + configure( + apiToken: string, + region: string | null, + endpointUrl: string | null, + fallbackEndpointUrls: string[], + extendedResponseFormat: boolean, + pluginVersion: string, + allowUseOfLocationData: boolean, + locationTimeoutMillis: Double + ): void + + getVisitorId(tags: UnsafeObject | null, linkedId: string | null): Promise + + getVisitorIdWithTimeout(tags: UnsafeObject | null, linkedId: string | null, timeout: Double): Promise + + getVisitorData(tags: UnsafeObject | null, linkedId: string | null): Promise + + getVisitorDataWithTimeout( + tags: UnsafeObject | null, + linkedId: string | null, + timeout: Double + ): Promise +} + +export default TurboModuleRegistry.getEnforcing('RNFingerprintjsPro') diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 9e441b17..f5869a4f 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -406,14 +406,3 @@ export interface VisitorQueryContext extends VisitorQueryResult { } export type VisitorId = string - -/** - * Raw visitor data returned by the native module. The visitor payload is delivered as a JSON - * string (`visitorDataJson`) that the SDK parses and merges with the top-level fields. - */ -export interface NativeVisitorData { - requestId: string - confidenceScore: number - visitorDataJson: string - sealedResult?: string -} From 63b1edcb63bb3ed0e5818467279ccfc19129a176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Thu, 23 Jul 2026 14:31:28 +0200 Subject: [PATCH 03/39] feat(android): migrate native module to turbomodule (backward compatible) --- sdk/android/build.gradle | 24 +++++++++++ .../reactnative/RNFingerprintjsProModule.kt | 40 +++++++++++++------ .../reactnative/RNFingerprintjsProPackage.kt | 38 ++++++++++++++++++ .../reactnative/RNFingerprintjsProSpec.kt | 12 ++++++ .../reactnative/RNFingerprintjsProPackage.kt | 11 +++-- .../reactnative/RNFingerprintjsProSpec.kt | 36 +++++++++++++++++ 6 files changed, 145 insertions(+), 16 deletions(-) create mode 100644 sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt create mode 100644 sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt rename sdk/android/src/{main => oldarch}/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt (50%) create mode 100644 sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt diff --git a/sdk/android/build.gradle b/sdk/android/build.gradle index 4e75641d..d162c37a 100644 --- a/sdk/android/build.gradle +++ b/sdk/android/build.gradle @@ -3,6 +3,16 @@ plugins { id 'org.jetbrains.kotlin.android' } +def isNewArchitectureEnabled() { + return project.hasProperty("newArchEnabled") && project.getProperty("newArchEnabled") == "true" +} + +// On the New Architecture the React Gradle plugin runs Codegen for this library, generating the +// `NativeRNFingerprintjsProSpec` base class from `src/specs/NativeRNFingerprintjsPro.ts`. +if (isNewArchitectureEnabled()) { + apply plugin: 'com.facebook.react' +} + apply from: 'fingerprint.gradle' android { @@ -33,6 +43,20 @@ android { lint { abortOnError = false } + + sourceSets { + main { + if (isNewArchitectureEnabled()) { + java.srcDirs += [ + 'src/newarch', + // Generated Codegen sources (the `NativeRNFingerprintjsProSpec` base class). + layout.buildDirectory.dir('generated/source/codegen/java').get().asFile.path + ] + } else { + java.srcDirs += ['src/oldarch'] + } + } + } } repositories { diff --git a/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProModule.kt b/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProModule.kt index 654f8a2a..f2e5afb5 100644 --- a/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProModule.kt +++ b/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProModule.kt @@ -1,6 +1,7 @@ package com.fingerprintjs.reactnative import com.facebook.react.bridge.* +import com.facebook.react.module.annotations.ReactModule import com.fingerprintjs.android.fpjs_pro.Configuration import com.fingerprintjs.android.fpjs_pro.FingerprintJS import com.fingerprintjs.android.fpjs_pro.FingerprintJSProResponse @@ -31,15 +32,16 @@ import com.fingerprintjs.android.fpjs_pro.ProxyIntegrationSecretEnvironmentMisma import java.lang.Exception -class RNFingerprintjsProModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { +@ReactModule(name = RNFingerprintjsProModule.NAME) +class RNFingerprintjsProModule(reactContext: ReactApplicationContext) : RNFingerprintjsProSpec(reactContext) { private var fpjsClient: FingerprintJS? = null override fun getName(): String { - return "RNFingerprintjsPro" + return NAME } @ReactMethod - fun configure( + override fun configure( apiToken: String, regionKey: String?, endpointUrl: String?, @@ -71,12 +73,26 @@ class RNFingerprintjsProModule(reactContext: ReactApplicationContext) : ReactCon } @ReactMethod - fun getVisitorId(tags: ReadableMap?, linkedId: String?, promise: Promise) { - this.getVisitorIdWithTimeout(tags, linkedId, null, promise) + override fun getVisitorId(tags: ReadableMap?, linkedId: String?, promise: Promise) { + getVisitorIdInternal(tags, linkedId, null, promise) } @ReactMethod - fun getVisitorIdWithTimeout(tags: ReadableMap?, linkedId: String?, timeout: Int?, promise: Promise) { + override fun getVisitorIdWithTimeout(tags: ReadableMap?, linkedId: String?, timeout: Double, promise: Promise) { + getVisitorIdInternal(tags, linkedId, timeout.toInt(), promise) + } + + @ReactMethod + override fun getVisitorData(tags: ReadableMap?, linkedId: String?, promise: Promise) { + getVisitorDataInternal(tags, linkedId, null, promise) + } + + @ReactMethod + override fun getVisitorDataWithTimeout(tags: ReadableMap?, linkedId: String?, timeout: Double, promise: Promise) { + getVisitorDataInternal(tags, linkedId, timeout.toInt(), promise) + } + + private fun getVisitorIdInternal(tags: ReadableMap?, linkedId: String?, timeout: Int?, promise: Promise) { try { if (timeout != null) { fpjsClient?.getVisitorId( @@ -99,13 +115,7 @@ class RNFingerprintjsProModule(reactContext: ReactApplicationContext) : ReactCon } } - @ReactMethod - fun getVisitorData(tags: ReadableMap?, linkedId: String?, promise: Promise) { - this.getVisitorDataWithTimeout(tags, linkedId, null, promise) - } - - @ReactMethod - fun getVisitorDataWithTimeout(tags: ReadableMap?, linkedId: String?, timeout: Int?, promise: Promise) { + private fun getVisitorDataInternal(tags: ReadableMap?, linkedId: String?, timeout: Int?, promise: Promise) { try { val callback = { result: FingerprintJSProResponse -> val visitorData = Arguments.createMap().apply { @@ -174,4 +184,8 @@ class RNFingerprintjsProModule(reactContext: ReactApplicationContext) : ReactCon } return errorType + ":" + error.description } + + companion object { + const val NAME = "RNFingerprintjsPro" + } } diff --git a/sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt b/sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt new file mode 100644 index 00000000..a45bf803 --- /dev/null +++ b/sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt @@ -0,0 +1,38 @@ +package com.fingerprintjs.reactnative + +import com.facebook.react.BaseReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.model.ReactModuleInfo +import com.facebook.react.module.model.ReactModuleInfoProvider + + +/** + * New-architecture package. Uses `BaseReactPackage` + `ReactModuleInfoProvider` so the module is + * registered as a TurboModule. These APIs only exist on newer React Native; the old-architecture + * counterpart in `src/oldarch` uses the legacy [com.facebook.react.ReactPackage] API instead. + */ +class RNFingerprintjsProPackage : BaseReactPackage() { + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { + return if (name == RNFingerprintjsProModule.NAME) { + RNFingerprintjsProModule(reactContext) + } else { + null + } + } + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { + return ReactModuleInfoProvider { + mapOf( + RNFingerprintjsProModule.NAME to ReactModuleInfo( + RNFingerprintjsProModule.NAME, + RNFingerprintjsProModule.NAME, + false, // canOverrideExistingModule + false, // needsEagerInit + false, // isCxxModule + true // isTurboModule + ) + ) + } + } +} diff --git a/sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt b/sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt new file mode 100644 index 00000000..1e7a5c3f --- /dev/null +++ b/sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt @@ -0,0 +1,12 @@ +package com.fingerprintjs.reactnative + +import com.facebook.react.bridge.ReactApplicationContext + +/** + * New-architecture base class. Extends the Codegen-generated `NativeRNFingerprintjsProSpec` + * (produced from `src/specs/NativeRNFingerprintjsPro.ts`) so `RNFingerprintjsProModule` is a real + * TurboModule. The generated class lives in this same package (see `codegenConfig.android + * .javaPackageName` in `package.json`). + */ +abstract class RNFingerprintjsProSpec(reactContext: ReactApplicationContext) : + NativeRNFingerprintjsProSpec(reactContext) diff --git a/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt b/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt similarity index 50% rename from sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt rename to sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt index 4efd17ed..e59ad813 100644 --- a/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt +++ b/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt @@ -4,12 +4,17 @@ import com.facebook.react.ReactPackage import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.uimanager.ViewManager -import java.util.* - +/** + * Old-architecture package. Registers the module through the legacy [ReactPackage] API, which is + * available across every supported React Native version (including the oldest in the CI matrix). + * The New Architecture counterpart in `src/newarch` uses `BaseReactPackage` + + * `ReactModuleInfoProvider`, which only exist on newer React Native, so keeping the two apart + * avoids referencing APIs that are missing on old React Native. + */ class RNFingerprintjsProPackage : ReactPackage { override fun createNativeModules(reactContext: ReactApplicationContext): List { - return Arrays.asList(RNFingerprintjsProModule(reactContext)) + return listOf(RNFingerprintjsProModule(reactContext)) } override fun createViewManagers(reactContext: ReactApplicationContext): List> { diff --git a/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt b/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt new file mode 100644 index 00000000..aa7b6478 --- /dev/null +++ b/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt @@ -0,0 +1,36 @@ +package com.fingerprintjs.reactnative + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap + +/** + * Old-architecture base class. Mirrors the method surface of the Codegen-generated + * `NativeRNFingerprintjsProSpec` so `RNFingerprintjsProModule` can extend a single `...Spec` type + * regardless of the architecture. The concrete implementation annotates the overrides with + * `@ReactMethod` for the legacy bridge to pick them up. + */ +abstract class RNFingerprintjsProSpec(reactContext: ReactApplicationContext) : + ReactContextBaseJavaModule(reactContext) { + + abstract fun configure( + apiToken: String, + regionKey: String?, + endpointUrl: String?, + fallbackEndpointUrls: ReadableArray, + extendedResponseFormat: Boolean, + pluginVersion: String, + allowUseOfLocationData: Boolean, + locationTimeoutMillis: Double + ) + + abstract fun getVisitorId(tags: ReadableMap?, linkedId: String?, promise: Promise) + + abstract fun getVisitorIdWithTimeout(tags: ReadableMap?, linkedId: String?, timeout: Double, promise: Promise) + + abstract fun getVisitorData(tags: ReadableMap?, linkedId: String?, promise: Promise) + + abstract fun getVisitorDataWithTimeout(tags: ReadableMap?, linkedId: String?, timeout: Double, promise: Promise) +} From 65097ef76fb96f53a4dc52049d4fbbc463941426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Fri, 24 Jul 2026 11:27:52 +0200 Subject: [PATCH 04/39] feat(ios): migrate native module to turbomodule (backward compatible) --- sdk/RNFingerprintjsPro.podspec | 5 ++ sdk/ios/RNFingerprintjsPro.m | 41 ----------- sdk/ios/RNFingerprintjsPro.mm | 68 +++++++++++++++++++ sdk/ios/RNFingerprintjsPro.swift | 26 ++++--- .../project.pbxproj | 8 +-- 5 files changed, 94 insertions(+), 54 deletions(-) delete mode 100644 sdk/ios/RNFingerprintjsPro.m create mode 100644 sdk/ios/RNFingerprintjsPro.mm diff --git a/sdk/RNFingerprintjsPro.podspec b/sdk/RNFingerprintjsPro.podspec index 0a612662..57961ef8 100644 --- a/sdk/RNFingerprintjsPro.podspec +++ b/sdk/RNFingerprintjsPro.podspec @@ -17,4 +17,9 @@ Pod::Spec.new do |s| # See: https://guides.cocoapods.org/using/the-podfile.html#specifying-pod-versions # ~> 2.8.2 means >= 2.8.2 and < 2.9.0 (pessimistic operator) s.dependency "FingerprintPro", '~> 2.17.0' + + # Wires up the TurboModule/Codegen dependencies (ReactCommon, generated specs, ...) and defines + # `RCT_NEW_ARCH_ENABLED` for the pod when the app is built with the New Architecture. On the old + # architecture it is effectively a no-op, keeping the module backward compatible. + install_modules_dependencies(s) end diff --git a/sdk/ios/RNFingerprintjsPro.m b/sdk/ios/RNFingerprintjsPro.m deleted file mode 100644 index 9b551ae8..00000000 --- a/sdk/ios/RNFingerprintjsPro.m +++ /dev/null @@ -1,41 +0,0 @@ -#import - -@interface RCT_EXTERN_MODULE(RNFingerprintjsPro, NSObject) - -RCT_EXTERN_METHOD(configure:(NSString *)apiToken - region:(NSString *)region - endpoint:(NSString *)endpointUrl - endpointFallbacks:(NSArray *)fallbackEndpointUrls - extendedResponseFormat:(BOOL)extendedResponseFormat - pluginVersion:(NSString *)pluginVersion - allowUseOfLocationData:(BOOL)allowUseOfLocationData - locationTimeoutMillis:(nonnull NSNumber)locationTimeoutMillis -) - -RCT_EXTERN_METHOD(getVisitorId:(NSDictionary *)tags - linkedId:(NSString *)linkedId - resolve:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject -) - -RCT_EXTERN_METHOD(getVisitorIdWithTimeout:(NSDictionary *)tags - linkedId:(NSString *)linkedId - timeout:(nonnull NSNumber)timeout - resolve:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject -) - -RCT_EXTERN_METHOD(getVisitorData:(NSDictionary *)tags - linkedId:(NSString *)linkedId - resolve:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject -) - -RCT_EXTERN_METHOD(getVisitorDataWithTimeout:(NSDictionary *)tags - linkedId:(NSString *)linkedId - timeout:(nonnull NSNumber)timeout - resolve:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject -) - -@end diff --git a/sdk/ios/RNFingerprintjsPro.mm b/sdk/ios/RNFingerprintjsPro.mm new file mode 100644 index 00000000..b4d2ef19 --- /dev/null +++ b/sdk/ios/RNFingerprintjsPro.mm @@ -0,0 +1,68 @@ +#import + +@interface RCT_EXTERN_MODULE(RNFingerprintjsPro, NSObject) + +RCT_EXTERN_METHOD(configure:(NSString *)apiToken + region:(NSString *)region + endpointUrl:(NSString *)endpointUrl + fallbackEndpointUrls:(NSArray *)fallbackEndpointUrls + extendedResponseFormat:(BOOL)extendedResponseFormat + pluginVersion:(NSString *)pluginVersion + allowUseOfLocationData:(BOOL)allowUseOfLocationData + locationTimeoutMillis:(double)locationTimeoutMillis +) + +RCT_EXTERN_METHOD(getVisitorId:(NSDictionary *)tags + linkedId:(NSString *)linkedId + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +) + +RCT_EXTERN_METHOD(getVisitorIdWithTimeout:(NSDictionary *)tags + linkedId:(NSString *)linkedId + timeout:(double)timeout + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +) + +RCT_EXTERN_METHOD(getVisitorData:(NSDictionary *)tags + linkedId:(NSString *)linkedId + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +) + +RCT_EXTERN_METHOD(getVisitorDataWithTimeout:(NSDictionary *)tags + linkedId:(NSString *)linkedId + timeout:(double)timeout + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +) + +@end + +#ifdef RCT_NEW_ARCH_ENABLED +// On the New Architecture, expose the Swift class as a real TurboModule backed by the +// Codegen-generated spec. The Swift implementation already exposes the exact selectors the +// generated `NativeRNFingerprintjsProSpecJSI` invokes. +#import + +// Import the Swift-generated interface so the category below can extend the Swift class. +#if __has_include("RNFingerprintjsPro-Swift.h") +#import "RNFingerprintjsPro-Swift.h" +#else +#import +#endif + +@interface RNFingerprintjsPro (TurboModule) +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params; +@end + +@implementation RNFingerprintjsPro (TurboModule) +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} +@end +#endif diff --git a/sdk/ios/RNFingerprintjsPro.swift b/sdk/ios/RNFingerprintjsPro.swift index 23d47f54..02f99a02 100644 --- a/sdk/ios/RNFingerprintjsPro.swift +++ b/sdk/ios/RNFingerprintjsPro.swift @@ -11,21 +11,25 @@ class RNFingerprintjsPro: NSObject { @objc static func requiresMainQueueSetup() -> Bool { false } - @objc(configure:region:endpoint:endpointFallbacks:extendedResponseFormat:pluginVersion:allowUseOfLocationData:locationTimeoutMillis:) - public func configure(_ apiToken: String, _ region: String? = "us", _ endpoint: String?, _ endpointFallbacks: [String] = [], _ extendedResponseFormat: Bool = false, _ pluginVersion: String, allowUseOfLocationData: Bool = false, locationTimeoutMillis: NSNumber?) -> Void { - let region = RNFingerprintjsPro.parseRegion(region, endpoint: endpoint, endpointFallbacks: endpointFallbacks) + @objc(configure:region:endpointUrl:fallbackEndpointUrls:extendedResponseFormat:pluginVersion:allowUseOfLocationData:locationTimeoutMillis:) + public func configure(_ apiToken: String, _ region: String?, _ endpointUrl: String?, _ fallbackEndpointUrls: [String], _ extendedResponseFormat: Bool, _ pluginVersion: String, _ allowUseOfLocationData: Bool, _ locationTimeoutMillis: Double) -> Void { + let region = RNFingerprintjsPro.parseRegion(region, endpoint: endpointUrl, endpointFallbacks: fallbackEndpointUrls) let integrationInfo = [("fingerprint-pro-react-native", pluginVersion)] let configuration = Configuration(apiKey: apiToken, region: region, integrationInfo: integrationInfo, extendedResponseFormat: extendedResponseFormat, allowUseOfLocationData: allowUseOfLocationData) fpjsClient = FingerprintProFactory.getInstance(configuration) } - @objc(getVisitorId:linkedId:resolve:rejecter:) + @objc(getVisitorId:linkedId:resolve:reject:) public func getVisitorId(tags: [String: Any]?, linkedId: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { self.getVisitorId(tags: tags, linkedId: linkedId, timeout: nil, resolve: resolve, reject: reject) } - @objc(getVisitorIdWithTimeout:linkedId:timeout:resolve:rejecter:) - public func getVisitorId(tags: [String: Any]?, linkedId: String?, timeout: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { + @objc(getVisitorIdWithTimeout:linkedId:timeout:resolve:reject:) + public func getVisitorId(tags: [String: Any]?, linkedId: String?, timeout: Double, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { + self.getVisitorId(tags: tags, linkedId: linkedId, timeout: NSNumber(value: timeout), resolve: resolve, reject: reject) + } + + private func getVisitorId(tags: [String: Any]?, linkedId: String?, timeout: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { let metadata = RNFingerprintjsPro.prepareMetadata(linkedId, tags: tags) let completionHandler: FingerprintPro.VisitorIdBlock = { visitorIdResult in @@ -45,13 +49,17 @@ class RNFingerprintjsPro: NSObject { } } - @objc(getVisitorData:linkedId:resolve:rejecter:) + @objc(getVisitorData:linkedId:resolve:reject:) public func getVisitorData(tags: [String: Any]?, linkedId: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { self.getVisitorData(tags: tags, linkedId: linkedId, timeout: nil, resolve: resolve, reject: reject) } - @objc(getVisitorDataWithTimeout:linkedId:timeout:resolve:rejecter:) - public func getVisitorData(tags: [String: Any]?, linkedId: String?, timeout: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { + @objc(getVisitorDataWithTimeout:linkedId:timeout:resolve:reject:) + public func getVisitorData(tags: [String: Any]?, linkedId: String?, timeout: Double, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { + self.getVisitorData(tags: tags, linkedId: linkedId, timeout: NSNumber(value: timeout), resolve: resolve, reject: reject) + } + + private func getVisitorData(tags: [String: Any]?, linkedId: String?, timeout: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { let metadata = RNFingerprintjsPro.prepareMetadata(linkedId, tags: tags) let completionHandler: FingerprintPro.VisitorIdResponseBlock = { visitorIdResponseResult in diff --git a/sdk/ios/RNFingerprintjsPro.xcodeproj/project.pbxproj b/sdk/ios/RNFingerprintjsPro.xcodeproj/project.pbxproj index 5561528e..c67aef03 100644 --- a/sdk/ios/RNFingerprintjsPro.xcodeproj/project.pbxproj +++ b/sdk/ios/RNFingerprintjsPro.xcodeproj/project.pbxproj @@ -8,7 +8,7 @@ /* Begin PBXBuildFile section */ 10F8301B27A9CBEC00149386 /* RNFingerprintjsPro.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F8301A27A9CBEC00149386 /* RNFingerprintjsPro.swift */; }; - B3E7B58A1CC2AC0600A0062D /* RNFingerprintjsPro.m in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* RNFingerprintjsPro.m */; }; + B3E7B58A1CC2AC0600A0062D /* RNFingerprintjsPro.mm in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* RNFingerprintjsPro.mm */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -28,7 +28,7 @@ 10F8301A27A9CBEC00149386 /* RNFingerprintjsPro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNFingerprintjsPro.swift; sourceTree = ""; }; 134814201AA4EA6300B7C361 /* libRNFingerprintjsPro.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNFingerprintjsPro.a; sourceTree = BUILT_PRODUCTS_DIR; }; 7DC52BF728242E1900F78B99 /* RNFingerprintjsPro.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = RNFingerprintjsPro.podspec; path = ../RNFingerprintjsPro.podspec; sourceTree = ""; }; - B3E7B5891CC2AC0600A0062D /* RNFingerprintjsPro.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFingerprintjsPro.m; sourceTree = ""; }; + B3E7B5891CC2AC0600A0062D /* RNFingerprintjsPro.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RNFingerprintjsPro.mm; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -54,7 +54,7 @@ isa = PBXGroup; children = ( 7DC52BF728242E1900F78B99 /* RNFingerprintjsPro.podspec */, - B3E7B5891CC2AC0600A0062D /* RNFingerprintjsPro.m */, + B3E7B5891CC2AC0600A0062D /* RNFingerprintjsPro.mm */, 10F8301A27A9CBEC00149386 /* RNFingerprintjsPro.swift */, 134814211AA4EA7D00B7C361 /* Products */, 10F8301927A9CBEC00149386 /* RNFingerprintjsPro-Bridging-Header.h */, @@ -119,7 +119,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - B3E7B58A1CC2AC0600A0062D /* RNFingerprintjsPro.m in Sources */, + B3E7B58A1CC2AC0600A0062D /* RNFingerprintjsPro.mm in Sources */, 10F8301B27A9CBEC00149386 /* RNFingerprintjsPro.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; From a565e98a7ba251527c0769767b8cc76f862810f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Fri, 24 Jul 2026 14:44:53 +0200 Subject: [PATCH 05/39] feat(e2e-app): add architecture info component --- e2e-app/App.tsx | 2 ++ e2e-app/src/components/ArchInfo.tsx | 13 +++++++++++++ e2e-app/src/global.d.ts | 6 ++++++ 3 files changed, 21 insertions(+) create mode 100644 e2e-app/src/components/ArchInfo.tsx create mode 100644 e2e-app/src/global.d.ts diff --git a/e2e-app/App.tsx b/e2e-app/App.tsx index 14f780cc..d67cecf5 100644 --- a/e2e-app/App.tsx +++ b/e2e-app/App.tsx @@ -6,6 +6,7 @@ import { testIds } from '@/e2e/ids' import { useEffect } from 'react' import { testTags } from '@/e2e/tags' import config from '@/src/config' +import { ArchInfo } from '@/src/components/ArchInfo' function InnerApp() { const { isLoading, error, data, getData } = useVisitorData() @@ -27,6 +28,7 @@ function InnerApp() { flex: 1, }} > + {isLoading ? Loading... : null} {error ? ( diff --git a/e2e-app/src/components/ArchInfo.tsx b/e2e-app/src/components/ArchInfo.tsx new file mode 100644 index 00000000..278be7b0 --- /dev/null +++ b/e2e-app/src/components/ArchInfo.tsx @@ -0,0 +1,13 @@ +import { Text, View } from 'react-native' + +export function ArchInfo() { + const hasTurboModules = global.__turboModuleProxy !== null + const isBridgeless = global.RN$Bridgeless + + return ( + + {`TurboModules: ${hasTurboModules}`} + {`Bridgeless: ${isBridgeless}`} + + ) +} diff --git a/e2e-app/src/global.d.ts b/e2e-app/src/global.d.ts new file mode 100644 index 00000000..c749b701 --- /dev/null +++ b/e2e-app/src/global.d.ts @@ -0,0 +1,6 @@ +declare global { + var __turboModuleProxy: unknown + var RN$Bridgeless: boolean +} + +export {} From d35f521eb38a0dbaa17b1a89f697073e348da283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Fri, 24 Jul 2026 15:03:04 +0200 Subject: [PATCH 06/39] fix(sdk): exclude build artifacts from pod source files --- sdk/RNFingerprintjsPro.podspec | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sdk/RNFingerprintjsPro.podspec b/sdk/RNFingerprintjsPro.podspec index 57961ef8..7e36e6fe 100644 --- a/sdk/RNFingerprintjsPro.podspec +++ b/sdk/RNFingerprintjsPro.podspec @@ -11,6 +11,11 @@ Pod::Spec.new do |s| s.tvos.deployment_target = "15.0" s.source_files = "ios/**/*.{h,m,mm,swift}" + # Never sweep build artifacts (e.g. generated Codegen headers under ios/build) into the pod's + # sources. Otherwise CocoaPods exposes the C++ `*Spec.h`/`*SpecJSI.h` as public headers of this + # pod, and building the Swift pod's ObjC module (`-import-underlying-module`) tries to compile + # them as Obj-C, failing with "This file must be compiled as Obj-C++". + s.exclude_files = "ios/build/**/*" # s.requires_arc = true s.dependency "React-Core" From 1659c3ecd9c6fabb8f933a40f83c84d3bad195e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Fri, 24 Jul 2026 15:03:19 +0200 Subject: [PATCH 07/39] chore: add temporary new/old arch logs --- .../fingerprintjs/reactnative/RNFingerprintjsProPackage.kt | 5 +++++ .../fingerprintjs/reactnative/RNFingerprintjsProPackage.kt | 4 ++++ sdk/ios/RNFingerprintjsPro.mm | 3 +++ sdk/ios/RNFingerprintjsPro.swift | 7 +++++++ 4 files changed, 19 insertions(+) diff --git a/sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt b/sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt index a45bf803..778ab5cb 100644 --- a/sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt +++ b/sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt @@ -1,5 +1,6 @@ package com.fingerprintjs.reactnative +import android.util.Log import com.facebook.react.BaseReactPackage import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext @@ -15,6 +16,10 @@ import com.facebook.react.module.model.ReactModuleInfoProvider class RNFingerprintjsProPackage : BaseReactPackage() { override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { return if (name == RNFingerprintjsProModule.NAME) { + // TEMP(new-arch verification): this new-arch-only package is used only when the module + // is resolved as a TurboModule (New Architecture). The old-arch build uses the legacy + // `ReactPackage` in `src/oldarch` instead. + Log.d("RNFingerprintjsPro", "getModule via new-arch package — running as a TurboModule (New Architecture)") RNFingerprintjsProModule(reactContext) } else { null diff --git a/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt b/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt index e59ad813..5be66e6c 100644 --- a/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt +++ b/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt @@ -1,5 +1,6 @@ package com.fingerprintjs.reactnative +import android.util.Log import com.facebook.react.ReactPackage import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext @@ -14,6 +15,9 @@ import com.facebook.react.uimanager.ViewManager */ class RNFingerprintjsProPackage : ReactPackage { override fun createNativeModules(reactContext: ReactApplicationContext): List { + // TEMP(old-arch verification): this legacy `ReactPackage` is only compiled/used on the Old + // Architecture. The new-arch build uses `BaseReactPackage` in `src/newarch` instead. + Log.d("RNFingerprintjsPro", "createNativeModules via legacy ReactPackage — running on the Old Architecture") return listOf(RNFingerprintjsProModule(reactContext)) } diff --git a/sdk/ios/RNFingerprintjsPro.mm b/sdk/ios/RNFingerprintjsPro.mm index b4d2ef19..5f249b7e 100644 --- a/sdk/ios/RNFingerprintjsPro.mm +++ b/sdk/ios/RNFingerprintjsPro.mm @@ -62,6 +62,9 @@ @implementation RNFingerprintjsPro (TurboModule) - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { + // TEMP(new-arch verification): if this fires, the module is instantiated as a real TurboModule + // via the Codegen-generated JSI class (not the legacy interop shim). + NSLog(@"[RNFingerprintjsPro] getTurboModule called — running as a TurboModule (New Architecture)"); return std::make_shared(params); } @end diff --git a/sdk/ios/RNFingerprintjsPro.swift b/sdk/ios/RNFingerprintjsPro.swift index 02f99a02..e72d693c 100644 --- a/sdk/ios/RNFingerprintjsPro.swift +++ b/sdk/ios/RNFingerprintjsPro.swift @@ -2,6 +2,7 @@ // RNFingerprintjsPro.swift // RNFingerprintjsPro // +import Foundation import FingerprintPro @objc(RNFingerprintjsPro) @@ -13,6 +14,12 @@ class RNFingerprintjsPro: NSObject { @objc(configure:region:endpointUrl:fallbackEndpointUrls:extendedResponseFormat:pluginVersion:allowUseOfLocationData:locationTimeoutMillis:) public func configure(_ apiToken: String, _ region: String?, _ endpointUrl: String?, _ fallbackEndpointUrls: [String], _ extendedResponseFormat: Bool, _ pluginVersion: String, _ allowUseOfLocationData: Bool, _ locationTimeoutMillis: Double) -> Void { + #if !RCT_NEW_ARCH_ENABLED + // TEMP(old-arch verification): compiled only when `RCT_NEW_ARCH_ENABLED` is NOT set, i.e. the + // module is registered via the legacy `RCT_EXTERN_MODULE` bridge (Old Architecture). On the + // New Architecture, `getTurboModule` in RNFingerprintjsPro.mm logs instead. + NSLog("[RNFingerprintjsPro] configure via legacy bridge — running on the Old Architecture") + #endif let region = RNFingerprintjsPro.parseRegion(region, endpoint: endpointUrl, endpointFallbacks: fallbackEndpointUrls) let integrationInfo = [("fingerprint-pro-react-native", pluginVersion)] let configuration = Configuration(apiKey: apiToken, region: region, integrationInfo: integrationInfo, extendedResponseFormat: extendedResponseFormat, allowUseOfLocationData: allowUseOfLocationData) From 504b63effb97395413f3ac8daa3c6f84a0703417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Fri, 24 Jul 2026 15:05:34 +0200 Subject: [PATCH 08/39] chore(e2e-tests): remove react-native pinning step --- .github/workflows/e2e-tests.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index d191cd54..0ea73634 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -68,19 +68,13 @@ jobs: run: pnpm exec expo install --fix working-directory: e2e-app - # `expo install --fix` aligns every bundled package to the Expo SDK, including coercing - # react-native to the SDK's bundled version. Re-pin it to the version under test so the - # matrix actually exercises that RN version rather than the SDK's default. - - name: Pin react-native to the tested version - if: matrix.react-native != 'local' - run: pnpm add react-native@${{ matrix.react-native }} - working-directory: e2e-app - - name: Setup fresh node_modules if: matrix.react-native != 'local' run: rm -rf node_modules && pnpm install --frozen-lockfile working-directory: e2e-app + - run: pnpm why react-native + - name: Prebuild run: pnpm prebuild working-directory: e2e-app From 5dbc29c73047807f333efc07a2cdd08947be666e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Fri, 24 Jul 2026 15:31:32 +0200 Subject: [PATCH 09/39] chore(android): update gradle config for architecture compatibility --- sdk/android/gradle.properties | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/sdk/android/gradle.properties b/sdk/android/gradle.properties index fa4feae5..b441ddb9 100644 --- a/sdk/android/gradle.properties +++ b/sdk/android/gradle.properties @@ -32,9 +32,8 @@ FLIPPER_VERSION=0.125.0 # ./gradlew -PreactNativeArchitectures=x86_64 reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 -# Use this property to enable support to the new architecture. -# This will allow you to use TurboModules and the Fabric render in -# your application. You should enable this flag either if you want -# to write custom TurboModules/Fabric components OR use libraries that -# are providing them. -newArchEnabled=false +# NOTE: `newArchEnabled` is intentionally NOT set here. This library must follow the architecture +# of the consuming app. Hardcoding it (e.g. `newArchEnabled=false`) shadows the app's value for +# this subproject on some React Native versions, so the library would skip Codegen while the app +# still autolinks it as a TurboModule — breaking the app's native build. The value is inherited +# from the consuming app's gradle.properties instead (see `isNewArchitectureEnabled()` in build.gradle). From a993f0b90724326613182f4d5c1ae422e7731ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Wed, 29 Jul 2026 12:56:08 +0200 Subject: [PATCH 10/39] chore(e2e-tests): pin react-native version and hoist deps --- .github/workflows/e2e-tests.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 0ea73634..a896cf10 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -68,15 +68,20 @@ jobs: run: pnpm exec expo install --fix working-directory: e2e-app + - name: Pin react-native to the tested version + if: matrix.react-native != 'local' + run: pnpm add react-native@${{ matrix.react-native }} + working-directory: e2e-app + - name: Setup fresh node_modules if: matrix.react-native != 'local' - run: rm -rf node_modules && pnpm install --frozen-lockfile + run: rm -rf node_modules && pnpm install --frozen-lockfile --shamefully-hoist working-directory: e2e-app - run: pnpm why react-native - name: Prebuild - run: pnpm prebuild + run: pnpm prebuild && pnpm install --frozen-lockfile --shamefully-hoist working-directory: e2e-app - run: pnpm exec detox build --configuration android.emu.release @@ -268,11 +273,11 @@ jobs: - name: Setup fresh node_modules if: matrix.react-native != 'local' - run: rm -rf node_modules && pnpm install --frozen-lockfile + run: rm -rf node_modules && pnpm install --frozen-lockfile --shamefully-hoist working-directory: e2e-app - name: Prebuild - run: pnpm prebuild + run: pnpm prebuild && pnpm install --frozen-lockfile --shamefully-hoist working-directory: e2e-app - run: pnpm exec detox build --configuration ios.sim.release From 5d8c1cff927f095658535f3c95ecce069af01b8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Wed, 29 Jul 2026 13:04:50 +0200 Subject: [PATCH 11/39] chore(e2e-tests): add dependency inspection and pod install steps --- .github/workflows/e2e-tests.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index a896cf10..a5b0bdd0 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -276,9 +276,14 @@ jobs: run: rm -rf node_modules && pnpm install --frozen-lockfile --shamefully-hoist working-directory: e2e-app + - run: pnpm why react-native + - name: Prebuild run: pnpm prebuild && pnpm install --frozen-lockfile --shamefully-hoist working-directory: e2e-app + + - run: pod install + working-directory: e2e-app/ios - run: pnpm exec detox build --configuration ios.sim.release working-directory: e2e-app From 8d0ddd8a859704b483bb9a9b12527195986ba241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Wed, 29 Jul 2026 13:38:46 +0200 Subject: [PATCH 12/39] chore(e2e-tests): remove shameful hoisting and pod install steps --- .github/workflows/e2e-tests.yml | 11 ++++------- e2e-app/.npmrc | 1 + 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index a5b0bdd0..a304d99e 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -75,13 +75,13 @@ jobs: - name: Setup fresh node_modules if: matrix.react-native != 'local' - run: rm -rf node_modules && pnpm install --frozen-lockfile --shamefully-hoist + run: rm -rf node_modules && pnpm install --frozen-lockfile working-directory: e2e-app - run: pnpm why react-native - name: Prebuild - run: pnpm prebuild && pnpm install --frozen-lockfile --shamefully-hoist + run: pnpm prebuild working-directory: e2e-app - run: pnpm exec detox build --configuration android.emu.release @@ -273,17 +273,14 @@ jobs: - name: Setup fresh node_modules if: matrix.react-native != 'local' - run: rm -rf node_modules && pnpm install --frozen-lockfile --shamefully-hoist + run: rm -rf node_modules && pnpm install --frozen-lockfile working-directory: e2e-app - run: pnpm why react-native - name: Prebuild - run: pnpm prebuild && pnpm install --frozen-lockfile --shamefully-hoist + run: pnpm prebuild working-directory: e2e-app - - - run: pod install - working-directory: e2e-app/ios - run: pnpm exec detox build --configuration ios.sim.release working-directory: e2e-app diff --git a/e2e-app/.npmrc b/e2e-app/.npmrc index 10b8fd75..84dd719e 100644 --- a/e2e-app/.npmrc +++ b/e2e-app/.npmrc @@ -1,2 +1,3 @@ node-linker=hoisted public-hoist-pattern[]=* +shamefully-hoist=true From a97dde49332110057bacdcbfba1bf77ff6df3f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Wed, 29 Jul 2026 13:41:16 +0200 Subject: [PATCH 13/39] chore(e2e-app): update pnpm config to re-enable shameful hoisting --- e2e-app/.npmrc | 1 - e2e-app/pnpm-workspace.yaml | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/e2e-app/.npmrc b/e2e-app/.npmrc index 84dd719e..10b8fd75 100644 --- a/e2e-app/.npmrc +++ b/e2e-app/.npmrc @@ -1,3 +1,2 @@ node-linker=hoisted public-hoist-pattern[]=* -shamefully-hoist=true diff --git a/e2e-app/pnpm-workspace.yaml b/e2e-app/pnpm-workspace.yaml index c2741eb7..b22abf00 100644 --- a/e2e-app/pnpm-workspace.yaml +++ b/e2e-app/pnpm-workspace.yaml @@ -4,3 +4,5 @@ allowBuilds: detox: true dtrace-provider: true esbuild: true + +shamefullyHoist: true \ No newline at end of file From 217f6d37d13ef1c2888d6436a8256b87456bb998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Wed, 29 Jul 2026 15:18:31 +0200 Subject: [PATCH 14/39] chore(e2e-tests): add missing working-directory in steps --- .github/workflows/e2e-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index a304d99e..e72577d6 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -79,6 +79,7 @@ jobs: working-directory: e2e-app - run: pnpm why react-native + working-directory: e2e-app - name: Prebuild run: pnpm prebuild @@ -277,6 +278,7 @@ jobs: working-directory: e2e-app - run: pnpm why react-native + working-directory: e2e-app - name: Prebuild run: pnpm prebuild From d2bf27514757d0244a43d33a5ed41221bf901422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Fri, 31 Jul 2026 11:15:26 +0200 Subject: [PATCH 15/39] feat(e2e-app): add getData performance tracking logic --- e2e-app/App.tsx | 139 ++++++++++++++++++++++++++++---- e2e-app/e2e/ids.ts | 8 ++ e2e-app/e2e/performance.test.ts | 61 ++++++++++++++ 3 files changed, 192 insertions(+), 16 deletions(-) create mode 100644 e2e-app/e2e/performance.test.ts diff --git a/e2e-app/App.tsx b/e2e-app/App.tsx index d67cecf5..19c93b4b 100644 --- a/e2e-app/App.tsx +++ b/e2e-app/App.tsx @@ -3,14 +3,77 @@ import { Pressable, SafeAreaView, Text, View } from 'react-native' import { FingerprintJsProProvider, useVisitorData } from '@fingerprintjs/fingerprintjs-pro-react-native' import { testIds } from '@/e2e/ids' -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import { testTags } from '@/e2e/tags' import config from '@/src/config' import { ArchInfo } from '@/src/components/ArchInfo' +export type BenchmarkStats = { + min: number + max: number + avg: number + p50: number + p95: number +} + function InnerApp() { const { isLoading, error, data, getData } = useVisitorData() + const [isBenchmarking, setIsBenchmarking] = useState(false) + const [benchmarkProgress, setBenchmarkProgress] = useState(null) + const [benchmarkResults, setBenchmarkResults] = useState(null) + const [benchmarkError, setBenchmarkError] = useState(null) + + const doGetData = async () => { + const tags = config.useTags ? testTags : undefined + + await getData(tags, config.linkedId) + } + + const doMeasurePerformance = async () => { + setIsBenchmarking(true) + setBenchmarkResults(null) + setBenchmarkError(null) + setBenchmarkProgress('Starting benchmark...') + + const iterations = 100 + const durations: number[] = [] + const tags = config.useTags ? testTags : undefined + + try { + for (let i = 0; i < iterations; i++) { + setBenchmarkProgress(`Running iteration ${i + 1}/${iterations}...`) + + const start = performance.now() + await getData(tags, config.linkedId) + const end = performance.now() + + durations.push(end - start) + + // Give the JS thread a small break to update state and render progress + await new Promise((resolve) => { + setTimeout(resolve, 50) + }) + } + + const min = Math.min(...durations) + const max = Math.max(...durations) + const avg = durations.reduce((a, b) => a + b, 0) / durations.length + const sorted = [...durations].sort((a, b) => a - b) + const p50 = sorted[Math.floor(durations.length * 0.5)] + const p95 = sorted[Math.floor(durations.length * 0.95)] + + setBenchmarkResults({ min, max, avg, p50, p95 }) + setBenchmarkProgress(null) + } catch (err) { + console.error(err) + setBenchmarkError(err instanceof Error ? err.message : String(err)) + setBenchmarkProgress(null) + } finally { + setIsBenchmarking(false) + } + } + useEffect(() => { if (error) { console.error(error) @@ -19,7 +82,7 @@ function InnerApp() { return ( - {/* eslint-disable-next-line @typescript-eslint/no-deprecated */} + {} ) : null} {data ? {JSON.stringify(data)} : null} - { - const tags = config.useTags ? testTags : undefined - - await getData(tags, config.linkedId) - }} - style={{ - paddingBottom: 48, - paddingLeft: 24, - }} - > - Get data - + + + + Get data + + + + + {isBenchmarking ? 'Measuring...' : 'Measure Performance'} + + + + + {benchmarkProgress ? ( + + {benchmarkProgress} + + ) : null} + + {benchmarkError ? Error: {benchmarkError} : null} + + {benchmarkResults ? ( + + + Benchmark Results (100 runs) + + Min: {benchmarkResults.min.toFixed(2)}ms + Max: {benchmarkResults.max.toFixed(2)}ms + Median (p50): {benchmarkResults.p50.toFixed(2)}ms + 95th Percentile: {benchmarkResults.p95.toFixed(2)}ms + + Avg: {benchmarkResults.avg.toFixed(2)}ms + + + ) : null} ) diff --git a/e2e-app/e2e/ids.ts b/e2e-app/e2e/ids.ts index 9e4473fd..31763e36 100644 --- a/e2e-app/e2e/ids.ts +++ b/e2e-app/e2e/ids.ts @@ -6,4 +6,12 @@ export const testIds = { errorCause: 'error-cause', data: 'data', getData: 'get-data', + getDataDuration: 'get-data-duration', + measurePerformance: 'measure-performance', + benchmarkProgress: 'benchmark-progress', + benchmarkMin: 'benchmark-min', + benchmarkMax: 'benchmark-max', + benchmarkAvg: 'benchmark-avg', + benchmarkP50: 'benchmark-p50', + benchmarkP95: 'benchmark-p95', } diff --git a/e2e-app/e2e/performance.test.ts b/e2e-app/e2e/performance.test.ts new file mode 100644 index 00000000..3aa6d26d --- /dev/null +++ b/e2e-app/e2e/performance.test.ts @@ -0,0 +1,61 @@ +import { by, element, waitFor, device } from 'detox' +import { it, describe, beforeAll } from '@jest/globals' +import { testIds } from './ids' +import { getElementText } from './getElementText' +import { wait } from './wait' +import { Config } from '@/src/config.types' + +const apiKey = process.env.MINIMUM_US_DEFAULT_PUBLIC_KEY +const region = 'us' + +async function launchApp() { + if (!apiKey) { + throw new Error('MINIMUM_US_DEFAULT_PUBLIC_KEY is required to run performance tests') + } + + await device.launchApp({ + newInstance: true, + launchArgs: { + apiKey, + region, + } as Config, + }) + await wait(4000) +} + +describe('SDK Architecture Performance Benchmark', () => { + beforeAll(async () => { + await launchApp() + }) + + it('triggers the Measure Performance button and reports final statistics', async () => { + console.log('Tapping the Measure Performance button in the app...') + await element(by.id(testIds.measurePerformance)).tap() + + console.log('Waiting for the 100-run benchmark to complete...') + + // 100 runs can take ~20-30 seconds depending on network conditions. Wait up to 90 seconds. + await waitFor(element(by.id(testIds.benchmarkAvg))) + .toExist() + .withTimeout(90_000) + + console.log('Benchmark complete! Reading statistics...') + + const minText = await getElementText(element(by.id(testIds.benchmarkMin))) + const maxText = await getElementText(element(by.id(testIds.benchmarkMax))) + const p50Text = await getElementText(element(by.id(testIds.benchmarkP50))) + const p95Text = await getElementText(element(by.id(testIds.benchmarkP95))) + const avgText = await getElementText(element(by.id(testIds.benchmarkAvg))) + + const archText = global.__turboModuleProxy !== null ? 'TurboModules (New Arch)' : 'Bridge (Old Arch)' + + console.log('\n==================================================') + console.log(`BENCHMARK RESULTS FROM APP UI - ${archText}`) + console.log(`- ${minText}`) + console.log(`- ${maxText}`) + console.log(`- ${p50Text}`) + console.log(`- ${p95Text}`) + console.log(`- ${avgText}`) + console.log('==================================================\n') + }) +}) From 0be97a822b1d76740052cef059ff55ec4f6eb1de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Fri, 31 Jul 2026 16:54:53 +0200 Subject: [PATCH 16/39] feat(e2e-app): add UI Manager info to benchmark logs --- e2e-app/e2e/ids.ts | 1 + e2e-app/e2e/performance.test.ts | 5 ++--- e2e-app/src/components/ArchInfo.tsx | 6 ++++++ e2e-app/src/global.d.ts | 1 + 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/e2e-app/e2e/ids.ts b/e2e-app/e2e/ids.ts index 31763e36..d9d50e13 100644 --- a/e2e-app/e2e/ids.ts +++ b/e2e-app/e2e/ids.ts @@ -14,4 +14,5 @@ export const testIds = { benchmarkAvg: 'benchmark-avg', benchmarkP50: 'benchmark-p50', benchmarkP95: 'benchmark-p95', + uiManager: 'ui-manager', } diff --git a/e2e-app/e2e/performance.test.ts b/e2e-app/e2e/performance.test.ts index 3aa6d26d..e9aee979 100644 --- a/e2e-app/e2e/performance.test.ts +++ b/e2e-app/e2e/performance.test.ts @@ -46,11 +46,10 @@ describe('SDK Architecture Performance Benchmark', () => { const p50Text = await getElementText(element(by.id(testIds.benchmarkP50))) const p95Text = await getElementText(element(by.id(testIds.benchmarkP95))) const avgText = await getElementText(element(by.id(testIds.benchmarkAvg))) - - const archText = global.__turboModuleProxy !== null ? 'TurboModules (New Arch)' : 'Bridge (Old Arch)' + const uiManagerText = await getElementText(element(by.id(testIds.uiManager))) console.log('\n==================================================') - console.log(`BENCHMARK RESULTS FROM APP UI - ${archText}`) + console.log(`BENCHMARK RESULTS FROM APP UI - ${uiManagerText}`) console.log(`- ${minText}`) console.log(`- ${maxText}`) console.log(`- ${p50Text}`) diff --git a/e2e-app/src/components/ArchInfo.tsx b/e2e-app/src/components/ArchInfo.tsx index 278be7b0..0acb7132 100644 --- a/e2e-app/src/components/ArchInfo.tsx +++ b/e2e-app/src/components/ArchInfo.tsx @@ -1,13 +1,19 @@ import { Text, View } from 'react-native' +import { testIds } from '@/e2e/ids' export function ArchInfo() { const hasTurboModules = global.__turboModuleProxy !== null const isBridgeless = global.RN$Bridgeless + const uiManager = global.nativeFabricUIManager ? 'Fabric' : 'Paper' return ( {`TurboModules: ${hasTurboModules}`} {`Bridgeless: ${isBridgeless}`} + + UI Manager: + {uiManager} + ) } diff --git a/e2e-app/src/global.d.ts b/e2e-app/src/global.d.ts index c749b701..9d2e2b8d 100644 --- a/e2e-app/src/global.d.ts +++ b/e2e-app/src/global.d.ts @@ -1,6 +1,7 @@ declare global { var __turboModuleProxy: unknown var RN$Bridgeless: boolean + var nativeFabricUIManager: boolean } export {} From 90a2f4e41f7cc9ce4cfc7b5c1bc9d304af9fa763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Mon, 3 Aug 2026 10:17:05 +0200 Subject: [PATCH 17/39] refactor(e2e-app): adjust UI Manager text structure --- e2e-app/src/components/ArchInfo.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/e2e-app/src/components/ArchInfo.tsx b/e2e-app/src/components/ArchInfo.tsx index 0acb7132..60d3fc58 100644 --- a/e2e-app/src/components/ArchInfo.tsx +++ b/e2e-app/src/components/ArchInfo.tsx @@ -10,10 +10,8 @@ export function ArchInfo() { {`TurboModules: ${hasTurboModules}`} {`Bridgeless: ${isBridgeless}`} - - UI Manager: - {uiManager} - + UI Manager: + {uiManager} ) } From fdf6bf8898a59ca3e5bb423605526a7989660d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Mon, 3 Aug 2026 10:37:54 +0200 Subject: [PATCH 18/39] feat(e2e-app): add retry logic for detox build steps --- .github/workflows/e2e-tests.yml | 4 +-- e2e-app/scripts/retry-cmd.mjs | 57 +++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 e2e-app/scripts/retry-cmd.mjs diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index e72577d6..7fb30082 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -85,7 +85,7 @@ jobs: run: pnpm prebuild working-directory: e2e-app - - run: pnpm exec detox build --configuration android.emu.release + - run: node scripts/retry-cmd.mjs --retries=3 "pnpm exec detox build --configuration android.emu.release" working-directory: e2e-app - name: Upload artifacts @@ -284,7 +284,7 @@ jobs: run: pnpm prebuild working-directory: e2e-app - - run: pnpm exec detox build --configuration ios.sim.release + - run: node scripts/retry-cmd.mjs --retries=3 "pnpm exec detox build --configuration ios.sim.release" working-directory: e2e-app - name: Upload artifacts diff --git a/e2e-app/scripts/retry-cmd.mjs b/e2e-app/scripts/retry-cmd.mjs new file mode 100644 index 00000000..7bb21977 --- /dev/null +++ b/e2e-app/scripts/retry-cmd.mjs @@ -0,0 +1,57 @@ +#!/usr/bin/env node +import { spawnSync } from 'node:child_process' + +// Usage: node retry-cmd.js --retries=3 "" +// Runs and retries up to additional times if it exits non-zero. + +function parseArgs(argv) { + let retries = 0 + const rest = [] + + for (const arg of argv) { + const match = /^--retries=(\d+)$/.exec(arg) + if (match) { + retries = Number(match[1]) + } else { + rest.push(arg) + } + } + + return { retries, command: rest.join(' ') } +} + +const { retries, command } = parseArgs(process.argv.slice(2)) + +if (!command) { + console.error('Usage: node retry.js --retries= ""') + process.exit(1) +} + +const totalAttempts = retries + 1 + +for (let attempt = 1; attempt <= totalAttempts; attempt++) { + if (attempt > 1) { + console.log(`[retry] Attempt ${attempt}/${totalAttempts}: ${command}`) + } + + const result = spawnSync(command, { stdio: 'inherit', shell: true }) + + if (result.error) { + console.error(`[retry] Failed to start command: ${result.error.message}`) + } + + const code = result.status + + if (code === 0) { + process.exit(0) + } + + console.error(`[retry] Attempt ${attempt} exited with code ${code ?? 'null'}.`) + + if (attempt < totalAttempts) { + console.error('[retry] Retrying...') + } +} + +console.error(`[retry] Command failed after ${totalAttempts} attempt(s).`) +process.exit(1) From aac6ceefaa9678c3fad452f3cbf2deb1a1466e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Mon, 3 Aug 2026 10:57:02 +0200 Subject: [PATCH 19/39] chore(android): add comments for new architecture enablement --- sdk/android/build.gradle | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sdk/android/build.gradle b/sdk/android/build.gradle index d162c37a..54628f17 100644 --- a/sdk/android/build.gradle +++ b/sdk/android/build.gradle @@ -4,11 +4,13 @@ plugins { } def isNewArchitectureEnabled() { + // To opt-in for the New Architecture, you can either: + // - Set `newArchEnabled` to true inside the `gradle.properties` file + // - Invoke gradle with `-newArchEnabled=true` + // - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true` return project.hasProperty("newArchEnabled") && project.getProperty("newArchEnabled") == "true" } -// On the New Architecture the React Gradle plugin runs Codegen for this library, generating the -// `NativeRNFingerprintjsProSpec` base class from `src/specs/NativeRNFingerprintjsPro.ts`. if (isNewArchitectureEnabled()) { apply plugin: 'com.facebook.react' } From e446eefd310a6cab6d75d30e67e4c5110d1eb237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Mon, 3 Aug 2026 11:05:31 +0200 Subject: [PATCH 20/39] refactor: remove temporary logs for architecture verification --- .../fingerprintjs/reactnative/RNFingerprintjsProPackage.kt | 5 ----- .../fingerprintjs/reactnative/RNFingerprintjsProPackage.kt | 4 ---- sdk/ios/RNFingerprintjsPro.mm | 3 --- sdk/ios/RNFingerprintjsPro.swift | 7 ------- 4 files changed, 19 deletions(-) diff --git a/sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt b/sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt index 778ab5cb..a45bf803 100644 --- a/sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt +++ b/sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt @@ -1,6 +1,5 @@ package com.fingerprintjs.reactnative -import android.util.Log import com.facebook.react.BaseReactPackage import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext @@ -16,10 +15,6 @@ import com.facebook.react.module.model.ReactModuleInfoProvider class RNFingerprintjsProPackage : BaseReactPackage() { override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { return if (name == RNFingerprintjsProModule.NAME) { - // TEMP(new-arch verification): this new-arch-only package is used only when the module - // is resolved as a TurboModule (New Architecture). The old-arch build uses the legacy - // `ReactPackage` in `src/oldarch` instead. - Log.d("RNFingerprintjsPro", "getModule via new-arch package — running as a TurboModule (New Architecture)") RNFingerprintjsProModule(reactContext) } else { null diff --git a/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt b/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt index 5be66e6c..e59ad813 100644 --- a/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt +++ b/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt @@ -1,6 +1,5 @@ package com.fingerprintjs.reactnative -import android.util.Log import com.facebook.react.ReactPackage import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext @@ -15,9 +14,6 @@ import com.facebook.react.uimanager.ViewManager */ class RNFingerprintjsProPackage : ReactPackage { override fun createNativeModules(reactContext: ReactApplicationContext): List { - // TEMP(old-arch verification): this legacy `ReactPackage` is only compiled/used on the Old - // Architecture. The new-arch build uses `BaseReactPackage` in `src/newarch` instead. - Log.d("RNFingerprintjsPro", "createNativeModules via legacy ReactPackage — running on the Old Architecture") return listOf(RNFingerprintjsProModule(reactContext)) } diff --git a/sdk/ios/RNFingerprintjsPro.mm b/sdk/ios/RNFingerprintjsPro.mm index 5f249b7e..b4d2ef19 100644 --- a/sdk/ios/RNFingerprintjsPro.mm +++ b/sdk/ios/RNFingerprintjsPro.mm @@ -62,9 +62,6 @@ @implementation RNFingerprintjsPro (TurboModule) - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { - // TEMP(new-arch verification): if this fires, the module is instantiated as a real TurboModule - // via the Codegen-generated JSI class (not the legacy interop shim). - NSLog(@"[RNFingerprintjsPro] getTurboModule called — running as a TurboModule (New Architecture)"); return std::make_shared(params); } @end diff --git a/sdk/ios/RNFingerprintjsPro.swift b/sdk/ios/RNFingerprintjsPro.swift index e72d693c..02f99a02 100644 --- a/sdk/ios/RNFingerprintjsPro.swift +++ b/sdk/ios/RNFingerprintjsPro.swift @@ -2,7 +2,6 @@ // RNFingerprintjsPro.swift // RNFingerprintjsPro // -import Foundation import FingerprintPro @objc(RNFingerprintjsPro) @@ -14,12 +13,6 @@ class RNFingerprintjsPro: NSObject { @objc(configure:region:endpointUrl:fallbackEndpointUrls:extendedResponseFormat:pluginVersion:allowUseOfLocationData:locationTimeoutMillis:) public func configure(_ apiToken: String, _ region: String?, _ endpointUrl: String?, _ fallbackEndpointUrls: [String], _ extendedResponseFormat: Bool, _ pluginVersion: String, _ allowUseOfLocationData: Bool, _ locationTimeoutMillis: Double) -> Void { - #if !RCT_NEW_ARCH_ENABLED - // TEMP(old-arch verification): compiled only when `RCT_NEW_ARCH_ENABLED` is NOT set, i.e. the - // module is registered via the legacy `RCT_EXTERN_MODULE` bridge (Old Architecture). On the - // New Architecture, `getTurboModule` in RNFingerprintjsPro.mm logs instead. - NSLog("[RNFingerprintjsPro] configure via legacy bridge — running on the Old Architecture") - #endif let region = RNFingerprintjsPro.parseRegion(region, endpoint: endpointUrl, endpointFallbacks: fallbackEndpointUrls) let integrationInfo = [("fingerprint-pro-react-native", pluginVersion)] let configuration = Configuration(apiKey: apiToken, region: region, integrationInfo: integrationInfo, extendedResponseFormat: extendedResponseFormat, allowUseOfLocationData: allowUseOfLocationData) From e44322bc15603393cbac731530439b63814823e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Mon, 3 Aug 2026 12:20:02 +0200 Subject: [PATCH 21/39] chore(e2e-tests): add retry logic for detox test steps --- .github/workflows/e2e-tests.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 7fb30082..3c88a7e4 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -191,8 +191,9 @@ jobs: emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: true arch: x86_64 + working-directory: e2e-app avd-name: ${{ steps.device.outputs.AVD_NAME }} - script: cd e2e-app && pnpm exec detox test --configuration android.emu.release --headless --record-logs all --record-videos failing + script: node scripts/retry-cmd.mjs --retries=3 "pnpm exec detox test --configuration android.emu.release --headless --record-logs all --record-videos failing" - name: Upload artifacts if: always() @@ -358,7 +359,7 @@ jobs: if: github.event_name != 'release' working-directory: e2e-app - - run: pnpm exec detox test --configuration ios.sim.release --cleanup --headless --record-logs all --record-videos failing + - run: node scripts/retry-cmd.mjs --retries=3 "pnpm exec detox test --configuration ios.sim.release --cleanup --headless --record-logs all --record-videos failing" working-directory: e2e-app env: MINIMUM_US_DEFAULT_PUBLIC_KEY: ${{ secrets.MINIMUM_US_DEFAULT_PUBLIC_KEY }} From 217c43fe79e564e9486de8cdbe2277d494e4b9fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Mon, 3 Aug 2026 15:11:06 +0200 Subject: [PATCH 22/39] chore: remove benchmark logic --- e2e-app/App.tsx | 104 +------------------------------- e2e-app/e2e/ids.ts | 8 --- e2e-app/e2e/performance.test.ts | 60 ------------------ 3 files changed, 3 insertions(+), 169 deletions(-) delete mode 100644 e2e-app/e2e/performance.test.ts diff --git a/e2e-app/App.tsx b/e2e-app/App.tsx index 19c93b4b..ce773f65 100644 --- a/e2e-app/App.tsx +++ b/e2e-app/App.tsx @@ -1,79 +1,23 @@ // SafeAreaView is deprecated in newer RN versions, react-native-safe-area-context is recommended // but it doesn't work nicely with older RN versions that we also run tests against, so keep SafeAreaView import from react-native for now +/* eslint-disable @typescript-eslint/no-deprecated */ import { Pressable, SafeAreaView, Text, View } from 'react-native' import { FingerprintJsProProvider, useVisitorData } from '@fingerprintjs/fingerprintjs-pro-react-native' import { testIds } from '@/e2e/ids' -import { useEffect, useState } from 'react' +import { useEffect } from 'react' import { testTags } from '@/e2e/tags' import config from '@/src/config' import { ArchInfo } from '@/src/components/ArchInfo' -export type BenchmarkStats = { - min: number - max: number - avg: number - p50: number - p95: number -} - function InnerApp() { const { isLoading, error, data, getData } = useVisitorData() - const [isBenchmarking, setIsBenchmarking] = useState(false) - const [benchmarkProgress, setBenchmarkProgress] = useState(null) - const [benchmarkResults, setBenchmarkResults] = useState(null) - const [benchmarkError, setBenchmarkError] = useState(null) - const doGetData = async () => { const tags = config.useTags ? testTags : undefined await getData(tags, config.linkedId) } - const doMeasurePerformance = async () => { - setIsBenchmarking(true) - setBenchmarkResults(null) - setBenchmarkError(null) - setBenchmarkProgress('Starting benchmark...') - - const iterations = 100 - const durations: number[] = [] - const tags = config.useTags ? testTags : undefined - - try { - for (let i = 0; i < iterations; i++) { - setBenchmarkProgress(`Running iteration ${i + 1}/${iterations}...`) - - const start = performance.now() - await getData(tags, config.linkedId) - const end = performance.now() - - durations.push(end - start) - - // Give the JS thread a small break to update state and render progress - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - } - - const min = Math.min(...durations) - const max = Math.max(...durations) - const avg = durations.reduce((a, b) => a + b, 0) / durations.length - const sorted = [...durations].sort((a, b) => a - b) - const p50 = sorted[Math.floor(durations.length * 0.5)] - const p95 = sorted[Math.floor(durations.length * 0.95)] - - setBenchmarkResults({ min, max, avg, p50, p95 }) - setBenchmarkProgress(null) - } catch (err) { - console.error(err) - setBenchmarkError(err instanceof Error ? err.message : String(err)) - setBenchmarkProgress(null) - } finally { - setIsBenchmarking(false) - } - } - useEffect(() => { if (error) { console.error(error) @@ -82,7 +26,6 @@ function InnerApp() { return ( - {} Get data - - - - {isBenchmarking ? 'Measuring...' : 'Measure Performance'} - - - - {benchmarkProgress ? ( - - {benchmarkProgress} - - ) : null} - - {benchmarkError ? Error: {benchmarkError} : null} - - {benchmarkResults ? ( - - - Benchmark Results (100 runs) - - Min: {benchmarkResults.min.toFixed(2)}ms - Max: {benchmarkResults.max.toFixed(2)}ms - Median (p50): {benchmarkResults.p50.toFixed(2)}ms - 95th Percentile: {benchmarkResults.p95.toFixed(2)}ms - - Avg: {benchmarkResults.avg.toFixed(2)}ms - - - ) : null} ) diff --git a/e2e-app/e2e/ids.ts b/e2e-app/e2e/ids.ts index d9d50e13..b5fbb137 100644 --- a/e2e-app/e2e/ids.ts +++ b/e2e-app/e2e/ids.ts @@ -6,13 +6,5 @@ export const testIds = { errorCause: 'error-cause', data: 'data', getData: 'get-data', - getDataDuration: 'get-data-duration', - measurePerformance: 'measure-performance', - benchmarkProgress: 'benchmark-progress', - benchmarkMin: 'benchmark-min', - benchmarkMax: 'benchmark-max', - benchmarkAvg: 'benchmark-avg', - benchmarkP50: 'benchmark-p50', - benchmarkP95: 'benchmark-p95', uiManager: 'ui-manager', } diff --git a/e2e-app/e2e/performance.test.ts b/e2e-app/e2e/performance.test.ts deleted file mode 100644 index e9aee979..00000000 --- a/e2e-app/e2e/performance.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { by, element, waitFor, device } from 'detox' -import { it, describe, beforeAll } from '@jest/globals' -import { testIds } from './ids' -import { getElementText } from './getElementText' -import { wait } from './wait' -import { Config } from '@/src/config.types' - -const apiKey = process.env.MINIMUM_US_DEFAULT_PUBLIC_KEY -const region = 'us' - -async function launchApp() { - if (!apiKey) { - throw new Error('MINIMUM_US_DEFAULT_PUBLIC_KEY is required to run performance tests') - } - - await device.launchApp({ - newInstance: true, - launchArgs: { - apiKey, - region, - } as Config, - }) - await wait(4000) -} - -describe('SDK Architecture Performance Benchmark', () => { - beforeAll(async () => { - await launchApp() - }) - - it('triggers the Measure Performance button and reports final statistics', async () => { - console.log('Tapping the Measure Performance button in the app...') - await element(by.id(testIds.measurePerformance)).tap() - - console.log('Waiting for the 100-run benchmark to complete...') - - // 100 runs can take ~20-30 seconds depending on network conditions. Wait up to 90 seconds. - await waitFor(element(by.id(testIds.benchmarkAvg))) - .toExist() - .withTimeout(90_000) - - console.log('Benchmark complete! Reading statistics...') - - const minText = await getElementText(element(by.id(testIds.benchmarkMin))) - const maxText = await getElementText(element(by.id(testIds.benchmarkMax))) - const p50Text = await getElementText(element(by.id(testIds.benchmarkP50))) - const p95Text = await getElementText(element(by.id(testIds.benchmarkP95))) - const avgText = await getElementText(element(by.id(testIds.benchmarkAvg))) - const uiManagerText = await getElementText(element(by.id(testIds.uiManager))) - - console.log('\n==================================================') - console.log(`BENCHMARK RESULTS FROM APP UI - ${uiManagerText}`) - console.log(`- ${minText}`) - console.log(`- ${maxText}`) - console.log(`- ${p50Text}`) - console.log(`- ${p95Text}`) - console.log(`- ${avgText}`) - console.log('==================================================\n') - }) -}) From e83c165cc7282d6820f70d06d8ad1ae8e11450a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Mon, 3 Aug 2026 15:36:10 +0200 Subject: [PATCH 23/39] feat(react-native): add TurboModule support for new architecture --- .changeset/new-architecture-support.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/new-architecture-support.md diff --git a/.changeset/new-architecture-support.md b/.changeset/new-architecture-support.md new file mode 100644 index 00000000..eecbbf13 --- /dev/null +++ b/.changeset/new-architecture-support.md @@ -0,0 +1,5 @@ +--- +'@fingerprintjs/fingerprintjs-pro-react-native': minor +--- + +Added React Native New Architecture (TurboModule) support. The native module is now exposed as a TurboModule via a Codegen spec while remaining fully backward compatible with the old architecture From 9081de5340466399be458bc91056a355a9199cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Mon, 3 Aug 2026 16:53:06 +0200 Subject: [PATCH 24/39] refactor(e2e-app): ensure boolean casting for global variables --- e2e-app/src/components/ArchInfo.tsx | 4 ++-- e2e-app/src/global.d.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/e2e-app/src/components/ArchInfo.tsx b/e2e-app/src/components/ArchInfo.tsx index 60d3fc58..21e3dffb 100644 --- a/e2e-app/src/components/ArchInfo.tsx +++ b/e2e-app/src/components/ArchInfo.tsx @@ -2,8 +2,8 @@ import { Text, View } from 'react-native' import { testIds } from '@/e2e/ids' export function ArchInfo() { - const hasTurboModules = global.__turboModuleProxy !== null - const isBridgeless = global.RN$Bridgeless + const hasTurboModules = Boolean(global.__turboModuleProxy) + const isBridgeless = Boolean(global.RN$Bridgeless) const uiManager = global.nativeFabricUIManager ? 'Fabric' : 'Paper' return ( diff --git a/e2e-app/src/global.d.ts b/e2e-app/src/global.d.ts index 9d2e2b8d..c7df60a4 100644 --- a/e2e-app/src/global.d.ts +++ b/e2e-app/src/global.d.ts @@ -1,7 +1,7 @@ declare global { - var __turboModuleProxy: unknown - var RN$Bridgeless: boolean - var nativeFabricUIManager: boolean + var __turboModuleProxy: ?unknown + var RN$Bridgeless: ?boolean + var nativeFabricUIManager: ?boolean } export {} From a12776ce84b6790503a3bb15b1a39c224b6d114f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Tue, 4 Aug 2026 12:34:16 +0200 Subject: [PATCH 25/39] chore(e2e-tests): update retry wait duration --- e2e-app/e2e/identification.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e-app/e2e/identification.test.ts b/e2e-app/e2e/identification.test.ts index 2b9ad2bf..4b129b5e 100644 --- a/e2e-app/e2e/identification.test.ts +++ b/e2e-app/e2e/identification.test.ts @@ -21,7 +21,7 @@ async function launchApp(params?: DeviceLaunchAppConfig) { } jest.retryTimes(3, { - waitBeforeRetry: 10_000, + waitBeforeRetry: 1000, logErrorsBeforeRetry: false, }) From 336691a2389b99269e4b9ac9fd38f7a0e4c450f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Mon, 3 Aug 2026 16:54:16 +0200 Subject: [PATCH 26/39] chore: apply copilot suggestions Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- e2e-app/scripts/retry-cmd.mjs | 2 +- sdk/android/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e-app/scripts/retry-cmd.mjs b/e2e-app/scripts/retry-cmd.mjs index 7bb21977..82569304 100644 --- a/e2e-app/scripts/retry-cmd.mjs +++ b/e2e-app/scripts/retry-cmd.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node import { spawnSync } from 'node:child_process' -// Usage: node retry-cmd.js --retries=3 "" +// Usage: node retry-cmd.mjs --retries=3 "" // Runs and retries up to additional times if it exits non-zero. function parseArgs(argv) { diff --git a/sdk/android/build.gradle b/sdk/android/build.gradle index 54628f17..8ba5a8ea 100644 --- a/sdk/android/build.gradle +++ b/sdk/android/build.gradle @@ -6,7 +6,7 @@ plugins { def isNewArchitectureEnabled() { // To opt-in for the New Architecture, you can either: // - Set `newArchEnabled` to true inside the `gradle.properties` file - // - Invoke gradle with `-newArchEnabled=true` + // - Invoke gradle with `-PnewArchEnabled=true` // - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true` return project.hasProperty("newArchEnabled") && project.getProperty("newArchEnabled") == "true" } From 18a4159ef75801576dedd00a94c402f5a62378d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Tue, 4 Aug 2026 15:03:28 +0200 Subject: [PATCH 27/39] test(e2e-tests): add matrix support for architecture testing --- .github/workflows/e2e-tests.yml | 68 +++++++++++++++---- e2e-app/app-config-tools/arch.ts | 7 +- .../app-config-tools/reactNativeMetadata.ts | 12 ++-- e2e-app/app.config.ts | 7 ++ 4 files changed, 75 insertions(+), 19 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 3c88a7e4..f116db86 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -26,10 +26,20 @@ jobs: max-parallel: 3 matrix: react-native: ${{ fromJSON(needs.build-matrix.outputs.react-native-matrix) }} + architecture: [old, new] + exclude: + # New architecture is not tested on the Expo 50/51 SDKs (experimental there) + - { react-native: '0.73', architecture: new } + - { react-native: '0.74', architecture: new } + - { react-native: '0.75', architecture: new } + # RN 0.82+ (Expo SDK 55+) removed the ability to disable the new architecture + - { react-native: '0.83', architecture: old } + - { react-native: '0.85', architecture: old } + - { react-native: 'local', architecture: old } runs-on: ubuntu-latest needs: build-matrix - name: Build android for E2E tests for react-native ${{ matrix.react-native }} + name: Build android for E2E tests for react-native ${{ matrix.react-native }} (${{ matrix.architecture }} arch) steps: - uses: actions/checkout@v7 @@ -84,6 +94,8 @@ jobs: - name: Prebuild run: pnpm prebuild working-directory: e2e-app + env: + E2E_NEW_ARCH: ${{ matrix.architecture == 'new' }} - run: node scripts/retry-cmd.mjs --retries=3 "pnpm exec detox build --configuration android.emu.release" working-directory: e2e-app @@ -91,7 +103,7 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v7 with: - name: android-app-release-${{ matrix.react-native }} + name: android-app-release-${{ matrix.react-native }}-${{ matrix.architecture }} path: e2e-app/android/app/build retention-days: 3 @@ -100,12 +112,22 @@ jobs: - build-android - build-matrix runs-on: ubuntu-latest - name: Android E2E Tests for react-native ${{ matrix.react-native }} + name: Android E2E Tests for react-native ${{ matrix.react-native }} (${{ matrix.architecture }} arch) strategy: max-parallel: 3 fail-fast: false matrix: react-native: ${{ fromJSON(needs.build-matrix.outputs.react-native-matrix) }} + architecture: [old, new] + exclude: + # New architecture is not tested on the Expo 50/51 SDKs (experimental there) + - { react-native: '0.73', architecture: new } + - { react-native: '0.74', architecture: new } + - { react-native: '0.75', architecture: new } + # RN 0.82+ (Expo SDK 55+) removed the ability to disable the new architecture + - { react-native: '0.83', architecture: old } + - { react-native: '0.85', architecture: old } + - { react-native: 'local', architecture: old } steps: - uses: actions/checkout@v7 @@ -145,7 +167,7 @@ jobs: - uses: actions/download-artifact@v8 with: - name: android-app-release-${{ matrix.react-native }} + name: android-app-release-${{ matrix.react-native }}-${{ matrix.architecture }} path: e2e-app/android/app/build - name: Get device name @@ -199,7 +221,7 @@ jobs: if: always() uses: actions/upload-artifact@v7 with: - name: android-detox-artifacts-${{matrix.react-native}} + name: android-detox-artifacts-${{matrix.react-native}}-${{ matrix.architecture }} path: '**/detox-artifacts' retention-days: 3 @@ -207,19 +229,29 @@ jobs: name: Delete build artifact if: success() with: - name: android-app-release-${{ matrix.react-native }} + name: android-app-release-${{ matrix.react-native }}-${{ matrix.architecture }} build-ios: env: HOMEBREW_NO_AUTO_UPDATE: 1 HOMEBREW_NO_INSTALL_CLEANUP: 1 runs-on: macos-26 - name: Build iOS for E2E tests for react-native ${{ matrix.react-native }} + name: Build iOS for E2E tests for react-native ${{ matrix.react-native }} (${{ matrix.architecture }} arch) strategy: max-parallel: 3 fail-fast: false matrix: react-native: ${{ fromJSON(needs.build-matrix.outputs.react-native-matrix) }} + architecture: [old, new] + exclude: + # New architecture is not tested on the Expo 50/51 SDKs (experimental there) + - { react-native: '0.73', architecture: new } + - { react-native: '0.74', architecture: new } + - { react-native: '0.75', architecture: new } + # RN 0.82+ (Expo SDK 55+) removed the ability to disable the new architecture + - { react-native: '0.83', architecture: old } + - { react-native: '0.85', architecture: old } + - { react-native: 'local', architecture: old } needs: build-matrix steps: - uses: actions/checkout@v7 @@ -284,6 +316,8 @@ jobs: - name: Prebuild run: pnpm prebuild working-directory: e2e-app + env: + E2E_NEW_ARCH: ${{ matrix.architecture == 'new' }} - run: node scripts/retry-cmd.mjs --retries=3 "pnpm exec detox build --configuration ios.sim.release" working-directory: e2e-app @@ -291,7 +325,7 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v7 with: - name: ios-app-release-${{ matrix.react-native }} + name: ios-app-release-${{ matrix.react-native }}-${{ matrix.architecture }} path: e2e-app/ios/build/Build retention-days: 3 @@ -300,7 +334,7 @@ jobs: HOMEBREW_NO_AUTO_UPDATE: 1 HOMEBREW_NO_INSTALL_CLEANUP: 1 runs-on: macos-26 - name: iOS E2E Tests for react-native ${{ matrix.react-native }} + name: iOS E2E Tests for react-native ${{ matrix.react-native }} (${{ matrix.architecture }} arch) needs: - build-ios - build-matrix @@ -309,6 +343,16 @@ jobs: fail-fast: false matrix: react-native: ${{ fromJSON(needs.build-matrix.outputs.react-native-matrix) }} + architecture: [old, new] + exclude: + # New architecture is not tested on the Expo 50/51 SDKs (experimental there) + - { react-native: '0.73', architecture: new } + - { react-native: '0.74', architecture: new } + - { react-native: '0.75', architecture: new } + # RN 0.82+ (Expo SDK 55+) removed the ability to disable the new architecture + - { react-native: '0.83', architecture: old } + - { react-native: '0.85', architecture: old } + - { react-native: 'local', architecture: old } steps: - uses: actions/checkout@v7 @@ -337,7 +381,7 @@ jobs: - uses: actions/download-artifact@v8 with: - name: ios-app-release-${{ matrix.react-native }} + name: ios-app-release-${{ matrix.react-native }}-${{ matrix.architecture }} path: e2e-app/ios/build/Build - name: Start iPhone simulator @@ -373,7 +417,7 @@ jobs: if: always() uses: actions/upload-artifact@v7 with: - name: ios-detox-artifacts-${{matrix.react-native}} + name: ios-detox-artifacts-${{matrix.react-native}}-${{ matrix.architecture }} path: '**/detox-artifacts' retention-days: 3 @@ -381,7 +425,7 @@ jobs: name: Delete build artifact if: success() with: - name: ios-app-release-${{ matrix.react-native }} + name: ios-app-release-${{ matrix.react-native }}-${{ matrix.architecture }} e2e-tests-web: name: Web E2E tests diff --git a/e2e-app/app-config-tools/arch.ts b/e2e-app/app-config-tools/arch.ts index d8cebe82..f177f688 100644 --- a/e2e-app/app-config-tools/arch.ts +++ b/e2e-app/app-config-tools/arch.ts @@ -1,9 +1,14 @@ -let newArch = false +let newArch = true export function disableNewArch() { newArch = false } export function getNewArch() { + const env = process.env.E2E_NEW_ARCH + if (env === 'true' || env === 'false') { + return env === 'true' + } + return newArch } diff --git a/e2e-app/app-config-tools/reactNativeMetadata.ts b/e2e-app/app-config-tools/reactNativeMetadata.ts index 1fab0807..6fe81c3b 100644 --- a/e2e-app/app-config-tools/reactNativeMetadata.ts +++ b/e2e-app/app-config-tools/reactNativeMetadata.ts @@ -1,6 +1,6 @@ import { disableNewArch } from './arch' import { setCompileSdkVersion, stripAndroidExtraBuildProperties } from './android' -import { withFingerprintMavenRepo, withNewArchFlag, withSplashscreen } from './expoConfigPatchers' +import { withFingerprintMavenRepo, withSplashscreen } from './expoConfigPatchers' // config is typed as any, as different expo version have different types for the config object // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -23,23 +23,23 @@ type ReactNativeMetadata = { const reactNativeMetadata: Record = { 0.73: { callbacks: [disableNewArch, stripAndroidExtraBuildProperties, setCompileSdkVersion(34)], - patchExpoConfig: [withNewArchFlag, withSplashscreen, withFingerprintMavenRepo], + patchExpoConfig: [withSplashscreen, withFingerprintMavenRepo], }, 0.74: { callbacks: [disableNewArch, stripAndroidExtraBuildProperties, setCompileSdkVersion(34)], - patchExpoConfig: [withNewArchFlag, withSplashscreen], + patchExpoConfig: [withSplashscreen], }, 0.75: { callbacks: [disableNewArch, stripAndroidExtraBuildProperties, setCompileSdkVersion(34)], - patchExpoConfig: [withNewArchFlag, withSplashscreen], + patchExpoConfig: [withSplashscreen], }, 0.76: { callbacks: [stripAndroidExtraBuildProperties, setCompileSdkVersion(35)], - patchExpoConfig: [withNewArchFlag, withSplashscreen], + patchExpoConfig: [withSplashscreen], }, 0.77: { callbacks: [stripAndroidExtraBuildProperties, setCompileSdkVersion(35)], - patchExpoConfig: [withNewArchFlag, withSplashscreen], + patchExpoConfig: [withSplashscreen], }, } diff --git a/e2e-app/app.config.ts b/e2e-app/app.config.ts index 08b780f8..8b1c4007 100644 --- a/e2e-app/app.config.ts +++ b/e2e-app/app.config.ts @@ -5,6 +5,7 @@ import { dependencies } from './package.json' import * as semver from 'semver' import { handleReactNativeVersion, patchExpoConfig } from './app-config-tools/reactNativeMetadata' import { getAndroidBuildProperties } from './app-config-tools/android' +import { withNewArchFlag } from './app-config-tools/expoConfigPatchers' let rawReactNativeVersion = dependencies['react-native'] @@ -48,4 +49,10 @@ const config: ExpoConfig = { ], } +// Set newArchEnabled explicitly for every RN version so that E2E_NEW_ARCH can force either +// architecture at prebuild time (versions without a metadata entry would otherwise inherit +// Expo's default and never write the flag). +// eslint-disable-next-line @typescript-eslint/no-explicit-any +withNewArchFlag(config as any) + export default patchExpoConfig(rnVersionStr, config) From c7a441cfacd321b06dfe8873e0c66cbd52186c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Tue, 4 Aug 2026 16:19:25 +0200 Subject: [PATCH 28/39] refactor: unify method signatures and simplify logic --- .../FingerprintJsProProvider.test.ts | 119 ++++++++---------- sdk/__tests__/setup.ts | 2 - sdk/__tests__/useVisitorData.test.ts | 15 +-- .../reactnative/RNFingerprintjsProModule.kt | 28 ++--- .../reactnative/RNFingerprintjsProSpec.kt | 18 ++- sdk/ios/RNFingerprintjsPro.mm | 24 +--- sdk/ios/RNFingerprintjsPro.swift | 30 +---- sdk/src/FingerprintJsProAgent.ts | 29 ++--- sdk/src/specs/NativeRNFingerprintjsPro.ts | 30 ++--- 9 files changed, 112 insertions(+), 183 deletions(-) diff --git a/sdk/__tests__/FingerprintJsProProvider.test.ts b/sdk/__tests__/FingerprintJsProProvider.test.ts index 1fd8104f..b995acf3 100644 --- a/sdk/__tests__/FingerprintJsProProvider.test.ts +++ b/sdk/__tests__/FingerprintJsProProvider.test.ts @@ -5,7 +5,7 @@ import { FingerprintJsProContext } from '../src/FingerprintJsProContext' import { NativeModules } from 'react-native' import { FingerprintJsProAgent } from '../src' -const { configure, getVisitorId, getVisitorData, getVisitorIdWithTimeout, getVisitorDataWithTimeout } = +const { configure, getVisitorId, getVisitorData } = // eslint-disable-next-line @typescript-eslint/consistent-type-assertions NativeModules.RNFingerprintjsPro as unknown as Record @@ -28,13 +28,13 @@ describe(`FingerprintJsProProvider`, () => { expect(configure).toHaveBeenCalledWith( options.apiKey, - options.region, - options.endpointUrl, - [], - false, pluginVersion, false, - 5000 + [], + false, + 5000, + options.region, + options.endpointUrl ) }) @@ -51,13 +51,13 @@ describe(`FingerprintJsProProvider`, () => { expect(configure).toHaveBeenCalledWith( options.apiKey, - options.region, - options.endpointUrl, - options.fallbackEndpointUrls, - false, pluginVersion, false, - 5000 + options.fallbackEndpointUrls, + false, + 5000, + options.region, + options.endpointUrl ) }) @@ -74,13 +74,13 @@ describe(`FingerprintJsProProvider`, () => { expect(configure).toHaveBeenCalledWith( options.apiKey, - options.region, - options.endpointUrl, - [], - options.extendedResponseFormat, pluginVersion, + options.extendedResponseFormat, + [], false, - 5000 + 5000, + options.region, + options.endpointUrl ) }) @@ -98,13 +98,13 @@ describe(`FingerprintJsProProvider`, () => { expect(configure).toHaveBeenCalledWith( options.apiKey, - options.region, - options.endpointUrl, - [], - false, pluginVersion, + false, + [], true, - options.locationTimeoutMillisAndroid + options.locationTimeoutMillisAndroid, + options.region, + options.endpointUrl ) }) @@ -113,25 +113,25 @@ describe(`FingerprintJsProProvider`, () => { const fingerprintClient = new FingerprintJsProAgent(options) void fingerprintClient.getVisitorId() - expect(getVisitorId).toHaveBeenCalledWith(null, null) + expect(getVisitorId).toHaveBeenCalledWith(null, null, null) }) - it('should pass timeout to `getVisitorIdWithTimeout` function', () => { + it('should pass timeout to `getVisitorId` function', () => { const options = getDefaultLoadOptions() options.requestOptions = { timeout: 18_000 } const fingerprintClient = new FingerprintJsProAgent(options) void fingerprintClient.getVisitorId() - expect(getVisitorIdWithTimeout).toHaveBeenCalledWith(null, null, options.requestOptions.timeout) + expect(getVisitorId).toHaveBeenCalledWith(null, null, options.requestOptions.timeout) }) - it('should pass timeout to `getVisitorIdWithTimeout` function when timeout is 0', () => { + it('should pass timeout to `getVisitorId` function when timeout is 0', () => { const options = getDefaultLoadOptions() options.requestOptions = { timeout: 0 } const fingerprintClient = new FingerprintJsProAgent(options) void fingerprintClient.getVisitorId() - expect(getVisitorIdWithTimeout).toHaveBeenCalledWith(null, null, options.requestOptions.timeout) + expect(getVisitorId).toHaveBeenCalledWith(null, null, options.requestOptions.timeout) }) it('For `getVisitorId` function timeout from params should be more important than the timeout from client configuration', () => { @@ -141,20 +141,11 @@ describe(`FingerprintJsProProvider`, () => { options.requestOptions = { timeout: clientTimeout } const fingerprintClient = new FingerprintJsProAgent(options) - const mockedJsonAnswer = { - visitorId: mockedVisitorId, - } - getVisitorIdWithTimeout.mockReturnValueOnce( - Promise.resolve({ - requestId: mockedRequestId, - confidenceScore: mockedConfidenceScore, - visitorDataJson: JSON.stringify(mockedJsonAnswer), - }) - ) + getVisitorId.mockReturnValueOnce(Promise.resolve(mockedVisitorId)) void fingerprintClient.getVisitorId(undefined, undefined, { timeout: getRequestTimeout }) - expect(getVisitorIdWithTimeout).toHaveBeenCalledWith(null, null, getRequestTimeout) + expect(getVisitorId).toHaveBeenCalledWith(null, null, getRequestTimeout) }) it('should call `getVisitorData` function when there is no timeout', () => { @@ -174,10 +165,10 @@ describe(`FingerprintJsProProvider`, () => { void fingerprintClient.getVisitorData() - expect(getVisitorData).toHaveBeenCalledWith(null, null) + expect(getVisitorData).toHaveBeenCalledWith(null, null, null) }) - it('should pass timeout to `getVisitorDataWithTimeout` function', () => { + it('should pass timeout to `getVisitorData` function', () => { const options = getDefaultLoadOptions() options.requestOptions = { timeout: 18_000 } const fingerprintClient = new FingerprintJsProAgent(options) @@ -185,7 +176,7 @@ describe(`FingerprintJsProProvider`, () => { const mockedJsonAnswer = { visitorId: mockedVisitorId, } - getVisitorDataWithTimeout.mockReturnValueOnce( + getVisitorData.mockReturnValueOnce( Promise.resolve({ requestId: mockedRequestId, confidenceScore: mockedConfidenceScore, @@ -195,10 +186,10 @@ describe(`FingerprintJsProProvider`, () => { void fingerprintClient.getVisitorData() - expect(getVisitorDataWithTimeout).toHaveBeenCalledWith(null, null, options.requestOptions.timeout) + expect(getVisitorData).toHaveBeenCalledWith(null, null, options.requestOptions.timeout) }) - it('should pass timeout to `getVisitorDataWithTimeout` function when timeout is 0', () => { + it('should pass timeout to `getVisitorData` function when timeout is 0', () => { const options = getDefaultLoadOptions() options.requestOptions = { timeout: 0 } const fingerprintClient = new FingerprintJsProAgent(options) @@ -206,7 +197,7 @@ describe(`FingerprintJsProProvider`, () => { const mockedJsonAnswer = { visitorId: mockedVisitorId, } - getVisitorDataWithTimeout.mockReturnValueOnce( + getVisitorData.mockReturnValueOnce( Promise.resolve({ requestId: mockedRequestId, confidenceScore: mockedConfidenceScore, @@ -216,7 +207,7 @@ describe(`FingerprintJsProProvider`, () => { void fingerprintClient.getVisitorData() - expect(getVisitorDataWithTimeout).toHaveBeenCalledWith(null, null, options.requestOptions.timeout) + expect(getVisitorData).toHaveBeenCalledWith(null, null, options.requestOptions.timeout) }) it('For `getVisitorData` function timeout from params should be more important than the timeout from client configuration', () => { @@ -229,7 +220,7 @@ describe(`FingerprintJsProProvider`, () => { const mockedJsonAnswer = { visitorId: mockedVisitorId, } - getVisitorDataWithTimeout.mockReturnValueOnce( + getVisitorData.mockReturnValueOnce( Promise.resolve({ requestId: mockedRequestId, confidenceScore: mockedConfidenceScore, @@ -239,7 +230,7 @@ describe(`FingerprintJsProProvider`, () => { void fingerprintClient.getVisitorData(undefined, undefined, { timeout: getRequestTimeout }) - expect(getVisitorDataWithTimeout).toHaveBeenCalledWith(null, null, getRequestTimeout) + expect(getVisitorData).toHaveBeenCalledWith(null, null, getRequestTimeout) }) describe('agent params changes propagation', () => { @@ -253,7 +244,7 @@ describe(`FingerprintJsProProvider`, () => { renderProvider({ apiKey: 'key-1' }) expect(configure).toHaveBeenCalledTimes(1) - expect(configure).toHaveBeenLastCalledWith('key-1', null, null, [], false, pluginVersion, false, 5000) + expect(configure).toHaveBeenLastCalledWith('key-1', pluginVersion, false, [], false, 5000, null, null) }) it('reconfigures the agent when a param changes by value', () => { @@ -263,12 +254,12 @@ describe(`FingerprintJsProProvider`, () => { rerenderWithParams({ apiKey: 'key-2' }) expect(configure).toHaveBeenCalledTimes(2) - expect(configure).toHaveBeenLastCalledWith('key-2', null, null, [], false, pluginVersion, false, 5000) + expect(configure).toHaveBeenLastCalledWith('key-2', pluginVersion, false, [], false, 5000, null, null) }) it('propagates changes across every configure argument', () => { const { rerenderWithParams } = renderProvider({ apiKey: 'key' }) - expect(configure).toHaveBeenLastCalledWith('key', null, null, [], false, pluginVersion, false, 5000) + expect(configure).toHaveBeenLastCalledWith('key', pluginVersion, false, [], false, 5000, null, null) rerenderWithParams({ apiKey: 'key', @@ -282,13 +273,13 @@ describe(`FingerprintJsProProvider`, () => { expect(configure).toHaveBeenCalledTimes(2) expect(configure).toHaveBeenLastCalledWith( 'key', - 'eu', - 'https://example.com', - [], - true, pluginVersion, true, - 6000 + [], + true, + 6000, + 'eu', + 'https://example.com' ) }) @@ -296,13 +287,13 @@ describe(`FingerprintJsProProvider`, () => { const { rerenderWithParams } = renderProvider({ apiKey: 'key', fallbackEndpointUrls: ['https://a.example'] }) expect(configure).toHaveBeenLastCalledWith( 'key', - null, - null, - ['https://a.example'], - false, pluginVersion, false, - 5000 + ['https://a.example'], + false, + 5000, + null, + null ) rerenderWithParams({ apiKey: 'key', fallbackEndpointUrls: ['https://a.example', 'https://b.example'] }) @@ -310,13 +301,13 @@ describe(`FingerprintJsProProvider`, () => { expect(configure).toHaveBeenCalledTimes(2) expect(configure).toHaveBeenLastCalledWith( 'key', - null, - null, - ['https://a.example', 'https://b.example'], - false, pluginVersion, false, - 5000 + ['https://a.example', 'https://b.example'], + false, + 5000, + null, + null ) }) diff --git a/sdk/__tests__/setup.ts b/sdk/__tests__/setup.ts index e6dca3fa..ceecf20e 100644 --- a/sdk/__tests__/setup.ts +++ b/sdk/__tests__/setup.ts @@ -3,8 +3,6 @@ jest.mock('react-native', () => { configure: jest.fn(), getVisitorId: jest.fn(), getVisitorData: jest.fn(), - getVisitorDataWithTimeout: jest.fn(), - getVisitorIdWithTimeout: jest.fn(), } return { diff --git a/sdk/__tests__/useVisitorData.test.ts b/sdk/__tests__/useVisitorData.test.ts index 87e75d97..d7153a7b 100644 --- a/sdk/__tests__/useVisitorData.test.ts +++ b/sdk/__tests__/useVisitorData.test.ts @@ -4,10 +4,7 @@ import { RequestOptions, useVisitorData } from '../src' import { createWrapper } from './helpers' // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -const { getVisitorData, getVisitorDataWithTimeout } = NativeModules.RNFingerprintjsPro as unknown as Record< - string, - jest.Mock -> +const { getVisitorData } = NativeModules.RNFingerprintjsPro as unknown as Record const mockedVisitorId = 'some visitor id' const mockedRequestId = 'some request id' @@ -136,7 +133,7 @@ describe('useVisitorData', () => { void result.current.getData(mockedTags, mockedLinkedId) }) await waitFor(() => { - expect(getVisitorData).toHaveBeenCalledWith(mockedTags, mockedLinkedId) + expect(getVisitorData).toHaveBeenCalledWith(mockedTags, mockedLinkedId, null) }) }) @@ -173,11 +170,11 @@ describe('useVisitorData', () => { void result.current.getData(mockedTags, mockedLinkedId, options) }) await waitFor(() => { - expect(getVisitorData).toHaveBeenCalledWith(mockedTags, mockedLinkedId) + expect(getVisitorData).toHaveBeenCalledWith(mockedTags, mockedLinkedId, null) }) }) - it('non-empty timeout should call `getVisitorDataWithTimeout` function', async () => { + it('non-empty timeout should pass the timeout to `getVisitorData` function', async () => { const mockedJsonAnswer = { visitorId: mockedVisitorId, } @@ -196,7 +193,7 @@ describe('useVisitorData', () => { const options: RequestOptions = { timeout: 15_000 } - getVisitorDataWithTimeout.mockReturnValueOnce( + getVisitorData.mockReturnValueOnce( Promise.resolve({ requestId: mockedRequestId, confidenceScore: mockedConfidenceScore, @@ -210,7 +207,7 @@ describe('useVisitorData', () => { void result.current.getData(mockedTags, mockedLinkedId, options) }) await waitFor(() => { - expect(getVisitorDataWithTimeout).toHaveBeenCalledWith(mockedTags, mockedLinkedId, options.timeout) + expect(getVisitorData).toHaveBeenCalledWith(mockedTags, mockedLinkedId, options.timeout) }) }) }) diff --git a/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProModule.kt b/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProModule.kt index f2e5afb5..67663ec7 100644 --- a/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProModule.kt +++ b/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProModule.kt @@ -43,13 +43,13 @@ class RNFingerprintjsProModule(reactContext: ReactApplicationContext) : RNFinger @ReactMethod override fun configure( apiToken: String, - regionKey: String?, - endpointUrl: String?, - fallbackEndpointUrls: ReadableArray, - extendedResponseFormat: Boolean, pluginVersion: String, + extendedResponseFormat: Boolean, + fallbackEndpointUrls: ReadableArray, allowUseOfLocationData: Boolean, - locationTimeoutMillis: Double + locationTimeoutMillis: Double, + regionKey: String?, + endpointUrl: String? ) { val factory = FingerprintJSFactory(reactApplicationContext) val region = when(regionKey) { @@ -73,23 +73,13 @@ class RNFingerprintjsProModule(reactContext: ReactApplicationContext) : RNFinger } @ReactMethod - override fun getVisitorId(tags: ReadableMap?, linkedId: String?, promise: Promise) { - getVisitorIdInternal(tags, linkedId, null, promise) - } - - @ReactMethod - override fun getVisitorIdWithTimeout(tags: ReadableMap?, linkedId: String?, timeout: Double, promise: Promise) { - getVisitorIdInternal(tags, linkedId, timeout.toInt(), promise) - } - - @ReactMethod - override fun getVisitorData(tags: ReadableMap?, linkedId: String?, promise: Promise) { - getVisitorDataInternal(tags, linkedId, null, promise) + override fun getVisitorId(tags: ReadableMap?, linkedId: String?, timeout: Double?, promise: Promise) { + getVisitorIdInternal(tags, linkedId, timeout?.toInt(), promise) } @ReactMethod - override fun getVisitorDataWithTimeout(tags: ReadableMap?, linkedId: String?, timeout: Double, promise: Promise) { - getVisitorDataInternal(tags, linkedId, timeout.toInt(), promise) + override fun getVisitorData(tags: ReadableMap?, linkedId: String?, timeout: Double?, promise: Promise) { + getVisitorDataInternal(tags, linkedId, timeout?.toInt(), promise) } private fun getVisitorIdInternal(tags: ReadableMap?, linkedId: String?, timeout: Int?, promise: Promise) { diff --git a/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt b/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt index aa7b6478..cd759799 100644 --- a/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt +++ b/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt @@ -17,20 +17,16 @@ abstract class RNFingerprintjsProSpec(reactContext: ReactApplicationContext) : abstract fun configure( apiToken: String, - regionKey: String?, - endpointUrl: String?, - fallbackEndpointUrls: ReadableArray, - extendedResponseFormat: Boolean, pluginVersion: String, + extendedResponseFormat: Boolean, + fallbackEndpointUrls: ReadableArray, allowUseOfLocationData: Boolean, - locationTimeoutMillis: Double + locationTimeoutMillis: Double, + regionKey: String?, + endpointUrl: String? ) - abstract fun getVisitorId(tags: ReadableMap?, linkedId: String?, promise: Promise) - - abstract fun getVisitorIdWithTimeout(tags: ReadableMap?, linkedId: String?, timeout: Double, promise: Promise) - - abstract fun getVisitorData(tags: ReadableMap?, linkedId: String?, promise: Promise) + abstract fun getVisitorId(tags: ReadableMap?, linkedId: String?, timeout: Double?, promise: Promise) - abstract fun getVisitorDataWithTimeout(tags: ReadableMap?, linkedId: String?, timeout: Double, promise: Promise) + abstract fun getVisitorData(tags: ReadableMap?, linkedId: String?, timeout: Double?, promise: Promise) } diff --git a/sdk/ios/RNFingerprintjsPro.mm b/sdk/ios/RNFingerprintjsPro.mm index b4d2ef19..133325fa 100644 --- a/sdk/ios/RNFingerprintjsPro.mm +++ b/sdk/ios/RNFingerprintjsPro.mm @@ -3,37 +3,25 @@ @interface RCT_EXTERN_MODULE(RNFingerprintjsPro, NSObject) RCT_EXTERN_METHOD(configure:(NSString *)apiToken - region:(NSString *)region - endpointUrl:(NSString *)endpointUrl - fallbackEndpointUrls:(NSArray *)fallbackEndpointUrls - extendedResponseFormat:(BOOL)extendedResponseFormat pluginVersion:(NSString *)pluginVersion + extendedResponseFormat:(BOOL)extendedResponseFormat + fallbackEndpointUrls:(NSArray *)fallbackEndpointUrls allowUseOfLocationData:(BOOL)allowUseOfLocationData locationTimeoutMillis:(double)locationTimeoutMillis + region:(NSString * _Nullable)region + endpointUrl:(NSString * _Nullable)endpointUrl ) RCT_EXTERN_METHOD(getVisitorId:(NSDictionary *)tags linkedId:(NSString *)linkedId - resolve:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject -) - -RCT_EXTERN_METHOD(getVisitorIdWithTimeout:(NSDictionary *)tags - linkedId:(NSString *)linkedId - timeout:(double)timeout + timeout:(NSNumber * _Nullable)timeout resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject ) RCT_EXTERN_METHOD(getVisitorData:(NSDictionary *)tags linkedId:(NSString *)linkedId - resolve:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject -) - -RCT_EXTERN_METHOD(getVisitorDataWithTimeout:(NSDictionary *)tags - linkedId:(NSString *)linkedId - timeout:(double)timeout + timeout:(NSNumber * _Nullable)timeout resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject ) diff --git a/sdk/ios/RNFingerprintjsPro.swift b/sdk/ios/RNFingerprintjsPro.swift index 02f99a02..d927ebeb 100644 --- a/sdk/ios/RNFingerprintjsPro.swift +++ b/sdk/ios/RNFingerprintjsPro.swift @@ -11,25 +11,16 @@ class RNFingerprintjsPro: NSObject { @objc static func requiresMainQueueSetup() -> Bool { false } - @objc(configure:region:endpointUrl:fallbackEndpointUrls:extendedResponseFormat:pluginVersion:allowUseOfLocationData:locationTimeoutMillis:) - public func configure(_ apiToken: String, _ region: String?, _ endpointUrl: String?, _ fallbackEndpointUrls: [String], _ extendedResponseFormat: Bool, _ pluginVersion: String, _ allowUseOfLocationData: Bool, _ locationTimeoutMillis: Double) -> Void { + @objc(configure:pluginVersion:extendedResponseFormat:fallbackEndpointUrls:allowUseOfLocationData:locationTimeoutMillis:region:endpointUrl:) + public func configure(_ apiToken: String, _ pluginVersion: String, _ extendedResponseFormat: Bool, _ fallbackEndpointUrls: [String], _ allowUseOfLocationData: Bool, _ locationTimeoutMillis: Double, _ region: String?, _ endpointUrl: String?) -> Void { let region = RNFingerprintjsPro.parseRegion(region, endpoint: endpointUrl, endpointFallbacks: fallbackEndpointUrls) let integrationInfo = [("fingerprint-pro-react-native", pluginVersion)] let configuration = Configuration(apiKey: apiToken, region: region, integrationInfo: integrationInfo, extendedResponseFormat: extendedResponseFormat, allowUseOfLocationData: allowUseOfLocationData) fpjsClient = FingerprintProFactory.getInstance(configuration) } - @objc(getVisitorId:linkedId:resolve:reject:) - public func getVisitorId(tags: [String: Any]?, linkedId: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { - self.getVisitorId(tags: tags, linkedId: linkedId, timeout: nil, resolve: resolve, reject: reject) - } - - @objc(getVisitorIdWithTimeout:linkedId:timeout:resolve:reject:) - public func getVisitorId(tags: [String: Any]?, linkedId: String?, timeout: Double, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { - self.getVisitorId(tags: tags, linkedId: linkedId, timeout: NSNumber(value: timeout), resolve: resolve, reject: reject) - } - - private func getVisitorId(tags: [String: Any]?, linkedId: String?, timeout: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { + @objc(getVisitorId:linkedId:timeout:resolve:reject:) + public func getVisitorId(tags: [String: Any]?, linkedId: String?, timeout: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { let metadata = RNFingerprintjsPro.prepareMetadata(linkedId, tags: tags) let completionHandler: FingerprintPro.VisitorIdBlock = { visitorIdResult in @@ -49,17 +40,8 @@ class RNFingerprintjsPro: NSObject { } } - @objc(getVisitorData:linkedId:resolve:reject:) - public func getVisitorData(tags: [String: Any]?, linkedId: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { - self.getVisitorData(tags: tags, linkedId: linkedId, timeout: nil, resolve: resolve, reject: reject) - } - - @objc(getVisitorDataWithTimeout:linkedId:timeout:resolve:reject:) - public func getVisitorData(tags: [String: Any]?, linkedId: String?, timeout: Double, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { - self.getVisitorData(tags: tags, linkedId: linkedId, timeout: NSNumber(value: timeout), resolve: resolve, reject: reject) - } - - private func getVisitorData(tags: [String: Any]?, linkedId: String?, timeout: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { + @objc(getVisitorData:linkedId:timeout:resolve:reject:) + public func getVisitorData(tags: [String: Any]?, linkedId: String?, timeout: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { let metadata = RNFingerprintjsPro.prepareMetadata(linkedId, tags: tags) let completionHandler: FingerprintPro.VisitorIdResponseBlock = { visitorIdResponseResult in diff --git a/sdk/src/FingerprintJsProAgent.ts b/sdk/src/FingerprintJsProAgent.ts index fc1c9eaf..bf4731a8 100644 --- a/sdk/src/FingerprintJsProAgent.ts +++ b/sdk/src/FingerprintJsProAgent.ts @@ -2,7 +2,7 @@ import { UnknownError } from './errors' import RNFingerprintjsPro, { type NativeVisitorData } from './specs/NativeRNFingerprintjsPro' import type { FingerprintJsProAgentParams, ProAgent, RequestOptions, Tags, VisitorData, VisitorId } from './types' import { unwrapError } from './unwrapError' -import { isDefined, isTruthy } from './utils' +import { isTruthy } from './utils' const packageVersion = '__VERSION__' @@ -31,13 +31,13 @@ export class FingerprintJsProAgent implements ProAgent { try { RNFingerprintjsPro.configure( apiKey, - region ?? null, - endpointUrl ?? null, - fallbackEndpointUrls, - extendedResponseFormat, packageVersion, + extendedResponseFormat, + fallbackEndpointUrls, allowUseOfLocationData, - locationTimeoutMillisAndroid + locationTimeoutMillisAndroid, + region ?? null, + endpointUrl ?? null ) this.requestOptions = requestOptions } catch (e) { @@ -55,11 +55,7 @@ export class FingerprintJsProAgent implements ProAgent { public async getVisitorId(tags?: Tags, linkedId?: string, options?: RequestOptions): Promise { try { const timeout = options?.timeout ?? this.requestOptions.timeout - if (isDefined(timeout)) { - return await RNFingerprintjsPro.getVisitorIdWithTimeout(tags ?? null, linkedId ?? null, timeout) - } - - return await RNFingerprintjsPro.getVisitorId(tags ?? null, linkedId ?? null) + return await RNFingerprintjsPro.getVisitorId(tags ?? null, linkedId ?? null, timeout ?? null) } catch (error) { if (error instanceof Error) { throw unwrapError(error) @@ -81,12 +77,11 @@ export class FingerprintJsProAgent implements ProAgent { public async getVisitorData(tags?: Tags, linkedId?: string, options?: RequestOptions): Promise { try { const timeout = options?.timeout ?? this.requestOptions.timeout - let visitorData: NativeVisitorData - if (isDefined(timeout)) { - visitorData = await RNFingerprintjsPro.getVisitorDataWithTimeout(tags ?? null, linkedId ?? null, timeout) - } else { - visitorData = await RNFingerprintjsPro.getVisitorData(tags ?? null, linkedId ?? null) - } + const visitorData: NativeVisitorData = await RNFingerprintjsPro.getVisitorData( + tags ?? null, + linkedId ?? null, + timeout ?? null + ) const { requestId, confidenceScore, visitorDataJson, sealedResult } = visitorData // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const result = { diff --git a/sdk/src/specs/NativeRNFingerprintjsPro.ts b/sdk/src/specs/NativeRNFingerprintjsPro.ts index c14b2cb9..00d32533 100644 --- a/sdk/src/specs/NativeRNFingerprintjsPro.ts +++ b/sdk/src/specs/NativeRNFingerprintjsPro.ts @@ -19,34 +19,26 @@ export interface NativeVisitorData { /** * Codegen TurboModule spec for the `RNFingerprintjsPro` native module. * - * Optional arguments are modelled as nullable (`| null`) rather than optional (`?`) because - * Codegen relies on positional arguments and some optional values are followed by required ones - * (e.g. `timeout`). Nullability maps onto the native modules, which already treat absent - * `tags`/`linkedId`/`region`/`endpointUrl` as "not provided". + * Optional arguments are modelled as nullable (`| null`) — Codegen relies on positional arguments, + * and nullability maps onto the native modules, which treat `null` as "not provided". Genuinely + * optional parameters (those without an SDK-side default: `region`, `endpointUrl`, `timeout`) come + * last in each signature. */ export interface Spec extends TurboModule { configure( apiToken: string, - region: string | null, - endpointUrl: string | null, - fallbackEndpointUrls: string[], - extendedResponseFormat: boolean, pluginVersion: string, + extendedResponseFormat: boolean, + fallbackEndpointUrls: string[], allowUseOfLocationData: boolean, - locationTimeoutMillis: Double + locationTimeoutMillis: Double, + region: string | null, + endpointUrl: string | null ): void - getVisitorId(tags: UnsafeObject | null, linkedId: string | null): Promise - - getVisitorIdWithTimeout(tags: UnsafeObject | null, linkedId: string | null, timeout: Double): Promise - - getVisitorData(tags: UnsafeObject | null, linkedId: string | null): Promise + getVisitorId(tags: UnsafeObject | null, linkedId: string | null, timeout: Double | null): Promise - getVisitorDataWithTimeout( - tags: UnsafeObject | null, - linkedId: string | null, - timeout: Double - ): Promise + getVisitorData(tags: UnsafeObject | null, linkedId: string | null, timeout: Double | null): Promise } export default TurboModuleRegistry.getEnforcing('RNFingerprintjsPro') From 23f53cdb3f21565a3fe95644ca94a1b5653ec834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Wed, 5 Aug 2026 09:26:24 +0200 Subject: [PATCH 29/39] refactor: enforce non-null timeout with sentinel values --- sdk/__tests__/FingerprintJsProProvider.test.ts | 4 ++-- sdk/__tests__/useVisitorData.test.ts | 4 ++-- .../reactnative/RNFingerprintjsProModule.kt | 11 +++++++---- .../reactnative/RNFingerprintjsProSpec.kt | 4 ++-- sdk/ios/RNFingerprintjsPro.mm | 4 ++-- sdk/ios/RNFingerprintjsPro.swift | 10 ++++++---- sdk/src/FingerprintJsProAgent.ts | 11 +++++++++-- sdk/src/specs/NativeRNFingerprintjsPro.ts | 15 +++++++++------ 8 files changed, 39 insertions(+), 24 deletions(-) diff --git a/sdk/__tests__/FingerprintJsProProvider.test.ts b/sdk/__tests__/FingerprintJsProProvider.test.ts index b995acf3..b3c16ef6 100644 --- a/sdk/__tests__/FingerprintJsProProvider.test.ts +++ b/sdk/__tests__/FingerprintJsProProvider.test.ts @@ -113,7 +113,7 @@ describe(`FingerprintJsProProvider`, () => { const fingerprintClient = new FingerprintJsProAgent(options) void fingerprintClient.getVisitorId() - expect(getVisitorId).toHaveBeenCalledWith(null, null, null) + expect(getVisitorId).toHaveBeenCalledWith(null, null, -1) }) it('should pass timeout to `getVisitorId` function', () => { @@ -165,7 +165,7 @@ describe(`FingerprintJsProProvider`, () => { void fingerprintClient.getVisitorData() - expect(getVisitorData).toHaveBeenCalledWith(null, null, null) + expect(getVisitorData).toHaveBeenCalledWith(null, null, -1) }) it('should pass timeout to `getVisitorData` function', () => { diff --git a/sdk/__tests__/useVisitorData.test.ts b/sdk/__tests__/useVisitorData.test.ts index d7153a7b..a00c2524 100644 --- a/sdk/__tests__/useVisitorData.test.ts +++ b/sdk/__tests__/useVisitorData.test.ts @@ -133,7 +133,7 @@ describe('useVisitorData', () => { void result.current.getData(mockedTags, mockedLinkedId) }) await waitFor(() => { - expect(getVisitorData).toHaveBeenCalledWith(mockedTags, mockedLinkedId, null) + expect(getVisitorData).toHaveBeenCalledWith(mockedTags, mockedLinkedId, -1) }) }) @@ -170,7 +170,7 @@ describe('useVisitorData', () => { void result.current.getData(mockedTags, mockedLinkedId, options) }) await waitFor(() => { - expect(getVisitorData).toHaveBeenCalledWith(mockedTags, mockedLinkedId, null) + expect(getVisitorData).toHaveBeenCalledWith(mockedTags, mockedLinkedId, -1) }) }) diff --git a/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProModule.kt b/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProModule.kt index 67663ec7..fd9109a5 100644 --- a/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProModule.kt +++ b/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProModule.kt @@ -73,15 +73,18 @@ class RNFingerprintjsProModule(reactContext: ReactApplicationContext) : RNFinger } @ReactMethod - override fun getVisitorId(tags: ReadableMap?, linkedId: String?, timeout: Double?, promise: Promise) { - getVisitorIdInternal(tags, linkedId, timeout?.toInt(), promise) + override fun getVisitorId(tags: ReadableMap?, linkedId: String?, timeout: Double, promise: Promise) { + getVisitorIdInternal(tags, linkedId, timeout.toTimeoutOrNull(), promise) } @ReactMethod - override fun getVisitorData(tags: ReadableMap?, linkedId: String?, timeout: Double?, promise: Promise) { - getVisitorDataInternal(tags, linkedId, timeout?.toInt(), promise) + override fun getVisitorData(tags: ReadableMap?, linkedId: String?, timeout: Double, promise: Promise) { + getVisitorDataInternal(tags, linkedId, timeout.toTimeoutOrNull(), promise) } + // A negative sentinel means "no timeout" (see the JS spec: the legacy bridge can't pass a null number). + private fun Double.toTimeoutOrNull(): Int? = if (this >= 0) this.toInt() else null + private fun getVisitorIdInternal(tags: ReadableMap?, linkedId: String?, timeout: Int?, promise: Promise) { try { if (timeout != null) { diff --git a/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt b/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt index cd759799..4e9f0e1a 100644 --- a/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt +++ b/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt @@ -26,7 +26,7 @@ abstract class RNFingerprintjsProSpec(reactContext: ReactApplicationContext) : endpointUrl: String? ) - abstract fun getVisitorId(tags: ReadableMap?, linkedId: String?, timeout: Double?, promise: Promise) + abstract fun getVisitorId(tags: ReadableMap?, linkedId: String?, timeout: Double, promise: Promise) - abstract fun getVisitorData(tags: ReadableMap?, linkedId: String?, timeout: Double?, promise: Promise) + abstract fun getVisitorData(tags: ReadableMap?, linkedId: String?, timeout: Double, promise: Promise) } diff --git a/sdk/ios/RNFingerprintjsPro.mm b/sdk/ios/RNFingerprintjsPro.mm index 133325fa..21977feb 100644 --- a/sdk/ios/RNFingerprintjsPro.mm +++ b/sdk/ios/RNFingerprintjsPro.mm @@ -14,14 +14,14 @@ @interface RCT_EXTERN_MODULE(RNFingerprintjsPro, NSObject) RCT_EXTERN_METHOD(getVisitorId:(NSDictionary *)tags linkedId:(NSString *)linkedId - timeout:(NSNumber * _Nullable)timeout + timeout:(double)timeout resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject ) RCT_EXTERN_METHOD(getVisitorData:(NSDictionary *)tags linkedId:(NSString *)linkedId - timeout:(NSNumber * _Nullable)timeout + timeout:(double)timeout resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject ) diff --git a/sdk/ios/RNFingerprintjsPro.swift b/sdk/ios/RNFingerprintjsPro.swift index d927ebeb..ab002b10 100644 --- a/sdk/ios/RNFingerprintjsPro.swift +++ b/sdk/ios/RNFingerprintjsPro.swift @@ -20,7 +20,7 @@ class RNFingerprintjsPro: NSObject { } @objc(getVisitorId:linkedId:timeout:resolve:reject:) - public func getVisitorId(tags: [String: Any]?, linkedId: String?, timeout: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { + public func getVisitorId(tags: [String: Any]?, linkedId: String?, timeout: Double, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { let metadata = RNFingerprintjsPro.prepareMetadata(linkedId, tags: tags) let completionHandler: FingerprintPro.VisitorIdBlock = { visitorIdResult in @@ -33,7 +33,8 @@ class RNFingerprintjsPro: NSObject { } } - if let timeout = timeout?.doubleValue { + // A negative sentinel means "no timeout" (see the JS spec). + if timeout >= 0 { fpjsClient?.getVisitorId(metadata, timeout: timeout / 1000, completion: completionHandler) } else { fpjsClient?.getVisitorId(metadata, completion: completionHandler) @@ -41,7 +42,7 @@ class RNFingerprintjsPro: NSObject { } @objc(getVisitorData:linkedId:timeout:resolve:reject:) - public func getVisitorData(tags: [String: Any]?, linkedId: String?, timeout: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { + public func getVisitorData(tags: [String: Any]?, linkedId: String?, timeout: Double, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { let metadata = RNFingerprintjsPro.prepareMetadata(linkedId, tags: tags) let completionHandler: FingerprintPro.VisitorIdResponseBlock = { visitorIdResponseResult in @@ -60,7 +61,8 @@ class RNFingerprintjsPro: NSObject { } } - if let timeout = timeout?.doubleValue { + // A negative sentinel means "no timeout" (see the JS spec). + if timeout >= 0 { fpjsClient?.getVisitorIdResponse(metadata, timeout: timeout / 1000, completion: completionHandler) } else { fpjsClient?.getVisitorIdResponse(metadata, completion: completionHandler) diff --git a/sdk/src/FingerprintJsProAgent.ts b/sdk/src/FingerprintJsProAgent.ts index bf4731a8..17df6457 100644 --- a/sdk/src/FingerprintJsProAgent.ts +++ b/sdk/src/FingerprintJsProAgent.ts @@ -6,6 +6,13 @@ import { isTruthy } from './utils' const packageVersion = '__VERSION__' +/** + * Sentinel passed to the native `timeout` argument when no timeout is set. A negative value means + * "no timeout". We can't pass `null` because the legacy Android bridge (old architecture) cannot + * accept a null number argument. + */ +const NO_TIMEOUT = -1 + /** * * @group API Client approach @@ -55,7 +62,7 @@ export class FingerprintJsProAgent implements ProAgent { public async getVisitorId(tags?: Tags, linkedId?: string, options?: RequestOptions): Promise { try { const timeout = options?.timeout ?? this.requestOptions.timeout - return await RNFingerprintjsPro.getVisitorId(tags ?? null, linkedId ?? null, timeout ?? null) + return await RNFingerprintjsPro.getVisitorId(tags ?? null, linkedId ?? null, timeout ?? NO_TIMEOUT) } catch (error) { if (error instanceof Error) { throw unwrapError(error) @@ -80,7 +87,7 @@ export class FingerprintJsProAgent implements ProAgent { const visitorData: NativeVisitorData = await RNFingerprintjsPro.getVisitorData( tags ?? null, linkedId ?? null, - timeout ?? null + timeout ?? NO_TIMEOUT ) const { requestId, confidenceScore, visitorDataJson, sealedResult } = visitorData // eslint-disable-next-line @typescript-eslint/consistent-type-assertions diff --git a/sdk/src/specs/NativeRNFingerprintjsPro.ts b/sdk/src/specs/NativeRNFingerprintjsPro.ts index 00d32533..d31155c7 100644 --- a/sdk/src/specs/NativeRNFingerprintjsPro.ts +++ b/sdk/src/specs/NativeRNFingerprintjsPro.ts @@ -19,10 +19,13 @@ export interface NativeVisitorData { /** * Codegen TurboModule spec for the `RNFingerprintjsPro` native module. * - * Optional arguments are modelled as nullable (`| null`) — Codegen relies on positional arguments, - * and nullability maps onto the native modules, which treat `null` as "not provided". Genuinely - * optional parameters (those without an SDK-side default: `region`, `endpointUrl`, `timeout`) come - * last in each signature. + * Optional string/object arguments are modelled as nullable (`| null`); Codegen relies on positional + * arguments and native treats `null` as "not provided". Genuinely optional parameters (those without + * an SDK-side default: `region`, `endpointUrl`, `timeout`) come last in each signature. + * + * `timeout` is a NON-nullable `Double` using a negative sentinel (< 0 means "no timeout") rather than + * `Double | null`, because the legacy (old-architecture) Android bridge cannot pass a null number + * argument — it unboxes every numeric arg via `ReadableNativeArray.getDouble()` and NPEs on null. */ export interface Spec extends TurboModule { configure( @@ -36,9 +39,9 @@ export interface Spec extends TurboModule { endpointUrl: string | null ): void - getVisitorId(tags: UnsafeObject | null, linkedId: string | null, timeout: Double | null): Promise + getVisitorId(tags: UnsafeObject | null, linkedId: string | null, timeout: Double): Promise - getVisitorData(tags: UnsafeObject | null, linkedId: string | null, timeout: Double | null): Promise + getVisitorData(tags: UnsafeObject | null, linkedId: string | null, timeout: Double): Promise } export default TurboModuleRegistry.getEnforcing('RNFingerprintjsPro') From 5efd0efdad3f6760cff4131992ec3e659d93f960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Wed, 5 Aug 2026 14:02:29 +0200 Subject: [PATCH 30/39] refactor: remove legacy architecture support and dependencies --- .changeset/new-architecture-support.md | 6 +- .github/workflows/e2e-tests.yml | 70 ++++--------------- README.md | 4 +- e2e-app/app-config-tools/android.ts | 12 ---- e2e-app/app-config-tools/arch.ts | 14 ---- .../app-config-tools/expoConfigPatchers.ts | 20 ------ .../app-config-tools/reactNativeMetadata.ts | 30 ++------ e2e-app/app.config.ts | 7 +- e2e-app/plugins/withAndroidMavenRepo.js | 36 ---------- e2e-app/scripts/compatibility.mjs | 32 --------- sdk/README.md | 4 +- .../FingerprintJsProProvider.test.ts | 4 +- sdk/__tests__/useVisitorData.test.ts | 4 +- sdk/android/build.gradle | 25 ++----- sdk/android/gradle.properties | 6 -- .../reactnative/RNFingerprintjsProModule.kt | 16 ++--- .../reactnative/RNFingerprintjsProPackage.kt | 4 +- .../reactnative/RNFingerprintjsProSpec.kt | 12 ---- .../reactnative/RNFingerprintjsProPackage.kt | 23 ------ .../reactnative/RNFingerprintjsProSpec.kt | 32 --------- sdk/ios/RNFingerprintjsPro.mm | 12 ++-- sdk/ios/RNFingerprintjsPro.swift | 10 ++- sdk/package.json | 4 +- sdk/src/FingerprintJsProAgent.ts | 11 +-- sdk/src/specs/NativeRNFingerprintjsPro.ts | 14 ++-- 25 files changed, 60 insertions(+), 352 deletions(-) delete mode 100644 e2e-app/app-config-tools/arch.ts delete mode 100644 e2e-app/app-config-tools/expoConfigPatchers.ts delete mode 100644 e2e-app/plugins/withAndroidMavenRepo.js rename sdk/android/src/{newarch => main}/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt (79%) delete mode 100644 sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt delete mode 100644 sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt delete mode 100644 sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt diff --git a/.changeset/new-architecture-support.md b/.changeset/new-architecture-support.md index eecbbf13..082fbd0b 100644 --- a/.changeset/new-architecture-support.md +++ b/.changeset/new-architecture-support.md @@ -1,5 +1,7 @@ --- -'@fingerprintjs/fingerprintjs-pro-react-native': minor +'@fingerprintjs/fingerprintjs-pro-react-native': major --- -Added React Native New Architecture (TurboModule) support. The native module is now exposed as a TurboModule via a Codegen spec while remaining fully backward compatible with the old architecture +Dropped React Native Old Architecture support. The native module is now a TurboModule (New Architecture only) exposed via a Codegen spec. + +This is a breaking change: the SDK now requires React Native >= 0.79 (Expo SDK >= 53) with the [New Architecture](https://reactnative.dev/architecture/landing-page) enabled. diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index f116db86..622e2890 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -18,7 +18,7 @@ jobs: steps: - id: set-matrix run: | - echo "matrix=['local', '0.73', '0.74', '0.75', '0.76', '0.77', '0.79', '0.81', '0.83', '0.85']" >> $GITHUB_OUTPUT + echo "matrix=['local', '0.79', '0.81', '0.83', '0.85']" >> $GITHUB_OUTPUT build-android: strategy: @@ -26,20 +26,10 @@ jobs: max-parallel: 3 matrix: react-native: ${{ fromJSON(needs.build-matrix.outputs.react-native-matrix) }} - architecture: [old, new] - exclude: - # New architecture is not tested on the Expo 50/51 SDKs (experimental there) - - { react-native: '0.73', architecture: new } - - { react-native: '0.74', architecture: new } - - { react-native: '0.75', architecture: new } - # RN 0.82+ (Expo SDK 55+) removed the ability to disable the new architecture - - { react-native: '0.83', architecture: old } - - { react-native: '0.85', architecture: old } - - { react-native: 'local', architecture: old } runs-on: ubuntu-latest needs: build-matrix - name: Build android for E2E tests for react-native ${{ matrix.react-native }} (${{ matrix.architecture }} arch) + name: Build android for E2E tests for react-native ${{ matrix.react-native }} steps: - uses: actions/checkout@v7 @@ -94,8 +84,6 @@ jobs: - name: Prebuild run: pnpm prebuild working-directory: e2e-app - env: - E2E_NEW_ARCH: ${{ matrix.architecture == 'new' }} - run: node scripts/retry-cmd.mjs --retries=3 "pnpm exec detox build --configuration android.emu.release" working-directory: e2e-app @@ -103,7 +91,7 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v7 with: - name: android-app-release-${{ matrix.react-native }}-${{ matrix.architecture }} + name: android-app-release-${{ matrix.react-native }} path: e2e-app/android/app/build retention-days: 3 @@ -112,22 +100,12 @@ jobs: - build-android - build-matrix runs-on: ubuntu-latest - name: Android E2E Tests for react-native ${{ matrix.react-native }} (${{ matrix.architecture }} arch) + name: Android E2E Tests for react-native ${{ matrix.react-native }} strategy: max-parallel: 3 fail-fast: false matrix: react-native: ${{ fromJSON(needs.build-matrix.outputs.react-native-matrix) }} - architecture: [old, new] - exclude: - # New architecture is not tested on the Expo 50/51 SDKs (experimental there) - - { react-native: '0.73', architecture: new } - - { react-native: '0.74', architecture: new } - - { react-native: '0.75', architecture: new } - # RN 0.82+ (Expo SDK 55+) removed the ability to disable the new architecture - - { react-native: '0.83', architecture: old } - - { react-native: '0.85', architecture: old } - - { react-native: 'local', architecture: old } steps: - uses: actions/checkout@v7 @@ -167,7 +145,7 @@ jobs: - uses: actions/download-artifact@v8 with: - name: android-app-release-${{ matrix.react-native }}-${{ matrix.architecture }} + name: android-app-release-${{ matrix.react-native }} path: e2e-app/android/app/build - name: Get device name @@ -221,7 +199,7 @@ jobs: if: always() uses: actions/upload-artifact@v7 with: - name: android-detox-artifacts-${{matrix.react-native}}-${{ matrix.architecture }} + name: android-detox-artifacts-${{matrix.react-native}} path: '**/detox-artifacts' retention-days: 3 @@ -229,29 +207,19 @@ jobs: name: Delete build artifact if: success() with: - name: android-app-release-${{ matrix.react-native }}-${{ matrix.architecture }} + name: android-app-release-${{ matrix.react-native }} build-ios: env: HOMEBREW_NO_AUTO_UPDATE: 1 HOMEBREW_NO_INSTALL_CLEANUP: 1 runs-on: macos-26 - name: Build iOS for E2E tests for react-native ${{ matrix.react-native }} (${{ matrix.architecture }} arch) + name: Build iOS for E2E tests for react-native ${{ matrix.react-native }} strategy: max-parallel: 3 fail-fast: false matrix: react-native: ${{ fromJSON(needs.build-matrix.outputs.react-native-matrix) }} - architecture: [old, new] - exclude: - # New architecture is not tested on the Expo 50/51 SDKs (experimental there) - - { react-native: '0.73', architecture: new } - - { react-native: '0.74', architecture: new } - - { react-native: '0.75', architecture: new } - # RN 0.82+ (Expo SDK 55+) removed the ability to disable the new architecture - - { react-native: '0.83', architecture: old } - - { react-native: '0.85', architecture: old } - - { react-native: 'local', architecture: old } needs: build-matrix steps: - uses: actions/checkout@v7 @@ -316,8 +284,6 @@ jobs: - name: Prebuild run: pnpm prebuild working-directory: e2e-app - env: - E2E_NEW_ARCH: ${{ matrix.architecture == 'new' }} - run: node scripts/retry-cmd.mjs --retries=3 "pnpm exec detox build --configuration ios.sim.release" working-directory: e2e-app @@ -325,7 +291,7 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v7 with: - name: ios-app-release-${{ matrix.react-native }}-${{ matrix.architecture }} + name: ios-app-release-${{ matrix.react-native }} path: e2e-app/ios/build/Build retention-days: 3 @@ -334,7 +300,7 @@ jobs: HOMEBREW_NO_AUTO_UPDATE: 1 HOMEBREW_NO_INSTALL_CLEANUP: 1 runs-on: macos-26 - name: iOS E2E Tests for react-native ${{ matrix.react-native }} (${{ matrix.architecture }} arch) + name: iOS E2E Tests for react-native ${{ matrix.react-native }} needs: - build-ios - build-matrix @@ -343,16 +309,6 @@ jobs: fail-fast: false matrix: react-native: ${{ fromJSON(needs.build-matrix.outputs.react-native-matrix) }} - architecture: [old, new] - exclude: - # New architecture is not tested on the Expo 50/51 SDKs (experimental there) - - { react-native: '0.73', architecture: new } - - { react-native: '0.74', architecture: new } - - { react-native: '0.75', architecture: new } - # RN 0.82+ (Expo SDK 55+) removed the ability to disable the new architecture - - { react-native: '0.83', architecture: old } - - { react-native: '0.85', architecture: old } - - { react-native: 'local', architecture: old } steps: - uses: actions/checkout@v7 @@ -381,7 +337,7 @@ jobs: - uses: actions/download-artifact@v8 with: - name: ios-app-release-${{ matrix.react-native }}-${{ matrix.architecture }} + name: ios-app-release-${{ matrix.react-native }} path: e2e-app/ios/build/Build - name: Start iPhone simulator @@ -417,7 +373,7 @@ jobs: if: always() uses: actions/upload-artifact@v7 with: - name: ios-detox-artifacts-${{matrix.react-native}}-${{ matrix.architecture }} + name: ios-detox-artifacts-${{matrix.react-native}} path: '**/detox-artifacts' retention-days: 3 @@ -425,7 +381,7 @@ jobs: name: Delete build artifact if: success() with: - name: ios-app-release-${{ matrix.react-native }}-${{ matrix.architecture }} + name: ios-app-release-${{ matrix.react-native }} e2e-tests-web: name: Web E2E tests diff --git a/README.md b/README.md index 1ae08ee4..8f0f4f38 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,8 @@ application to call the native Fingerprint Pro libraries (Android and iOS) and i ## Requirements and limitations -- React Native versions 0.73 through 0.86 are supported -- Expo 51.0.0 or higher is supported +- React Native 0.79 or higher is supported (New Architecture only) +- Expo 53.0.0 or higher is supported - Android 6.0 (API level 23+) or higher - iOS 13+/tvOS 15+, Swift 5.9 or higher (stable releases) diff --git a/e2e-app/app-config-tools/android.ts b/e2e-app/app-config-tools/android.ts index 1360b7fc..12da4e55 100644 --- a/e2e-app/app-config-tools/android.ts +++ b/e2e-app/app-config-tools/android.ts @@ -20,18 +20,6 @@ export function withAndroidBuildProperties(props: Partial void @@ -19,29 +15,11 @@ type ReactNativeMetadata = { * - The key represents the version of React Native. * - The value is an object containing: * - `callbacks`: An array of functions to execute in the context of the respective React Native version. + * + * The SDK supports React Native >= 0.79 (New Architecture only), so no version currently needs + * version-specific handling. New entries can be added here if a future version requires it. */ -const reactNativeMetadata: Record = { - 0.73: { - callbacks: [disableNewArch, stripAndroidExtraBuildProperties, setCompileSdkVersion(34)], - patchExpoConfig: [withSplashscreen, withFingerprintMavenRepo], - }, - 0.74: { - callbacks: [disableNewArch, stripAndroidExtraBuildProperties, setCompileSdkVersion(34)], - patchExpoConfig: [withSplashscreen], - }, - 0.75: { - callbacks: [disableNewArch, stripAndroidExtraBuildProperties, setCompileSdkVersion(34)], - patchExpoConfig: [withSplashscreen], - }, - 0.76: { - callbacks: [stripAndroidExtraBuildProperties, setCompileSdkVersion(35)], - patchExpoConfig: [withSplashscreen], - }, - 0.77: { - callbacks: [stripAndroidExtraBuildProperties, setCompileSdkVersion(35)], - patchExpoConfig: [withSplashscreen], - }, -} +const reactNativeMetadata: Record = {} export function handleReactNativeVersion(rnVersion: string) { if (reactNativeMetadata[rnVersion]?.callbacks?.length) { diff --git a/e2e-app/app.config.ts b/e2e-app/app.config.ts index 8b1c4007..56367f82 100644 --- a/e2e-app/app.config.ts +++ b/e2e-app/app.config.ts @@ -5,7 +5,6 @@ import { dependencies } from './package.json' import * as semver from 'semver' import { handleReactNativeVersion, patchExpoConfig } from './app-config-tools/reactNativeMetadata' import { getAndroidBuildProperties } from './app-config-tools/android' -import { withNewArchFlag } from './app-config-tools/expoConfigPatchers' let rawReactNativeVersion = dependencies['react-native'] @@ -49,10 +48,8 @@ const config: ExpoConfig = { ], } -// Set newArchEnabled explicitly for every RN version so that E2E_NEW_ARCH can force either -// architecture at prebuild time (versions without a metadata entry would otherwise inherit -// Expo's default and never write the flag). +// The SDK supports the New Architecture only (React Native >= 0.79), so always enable it. // eslint-disable-next-line @typescript-eslint/no-explicit-any -withNewArchFlag(config as any) +;(config as any).newArchEnabled = true export default patchExpoConfig(rnVersionStr, config) diff --git a/e2e-app/plugins/withAndroidMavenRepo.js b/e2e-app/plugins/withAndroidMavenRepo.js deleted file mode 100644 index d371017a..00000000 --- a/e2e-app/plugins/withAndroidMavenRepo.js +++ /dev/null @@ -1,36 +0,0 @@ -const { withProjectBuildGradle } = require('@expo/config-plugins') - -// The Fingerprint Android SDK (com.fingerprint.android:pro) is hosted on a dedicated Maven repo. -// The SDK's config plugin adds it via the `android.extraMavenRepos` gradle property, but that is -// only honored by expo-modules-autolinking >= 1.11 (Expo SDK 51+). Expo 50 (RN 0.73) ships 1.10.x, -// which ignores it, so the repo is never added and the build fails with "Could not find any -// matches for com.fingerprint.android:pro". This injects it straight into the root build.gradle's -// allprojects.repositories block. Only wired in for the Expo 50 / RN 0.73 build; an Expo 50 (or -// bare RN) customer adds this repo manually the same way, per the SDK README. -const MAVEN_REPO = 'https://maven.fpregistry.io/releases' - -module.exports = (config) => { - return withProjectBuildGradle(config, (gradleConfig) => { - if (gradleConfig.modResults.language !== 'groovy') { - throw new Error('withAndroidMavenRepo: expected a groovy build.gradle') - } - - const contents = gradleConfig.modResults.contents - - if (contents.includes(MAVEN_REPO)) { - return gradleConfig - } - - const updated = contents.replace( - /allprojects\s*\{\s*repositories\s*\{/, - (match) => `${match}\n maven { url '${MAVEN_REPO}' }` - ) - - if (updated === contents) { - throw new Error('withAndroidMavenRepo: could not find the allprojects.repositories block in build.gradle') - } - - gradleConfig.modResults.contents = updated - return gradleConfig - }) -} diff --git a/e2e-app/scripts/compatibility.mjs b/e2e-app/scripts/compatibility.mjs index 430a1cdb..bedbb314 100644 --- a/e2e-app/scripts/compatibility.mjs +++ b/e2e-app/scripts/compatibility.mjs @@ -4,38 +4,6 @@ import pkg from '../package.json' with { type: 'json' } const devPackages = Object.keys(pkg.devDependencies) const reactNativeMetadata = { - 0.73: { - // Expo 50 (the only SDK bundling RN 0.73) predates autolinking's `android.extraMavenRepos` - // support, so the SDK's Maven repo is injected directly via plugins/withAndroidMavenRepo.js - // (wired in for 0.73 through reactNativeMetadata). @config-plugins/detox@7 is the SDK 50 peer. - packages: ['expo@50', 'detox@20.20.3', '@config-plugins/detox@7', 'react@18.2.0', 'react-dom@18.2.0'], - }, - 0.74: { - packages: ['expo@51', 'detox@20.20.3', '@config-plugins/detox@8', 'react@18.2.0', 'react-dom@18.2.0'], - }, - 0.75: { - packages: [ - 'expo@51', - 'detox@20.20.3', - '@config-plugins/detox@8', - 'react@18.2.0', - 'react-dom@18.2.0', - '@react-native-community/cli@14', - ], - }, - 0.76: { - packages: [ - 'expo@52', - 'detox@20.28.0', - '@config-plugins/detox@9', - 'react@18.3.1', - 'react-dom@18.3.1', - '@react-native-community/cli@15', - ], - }, - 0.77: { - packages: ['expo@52', 'detox@20.28.0', '@config-plugins/detox@9', 'react@18.3.1', 'react-dom@18.3.1'], - }, 0.79: { packages: ['expo@53', 'detox@20.37.0', '@config-plugins/detox@11', 'react@19.0.0', 'react-dom@19.0.0'], }, diff --git a/sdk/README.md b/sdk/README.md index 54c0ad92..8f0f4f38 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -62,8 +62,8 @@ application to call the native Fingerprint Pro libraries (Android and iOS) and i ## Requirements and limitations -- React Native versions 0.73 through 0.85 are supported -- Expo 51.0.0 or higher is supported +- React Native 0.79 or higher is supported (New Architecture only) +- Expo 53.0.0 or higher is supported - Android 6.0 (API level 23+) or higher - iOS 13+/tvOS 15+, Swift 5.9 or higher (stable releases) diff --git a/sdk/__tests__/FingerprintJsProProvider.test.ts b/sdk/__tests__/FingerprintJsProProvider.test.ts index b3c16ef6..b995acf3 100644 --- a/sdk/__tests__/FingerprintJsProProvider.test.ts +++ b/sdk/__tests__/FingerprintJsProProvider.test.ts @@ -113,7 +113,7 @@ describe(`FingerprintJsProProvider`, () => { const fingerprintClient = new FingerprintJsProAgent(options) void fingerprintClient.getVisitorId() - expect(getVisitorId).toHaveBeenCalledWith(null, null, -1) + expect(getVisitorId).toHaveBeenCalledWith(null, null, null) }) it('should pass timeout to `getVisitorId` function', () => { @@ -165,7 +165,7 @@ describe(`FingerprintJsProProvider`, () => { void fingerprintClient.getVisitorData() - expect(getVisitorData).toHaveBeenCalledWith(null, null, -1) + expect(getVisitorData).toHaveBeenCalledWith(null, null, null) }) it('should pass timeout to `getVisitorData` function', () => { diff --git a/sdk/__tests__/useVisitorData.test.ts b/sdk/__tests__/useVisitorData.test.ts index a00c2524..d7153a7b 100644 --- a/sdk/__tests__/useVisitorData.test.ts +++ b/sdk/__tests__/useVisitorData.test.ts @@ -133,7 +133,7 @@ describe('useVisitorData', () => { void result.current.getData(mockedTags, mockedLinkedId) }) await waitFor(() => { - expect(getVisitorData).toHaveBeenCalledWith(mockedTags, mockedLinkedId, -1) + expect(getVisitorData).toHaveBeenCalledWith(mockedTags, mockedLinkedId, null) }) }) @@ -170,7 +170,7 @@ describe('useVisitorData', () => { void result.current.getData(mockedTags, mockedLinkedId, options) }) await waitFor(() => { - expect(getVisitorData).toHaveBeenCalledWith(mockedTags, mockedLinkedId, -1) + expect(getVisitorData).toHaveBeenCalledWith(mockedTags, mockedLinkedId, null) }) }) diff --git a/sdk/android/build.gradle b/sdk/android/build.gradle index 8ba5a8ea..b6b3e9f9 100644 --- a/sdk/android/build.gradle +++ b/sdk/android/build.gradle @@ -3,17 +3,9 @@ plugins { id 'org.jetbrains.kotlin.android' } -def isNewArchitectureEnabled() { - // To opt-in for the New Architecture, you can either: - // - Set `newArchEnabled` to true inside the `gradle.properties` file - // - Invoke gradle with `-PnewArchEnabled=true` - // - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true` - return project.hasProperty("newArchEnabled") && project.getProperty("newArchEnabled") == "true" -} - -if (isNewArchitectureEnabled()) { - apply plugin: 'com.facebook.react' -} +// The SDK supports the New Architecture only. Applying the React Gradle plugin runs library Codegen +// (generates the `NativeRNFingerprintjsProSpec` base class). +apply plugin: 'com.facebook.react' apply from: 'fingerprint.gradle' @@ -48,15 +40,8 @@ android { sourceSets { main { - if (isNewArchitectureEnabled()) { - java.srcDirs += [ - 'src/newarch', - // Generated Codegen sources (the `NativeRNFingerprintjsProSpec` base class). - layout.buildDirectory.dir('generated/source/codegen/java').get().asFile.path - ] - } else { - java.srcDirs += ['src/oldarch'] - } + // Generated Codegen sources (the `NativeRNFingerprintjsProSpec` base class). + java.srcDirs += [layout.buildDirectory.dir('generated/source/codegen/java').get().asFile.path] } } } diff --git a/sdk/android/gradle.properties b/sdk/android/gradle.properties index b441ddb9..19b45129 100644 --- a/sdk/android/gradle.properties +++ b/sdk/android/gradle.properties @@ -31,9 +31,3 @@ FLIPPER_VERSION=0.125.0 # You can also override it from the CLI using # ./gradlew -PreactNativeArchitectures=x86_64 reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 - -# NOTE: `newArchEnabled` is intentionally NOT set here. This library must follow the architecture -# of the consuming app. Hardcoding it (e.g. `newArchEnabled=false`) shadows the app's value for -# this subproject on some React Native versions, so the library would skip Codegen while the app -# still autolinks it as a TurboModule — breaking the app's native build. The value is inherited -# from the consuming app's gradle.properties instead (see `isNewArchitectureEnabled()` in build.gradle). diff --git a/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProModule.kt b/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProModule.kt index fd9109a5..694f8c31 100644 --- a/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProModule.kt +++ b/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProModule.kt @@ -33,14 +33,13 @@ import java.lang.Exception @ReactModule(name = RNFingerprintjsProModule.NAME) -class RNFingerprintjsProModule(reactContext: ReactApplicationContext) : RNFingerprintjsProSpec(reactContext) { +class RNFingerprintjsProModule(reactContext: ReactApplicationContext) : NativeRNFingerprintjsProSpec(reactContext) { private var fpjsClient: FingerprintJS? = null override fun getName(): String { return NAME } - @ReactMethod override fun configure( apiToken: String, pluginVersion: String, @@ -72,19 +71,14 @@ class RNFingerprintjsProModule(reactContext: ReactApplicationContext) : RNFinger fpjsClient = factory.createInstance(configuration) } - @ReactMethod - override fun getVisitorId(tags: ReadableMap?, linkedId: String?, timeout: Double, promise: Promise) { - getVisitorIdInternal(tags, linkedId, timeout.toTimeoutOrNull(), promise) + override fun getVisitorId(tags: ReadableMap?, linkedId: String?, timeout: Double?, promise: Promise) { + getVisitorIdInternal(tags, linkedId, timeout?.toInt(), promise) } - @ReactMethod - override fun getVisitorData(tags: ReadableMap?, linkedId: String?, timeout: Double, promise: Promise) { - getVisitorDataInternal(tags, linkedId, timeout.toTimeoutOrNull(), promise) + override fun getVisitorData(tags: ReadableMap?, linkedId: String?, timeout: Double?, promise: Promise) { + getVisitorDataInternal(tags, linkedId, timeout?.toInt(), promise) } - // A negative sentinel means "no timeout" (see the JS spec: the legacy bridge can't pass a null number). - private fun Double.toTimeoutOrNull(): Int? = if (this >= 0) this.toInt() else null - private fun getVisitorIdInternal(tags: ReadableMap?, linkedId: String?, timeout: Int?, promise: Promise) { try { if (timeout != null) { diff --git a/sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt b/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt similarity index 79% rename from sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt rename to sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt index a45bf803..60dd7820 100644 --- a/sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt +++ b/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt @@ -8,9 +8,7 @@ import com.facebook.react.module.model.ReactModuleInfoProvider /** - * New-architecture package. Uses `BaseReactPackage` + `ReactModuleInfoProvider` so the module is - * registered as a TurboModule. These APIs only exist on newer React Native; the old-architecture - * counterpart in `src/oldarch` uses the legacy [com.facebook.react.ReactPackage] API instead. + * Registers the module as a TurboModule via `BaseReactPackage` + `ReactModuleInfoProvider`. */ class RNFingerprintjsProPackage : BaseReactPackage() { override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { diff --git a/sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt b/sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt deleted file mode 100644 index 1e7a5c3f..00000000 --- a/sdk/android/src/newarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.fingerprintjs.reactnative - -import com.facebook.react.bridge.ReactApplicationContext - -/** - * New-architecture base class. Extends the Codegen-generated `NativeRNFingerprintjsProSpec` - * (produced from `src/specs/NativeRNFingerprintjsPro.ts`) so `RNFingerprintjsProModule` is a real - * TurboModule. The generated class lives in this same package (see `codegenConfig.android - * .javaPackageName` in `package.json`). - */ -abstract class RNFingerprintjsProSpec(reactContext: ReactApplicationContext) : - NativeRNFingerprintjsProSpec(reactContext) diff --git a/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt b/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt deleted file mode 100644 index e59ad813..00000000 --- a/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.fingerprintjs.reactnative - -import com.facebook.react.ReactPackage -import com.facebook.react.bridge.NativeModule -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.uimanager.ViewManager - -/** - * Old-architecture package. Registers the module through the legacy [ReactPackage] API, which is - * available across every supported React Native version (including the oldest in the CI matrix). - * The New Architecture counterpart in `src/newarch` uses `BaseReactPackage` + - * `ReactModuleInfoProvider`, which only exist on newer React Native, so keeping the two apart - * avoids referencing APIs that are missing on old React Native. - */ -class RNFingerprintjsProPackage : ReactPackage { - override fun createNativeModules(reactContext: ReactApplicationContext): List { - return listOf(RNFingerprintjsProModule(reactContext)) - } - - override fun createViewManagers(reactContext: ReactApplicationContext): List> { - return emptyList() - } -} diff --git a/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt b/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt deleted file mode 100644 index 4e9f0e1a..00000000 --- a/sdk/android/src/oldarch/java/com/fingerprintjs/reactnative/RNFingerprintjsProSpec.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.fingerprintjs.reactnative - -import com.facebook.react.bridge.Promise -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReactContextBaseJavaModule -import com.facebook.react.bridge.ReadableArray -import com.facebook.react.bridge.ReadableMap - -/** - * Old-architecture base class. Mirrors the method surface of the Codegen-generated - * `NativeRNFingerprintjsProSpec` so `RNFingerprintjsProModule` can extend a single `...Spec` type - * regardless of the architecture. The concrete implementation annotates the overrides with - * `@ReactMethod` for the legacy bridge to pick them up. - */ -abstract class RNFingerprintjsProSpec(reactContext: ReactApplicationContext) : - ReactContextBaseJavaModule(reactContext) { - - abstract fun configure( - apiToken: String, - pluginVersion: String, - extendedResponseFormat: Boolean, - fallbackEndpointUrls: ReadableArray, - allowUseOfLocationData: Boolean, - locationTimeoutMillis: Double, - regionKey: String?, - endpointUrl: String? - ) - - abstract fun getVisitorId(tags: ReadableMap?, linkedId: String?, timeout: Double, promise: Promise) - - abstract fun getVisitorData(tags: ReadableMap?, linkedId: String?, timeout: Double, promise: Promise) -} diff --git a/sdk/ios/RNFingerprintjsPro.mm b/sdk/ios/RNFingerprintjsPro.mm index 21977feb..030ac544 100644 --- a/sdk/ios/RNFingerprintjsPro.mm +++ b/sdk/ios/RNFingerprintjsPro.mm @@ -14,24 +14,23 @@ @interface RCT_EXTERN_MODULE(RNFingerprintjsPro, NSObject) RCT_EXTERN_METHOD(getVisitorId:(NSDictionary *)tags linkedId:(NSString *)linkedId - timeout:(double)timeout + timeout:(NSNumber * _Nullable)timeout resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject ) RCT_EXTERN_METHOD(getVisitorData:(NSDictionary *)tags linkedId:(NSString *)linkedId - timeout:(double)timeout + timeout:(NSNumber * _Nullable)timeout resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject ) @end -#ifdef RCT_NEW_ARCH_ENABLED -// On the New Architecture, expose the Swift class as a real TurboModule backed by the -// Codegen-generated spec. The Swift implementation already exposes the exact selectors the -// generated `NativeRNFingerprintjsProSpecJSI` invokes. +// Expose the Swift class as a real TurboModule backed by the Codegen-generated spec. The Swift +// implementation already exposes the exact selectors the generated +// `NativeRNFingerprintjsProSpecJSI` invokes. #import // Import the Swift-generated interface so the category below can extend the Swift class. @@ -53,4 +52,3 @@ @implementation RNFingerprintjsPro (TurboModule) return std::make_shared(params); } @end -#endif diff --git a/sdk/ios/RNFingerprintjsPro.swift b/sdk/ios/RNFingerprintjsPro.swift index ab002b10..d927ebeb 100644 --- a/sdk/ios/RNFingerprintjsPro.swift +++ b/sdk/ios/RNFingerprintjsPro.swift @@ -20,7 +20,7 @@ class RNFingerprintjsPro: NSObject { } @objc(getVisitorId:linkedId:timeout:resolve:reject:) - public func getVisitorId(tags: [String: Any]?, linkedId: String?, timeout: Double, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { + public func getVisitorId(tags: [String: Any]?, linkedId: String?, timeout: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { let metadata = RNFingerprintjsPro.prepareMetadata(linkedId, tags: tags) let completionHandler: FingerprintPro.VisitorIdBlock = { visitorIdResult in @@ -33,8 +33,7 @@ class RNFingerprintjsPro: NSObject { } } - // A negative sentinel means "no timeout" (see the JS spec). - if timeout >= 0 { + if let timeout = timeout?.doubleValue { fpjsClient?.getVisitorId(metadata, timeout: timeout / 1000, completion: completionHandler) } else { fpjsClient?.getVisitorId(metadata, completion: completionHandler) @@ -42,7 +41,7 @@ class RNFingerprintjsPro: NSObject { } @objc(getVisitorData:linkedId:timeout:resolve:reject:) - public func getVisitorData(tags: [String: Any]?, linkedId: String?, timeout: Double, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { + public func getVisitorData(tags: [String: Any]?, linkedId: String?, timeout: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { let metadata = RNFingerprintjsPro.prepareMetadata(linkedId, tags: tags) let completionHandler: FingerprintPro.VisitorIdResponseBlock = { visitorIdResponseResult in @@ -61,8 +60,7 @@ class RNFingerprintjsPro: NSObject { } } - // A negative sentinel means "no timeout" (see the JS spec). - if timeout >= 0 { + if let timeout = timeout?.doubleValue { fpjsClient?.getVisitorIdResponse(metadata, timeout: timeout / 1000, completion: completionHandler) } else { fpjsClient?.getVisitorIdResponse(metadata, completion: completionHandler) diff --git a/sdk/package.json b/sdk/package.json index 3074f042..23d85fd6 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -84,10 +84,10 @@ }, "peerDependencies": { "@fingerprintjs/fingerprintjs-pro-spa": "^1.3.2", - "expo": ">=51.0.0", + "expo": ">=53.0.0", "react": "*", "react-dom": "*", - "react-native": "*" + "react-native": ">=0.79.0" }, "peerDependenciesMeta": { "@fingerprintjs/fingerprintjs-pro-spa": { diff --git a/sdk/src/FingerprintJsProAgent.ts b/sdk/src/FingerprintJsProAgent.ts index 17df6457..bf4731a8 100644 --- a/sdk/src/FingerprintJsProAgent.ts +++ b/sdk/src/FingerprintJsProAgent.ts @@ -6,13 +6,6 @@ import { isTruthy } from './utils' const packageVersion = '__VERSION__' -/** - * Sentinel passed to the native `timeout` argument when no timeout is set. A negative value means - * "no timeout". We can't pass `null` because the legacy Android bridge (old architecture) cannot - * accept a null number argument. - */ -const NO_TIMEOUT = -1 - /** * * @group API Client approach @@ -62,7 +55,7 @@ export class FingerprintJsProAgent implements ProAgent { public async getVisitorId(tags?: Tags, linkedId?: string, options?: RequestOptions): Promise { try { const timeout = options?.timeout ?? this.requestOptions.timeout - return await RNFingerprintjsPro.getVisitorId(tags ?? null, linkedId ?? null, timeout ?? NO_TIMEOUT) + return await RNFingerprintjsPro.getVisitorId(tags ?? null, linkedId ?? null, timeout ?? null) } catch (error) { if (error instanceof Error) { throw unwrapError(error) @@ -87,7 +80,7 @@ export class FingerprintJsProAgent implements ProAgent { const visitorData: NativeVisitorData = await RNFingerprintjsPro.getVisitorData( tags ?? null, linkedId ?? null, - timeout ?? NO_TIMEOUT + timeout ?? null ) const { requestId, confidenceScore, visitorDataJson, sealedResult } = visitorData // eslint-disable-next-line @typescript-eslint/consistent-type-assertions diff --git a/sdk/src/specs/NativeRNFingerprintjsPro.ts b/sdk/src/specs/NativeRNFingerprintjsPro.ts index d31155c7..d1baea39 100644 --- a/sdk/src/specs/NativeRNFingerprintjsPro.ts +++ b/sdk/src/specs/NativeRNFingerprintjsPro.ts @@ -19,13 +19,9 @@ export interface NativeVisitorData { /** * Codegen TurboModule spec for the `RNFingerprintjsPro` native module. * - * Optional string/object arguments are modelled as nullable (`| null`); Codegen relies on positional - * arguments and native treats `null` as "not provided". Genuinely optional parameters (those without - * an SDK-side default: `region`, `endpointUrl`, `timeout`) come last in each signature. - * - * `timeout` is a NON-nullable `Double` using a negative sentinel (< 0 means "no timeout") rather than - * `Double | null`, because the legacy (old-architecture) Android bridge cannot pass a null number - * argument — it unboxes every numeric arg via `ReadableNativeArray.getDouble()` and NPEs on null. + * Optional string/object/number arguments are modelled as nullable (`| null`); Codegen relies on + * positional arguments and native treats `null` as "not provided". Genuinely optional parameters + * (those without an SDK-side default: `region`, `endpointUrl`, `timeout`) come last in each signature. */ export interface Spec extends TurboModule { configure( @@ -39,9 +35,9 @@ export interface Spec extends TurboModule { endpointUrl: string | null ): void - getVisitorId(tags: UnsafeObject | null, linkedId: string | null, timeout: Double): Promise + getVisitorId(tags: UnsafeObject | null, linkedId: string | null, timeout: Double | null): Promise - getVisitorData(tags: UnsafeObject | null, linkedId: string | null, timeout: Double): Promise + getVisitorData(tags: UnsafeObject | null, linkedId: string | null, timeout: Double | null): Promise } export default TurboModuleRegistry.getEnforcing('RNFingerprintjsPro') From d1406f2ae6e752e8713ccc42124a8eb7fb858c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Wed, 5 Aug 2026 14:03:14 +0200 Subject: [PATCH 31/39] chore: enter pre-release mode --- .changeset/pre.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 00000000..e4bf2ce4 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,8 @@ +{ + "mode": "pre", + "tag": "rc", + "initialVersions": { + "@fingerprintjs/fingerprintjs-pro-react-native": "3.17.0" + }, + "changesets": [] +} From 919569026887fb820f32a8742e40f44549baf1a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Tue, 15 Sep 2026 15:28:04 +0200 Subject: [PATCH 32/39] refactor(ci): remove retry-cmd script and update workflows --- .github/workflows/e2e-tests.yml | 8 ++--- e2e-app/scripts/retry-cmd.mjs | 57 --------------------------------- 2 files changed, 4 insertions(+), 61 deletions(-) delete mode 100644 e2e-app/scripts/retry-cmd.mjs diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 622e2890..fe87811b 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -85,7 +85,7 @@ jobs: run: pnpm prebuild working-directory: e2e-app - - run: node scripts/retry-cmd.mjs --retries=3 "pnpm exec detox build --configuration android.emu.release" + - run: pnpm exec detox build --configuration android.emu.release working-directory: e2e-app - name: Upload artifacts @@ -193,7 +193,7 @@ jobs: arch: x86_64 working-directory: e2e-app avd-name: ${{ steps.device.outputs.AVD_NAME }} - script: node scripts/retry-cmd.mjs --retries=3 "pnpm exec detox test --configuration android.emu.release --headless --record-logs all --record-videos failing" + script: pnpm exec detox test --configuration android.emu.release --headless --record-logs all --record-videos failing - name: Upload artifacts if: always() @@ -285,7 +285,7 @@ jobs: run: pnpm prebuild working-directory: e2e-app - - run: node scripts/retry-cmd.mjs --retries=3 "pnpm exec detox build --configuration ios.sim.release" + - run: pnpm exec detox build --configuration ios.sim.release working-directory: e2e-app - name: Upload artifacts @@ -359,7 +359,7 @@ jobs: if: github.event_name != 'release' working-directory: e2e-app - - run: node scripts/retry-cmd.mjs --retries=3 "pnpm exec detox test --configuration ios.sim.release --cleanup --headless --record-logs all --record-videos failing" + - run: pnpm exec detox test --configuration ios.sim.release --cleanup --headless --record-logs all --record-videos failing working-directory: e2e-app env: MINIMUM_US_DEFAULT_PUBLIC_KEY: ${{ secrets.MINIMUM_US_DEFAULT_PUBLIC_KEY }} diff --git a/e2e-app/scripts/retry-cmd.mjs b/e2e-app/scripts/retry-cmd.mjs deleted file mode 100644 index 82569304..00000000 --- a/e2e-app/scripts/retry-cmd.mjs +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env node -import { spawnSync } from 'node:child_process' - -// Usage: node retry-cmd.mjs --retries=3 "" -// Runs and retries up to additional times if it exits non-zero. - -function parseArgs(argv) { - let retries = 0 - const rest = [] - - for (const arg of argv) { - const match = /^--retries=(\d+)$/.exec(arg) - if (match) { - retries = Number(match[1]) - } else { - rest.push(arg) - } - } - - return { retries, command: rest.join(' ') } -} - -const { retries, command } = parseArgs(process.argv.slice(2)) - -if (!command) { - console.error('Usage: node retry.js --retries= ""') - process.exit(1) -} - -const totalAttempts = retries + 1 - -for (let attempt = 1; attempt <= totalAttempts; attempt++) { - if (attempt > 1) { - console.log(`[retry] Attempt ${attempt}/${totalAttempts}: ${command}`) - } - - const result = spawnSync(command, { stdio: 'inherit', shell: true }) - - if (result.error) { - console.error(`[retry] Failed to start command: ${result.error.message}`) - } - - const code = result.status - - if (code === 0) { - process.exit(0) - } - - console.error(`[retry] Attempt ${attempt} exited with code ${code ?? 'null'}.`) - - if (attempt < totalAttempts) { - console.error('[retry] Retrying...') - } -} - -console.error(`[retry] Command failed after ${totalAttempts} attempt(s).`) -process.exit(1) From 3c5913b4ae5b1e0cdb29e12c28fd1cfcf95c5d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Tue, 15 Sep 2026 15:29:33 +0200 Subject: [PATCH 33/39] docs: restore comment --- .github/workflows/e2e-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index fe87811b..56332208 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -68,6 +68,9 @@ jobs: run: pnpm exec expo install --fix working-directory: e2e-app + # `expo install --fix` aligns every bundled package to the Expo SDK, including coercing + # react-native to the SDK's bundled version. Re-pin it to the version under test so the + # matrix actually exercises that RN version rather than the SDK's default. - name: Pin react-native to the tested version if: matrix.react-native != 'local' run: pnpm add react-native@${{ matrix.react-native }} From 6e64a6bef63be5efda379035cc7af5087e413e1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Tue, 15 Sep 2026 15:30:20 +0200 Subject: [PATCH 34/39] refactor(android): remove unused method withAndroidBuildProperties --- e2e-app/app-config-tools/android.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/e2e-app/app-config-tools/android.ts b/e2e-app/app-config-tools/android.ts index 12da4e55..422551f7 100644 --- a/e2e-app/app-config-tools/android.ts +++ b/e2e-app/app-config-tools/android.ts @@ -14,12 +14,6 @@ const androidBuildProperties: AndroidBuildProperties = { enableBundleCompression: false, } -export function withAndroidBuildProperties(props: Partial) { - return () => { - Object.assign(androidBuildProperties, props) - } -} - export function getAndroidBuildProperties() { return { ...androidBuildProperties } } From 59208e4d5ad399db7f6426256ddf060ebc495806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Tue, 15 Sep 2026 15:34:23 +0200 Subject: [PATCH 35/39] test(e2e): add architecture verification test --- e2e-app/e2e/arch.test.ts | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 e2e-app/e2e/arch.test.ts diff --git a/e2e-app/e2e/arch.test.ts b/e2e-app/e2e/arch.test.ts new file mode 100644 index 00000000..d08bb268 --- /dev/null +++ b/e2e-app/e2e/arch.test.ts @@ -0,0 +1,39 @@ +import { by, device, element, waitFor } from 'detox' +import { beforeAll, describe, expect, it, jest } from '@jest/globals' +import { DeviceLaunchAppConfig } from 'detox/detox' +import { wait } from './wait' +import { Config } from '@/src/config.types' +import { testIds } from '@/e2e/ids' +import { getElementText } from './getElementText' + +async function launchApp(params?: DeviceLaunchAppConfig) { + await device.launchApp(params) + + await wait(4000) +} + +jest.retryTimes(3, { + waitBeforeRetry: 1000, + logErrorsBeforeRetry: false, +}) + +describe('React Native architecture', () => { + beforeAll(async () => { + await launchApp({ + newInstance: true, + launchArgs: { + apiKey: '', + region: 'us', + } as Config, + }) + }) + + it('runs on fabric', async () => { + await waitFor(element(by.id(testIds.uiManager))) + .toExist() + .withTimeout(10_000) + + const uiManager = await getElementText(element(by.id(testIds.uiManager))) + expect(uiManager.toLowerCase()).toEqual('fabric') + }) +}) From 714561389a0b7457044a2e829fc4d985c63ecea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Tue, 15 Sep 2026 15:35:52 +0200 Subject: [PATCH 36/39] fix(e2e): update global type definitions --- e2e-app/src/global.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e-app/src/global.d.ts b/e2e-app/src/global.d.ts index c7df60a4..8bf6f312 100644 --- a/e2e-app/src/global.d.ts +++ b/e2e-app/src/global.d.ts @@ -1,7 +1,7 @@ declare global { var __turboModuleProxy: ?unknown - var RN$Bridgeless: ?boolean - var nativeFabricUIManager: ?boolean + var RN$Bridgeless: boolean | undefined + var nativeFabricUIManager: boolean | undefined } export {} From 505e4008368b410f39871045dba715a8ad193d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Tue, 15 Sep 2026 16:18:28 +0200 Subject: [PATCH 37/39] fix(e2e): update testIds import path --- e2e-app/e2e/arch.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e-app/e2e/arch.test.ts b/e2e-app/e2e/arch.test.ts index d08bb268..bf0606bd 100644 --- a/e2e-app/e2e/arch.test.ts +++ b/e2e-app/e2e/arch.test.ts @@ -3,7 +3,7 @@ import { beforeAll, describe, expect, it, jest } from '@jest/globals' import { DeviceLaunchAppConfig } from 'detox/detox' import { wait } from './wait' import { Config } from '@/src/config.types' -import { testIds } from '@/e2e/ids' +import { testIds } from './ids' import { getElementText } from './getElementText' async function launchApp(params?: DeviceLaunchAppConfig) { From fd2b23b74f7f0c42e4dfe49b1d5104cc792a2ab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Wed, 16 Sep 2026 12:05:11 +0200 Subject: [PATCH 38/39] chore: revert retry-cmd script --- .github/workflows/e2e-tests.yml | 8 ++--- e2e-app/scripts/retry-cmd.mjs | 57 +++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 e2e-app/scripts/retry-cmd.mjs diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 56332208..80e258a0 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -88,7 +88,7 @@ jobs: run: pnpm prebuild working-directory: e2e-app - - run: pnpm exec detox build --configuration android.emu.release + - run: node scripts/retry-cmd.mjs --retries=3 "pnpm exec detox build --configuration android.emu.release" working-directory: e2e-app - name: Upload artifacts @@ -196,7 +196,7 @@ jobs: arch: x86_64 working-directory: e2e-app avd-name: ${{ steps.device.outputs.AVD_NAME }} - script: pnpm exec detox test --configuration android.emu.release --headless --record-logs all --record-videos failing + script: node scripts/retry-cmd.mjs --retries=3 "pnpm exec detox test --configuration android.emu.release --headless --record-logs all --record-videos failing" - name: Upload artifacts if: always() @@ -288,7 +288,7 @@ jobs: run: pnpm prebuild working-directory: e2e-app - - run: pnpm exec detox build --configuration ios.sim.release + - run: node scripts/retry-cmd.mjs --retries=3 "pnpm exec detox build --configuration ios.sim.release" working-directory: e2e-app - name: Upload artifacts @@ -362,7 +362,7 @@ jobs: if: github.event_name != 'release' working-directory: e2e-app - - run: pnpm exec detox test --configuration ios.sim.release --cleanup --headless --record-logs all --record-videos failing + - run: node scripts/retry-cmd.mjs --retries=3 "pnpm exec detox test --configuration ios.sim.release --cleanup --headless --record-logs all --record-videos failing" working-directory: e2e-app env: MINIMUM_US_DEFAULT_PUBLIC_KEY: ${{ secrets.MINIMUM_US_DEFAULT_PUBLIC_KEY }} diff --git a/e2e-app/scripts/retry-cmd.mjs b/e2e-app/scripts/retry-cmd.mjs new file mode 100644 index 00000000..b844a781 --- /dev/null +++ b/e2e-app/scripts/retry-cmd.mjs @@ -0,0 +1,57 @@ +#!/usr/bin/env node +import { spawnSync } from 'node:child_process' + +// Usage: node retry-cmd.mjs --retries=3 "" +// Runs and retries up to additional times if it exits non-zero. + +function parseArgs(argv) { + let retries = 0 + const rest = [] + + for (const arg of argv) { + const match = /^--retries=(\d+)$/.exec(arg) + if (match) { + retries = Number(match[1]) + } else { + rest.push(arg) + } + } + + return { retries, command: rest.join(' ') } +} + +const { retries, command } = parseArgs(process.argv.slice(2)) + +if (!command) { + console.error('Usage: node retry-cmd.mjs --retries= ""') + process.exit(1) +} + +const totalAttempts = retries + 1 + +for (let attempt = 1; attempt <= totalAttempts; attempt++) { + if (attempt > 1) { + console.log(`[retry] Attempt ${attempt}/${totalAttempts}: ${command}`) + } + + const result = spawnSync(command, { stdio: 'inherit', shell: true }) + + if (result.error) { + console.error(`[retry] Failed to start command: ${result.error.message}`) + } + + const code = result.status + + if (code === 0) { + process.exit(0) + } + + console.error(`[retry] Attempt ${attempt} exited with code ${code ?? 'null'}.`) + + if (attempt < totalAttempts) { + console.error('[retry] Retrying...') + } +} + +console.error(`[retry] Command failed after ${totalAttempts} attempt(s).`) +process.exit(1) From ba3217dfe3cc17e61ffe024802508982c0482fd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Thu, 17 Sep 2026 12:13:39 +0200 Subject: [PATCH 39/39] fix(e2e): use env variable for apiKey in arch test --- e2e-app/e2e/arch.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e-app/e2e/arch.test.ts b/e2e-app/e2e/arch.test.ts index bf0606bd..588de481 100644 --- a/e2e-app/e2e/arch.test.ts +++ b/e2e-app/e2e/arch.test.ts @@ -22,7 +22,7 @@ describe('React Native architecture', () => { await launchApp({ newInstance: true, launchArgs: { - apiKey: '', + apiKey: process.env.MINIMUM_US_DEFAULT_PUBLIC_KEY, region: 'us', } as Config, })