π― Learning Path:
Welcome to the REL-ID Step-Up Authentication with Notifications codelab! This tutorial builds upon your existing MFA implementation to add secure re-authentication for sensitive notification actions using REL-ID SDK's step-up authentication capabilities.
In this codelab, you'll enhance your existing notification application with:
challengeMode = 3 (RDNA_OP_AUTHORIZE_NOTIFICATION)getPassword callback preservation for challengeMode 3By completing this codelab, you'll master:
Before 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-react-native.git
Navigate to the relid-step-up-auth-notification folder in the repository you cloned earlier
This codelab extends your notification application with three core step-up authentication components:
getPassword callback preservation for challengeMode 3 in GetNotificationsScreenonUpdateNotification event handler for critical errors (110, 153, 131)Before implementing step-up authentication, let's understand the key SDK events and APIs that power the notification action re-authentication workflow.
Step-up authentication is a security mechanism that requires users to re-authenticate when performing sensitive operations, even if they're already logged in. For notification actions, this adds an extra layer of security.
User Logged In β Acts on Notification β updateNotification() API β
SDK Checks if Action Requires Auth β Step-Up Authentication Required β
Password or LDA Verification β onUpdateNotification Event β Success/Failure
The step-up authentication process follows this event-driven pattern:
User Taps Notification Action β updateNotification(uuid, action) API Called β
SDK Determines Auth Method (Based on Login Method + Enrolled Credentials) β
IF Password Required:
SDK Triggers getPassword Event (challengeMode=3) β
StepUpPasswordDialog Displays β User Enters Password β
setPassword(password, 3) API β onUpdateNotification Event
IF LDA Required:
SDK Prompts Biometric Internally β User Authenticates β
onUpdateNotification Event (No getPassword event)
IF LDA Cancelled AND Password Enrolled:
SDK Directly Triggers getPassword Event (challengeMode=3) β No Error, Seamless Fallback β
StepUpPasswordDialog Displays β User Enters Password β
setPassword(password, 3) API β onUpdateNotification Event
IF LDA Cancelled AND Password NOT Enrolled:
onUpdateNotification Event with error code 131 β
Show Alert "Authentication Cancelled" β User Can Retry LDA
Challenge Mode 3 is specifically for notification action authorization:
Challenge Mode | Purpose | User Action Required | Screen | Trigger |
| Verify existing password | Enter password to login | VerifyPasswordScreen | User login attempt |
| Set new password | Create password during activation | SetPasswordScreen | First-time activation |
| Update password (user-initiated) | Provide current + new password | UpdatePasswordScreen | User taps "Update Password" |
| Authorize notification action | Re-enter password for verification | StepUpPasswordDialog (Modal) | updateNotification() requires auth |
| Update expired password | Provide current + new password | UpdateExpiryPasswordScreen | Server detects expired password |
Important: The SDK automatically determines which authentication method to use based on:
Login Method | Enrolled Methods | Step-Up Authentication Method | SDK Behavior |
Password | Password only | Password | SDK triggers |
LDA | LDA only | LDA | SDK prompts biometric internally, no |
Password | Both Password & LDA | Password | SDK triggers |
LDA | Both Password & LDA | LDA (with Password fallback) | SDK attempts LDA first. If user cancels, SDK directly triggers |
The REL-ID SDK triggers these main events during step-up authentication:
Event Type | Description | User Action Required |
Password required for notification action authorization | User re-enters password for verification | |
Notification action result (success/failure/auth errors) | System handles response and displays result |
Step-up authentication can fail with these critical errors:
Error/Status Code | Type | Meaning | SDK Behavior | Action Required |
| Status | Success - action completed | Continue normal flow | Display success message |
| Status | Password expired during action | SDK triggers logout | Show alert BEFORE logout |
| Status | Attempts exhausted | SDK triggers logout | Show alert BEFORE logout |
| Error | LDA cancelled and Password NOT enrolled | No fallback available | Show alert, allow retry |
Add these TypeScript definitions to understand the updateNotification API structure:
// src/uniken/services/rdnaService.ts (notification APIs)
/**
* Updates notification with user's action selection
* @param notificationUUID The unique identifier of the notification
* @param action The action selected by the user
* @returns Promise<RDNASyncResponse> that resolves with sync response structure
*
* Note: If action requires authentication, SDK will trigger:
* - getPassword event with challengeMode 3 (if password required)
* - Biometric prompt internally (if LDA required)
*/
async updateNotification(
notificationUUID: string,
action: string
): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
RdnaClient.updateNotification(notificationUUID, action, response => {
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
resolve(result);
} else {
reject(result);
}
});
});
}
The REL-ID SDK includes comprehensive Identity Document Verification (IDV) capabilities with selfie capture functionality. This section covers the IDV selfie process start confirmation flow.
IDV (Identity Document Verification) selfie process is a biometric verification step that captures the user's selfie and compares it with their identity documents for verification purposes.
IDV Flow Trigger β getIDVSelfieProcessStartConfirmation Event β
User Confirms Selfie Capture β setIDVSelfieProcessStartConfirmation API β
SDK Initiates Selfie Capture Process β IDV Verification Complete
The IDV selfie process follows this event-driven pattern:
SDK Triggers IDV Flow β getIDVSelfieProcessStartConfirmation Event β
IDVSelfieProcessStartConfirmationScreen Displays β User Reviews Guidelines β
User Selects Camera (Front/Back) β User Taps "Capture Selfie" β
setIDVSelfieProcessStartConfirmation(true, useBackCamera, idvWorkflow) API β
SDK Initiates Camera/Capture Process
The IDV selfie process supports various workflow types for different verification scenarios:
IDV Workflow | Type | Description | Usage Context |
0 | IDV activation process | Initial identity verification during enrollment | New user activation |
1 | IDV activation with template verification | Activation with existing biometric template matching | User with existing template |
2 | Additional device activation | Adding new device to existing identity | Device enrollment |
3 | Additional device without template | Device enrollment without existing template | New device, no template |
4 | Account recovery with template | Recovering access with existing biometric data | Account recovery |
5 | Account recovery without template | Recovery requiring new biometric enrollment | Account recovery, new template |
6 | Post-login KYC process | Know Your Customer verification after login | Compliance verification |
8 | Post-login selfie biometric | Biometric authentication after login | Enhanced security |
9 | Step-up authentication | Additional verification for sensitive operations | Transaction verification |
10 | Biometric opt-in process | User enrolling in biometric authentication | Enrollment process |
13 | Agent KYC process | Agent-assisted customer verification | Customer service |
15 | Login selfie biometric | Biometric verification during login | Login verification |
The implementation includes these key components:
Let's create the modal password dialog component that will be displayed when step-up authentication is required.
The StepUpPasswordDialog needs to:
Create a new file for the password dialog modal:
// src/uniken/components/modals/StepUpPasswordDialog.tsx
/**
* Step-Up Password Dialog Component
*
* Modal dialog for step-up authentication during notification actions.
* Handles challengeMode = 3 (RDNA_OP_AUTHORIZE_NOTIFICATION) when the SDK
* requires password verification before allowing a notification action.
*
* Features:
* - Password input with visibility toggle
* - Attempts left counter with color-coding
* - Error message display
* - Loading state during authentication
* - Notification context display (title)
* - Auto-focus on password field
* - Auto-clear password on error
* - Keyboard management with ScrollView
*
* Usage:
* ```tsx
* <StepUpPasswordDialog
* visible={showStepUpAuth}
* notificationTitle="Payment Approval"
* notificationMessage="Approve payment of $500"
* userID="john.doe"
* attemptsLeft={3}
* errorMessage="Incorrect password"
* isSubmitting={false}
* onSubmitPassword={(password) => handlePasswordSubmit(password)}
* onCancel={() => setShowStepUpAuth(false)}
* />
* ```
*/
import React, { useState, useEffect, useRef } from 'react';
import {
Modal,
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
ScrollView,
BackHandler,
ActivityIndicator,
KeyboardAvoidingView,
Platform,
} from 'react-native';
interface StepUpPasswordDialogProps {
visible: boolean;
notificationTitle: string;
notificationMessage: string;
userID: string;
attemptsLeft: number;
errorMessage?: string;
isSubmitting: boolean;
onSubmitPassword: (password: string) => void;
onCancel: () => void;
}
const StepUpPasswordDialog: React.FC<StepUpPasswordDialogProps> = ({
visible,
notificationTitle,
notificationMessage, // Keep for future use
userID, // Keep for future use
attemptsLeft,
errorMessage,
isSubmitting,
onSubmitPassword,
onCancel,
}) => {
const [password, setPassword] = useState<string>('');
const [showPassword, setShowPassword] = useState<boolean>(false);
const passwordInputRef = useRef<TextInput>(null);
// Clear password when modal becomes visible
useEffect(() => {
if (visible) {
setPassword('');
setShowPassword(false);
// Auto-focus password input after a short delay
setTimeout(() => {
passwordInputRef.current?.focus();
}, 300);
}
}, [visible]);
// Clear password field when error message changes (wrong password)
// This ensures the field is cleared when SDK triggers getPassword again after failure
useEffect(() => {
if (errorMessage) {
setPassword('');
}
}, [errorMessage]);
// Disable hardware back button when modal is visible
useEffect(() => {
const handleBackPress = () => {
if (visible && !isSubmitting) {
onCancel();
return true; // Prevent default back action
}
return false;
};
const backHandler = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
return () => backHandler.remove();
}, [visible, isSubmitting, onCancel]);
const handleSubmit = () => {
if (!password.trim() || isSubmitting) {
return;
}
onSubmitPassword(password.trim());
};
const handlePasswordChange = (text: string) => {
setPassword(text);
};
// Color-code attempts based on remaining count
const getAttemptsColor = (): string => {
if (attemptsLeft === 1) return '#dc2626'; // Red
if (attemptsLeft === 2) return '#f59e0b'; // Orange
return '#10b981'; // Green
};
return (
<Modal
visible={visible}
transparent={true}
animationType="fade"
onRequestClose={() => {
if (!isSubmitting) {
onCancel();
}
}}
>
<KeyboardAvoidingView
style={styles.modalOverlay}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
>
<View style={styles.modalContainer}>
{/* Header */}
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>π Authentication Required</Text>
<Text style={styles.modalSubtitle}>
Please verify your password to authorize this action
</Text>
</View>
<ScrollView
style={styles.scrollContainer}
contentContainerStyle={styles.contentContainer}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* Notification Title */}
<View style={styles.notificationContainer}>
<Text style={styles.notificationTitle}>{notificationTitle}</Text>
</View>
{/* Attempts Left Counter */}
{attemptsLeft <= 3 && (
<View style={[
styles.attemptsContainer,
{ backgroundColor: `${getAttemptsColor()}20` }
]}>
<Text style={[
styles.attemptsText,
{ color: getAttemptsColor() }
]}>
{attemptsLeft} attempt{attemptsLeft !== 1 ? 's' : ''} remaining
</Text>
</View>
)}
{/* Error Display */}
{errorMessage && (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>{errorMessage}</Text>
</View>
)}
{/* Password Input */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>Password</Text>
<View style={styles.passwordInputWrapper}>
<TextInput
ref={passwordInputRef}
style={styles.passwordInput}
value={password}
onChangeText={handlePasswordChange}
placeholder="Enter your password"
placeholderTextColor="#9ca3af"
secureTextEntry={!showPassword}
autoCapitalize="none"
autoCorrect={false}
returnKeyType="done"
onSubmitEditing={handleSubmit}
editable={!isSubmitting}
/>
<TouchableOpacity
style={styles.visibilityButton}
onPress={() => setShowPassword(!showPassword)}
disabled={isSubmitting}
>
<Text style={styles.visibilityIcon}>
{showPassword ? 'ποΈ' : 'π'}
</Text>
</TouchableOpacity>
</View>
</View>
</ScrollView>
{/* Action Buttons */}
<View style={styles.buttonContainer}>
<TouchableOpacity
style={[
styles.submitButton,
(!password.trim() || isSubmitting) && styles.buttonDisabled
]}
onPress={handleSubmit}
disabled={!password.trim() || isSubmitting}
>
{isSubmitting ? (
<View style={styles.buttonLoadingContent}>
<ActivityIndicator size="small" color="#ffffff" />
<Text style={styles.submitButtonText}>Verifying...</Text>
</View>
) : (
<Text style={styles.submitButtonText}>Verify & Continue</Text>
)}
</TouchableOpacity>
<TouchableOpacity
style={[styles.cancelButton, isSubmitting && styles.buttonDisabled]}
onPress={onCancel}
disabled={isSubmitting}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</Modal>
);
};
const styles = StyleSheet.create({
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.75)',
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
modalContainer: {
backgroundColor: '#ffffff',
borderRadius: 16,
width: '100%',
maxWidth: 480,
maxHeight: '80%',
elevation: 10,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
},
modalHeader: {
backgroundColor: '#3b82f6',
padding: 20,
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
alignItems: 'center',
},
modalTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#ffffff',
textAlign: 'center',
marginBottom: 8,
},
modalSubtitle: {
fontSize: 14,
color: '#dbeafe',
textAlign: 'center',
lineHeight: 20,
},
scrollContainer: {
flexGrow: 0,
},
contentContainer: {
padding: 20,
},
notificationContainer: {
backgroundColor: '#f0f9ff',
borderRadius: 8,
padding: 12,
marginBottom: 16,
borderLeftWidth: 4,
borderLeftColor: '#3b82f6',
},
notificationTitle: {
fontSize: 15,
fontWeight: '600',
color: '#1e40af',
textAlign: 'center',
},
attemptsContainer: {
borderRadius: 8,
padding: 12,
marginBottom: 16,
alignItems: 'center',
},
attemptsText: {
fontSize: 14,
fontWeight: '600',
},
errorContainer: {
backgroundColor: '#fef2f2',
borderRadius: 8,
padding: 12,
marginBottom: 16,
borderLeftWidth: 4,
borderLeftColor: '#dc2626',
},
errorText: {
fontSize: 14,
color: '#7f1d1d',
lineHeight: 20,
textAlign: 'center',
},
inputContainer: {
marginBottom: 16,
},
inputLabel: {
fontSize: 14,
fontWeight: '600',
color: '#374151',
marginBottom: 8,
},
passwordInputWrapper: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderColor: '#d1d5db',
borderRadius: 8,
backgroundColor: '#ffffff',
},
passwordInput: {
flex: 1,
padding: 12,
fontSize: 16,
color: '#1f2937',
},
visibilityButton: {
padding: 12,
},
visibilityIcon: {
fontSize: 20,
},
buttonContainer: {
padding: 20,
borderTopWidth: 1,
borderTopColor: '#f3f4f6',
},
submitButton: {
backgroundColor: '#3b82f6',
padding: 16,
borderRadius: 8,
alignItems: 'center',
marginBottom: 12,
elevation: 2,
},
submitButtonText: {
color: '#ffffff',
fontSize: 16,
fontWeight: '600',
},
cancelButton: {
backgroundColor: '#f3f4f6',
padding: 16,
borderRadius: 8,
alignItems: 'center',
},
cancelButtonText: {
color: '#6b7280',
fontSize: 16,
fontWeight: '600',
},
buttonDisabled: {
opacity: 0.6,
},
buttonLoadingContent: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
});
export default StepUpPasswordDialog;
Update the modals index file to export the new component:
// src/uniken/components/modals/index.ts
export {default as StepUpPasswordDialog} from './StepUpPasswordDialog';
The following image showcase screen from the sample application:

Now let's implement the screen-level event handler that will intercept getPassword events with challengeMode = 3 and display our step-up password dialog.
The callback preservation pattern ensures our screen-level handler doesn't break existing global handlers:
// Preserve original handler
const originalHandler = eventManager.onGetPasswordCallback;
// Set new handler that chains with original
eventManager.onGetPasswordCallback = (data: RDNAGetPasswordData) => {
if (data.challengeMode === 3) {
// Handle challengeMode 3 in screen
handleScreenSpecificLogic(data);
} else {
// Pass other modes to original handler
if (originalHandler) originalHandler(data);
}
};
// Cleanup: restore original handler when screen unmounts
return () => {
eventManager.onGetPasswordCallback = originalHandler;
};
Add step-up authentication state management to your GetNotificationsScreen:
// src/tutorial/screens/notification/GetNotificationsScreen.tsx (additions)
import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
FlatList,
TouchableOpacity,
Alert,
ActivityIndicator,
StyleSheet,
Modal,
ScrollView,
BackHandler,
RefreshControl,
} from 'react-native';
import { useRoute, useNavigation } from '@react-navigation/native';
import type { RouteProp } from '@react-navigation/native';
import rdnaService from '../../../uniken/services/rdnaService';
import { StepUpPasswordDialog } from '../../../uniken/components/modals';
import type {
RDNAGetNotificationsData,
RDNANotificationItem,
RDNANotificationAction,
RDNAUpdateNotificationData,
RDNAGetPasswordData,
RDNASyncResponse,
} from '../../../uniken/types/rdnaEvents';
import { RDNASyncUtils } from '../../../uniken/types/rdnaEvents';
const GetNotificationsScreen: React.FC = () => {
const route = useRoute<RouteProp<any>>();
const navigation = useNavigation<any>();
// Existing notification state
const [notifications, setNotifications] = useState<RDNANotificationItem[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [refreshing, setRefreshing] = useState<boolean>(false);
const [selectedNotification, setSelectedNotification] = useState<RDNANotificationItem | null>(null);
const [showActionModal, setShowActionModal] = useState<boolean>(false);
const [selectedAction, setSelectedAction] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [actionLoading, setActionLoading] = useState<boolean>(false);
// Step-up authentication state
const [showStepUpAuth, setShowStepUpAuth] = useState<boolean>(false);
const [stepUpNotificationUUID, setStepUpNotificationUUID] = useState<string | null>(null);
const [stepUpNotificationTitle, setStepUpNotificationTitle] = useState<string>('');
const [stepUpNotificationMessage, setStepUpNotificationMessage] = useState<string>('');
const [stepUpAction, setStepUpAction] = useState<string | null>(null);
const [stepUpAttemptsLeft, setStepUpAttemptsLeft] = useState<number>(3);
const [stepUpErrorMessage, setStepUpErrorMessage] = useState<string>('');
const [stepUpSubmitting, setStepUpSubmitting] = useState<boolean>(false);
// ... existing code ...
};
Add the handler that intercepts getPassword events with challengeMode = 3:
// src/tutorial/screens/notification/GetNotificationsScreen.tsx (addition)
/**
* Handle getPassword event for step-up authentication (challengeMode = 3)
* This handler intercepts only challengeMode 3 and passes other modes to the global handler
*/
const handleGetPasswordStepUp = useCallback((
data: RDNAGetPasswordData,
originalHandler?: (data: RDNAGetPasswordData) => void
) => {
console.log('GetNotificationsScreen - getPassword event:', {
challengeMode: data.challengeMode,
attemptsLeft: data.attemptsLeft,
statusCode: data.challengeResponse.status.statusCode
});
// Only handle challengeMode 3 (RDNA_OP_AUTHORIZE_NOTIFICATION)
if (data.challengeMode !== 3) {
console.log('GetNotificationsScreen - Not challengeMode 3, passing to original handler');
// Pass to original handler for other challenge modes (0, 1, 2, 4)
if (originalHandler) {
originalHandler(data);
}
return;
}
// Hide action modal to show step-up modal on top
setShowActionModal(false);
// Update step-up auth state
setStepUpAttemptsLeft(data.attemptsLeft);
setStepUpSubmitting(false);
// Check for error status codes
const statusCode = data.challengeResponse.status.statusCode;
const statusMessage = data.challengeResponse.status.statusMessage;
if (statusCode !== 100) {
// Failed authentication - show error
setStepUpErrorMessage(statusMessage || 'Authentication failed. Please try again.');
} else {
// Clear any previous errors
setStepUpErrorMessage('');
}
// Show step-up modal
setShowStepUpAuth(true);
}, []);
Wire up the event handler with proper lifecycle management:
// src/tutorial/screens/notification/GetNotificationsScreen.tsx (addition)
useEffect(() => {
loadNotifications();
// Set up event handlers
const eventManager = rdnaService.getEventManager();
// Preserve original getPassword handler
const originalGetPasswordHandler = eventManager.onGetPasswordCallback;
// Set new handler that chains with original
eventManager.onGetPasswordCallback = (data: RDNAGetPasswordData) => {
handleGetPasswordStepUp(data, originalGetPasswordHandler);
};
// Set notification event handlers
eventManager.setGetNotificationsHandler(handleNotificationsReceived);
eventManager.setUpdateNotificationHandler(handleUpdateNotificationReceived);
// Cleanup: restore original handlers when screen unmounts
return () => {
eventManager.onGetPasswordCallback = originalGetPasswordHandler;
eventManager.setGetNotificationsHandler(undefined);
eventManager.setUpdateNotificationHandler(undefined);
};
}, [handleGetPasswordStepUp, handleNotificationsReceived, handleUpdateNotificationReceived]);
Now let's implement the password submission handler that will be called when the user submits their password from the StepUpPasswordDialog.
Add the handler that submits the password to the SDK:
// src/tutorial/screens/notification/GetNotificationsScreen.tsx (addition)
/**
* Handle password submission from StepUpPasswordDialog
* Calls setPassword API with challengeMode 3
*/
const handleStepUpPasswordSubmit = useCallback(async (password: string) => {
console.log('GetNotificationsScreen - Submitting step-up password');
setStepUpSubmitting(true);
setStepUpErrorMessage('');
try {
// Call setPassword with challengeMode 3 for step-up auth
await rdnaService.setPassword(password, 3);
console.log('GetNotificationsScreen - setPassword API call successful');
// If successful, SDK will trigger onUpdateNotification event
// Keep modal open until we receive the response
} catch (error) {
console.error('GetNotificationsScreen - setPassword API error:', error);
setStepUpSubmitting(false);
// Extract error message
const errorMessage = RDNASyncUtils.getErrorMessage(error);
setStepUpErrorMessage(errorMessage);
}
}, []);
/**
* Handle step-up authentication cancellation
* Closes the dialog and resets state
*/
const handleStepUpCancel = useCallback(() => {
console.log('GetNotificationsScreen - Step-up authentication cancelled');
setShowStepUpAuth(false);
setStepUpNotificationUUID(null);
setStepUpAction(null);
setStepUpErrorMessage('');
setStepUpSubmitting(false);
}, []);
When user selects an action, store the notification context for the step-up dialog:
// src/tutorial/screens/notification/GetNotificationsScreen.tsx (modification)
/**
* Handle notification action selection
* Calls updateNotification API, which may trigger step-up auth
*/
const handleActionPress = async (action: RDNANotificationAction) => {
if (!selectedNotification || actionLoading) {
return;
}
const notification = selectedNotification;
console.log('GetNotificationsScreen - Action selected:', action.action);
setSelectedAction(action.action);
setActionLoading(true);
// Store notification context for potential step-up auth
setStepUpNotificationUUID(notification.notification_uuid);
setStepUpNotificationTitle(notification.body[0]?.subject || 'Notification Action');
setStepUpNotificationMessage(notification.body[0]?.message || '');
setStepUpAction(action.action);
setStepUpAttemptsLeft(3); // Reset attempts
setStepUpErrorMessage(''); // Clear errors
try {
console.log('GetNotificationsScreen - Calling updateNotification API');
await rdnaService.updateNotification(notification.notification_uuid, action.action);
console.log('GetNotificationsScreen - UpdateNotification API call successful');
// Response will be handled by handleUpdateNotificationReceived
// If step-up auth is required, SDK will trigger getPassword with challengeMode 3
} catch (error) {
console.error('GetNotificationsScreen - UpdateNotification API error:', error);
setActionLoading(false);
setStepUpNotificationUUID(null);
setStepUpAction(null);
// Extract error message from the error object
const errorMessage = RDNASyncUtils.getErrorMessage(error);
Alert.alert(
'Update Failed',
errorMessage,
[{ text: 'OK' }]
);
}
};
Now let's implement comprehensive error handling for the onUpdateNotification event, including critical errors that require alerts before logout.
Add the handler that processes onUpdateNotification events with proper error handling:
// src/tutorial/screens/notification/GetNotificationsScreen.tsx (addition)
/**
* Handle onUpdateNotification event
* Processes success, critical errors, and LDA cancellation
*/
const handleUpdateNotificationReceived = useCallback((data: RDNAUpdateNotificationData) => {
console.log('GetNotificationsScreen - onUpdateNotification event:', {
errorCode: data.error.longErrorCode,
responseData: data.responseData
});
setActionLoading(false);
setStepUpSubmitting(false);
// Check for LDA cancelled (error code 131)
// This only occurs when LDA is cancelled AND Password is NOT enrolled
// If Password IS enrolled, SDK directly triggers getPassword (no error)
if (data.error.longErrorCode === 131) {
console.log('GetNotificationsScreen - LDA cancelled, Password not enrolled');
setShowStepUpAuth(false);
Alert.alert(
'Authentication Cancelled',
'Local device authentication was cancelled. Please try again.',
[{
text: 'OK',
onPress: () => {
// Keep action modal open to allow user to retry LDA
// Action modal is still visible for retry
}
}]
);
return;
}
// Extract response data
const responseData = data.responseData;
const statusCode = responseData?.StatusCode;
const statusMessage = responseData?.StatusMessage || 'Action completed successfully';
console.log('GetNotificationsScreen - Response status:', {
statusCode,
statusMessage
});
if (statusCode === 100) {
// Success - action completed
console.log('GetNotificationsScreen - Notification action successful');
setShowStepUpAuth(false);
setShowActionModal(false);
Alert.alert(
'Success',
statusMessage,
[{
text: 'OK',
onPress: () => {
// Navigate to dashboard
navigation.navigate('DrawerNavigator', { screen: 'Dashboard' });
}
}]
);
// Reload notifications to reflect the change
loadNotifications();
} else if (statusCode === 110 || statusCode === 153) {
// Critical errors - show alert BEFORE SDK logout
// statusCode 110 = Password expired during action
// statusCode 153 = Attempts exhausted
console.log('GetNotificationsScreen - Critical error, SDK will trigger logout');
setShowStepUpAuth(false);
setShowActionModal(false);
Alert.alert(
'Authentication Failed',
statusMessage,
[{
text: 'OK',
onPress: () => {
console.log('GetNotificationsScreen - Waiting for SDK to trigger logout flow');
// SDK will automatically trigger onUserLoggedOff event
// SDKEventProvider will handle navigation to login
}
}]
);
} else {
// Other errors
console.log('GetNotificationsScreen - Update notification failed with status:', statusCode);
setShowStepUpAuth(false);
setShowActionModal(false);
Alert.alert(
'Update Failed',
statusMessage,
[{ text: 'OK' }]
);
}
}, [navigation, loadNotifications]);
The error handling flow for different scenarios:
LDA Cancelled (Password IS enrolled):
User cancels biometric β SDK directly triggers getPassword (challengeMode 3) β
No error, seamless fallback β StepUpPasswordDialog shows
LDA Cancelled (Password NOT enrolled):
User cancels biometric β onUpdateNotification (error code 131) β
Show alert "Authentication Cancelled" β User can retry LDA
Password Expired (statusCode 110):
Password authentication fails β onUpdateNotification (statusCode 110) β
Show alert "Authentication Failed - Password Expired" β
SDK triggers onUserLoggedOff β SDKEventProvider navigates to login
Attempts Exhausted (statusCode 153):
Too many failed attempts β onUpdateNotification (statusCode 153) β
Show alert "Authentication Failed - Attempts Exhausted" β
SDK triggers onUserLoggedOff β SDKEventProvider navigates to login
Success (statusCode 100):
Authentication successful β onUpdateNotification (statusCode 100) β
Show alert "Success" β Navigate to dashboard
Now let's add the dialog to the screen's render method so it displays when step-up authentication is required.
Add the StepUpPasswordDialog component to your GetNotificationsScreen render:
// src/tutorial/screens/notification/GetNotificationsScreen.tsx (addition to return statement)
return (
<View style={styles.container}>
{/* Existing notification list UI */}
<FlatList
data={notifications}
renderItem={renderNotificationItem}
keyExtractor={(item) => item.notification_uuid}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
ListEmptyComponent={
loading ? (
<View style={styles.centerContainer}>
<ActivityIndicator size="large" color="#3b82f6" />
<Text style={styles.loadingText}>Loading notifications...</Text>
</View>
) : (
<View style={styles.centerContainer}>
<Text style={styles.emptyText}>No notifications available</Text>
</View>
)
}
/>
{/* Existing Action Selection Modal */}
<Modal
visible={showActionModal}
transparent={true}
animationType="slide"
onRequestClose={() => !actionLoading && setShowActionModal(false)}
>
{/* ... existing action modal content ... */}
</Modal>
{/* Step-Up Password Dialog */}
<StepUpPasswordDialog
visible={showStepUpAuth}
notificationTitle={stepUpNotificationTitle}
notificationMessage={stepUpNotificationMessage}
userID={route.params?.userID || ''}
attemptsLeft={stepUpAttemptsLeft}
errorMessage={stepUpErrorMessage}
isSubmitting={stepUpSubmitting}
onSubmitPassword={handleStepUpPasswordSubmit}
onCancel={handleStepUpCancel}
/>
</View>
);
Ensure all callback handlers are wrapped in useCallback to prevent closure issues:
// src/tutorial/screens/notification/GetNotificationsScreen.tsx (verification)
// Wrap all event handlers in useCallback
const handleGetPasswordStepUp = useCallback((data, originalHandler) => {
// ... implementation ...
}, []); // Empty deps - no external dependencies
const handleUpdateNotificationReceived = useCallback((data) => {
// ... implementation ...
}, [navigation, loadNotifications]); // Depends on navigation and loadNotifications
const handleNotificationsReceived = useCallback((data) => {
// ... implementation ...
}, []); // Empty deps
const handleStepUpPasswordSubmit = useCallback(async (password) => {
// ... implementation ...
}, []); // Empty deps
const handleStepUpCancel = useCallback(() => {
// ... implementation ...
}, []); // Empty deps
Now let's test the complete step-up authentication implementation with various scenarios.
Before testing, ensure your REL-ID server is configured for step-up authentication:
Test the basic password step-up flow:
getNotifications() succeededgetPassword event triggered again with erroronUpdateNotification event with statusCode 100Test biometric authentication step-up:
getPassword event triggeredonUpdateNotification event with statusCode 100Test automatic fallback when user cancels biometric (requires both Password & LDA enrolled):
getPassword with challengeMode 3onUpdateNotification event with statusCode 100Test error handling when password expires during action:
onUpdateNotification receives statusCode 110onUserLoggedOff eventTest error handling when authentication attempts are exhausted:
onUpdateNotification receives statusCode 153onUserLoggedOff eventTest that keyboard doesn't hide action buttons:
Use this checklist to verify your implementation:
Now let's implement the complete IDV selfie process functionality, including the callback handler, API method, and screen integration.
First, add the IDV selfie API method to the RDNA service:
// src/uniken/services/rdnaService.ts
/**
* Sets IDV selfie process start confirmation for IDV authentication flows
*
* This method confirms or cancels the IDV selfie capture process during various IDV workflows.
* It processes the user's decision and camera preference for selfie capture.
* Uses sync response pattern similar to other API methods.
*
* IDV Workflow Types:
* - 0: IDV activation process
* - 1: IDV activation with template verification
* - 2: Additional device activation
* - 3: Additional device without template
* - 4: Account recovery with template
* - 5: Account recovery without template
* - 6: Post-login KYC process
* - 8: Post-login selfie biometric
* - 9: Step-up authentication
* - 10: Biometric opt-in process
* - 13: Agent KYC process
* - 15: Login selfie biometric
*
* Response Validation Logic (following reference app pattern):
* 1. Check error.longErrorCode: 0 = success, > 0 = error
* 2. Async events will be handled by event listeners for subsequent steps
* 3. Success typically navigates to selfie capture flow or next step
*
* @param confirmation User's confirmation decision (true = proceed, false = cancel)
* @param useBackCamera Camera preference (true = back camera, false = front camera)
* @param idvWorkflow IDV workflow type identifier
* @returns Promise<RDNASyncResponse> that resolves with sync response structure
*/
async setIDVSelfieProcessStartConfirmation(
confirmation: boolean,
useBackCamera: boolean,
idvWorkflow: number
): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
console.log('RdnaService - Setting IDV selfie process start confirmation:', {
confirmation,
useBackCamera,
idvWorkflow
});
RdnaClient.setIDVSelfieProcessStartConfirmation(confirmation, useBackCamera, idvWorkflow, response => {
console.log('RdnaService - SetIDVSelfieProcessStartConfirmation sync callback received');
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaService - SetIDVSelfieProcessStartConfirmation sync response success, waiting for async events');
resolve(result);
} else {
console.error('RdnaService - SetIDVSelfieProcessStartConfirmation sync response error:', result);
reject(result);
}
});
});
}
Add the TypeScript types for IDV selfie functionality:
// src/uniken/types/rdnaEvents.ts
// --- IDV Events ---
/**
* RDNA Get IDV Selfie Process Start Confirmation Data
* Event triggered for IDV selfie capture confirmation
*/
export interface RDNAGetIDVSelfieProcessStartConfirmationData extends RDNAEvent {
idvWorkflow: number;
useDeviceBackCamera: boolean;
}
// IDV Callbacks
export type RDNAGetIDVSelfieProcessStartConfirmationCallback = (
data: RDNAGetIDVSelfieProcessStartConfirmationData
) => void;
Add the event handler to the RDNA event manager:
// src/uniken/services/rdnaEventManager.ts
import type {
// ... existing imports
RDNAGetIDVSelfieProcessStartConfirmationData,
RDNAGetIDVSelfieProcessStartConfirmationCallback
} from '../types/rdnaEvents';
class RdnaEventManager {
// ... existing properties
// IDV handlers
private getIDVSelfieProcessStartConfirmationHandler?: RDNAGetIDVSelfieProcessStartConfirmationCallback;
constructor() {
this.rdnaEmitter = new NativeEventEmitter(NativeModules.RdnaClient);
this.registerEventListeners();
}
/**
* Registers native event listeners for all SDK events
*/
private registerEventListeners() {
console.log('RdnaEventManager - Registering native event listeners');
this.listeners.push(
// ... existing listeners
// IDV event listeners
this.rdnaEmitter.addListener('getIDVSelfieProcessStartConfirmation', this.onGetIDVSelfieProcessStartConfirmation.bind(this))
);
console.log('RdnaEventManager - Native event listeners registered');
}
/**
* Handles IDV selfie process start confirmation events
* @param response Raw response from native SDK
*/
private onGetIDVSelfieProcessStartConfirmation(response: RDNAJsonResponse) {
console.log("RdnaEventManager - Get IDV selfie process start confirmation event received");
try {
const idvSelfieData: RDNAGetIDVSelfieProcessStartConfirmationData = JSON.parse(response.response);
console.log("RdnaEventManager - IDV selfie process start confirmation data:", {
idvWorkflow: idvSelfieData.idvWorkflow,
useDeviceBackCamera: idvSelfieData.useDeviceBackCamera,
statusCode: idvSelfieData.challengeResponse.status.statusCode
});
if (this.getIDVSelfieProcessStartConfirmationHandler) {
this.getIDVSelfieProcessStartConfirmationHandler(idvSelfieData);
}
} catch (error) {
console.error("RdnaEventManager - Failed to parse IDV selfie process start confirmation:", error);
}
}
// IDV Handler Setters
public setGetIDVSelfieProcessStartConfirmationHandler(
callback?: RDNAGetIDVSelfieProcessStartConfirmationCallback
): void {
this.getIDVSelfieProcessStartConfirmationHandler = callback;
}
/**
* Cleans up all event listeners and handlers
*/
public cleanup() {
console.log('RdnaEventManager - Cleaning up event listeners and handlers');
// ... existing cleanup code
// Clear IDV handlers
this.getIDVSelfieProcessStartConfirmationHandler = undefined;
console.log('RdnaEventManager - Cleanup completed');
}
}
Update the navigation types and routes:
// src/tutorial/navigation/AppNavigator.tsx
// Import RDNA types
import type {
// ... existing imports
RDNAGetIDVSelfieProcessStartConfirmationData
} from '../../uniken/types/rdnaEvents';
import IDVSelfieProcessStartConfirmationScreen from '../screens/idv/IDVSelfieProcessStartConfirmationScreen';
// IDV Selfie Process Start Confirmation Screen Parameters
interface IDVSelfieProcessStartConfirmationScreenParams {
eventName: string;
eventData: RDNAGetIDVSelfieProcessStartConfirmationData;
title: string;
subtitle: string;
responseData?: RDNAGetIDVSelfieProcessStartConfirmationData; // Direct response data
}
export type RootStackParamList = {
// ... existing routes
IDVSelfieProcessStartConfirmationScreen: IDVSelfieProcessStartConfirmationScreenParams;
};
const AppNavigator = () => {
return (
<NavigationContainer ref={navigationRef}>
<Stack.Navigator screenOptions={{headerShown: false}} initialRouteName="TutorialHome">
{/* ... existing screens */}
<Stack.Screen
name="IDVSelfieProcessStartConfirmationScreen"
component={IDVSelfieProcessStartConfirmationScreen}
options={{
title: 'IDV Selfie Process Start Confirmation',
headerShown: false,
}}
/>
{/* ... remaining screens */}
</Stack.Navigator>
</NavigationContainer>
);
};
Integrate the IDV selfie callback with automatic navigation:
// src/uniken/providers/SDKEventProvider.tsx
import type {
// ... existing imports
RDNAGetIDVSelfieProcessStartConfirmationData
} from '../types/rdnaEvents';
export const SDKEventProvider: React.FC<SDKEventProviderProps> = ({ children }) => {
// ... existing state and handlers
/**
* Event handler for IDV selfie process start confirmation
*/
const handleGetIDVSelfieProcessStartConfirmation = useCallback((
data: RDNAGetIDVSelfieProcessStartConfirmationData
) => {
console.log('SDKEventProvider - Get IDV selfie process start confirmation event received');
console.log('SDKEventProvider - IDV Workflow:', data.idvWorkflow);
console.log('SDKEventProvider - Use back camera:', data.useDeviceBackCamera);
console.log('SDKEventProvider - Challenge status:', data.challengeResponse.status.statusCode);
// Use navigateOrUpdate to prevent duplicate screens and update existing screen with new event data
NavigationService.navigateOrUpdate('IDVSelfieProcessStartConfirmationScreen', {
eventName: 'getIDVSelfieProcessStartConfirmation',
eventData: data,
title: 'IDV Selfie Capture',
subtitle: 'Prepare to capture your selfie for identity verification',
// Pass response data directly
responseData: data,
});
}, []);
/**
* Set up SDK Event Subscriptions on mount
*/
useEffect(() => {
const eventManager = rdnaService.getEventManager();
// ... existing handler registrations
eventManager.setGetIDVSelfieProcessStartConfirmationHandler(handleGetIDVSelfieProcessStartConfirmation);
// Only cleanup on component unmount
return () => {
console.log('SDKEventProvider - Component unmounting, cleaning up event handlers');
eventManager.cleanup();
};
}, []); // Empty dependency array - setup once on mount
// ... rest of component
};
To test the IDV selfie functionality:
Prerequisites:
Testing Scenarios:
SDK Triggers IDV β getIDVSelfieProcessStartConfirmation event β
Screen displays with guidelines β User selects camera β
User taps "Capture Selfie" β setIDVSelfieProcessStartConfirmation(true, false, 9) β
SDK initiates selfie capture flow
User on IDV selfie screen β User taps cancel/close button β
setIDVSelfieProcessStartConfirmation(false, false, idvWorkflow) β
SDK cancels IDV flow
User toggles camera switch β useBackCamera state updates β
API call includes camera preference β setIDVSelfieProcessStartConfirmation(true, true, idvWorkflow)
Callback Implementation:
getIDVSelfieProcessStartConfirmation event registered in event manageridvWorkflow and useDeviceBackCameraAPI Implementation:
setIDVSelfieProcessStartConfirmation method implemented in RdnaServiceScreen Integration:
idvWorkflowuseBackCamera stateconfirmation: falseType Safety:
RDNAGetIDVSelfieProcessStartConfirmationData interface definedPro Tip: The IDV selfie screen uses the same event-driven pattern as other SDK screens. The key is properly handling the idvWorkflow parameter to show appropriate guidelines and passing the correct camera preference to the SDK.
Let's understand why we chose screen-level handling for challengeMode = 3 instead of global handling.
The implementation handles getPassword with challengeMode = 3 at the screen level (GetNotificationsScreen) rather than globally. This is a deliberate architectural choice with significant benefits.
Advantages:
// GetNotificationsScreen.tsx - Screen-level approach
const handleGetPasswordStepUp = useCallback((data, originalHandler) => {
// Only handle challengeMode 3
if (data.challengeMode !== 3) {
if (originalHandler) originalHandler(data);
return;
}
// Screen has direct access to notification context
setShowStepUpAuth(true);
// Notification title, message, action already in state
}, []);
Disadvantages if we used global approach:
// SDKEventProvider.tsx - Global approach (NOT USED)
const handleGetPassword = useCallback((data) => {
if (data.challengeMode === 3) {
// Problems:
// - Notification context not available here
// - Need complex state management to pass data
// - Navigation to new screen breaks UX
NavigationService.navigate('StepUpAuthScreen', { ??? });
}
}, []);
Aspect | Screen-Level Handler (β Current) | Global Handler (β Alternative) |
Context Access | Direct access to notification data | Need state management layer |
UI Pattern | Modal overlay on same screen | Navigate to new screen |
Modal Management | Simple (close one, open another) | Complex (cross-screen modals) |
Code Locality | All related code in one place | Scattered across multiple files |
Maintenance | Easy to understand and modify | Hard to trace flow |
Cleanup | Automatic on unmount | Manual cleanup needed |
Reusability | Pattern reusable for other screens | Tightly coupled to specific flow |
State Management | Local useState, no props | Need global state (Redux/Context) |
Screen-level handlers are recommended when:
Global handlers are appropriate when:
Let's address common issues you might encounter when implementing step-up authentication.
Symptoms:
getPassword event logged but modal doesn't displayPossible Causes & Solutions:
useCallback causing closure issues// β Wrong - No useCallback
const handleGetPasswordStepUp = (data, originalHandler) => {
// ... implementation ...
};
// β
Correct - With useCallback
const handleGetPasswordStepUp = useCallback((data, originalHandler) => {
// ... implementation ...
}, []); // Empty dependencies
// β Wrong - Missing handleGetPasswordStepUp in deps
useEffect(() => {
const eventManager = rdnaService.getEventManager();
const originalHandler = eventManager.onGetPasswordCallback;
eventManager.onGetPasswordCallback = (data) => {
handleGetPasswordStepUp(data, originalHandler);
};
return () => {
eventManager.onGetPasswordCallback = originalHandler;
};
}, []); // Missing handleGetPasswordStepUp
// β
Correct - Include handler in deps
useEffect(() => {
const eventManager = rdnaService.getEventManager();
const originalHandler = eventManager.onGetPasswordCallback;
eventManager.onGetPasswordCallback = (data) => {
handleGetPasswordStepUp(data, originalHandler);
};
return () => {
eventManager.onGetPasswordCallback = originalHandler;
};
}, [handleGetPasswordStepUp]); // Include handler
// β Wrong - Action modal still visible
setShowStepUpAuth(true);
// β
Correct - Close action modal first
setShowActionModal(false);
setShowStepUpAuth(true);
Symptoms:
getPassword triggers againSolution: Add useEffect to clear password when error changes
// β
Correct - Auto-clear password on error
useEffect(() => {
if (errorMessage) {
setPassword('');
}
}, [errorMessage]);
Symptoms:
Solution: Ensure proper ScrollView and maxHeight configuration
// β
Correct - ScrollView with proper config
<View style={styles.modalContainer}> {/* maxHeight: '80%' */}
<ScrollView
style={styles.scrollContainer} {/* flexGrow: 0 */}
contentContainerStyle={styles.contentContainer}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* Content */}
</ScrollView>
<View style={styles.buttonContainer}>
{/* Buttons outside ScrollView */}
</View>
</View>
Symptoms:
getPassword events for other modes not handledSolution: Ensure proper cleanup in useEffect return
// β
Correct - Restore original handler on cleanup
useEffect(() => {
const eventManager = rdnaService.getEventManager();
const originalHandler = eventManager.onGetPasswordCallback;
eventManager.onGetPasswordCallback = (data) => {
handleGetPasswordStepUp(data, originalHandler);
};
// Critical: restore original handler
return () => {
eventManager.onGetPasswordCallback = originalHandler;
};
}, [handleGetPasswordStepUp]);
Symptoms:
Solution: Ensure alert is shown in onUpdateNotification handler
// β
Correct - Show alert BEFORE SDK logout
if (statusCode === 110 || statusCode === 153) {
setShowStepUpAuth(false);
setShowActionModal(false);
// Show alert first
Alert.alert(
'Authentication Failed',
statusMessage,
[{
text: 'OK',
onPress: () => {
// SDK will trigger logout after this
console.log('Waiting for SDK logout');
}
}]
);
}
Symptoms:
Solution: Verify both Password and LDA are enrolled
// This fallback only works when BOTH Password and LDA are enrolled
// If only LDA is enrolled, cancellation should allow retry, not fallback
// In onUpdateNotification handler:
if (data.error.longErrorCode === 131) {
// LDA cancelled
Alert.alert(
'Authentication Cancelled',
'Local device authentication was cancelled. Please try again.',
[{ text: 'OK' }]
);
// SDK will automatically trigger getPassword if Password is enrolled
// Otherwise, user can retry LDA by tapping action again
}
Symptoms:
Solution: Implement BackHandler in StepUpPasswordDialog
// β
Already implemented in StepUpPasswordDialog
useEffect(() => {
const handleBackPress = () => {
if (visible && !isSubmitting) {
onCancel();
return true; // Prevent default back action
}
return false;
};
const backHandler = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
return () => backHandler.remove();
}, [visible, isSubmitting, onCancel]);
Enable detailed logging to troubleshoot issues:
// Add detailed console logs at each step
console.log('GetNotificationsScreen - getPassword event:', {
challengeMode: data.challengeMode,
attemptsLeft: data.attemptsLeft,
statusCode: data.challengeResponse.status.statusCode,
statusMessage: data.challengeResponse.status.statusMessage
});
console.log('GetNotificationsScreen - State before showing modal:', {
showStepUpAuth,
stepUpAttemptsLeft,
stepUpErrorMessage,
stepUpNotificationTitle
});
console.log('GetNotificationsScreen - onUpdateNotification:', {
errorCode: data.error.longErrorCode,
statusCode: responseData?.StatusCode,
statusMessage: responseData?.StatusMessage
});
Let's review important security considerations for step-up authentication implementation.
Never log or expose passwords:
// β Wrong - Logging password
console.log('Password submitted:', password);
// β
Correct - Only log that password was submitted
console.log('Password submitted for step-up auth');
Clear sensitive data on unmount:
// β
Correct - Clear password on unmount
useEffect(() => {
return () => {
setPassword('');
setStepUpErrorMessage('');
};
}, []);
Never bypass step-up authentication:
// β Wrong - Allowing action without auth
if (requiresAuth) {
// Don't try to bypass by calling action again
}
// β
Correct - Always respect SDK's auth requirement
try {
await rdnaService.updateNotification(uuid, action);
// Let SDK handle auth requirement via events
} catch (error) {
// Handle API errors only
}
Don't expose sensitive information in error messages:
// β Wrong - Exposing system details
Alert.alert('Error', `Database connection failed: ${sqlError.details}`);
// β
Correct - User-friendly generic message
Alert.alert('Error', 'Unable to process action. Please try again.');
Respect server-configured attempt limits:
// β
Correct - Use SDK-provided attempts
setStepUpAttemptsLeft(data.attemptsLeft);
// β Wrong - Ignoring SDK attempts and implementing custom limit
const maxAttempts = 5; // Don't do this
Handle critical errors properly:
// β
Correct - Show alert BEFORE logout
if (statusCode === 110 || statusCode === 153) {
Alert.alert(
'Authentication Failed',
statusMessage,
[{
text: 'OK',
onPress: () => {
// SDK will trigger logout automatically
}
}]
);
}
Implement proper LDA cancellation handling:
// β
Correct - Allow retry or fallback based on enrollment
if (data.error.longErrorCode === 131) {
// If both Password & LDA enrolled: SDK falls back to password
// If only LDA enrolled: Allow user to retry LDA
Alert.alert(
'Authentication Cancelled',
'Local device authentication was cancelled. Please try again.'
);
}
Prevent dismissal during sensitive operations:
// β
Correct - Disable dismissal during submission
<Modal
visible={visible}
onRequestClose={() => {
if (!isSubmitting) {
onCancel();
}
}}
>
// Disable cancel button during submission
<TouchableOpacity
onPress={onCancel}
disabled={isSubmitting}
>
Log security-relevant events:
// β
Correct - Log auth attempts and results
console.log('Step-up authentication initiated for notification:', notificationUUID);
console.log('Step-up authentication result:', {
success: statusCode === 100,
attemptsRemaining: attemptsLeft,
authMethod: challengeMode === 3 ? 'Password' : 'LDA'
});
Always test these security scenarios:
Let's optimize the step-up authentication implementation for better performance.
Wrap all callbacks to prevent unnecessary re-renders:
// β
Correct - Minimal dependencies
const handleGetPasswordStepUp = useCallback((data, originalHandler) => {
// Pure function, no external dependencies
}, []); // Empty deps
const handleStepUpPasswordSubmit = useCallback(async (password) => {
// Only uses rdnaService, no state dependencies
}, []); // Empty deps
// β
Correct - Include necessary dependencies
const handleUpdateNotificationReceived = useCallback((data) => {
// Uses navigation and loadNotifications
}, [navigation, loadNotifications]); // Include deps
Batch related state updates:
// β Wrong - Multiple state updates
setShowActionModal(false);
setShowStepUpAuth(true);
setStepUpErrorMessage('');
setStepUpAttemptsLeft(3);
// β
Better - Use functional updates to batch
React.unstable_batchedUpdates(() => {
setShowActionModal(false);
setShowStepUpAuth(true);
setStepUpErrorMessage('');
setStepUpAttemptsLeft(3);
});
// Note: In React 18+, batching is automatic
Only render modal when needed:
// β
Correct - Conditional rendering
{showStepUpAuth && (
<StepUpPasswordDialog
visible={showStepUpAuth}
// ... props
/>
)}
// Or use visible prop (modal handles internal optimization)
<StepUpPasswordDialog
visible={showStepUpAuth}
// ... props
/>
Optional: Debounce validation if implementing complex password rules:
// Optional optimization for complex validation
import { useMemo } from 'react';
import debounce from 'lodash/debounce';
const debouncedValidation = useMemo(
() => debounce((password) => {
// Complex validation logic
}, 300),
[]
);
const handlePasswordChange = (text: string) => {
setPassword(text);
debouncedValidation(text);
};
Cache attempts color calculation:
// β
Correct - Memoize color calculation
const attemptsColor = useMemo(() => {
if (attemptsLeft === 1) return '#dc2626';
if (attemptsLeft === 2) return '#f59e0b';
return '#10b981';
}, [attemptsLeft]);
Optional: Lazy load dialog content:
// Optional optimization for very complex dialogs
const StepUpPasswordDialog = React.lazy(() =>
import('../../../uniken/components/modals/StepUpPasswordDialog')
);
// Wrap with Suspense
<Suspense fallback={<ActivityIndicator />}>
{showStepUpAuth && (
<StepUpPasswordDialog
visible={showStepUpAuth}
// ... props
/>
)}
</Suspense>
Clean up timers and listeners:
// β
Already implemented - Cleanup pattern
useEffect(() => {
const timeout = setTimeout(() => {
passwordInputRef.current?.focus();
}, 300);
return () => clearTimeout(timeout);
}, [visible]);
useEffect(() => {
const backHandler = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
return () => backHandler.remove();
}, [visible, isSubmitting, onCancel]);
Monitor step-up auth performance:
// Optional: Add performance monitoring
const startTime = performance.now();
try {
await rdnaService.setPassword(password, 3);
const duration = performance.now() - startTime;
console.log(`Step-up auth completed in ${duration}ms`);
} catch (error) {
const duration = performance.now() - startTime;
console.log(`Step-up auth failed after ${duration}ms`);
}
Congratulations! You've successfully implemented step-up authentication for notification actions with REL-ID SDK.
In this codelab, you've learned how to:
β Understand Step-Up Authentication: Learned when and why re-authentication is required for sensitive operations
β Create StepUpPasswordDialog: Built a modal password dialog with attempts counter, error handling, and keyboard management
β
Implement Screen-Level Event Handler: Used callback preservation pattern to handle challengeMode = 3 at screen level
β Handle LDA and Password Flows: Supported both biometric authentication and password-based step-up with automatic fallback
β Manage Critical Errors: Properly handled status codes 110, 153 with alerts before logout and error code 131 with alert
β Optimize Keyboard Behavior: Implemented ScrollView and KeyboardAvoidingView to prevent buttons from being hidden
β Auto-Clear Password Fields: Automatically cleared password when authentication failed and SDK triggered retry
β Understand Architecture Decisions: Learned why screen-level handlers are better than global handlers for step-up auth
Authentication Method Selection:
Error Handling:
RDNASyncUtils.getErrorMessage() for user-friendly errorsArchitecture Pattern:
Security Best Practices:
Thank you for completing this codelab! You now have the knowledge to implement secure, production-ready step-up authentication for notification actions in your React Native applications.
Happy Coding! π