React Native SDK
Build with

The React Native SDK mirrors the web SDK's core API (same props, same callbacks) but runs on-device, native liveness: Apple Vision on iOS and Google ML Kit on Android, via a react-native-vision-camera v5 (Nitro) frame processor. Because it ships native code, it needs a custom native build and does not run in Expo Go. For shared concepts (supported countries, branding, results, and errors), see Client SDKs.

Feature availability. This SDK is at full parity with the web and Flutter SDKs: workflow embedding (workflowId), the capture add-ons (contact OTP, proof of address, questionnaire), business (KYB) verification, any Global Documents country, plus NFC chip reading, which the web SDK can't do at all (browsers can't talk to a passport chip). See Feature availability.

Requirements
Build with

RequirementMinimum
iOS deployment target15.1
Android minSdkVersion24 (Android 7.0) · compileSdk 34 · NDK 27.1
Expo SDK56 (React 19, React Native 0.85)
React Native0.83+, with the New Architecture enabled (VisionCamera v5 / Nitro requires it; Expo SDK 56 enables it by default)
Build toolchainXcode + CocoaPods (iOS) · JDK 17 for Android Gradle builds
RuntimeA dev client or bare build, not Expo Go

Peer dependencies to install in your app: expo (≥56), react (≥19), react-native (≥0.83), react-native-vision-camera (v5), react-native-vision-camera-worklets (≥5), react-native-worklets (≥0.8), react-native-nitro-modules (≥0.35), react-native-nitro-image (≥0.15), react-native-safe-area-context (≥4), react-native-svg (≥15).

Install
Build with

Pick the path that matches your project.

shell
npx expo install @myazahq/kyc-sdk-react-native \
  react-native-vision-camera react-native-vision-camera-worklets \
  react-native-worklets react-native-nitro-modules react-native-nitro-image \
  react-native-safe-area-context react-native-svg

Add the config plugins to app.json (the SDK plugin adds the iOS camera-usage string and Android CAMERA / INTERNET permissions; the VisionCamera plugin wires up the camera + frame processors), then build a dev client:

json
// app.json
{
  "expo": {
    "plugins": [
      ["react-native-vision-camera", { "enableMicrophonePermission": false }],
      "@myazahq/kyc-sdk-react-native"
    ]
  }
}
shell
npx expo prebuild
npx expo run:ios       # or: npx expo run:android

