Learning Path:
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.
You'll enhance your existing data signing app with:
By completing this codelab, you'll master:
authLevel = .RDNA_AUTH_LEVEL_4 with authenticatorType = .RDNA_IDV_SERVER_BIOMETRIC for server biometricBefore starting, ensure you have:
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.
IDV Workflow 16 is the server biometric step-up authentication flow specifically designed for data signing operations. Unlike other IDV workflows, workflow 16:
authenticateUserAndSignData() is called with authLevel = .RDNA_AUTH_LEVEL_4 and authenticatorType = .RDNA_IDV_SERVER_BIOMETRICIDV 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 |
Data signing uses a view controller-level handler because:
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
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.
We'll modify DataSigningInputViewController.swift to add three key pieces:
Why NavigationController?
navigationController propertytopViewControllerguard let navigationController = navigationController else { return false }
if let topVC = navigationController.topViewController,
topVC is IDVSelfieProcessStartConfirmationViewController {
navigationController.popViewController(animated: false)
return true
}
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()
Why 300ms?
if selfieScreenWasClosed {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
self?.processDataSigningResult(dataSigningDetails, status, error)
}
return
}
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
In this codelab, you'll modify only one file:
File | Purpose | Changes |
| 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.
Every UIViewController has a built-in navigationController property that provides access to its parent navigation controller. We'll use this to:
topViewController to see what's on toppopViewController(animated:) to dismiss itIn 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
The navigationController property provides:
is operator for reliable type checkingYour DataSigningInputViewController should already be embedded in a navigation controller from the MFA codelab setup. The storyboard should show:
UINavigationController
└── DataSigningInputViewController
Now that we understand navigationController access, we'll use it to:
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.
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)
}
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)
Now we have error priority processing ready. In the next step, we'll implement the main handler that:
processDataSigningResult methodNow let's implement the main handler that detects and closes the selfie screen before processing results.
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 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
}
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:
navigationController: Built-in property from UIViewControllertopViewController: Gets the active view controller on top of navigation stackis IDVSelfieProcessStartConfirmationViewController: Swift type checkingpopViewController(animated: false): Closes the screen without animationlet 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?:
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
Scenario 1: Server Biometric Success
Scenario 2: Server Biometric Error
Scenario 3: Password/LDA Success
Scenario 4: Password/LDA Error
The implementation handles:
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.
Expected Error Messages:
Core Functionality:
Error Handling:
Other Authentication Methods:
Edge Cases:
Issue: Selfie screen doesn't close
navigationController property is not nilIssue: 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!
authLevel = .RDNA_AUTH_LEVEL_4 with server biometricNavigationController 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)
Before deploying, ensure:
Explore More REL-ID Features:
Enhance Your Implementation:
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!