diff --git a/.changeset/new-architecture-support.md b/.changeset/new-architecture-support.md new file mode 100644 index 00000000..082fbd0b --- /dev/null +++ b/.changeset/new-architecture-support.md @@ -0,0 +1,7 @@ +--- +'@fingerprintjs/fingerprintjs-pro-react-native': major +--- + +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/.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": [] +} diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index d191cd54..80e258a0 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: @@ -81,11 +81,14 @@ jobs: run: rm -rf node_modules && pnpm install --frozen-lockfile working-directory: e2e-app + - run: pnpm why react-native + working-directory: e2e-app + - name: Prebuild 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 @@ -191,8 +194,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() @@ -277,11 +281,14 @@ jobs: run: rm -rf node_modules && pnpm install --frozen-lockfile working-directory: e2e-app + - run: pnpm why react-native + working-directory: e2e-app + - name: Prebuild 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 @@ -355,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/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.tsx b/e2e-app/App.tsx index 14f780cc..ce773f65 100644 --- a/e2e-app/App.tsx +++ b/e2e-app/App.tsx @@ -1,15 +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 } 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() + const doGetData = async () => { + const tags = config.useTags ? testTags : undefined + + await getData(tags, config.linkedId) + } + useEffect(() => { if (error) { console.error(error) @@ -18,7 +26,6 @@ function InnerApp() { return ( - {/* eslint-disable-next-line @typescript-eslint/no-deprecated */} + {isLoading ? Loading... : null} {error ? ( @@ -37,20 +45,23 @@ function InnerApp() { ) : 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 + + ) diff --git a/e2e-app/app-config-tools/android.ts b/e2e-app/app-config-tools/android.ts index 1360b7fc..422551f7 100644 --- a/e2e-app/app-config-tools/android.ts +++ b/e2e-app/app-config-tools/android.ts @@ -14,24 +14,6 @@ const androidBuildProperties: AndroidBuildProperties = { enableBundleCompression: false, } -export function withAndroidBuildProperties(props: Partial) { - return () => { - Object.assign(androidBuildProperties, props) - } -} - -export function setCompileSdkVersion(version: number) { - return withAndroidBuildProperties({ - compileSdkVersion: version, - }) -} - -export function stripAndroidExtraBuildProperties() { - delete androidBuildProperties.compileSdkVersion - delete androidBuildProperties.targetSdkVersion - delete androidBuildProperties.buildToolsVersion -} - export function getAndroidBuildProperties() { return { ...androidBuildProperties } } diff --git a/e2e-app/app-config-tools/arch.ts b/e2e-app/app-config-tools/arch.ts deleted file mode 100644 index d8cebe82..00000000 --- a/e2e-app/app-config-tools/arch.ts +++ /dev/null @@ -1,9 +0,0 @@ -let newArch = false - -export function disableNewArch() { - newArch = false -} - -export function getNewArch() { - return newArch -} diff --git a/e2e-app/app-config-tools/expoConfigPatchers.ts b/e2e-app/app-config-tools/expoConfigPatchers.ts deleted file mode 100644 index ce52dfad..00000000 --- a/e2e-app/app-config-tools/expoConfigPatchers.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { getNewArch } from './arch' - -export function withNewArchFlag(config: { newArchEnabled?: boolean }) { - config.newArchEnabled = getNewArch() -} - -// Older expo versions require splash to be defined -export function withSplashscreen(config: { splash?: { backgroundColor?: string } }) { - config.splash = { - backgroundColor: '#ffffff', - } -} - -// Expo SDK 50's autolinking (1.10.x) ignores the `android.extraMavenRepos` gradle property the -// Fingerprint SDK plugin uses, so the Fingerprint Maven repo must be injected into build.gradle -// directly. Only needed for Expo 50 (RN 0.73). -export function withFingerprintMavenRepo(config: { plugins?: unknown[] }) { - config.plugins ??= [] - config.plugins.push('./plugins/withAndroidMavenRepo.js') -} diff --git a/e2e-app/app-config-tools/reactNativeMetadata.ts b/e2e-app/app-config-tools/reactNativeMetadata.ts index 1fab0807..3e030cf7 100644 --- a/e2e-app/app-config-tools/reactNativeMetadata.ts +++ b/e2e-app/app-config-tools/reactNativeMetadata.ts @@ -1,7 +1,3 @@ -import { disableNewArch } from './arch' -import { setCompileSdkVersion, stripAndroidExtraBuildProperties } from './android' -import { withFingerprintMavenRepo, withNewArchFlag, 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 type ExpoConfigPatcher = (config: any) => 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: [withNewArchFlag, withSplashscreen, withFingerprintMavenRepo], - }, - 0.74: { - callbacks: [disableNewArch, stripAndroidExtraBuildProperties, setCompileSdkVersion(34)], - patchExpoConfig: [withNewArchFlag, withSplashscreen], - }, - 0.75: { - callbacks: [disableNewArch, stripAndroidExtraBuildProperties, setCompileSdkVersion(34)], - patchExpoConfig: [withNewArchFlag, withSplashscreen], - }, - 0.76: { - callbacks: [stripAndroidExtraBuildProperties, setCompileSdkVersion(35)], - patchExpoConfig: [withNewArchFlag, withSplashscreen], - }, - 0.77: { - callbacks: [stripAndroidExtraBuildProperties, setCompileSdkVersion(35)], - patchExpoConfig: [withNewArchFlag, 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 08b780f8..56367f82 100644 --- a/e2e-app/app.config.ts +++ b/e2e-app/app.config.ts @@ -48,4 +48,8 @@ const config: ExpoConfig = { ], } +// The SDK supports the New Architecture only (React Native >= 0.79), so always enable it. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +;(config as any).newArchEnabled = true + export default patchExpoConfig(rnVersionStr, config) diff --git a/e2e-app/e2e/arch.test.ts b/e2e-app/e2e/arch.test.ts new file mode 100644 index 00000000..588de481 --- /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 './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: process.env.MINIMUM_US_DEFAULT_PUBLIC_KEY, + 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') + }) +}) 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, }) diff --git a/e2e-app/e2e/ids.ts b/e2e-app/e2e/ids.ts index 9e4473fd..b5fbb137 100644 --- a/e2e-app/e2e/ids.ts +++ b/e2e-app/e2e/ids.ts @@ -6,4 +6,5 @@ export const testIds = { errorCause: 'error-cause', data: 'data', getData: 'get-data', + uiManager: 'ui-manager', } 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/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 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/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) diff --git a/e2e-app/src/components/ArchInfo.tsx b/e2e-app/src/components/ArchInfo.tsx new file mode 100644 index 00000000..21e3dffb --- /dev/null +++ b/e2e-app/src/components/ArchInfo.tsx @@ -0,0 +1,17 @@ +import { Text, View } from 'react-native' +import { testIds } from '@/e2e/ids' + +export function ArchInfo() { + const hasTurboModules = Boolean(global.__turboModuleProxy) + const isBridgeless = Boolean(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 new file mode 100644 index 00000000..8bf6f312 --- /dev/null +++ b/e2e-app/src/global.d.ts @@ -0,0 +1,7 @@ +declare global { + var __turboModuleProxy: ?unknown + var RN$Bridgeless: boolean | undefined + var nativeFabricUIManager: boolean | undefined +} + +export {} diff --git a/sdk/README.md b/sdk/README.md index 1ae08ee4..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.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/sdk/RNFingerprintjsPro.podspec b/sdk/RNFingerprintjsPro.podspec index 0a612662..7e36e6fe 100644 --- a/sdk/RNFingerprintjsPro.podspec +++ b/sdk/RNFingerprintjsPro.podspec @@ -11,10 +11,20 @@ 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" # 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/__tests__/FingerprintJsProProvider.test.ts b/sdk/__tests__/FingerprintJsProProvider.test.ts index c725ea26..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, getVisitorData, getVisitorIdWithTimeout, getVisitorDataWithTimeout } = +const { configure, getVisitorId, getVisitorData } = // eslint-disable-next-line @typescript-eslint/consistent-type-assertions NativeModules.RNFingerprintjsPro as unknown as Record @@ -26,15 +26,15 @@ describe(`FingerprintJsProProvider`, () => { wrapper, }) - expect(NativeModules.RNFingerprintjsPro.configure).toHaveBeenCalledWith( + expect(configure).toHaveBeenCalledWith( options.apiKey, - options.region, - options.endpointUrl, - [], - false, pluginVersion, false, - 5000 + [], + false, + 5000, + options.region, + options.endpointUrl ) }) @@ -49,15 +49,15 @@ describe(`FingerprintJsProProvider`, () => { wrapper, }) - expect(NativeModules.RNFingerprintjsPro.configure).toHaveBeenCalledWith( + expect(configure).toHaveBeenCalledWith( options.apiKey, - options.region, - options.endpointUrl, - options.fallbackEndpointUrls, - false, pluginVersion, false, - 5000 + options.fallbackEndpointUrls, + false, + 5000, + options.region, + options.endpointUrl ) }) @@ -72,15 +72,15 @@ describe(`FingerprintJsProProvider`, () => { wrapper, }) - expect(NativeModules.RNFingerprintjsPro.configure).toHaveBeenCalledWith( + expect(configure).toHaveBeenCalledWith( options.apiKey, - options.region, - options.endpointUrl, - [], - options.extendedResponseFormat, pluginVersion, + options.extendedResponseFormat, + [], false, - 5000 + 5000, + options.region, + options.endpointUrl ) }) @@ -96,15 +96,15 @@ describe(`FingerprintJsProProvider`, () => { wrapper, }) - expect(NativeModules.RNFingerprintjsPro.configure).toHaveBeenCalledWith( + expect(configure).toHaveBeenCalledWith( options.apiKey, - options.region, - options.endpointUrl, - [], - false, pluginVersion, + false, + [], true, - options.locationTimeoutMillisAndroid + options.locationTimeoutMillisAndroid, + options.region, + options.endpointUrl ) }) @@ -113,33 +113,25 @@ describe(`FingerprintJsProProvider`, () => { const fingerprintClient = new FingerprintJsProAgent(options) void fingerprintClient.getVisitorId() - expect(NativeModules.RNFingerprintjsPro.getVisitorId).toHaveBeenCalledWith(undefined, undefined) + 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(NativeModules.RNFingerprintjsPro.getVisitorIdWithTimeout).toHaveBeenCalledWith( - undefined, - undefined, - 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(NativeModules.RNFingerprintjsPro.getVisitorIdWithTimeout).toHaveBeenCalledWith( - undefined, - undefined, - 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', () => { @@ -149,20 +141,11 @@ describe(`FingerprintJsProProvider`, () => { options.requestOptions = { timeout: clientTimeout } const fingerprintClient = new FingerprintJsProAgent(options) - const mockedJsonAnswer = { - visitorId: mockedVisitorId, - } - getVisitorIdWithTimeout.mockReturnValueOnce( - Promise.resolve([mockedRequestId, mockedConfidenceScore, JSON.stringify(mockedJsonAnswer)]) - ) + getVisitorId.mockReturnValueOnce(Promise.resolve(mockedVisitorId)) void fingerprintClient.getVisitorId(undefined, undefined, { timeout: getRequestTimeout }) - expect(NativeModules.RNFingerprintjsPro.getVisitorIdWithTimeout).toHaveBeenCalledWith( - undefined, - undefined, - getRequestTimeout - ) + expect(getVisitorId).toHaveBeenCalledWith(null, null, getRequestTimeout) }) it('should call `getVisitorData` function when there is no timeout', () => { @@ -173,15 +156,19 @@ 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() - expect(NativeModules.RNFingerprintjsPro.getVisitorData).toHaveBeenCalledWith(undefined, undefined) + 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) @@ -189,20 +176,20 @@ describe(`FingerprintJsProProvider`, () => { const mockedJsonAnswer = { visitorId: mockedVisitorId, } - getVisitorDataWithTimeout.mockReturnValueOnce( - Promise.resolve([mockedRequestId, mockedConfidenceScore, JSON.stringify(mockedJsonAnswer)]) + getVisitorData.mockReturnValueOnce( + Promise.resolve({ + requestId: mockedRequestId, + confidenceScore: mockedConfidenceScore, + visitorDataJson: JSON.stringify(mockedJsonAnswer), + }) ) void fingerprintClient.getVisitorData() - expect(NativeModules.RNFingerprintjsPro.getVisitorDataWithTimeout).toHaveBeenCalledWith( - undefined, - undefined, - 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) @@ -210,17 +197,17 @@ describe(`FingerprintJsProProvider`, () => { const mockedJsonAnswer = { visitorId: mockedVisitorId, } - getVisitorDataWithTimeout.mockReturnValueOnce( - Promise.resolve([mockedRequestId, mockedConfidenceScore, JSON.stringify(mockedJsonAnswer)]) + getVisitorData.mockReturnValueOnce( + Promise.resolve({ + requestId: mockedRequestId, + confidenceScore: mockedConfidenceScore, + visitorDataJson: JSON.stringify(mockedJsonAnswer), + }) ) void fingerprintClient.getVisitorData() - expect(NativeModules.RNFingerprintjsPro.getVisitorDataWithTimeout).toHaveBeenCalledWith( - undefined, - undefined, - 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', () => { @@ -233,17 +220,17 @@ describe(`FingerprintJsProProvider`, () => { const mockedJsonAnswer = { visitorId: mockedVisitorId, } - getVisitorDataWithTimeout.mockReturnValueOnce( - Promise.resolve([mockedRequestId, mockedConfidenceScore, JSON.stringify(mockedJsonAnswer)]) + getVisitorData.mockReturnValueOnce( + Promise.resolve({ + requestId: mockedRequestId, + confidenceScore: mockedConfidenceScore, + visitorDataJson: JSON.stringify(mockedJsonAnswer), + }) ) void fingerprintClient.getVisitorData(undefined, undefined, { timeout: getRequestTimeout }) - expect(NativeModules.RNFingerprintjsPro.getVisitorDataWithTimeout).toHaveBeenCalledWith( - undefined, - undefined, - getRequestTimeout - ) + expect(getVisitorData).toHaveBeenCalledWith(null, null, getRequestTimeout) }) describe('agent params changes propagation', () => { @@ -257,7 +244,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', pluginVersion, false, [], false, 5000, null, null) }) it('reconfigures the agent when a param changes by value', () => { @@ -267,12 +254,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', pluginVersion, false, [], false, 5000, null, null) }) 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', pluginVersion, false, [], false, 5000, null, null) rerenderWithParams({ apiKey: 'key', @@ -286,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' ) }) @@ -300,13 +287,13 @@ describe(`FingerprintJsProProvider`, () => { const { rerenderWithParams } = renderProvider({ apiKey: 'key', fallbackEndpointUrls: ['https://a.example'] }) expect(configure).toHaveBeenLastCalledWith( 'key', - undefined, - undefined, - ['https://a.example'], - false, pluginVersion, false, - 5000 + ['https://a.example'], + false, + 5000, + null, + null ) rerenderWithParams({ apiKey: 'key', fallbackEndpointUrls: ['https://a.example', 'https://b.example'] }) @@ -314,13 +301,13 @@ describe(`FingerprintJsProProvider`, () => { expect(configure).toHaveBeenCalledTimes(2) expect(configure).toHaveBeenLastCalledWith( 'key', - undefined, - undefined, - ['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 1337e119..ceecf20e 100644 --- a/sdk/__tests__/setup.ts +++ b/sdk/__tests__/setup.ts @@ -1,19 +1,18 @@ 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(), + } 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/__tests__/useVisitorData.test.ts b/sdk/__tests__/useVisitorData.test.ts index 7955f235..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' @@ -43,7 +40,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 +120,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() @@ -128,7 +133,7 @@ describe('useVisitorData', () => { void result.current.getData(mockedTags, mockedLinkedId) }) await waitFor(() => { - expect(getVisitorData).toHaveBeenCalledWith(mockedTags, mockedLinkedId) + expect(getVisitorData).toHaveBeenCalledWith(mockedTags, mockedLinkedId, null) }) }) @@ -152,7 +157,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() @@ -161,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, } @@ -184,8 +193,12 @@ describe('useVisitorData', () => { const options: RequestOptions = { timeout: 15_000 } - getVisitorDataWithTimeout.mockReturnValueOnce( - Promise.resolve([mockedRequestId, mockedConfidenceScore, JSON.stringify(mockedJsonAnswer)]) + getVisitorData.mockReturnValueOnce( + Promise.resolve({ + requestId: mockedRequestId, + confidenceScore: mockedConfidenceScore, + visitorDataJson: JSON.stringify(mockedJsonAnswer), + }) ) const wrapper = createWrapper() @@ -194,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/build.gradle b/sdk/android/build.gradle index 4e75641d..b6b3e9f9 100644 --- a/sdk/android/build.gradle +++ b/sdk/android/build.gradle @@ -3,6 +3,10 @@ plugins { id 'org.jetbrains.kotlin.android' } +// 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' android { @@ -33,6 +37,13 @@ android { lint { abortOnError = false } + + sourceSets { + main { + // Generated Codegen sources (the `NativeRNFingerprintjsProSpec` base class). + java.srcDirs += [layout.buildDirectory.dir('generated/source/codegen/java').get().asFile.path] + } + } } repositories { diff --git a/sdk/android/gradle.properties b/sdk/android/gradle.properties index fa4feae5..19b45129 100644 --- a/sdk/android/gradle.properties +++ b/sdk/android/gradle.properties @@ -31,10 +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 - -# 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 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..694f8c31 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,23 +32,23 @@ 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) : NativeRNFingerprintjsProSpec(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?, - 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) { @@ -70,13 +71,15 @@ class RNFingerprintjsProModule(reactContext: ReactApplicationContext) : ReactCon fpjsClient = factory.createInstance(configuration) } - @ReactMethod - fun getVisitorId(tags: ReadableMap?, linkedId: String?, promise: Promise) { - this.getVisitorIdWithTimeout(tags, linkedId, null, promise) + override fun getVisitorId(tags: ReadableMap?, linkedId: String?, timeout: Double?, promise: Promise) { + getVisitorIdInternal(tags, linkedId, timeout?.toInt(), promise) } - @ReactMethod - fun getVisitorIdWithTimeout(tags: ReadableMap?, linkedId: String?, timeout: Int?, promise: 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) { try { if (timeout != null) { fpjsClient?.getVisitorId( @@ -99,16 +102,16 @@ 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 -> - 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) { @@ -168,4 +171,8 @@ class RNFingerprintjsProModule(reactContext: ReactApplicationContext) : ReactCon } return errorType + ":" + error.description } + + companion object { + const val NAME = "RNFingerprintjsPro" + } } diff --git a/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt b/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt index 4efd17ed..60dd7820 100644 --- a/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt +++ b/sdk/android/src/main/java/com/fingerprintjs/reactnative/RNFingerprintjsProPackage.kt @@ -1,18 +1,36 @@ package com.fingerprintjs.reactnative -import com.facebook.react.ReactPackage +import com.facebook.react.BaseReactPackage import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.uimanager.ViewManager -import java.util.* +import com.facebook.react.module.model.ReactModuleInfo +import com.facebook.react.module.model.ReactModuleInfoProvider -class RNFingerprintjsProPackage : ReactPackage { - override fun createNativeModules(reactContext: ReactApplicationContext): List { - return Arrays.asList(RNFingerprintjsProModule(reactContext)) +/** + * Registers the module as a TurboModule via `BaseReactPackage` + `ReactModuleInfoProvider`. + */ +class RNFingerprintjsProPackage : BaseReactPackage() { + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { + return if (name == RNFingerprintjsProModule.NAME) { + RNFingerprintjsProModule(reactContext) + } else { + null + } } - override fun createViewManagers(reactContext: ReactApplicationContext): List> { - return emptyList() + 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/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..030ac544 --- /dev/null +++ b/sdk/ios/RNFingerprintjsPro.mm @@ -0,0 +1,54 @@ +#import + +@interface RCT_EXTERN_MODULE(RNFingerprintjsPro, NSObject) + +RCT_EXTERN_METHOD(configure:(NSString *)apiToken + 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 + timeout:(NSNumber * _Nullable)timeout + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +) + +RCT_EXTERN_METHOD(getVisitorData:(NSDictionary *)tags + linkedId:(NSString *)linkedId + timeout:(NSNumber * _Nullable)timeout + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +) + +@end + +// 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 diff --git a/sdk/ios/RNFingerprintjsPro.swift b/sdk/ios/RNFingerprintjsPro.swift index 12a4e917..d927ebeb 100644 --- a/sdk/ios/RNFingerprintjsPro.swift +++ b/sdk/ios/RNFingerprintjsPro.swift @@ -11,20 +11,15 @@ 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: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:rejecter:) - 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:) + @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) @@ -45,25 +40,20 @@ class RNFingerprintjsPro: NSObject { } } - @objc(getVisitorData:linkedId:resolve:rejecter:) - 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:) + @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 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/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; diff --git a/sdk/package.json b/sdk/package.json index ed75e7d5..a9a0f895 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", @@ -75,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 e200c66a..bf4731a8 100644 --- a/sdk/src/FingerprintJsProAgent.ts +++ b/sdk/src/FingerprintJsProAgent.ts @@ -1,16 +1,8 @@ -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' +import { isTruthy } from './utils' const packageVersion = '__VERSION__' @@ -37,15 +29,15 @@ export class FingerprintJsProAgent implements ProAgent { locationTimeoutMillisAndroid = 5000, }: FingerprintJsProAgentParams) { try { - NativeModules.RNFingerprintjsPro.configure( + RNFingerprintjsPro.configure( apiKey, - region, - endpointUrl, - fallbackEndpointUrls, - extendedResponseFormat, packageVersion, + extendedResponseFormat, + fallbackEndpointUrls, allowUseOfLocationData, - locationTimeoutMillisAndroid + locationTimeoutMillisAndroid, + region ?? null, + endpointUrl ?? null ) this.requestOptions = requestOptions } catch (e) { @@ -63,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 NativeModules.RNFingerprintjsPro.getVisitorIdWithTimeout(tags, linkedId, timeout) - } - - return await NativeModules.RNFingerprintjsPro.getVisitorId(tags, linkedId) + return await RNFingerprintjsPro.getVisitorId(tags ?? null, linkedId ?? null, timeout ?? null) } catch (error) { if (error instanceof Error) { throw unwrapError(error) @@ -89,16 +77,15 @@ 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 - if (isDefined(timeout)) { - visitorData = await NativeModules.RNFingerprintjsPro.getVisitorDataWithTimeout(tags, linkedId, timeout) - } else { - visitorData = await NativeModules.RNFingerprintjsPro.getVisitorData(tags, linkedId) - } - const [requestId, confidenceScore, visitorDataJsonString, sealedResult] = visitorData + 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 = { - ...JSON.parse(visitorDataJsonString), + ...JSON.parse(visitorDataJson), requestId, confidence: { score: confidenceScore, 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..d1baea39 --- /dev/null +++ b/sdk/src/specs/NativeRNFingerprintjsPro.ts @@ -0,0 +1,43 @@ +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 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( + apiToken: string, + pluginVersion: string, + extendedResponseFormat: boolean, + fallbackEndpointUrls: string[], + allowUseOfLocationData: boolean, + locationTimeoutMillis: Double, + region: string | null, + endpointUrl: string | null + ): void + + getVisitorId(tags: UnsafeObject | null, linkedId: string | null, timeout: Double | null): Promise + + getVisitorData(tags: UnsafeObject | null, linkedId: string | null, timeout: Double | null): Promise +} + +export default TurboModuleRegistry.getEnforcing('RNFingerprintjsPro') diff --git a/sdk/src/types.ts b/sdk/src/types.ts index bb7c83ec..f5869a4f 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -406,10 +406,3 @@ export interface VisitorQueryContext extends VisitorQueryResult { } export type VisitorId = string - -export type NativeVisitorData = [ - requestId: string, - confidenceScore: string, - visitorDataJsonString: string, - sealedResult?: string, -]