🎯 Learning Path:

  1. Complete REL-ID Complete Activation & Login Flow Codelab
  2. Complete REL-ID Additional Device Activation Flow With Notifications Codelab
  3. Complete REL-ID Step-Up Authentication with Notifications (Password)
  4. You are here → Server Biometric Step-Up Authentication with IDV Workflow 9

Welcome to the REL-ID Server Biometric Step-Up Authentication codelab! This tutorial enhances your notification application with high-security biometric verification using liveness detection for sensitive notification actions.

What You'll Build

In this codelab, you'll enhance your existing notification application with:

What You'll Learn

By completing this codelab, you'll master:

  1. Server Biometric Flow: Understanding authLevel=4 and how it differs from password (challengeMode 3)
  2. IDV Workflow 9: Implementing notification-specific selfie capture with liveness detection
  3. Event Handler Registration: Using deinit cleanup pattern for reliable handler registration
  4. Handler Chaining: Preserving global handlers while adding screen-specific logic
  5. Modal Lifecycle Management: Closing action modal before selfie screen appears
  6. Navigation Stack Pattern: Navigating across view controller hierarchies to close screens
  7. Screen Transition Delays: Ensuring smooth UI updates with proper timing
  8. Liveness Detection: Integrating Aware FaceCapture library for anti-spoofing

Prerequisites

Before starting this codelab, ensure you have:

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-public/relid-codelab-ios.git

Navigate to the relid-IDV-post-login-step-up-auth-notification folder in the repository you cloned earlier

Codelab Architecture Overview

This codelab adds server biometric step-up authentication to your notification application with three core enhancements:

  1. IDV Event Handler: onGetIDVSelfieProcessStartConfirmation handler for workflow 9 with handler chaining pattern
  2. Action Modal Management: Close action modal before selfie screen appears to prevent UI blocking
  3. Navigation Stack Pattern: Cross-hierarchy screen closure using navigationController.popToViewController() for smooth transitions

Before implementing server biometric authentication, let's understand the key differences from password step-up and the unique event flow.

Password vs Server Biometric Step-Up

The password step-up codelab (prerequisite) uses Challenge Mode 3 for re-authentication. Server biometric authentication uses a different mechanism:

Aspect

Password Step-Up (Prerequisite)

Server Biometric Step-Up (This Codelab)

Backend Configuration

authLevel = 1

authLevel = 4

SDK Event Triggered

onGetPassword event

onGetIDVSelfieProcessStartConfirmation event

Event Parameter

challengeMode = 3

idvWorkflow = 9

UI Pattern

Modal overlay (StepUpPasswordViewController)

Full screen navigation (IDVSelfieProcessStartConfirmationViewController)

Authentication Method

Password input verification

Selfie capture with liveness detection

Backend Verification

Password hash verification

Biometric template matching

Security Level

Standard re-authentication

High-security with anti-spoofing

Server Biometric Event Flow

The server biometric step-up authentication follows this event-driven pattern:

User Taps Notification Action → updateNotification(uuid, action) API Called →
Backend Determines authLevel = 4 (Server Biometric Required) →

SDK Triggers: onGetIDVSelfieProcessStartConfirmation Event (idvWorkflow: 9) →

[Screen-Level Handler Executes FIRST]:
  handleSelfieProcessStartForNotification() →
  Checks if idvWorkflow.rawValue == 9 →
  Calls dismissActionModal() → Action Modal Closes →

[Global Handler Executes SECOND]:
  originalGetSelfieHandler() (from RDNAIDVDelegateManager) →
  Navigates to IDVSelfieProcessStartConfirmationViewController →

User Reviews Guidelines → Taps "Start Selfie Capture" →
SDK Captures Selfie → Liveness Detection (Aware FaceCapture) →
SDK Verifies Biometric Template on Server →

SDK Triggers: onUpdateNotification Event →

[Screen-Level Handler Executes]:
  handleUpdateNotificationReceived() →
  Detects Selfie Screen via Navigation Stack →
  Closes Selfie Screen with popToViewController() →
  Delays 300ms for Screen Transition →
  Displays Success/Error Alert →
  Navigates to Dashboard

AuthLevel Configuration

