π― 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);
}
});
});
}
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:
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! π