npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-native-kyc-insight

v0.1.3

Published

KYC Insight React Native SDK — native iOS + Android verification widget bridge (liveness, document capture, BVN / NIN / DL / Passport / CAC).

Readme

react-native-kyc-insight

React Native bridge for the KYC Insight identity verification widget. A thin wrapper over the native iOS + Android SDKs — same config, same lifecycle callbacks, one TypeScript surface.

  • Active-vision liveness with on-device face detection
  • Document capture + file upload
  • BVN / NIN / Driver's License / Passport / CAC consent flows
  • Verdict-aware result screen

Install

yarn add react-native-kyc-insight
# or
npm install react-native-kyc-insight

iOS

cd ios && pod install

The CocoaPods spec pulls in the native KYCWidget pod automatically. iOS 15.0+ required.

Android

Nothing extra to do — autolinking picks up the module. The native ng.netapps:kyc-insight artifact is pulled from Maven Central transitively. minSdk 24+ required.

If you want to pin a specific version of the native SDK, override the dependency in your app's android/app/build.gradle:

dependencies {
  implementation 'ng.netapps:kyc-insight:0.1.0'
}

Permissions

iOS — required Info.plist keys

iOS will crash your app the moment the widget touches the camera / microphone / photo library if your host app's Info.plist doesn't declare a usage description. This is enforced by the OS (TCC), not by the SDK — the strings must live in your app's plist, not in the pod.

Add these three keys to ios/<YourApp>/Info.plist:

<key>NSCameraUsageDescription</key>
<string>This app uses the camera to verify your identity (liveness check) and capture document photos.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app records short audio with the liveness video when required.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app lets you upload documents from your photo library.</string>

The exact wording is up to you — App Review reads it. Empty strings will fail submission.

Android — runtime permission

The SDK declares android.permission.CAMERA via manifest-merger. Your host app must still request the runtime permission on API 23+ before calling present(). With the AndroidX activity-result API:

import { PermissionsAndroid } from 'react-native';

const granted = await PermissionsAndroid.request(
  PermissionsAndroid.PERMISSIONS.CAMERA,
);
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
  await widget.present();
}

Quick start

The easiest way to use the SDK in a React Native component is the useKYCWidget hook — it owns the lifecycle for you (auto-cleanup on unmount, listener management, state machine).

import { useKYCWidget } from 'react-native-kyc-insight';

function VerifyScreen() {
  const { present, close, state, error, verdict, currentLevel } =
    useKYCWidget(
      {
        publicKey: 'NA_PUB_PROD-xxxxxxxxxxxxxxxxxxxxxxxx',
        userRef:   'user-1234',
        slug:      'supplier_registration',
        name:      'Lawrence Olu',
        levelSlug: 'tier_1',
      },
      {
        onSuccess:           ()  => navigation.replace('Home'),
        onLivenessSubmitted: (v) => analytics.track('liveness', v),
        onError:             (e) => Alert.alert(e.message),
      },
    );

  return (
    <>
      <Button
        title="Verify"
        onPress={present}
        disabled={state !== 'idle' && state !== 'closed'}
      />
      {state === 'active' && <Button title="Cancel" onPress={close} />}
      {error && <Text style={{ color: 'red' }}>{error.message}</Text>}
    </>
  );
}

If you'd rather manage the widget imperatively, use the KYCWidget class directly:

import { KYCWidget } from 'react-native-kyc-insight';

const widget = new KYCWidget({ /* same config */ });
widget.addListener('success', () => console.log('All tiers approved'));
widget.addListener('close',   () => widget.destroy());
await widget.present();

Hook API — useKYCWidget(config, handlers?)

function useKYCWidget(
  config: KYCWidgetConfig,
  handlers?: UseKYCWidgetHandlers,
): {
  present: () => Promise<void>;
  close:   () => void;
  state:   'idle' | 'presenting' | 'active' | 'closed';
  error:   KYCWidgetError | null;
  verdict: KYCLivenessVerdict | null;
  currentLevel: KYCWidgetLevel | null;
};