Important: The authentication method is determined by the authLevel configured in the backend for each notification action:

AuthLevel

Authentication Method

SDK Behavior

Use Case

1

Password

SDK triggers onGetPassword with challengeMode 3

Standard security actions (prerequisite codelab)

3

LDA (Local Device Auth)

SDK handles biometric internally

Standard biometric actions

4

Server Biometric

SDK triggers onGetIDVSelfieProcessStartConfirmation with idvWorkflow 9

High-security actions requiring liveness

IDV Workflow 9 - Notification Step-Up

IDV Workflow 9 is specifically designed for notification step-up authentication:

IDV Workflow

Purpose

When Used

Screen Navigation

9

Notification Step-Up Authentication

Sensitive notification actions with authLevel=4

Must close action modal first

10

Biometric Opt-In

User enrolls in biometric authentication

Standalone flow

6

Post-Login KYC

Know Your Customer verification

Standalone flow

8

Post-Login Selfie

Biometric authentication after login

Standalone flow

15

Login Selfie

Biometric verification during login

Pre-login flow

Key Server Biometric Components

The REL-ID SDK uses these components for server biometric step-up:

Component

Description

Developer Action

Aware FaceCapture Library

Embedded in RELIDIDV framework for liveness detection

Pre-configured, no action needed

onGetIDVSelfieProcessStartConfirmation

SDK event when selfie capture is required (idvWorkflow: 9)

Register handler in GetNotificationsViewController

IDVSelfieProcessStartConfirmationViewController

Selfie capture initiation screen with guidelines

Already implemented in project

onUpdateNotification

SDK event with authentication result

Handle result, close selfie screen, show alert

Now let's implement the screen-level event handler that intercepts onGetIDVSelfieProcessStartConfirmation events for workflow 9.

Understanding Handler Chaining Pattern

For server biometric step-up, we need two actions when the selfie event is triggered:

  1. Screen-Level Logic: Close the action modal to prevent UI blocking
  2. Global Navigation: Navigate to IDVSelfieProcessStartConfirmationViewController

This requires a handler chaining pattern where our screen handler executes first, then calls the global handler:

// Screen handler executes first
handleSelfieProcessStartForNotification(idvWorkflow: idvWorkflow) // Close modal

// Then call original handler
originalGetSelfieProcessStartHandler?(idvWorkflow) // Navigate to selfie screen

Implement Selfie Process Start Handler

Add the handler that intercepts workflow 9 events to close the action modal:

// GetNotificationsViewController.swift

/// Handle selfie process start confirmation for notification step-up (idvWorkflow: 9)
///
/// This event is triggered when SDK starts server biometric authentication
/// for a notification action with authLevel = 4.
///
/// Critical: Close action modal BEFORE SDK navigates to selfie screen
private func handleSelfieProcessStartForNotification(idvWorkflow: RDNAIDVWorkflow) {
    print("GetNotificationsViewController - Selfie process start confirmation received:")
    print("  idvWorkflow: \(idvWorkflow.rawValue)")

    // Check if this is notification step-up flow (idvWorkFlow: 9)
    if idvWorkflow.rawValue == 9 {  // IDV_POSTLOGIN_STEPUP_NOTIFICATION
        print("GetNotificationsViewController - Server biometric step-up for notification detected")

        // CRITICAL: Close action modal to allow selfie screen to appear on top
        dismissActionModal()

        print("GetNotificationsViewController - Action modal closed, selfie screen will appear")

        // SDK will automatically navigate to IDVSelfieProcessStartConfirmationViewController
        // Global handler in SDKIDVEventProvider will handle navigation
    } else {
        print("GetNotificationsViewController - Different IDV workflow, ignoring in notification screen: \(idvWorkflow.rawValue)")
    }
}

Register Handler with Chaining Pattern

Register the handler in setupEventHandlers() to preserve the global handler:

// GetNotificationsViewController.swift

