🎯 Learning Path:
Welcome to the REL-ID Forgot Password codelab! This tutorial builds upon your existing MFA implementation to add secure password recovery capabilities using REL-ID SDK's verification challenge.
In this codelab, you'll enhance your existing MFA application with:
By completing this codelab, you'll master:
forgotPassword() API with proper sync response handlingchallengeMode and ENABLE_FORGOT_PASSWORD configurationgetActivationCode events for OTP/email verificationBefore starting this codelab, ensure you have:
ENABLE_FORGOT_PASSWORD capabilityThe 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-forgot-password folder in the repository you cloned earlier
This codelab extends your MFA application with three core forgot password components:
Before implementing forgot password functionality, let's understand the key SDK events and APIs that power the password recovery workflow.
The password recovery process follows this event-driven pattern:
VerifyPasswordScreen (challengeMode=0 and ENABLE_FORGOT_PASSWORD=true) → forgotPassword() API → getActivationCode Event →
User Enters OTP → setActivationCode() API → getUserConsentForLDA/getPassword Event →
Password Reset Complete → onUserLoggedIn Event → Dashboard
The REL-ID SDK triggers these main events during forgot password flow:
Event Type | Description | User Action Required |
| getActivationCode | Verification challenge triggered after forgotPassword() | User enters OTP/verification code | | getUserConsentForLDA | LDA setup required after verification (Path A) | User approves biometric authentication setup | | getPassword | Direct password reset required (Path B) | User creates new password with policy validation | | onUserLoggedIn | Automatic login after successful password reset | System navigates to dashboard automatically |
Forgot password functionality requires specific conditions:
// Forgot password display conditions
challengeMode === 0 AND ENABLE_FORGOT_PASSWORD === "true"
Condition | Description | Display Forgot Password |
| Manual password entry mode | ✅ Required condition |
| Password creation mode | ❌ Not applicable |
| Server feature enabled | ✅ Required configuration |
| Server feature disabled | ❌ Hide forgot password link |
Add these TypeScript definitions to understand the forgot password API structure:
// src/uniken/services/rdnaService.ts (forgot password addition)
/**
* Initiates forgot password flow for password reset
* @param userId Optional user ID for the forgot password flow
* @returns Promise<RDNASyncResponse> that resolves with sync response structure
*/
async forgotPassword(userId?: string): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
RdnaClient.forgotPassword(userId, response => {
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
resolve(result);
} else {
reject(result);
}
});
});
}
Let's implement the forgot password API in your service layer following established REL-ID SDK patterns.
Add the forgot password method to your existing service implementation:
// src/uniken/services/rdnaService.ts (addition to existing class)
/**
* Initiates forgot password flow for password reset
*
* This method initiates the forgot password flow when challengeMode == 0 and ENABLE_FORGOT_PASSWORD is true.
* It triggers a verification challenge followed by password reset process.
* Can only be used on an active device and requires user verification.
* Uses sync response pattern similar to other API methods.
*
* @see https://developer.uniken.com/docs/forgot-password
*
* Workflow:
* 1. User initiates forgot password
* 2. SDK triggers verification challenge (e.g., activation code, email OTP)
* 3. User completes challenge
* 4. SDK validates challenge
* 5. User sets new password
* 6. SDK logs user in automatically
*
* Response Validation Logic (following reference app pattern):
* 1. Check error.longErrorCode: 0 = success, > 0 = error
* 2. Success typically starts verification challenge flow
* 3. Error Code 170 = Feature not supported
* 4. Async events will be handled by event listeners
*
* @param userID user ID for the forgot password flow (React Native specific)
* @returns Promise<RDNASyncResponse> that resolves with sync response structure
*/
async forgotPassword(userId?: string): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
console.log('RdnaService - Initiating forgot password flow for userId:', userId || 'current user');
RdnaClient.forgotPassword(userId, response => {
console.log('RdnaService - ForgotPassword sync callback received');
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaService - ForgotPassword sync response success, starting verification challenge');
resolve(result);
} else {
console.error('RdnaService - ForgotPassword 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 |
Error Handling | Proper reject/resolve based on sync response |
Now let's enhance your VerifyPasswordScreen to display forgot password functionality conditionally based on challenge mode and server configuration.
Implement the logic to determine when forgot password should be available:
// src/tutorial/screens/mfa/VerifyPasswordScreen.tsx (additions)
/**
* Check if forgot password is enabled from challenge info
* According to documentation: Show "Forgot Password" only when:
* - challengeMode is 0 (manual password entry)
* - ENABLE_FORGOT_PASSWORD is true
*/
const isForgotPasswordEnabled = (): boolean => {
if (challengeMode !== 0) return false;
// Check for ENABLE_FORGOT_PASSWORD in responseData (if available)
if (responseData && RDNAEventUtils.getChallengeValue) {
const enableForgotPassword = RDNAEventUtils.getChallengeValue(responseData, 'ENABLE_FORGOT_PASSWORD');
return enableForgotPassword === 'true';
}
// Default to true for challengeMode 0 if configuration is not available
// This maintains backward compatibility
// This can be handled on the app side as well if not configured on the server
return true;
};
Enhance your screen's state management to handle forgot password loading:
// src/tutorial/screens/mfa/VerifyPasswordScreen.tsx (state additions)
const [password, setPassword] = useState<string>('');
const [error, setError] = useState<string>('');
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [isForgotPasswordLoading, setIsForgotPasswordLoading] = useState<boolean>(false);
/**
* Check if any operation is in progress
*/
const isLoading = (): boolean => {
return isSubmitting || isForgotPasswordLoading;
};
Add the forgot password handling logic with proper error management:
// src/tutorial/screens/mfa/VerifyPasswordScreen.tsx (handler implementation)
/**
* Handle forgot password flow
*/
const handleForgotPassword = async () => {
if (isForgotPasswordLoading || isSubmitting) return;
setIsForgotPasswordLoading(true);
setError('');
try {
console.log('VerifyPasswordScreen - Initiating forgot password flow for userID:', userID);
const syncResponse: RDNASyncResponse = await rdnaService.forgotPassword(userID);
console.log('VerifyPasswordScreen - ForgotPassword sync response successful, waiting for async events');
console.log('VerifyPasswordScreen - Sync response received:', {
longErrorCode: syncResponse.error?.longErrorCode,
shortErrorCode: syncResponse.error?.shortErrorCode,
errorString: syncResponse.error?.errorString
});
// Success case - SDK will trigger getActivationCode event
// No navigation needed here as event manager handles the flow
} catch (error) {
// This catch block handles sync response errors (rejected promises)
console.error('VerifyPasswordScreen - ForgotPassword sync error:', error);
// Cast the error back to RDNASyncResponse as per existing pattern
const result: RDNASyncResponse = error as RDNASyncResponse;
const errorMessage = RDNASyncUtils.getErrorMessage(result);
setError(errorMessage);
} finally {
setIsForgotPasswordLoading(false);
}
};
Implement the forgot password link with proper loading states:
{/* Forgot Password Link - Only show when challengeMode == 0 and ENABLE_FORGOT_PASSWORD is true */}
{isForgotPasswordEnabled() && (
<TouchableOpacity
style={styles.forgotPasswordContainer}
onPress={handleForgotPassword}
disabled={isLoading()}
>
{isForgotPasswordLoading ? (
<View style={styles.forgotPasswordLoadingContainer}>
<ActivityIndicator size="small" color="#3498db" />
<Text style={styles.forgotPasswordLoadingText}>Initiating password reset...</Text>
</View>
) : (
<Text style={[styles.forgotPasswordText, isLoading() && styles.forgotPasswordTextDisabled]}>
Forgot Password?
</Text>
)}
</TouchableOpacity>
)}
The forgot password flow involves a sequence of events that your event manager must handle properly. Let's ensure your event handling supports the complete flow.
After calling forgotPassword(), the SDK triggers this event sequence:
// Complete forgot password event flow
forgotPassword() → getActivationCode → getUserConsentForLDA/getPassword → onUserLoggedIn
Ensure your rdnaEventManager.ts has proper handlers for all forgot password events:
// src/uniken/services/rdnaEventManager.ts (verify these handlers exist)
/**
* Handle activation code request (triggered after forgotPassword)
*/
onGetActivationCode: (data: RDNAGetActivationCodeData) => {
console.log('EventManager - onGetActivationCode triggered for forgot password flow');
NavigationService.navigate('ActivationCodeScreen', {
eventData: data,
title: 'Verify Identity',
subtitle: 'Enter the verification code sent to your registered email or phone',
responseData: data
});
},
/**
* Handle LDA consent request (one possible path after verification)
*/
onGetUserConsentForLDA: (data: RDNAGetUserConsentForLDAData) => {
console.log('EventManager - onGetUserConsentForLDA triggered in forgot password flow');
NavigationService.navigate('UserLDAConsentScreen', {
eventData: data,
title: 'Biometric Setup',
subtitle: 'Set up biometric authentication for your new password',
responseData: data
});
},
/**
* Handle password reset request (alternative path after verification)
*/
onGetPassword: (data: RDNAGetPasswordData) => {
console.log('EventManager - onGetPassword triggered in forgot password flow');
NavigationService.navigate('SetPasswordScreen', {
eventData: data,
title: 'Set New Password',
subtitle: 'Create a new secure password for your account',
challengeMode: data.challengeMode,
responseData: data
});
},
/**
* Handle successful login (final step of forgot password flow)
*/
onUserLoggedIn: (data: RDNAUserLoggedInData) => {
console.log('EventManager - onUserLoggedIn triggered - forgot password flow complete');
NavigationService.navigate('Dashboard', {
userSession: data,
loginMethod: 'forgot_password_reset'
});
}
Update your navigation types to support the forgot password parameters and ensure type safety throughout the flow.
Update your navigation type definitions:
// src/tutorial/navigation/AppNavigator.tsx (type enhancements)
export type RootStackParamList = {
// Enhanced VerifyPasswordScreen for forgot password
VerifyPasswordScreen: {
eventData: RDNAGetPasswordData;
title: string;
subtitle: string;
userID?: string; // Added for forgot password context
challengeMode?: number; // Added for conditional logic
attemptsLeft?: number; // Added for attempt tracking
responseData?: RDNAGetPasswordData;
};
// Existing screens enhanced for forgot password context
ActivationCodeScreen: {
eventData: RDNAGetActivationCodeData;
title: string;
subtitle: string;
responseData?: RDNAGetActivationCodeData;
};
SetPasswordScreen: {
eventData: RDNAGetPasswordData;
title: string;
subtitle: string;
challengeMode: number;
responseData?: RDNAGetPasswordData;
};
UserLDAConsentScreen: {
eventData: RDNAGetUserConsentForLDAData;
title: string;
subtitle: string;
responseData?: RDNAGetUserConsentForLDAData;
};
Dashboard: {
userSession?: RDNAUserLoggedInData;
loginMethod?: 'forgot_password_reset' | 'standard_login' | 'biometric_login';
};
};
Ensure proper parameter passing when navigating to VerifyPasswordScreen:
// Example navigation with forgot password context
NavigationService.navigate('VerifyPasswordScreen', {
eventData: passwordData,
title: 'Verify Password',
subtitle: 'Enter your password to continue',
userID: passwordData.userID,
challengeMode: passwordData.challengeMode,
attemptsLeft: passwordData.attemptsLeft,
responseData: passwordData
});
Let's test your forgot password implementation with comprehensive scenarios to ensure proper functionality.
Setup Requirements:
ENABLE_FORGOT_PASSWORD = "true"challengeMode = 0Test Steps:
Expected Results:
Setup Requirements:
ENABLE_FORGOT_PASSWORD = "false" or missingchallengeMode = 0Test Steps:
Expected Results:
Setup Requirements:
challengeMode = 1 (password creation mode)Test Steps:
Expected Results:
Prepare your forgot password implementation for production deployment with these essential considerations.
Here's your complete reference implementation combining all the patterns and best practices covered in this codelab.
// src/tutorial/screens/mfa/VerifyPasswordScreen.tsx (complete implementation)
import React, { useState, useRef, useEffect } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
StatusBar,
ScrollView,
ActivityIndicator,
SafeAreaView,
} from 'react-native';
import { useRoute } from '@react-navigation/native';
import type { RouteProp } from '@react-navigation/native';
import { RDNAEventUtils, RDNASyncUtils } from '../../../uniken/types/rdnaEvents';
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 VerifyPasswordScreenRouteProp = RouteProp<RootStackParamList, 'VerifyPasswordScreen'>;
const VerifyPasswordScreen: React.FC = () => {
const route = useRoute<VerifyPasswordScreenRouteProp>();
const {
eventData,
title,
subtitle,
userID,
challengeMode = 0,
attemptsLeft = 0,
responseData,
} = route.params;
// State management
const [password, setPassword] = useState<string>('');
const [error, setError] = useState<string>('');
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [isForgotPasswordLoading, setIsForgotPasswordLoading] = useState<boolean>(false);
const passwordRef = useRef<TextInput>(null);
/**
* Check if forgot password is enabled from challenge info
*/
const isForgotPasswordEnabled = (): boolean => {
if (challengeMode !== 0) return false;
if (responseData && RDNAEventUtils.getChallengeValue) {
const enableForgotPassword = RDNAEventUtils.getChallengeValue(responseData, 'ENABLE_FORGOT_PASSWORD');
return enableForgotPassword === 'true';
}
return true;
};
/**
* Handle forgot password flow
*/
const handleForgotPassword = async () => {
if (isForgotPasswordLoading || isSubmitting) return;
setIsForgotPasswordLoading(true);
setError('');
try {
const syncResponse: RDNASyncResponse = await rdnaService.forgotPassword(userID);
console.log('ForgotPassword sync response successful');
} catch (error) {
const result: RDNASyncResponse = error as RDNASyncResponse;
const errorMessage = RDNASyncUtils.getErrorMessage(result);
setError(errorMessage);
} finally {
setIsForgotPasswordLoading(false);
}
};
/**
* Handle password verification
*/
const handleVerifyPassword = async () => {
if (isSubmitting) return;
const trimmedPassword = password.trim();
if (!trimmedPassword) {
setError('Please enter your password');
return;
}
setIsSubmitting(true);
setError('');
try {
const syncResponse: RDNASyncResponse = await rdnaService.setPassword(trimmedPassword, challengeMode);
console.log('SetPassword sync response successful');
} catch (error) {
const result: RDNASyncResponse = error as RDNASyncResponse;
const errorMessage = RDNASyncUtils.getErrorMessage(result);
setError(errorMessage);
setPassword('');
} finally {
setIsSubmitting(false);
}
};
const isFormValid = (): boolean => {
return password.trim().length > 0 && !error;
};
const isLoading = (): boolean => {
return isSubmitting || isForgotPasswordLoading;
};
return (
<SafeAreaView style={styles.safeArea}>
<StatusBar barStyle="dark-content" backgroundColor="#f8f9fa" />
<ScrollView style={styles.container}>
<CloseButton
onPress={async () => {
try {
await rdnaService.resetAuthState();
} catch (error) {
console.error('ResetAuthState error:', error);
}
}}
disabled={isLoading()}
/>
<View style={styles.content}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.subtitle}>{subtitle}</Text>
{userID && (
<View style={styles.userContainer}>
<Text style={styles.welcomeText}>Welcome back</Text>
<Text style={styles.userNameText}>{userID}</Text>
</View>
)}
{attemptsLeft > 0 && (
<StatusBanner
type="warning"
message={`${attemptsLeft} attempt${attemptsLeft !== 1 ? 's' : ''} remaining`}
/>
)}
{error && (
<StatusBanner
type="error"
message={error}
/>
)}
<Input
label="Password"
value={password}
onChangeText={(text) => {
setPassword(text);
if (error) setError('');
}}
placeholder="Enter your password"
secureTextEntry={true}
returnKeyType="done"
onSubmitEditing={handleVerifyPassword}
editable={!isLoading()}
autoFocus={true}
/>
<Button
title={isSubmitting ? 'Verifying...' : 'Verify Password'}
onPress={handleVerifyPassword}
loading={isSubmitting}
disabled={!isFormValid() || isLoading()}
/>
{isForgotPasswordEnabled() && (
<TouchableOpacity
style={styles.forgotPasswordContainer}
onPress={handleForgotPassword}
disabled={isLoading()}
>
{isForgotPasswordLoading ? (
<View style={styles.forgotPasswordLoadingContainer}>
<ActivityIndicator size="small" color="#3498db" />
<Text style={styles.forgotPasswordLoadingText}>
Initiating password reset...
</Text>
</View>
) : (
<Text style={[
styles.forgotPasswordText,
isLoading() && styles.forgotPasswordTextDisabled
]}>
Forgot Password?
</Text>
)}
</TouchableOpacity>
)}
<View style={styles.helpContainer}>
<Text style={styles.helpText}>
Enter your password to verify your identity and continue.
</Text>
</View>
</View>
</ScrollView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#f8f9fa',
},
container: {
flex: 1,
},
content: {
flex: 1,
padding: 20,
paddingTop: 80,
},
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,
},
forgotPasswordContainer: {
alignItems: 'center',
marginTop: 16,
marginBottom: 16,
},
forgotPasswordText: {
fontSize: 16,
color: '#3498db',
textDecorationLine: 'underline',
fontWeight: '500',
},
forgotPasswordTextDisabled: {
color: '#bdc3c7',
},
forgotPasswordLoadingContainer: {
flexDirection: 'row',
alignItems: 'center',
},
forgotPasswordLoadingText: {
fontSize: 16,
color: '#3498db',
marginLeft: 8,
fontWeight: '500',
},
helpContainer: {
backgroundColor: '#e8f4f8',
borderRadius: 8,
padding: 16,
marginTop: 20,
},
helpText: {
fontSize: 14,
color: '#2c3e50',
textAlign: 'center',
lineHeight: 20,
},
});
export default VerifyPasswordScreen;
The following image showcase screen from the sample application:

Congratulations! You've successfully implemented secure forgot password functionality with the REL-ID SDK.
✅ Conditional Forgot Password UI - Smart display logic based on challenge mode and server configuration
✅ Secure API Integration - Proper forgotPassword() implementation with error handling
✅ Event Chain Management - Complete flow from verification to password reset to login
✅ Production-Ready Code - Comprehensive error handling, loading states, and security practices
✅ User Experience Excellence - Clear feedback, intuitive flow, and accessibility support
🔐 You've mastered secure password recovery with REL-ID SDK!
Your implementation provides users with a seamless, secure way to recover their accounts while maintaining the highest security standards. Use this foundation to build robust authentication experiences that users can trust.