What the hook does for you

  • Lifecycle ownership — creates the native widget on mount, destroys it on unmount. You never call new KYCWidget(...) or destroy() yourself.
  • Listener cleanup — wires all 8 native events under the hood and removes them when the component unmounts.
  • Handler stability — handlers passed in handlers are pinned in a ref, so passing inline arrow functions doesn't rewire native listeners on every render.
  • State surfacing — returns state, error, verdict, currentLevel as React state so your UI re-renders when the native side fires events.
  • Config re-creation — recreates the underlying widget only when a config field actually changes (deep equality via JSON.stringify), not on every render where the caller spreads a new object.

Returned fields

| Field | Description | |---|---| | present() | Launch the widget. Resolves once the native modal mounts; rejects with E_CONFIG / E_NO_HOST. Clears the previous error. | | close() | Dismiss the widget. Same effect as the user tapping the close button. Idempotent. | | state | One of 'idle' (initial / after close), 'presenting' (after present(), before ready), 'active' (after ready), 'closed' (after close). | | error | The most recent KYCWidgetError surfaced by the SDK. Cleared on each present() call. | | verdict | The most recent KYCLivenessVerdict (after livenessSubmitted). null until the user completes liveness. | | currentLevel | The most recent KYCWidgetLevel (after levelChange or levelApproved). null until the user enters their first tier. |

Handler bag

All handlers are optional — pass only what you need:

interface UseKYCWidgetHandlers {
  onReady?:             () => void;
  onLevelChange?:       (level: KYCWidgetLevel)        => void;
  onLevelApproved?:     (level: KYCWidgetLevel)        => void;
  onSubmit?:            (payload: unknown)             => void;
  onSuccess?:           (result: unknown)              => void;
  onError?:             (error: KYCWidgetError)        => void;
  onClose?:             ()                             => void;
  onLivenessSubmitted?: (verdict: KYCLivenessVerdict)  => void;
}

Each handler maps 1:1 to a native event — see Events below for full semantics.

Class API

new KYCWidget(config)

| Field | Required | Description | |---|---|---| | publicKey | yes | Merchant's NA_PUB_* key. | | userRef | yes | Stable identifier for the end user. Reusing the same value returns the same customer record. | | slug | yes | KYC group slug. | | name | yes | End-user display name. | | levelSlug | yes | Starting tier slug. | | vName | no | Billing-line alias for verification-link integrations. | | apiEnvironment | no | 'TEST' or 'LIVE' (default). |

Methods

| Method | Returns | Description | |---|---|---| | present() | Promise<void> | Validate the config and launch the native widget modally over the current RN host. Resolves once the widget is mounted; rejects with E_CONFIG on validation failure or E_NO_HOST if no Activity / view controller is available to present from. | | destroy() | void | Programmatic close. Dismisses the active widget, fires the close event, and clears native state. Idempotent — safe to call when no widget is presented. Use this when you need to close the widget from JS (back-press handler, timeout, after your own success/cancel handling, navigating away in React Navigation, etc.). | | close() | void | Alias for destroy(). Same behaviour — provided for the modal-style .close() idiom many RN libraries follow. | | addListener(event, handler) | () => void | Subscribe to a lifecycle event. Returns an unsubscribe function — call it when your component unmounts to avoid leaks. The full event catalogue is below. |

Programmatic close

const widget = new KYCWidget(config);
await widget.present();

// Later, anywhere in your app — even from outside the component that
// created the widget — call destroy() (or close()) to dismiss it:
widget.destroy();
// or
widget.close();

Both methods do the same thing. Calling either:

  1. Dismisses the native modal / finishes the host Activity.
  2. Tears down the native KYC session and frees its resources (camera, ML Kit detector, network client).
  3. Fires the close event back to your JS listeners, so a single handler can react to both user-initiated and programmatic closes.

A common pattern is to wire destroy() to React Navigation's back intent:

useEffect(() => {
  const unsubscribe = navigation.addListener('beforeRemove', () => {
    widget.destroy();
  });
  return unsubscribe;
}, [navigation, widget]);

Events

Subscribe with widget.addListener(name, handler). The handler runs on the JS thread. All eight native lifecycle callbacks are exposed:

ready

The widget has mounted, fetched its schema from the backend, and is ready for user input. This is your signal that the verification UI is now visible to the user.

Payload: none.

widget.addListener('ready', () => {
  // Hide your own loading spinner, log "session started" analytics, etc.
});

levelChange

The user moved to a new tier — either by submitting the last section of the previous tier, or by navigating manually through the journey outline. Fires on every tier transition, including the initial landing.