private func setupEventHandlers() {
    print("GetNotificationsViewController - setupEventHandlers called")

    // Setup SDK event handlers
    RDNADelegateManager.shared.onGetNotifications = { [weak self] status in
        print("GetNotificationsViewController - onGetNotifications handler FIRED")
        self?.handleNotificationsReceived(status: status)
    }

    RDNADelegateManager.shared.onUpdateNotification = { [weak self] status in
        print("GetNotificationsViewController - onUpdateNotification handler FIRED")
        self?.handleUpdateNotificationReceived(status: status)
    }
    print("GetNotificationsViewController - onUpdateNotification handler registered")

    // STEP-UP AUTH: Preserve original getPassword handler and chain for challengeMode 3
    originalGetPasswordHandler = RDNADelegateManager.shared.onGetPassword

    RDNADelegateManager.shared.onGetPassword = { [weak self] userID, challengeMode, attemptsLeft, response, error in
        self?.handleGetPasswordStepUp(userID: userID, challengeMode: challengeMode, attemptsLeft: attemptsLeft, response: response, error: error)
    }

    // IDV SELFIE HANDLER: Register IDV selfie handler for server biometric step-up (idvWorkflow: 9)
    print("GetNotificationsViewController - Registering IDV selfie handler")
    originalGetSelfieProcessStartHandler = RDNAIDVDelegateManager.shared.onGetIDVSelfieProcessStartConfirmation
    print("GetNotificationsViewController - Original selfie handler exists: \(originalGetSelfieProcessStartHandler != nil)")

    RDNAIDVDelegateManager.shared.onGetIDVSelfieProcessStartConfirmation = { [weak self] idvWorkflow in
        // First handle notification-specific logic (close modal)
        self?.handleSelfieProcessStartForNotification(idvWorkflow: idvWorkflow)

        // Then call original handler for navigation
        self?.originalGetSelfieProcessStartHandler?(idvWorkflow)
    }
    print("GetNotificationsViewController - IDV selfie handler registered")
}

Cleanup Pattern in deinit

Add cleanup in deinit to match the iOS lifecycle pattern:

// GetNotificationsViewController.swift

// Properties
private var originalGetSelfieProcessStartHandler: ((_ idvWorkflow: RDNAIDVWorkflow) -> Void)?

deinit {
    // Cleanup event handlers (matches UIKit lifecycle)
    // Note: Cleanup is in deinit (on view controller deallocation), not viewWillDisappear
    // This matches the pattern where handlers should persist when screen is just hidden
    print("GetNotificationsViewController - deinit, cleaning up event handlers")
    cleanupEventHandlers()
}

private func cleanupEventHandlers() {
    print("GetNotificationsViewController - cleanupEventHandlers called")

    // Cleanup SDK event handlers
    print("GetNotificationsViewController - Removing onGetNotifications handler")
    RDNADelegateManager.shared.onGetNotifications = nil

    print("GetNotificationsViewController - Removing onUpdateNotification handler")
    RDNADelegateManager.shared.onUpdateNotification = nil

    // STEP-UP AUTH: Restore original getPassword handler
    print("GetNotificationsViewController - Restoring original getPassword handler")
    RDNADelegateManager.shared.onGetPassword = originalGetPasswordHandler
    originalGetPasswordHandler = nil

    // IDV SELFIE HANDLER: Restore original selfie handler
    print("GetNotificationsViewController - Restoring original selfie handler")
    RDNAIDVDelegateManager.shared.onGetIDVSelfieProcessStartConfirmation = originalGetSelfieProcessStartHandler
    originalGetSelfieProcessStartHandler = nil

    print("GetNotificationsViewController - All handlers cleaned up")
}

Now let's implement the enhanced onUpdateNotification handler that closes the selfie screen before showing alerts.

Understanding Navigation Stack Pattern

After server biometric verification completes, the onUpdateNotification event is triggered. At this point, the selfie screen (IDVSelfieProcessStartConfirmationViewController) may still be visible on top of the GetNotificationsViewController.

We need to:

  1. Detect if the selfie screen is currently visible
  2. Close the selfie screen using navigation controller
  3. Delay 300ms for smooth screen transition
  4. Display success/error alert on GetNotificationsViewController

The navigation stack pattern allows us to navigate across hierarchies:

UINavigationController
  ├─ GetNotificationsViewController (self)
  └─ IDVSelfieProcessStartConfirmationViewController (pushed on top)

To close the selfie screen from GetNotificationsViewController, we check the navigation stack:

if let navController = navigationController {
    let selfIndex = navController.viewControllers.firstIndex(of: self)
    if selfIndex < navController.viewControllers.count - 1 {
        // There are screens above us
        navController.popToViewController(self, animated: false)
    }
}

Extract Result Processing Logic

First, extract the notification update result processing into a separate function:

// GetNotificationsViewController.swift

/// Handle notification update result
/// Extracted logic to process notification update response
/// This allows reuse whether we delay (biometric) or process immediately (password)
private func handleNotificationUpdateResult(status: RDNAStatusUpdateNotification) {
    print("GetNotificationsViewController - Processing notification update result")

    // STEP-UP AUTH: Check for LDA cancelled (error code 131)
    let error = status.error
    if error.longErrorCode == 131 {
        print("GetNotificationsViewController - LDA cancelled, Password not enrolled")
        dismissStepUpModal()

        showAlert(
            title: "Authentication Cancelled",
            message: "Local device authentication was cancelled. Please try again."
        ) {
            // Keep action modal open to allow user to retry
        }
        return
    }

    // Check for other SDK errors
    if error.longErrorCode != 0 {
        let errorMessage = error.errorString
        print("GetNotificationsViewController - Update error: \(errorMessage)")

        // Dismiss modals FIRST, then show alert
        dismissStepUpModal()
        dismissActionModal()

        // Show alert after a short delay (let modals dismiss first)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
            self.showAlert(title: "Update Failed", message: errorMessage) {
                // Refresh notifications after error
                self.loadNotifications()
            }
        }
        return
    }

    // Check response status
    let responseStatus = status.status
    let statusCode = responseStatus.statusCode.rawValue

    if statusCode == 0 || statusCode == 100 {  // RESP_STATUS_SUCCESS
        print("GetNotificationsViewController - Update successful")
        // Success - dismiss modals and show success alert
        dismissStepUpModal()
        dismissActionModal()

        // Refresh notifications
        loadNotifications()

        // Show success alert after a short delay (let modals dismiss first)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
            self.showAlert(
                title: "Success",
                message: responseStatus.message ?? "Action completed successfully"
            )
        }

    } else if statusCode == 110 || statusCode == 153 {
        // STEP-UP AUTH: Critical errors (110=password expired, 153=attempts exhausted)
        print("GetNotificationsViewController - Critical error: \(statusCode)")
        let statusMessage = responseStatus.message

        // Dismiss modals FIRST
        dismissStepUpModal()
        dismissActionModal()

        // Show alert BEFORE SDK triggers logout
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
            self.showAlert(
                title: "Authentication Failed",
                message: statusMessage ?? "Authentication failed"
            ) {
                print("GetNotificationsViewController - Waiting for SDK to trigger logout flow")
                // SDK will automatically trigger onUserLoggedOff event
            }
        }
    } else {
        let statusMessage = responseStatus.message ?? "Update failed"
        print("GetNotificationsViewController - Update status error: \(statusMessage)")

        // Dismiss modals FIRST, then show alert
        dismissStepUpModal()
        dismissActionModal()

        // Show alert after a short delay (let modals dismiss first)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
            self.showAlert(title: "Update Failed", message: statusMessage) {
                // Refresh notifications after alert
                self.loadNotifications()
            }
        }
    }
}

Implement Navigation Stack Screen Closure

Enhance the handleUpdateNotificationReceived handler to detect and close the selfie screen:

// GetNotificationsViewController.swift

