🎯 Learning Path:

  1. Complete RELID Initialization first
  2. You are here β†’ MFA Activation & Login Implementation

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.

πŸš€ What You'll Build

By the end of this codelab, you'll have a complete MFA system that handles:

πŸ“± Activation Flow (First-Time Users):

πŸ” Login Flow (Returning Users):

βœ… Technical Requirements

Before starting, verify you have:

βœ… Knowledge Prerequisites

You should be comfortable with:

Get the Code from GitHub

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

Platform-Specific Permissions

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.

Understanding MFA Flow Types

Quick Overview: This codelab covers two flows:

πŸ“Š Flow Comparison at a Glance

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

πŸ”„ What Does Activation and Login Flow Mean

Activation Flow Occurs When:

Login Flow Occurs When:

πŸ—οΈ Plugin Architecture & Event System

The Plugin uses an event-driven architecture where:

  1. Plugin Events β†’ Trigger challenges requiring user input
  2. API Calls β†’ Respond to challenges with user data
  3. Event Handling β†’ Handle event responses and navigate accordingly

Key Architecture Pattern: Promise + Event

// Synchronous API response
const syncResponse = await rdnaService.setUser(username);

// Asynchronous event handling  
rdnaEventManager.onGetUser = (challenge) => {
  // Handle the challenge in UI
  navigation.navigate('CheckUserScreen');
};

πŸ“‹ Quick Reference: Core Events & APIs

SDK Event

API Response

Purpose

Flow

getUser

setUser()

User identification

Both

getActivationCode

setActivationCode()

OTP verification

Activation

getUserConsentForLDA

setUserConsentForLDA()

Biometric setup

Activation

getPassword

setPassword()

Password setup/verify

Both

onUserLoggedIn

N/A

Success notification(user logged in)

Both

onUserLoggedOff

logOff()

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.

Understanding Activation Flow Sequence

Please refer to the flow diagram from uniken developer documentation portal, user activation

Activation Challenge Phases

Phase

Challenge Type

User Action

SDK Validation

Result

1. User ID

checkuser

Enter username/email

Validates user exists/format

Proceeds or repeats getUser

2. OTP Verify

otp

Enter activation code

Validates code from email/SMS

Proceeds or shows error

3. Device Auth

pass

Choose biometric or password

Sets up device authentication

Completes activation

4. Success

N/A

Automatic navigation

User session established

User activated & logged in

πŸ”„ Cyclical Challenge Handling

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.

Core Type Definitions

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;
}

MFA Event Data Types

