Learning Path:

  1. Complete REL-ID Complete Activation & Login Flow Codelab
  2. Complete REL-ID Data Signing Flow Codelab
  3. You are here → Add IDV Server Biometric Step-Up (Workflow 16)

Welcome to the REL-ID IDV Server Biometric Step-Up for Data Signing codelab! This tutorial shows you how to add high-security biometric authentication with liveness detection to your existing data signing implementation.

What You'll Build

You'll enhance your existing data signing app with:

What You'll Learn

By completing this codelab, you'll master:

  1. IDV Workflow 16 Integration: Implementing authLevel = .RDNA_AUTH_LEVEL_4 with authenticatorType = .RDNA_IDV_SERVER_BIOMETRIC for server biometric
  2. NavigationController Pattern: Direct navigation stack access for cross-hierarchy screen management
  3. Selfie Screen Detection: Detecting and closing IDVSelfieProcessStartConfirmationViewController in callback handler
  4. Screen Transition Timing: Adding 300ms delay after selfie closure for smooth UI updates using DispatchQueue.asyncAfter
  5. Error Priority Checking: Implementing proper error handling (error code first, then status code)
  6. View Controller-Level Architecture: Understanding why data signing uses view controller-level callback handlers

Prerequisites

Before starting, ensure you have:

Get the Code

Navigate to the relid-IDV-post-login-data-signing folder in your cloned repository:

cd relid-codelab-ios/relid-IDV-post-login-data-signing

Before implementing, let's understand what makes IDV Workflow 16 unique and why server biometric step-up requires special handling.

What is IDV Workflow 16?

IDV Workflow 16 is the server biometric step-up authentication flow specifically designed for data signing operations. Unlike other IDV workflows, workflow 16:

IDV Workflow Comparison

IDV Workflow

Scenario

When Used

Handler Location

16

Data Signing Step-Up Auth

High-security data signing

View Controller-level closure

9

Notification Step-Up Auth

Sensitive notification actions

View Controller-level closure

10

Biometric Opt-In

User enrolling in biometric

Standalone flow

6

Post-Login KYC

Know Your Customer verification

Standalone flow

Why View Controller-Level Handler?

Data signing uses a view controller-level handler because:

  1. No Complex State Management: Direct access to view controller properties and methods
  2. Clean Architecture: All data signing state lives in DataSigningInputViewController
  3. Simple Navigation: Direct navigationController access for screen management
  4. Reusability: Can be used from any screen that implements data signing
  5. Simpler Implementation: No need for complex callback preservation patterns

Server Biometric Event Flow

User Submits Form (authLevel=4, authenticatorType=1) →
authenticateUserAndSignData(payload, .RDNA_AUTH_LEVEL_4, .RDNA_IDV_SERVER_BIOMETRIC, reason) →
SDK Returns Sync Response →
SDK Triggers: onGetIDVSelfieProcessStartConfirmation (idvWorkflow: 16) →
  [Global Handler] Navigate to IDVSelfieProcessStartConfirmationViewController →
User Starts Selfie Capture →
SDK Captures Selfie with Liveness Detection →
SDK Verifies Biometric on Server →
SDK Signs Data →
SDK Triggers: onAuthenticateUserAndSignData →
  [View Controller Handler] Close selfie screen if present →
  [View Controller Handler] Wait 300ms for transition →
  [View Controller Handler] Check errors (priority: error code → status code) →
  [View Controller Handler] Navigate to result screen or show error

When to Use Server Biometric (AuthLevel 4)?

Use authLevel = .RDNA_AUTH_LEVEL_4 with authenticatorType = .RDNA_IDV_SERVER_BIOMETRIC for:

Let's understand the technical architecture we'll implement for IDV workflow 16 in DataSigningInputViewController.swift.

View Controller-Level Handler Pattern

We'll modify DataSigningInputViewController.swift to add three key pieces:

  1. NavigationController Access: Direct access to navigation stack for screen detection
  2. Error Priority Processing: Function to check errors before showing results
  3. Selfie Screen Auto-Closure: Logic to detect and close selfie screen

Key Technical Concepts

1. NavigationController Pattern

Why NavigationController?

guard let navigationController = navigationController else { return false }

if let topVC = navigationController.topViewController,
   topVC is IDVSelfieProcessStartConfirmationViewController {
    navigationController.popViewController(animated: false)
    return true
}

2. Error Priority Checking

Priority 1: Check error.longErrorCode != 0

Priority 2: Check status.statusCode != 100

// PRIORITY 1: Check error code first
if error.longErrorCode != 0 {
    showAlert(title: "Failed", message: error.errorString ?? "Error")
    return // Stop processing
}

