🎯 Learning Path:

  1. Complete RELID Initialization Flow Codelab
  2. Complete RELID Complete Activation & Login Flow Codelab
  3. Complete RELID IDV MFA Activation Flow Codelab (Recommended)
  4. You are here β†’ Post-Login KYC Flow Implementation

Welcome to the RELID IDV Post-Login KYC Flow codelab! This tutorial teaches you how to implement comprehensive Know Your Customer (KYC) identity verification for activated customers using document scanning, facial recognition, and biometric authentication.

What You'll Build

In this codelab, you'll implement a complete post-login KYC workflow for activated customers:

Key Difference: Post-Login KYC vs Pre-Login IDV

This codelab demonstrates post-login KYC for activated customers, where identity verification occurs after user login for compliance purposes:

Pre-Login IDV Flow (Challenge Mode 8):
Activation Code β†’ IDV (Document β†’ Selfie β†’ Consent) β†’ Password Setup β†’ Dashboard

Post-Login KYC Flow (Challenge Mode 9 - This Codelab):
Login β†’ Dashboard β†’ Navigate to KYC β†’ IDV (Document β†’ Selfie β†’ Consent) β†’ Completion

Critical Differences:

What You'll Learn

Prerequisites

Technology Stack

Post-Login KYC Flow Diagram

Understanding the complete flow is crucial before implementation:

1. User Login (Activated Customer)
   ↓ (Existing credentials authenticated)

2. Dashboard β†’ Open Side Menu
   ↓ (User navigates to KYC option)

3. πŸ†• Activated Customer KYC Screen
   β”œβ”€ Display KYC information and requirements
   β”œβ”€ User clicks "Start KYC Verification"
   β”œβ”€ View controller-level event handler registered (persistent)
   β”œβ”€ initiateActivatedCustomerKYC("Post Login KYC")
   β”‚  β€’ workflow: 6 (RDNA_IDV_POSTLOGIN_KYC)
   β”‚  β€’ challengeMode: 9 (post-login)
   └─ SDK initiates post-login KYC workflow
   ↓

4. πŸ“„ IDV Document Scan Process
   β”œβ”€ getIDVDocumentScanProcessStartConfirmation event
   β”‚  β€’ idvWorkflow: 6 (RDNA_IDV_POSTLOGIN_KYC)
   β”œβ”€ setIDVDocumentScanProcessStartConfirmation(true, 6)
   β”œβ”€ Native Document Capture (Regula SDK)
   β”œβ”€ getIDVConfirmDocumentDetails event
   β”‚  β€’ challengeMode: 9
   β”‚  β€’ documentDetails: OCR-extracted document fields
   β”‚  β€’ Close button: cancel API (mode 9) β†’ triggers onIDVActivatedCustomerKYCResponse
   └─ setIDVConfirmDocumentDetails(true, 9)
   ↓

5. 🀳 IDV Selfie Capture Process
   β”œβ”€ getIDVSelfieProcessStartConfirmation event
   β”‚  β€’ idvWorkflow: 6 (RDNA_IDV_POSTLOGIN_KYC)
   β”œβ”€ setIDVSelfieProcessStartConfirmation(true, false, 6)
   β”œβ”€ Native Selfie Capture (Liveness Detection)
   β”œβ”€ getIDVSelfieConfirmation event
   β”‚  β€’ challengeMode: 9
   β”‚  β€’ selfieDetails: Biometric match results
   β”‚  β€’ Close button: navigate directly (mode 9) β†’ no cancel API
   └─ setIDVSelfieConfirmation("true", 9)
   ↓

6. πŸ” IDV Biometric Consent
   β”œβ”€ getIDVBiometricOptInConsent event
   β”‚  β€’ challengeMode: 9
   β”‚  β€’ status at root level (no challengeResponse for mode 9)
   β”‚  β€’ Close button: navigate directly (mode 9)
   └─ setIDVBiometricOptInConsent(true, 9)
   ↓

7. βœ… KYC Completion
   β”œβ”€ onIDVActivatedCustomerKYCResponse event triggered
   β”œβ”€ View controller-level handler receives event (even if on different screen)
   β”œβ”€ Handler navigates back to ActivatedCustomerKYCViewController
   β”‚  β€’ navigationController?.popToViewController(_, animated: true)
   β”œβ”€ Dual validation performed:
   β”‚  β€’ Check error.longErrorCode (0 = OK, 240 = User Cancelled)
   β”‚  β€’ Check status.statusCode (100 or 0 = success)
   β”œβ”€ Success/Error alert shown
   └─ User clicks "Go to Dashboard" β†’ Navigate to Dashboard

IDV Workflow and Challenge Mode Values

For post-login KYC flow, you'll use these specific values:

Parameter

Value

Description

idvWorkflow

6

RDNA_IDV_POSTLOGIN_KYC - Post-login KYC workflow

challengeMode

9

Post-login challenge mode

Comparison with Pre-Login Flow:

Aspect

Pre-Login (Activation)

Post-Login (KYC)

Workflow

0-5 (Activation workflows)

6 (RDNA_IDV_POSTLOGIN_KYC)

Challenge Mode

8

9

Entry Point

ActivationCodeViewController

ActivatedCustomerKYCViewController (side menu)

Close Button (Document Confirm)

resetAuthState()

cancel API β†’ triggers event

Close Button (Selfie Confirm)

resetAuthState()

Direct navigation

Close Button (Biometric Consent)

resetAuthState()

Direct navigation

Response Structure

challengeResponse.status

status at root level

Event Handler

Global delegate only

View controller-level + global delegate

Before You Begin

Ensure you have completed these requirements:

1. Previous Codelabs Completed

2. Development Environment

# Required tools
Xcode >= 14.0
Swift >= 5.5
CocoaPods (for dependency management)

Verify your Xcode installation:

xcode-select --version

3. RELID 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-ios.git

Navigate to the relid-IDV-post-login-kyc folder in the repository you cloned earlier:

cd relid-codelab-ios/relid-IDV-post-login-kyc

Project Structure

The post-login KYC codelab follows this comprehensive structure:

relid-IDV-post-login-kyc/
β”œβ”€β”€ πŸ“± Complete Swift/iOS MFA + IDV App
β”‚   β”œβ”€β”€ relidcodelab.xcodeproj       # Xcode project
β”‚   β”œβ”€β”€ relidcodelab.xcworkspace     # CocoaPods workspace (USE THIS!)
β”‚   β”œβ”€β”€ Podfile                       # CocoaPods configuration
β”‚   β”œβ”€β”€ Pods/                         # CocoaPods dependencies (auto-generated)
β”‚   β”‚   β”œβ”€β”€ DocumentReader/               # Regula document reader (via RELIDIDV pod)
β”‚   β”‚   β”œβ”€β”€ DocumentReaderFullAuthRFID/   # Regula RFID authentication (via RELIDIDV pod)
β”‚   β”‚   β”œβ”€β”€ onnxruntime-c/                # ONNX runtime for ML models (via RELIDIDV pod)
β”‚   β”‚   β”œβ”€β”€ IQKeyboardManagerSwift/       # Keyboard management
β”‚   β”‚   β”œβ”€β”€ SideMenu/                     # Drawer navigation
β”‚   β”‚   └── Firebase/                     # Push notifications (FCM)
β”‚   β”œβ”€β”€ RELID.xcframework             # πŸ“¦ REL-ID Native SDK (Main SDK - REQUIRED)
β”‚   β”‚                                     # - Core MFA authentication APIs
β”‚   β”‚                                     # - Base RDNA client functionality
β”‚   β”‚                                     # - Session management
β”‚   β”œβ”€β”€ RELID_IDV/                    # πŸ“¦ REL-ID IDV Local Pod (IDV Components - REQUIRED)
β”‚   β”‚   β”œβ”€β”€ RELIDIDV.podspec              # Pod specification
β”‚   β”‚   └── AwFaceCapture.xcframework     # Biometric selfie capture framework
β”‚   β”‚                                     # Dependencies (auto-installed via CocoaPods):
β”‚   β”‚                                     #   - DocumentReader 8.2.4934 (Regula)
β”‚   β”‚                                     #   - DocumentReaderFullAuthRFID 8.2.13463
β”‚   β”‚                                     #   - onnxruntime-c (ML runtime)
β”‚   └── relidcodelab/idv-artifacts/   # πŸ†• IDV Assets Directory (REQUIRED)
β”‚       β”œβ”€β”€ db.dat                        # Regula document recognition database (105MB)
β”‚       β”œβ”€β”€ regula.license                # Regula document reader license for iOS
β”‚       └── Certificates.bundle/          # ICAO PKD certificates bundle
β”‚           └── icaopkd-002-complete-000325.ldif  # Document validation certificates
β”‚
β”œβ”€β”€ πŸ“¦ IDV Post-Login KYC Source Architecture
β”‚   └── relidcodelab/Sources/
β”‚       β”œβ”€β”€ Tutorial/                 # Enhanced MFA + IDV flow
β”‚       β”‚   β”œβ”€β”€ Navigation/           # Enhanced navigation with IDV support
β”‚       β”‚   β”‚   β”œβ”€β”€ AppCoordinator.swift            # Event-driven navigation coordinator
β”‚       β”‚   β”‚   └── AppIDVCoordinator.swift         # πŸ†• IDV-specific coordinator
β”‚       β”‚   β”‚                                      # - IDV event routing
β”‚       β”‚   β”‚                                      # - Screen state management
β”‚       β”‚   β”‚                                      # - showActivatedCustomerKYC()
β”‚       β”‚   └── Screens/              # Enhanced screens with IDV
β”‚       β”‚       β”œβ”€β”€ Components/       # Reusable UI components
β”‚       β”‚       β”‚   └── UIColor+Extensions.swift
β”‚       β”‚       β”œβ”€β”€ MFA/              # πŸ” MFA screens (base authentication)
β”‚       β”‚       β”‚   β”œβ”€β”€ CheckUserViewController.swift        # User validation
β”‚       β”‚       β”‚   β”œβ”€β”€ ActivationCodeViewController.swift   # OTP verification
β”‚       β”‚       β”‚   β”œβ”€β”€ SetPasswordViewController.swift      # Password creation
β”‚       β”‚       β”‚   β”œβ”€β”€ VerifyPasswordViewController.swift   # Password verification
β”‚       β”‚       β”‚   β”œβ”€β”€ DashboardViewController.swift        # Main dashboard
β”‚       β”‚       β”‚   β”œβ”€β”€ MenuViewController.swift             # πŸ†• Enhanced with KYC menu item
β”‚       β”‚       β”‚   └── ...                                  # Other MFA screens
β”‚       β”‚       └── IDV/              # πŸ†• IDV Post-Login KYC screens (6 screens)
β”‚       β”‚           β”œβ”€β”€ ActivatedCustomerKYCViewController.swift              # πŸ†• Post-Login KYC entry point
β”‚       β”‚           β”‚                                                        # - KYC initiation screen
β”‚       β”‚           β”‚                                                        # - Persistent event handling
β”‚       β”‚           β”œβ”€β”€ IDVDocumentProcessStartConfirmationViewController.swift  # Document scan initiation
β”‚       β”‚           β”œβ”€β”€ IDVConfirmDocumentDetailsViewController.swift            # πŸ†• Enhanced for mode 9
β”‚       β”‚           β”œβ”€β”€ IDVSelfieProcessStartConfirmationViewController.swift    # Selfie capture initiation
β”‚       β”‚           β”œβ”€β”€ IDVSelfieConfirmationViewController.swift               # πŸ†• Enhanced for mode 9
β”‚       β”‚           └── IDVBiometricOptInConsentViewController.swift            # πŸ†• Enhanced for mode 9
β”‚       └── Uniken/                   # πŸ›‘οΈ Enhanced REL-ID Integration
β”‚           β”œβ”€β”€ Services/             # πŸ†• Enhanced SDK service layer
β”‚           β”‚   β”œβ”€β”€ RDNAService.swift                # Base MFA service layer
β”‚           β”‚   β”œβ”€β”€ RDNADelegateManager.swift        # Base MFA delegate callbacks
β”‚           β”‚   └── IDV/                             # πŸ†• IDV service layer
β”‚           β”‚       β”œβ”€β”€ RDNAIDVService.swift         # Complete IDV API methods
β”‚           β”‚       β”‚                                # - πŸ†• initiateActivatedCustomerKYC()
β”‚           β”‚       β”‚                                # - setIDVDocumentScanProcessStartConfirmation()
β”‚           β”‚       β”‚                                # - setIDVConfirmDocumentDetails()
β”‚           β”‚       β”‚                                # - setIDVSelfieProcessStartConfirmation()
β”‚           β”‚       β”‚                                # - setIDVSelfieConfirmation()
β”‚           β”‚       β”‚                                # - setIDVBiometricOptInConsent()
β”‚           β”‚       └── RDNAIDVDelegateManager.swift # Complete IDV delegate callbacks
β”‚           β”‚                                        # - getIDVDocumentScanProcessStartConfirmation
β”‚           β”‚                                        # - getIDVConfirmDocumentDetails
β”‚           β”‚                                        # - getIDVSelfieProcessStartConfirmation
β”‚           β”‚                                        # - getIDVSelfieConfirmation
β”‚           β”‚                                        # - getIDVBiometricOptInConsent
β”‚           β”‚                                        # - πŸ†• onIDVActivatedCustomerKYCResponse
β”‚           β”œβ”€β”€ Utils/                # Helper utilities
β”‚           β”‚   └── ConnectionProfileParser.swift   # Profile configuration
β”‚           └── CP/                   # Connection Profile
β”‚               └── agent_info.json
β”‚
└── πŸ“š Project Configuration
    β”œβ”€β”€ Podfile                      # CocoaPods dependencies (RELID SDK, Regula, IQKeyboardManager)
    β”œβ”€β”€ Info.plist                   # Camera permissions configuration
    └── Base.lproj/
        β”œβ”€β”€ Main.storyboard          # πŸ†• Complete UI with all MFA + IDV + KYC scenes
        └── LaunchScreen.storyboard

Installation

Step 1: Clone and Setup

# Navigate to the codelab folder
cd relid-codelab-ios/relid-IDV-post-login-kyc

# Add the `RELID.xcframework` to your project:
#   - Drag and drop `RELID.xcframework` into your Xcode project
#   - Select your target β†’ General β†’ Frameworks, Libraries, and Embedded Content
#   - Set embed mode to "Embed & Sign"

# Place the RELIDIDV local pod (IDV libs) at ./RELID_IDV/
# (Contact REL-ID team for the framework)

Step 2: Install CocoaPods Dependencies

# Install CocoaPods (if not already installed)
sudo gem install cocoapods

# Install dependencies
pod install

Step 3: Configure IDV Assets

The following IDV assets need to be configured in your project at the specified paths:

iOS Configuration:

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

Step 4: Verify Asset Configuration

After adding IDV assets to your project:

# Verify IDV assets are present
ls relidcodelab/idv-artifacts/
# Should show: db.dat, regula.license, Certificates.bundle/

# Check file sizes
du -h relidcodelab/idv-artifacts/db.dat
# Should show: ~105M

In Xcode:

  1. Select each asset file in Project Navigator
  2. Check File Inspector (right panel)
  3. Verify Target Membership includes relidcodelab
  4. Confirm files are in Copy Bundle Resources build phase

Step 5: Configure required permissions in Info.plist:

<!-- Camera Access -->
<key>NSCameraUsageDescription</key>
<string>Camera access is required for document scanning and selfie capture</string>

<!-- NFC Access (for ePassport scanning) -->
<key>NFCReaderUsageDescription</key>
<string>NFC access for reading electronic passport data</string>
<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
<array>
    <string>A0000002471001</string>
    <string>A0000002472001</string>
    <string>00000000000000</string>
</array>

Step 6: Open and Run the Application

# Open the workspace (NOT the .xcodeproj!)
open relidcodelab.xcworkspace

In Xcode:

  1. Select a simulator or device target
  2. Click Product β†’ Run (or press ⌘R)
  3. The app will build and launch

How iOS SDKs Work

iOS SDKs are distributed as frameworks (.framework or .xcframework) that link with your application.

RELID iOS SDK Architecture:

Integration Pattern

The RELID iOS SDK uses a delegate pattern for event callbacks:

import UIKit
import RELID

class ActivatedCustomerKYCViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Register event handler
        setupEventHandlers()
    }

    private func setupEventHandlers() {
        // Register persistent event handler for KYC completion
        RDNAIDVDelegateManager.shared.onIDVActivatedCustomerKYCResponse = { [weak self] userID, status, error in
            self?.handleKYCCompletion(userID: userID, status: status, error: error)
        }
    }

    @IBAction func startKYCButtonTapped(_ sender: UIButton) {
        // Initiate KYC workflow
        let error = RDNAIDVService.shared.initiateActivatedCustomerKYC(reason: "Post Login KYC")

        if error.longErrorCode == 0 {
            print("KYC initiated successfully")
            // SDK will auto-trigger IDV workflow events
        } else {
            print("Failed to initiate KYC: \\(error.errorString ?? "")")
        }
    }

    private func handleKYCCompletion(userID: String, status: RDNARequestStatus, error: RDNAError) {
        // Dual validation pattern
        let errorCode = error.longErrorCode
        let statusCode = status.statusCode

        if errorCode == 0 && (statusCode == 100 || statusCode == 0) {
            // Success
            showSuccessAlert()
        } else {
            // Error
            showErrorAlert(message: error.errorString ?? "KYC verification failed")
        }
    }
}