Payload: { slug: string, index: number }

  • slug — the tier's identifier (e.g. 'tier_1').
  • index — its zero-based position in the schema.steps array.
widget.addListener('levelChange', (level) => {
  console.log(`Now on ${level.slug} (#${level.index})`);
});

levelApproved

Every section in a tier has been fully approved server-side and is no longer flagged for update. Fires once per transition — never double-fires on a refresh. Useful for kicking off your own backend hooks when a customer crosses a verification milestone.

Payload: { slug: string, index: number } (same shape as levelChange).

widget.addListener('levelApproved', (level) => {
  analytics.track('kyc_tier_approved', { slug: level.slug });
});

submit

A section was successfully submitted to the backend (the user tapped Continue / Submit and the backend accepted the payload). Fires before the cursor advances to the next section.

Payload: the section object — { id, name, providerType, status }. Useful as a fine-grained progress signal; for tier-level callbacks prefer levelApproved.

widget.addListener('submit', (section) => {
  console.log('Submitted section:', section);
});

success

Every tier in the user's flow is now approved or pending review. Terminal event — the widget is about to show the "Verification submitted" screen.

Payload: none (currently — may carry the schema in a future version).

widget.addListener('success', () => {
  // The customer has finished. Navigate away, refresh your local
  // user-state cache, etc.
});

livenessSubmitted

The backend's risk scorer has evaluated a liveness session and returned a verdict. Fires before levelApproved so your analytics see the score with the approval decision.

Payload:

{
  sessionToken: string;
  status: 'passed' | 'failed' | 'requires_manual_review' | 'expired' | 'submitted';
  riskScore: number | null;   // analytics only — never shown to the user
  failureReason: string | null;
}
  • status === 'passed' — auto-approved; the user advances.
  • status === 'requires_manual_review' — queued for a reviewer; the user advances but the section status becomes pending.
  • status === 'failed' or 'expired' — the user is asked to retake; failureReason carries the user-friendly hint.
  • riskScore — the backend's internal score (0-1 typically). The widget UI never displays this to the user — it's signal for your analytics, dashboards, and review queues only.
widget.addListener('livenessSubmitted', (v) => {
  analytics.track('liveness_verdict', {
    status: v.status,
    risk_score: v.riskScore,
  });
});

error

A fatal load or submission failure. The widget will surface a recovery banner with a "Try again" button to the user; you receive a typed copy so you can log / report / alert.

Payload: { type, message, detail? } where type is one of:

| Type | Meaning | |---|---| | missingRequiredConfig | A required config field was empty. detail holds the field name. | | loadFailed | createMerchantCustomer or initial session load failed. message carries the network/server reason. | | submissionFailed | The backend rejected a section submission. | | cameraUnavailable | No usable camera (rare — emulators without virtual cameras). | | permissionDenied | Runtime permission denied. detail holds the permission kind (e.g. 'Camera'). | | externalConsentFailed | An out-of-app consent step (NIN auth, BVN consent) failed or was cancelled. | | unknown | Fallback for un-typed errors. |

widget.addListener('error', (err) => {
  if (err.type === 'permissionDenied') {
    Alert.alert(`${err.detail} permission required`);
  } else {
    crashlytics().recordError(new Error(err.message));
  }
});

close

The widget was dismissed — either by the user tapping the close button or by your code calling widget.destroy() / widget.close(). Always the last event fired in a session's lifetime; once it fires the widget instance is unusable, construct a new one if you need another session.

Payload: none.

widget.addListener('close', () => {
  // Optional: refresh whatever screen sits behind the modal.
  refreshCustomerState();
});

Cleanup

Each addListener returns its own unsubscribe function. Inside a component, call all of them on unmount:

useEffect(() => {
  const widget = new KYCWidget(config);
  const subs = [
    widget.addListener('ready',             () => {}),
    widget.addListener('levelChange',       () => {}),
    widget.addListener('levelApproved',     () => {}),
    widget.addListener('submit',            () => {}),
    widget.addListener('success',           () => {}),
    widget.addListener('livenessSubmitted', () => {}),
    widget.addListener('error',             () => {}),
    widget.addListener('close',             () => {}),
  ];
  widget.present();
  return () => {
    for (const u of subs) u();
    widget.destroy();
  };
}, []);

License

MIT