// PRIORITY 2: Check status code
if status.statusCode != 100 {
    showAlert(title: "Failed", message: status.statusMessage ?? "Failed")
    return // Stop processing
}

// SUCCESS: Both checks passed
navigateToResults()

3. Screen Transition Delay

Why 300ms?

if selfieScreenWasClosed {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
        self?.processDataSigningResult(dataSigningDetails, status, error)
    }
    return
}

Implementation Flow

onAuthenticateUserAndSignData Event Triggered →
  ↓
handleSigningResult Called →
  ↓
Check if IDVSelfieProcessStartConfirmationViewController is topViewController →
  ↓
If Active:
  ├─ Close screen with navigationController.popViewController(animated: false)
  ├─ Wait 300ms for transition using DispatchQueue.asyncAfter
  └─ Process result with error priority checking
  ↓
If Not Active:
  └─ Process result immediately
  ↓
processDataSigningResult:
  ├─ Check error.longErrorCode != 0 (Priority 1)
  ├─ Check status.statusCode != 100 (Priority 2)
  └─ If success: Navigate to result screen

Files You'll Modify

In this codelab, you'll modify only one file:

File

Purpose

Changes

DataSigningInputViewController.swift

Input screen & event handler

Add navigationController logic, error priority processing, selfie auto-closure

All other files (screens, services, types) remain unchanged from the base data signing codelab.

Let's start by understanding how we'll use the navigationController property to detect and close the selfie screen.

Understanding NavigationController Property

Every UIViewController has a built-in navigationController property that provides access to its parent navigation controller. We'll use this to:

NavigationController Access Pattern

In DataSigningInputViewController.swift, we'll use this pattern:

guard let navigationController = navigationController else {
    print("DataSigningInputViewController - No navigation controller")
    return false
}

// Check if IDVSelfieProcessStartConfirmationViewController is on top
if let topVC = navigationController.topViewController,
   topVC is IDVSelfieProcessStartConfirmationViewController {
    // Close the screen
    navigationController.popViewController(animated: false)
    return true
}

return false

Why This Pattern?

The navigationController property provides:

Verify Navigation Setup

Your DataSigningInputViewController should already be embedded in a navigation controller from the MFA codelab setup. The storyboard should show:

UINavigationController
  └── DataSigningInputViewController

What's Next?

Now that we understand navigationController access, we'll use it to:

  1. Detect when IDVSelfieProcessStartConfirmationViewController is on top
  2. Close the screen before processing results

Let's move to the next step to add error priority processing.

Now let's add a dedicated method to process data signing results with proper error priority checking.

Add processDataSigningResult Method

In DataSigningInputViewController.swift, locate the handleSigningResult method (around line 345 in the previous implementation).

Add this new method before handleSigningResult:

// Line 395 - Process result with error priority checking
private func processDataSigningResult(dataSigningDetails: RDNADataSigningDetails,
                                     status: RDNARequestStatus,
                                     error: RDNAError) {
    isLoading = false

    // Hide password modal if visible (do this for all cases)
    if presentedViewController is PasswordChallengeModalViewController {
        dismiss(animated: false)
    }

    // PRIORITY 1: Check error code first
    if error.longErrorCode != 0 {
        // Error exists - show error message
        print("DataSigningInputViewController - Data signing error:")
        print("  Error Code: \(error.longErrorCode)")
        print("  Error Message: \(error.errorString ?? "N/A")")

        showAlert(title: "Data Signing Failed",
                 message: error.errorString ?? "An error occurred during data signing")
        return // Stop processing
    }

    // PRIORITY 2: Check status code
    if status.statusCode != 100 {
        // Status is not 100 - show status message
        print("DataSigningInputViewController - Data signing status error:")
        print("  Status Code: \(status.statusCode)")
        print("  Status Message: \(status.statusMessage ?? "N/A")")

        showAlert(title: "Data Signing Failed",
                 message: status.statusMessage ?? "Data signing failed with an unknown error")
        return // Stop processing
    }

    // SUCCESS: Both error and status checks passed
    print("DataSigningInputViewController - Data signing completed successfully")
    print("  Status Code: \(status.statusCode)")
    print("  Payload Signature: \(dataSigningDetails.payloadSignature ?? "N/A")")
    print("  Signature ID: \(dataSigningDetails.dataSignatureID ?? "N/A")")

    // Navigate to results screen
    AppCoordinator.shared.showDataSigningResult(dataSigningDetails: dataSigningDetails)
}

Understanding Error Priority

Priority 1 - Error Code Check:

if error.longErrorCode != 0 {
    // Authentication failed, biometric mismatch, user cancelled, etc.
    showAlert(title: "Data Signing Failed", message: error.errorString ?? "Error")
    return // Stop - don't check status code
}

