π― Learning Path:
This comprehensive codelab teaches you to implement complete Multi-Factor Authentication using the react-native-rdna-client plugin. You'll build both Activation Flow (first-time users) and Login Flow (returning users) with error handling and security practices.
By the end of this codelab, you'll have a complete MFA system that handles:
π± Activation Flow (First-Time Users):
π Login Flow (Returning Users):
Before starting, verify you have:
You should be comfortable with:
The code to get started is stored 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
folder in the repository you cloned earlier
The RELID SDK requires specific permissions for optimal MFA functionality:
iOS Configuration: Refer to the iOS Permissions Documentation for complete Info.plist configuration including:
Android Configuration: Refer to the Android Permissions Documentation for runtime and normal permissions required for MFA features.
Quick Overview: This codelab covers two flows:
Aspect | π Activation Flow | π Login Flow |
When | First-time users, new devices | Returning users, registered devices |
Purpose | Device registration + auth setup | Quick authentication |
Steps | Username β OTP β Device Auth or Password β Success | Username β Device Auth/Password β Success |
Device Auth(LDA) | Optional setup during flow | Automatic if previously enabled |
Activation Flow Occurs When:
Login Flow Occurs When:
The Plugin uses an event-driven architecture where:
// Synchronous API response
const syncResponse = await rdnaService.setUser(username);
// Asynchronous event handling
rdnaEventManager.onGetUser = (challenge) => {
// Handle the challenge in UI
navigation.navigate('CheckUserScreen');
};
SDK Event | API Response | Purpose | Flow |
|
| User identification | Both |
|
| OTP verification | Activation |
|
| Biometric setup | Activation |
|
| Password setup/verify | Both |
| N/A | Success notification(user logged in) | Both |
|
| User Session cleanup(if user logged in) | Both |
Both - Activation and Login
π What We're Building: Complete first-time user registration with device enrollment, OTP verification, and LDA setup.
Please refer to the flow diagram from uniken developer documentation portal, user activation
Phase | Challenge Type | User Action | SDK Validation | Result |
1. User ID |
| Enter username/email | Validates user exists/format | Proceeds or repeats |
2. OTP Verify |
| Enter activation code | Validates code from email/SMS | Proceeds or shows error |
3. Device Auth |
| Choose biometric or password | Sets up device authentication | Completes activation |
4. Success | N/A | Automatic navigation | User session established | User activated & logged in |
Important: The getUser
event can trigger multiple times if:
Your UI must handle repeated events gracefully without breaking navigation.
Before implementing the activation flow, establish comprehensive TypeScript types for type safety and better development experience.
Create or extend your existing type definitions file:
// src/uniken/types/rdnaEvents.ts
/**
* Standard RDNA Error Structure
* Used across all APIs and events for consistent error handling
*/
export interface RDNAError {
longErrorCode: number;
shortErrorCode: number;
errorString: string;
}
/**
* Standard RDNA Status Structure
* Used in challenge responses and API responses
*/
export interface RDNAStatus {
statusCode: number;
statusMessage: string;
}
/**
* Standard RDNA Session Structure
* Contains session information from the SDK
*/
export interface RDNASession {
sessionType: number;
sessionID: string;
}
/**
* Standard RDNA Additional Info Structure
* Contains comprehensive session and configuration data
*/
export interface RDNAAdditionalInfo {
DNAProxyPort: number;
isAdUser: number;
isDNAProxyLocalHostOnly: number;
jwtJsonTokenInfo: string;
settings: string;
mtlsP12Bundle: string;
configSettings: string;
loginIDs: any[];
availableGroups: any[];
idvAuditInfo: string;
idvUserRole: string;
currentWorkFlow: string;
isMTDDownloadOnly: number;
}
/**
* Standard RDNA Challenge Response Structure
* Complete challenge response with all components
*/
export interface RDNAChallengeResponse {
status: RDNAStatus;
session: RDNASession;
additionalInfo: RDNAAdditionalInfo;
challengeInfo: Array<{key: string; value: string}>;
}
/**
* Base RDNA Event Structure
* Foundation for all asynchronous SDK events
*/
export interface RDNAEvent {
challengeResponse: RDNAChallengeResponse;
error: RDNAError;
}
/**
* RDNA Sync Response Structure
* Used for synchronous API responses
*/
export interface RDNASyncResponse {
error: RDNAError;
}
/**
* RDNA JSON Response Structure
* Used for simple JSON string responses
*/
export interface RDNAJsonResponse {
response: string;
}
/**
* RDNA Get User Data
* User information request event
*/
export interface RDNAGetUserData extends RDNAEvent {
recentLoggedInUser: string;
rememberedUsers: string[];
}
/**
* RDNA Get Activation Code Data
* Activation code request event
*/
export interface RDNAGetActivationCodeData extends RDNAEvent {
userID: string;
verificationKey: string;
attemptsLeft: number;
}
/**
* RDNA Get User Consent For LDA Data
* User consent request for LDA authentication
*/
export interface RDNAGetUserConsentForLDAData extends RDNAEvent {
userID: string;
challengeMode: number;
authenticationType: number;
}
/**
* RDNA Get Password Data
* Password request event
*/
export interface RDNAGetPasswordData extends RDNAEvent {
userID: string;
challengeMode: number;
attemptsLeft: number;
}
/**
* RDNA User Logged In Data
* User login completion event with full session and JWT information
*/
export interface RDNAUserLoggedInData extends RDNAEvent {
userID: string;
}
// Event callback function types
export type RDNAGetUserCallback = (data: RDNAGetUserData) => void;
export type RDNAGetActivationCodeCallback = (data: RDNAGetActivationCodeData) => void;
export type RDNAGetUserConsentForLDACallback = (data: RDNAGetUserConsentForLDAData) => void;
export type RDNAGetPasswordCallback = (data: RDNAGetPasswordData) => void;
export type RDNAUserLoggedInCallback = (data: RDNAUserLoggedInData) => void;
/**
* Utility functions for handling RDNA responses and errors
*/
export class RDNAEventUtils {
/**
* Check if event data contains API errors
*/
static hasApiError(data: RDNAEvent): boolean {
return data.error && data.error.longErrorCode > 0;
}
/**
* Check if event data contains status errors
*/
static hasStatusError(data: RDNAEvent): boolean {
return data.challengeResponse.status.statusCode !== 100;
}
/**
* Get user-friendly error message from event data
*/
static getErrorMessage(data: RDNAEvent): string {
if (this.hasApiError(data)) {
return data.error.errorString || 'An API error occurred';
}
if (this.hasStatusError(data)) {
return data.challengeResponse.status.statusMessage || 'A status error occurred';
}
return 'Unknown error occurred';
}
}
/**
* Utility functions for handling sync responses
*/
export class RDNASyncUtils {
/**
* Get user-friendly error message from sync response
*/
static getErrorMessage(response: RDNASyncResponse): string {
return response.error.errorString || 'An error occurred. Please try again.';
}
/**
* Check if sync response indicates success
*/
static isSuccess(response: RDNASyncResponse): boolean {
return response.error.longErrorCode === 0;
}
}
Building on your existing RELID event manager, add support for activation flow events. The activation flow requires handling four key MFA events.
Extend your event manager to handle activation-specific events:
// src/uniken/services/rdnaEventManager.ts (additions for activation)
class RdnaEventManager {
// Add MFA event handlers
private getUserHandler?: RDNAGetUserCallback;
private getActivationCodeHandler?: RDNAGetActivationCodeCallback;
private getUserConsentForLDAHandler?: RDNAGetUserConsentForLDACallback;
private getPasswordHandler?: RDNAGetPasswordCallback;
private onUserLoggedInHandler?: RDNAUserLoggedInCallback;
private registerEventListeners() {
// ... existing initialization and MTD listeners ...
// Register activation flow event listeners
this.listeners.push(
this.rdnaEmitter.addListener('getUser', this.onGetUser.bind(this)),
this.rdnaEmitter.addListener('getActivationCode', this.onGetActivationCode.bind(this)),
this.rdnaEmitter.addListener('getUserConsentForLDA', this.onGetUserConsentForLDA.bind(this)),
this.rdnaEmitter.addListener('getPassword', this.onGetPassword.bind(this)),
this.rdnaEmitter.addListener('onUserLoggedIn', this.onUserLoggedIn.bind(this))
);
}
}
Each activation event requires specific handling with proper JSON parsing:
/**
* Handles user identification challenge (always first in activation flow)
* Can be triggered multiple times if user validation fails
*/
private onGetUser(response: RDNAJsonResponse) {
console.log("RdnaEventManager - Get user event received");
try {
const getUserData: RDNAGetUserData = JSON.parse(response.response);
console.log("RdnaEventManager - Get user status:", getUserData.challengeResponse.status.statusCode);
console.log("RdnaEventManager - Recent logged in user:", getUserData.recentLoggedInUser);
console.log("RdnaEventManager - Remembered users:", getUserData.rememberedUsers);
if (this.getUserHandler) {
this.getUserHandler(getUserData);
}
} catch (error) {
console.error("RdnaEventManager - Failed to parse get user:", error);
}
}
/**
* Handles activation code challenge (OTP verification)
* Provides attempts left information for user feedback
*/
private onGetActivationCode(response: RDNAJsonResponse) {
console.log("RdnaEventManager - Get activation code event received");
try {
const getActivationCodeData: RDNAGetActivationCodeData = JSON.parse(response.response);
console.log("RdnaEventManager - UserID:", getActivationCodeData.userID);
console.log("RdnaEventManager - Attempts left:", getActivationCodeData.attemptsLeft);
console.log("RdnaEventManager - Verification key:", getActivationCodeData.verificationKey);
if (this.getActivationCodeHandler) {
this.getActivationCodeHandler(getActivationCodeData);
}
} catch (error) {
console.error("RdnaEventManager - Failed to parse get activation code:", error);
}
}
/**
* Handles Local Device Authentication consent request
* Triggered when biometric authentication is available
*/
private onGetUserConsentForLDA(response: RDNAJsonResponse) {
console.log("RdnaEventManager - Get user consent for LDA event received");
try {
const getUserConsentForLDAData: RDNAGetUserConsentForLDAData = JSON.parse(response.response);
console.log("RdnaEventManager - UserID:", getUserConsentForLDAData.userID);
console.log("RdnaEventManager - Challenge mode:", getUserConsentForLDAData.challengeMode);
console.log("RdnaEventManager - Authentication type:", getUserConsentForLDAData.authenticationType);
if (this.getUserConsentForLDAHandler) {
this.getUserConsentForLDAHandler(getUserConsentForLDAData);
}
} catch (error) {
console.error("RdnaEventManager - Failed to parse get user consent for LDA:", error);
}
}
/**
* Handles password authentication challenge
* Fallback when biometric authentication is not available
*/
private onGetPassword(response: RDNAJsonResponse) {
console.log("RdnaEventManager - Get password event received");
try {
const getPasswordData: RDNAGetPasswordData = JSON.parse(response.response);
console.log("RdnaEventManager - UserID:", getPasswordData.userID);
console.log("RdnaEventManager - Challenge mode:", getPasswordData.challengeMode);
console.log("RdnaEventManager - Attempts left:", getPasswordData.attemptsLeft);
if (this.getPasswordHandler) {
this.getPasswordHandler(getPasswordData);
}
} catch (error) {
console.error("RdnaEventManager - Failed to parse get password:", error);
}
}
/**
* Handles successful activation completion
* Provides session information and JWT token details
*/
private onUserLoggedIn(response: RDNAJsonResponse) {
console.log("RdnaEventManager - User logged in event received");
try {
const userLoggedInData: RDNAUserLoggedInData = JSON.parse(response.response);
console.log("RdnaEventManager - UserID:", userLoggedInData.userID);
console.log("RdnaEventManager - Session ID:", userLoggedInData.challengeResponse.session.sessionID);
console.log("RdnaEventManager - Session type:", userLoggedInData.challengeResponse.session.sessionType);
if (this.onUserLoggedInHandler) {
this.onUserLoggedInHandler(userLoggedInData);
}
} catch (error) {
console.error("RdnaEventManager - Failed to parse user logged in:", error);
}
}
Provide public methods for setting event handlers:
// Public setter methods for activation event handlers
public setGetUserHandler(callback?: RDNAGetUserCallback): void {
this.getUserHandler = callback;
}
public setGetActivationCodeHandler(callback?: RDNAGetActivationCodeCallback): void {
this.getActivationCodeHandler = callback;
}
public setGetUserConsentForLDAHandler(callback?: RDNAGetUserConsentForLDACallback): void {
this.getUserConsentForLDAHandler = callback;
}
public setGetPasswordHandler(callback?: RDNAGetPasswordCallback): void {
this.getPasswordHandler = callback;
}
public setOnUserLoggedInHandler(callback?: RDNAUserLoggedInCallback): void {
this.onUserLoggedInHandler = callback;
}
// Cleanup method to clear all handlers
public clearActivationHandlers(): void {
this.getUserHandler = undefined;
this.getActivationCodeHandler = undefined;
this.getUserConsentForLDAHandler = undefined;
this.getPasswordHandler = undefined;
this.onUserLoggedInHandler = undefined;
}
setCallbacks
method for easy component integrationAdd the activation flow APIs to your RELID service. These APIs respond to the activation challenges with user-provided data.
Add these methods to your RdnaService
class:
// src/uniken/services/rdnaService.ts (activation API additions)
export class RdnaService {
/**
* Sets the user identifier for activation flow
* Responds to getUser challenge - can be called multiple times
* @param username User identifier (username, email, etc.)
* @returns Promise<RDNASyncResponse>
*/
async setUser(username: string): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
console.log('RdnaService - Setting user for activation flow:', username);
RdnaClient.setUser(username, response => {
console.log('RdnaService - SetUser sync callback received');
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaService - SetUser sync response success, waiting for async events');
resolve(result);
} else {
console.error('RdnaService - SetUser sync response error:', result);
reject(result);
}
});
});
}
/**
* Sets the activation code for user verification
* Responds to getActivationCode challenge
* @param activationCode OTP or activation code from user
* @returns Promise<RDNASyncResponse>
*/
async setActivationCode(activationCode: string): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
console.log('RdnaService - Setting activation code for activation flow');
RdnaClient.setActivationCode(activationCode, response => {
console.log('RdnaService - SetActivationCode sync callback received');
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaService - SetActivationCode sync response success, waiting for async events');
resolve(result);
} else {
console.error('RdnaService - SetActivationCode sync response error:', result);
reject(result);
}
});
});
}
/**
* Sets user consent for Local Device Authentication (biometric)
* Responds to getUserConsentForLDA challenge
* @param isEnrollLDA User consent decision (true = approve, false = reject)
* @param challengeMode Challenge mode from getUserConsentForLDA event
* @param authenticationType Authentication type from getUserConsentForLDA event
* @returns Promise<RDNASyncResponse>
*/
async setUserConsentForLDA(isEnrollLDA: boolean, challengeMode: number, authenticationType: number): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
console.log('RdnaService - Setting user consent for LDA:', {
isEnrollLDA,
challengeMode,
authenticationType
});
RdnaClient.setUserConsentForLDA(isEnrollLDA, challengeMode, authenticationType, response => {
console.log('RdnaService - SetUserConsentForLDA sync callback received');
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaService - SetUserConsentForLDA sync response success, waiting for async events');
resolve(result);
} else {
console.error('RdnaService - SetUserConsentForLDA sync response error:', result);
reject(result);
}
});
});
}
/**
* Sets the password for authentication
* Responds to getPassword challenge (fallback when LDA not available)
* @param password User password
* @param challengeMode Challenge mode from getPassword event (default: 1)
* @returns Promise<RDNASyncResponse>
*/
async setPassword(password: string, challengeMode: number = 1): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
console.log('RdnaService - Setting password for activation flow');
RdnaClient.setPassword(password, challengeMode, response => {
console.log('RdnaService - SetPassword sync callback received');
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaService - SetPassword sync response success, waiting for async events');
resolve(result);
} else {
console.error('RdnaService - SetPassword sync response error:', result);
reject(result);
}
});
});
}
/**
* Requests resend of activation code
* Can be called when user doesn't receive initial activation code
* @returns Promise<RDNASyncResponse>
*/
async resendActivationCode(): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
console.log('RdnaService - Requesting resend of activation code');
RdnaClient.resendActivationCode(response => {
console.log('RdnaService - ResendActivationCode sync callback received');
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaService - ResendActivationCode sync response success, waiting for new getActivationCode event');
resolve(result);
} else {
console.error('RdnaService - ResendActivationCode sync response error:', result);
reject(result);
}
});
});
}
/**
* Resets authentication state and returns to initial flow
* @returns Promise<RDNASyncResponse>
*/
async resetAuthState(): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
console.log('RdnaService - Resetting authentication state');
RdnaClient.resetAuthState(response => {
console.log('RdnaService - ResetAuthState sync callback received');
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaService - ResetAuthState sync response success, waiting for new getUser event');
resolve(result);
} else {
console.error('RdnaService - ResetAuthState sync response error:', result);
reject(result);
}
});
});
}
}
The resetAuthState
API is a critical method for managing authentication flow state. It provides a clean way to reset the current authentication session and return the SDK to its initial state.
The resetAuthState
API should be called in these pre-login scenarios:
Note: These use cases only apply during the authentication process, before onUserLoggedIn
event is triggered.
// The resetAuthState API triggers a clean state transition
RdnaClient.resetAuthState((syncResponse) => {
// Immediate synchronous response
console.log("Reset auth state response", syncResponse);
// The SDK will immediately trigger a new 'getUser' event
// This allows you to restart the authentication flow cleanly
});
Key Behaviors:
getUser
event after successful resetHere's how resetAuthState
is typically used in UI components for user cancellation:
// Example from CheckUserScreen - handling close/cancel button
const handleClose = async () => {
try {
console.log('CheckUserScreen - Calling resetAuthState');
await rdnaService.resetAuthState();
console.log('CheckUserScreen - ResetAuthState successful');
// SDK will automatically trigger getUser event to restart flow
} catch (error) {
console.error('CheckUserScreen - ResetAuthState error:', error);
// Handle reset failure appropriately
}
};
resetAuthState
is asynchronous and should be properly awaitedgetUser
eventresetAuthState
over navigation-only solutions when canceling flowsUser Cancels/Error Occurs
β
Call resetAuthState()
β
SDK Clears Session State
β
Synchronous Response (success/error)
β
SDK Triggers getUser Event
β
App Handles Fresh Authentication Flow
The resendActivationCode
API is used when the user has not received their activation code (OTP) via email or SMS and requests a new one.
Calling this method sends a new OTP to the user and triggers a new getActivationCode
event. This allows users to receive a fresh activation code without having to restart the entire authentication process.
The resendActivationCode
API should be used in these scenarios:
// The resendActivationCode API sends a new OTP and triggers fresh event
RdnaClient.resendActivationCode((syncResponse) => {
// Immediate synchronous response
console.log("Resend activation code response", syncResponse);
// The SDK will trigger a new 'getActivationCode' event
// This provides fresh OTP data to the application
});
Key Behaviors:
getActivationCode
event with new OTP detailsresetAuthState
)Here's how resendActivationCode
is typically used in activation code screens:
// Example from ActivationCodeScreen - handling resend button
const handleResendActivationCode = async () => {
if (isResending || isValidating) return;
setIsResending(true);
setError('');
try {
console.log('ActivationCodeScreen - Requesting resend of activation code');
await rdnaService.resendActivationCode();
console.log('ActivationCodeScreen - ResendActivationCode sync response successful, waiting for new getActivationCode event');
setValidationResult({
success: true,
message: 'New activation code sent! Please check your email or SMS.'
});
} catch (error) {
console.error('ActivationCodeScreen - ResendActivationCode error:', error);
setError('Failed to resend activation code. Please try again.');
} finally {
setIsResending(false);
}
};
getActivationCode
event with updated dataUser Requests Resend
β
Call resendActivationCode()
β
SDK Sends New OTP (Email/SMS)
β
Synchronous Response (success/error)
β
SDK Triggers getActivationCode Event
β
App Receives Fresh OTP Data
All activation APIs follow the same response handling pattern:
longErrorCode === 0
means SDK accepted the dataRDNASyncResponse
with error detailsCreate a centralized navigation service to handle programmatic navigation throughout the activation flow.
// src/tutorial/navigation/NavigationService.ts
import { createNavigationContainerRef, CommonActions } from '@react-navigation/native';
import type { RootStackParamList } from './AppNavigator';
export const navigationRef = createNavigationContainerRef<RootStackParamList>();
export const NavigationService = {
/**
* Navigate to a specific screen
*/
navigate: (name: keyof RootStackParamList, params?: any) => {
if (navigationRef.isReady()) {
console.log('NavigationService: Navigating to', name, params);
navigationRef.navigate(name as any, params);
} else {
console.warn('NavigationService: Navigation not ready, cannot navigate to', name);
}
},
/**
* Smart navigation that prevents duplicate screens and updates existing screens
* - If already on target screen: updates params with new event data
* - If different screen: navigates normally
* This prevents navigation stack pollution when SDK events fire repeatedly
*/
navigateOrUpdate: (name: keyof RootStackParamList, params?: any) => {
if (!navigationRef.isReady()) {
console.warn('NavigationService: Navigation not ready, cannot navigate to', name);
return;
}
const currentRoute = navigationRef.getCurrentRoute();
if (currentRoute?.name === name) {
// Already on target screen - update params with new event data
navigationRef.setParams(params);
console.log('NavigationService: Updating existing screen', name, 'with new params');
} else {
// Different screen - navigate normally
navigationRef.navigate(name as any, params);
console.log('NavigationService: Navigating to new screen', name);
}
},
/**
* Push screen to navigation stack
*/
push: (name: keyof RootStackParamList, params?: any) => {
if (navigationRef.isReady()) {
console.log('NavigationService: Pushing to', name, params);
navigationRef.dispatch(
CommonActions.navigate({
name: name as any,
params,
})
);
} else {
console.warn('NavigationService: Navigation not ready, cannot push to', name);
}
},
/**
* Reset navigation stack to specific screen
*/
reset: (routeName: keyof RootStackParamList) => {
if (navigationRef.isReady()) {
console.log('NavigationService: Resetting navigation to', routeName);
navigationRef.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: routeName as any }],
})
);
} else {
console.warn('NavigationService: Navigation not ready, cannot reset to', routeName);
}
},
/**
* Check if navigation is ready
*/
isReady: () => {
return navigationRef.isReady();
},
/**
* Get current route information
*/
getCurrentRoute: () => {
if (navigationRef.isReady()) {
return navigationRef.getCurrentRoute();
}
return null;
}
};
export default NavigationService;
// src/tutorial/navigation/AppNavigator.tsx
export type RootStackParamList = {
// Initialization Screens
TutorialHome: undefined;
TutorialSuccess: { userID?: string; sessionID?: string };
TutorialError: { error?: string };
SecurityExit: undefined;
// Activation Flow Screens
CheckUserScreen: {
eventData?: RDNAGetUserData;
responseData?: RDNAGetUserData;
title: string;
subtitle: string;
placeholder: string;
buttonText: string;
};
ActivationCodeScreen: {
eventData?: RDNAGetActivationCodeData;
responseData?: RDNAGetActivationCodeData;
title: string;
subtitle: string;
placeholder: string;
buttonText: string;
attemptsLeft: number;
};
UserLDAConsentScreen: {
eventData?: RDNAGetUserConsentForLDAData;
responseData?: RDNAGetUserConsentForLDAData;
title: string;
subtitle: string;
};
SetPasswordScreen: {
eventData?: RDNAGetPasswordData;
responseData?: RDNAGetPasswordData;
title: string;
subtitle: string;
attemptsLeft: number;
};
VerifyPasswordScreen: {
eventData?: RDNAGetPasswordData;
responseData?: RDNAGetPasswordData;
title: string;
subtitle: string;
attemptsLeft: number;
};
// Dashboard
DashboardScreen: {
userID?: string;
sessionID?: string;
sessionType?: number;
};
};
Create a centralized SDK Event Provider to handle all REL-ID SDK events and coordinate navigation throughout the activation flow. This provider acts as the central nervous system for your MFA application.
The SDKEventProvider is a React Context provider that:
Create the main SDK Event Provider:
// src/uniken/providers/SDKEventProvider.tsx
/**
* SDK Event Provider
*
* Centralized React Context provider for REL-ID SDK event handling.
* Manages all SDK events, screen state, and navigation logic in one place.
*
* Key Features:
* - Consolidated event handling for all SDK events
* - Screen state management for active screen tracking
* - Response routing to appropriate screens
* - Navigation logic for different event types
* - React lifecycle integration
*
* Usage:
* ```typescript
* <SDKEventProvider>
* <YourApp />
* </SDKEventProvider>
* ```
*/
import React, { createContext, useContext, useEffect, useCallback, ReactNode, useState } from 'react';
import rdnaService from '../services/rdnaService';
import NavigationService from '../../tutorial/navigation/NavigationService';
import type {
RDNAInitializedData,
RDNAGetUserData,
RDNAGetActivationCodeData,
RDNAGetUserConsentForLDAData,
RDNAGetPasswordData,
RDNAUserLoggedInData,
RDNAUserLoggedOffData,
RDNACredentialsAvailableForUpdateData
} from '../types/rdnaEvents';
/**
* SDK Event Context Interface - Simplified for direct navigation approach
*/
interface SDKEventContextType {}
/**
* SDK Event Context
*/
const SDKEventContext = createContext<SDKEventContextType | undefined>(undefined);
/**
* SDK Event Provider Props
*/
interface SDKEventProviderProps {
children: ReactNode;
}
/**
* SDK Event Provider Component
*/
export const SDKEventProvider: React.FC<SDKEventProviderProps> = ({ children }) => {
const [currentScreen, setCurrentScreen] = useState<string | null>(null);
/**
* Event handler for successful initialization
*/
const handleInitialized = useCallback((data: RDNAInitializedData) => {
console.log('SDKEventProvider - Successfully initialized, Session ID:', data.session.sessionID);
//In the MFA (Multi-Factor Authentication) flow, there is no need to explicitly handle this event.
//When this event is triggered, the SDK will automatically invoke one of the following methodsβgetUser, getActivationCode, or getPasswordβdepending on the user state and the current state of the SDK.
}, []);
/**
* Event handler for get user requests
*/
const handleGetUser = useCallback((data: RDNAGetUserData) => {
console.log('SDKEventProvider - Get user event received, status:', data.challengeResponse.status.statusCode);
// Use navigateOrUpdate to prevent duplicate screens and update existing screen with new event data
NavigationService.navigateOrUpdate('CheckUserScreen', {
eventData: data,
inputType: 'text',
title: 'Set User',
subtitle: 'Enter your username to continue',
placeholder: 'Enter your username',
buttonText: 'Set User',
// Pass response data directly
responseData: data,
});
}, []);
/**
* Event handler for get activation code requests
*/
const handleGetActivationCode = useCallback((data: RDNAGetActivationCodeData) => {
console.log('SDKEventProvider - Get activation code event received, status:', data.challengeResponse.status.statusCode);
console.log('SDKEventProvider - UserID:', data.userID, 'AttemptsLeft:', data.attemptsLeft);
// Use navigateOrUpdate to prevent duplicate screens and update existing screen with new event data
NavigationService.navigateOrUpdate('ActivationCodeScreen', {
eventData: data,
inputType: 'text',
title: 'Enter Activation Code',
subtitle: `Enter the activation code for user: ${data.userID}`,
placeholder: 'Enter activation code',
buttonText: 'Verify Code',
attemptsLeft: data.attemptsLeft,
// Pass response data directly
responseData: data,
});
}, []);
/**
* Event handler for get user consent for LDA requests
*/
const handleGetUserConsentForLDA = useCallback((data: RDNAGetUserConsentForLDAData) => {
console.log('SDKEventProvider - Get user consent for LDA event received, status:', data.challengeResponse.status.statusCode);
console.log('SDKEventProvider - UserID:', data.userID, 'ChallengeMode:', data.challengeMode, 'AuthenticationType:', data.authenticationType);
// Use navigateOrUpdate to prevent duplicate screens and update existing screen with new event data
NavigationService.navigateOrUpdate('UserLDAConsentScreen', {
eventData: data,
title: 'Local Device Authentication Consent',
subtitle: `Grant permission for device authentication for user: ${data.userID}`,
// Pass response data directly
responseData: data,
});
}, []);
/**
* 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 {
// challengeMode = 1: Set new password
NavigationService.navigateOrUpdate('SetPasswordScreen', {
eventData: data,
title: 'Set Password',
subtitle: `Create a secure password for user: ${data.userID}`,
responseData: data,
});
}
}, []);
/**
* Event handler for user logged in event
*/
const handleUserLoggedIn = useCallback((data: RDNAUserLoggedInData) => {
console.log('SDKEventProvider - User logged in event received for user:', data.userID);
console.log('SDKEventProvider - Session ID:', data.challengeResponse.session.sessionID);
console.log('SDKEventProvider - Current workflow:', data.challengeResponse.additionalInfo.currentWorkFlow);
// Extract session and JWT information
const sessionID = data.challengeResponse.session.sessionID;
const sessionType = data.challengeResponse.session.sessionType;
const additionalInfo = data.challengeResponse.additionalInfo;
const jwtToken = additionalInfo.jwtJsonTokenInfo;
const userRole = additionalInfo.idvUserRole;
const currentWorkFlow = additionalInfo.currentWorkFlow;
// Navigate to DrawerNavigator with all session data
NavigationService.navigate('DrawerNavigator', {
screen: 'DashboardHome',
params: {
userID: data.userID,
sessionID,
sessionType,
jwtToken,
loginTime: new Date().toLocaleString(),
userRole,
currentWorkFlow,
}
});
}, []);
/**
* Event handler for user logged off event
*/
const handleUserLoggedOff = useCallback((data: RDNAUserLoggedOffData) => {
console.log('SDKEventProvider - User logged off event received for user:', data.userID);
console.log('SDKEventProvider - Session ID:', data.challengeResponse.session.sessionID);
// Log the event as requested - no further action needed
// The getUser event will be triggered automatically by the SDK and handled by existing logic
}, []);
/**
* Event handler for credentials available for update event
*/
const handleCredentialsAvailableForUpdate = useCallback((data: RDNACredentialsAvailableForUpdateData) => {
console.log('SDKEventProvider - Credentials available for update event received for user:', data.userID);
console.log('SDKEventProvider - Available options:', data.options);
// For now, do nothing as requested
// This could be used to show update options to the user in the future
}, []);
/**
* Set up SDK Event Subscriptions on mount
*/
useEffect(() => {
const eventManager = rdnaService.getEventManager();
eventManager.setInitializedHandler(handleInitialized);
eventManager.setGetUserHandler(handleGetUser);
eventManager.setGetActivationCodeHandler(handleGetActivationCode);
eventManager.setGetUserConsentForLDAHandler(handleGetUserConsentForLDA);
eventManager.setGetPasswordHandler(handleGetPassword);
eventManager.setOnUserLoggedInHandler(handleUserLoggedIn);
eventManager.setCredentialsAvailableForUpdateHandler(handleCredentialsAvailableForUpdate);
eventManager.setOnUserLoggedOffHandler(handleUserLoggedOff);
// Only cleanup on component unmount
return () => {
console.log('SDKEventProvider - Component unmounting, cleaning up event handlers');
eventManager.cleanup();
};
}, []); // Empty dependency array - setup once on mount
/**
* Context Value - Simplified for direct navigation approach
*/
const contextValue: SDKEventContextType = {};
return (
<SDKEventContext.Provider value={contextValue}>
{children}
</SDKEventContext.Provider>
);
};
export default SDKEventProvider;
Create an index file for easy importing:
// src/uniken/providers/index.ts
/**
* Providers Index
*
* Exports all SDK-related providers for easy importing.
*/
export { SDKEventProvider} from './SDKEventProvider';
The SDKEventProvider handles several critical events in the MFA flow:
CheckUserScreen
with appropriate parametersActivationCodeScreen
with user contextchallengeMode
: challengeMode = 0
: Verify existing passwordchallengeMode = 1
: Set new passwordUpdate your main App component to use the SDKEventProvider:
// App.tsx
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
*/
import React from 'react';
import {StatusBar, useColorScheme} from 'react-native';
import {MTDThreatProvider} from './src/uniken/MTDContext';
import {SDKEventProvider} from './src/uniken/providers/SDKEventProvider';
import {AppNavigator} from './src/tutorial/navigation';
function App() {
const isDarkMode = useColorScheme() === 'dark';
return (
<MTDThreatProvider>
<SDKEventProvider>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<AppNavigator />
</SDKEventProvider>
</MTDThreatProvider>
);
}
export default App;
The provider hierarchy ensures proper event flow:
This structure provides:
Create the first screen in the activation flow - user identification. This screen handles the getUser
challenge and can be triggered multiple times.
The CheckUserScreen
handles username input and the setUser
API call:
// src/tutorial/screens/mfa/CheckUserScreen.tsx
import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
StatusBar,
KeyboardAvoidingView,
Platform,
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 { RDNASyncResponse } from '../../../uniken/types/rdnaEvents';
import rdnaService from '../../../uniken/services/rdnaService';
import { Button, Input, StatusBanner } from '../components';
import type { RootStackParamList } from '../../navigation/AppNavigator';
type CheckUserScreenRouteProp = RouteProp<RootStackParamList, 'CheckUserScreen'>;
const CheckUserScreen: React.FC = () => {
const route = useRoute<CheckUserScreenRouteProp>();
const {
eventData,
title = 'Set User',
subtitle = 'Enter your username to continue',
placeholder = 'Enter username',
buttonText = 'Set User',
responseData,
} = route.params;
const [username, setUsername] = useState<string>('');
const [error, setError] = useState<string>('');
const [isValidating, setIsValidating] = useState<boolean>(false);
const [validationResult, setValidationResult] = useState<{
success: boolean;
message: string;
} | null>(null);
/**
* Handle close button - reset authentication state
*/
const handleClose = async () => {
try {
console.log('CheckUserScreen - Calling resetAuthState');
await rdnaService.resetAuthState();
console.log('CheckUserScreen - ResetAuthState successful');
} catch (error) {
console.error('CheckUserScreen - ResetAuthState error:', error);
}
};
/**
* Handle response data for cyclical getUser events
* This may be called multiple times if user validation fails
*/
useEffect(() => {
if (responseData) {
console.log('CheckUserScreen - Processing response data from route params:', responseData);
// Check for API errors first
if (RDNAEventUtils.hasApiError(responseData)) {
const errorMessage = RDNAEventUtils.getErrorMessage(responseData);
console.log('CheckUserScreen - API error:', errorMessage);
setError(errorMessage);
setValidationResult({
success: false,
message: errorMessage
});
return;
}
// Check for status errors (user validation failed)
if (RDNAEventUtils.hasStatusError(responseData)) {
const errorMessage = RDNAEventUtils.getErrorMessage(responseData);
console.log('CheckUserScreen - Status error:', errorMessage);
setError(errorMessage);
setValidationResult({
success: false,
message: errorMessage
});
return;
}
// Success - ready for username input
setValidationResult({
success: true,
message: 'Ready to enter username'
});
console.log('CheckUserScreen - Successfully processed response data');
}
}, [responseData]);
/**
* Handle username input changes
*/
const handleUsernameChange = (text: string) => {
setUsername(text);
if (error) {
setError('');
}
if (validationResult) {
setValidationResult(null);
}
};
/**
* Handle user validation via setUser API
*/
const handleValidateUser = async () => {
const trimmedUsername = username.trim();
setIsValidating(true);
setError('');
setValidationResult(null);
try {
console.log('CheckUserScreen - Setting user:', trimmedUsername);
const syncResponse: RDNASyncResponse = await rdnaService.setUser(trimmedUsername);
console.log('CheckUserScreen - SetUser sync response successful, waiting for async events');
// Success indication - async events will be handled by event listeners
setValidationResult({
success: true,
message: 'User set successfully! Waiting for next step...'
});
} catch (error) {
// Handle sync response errors (rejected promises)
console.error('CheckUserScreen - SetUser sync error:', error);
const result: RDNASyncResponse = error as RDNASyncResponse;
const errorMessage = RDNASyncUtils.getErrorMessage(result);
setError(errorMessage);
} finally {
setIsValidating(false);
}
};
/**
* Form validation
*/
const isFormValid = (): boolean => {
return username.trim().length > 0 && !error;
};
return (
<SafeAreaView style={styles.safeArea}>
<StatusBar barStyle="dark-content" backgroundColor="#f8f9fa" />
{/* Close Button */}
<CloseButton
onPress={handleClose}
disabled={isValidating}
/>
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<View style={styles.content}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.subtitle}>{subtitle}</Text>
{/* Validation Result Display */}
{validationResult && (
<StatusBanner
type={validationResult.success ? 'success' : 'error'}
message={validationResult.message}
/>
)}
{/* Username Input */}
<Input
label="Username"
value={username}
onChangeText={handleUsernameChange}
placeholder={placeholder}
returnKeyType="done"
onSubmitEditing={handleValidateUser}
editable={!isValidating}
keyboardType="default"
error={error}
/>
{/* Validate Button */}
<Button
title={isValidating ? 'Setting User...' : buttonText}
onPress={handleValidateUser}
loading={isValidating}
disabled={!isFormValid()}
/>
{/* Help Text */}
<View style={styles.helpContainer}>
<Text style={styles.helpText}>
Enter your username to set the user for the SDK session.
</Text>
</View>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#f8f9fa',
},
container: {
flex: 1,
},
content: {
flex: 1,
padding: 20,
paddingTop: 60, // Add space for close button
justifyContent: 'center',
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#2c3e50',
textAlign: 'center',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: '#7f8c8d',
textAlign: 'center',
marginBottom: 20,
},
resultContainer: {
borderRadius: 8,
padding: 16,
marginBottom: 20,
borderLeftWidth: 4,
},
successContainer: {
backgroundColor: '#f0f8f0',
borderLeftColor: '#27ae60',
},
errorContainer: {
backgroundColor: '#fff0f0',
borderLeftColor: '#e74c3c',
},
resultText: {
fontSize: 14,
fontWeight: '500',
textAlign: 'center',
},
successText: {
color: '#27ae60',
},
errorText: {
color: '#e74c3c',
},
inputContainer: {
marginBottom: 24,
},
inputLabel: {
fontSize: 16,
fontWeight: 'bold',
color: '#2c3e50',
marginBottom: 8,
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 16,
fontSize: 16,
backgroundColor: '#fff',
color: '#2c3e50',
},
inputError: {
borderColor: '#e74c3c',
},
validateButton: {
backgroundColor: '#3498db',
borderRadius: 8,
padding: 16,
alignItems: 'center',
marginBottom: 12,
},
validateButtonDisabled: {
backgroundColor: '#bdc3c7',
},
validateButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
marginLeft: 8,
},
loadingContainer: {
flexDirection: 'row',
alignItems: 'center',
},
helpContainer: {
marginTop: 20,
padding: 16,
backgroundColor: '#ecf0f1',
borderRadius: 8,
},
helpText: {
fontSize: 14,
color: '#7f8c8d',
textAlign: 'center',
lineHeight: 20,
},
});
export default CheckUserScreen;
Status Code | Event Name | Meaning |
101 |
| Triggered when an invalid user is provided in setuser API. |
138 |
| User is blocked due to exceeded OTP attempts or blocked by admin or previously blocked in other flow |
The CheckUserScreen
demonstrates several important patterns:
getUser
events if validation failsRDNAEventUtils
for consistent error checkingThe following image showcases screen from the sample application:
Create the activation code input screen that handles OTP verification during the activation flow.
The ActivationCodeScreen
handles OTP input and provides resend functionality:
// Key features from src/tutorial/screens/mfa/ActivationCodeScreen.tsx
const ActivationCodeScreen: React.FC = () => {
const route = useRoute<ActivationCodeScreenRouteProp>();
const {
eventData,
title = 'Enter Activation Code',
subtitle = 'Enter the activation code to continue',
placeholder = 'Enter activation code',
buttonText = 'Verify Code',
attemptsLeft = 0,
responseData,
} = route.params;
const [activationCode, setActivationCode] = useState<string>('');
const [error, setError] = useState<string>('');
const [isValidating, setIsValidating] = useState<boolean>(false);
const [isResending, setIsResending] = useState<boolean>(false);
const [validationResult, setValidationResult] = useState<{
success: boolean;
message: string;
} | null>(null);
/**
* Handle close button - direct resetAuthState call
*/
const handleClose = async () => {
try {
console.log('ActivationCodeScreen - Calling resetAuthState');
await rdnaService.resetAuthState();
console.log('ActivationCodeScreen - ResetAuthState successful');
} catch (error) {
console.error('ActivationCodeScreen - ResetAuthState error:', error);
}
};
/**
* Handle activation code validation via setActivationCode API
*/
const handleValidateActivationCode = async () => {
const trimmedActivationCode = activationCode.trim();
setIsValidating(true);
setError('');
setValidationResult(null);
try {
console.log('ActivationCodeScreen - Setting activation code:', trimmedActivationCode);
const syncResponse: RDNASyncResponse = await rdnaService.setActivationCode(trimmedActivationCode);
console.log('ActivationCodeScreen - SetActivationCode sync response successful, waiting for async events');
setValidationResult({
success: true,
message: 'Activation code set successfully! Waiting for next step...'
});
} catch (error) {
console.error('ActivationCodeScreen - SetActivationCode sync error:', error);
const result: RDNASyncResponse = error as RDNASyncResponse;
const errorMessage = RDNASyncUtils.getErrorMessage(result);
setError(errorMessage);
} finally {
setIsValidating(false);
}
};
/**
* Handle resend activation code functionality
*/
const handleResendActivationCode = async () => {
if (isResending || isValidating) return;
setIsResending(true);
setError('');
setValidationResult(null);
try {
console.log('ActivationCodeScreen - Requesting resend of activation code');
await rdnaService.resendActivationCode();
console.log('ActivationCodeScreen - ResendActivationCode sync response successful, waiting for new getActivationCode event');
setValidationResult({
success: true,
message: 'New activation code sent! Please check your email or SMS.'
});
// Clear the current activation code input
setActivationCode('');
} catch (error) {
console.error('ActivationCodeScreen - ResendActivationCode sync error:', error);
const result: RDNASyncResponse = error as RDNASyncResponse;
const errorMessage = RDNASyncUtils.getErrorMessage(result);
setError(errorMessage);
} finally {
setIsResending(false);
}
};
return (
<SafeAreaView style={styles.safeArea}>
<StatusBar barStyle="dark-content" backgroundColor="#f8f9fa" />
{/* Close Button */}
<CloseButton
onPress={handleClose}
disabled={isValidating || isResending}
/>
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<View style={styles.content}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.subtitle}>{subtitle}</Text>
{/* Attempts Left Display */}
{attemptsLeft > 0 && (
<StatusBanner
type="warning"
message={`Attempts remaining: ${attemptsLeft}`}
/>
)}
{/* Validation Result */}
{validationResult && (
<StatusBanner
type={validationResult.success ? 'success' : 'error'}
message={validationResult.message}
/>
)}
{/* Activation Code Input */}
<Input
label="Activation Code"
value={activationCode}
onChangeText={handleActivationCodeChange}
placeholder={placeholder}
returnKeyType="done"
onSubmitEditing={handleValidateActivationCode}
editable={!isValidating}
keyboardType="default"
error={error}
/>
{/* Validate Button */}
<Button
title={isValidating ? 'Setting Activation Code...' : buttonText}
onPress={handleValidateActivationCode}
loading={isValidating}
disabled={!isFormValid() || isResending}
/>
{/* Resend Button */}
<Button
title={isResending ? 'Sending...' : 'Resend Activation Code'}
onPress={handleResendActivationCode}
loading={isResending}
disabled={isValidating}
variant="secondary"
/>
{/* Help Text */}
<View style={styles.helpContainer}>
<Text style={styles.helpText}>
Enter the activation code you received to verify your identity. If you haven't received it, click "Resend Activation Code" to get a new one.
</Text>
</View>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
// Add comprehensive styling
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#f8f9fa',
},
container: {
flex: 1,
},
content: {
flex: 1,
padding: 20,
paddingTop: 60, // Add space for close button
justifyContent: 'center',
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#2c3e50',
textAlign: 'center',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: '#7f8c8d',
textAlign: 'center',
marginBottom: 20,
},
attemptsContainer: {
backgroundColor: '#fff3cd',
borderRadius: 8,
padding: 12,
marginBottom: 20,
borderLeftWidth: 4,
borderLeftColor: '#ffc107',
},
attemptsText: {
fontSize: 14,
color: '#856404',
fontWeight: '500',
textAlign: 'center',
},
resultContainer: {
borderRadius: 8,
padding: 16,
marginBottom: 20,
borderLeftWidth: 4,
},
successContainer: {
backgroundColor: '#f0f8f0',
borderLeftColor: '#27ae60',
},
errorContainer: {
backgroundColor: '#fff0f0',
borderLeftColor: '#e74c3c',
},
resultText: {
fontSize: 14,
fontWeight: '500',
textAlign: 'center',
},
successText: {
color: '#27ae60',
},
errorText: {
color: '#e74c3c',
},
inputContainer: {
marginBottom: 24,
},
inputLabel: {
fontSize: 16,
fontWeight: 'bold',
color: '#2c3e50',
marginBottom: 8,
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 16,
fontSize: 16,
backgroundColor: '#fff',
color: '#2c3e50',
},
inputError: {
borderColor: '#e74c3c',
},
validateButton: {
backgroundColor: '#3498db',
borderRadius: 8,
padding: 16,
alignItems: 'center',
marginBottom: 12,
},
validateButtonDisabled: {
backgroundColor: '#bdc3c7',
},
validateButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
marginLeft: 8,
},
resendButton: {
backgroundColor: 'transparent',
borderWidth: 2,
borderColor: '#3498db',
borderRadius: 8,
padding: 16,
alignItems: 'center',
marginBottom: 12,
},
resendButtonDisabled: {
borderColor: '#bdc3c7',
},
resendButtonText: {
color: '#3498db',
fontSize: 16,
fontWeight: 'bold',
marginLeft: 8,
},
loadingContainer: {
flexDirection: 'row',
alignItems: 'center',
},
helpContainer: {
marginTop: 20,
padding: 16,
backgroundColor: '#ecf0f1',
borderRadius: 8,
},
helpText: {
fontSize: 14,
color: '#7f8c8d',
textAlign: 'center',
lineHeight: 20,
},
});
Status Code | Event Name | Meaning |
106 |
| Triggered when an invalid otp is provided in setActivationCode API. |
getActivationCode
event dataThe following image showcases screen from the sample application:
This section outlines the decision-making flow for Local Device Authentication (LDA) in the plugin. It guides how the plugin handles user input for biometric or password-based setup during activation flow.
Plugin checks if LDA is available:
getUserConsentForLDA Event
β
User Decision:
ββ Allow Biometric β setUserConsentForLDA(true, challengeMode, authType) β SDK handles biometric β onUserLoggedIn
ββ Use Password β setUserConsentForLDA(false, challengeMode, authType) β getPassword Event β Password Screen
β
setPassword(password, challengeMode) β onUserLoggedIn
Alternative Flow (No LDA):
getPassword Event β Password Screen β setPassword(password, challengeMode) β onUserLoggedIn
After OTP verification, users need to set up device authentication. The SDK will trigger either getUserConsentForLDA
(biometric) or getPassword
(password) events. Create the final authentication screens that handle either biometric consent or password input, depending on device capabilities.
The UserLDAConsentScreen
handles biometric authentication consent:
// src/tutorial/screens/mfa/UserLDAConsentScreen.tsx
import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
StatusBar,
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 { RDNASyncResponse } from '../../../uniken/types/rdnaEvents';
import rdnaService from '../../../uniken/services/rdnaService';
import { CloseButton, Button, StatusBanner } from '../components';
import type { RootStackParamList } from '../../navigation/AppNavigator';
type UserLDAConsentScreenRouteProp = RouteProp<RootStackParamList, 'UserLDAConsentScreen'>;
const UserLDAConsentScreen: React.FC = () => {
const route = useRoute<UserLDAConsentScreenRouteProp>();
const {
eventData,
title = 'Biometric Authentication',
subtitle = 'Allow biometric authentication for faster future logins?',
responseData,
} = route.params;
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const [error, setError] = useState<string>('');
const [validationResult, setValidationResult] = useState<{
success: boolean;
message: string;
} | null>(null);
/**
* Handle close button - reset authentication state
*/
const handleClose = async () => {
try {
console.log('UserLDAConsentScreen - Calling resetAuthState');
await rdnaService.resetAuthState();
console.log('UserLDAConsentScreen - ResetAuthState successful');
} catch (error) {
console.error('UserLDAConsentScreen - ResetAuthState error:', error);
}
};
/**
* Handle response data processing
*/
useEffect(() => {
if (responseData) {
console.log('UserLDAConsentScreen - Processing response data:', responseData);
if (RDNAEventUtils.hasApiError(responseData)) {
const errorMessage = RDNAEventUtils.getErrorMessage(responseData);
setError(errorMessage);
setValidationResult({
success: false,
message: errorMessage
});
return;
}
if (RDNAEventUtils.hasStatusError(responseData)) {
const errorMessage = RDNAEventUtils.getErrorMessage(responseData);
setError(errorMessage);
setValidationResult({
success: false,
message: errorMessage
});
return;
}
setValidationResult({
success: true,
message: 'Ready to choose authentication method'
});
}
}, [responseData]);
/**
* Handle user consent for biometric authentication
* Uses challengeMode and authenticationType from eventData
*/
const handleUserConsent = async (consent: boolean) => {
if (!eventData) {
setError('Event data not available');
return;
}
setIsProcessing(true);
setError('');
setValidationResult(null);
try {
console.log('UserLDAConsentScreen - Setting user consent for LDA:', consent);
console.log('UserLDAConsentScreen - Challenge mode:', eventData.challengeMode);
console.log('UserLDAConsentScreen - Authentication type:', eventData.authenticationType);
const syncResponse: RDNASyncResponse = await rdnaService.setUserConsentForLDA(
consent,
eventData.challengeMode,
eventData.authenticationType
);
console.log('UserLDAConsentScreen - SetUserConsentForLDA sync response successful');
if (consent) {
// Positive consent - SDK will handle biometric prompt
console.log('UserLDAConsentScreen - User granted consent, waiting for biometric authentication');
setValidationResult({
success: true,
message: 'Biometric authentication enabled. SDK will handle biometric prompt...'
});
} else {
// Negative consent - will trigger getPassword event
console.log('UserLDAConsentScreen - User declined consent, waiting for password challenge');
setValidationResult({
success: true,
message: 'Password authentication selected. Please wait for password prompt...'
});
}
} catch (error) {
console.error('UserLDAConsentScreen - SetUserConsentForLDA sync error:', error);
const result: RDNASyncResponse = error as RDNASyncResponse;
const errorMessage = RDNASyncUtils.getErrorMessage(result);
setError(errorMessage);
} finally {
setIsProcessing(false);
}
};
const handleAllowBiometric = () => handleUserConsent(true);
const handleUsePassword = () => handleUserConsent(false);
return (
<SafeAreaView style={styles.safeArea}>
<StatusBar barStyle="dark-content" backgroundColor="#f8f9fa" />
{/* Close Button */}
<CloseButton
onPress={handleClose}
disabled={isProcessing}
/>
<View style={styles.container}>
<View style={styles.content}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.subtitle}>{subtitle}</Text>
<View style={styles.iconContainer}>
<Text style={styles.biometricIcon}>π</Text>
</View>
{/* Validation Result */}
{validationResult && (
<StatusBanner
type={validationResult.success ? 'success' : 'error'}
message={validationResult.message}
/>
)}
{error && (
<StatusBanner
type="error"
message={error}
/>
)}
<View style={styles.buttonContainer}>
{/* Allow Biometric Button */}
<Button
title={isProcessing ? 'Processing...' : 'Allow Biometric Authentication'}
onPress={handleAllowBiometric}
loading={isProcessing}
variant="success"
style={styles.button}
/>
{/* Use Password Button */}
<Button
title="Use Password Instead"
onPress={handleUsePassword}
disabled={isProcessing}
variant="secondary"
style={styles.button}
/>
</View>
{/* Information Text */}
<View style={styles.infoContainer}>
<Text style={styles.infoText}>
Biometric authentication provides a faster and more secure way to access your account.
You can always use your password as an alternative.
</Text>
</View>
</View>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#f8f9fa',
},
container: {
flex: 1,
},
content: {
flex: 1,
padding: 20,
paddingTop: 60, // Add space for close button
justifyContent: 'center',
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#2c3e50',
textAlign: 'center',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: '#7f8c8d',
textAlign: 'center',
marginBottom: 30,
},
iconContainer: {
alignItems: 'center',
marginBottom: 30,
},
biometricIcon: {
fontSize: 64,
marginBottom: 20,
},
resultContainer: {
borderRadius: 8,
padding: 16,
marginBottom: 20,
borderLeftWidth: 4,
},
successContainer: {
backgroundColor: '#f0f8f0',
borderLeftColor: '#27ae60',
},
errorContainer: {
backgroundColor: '#fff0f0',
borderLeftColor: '#e74c3c',
borderRadius: 8,
padding: 16,
marginBottom: 20,
},
resultText: {
fontSize: 14,
fontWeight: '500',
textAlign: 'center',
},
successText: {
color: '#27ae60',
},
errorText: {
color: '#e74c3c',
fontSize: 14,
textAlign: 'center',
},
buttonContainer: {
marginTop: 20,
},
primaryButton: {
backgroundColor: '#3498db',
borderRadius: 8,
padding: 16,
alignItems: 'center',
marginBottom: 12,
},
secondaryButton: {
backgroundColor: 'transparent',
borderWidth: 2,
borderColor: '#3498db',
borderRadius: 8,
padding: 16,
alignItems: 'center',
marginBottom: 12,
},
buttonDisabled: {
opacity: 0.6,
},
primaryButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
marginLeft: 8,
},
secondaryButtonText: {
color: '#3498db',
fontSize: 16,
fontWeight: 'bold',
},
loadingContainer: {
flexDirection: 'row',
alignItems: 'center',
},
infoContainer: {
marginTop: 20,
padding: 16,
backgroundColor: '#ecf0f1',
borderRadius: 8,
},
infoText: {
fontSize: 14,
color: '#7f8c8d',
textAlign: 'center',
lineHeight: 20,
},
});
export default UserLDAConsentScreen;
The following image showcases screen from the sample application:
The Set Password screen handles password creation during the MFA enrollment flow. This screen processes getPassword
events from the SDK and includes dynamic password policy parsing for enhanced user experience.
RELID_PASSWORD_POLICY
from challenge dataCreate the password setup screen at src/tutorial/screens/mfa/SetPasswordScreen.tsx
:
import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
Alert,
StyleSheet,
SafeAreaView,
KeyboardAvoidingView,
Platform
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { rdnaService } from '../../../uniken/services/rdnaService';
import { rdnaEventManager } from '../../../uniken/services/rdnaEventManager';
import { Button, Input, StatusBanner } from '../components';
interface GetPasswordChallenge {
challengeId: string;
challengeMode: number;
challenge: string;
message?: string;
}
const SetPasswordScreen: React.FC = () => {
const navigation = useNavigation();
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [currentChallenge, setCurrentChallenge] = useState<GetPasswordChallenge | null>(null);
const [showPassword, setShowPassword] = useState(false);
// Handle getPassword events
const handleGetPassword = useCallback((challenge: GetPasswordChallenge) => {
console.log('π± SetPasswordScreen: Received getPassword challenge:', challenge);
setCurrentChallenge(challenge);
setIsLoading(false);
}, []);
// Handle successful login after password setup
const handleUserLoggedIn = useCallback((sessionData: any) => {
console.log('β
SetPasswordScreen: User logged in successfully');
setIsLoading(false);
navigation.navigate('DashboardScreen' as never);
}, [navigation]);
// Setup event handlers
useEffect(() => {
const eventCallbacks = {
onGetPassword: handleGetPassword,
onUserLoggedIn: handleUserLoggedIn,
};
rdnaEventManager.setCallbacks(eventCallbacks);
// Cleanup
return () => {
rdnaEventManager.onGetPassword = undefined;
rdnaEventManager.onUserLoggedIn = undefined;
};
}, [handleGetPassword, handleUserLoggedIn]);
// Validate password strength
const validatePassword = (pwd: string) => {
const minLength = 8;
const hasUpperCase = /[A-Z]/.test(pwd);
const hasLowerCase = /[a-z]/.test(pwd);
const hasNumbers = /\d/.test(pwd);
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(pwd);
if (pwd.length < minLength) {
return `Password must be at least ${minLength} characters long`;
}
if (!hasUpperCase) {
return 'Password must contain at least one uppercase letter';
}
if (!hasLowerCase) {
return 'Password must contain at least one lowercase letter';
}
if (!hasNumbers) {
return 'Password must contain at least one number';
}
if (!hasSpecialChar) {
return 'Password must contain at least one special character';
}
return null;
};
// Handle password submission
const handleSubmitPassword = async () => {
if (!password) {
setError('Please enter a password');
return;
}
if (!confirmPassword) {
setError('Please confirm your password');
return;
}
if (password !== confirmPassword) {
setError('Passwords do not match');
return;
}
const passwordValidation = validatePassword(password);
if (passwordValidation) {
setError(passwordValidation);
return;
}
if (!currentChallenge) {
setError('No active challenge. Please restart the flow.');
return;
}
setIsLoading(true);
setError('');
try {
// Call setPassword API
const response = await rdnaService.setPassword(password, currentChallenge.challengeMode);
// Check synchronous response
if (response.data.resultCode !== 'RESULT_SUCCESS') {
setIsLoading(false);
setError(response.data.resultDescription || 'Failed to set password');
return;
}
// Success - wait for next SDK event
console.log('β
setPassword API success, waiting for next event...');
} catch (error: any) {
setIsLoading(false);
console.error('β Error setting password:', error);
setError(error.message || 'Network error occurred');
}
};
const getPasswordStrength = (pwd: string) => {
let score = 0;
if (pwd.length >= 8) score++;
if (/[A-Z]/.test(pwd)) score++;
if (/[a-z]/.test(pwd)) score++;
if (/\d/.test(pwd)) score++;
if (/[!@#$%^&*(),.?":{}|<>]/.test(pwd)) score++;
if (score < 2) return { strength: 'Weak', color: '#FF4444' };
if (score < 4) return { strength: 'Medium', color: '#FFA500' };
return { strength: 'Strong', color: '#4CAF50' };
};
const passwordStrength = password ? getPasswordStrength(password) : null;
return (
<SafeAreaView style={styles.container}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardView}
>
<View style={styles.content}>
<View style={styles.header}>
<Text style={styles.title}>Set Device Password</Text>
<Text style={styles.subtitle}>
Create a strong password to secure your account on this device.
</Text>
</View>
<View style={styles.form}>
<View style={styles.inputGroup}>
<Input
label="Password"
placeholder="Enter your password"
value={password}
onChangeText={(text) => {
setPassword(text);
if (error) setError('');
}}
secureTextEntry={!showPassword}
autoCapitalize="none"
autoCorrect={false}
editable={!isLoading}
/>
<TouchableOpacity
style={styles.showPasswordButton}
onPress={() => setShowPassword(!showPassword)}
>
<Text style={styles.showPasswordText}>
{showPassword ? 'Hide' : 'Show'}
</Text>
</TouchableOpacity>
</View>
{passwordStrength && (
<View style={styles.strengthContainer}>
<Text style={[styles.strengthText, { color: passwordStrength.color }]}>
Strength: {passwordStrength.strength}
</Text>
</View>
)}
</View>
<Input
label="Confirm Password"
placeholder="Confirm your password"
value={confirmPassword}
onChangeText={(text) => {
setConfirmPassword(text);
if (error) setError('');
}}
secureTextEntry={!showPassword}
editable={!isLoading}
error={error}
/>
</View>
<View style={styles.requirements}>
<Text style={styles.requirementsTitle}>Password Requirements:</Text>
<Text style={styles.requirementItem}>β’ At least 8 characters long</Text>
<Text style={styles.requirementItem}>β’ One uppercase letter (A-Z)</Text>
<Text style={styles.requirementItem}>β’ One lowercase letter (a-z)</Text>
<Text style={styles.requirementItem}>β’ One number (0-9)</Text>
<Text style={styles.requirementItem}>β’ One special character (!@#$%^&*)</Text>
</View>
<Button
title={isLoading ? 'Setting Password...' : 'Set Password'}
onPress={handleSubmitPassword}
loading={isLoading}
disabled={!password || !confirmPassword}
/>
{/* Debug Information (remove in production) */}
{__DEV__ && currentChallenge && (
<View style={styles.debugInfo}>
<Text style={styles.debugTitle}>Debug Info:</Text>
<Text style={styles.debugText}>Challenge ID: {currentChallenge.challengeId}</Text>
<Text style={styles.debugText}>Challenge Mode: {currentChallenge.challengeMode}</Text>
</View>
)}
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F5F5',
},
keyboardView: {
flex: 1,
},
content: {
flex: 1,
padding: 20,
justifyContent: 'center',
},
header: {
marginBottom: 32,
alignItems: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: '#666',
textAlign: 'center',
lineHeight: 22,
},
form: {
marginBottom: 24,
},
inputGroup: {
marginBottom: 20,
},
label: {
fontSize: 16,
fontWeight: '600',
color: '#333',
marginBottom: 8,
},
passwordContainer: {
flexDirection: 'row',
alignItems: 'center',
},
input: {
flex: 1,
borderWidth: 1,
borderColor: '#DDD',
borderRadius: 8,
padding: 16,
fontSize: 16,
backgroundColor: '#FFF',
},
inputError: {
borderColor: '#FF4444',
},
showPasswordButton: {
position: 'absolute',
right: 16,
paddingVertical: 4,
paddingHorizontal: 8,
},
showPasswordText: {
color: '#0066CC',
fontSize: 14,
fontWeight: '600',
},
strengthContainer: {
marginTop: 8,
},
strengthText: {
fontSize: 14,
fontWeight: '600',
},
errorText: {
color: '#FF4444',
fontSize: 14,
marginTop: 8,
},
requirements: {
backgroundColor: '#F8F9FA',
padding: 16,
borderRadius: 8,
marginBottom: 24,
},
requirementsTitle: {
fontSize: 14,
fontWeight: '600',
color: '#333',
marginBottom: 8,
},
requirementItem: {
fontSize: 12,
color: '#666',
marginBottom: 4,
},
button: {
backgroundColor: '#0066CC',
padding: 16,
borderRadius: 8,
alignItems: 'center',
},
buttonDisabled: {
backgroundColor: '#CCC',
},
buttonText: {
color: '#FFF',
fontSize: 16,
fontWeight: '600',
},
debugInfo: {
marginTop: 20,
padding: 12,
backgroundColor: '#F0F0F0',
borderRadius: 8,
},
debugTitle: {
fontWeight: 'bold',
marginBottom: 4,
},
debugText: {
fontSize: 12,
color: '#666',
},
});
export default SetPasswordScreen;
The screen includes dynamic password policy parsing functionality through a dedicated utility system:
1. Password Policy Utilities (
src/uniken/utils/passwordPolicyUtils.ts
)
export interface PasswordPolicy {
minL: number; // minimum length
maxL: number; // maximum length
minDg: number; // minimum digits
minUc: number; // minimum uppercase letters
minLc: number; // minimum lowercase letters
minSc: number; // minimum special characters
charsNotAllowed: string; // characters that are not allowed
Repetition: number; // max allowed repeated characters
UserIDcheck: boolean; // whether User ID should not be included
SeqCheck: string; // disallow sequential characters
BlackListedCommonPassword: string; // if it should not be a common password
msg?: string; // optional message from server
SDKValidation?: boolean; // SDK validation flag
}
export function parseAndGeneratePolicyMessage(policyJsonString: string): string {
try {
const policy: PasswordPolicy = JSON.parse(policyJsonString);
return generatePasswordPolicyMessage(policy);
} catch (error) {
console.error('Failed to parse password policy JSON:', error);
return 'Please enter a secure password according to your organization\'s policy';
}
}
2. Policy Extraction Process
The screen uses RDNAEventUtils.getChallengeValue()
to extract the RELID_PASSWORD_POLICY
key from the challenge data:
// Extract and process password policy from challengeInfo
const policyJsonString = RDNAEventUtils.getChallengeValue(responseData, 'RELID_PASSWORD_POLICY');
if (policyJsonString) {
const policyMessage = parseAndGeneratePolicyMessage(policyJsonString);
setPasswordPolicyMessage(policyMessage);
}
3. Dynamic Policy Display
The policy message is displayed in a dedicated UI container that shows either:
msg
field (if valid and not "Invalid password policy")4. Integration Benefits
This implementation ensures that users always receive clear, actionable password requirements directly from your organization's security policy configuration.
Status Code | Event Name | Meaning |
190 |
| Triggered when the provided password does not meet the password policy requirements in the setPassword API. |
164 |
| Please enter a new password. The password you entered using setPassword API has been used previously. You are not allowed to reuse any of your last 5 passwords. |
The following image showcases screen from the sample application:
The Dashboard screen serves as the primary landing destination after successful activation or login completion. When the plugin triggers the onUserLoggedIn
event, it indicates that the user session has started and provides session details including userID and session tokens. This event should trigger navigation to the Dashboard screen, marking the successful completion of either the activation or login flow.
Complete MFA systems need secure logout functionality with proper session cleanup.
The logout flow follows this sequence:
logOff()
API to clean up sessiononUserLoggedOff
eventgetUser
event (flow restarts)The logOff
API was already added in the previous service layer section. Here's the implementation again for reference:
// In rdnaService.ts - already implemented above
async logOff(): Promise<RDNASyncResponse> {
console.log('π [API] Logging off user');
try {
const result = await rdnaClient.logOff();
console.log('β
[API] logOff response:', result);
return result;
} catch (error) {
console.error('β [API] logOff error:', error);
throw error;
}
}
Create src/tutorial/screens/tutorial/DashboardScreen.tsx
to demonstrate logout:
import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
TouchableOpacity,
Alert,
ActivityIndicator,
StyleSheet,
SafeAreaView,
ScrollView
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { rdnaService } from '../../../uniken/services/rdnaService';
import { rdnaEventManager } from '../../../uniken/services/rdnaEventManager';
const DashboardScreen: React.FC = () => {
const navigation = useNavigation();
const [isLoggingOut, setIsLoggingOut] = useState(false);
const [userInfo, setUserInfo] = useState<any>(null);
// Handle onUserLoggedOff event
const handleUserLoggedOff = useCallback(() => {
console.log('π DashboardScreen: User logged off event received');
setIsLoggingOut(false);
// Navigate back to initial screen
navigation.reset({
index: 0,
routes: [{ name: 'InitialScreen' as never }],
});
}, [navigation]);
// Handle getUser event (triggered after logout)
const handleGetUser = useCallback((challenge: any) => {
console.log('π DashboardScreen: getUser event after logout, navigating to CheckUserScreen');
navigation.navigate('CheckUserScreen' as never);
}, [navigation]);
// Setup event handlers
useEffect(() => {
const eventCallbacks = {
onUserLoggedOff: handleUserLoggedOff,
onGetUser: handleGetUser,
};
rdnaEventManager.setCallbacks(eventCallbacks);
// Cleanup
return () => {
rdnaEventManager.onUserLoggedOff = undefined;
rdnaEventManager.onGetUser = undefined;
};
}, [handleUserLoggedOff, handleGetUser]);
// Handle logout button press
const handleLogout = () => {
Alert.alert(
'Confirm Logout',
'Are you sure you want to log out?',
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Logout',
style: 'destructive',
onPress: performLogout,
},
]
);
};
// Perform actual logout
const performLogout = async () => {
setIsLoggingOut(true);
try {
// Call logOff API
const response = await rdnaService.logOff();
if (response.data.resultCode !== 'RESULT_SUCCESS') {
setIsLoggingOut(false);
Alert.alert(
'Logout Error',
response.data.resultDescription || 'Failed to logout properly',
[{ text: 'OK' }]
);
return;
}
// Success - wait for onUserLoggedOff event
console.log('β
logOff API success, waiting for onUserLoggedOff event...');
} catch (error: any) {
setIsLoggingOut(false);
console.error('β Error during logout:', error);
Alert.alert(
'Logout Error',
'Failed to logout. Please try again.',
[{ text: 'OK' }]
);
}
};
return (
<SafeAreaView style={styles.container}>
<ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContent}>
<View style={styles.header}>
<Text style={styles.title}>Welcome to Your Dashboard</Text>
<Text style={styles.subtitle}>You have successfully completed MFA activation!</Text>
</View>
<View style={styles.statusCard}>
<Text style={styles.statusTitle}>β
MFA Status</Text>
<Text style={styles.statusText}>Your account is fully activated and secured with multi-factor authentication.</Text>
</View>
<View style={styles.featuresCard}>
<Text style={styles.featuresTitle}>π What You've Accomplished</Text>
<View style={styles.featuresList}>
<Text style={styles.featureItem}>β User identity verified</Text>
<Text style={styles.featureItem}>β Activation code confirmed</Text>
<Text style={styles.featureItem}>β Device authentication configured</Text>
<Text style={styles.featureItem}>β Secure session established</Text>
</View>
</View>
<View style={styles.nextStepsCard}>
<Text style={styles.nextStepsTitle}>π Next Steps</Text>
<Text style={styles.nextStepsText}>
From now on, when you log in again, you'll experience the streamlined login flow with biometric authentication or password verification.
</Text>
</View>
<View style={styles.actions}>
<TouchableOpacity
style={[styles.logoutButton, isLoggingOut && styles.buttonDisabled]}
onPress={handleLogout}
disabled={isLoggingOut}
>
{isLoggingOut ? (
<ActivityIndicator color="#FFFFFF" />
) : (
<Text style={styles.logoutButtonText}>Logout</Text>
)}
</TouchableOpacity>
</View>
{/* Debug Information (remove in production) */}
{__DEV__ && (
<View style={styles.debugInfo}>
<Text style={styles.debugTitle}>Debug Info:</Text>
<Text style={styles.debugText}>Screen: DashboardScreen</Text>
<Text style={styles.debugText}>Logout Status: {isLoggingOut ? 'In Progress' : 'Ready'}</Text>
<Text style={styles.debugText}>Time: {new Date().toLocaleTimeString()}</Text>
</View>
)}
</ScrollView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F5F5',
},
scrollView: {
flex: 1,
},
scrollContent: {
padding: 20,
},
header: {
alignItems: 'center',
marginBottom: 32,
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#333',
marginBottom: 8,
textAlign: 'center',
},
subtitle: {
fontSize: 16,
color: '#666',
textAlign: 'center',
lineHeight: 22,
},
statusCard: {
backgroundColor: '#E8F5E8',
padding: 20,
borderRadius: 12,
marginBottom: 20,
borderLeftWidth: 4,
borderLeftColor: '#4CAF50',
},
statusTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#2E7D32',
marginBottom: 8,
},
statusText: {
fontSize: 14,
color: '#2E7D32',
lineHeight: 20,
},
featuresCard: {
backgroundColor: '#FFF',
padding: 20,
borderRadius: 12,
marginBottom: 20,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
featuresTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#333',
marginBottom: 16,
},
featuresList: {
gap: 8,
},
featureItem: {
fontSize: 14,
color: '#4CAF50',
lineHeight: 20,
},
nextStepsCard: {
backgroundColor: '#E3F2FD',
padding: 20,
borderRadius: 12,
marginBottom: 32,
borderLeftWidth: 4,
borderLeftColor: '#2196F3',
},
nextStepsTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#1976D2',
marginBottom: 8,
},
nextStepsText: {
fontSize: 14,
color: '#1976D2',
lineHeight: 20,
},
actions: {
alignItems: 'center',
},
logoutButton: {
backgroundColor: '#FF4444',
paddingHorizontal: 32,
paddingVertical: 16,
borderRadius: 8,
minWidth: 120,
alignItems: 'center',
},
logoutButtonText: {
color: '#FFF',
fontSize: 16,
fontWeight: '600',
},
buttonDisabled: {
backgroundColor: '#CCC',
},
debugInfo: {
marginTop: 32,
padding: 12,
backgroundColor: '#F0F0F0',
borderRadius: 8,
},
debugTitle: {
fontWeight: 'bold',
marginBottom: 4,
},
debugText: {
fontSize: 12,
color: '#666',
},
});
export default DashboardScreen;
User clicks "Logout" button
β
Confirmation dialog
β
logOff() API call
β
Check sync response
β
onUserLoggedOff event triggered
β
Navigate to Initial Screen
β
getUser event automatically triggered
β
Navigate to CheckUserScreen (flow restarts)
onUserLoggedOff
event for navigationgetUser
after logoutThe logout functionality has been fully implemented in the codebase:
logOff()
API added to rdnaService.ts:373
onUserLoggedOff
handler implemented in rdnaEventManager.ts
DashboardScreen.tsx
DrawerContent.tsx
The following images showcase screens from the sample application:
|
|
π What We're Building: Streamlined authentication for returning users with biometric prompts and password verification.
Key Differences:
Aspect | Activation Flow | Login Flow |
User Type | First-time users | Returning users |
OTP Required | Always required | Usually not required |
Biometric Setup | User chooses to enable | Automatic prompt if enabled |
Password Setup | Creates new password | Verifies existing password |
Navigation | Multiple screens | Fewer screens |
Login Flow Triggers When:
Flow Detection: The same plgin events (getUser
, getPassword
) are used for both flows. The difference is in:
SDK Initialization Complete
β
getUser Event (Challenge: checkuser)
β
setUser API Call β User Recognition
β
[SDK Decision - Skip OTP for known users]
β
Device Authentication:
ββ LDA Enabled? β [Automatic Biometric Prompt]
β ββ Success β onUserLoggedIn Event
β ββ Failed/Cancelled β getUser Event with error
β
ββ Password Only? β getPassword Event (verification mode)
β
setPassword API Call
β
onUserLoggedIn Event (Success) β Dashboard Screen
Same screen will be used which in activation flow.
Status Code | Event Name | Meaning |
141 |
| Triggered when an user is blocked due to exceeded verify password attempts. This means the user can be unblocked using the resetBlockedUserAccount API. The implementation of this API can be found in the codelab. |
The Verify Password screen handles password verification during the login flow when users need to enter their existing password. This screen is triggered when challengeMode = 0
in the getPassword
event, indicating password verification rather than password creation.
Create the password verification screen at src/tutorial/screens/mfa/VerifyPasswordScreen.tsx
:
import React, { useState, useRef, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
StatusBar,
ScrollView,
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;
const [password, setPassword] = useState<string>('');
const [error, setError] = useState<string>('');
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const passwordRef = useRef<TextInput>(null);
// Handle close button - direct resetAuthState call
const handleClose = async () => {
try {
console.log('VerifyPasswordScreen - Calling resetAuthState');
await rdnaService.resetAuthState();
console.log('VerifyPasswordScreen - ResetAuthState successful');
} catch (error) {
console.error('VerifyPasswordScreen - ResetAuthState error:', error);
}
};
// Process response data for error handling
useEffect(() => {
if (responseData) {
console.log('VerifyPasswordScreen - Processing response data for errors:', responseData);
// Check for API errors first
if (RDNAEventUtils.hasApiError(responseData)) {
const errorMessage = RDNAEventUtils.getErrorMessage(responseData);
console.log('VerifyPasswordScreen - API error:', errorMessage);
setError(errorMessage);
return;
}
// Check for status errors
if (RDNAEventUtils.hasStatusError(responseData)) {
const errorMessage = RDNAEventUtils.getErrorMessage(responseData);
console.log('VerifyPasswordScreen - Status error:', errorMessage);
setError(errorMessage);
return;
}
// Success - clear any previous errors
setError('');
console.log('VerifyPasswordScreen - Successfully processed response data');
}
}, [responseData]);
// Handle password input change
const handlePasswordChange = (text: string) => {
setPassword(text);
if (error) {
setError('');
}
};
// Reset form input
const resetInput = () => {
setPassword('');
if (passwordRef.current) {
passwordRef.current.focus();
}
};
// Handle password verification
const handleVerifyPassword = async () => {
if (isSubmitting) return;
const trimmedPassword = password.trim();
// Basic validation
if (!trimmedPassword) {
setError('Please enter your password');
if (passwordRef.current) {
passwordRef.current.focus();
}
return;
}
setIsSubmitting(true);
setError('');
try {
console.log('VerifyPasswordScreen - Verifying password with challengeMode:', challengeMode);
const syncResponse: RDNASyncResponse = await rdnaService.setPassword(trimmedPassword, challengeMode);
console.log('VerifyPasswordScreen - SetPassword sync response successful, waiting for async events');
} catch (error) {
// This catch block handles sync response errors (rejected promises)
console.error('VerifyPasswordScreen - SetPassword sync error:', error);
// Cast the error back to RDNASyncResponse as per service pattern
const result: RDNASyncResponse = error as RDNASyncResponse;
const errorMessage = RDNASyncUtils.getErrorMessage(result);
setError(errorMessage);
resetInput();
} finally {
setIsSubmitting(false);
}
};
// Check if form is valid
const isFormValid = (): boolean => {
return password.trim().length > 0 && !error;
};
return (
<SafeAreaView style={styles.safeArea}>
<StatusBar barStyle="dark-content" backgroundColor="#f8f9fa" />
<ScrollView style={styles.container}>
{/* 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 */}
{userID && (
<View style={styles.userContainer}>
<Text style={styles.welcomeText}>Welcome back</Text>
<Text style={styles.userNameText}>{userID}</Text>
</View>
)}
{/* Attempts Left Information */}
{attemptsLeft > 0 && (
<StatusBanner
type="warning"
message={`${attemptsLeft} attempt${attemptsLeft !== 1 ? 's' : ''} remaining`}
/>
)}
{/* Error Display */}
{error && (
<StatusBanner
type="error"
message={error}
/>
)}
{/* Password Input */}
<Input
label="Password"
value={password}
onChangeText={handlePasswordChange}
placeholder="Enter your password"
secureTextEntry={true}
returnKeyType="done"
onSubmitEditing={handleVerifyPassword}
editable={!isSubmitting}
autoFocus={true}
error={error}
/>
{/* Submit Button */}
<Button
title={isSubmitting ? 'Verifying...' : 'Verify Password'}
onPress={handleVerifyPassword}
loading={isSubmitting}
disabled={!isFormValid()}
/>
{/* Help Text */}
<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, // 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,
},
attemptsContainer: {
alignItems: 'center',
marginBottom: 20,
},
attemptsText: {
fontSize: 14,
color: '#e67e22',
fontWeight: '500',
backgroundColor: '#fef5e7',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
borderWidth: 1,
borderColor: '#f39c12',
},
errorContainer: {
backgroundColor: '#fff0f0',
borderLeftColor: '#e74c3c',
borderLeftWidth: 4,
borderRadius: 8,
padding: 16,
marginBottom: 20,
},
errorText: {
color: '#e74c3c',
fontSize: 14,
fontWeight: '500',
textAlign: 'center',
},
inputContainer: {
marginBottom: 20,
},
inputLabel: {
fontSize: 16,
fontWeight: 'bold',
color: '#2c3e50',
marginBottom: 8,
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 16,
fontSize: 16,
backgroundColor: '#fff',
color: '#2c3e50',
},
inputError: {
borderColor: '#e74c3c',
},
submitButton: {
backgroundColor: '#3498db',
borderRadius: 8,
padding: 16,
alignItems: 'center',
marginBottom: 16,
},
submitButtonDisabled: {
backgroundColor: '#bdc3c7',
},
submitButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
marginLeft: 8,
},
loadingContainer: {
flexDirection: 'row',
alignItems: 'center',
},
helpContainer: {
backgroundColor: '#e8f4f8',
borderRadius: 8,
padding: 16,
marginTop: 20,
},
helpText: {
fontSize: 14,
color: '#2c3e50',
textAlign: 'center',
lineHeight: 20,
},
});
export default VerifyPasswordScreen;
Status Code | Event Name | Meaning |
102 |
| Triggered when an invalid password is provided in setPassword API with challegeMode = 0. |
Login Flow Optimizations:
Challenge Mode Handling: The screen automatically handles challengeMode = 0
for password verification:
// in getPassword event response, challengeMode = 0 indicates login flow password verification
//.setPassword API with same challengeMode
const syncResponse: RDNASyncResponse = await rdnaService.setPassword(trimmedPassword, challengeMode);
The following image showcases screen from the sample application:
This implementation provides a focused, user-friendly interface for password verification during login flows while maintaining consistency with the overall app design and error handling patterns.
The screens we built for activation automatically handle login flow contexts. Let's set up the navigation and integration.
The same src/tutorial/navigation/SDKEventProvider.tsx
for event handling.
Test both activation and login flows to ensure proper implementation.
Before Testing:
1. Launch app β InitialScreen
2. Tap "Start MFA" β getUser event β CheckUserScreen
3. Enter username β setUser API β getActivationCode event β ActivationCodeScreen
4. Enter OTP β setActivationCode API β getUserConsentForLDA event β UserLDAConsentScreen
5. Enable biometric β setUserConsentForLDA API β [Biometric prompt]
6. Complete biometric β onUserLoggedIn event β DashboardScreen
β Validation Points:
1. Launch app (user previously activated)
2. SDK auto-triggers getUser event β CheckUserScreen
3. Enter username β setUser API β [Automatic biometric prompt] 4. Complete biometric authentication β onUserLoggedIn event β DashboardScreen
β Validation Points:
1. Launch app β CheckUserScreen
2. Enter username β setUser API β getPassword event β SetPasswordScreen (verify mode)
3. Enter password β setPassword API β onUserLoggedIn event β DashboardScreen
β Validation Points:
1. From Dashboard β Tap logout β Confirmation dialog
2. Confirm logout β logOff API β onUserLoggedOff event β InitialScreen
3. Plugin auto-triggers getUser event β CheckUserScreen
4. Complete login flow β Back to Dashboard
β Validation Points:
Monitor console logs for proper event flow:
// Expected activation flow logs
π± [EVENT] getUser triggered
π [API] Setting user: username
π± [EVENT] getActivationCode triggered
π [API] Setting activation code
π± [EVENT] getUserConsentForLDA triggered
π [API] Setting LDA consent
β
[EVENT] User logged in successfully
// Expected login flow logs
π± [EVENT] getUser triggered
π [API] Setting user: username
[Biometric prompt - no API logs]
β
[EVENT] User logged in successfully
Ensure proper screen navigation:
// Activation flow navigation
InitialScreen β CheckUserScreen β ActivationCodeScreen β
UserLDAConsentScreen β DashboardScreen
// Login flow navigation (typical)
InitialScreen β CheckUserScreen β DashboardScreen
// Login flow navigation (password validation)
InitialScreen β CheckUserScreen β SetPasswordScreen β DashboardScreen
Problem: Plugin events not firing after API calls
Solutions:
// 1. Verify event handler registration
useEffect(() => {
console.log('π§ Registering event handlers...');
rdnaEventManager.setCallbacks({
onGetUser: handleGetUser,
// ... other handlers
});
return () => {
console.log('π§ Cleaning up event handlers...');
rdnaEventManager.onGetUser = undefined;
};
}, [handleGetUser]);
// 2. Check callback preservation
const handleGetUser = useCallback((challenge) => {
console.log('β
getUser event received:', challenge);
// Handler logic...
}, []); // Ensure dependencies are correct
Problem: Same screen appears multiple times in navigation stack when SDK events fire repeatedly
Symptoms:
Root Cause: Using NavigationService.navigate()
for SDK event-driven navigation creates new screen instances even when already on the target screen.
Solution: Use NavigationService.navigateOrUpdate()
for all SDK event handlers:
// β Problem: Creates duplicate screens
const handleGetUser = useCallback((data: RDNAGetUserData) => {
NavigationService.navigate('CheckUserScreen', { eventData: data });
}, []);
// β
Solution: Prevents duplicates and updates existing screen
const handleGetUser = useCallback((data: RDNAGetUserData) => {
NavigationService.navigateOrUpdate('CheckUserScreen', { eventData: data });
}, []);
How navigateOrUpdate Works:
Problem: Screen doesn't reflect updated event data when SDK event fires multiple times
Symptoms:
Solution: Ensure screens use route.params
directly or listen for param changes:
// β
Screen automatically updates when params change
const CheckUserScreen = ({ route }) => {
const { eventData, responseData } = route.params;
// React Navigation automatically re-renders when params update
return (
<View>
<Text>Status: {responseData?.challengeResponse?.status?.statusCode}</Text>
{/* Screen content updates automatically */}
</View>
);
};
// β
If you need to trigger side effects on param changes
const CheckUserScreen = ({ route }) => {
const { eventData } = route.params;
useEffect(() => {
// Handle new event data
console.log('New event data received:', eventData);
}, [eventData]); // Re-runs when eventData param changes
return <YourScreenContent />;
};
Problem: App becomes slow or unresponsive during navigation
Performance Benefits of navigateOrUpdate:
Monitoring Navigation Stack:
// Debug navigation stack depth
const debugNavigationStack = () => {
if (navigationRef.isReady()) {
const state = navigationRef.getRootState();
console.log('π Navigation stack depth:', state.routes.length);
console.log('π Current screens:', state.routes.map(r => r.name));
}
};
// Call periodically during development
useEffect(() => {
const interval = setInterval(debugNavigationStack, 5000);
return () => clearInterval(interval);
}, []);
Problem: API calls returning error codes
Debugging:
const response = await rdnaService.setUser(username);
console.log('π API Response:', {
resultCode: response.data.resultCode,
resultDescription: response.data.resultDescription,
additionalInfo: response.data.additionalInfo
});
// Common error codes:
// RESULT_FAILURE - General failure
// RESULT_INVALID_INPUT - Invalid input parameters
// RESULT_SESSION_EXPIRED - Session timeout
Problem: Valid activation code expired
Solutions:
// Ensure to get new activation code or not received code
await rdnaService.resendActivationCode();
Problem: Screen state not persisting during navigation
Solutions:
// Use proper state management
const [formData, setFormData] = useState({
username: '',
isLoading: false,
error: ''
});
// Preserve state during navigation
useEffect(() => {
// Save state to context or AsyncStorage if needed
}, [formData]);
Problem: LDA consent Not Showin for biometric prompt
Solutions:
Ensure that the required permission is declared in the code and granted at runtime, and that biometric authentication is available on the device.
// β DON'T: Store credentials in plain text
const user = {
username: 'user@example.com',
password: 'plaintext123', // Never store passwords
activationCode: '123456' // Never log activation codes
};
// β
DO: Handle credentials securely
const handleUserInput = (username: string) => {
// Validate input format
if (!isValidEmail(username)) {
setError('Please enter a valid email address');
return;
}
// Send to SDK immediately, don't store
rdnaService.setUser(username);
// Clear sensitive form data
setTimeout(() => {
setUsername('');
}, 1000);
};
// β DON'T: Expose sensitive information in errors
if (error.message.includes('user not found')) {
setError('User does not exist in our system'); // Reveals user existence
}
// β
DO: Use generic error messages
const getSafeErrorMessage = (error: any) => {
// Map specific errors to user-friendly messages
const errorMap = {
'RESULT_INVALID_USER': 'Please check your credentials and try again',
'RESULT_INVALID_CODE': 'Invalid activation code. Please try again',
'RESULT_EXPIRED': 'Your session has expired. Please start over'
};
return errorMap[error.code] || 'An error occurred. Please try again';
};
const performSecureLogout = async () => {
try {
// 1. Clear sensitive data from memory
setUsername('');
setActivationCode('');
setError('');
// 2. Call SDK logout
await rdnaService.logOff();
// 3. Clear navigation stack
navigation.reset({
index: 0,
routes: [{ name: 'InitialScreen' }],
});
// 4. Clear any cached data
} catch (error) {
console.error('Logout error:', error);
// Force navigation even on error
navigation.reset({
index: 0,
routes: [{ name: 'InitialScreen' }],
});
}
};
// Clear sensitive data from component state
useEffect(() => {
return () => {
// Component cleanup
setPassword('');
setActivationCode('');
setError('');
};
}, []);
// Handle app backgrounding
useEffect(() => {
const handleAppStateChange = (nextAppState: string) => {
if (nextAppState === 'background' || nextAppState === 'inactive') {
// Clear sensitive data when app goes to background
setPassword('');
setActivationCode('');
}
};
const subscription = AppState.addEventListener('change', handleAppStateChange);
return () => subscription?.remove();
}, []);
β Security Review:
β Performance Review:
β User Experience Review:
Congratulations! You've successfully implemented a complete, production-ready MFA Activation and Login flow using the plugin:
β Complete MFA Implementation:
Your implementation provides:
Next User Login Experience:
When users return to your app, they'll experience the optimized login flow:
getUser
eventWith this foundation, you're ready to explore:
Advanced MFA Features:
π― You're now ready to deploy a production-grade MFA system! Your implementation demonstrates enterprise-level security practices and provides an excellent foundation for building secure, user-friendly authentication experiences.
The complete working implementation is available in the sample app for reference and further customization.