Key SDK Components

RDNAIDVService:

RDNAIDVDelegateManager:

View Controllers:

Overview

The RDNAIDVService class provides a singleton service for Identity Verification (IDV) related SDK operations. It wraps the native RELID SDK IDV API with type-safe interfaces.

RDNAIDVService.swift

The service includes the initiateActivatedCustomerKYC() method which initiates the post-login KYC workflow:

// Sources/Uniken/Services/IDV/RDNAIDVService.swift

import Foundation
import RELID

/**
 * REL-ID IDV SDK Service
 *
 * Provides a singleton service for Identity Verification (IDV) related SDK operations.
 * Wraps the native RELID SDK IDV API with type-safe interface.
 * Handles document scanning, selfie capture, and biometric consent operations.
 *
 * 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
 *
 * **KYC APIs:**
 * - initiateActivatedCustomerKYC: Initiate post-login KYC workflow
 */
class RDNAIDVService {

    // MARK: - Singleton

    static let shared = RDNAIDVService()
    private let rdna = RDNA.sharedInstance()

    private init() {
        print("RDNAIDVService - Service initialized")
    }

    // MARK: - KYC APIs

    /**
     * Initiates activated customer KYC workflow (Post-Login)
     *
     * This API initiates the KYC (Know Your Customer) process for an already activated customer
     * who needs to complete identity verification post-login.
     *
     * **Workflow:**
     * 1. User must be logged in (user session established)
     * 2. Call this API to start post-login KYC
     * 3. SDK automatically triggers IDV workflow events:
     *    - getIDVDocumentScanProcessStartConfirmation (workflow: IDV_POSTLOGIN_KYC = 6)
     *    - getIDVConfirmDocumentDetails (challengeMode: RDNA_IDV_API_BIOMETRIC_AND_DOCUMENT_SCAN = 9)
     *    - getIDVSelfieProcessStartConfirmation (workflow: IDV_POSTLOGIN_KYC = 6)
     *    - getIDVSelfieConfirmation (challengeMode: RDNA_IDV_API_BIOMETRIC_AND_DOCUMENT_SCAN = 9)
     *    - getIDVBiometricOptInConsent (challengeMode: RDNA_IDV_API_BIOMETRIC_AND_DOCUMENT_SCAN = 9)
     * 4. Finally triggers: onIDVActivatedCustomerKYCResponse
     *
     * **Response Validation:**
     * 1. Check error.longErrorCode immediately (synchronous error check)
     * 2. Wait for onIDVActivatedCustomerKYCResponse callback for final result
     * 3. In callback, validate BOTH:
     *    - error.longErrorCode (SDK error)
     *    - status.statusCode (backend error)
     * 4. Success only if: error.longErrorCode == 0 AND (statusCode == 100 OR statusCode == 0)
     *
     * - Parameter reason: Reason for initiating KYC (e.g., "Post Login KYC")
     * - Returns: RDNAError indicating immediate success or failure
     */
    func initiateActivatedCustomerKYC(reason: String) -> RDNAError {
        print("RDNAIDVService - Initiating activated customer KYC: reason=\\(reason)")

        let error = rdna.initiateActivatedCustomerKYC(reason)

        if error.longErrorCode == 0 {
            print("RDNAIDVService - Activated customer KYC initiated successfully")
            print("RDNAIDVService - SDK will auto-trigger IDV workflow events (workflow=6, challengeMode=9)")
        } else {
            print("RDNAIDVService - Failed to initiate activated customer KYC: \\(error.longErrorCode) - \\(error.errorString ?? "")")
        }

        return error
    }

    // MARK: - Document Scan APIs

    func setIDVDocumentScanProcessStartConfirmation(
        isConfirm: Bool,
        idvWorkflow: RDNAIDVWorkflow
    ) -> RDNAError {
        // Implementation as shown in previous sections
        return rdna.setIDVDocumentScanProcessStartConfirmation(isConfirm, idvWorkflow: idvWorkflow)
    }

    func setIDVConfirmDocumentDetails(
        isConfirm: Bool,
        challengeMode: RDNAChallengeOpMode
    ) -> RDNAError {
        // Implementation as shown in previous sections
        return rdna.setIDVConfirmDocumentDetails(isConfirm, challenge: challengeMode)
    }

    // MARK: - Selfie Capture APIs

    func setIDVSelfieProcessStartConfirmation(
        isConfirm: Bool,
        useDeviceBackCamera: Bool,
        idvWorkflow: RDNAIDVWorkflow
    ) -> RDNAError {
        // Implementation as shown in previous sections
        return rdna.setIDVSelfieProcessStartConfirmation(isConfirm, useDeviceBackCamera: useDeviceBackCamera, idvWorkflow: idvWorkflow)
    }

    func setIDVSelfieConfirmation(
        action: String,
        challengeMode: RDNAChallengeOpMode
    ) -> RDNAError {
        // Implementation as shown in previous sections
        return rdna.setIDVSelfieConfirmation(action, challenge: challengeMode)
    }

    // MARK: - Biometric Consent APIs

    func setIDVBiometricOptInConsent(
        isOptIn: Bool,
        challengeMode: RDNAChallengeOpMode
    ) -> RDNAError {
        // Implementation as shown in previous sections
        return rdna.setIDVBiometricOptInConsent(isOptIn, challenge: challengeMode)
    }
}

Key Points

Overview

The RDNAIDVDelegateManager class manages IDV-specific SDK callbacks. It implements IDV protocol methods from RELID SDK and dispatches to closures.

RDNAIDVDelegateManager.swift

The delegate manager includes the onIDVActivatedCustomerKYCResponse closure property:

// Sources/Uniken/Services/IDV/RDNAIDVDelegateManager.swift

import Foundation
import RELID

/**
 * RDNAIDVDelegateManager - Manages IDV-specific SDK callbacks
 *
 * Implements IDV protocol methods from RELID SDK and dispatches to closures.
 * Handles document scanning, selfie capture, and biometric consent callbacks.
 *
 * **IDV Protocols Implemented (Modular):**
 * - RDNAIDVDocumentProcessingCallbacks (6 methods)
 * - RDNAIDVSelfieProcessingCallbacks (2 methods)
 * - RDNAIDVBiometricManagementCallbacks (10 methods)
 * - RDNAIDVKYCManagementCallbacks (6 methods)
 */
class RDNAIDVDelegateManager: NSObject,
                               RDNAIDVDocumentProcessingCallbacks,
                               RDNAIDVSelfieProcessingCallbacks,
                               RDNAIDVBiometricManagementCallbacks,
                               RDNAIDVKYCManagementCallbacks {

    // MARK: - Singleton

    static let shared = RDNAIDVDelegateManager()

    private override init() {
        super.init()
        print("RDNAIDVDelegateManager - Initialized")
    }

    // MARK: - KYC Callback Closures

    /// Closure invoked when activated customer KYC process completes
    /// IDV-SPECIFIC (POST-LOGIN): Navigate back to ActivatedCustomerKYCViewController
    /// This callback fires after completing the entire IDV workflow (document scan, selfie, biometric consent)
    /// - Parameters:
    ///   - userID: User ID who completed KYC
    ///   - status: Status with statusCode (100=success, other=error)
    ///   - error: Error information (check longErrorCode)
    var onIDVActivatedCustomerKYCResponse: ((_ userID: String, _ status: RDNARequestStatus, _ error: RDNAError) -> Void)?

    // MARK: - Document Scan Callback Closures

    var onGetIDVDocumentScanProcessStartConfirmation: ((_ idvWorkflow: RDNAIDVWorkflow) -> Void)?
    var onGetIDVConfirmDocumentDetails: ((_ challengeMode: RDNAChallengeOpMode, _ responseDataJSON: String) -> Void)?

    // MARK: - Selfie Capture Callback Closures

    var onGetIDVSelfieProcessStartConfirmation: ((_ idvWorkflow: RDNAIDVWorkflow) -> Void)?
    var onGetIDVSelfieConfirmation: ((_ userID: String, _ challengeMode: RDNAChallengeOpMode, _ responseDataJSON: String) -> Void)?

    // MARK: - Biometric Consent Callback Closures

    var onGetIDVBiometricOptInConsent: ((_ userID: String, _ attemptsLeft: Int, _ challengeMode: RDNAChallengeOpMode, _ idvResponseJSON: String, _ status: [AnyHashable: Any], _ challengeResponse: RDNAChallengeResponse, _ error: RDNAError) -> Void)?
}