Priority 2 - Status Code Check:

if status.statusCode != 100 {
    // Operation failed at SDK level
    showAlert(title: "Data Signing Failed", message: status.statusMessage ?? "Failed")
    return // Stop - don't navigate
}

Success Path:

// Both checks passed - safe to show results
AppCoordinator.shared.showDataSigningResult(dataSigningDetails: dataSigningDetails)

Key Features

  1. State Updates: Updates loading state, hides password modal
  2. Priority Checking: Error code checked before status code
  3. Early Return: Stops processing on error (prevents navigation behind alerts)
  4. User-Friendly Messages: Uses SDK error/status messages for readable errors
  5. Form Preservation: Keeps form data for retry on error

What's Next?

Now we have error priority processing ready. In the next step, we'll implement the main handler that:

  1. Detects if selfie screen is active
  2. Closes it if present
  3. Waits 300ms for transition
  4. Calls our processDataSigningResult method

Now let's implement the main handler that detects and closes the selfie screen before processing results.

Replace handleSigningResult Method

In DataSigningInputViewController.swift, replace your existing

handleSigningResult

method with this enhanced version:

// Line 345 - Main handler with selfie screen auto-closure
private func handleSigningResult(dataSigningDetails: RDNADataSigningDetails,
                                 status: RDNARequestStatus,
                                 error: RDNAError) {
    print("DataSigningInputViewController - Data signing response received")

    // Check if selfie screen is active and close it before processing
    let selfieScreenWasClosed = closeSelfieScreenIfPresent()

    if selfieScreenWasClosed {
        // Add 300ms delay for screen transition
        print("DataSigningInputViewController - Delaying result processing for screen transition (300ms)")

        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
            print("DataSigningInputViewController - Processing data signing result after screen close")
            self?.processDataSigningResult(dataSigningDetails: dataSigningDetails,
                                           status: status, error: error)
        }

        return // Exit early, let asyncAfter handle the rest
    }

    // Selfie screen not present (password/LDA step-up), process immediately
    print("DataSigningInputViewController - Processing data signing result immediately")
    processDataSigningResult(dataSigningDetails: dataSigningDetails,
                            status: status, error: error)
}

Add closeSelfieScreenIfPresent Method

Add this helper method after handleSigningResult:

// Line 370 - Check if selfie screen is active and close it
private func closeSelfieScreenIfPresent() -> Bool {
    print("DataSigningInputViewController - Checking if selfie screen needs to be closed for data signing")

    guard let navigationController = navigationController else {
        print("DataSigningInputViewController - No navigation controller")
        return false
    }

    // Check if IDVSelfieProcessStartConfirmationViewController is on top
    if let topVC = navigationController.topViewController,
       topVC is IDVSelfieProcessStartConfirmationViewController {
        print("DataSigningInputViewController - IDVSelfieProcessStartConfirmationViewController detected for data signing, closing it")

        // Navigate back to close selfie screen
        navigationController.popViewController(animated: false)

        print("DataSigningInputViewController - Selfie screen closed")
        return true // Screen was present and closed
    } else {
        let topVCName = navigationController.topViewController.map { String(describing: type(of: $0)) } ?? "nil"
        print("DataSigningInputViewController - No IDVSelfieProcessStartConfirmationViewController on top, current VC: \(topVCName)")
    }

    return false // Screen not present
}

Understanding the Implementation

Part 1: Screen Detection Method

private func closeSelfieScreenIfPresent() -> Bool {
    // Check if navigation controller exists
    guard let navigationController = navigationController else {
        return false // Not available, can't detect screen
    }

    // Get current top view controller from navigation stack
    if let topVC = navigationController.topViewController,
       topVC is IDVSelfieProcessStartConfirmationViewController {
        // Close the screen
        navigationController.popViewController(animated: false)
        return true // Screen was closed
    }

    return false // No selfie screen present
}

Key Points:

Part 2: Conditional Processing

let selfieScreenWasClosed = closeSelfieScreenIfPresent()

if selfieScreenWasClosed {
    // Screen was closed - add delay for smooth transition
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
        self?.processDataSigningResult(dataSigningDetails, status, error)
    }
    return
}

// No screen to close - process immediately
processDataSigningResult(dataSigningDetails, status, error)

Why This Pattern?:

Complete Event Flow

1. onAuthenticateUserAndSignData callback fires
   ↓
2. handleSigningResult called with response
   ↓
3. closeSelfieScreenIfPresent() executes:
   ├─ Check navigationController exists → ✓
   ├─ Get topViewController → IDVSelfieProcessStartConfirmationViewController
   ├─ Call navigationController.popViewController(animated: false) → Screen closes
   └─ Return true
   ↓