/// Handle update notification response from onUpdateNotification event
///
/// Updated: Now checks if selfie screen is active and closes it before showing alerts
/// This prevents alerts from appearing behind the selfie screen during server biometric step-up
private func handleUpdateNotificationReceived(status: RDNAStatusUpdateNotification) {
    print("GetNotificationsViewController - ========================================")
    print("GetNotificationsViewController - handleUpdateNotificationReceived CALLED")
    print("GetNotificationsViewController - Error code: \(status.error.longErrorCode)")
    print("GetNotificationsViewController - Status code: \(status.status.statusCode.rawValue)")
    print("GetNotificationsViewController - ========================================")

    actionLoading = false
    actionModal?.isActionLoading = false // Reset modal loading state
    stepUpSubmitting = false

    /**
     * Check if selfie screen is active and close it before processing
     * Pattern: Check if there are view controllers on the navigation stack above us
     * If yes, pop back to self (closes selfie screen and any other screens)
     */
    func closeSelfieScreenIfPresent() -> Bool {
        print("GetNotificationsViewController - [closeSelfieScreenIfPresent] Starting check...")

        // Get navigation controller
        guard let navController = navigationController else {
            print("GetNotificationsViewController - [closeSelfieScreenIfPresent] ❌ Navigation controller is NIL")
            return false
        }

        print("GetNotificationsViewController - [closeSelfieScreenIfPresent] Navigation controller exists")
        print("GetNotificationsViewController - [closeSelfieScreenIfPresent] Total VCs in stack: \(navController.viewControllers.count)")

        // Print all VCs in the stack
        for (index, vc) in navController.viewControllers.enumerated() {
            print("GetNotificationsViewController - [closeSelfieScreenIfPresent]   [\(index)] \(type(of: vc))")
        }

        // Check if there are other view controllers on the navigation stack above us
        if let selfIndex = navController.viewControllers.firstIndex(of: self) {
            print("GetNotificationsViewController - [closeSelfieScreenIfPresent] Found self at index: \(selfIndex)")

            if selfIndex < navController.viewControllers.count - 1 {
                // There are screens on top of us (like IDVSelfieProcessStartConfirmationViewController)
                let vcsAbove = navController.viewControllers.count - 1 - selfIndex
                print("GetNotificationsViewController - [closeSelfieScreenIfPresent] ✅ Found \(vcsAbove) view controller(s) above us")

                // List what's above us
                for i in (selfIndex + 1)..<navController.viewControllers.count {
                    print("GetNotificationsViewController - [closeSelfieScreenIfPresent]   Above us: \(type(of: navController.viewControllers[i]))")
                }

                print("GetNotificationsViewController - [closeSelfieScreenIfPresent] Calling popToViewController(self, animated: false)")
                navController.popToViewController(self, animated: false)
                print("GetNotificationsViewController - [closeSelfieScreenIfPresent] ✅ popToViewController completed")

                return true
            } else {
                print("GetNotificationsViewController - [closeSelfieScreenIfPresent] ❌ No view controllers above us (we're at top)")
                print("GetNotificationsViewController - [closeSelfieScreenIfPresent]   selfIndex: \(selfIndex), count: \(navController.viewControllers.count)")
            }
        } else {
            print("GetNotificationsViewController - [closeSelfieScreenIfPresent] ❌ Could not find self in navigation stack!")
        }

        return false
    }

    // Check and close selfie screen if present
    print("GetNotificationsViewController - About to call closeSelfieScreenIfPresent()")
    let selfieScreenWasClosed = closeSelfieScreenIfPresent()
    print("GetNotificationsViewController - closeSelfieScreenIfPresent() returned: \(selfieScreenWasClosed)")

    if selfieScreenWasClosed {
        // Delay processing to allow screen transition to complete
        print("GetNotificationsViewController - ✅ Selfie screen was closed, delaying result processing for screen transition (300ms)")

        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
            print("GetNotificationsViewController - Processing notification update result after screen close (delayed)")
            self.handleNotificationUpdateResult(status: status)
        }

        print("GetNotificationsViewController - Exiting early, asyncAfter will handle processing")
        return // Exit early, let asyncAfter handle the rest
    }

    // Selfie screen not present (password/LDA step-up), process immediately
    print("GetNotificationsViewController - ❌ Selfie screen was NOT closed (not present)")
    print("GetNotificationsViewController - Processing notification update result immediately")
    handleNotificationUpdateResult(status: status)
}

Now let's test the complete server biometric step-up implementation.

Server Configuration

Before testing, ensure your REL-ID server is configured for server biometric step-up:

Test Scenario 1: Server Biometric Success Flow

