🎯 Learning Path:
Welcome to the REL-ID Server Biometric Step-Up Authentication for Notifications codelab! This tutorial teaches you how to implement high-security notification actions that require additional authentication (password, LDA, or server biometric) based on backend configuration.
In this codelab, you'll implement a notification action system with three authentication levels:
Password Step-Up Authentication (authLevel = 1):
onCancel to hide loading, onSubmit to track stateLDA Step-Up Authentication (authLevel = 3):
Server Biometric Step-Up Authentication (authLevel = 4 - PRIMARY FOCUS):
CRITICAL: This implementation is fundamentally different from other IDV flows:
❌ OTHER IDV FLOWS (Opt-In, KYC, Additional Document Scan):
- Screen registers getSelfieProcessStartConfirmationHandler
- Screen implements handleSelfieProcessStart() method
- Screen manages navigation to selfie screen
- Screen implements handler chaining patterns
✅ SERVER BIOMETRIC STEP-UP FOR NOTIFICATIONS (Workflow 9):
- SDK automatically detects authLevel=4 from backend
- SDKIDVEventProvider handles ALL selfie navigation
- GetNotificationsScreen ONLY handles onUpdateNotification result event
- NO screen-level IDV event handlers needed
- NO getSelfieProcessStartConfirmationHandler registration
- NO handleSelfieProcessStart() method
By completing this codelab, you'll master:
onCancel and onSubmit for UI state managementBefore starting this codelab, ensure you have:
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/codelab-cordova.git
Navigate to the relid-IDV-post-login-step-up-auth-notification folder in the repository you cloned earlier
This codelab demonstrates a notification action system with four core components:
The screen is intentionally simple because the SDK handles most of the workflow 9 complexity automatically.
Before implementing step-up authentication, let's understand the complete authentication flow and how the backend determines the authentication method.
The backend configures the authLevel for each notification action. The SDK automatically uses the appropriate authentication method:
AuthLevel | Authentication Method | When Used | Implementation Complexity |
1 | Password | Standard security actions | Medium (dialog with callbacks) |
3 | LDA (Local Device Auth) | Standard biometric actions | Low (SDK handles internally) |
4 | Server Biometric | High-security actions requiring liveness | Low (SDK handles automatically) |
Understanding the complete flow is crucial:
1. User Opens GetNotifications Screen
└─> Screen loads and displays notifications automatically
2. User Taps Notification Card
└─> Action modal appears with available actions
3. User Selects Action and Taps Action Button
└─> StepUpAuthManager.setContext() called with notification details and callbacks
└─> Modal closes
└─> Loading indicator shown
└─> rdnaService.updateNotification(notificationUUID, actionValue) called
4. SDK Receives Backend Response with authLevel = 4
└─> SDK AUTOMATICALLY detects workflow 9 requirement
└─> SDK checks if user has biometric template (from opt-in)
5. SDK Automatically Triggers Selfie Capture
└─> SDKIDVEventProvider receives getIDVSelfieProcessStartConfirmation event
└─> SDKIDVEventProvider AUTOMATICALLY navigates to IDVSelfieProcessStart screen
└─> NO GetNotificationsScreen involvement needed!
6. User Captures Selfie on Native Screen
└─> Liveness detection active (Aware FaceCapture)
└─> SDK sends selfie to server for template matching
7. SDK Receives Verification Result
└─> SDK triggers onUpdateNotification event
└─> GetNotificationsScreen.handleUpdateNotificationResponse() executes
8. GetNotificationsScreen Handles Result
└─> Navigates back to GetNotifications screen
└─> Waits 300ms for screen transition
└─> Shows alert with result
└─> Reloads notifications after alert dismissed
For password authentication, the flow is different:
1. User Selects Notification Action
└─> StepUpAuthManager.setContext() called with callbacks
└─> rdnaService.updateNotification() called
2. SDK Receives Backend Response with authLevel = 1
└─> SDK triggers getPassword event with challengeMode = 3
└─> SDKEventProvider delegates to StepUpAuthManager
└─> StepUpPasswordDialog.show() displays password dialog
3. User Enters Password
└─> onSubmit callback fired (keeps loading visible)
└─> rdnaService.setPassword(password, 3) called
4. SDK Validates Password
└─> If valid: Processes updateNotification
└─> If invalid: Shows error, keeps dialog open
└─> If cancelled: onCancel callback fired (hides loading)
5. SDK Triggers onUpdateNotification Event
└─> GetNotificationsScreen.handleUpdateNotificationResponse() executes
└─> Navigate back → Wait 300ms → Show alert → Reload
For LDA authentication:
1. User Selects Notification Action
└─> rdnaService.updateNotification() called
2. SDK Receives Backend Response with authLevel = 3
└─> SDK INTERNALLY triggers LDA prompt (Face ID, Touch ID, Fingerprint)
└─> NO dialog shown by application
3. User Authenticates with Biometric
└─> If success: SDK processes updateNotification
└─> If cancelled: SDK returns error code 131 (silent handling)
4. SDK Triggers onUpdateNotification Event
└─> If error code 131: Navigate back without showing alert
└─> Otherwise: Navigate back → Wait 300ms → Show alert → Reload
Component | Responsibility | Workflow 9 Involvement |
GetNotificationsScreen | Display notifications, handle action selection, process results | ✅ Handles result only |
StepUpAuthManager | Manage password step-up context and callbacks | ✅ Password flow only |
StepUpPasswordDialog | Display password dialog with notification context | ✅ Password flow only |
SDKIDVEventProvider | Global handler for workflow 9 selfie navigation | ✅ Handles ALL selfie navigation |
SDKEventProvider | Global handler for password step-up (challengeMode 3) | ✅ Password flow only |
Let's set up the project environment and install the REL-ID Cordova plugin.
Install required npm packages:
npm install
Add the REL-ID Cordova plugin from the local directory:
cordova plugin add ./RdnaClient
The project uses an automated Cordova hook for IDV asset management, but server biometric step-up authentication does not require document scanning assets. Unlike document scanning flows (Additional Document Scan, KYC), this flow only needs the Aware FaceCapture library for biometric verification.
Document Scanner Assets (Not Required):
regula.license - Document reader license (only for document scanning)db.dat - 110.5MB Regula document recognition database (only for document scanning)Certificates.bundle/ - ICAO PKD certificates (only for document scanning)idv-native-resources/ folder - Not needed for biometric authenticationKey Difference: This codelab demonstrates server biometric step-up authentication for notifications which only uses selfie capture with liveness detection when authLevel=4. It does NOT scan identity documents, so document scanner assets are not required.
Biometric Capture Library (Pre-configured):
cordova-plugin-rdna plugin If you're building a document scanning flow (like KYC or Additional Document Scan), you'll need the automated hook structure:
your-cordova-project/
├── idv-native-resources/ # Only needed for document scanning flows
│ ├── common/ # Shared files (regula.license, db.dat)
│ ├── ios/ # iOS certificates
│ └── android/ # Android certificates
├── hooks/
│ └── after_prepare/
│ └── copy_idv_native_resources.js # Automated hook script
Hook Script: hooks/after_prepare/copy_idv_native_resources.js
When you run cordova prepare or cordova build, this hook automatically copies assets to platform-specific directories.
For This Server Biometric Step-Up Notification Codelab:
idv-native-resources/ folder is optional (can be empty or omitted)Add iOS and Android platforms:
cordova platform add ios
cordova platform add android
Prepare the project for both platforms:
cordova prepare
This command:
For iOS:
cordova run ios
For Android:
cordova run android
The StepUpAuthManager is a centralized module that manages password step-up authentication context and callbacks. Let's understand its implementation.
The StepUpAuthManager provides:
onCancel and onSubmit callbacks for loading stateThe context includes notification details and lifecycle callbacks:
// www/src/uniken/managers/StepUpAuthManager.js
/**
* Step-up authentication context
* Stores notification details when updateNotification triggers step-up auth
*/
_context: {
notificationUUID: null,
notificationTitle: '',
notificationMessage: '',
action: null,
userID: '',
sessionParams: {},
onCancel: null, // Callback when user cancels password dialog
onSubmit: null // Callback when user submits password
},
Before calling updateNotification, GetNotificationsScreen sets the context:
// www/src/tutorial/screens/notification/GetNotificationsScreen.js
// Set step-up context in StepUpAuthManager (modular state management)
// SDKEventProvider will delegate to StepUpAuthManager when challengeMode 3 is triggered
if (typeof StepUpAuthManager !== 'undefined') {
StepUpAuthManager.setContext({
notificationUUID: notificationUUID,
notificationTitle: title,
notificationMessage: message,
action: actionValue,
userID: this.sessionParams.userID || '',
sessionParams: this.sessionParams,
// Callback when user cancels password dialog
onCancel: () => {
console.log('GetNotificationsScreen - Password dialog cancelled by user');
this.showLoading(false); // Hide loading indicator
},
// Callback when user submits password
onSubmit: () => {
console.log('GetNotificationsScreen - Password submitted, waiting for onUpdateNotification event');
// Keep loading indicator visible, wait for onUpdateNotification event
}
});
}
OnCancel Callback Flow:
User Taps Notification Action
└─> updateNotification() called, loading shown
└─> Backend responds with authLevel=1 (password required)
└─> SDK triggers getPassword (challengeMode 3)
└─> StepUpPasswordDialog shows
User Taps "Cancel" in Dialog
└─> StepUpAuthManager.handleCancel() called
└─> onCancel callback fired
└─> this.showLoading(false) executes in GetNotificationsScreen
└─> Loading indicator hidden
└─> Dialog closes
└─> Context cleared
OnSubmit Callback Flow:
User Enters Password and Taps "Submit"
└─> StepUpAuthManager.handlePasswordSubmit() called
└─> onSubmit callback fired
└─> Comment explains: "Keep loading indicator visible"
└─> rdnaService.setPassword(password, 3) called
└─> If valid: SDK processes updateNotification
└─> onUpdateNotification event fires
└─> handleUpdateNotificationResponse() handles result
└─> Loading hidden, dialog hidden, alert shown
setContext Method:
// www/src/uniken/managers/StepUpAuthManager.js
setContext(context) {
console.log('StepUpAuthManager - Setting context:', JSON.stringify({
notificationUUID: context.notificationUUID,
notificationTitle: context.notificationTitle,
action: context.action,
userID: context.userID,
hasOnCancel: !!context.onCancel,
hasOnSubmit: !!context.onSubmit
}, null, 2));
this._context = {
notificationUUID: context.notificationUUID || null,
notificationTitle: context.notificationTitle || '',
notificationMessage: context.notificationMessage || '',
action: context.action || null,
userID: context.userID || '',
sessionParams: context.sessionParams || {},
onCancel: context.onCancel || null,
onSubmit: context.onSubmit || null
};
}
handleCancel Method with Callback:
// www/src/uniken/managers/StepUpAuthManager.js
handleCancel() {
console.log('StepUpAuthManager - Authentication cancelled');
// Call onCancel callback if provided (e.g., to hide loading indicator)
if (this._context.onCancel) {
console.log('StepUpAuthManager - Calling onCancel callback');
this._context.onCancel();
}
// Clear context
this.clearContext();
// Hide modal
this.hidePasswordDialog();
}
handlePasswordSubmit Method with Callback:
// www/src/uniken/managers/StepUpAuthManager.js
handlePasswordSubmit(password) {
console.log('StepUpAuthManager - Password submitted');
// Call onSubmit callback if provided (e.g., to keep loading indicator visible)
if (this._context.onSubmit) {
console.log('StepUpAuthManager - Calling onSubmit callback');
this._context.onSubmit();
}
// Update modal to show loading state
if (typeof StepUpPasswordDialog !== 'undefined') {
StepUpPasswordDialog.update({ isSubmitting: true });
}
// Call setPassword with challengeMode 3
rdnaService.setPassword(password, 3)
.then((syncResponse) => {
console.log('StepUpAuthManager - setPassword sync response:', JSON.stringify({
longErrorCode: syncResponse.error?.longErrorCode,
errorString: syncResponse.error?.errorString
}, null, 2));
// If setPassword succeeds, SDK will either:
// 1. Trigger getPassword again if password is wrong (with error status)
// 2. Process the updateNotification and trigger onUpdateNotification
// The modal will stay visible until we get the final response
})
.catch((error) => {
console.error('StepUpAuthManager - setPassword sync error:', JSON.stringify(error, null, 2));
const errorMessage = error?.error?.errorString || 'Failed to verify password';
// Update modal with error
if (typeof StepUpPasswordDialog !== 'undefined') {
StepUpPasswordDialog.update({
errorMessage: errorMessage,
isSubmitting: false
});
}
});
}
Now let's implement the result handler that processes the onUpdateNotification event with the navigate-back-before-alert pattern.
This pattern is crucial for smooth UX in Single Page Applications:
CORRECT PATTERN (What We Use):
1. Navigate back to GetNotifications screen
2. Wait 300ms for screen transition to complete
3. Show alert with result
4. Reload notifications after alert dismissed
WHY THIS WORKS:
- Alert appears on correct screen (GetNotifications)
- Screen transition completes before alert shows
- No visual glitches or flickering
- User sees smooth navigation flow
The method handles three authentication scenarios:
// www/src/tutorial/screens/notification/GetNotificationsScreen.js
/**
* Handle updateNotification response event
*
* This handler receives the event after:
* - Password step-up authentication (challengeMode 3) completes
* - IDV selfie step-up authentication (idvWorkflow 9) completes
* - LDA authentication completes
*
* Pattern (matches AdditionalDocumentScanScreen):
* 1. Navigate back to GetNotifications screen
* 2. Show alert with 300ms delay
* 3. Refresh notifications after alert
*/
handleUpdateNotificationResponse(data) {
console.log('GetNotificationsScreen - Processing update notification response');
this.showLoading(false);
// Clear step-up context and hide modal via StepUpAuthManager
if (typeof StepUpAuthManager !== 'undefined') {
StepUpAuthManager.clearContext();
StepUpAuthManager.hidePasswordDialog();
}
const errorCode = data.error?.longErrorCode;
const statusCode = data.pArgs?.response?.StatusCode;
const statusMsg = data.pArgs?.response?.StatusMsg || 'Unknown error';
const errorString = data.error?.errorString || 'Failed to update notification';
// FIRST: Check error code (API-level error, including LDA cancelled)
if (errorCode !== 0) {
console.error('GetNotificationsScreen - API error:', errorCode, errorString);
// Handle LDA cancelled (errorCode 131) - Silent return
if (errorCode === 131) {
console.log('GetNotificationsScreen - LDA cancelled by user, silent return');
// Navigate back to GetNotifications without showing error
NavigationService.navigate('GetNotifications', this.sessionParams);
return;
}
// Navigate back to GetNotifications screen
NavigationService.navigate('GetNotifications', this.sessionParams);
// Show error alert after navigation
setTimeout(() => {
alert(`Notification Action Failed\n\nUnable to complete notification action.\n\n${errorString}\n\nError Code: ${errorCode}`);
this.loadNotifications(); // Refresh after alert
}, 300);
return;
}
// SECOND: Check status code (backend validation)
if (statusCode === 100) {
// Success case
console.log('GetNotificationsScreen - Notification updated successfully:', statusMsg);
// Navigate back to GetNotifications screen
NavigationService.navigate('GetNotifications', this.sessionParams);
// Show success alert after navigation
setTimeout(() => {
alert(statusMsg || 'Notification action completed successfully');
this.loadNotifications(); // Refresh after alert
}, 300);
} else if (statusCode === 110 || statusCode === 153) {
// Critical errors: Password expired (110) or Attempts exhausted (153)
console.warn('GetNotificationsScreen - Critical error, user will be logged out:', statusCode);
// Navigate back to GetNotifications screen
NavigationService.navigate('GetNotifications', this.sessionParams);
// Show critical error alert
setTimeout(() => {
alert(`Authentication Failed\n\n${statusMsg}\n\nYou will be logged out.`);
}, 300);
// SDK will automatically trigger onUserLoggedOff → getUser
} else {
console.error('GetNotificationsScreen - StatusCode error:', statusCode, statusMsg);
// Navigate back to GetNotifications screen
NavigationService.navigate('GetNotifications', this.sessionParams);
// Show error alert
setTimeout(() => {
alert(`Notification Action Failed\n\n${statusMsg}\n\nStatus Code: ${statusCode}`);
this.loadNotifications(); // Refresh after alert
}, 300);
}
}
1. Silent LDA Cancellation (Error Code 131):
// Handle LDA cancelled (errorCode 131) - Silent return
if (errorCode === 131) {
console.log('GetNotificationsScreen - LDA cancelled by user, silent return');
// Navigate back to GetNotifications without showing error
NavigationService.navigate('GetNotifications', this.sessionParams);
return; // NO alert shown
}
2. Navigate-Back-Before-Alert Pattern:
// Navigate back to GetNotifications screen
NavigationService.navigate('GetNotifications', this.sessionParams);
// Show success alert after navigation
setTimeout(() => {
alert(statusMsg || 'Notification action completed successfully');
this.loadNotifications(); // Refresh after alert
}, 300);
3. Auto-Refresh After Alert:
setTimeout(() => {
alert(statusMsg || 'Notification action completed successfully');
this.loadNotifications(); // Refresh notifications list
}, 300);
4. Critical Status Code Handling:
} else if (statusCode === 110 || statusCode === 153) {
// Critical errors: Password expired (110) or Attempts exhausted (153)
console.warn('GetNotificationsScreen - Critical error, user will be logged out:', statusCode);
// Navigate back to GetNotifications screen
NavigationService.navigate('GetNotifications', this.sessionParams);
// Show critical error alert
setTimeout(() => {
alert(`Authentication Failed\n\n${statusMsg}\n\nYou will be logged out.`);
}, 300);
// SDK will automatically trigger onUserLoggedOff → getUser
}
Notice that this handler does NOT include:
handleSelfieProcessStartForNotification() methodsetGetSelfieProcessStartConfirmationHandler() callsWhy?
Now let's implement the action selection flow that sets context and calls the updateNotification API.
The action selection flow follows this sequence:
1. User Taps Notification Card
└─> showActionModal(notification) displays modal
2. User Taps Action Button (e.g., "Approve", "Reject")
└─> handleActionButtonClick(actionValue) executes
3. Set StepUpAuthManager Context with Callbacks
└─> Context includes notification details
└─> onCancel callback to hide loading
└─> onSubmit callback to track password submission
4. Close Modal and Show Loading
└─> Modal closes
└─> Loading indicator shows
5. Call updateNotification API
└─> rdnaService.updateNotification(notificationUUID, actionValue)
6. Backend Responds with AuthLevel
└─> authLevel=1: SDK triggers getPassword (challengeMode 3)
└─> authLevel=3: SDK triggers LDA internally
└─> authLevel=4: SDK triggers workflow 9 selfie capture
7. Authentication Completes
└─> onUpdateNotification event fires
└─> handleUpdateNotificationResponse() processes result
This method is called when the user taps an action button in the modal:
// www/src/tutorial/screens/notification/GetNotificationsScreen.js
/**
* Handle action button click (React Native style - direct action)
*/
handleActionButtonClick(actionValue) {
if (!this.currentNotification || !actionValue) {
console.error('GetNotificationsScreen - Invalid action or notification');
return;
}
// Store UUID BEFORE closing modal (closeActionModal sets currentNotification to null!)
const notificationUUID = this.currentNotification.notification_uuid;
const title = this.currentNotification.body?.[0]?.subject || 'Notification Action';
const message = this.currentNotification.body?.[0]?.message || '';
console.log('GetNotificationsScreen - Action button clicked:', actionValue);
console.log('GetNotificationsScreen - Notification UUID:', notificationUUID);
// Set step-up context in StepUpAuthManager (modular state management)
// SDKEventProvider will delegate to StepUpAuthManager when challengeMode 3 is triggered
if (typeof StepUpAuthManager !== 'undefined') {
StepUpAuthManager.setContext({
notificationUUID: notificationUUID,
notificationTitle: title,
notificationMessage: message,
action: actionValue,
userID: this.sessionParams.userID || '',
sessionParams: this.sessionParams,
// Callback when user cancels password dialog
onCancel: () => {
console.log('GetNotificationsScreen - Password dialog cancelled by user');
this.showLoading(false); // Hide loading indicator
},
// Callback when user submits password
onSubmit: () => {
console.log('GetNotificationsScreen - Password submitted, waiting for onUpdateNotification event');
// Keep loading indicator visible, wait for onUpdateNotification event
}
});
}
// Close modal
this.closeActionModal();
// Show loading
this.showLoading(true);
// Call SDK updateNotification API
rdnaService.updateNotification(notificationUUID, actionValue)
.then((syncResponse) => {
console.log('GetNotificationsScreen - UpdateNotification sync response:', JSON.stringify(syncResponse, null, 2));
// Waiting for onUpdateNotification event
// If step-up auth is required, SDK will trigger getPassword with challengeMode 3
// SDKEventProvider will handle it and show StepUpPasswordDialog
})
.catch((error) => {
console.error('GetNotificationsScreen - UpdateNotification error:', JSON.stringify(error, null, 2));
this.showLoading(false);
this.showError('Failed to update notification. Please try again.');
// Clear step-up context on error
if (typeof StepUpAuthManager !== 'undefined') {
StepUpAuthManager.clearContext();
}
});
}
1. Store Notification Data Before Closing Modal:
// Store UUID BEFORE closing modal (closeActionModal sets currentNotification to null!)
const notificationUUID = this.currentNotification.notification_uuid;
const title = this.currentNotification.body?.[0]?.subject || 'Notification Action';
const message = this.currentNotification.body?.[0]?.message || '';
2. Set Context with Callbacks:
StepUpAuthManager.setContext({
notificationUUID: notificationUUID,
notificationTitle: title,
notificationMessage: message,
action: actionValue,
userID: this.sessionParams.userID || '',
sessionParams: this.sessionParams,
// Callback when user cancels password dialog
onCancel: () => {
console.log('GetNotificationsScreen - Password dialog cancelled by user');
this.showLoading(false); // Hide loading indicator
},
// Callback when user submits password
onSubmit: () => {
console.log('GetNotificationsScreen - Password submitted, waiting for onUpdateNotification event');
// Keep loading indicator visible, wait for onUpdateNotification event
}
});
3. Close Modal and Show Loading:
// Close modal
this.closeActionModal();
// Show loading
this.showLoading(true);
4. Call updateNotification API:
rdnaService.updateNotification(notificationUUID, actionValue)
.then((syncResponse) => {
console.log('GetNotificationsScreen - UpdateNotification sync response:', JSON.stringify(syncResponse, null, 2));
// Waiting for onUpdateNotification event
// If step-up auth is required, SDK will trigger getPassword with challengeMode 3
// SDKEventProvider will handle it and show StepUpPasswordDialog
})
.catch((error) => {
console.error('GetNotificationsScreen - UpdateNotification error:', JSON.stringify(error, null, 2));
this.showLoading(false);
this.showError('Failed to update notification. Please try again.');
// Clear step-up context on error
if (typeof StepUpAuthManager !== 'undefined') {
StepUpAuthManager.clearContext();
}
});
5. Error Handling:
.catch((error) => {
console.error('GetNotificationsScreen - UpdateNotification error:', JSON.stringify(error, null, 2));
this.showLoading(false);
this.showError('Failed to update notification. Please try again.');
// Clear step-up context on error
if (typeof StepUpAuthManager !== 'undefined') {
StepUpAuthManager.clearContext();
}
});
This method displays the action modal when user taps a notification card:
// www/src/tutorial/screens/notification/GetNotificationsScreen.js
/**
* Show action modal for notification (matches React Native UI)
*/
showActionModal(notification) {
console.log('GetNotificationsScreen - Opening action modal for notification:', notification.notification_uuid);
this.currentNotification = notification;
// Update modal content
const modalTitle = document.getElementById('notification-modal-title');
const modalBody = document.getElementById('notification-modal-body');
// Extract notification data (Cordova plugin structure)
const title = notification.body?.[0]?.subject || 'Notification';
const message = notification.body?.[0]?.message || '';
const createdTimestamp = notification.create_ts || '';
const expiryTimestamp = notification.expiry_timestamp || '';
const uuid = notification.notification_uuid || '';
const actions = notification.actions || [];
// Set modal title
if (modalTitle) modalTitle.textContent = 'Notification Actions';
// Build modal body with full details + action buttons (React Native style)
if (modalBody) {
modalBody.innerHTML = `
<div class="notification-details">
<h3 class="detail-title">${title}</h3>
<p class="detail-message">${message.replace(/<br\/>/g, '<br>')}</p>
<p class="detail-time">Created: ${this.formatTimestamp(createdTimestamp)}</p>
${expiryTimestamp ? `<p class="detail-time">Expires: ${this.formatTimestamp(expiryTimestamp)}</p>` : ''}
<p class="detail-uuid">ID: ${uuid}</p>
</div>
<div class="action-buttons-container">
${actions.map(action => `
<button class="action-button" data-action="${action.action}">
${action.label}
</button>
`).join('')}
</div>
`;
// Attach click handlers to action buttons
const actionButtons = modalBody.querySelectorAll('.action-button');
actionButtons.forEach(btn => {
btn.onclick = () => this.handleActionButtonClick(btn.getAttribute('data-action'));
});
}
// Show modal
const modal = document.getElementById('notification-action-modal');
if (modal) modal.style.display = 'flex';
}
Now let's implement the event handler registration that connects SDK events to our screen methods.
GetNotificationsScreen registers TWO event handlers:
getNotifications() APIupdateNotification() API (after authentication)CRITICAL: GetNotificationsScreen does NOT register:
getSelfieProcessStartConfirmationHandler - Handled by SDKIDVEventProvidergetIDVOptInCapturedFrameConfirmationHandler - Not needed for workflow 9onIDVOptInStatusHandler - Not needed for workflow 9// www/src/tutorial/screens/notification/GetNotificationsScreen.js
/**
* Register SDK event handlers for notifications
*/
registerSDKEventHandlers() {
const eventManager = rdnaService.getEventManager();
// Handle getNotifications response
eventManager.setGetNotificationsHandler((data) => {
console.log('GetNotificationsScreen - onGetNotifications event received');
this.handleGetNotificationsResponse(data);
});
// Handle updateNotification response
eventManager.setUpdateNotificationHandler((data) => {
console.log('GetNotificationsScreen - onUpdateNotification event received');
this.handleUpdateNotificationResponse(data);
});
// Note: getPassword (challengeMode 3) is handled centrally by SDKEventProvider
// We just need to set context before calling updateNotification
console.log('GetNotificationsScreen - SDK event handlers registered');
}
1. Handler Binding:
eventManager.setGetNotificationsHandler((data) => {
console.log('GetNotificationsScreen - onGetNotifications event received');
this.handleGetNotificationsResponse(data);
});
2. Centralized Password Handling:
// Note: getPassword (challengeMode 3) is handled centrally by SDKEventProvider
// We just need to set context before calling updateNotification
3. No IDV Event Handlers:
Notice we do NOT register:
// ❌ NOT NEEDED for workflow 9:
// eventManager.setGetSelfieProcessStartConfirmationHandler(...)
// eventManager.setIDVOptInCapturedFrameConfirmationHandler(...)
// eventManager.setIDVOptInStatusHandler(...)
The method is called during screen initialization:
// www/src/tutorial/screens/notification/GetNotificationsScreen.js
/**
* Called when screen content is loaded (SPA lifecycle)
*
* @param {Object} params - Navigation parameters
*/
onContentLoaded(params) {
console.log('GetNotificationsScreen - Content loaded');
// Store session params for navigation back to Dashboard
this.sessionParams = params || {};
// Store session params globally for use across screens (e.g., UpdatePassword)
if (typeof SDKEventProvider !== 'undefined') {
SDKEventProvider.setSessionParams(this.sessionParams);
}
// Setup UI
this.setupEventListeners();
this.registerSDKEventHandlers(); // Register SDK event handlers
// Auto-load notifications
this.loadNotifications();
}
Now let's test all three authentication scenarios: password, LDA, and server biometric step-up.
Before testing, ensure:
Test password authentication for notification actions:
Setup: Backend configures notification action with authLevel = 1
Test Steps:
Console logs:
- "GetNotificationsScreen - Loading notifications from server"
- "GetNotificationsScreen - onGetNotifications event received"
- "GetNotificationsScreen - Received X notifications"
- "GetNotificationsScreen - Rendered X notifications"
Console logs:
- "GetNotificationsScreen - Action button clicked: Approve"
- "StepUpAuthManager - Setting context"
- "GetNotificationsScreen - UpdateNotification sync response"
Console logs:
- "GetNotificationsScreen - Password dialog cancelled by user"
Console logs:
- "GetNotificationsScreen - Password submitted, waiting for onUpdateNotification event"
Console logs:
- "GetNotificationsScreen - onUpdateNotification event received"
- "GetNotificationsScreen - Processing update notification response"
- "GetNotificationsScreen - Notification updated successfully"
- "GetNotificationsScreen - Navigating back to GetNotifications screen"
Console logs:
- "GetNotificationsScreen - Loading notifications from server"
Verification Checklist:
Test LDA authentication for notification actions:
Setup: Backend configures notification action with authLevel = 3
Test Steps:
Console logs:
- "GetNotificationsScreen - onUpdateNotification event received"
- "GetNotificationsScreen - Notification updated successfully"
Console logs:
- "GetNotificationsScreen - API error: 131"
- "GetNotificationsScreen - LDA cancelled by user, silent return"
Verification Checklist:
Test server biometric authentication for notification actions (PRIMARY FOCUS):
Setup:
authLevel = 4Test Steps:
Console logs:
- "GetNotificationsScreen - Action button clicked"
- "GetNotificationsScreen - UpdateNotification sync response"
- "SDKIDVEventProvider - getIDVSelfieProcessStartConfirmation event received"
- "SDKIDVEventProvider - idvWorkflow: 9"
- "SDKIDVEventProvider - Navigating to IDVSelfieProcessStart screen"
Console logs:
- "Processing selfie..."
- "Verifying biometric template..."
Console logs:
- "GetNotificationsScreen - onUpdateNotification event received"
- "GetNotificationsScreen - Processing update notification response"
- "GetNotificationsScreen - Notification updated successfully"
- "GetNotificationsScreen - Navigating back to GetNotifications screen"
Verification Checklist:
Test password expiry and attempt exhaustion:
Test Steps (Password Expiry - Status Code 110):
Console logs:
- "GetNotificationsScreen - StatusCode error: 110"
- "GetNotificationsScreen - Critical error, user will be logged out: 110"
Test Steps (Attempts Exhausted - Status Code 153):
Console logs:
- "GetNotificationsScreen - StatusCode error: 153"
- "GetNotificationsScreen - Critical error, user will be logged out: 153"
Verification Checklist:
Issue 1: Password Dialog Not Appearing
Symptoms: Notification action triggered, but no password dialog shows
Solution:
// Verify StepUpAuthManager context is set BEFORE updateNotification
// Check console logs:
console.log('StepUpAuthManager - Setting context'); // Should appear BEFORE updateNotification
// Verify SDKEventProvider is handling getPassword event
console.log('SDKEventProvider - getPassword event received, challengeMode: 3');
Issue 2: Loading Indicator Stuck After Cancel
Symptoms: User cancels password dialog, but loading remains visible
Solution:
// Verify onCancel callback is set and called
onCancel: () => {
console.log('GetNotificationsScreen - Password dialog cancelled by user');
this.showLoading(false); // This MUST be called
}
Issue 3: Alert Appears During Screen Transition
Symptoms: Alert shows while screen is still transitioning, causing visual glitch
Solution:
// Verify 300ms delay is present
NavigationService.navigate('GetNotifications', this.sessionParams);
// MUST have setTimeout with 300ms delay
setTimeout(() => {
alert(statusMsg || 'Notification action completed successfully');
this.loadNotifications();
}, 300); // CRITICAL: 300ms delay
Issue 4: Selfie Screen Not Appearing (authLevel=4)
Symptoms: UpdateNotification called with authLevel=4, but selfie screen doesn't appear
Solution:
// Verify user has completed biometric opt-in
// Check template status before testing
// Verify SDKIDVEventProvider is handling getIDVSelfieProcessStartConfirmation
// Check console logs:
console.log('SDKIDVEventProvider - getIDVSelfieProcessStartConfirmation event received');
console.log('SDKIDVEventProvider - idvWorkflow: 9');
console.log('SDKIDVEventProvider - Navigating to IDVSelfieProcessStart screen');
Issue 5: Notifications Not Refreshing After Success
Symptoms: Alert shows, but notification list doesn't update
Solution:
// Verify loadNotifications() is called after alert
setTimeout(() => {
alert(statusMsg || 'Notification action completed successfully');
this.loadNotifications(); // MUST be called
}, 300);
Let's understand the key architectural decisions and design patterns used in this implementation.
Other IDV Flows (Opt-In, KYC, Additional Document Scan):
Screen registers getSelfieProcessStartConfirmationHandler
↓
Screen implements handleSelfieProcessStart() method
↓
Screen detects idvWorkflow parameter
↓
Screen navigates to IDVSelfieProcessStart screen
↓
Screen handles result and shows modals
Workflow 9 (Notification Step-Up):
GetNotificationsScreen calls updateNotification()
↓
SDK detects authLevel=4 from backend
↓
SDKIDVEventProvider receives getIDVSelfieProcessStartConfirmation event
↓
SDKIDVEventProvider automatically navigates to IDVSelfieProcessStart
↓
User completes selfie capture
↓
SDK triggers onUpdateNotification event
↓
GetNotificationsScreen handles result only
Design Goal: Keep GetNotificationsScreen focused on notification display and action selection, delegate password step-up logic to a separate manager.
Benefits:
Pattern:
GetNotificationsScreen (Context Provider)
↓ setContext()
StepUpAuthManager (Context Storage + Password Logic)
↑ onCancel/onSubmit callbacks
GetNotificationsScreen (Loading State Management)
Why This Pattern Exists:
In Single Page Applications, modals and alerts can appear on the wrong screen if navigation timing is incorrect. This pattern ensures alerts appear on the correct screen after smooth transitions.
Pattern:
// STEP 1: Navigate back to target screen
NavigationService.navigate('GetNotifications', this.sessionParams);
// STEP 2: Wait 300ms for screen transition animation
setTimeout(() => {
// STEP 3: Show alert (now on correct screen)
alert(statusMsg || 'Notification action completed successfully');
// STEP 4: Refresh data after alert dismissed
this.loadNotifications();
}, 300);
Timing Breakdown:
Design Goal: When users cancel LDA authentication (Face ID, Touch ID, Fingerprint), they intentionally chose not to proceed. Showing an error alert would be redundant and annoying.
Pattern:
// Check for LDA cancellation (error code 131)
if (errorCode === 131) {
console.log('GetNotificationsScreen - LDA cancelled by user, silent return');
// Navigate back WITHOUT showing alert
NavigationService.navigate('GetNotifications', this.sessionParams);
return; // NO alert, NO error message
}
User Experience:
User Taps Notification Action
↓
Native LDA Prompt Appears (Face ID)
↓
User Taps "Cancel" on Face ID Prompt
↓
Screen Returns to GetNotifications (Silent)
↓
User Can Retry or Choose Different Action
REL-ID SDK Pattern:
All SDK APIs follow the sync response + async event pattern:
1. Call SDK API (e.g., updateNotification)
↓
2. Receive SYNC response (validates request format)
↓
3. SDK processes request (authentication, backend communication)
↓
4. SDK triggers ASYNC event (e.g., onUpdateNotification)
↓
5. Event handler processes result
Example:
// SYNC: Immediate response
rdnaService.updateNotification(notificationUUID, actionValue)
.then((syncResponse) => {
console.log('UpdateNotification sync response:', syncResponse);
// syncResponse only validates request was accepted
// DOES NOT contain final result
});
// ASYNC: Final result via event
eventManager.setUpdateNotificationHandler((data) => {
console.log('onUpdateNotification event received');
// data contains final result after authentication
this.handleUpdateNotificationResponse(data);
});
Design Goal: Allow StepUpAuthManager to notify GetNotificationsScreen about password dialog lifecycle events without tight coupling.
Pattern:
// GetNotificationsScreen sets callbacks
StepUpAuthManager.setContext({
// ... notification data ...
onCancel: () => {
this.showLoading(false); // Hide loading when user cancels
},
onSubmit: () => {
// Keep loading visible when user submits password
// Wait for onUpdateNotification event
}
});
// StepUpAuthManager calls callbacks at appropriate times
handleCancel() {
if (this._context.onCancel) {
this._context.onCancel(); // Notify screen of cancellation
}
this.clearContext();
this.hidePasswordDialog();
}
handlePasswordSubmit(password) {
if (this._context.onSubmit) {
this._context.onSubmit(); // Notify screen of submission
}
// Continue with password validation
}
Benefits:
Let's review important security considerations for notification step-up authentication.
Avoid logging:
// ❌ Wrong - Logging password
console.log('Password submitted:', password);
// ❌ Wrong - Logging full notification data (may contain PII)
console.log('Notification data:', JSON.stringify(notification));
// ❌ Wrong - Logging biometric data
console.log('Biometric template:', data.biometricTemplate);
Safe logging:
// ✅ Correct - Log only non-sensitive metadata
console.log('Password submitted, waiting for validation');
// ✅ Correct - Log only notification UUID
console.log('Notification UUID:', notification.notification_uuid);
// ✅ Correct - Log only status codes
console.log('Authentication result:', {
statusCode: data.status?.statusCode,
errorCode: data.error?.longErrorCode
});
Always respect backend authLevel configuration:
// ✅ Correct - Let SDK handle authentication based on backend config
rdnaService.updateNotification(notificationUUID, actionValue)
.then(() => {
// SDK will automatically trigger appropriate authentication:
// authLevel=1: Password (challengeMode 3)
// authLevel=3: LDA (internal)
// authLevel=4: Server Biometric (workflow 9)
});
// ❌ Wrong - Hardcoding authentication method
// Always show password dialog regardless of authLevel
StepUpPasswordDialog.show(...);
Critical status codes require immediate action:
// Status Code 110: Password Expired
// Status Code 153: Attempts Exhausted
if (statusCode === 110 || statusCode === 153) {
console.warn('Critical error, user will be logged out:', statusCode);
// Show warning alert
alert(`Authentication Failed\n\n${statusMsg}\n\nYou will be logged out.`);
// SDK will automatically log out user
// DO NOT try to prevent logout
}
For authLevel=4 scenarios:
// ✅ Correct - SDK validates template on server
// Template matching happens on REL-ID server
// Client cannot manipulate biometric data
// ❌ Wrong - Client-side template validation
// Never trust client-side biometric validation
// Always use server-side validation (authLevel=4)
Clear context after use:
// ✅ Correct - Clear context after authentication completes
handleUpdateNotificationResponse(data) {
// ... process result ...
// Clear context immediately
if (typeof StepUpAuthManager !== 'undefined') {
StepUpAuthManager.clearContext();
StepUpAuthManager.hidePasswordDialog();
}
}
// ❌ Wrong - Leaving context set indefinitely
// Context contains notification details and callbacks
// Clear it to prevent memory leaks and security issues
Respect platform biometric policies:
// ✅ Correct - Let SDK handle LDA internally
// SDK respects platform security policies:
// - iOS: Face ID, Touch ID enrollment requirements
// - Android: Fingerprint, Face authentication policies
// ❌ Wrong - Bypassing LDA
// Never provide a way to skip LDA when authLevel=3
if (errorCode === 131) {
// User cancelled LDA - silent return
// DO NOT offer alternative authentication without backend approval
return;
}
Congratulations! You've successfully implemented server biometric step-up authentication for notifications with REL-ID SDK in Cordova.
In this codelab, you've learned how to:
✅ Understand AuthLevel Configuration: Learned how backend determines authentication method (password, LDA, server biometric)
✅ Implement StepUpAuthManager: Built centralized manager for password step-up with callback pattern
✅ Use Callback Pattern: Implemented onCancel and onSubmit callbacks for loading state management
✅ Handle Workflow 9 Automatically: Understood that SDK handles selfie navigation via SDKIDVEventProvider
✅ Implement Navigate-Back-Before-Alert: Applied SPA pattern for smooth screen transitions with 300ms delay
✅ Handle LDA Silent Cancellation: Processed error code 131 without showing alert for better UX
✅ Process Result Events: Handled onUpdateNotification event with success, error, and critical status codes
✅ Auto-Refresh Notifications: Reloaded notification list after actions complete
✅ Distinguish Workflow 9: Recognized that notification step-up is simpler than other IDV flows
Workflow 9 is SDK-Driven:
Modular State Management:
Navigate-Back-Before-Alert Pattern:
Silent LDA Cancellation:
AuthLevel Configuration:
Aspect | Workflow 9 (Notification Step-Up) | Other IDV Flows (Opt-In, KYC) |
Selfie Navigation | SDK automatic (SDKIDVEventProvider) | Screen-driven (getSelfieProcessStartConfirmationHandler) |
Screen Complexity | Low (only handles result) | High (manages entire flow) |
Event Handlers | 2 (getNotifications, updateNotification) | 4+ (selfie start, frame confirmation, status, etc.) |
Handler Chaining | Not needed | Required to preserve global handlers |
Navigation Logic | SDK-managed | Screen-managed |
Implementation Effort | Low | Medium-High |
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 Cordova applications.
Happy Coding!