4. selfieScreenWasClosed === true
   ↓
5. DispatchQueue.main.asyncAfter 300ms
   ↓
6. processDataSigningResult executes:
   ├─ Check error.longErrorCode != 0 → ✓ (0 = success)
   ├─ Check status.statusCode != 100 → ✓ (100 = success)
   └─ Navigate to DataSigningResultViewController
   ↓
7. User sees results on clean screen

Common Scenarios

Scenario 1: Server Biometric Success

Scenario 2: Server Biometric Error

Scenario 3: Password/LDA Success

Scenario 4: Password/LDA Error

Error Handling

The implementation handles:

What's Next?

Your implementation is complete! Let's test it to verify everything works correctly.

Let's verify your IDV workflow 16 implementation works correctly across all scenarios.

Test 1: Server Biometric Success Flow

  1. Fill Data Signing Form:
    • Payload: "Transfer $10,000 to account 123456"
    • Auth Level: "RDNA_AUTH_LEVEL_4 (4)"
    • Authenticator Type: "RDNA_IDV_SERVER_BIOMETRIC (1)"
    • Reason: "High-value financial transaction"
  2. Tap "Sign Data" → Loading state activates
  3. Verify Selfie Screen:
    • IDVSelfieProcessStartConfirmationViewController appears
    • Screen shows data signing specific guidelines
    • "Start Selfie Capture" button visible
  4. Complete Biometric Capture:
    • Tap "Start Selfie Capture"
    • Complete selfie with liveness detection
    • Wait for server verification
  5. Verify Auto-Closure:
    • Selfie screen automatically closes
    • DataSigningResultViewController appears (not behind selfie screen)
    • No flicker or UI glitches
    • Signature details all present

Test 2: Server Biometric Error Handling

  1. Trigger Error Scenario:
    • Use same form values as Test 1
    • Cancel biometric capture OR
    • Fail liveness detection
  2. Verify Error Handling:
    • Selfie screen closes automatically
    • UIAlertController appears on DataSigningInputViewController (not behind selfie screen)
    • Alert shows user-friendly error message
    • Form data preserved for retry

Expected Error Messages:

Test 3: Password/LDA Flow (No Selfie Screen)

  1. Test Password Flow:
    • Auth Level: "RDNA_AUTH_LEVEL_3 (3)"
    • Authenticator Type: "RDNA_AUTH_PASS (2)"
    • Complete password entry
    • Results appear immediately (no 300ms delay)
    • No selfie screen involved
  2. Test LDA Flow:
    • Auth Level: "RDNA_AUTH_LEVEL_3 (3)"
    • Authenticator Type: "RDNA_AUTH_LDA (3)"
    • Complete biometric prompt
    • Results appear immediately
    • No screen closure logic triggered

Test 4: Navigation Hierarchy

  1. Test from Different Screens:
    • Launch from dashboard
    • Launch from side menu
    • Launch after deep link
    • All cases should detect and close selfie screen correctly
  2. Test Navigation State:
    • Go back during selfie screen (before capture)
    • No errors in logs
    • State resets properly

Verification Checklist

Core Functionality:

Error Handling:

Other Authentication Methods:

Edge Cases:

Common Issues & Solutions

Issue: Selfie screen doesn't close

Issue: Alert appears behind selfie screen

Issue: Results appear for error responses

Issue: Password/LDA delayed unnecessarily

You've successfully added IDV Server Biometric Step-Up Authentication (Workflow 16) to your data signing implementation!

What You Accomplished

Key Implementation Patterns

NavigationController for Cross-Hierarchy Management:

guard let navigationController = navigationController else { return false }

if let topVC = navigationController.topViewController,
   topVC is IDVSelfieProcessStartConfirmationViewController {
    navigationController.popViewController(animated: false)
    return true
}

Error Priority Checking:

// Priority 1: Error code
if error.longErrorCode != 0 {
    showAlert(title: "Failed", message: error.errorString ?? "Error")
    return
}

// Priority 2: Status code
if status.statusCode != 100 {
    showAlert(title: "Failed", message: status.statusMessage ?? "Failed")
    return
}

Conditional Delay for Smooth Transitions:

if selfieScreenWasClosed {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
        self?.processDataSigningResult(dataSigningDetails, status, error)
    }
    return
}
processDataSigningResult(dataSigningDetails, status, error)

Production Checklist

Before deploying, ensure:

Next Steps

Explore More REL-ID Features:

Enhance Your Implementation:

Additional Resources

Congratulations! You've mastered IDV Server Biometric Step-Up Authentication for Data Signing with REL-ID SDK on iOS!

Your app now provides high-security biometric authentication with liveness detection for sensitive data signing operations, with a smooth, professional user experience!