/**
 * 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 Types

// 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;

Error Handling Utility Types

/**
 * 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.

Adding MFA Event Handlers

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))
    );
  }
}

Implementing Event Handlers

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);
  }
}

Event Handler Registration Methods

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;
}

πŸ”‘ Key Implementation Features

Add the activation flow APIs to your RELID service. These APIs respond to the activation challenges with user-provided data.

Core Activation APIs

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);
        }
      });
    });
  }
}

Understanding resetAuthState API

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.

When to Use resetAuthState

The resetAuthState API should be called in these pre-login scenarios:

  1. User-Initiated Cancellation: When the user decides to cancel the authentication process
  2. Switching Users: When switching between different user accounts during the login process
  3. Error Recovery: When recovering from authentication errors or timeout conditions during login

Note: These use cases only apply during the authentication process, before onUserLoggedIn event is triggered.

How resetAuthState Works

// 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:

Implementation Pattern in UI Components

Here'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
  }
};

Best Practices

  1. Always await the response: resetAuthState is asynchronous and should be properly awaited
  2. Handle errors gracefully: Wrap calls in try/catch blocks for error handling
  3. Expect getUser event: After successful reset, the SDK will trigger a new getUser event
  4. Use for clean transitions: Prefer resetAuthState over navigation-only solutions when canceling flows

Flow Diagram

User Cancels/Error Occurs
         ↓
Call resetAuthState()
         ↓
SDK Clears Session State
         ↓
Synchronous Response (success/error)
         ↓
SDK Triggers getUser Event
         ↓
App Handles Fresh Authentication Flow

Understanding resendActivationCode API

The resendActivationCode API is used when the user has not received their activation code (OTP) via email or SMS and requests a new one.

Purpose and Functionality

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.

When to Use resendActivationCode

The resendActivationCode API should be used in these scenarios:

  1. OTP Not Received: When the user reports they haven't received the initial activation code
  2. Code Expired: When the activation code has expired before the user could enter it
  3. Delivery Issues: When there are suspected issues with email or SMS delivery
  4. User Request: When the user explicitly requests a new activation code

How resendActivationCode Works

// 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:

Implementation Pattern in UI Components

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);
  }
};

Best Practices

  1. Prevent Multiple Requests: Disable the resend button while a request is in progress
  2. Provide User Feedback: Show loading states and success/failure messages
  3. Handle Rate Limiting: Be aware that there may be limits on how frequently codes can be resent
  4. Expect New Event: After successful resend, wait for the new getActivationCode event with updated data

Flow Diagram

User Requests Resend
         ↓
Call resendActivationCode()
         ↓
SDK Sends New OTP (Email/SMS)
         ↓
Synchronous Response (success/error)
         ↓
SDK Triggers getActivationCode Event
         ↓
App Receives Fresh OTP Data

API Response Handling Pattern

All activation APIs follow the same response handling pattern:

  1. Synchronous Response: Immediate callback indicates if API call was accepted
  2. Success Condition: longErrorCode === 0 means SDK accepted the data
  3. Asynchronous Events: Next challenge event triggered after successful API call
  4. Error Handling: Rejected promises contain RDNASyncResponse with error details

Create a centralized navigation service to handle programmatic navigation throughout the activation flow.

Navigation Service Implementation

// 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;

Navigation Type Definitions

// 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.

Understanding the SDK Event Provider

The SDKEventProvider is a React Context provider that:

SDKEventProvider Implementation

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;

Provider Index File

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';

Key Event Handlers Explained

The SDKEventProvider handles several critical events in the MFA flow:

1. handleInitialized

2. handleGetUser

3. handleGetActivationCode

4. handleGetUserConsentForLDA

5. handleGetPassword

6. handleUserLoggedIn

Integrating SDKEventProvider in App.tsx

Update 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;

Provider Hierarchy and Benefits

The provider hierarchy ensures proper event flow:

  1. MTDThreatProvider (outermost): Handles security threats
  2. SDKEventProvider (middle): Manages SDK events and navigation
  3. AppNavigator (innermost): Provides navigation infrastructure

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.

CheckUserScreen Implementation

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;

Specific Status Code handling

Status Code

Event Name

Meaning

101

getUser

Triggered when an invalid user is provided in setuser API.

138

getUser

User is blocked due to exceeded OTP attempts or blocked by admin or previously blocked in other flow

Key Implementation Features

The CheckUserScreen demonstrates several important patterns:

The following image showcases screen from the sample application:

Check User Screen

Create the activation code input screen that handles OTP verification during the activation flow.

ActivationCodeScreen Implementation

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,
  },
});

Specific Status Code handling

Status Code

Event Name

Meaning

106

getActivationCode

Triggered when an invalid otp is provided in setActivationCode API.

Activation Code Screen Features

The following image showcases screen from the sample application:

Activation Code Screen

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.

User LDA Consent Screen

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:

User LDA Consent Screen

Set Password Authentication Screen

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.

Key Features

Create 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;

Password Policy Parsing Implementation

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:

4. Integration Benefits

This implementation ensures that users always receive clear, actionable password requirements directly from your organization's security policy configuration.

Specific Status Code handling

Status Code

Event Name

Meaning

190

getPassword

Triggered when the provided password does not meet the password policy requirements in the setPassword API.

164

getPassword

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:

Set Password Screen

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.

Implementing User Logout API and Events

Complete MFA systems need secure logout functionality with proper session cleanup.

Understanding Logout Flow

The logout flow follows this sequence:

  1. User initiates logout (button press)
  2. Call logOff() API to clean up session
  3. SDK triggers onUserLoggedOff event
  4. Navigate back to initial screen
  5. SDK automatically triggers getUser event (flow restarts)

Adding logOff API to Service Layer

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;
  }
}

Building Dashboard with Logout Functionality

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;

Logout Flow Sequence

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)

πŸ”‘ Key Implementation Notes

βœ… Implementation Status

The logout functionality has been fully implemented in the codebase:

The following images showcase screens from the sample application:

Dashboard Screen Screen

LogOut Screen Screen

πŸ“ What We're Building: Streamlined authentication for returning users with biometric prompts and password verification.

Login Flow vs Activation Flow

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

When Login Flow Occurs

Login Flow Triggers When:

Flow Detection: The same plgin events (getUser, getPassword) are used for both flows. The difference is in:

Login Flow Events Sequence

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.

Specific Status Code handling

Status Code

Event Name

Meaning

141

getuser

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.

Key Features

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;

Specific Status Code handling

Status Code

Event Name

Meaning

102

getPassword

Triggered when an invalid password is provided in setPassword API with challegeMode = 0.

Key Differences from SetPasswordScreen

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:

Verify Password Screen

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.

SDK Event Provider

The same src/tutorial/navigation/SDKEventProvider.tsx for event handling.

Test both activation and login flows to ensure proper implementation.

Testing Preparation

Before Testing:

  1. Clear app data to simulate first-time user
  2. Ensure device has biometric authentication available
  3. Have valid test credentials ready
  4. Enable debug logging in development

End-to-End Testing Scenarios

Scenario 1: Complete Activation Flow (New User)

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:

Scenario 2: Login Flow (Returning User with Biometric)

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:

Scenario 3: Login Flow with Password

1. Launch app β†’ CheckUserScreen
2. Enter username β†’ setUser API β†’ getPassword event β†’ SetPasswordScreen (verify mode)
3. Enter password β†’ setPassword API β†’ onUserLoggedIn event β†’ DashboardScreen

βœ… Validation Points:

Scenario 4: Logout and Re-login Cycle

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:

Testing Validation Points

Event Sequence Validation

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

Navigation Flow Verification

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

Event Handler Issues

Event Not Triggering

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

Navigation Stack Issues

Duplicate Screens in Navigation Stack

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:

  1. Checks if currently on target screen
  2. If YES: Updates screen params with new event data (no navigation)
  3. If NO: Navigates to screen normally

Screen Not Updating with New Event Data

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 />;
};

Navigation Performance Issues

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);
}, []);

API Response Issues

Sync Response Errors

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

Activation Code Expired

Problem: Valid activation code expired

Solutions:

// Ensure to get new activation code or not received code
  await rdnaService.resendActivationCode();

Navigation Issues

Screen State Persistence

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]);

Biometric Authentication Problems

Biometric Prompt LDA consent Not Showing

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.

Secure Data Handling

User Credentials Protection

// ❌ 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);
};

Error Message Security

// ❌ 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';
};

Session Management Security

Secure Logout Implementation

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' }],
    });
  }
};

Memory Security

Secure Memory Management

// 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();
}, []);

Production Deployment Checklist

βœ… Security Review:

βœ… Performance Review:

βœ… User Experience Review:

Congratulations! You've successfully implemented a complete, production-ready MFA Activation and Login flow using the plugin:

πŸŽ‰ What You've Built

βœ… Complete MFA Implementation:

πŸ† Key Implementation Patterns Mastered

  1. Challenge-Response Pattern: Understanding how SDK events map to API calls
  2. Promise + Event Pattern: Combining synchronous API responses with asynchronous event handling
  3. Cyclical Challenge Handling: Managing repeated events gracefully
  4. Flow Detection Logic: Distinguishing between activation and login contexts
  5. Error Boundary Implementation: Graceful error handling at API and UI levels
  6. Callback Preservation: Maintaining multiple event consumers without conflicts

πŸš€ Architecture Benefits

Your implementation provides:

πŸ”„ What Happens Next

Next User Login Experience:

When users return to your app, they'll experience the optimized login flow:

  1. SDK automatically triggers getUser event
  2. User enters credentials β†’ Instant biometric prompt (if enabled)
  3. One-touch authentication β†’ Direct access to app

πŸš€ Next Steps & Advanced Features

With this foundation, you're ready to explore:

Advanced MFA Features:

πŸ“š References & Resources

🎯 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.