π― Learning Path:
Welcome to the REL-ID Password Expiry codelab! This tutorial builds upon your existing MFA implementation to add secure expired password update capabilities using REL-ID SDK's updatePassword API.
In this codelab, you'll enhance your existing MFA application with:
challengeMode = 4 (RDNA_OP_UPDATE_ON_EXPIRY)RELID_PASSWORD_POLICY requirementsBy completing this codelab, you'll master:
updatePassword(current, new, 4) with proper handlingRELID_PASSWORD_POLICY from challenge dataBefore 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-MFA-password-expiry folder in the repository you cloned earlier
This codelab extends your MFA application with three core password expiry components:
Before implementing password expiry functionality, let's understand the key SDK events and APIs that power the expired password update workflow.
The password expiry process follows this event-driven pattern:
Login with Expired Password with challengeMode=0(RDNA_CHALLENGE_OP_VERIFY) β Server Detects Expiry (statusCode 118) β
SDK Triggers getPassword Event with challengeMode=4(RDNA_OP_UPDATE_ON_EXPIRY) β UpdateExpiryPasswordScreen Displays β
User Updates Password β updatePassword(current, new, 4) API β onUserLoggedIn Event β Dashboard
When a user's password expires, the login flow changes:
Step | Event | Description |
1. User Login | VerifyPasswordScreen with | User enters credentials for standard login |
2. Password Expired | Server returns | Server detects password has expired |
3. SDK Re-triggers |
| SDK automatically requests password update |
4. User Shows Screen | UpdateExpiryPasswordScreen displays | Show UpdateExpiryPasswordScreen with current, new, and confirm password fields |
5. User Update Password | updatePassword API | User must provide current and new password |
Challenge Mode 4 is specifically for expired password updates:
Challenge Mode | Purpose | User Action Required | Screen |
| Verify existing password | Enter password to login | VerifyPasswordScreen |
| Set new password | Create password during activation | SetPasswordScreen |
| Update expired password | Provide current + new password | UpdateExpiryPasswordScreen |
The REL-ID SDK triggers these main events during password expiry flow:
Event Type | Description | User Action Required |
Password expiry detected, update required | User provides current and new passwords | |
Automatic login after successful password update | System navigates to dashboard automatically |
Password expiry flow uses the same default policy key as password creation.:
Flow | Policy Key | Description |
Password Creation (challengeMode=1) |
| Policy for new password creation |
Password Expiry (challengeMode=4) |
| Policy for expired password update |
The server maintains password history and detects reuse:
Status Code | Meaning | Action |
| Password has expired | Initial trigger for password update |
| Password reuse detected | Clear fields and prompt for different password |
Add these TypeScript definitions to understand the updatePassword API structure:
// src/uniken/services/rdnaService.ts (password expiry addition)
/**
* Updates password when expired (Password Expiry Flow)
* @param currentPassword The user's current password
* @param newPassword The new password to set
* @param challengeMode Challenge mode (should be 4 for RDNA_OP_UPDATE_ON_EXPIRY)
* @returns Promise<RDNASyncResponse> that resolves with sync response structure
*/
async updatePassword(
currentPassword: string,
newPassword: string,
challengeMode: number = 4
): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
RdnaClient.updatePassword(currentPassword, newPassword, challengeMode, response => {
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
resolve(result);
} else {
reject(result);
}
});
});
}
Let's implement the updatePassword API in your service layer following established REL-ID SDK patterns.
Add the updatePassword method to your existing service implementation:
// src/uniken/services/rdnaService.ts (addition to existing class)
/**
* Updates password when expired (Password Expiry Flow)
*
* This method is specifically used for updating expired passwords during the MFA flow.
* When a password is expired during login (challengeMode=0), the SDK automatically
* re-triggers getPassword() with challengeMode=4 (RDNA_OP_UPDATE_ON_EXPIRY).
* The app should then call this method with both current and new passwords.
* Uses sync response pattern similar to setPassword() method.
*
* @see https://developer.uniken.com/docs/password-expiry
*
* Workflow:
* 1. User logs in with expired password (challengeMode=0)
* 2. Server detects expiry (statusCode=118)
* 3. SDK triggers getPassword with challengeMode=4
* 4. App displays UpdateExpiryPasswordScreen
* 5. User provides current and new passwords
* 6. App calls updatePassword(current, new, 4)
* 7. SDK validates and updates password
* 8. SDK logs user in automatically (onUserLoggedIn event)
*
* Response Validation Logic:
* 1. Check error.longErrorCode: 0 = success, > 0 = error
* 2. On success, triggers onUserLoggedIn event immediately
* 3. On failure, may trigger getPassword again with error status
* 4. StatusCode 164 = Password reuse error (used in last N passwords)
* 5. Async events will be handled by event listeners
*
* @param currentPassword The user's current password
* @param newPassword The new password to set
* @param challengeMode Challenge mode (should be 4 for RDNA_OP_UPDATE_ON_EXPIRY)
* @returns Promise<RDNASyncResponse> that resolves with sync response structure
*/
async updatePassword(
currentPassword: string,
newPassword: string,
challengeMode: number = 4
): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
console.log('RdnaService - Updating expired password with challengeMode:', challengeMode);
RdnaClient.updatePassword(currentPassword, newPassword, challengeMode, response => {
console.log('RdnaService - UpdatePassword sync callback received');
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaService - UpdatePassword sync response success, waiting for onUserLoggedIn event');
resolve(result);
} else {
console.error('RdnaService - UpdatePassword sync response error:', result);
reject(result);
}
});
});
}
Notice how this implementation follows the exact pattern established by other service methods:
Pattern Element | Implementation Detail |
Promise Wrapper | Wraps native sync SDK callback in Promise for async/await usage |
Error Checking | Validates |
Logging Strategy | Comprehensive console logging for debugging (without exposing passwords) |
Error Handling | Proper reject/resolve based on sync response |
Challenge Mode | Defaults to 4 (RDNA_OP_UPDATE_ON_EXPIRY) but accepts as parameter |
Now let's enhance your SDKEventProvider to detect and route challengeMode 4 to the UpdateExpiryPasswordScreen.
Update your existing handleGetPassword callback in SDKEventProvider:
// src/uniken/providers/SDKEventProvider.tsx (enhancement to existing handler)
/**
* Event handler for get password requests
*/
const handleGetPassword = useCallback((data: RDNAGetPasswordData) => {
console.log('SDKEventProvider - Get password event received, status:', data.challengeResponse.status.statusCode);
console.log('SDKEventProvider - UserID:', data.userID, 'ChallengeMode:', data.challengeMode, 'AttemptsLeft:', data.attemptsLeft);
// Navigate to appropriate screen based on challengeMode
if (data.challengeMode === 0) {
// challengeMode = 0: Verify existing password
NavigationService.navigateOrUpdate('VerifyPasswordScreen', {
eventData: data,
title: 'Verify Password',
subtitle: `Enter your password to continue`,
userID: data.userID,
challengeMode: data.challengeMode,
attemptsLeft: data.attemptsLeft,
responseData: data,
});
} else if (data.challengeMode === 4) {
// challengeMode = 4: Update expired password (RDNA_OP_UPDATE_ON_EXPIRY)
// Extract status message from response (e.g., "Password has expired. Please contact the admin.")
const statusMessage = data.challengeResponse?.status?.statusMessage ||
'Your password has expired. Please update it to continue.';
NavigationService.navigateOrUpdate('UpdateExpiryPasswordScreen', {
eventData: data,
title: 'Update Expired Password',
subtitle: statusMessage,
responseData: data,
});
} else {
// challengeMode = 1: Set new password
NavigationService.navigateOrUpdate('SetPasswordScreen', {
eventData: data,
title: 'Set Password',
subtitle: `Create a secure password for user: ${data.userID}`,
responseData: data,
});
}
}, []);
The enhanced routing logic handles three password scenarios:
Challenge Mode | Screen | Purpose |
| VerifyPasswordScreen | Verify existing password for login |
| SetPasswordScreen | Set new password during activation |
| UpdateExpiryPasswordScreen | Update expired password |
Extract the server's status message for better user experience:
// Extract dynamic status message from server response
const statusMessage = data.challengeResponse?.status?.statusMessage ||
'Your password has expired. Please update it to continue.';
Status Code | Typical Status Message |
| "Password has expired. Please contact the admin." |
| "Please enter a new password as your entered password has been used by you previously. You are not allowed to use last N passwords." |
Now let's create the UpdateExpiryPasswordScreen component with three password fields, comprehensive validation, and keyboard management.
Create a new file for the password expiry screen:
// src/tutorial/screens/mfa/UpdateExpiryPasswordScreen.tsx
import React, { useState, useEffect, useRef } from 'react';
import {
View,
Text,
TextInput,
StyleSheet,
StatusBar,
ScrollView,
Alert,
SafeAreaView,
KeyboardAvoidingView,
Platform,
} from 'react-native';
import { useRoute } from '@react-navigation/native';
import type { RouteProp } from '@react-navigation/native';
import { RDNAEventUtils, RDNASyncUtils } from '../../../uniken/types/rdnaEvents';
import { parseAndGeneratePolicyMessage } from '../../../uniken/utils';
import type { RDNAGetPasswordData, RDNASyncResponse } from '../../../uniken/types/rdnaEvents';
import rdnaService from '../../../uniken/services/rdnaService';
import { CloseButton, Button, Input, StatusBanner } from '../components';
import type { RootStackParamList } from '../../navigation/AppNavigator';
type UpdateExpiryPasswordScreenRouteProp = RouteProp<RootStackParamList, 'UpdateExpiryPasswordScreen'>;
/**
* Update Expiry Password Screen Component
*/
const UpdateExpiryPasswordScreen: React.FC = () => {
const route = useRoute<UpdateExpiryPasswordScreenRouteProp>();
const {
eventData,
title,
subtitle,
responseData,
} = route.params;
const [currentPassword, setCurrentPassword] = useState<string>('');
const [newPassword, setNewPassword] = useState<string>('');
const [confirmPassword, setConfirmPassword] = useState<string>('');
const [error, setError] = useState<string>('');
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [challengeMode, setChallengeMode] = useState<number>(4);
const [userName, setUserName] = useState<string>('');
const [passwordPolicyMessage, setPasswordPolicyMessage] = useState<string>('');
const currentPasswordRef = useRef<TextInput>(null);
const newPasswordRef = useRef<TextInput>(null);
const confirmPasswordRef = useRef<TextInput>(null);
/**
* Handle close button - direct resetAuthState call
*/
const handleClose = async () => {
try {
console.log('UpdateExpiryPasswordScreen - Calling resetAuthState');
await rdnaService.resetAuthState();
console.log('UpdateExpiryPasswordScreen - ResetAuthState successful');
} catch (error) {
console.error('UpdateExpiryPasswordScreen - ResetAuthState error:', error);
}
};
/**
* Handle response data from route params
*/
useEffect(() => {
if (responseData) {
console.log('UpdateExpiryPasswordScreen - Processing response data from route params:', responseData);
// Extract challenge data
setUserName(responseData.userID || '');
setChallengeMode(responseData.challengeMode || 4);
// Extract and process password policy from RELID_PASSWORD_POLICY
const policyJsonString = RDNAEventUtils.getChallengeValue(responseData, 'RELID_PASSWORD_POLICY');
if (policyJsonString) {
const policyMessage = parseAndGeneratePolicyMessage(policyJsonString);
setPasswordPolicyMessage(policyMessage);
console.log('UpdateExpiryPasswordScreen - Password policy extracted:', policyMessage);
}
console.log('UpdateExpiryPasswordScreen - Processed password data:', {
userID: responseData.userID,
challengeMode: responseData.challengeMode,
passwordPolicy: policyJsonString ? 'Found' : 'Not found',
});
// Check for API errors first
if (RDNAEventUtils.hasApiError(responseData)) {
const errorMessage = RDNAEventUtils.getErrorMessage(responseData);
console.log('UpdateExpiryPasswordScreen - API error:', errorMessage);
setError(errorMessage);
// Clear password fields on error
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
return;
}
// Check for status errors (including password reuse errors like statusCode 164)
if (RDNAEventUtils.hasStatusError(responseData)) {
const errorMessage = RDNAEventUtils.getErrorMessage(responseData);
console.log('UpdateExpiryPasswordScreen - Status error:', errorMessage);
setError(errorMessage);
// Clear password fields on error (e.g., password reuse)
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
return;
}
}
}, [responseData]);
// ... (Continue with remaining handler functions)
};
export default UpdateExpiryPasswordScreen;
Feature | Implementation Detail |
Three Password Fields | Current, new, and confirm password with refs for focus management |
Keyboard Management | KeyboardAvoidingView with platform-specific behavior (iOS: padding, Android: height) |
Policy Extraction | Extracts from |
Error Handling | Automatic field clearing on API and status errors |
Loading States | Proper isSubmitting state management |
Let's implement comprehensive password validation for the three-field form.
Implement handlers to clear errors when user types:
// src/tutorial/screens/mfa/UpdateExpiryPasswordScreen.tsx (additions)
/**
* Handle input changes
*/
const handleCurrentPasswordChange = (text: string) => {
setCurrentPassword(text);
if (error) {
setError('');
}
};
const handleNewPasswordChange = (text: string) => {
setNewPassword(text);
if (error) {
setError('');
}
};
const handleConfirmPasswordChange = (text: string) => {
setConfirmPassword(text);
if (error) {
setError('');
}
};
/**
* Reset form inputs
*/
const resetInputs = () => {
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
if (currentPasswordRef.current) {
currentPasswordRef.current.focus();
}
};
Add the main validation and update logic:
// src/tutorial/screens/mfa/UpdateExpiryPasswordScreen.tsx (additions)
/**
* Handle password update submission
*/
const handleUpdatePassword = async () => {
if (isSubmitting) return;
const trimmedCurrentPassword = currentPassword.trim();
const trimmedNewPassword = newPassword.trim();
const trimmedConfirmPassword = confirmPassword.trim();
// Basic validation
if (!trimmedCurrentPassword) {
setError('Please enter your current password');
if (currentPasswordRef.current) {
currentPasswordRef.current.focus();
}
return;
}
if (!trimmedNewPassword) {
setError('Please enter a new password');
if (newPasswordRef.current) {
newPasswordRef.current.focus();
}
return;
}
if (!trimmedConfirmPassword) {
setError('Please confirm your new password');
if (confirmPasswordRef.current) {
confirmPasswordRef.current.focus();
}
return;
}
// Check password match
if (trimmedNewPassword !== trimmedConfirmPassword) {
setError('New password and confirm password do not match');
Alert.alert(
'Password Mismatch',
'New password and confirm password do not match',
[{ text: 'OK', onPress: () => {
setNewPassword('');
setConfirmPassword('');
if (newPasswordRef.current) {
newPasswordRef.current.focus();
}
}}]
);
return;
}
// Check if new password is same as current password
if (trimmedCurrentPassword === trimmedNewPassword) {
setError('New password must be different from current password');
Alert.alert(
'Invalid New Password',
'Your new password must be different from your current password',
[{ text: 'OK', onPress: () => {
setNewPassword('');
setConfirmPassword('');
if (newPasswordRef.current) {
newPasswordRef.current.focus();
}
}}]
);
return;
}
setIsSubmitting(true);
setError('');
try {
console.log('UpdateExpiryPasswordScreen - Updating password with challengeMode:', challengeMode);
const syncResponse: RDNASyncResponse = await rdnaService.updatePassword(
trimmedCurrentPassword,
trimmedNewPassword,
challengeMode
);
console.log('UpdateExpiryPasswordScreen - UpdatePassword sync response successful, waiting for async events');
console.log('UpdateExpiryPasswordScreen - Sync response received:', {
longErrorCode: syncResponse.error?.longErrorCode,
shortErrorCode: syncResponse.error?.shortErrorCode,
errorString: syncResponse.error?.errorString
});
// Success - wait for onUserLoggedIn event
// Event handlers in SDKEventProvider will handle the navigation
} catch (error) {
// This catch block handles sync response errors (rejected promises)
console.error('UpdateExpiryPasswordScreen - UpdatePassword sync error:', error);
// Cast the error back to RDNASyncResponse
const result: RDNASyncResponse = error as RDNASyncResponse;
const errorMessage = RDNASyncUtils.getErrorMessage(result);
setError(errorMessage);
resetInputs();
} finally {
setIsSubmitting(false);
}
};
/**
* Check if form is valid
*/
const isFormValid = (): boolean => {
return (
currentPassword.trim().length > 0 &&
newPassword.trim().length > 0 &&
confirmPassword.trim().length > 0 &&
!error
);
};
Validation Rule | Error Message | Action |
Current password empty | "Please enter your current password" | Focus current password field |
New password empty | "Please enter a new password" | Focus new password field |
Confirm password empty | "Please confirm your new password" | Focus confirm password field |
Passwords don't match | "New password and confirm password do not match" | Clear new and confirm fields |
New = Current password | "New password must be different from current password" | Clear new and confirm fields |
Now let's build the complete UI with password policy requirements display and keyboard management.
Complete the component with the full UI including keyboard management:
// src/tutorial/screens/mfa/UpdateExpiryPasswordScreen.tsx (UI rendering)
return (
<SafeAreaView style={styles.safeArea}>
<StatusBar barStyle="dark-content" backgroundColor="#f8f9fa" />
<KeyboardAvoidingView
style={styles.keyboardAvoidingView}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
>
<ScrollView
style={styles.container}
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
>
{/* Close Button */}
<CloseButton
onPress={handleClose}
disabled={isSubmitting}
/>
<View style={styles.content}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.subtitle}>{subtitle}</Text>
{/* User Information */}
{userName && (
<View style={styles.userContainer}>
<Text style={styles.welcomeText}>Welcome</Text>
<Text style={styles.userNameText}>{userName}</Text>
</View>
)}
{/* Password Policy Display */}
{passwordPolicyMessage && (
<View style={styles.policyContainer}>
<Text style={styles.policyTitle}>Password Requirements</Text>
<Text style={styles.policyText}>{passwordPolicyMessage}</Text>
</View>
)}
{/* Error Display */}
{error && (
<StatusBanner
type="error"
message={error}
/>
)}
{/* Current Password Input */}
<Input
ref={currentPasswordRef}
label="Current Password"
value={currentPassword}
onChangeText={handleCurrentPasswordChange}
placeholder="Enter current password"
secureTextEntry={true}
returnKeyType="next"
onSubmitEditing={() => newPasswordRef.current?.focus()}
editable={!isSubmitting}
autoFocus={true}
containerStyle={styles.inputContainer}
/>
{/* New Password Input */}
<Input
ref={newPasswordRef}
label="New Password"
value={newPassword}
onChangeText={handleNewPasswordChange}
placeholder="Enter new password"
secureTextEntry={true}
returnKeyType="next"
onSubmitEditing={() => confirmPasswordRef.current?.focus()}
editable={!isSubmitting}
containerStyle={styles.inputContainer}
/>
{/* Confirm New Password Input */}
<Input
ref={confirmPasswordRef}
label="Confirm New Password"
value={confirmPassword}
onChangeText={handleConfirmPasswordChange}
placeholder="Confirm new password"
secureTextEntry={true}
returnKeyType="done"
onSubmitEditing={handleUpdatePassword}
editable={!isSubmitting}
containerStyle={styles.inputContainer}
/>
{/* Submit Button */}
<Button
title={isSubmitting ? 'Updating Password...' : 'Update Password'}
onPress={handleUpdatePassword}
loading={isSubmitting}
disabled={!isFormValid()}
/>
{/* Help Text */}
<View style={styles.helpContainer}>
<Text style={styles.helpText}>
Update your password. Your new password must be different from your current password.
</Text>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
Component | Purpose | Key Props |
KeyboardAvoidingView | Manages keyboard visibility for input fields | Platform-specific behavior, keyboardVerticalOffset |
ScrollView | Scrollable container with keyboard handling | keyboardShouldPersistTaps="handled" |
CloseButton | Allow user to cancel and reset auth |
|
Policy Container | Display password requirements | Shows parsed RELID_PASSWORD_POLICY |
Three Input Fields | Current, new, confirm passwords | Each with ref prop for keyboard navigation (currentPasswordRef, newPasswordRef, confirmPasswordRef) |
StatusBanner | Display errors (including statusCode 164) | Conditional rendering |
Submit Button | Trigger password update | Disabled until form valid |
The KeyboardAvoidingView ensures input fields remain visible when keyboard appears:
Platform | Behavior | Description |
iOS |
| Adds padding to shift content above keyboard |
Android |
| Adjusts view height to accommodate keyboard |
The keyboardShouldPersistTaps="handled" prop allows users to tap buttons even when keyboard is visible, providing seamless interaction.
Each Input component has a ref prop attached for smooth keyboard navigation:
Input Field | Ref | Next Field Focus |
Current Password |
| Focuses |
New Password |
| Focuses |
Confirm Password |
| Calls |
This creates a seamless user experience where pressing the keyboard's "Next" button automatically advances to the next field, and "Done" on the final field submits the form.
Complete the component with consistent styling including keyboard management styles:
// src/tutorial/screens/mfa/UpdateExpiryPasswordScreen.tsx (styles)
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#f8f9fa',
},
keyboardAvoidingView: {
flex: 1,
},
container: {
flex: 1,
},
scrollContent: {
flexGrow: 1,
},
content: {
flex: 1,
padding: 20,
paddingTop: 80, // Add space for close button
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#2c3e50',
textAlign: 'center',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: '#7f8c8d',
textAlign: 'center',
marginBottom: 30,
},
userContainer: {
alignItems: 'center',
marginBottom: 20,
},
welcomeText: {
fontSize: 18,
color: '#2c3e50',
marginBottom: 4,
},
userNameText: {
fontSize: 20,
fontWeight: 'bold',
color: '#3498db',
marginBottom: 20,
},
inputContainer: {
marginBottom: 20,
},
helpContainer: {
backgroundColor: '#e8f4f8',
borderRadius: 8,
padding: 16,
marginTop: 20,
},
helpText: {
fontSize: 14,
color: '#2c3e50',
textAlign: 'center',
lineHeight: 20,
},
policyContainer: {
backgroundColor: '#f0f8ff',
borderLeftColor: '#3498db',
borderLeftWidth: 4,
borderRadius: 8,
padding: 16,
marginBottom: 20,
},
policyTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#2c3e50',
marginBottom: 8,
},
policyText: {
fontSize: 14,
color: '#2c3e50',
lineHeight: 20,
},
});
The following images showcase screens from the sample application:
|
|
Let's register the UpdateExpiryPasswordScreen in your navigation configuration.
Add the screen parameters to your navigation types:
// src/tutorial/navigation/AppNavigator.tsx (type additions)
// Update Expiry Password Screen Parameters
interface UpdateExpiryPasswordScreenParams {
eventData: RDNAGetPasswordData;
title: string;
subtitle: string;
responseData?: RDNAGetPasswordData; // Direct response data
}
export type RootStackParamList = {
// ... other routes
UpdateExpiryPasswordScreen: UpdateExpiryPasswordScreenParams;
};
Add the screen to your navigation stack:
// src/tutorial/navigation/AppNavigator.tsx (screen registration)
import { UpdateExpiryPasswordScreen } from '../screens/mfa';
// In your Stack.Navigator:
<Stack.Screen
name="UpdateExpiryPasswordScreen"
component={UpdateExpiryPasswordScreen}
options={{
title: 'Update Expired Password',
headerShown: false,
}}
/>
Add the screen to your MFA screens export:
// src/tutorial/screens/mfa/index.ts
export { default as CheckUserScreen } from './CheckUserScreen';
export { default as ActivationCodeScreen } from './ActivationCodeScreen';
export { default as UserLDAConsentScreen } from './UserLDAConsentScreen';
export { default as SetPasswordScreen } from './SetPasswordScreen';
export { default as VerifyPasswordScreen } from './VerifyPasswordScreen';
export { default as UpdateExpiryPasswordScreen } from './UpdateExpiryPasswordScreen';
export { default as VerifyAuthScreen } from './VerifyAuthScreen';
export { default as DashboardScreen } from './DashboardScreen';
Verify your navigation flow is complete:
Step | Navigation Event | Screen |
1. User Login |
| VerifyPasswordScreen |
2. Password Expired |
| UpdateExpiryPasswordScreen |
3. Password Updated |
| DashboardScreen |
Now let's test the complete password expiry implementation with various scenarios.
Follow these steps to test standard password expiry:
Test password reuse error handling:
Test all validation rules:
Test Case | Expected Error | Expected Behavior |
Empty current password | "Please enter your current password" | Focus current password field |
Empty new password | "Please enter a new password" | Focus new password field |
Empty confirm password | "Please confirm your new password" | Focus confirm password field |
Passwords don't match | "New password and confirm password do not match" | Alert + clear new/confirm fields |
New = Current password | "New password must be different from current password" | Alert + clear new/confirm fields |
Test password policy enforcement:
If you encounter issues, check these areas:
Issue | Possible Cause | Solution |
Policy not displaying | Usung default policy key βRELID_PASSWORD_POLICY' | Update extraction key to RELID_PASSWORD_POLICY |
Fields not clearing | Missing field clear logic in error handling | Add setCurrentPassword(''), setNewPassword(''), setConfirmPassword('') |
Navigation not working | challengeMode 4 not routed in SDKEventProvider | Add if (data.challengeMode === 4) routing |
API not called | Form validation failing | Check isFormValid() logic |
Keyboard covering inputs | Missing KeyboardAvoidingView | Wrap ScrollView with KeyboardAvoidingView and platform-specific behavior |
Scroll not working with keyboard | Missing keyboardShouldPersistTaps | Add keyboardShouldPersistTaps="handled" to ScrollView |
Before deploying password expiry functionality to production, review these important considerations.
Practice | Implementation | Importance |
Never log passwords | Remove all console.log statements that might expose passwords | Critical |
Password history | Respect server-configured history limits | High |
Policy enforcement | Always display and enforce RELID_PASSWORD_POLICY | High |
Error handling | Clear fields on all errors to prevent data exposure | High |
Enhance user experience with these patterns:
// 1. Clear, specific error messages
if (trimmedNewPassword === trimmedCurrentPassword) {
setError('New password must be different from current password');
// Show alert with actionable guidance
Alert.alert('Invalid New Password', 'Your new password must be different from your current password');
}
// 2. Automatic field clearing on errors
if (RDNAEventUtils.hasStatusError(responseData)) {
setError(errorMessage);
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
}
// 3. Keyboard navigation
<Input
returnKeyType="next"
onSubmitEditing={() => newPasswordRef.current?.focus()}
/>
// 4. Keyboard management for better input visibility
<KeyboardAvoidingView
style={styles.keyboardAvoidingView}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
>
<ScrollView
style={styles.container}
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
>
{/* Input fields */}
</ScrollView>
</KeyboardAvoidingView>
Consideration | Implementation |
Field refs | Use useRef for efficient focus management |
Memoization | Consider useMemo for password policy parsing if expensive |
Loading states | Show clear loading indicators during API calls |
Error recovery | Implement retry logic with proper state cleanup |
Before production deployment, verify:
Congratulations! You've successfully implemented REL-ID Password Expiry functionality in your React Native application.
You now have:
β Password Expiry Detection: Automatic detection and routing of challengeMode 4
β UpdatePassword API: Full integration with proper error handling
β Three-Field Validation: Current, new, and confirm password validation
β Password Policy Display: Extraction and display of RELID_PASSWORD_POLICY
β Password Reuse Handling: StatusCode 164 detection with automatic field clearing
β Production-Ready: Secure, user-friendly password expiry flow
Thank you for completing the REL-ID Password Expiry Flow Codelab!
You're now equipped to build secure, production-ready password expiry workflows that provide excellent user experience while maintaining strong security standards.