Guides

Add device fingerprinting to a React Native app

Add device fingerprinting to a React Native app with one TypeScript API over the native iOS and Android SDKs, then verify the result on your server.

This tutorial adds device fingerprinting to a React Native app. @fingerly/react-native is a thin bridge to the native iOS and Android SDKs, because almost every signal worth having on a phone is out of reach of JavaScript. You write one TypeScript API, and each platform keeps its own identity rules.

Before you start

  • React Native 0.73 or newer, including the New Architecture, with iOS 13 and Android 5.0 (API 21) or newer.
  • A development build. Expo Go cannot load native modules.
  • A public key for the development environment, and a secret key for your server.
  • A decision about consent: the SDK collects nothing until consent is granted.

Add the SDK and verify the result

  1. Step 1: Create your keys

    In the Fingerly dashboard, open Integration > SDK keys and create a public key in the development environment, and a secret key in the same environment for your server. Development keys run the same detection as production and cost nothing. A production key must also name your app, an Android package or an Apple app ID, as in API keys and environments.

  2. Step 2: Install the package

    npm install @fingerly/react-native
    cd ios && pod install
    

    Autolinking handles the rest. On iOS, pod install downloads the native framework and checks it against a pinned checksum. On Android, the package adds Fingerly's Maven repository to the build itself.

  3. Step 3: Load the client and identify at sign-in

    fingerly.ts
    import { load } from '@fingerly/react-native'
    
    // consent: 'granted' only once you have consent (or another lawful basis)
    const fingerly = await load({ apiKey: 'fly_pk_development_…', consent: 'granted' })
    
    export async function signIn(email: string, password: string) {
      const { requestId } = await fingerly.identify({ tag: 'sign-in' })
      return api.signIn({ email, password, requestId })
    }
    

    Call signIn from a button's onPress. The consent state is shared by the whole app, so pass it on every load.

    SignInScreen.tsx
    import { signIn } from './fingerly'
    
    <Button title="Sign in" onPress={() => signIn(email, password)} />
    

    identify resolves with requestId, visitorId, visitorIsNew, visitorConfidence, identifiable, duplicate, state, suspectScore, suspectLevel and report. Several are null when the server did not say, which is not the same as 0 or false.

  4. Step 4: Verify on your server

    A phone is a device you do not control, so the decision is never made in the app. Send the requestId to your backend with the action, read the stored event with your secret key, and check it belongs to this action and is recent.

    Sign-in handler (Node.js)
    import { FingerlyAPIError, load } from '@fingerly/node'
    
    const fingerly = load({ secretKey: process.env.FINGERLY_SECRET_KEY! })
    
    export async function afterPasswordCheck(requestId: unknown) {
      if (typeof requestId !== 'string' || requestId === '') return 'second-factor' // missing evidence
    
      try {
        const event = await fingerly.events.get(requestId)
        if (event.tag !== 'sign-in') return 'second-factor'
        if (Date.now() - Date.parse(event.occurred_at) > 2 * 60 * 1000) return 'second-factor'
        return event.suspect_level === 'high' ? 'second-factor' : 'allow'
      } catch (error) {
        if (error instanceof FingerlyAPIError && error.code === 'event_not_found') return 'second-factor'
        throw error // still pending: answer with a retry, never a refusal
      }
    }
    

    Also check the request ID has not been used before, and answer a pending event with a retry rather than a refusal. Server-side verification explains each check, and the use-case recipes include a helper that does all of them.

  5. Step 5: Handle errors

    errors.ts
    import { FingerlyError } from '@fingerly/react-native'
    
    try {
      await fingerly.identify()
    } catch (error) {
      if (error instanceof FingerlyError && error.status === 401) {
        // the key is wrong or revoked
      }
    }
    

    Retries happen in the native layer. Without consent, calls throw ConsentError, a FingerlyError whose code is consent_required. If the native module is missing, every call throws a FingerlyError that explains how to rebuild.

  6. Step 6: Run it on both platforms

    Run a development build on a device or simulator for each platform, sign in, then open Identification > Events in the dashboard. Simulators and emulators record their own signals, which is expected there.

What differs between iOS and Android

iOSAndroid
The identityA random install ID in the Keychain.A random install ID in a file excluded from backups.
After a reinstallThe same visitor ID.A new visitor ID.
Proof the report comes from your appApp Attest, once you add the capability and your Apple app ID to the key.Hardware-backed attestation, which works without configuration.
automatic optionJoins an identification in flight and waits out a transient failure.Accepted and has no effect yet.

To remove the stored identity for an erasure request or account deletion, call deleteStoredIdentity(). See account deletion and erasure.

Common questions

Does it work with Expo?

In a development build, yes. Expo Go cannot load native modules, so it cannot run this package.

Does React Native fingerprint a device in JavaScript?

No. Collection happens in the native iOS and Android SDKs, and JavaScript gets typed results. That is why one TypeScript API can report what a pure JavaScript library cannot reach.

Do I need CocoaPods for iOS?

Autolinking runs pod install for the bridge, which downloads the native framework and checks it against a pinned checksum. Your Podfile needs no entry for it.

Is the result the same on iOS and Android?

The fields are the same. The identity rules differ: an iOS install ID survives a reinstall and an Android one does not. See visitor identification.

How do I get the native SDK version?

Call nativeVersion(). It returns the version of the native SDK that did the collecting.