// MARK: - RDNAIDVKYCManagementCallbacks Protocol

extension RDNAIDVDelegateManager {

    /// onIDVActivatedCustomerKYCResponse - Called with activated customer KYC response
    /// **Callback Flow:**
    /// 1. User calls initiateActivatedCustomerKYC("Post Login KYC")
    /// 2. SDK triggers IDV workflow events (document scan, selfie, biometric consent)
    /// 3. After workflow completes, this callback fires
    /// 4. Navigate back to ActivatedCustomerKYCViewController
    /// 5. Show success/error alert modal
    ///
    /// **Dual Validation Pattern:**
    /// - First check: error.longErrorCode (SDK synchronous error)
    /// - Second check: status.statusCode (backend async validation)
    /// - Success: longErrorCode == 0 AND (statusCode == 100 OR statusCode == 0)
    func onIDVActivatedCustomerKYCResponse(_ userID: String, status: RDNARequestStatus, error: RDNAError) {
        print("RDNAIDVDelegateManager - onIDVActivatedCustomerKYCResponse called")
        print("RDNAIDVDelegateManager - UserID: \\(userID)")
        print("RDNAIDVDelegateManager - Error Code: \\(error.longErrorCode) - \\(error.errorString ?? "")")
        print("RDNAIDVDelegateManager - Status Code: \\(status.statusCode) - \\(status.statusMessage ?? "")")

        DispatchQueue.main.async { [weak self] in
            self?.onIDVActivatedCustomerKYCResponse?(userID, status, error)
        }
    }

    // Other IDV protocol methods implemented here...
    // (Document scan, selfie, biometric consent delegate methods)
}

Key Features

Overview

The post-login KYC flow is accessed through the side menu (drawer navigation) available from the Dashboard. The menu includes a dedicated "Activated Customer KYC" option that navigates users to the KYC entry point.

MenuViewController Integration

Add the KYC menu item to MenuViewController.swift:

// Sources/Tutorial/Screens/MFA/MenuViewController.swift

enum MenuAction {
    case dashboard
    case deviceManagement
    case activatedCustomerKYC  // πŸ†• New menu action
    // ... other actions
}

private func setupMenuItems() {
    menuItems = [
        MenuItem(icon: "🏠", title: "Dashboard", subtitle: nil, action: .dashboard),
        MenuItem(icon: "πŸ“±", title: "Device Management", subtitle: nil, action: .deviceManagement),
        MenuItem(icon: "βœ“", title: "Activated Customer KYC", subtitle: nil, action: .activatedCustomerKYC),  // πŸ†• New item
        // ... other items
    ]
}

private func handleMenuAction(_ action: MenuAction) {
    switch action {
    case .activatedCustomerKYC:
        navigateToActivatedCustomerKYC(using: navigationController)
    // ... other cases
    }
}

private func navigateToActivatedCustomerKYC(using navController: UINavigationController?) {
    // Check if already on the screen
    if navController?.viewControllers.last is ActivatedCustomerKYCViewController {
        return
    }

    // Navigate using IDV Coordinator
    AppIDVCoordinator.shared.showActivatedCustomerKYC()
}

AppIDVCoordinator Navigation

Add the public navigation method to AppIDVCoordinator.swift:

// Sources/Tutorial/Navigation/AppIDVCoordinator.swift

class AppIDVCoordinator {
    static let shared = AppIDVCoordinator()

    /// Navigate to Activated Customer KYC screen (Post-Login)
    func showActivatedCustomerKYC() {
        guard let vc = storyboard.instantiateViewController(
            withIdentifier: "ActivatedCustomerKYCViewController"
        ) as? ActivatedCustomerKYCViewController else {
            print("IDVCoordinator - Failed to instantiate ActivatedCustomerKYCViewController")
            return
        }

        DispatchQueue.main.async { [weak self] in
            self?.navigationController?.pushViewController(vc, animated: true)
            print("IDVCoordinator - Navigated to ActivatedCustomerKYC")
        }
    }
}

Side Menu UI Preview

Side Menu with KYC Item

Overview

The ActivatedCustomerKYCViewController is the entry point for post-login KYC verification. It displays KYC information, educational content, and initiates the KYC workflow.

ActivatedCustomerKYC Screen UI Preview

Activated Customer KYC Screen - Initial State

Initial State - Ready to Start KYC

Activated Customer KYC Screen - Processing State

Processing State - KYC Initiated

ActivatedCustomerKYCViewController.swift

// Sources/Tutorial/Screens/IDV/ActivatedCustomerKYCViewController.swift

import UIKit
import RELID

/**
 * ActivatedCustomerKYCViewController
 *
 * Entry point screen for post-login KYC verification flow for activated customers.
 * Features:
 * - Initiates activated customer KYC workflow via initiateActivatedCustomerKYC API
 * - View controller-level persistent event handling for onIDVActivatedCustomerKYCResponse
 * - Dual validation pattern (SDK sync error + backend status code)
 * - Educational content about KYC process
 * - Tutorial-style UI with numbered steps
 *
 * **Event Handler Lifecycle:**
 * - Registered on viewDidLoad (persists even when IDV screens are on top)
 * - Only cleanup on deinit
 * - Navigates back to this screen automatically after KYC completion
 *
 * **Dual Validation Pattern:**
 * 1. Check error.longErrorCode first (SDK synchronous error)
 * 2. Then check status.statusCode (backend asynchronous validation)
 * 3. Success only if: longErrorCode == 0 AND (statusCode == 100 OR statusCode == 0)
 */
class ActivatedCustomerKYCViewController: UIViewController {

    // MARK: - IBOutlets

    @IBOutlet weak var scrollView: UIScrollView!
    @IBOutlet weak var menuButton: UIButton!
    @IBOutlet weak var headerTitleLabel: UILabel!
    @IBOutlet weak var subtitleLabel: UILabel!

    // KYC Information
    @IBOutlet weak var kycInfoContainer: UIView!
    @IBOutlet weak var kycInfoTitleLabel: UILabel!
    @IBOutlet weak var kycInfoTextLabel: UILabel!

    // Guidelines
    @IBOutlet weak var guidelinesContainer: UIView!
    @IBOutlet weak var step1TitleLabel: UILabel!
    @IBOutlet weak var step2TitleLabel: UILabel!
    @IBOutlet weak var step3TitleLabel: UILabel!
    @IBOutlet weak var step4TitleLabel: UILabel!

    // Requirements
    @IBOutlet weak var requirementsContainer: UIView!
    @IBOutlet weak var requirement1Label: UILabel!
    @IBOutlet weak var requirement2Label: UILabel!
    @IBOutlet weak var requirement3Label: UILabel!
    @IBOutlet weak var requirement4Label: UILabel!

    // Actions
    @IBOutlet weak var startButton: UIButton!
    @IBOutlet weak var loadingIndicator: UIActivityIndicatorView!

    // Error Banner
    @IBOutlet weak var errorBannerContainer: UIView!
    @IBOutlet weak var errorBannerLabel: UILabel!

    // MARK: - State Properties

    private var isProcessing = false {
        didSet {
            updateStartButtonState()
        }
    }

    private var errorMessage: String? {
        didSet {
            updateErrorBanner()
        }
    }

    // MARK: - Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()

        setupUI()
        setupEventHandlers()

        print("ActivatedCustomerKYCViewController - View loaded")
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        // Reset state on screen focus
        isProcessing = false
        errorMessage = nil

