🎯 Learning Path:
Welcome to the REL-ID Identity Verification MFA Activation Flow codelab! This tutorial teaches you how to implement comprehensive identity verification during the user activation process using document scanning, facial recognition, and biometric authentication.
In this codelab, you'll enhance your existing REL-ID MFA activation flow with integrated identity verification:
This codelab demonstrates IDV integrated within MFA activation, where identity verification occurs immediately after the activation code and before password setup:
Standard MFA Activation Flow:
Activation Code → Password Setup → Dashboard
IDV MFA Activation Flow (This Codelab):
Activation Code → IDV (Document → Selfie → Consent) → Password Setup → Dashboard
Understanding the complete flow is crucial before implementation:
1. SDK Initialization
↓ (rdnaService.initialize)
2. User Registration
↓ (CheckUserScreen → getUser event)
3. Activation Code Verification
↓ (ActivationCodeScreen → activation code validated)
4. 📄 IDV Document Scan Process
├─ getIDVDocumentScanProcessStartConfirmation event
│ • challengeMode: 8 (activation)
│ • idvWorkflow: 0 (RDNAIDVWorkflowActivation)
├─ setIDVDocumentScanProcessStartConfirmation(true, 0)
├─ Native Document Capture (Regula SDK)
├─ getIDVConfirmDocumentDetails event
│ • challengeMode: 8
│ • response_data: OCR-extracted document fields
└─ setIDVConfirmDocumentDetails(true, challengeMode)
↓
5. 🤳 IDV Selfie Capture Process
├─ getIDVSelfieProcessStartConfirmation event
│ • idvWorkflow: 0 (RDNAIDVWorkflowActivation)
├─ setIDVSelfieProcessStartConfirmation(true, false, 0)
├─ Native Selfie Capture (Liveness Detection)
├─ getIDVSelfieConfirmation event
│ • challengeMode: 8
│ • response_data: Biometric match results
└─ setIDVSelfieConfirmation("true", challengeMode)
↓
6. 🔐 IDV Biometric Consent
├─ getIDVBiometricOptInConsent event
│ • challengeMode: 8
└─ setIDVBiometricOptInConsent(true, challengeMode)
↓
7. 🔑 Password Setup
↓ (SetPasswordScreen → getPassword event)
8. ✅ onUserLoggedIn Event
↓
9. Dashboard Navigation
For MFA activation flow, you'll use these specific values:
Parameter | Value | Description |
idvWorkflow |
|
|
challengeMode |
| Activation challenge mode |
Ensure you have completed these requirements:
# Required tools
Node.js >= 16
npm or yarn
React Native CLI
Xcode (for iOS) or Android Studio (for Android)
The code to get started can be found in a GitHub repository.
You can clone the repository using the following command:
git clone https://github.com/uniken-team/relid-codelab-react-native.git
Navigate to the relid-IDV-MFA-activation-flow folder in the repository you cloned earlier:
cd relid-codelab-react-native/relid-IDV-MFA-activation-flow
The IDV MFA Activation codelab follows this structure:
relid-IDV-MFA-activation-flow/
├── src/
│ ├── tutorial/
│ │ ├── screens/
│ │ │ ├── idv/ # IDV Screens
│ │ │ │ ├── IDVDocumentProcessStartConfirmationScreen.tsx
│ │ │ │ ├── IDVConfirmDocumentDetailsScreen.tsx
│ │ │ │ ├── IDVSelfieProcessStartConfirmationScreen.tsx
│ │ │ │ ├── IDVSelfieConfirmationScreen.tsx
│ │ │ │ ├── IDVBiometricOptInConsentScreen.tsx
│ │ │ │ └── index.ts
│ │ │ └── mfa/ # MFA Screens
│ │ │ ├── CheckUserScreen.tsx
│ │ │ ├── ActivationCodeScreen.tsx
│ │ │ ├── SetPasswordScreen.tsx
│ │ │ └── index.ts
│ │ └── navigation/
│ │ ├── AppNavigator.tsx # Main navigator with IDV routes
│ │ └── NavigationService.ts
│ └── uniken/
│ ├── services/
│ │ ├── idv/
│ │ │ ├── rdnaIDVService.ts # IDV Service Layer
│ │ │ └── rdnaIDVEventManager.ts # IDV Event Manager
│ │ └── rdnaService.ts # Main RDNA Service
│ ├── providers/
│ │ ├── idv/
│ │ │ └── SDKIDVEventProvider.tsx # IDV Event Provider
│ │ └── SDKEventProvider.tsx # Main Event Provider
│ └── types/
│ └── idv/
│ └── rdnaIDVEvents.ts # IDV Type Definitions
└── android/app/src/main/ # Android IDV Assets
├── assets/
│ └── Regula/
│ ├── db.dat # Document recognition DB (110.5MB)
│ └── certificates/
│ └── icaopkd-002-complete-000325.ldif
└── res/raw/
└── regula.license # Regula license file
└── ios/ # iOS IDV Assets
└── idv-artifacts/
├── db.dat # Document recognition DB (110.5MB)
├── regula.license # Regula license file
└── Certificates.bundle/
└── icaopkd-002-complete-000325.ldif
# Navigate to the codelab folder
cd relid-IDV-MFA-activation-flow
# Place the react-native-rdna-client plugin at root folder
# (Contact REL-ID team for the plugin)
# Install dependencies
npm install
cd ios && pod install && cd ..
The following IDV assets need to be configured in your project at the specified paths:
Android Configuration:
Place the following files (provided by Uniken admin team) at these locations:
android/app/src/main/assets/Regula/db.datandroid/app/src/main/assets/Regula/certificates/icaopkd-002-complete-000325.ldifandroid/app/src/main/res/raw/regula.licenseiOS Configuration:
Place the following files (provided by Uniken admin team) at these locations:
ios/idv-artifacts/db.datios/idv-artifacts/regula.licenseios/idv-artifacts/Certificates.bundle/icaopkd-002-complete-000325.ldif# Android
npx react-native run-android
# iOS
npx react-native run-ios
This codelab implements IDV functionality using a clean, isolated architecture that follows single responsibility principles. All IDV-related code is separated into dedicated directories, making the codebase maintainable and scalable.
idv/ subdirectoriesThe IDV implementation consists of 6 core files organized in 3 layers:
src/uniken/
├── services/idv/ # SERVICE LAYER
│ ├── rdnaIDVService.ts # IDV API wrapper (Promise-based)
│ └── rdnaIDVEventManager.ts # Native event manager (auto-init)
│
├── providers/idv/ # PROVIDER LAYER
│ └── SDKIDVEventProvider.tsx # React Context provider
│
└── types/idv/ # TYPE DEFINITIONS
└── rdnaIDVEvents.ts # Type-safe event interfaces
src/tutorial/
└── screens/idv/ # UI LAYER
├── IDVDocumentProcessStartConfirmationScreen.tsx
├── IDVConfirmDocumentDetailsScreen.tsx
├── IDVSelfieProcessStartConfirmationScreen.tsx
├── IDVSelfieConfirmationScreen.tsx
├── IDVBiometricOptInConsentScreen.tsx
└── index.ts
The IDV implementation follows a clear 3-layer pattern that separates concerns:
┌─────────────────────────────────────────────────────┐
│ LAYER 3: UI LAYER (React Components) │
│ • 5 IDV Screens (Document, Selfie, Consent) │
│ • Display UI, handle user interactions │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ LAYER 2: PROVIDER LAYER (React Context) │
│ • SDKIDVEventProvider.tsx │
│ • Connects events to navigation │
│ • Manages IDV state (current workflow) │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ LAYER 1: SERVICE LAYER (Business Logic) │
│ • rdnaIDVService.ts → API calls (Promise-based) │
│ • rdnaIDVEventManager.ts → Native event handling │
│ • rdnaIDVEvents.ts → Type definitions │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ NATIVE LAYER (REL-ID SDK) │
│ • react-native-rdna-client │
│ • Native document scanner (Regula) │
│ • Native selfie capture (Liveness) │
└─────────────────────────────────────────────────────┘
Here's how a document scan event flows through all layers:
// 1️⃣ NATIVE LAYER: SDK fires event
Native SDK → fires 'getIDVDocumentScanProcessStartConfirmation'
// 2️⃣ SERVICE LAYER: Event Manager catches it
RdnaIDVEventManager.onGetDocumentScanStartConfirmation()
→ Parses JSON response
→ Calls registered handler
// 3️⃣ PROVIDER LAYER: Provider handles navigation
SDKIDVEventProvider.handleGetDocumentScanStartConfirmation()
→ Updates state (currentIDVWorkflow)
→ Calls NavigationService.navigate('IDVDocumentScanStart')
// 4️⃣ UI LAYER: Screen displays
IDVDocumentProcessStartConfirmationScreen
→ Shows guidelines
→ User clicks "Scan Document"
→ Calls rdnaIDVService.setIDVDocumentScanProcessStartConfirmation()
// 5️⃣ SERVICE LAYER: API call
rdnaIDVService.setIDVDocumentScanProcessStartConfirmation()
→ Wraps native API in Promise
→ Returns sync response
// 6️⃣ NATIVE LAYER: Opens camera
SDK opens Regula document scanner
→ User captures document
→ SDK fires next event (getIDVConfirmDocumentDetails)
→ Cycle continues...
Both the Event Manager and Service use singleton pattern for global state management:
// Event Manager - One instance for all IDV events
class RdnaIDVEventManager {
private static instance = new RdnaIDVEventManager();
private constructor() {
this.registerEventListeners(); // ← Auto-init on module load!
}
static getInstance() { return RdnaIDVEventManager.instance; }
}
Why? Ensures event listeners are registered once before any native events fire, preventing race conditions.
The Event Provider uses React Context to share IDV state across components:
// Provider manages state and navigation
export const SDKIDVEventProvider = ({ children }) => {
const [currentIDVWorkflow, setCurrentIDVWorkflow] = useState(null);
const handleEvent = (data) => {
setCurrentIDVWorkflow(data.idvWorkflow); // Update state
NavigationService.navigate('IDVScreen'); // Navigate
};
useEffect(() => {
eventManager.setHandler(handleEvent); // Register on mount
return () => eventManager.setHandler(undefined); // Cleanup
}, []);
return <Context.Provider value={{ currentIDVWorkflow }}>...</Context.Provider>;
};
Why? Centralizes navigation logic and provides IDV state to all screens via React Context.
All event data uses TypeScript interfaces with reusable common types:
// Reusable common types from base RDNA events
import type { RDNAError, RDNAChallengeResponse } from '../rdnaEvents';
// IDV-specific event data
export interface RDNAIDVGetBiometricOptInConsentData {
userID: string;
challengeMode: RDNAChallengeOpMode;
challengeResponse: RDNAChallengeResponse; // ← Reusable!
error: RDNAError; // ← Reusable!
// ... IDV-specific fields
}
Why? Type safety catches errors at compile-time, and reusing common types ensures consistency between MFA and IDV.
The Service wraps callback-based native APIs with Promises for async/await:
// Native SDK uses callbacks
RdnaClient.setIDVDocumentScanProcessStartConfirmation(isConfirm, workflow, callback);
// Service wraps in Promise for async/await
async setIDVDocumentScanProcessStartConfirmation(isConfirm, workflow) {
return new Promise((resolve, reject) => {
RdnaClient.setIDVDocumentScanProcessStartConfirmation(isConfirm, workflow, response => {
response.error.longErrorCode === 0 ? resolve(response) : reject(response);
});
});
}
// Screens can use async/await
await rdnaIDVService.setIDVDocumentScanProcessStartConfirmation(true, 0);
Why? Modern async/await syntax is cleaner and easier to handle than nested callbacks.
The final piece is integrating SDKIDVEventProvider into your App.tsx with proper nesting:
// App.tsx - Provider Nesting Pattern
import { SDKEventProvider } from './src/uniken/providers/SDKEventProvider';
import { SDKIDVEventProvider } from './src/uniken/providers/idv/SDKIDVEventProvider';
const App = () => {
return (
<SDKEventProvider> {/* ← MFA events (outer) */}
<SDKIDVEventProvider> {/* ← IDV events (inner) */}
<AppNavigator /> {/* ← All screens */}
</SDKIDVEventProvider>
</SDKEventProvider>
);
};
Why this nesting order?
<SDKEventProvider> Handles: getUser, getPassword, onUserLoggedIn, etc.
↓
<SDKIDVEventProvider> Handles: getIDVDocumentScan, getIDVSelfie, etc.
↓
<AppNavigator> All screens can access both contexts
✅ All IDV code under idv/ subdirectories
✅ Easy to locate and modify IDV features
✅ No mixing of MFA and IDV code
✅ Clean separation of concerns
✅ rdnaIDVEventManager.ts → Handles native events only
✅ rdnaIDVService.ts → Handles API calls only
✅ SDKIDVEventProvider.tsx → Handles React integration only
✅ rdnaIDVEvents.ts → Type definitions only
✅ Screen components → UI rendering only
✅ Event manager initializes on module load
✅ Listeners registered before native events fire
✅ Prevents race conditions
✅ No manual initialization required
✅ Complete TypeScript interfaces for all events
✅ Reusable common types (RDNAError, RDNAChallengeResponse)
✅ SDK type integration (RDNAIDVWorkflow, RDNAChallengeOpMode)
✅ Compile-time type checking
✅ All navigation logic in SDKIDVEventProvider
✅ Single source of truth for event handling
✅ Consistent event-to-screen mapping
✅ Easy to trace event flow
The Event Manager is the foundation of the IDV architecture. It handles all native SDK events using the Singleton pattern with auto-initialization to prevent race conditions.
src/uniken/services/idv/rdnaIDVEventManager.ts
Create the file at src/uniken/services/idv/rdnaIDVEventManager.ts:
import { NativeEventEmitter, NativeModules } from 'react-native';
import type { RDNAJsonResponse } from '../../types/rdnaEvents';
import type {
RDNAIDVGetDocumentScanStartConfirmationData,
RDNAIDVGetDocumentScanStartConfirmationCallback,
RDNAIDVGetConfirmDocumentDetailsData,
RDNAIDVGetConfirmDocumentDetailsCallback,
RDNAIDVGetSelfieProcessStartConfirmationData,
RDNAIDVGetSelfieProcessStartConfirmationCallback,
RDNAIDVGetSelfieConfirmationData,
RDNAIDVGetSelfieConfirmationCallback,
RDNAIDVGetBiometricOptInConsentData,
RDNAIDVGetBiometricOptInConsentCallback,
} from '../../types/idv/rdnaIDVEvents';
/**
* REL-ID IDV SDK Event Manager
*
* Manages Identity Verification (IDV) events in a centralized, type-safe manner.
* Auto-initializes on module load to prevent race conditions.
*
* Supported Events (5 total):
* - getIDVDocumentScanProcessStartConfirmation
* - getIDVConfirmDocumentDetails
* - getIDVSelfieProcessStartConfirmation
* - getIDVSelfieConfirmation
* - getIDVBiometricOptInConsent
*/
class RdnaIDVEventManager {
private static instance: RdnaIDVEventManager = new RdnaIDVEventManager();
private rdnaEmitter: NativeEventEmitter;
private listeners: Array<any> = [];
// Event handler storage (one handler per event type)
private getDocumentScanStartConfirmationHandler?: RDNAIDVGetDocumentScanStartConfirmationCallback;
private getConfirmDocumentDetailsHandler?: RDNAIDVGetConfirmDocumentDetailsCallback;
private getSelfieProcessStartConfirmationHandler?: RDNAIDVGetSelfieProcessStartConfirmationCallback;
private getSelfieConfirmationHandler?: RDNAIDVGetSelfieConfirmationCallback;
private getBiometricOptInConsentHandler?: RDNAIDVGetBiometricOptInConsentCallback;
private constructor() {
console.log('RdnaIDVEventManager - Auto-initializing on module load');
this.rdnaEmitter = new NativeEventEmitter(NativeModules.RdnaClient);
this.registerEventListeners();
}
static getInstance(): RdnaIDVEventManager {
return RdnaIDVEventManager.instance;
}
/**
* Registers all native event listeners
*/
private registerEventListeners() {
console.log('RdnaIDVEventManager - Registering native event listeners');
// Document Scan Events
this.listeners.push(
this.rdnaEmitter.addListener(
'getIDVDocumentScanProcessStartConfirmation',
this.onGetDocumentScanStartConfirmation.bind(this)
)
);
this.listeners.push(
this.rdnaEmitter.addListener(
'getIDVConfirmDocumentDetails',
this.onGetConfirmDocumentDetails.bind(this)
)
);
// Selfie Capture Events
this.listeners.push(
this.rdnaEmitter.addListener(
'getIDVSelfieProcessStartConfirmation',
this.onGetSelfieProcessStartConfirmation.bind(this)
)
);
this.listeners.push(
this.rdnaEmitter.addListener(
'getIDVSelfieConfirmation',
this.onGetSelfieConfirmation.bind(this)
)
);
// Biometric Consent Events
this.listeners.push(
this.rdnaEmitter.addListener(
'getIDVBiometricOptInConsent',
this.onGetBiometricOptInConsent.bind(this)
)
);
console.log('RdnaIDVEventManager - All native event listeners registered');
}
// ================================================================
// EVENT HANDLER IMPLEMENTATIONS
// ================================================================
private onGetDocumentScanStartConfirmation(response: RDNAJsonResponse) {
console.log('RdnaIDVEventManager - Document scan start confirmation event received');
try {
const data: RDNAIDVGetDocumentScanStartConfirmationData = JSON.parse(response.response);
console.log('IDV Workflow:', data.idvWorkflow);
if (this.getDocumentScanStartConfirmationHandler) {
this.getDocumentScanStartConfirmationHandler(data);
}
} catch (error) {
console.error('RdnaIDVEventManager - Failed to parse document scan confirmation:', error);
}
}
private onGetConfirmDocumentDetails(response: RDNAJsonResponse) {
console.log('RdnaIDVEventManager - Document details confirmation event received');
try {
const data: RDNAIDVGetConfirmDocumentDetailsData = JSON.parse(response.response);
console.log('Challenge Mode:', data.challengeMode);
if (this.getConfirmDocumentDetailsHandler) {
this.getConfirmDocumentDetailsHandler(data);
}
} catch (error) {
console.error('RdnaIDVEventManager - Failed to parse document details:', error);
}
}
private onGetSelfieProcessStartConfirmation(response: RDNAJsonResponse) {
console.log('RdnaIDVEventManager - Selfie process start confirmation event received');
try {
const data: RDNAIDVGetSelfieProcessStartConfirmationData = JSON.parse(response.response);
console.log('IDV Workflow:', data.idvWorkflow);
if (this.getSelfieProcessStartConfirmationHandler) {
this.getSelfieProcessStartConfirmationHandler(data);
}
} catch (error) {
console.error('RdnaIDVEventManager - Failed to parse selfie confirmation:', error);
}
}
private onGetSelfieConfirmation(response: RDNAJsonResponse) {
console.log('RdnaIDVEventManager - Selfie confirmation event received');
try {
const data: RDNAIDVGetSelfieConfirmationData = JSON.parse(response.response);
console.log('Challenge Mode:', data.challengeMode);
console.log('User ID:', data.userID);
if (this.getSelfieConfirmationHandler) {
this.getSelfieConfirmationHandler(data);
}
} catch (error) {
console.error('RdnaIDVEventManager - Failed to parse selfie confirmation:', error);
}
}
private onGetBiometricOptInConsent(response: RDNAJsonResponse) {
console.log('RdnaIDVEventManager - Biometric consent event received');
try {
const data: RDNAIDVGetBiometricOptInConsentData = JSON.parse(response.response);
console.log('Challenge Mode:', data.challengeMode);
console.log('User ID:', data.userID);
if (this.getBiometricOptInConsentHandler) {
this.getBiometricOptInConsentHandler(data);
}
} catch (error) {
console.error('RdnaIDVEventManager - Failed to parse biometric consent:', error);
}
}
// ================================================================
// PUBLIC API - Handler Setters
// ================================================================
public setGetDocumentScanStartConfirmationHandler(
callback?: RDNAIDVGetDocumentScanStartConfirmationCallback
): void {
this.getDocumentScanStartConfirmationHandler = callback;
}
public setGetConfirmDocumentDetailsHandler(
callback?: RDNAIDVGetConfirmDocumentDetailsCallback
): void {
this.getConfirmDocumentDetailsHandler = callback;
}
public setGetSelfieProcessStartConfirmationHandler(
callback?: RDNAIDVGetSelfieProcessStartConfirmationCallback
): void {
this.getSelfieProcessStartConfirmationHandler = callback;
}
public setGetSelfieConfirmationHandler(
callback?: RDNAIDVGetSelfieConfirmationCallback
): void {
this.getSelfieConfirmationHandler = callback;
}
public setGetBiometricOptInConsentHandler(
callback?: RDNAIDVGetBiometricOptInConsentCallback
): void {
this.getBiometricOptInConsentHandler = callback;
}
// ================================================================
// CLEANUP
// ================================================================
public cleanup() {
console.log('RdnaIDVEventManager - Cleaning up event listeners');
// Remove native event listeners
this.listeners.forEach(listener => {
if (listener && listener.remove) {
listener.remove();
}
});
this.listeners = [];
// Clear all handlers
this.getDocumentScanStartConfirmationHandler = undefined;
this.getConfirmDocumentDetailsHandler = undefined;
this.getSelfieProcessStartConfirmationHandler = undefined;
this.getSelfieConfirmationHandler = undefined;
this.getBiometricOptInConsentHandler = undefined;
console.log('RdnaIDVEventManager - Cleanup completed');
}
}
export default RdnaIDVEventManager;
private static instance = new RdnaIDVEventManager(); // Created on module load
private constructor() {
this.registerEventListeners(); // Auto-registers immediately
}
static getInstance() {
return RdnaIDVEventManager.instance;
}
// One handler per event type (simple, not array-based)
private getDocumentScanStartConfirmationHandler?: Callback;
private getConfirmDocumentDetailsHandler?: Callback;
// ... etc for all 5 events
// External code calls this to register handlers
public setGetDocumentScanStartConfirmationHandler(callback?: Callback): void {
this.getDocumentScanStartConfirmationHandler = callback;
}
// When event fires, manager calls the registered callback
private onGetDocumentScanStartConfirmation(response) {
if (this.getDocumentScanStartConfirmationHandler) {
this.getDocumentScanStartConfirmationHandler(parsedData);
}
}
The Type Definitions file provides complete TypeScript interfaces for all IDV events. It uses reusable common types from the base RDNA events to ensure consistency across MFA and IDV.
src/uniken/types/idv/rdnaIDVEvents.ts
RDNAError, RDNAChallengeResponse from ../rdnaEventsRDNAIDVWorkflow and RDNAChallengeOpMode from SDKCreate the file at src/uniken/types/idv/rdnaIDVEvents.ts:
import { RDNAIDVWorkflow, RDNAChallengeOpMode } from 'react-native-rdna-client/src/index';
import type { RDNAError, RDNAChallengeResponse } from '../rdnaEvents';
// ================================================================
// DOCUMENT SCAN EVENT TYPES
// ================================================================
/**
* Get IDV Document Scan Process Start Confirmation Event Data
*
* Triggered before document scan initiation.
* Response: Call setIDVDocumentScanProcessStartConfirmation(isConfirm, idvWorkflow)
*/
export interface RDNAIDVGetDocumentScanStartConfirmationData {
idvWorkflow: RDNAIDVWorkflow; // For activation: 0 (RDNAIDVWorkflowActivation)
}
/**
* Get IDV Confirm Document Details Event Data
*
* Triggered after document scan with OCR-extracted data.
* Response: Call setIDVConfirmDocumentDetails(isConfirm, challengeMode)
*/
export interface RDNAIDVGetConfirmDocumentDetailsData {
challengeMode: RDNAChallengeOpMode; // For activation: 8
response_data: Record<string, any>; // Dynamic OCR structure (varies by document type)
}
// ================================================================
// SELFIE EVENT TYPES
// ================================================================
/**
* Get IDV Selfie Process Start Confirmation Event Data
*
* Triggered before selfie capture.
* Response: Call setIDVSelfieProcessStartConfirmation(isConfirm, useBackCamera, idvWorkflow)
*/
export interface RDNAIDVGetSelfieProcessStartConfirmationData {
idvWorkflow: RDNAIDVWorkflow; // For activation: 0
}
/**
* Get IDV Selfie Confirmation Event Data
*
* Triggered after selfie capture with biometric analysis results.
* Response: Call setIDVSelfieConfirmation(action, challengeMode)
*/
export interface RDNAIDVGetSelfieConfirmationData {
userID: string;
challengeMode: RDNAChallengeOpMode; // For activation: 8
response_data: {
response_data: {
// Biometric match result
biometric_result: {
display_text: string; // "MATCHED" or "NOT_MATCHED"
result_criteria: string;
};
// Face matching score
face_matcher_response: {
score: number; // 0-1 range (e.g., 0.95 = 95% match)
};
// Liveness detection results
analyze_liveness_response: {
video: {
liveness_result: {
score: number; // 0-100 range (e.g., 92 = 92% liveness)
};
autocapture_result: {
selfie_image: string; // Base64 encoded selfie
};
};
};
// Document photo for comparison
document_detail: {
document_info: {
portrait_image: string; // Base64 encoded document photo
};
};
};
// Dynamic action buttons based on match result
action_buttons: {
success_button: Array<{ button_text: string; key: string }>;
failure_button: Array<{ button_text: string; key: string }>;
};
};
}
// ================================================================
// BIOMETRIC CONSENT EVENT TYPES
// ================================================================
/**
* Get IDV Biometric Opt-In Consent Event Data
*
* Triggered to request user consent for biometric template storage.
* Response: Call setIDVBiometricOptInConsent(isOptIn, challengeMode)
*/
export interface RDNAIDVGetBiometricOptInConsentData {
userID: string;
attemptsLeft: number;
challengeMode: RDNAChallengeOpMode;
idvResponse: string;
challengeResponse: RDNAChallengeResponse; // ← Reusable common type
error: RDNAError; // ← Reusable common type
biometricTemplate?: {
templateData?: string;
templateType: string;
};
}
// ================================================================
// CALLBACK TYPES
// ================================================================
export type RDNAIDVGetDocumentScanStartConfirmationCallback =
(data: RDNAIDVGetDocumentScanStartConfirmationData) => void;
export type RDNAIDVGetConfirmDocumentDetailsCallback =
(data: RDNAIDVGetConfirmDocumentDetailsData) => void;
export type RDNAIDVGetSelfieProcessStartConfirmationCallback =
(data: RDNAIDVGetSelfieProcessStartConfirmationData) => void;
export type RDNAIDVGetSelfieConfirmationCallback =
(data: RDNAIDVGetSelfieConfirmationData) => void;
export type RDNAIDVGetBiometricOptInConsentCallback =
(data: RDNAIDVGetBiometricOptInConsentData) => void;
// Re-export SDK types for convenience
export type { RDNAIDVWorkflow, RDNAChallengeOpMode };
Each event has a dedicated interface documenting:
challengeMode: 8)Callback types ensure type safety when registering event handlers:
// Type-safe callback signature
type RDNAIDVGetDocumentScanStartConfirmationCallback =
(data: RDNAIDVGetDocumentScanStartConfirmationData) => void;
// Usage in Event Manager
public setGetDocumentScanStartConfirmationHandler(
callback?: RDNAIDVGetDocumentScanStartConfirmationCallback
): void {
this.handler = callback; // ← TypeScript validates this
}
SDK-provided types are used directly:
import { RDNAIDVWorkflow, RDNAChallengeOpMode } from 'react-native-rdna-client';
// Used in interfaces
export interface RDNAIDVGetDocumentScanStartConfirmationData {
idvWorkflow: RDNAIDVWorkflow; // ← SDK enum type
}
The IDV functionality is implemented using a singleton service pattern that wraps the native RdnaClient IDV APIs with type-safe, Promise-based interfaces.
Create the IDV service layer at src/uniken/services/idv/rdnaIDVService.ts:
import RdnaClient, { RDNAChallengeOpMode } from 'react-native-rdna-client/src/index';
import RdnaIDVEventManager from './rdnaIDVEventManager';
import type { RDNASyncResponse } from '../../types/rdnaEvents';
import type { RDNAIDVWorkflow } from '../../types/idv/rdnaIDVEvents';
/**
* REL-ID IDV SDK Service
*
* Provides singleton service for Identity Verification (IDV) operations.
* Wraps native RdnaClient IDV API with type-safe, Promise-based interface.
*
* IDV APIs:
* - setIDVDocumentScanProcessStartConfirmation: Start/cancel document scan
* - setIDVConfirmDocumentDetails: Confirm/reject document OCR data
* - setIDVSelfieProcessStartConfirmation: Start/cancel selfie capture
* - setIDVSelfieConfirmation: Confirm/reject selfie biometric results
* - setIDVBiometricOptInConsent: User consent for biometric template storage
*
* All methods follow sync+async callback pattern:
* 1. Synchronous response validates request acceptance (error.longErrorCode === 0)
* 2. Asynchronous events deliver results through event handlers
* 3. Promise resolves/rejects based on sync response validation
*/
export class RdnaIDVService {
private static instance: RdnaIDVService = new RdnaIDVService();
private eventManager: RdnaIDVEventManager;
private constructor() {
console.log('RdnaIDVService - Initializing service');
this.eventManager = RdnaIDVEventManager.getInstance();
}
static getInstance(): RdnaIDVService {
return RdnaIDVService.instance;
}
getEventManager(): RdnaIDVEventManager {
return this.eventManager;
}
// =================================================================
// DOCUMENT SCAN APIs
// =================================================================
/**
* Sets document scan process start confirmation
*
* Response to getIDVDocumentScanProcessStartConfirmation event.
* Initiates or cancels document scan process during IDV workflows.
*
* @param isConfirm true to start scan, false to cancel IDV flow
* @param idvWorkflow IDV workflow enum (0 for activation)
* @returns Promise<RDNASyncResponse>
*/
async setIDVDocumentScanProcessStartConfirmation(
isConfirm: boolean,
idvWorkflow: RDNAIDVWorkflow
): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
console.log('RdnaIDVService - Setting document scan confirmation:', {
isConfirm,
idvWorkflow,
});
RdnaClient.setIDVDocumentScanProcessStartConfirmation(
isConfirm,
idvWorkflow,
response => {
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaIDVService - Document scan confirmation success');
resolve(result);
} else {
console.error('RdnaIDVService - Document scan confirmation error:', result);
reject(result);
}
}
);
});
}
/**
* Sets document details confirmation
*
* Response to getIDVConfirmDocumentDetails event.
* Confirms or rejects OCR-extracted document information.
*
* @param isConfirm true to confirm details, false to reject/rescan
* @param challengeMode Challenge operation mode from event (8)
* @returns Promise<RDNASyncResponse>
*/
async setIDVConfirmDocumentDetails(
isConfirm: boolean,
challengeMode: RDNAChallengeOpMode
): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
console.log('RdnaIDVService - Setting confirm document details:', {
isConfirm,
challengeMode,
});
RdnaClient.setIDVConfirmDocumentDetails(
isConfirm,
challengeMode,
response => {
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaIDVService - Document details confirmation success');
resolve(result);
} else {
console.error('RdnaIDVService - Document details confirmation error:', result);
reject(result);
}
}
);
});
}
// =================================================================
// SELFIE CAPTURE APIs
// =================================================================
/**
* Sets selfie process start confirmation
*
* Response to getIDVSelfieProcessStartConfirmation event.
* Initiates or cancels selfie capture process.
*
* @param isConfirm true to start capture, false to cancel
* @param useDeviceBackCamera true for back camera, false for front (default: false)
* @param idvWorkflow IDV workflow enum (0 for activation)
* @returns Promise<RDNASyncResponse>
*/
async setIDVSelfieProcessStartConfirmation(
isConfirm: boolean,
useDeviceBackCamera: boolean,
idvWorkflow: RDNAIDVWorkflow
): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
console.log('RdnaIDVService - Setting selfie process start confirmation:', {
isConfirm,
useDeviceBackCamera,
idvWorkflow,
});
RdnaClient.setIDVSelfieProcessStartConfirmation(
isConfirm,
useDeviceBackCamera,
idvWorkflow,
response => {
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaIDVService - Selfie process start confirmation success');
resolve(result);
} else {
console.error('RdnaIDVService - Selfie process start confirmation error:', result);
reject(result);
}
}
);
});
}
/**
* Sets selfie confirmation
*
* Response to getIDVSelfieConfirmation event.
* Confirms or rejects captured selfie and biometric analysis results.
*
* @param action User confirmation action ("true" for accept, "false" for reject)
* @param challengeMode Challenge mode from event (8)
* @returns Promise<RDNASyncResponse>
*/
async setIDVSelfieConfirmation(
action: string,
challengeMode: RDNAChallengeOpMode
): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
console.log('RdnaIDVService - Setting selfie confirmation:', {
action,
challengeMode,
});
RdnaClient.setIDVSelfieConfirmation(
action,
challengeMode,
response => {
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaIDVService - Selfie confirmation success');
resolve(result);
} else {
console.error('RdnaIDVService - Selfie confirmation error:', result);
reject(result);
}
}
);
});
}
// =================================================================
// BIOMETRIC CONSENT APIs
// =================================================================
/**
* Sets biometric opt-in consent
*
* Response to getIDVBiometricOptInConsent event.
* Submits user's consent for biometric template storage.
*
* @param isOptIn true to allow storage, false to deny
* @param challengeMode Challenge mode from event (8)
* @returns Promise<RDNASyncResponse>
*/
async setIDVBiometricOptInConsent(
isOptIn: boolean,
challengeMode: RDNAChallengeOpMode
): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
console.log('RdnaIDVService - Setting biometric opt-in consent:', {
isOptIn,
challengeMode,
});
RdnaClient.setIDVBiometricOptInConsent(
isOptIn,
challengeMode,
response => {
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaIDVService - Biometric consent success');
resolve(result);
} else {
console.error('RdnaIDVService - Biometric consent error:', result);
reject(result);
}
}
);
});
}
cleanup(): void {
console.log('RdnaIDVService - Cleaning up service');
this.eventManager.cleanup();
}
}
export default RdnaIDVService.getInstance();
error.longErrorCode === 0 for success validationThe IDV Event Provider manages all IDV event subscriptions and navigation logic. Create src/uniken/providers/idv/SDKIDVEventProvider.tsx:
import React, { createContext, useContext, useEffect, useCallback, ReactNode, useState } from 'react';
import rdnaIDVService from '../../services/idv/rdnaIDVService';
import NavigationService from '../../../tutorial/navigation/NavigationService';
import type {
RDNAIDVGetDocumentScanStartConfirmationData,
RDNAIDVGetConfirmDocumentDetailsData,
RDNAIDVGetSelfieProcessStartConfirmationData,
RDNAIDVGetSelfieConfirmationData,
RDNAIDVGetBiometricOptInConsentData,
RDNAIDVWorkflow,
} from '../../types/idv/rdnaIDVEvents';
/**
* IDV Event Context Interface
*/
interface SDKIDVEventContextType {
currentIDVWorkflow: RDNAIDVWorkflow | null;
clearIDVState: () => void;
}
const SDKIDVEventContext = createContext<SDKIDVEventContextType | undefined>(undefined);
export const useIDVEvents = (): SDKIDVEventContextType => {
const context = useContext(SDKIDVEventContext);
if (!context) {
throw new Error('useIDVEvents must be used within SDKIDVEventProvider');
}
return context;
};
interface SDKIDVEventProviderProps {
children: ReactNode;
}
export const SDKIDVEventProvider: React.FC<SDKIDVEventProviderProps> = ({ children }) => {
const [currentIDVWorkflow, setCurrentIDVWorkflow] = useState<RDNAIDVWorkflow | null>(null);
// =================================================================
// DOCUMENT SCAN EVENT HANDLERS
// =================================================================
/**
* Handles getIDVDocumentScanProcessStartConfirmation event
* Navigates to document scan start screen
*/
const handleGetDocumentScanStartConfirmation = useCallback(
(data: RDNAIDVGetDocumentScanStartConfirmationData) => {
console.log('SDKIDVEventProvider - Document scan start confirmation requested');
console.log('IDV Workflow:', data.idvWorkflow);
setCurrentIDVWorkflow(data.idvWorkflow);
NavigationService.navigateOrUpdate('IDVDocumentScanStart', {
idvWorkflow: data.idvWorkflow,
eventData: data,
});
},
[]
);
/**
* Handles getIDVConfirmDocumentDetails event
* Navigates to confirm document details screen
*/
const handleGetConfirmDocumentDetails = useCallback(
(data: RDNAIDVGetConfirmDocumentDetailsData) => {
console.log('SDKIDVEventProvider - Document details confirmation requested');
console.log('Challenge Mode:', data.challengeMode);
NavigationService.navigateOrUpdate('IDVConfirmDocumentDetails', {
challengeMode: data.challengeMode,
eventData: data,
});
},
[]
);
// =================================================================
// SELFIE CAPTURE EVENT HANDLERS
// =================================================================
/**
* Handles getIDVSelfieProcessStartConfirmation event
* Navigates to selfie process start screen
*/
const handleGetSelfieProcessStartConfirmation = useCallback(
(data: RDNAIDVGetSelfieProcessStartConfirmationData) => {
console.log('SDKIDVEventProvider - Selfie process start confirmation requested');
console.log('IDV Workflow:', data.idvWorkflow);
NavigationService.navigateOrUpdate('IDVSelfieProcessStart', {
idvWorkflow: data.idvWorkflow,
eventData: data,
});
},
[]
);
/**
* Handles getIDVSelfieConfirmation event
* Navigates to selfie confirmation screen
*/
const handleGetSelfieConfirmation = useCallback(
(data: RDNAIDVGetSelfieConfirmationData) => {
console.log('SDKIDVEventProvider - Selfie confirmation requested');
console.log('Challenge Mode:', data.challengeMode);
NavigationService.navigateOrUpdate('IDVSelfieConfirmation', {
challengeMode: data.challengeMode,
eventData: data,
});
},
[]
);
// =================================================================
// BIOMETRIC CONSENT EVENT HANDLERS
// =================================================================
/**
* Handles getIDVBiometricOptInConsent event
* Navigates to biometric consent screen
*/
const handleGetBiometricOptInConsent = useCallback(
(data: RDNAIDVGetBiometricOptInConsentData) => {
console.log('SDKIDVEventProvider - Biometric opt-in consent requested');
console.log('Challenge Mode:', data.challengeMode);
NavigationService.navigateOrUpdate('IDVBiometricOptInConsent', {
challengeMode: data.challengeMode,
eventData: data,
});
},
[]
);
// =================================================================
// HELPER FUNCTIONS
// =================================================================
const clearIDVState = useCallback(() => {
console.log('SDKIDVEventProvider - Clearing IDV state');
setCurrentIDVWorkflow(null);
}, []);
// =================================================================
// EVENT HANDLER REGISTRATION
// =================================================================
useEffect(() => {
console.log('SDKIDVEventProvider - Registering IDV event handlers');
const eventManager = rdnaIDVService.getEventManager();
// Register all event handlers
eventManager.setGetDocumentScanStartConfirmationHandler(handleGetDocumentScanStartConfirmation);
eventManager.setGetConfirmDocumentDetailsHandler(handleGetConfirmDocumentDetails);
eventManager.setGetSelfieProcessStartConfirmationHandler(handleGetSelfieProcessStartConfirmation);
eventManager.setGetSelfieConfirmationHandler(handleGetSelfieConfirmation);
eventManager.setGetBiometricOptInConsentHandler(handleGetBiometricOptInConsent);
// Cleanup
return () => {
console.log('SDKIDVEventProvider - Cleaning up IDV event handlers');
eventManager.setGetDocumentScanStartConfirmationHandler(undefined);
eventManager.setGetConfirmDocumentDetailsHandler(undefined);
eventManager.setGetSelfieProcessStartConfirmationHandler(undefined);
eventManager.setGetSelfieConfirmationHandler(undefined);
eventManager.setGetBiometricOptInConsentHandler(undefined);
};
}, [
handleGetDocumentScanStartConfirmation,
handleGetConfirmDocumentDetails,
handleGetSelfieProcessStartConfirmation,
handleGetSelfieConfirmation,
handleGetBiometricOptInConsent
]);
const contextValue: SDKIDVEventContextType = {
currentIDVWorkflow,
clearIDVState,
};
return (
<SDKIDVEventContext.Provider value={contextValue}>
{children}
</SDKIDVEventContext.Provider>
);
};
export default SDKIDVEventProvider;
In your App.tsx, nest the providers correctly:
import { SDKEventProvider } from './src/uniken/providers/SDKEventProvider';
import { SDKIDVEventProvider } from './src/uniken/providers/idv/SDKIDVEventProvider';
const App = () => {
return (
<SDKEventProvider>
<SDKIDVEventProvider>
<AppNavigator />
</SDKIDVEventProvider>
</SDKEventProvider>
);
};
After the activation code is validated, the SDK triggers the getIDVDocumentScanProcessStartConfirmation event with:
{
idvWorkflow: 0, // RDNAIDVWorkflowActivation
// Additional event data
}
The event handler (in SDKIDVEventProvider) automatically navigates to IDVDocumentScanStart screen.
Create src/tutorial/screens/idv/IDVDocumentProcessStartConfirmationScreen.tsx:
import React, { useState } from 'react';
import { View, Text, StyleSheet, SafeAreaView, ScrollView } from 'react-native';
import { useRoute } from '@react-navigation/native';
import rdnaIDVService from '../../../uniken/services/idv/rdnaIDVService';
import { CloseButton, Button, StatusBanner } from '../components';
import type { RDNAIDVWorkflow } from '../../../uniken/types/idv/rdnaIDVEvents';
const IDVDocumentProcessStartConfirmationScreen: React.FC = () => {
const route = useRoute();
const { idvWorkflow, eventData } = route.params || {};
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState('');
/**
* Handle Scan Document Button
* Calls setIDVDocumentScanProcessStartConfirmation with isConfirm=true
*/
const handleScanDocument = async () => {
if (!idvWorkflow && idvWorkflow !== 0) {
setError('Invalid workflow. Unable to start document scan.');
return;
}
setIsProcessing(true);
setError('');
try {
console.log('Starting document scan with idvWorkflow:', idvWorkflow);
// Call API to start document scan
await rdnaIDVService.setIDVDocumentScanProcessStartConfirmation(
true, // isConfirm = true to start
idvWorkflow as RDNAIDVWorkflow
);
console.log('Document scan started successfully');
// SDK opens native document scanner
// Results handled by event provider
} catch (error) {
console.error('Failed to start document scan:', error);
const errorMessage = RDNASyncUtils.getErrorMessage(error);
setError(errorMessage);
} finally {
setIsProcessing(false);
}
};
/**
* Handle Close Button
* Calls setIDVDocumentScanProcessStartConfirmation with isConfirm=false
*/
const handleClose = async () => {
setIsProcessing(true);
setError('');
try {
await rdnaIDVService.setIDVDocumentScanProcessStartConfirmation(
false, // isConfirm = false to cancel
idvWorkflow as RDNAIDVWorkflow
);
console.log('Document scan cancelled');
} catch (error) {
console.error('Failed to cancel document scan:', error);
setError(RDNASyncUtils.getErrorMessage(error));
} finally {
setIsProcessing(false);
}
};
return (
<SafeAreaView style={styles.safeArea}>
<CloseButton onPress={handleClose} disabled={isProcessing} />
<ScrollView style={styles.scrollView}>
<View style={styles.content}>
<Text style={styles.title}>Document Scan</Text>
<Text style={styles.subtitle}>Prepare to scan your identity document</Text>
{/* Document Icon */}
<View style={styles.iconContainer}>
<Text style={styles.documentIcon}>📋</Text>
</View>
{error && <StatusBanner type="error" message={error} />}
{/* Guidelines */}
<View style={styles.guidelinesContainer}>
<Text style={styles.guidelinesTitle}>Scanning Guidelines</Text>
<Text style={styles.guidelineText}>
• Position document within frame with all corners visible{'\n'}
• Avoid shadows, glare, and blurred images{'\n'}
• Ensure document is flat and text is readable{'\n'}
• Use good lighting for best results
</Text>
</View>
<Button
title={isProcessing ? 'Starting Scan...' : 'Scan Document'}
onPress={handleScanDocument}
loading={isProcessing}
disabled={isProcessing}
/>
<View style={styles.helpContainer}>
<Text style={styles.helpText}>
The document scanner will guide you through the process.
Follow the on-screen instructions for best results.
</Text>
</View>
</View>
</ScrollView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
safeArea: { flex: 1, backgroundColor: '#f8f9fa' },
scrollView: { flex: 1 },
content: { flex: 1, padding: 20, paddingTop: 80 },
title: { fontSize: 28, fontWeight: 'bold', color: '#2c3e50', textAlign: 'center', marginBottom: 8 },
subtitle: { fontSize: 16, color: '#7f8c8d', textAlign: 'center', marginBottom: 32 },
iconContainer: { alignItems: 'center', marginBottom: 32 },
documentIcon: { fontSize: 80 },
guidelinesContainer: { backgroundColor: '#fff', borderRadius: 12, padding: 20, marginBottom: 24 },
guidelinesTitle: { fontSize: 18, fontWeight: 'bold', marginBottom: 12 },
guidelineText: { fontSize: 15, lineHeight: 24, color: '#34495e' },
helpContainer: { marginTop: 20, padding: 16, backgroundColor: '#ecf0f1', borderRadius: 8 },
helpText: { fontSize: 14, color: '#7f8c8d', textAlign: 'center', lineHeight: 20 },
});
export default IDVDocumentProcessStartConfirmationScreen;
The following image showcases screen from the sample application:

When setIDVDocumentScanProcessStartConfirmation(true, 0) is called:
getIDVConfirmDocumentDetails event with extracted dataWhen implementing the Document Scan Start screen, handle the following error code:
Error Code | Message | Handling Strategy |
146 | "IDV identity verification canceled by user" | User clicked close/cancel button. This is expected user behavior. Show cancellation message or navigate back to previous screen. |
0 (RDNAIDVWorkflowActivation) from event datafalse cancels IDV and returns to previous workflowAfter document scan completion, SDK triggers event with OCR-extracted document data:
{
challengeMode: 8, // For device activation
response_data: {
version: "3.0",
document_info: {
// Document metadata, type codes, locale, images (front, back, signature, portrait)
document_type_code: [/* Internal numeric codes */],
locale: "en_US",
front_side_image: "base64_encoded_image",
// ... more document metadata
},
document_status: {
overall_document_status: "OK", // or "WARNING" or "ERROR"
error_list: [/* Array of validation errors */],
warning_list: [/* Array of warnings */],
checks_performed: [/* Validation results across categories */]
},
identity_data: {
// Simplified key-value pairs of extracted information
// name, date_of_birth, document_number, expiry_date, etc.
},
identity_data_details: {
// Locale-specific extraction data with per-field sources and statuses
// field_sources: M (MRZ), B (Barcode), V (Visual)
}
}
}
Complete OCR Data Structure: The response structure is complex and varies by document type. For the full response structure with all fields, validation statuses, field sources, and detailed examples, refer to:
📄 Sample OCR Data v3.0 JSON Documentation
When implementing the document confirmation screen, focus on these critical fields:
identity_data: Extracted information (name, DOB, document number, etc.) - display for user reviewdocument_status.overall_document_status: Overall validation result ("OK", "WARNING", or "ERROR")document_status.error_list: Array of validation errors - display to user if presentdocument_status.warning_list: Array of warnings - display to user if presentdocument_status.checks_performed: Detailed validation results (Data Validation, Document Authenticity, etc.)Create src/tutorial/screens/idv/IDVConfirmDocumentDetailsScreen.tsx:
import React, { useState } from 'react';
import { View, Text, Image, ScrollView, SafeAreaView } from 'react-native';
import { useRoute } from '@react-navigation/native';
import rdnaIDVService from '../../../uniken/services/idv/rdnaIDVService';
import { CloseButton, Button, StatusBanner } from '../components';
import type { RDNAChallengeOpMode } from '../../../uniken/types/idv/rdnaIDVEvents';
const IDVConfirmDocumentDetailsScreen: React.FC = () => {
const route = useRoute();
const { challengeMode, eventData } = route.params || {};
const responseData = eventData?.response_data;
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState('');
/**
* Handle Confirm Button
* Calls setIDVConfirmDocumentDetails with isConfirm=true
*/
const handleConfirm = async () => {
if (!challengeMode && challengeMode !== 0) {
setError('Invalid challenge mode');
return;
}
setIsProcessing(true);
setError('');
try {
console.log('Confirming document details');
await rdnaIDVService.setIDVConfirmDocumentDetails(
true, // isConfirm = true
challengeMode as RDNAChallengeOpMode
);
console.log('Document details confirmed');
// SDK continues to selfie capture
} catch (error) {
console.error('Failed to confirm document:', error);
setError(RDNASyncUtils.getErrorMessage(error));
} finally {
setIsProcessing(false);
}
};
/**
* Handle Rescan Button
* Calls setIDVConfirmDocumentDetails with isConfirm=false
*/
const handleRescan = async () => {
setIsProcessing(true);
setError('');
try {
await rdnaIDVService.setIDVConfirmDocumentDetails(
false, // isConfirm = false to rescan
challengeMode as RDNAChallengeOpMode
);
console.log('Document rescan requested');
} catch (error) {
console.error('Failed to request rescan:', error);
setError(RDNASyncUtils.getErrorMessage(error));
} finally {
setIsProcessing(false);
}
};
// Extract document fields
const documentInfo = responseData?.document_detail?.document_info;
const validationStatus = responseData?.validation_status;
/**
* Render document field with validation status
*/
const renderField = (label: string, value: any, fieldKey: string) => {
if (!value) return null;
const status = validationStatus?.[fieldKey] || 'OK';
const statusColor =
status === 'OK' ? '#27ae60' :
status === 'WARNING' ? '#f39c12' : '#e74c3c';
return (
<View style={styles.fieldRow}>
<View style={styles.fieldInfo}>
<Text style={styles.fieldLabel}>{label}</Text>
<Text style={styles.fieldValue}>{value}</Text>
</View>
<View style={[styles.statusBadge, { backgroundColor: statusColor }]}>
<Text style={styles.statusText}>{status}</Text>
</View>
</View>
);
};
return (
<SafeAreaView style={styles.safeArea}>
<CloseButton onPress={handleClose} disabled={isProcessing} />
<ScrollView style={styles.scrollView}>
<View style={styles.content}>
<Text style={styles.title}>Confirm Document Details</Text>
<Text style={styles.subtitle}>Review extracted information</Text>
{/* Document Photo */}
{documentInfo?.portrait_image && (
<Image
source={{ uri: `data:image/jpg;base64,${documentInfo.portrait_image}` }}
style={styles.documentImage}
/>
)}
{error && <StatusBanner type="error" message={error} />}
{/* Document Fields */}
<View style={styles.fieldsContainer}>
{renderField('Full Name', documentInfo?.name, 'name')}
{renderField('Document Number', documentInfo?.document_number, 'document_number')}
{renderField('Date of Birth', documentInfo?.date_of_birth, 'date_of_birth')}
{renderField('Expiry Date', documentInfo?.expiry_date, 'expiry_date')}
{renderField('Nationality', documentInfo?.nationality, 'nationality')}
</View>
{/* Action Buttons */}
<View style={styles.buttonContainer}>
<Button
title={isProcessing ? 'Processing...' : 'Rescan'}
onPress={handleRescan}
variant="secondary"
disabled={isProcessing}
style={styles.button}
/>
<Button
title={isProcessing ? 'Processing...' : 'Confirm'}
onPress={handleConfirm}
disabled={isProcessing}
style={styles.button}
/>
</View>
</View>
</ScrollView>
</SafeAreaView>
);
};
export default IDVConfirmDocumentDetailsScreen;
The following images showcase different states of the screen from the sample application:
Document Details Screen |
Document Details Screen |
Status | Description | User Action |
OK | Field extracted and validated successfully | Can proceed |
WARNING | Field extracted but may have minor issues | Review and confirm or rescan |
ERROR | Field failed validation or extraction | Should rescan for better results |
After document confirmation, SDK triggers:
{
idvWorkflow: 0, // RDNAIDVWorkflowActivation
// Additional event data
}
Create src/tutorial/screens/idv/IDVSelfieProcessStartConfirmationScreen.tsx:
import React, { useState } from 'react';
import { View, Text, ScrollView, SafeAreaView } from 'react-native';
import { useRoute } from '@react-navigation/native';
import rdnaIDVService from '../../../uniken/services/idv/rdnaIDVService';
import { CloseButton, Button, StatusBanner } from '../components';
const IDVSelfieProcessStartConfirmationScreen: React.FC = () => {
const route = useRoute();
const { idvWorkflow, eventData } = route.params || {};
const [isProcessing, setIsProcessing] = useState(false);
const [useFrontCamera, setUseFrontCamera] = useState(true);
const [error, setError] = useState('');
/**
* Handle Capture Selfie Button
* Calls setIDVSelfieProcessStartConfirmation with camera selection
*/
const handleCaptureSelfie = async () => {
setIsProcessing(true);
setError('');
try {
console.log('Starting selfie capture with front camera:', useFrontCamera);
await rdnaIDVService.setIDVSelfieProcessStartConfirmation(
true, // isConfirm = true
!useFrontCamera, // useDeviceBackCamera (inverted)
idvWorkflow
);
console.log('Selfie capture started');
// SDK opens native selfie camera with liveness detection
} catch (error) {
console.error('Failed to start selfie capture:', error);
setError(RDNASyncUtils.getErrorMessage(error));
} finally {
setIsProcessing(false);
}
};
return (
<SafeAreaView style={styles.safeArea}>
<CloseButton onPress={handleClose} disabled={isProcessing} />
<ScrollView style={styles.scrollView}>
<View style={styles.content}>
<Text style={styles.title}>Selfie Capture</Text>
<Text style={styles.subtitle}>Prepare to capture your selfie</Text>
{/* Selfie Icon */}
<View style={styles.iconContainer}>
<Text style={styles.selfieIcon}>🤳</Text>
</View>
{error && <StatusBanner type="error" message={error} />}
{/* Camera Selection */}
<View style={styles.cameraSelectionContainer}>
<Text style={styles.cameraLabel}>Select Camera:</Text>
<View style={styles.cameraButtons}>
<Button
title="Front Camera"
onPress={() => setUseFrontCamera(true)}
variant={useFrontCamera ? 'primary' : 'secondary'}
style={styles.cameraButton}
/>
<Button
title="Back Camera"
onPress={() => setUseFrontCamera(false)}
variant={!useFrontCamera ? 'primary' : 'secondary'}
style={styles.cameraButton}
/>
</View>
</View>
{/* Guidelines */}
<View style={styles.guidelinesContainer}>
<Text style={styles.guidelinesTitle}>Selfie Guidelines</Text>
<Text style={styles.guidelineText}>
• Look directly at the camera{'\n'}
• Ensure your face is well-lit and clearly visible{'\n'}
• Remove glasses, hats, or face coverings{'\n'}
• Keep a neutral expression{'\n'}
• Follow on-screen liveness detection instructions
</Text>
</View>
<Button
title={isProcessing ? 'Starting Camera...' : 'Capture Selfie'}
onPress={handleCaptureSelfie}
loading={isProcessing}
disabled={isProcessing}
/>
<View style={styles.helpContainer}>
<Text style={styles.helpText}>
The selfie capture includes liveness detection to ensure security.
Follow the on-screen instructions during capture.
</Text>
</View>
</View>
</ScrollView>
</SafeAreaView>
);
};
export default IDVSelfieProcessStartConfirmationScreen;
The following image showcases screen from the sample application:

When setIDVSelfieProcessStartConfirmation(true, false, 0) is called:
getIDVSelfieConfirmation event with resultsThe useDeviceBackCamera parameter:
false = Use front camera (pass !useFrontCamera when useFrontCamera = true)true = Use back cameraAfter selfie capture, SDK triggers event with biometric results:
{
challengeMode: 8,
response_data: {
response_data: {
// Biometric match result
biometric_result: {
display_text: "MATCHED", // or "NOT_MATCHED"
result_criteria: "FACE_MATCH",
},
// Face matching score
face_matcher_response: {
score: 0.95, // 0-1 range (95% match)
},
// Liveness detection score
analyze_liveness_response: {
video: {
liveness_result: {
score: 92, // 0-100 range (92% liveness)
},
autocapture_result: {
selfie_image: "base64_encoded_selfie"
}
}
},
// Document photo for comparison
document_detail: {
document_info: {
portrait_image: "base64_encoded_document_photo"
}
}
},
// Dynamic action buttons (server-controlled)
action_buttons: {
success_button: [
{ button_text: "Continue", key: "continue-flow" }
],
failure_button: [
{ button_text: "Continue Anyway", key: "continue-flow" },
{ button_text: "Rescan Document", key: "reinit-idv-document-scan" },
{ button_text: "Recapture Selfie", key: "reinit-idv-selfie" }
]
}
}
}
The most complex screen - displays comparison and handles dynamic buttons:
import React, { useState } from 'react';
import { View, Text, Image, ScrollView, SafeAreaView } from 'react-native';
import { useRoute } from '@react-navigation/native';
import rdnaIDVService from '../../../uniken/services/idv/rdnaIDVService';
import { CloseButton, Button, StatusBanner } from '../components';
const IDVSelfieConfirmationScreen: React.FC = () => {
const route = useRoute();
const { challengeMode, eventData } = route.params || {};
const responseData = eventData?.response_data;
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState('');
/**
* Handle action button click
* Calls setIDVSelfieConfirmation with action key
*/
const handleAction = async (actionKey: string) => {
setIsProcessing(true);
setError('');
try {
console.log('Processing selfie action:', actionKey);
await rdnaIDVService.setIDVSelfieConfirmation(
actionKey, // Action key from button: "continue-flow", "reinit-idv-document-scan", or "reinit-idv-selfie"
challengeMode
);
console.log('Selfie action processed');
} catch (error) {
console.error('Failed to process selfie action:', error);
setError(RDNASyncUtils.getErrorMessage(error));
} finally {
setIsProcessing(false);
}
};
// Extract data
const biometricResult = responseData?.response_data?.biometric_result;
const faceMatchScore = responseData?.response_data?.face_matcher_response?.score;
const livenessScore = responseData?.response_data?.analyze_liveness_response?.video?.liveness_result?.score;
const selfieImage = responseData?.response_data?.analyze_liveness_response?.video?.autocapture_result?.selfie_image;
const documentPhoto = responseData?.response_data?.document_detail?.document_info?.portrait_image;
const isMatched = biometricResult?.display_text === 'MATCHED';
return (
<SafeAreaView style={styles.safeArea}>
<CloseButton onPress={handleClose} disabled={isProcessing} />
<ScrollView style={styles.scrollView}>
<View style={styles.content}>
<Text style={styles.title}>Confirm Selfie Details</Text>
<Text style={styles.subtitle}>Review biometric match result</Text>
{/* Side-by-Side Image Comparison */}
<View style={styles.imageComparison}>
{/* Document Photo */}
<View style={styles.imageContainer}>
<Text style={styles.imageLabel}>Document Photo</Text>
{documentPhoto && (
<Image
source={{ uri: `data:image/jpg;base64,${documentPhoto}` }}
style={styles.comparisonImage}
/>
)}
</View>
{/* Selfie Photo */}
<View style={styles.imageContainer}>
<Text style={styles.imageLabel}>Selfie Photo</Text>
{selfieImage && (
<Image
source={{ uri: `data:image/jpg;base64,${selfieImage}` }}
style={styles.comparisonImage}
/>
)}
</View>
</View>
{/* Match Result */}
<View style={styles.matchResultContainer}>
<View style={styles.matchHeader}>
<Text style={[styles.matchIcon, { color: isMatched ? '#27ae60' : '#e74c3c' }]}>
{isMatched ? '✓' : '✗'}
</Text>
<View>
<Text style={[styles.matchText, { color: isMatched ? '#27ae60' : '#e74c3c' }]}>
{isMatched ? 'Selfie matches document' : 'Selfie does not match'}
</Text>
<Text style={styles.matchStatus}>Status: {biometricResult?.display_text}</Text>
</View>
</View>
{/* Scores */}
<View style={styles.scoresContainer}>
{faceMatchScore !== undefined && (
<View style={styles.scoreRow}>
<Text style={styles.scoreLabel}>Face Match Score:</Text>
<Text style={styles.scoreValue}>{(faceMatchScore * 100).toFixed(2)}%</Text>
</View>
)}
{livenessScore !== undefined && (
<View style={styles.scoreRow}>
<Text style={styles.scoreLabel}>Liveness Score:</Text>
<Text style={styles.scoreValue}>{livenessScore}%</Text>
</View>
)}
</View>
</View>
{error && <StatusBanner type="error" message={error} />}
<View style={styles.helpContainer}>
<Text style={styles.helpText}>
Compare the selfie with your document photo. The biometric analysis
verifies your identity. If the match quality is poor, you can recapture.
</Text>
</View>
</View>
</ScrollView>
{/* Dynamic Action Buttons */}
{renderActionButtons()}
</SafeAreaView>
);
/**
* Render action buttons based on match result
*/
function renderActionButtons() {
const actionButtons = responseData?.action_buttons;
if (!actionButtons) return null;
const buttons = isMatched
? actionButtons.success_button
: actionButtons.failure_button;
return (
<View style={styles.buttonContainer}>
{buttons.map((button, index) => (
<Button
key={index}
title={isProcessing ? 'Processing...' : button.button_text}
onPress={() => handleAction(button.key)}
variant={button.button_text.includes('Continue') ? 'primary' : 'secondary'}
disabled={isProcessing}
style={styles.button}
/>
))}
</View>
);
}
};
export default IDVSelfieConfirmationScreen;
The following image showcases screen from the sample application:

Action keys are string values that control the IDV workflow after selfie confirmation. They are returned in the action_buttons object and must be passed to the setIDVSelfieConfirmation API.
Action Key | SDK Behavior | When to Use |
| Proceeds to next event ( | User accepts selfie match result and wants to continue |
| Triggers document rescan (fires | User believes document scan was poor quality or incorrect |
| Initiates selfie recapture flow | User wants to retake selfie (poor lighting, closed eyes, etc.) |
The action_buttons object provides server-controlled button configurations:
When Biometric Match SUCCEEDS (display_text: "MATCHED"):
{
"success_button": [
{
"key": "continue-flow",
"button_text": "Continue"
}
]
}
Only one option: proceed to next step.
When Biometric Match FAILS (display_text: "NOT_MATCHED"):
{
"failure_button": [
{
"key": "continue-flow",
"button_text": "Continue Anyway"
},
{
"key": "reinit-idv-document-scan",
"button_text": "Rescan Document"
},
{
"key": "reinit-idv-selfie",
"button_text": "Recapture Selfie"
}
]
}
Multiple options: user can choose recovery path.
1. SDK fires getIDVSelfieConfirmation event
↓
2. Event contains biometric_result ("MATCHED" or "NOT_MATCHED")
↓
3. Event contains action_buttons (success_button or failure_button arrays)
↓
4. App displays buttons based on match result
↓
5. User clicks a button
↓
6. App calls setIDVSelfieConfirmation(button.key, challengeMode)
↓
7. SDK executes the action:
• "continue-flow" → getIDVBiometricOptInConsent event
• "reinit-idv-document-scan" → Document rescan flow
• "reinit-idv-selfie" → Selfie recapture flow
button_text allows localization and custom messagingaction_buttons missingconst renderActionButtons = () => {
const actionButtons = responseData?.action_buttons;
const biometricResult = responseData?.response_data?.biometric_result;
// Determine which button array to use based on match result
const isMatched = biometricResult?.display_text === 'MATCHED';
const buttons = isMatched
? actionButtons.success_button
: actionButtons.failure_button;
// Render buttons dynamically
return buttons.map((button) => (
<Button
title={button.button_text} // ← Server-controlled text
onPress={() => handleAction(button.key)} // ← Pass action key to API
variant={button.button_text.includes('Continue') ? 'primary' : 'secondary'}
/>
));
};
const handleAction = async (action: string) => {
// Pass the action key exactly as received from server
await rdnaIDVService.setIDVSelfieConfirmation(
action, // "continue-flow", "reinit-idv-document-scan", or "reinit-idv-selfie"
challengeMode
);
};
When implementing the Selfie Confirmation screen, handle the following error codes:
Error Code | Message | Handling Strategy |
146 | "Selfie not confirmed" | User rejected selfie or clicked cancel. Prompt for retake or allow user to return to previous screen. This is expected user behavior. |
400 | "Invalid input or state" | Invalid action key or challengeMode passed to API. Verify that action keys come from the |
After selfie confirmation, SDK requests user consent for biometric template storage:
{
userID: "john.doe",
challengeMode: 8,
challengeResponse: {
additionalInfo: {
currentWorkFlow: "IDV_ACTIVATION"
}
}
}
Create src/tutorial/screens/idv/IDVBiometricOptInConsentScreen.tsx:
import React, { useState } from 'react';
import { View, Text, ScrollView, SafeAreaView } from 'react-native';
import { useRoute } from '@react-navigation/native';
import rdnaIDVService from '../../../uniken/services/idv/rdnaIDVService';
import { CloseButton, Button, StatusBanner } from '../components';
const IDVBiometricOptInConsentScreen: React.FC = () => {
const route = useRoute();
const { challengeMode, eventData } = route.params || {};
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState('');
/**
* Handle consent action (Approve or Reject)
*/
const handleConsentAction = async (isOptIn: boolean) => {
setIsProcessing(true);
setError('');
try {
console.log('Processing biometric consent:', isOptIn);
await rdnaIDVService.setIDVBiometricOptInConsent(
isOptIn, // true = approve, false = reject
challengeMode
);
console.log('Biometric consent processed');
// SDK continues to password setup (getPassword event)
} catch (error) {
console.error('Failed to process consent:', error);
setError(RDNASyncUtils.getErrorMessage(error));
} finally {
setIsProcessing(false);
}
};
/**
* Get consent description for activation flow (challengeMode 8)
*/
const getConsentDescription = () => {
return 'Your selfie biometric template will be securely stored for identity verification. This enables faster authentication and enhanced security for your account.';
};
return (
<SafeAreaView style={styles.safeArea}>
<CloseButton onPress={handleClose} disabled={isProcessing} />
<ScrollView style={styles.scrollView}>
<View style={styles.content}>
<Text style={styles.title}>Save Template</Text>
<Text style={styles.subtitle}>Biometric Template Storage Consent</Text>
{/* Consent Description */}
<View style={styles.descriptionContainer}>
<Text style={styles.descriptionTitle}>What you need to know:</Text>
<Text style={styles.descriptionText}>{getConsentDescription()}</Text>
<View style={styles.bulletContainer}>
<Text style={styles.bulletPoint}>
• Your biometric data will be encrypted and stored securely
</Text>
<Text style={styles.bulletPoint}>
• This enables faster authentication for future sessions
</Text>
<Text style={styles.bulletPoint}>
• You can manage your consent preferences at any time
</Text>
<Text style={styles.bulletPoint}>
• Rejecting will not affect your current authentication
</Text>
</View>
</View>
{error && <StatusBanner type="error" message={error} />}
{/* Action Buttons */}
<View style={styles.buttonContainer}>
<Button
title={isProcessing ? 'Processing...' : 'Reject'}
onPress={() => handleConsentAction(false)}
variant="secondary"
disabled={isProcessing}
style={styles.button}
/>
<Button
title={isProcessing ? 'Processing...' : 'Approve'}
onPress={() => handleConsentAction(true)}
disabled={isProcessing}
style={styles.button}
/>
</View>
{/* Help Text */}
<View style={styles.helpContainer}>
<Text style={styles.helpText}>
By approving, you consent to the secure storage of your biometric
template for identity verification purposes.
</Text>
</View>
</View>
</ScrollView>
</SafeAreaView>
);
};
export default IDVBiometricOptInConsentScreen;
The following image showcases screen from the sample application:

Action | Storage | Impact |
Approve ( | Biometric template stored securely | Faster future authentication, enhanced security |
Reject ( | Template not stored | Standard authentication methods only |
Once biometric consent is processed, the SDK triggers the standard getPassword event (from MFA flow):
// SDK triggers getPassword event
{
userID: "john.doe",
// ... password event data
}
Your existing MFA password screen (from previous codelab) handles this:
// SetPasswordScreen.tsx (from MFA codelab)
const SetPasswordScreen: React.FC = () => {
// ... existing password setup implementation
const handleSetPassword = async (password: string) => {
await rdnaService.setPassword(password, challengeMode);
// SDK triggers onUserLoggedIn event after password is set
};
};
After password setup, the SDK triggers onUserLoggedIn event:
// In SDKEventProvider.tsx
const handleUserLoggedIn = useCallback((data: RDNAUserLoggedInData) => {
console.log('User logged in successfully!');
NavigationService.navigateOrUpdate('DrawerNavigator', {
screen: 'Dashboard',
params: {
userID: data.userID,
sessionID: data.sessionID,
sessionType: data.sessionType,
jwtToken: data.jwtToken,
},
});
}, []);
1. ✅ SDK Initialization
2. ✅ User Registration (CheckUserScreen)
3. ✅ Activation Code Verification
4. ✅ IDV Document Scan → Confirm
5. ✅ IDV Selfie Capture → Confirm
6. ✅ IDV Biometric Consent
7. ✅ Password Setup
8. ✅ onUserLoggedIn Event
9. ✅ Dashboard Navigation
Add IDV routes to your AppNavigator.tsx:
import { IDVDocumentProcessStartConfirmationScreen, IDVConfirmDocumentDetailsScreen, IDVSelfieProcessStartConfirmationScreen, IDVSelfieConfirmationScreen, IDVBiometricOptInConsentScreen } from '../screens/idv';
import type { RDNAIDVWorkflow, RDNAChallengeOpMode } from '../../uniken/types/idv/rdnaIDVEvents';
// IDV Route Parameter Types
interface IDVDocumentScanStartParams {
idvWorkflow: RDNAIDVWorkflow;
eventData: RDNAIDVGetDocumentScanStartConfirmationData;
}
interface IDVConfirmDocumentDetailsParams {
challengeMode: RDNAChallengeOpMode;
eventData: RDNAIDVGetConfirmDocumentDetailsData;
}
// ... similar for other IDV screens
export type RootStackParamList = {
// ... existing MFA routes
// IDV Routes
IDVDocumentScanStart: IDVDocumentScanStartParams;
IDVConfirmDocumentDetails: IDVConfirmDocumentDetailsParams;
IDVSelfieProcessStart: IDVSelfieProcessStartParams;
IDVSelfieConfirmation: IDVSelfieConfirmationParams;
IDVBiometricOptInConsent: IDVBiometricOptInConsentParams;
};
const Stack = createNativeStackNavigator<RootStackParamList>();
const AppNavigator = () => {
return (
<NavigationContainer ref={navigationRef}>
<Stack.Navigator screenOptions={{headerShown: false}}>
{/* ... existing routes ... */}
{/* IDV Routes */}
<Stack.Screen
name="IDVDocumentScanStart"
component={IDVDocumentProcessStartConfirmationScreen}
options={{ title: 'Document Scan', headerShown: false }}
/>
<Stack.Screen
name="IDVConfirmDocumentDetails"
component={IDVConfirmDocumentDetailsScreen}
options={{ title: 'Confirm Document', headerShown: false }}
/>
<Stack.Screen
name="IDVSelfieProcessStart"
component={IDVSelfieProcessStartConfirmationScreen}
options={{ title: 'Selfie Capture', headerShown: false }}
/>
<Stack.Screen
name="IDVSelfieConfirmation"
component={IDVSelfieConfirmationScreen}
options={{ title: 'Confirm Selfie', headerShown: false }}
/>
<Stack.Screen
name="IDVBiometricOptInConsent"
component={IDVBiometricOptInConsentScreen}
options={{ title: 'Biometric Consent', headerShown: false }}
/>
</Stack.Navigator>
</NavigationContainer>
);
};
export default AppNavigator;
Test the complete IDV MFA activation flow:
# Run the application
npx react-native run-android
# or
npx react-native run-ios
Step-by-Step Testing:
Check console for proper event flow:
# Expected log sequence
RdnaService - Initialize called
SDKIDVEventProvider - Registering IDV event handlers
SDKIDVEventProvider - Document scan start confirmation requested
RdnaIDVService - Setting document scan confirmation: isConfirm=true, idvWorkflow=0
SDKIDVEventProvider - Document details confirmation requested
RdnaIDVService - Setting confirm document details: isConfirm=true, challengeMode=8
SDKIDVEventProvider - Selfie process start confirmation requested
RdnaIDVService - Setting selfie process start: isConfirm=true, idvWorkflow=0
SDKIDVEventProvider - Selfie confirmation requested
RdnaIDVService - Setting selfie confirmation: action=true, challengeMode=8
SDKIDVEventProvider - Biometric opt-in consent requested
RdnaIDVService - Setting biometric consent: isOptIn=true, challengeMode=8
SDKEventProvider - User logged in
Symptoms:
setIDVDocumentScanProcessStartConfirmation succeeds but camera doesn't openSolutions:
# 1. Verify IDV assets are present
# Android
ls -la android/app/src/main/assets/Regula/
ls -la android/app/src/main/res/raw/
# iOS
ls -la ios/idv-artifacts/
# 2. Check file sizes
# db.dat should be ~110.5MB
# regula.license should exist
# 3. Clean and rebuild
cd android && ./gradlew clean && cd ..
npx react-native run-android
# iOS
cd ios && pod install && cd ..
npx react-native run-ios
Symptoms:
Solutions:
// 1. Verify SDKIDVEventProvider is properly nested in App.tsx
<SDKEventProvider>
<SDKIDVEventProvider> {/* Must be inside SDKEventProvider */}
<AppNavigator />
</SDKIDVEventProvider>
</SDKEventProvider>
// 2. Check event handler registration
// In SDKIDVEventProvider.tsx, verify useEffect runs:
useEffect(() => {
console.log('Registering IDV event handlers'); // Should see this log
// ...
}, []);
// 3. Verify rdnaIDVService is initialized
import rdnaIDVService from '../services/idv/rdnaIDVService';
console.log('IDV Service:', rdnaIDVService); // Should not be null
Symptoms:
challengeMode or idvWorkflow is undefinedSolutions:
// WRONG: Hardcoding values
await rdnaIDVService.setIDVConfirmDocumentDetails(true, 8); // ❌
// CORRECT: Use values from event data
const { challengeMode, eventData } = route.params || {};
if (!challengeMode && challengeMode !== 0) {
console.error('Invalid challengeMode:', challengeMode);
return;
}
await rdnaIDVService.setIDVConfirmDocumentDetails(
true,
challengeMode as RDNAChallengeOpMode // ✅ From event
);
Symptoms:
Solutions:
// Verify getPassword event handler is registered in SDKEventProvider
const handleGetPassword = useCallback((data: RDNAGetPasswordData) => {
console.log('Password requested after IDV'); // Should see this log
NavigationService.navigate('SetPasswordScreen', { eventData: data });
}, []);
// In useEffect, ensure registration:
eventManager.setGetPasswordHandler(handleGetPassword);
Symptoms:
Solutions:
// 1. Verify all IDV screens are registered in AppNavigator.tsx
<Stack.Screen name="IDVDocumentScanStart" component={IDVDocumentProcessStartConfirmationScreen} />
<Stack.Screen name="IDVConfirmDocumentDetails" component={IDVConfirmDocumentDetailsScreen} />
<Stack.Screen name="IDVSelfieProcessStart" component={IDVSelfieProcessStartConfirmationScreen} />
<Stack.Screen name="IDVSelfieConfirmation" component={IDVSelfieConfirmationScreen} />
<Stack.Screen name="IDVBiometricOptInConsent" component={IDVBiometricOptInConsentScreen} />
// 2. Verify type definitions match in RootStackParamList
export type RootStackParamList = {
IDVDocumentScanStart: IDVDocumentScanStartParams;
// ... all IDV routes
};
// In each screen
useEffect(() => {
console.log('Screen mounted with params:', route.params);
}, [route.params]);
try {
const response = await rdnaIDVService.setIDVConfirmDocumentDetails(true, challengeMode);
console.log('Full API response:', JSON.stringify(response, null, 2));
} catch (error) {
console.error('Full error:', JSON.stringify(error, null, 2));
}
# Use React Native Debugger or Flipper
# Filter logs by "SDKIDVEventProvider" to see event sequence
Congratulations! You've successfully implemented the IDV MFA Activation Flow with REL-ID SDK. You now have:
challengeMode and idvWorkflow from event dataService Layer (rdnaIDVService)
↓
Event Manager (rdnaIDVEventManager)
↓
Event Provider (SDKIDVEventProvider)
↓
Navigation (NavigationService)
↓
UI Screens (5 IDV screens)
↓
User Actions → Service APIs → Events → Next Screen
This codelab covered IDV_ACTIVATION (idvWorkflow: 0). REL-ID SDK supports other workflows:
Before deploying to production:
Customize the IDV experience:
Thank you for completing the REL-ID Identity Verification MFA Activation Flow codelab! 🎉
You're now equipped to build secure, identity-verified authentication flows with REL-ID SDK.