Test the complete server biometric step-up flow:

  1. Complete MFA flow and log in to dashboard
  2. Navigate to Notifications screen from side menu
  3. Verify notifications loaded - Check that getNotifications() succeeded
  4. Tap notification with authLevel=4 - Select action requiring server biometric
  5. Verify action modal appears - Modal displays action details
  6. Submit action - Tap action button
  7. Verify handler execution:
    Console logs should show:
    - "GetNotificationsViewController - Calling updateNotification API"
    - "GetNotificationsViewController - Selfie process start confirmation received"
    - "GetNotificationsViewController - Server biometric step-up for notification detected"
    - "GetNotificationsViewController - Action modal closed, selfie screen will appear"
    - "RDNAIDVDelegateManager - Selfie process start confirmation requested"
    
  8. Verify modal closes - Action modal should close automatically
  9. Verify selfie screen appears - IDVSelfieProcessStartConfirmationViewController on top
  10. Review workflow 9 guidelines - Screen shows notification step-up specific text
  11. Tap "Start Selfie Capture" - Initiate biometric capture
  12. Complete selfie capture - SDK captures selfie with liveness detection
  13. Verify result handling:
    Console logs should show:
    - "GetNotificationsViewController - Received update notification event"
    - "GetNotificationsViewController - Checking if selfie screen needs to be closed"
    - "GetNotificationsViewController - Found X view controller(s) above us"
    - "GetNotificationsViewController - Calling popToViewController(self, animated: false)"
    - "GetNotificationsViewController - Selfie screen was closed, delaying result processing (300ms)"
    - "GetNotificationsViewController - Processing notification update result after screen close"
    
  14. Verify screen closes - Selfie screen closes with popToViewController()
  15. Verify alert appears - Success alert displays on GetNotificationsViewController (not behind selfie)
  16. Navigate to dashboard - Tap "OK" button
  17. Verify notifications refresh - Latest notification list loaded

Test Scenario 2: Server Biometric with User Cancellation

Test user cancellation during selfie capture:

  1. Steps 1-9 - Same as Scenario 1 (up to selfie screen appearing)
  2. Tap Cancel/Back - User decides not to complete biometric
  3. Verify selfie screen closes - Returns to GetNotificationsViewController
  4. Verify action modal state - User can retry action if needed

Test Scenario 3: Server Biometric Failure

Test server biometric verification failure:

  1. Steps 1-11 - Same as Scenario 1 (up to selfie capture)
  2. Complete poor quality selfie - Intentionally provide poor lighting/angle
  3. SDK returns failure - Server verification fails
  4. Verify error handling:
    • Selfie screen closes
    • Error alert displays: "Biometric verification failed"
    • User can retry action from notifications screen

Verification Checklist

Use this checklist to verify your implementation:

Let's understand the architecture decisions and troubleshoot common issues.

Why deinit for Handler Cleanup?

The implementation uses deinit for handler cleanup instead of viewWillDisappear. This is a deliberate architectural choice:

deinit (runs on deallocation):

viewWillDisappear (would run too early):

Why deinit is better:

// Scenario with deinit (correct):
1. Screen appears → setupEventHandlers() registers handler
2. User taps action → updateNotification() called
3. Selfie screen appears → viewWillDisappear() runs (but handlers remain)
4. Selfie completes → onUpdateNotification handler still active ✅
5. Screen deallocated → deinit runs, handlers cleaned up

// Scenario with viewWillDisappear (incorrect):
1. Screen appears → setupEventHandlers() registers handler
2. User taps action → updateNotification() called
3. Selfie screen appears → viewWillDisappear() runs, handlers removed ❌
4. Selfie completes → onUpdateNotification handler is nil, event missed ❌

Why Navigation Stack Pattern Instead of State Flag?

Current approach (better): Check navigation controller stack directly

Advantages of navigation-based check:

  1. Single source of truth: Navigation controller state is authoritative
  2. No manual tracking: No need to set/unset flags
  3. No synchronization bugs: Flag can't get out of sync with actual screen state
  4. Works with manual navigation: User can press back button, still detected
// ✅ Better: Navigation stack as source of truth
if let selfIndex = navController.viewControllers.firstIndex(of: self) {
    if selfIndex < navController.viewControllers.count - 1 {
        // Screens are definitely present above us
    }
}

// ❌ Worse: Manual flag tracking
if isSelfieScreenActive {
    // Flag might be out of sync with actual screen state
}