        print("ActivatedCustomerKYCViewController - View will appear, state reset")
    }

    deinit {
        // Cleanup event handler
        RDNAIDVDelegateManager.shared.onIDVActivatedCustomerKYCResponse = nil
        print("ActivatedCustomerKYCViewController - Event handler cleaned up")
    }

    // MARK: - Setup

    private func setupUI() {
        // Configure static UI elements
        headerTitleLabel.text = "Activated Customer KYC"
        subtitleLabel.text = "Complete your identity verification"

        kycInfoTitleLabel.text = "What is KYC?"
        kycInfoTextLabel.text = "KYC (Know Your Customer) is a verification process to confirm your identity. This helps ensure the security of your account and complies with regulatory requirements."

        step1TitleLabel.text = "1. Document Scan"
        step2TitleLabel.text = "2. Confirm Document Details"
        step3TitleLabel.text = "3. Selfie Capture"
        step4TitleLabel.text = "4. Biometric Consent"

        requirement1Label.text = "β€’ Have a valid government-issued ID ready"
        requirement2Label.text = "β€’ Ensure good lighting for document scanning"
        requirement3Label.text = "β€’ Position your face clearly for selfie capture"
        requirement4Label.text = "β€’ Allow camera permissions when prompted"

        startButton.layer.cornerRadius = 8
        loadingIndicator.hidesWhenStopped = true
        errorBannerContainer.isHidden = true
    }

    // MARK: - Event Handlers Setup

    private func setupEventHandlers() {
        // Register view controller-level persistent event handler for onIDVActivatedCustomerKYCResponse
        // This handler persists even when other IDV screens are on top
        RDNAIDVDelegateManager.shared.onIDVActivatedCustomerKYCResponse = { [weak self] userID, status, error in
            self?.handleActivatedCustomerKYCResponse(userID: userID, status: status, error: error)
        }

        print("ActivatedCustomerKYCViewController - Event handler registered (persistent)")
    }

    // MARK: - Event Handler Implementation

    private func handleActivatedCustomerKYCResponse(userID: String, status: RDNARequestStatus, error: RDNAError) {
        print("ActivatedCustomerKYCViewController - handleActivatedCustomerKYCResponse called")
        print("ActivatedCustomerKYCViewController - UserID: \\(userID)")
        print("ActivatedCustomerKYCViewController - Error: \\(error.longErrorCode) - \\(error.errorString ?? "")")
        print("ActivatedCustomerKYCViewController - Status: \\(status.statusCode) - \\(status.statusMessage ?? "")")

        // Navigate back to this screen (in case we're on another IDV screen)
        if let navController = navigationController {
            for vc in navController.viewControllers {
                if vc is ActivatedCustomerKYCViewController {
                    navController.popToViewController(vc, animated: true)
                    break
                }
            }
        }

        // Dual validation pattern
        let errorCode = error.longErrorCode
        let statusCode = status.statusCode

        var isSuccess = false
        var message = ""

        // Check sync error first
        if errorCode != 0 {
            // SDK sync error
            isSuccess = false
            message = "KYC verification failed: \\(error.errorString ?? "Unknown error")"
        } else if statusCode != 100 && statusCode != 0 {
            // Backend error
            isSuccess = false
            message = "KYC verification failed: \\(status.statusMessage ?? "Server error")"
        } else {
            // Success
            isSuccess = true
            message = "KYC verification completed successfully! Your identity has been verified."
        }

        // Reset state
        isProcessing = false

        // Show alert modal
        let alert = UIAlertController(
            title: isSuccess ? "Success" : "Error",
            message: message,
            preferredStyle: .alert
        )

        alert.addAction(UIAlertAction(title: "Go to Dashboard", style: .default) { [weak self] _ in
            self?.navigateToDashboard()
        })

        present(alert, animated: true)
    }

    // MARK: - IBActions

    @IBAction func menuButtonTapped(_ sender: UIButton) {
        // Open side menu
        print("ActivatedCustomerKYCViewController - Menu button tapped")
        MenuViewController.presentMenu(from: self)
    }

    @IBAction func startButtonTapped(_ sender: UIButton) {
        print("ActivatedCustomerKYCViewController - Start button tapped")

        guard !isProcessing else {
            print("ActivatedCustomerKYCViewController - Button disabled (processing)")
            return
        }

        isProcessing = true
        errorMessage = nil

        // Call SDK API
        let error = RDNAIDVService.shared.initiateActivatedCustomerKYC(reason: "Post Login KYC")

        if error.longErrorCode == 0 {
            print("ActivatedCustomerKYCViewController - KYC initiated successfully")
            print("ActivatedCustomerKYCViewController - SDK will auto-trigger IDV workflow events")
            // SDK will automatically trigger IDV workflow events
            // Keep isProcessing true until final callback
        } else {
            print("ActivatedCustomerKYCViewController - Failed to initiate KYC: \\(error.longErrorCode) - \\(error.errorString ?? "")")
            isProcessing = false
            errorMessage = error.errorString ?? "Failed to initiate KYC verification"
        }
    }

    // MARK: - Navigation

    private func navigateToDashboard() {
        print("ActivatedCustomerKYCViewController - Navigating to Dashboard")

        // Navigate to Dashboard
        if let navController = navigationController {
            for vc in navController.viewControllers {
                if vc is DashboardViewController {
                    navController.popToViewController(vc, animated: true)
                    return
                }
            }
        }

        // If Dashboard not found in stack, pop to root
        navigationController?.popToRootViewController(animated: true)
    }

    // MARK: - UI Updates

    private func updateStartButtonState() {
        startButton.isEnabled = !isProcessing

        if isProcessing {
            startButton.setTitle("Initiating...", for: .normal)
            startButton.backgroundColor = UIColor(hex: "#3498db")?.withAlphaComponent(0.7)
            loadingIndicator.startAnimating()
        } else {
            startButton.setTitle("Start KYC Verification", for: .normal)
            startButton.backgroundColor = UIColor(hex: "#3498db")
            loadingIndicator.stopAnimating()
        }
    }

    private func updateErrorBanner() {
        if let error = errorMessage, !error.isEmpty {
            errorBannerLabel.text = error
            errorBannerContainer.isHidden = false
        } else {
            errorBannerContainer.isHidden = true
        }
    }
}

Key Implementation Details

Event Handler Registration:

private func setupEventHandlers() {
    // Registered on viewDidLoad, persists until deinit
    RDNAIDVDelegateManager.shared.onIDVActivatedCustomerKYCResponse = { [weak self] userID, status, error in
        self?.handleActivatedCustomerKYCResponse(userID: userID, status: status, error: error)
    }
}

KYC Initiation:

let error = RDNAIDVService.shared.initiateActivatedCustomerKYC(reason: "Post Login KYC")

if error.longErrorCode == 0 {
    // Success: SDK will auto-trigger IDV workflow events
    // Keep isProcessing true until final callback
} else {
    // Sync error: Show error message
    isProcessing = false
    errorMessage = error.errorString
}

Dual Validation Pattern:

private func handleActivatedCustomerKYCResponse(userID: String, status: RDNARequestStatus, error: RDNAError) {
    // Check SDK sync error
    if error.longErrorCode != 0 {
        // SDK error
        showError(error.errorString)
    } else if status.statusCode != 100 && status.statusCode != 0 {
        // Backend error
        showError(status.statusMessage)
    } else {
        // Success
        showSuccess()
    }
}

Overview

The document scan process for post-login KYC uses workflow 6 (IDV_POSTLOGIN_KYC) and challenge mode 9. The close button behavior is different from pre-login flow.

Document Scan Start Confirmation

When SDK triggers getIDVDocumentScanProcessStartConfirmation delegate method via onGetIDVDocumentScanProcessStartConfirmation closure:

IDVDocumentProcessStartConfirmationViewController Key Changes

// Sources/Tutorial/Screens/IDV/IDVDocumentProcessStartConfirmationViewController.swift

import UIKit
import RELID

class IDVDocumentProcessStartConfirmationViewController: UIViewController {

    // Workflow passed by AppIDVCoordinator
    var idvWorkflow: RDNAIDVWorkflow = .IDV_ACTIVATION
    private var isProcessing = false

    @IBOutlet weak var startButton: UIButton!
    @IBOutlet weak var closeButton: UIButton!
    @IBOutlet weak var loadingIndicator: UIActivityIndicatorView!

    // User confirms to start document scan
    @IBAction func startDocumentScanTapped(_ sender: UIButton) {
        guard !isProcessing else { return }

        isProcessing = true
        updateButtonStates()

        print("IDVDocumentProcessStart - Starting document scan with workflow: \(idvWorkflow.rawValue)")

        // Pass workflow 6 to SDK
        let error = RDNAIDVService.shared.setIDVDocumentScanProcessStartConfirmation(
            isConfirm: true,
            idvWorkflow: idvWorkflow  // 6 for post-login KYC
        )

        if error.longErrorCode == 0 {
            print("IDVDocumentProcessStart - Document scan initiated")
            // SDK will open native camera for document capture
        } else {
            isProcessing = false
            updateButtonStates()
            showError(error.errorString ?? "Failed to start document scan")
        }
    }

