🎯 Learning Path:

  1. Complete REL-ID Initialization Flow Codelab
  2. Complete REL-ID Complete Activation & Login Flow Codelab
  3. You are here → Identity Verification MFA Activation Flow Implementation

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.

What You'll Build

In this codelab, you'll enhance your existing REL-ID MFA activation flow with integrated identity verification:

Key Difference: IDV During Activation vs MFA Activation

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

What You'll Learn

Prerequisites

Technology Stack

IDV MFA Activation Flow Diagram

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

IDV Workflow and Challenge Mode Values

For MFA activation flow, you'll use these specific values:

Parameter

Value

Description

idvWorkflow

0

RDNAIDVWorkflowActivation - Standard activation workflow

challengeMode

8

Activation challenge mode

Before You Begin

Ensure you have completed these requirements:

1. Previous Codelabs Completed

2. Development Environment

# Required tools
Flutter SDK >= 3.10.0
npm or yarn
React Native CLI
Xcode (for iOS) or Android Studio (for Android)

3. REL-ID SDK Credentials

Get the Code from GitHub

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

Project Structure

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

Installation

Step 1: Clone and Setup

# 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

Step 2: iOS Setup (CocoaPods)

cd ios && flutter pub get (handles iOS CocoaPods automatically) && cd ..

Step 3: Configure IDV Assets

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:

iOS Configuration:

Place the following files (provided by Uniken admin team) at these locations:

Step 4: Run the Application

# Android
flutter run -d android

# iOS
flutter run -d ios

Architecture Overview: Code Isolation & Single Responsibility

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.

Key Architectural Benefits

Complete IDV File Structure

The 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 3-Layer Architecture Pattern

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)                │
└─────────────────────────────────────────────────────┘

How the Layers Connect: Event Flow Example

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...

Key Architectural Patterns

1. Singleton Pattern (Event Manager & Service)

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.

2. Context Provider Pattern (Event Provider)

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.

3. Type-Safe Events (TypeScript Interfaces)

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.

4. Future-Based API Wrapper (Service)

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.

App Integration: Provider Nesting

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

Architecture Benefits Summary

1. Code Isolation

✅ All IDV code under idv/ subdirectories
✅ Easy to locate and modify IDV features
✅ No mixing of MFA and IDV code
✅ Clean separation of concerns

2. Single Responsibility

✅ 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

3. Auto-Initialization

✅ Event manager initializes on module load
✅ Listeners registered before native events fire
✅ Prevents race conditions
✅ No manual initialization required

4. Type Safety

✅ Complete TypeScript interfaces for all events
✅ Reusable common types (RDNAError, RDNAChallengeResponse)
✅ SDK typedef or class integration (RDNAIDVWorkflow, RDNAChallengeOpMode)
✅ Compile-time typedef or class checking

5. Centralized Event Management

✅ All navigation logic in SDKIDVEventProvider
✅ Single source of truth for event handling
✅ Consistent event-to-screen mapping
✅ Easy to trace event flow

Overview

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.

File Location

lib/uniken/services/idv/rdna_idv_event_manager.dart

Key Responsibilities

  1. ✅ Register Native Listeners: Automatically registers all 5 IDV event listeners on module load
  2. ✅ Parse Event Data: Converts JSON responses to type-safe interfaces
  3. ✅ Dispatch to Handlers: Calls registered Dart callbacks with parsed data
  4. ✅ Prevent Race Conditions: Initializes before any native events can fire
  5. ✅ Cleanup Management: Removes listeners and clears handlers on cleanup

Complete Implementation

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;
  }
}

Key Implementation Details

1. Singleton Pattern

private static instance = new RdnaIDVEventManager(); // Created on module load

private constructor() {
  this.registerEventListeners(); // Auto-registers immediately
}

static getInstance() {
  return RdnaIDVEventManager.instance;
}

2. Event Handler Storage

// One handler per event typedef or class (simple, not array-based)
private getDocumentScanStartConfirmationHandler?: Callback;
private getConfirmDocumentDetailsHandler?: Callback;
// ... etc for all 5 events

3. Public API Pattern

// 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);
  }
}

Types Come from the Plugin

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!

How It Works

// 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

IDV Event Data Format

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');
  }
}

Key Differences from MFA Events

Event Type

Data Format

Example

MFA Events

Typed objects

RDNAGetUser, RDNAGetPassword

IDV Events

Raw JSON strings

Parse with jsonDecode()

