🎯 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
Flutter SDK >= 3.10.0
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-flutter/relid-IDV-MFA-activation-flow
The IDV MFA Activation codelab follows this structure:
relid-IDV-MFA-activation-flow/
├── lib/
│ ├── tutorial/
│ │ ├── screens/
│ │ │ ├── idv/ # IDV Screens (5 screens)
│ │ │ │ ├── idv_document_scan_start_screen.dart
│ │ │ │ ├── idv_confirm_document_details_screen.dart
│ │ │ │ ├── idv_selfie_start_screen.dart
│ │ │ │ ├── idv_selfie_confirmation_screen.dart
│ │ │ │ └── idv_biometric_consent_screen.dart
│ │ │ ├── mfa/ # MFA Screens
│ │ │ │ ├── check_user_screen.dart
│ │ │ │ ├── activation_code_screen.dart
│ │ │ │ ├── set_password_screen.dart
│ │ │ │ ├── verify_password_screen.dart
│ │ │ │ └── dashboard_screen.dart
│ │ │ └── components/ # Reusable UI components
│ │ │ ├── custom_button.dart
│ │ │ ├── close_button.dart
│ │ │ └── status_banner.dart
│ │ └── navigation/
│ │ └── app_router.dart # GoRouter configuration
│ └── uniken/
│ ├── services/
│ │ ├── idv/
│ │ │ ├── rdna_idv_service.dart # IDV Service Layer
│ │ │ └── rdna_idv_event_manager.dart # IDV Event Manager
│ │ ├── rdna_service.dart # Main RDNA Service
│ │ └── rdna_event_manager.dart # Main Event Manager
│ ├── providers/
│ │ ├── idv/
│ │ │ └── sdk_idv_event_provider.dart # IDV Event Provider
│ │ ├── sdk_event_provider.dart # Main Event Provider
│ │ ├── session_provider.dart # Session management
│ │ └── mtd_threat_provider.dart # MTD threat handling
│ ├── utils/
│ │ ├── connection_profile_parser.dart # Asset loading
│ │ └── rdna_event_utils.dart # Event utilities
│ └── cp/
│ └── agent_info.json # Connection profile
└── android/app/lib/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 rdna_client plugin at root folder
# (Contact REL-ID team for the plugin)
# Install dependencies
flutter pub get
cd ios && flutter pub get (handles iOS CocoaPods automatically) && 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/lib/main/assets/Regula/db.datandroid/app/lib/main/assets/Regula/certificates/icaopkd-002-complete-000325.ldifandroid/app/lib/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
flutter run -d android
# iOS
flutter run -d 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:
lib/uniken/
├── services/idv/ # SERVICE LAYER
│ ├── rdna_idv_service.dart # IDV API wrapper (Future-based)
│ └── rdna_idv_event_manager.dart # Native event manager (auto-init)
│
├── providers/idv/ # PROVIDER LAYER
│ └── sdk_idv_event_provider.dart # Riverpod StateProvider provider
│
└── types/idv/ # TYPE DEFINITIONS
└── rdnaIDVEvents.dart # Type-safe event interfaces
lib/tutorial/
└── screens/idv/ # UI LAYER
├── idv_document_scan_start_screen.dart
├── idv_confirm_document_details_screen.dart
├── idv_selfie_start_screen.dart
├── idv_selfie_confirmation_screen.dart
├── idv_biometric_consent_screen.dart
└── index.dart
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 (Riverpod StateProvider) │
│ • sdk_idv_event_provider.dart │
│ • Connects events to navigation │
│ • Manages IDV state (current workflow) │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ LAYER 1: SERVICE LAYER (Business Logic) │
│ • rdna_idv_service.dart → API calls (Future-based) │
│ • rdna_idv_event_manager.dart → Native event handling │
│ • rdnaIDVEvents.dart → Type definitions │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ NATIVE LAYER (REL-ID SDK) │
│ • 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 Future
→ 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 {
static RdnaIDVEventManager? _instance;
RdnaIDVEventManager._() {
_registerEventListeners(); // ← Auto-init on first getInstance call!
}
static RdnaIDVEventManager getInstance(RdnaClient rdnaClient) {
_instance ??= RdnaIDVEventManager._();
return _instance!;
}
}
Why? Ensures event listeners are registered once before any native events fire, preventing race conditions.
The Event Provider uses Riverpod StateProvider to share IDV state across components:
// Provider manages state and navigation
class SDKIDVEventProviderWidget extends ConsumerStatefulWidget {
final Widget child;
const SDKIDVEventProviderWidget({required this.child});
void handleEvent(String data) {
setCurrentIDVWorkflow(data.idvWorkflow); // Update state
NavigationService.navigate('IDVScreen'); // Navigate
};
initState/dispose lifecycle methods(() => {
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 Riverpod StateProvider.
All event data uses TypeScript interfaces with reusable common types:
// Reusable common types from base RDNA events
import typedef or class { RDNAError, RDNAChallengeResponse } from '../rdnaEvents';
// IDV-specific event data
class 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 Futures for async/await:
// Native SDK uses callbacks
RdnaClient.setIDVDocumentScanProcessStartConfirmation(isConfirm, workflow, callback);
// Service wraps in Future for async/await
async setIDVDocumentScanProcessStartConfirmation(isConfirm, workflow) {
return new Future((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 main.dart with proper nesting:
// main.dart - Provider Nesting Pattern
import 'package:your_app/uniken/providers/sdk_event_provider.dart';
import 'package:your_app/uniken/providers/idv/sdk_idv_event_provider.dart';
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
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
✅ rdna_idv_event_manager.dart → Handles native events only
✅ rdna_idv_service.dart → Handles API calls only
✅ sdk_idv_event_provider.dart → Handles React integration only
✅ rdnaIDVEvents.dart → 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 typedef or class integration (RDNAIDVWorkflow, RDNAChallengeOpMode)
✅ Compile-time typedef or class 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.
lib/uniken/services/idv/rdna_idv_event_manager.dart
Create the file at lib/uniken/services/idv/rdna_idv_event_manager.dart:
import 'package:eventify/eventify.dart';
import 'package:rdna_client/rdna_client.dart';
/// Type definitions for IDV event callbacks
///
/// All IDV events return raw JSON strings that must be parsed by the application.
/// The String parameter contains the complete event data as a JSON string.
typedef RDNAIDVGetDocumentScanStartConfirmationCallback = void Function(String);
typedef RDNAIDVGetConfirmDocumentDetailsCallback = void Function(String);
typedef RDNAIDVGetSelfieProcessStartConfirmationCallback = void Function(String);
typedef RDNAIDVGetSelfieConfirmationCallback = void Function(String);
typedef RDNAIDVGetBiometricOptInConsentCallback = void Function(String);
/// REL-ID IDV SDK Event Manager
///
/// Manages Identity Verification (IDV) related SDK events in a centralized, type-safe manner.
/// Provides a singleton pattern for consistent IDV event handling across the application.
///
/// **IMPORTANT:** This manager auto-initializes when getInstance() is first called to ensure
/// event listeners are registered immediately, preventing race conditions.
///
/// ## Supported IDV Events
///
/// **Document Scan Events:**
/// - getIDVDocumentScanProcessStartConfirmation: Pre-scan consent request
/// - getIDVConfirmDocumentDetails: Post-scan OCR data validation
///
/// **Selfie Capture Events:**
/// - getIDVSelfieProcessStartConfirmation: Pre-selfie capture consent request
/// - getIDVSelfieConfirmation: Post-selfie biometric analysis validation
///
/// **Biometric Consent Events:**
/// - getIDVBiometricOptInConsent: User consent for biometric template storage
///
/// ## Key Features
/// - Singleton pattern for global IDV event management
/// - Type-safe callback handling with Dart types
/// - Automatic event listener registration and cleanup
/// - Single event handler per type for simplicity
/// - Raw JSON string event data (application must parse)
class RdnaIDVEventManager {
static RdnaIDVEventManager? _instance;
final RdnaClient _rdnaClient;
final List<Listener?> _listeners = [];
// Event handler storage
RDNAIDVGetDocumentScanStartConfirmationCallback? _getDocumentScanStartConfirmationHandler;
RDNAIDVGetConfirmDocumentDetailsCallback? _getConfirmDocumentDetailsHandler;
RDNAIDVGetSelfieProcessStartConfirmationCallback? _getSelfieProcessStartConfirmationHandler;
RDNAIDVGetSelfieConfirmationCallback? _getSelfieConfirmationHandler;
RDNAIDVGetBiometricOptInConsentCallback? _getBiometricOptInConsentHandler;
RdnaIDVEventManager._(this._rdnaClient) {
print('RdnaIDVEventManager - Initializing event manager');
_registerEventListeners();
}
static RdnaIDVEventManager getInstance(RdnaClient rdnaClient) {
_instance ??= RdnaIDVEventManager._(rdnaClient);
return _instance!;
}
void _registerEventListeners() {
print('RdnaIDVEventManager - Registering IDV event listeners');
_listeners.add(
_rdnaClient.on(
RdnaClient.getIDVDocumentScanProcessStartConfirmation,
_onGetDocumentScanStartConfirmation,
),
);
_listeners.add(
_rdnaClient.on(
RdnaClient.getIDVConfirmDocumentDetails,
_onGetConfirmDocumentDetails,
),
);
_listeners.add(
_rdnaClient.on(
RdnaClient.getIDVSelfieProcessStartConfirmation,
_onGetSelfieProcessStartConfirmation,
),
);
_listeners.add(
_rdnaClient.on(
RdnaClient.getIDVSelfieConfirmation,
_onGetSelfieConfirmation,
),
);
_listeners.add(
_rdnaClient.on(
RdnaClient.getIDVBiometricOptInConsent,
_onGetBiometricOptInConsent,
),
);
print('RdnaIDVEventManager - IDV event listeners registered');
}
void _onGetDocumentScanStartConfirmation(dynamic data) {
print('RdnaIDVEventManager - Document scan start confirmation event received');
if (_getDocumentScanStartConfirmationHandler != null) {
_getDocumentScanStartConfirmationHandler!(data as String);
}
}
void _onGetConfirmDocumentDetails(dynamic data) {
print('RdnaIDVEventManager - Document details confirmation event received');
if (_getConfirmDocumentDetailsHandler != null) {
_getConfirmDocumentDetailsHandler!(data as String);
}
}
void _onGetSelfieProcessStartConfirmation(dynamic data) {
print('RdnaIDVEventManager - Selfie process start confirmation event received');
if (_getSelfieProcessStartConfirmationHandler != null) {
_getSelfieProcessStartConfirmationHandler!(data as String);
}
}
void _onGetSelfieConfirmation(dynamic data) {
print('RdnaIDVEventManager - Selfie confirmation event received');
if (_getSelfieConfirmationHandler != null) {
_getSelfieConfirmationHandler!(data as String);
}
}
void _onGetBiometricOptInConsent(dynamic data) {
print('RdnaIDVEventManager - Biometric opt-in consent event received');
if (_getBiometricOptInConsentHandler != null) {
_getBiometricOptInConsentHandler!(data as String);
}
}
// Setter methods for event handlers
void setGetDocumentScanStartConfirmationHandler(RDNAIDVGetDocumentScanStartConfirmationCallback? callback) {
_getDocumentScanStartConfirmationHandler = callback;
}
void setGetConfirmDocumentDetailsHandler(RDNAIDVGetConfirmDocumentDetailsCallback? callback) {
_getConfirmDocumentDetailsHandler = callback;
}
void setGetSelfieProcessStartConfirmationHandler(RDNAIDVGetSelfieProcessStartConfirmationCallback? callback) {
_getSelfieProcessStartConfirmationHandler = callback;
}
void setGetSelfieConfirmationHandler(RDNAIDVGetSelfieConfirmationCallback? callback) {
_getSelfieConfirmationHandler = callback;
}
void setGetBiometricOptInConsentHandler(RDNAIDVGetBiometricOptInConsentCallback? callback) {
_getBiometricOptInConsentHandler = callback;
}
void cleanup() {
print('RdnaIDVEventManager - Cleaning up');
for (var listener in _listeners) {
if (listener != null) {
_rdnaClient.off(listener);
}
}
_listeners.clear();
_getDocumentScanStartConfirmationHandler = null;
_getConfirmDocumentDetailsHandler = null;
_getSelfieProcessStartConfirmationHandler = null;
_getSelfieConfirmationHandler = null;
_getBiometricOptInConsentHandler = null;
}
}
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 typedef or class (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);
}
}
Unlike React Native (which requires TypeScript interface definitions), Flutter gets all types directly from the
rdna_client
plugin package. You don't need to create custom type definition files!
// All types are imported from the plugin package
import 'package:rdna_client/rdna_client.dart';
// You get access to:
// - RdnaClient (main SDK class)
// - RDNASyncResponse, RDNAError
// - RDNAGetUser, RDNAGetPassword
// - RDNAInitialized, RDNAUserLoggedIn
// - RDNAChallengeOpMode enum
// - All other SDK types and enums
Important: IDV events return raw JSON strings (not typed objects):
// Event handler receives String parameter
typedef RDNAIDVGetDocumentScanStartConfirmationCallback = void Function(String);
// You parse the JSON in your screen
void _parseEventData() {
try {
final data = jsonDecode(widget.eventData);
final idvWorkflow = data['idvWorkflow'];
final responseData = data['response_data'];
// ... use the data
} catch (e) {
print('Error parsing: $e');
}
}
Event Type | Data Format | Example |
MFA Events | Typed objects |
|
IDV Events | Raw JSON strings | Parse with |
The IDV functionality is implemented using a singleton service pattern that wraps the native RdnaClient IDV APIs with type-safe, Future-based interfaces.
Create the IDV service layer at lib/uniken/services/idv/rdna_idv_service.dart:
import 'package:rdna_client/rdna_client.dart';
import 'rdna_idv_event_manager.dart';
/// REL-ID IDV SDK Service
///
/// Provides a singleton service for Identity Verification (IDV) related SDK operations.
/// Wraps the native RdnaClient IDV API with type-safe interface.
/// Handles document scanning and validation operations.
///
/// **IMPORTANT:** This service auto-initializes when getInstance() is first called to ensure event
/// listeners are registered BEFORE any native SDK events can fire. This prevents race
/// conditions where events fire before Dart event handlers are registered.
///
/// ## IDV APIs
///
/// **Document Scan APIs:**
/// - setIDVDocumentScanProcessStartConfirmation: Start/cancel document scan
/// - setIDVConfirmDocumentDetails: Confirm/reject scanned document OCR data
///
/// **Selfie Capture APIs:**
/// - setIDVSelfieProcessStartConfirmation: Start/cancel selfie capture
/// - setIDVSelfieConfirmation: Confirm/reject selfie biometric results
///
/// **Biometric Consent APIs:**
/// - setIDVBiometricOptInConsent: User consent for biometric template storage
///
/// All methods follow the Flutter plugin pattern:
/// 1. Synchronous response validates request acceptance (error.longErrorCode == 0)
/// 2. Asynchronous events deliver actual results through event handlers
/// 3. Return value contains sync response structure
///
/// ## See Also
/// - https://developer.uniken.com/docs/idv-integration
class RdnaIDVService {
static RdnaIDVService? _instance;
final RdnaClient _rdnaClient;
final RdnaIDVEventManager _eventManager;
RdnaIDVService._(this._rdnaClient, this._eventManager) {
print('RdnaIDVService - Service initialized, event listeners are ready');
}
/// Gets the singleton instance of RdnaIDVService
static RdnaIDVService getInstance() {
if (_instance == null) {
print('RdnaIDVService - Initializing service (auto-initialization on first call)');
final rdnaClient = RdnaClient();
final eventManager = RdnaIDVEventManager.getInstance(rdnaClient);
_instance = RdnaIDVService._(rdnaClient, eventManager);
}
return _instance!;
}
/// Gets the IDV event manager instance for external callback setup
///
/// ## Returns
/// RdnaIDVEventManager instance for registering event handlers
RdnaIDVEventManager getEventManager() {
return _eventManager;
}
/// Cleans up the service and event manager
///
/// Should be called when IDV functionality is no longer needed or during app cleanup.
void cleanup() {
print('RdnaIDVService - Cleaning up service');
_eventManager.cleanup();
}
// ================================================================================================
// DOCUMENT SCAN APIs
// ================================================================================================
/// Sets document scan process start confirmation
///
/// This API must be invoked in response to getIDVDocumentScanProcessStartConfirmation event.
/// Initiates or cancels the document scan process during IDV workflows.
///
/// ## See Also
/// https://developer.uniken.com/docs/idv-document-scan
///
/// ## Response Validation Logic
/// 1. Check error.longErrorCode: 0 = success, > 0 = error
/// 2. If isConfirm=true: SDK starts document capture UI
/// 3. If isConfirm=false: IDV flow terminates, redirects to getUser (pre-login) or appropriate workflow event (post-login)
/// 4. Error code 146 = "IDV identity verification canceled by user"
///
/// ## Parameters
/// - `isConfirm`: true to start document scan, false to cancel IDV flow
/// - `idvWorkflow`: IDV workflow enum value from the event
///
/// ## Returns
/// String response from the plugin (raw response)
///
/// ## Example
/// ```dart
/// final response = await rdnaIDVService.setIDVDocumentScanProcessStartConfirmation(
/// true,
/// 0 // idvWorkflow from event data
/// );
/// print('Document scan started: $response');
/// ```
Future<String> setIDVDocumentScanProcessStartConfirmation(
bool isConfirm,
int idvWorkflow,
) async {
print('RdnaIDVService - Setting document scan start confirmation:');
print(' isConfirm: $isConfirm');
print(' idvWorkflow: $idvWorkflow');
final response = await _rdnaClient.setIDVDocumentScanProcessStartConfirmation(
isConfirm,
idvWorkflow,
);
print('RdnaIDVService - SetIDVDocumentScanProcessStartConfirmation response received');
return response;
}
/// Sets document details confirmation
///
/// This API must be invoked in response to getIDVConfirmDocumentDetails event.
/// Confirms or rejects the OCR-extracted document information displayed to the user.
///
/// ## See Also
/// https://developer.uniken.com/docs/idv-document-validation
///
/// ## Response Validation Logic
/// 1. Check error.longErrorCode: 0 = success, > 0 = error
/// 2. If isConfirm=true: SDK proceeds with validated document data to next step in IDV workflow
/// 3. If isConfirm=false: SDK may allow document rescan or terminate based on workflow configuration
/// 4. challengeMode determines the operation context (biometric, document scan, etc.)
///
/// ## Parameters
/// - `isConfirm`: true to confirm document details, false to reject and possibly rescan
/// - `challengeMode`: Challenge operation mode value indicating the IDV operation context
///
/// ## Returns
/// String response from the plugin (raw response)
///
/// ## Example
/// ```dart
/// final response = await rdnaIDVService.setIDVConfirmDocumentDetails(
/// true,
/// RDNAChallengeOpMode.RDNA_SYNC_CHALLENGE.index
/// );
/// print('Document details confirmed: $response');
/// ```
Future<String> setIDVConfirmDocumentDetails(
bool isConfirm,
int challengeMode,
) async {
print('RdnaIDVService - Setting confirm document details:');
print(' isConfirm: $isConfirm');
print(' challengeMode: $challengeMode');
final response = await _rdnaClient.setIDVConfirmDocumentDetails(
isConfirm,
challengeMode,
);
print('RdnaIDVService - SetIDVConfirmDocumentDetails response received');
return response;
}
// ================================================================================================
// SELFIE CAPTURE APIs
// ================================================================================================
/// Sets selfie process start confirmation
///
/// This API must be invoked in response to getIDVSelfieProcessStartConfirmation event.
/// Initiates or cancels the selfie capture process during IDV workflows.
///
/// ## See Also
/// https://developer.uniken.com/docs/idv-selfie-capture
///
/// ## Response Validation Logic
/// 1. Check error.longErrorCode: 0 = success, > 0 = error
/// 2. If isConfirm=true: SDK starts selfie capture UI with specified camera
/// 3. If isConfirm=false: IDV flow terminates, redirects to appropriate workflow event
/// 4. useDeviceBackCamera determines which camera to use (default: front camera)
///
/// ## Parameters
/// - `isConfirm`: true to start selfie capture, false to cancel IDV flow
/// - `useDeviceBackCamera`: true to use back camera, false to use front camera (default: false)
/// - `idvWorkflow`: IDV workflow enum value from the event
///
/// ## Returns
/// String response from the plugin (raw response)
///
/// ## Example
/// ```dart
/// final response = await rdnaIDVService.setIDVSelfieProcessStartConfirmation(
/// true,
/// false, // use front camera
/// 0 // idvWorkflow from event data
/// );
/// print('Selfie capture started: $response');
/// ```
Future<String> setIDVSelfieProcessStartConfirmation(
bool isConfirm,
bool useDeviceBackCamera,
int idvWorkflow,
) async {
print('RdnaIDVService - Setting selfie process start confirmation:');
print(' isConfirm: $isConfirm');
print(' useDeviceBackCamera: $useDeviceBackCamera');
print(' idvWorkflow: $idvWorkflow');
final response = await _rdnaClient.setIDVSelfieProcessStartConfirmation(
isConfirm,
useDeviceBackCamera,
idvWorkflow,
);
print('RdnaIDVService - SetIDVSelfieProcessStartConfirmation response received');
return response;
}
/// Sets selfie confirmation
///
/// This API must be invoked in response to getIDVSelfieConfirmation event.
/// Confirms or rejects the captured selfie and biometric analysis results.
///
/// ## See Also
/// https://developer.uniken.com/docs/idv-selfie-confirmation
///
/// ## Response Validation Logic
/// 1. Check error.longErrorCode: 0 = success, > 0 = error
/// 2. If action="true": SDK proceeds with validated selfie data to next step in IDV workflow
/// 3. If action="false": SDK may allow selfie recapture or terminate based on workflow configuration
/// 4. challengeMode determines the operation context (biometric matching, liveness detection, etc.)
///
/// ## Parameters
/// - `action`: User confirmation action (typically "true" for accept, "false" for reject, or dynamic action key)
/// - `challengeMode`: Challenge operation mode value from the getIDVSelfieConfirmation event
///
/// ## Returns
/// String response from the plugin (raw response)
///
/// ## Example
/// ```dart
/// final response = await rdnaIDVService.setIDVSelfieConfirmation(
/// 'true', // or action key from event data
/// RDNAChallengeOpMode.RDNA_SYNC_CHALLENGE.index
/// );
/// print('Selfie confirmed: $response');
/// ```
Future<String> setIDVSelfieConfirmation(
String action,
int challengeMode,
) async {
print('RdnaIDVService - Setting selfie confirmation:');
print(' action: $action');
print(' challengeMode: $challengeMode');
final response = await _rdnaClient.setIDVSelfieConfirmation(
action,
challengeMode,
);
print('RdnaIDVService - SetIDVSelfieConfirmation response received');
return response;
}
// ================================================================================================
// BIOMETRIC CONSENT APIs
// ================================================================================================
/// Sets biometric opt-in consent
///
/// This API must be invoked in response to getIDVBiometricOptInConsent event.
/// Submits user's consent decision for biometric template storage during IDV workflows.
///
/// ## See Also
/// https://developer.uniken.com/docs/idv-biometric-consent
///
/// ## Response Validation Logic
/// 1. Check error.longErrorCode: 0 = success, > 0 = error
/// 2. If isOptIn=true: User consents to biometric template storage, SDK proceeds with IDV flow
/// 3. If isOptIn=false: User declines biometric storage, SDK may continue without storing template
/// 4. The IDV flow will continue to the next step
/// 5. Async events will be handled by event listeners
///
/// ## Parameters
/// - `isOptIn`: User consent decision (true = allow biometric storage, false = deny)
/// - `challengeMode`: Challenge mode from the getIDVBiometricOptInConsent event
///
/// ## Returns
/// String response from the plugin (raw response)
///
/// ## Example
/// ```dart
/// final response = await rdnaIDVService.setIDVBiometricOptInConsent(
/// true,
/// RDNAChallengeOpMode.RDNA_SYNC_CHALLENGE.index
/// );
/// print('Biometric consent submitted: $response');
/// ```
Future<String> setIDVBiometricOptInConsent(
bool isOptIn,
int challengeMode,
) async {
print('RdnaIDVService - Setting biometric opt-in consent:');
print(' isOptIn: $isOptIn');
print(' challengeMode: $challengeMode');
final response = await _rdnaClient.setIDVBiometricOptInConsent(
isOptIn,
challengeMode,
);
print('RdnaIDVService - SetIDVBiometricOptInConsent response received');
return response;
}
}
### Integrating Event Providers
In your `main.dart`, nest the providers correctly:
```dart
import 'package:your_app/uniken/providers/sdk_event_provider.dart';
import 'package:your_app/uniken/providers/idv/sdk_idv_event_provider.dart';
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return (
ProviderScope(
child: SDKEventProviderWidget(
child: SDKIDVEventProviderWidget(
child: MaterialApp.router(
routerConfig: appRouter,
),
),
),
)
);
> aside positive
> Nesting SDKIDVEventProvider inside SDKEventProvider ensures both MFA and IDV events are properly handled throughout the app.
---
## Document Scan Process Implementation
Duration: 5
### Step 1: Event - getIDVDocumentScanProcessStartConfirmation
After the activation code is validated, the SDK triggers the `getIDVDocumentScanProcessStartConfirmation` event with:
```dart
{
idvWorkflow: 0, // RDNAIDVWorkflowActivation
// Additional event data
}
The event handler (in SDKIDVEventProvider) automatically navigates to IDVDocumentScanStart screen.
Create lib/tutorial/screens/idv/idv_confirm_document_details_screen.dart:
import 'package:flutter/material.dart';
import 'dart:convert';
import '../../../uniken/services/idv/rdna_idv_service.dart';
/// IDV Confirm Document Details Screen
///
/// Displays OCR-extracted document information for user validation.
/// Shows document images, extracted identity data, and validation status.
class IDVConfirmDocumentDetailsScreen extends StatefulWidget {
final int challengeMode;
final String eventData;
const IDVConfirmDocumentDetailsScreen({
super.key,
required this.challengeMode,
required this.eventData,
});
@override
State<IDVConfirmDocumentDetailsScreen> createState() => _IDVConfirmDocumentDetailsScreenState();
}
class _IDVConfirmDocumentDetailsScreenState extends State<IDVConfirmDocumentDetailsScreen> {
bool _isProcessing = false;
Map<String, dynamic> _documentData = {};
Map<String, dynamic> _identityData = {};
String _documentStatus = '';
@override
void initState() {
super.initState();
_parseEventData();
}
void _parseEventData() {
try {
final data = jsonDecode(widget.eventData);
final responseData = data['response_data'];
setState(() {
_documentData = responseData['document_info'] ?? {};
_identityData = responseData['identity_data'] ?? {};
_documentStatus = responseData['document_status']?['overall_document_status'] ?? 'UNKNOWN';
});
} catch (e) {
print('Error parsing document data: $e');
}
}
/// Handle Confirm Button
Future<void> _handleConfirm() async {
setState(() => _isProcessing = true);
final rdnaIDVService = RdnaIDVService.getInstance();
final response = await rdnaIDVService.setIDVConfirmDocumentDetails(
true, // isConfirm = true
widget.challengeMode,
);
print('Document details confirmed: $response');
setState(() => _isProcessing = false);
}
/// Handle Rescan Button
Future<void> _handleRescan() async {
setState(() => _isProcessing = true);
final rdnaIDVService = RdnaIDVService.getInstance();
await rdnaIDVService.setIDVConfirmDocumentDetails(
false, // isConfirm = false to rescan
widget.challengeMode,
);
setState(() => _isProcessing = false);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
appBar: AppBar(title: const Text('Confirm Document Details')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
// Document Status Badge
_buildStatusBadge(),
const SizedBox(height: 20),
// Document Images
if (_documentData['front_side_image'] != null)
_buildDocumentImage('Front', _documentData['front_side_image']),
const SizedBox(height: 20),
// Identity Data
_buildIdentityDataCard(),
const SizedBox(height: 24),
// Action Buttons
Row(
children: [
Expanded(
child: CustomButton(
title: 'Rescan',
onPressed: _handleRescan,
disabled: _isProcessing,
),
),
const SizedBox(width: 12),
Expanded(
child: CustomButton(
title: 'Confirm',
onPressed: _handleConfirm,
isLoading: _isProcessing,
disabled: _isProcessing,
),
),
],
),
],
),
),
);
}
Widget _buildStatusBadge() {
Color statusColor = _documentStatus == 'OK'
? Colors.green
: _documentStatus == 'WARNING'
? Colors.orange
: Colors.red;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'Status: $_documentStatus',
style: TextStyle(color: statusColor, fontWeight: FontWeight.bold),
),
);
}
Widget _buildDocumentImage(String label, String base64Image) {
// Decode base64 image and display
return Column(
children: [
Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Image.memory(base64Decode(base64Image), height: 200),
],
);
}
Widget _buildIdentityDataCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _identityData.entries.map((entry) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Text('${entry.key}: ', style: const TextStyle(fontWeight: FontWeight.bold)),
Text('${entry.value}'),
],
),
);
}).toList(),
),
),
);
}
}
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, typedef or class 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 lib/tutorial/screens/idv/idv_document_scan_start_screen.dart:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../uniken/services/idv/rdna_idv_service.dart';
import '../components/custom_button.dart';
import '../components/close_button.dart';
import '../components/status_banner.dart';
/// IDV Document Scan Start Screen
///
/// Handles the IDV document scan process start confirmation event.
/// Displays scan requirements and allows users to start or cancel the scan.
class IDVDocumentScanStartScreen extends StatefulWidget {
final int idvWorkflow;
final String eventData;
const IDVDocumentScanStartScreen({
super.key,
required this.idvWorkflow,
required this.eventData,
});
@override
State<IDVDocumentScanStartScreen> createState() => _IDVDocumentScanStartScreenState();
}
class _IDVDocumentScanStartScreenState extends State<IDVDocumentScanStartScreen> {
bool _isProcessing = false;
String _error = '';
@override
void initState() {
super.initState();
print('IDVDocumentScanStart - Screen initialized with workflow: ${widget.idvWorkflow}');
}
/// Handle Scan Document Button
/// Calls setIDVDocumentScanProcessStartConfirmation with isConfirm=true
Future<void> _handleScanDocument() async {
setState(() {
_isProcessing = true;
_error = '';
});
print('IDVDocumentScanStart - Starting document scan');
final rdnaIDVService = RdnaIDVService.getInstance();
final response = await rdnaIDVService.setIDVDocumentScanProcessStartConfirmation(
true, // isConfirm = true to start scan
widget.idvWorkflow,
);
print('IDVDocumentScanStart - Response: $response');
// SDK opens native document scanner, results handled by event provider
setState(() => _isProcessing = false);
}
/// Handle Close Button
/// Calls setIDVDocumentScanProcessStartConfirmation with isConfirm=false
Future<void> _handleClose() async {
setState(() => _isProcessing = true);
final rdnaIDVService = RdnaIDVService.getInstance();
await rdnaIDVService.setIDVDocumentScanProcessStartConfirmation(
false, // isConfirm = false to cancel
widget.idvWorkflow,
);
print('IDVDocumentScanStart - Cancelled');
setState(() => _isProcessing = false);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
body: SafeArea(
child: Stack(
children: [
// Main Content
SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(20).copyWith(top: 80),
child: Column(
children: [
// Title
const Text(
'Document Scan',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Color(0xFF2C3E50),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
// Subtitle
const Text(
'Prepare to scan your identity document',
style: TextStyle(fontSize: 16, color: Color(0xFF7F8C8D)),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// Document Icon
const Text('📋', style: TextStyle(fontSize: 80)),
const SizedBox(height: 32),
// Error Banner
if (_error.isNotEmpty)
StatusBanner(type: 'error', message: _error),
// Guidelines
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Scanning Guidelines',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
const Text(
'• Position document within frame with all corners visible
'
'• Avoid shadows, glare, and blurred images
'
'• Ensure document is flat and text is readable
'
'• Use good lighting for best results',
style: TextStyle(fontSize: 15, color: Color(0xFF34495E)),
),
],
),
),
const SizedBox(height: 24),
// Scan Button
CustomButton(
title: _isProcessing ? 'Starting Scan...' : 'Scan Document',
onPressed: _handleScanDocument,
isLoading: _isProcessing,
disabled: _isProcessing,
),
],
),
),
),
// Close Button (Top-Left)
CustomCloseButton(
onPressed: _handleClose,
disabled: _isProcessing,
),
],
),
),
);
}
}
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 lib/tutorial/screens/idv/idv_selfie_start_screen.dart:
import 'package:flutter/material.dart';
import '../../../uniken/services/idv/rdna_idv_service.dart';
/// IDV Selfie Start Screen
///
/// Handles selfie capture initiation with camera selection.
/// Provides guidelines and starts the native selfie capture process.
class IDVSelfieStartScreen extends StatefulWidget {
final int idvWorkflow;
final String eventData;
const IDVSelfieStartScreen({
super.key,
required this.idvWorkflow,
required this.eventData,
});
@override
State<IDVSelfieStartScreen> createState() => _IDVSelfieStartScreenState();
}
class _IDVSelfieStartScreenState extends State<IDVSelfieStartScreen> {
bool _isProcessing = false;
bool _useBackCamera = false;
/// Handle Start Selfie Capture
Future<void> _handleStartSelfie() async {
setState(() => _isProcessing = true);
final rdnaIDVService = RdnaIDVService.getInstance();
final response = await rdnaIDVService.setIDVSelfieProcessStartConfirmation(
true, // isConfirm = true to start
_useBackCamera, // camera selection
widget.idvWorkflow,
);
print('Selfie capture started: $response');
setState(() => _isProcessing = false);
}
/// Handle Cancel
Future<void> _handleCancel() async {
setState(() => _isProcessing = true);
final rdnaIDVService = RdnaIDVService.getInstance();
await rdnaIDVService.setIDVSelfieProcessStartConfirmation(
false, // isConfirm = false to cancel
false,
widget.idvWorkflow,
);
setState(() => _isProcessing = false);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
body: SafeArea(
child: Stack(
children: [
SingleChildScrollView(
padding: const EdgeInsets.all(20).copyWith(top: 80),
child: Column(
children: [
const Text(
'Selfie Capture',
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
const Text(
'Prepare to capture your selfie for verification',
style: TextStyle(fontSize: 16, color: Color(0xFF7F8C8D)),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// Selfie Icon
const Text('🤳', style: TextStyle(fontSize: 80)),
const SizedBox(height: 32),
// Camera Selection
SwitchListTile(
title: const Text('Use Back Camera'),
subtitle: const Text('Toggle to use back camera instead of front'),
value: _useBackCamera,
onChanged: (value) => setState(() => _useBackCamera = value),
),
const SizedBox(height: 24),
// Guidelines
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Selfie Guidelines',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 12),
Text(
'• Look directly at the camera
'
'• Ensure your face is well-lit
'
'• Remove glasses and hats
'
'• Keep a neutral expression
'
'• Follow on-screen instructions for liveness detection',
style: TextStyle(fontSize: 15),
),
],
),
),
const SizedBox(height: 24),
// Start Button
CustomButton(
title: _isProcessing ? 'Starting...' : 'Capture Selfie',
onPressed: _handleStartSelfie,
isLoading: _isProcessing,
disabled: _isProcessing,
),
],
),
),
// Close Button
CustomCloseButton(
onPressed: _handleCancel,
disabled: _isProcessing,
),
],
),
),
);
}
}
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:
// Flutter: Widget-based, no React imports needed;
// Flutter equivalent: import 'package:flutter/material.dart';
import { useRoute } from 'go_router/native';
import rdnaIDVService from '../../../uniken/services/idv/rdnaIDVService';
import { CloseButton, Button, StatusBanner } from '../components';
const IDVSelfieConfirmationScreen: Widget = () => {
final route = useRoute();
const { challengeMode, eventData } = route.params || {};
final responseData = eventData?.response_data;
const [isProcessing, setIsProcessing] = StatefulWidget with setState(false);
const [error, setError] = StatefulWidget with setState('');
/**
* Handle action button click
* Calls setIDVSelfieConfirmation with action key
*/
final handleAction = async (actionKey: String) => {
setIsProcessing(true);
setError('');
try {
print('Processing selfie action:', actionKey);
await rdnaIDVService.setIDVSelfieConfirmation(
actionKey, // Action key from button: "continue-flow", "reinit-idv-document-scan", or "reinit-idv-selfie"
challengeMode
);
print('Selfie action processed');
} catch (error) {
print('Failed to process selfie action:', error);
setError(RDNASyncUtils.getErrorMessage(error));
} finally {
setIsProcessing(false);
}
};
// Extract data
final biometricResult = responseData?.response_data?.biometric_result;
final faceMatchScore = responseData?.response_data?.face_matcher_response?.score;
final livenessScore = responseData?.response_data?.analyze_liveness_response?.video?.liveness_result?.score;
final selfieImage = responseData?.response_data?.analyze_liveness_response?.video?.autocapture_result?.selfie_image;
final documentPhoto = responseData?.response_data?.document_detail?.document_info?.portrait_image;
final 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>
Container or Column/Row
<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() {
final actionButtons = responseData?.action_buttons;
if (!actionButtons) return null;
final 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>
);
}
};
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 missingfinal renderActionButtons = () => {
final actionButtons = responseData?.action_buttons;
final biometricResult = responseData?.response_data?.biometric_result;
// Determine which button array to use based on match result
final isMatched = biometricResult?.display_text === 'MATCHED';
final 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'}
/>
));
};
final 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
);
> aside positive
> **Important**: Always use the exact `key` values from the `action_buttons` object. Never hardcode action keys - the server controls which actions are available based on workflow configuration, match quality, and business rules.
### Error Handling for Selfie Confirmation
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 `action_buttons` object and challengeMode is valid. |
> aside positive
> **Critical**: Confirmation is mandatory - failure to respond to `getIDVSelfieConfirmation` stalls the IDV workflow. Always validate that action keys come from the `action_buttons` object and challengeMode is valid before calling the API. For more details, see the [getIDVSelfieConfirmation documentation](https://developer.uniken.com/docs/getidvselfieconfirmation).
---
## Biometric Consent Implementation
Duration: 3
### Step 1: Event - getIDVBiometricOptInConsent
After selfie confirmation, SDK requests user consent for biometric template storage:
```dart
{
userID: "john.doe",
challengeMode: 8,
challengeResponse: {
additionalInfo: {
currentWorkFlow: "IDV_ACTIVATION"
}
}
}
Create lib/tutorial/screens/idv/idv_biometric_consent_screen.dart:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:convert';
import '../../../uniken/services/idv/rdna_idv_service.dart';
import '../../../uniken/services/rdna_service.dart';
import '../components/custom_button.dart';
import '../components/close_button.dart';
import '../components/status_banner.dart';
/// IDV Biometric Opt-In Consent Screen
///
/// Displays biometric template storage consent options.
/// Users can accept or deny biometric template storage.
class IDVBiometricConsentScreen extends StatefulWidget {
final int challengeMode;
final String eventData;
const IDVBiometricConsentScreen({
super.key,
required this.challengeMode,
required this.eventData,
});
@override
State<IDVBiometricConsentScreen> createState() => _IDVBiometricConsentScreenState();
}
class _IDVBiometricConsentScreenState extends State<IDVBiometricConsentScreen> {
bool _isProcessing = false;
String _error = '';
@override
void initState() {
super.initState();
print('IDVBiometricConsent - Screen initialized');
print(' challengeMode: ${widget.challengeMode}');
}
/// Handle Close Button - Reset auth state
Future<void> _handleClose() async {
if (_isProcessing) return;
setState(() => _isProcessing = true);
final rdnaService = RdnaService.getInstance();
await rdnaService.resetAuthState();
setState(() => _isProcessing = false);
}
/// Handle Consent Action (Approve or Reject)
Future<void> _handleConsentAction(bool isOptIn) async {
setState(() {
_isProcessing = true;
_error = '';
});
print('IDVBiometricConsent - Processing consent: $isOptIn');
final rdnaIDVService = RdnaIDVService.getInstance();
await rdnaIDVService.setIDVBiometricOptInConsent(
isOptIn, // true = approve, false = reject
widget.challengeMode,
);
print('IDVBiometricConsent - Consent processed');
setState(() => _isProcessing = false);
}
/// Get consent description based on challenge mode
String _getConsentDescription() {
return 'Your selfie biometric template will be securely stored for identity verification. '
'This enables faster authentication and enhanced security for your account.';
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
body: SafeArea(
child: Stack(
children: [
SingleChildScrollView(
padding: const EdgeInsets.all(20).copyWith(top: 80),
child: Column(
children: [
// Title
const Text(
'Save Template',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Color(0xFF2C3E50),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
// Subtitle
const Text(
'Biometric Template Storage Consent',
style: TextStyle(fontSize: 16, color: Color(0xFF7F8C8D)),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
// Consent Description
Container(
decoration: BoxDecoration(
color: const Color(0xFFF0F8FF),
borderRadius: BorderRadius.circular(8),
border: const Border(
left: BorderSide(color: Color(0xFF3498DB), width: 4),
),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'What you need to know:',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF2C3E50),
),
),
const SizedBox(height: 12),
Text(
_getConsentDescription(),
style: const TextStyle(fontSize: 15, color: Color(0xFF34495E)),
),
const SizedBox(height: 12),
// Benefits List
const Text('• Your biometric data will be encrypted and stored securely',
style: TextStyle(fontSize: 14, color: Color(0xFF34495E))),
const SizedBox(height: 6),
const Text('• This enables faster authentication for future sessions',
style: TextStyle(fontSize: 14, color: Color(0xFF34495E))),
const SizedBox(height: 6),
const Text('• You can manage your consent preferences at any time',
style: TextStyle(fontSize: 14, color: Color(0xFF34495E))),
const SizedBox(height: 6),
const Text('• Rejecting will not affect your current authentication',
style: TextStyle(fontSize: 14, color: Color(0xFF34495E))),
],
),
),
const SizedBox(height: 24),
// Error Banner
if (_error.isNotEmpty)
StatusBanner(type: 'error', message: _error),
// Action Buttons
Row(
children: [
Expanded(
child: CustomButton(
title: _isProcessing ? 'Processing...' : 'Reject',
onPressed: () => _handleConsentAction(false),
variant: 'secondary',
disabled: _isProcessing,
),
),
const SizedBox(width: 12),
Expanded(
child: CustomButton(
title: _isProcessing ? 'Processing...' : 'Approve',
onPressed: () => _handleConsentAction(true),
disabled: _isProcessing,
),
),
],
),
const SizedBox(height: 20),
// Help Text
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'By approving, you consent to the secure storage of your biometric template for identity verification purposes.',
style: TextStyle(fontSize: 14, color: Color(0xFF7F8C8D)),
textAlign: TextAlign.center,
),
),
],
),
),
// Close Button
CustomCloseButton(
onPressed: _handleClose,
disabled: _isProcessing,
),
],
),
),
);
}
}
The following image showcases the 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",
challengeMode: 4, // RDNA_OP_SET_PWD
// ... password event data
}
This is the same password setup flow from the MFA Activation codelab. After password is set, the user is fully activated with IDV verification complete!
1. ✅ SDK Initialization
2. ✅ User Registration (getUser)
3. ✅ Activation Code Verification
4. ✅ IDV Document Scan → SDK captures document
5. ✅ IDV Document Details Confirmation → User reviews OCR data
6. ✅ IDV Selfie Capture → SDK captures selfie with liveness
7. ✅ IDV Selfie Confirmation → User reviews biometric match
8. ✅ IDV Biometric Consent → User approves/rejects template storage
9. ✅ Password Setup → User creates password
10. ✅ onUserLoggedIn → User authenticated and verified!
flutter run -d ios # or android
Issue | Solution |
Document scan fails | Ensure Regula assets (db.dat, license) are properly configured |
OCR data empty | Check document quality, lighting, and ensure document is supported |
Selfie liveness fails | Use real device (not emulator), ensure good lighting, face clearly visible |
Events not firing | Verify SDKIDVEventProvider is properly wrapped in main.dart |
Navigation not working | Check app_router.dart has all 5 IDV routes configured |
// Enable detailed logging
print('Event data: ${widget.eventData}');
final parsed = jsonDecode(widget.eventData);
print('Parsed: $parsed');
// Check event flow
// Each API call should trigger the next event
// Document scan -> Document details -> Selfie start -> Selfie confirm -> Consent -> Password
Congratulations! You've successfully implemented the REL-ID Identity Verification MFA Activation Flow in Flutter!
✅ Complete IDV Integration
✅ Flutter Architecture Mastery
✅ Production-Ready Implementation
Thank you for completing the REL-ID Identity Verification MFA Activation Flow codelab! 🎉
You're now ready to build secure, verified user experiences with Flutter and REL-ID IDV!