    // Close button behavior: resetAuthState for pre-login, cancel API for post-login
    @IBAction func closeTapped(_ sender: UIButton) {
        guard !isProcessing else { return }

        isProcessing = true
        updateButtonStates()

        // Check workflow to determine close behavior
        if idvWorkflow == .IDV_ACTIVATION || idvWorkflow.rawValue == 5 {
            // Pre-login flow: Call resetAuthState
            let error = RDNAService.shared.resetAuthState()
            if error.longErrorCode != 0 {
                isProcessing = false
                updateButtonStates()
                showError(error.errorString ?? "Failed to cancel")
            }
        } else {
            // Post-login flow (workflow 6): Call cancel API
            print("IDVDocumentProcessStart - Post-login flow, calling cancel API")
            let error = RDNAIDVService.shared.setIDVDocumentScanProcessStartConfirmation(
                isConfirm: false,
                idvWorkflow: idvWorkflow  // 6
            )
            // This triggers onIDVActivatedCustomerKYCResponse event with error 240 (User Cancelled)
            if error.longErrorCode != 0 {
                isProcessing = false
                updateButtonStates()
                showError(error.errorString ?? "Failed to cancel")
            }
        }
    }
}

Document Details Confirmation

After document capture, SDK triggers getIDVConfirmDocumentDetails delegate method with challenge mode 9:

IDVConfirmDocumentDetailsViewController Key Changes

// Sources/Tutorial/Screens/IDV/IDVConfirmDocumentDetailsViewController.swift

import UIKit
import RELID

class IDVConfirmDocumentDetailsViewController: UIViewController {

    // Challenge mode passed by AppIDVCoordinator
    var challengeMode: RDNAChallengeOpMode = RDNAChallengeOpMode(rawValue: 0)!
    var responseDataJSON: String = ""
    private var isProcessing = false

    @IBOutlet weak var confirmButton: UIButton!
    @IBOutlet weak var recaptureButton: UIButton!
    @IBOutlet weak var closeButton: UIButton!

    // User confirms document details are correct
    @IBAction func confirmDocumentTapped(_ sender: UIButton) {
        guard !isProcessing else { return }

        isProcessing = true
        updateButtonStates()

        print("IDVConfirmDocumentDetails - Confirming document with mode: \(challengeMode.rawValue)")

        let error = RDNAIDVService.shared.setIDVConfirmDocumentDetails(
            isConfirm: true,
            challengeMode: challengeMode  // 9 for post-login
        )

        if error.longErrorCode == 0 {
            print("IDVConfirmDocumentDetails - Document confirmed, waiting for selfie event")
        } else {
            isProcessing = false
            updateButtonStates()
            showError(error.errorString ?? "Failed to confirm document")
        }
    }

    // Close button: Challenge mode-based navigation
    @IBAction func closeTapped(_ sender: UIButton) {
        guard !isProcessing else { return }

        isProcessing = true
        updateButtonStates()

        if challengeMode.rawValue == 8 {
            // Pre-login flow: Call resetAuthState
            print("IDVConfirmDocumentDetails - Pre-login flow, calling resetAuthState")
            let error = RDNAService.shared.resetAuthState()
            if error.longErrorCode != 0 {
                isProcessing = false
                updateButtonStates()
                showError(error.errorString ?? "Failed to cancel")
            }
        } else {
            // Post-login flow (mode 9): Call cancel API
            print("IDVConfirmDocumentDetails - Post-login flow, calling cancel API")
            let error = RDNAIDVService.shared.setIDVConfirmDocumentDetails(
                isConfirm: false,
                challengeMode: challengeMode  // 9
            )
            // This triggers onIDVActivatedCustomerKYCResponse event
            // Persistent handler navigates back to ActivatedCustomerKYC screen
            if error.longErrorCode != 0 {
                isProcessing = false
                updateButtonStates()
                showError(error.errorString ?? "Failed to cancel")
            }
        }
    }
}

Document Flow Testing

Test the document scan process:

  1. Start KYC β†’ Click "Start KYC Verification" on ActivatedCustomerKYCViewController
  2. Document scan start β†’ Navigate to IDVDocumentProcessStartConfirmationViewController
  3. Verify workflow β†’ Console shows "workflow: 6"
  4. Start document scan β†’ Native camera opens for document capture
  5. Capture document β†’ Scan ID or passport
  6. Document confirmation β†’ Navigate to IDVConfirmDocumentDetailsViewController with OCR data
  7. Verify challenge mode β†’ Console shows "challengeMode: 9"
  8. Confirm document β†’ Click confirm to proceed to selfie

Test Close Button:

  1. Click close button β†’ Cancel API called (mode 9)
  2. Event triggered β†’ onIDVActivatedCustomerKYCResponse event with error 240
  3. Navigate back β†’ Handler navigates to ActivatedCustomerKYCViewController
  4. Error alert shown β†’ "KYC verification failed: User Cancelled (240)"

Overview

The selfie capture process for post-login KYC also uses workflow 6 and challenge mode 9, with different close button navigation patterns.

Selfie Process Start Confirmation

When SDK triggers getIDVSelfieProcessStartConfirmation delegate method:

IDVSelfieProcessStartConfirmationViewController Key Changes

// Sources/Tutorial/Screens/IDV/IDVSelfieProcessStartConfirmationViewController.swift

import UIKit
import RELID

class IDVSelfieProcessStartConfirmationViewController: UIViewController {

    var idvWorkflow: RDNAIDVWorkflow = .IDV_ACTIVATION
    private var isProcessing = false

    @IBOutlet weak var startButton: UIButton!
    @IBOutlet weak var cameraSwitch: UISwitch!

    // User confirms to start selfie capture
    @IBAction func startSelfieTapped(_ sender: UIButton) {
        guard !isProcessing else { return }

        isProcessing = true
        updateButtonStates()

        print("IDVSelfieProcessStart - Starting selfie capture with workflow: \(idvWorkflow.rawValue)")

        let error = RDNAIDVService.shared.setIDVSelfieProcessStartConfirmation(
            isConfirm: true,
            useDeviceBackCamera: cameraSwitch.isOn,  // false = front camera for selfies
            idvWorkflow: idvWorkflow  // 6 for post-login KYC
        )

        if error.longErrorCode == 0 {
            print("IDVSelfieProcessStart - Selfie capture initiated")
            // SDK will open native camera for selfie with liveness detection
        } else {
            isProcessing = false
            updateButtonStates()
            showError(error.errorString ?? "Failed to start selfie capture")
        }
    }

    // Close button behavior based on workflow
    @IBAction func closeTapped(_ sender: UIButton) {
        guard !isProcessing else { return }

        isProcessing = true
        updateButtonStates()

        if idvWorkflow == .IDV_ACTIVATION || idvWorkflow.rawValue == 5 {
            // Pre-login flow: Call resetAuthState
            let error = RDNAService.shared.resetAuthState()
            if error.longErrorCode != 0 {
                isProcessing = false
                updateButtonStates()
                showError(error.errorString ?? "Failed to cancel")
            }
        } else {
            // Post-login flow (workflow 6): Call cancel API
            print("IDVSelfieProcessStart - Post-login flow, calling cancel API")
            let error = RDNAIDVService.shared.setIDVSelfieProcessStartConfirmation(
                isConfirm: false,
                useDeviceBackCamera: false,
                idvWorkflow: idvWorkflow  // 6
            )
            // This triggers onIDVActivatedCustomerKYCResponse event with error 240
            if error.longErrorCode != 0 {
                isProcessing = false
                updateButtonStates()
                showError(error.errorString ?? "Failed to cancel")
            }
        }
    }
}

Selfie Confirmation

After selfie capture, SDK triggers getIDVSelfieConfirmation delegate method with challenge mode 9:

IDVSelfieConfirmationViewController Key Changes

// Sources/Tutorial/Screens/IDV/IDVSelfieConfirmationViewController.swift

import UIKit
import RELID

class IDVSelfieConfirmationViewController: UIViewController {

    var userID: String = ""
    var challengeMode: RDNAChallengeOpMode = RDNAChallengeOpMode(rawValue: 0)!
    var responseDataJSON: String = ""
    private var isProcessing = false

    @IBOutlet weak var confirmButton: UIButton!

    // User confirms selfie biometric results
    @IBAction func confirmSelfieTapped(_ sender: UIButton) {
        guard !isProcessing else { return }

        isProcessing = true
        updateButtonStates()

        print("IDVSelfieConfirmation - Confirming selfie with mode: \(challengeMode.rawValue)")

        let error = RDNAIDVService.shared.setIDVSelfieConfirmation(
            action: "true",  // String value, not Bool!
            challengeMode: challengeMode  // 9 for post-login
        )

        if error.longErrorCode == 0 {
            print("IDVSelfieConfirmation - Selfie confirmed, waiting for consent event")
        } else {
            isProcessing = false
            updateButtonStates()
            showError(error.errorString ?? "Failed to confirm selfie")
        }
    }