The SDK plugin accepts an optional custom camera prompt: ["@myazahq/kyc-sdk-react-native", { "cameraPermission": "Your message…" }]. It also adds the location permission strings by default (the Address Intelligence step's "Use my current location" shortcut and attest fix; foreground only, and never required to finish the flow) — pass { "location": false } to opt out if none of your workflows collect an address, or { "locationPermission": "Your message…" } to customise the iOS prompt.

Bare React Native app (no Expo prebuild)

The SDK uses a few expo-* modules, so add the Expo module runtime (you don't need the managed workflow), then install the SDK and its peers:

shell
# 1. One-time: add Expo modules to a bare RN app
npx install-expo-modules@latest

# 2. Install the SDK + peer dependencies
npm install @myazahq/kyc-sdk-react-native \
  react-native-vision-camera react-native-vision-camera-worklets \
  react-native-worklets react-native-nitro-modules react-native-nitro-image \
  react-native-safe-area-context react-native-svg \
  expo expo-image-manipulator expo-image-picker expo-speech expo-font \
  expo-glass-effect expo-application expo-crypto expo-device expo-localization

# 3. iOS pods
cd ios && pod install && cd ..

Then add the native permissions manually (the config plugin only runs under prebuild):

xml
<!-- iOS  ios/<App>/Info.plist -->
<key>NSCameraUsageDescription</key>
<string>We use the camera to photograph your ID and capture a live selfie.</string>
xml
<!-- Android  android/app/src/main/AndroidManifest.xml -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />

If your workflows use the Address Intelligence step, also add the location strings (iOS crashes on the permission request without the usage string):

xml
<!-- iOS  ios/<App>/Info.plist -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>Your location helps place the map pin on your address.</string>
xml
<!-- Android  android/app/src/main/AndroidManifest.xml -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

The native face-detector module (a VisionCamera v5 Nitro HybridObject plus its Android lib loader) is autolinked, so no manual linking is needed. Ensure the New Architecture is enabled and the worklets/frame-processor build is set up per VisionCamera's setup docs (it relies on react-native-worklets / react-native-vision-camera-worklets, installed above).

Presence reporting (Address Intelligence)

When a workflow enables presence verification, the SDK stores the confirmed pin on-device at capture. Call the reporter from your app on a natural moment (app open works well):

tsx
import { reportAddressPresence } from '@myazahq/kyc-sdk-react-native';

const result = await reportAddressPresence({
  apiKey: 'pk_live_…',
  externalUserId: 'user_42', // the same userId the KYC flow ran with
});
// result.reason: 'reported' | 'no_pin' | 'no_fix' | 'outside_fence' | 'network_error'

It never throws and never blocks startup. The geofence is evaluated on-device: only the derived day + night flag is transmitted, never a coordinate. A fix outside the fence sends nothing; a mock-location fix is reported flagged. clearPresencePin(externalUserId) drops the stored pin (sign-out, or after the watch resolves). Stored pins self-expire after 45 days for bounded checks; a pin captured under always-on monitoring never expires until revoked.

Background monitoring (OS geofencing)

The stronger tier: the OS wakes the SDK on fence crossings around the stored pin, app closed or not. Entries stamp a timestamp; exits fold the dwell into per-day aggregates on-device and flush them — the same privacy floor as the foreground tier. Three opt-ins, each deliberate:

  1. Install the optional peer: npx expo install expo-task-manager (without it the background tier simply does not exist).
  2. Declare background location via the config plugin — ["@myazahq/kyc-sdk-react-native", { "location": "always" }]. This changes your app's store review posture; the Background Location Declarations page carries the ready-to-paste Play Console and App Review texts.
  3. Register the task at your app's root module (before the component tree), then enable after capture:
tsx
// index.js
import { registerBackgroundPresence } from '@myazahq/kyc-sdk-react-native';
registerBackgroundPresence();

// later, once the KYC flow has stored a pin:
const result = await enableBackgroundPresence({ apiKey: 'pk_live_…', externalUserId: 'user_42' });
// result.reason: 'enabled' | 'module_missing' | 'no_pin' | 'foreground_denied' | 'background_denied' | 'start_failed'

disableBackgroundPresence() disarms the fence. A refusal at any step leaves the foreground tier working exactly as before: the tiers degrade, never break.

Which tier is running?

Permissions get revoked in Settings and nothing tells the app. Ask, and offer the only honest road back:

tsx
import { presenceStatus, openLocationSettings } from '@myazahq/kyc-sdk-react-native';

const status = await presenceStatus('user_42');
// status.tier: 'background' | 'foreground' | 'none' — plus pinStored, alwaysOn,
// both permission states and geofenceArmed
if (status.tier === 'none' && status.pinStored) {
  await openLocationSettings(); // no OS allows re-prompting in-app after a denial
}

Voice guidance is text-to-speech output. The SDK never records audio, so no microphone permission is requested or required (enableMicrophonePermission: false).

Usage
Build with

<MyazaKYC /> renders a "Verify Identity" trigger plus the full-screen flow. The API is identical to the web SDK, with the same props and callbacks. Unlike the web SDK there is no stylesheet to import: styling is built in.

Build the flow once in the dashboard as a workflow, then mount it by id. The country, ID types, capture steps, branding and copy all come from the workflow, so changing the flow is a re-publish rather than an app release, which matters even more on mobile, where a redeploy means an app-store round trip. Requires ≥ 2.1.0 (the 2.0.x line silently ignores the id).

tsx
import { MyazaKYC } from "@myazahq/kyc-sdk-react-native";

export default function VerifyScreen() {
  return (
    <MyazaKYC
      apiKey="pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
      workflowId="wf_AbC123dEf456"
      // Runtime data — a workflow is a shared template and cannot carry any of it.
      userId="user_42"
      userData={{ firstName: "Jane", lastName: "Doe" }}
      metadata={{ requestId: "order_1001" }}
      onSubmit={(submission) => console.log("submitted", submission.verificationId)}
      onError={(err) => console.error(err.code, err.message)}
      onClose={() => console.log("closed")}
    >
      Verify Identity
    </MyazaKYC>
  );
}

userData is worth passing. It is the name you believe the user has, and it is compared against the name read off their document, and that comparison is what produces dataMatch on the verification. Leave it out and the check simply never runs: there is nothing to compare the document against, and dataMatch comes back null.

It cannot live on the workflow. userId, userData and metadata are per-user runtime values, and a workflow is a template shared by every visitor, so these stay in code even when everything else moves to the dashboard.

Or configure everything in code

Skip the workflow and pass the flow's shape as props. Useful for a quick start or a single fixed flow; anything you'd change later means an app release.

tsx
import { MyazaKYC } from "@myazahq/kyc-sdk-react-native";

export default function VerifyScreen() {
  return (
    <MyazaKYC
      apiKey="pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
      country="NG"
      idTypes={["bvn", "drivers-license", "passport"]}
      userData={{ firstName: "Jane", lastName: "Doe" }}
      enableSelfie
      enableDocumentCapture
      enableLiveness
      showThemeToggle
      appearance={{ primaryColor: "#5645F5", companyName: "Myaza", logo: "default", theme: "light" }}
      consent={{ title: "Welcome, {firstName}", description: "A quick check to confirm it's really you." }}
      success={{ title: "You're all set, {firstName}!", description: "We'll email you once your verification is reviewed." }}
      metadata={{ requestId: "order_1001", userId: "user_42" }}
      onSubmit={(submission) => {
        // The verification was created; status is always 'pending'.
        // Reconcile the final result on your backend via webhook or a secret-key
        // GET /verifications/:id call (never from the client).
        console.log("submitted", submission.verificationId);
      }}
      onError={(err) => console.error(err.code, err.message)}
      onClose={() => console.log("closed")}
    >
      Verify Identity
    </MyazaKYC>
  );
}

The SDK accepts the same props as the web SDK's props table, including workflowId, livenessMode, deviceIntelligence, and the capture add-ons (contact OTP, proof of address, questionnaire, NFC). The one exception is className (React Native has no class names); style the trigger by passing style, or render your own trigger with the hook.

disableClose blocks user dismissal on native too: the iOS swipe-down and the Android back button. Because the built-in <MyazaKYC /> trigger has no external close handle, pair disableClose with the useMyazaKYC() hook and call its close() to dismiss the flow yourself.

Programmatic control
Build with

For a custom trigger, drive the flow with the useMyazaKYC() hook:

tsx
import { useMyazaKYC } from "@myazahq/kyc-sdk-react-native";
import { Pressable, Text } from "react-native";

function Trigger() {
  const { open, close, isOpen, currentStep } = useMyazaKYC({
    apiKey: "pk_test_…",
    country: "NG",
    onSubmit: (s) => console.log(s.verificationId),
  });

  return (
    <Pressable onPress={open} disabled={isOpen}>
      <Text>Verify ({currentStep ?? "idle"})</Text>
    </Pressable>
  );
}

The flow advances through the same KYCStep values as the web SDK: consentid-typeid-inputdocument-capturelivenesssubmitted.