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
consentisgranted.
Add the SDK and verify the result
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.
Step 2: Install the package
npm install @fingerly/react-native cd ios && pod installAutolinking handles the rest. On iOS,
pod installdownloads the native framework and checks it against a pinned checksum. On Android, the package adds Fingerly's Maven repository to the build itself.Step 3: Load the client and identify at sign-in
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
signInfrom a button'sonPress. The consent state is shared by the whole app, so pass it on everyload.import { signIn } from './fingerly' <Button title="Sign in" onPress={() => signIn(email, password)} />identifyresolves withrequestId,visitorId,visitorIsNew,visitorConfidence,identifiable,duplicate,state,suspectScore,suspectLevelandreport. Several arenullwhen the server did not say, which is not the same as0orfalse.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
requestIdto your backend with the action, read the stored event with your secret key, and check it belongs to this action and is recent.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.
Step 5: Handle errors
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, aFingerlyErrorwhosecodeisconsent_required. If the native module is missing, every call throws aFingerlyErrorthat explains how to rebuild.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
| iOS | Android | |
|---|---|---|
| The identity | A random install ID in the Keychain. | A random install ID in a file excluded from backups. |
| After a reinstall | The same visitor ID. | A new visitor ID. |
| Proof the report comes from your app | App Attest, once you add the capability and your Apple app ID to the key. | Hardware-backed attestation, which works without configuration. |
automatic option | Joins 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.