Common Issues and Solutions

Issue 1: Action Modal Not Hiding

Symptoms: Selfie screen appears behind action modal

Solution: Verify handler registration

// Check console logs:
// - "GetNotificationsViewController - Registering IDV selfie handler"
// - "GetNotificationsViewController - IDV selfie handler registered"
// - "GetNotificationsViewController - Original selfie handler exists: true"

// Ensure handler is registered in setupEventHandlers()
RDNAIDVDelegateManager.shared.onGetIDVSelfieProcessStartConfirmation = { [weak self] idvWorkflow in
    self?.handleSelfieProcessStartForNotification(idvWorkflow: idvWorkflow)
    self?.originalGetSelfieProcessStartHandler?(idvWorkflow)
}

Issue 2: Alert Appears Behind Selfie Screen

Symptoms: Success alert not visible, selfie screen still open

Solution: Ensure navigation stack check and delay are implemented

let selfieScreenWasClosed = closeSelfieScreenIfPresent()

if selfieScreenWasClosed {
    // CRITICAL: 300ms delay for screen transition
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
        self.handleNotificationUpdateResult(status: status)
    }
    return
}

Issue 3: Handler Not Triggering for Workflow 9

Symptoms: Console logs show selfie event but handler not executing

Solution: Check property name (common mistake)

// ✅ Correct property name
RDNAIDVDelegateManager.shared.onGetIDVSelfieProcessStartConfirmation = handler

// ❌ Wrong property name (easy to confuse)
RDNAIDVDelegateManager.shared.onIDVSelfieProcessStartConfirmation = handler

Issue 4: Other IDV Workflows Broken

Symptoms: Biometric opt-in, KYC flows not working after implementation

Solution: Ensure workflow 9 check doesn't block other workflows

if idvWorkflow.rawValue == 9 {
    dismissActionModal()
    // Only affect workflow 9
} else {
    print("Different IDV workflow, ignoring: \(idvWorkflow.rawValue)")
    // Don't interfere with workflows 10, 6, 8, 15, etc.
}

Let's review important security considerations for server biometric authentication.

Biometric Template Security

Never expose biometric templates:

// ❌ Wrong - Logging biometric data
print("Biometric template: \(selfieData.template)")

// ✅ Correct - Only log workflow and status
print("Server biometric verification:", [
    "idvWorkflow": idvWorkflow.rawValue,
    "success": statusCode == 100
])

Liveness Detection

Trust SDK's liveness detection:

The Aware FaceCapture library embedded in RELIDIDV framework provides:

AuthLevel Enforcement

Never bypass authLevel requirements:

// ❌ Wrong - Trying to bypass server biometric
if requiresServerBiometric {
    // Don't call updateNotification again to skip auth
}

// ✅ Correct - Always respect backend authLevel
let error = RDNAService.shared.updateNotification(uuid, response: action)
// Let SDK handle authLevel=4 requirement via events

Congratulations! You've successfully implemented server biometric step-up authentication for notification actions with REL-ID SDK.

What You've Accomplished

In this codelab, you've learned how to:

✅ Understand AuthLevel Configuration: Learned how authLevel=4 triggers server biometric (vs challengeMode 3 for password)

✅ Implement IDV Workflow 9: Handled notification-specific selfie capture with liveness detection

✅ Use deinit Cleanup Pattern: Applied proper iOS lifecycle cleanup for reliable event handling

✅ Chain Event Handlers: Preserved global navigation while adding screen-specific logic (close modal)

✅ Manage Modal Lifecycle: Closed action modal before selfie screen to prevent UI blocking

✅ Use Navigation Stack Pattern: Navigated across view controller hierarchies to close screens smoothly

✅ Implement Screen Transition Timing: Used 300ms delays for smooth UI updates before alerts

✅ Integrate Liveness Detection: Leveraged Aware FaceCapture library for anti-spoofing

Key Takeaways

AuthLevel vs Challenge Mode:

deinit Cleanup Pattern:

Navigation Stack Pattern:

Handler Chaining:

Additional Resources

Thank you for completing this codelab! You now have the knowledge to implement secure, production-ready server biometric step-up authentication for notification actions in your iOS applications.

Happy Coding!