    // Close button: Challenge mode-based navigation
    // Important: No cancel API available for selfie confirmation!
    @IBAction func closeTapped(_ sender: UIButton) {
        guard !isProcessing else { return }

        isProcessing = true
        updateButtonStates()

        if challengeMode.rawValue == 8 {
            // Pre-login flow: Call resetAuthState
            print("IDVSelfieConfirmation - Pre-login flow, calling resetAuthState")
            let error = RDNAService.shared.resetAuthState()
            if error.longErrorCode != 0 {
                isProcessing = false
                updateButtonStates()
                showError(error.errorString ?? "Failed to cancel")
            }
        } else {
            // Post-login flow (mode 9): Navigate directly to ActivatedCustomerKYC
            // No cancel API available for selfie confirmation
            print("IDVSelfieConfirmation - Post-login flow, navigating directly to ActivatedCustomerKYC")
            isProcessing = false

            if let navController = navigationController {
                for vc in navController.viewControllers {
                    if vc is ActivatedCustomerKYCViewController {
                        navController.popToViewController(vc, animated: true)
                        print("IDVSelfieConfirmation - Navigated to ActivatedCustomerKYC, button will be auto-enabled")
                        return
                    }
                }
            }

            // If not found, pop to root
            navigationController?.popToRootViewController(animated: true)
        }
    }
}

Selfie Flow Testing

Test the selfie capture process:

  1. Document confirmed β†’ After document confirmation, navigate to IDVSelfieProcessStartConfirmationViewController
  2. Verify workflow β†’ Console shows "workflow: 6"
  3. Start selfie capture β†’ Native camera opens with liveness detection
  4. Capture selfie β†’ Follow on-screen instructions for liveness check
  5. Selfie confirmation β†’ Navigate to IDVSelfieConfirmationViewController with biometric results
  6. Verify challenge mode β†’ Console shows "challengeMode: 9"
  7. Review biometric scores β†’ Face match score and liveness score displayed
  8. Confirm selfie β†’ Click confirm to proceed to biometric consent

Test Close Button:

  1. Click close button β†’ Direct navigation (mode 9, no cancel API)
  2. Navigate back β†’ Navigate to ActivatedCustomerKYCViewController directly
  3. Button re-enabled β†’ viewWillAppear resets button state
  4. No alert shown β†’ No event triggered, user can retry from beginning

Overview

The biometric consent for post-login KYC uses challenge mode 9 with a different response structure compared to pre-login flow.

Key Differences in Response Structure

Aspect

Pre-Login (Mode 8)

Post-Login (Mode 9)

challengeResponse

Present

Optional/Absent

status location

challengeResponse.status

Root level status

Swift access

Via challengeResponse parameter

Via status parameter

Biometric Opt-In Consent

When SDK triggers getIDVBiometricOptInConsent delegate method with challenge mode 9:

IDVBiometricOptInConsentViewController Key Changes

// Sources/Tutorial/Screens/IDV/IDVBiometricOptInConsentViewController.swift

import UIKit
import RELID

class IDVBiometricOptInConsentViewController: UIViewController {

    var userID: String = ""
    var attemptsLeft: Int = 0
    var challengeMode: RDNAChallengeOpMode = RDNAChallengeOpMode(rawValue: 0)!
    var idvResponseJSON: String = ""
    var status: [AnyHashable: Any] = [:]
    var challengeResponse: RDNAChallengeResponse?
    var sdkError: RDNAError?

    private var isProcessing = false

    @IBOutlet weak var approveButton: UIButton!
    @IBOutlet weak var rejectButton: UIButton!

    // User accepts or rejects biometric template storage
    @IBAction func approveTapped(_ sender: UIButton) {
        handleConsent(isOptIn: true)
    }

    @IBAction func rejectTapped(_ sender: UIButton) {
        handleConsent(isOptIn: false)
    }

    private func handleConsent(isOptIn: Bool) {
        guard !isProcessing else { return }

        isProcessing = true
        updateButtonStates()

        print("IDVBiometricOptInConsent - Processing consent with mode: \(challengeMode.rawValue), isOptIn: \(isOptIn)")

        let error = RDNAIDVService.shared.setIDVBiometricOptInConsent(
            isOptIn: isOptIn,  // true = accept, false = reject
            challengeMode: challengeMode  // 9 for post-login
        )

        if error.longErrorCode == 0 {
            print("IDVBiometricOptInConsent - Consent processed successfully")
            // SDK will trigger onIDVActivatedCustomerKYCResponse event with completion status
        } else {
            isProcessing = false
            updateButtonStates()
            showError(error.errorString ?? "Failed to process consent")
        }
    }

    // Close button: Challenge mode-based navigation
    @IBAction func closeTapped(_ sender: UIButton) {
        guard !isProcessing else { return }

        isProcessing = true
        updateButtonStates()

        if challengeMode.rawValue == 8 {
            // Pre-login flow: Call resetAuthState
            print("IDVBiometricOptInConsent - Pre-login flow, calling resetAuthState")
            let error = RDNAService.shared.resetAuthState()
            if error.longErrorCode != 0 {
                isProcessing = false
                updateButtonStates()
                showError(error.errorString ?? "Failed to cancel")
            }
        } else {
            // Post-login flow (mode 9): Navigate directly to ActivatedCustomerKYC
            print("IDVBiometricOptInConsent - Post-login flow, navigating directly to ActivatedCustomerKYC")
            isProcessing = false

            if let navController = navigationController {
                for vc in navController.viewControllers {
                    if vc is ActivatedCustomerKYCViewController {
                        navController.popToViewController(vc, animated: true)
                        print("IDVBiometricOptInConsent - Navigated to ActivatedCustomerKYC, button will be auto-enabled")
                        return
                    }
                }
            }

            // If not found, pop to root
            navigationController?.popToRootViewController(animated: true)
        }
    }
}

Handling Response Structure Differences

In the IDV delegate manager, parameters handle both structures:

RDNAIDVDelegateManager.swift

// Closure property for biometric consent
var onGetIDVBiometricOptInConsent: (
    (_ userID: String,
     _ attemptsLeft: Int,
     _ challengeMode: RDNAChallengeOpMode,
     _ idvResponseJSON: String,
     _ status: [AnyHashable: Any],
     _ challengeResponse: RDNAChallengeResponse,
     _ error: RDNAError) -> Void
)?

// Protocol implementation
func getIDVBiometricOpt(
    inConsent userID: String,
    response: RDNAChallengeResponse,
    challenge mode: RDNAChallengeOpMode,
    error: RDNAError
) {
    print("RDNAIDVDelegateManager - Biometric opt-in consent event received")
    print("RDNAIDVDelegateManager - User ID: \(userID)")
    print("RDNAIDVDelegateManager - Challenge Mode: \(mode.rawValue)")

    // For mode 9, status is at root level
    // For mode 8, status is in challengeResponse
    // SDK provides both parameters, use appropriate one based on mode

    DispatchQueue.main.async { [weak self] in
        self?.onGetIDVBiometricOptInConsent?(
            userID,
            0,  // attemptsLeft
            mode,
            "",  // idvResponseJSON
            [:],  // status dictionary
            response,
            error
        )
    }
}

Biometric Consent Testing

Test the biometric consent process:

  1. Selfie confirmed β†’ After selfie confirmation, navigate to IDVBiometricOptInConsentViewController
  2. Verify challenge mode β†’ Console shows "challengeMode: 9"
  3. Review consent information β†’ Biometric template storage explanation displayed
  4. Accept consent β†’ Click "Approve" button
  5. SDK processes consent β†’ setIDVBiometricOptInConsent(true, 9) called
  6. KYC completes β†’ SDK triggers onIDVActivatedCustomerKYCResponse event
  7. Handler receives event β†’ Persistent handler on ActivatedCustomerKYCViewController catches event
  8. Success alert shown β†’ "KYC verification completed successfully"

Test Close Button:

  1. Click close button β†’ Direct navigation (mode 9)
  2. Navigate back β†’ Navigate to ActivatedCustomerKYCViewController directly
  3. Button re-enabled β†’ viewWillAppear resets button state

Test Reject Consent:

  1. Click "Reject" β†’ setIDVBiometricOptInConsent(false, 9) called
  2. KYC still completes β†’ Rejection doesn't fail KYC, just doesn't store template
  3. Success alert shown β†’ "KYC verification completed successfully"

Overview

Now that you've implemented all the components, let's understand how they work together in the complete KYC flow.

