[{"data":1,"prerenderedAt":208},["ShallowReactive",2],{"doc:\u002Fdocs\u002Fdevice-fingerprinting-react-native":3},{"page":4,"toc":185,"updated":207},{"path":5,"title":6,"seoTitle":7,"description":8,"blocks":9},"\u002Fdocs\u002Fdevice-fingerprinting-react-native","Add device fingerprinting to a React Native app","React Native Device Fingerprinting: iOS and Android Tutorial","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.",[10,13,18,25,28,101,104,127,129,133,136,140,142,145,147,150,152,155,157,160,162],{"type":11,"text":12},"p","This tutorial adds **device fingerprinting** to a React Native app. `@fingerly\u002Freact-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.",{"type":14,"level":15,"text":16,"id":17},"heading",2,"Before you start","before-you-start",{"type":19,"items":20},"list",[21,22,23,24],"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`.",{"type":14,"level":15,"text":26,"id":27},"Add the SDK and verify the result","add-the-sdk-and-verify-the-result",{"type":29,"steps":30},"steps",[31,36,54,74,86,96],{"title":32,"blocks":33},"Create your keys",[34],{"type":11,"text":35},"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](\u002Fdocs\u002Fapi-keys#native-apps).",{"title":37,"blocks":38},"Install the package",[39,52],{"type":40,"samples":41},"code",[42,46,49],{"label":43,"lang":44,"code":45},"npm","bash","npm install @fingerly\u002Freact-native\ncd ios && pod install",{"label":47,"lang":44,"code":48},"pnpm","pnpm add @fingerly\u002Freact-native\ncd ios && pod install",{"label":50,"lang":44,"code":51},"yarn","yarn add @fingerly\u002Freact-native\ncd ios && pod install",{"type":11,"text":53},"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.",{"title":55,"blocks":56},"Load the client and identify at sign-in",[57,64,66,72],{"type":40,"samples":58},[59],{"label":60,"lang":61,"code":62,"check":63},"fingerly.ts","ts","import { load } from '@fingerly\u002Freact-native'\n\n\u002F\u002F consent: 'granted' only once you have consent (or another lawful basis)\nconst fingerly = await load({ apiKey: 'fly_pk_development_…', consent: 'granted' })\n\nexport async function signIn(email: string, password: string) {\n  const { requestId } = await fingerly.identify({ tag: 'sign-in' })\n  return api.signIn({ email, password, requestId })\n}",false,{"type":11,"text":65},"Call `signIn` from a button's `onPress`. The consent state is shared by the whole app, so pass it on every `load`.",{"type":40,"samples":67},[68],{"label":69,"lang":70,"code":71,"check":63},"SignInScreen.tsx","tsx","import { signIn } from '.\u002Ffingerly'\n\n\u003CButton title=\"Sign in\" onPress={() => signIn(email, password)} \u002F>",{"type":11,"text":73},"`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`.",{"title":75,"blocks":76},"Verify on your server",[77,79,84],{"type":11,"text":78},"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.",{"type":40,"samples":80},[81],{"label":82,"lang":61,"code":83,"check":63},"Sign-in handler (Node.js)","import { FingerlyAPIError, load } from '@fingerly\u002Fnode'\n\nconst fingerly = load({ secretKey: process.env.FINGERLY_SECRET_KEY! })\n\nexport async function afterPasswordCheck(requestId: unknown) {\n  if (typeof requestId !== 'string' || requestId === '') return 'second-factor' \u002F\u002F missing evidence\n\n  try {\n    const event = await fingerly.events.get(requestId)\n    if (event.tag !== 'sign-in') return 'second-factor'\n    if (Date.now() - Date.parse(event.occurred_at) > 2 * 60 * 1000) return 'second-factor'\n    return event.suspect_level === 'high' ? 'second-factor' : 'allow'\n  } catch (error) {\n    if (error instanceof FingerlyAPIError && error.code === 'event_not_found') return 'second-factor'\n    throw error \u002F\u002F still pending: answer with a retry, never a refusal\n  }\n}",{"type":11,"text":85},"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](\u002Fdocs\u002Fserver-side-verification) explains each check, and the [use-case recipes](\u002Fdocs\u002Fuse-cases) include a helper that does all of them.",{"title":87,"blocks":88},"Handle errors",[89,94],{"type":40,"samples":90},[91],{"label":92,"lang":61,"code":93,"check":63},"errors.ts","import { FingerlyError } from '@fingerly\u002Freact-native'\n\ntry {\n  await fingerly.identify()\n} catch (error) {\n  if (error instanceof FingerlyError && error.status === 401) {\n    \u002F\u002F the key is wrong or revoked\n  }\n}",{"type":11,"text":95},"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.",{"title":97,"blocks":98},"Run it on both platforms",[99],{"type":11,"text":100},"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.",{"type":14,"level":15,"text":102,"id":103},"What differs between iOS and Android","what-differs-between-ios-and-android",{"type":105,"columns":106,"rows":110},"table",[107,108,109],"","iOS","Android",[111,115,119,123],[112,113,114],"The identity","A random install ID in the Keychain.","A random install ID in a file excluded from backups.",[116,117,118],"After a reinstall","The same visitor ID.","A new visitor ID.",[120,121,122],"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.",[124,125,126],"`automatic` option","Joins an identification in flight and waits out a transient failure.","Accepted and has no effect yet.",{"type":11,"text":128},"To remove the stored identity for an erasure request or account deletion, call `deleteStoredIdentity()`. See [account deletion and erasure](\u002Fdocs\u002Fapp-store-privacy#account-deletion-and-erasure).",{"type":130,"tone":131,"text":132},"callout","warning","Do not use the browser SDK in a React Native WebView. Inside an app it collects nothing and the answer has `identifiable: false`. Use this package.",{"type":14,"level":15,"text":134,"id":135},"Common questions","common-questions",{"type":14,"level":137,"text":138,"id":139},3,"Does it work with Expo?","does-it-work-with-expo",{"type":11,"text":141},"In a development build, yes. Expo Go cannot load native modules, so it cannot run this package.",{"type":14,"level":137,"text":143,"id":144},"Does React Native fingerprint a device in JavaScript?","does-react-native-fingerprint-a-device-in-javascript",{"type":11,"text":146},"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.",{"type":14,"level":137,"text":148,"id":149},"Do I need CocoaPods for iOS?","do-i-need-cocoapods-for-ios",{"type":11,"text":151},"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.",{"type":14,"level":137,"text":153,"id":154},"Is the result the same on iOS and Android?","is-the-result-the-same-on-ios-and-android",{"type":11,"text":156},"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](\u002Fdocs\u002Fvisitor-identification#in-apps).",{"type":14,"level":137,"text":158,"id":159},"How do I get the native SDK version?","how-do-i-get-the-native-sdk-version",{"type":11,"text":161},"Call `nativeVersion()`. It returns the version of the native SDK that did the collecting.",{"type":163,"columns":15,"cards":164},"cards",[165,170,175,180],{"title":166,"text":167,"href":168,"logo":169},"React Native SDK","Every call, field and error.","\u002Fdocs\u002Fsdks\u002Freact-native","react",{"title":171,"text":172,"href":173,"logo":174},"iOS tutorial","The native Swift side.","\u002Fdocs\u002Fdevice-fingerprinting-ios","apple",{"title":176,"text":177,"href":178,"logo":179},"Android tutorial","The native Kotlin side.","\u002Fdocs\u002Fdevice-fingerprinting-android","android",{"title":181,"text":182,"href":183,"icon":184},"Server-side verification","The four checks before you trust a result.","\u002Fdocs\u002Fserver-side-verification","shield",[186,187,188,190,192,194,196,198,200,201,202,203,204,205,206],{"id":17,"text":16,"level":15},{"id":27,"text":26,"level":15},{"id":189,"text":32,"level":137},"step-create-your-keys",{"id":191,"text":37,"level":137},"step-install-the-package",{"id":193,"text":55,"level":137},"step-load-the-client-and-identify-at-sign-in",{"id":195,"text":75,"level":137},"step-verify-on-your-server",{"id":197,"text":87,"level":137},"step-handle-errors",{"id":199,"text":97,"level":137},"step-run-it-on-both-platforms",{"id":103,"text":102,"level":15},{"id":135,"text":134,"level":15},{"id":139,"text":138,"level":137},{"id":144,"text":143,"level":137},{"id":149,"text":148,"level":137},{"id":154,"text":153,"level":137},{"id":159,"text":158,"level":137},"2026-11-12T11:59:51.000Z",1794493376753]