🎯 Learning Path:
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.
In this codelab, you'll enhance your existing notification application with:
useEffect + useFocusEffect for reliable event handlingBy completing this codelab, you'll master:
useEffect + useFocusEffect pattern for reliable handler registrationBefore starting this codelab, ensure you have:
useState, useEffect, useCallback, useFocusEffectThe 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/codelab-react-native.git
Navigate to the relid-IDV-post-login-step-up-auth-notification folder in the repository you cloned earlier
This codelab adds server biometric step-up authentication to your notification application with three core enhancements:
getIDVSelfieProcessStartConfirmation handler for workflow 9 with dual registration patternnavigation.getParent() for smooth transitionsBefore implementing server biometric authentication, let's understand the key differences from password step-up and the unique event flow.
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 |
|
|
SDK Event Triggered |
|
|
Event Parameter |
|
|
UI Pattern | Modal dialog (StepUpPasswordDialog) | Full screen navigation (IDVSelfieProcessStart) |
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 |
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: getIDVSelfieProcessStartConfirmation Event (idvWorkflow: 9) →
[Screen-Level Handler Executes FIRST]:
handleSelfieProcessStartForNotification() →
Checks if data.idvWorkflow === 9 →
Calls setShowActionModal(false) → Action Modal Closes →
[Global Handler Executes SECOND]:
originalGetSelfieHandler() (from SDKIDVEventProvider) →
Navigates to IDVSelfieProcessStart Screen →
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 Parent Navigator →
Closes Selfie Screen with parentNav.goBack() →
Delays 300ms for Screen Transition →
Displays Success/Error Alert →
Navigates to Dashboard
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 | Standard security actions (prerequisite codelab) |
3 | LDA (Local Device Auth) | SDK handles biometric internally | Standard biometric actions |
4 | Server Biometric | SDK triggers | High-security actions requiring liveness |
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 |
The REL-ID SDK uses these components for server biometric step-up:
Component | Description | Developer Action |
Aware FaceCapture Library | Embedded in react-native-rdna-client for liveness detection | Pre-configured, no action needed |
getIDVSelfieProcessStartConfirmation | SDK event when selfie capture is required (idvWorkflow: 9) | Register handler in GetNotificationsScreen |
IDVSelfieProcessStart Screen | 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 getIDVSelfieProcessStartConfirmation events for workflow 9.
For server biometric step-up, we need two actions when the selfie event is triggered:
This requires a handler chaining pattern where our screen handler executes first, then calls the global handler:
// Screen handler executes first
handleSelfieProcessStartForNotification(data); // Close modal
// Then call global handler
if (originalHandler) originalHandler(data); // Navigate to selfie screen
Add the handler that intercepts workflow 9 events to close the action modal:
// src/tutorial/screens/notification/GetNotificationsScreen.tsx
// Add imports at the top
import { useFocusEffect } from '@react-navigation/native';
import rdnaIDVService from '../../../uniken/services/idv/rdnaIDVService';
import type {
RDNAIDVGetSelfieProcessStartConfirmationData
} from '../../../uniken/types/idv/rdnaIDVEvents';
/**
* Handle selfie process start for notification step-up (idvWorkflow: 9)
*
* CRITICAL: Close action modal BEFORE SDK navigates to selfie screen
*/
const handleSelfieProcessStartForNotification = useCallback(
(data: RDNAIDVGetSelfieProcessStartConfirmationData) => {
console.log('GetNotificationsScreen - Selfie process start confirmation received:', {
idvWorkflow: data.idvWorkflow,
challengeMode: data.challengeMode
});
// Check if this is notification step-up flow (idvWorkflow: 9)
if (data.idvWorkflow === 9) {
console.log('GetNotificationsScreen - Server biometric step-up for notification detected');
// CRITICAL: Close action modal to allow selfie screen to appear on top
setShowActionModal(false);
console.log('GetNotificationsScreen - Action modal closed, selfie screen will appear');
// SDK will automatically navigate to IDVSelfieProcessStart screen
// Global handler in SDKIDVEventProvider will handle navigation
} else {
console.log('GetNotificationsScreen - Different IDV workflow, ignoring:', data.idvWorkflow);
}
},
[]
);
Use both useEffect and useFocusEffect to ensure the handler persists through navigation events:
// src/tutorial/screens/notification/GetNotificationsScreen.tsx
/**
* Register event handlers on mount
* Includes IDV selfie handler for server biometric step-up
*/
useEffect(() => {
loadNotifications();
const eventManager = rdnaService.getEventManager();
const idvEventManager = rdnaIDVService.getEventManager();
// Register notification handlers
eventManager.setGetNotificationsHandler(handleNotificationsReceived);
eventManager.setUpdateNotificationHandler(handleUpdateNotificationReceived);
// Register password handler (challengeMode 3) - from prerequisite codelab
const originalGetPasswordHandler = (eventManager as any).getPasswordHandler;
const wrappedGetPasswordHandler = (data: RDNAGetPasswordData) => {
handleGetPasswordStepUp(data, originalGetPasswordHandler);
};
eventManager.setGetPasswordHandler(wrappedGetPasswordHandler);
// NEW: Register IDV selfie handler for server biometric step-up
console.log('GetNotificationsScreen - Registering IDV selfie handler');
const originalGetSelfieHandler = (idvEventManager as any).getSelfieProcessStartConfirmationHandler;
console.log('GetNotificationsScreen - Original selfie handler exists:', !!originalGetSelfieHandler);
const wrappedGetSelfieHandler = (data: RDNAIDVGetSelfieProcessStartConfirmationData) => {
// CRITICAL: Screen handler executes FIRST (close modal)
handleSelfieProcessStartForNotification(data);
// THEN: Global handler executes (navigate to selfie screen)
if (originalGetSelfieHandler) {
originalGetSelfieHandler(data);
}
};
idvEventManager.setGetSelfieProcessStartConfirmationHandler(wrappedGetSelfieHandler);
console.log('GetNotificationsScreen - IDV selfie handler registered');
// Cleanup: restore original handlers
return () => {
eventManager.setGetNotificationsHandler(undefined);
eventManager.setUpdateNotificationHandler(undefined);
eventManager.setGetPasswordHandler(originalGetPasswordHandler);
idvEventManager.setGetSelfieProcessStartConfirmationHandler(originalGetSelfieHandler);
};
}, [
handleNotificationsReceived,
handleUpdateNotificationReceived,
handleGetPasswordStepUp,
handleSelfieProcessStartForNotification
]);
Add useFocusEffect to ensure the handler remains active after navigation:
// src/tutorial/screens/notification/GetNotificationsScreen.tsx
/**
* Re-register IDV selfie handler when screen is focused
*
* CRITICAL: This ensures our handler is active even after navigation events
*/
useFocusEffect(
useCallback(() => {
console.log('GetNotificationsScreen - Screen focused, re-registering IDV selfie handler');
const idvEventManager = rdnaIDVService.getEventManager();
const originalGetSelfieHandler = (idvEventManager as any).getSelfieProcessStartConfirmationHandler;
const wrappedGetSelfieHandler = (data: RDNAIDVGetSelfieProcessStartConfirmationData) => {
handleSelfieProcessStartForNotification(data);
if (originalGetSelfieHandler) {
originalGetSelfieHandler(data);
}
};
idvEventManager.setGetSelfieProcessStartConfirmationHandler(wrappedGetSelfieHandler);
}, [handleSelfieProcessStartForNotification])
);
Now let's implement the enhanced onUpdateNotification handler that closes the selfie screen before showing alerts.
After server biometric verification completes, the onUpdateNotification event is triggered. At this point, the selfie screen (IDVSelfieProcessStart) may still be visible on top of the GetNotificationsScreen.
We need to:
The parent navigator pattern allows us to navigate across hierarchies:
DrawerNavigator (parent)
├─ GetNotificationsScreen (current screen)
└─ IDVSelfieProcessStart (modal screen on top)
To close the selfie screen from GetNotificationsScreen, we access the parent navigator:
const parentNav = navigation.getParent(); // Get DrawerNavigator
const state = parentNav.getState(); // Get navigation state
const currentRoute = state.routes[state.index]; // Get top screen
if (currentRoute?.name === 'IDVSelfieProcessStart') {
parentNav.goBack(); // Close selfie screen
}
First, extract the notification update result processing into a separate function:
// src/tutorial/screens/notification/GetNotificationsScreen.tsx
/**
* Handle notification update result
* Extracted logic to process notification update response
*/
const handleNotificationUpdateResult = useCallback((data: RDNAUpdateNotificationData) => {
console.log('GetNotificationsScreen - Processing notification update result');
// Check for errors first
if (data.error.longErrorCode !== 0) {
const errorMessage = data.error.errorString || 'Failed to update notification';
setShowStepUpAuth(false);
Alert.alert('Update Failed', errorMessage);
return;
}
const responseData = data.pArgs?.response;
const statusCode = responseData?.StatusCode;
const statusMessage = responseData?.StatusMsg || 'Unknown error occurred';
if (statusCode === 100) {
// Success - notification action completed
setShowStepUpAuth(false);
setShowActionModal(false);
Alert.alert(
'Success',
statusMessage || 'Notification action completed successfully',
[{
text: 'OK',
onPress: () => {
navigation.navigate('DrawerNavigator', { screen: 'Dashboard' });
}
}]
);
loadNotifications();
} else if (statusCode === 110 || statusCode === 153) {
// Critical errors - show alert BEFORE SDK logout
setShowStepUpAuth(false);
setShowActionModal(false);
Alert.alert('Authentication Failed', statusMessage);
} else {
// Other errors
setShowStepUpAuth(false);
Alert.alert('Update Failed', statusMessage);
}
}, [navigation, loadNotifications]);
Enhance the handleUpdateNotificationReceived handler to detect and close the selfie screen:
// src/tutorial/screens/notification/GetNotificationsScreen.tsx
/**
* Handle update notification response
*
* NEW: Checks if selfie screen is active and closes it before showing alerts
*/
const handleUpdateNotificationReceived = useCallback((data: RDNAUpdateNotificationData) => {
console.log('GetNotificationsScreen - Received update notification event');
setActionLoading(false);
setStepUpSubmitting(false);
/**
* NEW: Check if selfie screen is active and close it before processing
* Uses parent navigator pattern to check navigation stack
*/
const closeSelfieScreenIfPresent = () => {
console.log('GetNotificationsScreen - Checking if selfie screen needs to be closed');
try {
// Get parent navigator (DrawerNavigator)
const parentNav = navigation.getParent();
if (parentNav) {
const state = parentNav.getState();
const currentRoute = state.routes[state.index];
// Check if IDVSelfieProcessStart screen is on top
if (currentRoute?.name === 'IDVSelfieProcessStart') {
console.log('GetNotificationsScreen - IDVSelfieProcessStart screen detected, closing it');
// Navigate back to close selfie screen
parentNav.goBack();
console.log('GetNotificationsScreen - Selfie screen closed via parent navigator');
return true; // Screen was present and closed
} else {
console.log('GetNotificationsScreen - No IDVSelfieProcessStart screen on top, route:', currentRoute?.name);
}
} else {
console.warn('GetNotificationsScreen - Parent navigator not found');
}
} catch (error) {
console.error('GetNotificationsScreen - Error checking/closing selfie screen:', error);
}
return false; // Screen not present or couldn't close
};
// Check and close selfie screen if present
const selfieScreenWasClosed = closeSelfieScreenIfPresent();
if (selfieScreenWasClosed) {
// NEW: Delay processing to allow screen transition to complete
console.log('GetNotificationsScreen - Delaying result processing for screen transition (300ms)');
setTimeout(() => {
console.log('GetNotificationsScreen - Processing notification update result after screen close');
handleNotificationUpdateResult(data);
}, 300); // 300ms allows smooth screen transition
return; // Exit early, let setTimeout handle the rest
}
// Selfie screen not present (password/LDA step-up), process immediately
console.log('GetNotificationsScreen - Processing notification update result immediately');
handleNotificationUpdateResult(data);
}, [navigation, handleNotificationUpdateResult]);
Now let's test the complete server biometric step-up implementation.
Before testing, ensure your REL-ID server is configured for server biometric step-up:
Test the complete server biometric step-up flow:
getNotifications() succeededConsole logs should show:
- "GetNotificationsScreen - Calling updateNotification API"
- "GetNotificationsScreen - Selfie process start confirmation received"
- "GetNotificationsScreen - Server biometric step-up for notification detected"
- "GetNotificationsScreen - Action modal closed, selfie screen will appear"
- "SDKIDVEventProvider - Selfie process start confirmation requested"
Console logs should show:
- "GetNotificationsScreen - Received update notification event"
- "GetNotificationsScreen - Checking if selfie screen needs to be closed"
- "GetNotificationsScreen - IDVSelfieProcessStart screen detected, closing it"
- "GetNotificationsScreen - Selfie screen closed via parent navigator"
- "GetNotificationsScreen - Delaying result processing for screen transition (300ms)"
- "GetNotificationsScreen - Processing notification update result after screen close"
Test user cancellation during selfie capture:
Test server biometric verification failure:
Use this checklist to verify your implementation:
useEffect and useFocusEffectparentNav.goBack() before alertLet's understand the architecture decisions and troubleshoot common issues.
The implementation uses both useEffect and useFocusEffect for IDV handler registration. This is a deliberate architectural choice:
useEffect
(runs on mount):
useFocusEffect
(runs on screen focus):
Why both are needed:
// Scenario without useFocusEffect:
1. Screen mounts → useEffect registers handler
2. User navigates to selfie screen → SDKIDVEventProvider may re-register
3. User goes back to notifications screen → Screen focused
4. Handler might be lost → useFocusEffect re-registers it
Current approach (better): Check parent navigator state directly
Advantages of navigator-based check:
// ✅ Better: Navigator state as source of truth
const currentRoute = parentNav.getState().routes[state.index];
if (currentRoute?.name === 'IDVSelfieProcessStart') {
// Screen is definitely present
}
// ❌ Worse: Manual flag tracking
if (isSelfieScreenActive) {
// Flag might be out of sync with actual screen state
}
Issue 1: Action Modal Not Hiding
Symptoms: Selfie screen appears behind action modal
Solution: Verify handler registration
// Check console logs:
// - "GetNotificationsScreen - Registering IDV selfie handler"
// - "GetNotificationsScreen - IDV selfie handler registered"
// - "GetNotificationsScreen - Screen focused, re-registering IDV selfie handler"
// Ensure useFocusEffect is included
useFocusEffect(
useCallback(() => {
// Re-register handler
}, [handleSelfieProcessStartForNotification])
);
Issue 2: Alert Appears Behind Selfie Screen
Symptoms: Success alert not visible, selfie screen still open
Solution: Ensure parent navigator check and delay are implemented
const selfieScreenWasClosed = closeSelfieScreenIfPresent();
if (selfieScreenWasClosed) {
// CRITICAL: 300ms delay for screen transition
setTimeout(() => {
handleNotificationUpdateResult(data);
}, 300);
return;
}
Issue 3: Handler Not Triggering for Workflow 9
Symptoms: Console logs show selfie event but handler not executing
Solution: Check method name (common mistake)
// ✅ Correct method name
idvEventManager.setGetSelfieProcessStartConfirmationHandler(wrappedHandler);
// ❌ Wrong method name (easy to confuse)
idvEventManager.setSelfieProcessStartConfirmationHandler(wrappedHandler);
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 (data.idvWorkflow === 9) {
setShowActionModal(false);
// Only affect workflow 9
} else {
console.log('Different IDV workflow, ignoring:', data.idvWorkflow);
// Don't interfere with workflows 10, 6, 8, 15, etc.
}
Let's review important security considerations for server biometric authentication.
Never expose biometric templates:
// ❌ Wrong - Logging biometric data
console.log('Biometric template:', selfieData.template);
// ✅ Correct - Only log workflow and status
console.log('Server biometric verification:', {
idvWorkflow: data.idvWorkflow,
success: statusCode === 100
});
Trust SDK's liveness detection:
The Aware FaceCapture library embedded in react-native-rdna-client provides:
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
try {
await rdnaService.updateNotification(uuid, action);
// Let SDK handle authLevel=4 requirement via events
} catch (error) {
// Handle API errors only
}
Log security-relevant events:
// ✅ Correct - Audit logging
console.log('Server biometric step-up initiated:', {
notificationUUID,
action,
timestamp: new Date().toISOString()
});
console.log('Server biometric verification result:', {
success: statusCode === 100,
statusCode,
timestamp: new Date().toISOString()
});
Compliance considerations:
Congratulations! You've successfully implemented server biometric step-up authentication for notification actions with REL-ID SDK.
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 Dual Handler Registration: Applied useEffect + useFocusEffect pattern 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 Parent Navigator Pattern: Navigated across drawer/stack 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
AuthLevel vs Challenge Mode:
authLevel = 1, challengeMode = 3, modal dialogauthLevel = 4, idvWorkflow = 9, full screen selfie captureDual Registration Pattern:
useEffect: Register on mount, cleanup on unmountuseFocusEffect: Re-register on focus to survive navigation eventsParent Navigator Pattern:
navigation.getParent()state.routes[state.index]parentNav.goBack()Handler Chaining:
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 React Native applications.
Happy Coding! 🚀