Complete Flow Architecture

1. User Action (ActivatedCustomerKYCViewController)
   β”œβ”€ User clicks "Start KYC Verification"
   β”œβ”€ View controller calls: RDNAIDVService.shared.initiateActivatedCustomerKYC("Post Login KYC")
   └─ View controller registers persistent handler in viewDidLoad()

2. Service Layer (RDNAIDVService.swift)
   β”œβ”€ Calls SDK: rdna.initiateActivatedCustomerKYC(reason)
   β”œβ”€ SDK returns RDNAError (synchronous)
   β”œβ”€ Sync success: error.longErrorCode = 0
   └─ SDK initiates IDV workflow (workflow 6, mode 9)

3. IDV Workflow Events (SDK β†’ Delegate Manager β†’ Coordinator β†’ ViewControllers)
   β”œβ”€ getIDVDocumentScanProcessStartConfirmation β†’ Document Scan
   β”œβ”€ getIDVConfirmDocumentDetails β†’ Document Validation
   β”œβ”€ getIDVSelfieProcessStartConfirmation β†’ Selfie Capture
   β”œβ”€ getIDVSelfieConfirmation β†’ Selfie Verification
   └─ getIDVBiometricOptInConsent β†’ Biometric Consent

4. KYC Completion (onIDVActivatedCustomerKYCResponse)
   β”œβ”€ SDK triggers delegate method (after all steps complete or cancellation)
   β”œβ”€ RDNAIDVDelegateManager receives call
   β”œβ”€ Delegate Manager: onIDVActivatedCustomerKYCResponse closure invoked
   β”œβ”€ DispatchQueue.main.async ensures main thread
   └─ View Controller Handler: Closure registered in viewDidLoad()

5. View Controller Handler Processing
   β”œβ”€ Navigate back to ActivatedCustomerKYCViewController
   β”‚  β€’ Iterate through navigation stack
   β”‚  β€’ popToViewController(vc, animated: true)
   β”œβ”€ Dual Validation:
   β”‚  β”œβ”€ Check error.longErrorCode (0 = OK, 240 = cancelled, others = error)
   β”‚  └─ Check status.statusCode (100/0 = OK, others = backend error)
   β”œβ”€ Show Success/Error UIAlertController
   └─ Provide Dashboard navigation option

Handler Persistence Pattern

The view controller-level handler persists throughout the IDV flow:

// In ActivatedCustomerKYCViewController

override func viewDidLoad() {
    super.viewDidLoad()
    setupEventHandlers()
}

private func setupEventHandlers() {
    // This handler persists even when other screens push on top
    RDNAIDVDelegateManager.shared.onIDVActivatedCustomerKYCResponse = { [weak self] userID, status, error in
        self?.handleActivatedCustomerKYCResponse(userID: userID, status: status, error: error)
    }
}

deinit {
    // Only cleanup when view controller is deallocated
    RDNAIDVDelegateManager.shared.onIDVActivatedCustomerKYCResponse = nil
}

Lifecycle Point

Handler State

Purpose

viewDidLoad

Registered

Set up persistent closure

viewWillDisappear

Still active

Persists when other screens push

Other IDV screens on top

Still active

Catches completion event

deinit

Cleaned up

Only when VC deallocates

Completion Flow Testing

Test the complete KYC completion flow:

  1. Complete all IDV steps β†’ Document scan, document confirm, selfie capture, selfie confirm, biometric consent
  2. Biometric consent processed β†’ setIDVBiometricOptInConsent(true, 9) called
  3. SDK triggers delegate method β†’ onIDVActivatedCustomerKYCResponse protocol method called
  4. Delegate manager dispatches β†’ Closure invoked on main thread
  5. View controller handler executes β†’ Console shows "handleActivatedCustomerKYCResponse called"
  6. Navigate back β†’ popToViewController(vc, animated: true) called
  7. Dual validation β†’ Check error.longErrorCode and status.statusCode
  8. Success alert shown β†’ "KYC verification completed successfully"
  9. User taps button β†’ Navigate to Dashboard

Test Cancellation:

  1. Cancel at any IDV step β†’ Click close button (for screens with cancel API)
  2. Cancellation event β†’ onIDVActivatedCustomerKYCResponse with error 240
  3. View controller handler executes β†’ Persistent handler catches event
  4. Navigate back β†’ Navigate to ActivatedCustomerKYCViewController
  5. Error alert shown β†’ "KYC verification failed: User Cancelled (240)"
  6. User can retry β†’ Button re-enabled in viewWillAppear, can start KYC again

Test Direct Navigation:

  1. Close selfie or consent screen β†’ Screens without cancel API (mode 9)
  2. Direct navigation β†’ Navigate directly to ActivatedCustomerKYCViewController
  3. No event triggered β†’ No alert shown
  4. Button re-enabled β†’ viewWillAppear resets state, user can retry

Build with Xcode

  1. Open relidcodelab.xcworkspace (not .xcodeproj!)
  2. Select target device or simulator
  3. Press Cmd+R to build and run

Build with Command Line

# Clean build
xcodebuild -workspace relidcodelab.xcworkspace \\
           -scheme relidcodelab \\
           -sdk iphonesimulator clean build

# Run on simulator
xcodebuild -workspace relidcodelab.xcworkspace \\
           -scheme relidcodelab \\
           -sdk iphonesimulator \\
           -destination 'platform=iOS Simulator,name=iPhone 14'

Debugging

Complete Flow Test

Follow these steps to test the complete post-login KYC flow:

1. Login as Activated Customer

2. Access KYC Screen

3. Initiate KYC

4. Document Scan

5. Confirm Document Details

6. Selfie Capture

7. Selfie Confirmation

8. Biometric Consent

9. KYC Completion

Expected Results

Success Scenario:

βœ… KYC initiated successfully
βœ… Document scanned and validated
βœ… Selfie captured and matched
βœ… Biometric consent approved
βœ… Alert: "KYC verification completed successfully!"
βœ… Navigate to Dashboard

Error Scenarios:

❌ Sync Error: error.longErrorCode != 0
   β†’ Show error message immediately

❌ Backend Error: status.statusCode != 100 && statusCode != 0
   β†’ Show error message in completion alert

❌ User Cancellation: error.longErrorCode == 240
   β†’ Show "KYC cancelled" message in completion alert

Common Issues

1. "No such module β€˜RELIDIDV'"

Problem: Framework not linked properly

Solution:

# Clean build folder
rm -rf ~/Library/Developer/Xcode/DerivedData

# Reinstall pods
pod deintegrate
pod install

# Clean and rebuild in Xcode
Cmd+Shift+K (Clean)
Cmd+B (Build)

2. Document Scan Fails

Problem: Missing Regula database or license

Solution:

3. Camera Access Denied

Problem: Missing Info.plist permissions

Solution:

<!-- Add to Info.plist -->
<key>NSCameraUsageDescription</key>
<string>Camera access is required for document scanning and selfie capture</string>

4. Event Handler Not Called

Problem: Event handler not registered or deallocated

Solution:

5. Navigation Issues

Problem: View controller not found in navigation stack

Solution:

// Check if view controller exists before navigating
if let navController = navigationController {
    for vc in navController.viewControllers {
        if vc is ActivatedCustomerKYCViewController {
            navController.popToViewController(vc, animated: true)
            return
        }
    }
}

// Fallback: pop to root
navigationController?.popToRootViewController(animated: true)

6. Storyboard Connection Issues

Problem: IBOutlet not connected

Solution:

7. CocoaPods Errors

Problem: Version conflicts or pod repo out of date

Solution:

# Update pod repo
pod repo update

# Install with repo update
pod install --repo-update

# If still failing, try clean install
rm -rf Pods
rm Podfile.lock
pod install

8. "dyld: Library not loaded"

Problem: Framework not embedded

Solution:

What You've Accomplished

Congratulations! You've successfully implemented the RELID IDV Post-Login KYC Flow for iOS. You now have:

Key Takeaways

Architecture Patterns:

Workflow Specifics:

Validation Patterns:

Next Steps

Now that you've completed the post-login KYC flow, consider exploring:

  1. Pre-Login IDV Flow: Complete the MFA activation IDV codelab
  2. Biometric Authentication: Implement biometric login flows
  3. Document Management: Handle multiple document types
  4. Error Recovery: Implement retry mechanisms
  5. Analytics Integration: Track KYC completion rates
  6. Localization: Support multiple languages

Additional Resources

Feedback

We'd love to hear your feedback! Please submit issues or suggestions on our GitHub repository.

Thank you for completing the RELID IDV Post-Login KYC Flow iOS Codelab!