Why This Design?

Understanding the IDV Service Architecture

The IDV functionality is implemented using a singleton service pattern that wraps the native RdnaClient IDV APIs with type-safe, Future-based interfaces.

rdna_idv_service.dart Implementation

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.

Step 2: Document Scan Start 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:

IDV Document Scan Start Screen

Step 3: Native Document Capture

When setIDVDocumentScanProcessStartConfirmation(true, 0) is called:

  1. SDK opens native Regula document scanner UI
  2. User positions document within frame
  3. Camera captures document image
  4. Regula SDK performs OCR extraction
  5. SDK triggers getIDVConfirmDocumentDetails event with extracted data

Step 4: Error Handling

When 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.

Key Implementation Points

  1. idvWorkflow Parameter: Always use value 0 (RDNAIDVWorkflowActivation) from event data
  2. Cancel Flow: Passing false cancels IDV and returns to previous workflow
  3. Native UI: SDK handles camera class - no custom UI needed
  4. Error Handling: Check sync response for immediate validation errors

Step 1: Event - getIDVConfirmDocumentDetails

After 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

Key Fields to Display on UI

When implementing the document confirmation screen, focus on these critical fields:

Step 2: Confirm Document Details Screen

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 - Success State

Document Details Screen

Document Details - Error State

Document Details Screen

Validation Status Meanings

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

Step 1: Event - getIDVSelfieProcessStartConfirmation

After document confirmation, SDK triggers:

{
  idvWorkflow: 0,  // RDNAIDVWorkflowActivation
  // Additional event data
}

Step 2: Selfie Process Start Screen

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:

IDV Selfie Process Start Screen

Step 3: Native Selfie Capture

When setIDVSelfieProcessStartConfirmation(true, false, 0) is called:

  1. SDK opens native camera with specified orientation (front/back)
  2. Liveness detection guides user through head movements
  3. Camera captures selfie image
  4. SDK performs biometric analysis:
    • Face matching with document photo
    • Liveness score calculation
  5. SDK triggers getIDVSelfieConfirmation event with results

Camera Selection

The useDeviceBackCamera parameter:

Step 1: Event - getIDVSelfieConfirmation

After 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" }
      ]
    }
  }
}

Step 2: Selfie Confirmation Screen

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:

IDV Selfie Confirmation Screen

Understanding Action Keys (Critical Concept)

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.

The Three Action Keys

Action Key

SDK Behavior

When to Use

"continue-flow"

Proceeds to next event (getIDVBiometricOptInConsent for activation)

User accepts selfie match result and wants to continue

"reinit-idv-document-scan"

Triggers document rescan (fires getIDVDocumentScanProcessStartConfirmation event)

User believes document scan was poor quality or incorrect

"reinit-idv-selfie"

Initiates selfie recapture flow

User wants to retake selfie (poor lighting, closed eyes, etc.)

Server-Driven UI Pattern

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.

Implementation Flow

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

Why This Design Matters

Code Example: Dynamic Button Rendering

final 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"
    }
  }
}

Step 2: Biometric Consent Screen

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:

IDV Biometric Opt-In Consent Screen

Consent Options Impact

Action

Storage

Impact

Approve (true)

Biometric template stored securely

Faster future authentication, enhanced security

Reject (false)

Template not stored

Standard authentication methods only

Password Setup After IDV

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!

Complete IDV MFA Activation Flow Summary

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!

Testing the IDV Flow

  1. Prepare Test Documents
    • Valid government-issued ID (passport, driver's license, national ID)
    • Ensure good lighting and clear camera
    • Have test user credentials ready
  2. Test Complete Flow
    flutter run -d ios  # or android
    
  3. Verify Each Step
    • ✅ Document scan UI appears
    • ✅ OCR data extracted correctly
    • ✅ Selfie capture works with liveness
    • ✅ Biometric match result displayed
    • ✅ Consent screen appears
    • ✅ Password setup completes

Common Issues

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

Debugging Tips

// 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!

What You've Accomplished

✅ Complete IDV Integration

✅ Flutter Architecture Mastery

✅ Production-Ready Implementation

Key Takeaways

  1. Event-Driven Pattern: SDK events drive the entire IDV workflow
  2. No Try-Catch: Flutter IDV APIs return String, error handling via events
  3. Server-Controlled: Action buttons and workflows configured by backend
  4. Security First: Biometric data encrypted, consent-based storage

Next Steps

Resources

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!