🎯 Learning Path:

  1. Complete REL-ID Initialization Flow Codelab
  2. Complete REL-ID Complete IDV MFA Activation & Login Flow Codelab
  3. You are here → Post-Login Biometric Template Management Implementation

Welcome to the REL-ID IDV Post-Login Server Biometric Opt-In & Opt-Out Flow codelab! This tutorial teaches you how to implement secure biometric template creation and deletion workflows with LDA (Local Device Authentication) fallback and password validation for activated customers.

What You'll Build

In this codelab, you'll implement two complete biometric template management workflows:

Biometric Opt-In Flow (Template Creation):

Biometric Opt-Out Flow (Template Deletion):

Key Differences: Opt-In vs Opt-Out

This codelab demonstrates two distinct flows for biometric template management:

Opt-In Flow (Workflow 10, Challenge Mode 6):
LDA/Password → Selfie Capture → Confirm Frame → Create Template
(Creates NEW biometric template on server)

Opt-Out Flow (Workflow 11, Challenge Mode 7):
LDA/Password → Delete Template → Show Status
(Deletes EXISTING biometric template from server)

Critical Differences:

What You'll Learn

Prerequisites

Technology Stack

Biometric Opt-In Flow Diagram

Understanding the complete opt-in flow is crucial before implementation:

1. BiometricOptInScreen loads
   └─> checkTemplateStatus() checks if template exists on server

2. User clicks "Opt In to Biometric Authentication" button
   └─> setIsOptingIn(true)
   └─> rdnaIDVService.getIDVPasswordForBiometricOptIn(password, mode: 6)

3. SDK checks LDA (Local Device Authentication) availability

   A. LDA AVAILABLE
      └─> Shows LDA prompt (fingerprint/face/PIN)

      → If LDA SUCCESS: Proceeds to step 4

      → If LDA CANCELLED: Fallback to password dialog
         └─> Shows PasswordValidationDialog
         └─> User enters password and submits
         └─> Proceeds to step 4

   B. LDA NOT AVAILABLE
      └─> Fallback to password dialog
      └─> Shows PasswordValidationDialog
      └─> User enters password and submits
      └─> Proceeds to step 4

4. SDK validates authentication (LDA or password)
   └─> EVENT: GetPasswordVerification (idvWorkflow: 10)
   └─> If valid: SDK initiates selfie capture
   └─> If invalid: Shows error, stays on dialog

5. SDK starts selfie process
   └─> EVENT: GetSelfieProcessStartConfirmation (idvWorkflow: 10)
   └─> Password dialog closes automatically (if open)
   └─> Native IDVSelfieProcessStart screen appears (from parent stack)

6. User captures selfie on native screen

7. SDK sends captured frame for confirmation
   └─> EVENT: RDNAIDVOptInCapturedFrameConfirmation
   └─> data.capturedImage (base64)
   └─> Parent navigator pops IDVSelfieProcessStart screen
   └─> isOptInFlowActiveRef.current = true
   └─> Shows CapturedImageModal with preview

8. User reviews captured image and chooses action:

   A. APPROVE (action: 0)
      └─> Modal closes immediately
      └─> rdnaIDVService.setIDVBiometricOptInConfirmation(0)
      └─> SDK processes opt-in on server

   B. RECAPTURE (action: 1)
      └─> Modal STAYS OPEN
      └─> rdnaIDVService.setIDVBiometricOptInConfirmation(1)
      └─> SDK re-opens selfie screen
      └─> Returns to step 6 (new capture updates modal)

   C. CANCEL (action: 2)
      └─> Modal closes immediately
      └─> rdnaIDVService.setIDVBiometricOptInConfirmation(2)
      └─> SDK aborts opt-in process

9. SDK returns final status
   └─> EVENT: RDNAIDVBiometricOptInStatusResponse (challengeMode: 6)
   └─> data.status.statusCode & data.status.statusMessage
   └─> isOptInFlowActiveRef.current = false

10. Show result to user:
    └─> If statusCode === 100: "Opt-In Successful"
    └─> Else: "Opt-In Failed" with message
    └─> checkTemplateStatus() to refresh UI state

Biometric Opt-Out Flow Diagram

1. BiometricOptOutScreen loads
   └─> checkTemplateStatus() checks if template exists on server
   └─> Only shows opt-out button if template exists

2. User clicks "Opt Out of Biometric Authentication" button
   └─> setIsOptingOut(true)
   └─> rdnaIDVService.getIDVPasswordForBiometricOptOut(password, mode: 7)

3. SDK checks LDA (Local Device Authentication) availability

   A. LDA AVAILABLE
      └─> Shows LDA prompt (fingerprint/face/PIN)

      → If LDA SUCCESS: Proceeds to step 4

      → If LDA CANCELLED: Fallback to password dialog
         └─> Shows PasswordValidationDialog
         └─> User enters password and submits
         └─> Proceeds to step 4

   B. LDA NOT AVAILABLE
      └─> Fallback to password dialog
      └─> Shows PasswordValidationDialog
      └─> User enters password and submits
      └─> Proceeds to step 4

4. SDK validates authentication (LDA or password)
   └─> EVENT: GetPasswordVerification (idvWorkflow: 11)
   └─> Password dialog closes automatically
   └─> If valid: SDK directly processes opt-out (deletes template from server)
   └─> If invalid: Shows error, stays on dialog

5. SDK returns final status
   └─> EVENT: RDNAIDVBiometricOptOutStatusResponse (challengeMode: 7)
   └─> data.status.statusCode & data.status.statusMessage

6. Show result to user:
   └─> If statusCode === 100: "Opt-Out Successful - Template deleted"
   └─> Else: "Opt-Out Failed" with message
   └─> checkTemplateStatus() to refresh UI state

IDV Workflow and Challenge Mode Values

For biometric opt-in and opt-out, you'll use these specific values:

Flow

Workflow

Challenge Mode

Events

Opt-In

10

6

GetPasswordVerification (10)
GetSelfieProcessStartConfirmation (10)
RDNAIDVOptInCapturedFrameConfirmation
RDNAIDVBiometricOptInStatusResponse (mode: 6)

Opt-Out

11

7

GetPasswordVerification (11)
RDNAIDVBiometricOptOutStatusResponse (mode: 7)

Comparison with Other IDV Flows:

Aspect

KYC Flow

Server Biometric Auth

Opt-In Flow

Opt-Out Flow

Workflow

6

8

10

11

Challenge Mode

9

25

6

7

Template Check

No

Yes (required)

Yes (optional)

Yes (required)

Document Scan

Yes

No

No

No

Selfie Capture

Yes

Yes

Yes

No

Template Operation

Create new

Use existing

Create new

Delete existing

LDA Support

No

No

Yes

Yes

Password Fallback

No

No

Yes

Yes

Before You Begin

Ensure you have completed these requirements:

1. Previous Codelabs Completed

2. Development Environment

# Required tools
Node.js >= 16
npm or yarn
React Native CLI
Xcode (for iOS) or Android Studio (for Android)

3. REL-ID SDK Credentials

Get the Code from GitHub

The code to get started can be found in a GitHub repository.

You can clone the repository using the following command:

git clone https://github.com/uniken-team/relid-codelab-react-native.git

Navigate to the relid-IDV-post-login-server-biometric-optin-optout folder:

cd relid-codelab-react-native/relid-IDV-post-login-server-biometric-optin-optout

Project Structure

The biometric opt-in & opt-out codelab follows this structure:

relid-IDV-post-login-server-biometric-optin-optout/
├── src/
│   ├── tutorial/
│   │   ├── screens/
│   │   │   ├── idv/                                        # IDV Screens
│   │   │   │   ├── biometricOptIn/                         # 🆕 Opt-In Flow
│   │   │   │   │   └── BiometricOptInScreen.tsx           # Template creation
│   │   │   │   ├── biometricOptOut/                        # 🆕 Opt-Out Flow
│   │   │   │   │   └── BiometricOptOutScreen.tsx          # Template deletion
│   │   │   │   ├── IDVSelfieProcessStartConfirmationScreen.tsx  # ♻️ Reused for opt-in
│   │   │   │   └── index.ts
│   │   │   ├── mfa/                                        # MFA Screens
│   │   │   │   ├── DashboardScreen.tsx
│   │   │   │   └── ...
│   │   │   └── components/
│   │   │       ├── DrawerContent.tsx                       # 🆕 Opt-In/Opt-Out menu items
│   │   │       └── ...
│   │   └── navigation/
│   │       ├── AppNavigator.tsx
│   │       └── DrawerNavigator.tsx                         # 🆕 Opt-In/Opt-Out routes
│   └── uniken/
│       ├── components/
│       │   └── modals/
│       │       ├── PasswordValidationDialog.tsx            # 🆕 Reusable password dialog
│       │       ├── ThreatDetectionModal.tsx
│       │       ├── SessionModal.tsx
│       │       └── index.ts                                # 🆕 Export PasswordValidationDialog
│       ├── services/
│       │   └── idv/
│       │       ├── rdnaIDVService.ts                       # 🆕 Opt-in/opt-out APIs
│       │       │                                          # - getIDVPasswordForBiometricOptIn()
│       │       │                                          # - getIDVPasswordForBiometricOptOut()
│       │       │                                          # - setIDVBiometricOptInConfirmation()
│       │       │                                          # - setIDVBiometricOptOutConfirmation()
│       │       └── rdnaIDVEventManager.ts                  # 🆕 Event handlers
│       │                                                  # - onBiometricOptInStatus
│       │                                                  # - onBiometricOptOutStatus
│       ├── types/
│       │   └── idv/
│       │       └── rdnaIDVEvents.ts                        # 🆕 Type definitions
│       │                                                  # - RDNAIDVBiometricOptInStatusData
│       │                                                  # - RDNAIDVBiometricOptOutStatusData
│       └── providers/
│           └── idv/
│               └── SDKIDVEventProvider.tsx                 # Global handlers
└── react-native-rdna-client/                               # REL-ID Native Bridge
    └── (Aware FaceCapture Library embedded - opt-in only)

IDV Assets Configuration

The project includes pre-configured Aware FaceCapture library for opt-in:

Required Assets (Opt-In):

NOT Required (Opt-Out):

Overview

Before implementing the opt-in and opt-out flows, create a reusable PasswordValidationDialog component for LDA fallback authentication. This component is used by both flows when LDA is unavailable or cancelled.

Component Features

Implement PasswordValidationDialog Component

Create PasswordValidationDialog.tsx:

// src/uniken/components/modals/PasswordValidationDialog.tsx

import React, { useState, useEffect, useRef } from 'react';
import {
  Modal,
  View,
  Text,
  TextInput,
  TouchableOpacity,
  StyleSheet,
  ScrollView,
  BackHandler,
  ActivityIndicator,
  KeyboardAvoidingView,
  Platform,
} from 'react-native';

interface PasswordValidationDialogProps {
  visible: boolean;
  title?: string;
  message?: string;
  attemptsLeft: number;
  errorMessage?: string;
  isSubmitting: boolean;
  onSubmitPassword: (password: string) => void;
  onCancel: () => void;
}

const PasswordValidationDialog: React.FC<PasswordValidationDialogProps> = ({
  visible,
  title = '🔐 Authentication Required',
  message = 'Please verify your password to continue',
  attemptsLeft,
  errorMessage,
  isSubmitting,
  onSubmitPassword,
  onCancel,
}) => {
  const [password, setPassword] = useState<string>('');
  const [showPassword, setShowPassword] = useState<boolean>(false);
  const passwordInputRef = useRef<TextInput>(null);

  // Clear password when modal becomes visible
  useEffect(() => {
    if (visible) {
      setPassword('');
      setShowPassword(false);
      // Auto-focus password input after a short delay
      setTimeout(() => {
        passwordInputRef.current?.focus();
      }, 300);
    }
  }, [visible]);

  // Clear password field when error message changes (wrong password)
  useEffect(() => {
    if (errorMessage) {
      setPassword('');
    }
  }, [errorMessage]);

  // Disable hardware back button when modal is visible
  useEffect(() => {
    const handleBackPress = () => {
      if (visible && !isSubmitting) {
        onCancel();
        return true; // Prevent default back action
      }
      return false;
    };

    const backHandler = BackHandler.addEventListener('hardwareBackPress', handleBackPress);

    return () => backHandler.remove();
  }, [visible, isSubmitting, onCancel]);

  const handleSubmit = () => {
    if (!password.trim() || isSubmitting) {
      return;
    }
    onSubmitPassword(password.trim());
  };

  const handlePasswordChange = (text: string) => {
    setPassword(text);
  };

  const getAttemptsColor = (): string => {
    if (attemptsLeft === 1) return '#dc2626'; // Red
    if (attemptsLeft === 2) return '#f59e0b'; // Orange
    return '#10b981'; // Green
  };

  return (
    <Modal
      visible={visible}
      transparent={true}
      animationType="fade"
      onRequestClose={() => {
        if (!isSubmitting) {
          onCancel();
        }
      }}
    >
      <KeyboardAvoidingView
        style={styles.modalOverlay}
        behavior={Platform.OS === 'ios' ? 'padding' : undefined}
        keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
      >
        <View style={styles.modalContainer}>
          {/* Header */}
          <View style={styles.modalHeader}>
            <Text style={styles.modalTitle}>{title}</Text>
            <Text style={styles.modalSubtitle}>{message}</Text>
          </View>

          <ScrollView
            style={styles.scrollContainer}
            contentContainerStyle={styles.contentContainer}
            keyboardShouldPersistTaps="handled"
            showsVerticalScrollIndicator={false}
          >
            {/* Attempts Left Counter */}
            {attemptsLeft <= 3 && (
              <View style={[
                styles.attemptsContainer,
                { backgroundColor: `${getAttemptsColor()}20` }
              ]}>
                <Text style={[
                  styles.attemptsText,
                  { color: getAttemptsColor() }
                ]}>
                  {attemptsLeft} attempt{attemptsLeft !== 1 ? 's' : ''} remaining
                </Text>
              </View>
            )}

            {/* Error Display */}
            {errorMessage && (
              <View style={styles.errorContainer}>
                <Text style={styles.errorText}>{errorMessage}</Text>
              </View>
            )}

            {/* Password Input */}
            <View style={styles.inputContainer}>
              <Text style={styles.inputLabel}>Password</Text>
              <View style={styles.passwordInputWrapper}>
                <TextInput
                  ref={passwordInputRef}
                  style={styles.passwordInput}
                  value={password}
                  onChangeText={handlePasswordChange}
                  placeholder="Enter your password"
                  placeholderTextColor="#9ca3af"
                  secureTextEntry={!showPassword}
                  autoCapitalize="none"
                  autoCorrect={false}
                  returnKeyType="done"
                  onSubmitEditing={handleSubmit}
                  editable={!isSubmitting}
                />
                <TouchableOpacity
                  style={styles.visibilityButton}
                  onPress={() => setShowPassword(!showPassword)}
                  disabled={isSubmitting}
                >
                  <Text style={styles.visibilityIcon}>
                    {showPassword ? '👁️' : '🙈'}
                  </Text>
                </TouchableOpacity>
              </View>
            </View>
          </ScrollView>

          {/* Action Buttons */}
          <View style={styles.buttonContainer}>
            <TouchableOpacity
              style={[
                styles.submitButton,
                (!password.trim() || isSubmitting) && styles.buttonDisabled
              ]}
              onPress={handleSubmit}
              disabled={!password.trim() || isSubmitting}
            >
              {isSubmitting ? (
                <View style={styles.buttonLoadingContent}>
                  <ActivityIndicator size="small" color="#ffffff" />
                  <Text style={styles.submitButtonText}>Verifying...</Text>
                </View>
              ) : (
                <Text style={styles.submitButtonText}>Verify & Continue</Text>
              )}
            </TouchableOpacity>

            <TouchableOpacity
              style={[styles.cancelButton, isSubmitting && styles.buttonDisabled]}
              onPress={onCancel}
              disabled={isSubmitting}
            >
              <Text style={styles.cancelButtonText}>Cancel</Text>
            </TouchableOpacity>
          </View>
        </View>
      </KeyboardAvoidingView>
    </Modal>
  );
};

const styles = StyleSheet.create({
  modalOverlay: {
    flex: 1,
    backgroundColor: 'rgba(0, 0, 0, 0.75)',
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  modalContainer: {
    backgroundColor: '#ffffff',
    borderRadius: 16,
    width: '100%',
    maxWidth: 480,
    maxHeight: '80%',
    elevation: 10,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.3,
    shadowRadius: 8,
  },
  modalHeader: {
    backgroundColor: '#3b82f6',
    padding: 20,
    borderTopLeftRadius: 16,
    borderTopRightRadius: 16,
    alignItems: 'center',
  },
  modalTitle: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#ffffff',
    textAlign: 'center',
    marginBottom: 8,
  },
  modalSubtitle: {
    fontSize: 14,
    color: '#dbeafe',
    textAlign: 'center',
    lineHeight: 20,
  },
  scrollContainer: {
    flexGrow: 0,
  },
  contentContainer: {
    padding: 20,
  },
  attemptsContainer: {
    borderRadius: 8,
    padding: 12,
    marginBottom: 16,
    alignItems: 'center',
  },
  attemptsText: {
    fontSize: 14,
    fontWeight: '600',
  },
  errorContainer: {
    backgroundColor: '#fef2f2',
    borderRadius: 8,
    padding: 12,
    marginBottom: 16,
    borderLeftWidth: 4,
    borderLeftColor: '#dc2626',
  },
  errorText: {
    fontSize: 14,
    color: '#7f1d1d',
    lineHeight: 20,
    textAlign: 'center',
  },
  inputContainer: {
    marginBottom: 16,
  },
  inputLabel: {
    fontSize: 14,
    fontWeight: '600',
    color: '#374151',
    marginBottom: 8,
  },
  passwordInputWrapper: {
    flexDirection: 'row',
    alignItems: 'center',
    borderWidth: 1,
    borderColor: '#d1d5db',
    borderRadius: 8,
    backgroundColor: '#ffffff',
  },
  passwordInput: {
    flex: 1,
    padding: 12,
    fontSize: 16,
    color: '#1f2937',
  },
  visibilityButton: {
    padding: 12,
  },
  visibilityIcon: {
    fontSize: 20,
  },
  buttonContainer: {
    padding: 20,
    borderTopWidth: 1,
    borderTopColor: '#f3f4f6',
  },
  submitButton: {
    backgroundColor: '#3b82f6',
    padding: 16,
    borderRadius: 8,
    alignItems: 'center',
    marginBottom: 12,
    elevation: 2,
  },
  submitButtonText: {
    color: '#ffffff',
    fontSize: 16,
    fontWeight: '600',
  },
  cancelButton: {
    backgroundColor: '#f3f4f6',
    padding: 16,
    borderRadius: 8,
    alignItems: 'center',
  },
  cancelButtonText: {
    color: '#6b7280',
    fontSize: 16,
    fontWeight: '600',
  },
  buttonDisabled: {
    opacity: 0.6,
  },
  buttonLoadingContent: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 8,
  },
});

export default PasswordValidationDialog;

Export PasswordValidationDialog

Update src/uniken/components/modals/index.ts:

export {default as ThreatDetectionModal} from './ThreatDetectionModal';
export {default as SessionModal} from './SessionModal';
export {default as StepUpPasswordDialog} from './StepUpPasswordDialog';
export {default as PasswordValidationDialog} from './PasswordValidationDialog';

Key Points

PasswordValidationDialog UI Preview

Password Validation Dialog Modal

The PasswordValidationDialog shows password input field, attempts counter with color coding (green/orange/red), error messages, and action buttons for verify and cancel.

Overview

Implement biometric opt-in APIs in the IDV service layer. These APIs handle template creation workflow with LDA fallback and selfie capture.

Implement Opt-In Initiation API

Add the following method to rdnaIDVService.ts:

// src/uniken/services/idv/rdnaIDVService.ts

import RdnaClient from 'react-native-rdna-client/src/index';
import type { RDNASyncResponse } from '../../types/rdnaEvents';

class RdnaIDVService {
  // ... existing methods

  /**
   * Get IDV Password for Biometric Opt-In
   *
   * Initiates biometric opt-in workflow with LDA fallback support.
   * SDK automatically checks LDA availability and falls back to password dialog if needed.
   *
   * @param password - Empty string initially (SDK will handle LDA/password logic)
   * @param challengeMode - Challenge mode: 6 (biometric opt-in)
   * @returns Promise that resolves with sync response on successful initiation
   * @throws Promise that rejects with sync response on failure
   *
   * Flow:
   * 1. SDK checks LDA availability
   *    - If LDA available: Shows LDA prompt (fingerprint/face/PIN)
   *    - If LDA succeeds: Proceeds to password verification event
   *    - If LDA cancelled or unavailable: Shows PasswordValidationDialog
   * 2. SDK triggers GetPasswordVerification event (idvWorkflow: 10)
   *    - If valid: SDK initiates selfie capture
   *    - If invalid: Shows error, stays on dialog
   * 3. SDK triggers GetSelfieProcessStartConfirmation event (idvWorkflow: 10)
   * 4. After selfie capture and confirmation:
   *    - SDK triggers RDNAIDVBiometricOptInStatusResponse event (challengeMode: 6)
   *
   * Success Response:
   * - error.longErrorCode = 0
   *
   * Error Responses:
   * - Sync error: error.longErrorCode !== 0
   * - Backend error: Received in status events
   */
  async getIDVPasswordForBiometricOptIn(
    password: string,
    challengeMode: number
  ): Promise<RDNASyncResponse> {
    return new Promise((resolve, reject) => {
      console.log('RdnaIDVService - Getting IDV password for biometric opt-in');
      console.log('RdnaIDVService - Challenge Mode:', challengeMode);

      RdnaClient.getIDVPasswordForBiometricOptIn(password, challengeMode, response => {
        console.log('RdnaIDVService - GetIDVPasswordForBiometricOptIn sync callback received');
        const result: RDNASyncResponse = response;

        if (result.error && result.error.longErrorCode === 0) {
          console.log('RdnaIDVService - GetIDVPasswordForBiometricOptIn sync response success');
          console.log('RdnaIDVService - SDK will check LDA or show password dialog');
          resolve(result);
        } else {
          console.error('RdnaIDVService - GetIDVPasswordForBiometricOptIn sync response error:', result);
          reject(result);
        }
      });
    });
  }
}

Implement Opt-In Confirmation API

Add the following method to rdnaIDVService.ts:

  /**
   * Set IDV Biometric Opt-In Confirmation
   *
   * Submits user's decision on the captured selfie frame.
   *
   * @param status - User action:
   *                 0 = Approved (create template on server)
   *                 1 = Recapture (try again, triggers new selfie capture)
   *                 2 = Cancel (abort opt-in flow)
   * @returns Promise that resolves with sync response on successful submission
   * @throws Promise that rejects with sync response on failure
   *
   * Flow:
   * 1. SDK processes user action:
   *    - Approved (0): Creates biometric template on server
   *    - Recapture (1): Triggers new selfie capture (new RDNAIDVOptInCapturedFrameConfirmation)
   *    - Cancel (2): Aborts opt-in process
   * 2. SDK triggers RDNAIDVBiometricOptInStatusResponse event (challengeMode: 6)
   *    - statusCode: 100 = Success (template created)
   *    - statusCode: Other = Failed
   *
   * Success Response:
   * - error.longErrorCode = 0
   *
   * Error Responses:
   * - Sync error: error.longErrorCode !== 0
   * - Backend error: Received in status event
   *
   * IMPORTANT: Wrap this call in setTimeout(() => {}, 0) to ensure main thread execution
   */
  async setIDVBiometricOptInConfirmation(status: number): Promise<RDNASyncResponse> {
    return new Promise((resolve, reject) => {
      console.log('RdnaIDVService - Setting biometric opt-in confirmation with status:', status);

      RdnaClient.setIDVBiometricOptInConfirmation(status, response => {
        const result: RDNASyncResponse = response;
        if (result.error && result.error.longErrorCode === 0) {
          console.log('RdnaIDVService - SetIDVBiometricOptInConfirmation sync response success');
          resolve(result);
        } else {
          console.error('RdnaIDVService - SetIDVBiometricOptInConfirmation sync response error:', result);
          reject(result);
        }
      });
    });
  }
}

export default new RdnaIDVService();

Key Points

Overview

Implement biometric opt-out APIs in the IDV service layer. These APIs handle template deletion workflow with LDA fallback (no selfie capture).

Implement Opt-Out Initiation API

Add the following method to rdnaIDVService.ts:

// src/uniken/services/idv/rdnaIDVService.ts

  /**
   * Get IDV Password for Biometric Opt-Out
   *
   * Initiates biometric opt-out workflow with LDA fallback support.
   * SDK automatically checks LDA availability and falls back to password dialog if needed.
   *
   * @param password - Empty string initially (SDK will handle LDA/password logic)
   * @param challengeMode - Challenge mode: 7 (biometric opt-out)
   * @returns Promise that resolves with sync response on successful initiation
   * @throws Promise that rejects with sync response on failure
   *
   * Flow:
   * 1. SDK checks LDA availability
   *    - If LDA available: Shows LDA prompt (fingerprint/face/PIN)
   *    - If LDA succeeds: Proceeds to password verification event
   *    - If LDA cancelled or unavailable: Shows PasswordValidationDialog
   * 2. SDK triggers GetPasswordVerification event (idvWorkflow: 11)
   *    - If valid: SDK directly deletes template from server (NO selfie capture)
   *    - If invalid: Shows error, stays on dialog
   * 3. SDK triggers RDNAIDVBiometricOptOutStatusResponse event (challengeMode: 7)
   *
   * Success Response:
   * - error.longErrorCode = 0
   *
   * Error Responses:
   * - Sync error: error.longErrorCode !== 0
   * - Backend error: Received in status event
   */
  async getIDVPasswordForBiometricOptOut(
    password: string,
    challengeMode: number
  ): Promise<RDNASyncResponse> {
    return new Promise((resolve, reject) => {
      console.log('RdnaIDVService - Getting IDV password for biometric opt-out');
      console.log('RdnaIDVService - Challenge Mode:', challengeMode);

      RdnaClient.getIDVPasswordForBiometricOptOut(password, challengeMode, response => {
        console.log('RdnaIDVService - GetIDVPasswordForBiometricOptOut sync callback received');
        const result: RDNASyncResponse = response;

        if (result.error && result.error.longErrorCode === 0) {
          console.log('RdnaIDVService - GetIDVPasswordForBiometricOptOut sync response success');
          console.log('RdnaIDVService - SDK will check LDA or show password dialog');
          resolve(result);
        } else {
          console.error('RdnaIDVService - GetIDVPasswordForBiometricOptOut sync response error:', result);
          reject(result);
        }
      });
    });
  }
}

export default new RdnaIDVService();

Key Points

Overview

Add TypeScript interfaces for opt-in and opt-out status events. These events are triggered by the SDK after template operations complete.

Implement Opt-In Status Event Types

Add the following type definitions to rdnaIDVEvents.ts:

// src/uniken/types/idv/rdnaIDVEvents.ts

import type { RDNAError, RDNAStatus } from '../rdnaEvents';
import { RDNAChallengeOpMode } from 'react-native-rdna-client/src/index';

// ... existing IDV event types

// ================================================================================================
// IDV BIOMETRIC OPT-IN EVENT TYPES
// ================================================================================================

/**
 * RDNA IDV Biometric Opt-In Status Event Data
 * Response event triggered after biometric opt-in process completes
 *
 * This event is triggered when:
 * 1. User approves captured selfie and opt-in completes (success or failure)
 * 2. User cancels opt-in flow
 * 3. Backend returns an error during opt-in processing
 *
 * Challenge Mode: 6 (biometric opt-in)
 *
 * Response Structure:
 * - error: SDK error information
 *   - longErrorCode: 0 = success, others = errors
 * - status: Backend status information
 *   - statusCode: 100 = success (template created), others = failure
 *   - statusMessage: Human-readable status message
 * - idvResponse: JSON string (not typically parsed for opt-in)
 *
 * Validation Pattern:
 * 1. Check error.longErrorCode first (0 = OK)
 * 2. Then check status.statusCode (100 = template created successfully)
 *
 * Application should:
 * - Display success message when statusCode === 100
 * - Show error message for other status codes
 * - Reset flow state refs after processing
 */
export interface RDNAIDVBiometricOptInStatusData {
  /** User ID */
  userID: string;

  /** Remaining attempts */
  attemptsLeft: number;

  /** Challenge mode: 6 (biometric opt-in) */
  challengeMode: RDNAChallengeOpMode;

  /** JSON string - contains additional template info (not typically parsed) */
  idvResponse: string;

  /** Backend status information */
  status: RDNAStatus;

  /** SDK error information */
  error: RDNAError;
}

/**
 * Parsed structure of idvResponse from opt-in status event (if needed)
 */
export interface RDNAIDVBiometricOptInStatusResponse {
  /** Status code: 100 = Success (template created) */
  status_code?: number;

  /** Status message */
  status_message?: string;

  /** IL status code */
  il_status_code?: number;

  /** User ID */
  user_id?: string;

  /** For user ID */
  for_user_id?: string | null;

  /** Orchestration use case */
  orchestration_use_case?: string;

  /** Result boolean */
  result?: boolean;

  /** IDV audit information */
  idv_audit_info?: {
    orchestration_use_case?: string;
    reason?: string;
    idv_json_version?: string;
    idv_audit_transaction_id?: string;
    user_id?: string;
  };

  /** Error information */
  error?: {
    code: string;
    description: string;
  };
}

Implement Opt-Out Status Event Types

Add the following type definitions to rdnaIDVEvents.ts:

// ================================================================================================
// IDV BIOMETRIC OPT-OUT EVENT TYPES
// ================================================================================================

/**
 * RDNA IDV Biometric Opt-Out Status Event Data
 * Response event triggered after biometric opt-out process completes
 *
 * This event is triggered when:
 * 1. Template deletion completes (success or failure)
 * 2. Backend returns an error during opt-out processing
 *
 * Challenge Mode: 7 (biometric opt-out)
 *
 * Response Structure:
 * - error: SDK error information
 *   - longErrorCode: 0 = success, others = errors
 * - status: Backend status information
 *   - statusCode: 100 = success (template deleted), others = failure
 *   - statusMessage: Human-readable status message
 * - idvResponse: JSON string (not typically parsed for opt-out)
 *
 * Validation Pattern:
 * 1. Check error.longErrorCode first (0 = OK)
 * 2. Then check status.statusCode (100 = template deleted successfully)
 *
 * Application should:
 * - Display success message when statusCode === 100
 * - Show error message for other status codes
 * - Call checkTemplateStatus() to refresh UI after opt-out
 */
export interface RDNAIDVBiometricOptOutStatusData {
  /** User ID */
  userID: string;

  /** Remaining attempts */
  attemptsLeft: number;

  /** Challenge mode: 7 (biometric opt-out) */
  challengeMode: RDNAChallengeOpMode;

  /** JSON string - contains additional info (not typically parsed) */
  idvResponse: string;

  /** Backend status information */
  status: RDNAStatus;

  /** SDK error information */
  error: RDNAError;
}

/**
 * Parsed structure of idvResponse from opt-out status event (if needed)
 */
export interface RDNAIDVBiometricOptOutStatusResponse {
  /** Status code: 100 = Success (template deleted) */
  status_code?: number;

  /** Status message */
  status_message?: string;

  /** IL status code */
  il_status_code?: number;

  /** User ID */
  user_id?: string | null;

  /** For user ID */
  for_user_id?: string | null;

  /** Orchestration use case */
  orchestration_use_case?: string | null;

  /** Result boolean */
  result?: boolean;

  /** Error information */
  error?: {
    code: string;
    description: string;
  };
}

Key Points

Overview

Implement event listeners and handlers for opt-in and opt-out status events in the IDV Event Manager. Also support event handler chaining for GetPasswordVerification and GetSelfieProcessStartConfirmation events.

Implement Opt-In & Opt-Out Event Handlers

Add the following to rdnaIDVEventManager.ts:

// src/uniken/services/idv/rdnaIDVEventManager.ts

import { NativeEventEmitter, NativeModules } from 'react-native';
import type {
  RDNAIDVBiometricOptInStatusData,
  RDNAIDVBiometricOptOutStatusData,
} from '../../types/idv/rdnaIDVEvents';

class RdnaIDVEventManager {
  // ... existing properties

  private biometricOptInStatusHandler?:
    (data: RDNAIDVBiometricOptInStatusData) => void;

  private biometricOptOutStatusHandler?:
    (data: RDNAIDVBiometricOptOutStatusData) => void;

  constructor() {
    // ... existing constructor code

    // Register opt-in status event listener
    this.listeners.push(
      this.rdnaEmitter.addListener(
        'onBiometricOptInStatus',
        this.onBiometricOptInStatus.bind(this)
      )
    );

    // Register opt-out status event listener
    this.listeners.push(
      this.rdnaEmitter.addListener(
        'onBiometricOptOutStatus',
        this.onBiometricOptOutStatus.bind(this)
      )
    );
  }

  /**
   * Biometric Opt-In Status Event Handler
   * Triggered after opt-in process completes
   * Challenge Mode: 6
   */
  private onBiometricOptInStatus(response: RDNAJsonResponse) {
    console.log('RdnaIDVEventManager - Biometric opt-in status event received');

    try {
      console.log('RdnaIDVEventManager - onBiometricOptInStatus response:', response.response);
      const data: RDNAIDVBiometricOptInStatusData = JSON.parse(response.response);

      console.log('RdnaIDVEventManager - User ID:', data.userID);
      console.log('RdnaIDVEventManager - Attempts Left:', data.attemptsLeft);
      console.log('RdnaIDVEventManager - Challenge Mode:', data.challengeMode);
      console.log('RdnaIDVEventManager - Status Code:', data.status?.statusCode);
      console.log('RdnaIDVEventManager - Status Message:', data.status?.statusMessage);
      console.log('RdnaIDVEventManager - Error Code:', data.error?.longErrorCode);
      console.log('RdnaIDVEventManager - Error String:', data.error?.errorString);

      if (this.biometricOptInStatusHandler) {
        this.biometricOptInStatusHandler(data);
      } else {
        console.warn('RdnaIDVEventManager - No handler registered for biometric opt-in status');
      }
    } catch (error) {
      console.error('RdnaIDVEventManager - Failed to parse biometric opt-in status:', error);
    }
  }

  /**
   * Biometric Opt-Out Status Event Handler
   * Triggered after opt-out process completes
   * Challenge Mode: 7
   */
  private onBiometricOptOutStatus(response: RDNAJsonResponse) {
    console.log('RdnaIDVEventManager - Biometric opt-out status event received');

    try {
      console.log('RdnaIDVEventManager - onBiometricOptOutStatus response:', response.response);
      const data: RDNAIDVBiometricOptOutStatusData = JSON.parse(response.response);

      console.log('RdnaIDVEventManager - User ID:', data.userID);
      console.log('RdnaIDVEventManager - Attempts Left:', data.attemptsLeft);
      console.log('RdnaIDVEventManager - Challenge Mode:', data.challengeMode);
      console.log('RdnaIDVEventManager - Status Code:', data.status?.statusCode);
      console.log('RdnaIDVEventManager - Status Message:', data.status?.statusMessage);
      console.log('RdnaIDVEventManager - Error Code:', data.error?.longErrorCode);
      console.log('RdnaIDVEventManager - Error String:', data.error?.errorString);

      if (this.biometricOptOutStatusHandler) {
        this.biometricOptOutStatusHandler(data);
      } else {
        console.warn('RdnaIDVEventManager - No handler registered for biometric opt-out status');
      }
    } catch (error) {
      console.error('RdnaIDVEventManager - Failed to parse biometric opt-out status:', error);
    }
  }

  /**
   * Set handler for opt-in status event
   */
  setBiometricOptInStatusHandler(
    handler: ((data: RDNAIDVBiometricOptInStatusData) => void) | undefined
  ) {
    console.log('RdnaIDVEventManager - Setting biometric opt-in status handler');
    this.biometricOptInStatusHandler = handler;
  }

  /**
   * Set handler for opt-out status event
   */
  setBiometricOptOutStatusHandler(
    handler: ((data: RDNAIDVBiometricOptOutStatusData) => void) | undefined
  ) {
    console.log('RdnaIDVEventManager - Setting biometric opt-out status handler');
    this.biometricOptOutStatusHandler = handler;
  }

  /**
   * Get current password verification handler (for chaining)
   */
  getPasswordVerificationHandler() {
    return this.passwordVerificationHandler;
  }

  /**
   * Get current selfie process start confirmation handler (for chaining)
   */
  getSelfieProcessStartConfirmationHandler() {
    return this.selfieProcessStartConfirmationHandler;
  }

  // ... existing methods
}

export default new RdnaIDVEventManager();

Key Points

Overview

Implement the biometric opt-in screen with LDA fallback, password dialog, handler chaining, selfie capture, and conditional modal behavior. This is the most complex screen in the codelab.

Screen Setup and State Management

Create BiometricOptInScreen.tsx:

// src/tutorial/screens/idv/biometricOptIn/BiometricOptInScreen.tsx

import React, { useState, useCallback, useEffect, useRef } from 'react';
import {
  View,
  Text,
  StyleSheet,
  Alert,
  ActivityIndicator,
  Modal,
  Image,
  TouchableOpacity,
} from 'react-native';
import { useFocusEffect } from '@react-navigation/native';
import rdnaIDVService from '../../../../uniken/services/idv/rdnaIDVService';
import type { RDNASyncResponse } from '../../../../uniken/types/rdnaEvents';
import type {
  RDNAIDVCheckUserBiometricTemplateStatusData,
  RDNAIDVBiometricOptInStatusData,
  RDNAIDVGetPasswordVerificationData,
  RDNAIDVGetSelfieProcessStartConfirmationData,
  RDNAIDVOptInCapturedFrameConfirmationData,
} from '../../../../uniken/types/idv/rdnaIDVEvents';
import { RDNASyncUtils } from '../../../../uniken/types/rdnaEvents';
import { Button } from '../../components/Button';
import { PasswordValidationDialog } from '../../../../uniken/components/modals';
import { IDVStatus } from '../../../../uniken/types/idv/rdnaIDVEvents';

const BiometricOptInScreen: React.FC<{ navigation: any }> = ({ navigation }) => {
  // Template status state
  const [templateStatus, setTemplateStatus] = useState<{
    checked: boolean;
    exists: boolean;
    statusCode: number;
    statusMessage: string;
  }>({
    checked: false,
    exists: false,
    statusCode: 0,
    statusMessage: '',
  });

  // Loading states
  const [isCheckingStatus, setIsCheckingStatus] = useState(false);
  const [isOptingIn, setIsOptingIn] = useState(false);

  // Password dialog state
  const [showPasswordDialog, setShowPasswordDialog] = useState(false);
  const [passwordError, setPasswordError] = useState('');
  const [isPasswordSubmitting, setIsPasswordSubmitting] = useState(false);
  const [attemptsLeft, setAttemptsLeft] = useState(3);

  // Captured image modal state
  const [capturedImageModal, setCapturedImageModal] = useState<{
    visible: boolean;
    imageBase64: string;
  }>({
    visible: false,
    imageBase64: '',
  });

  // Ref to track if opt-in flow is active (survives remounts and focus effects)
  const isOptInFlowActiveRef = useRef(false);

  // ... continue with handlers in next section
};

Implement Template Status Check

  /**
   * Handles check user biometric template status response event
   * Status Codes:
   * - 100 = Template present (user can opt-out, should NOT opt-in)
   * - 600 = Template does not exist (user can opt-in)
   * - 400/500 = Error
   */
  const handleCheckUserBiometricTemplateStatusResponse = useCallback(
    (data: RDNAIDVCheckUserBiometricTemplateStatusData) => {
      console.log('BiometricOptInScreen - Check user biometric template status response received');

      const errorCode = data.error?.longErrorCode;
      const statusCode = data.status?.statusCode;
      const statusMessage = data.status?.statusMessage || 'Unknown status';

      // First check: error code
      if (errorCode !== 0) {
        setTemplateStatus({
          checked: true,
          exists: false,
          statusCode: statusCode || 0,
          statusMessage: `Error: ${data.error?.errorString}`,
        });
        setIsCheckingStatus(false);
        return;
      }

      // Second check: status code
      if (statusCode === 100) {
        // Template already exists - should NOT opt-in
        setTemplateStatus({
          checked: true,
          exists: true,
          statusCode: statusCode,
          statusMessage: statusMessage,
        });
      } else if (statusCode === 600) {
        // Template does not exist - CAN opt-in
        setTemplateStatus({
          checked: true,
          exists: false,
          statusCode: statusCode,
          statusMessage: statusMessage,
        });
      } else {
        // Other status codes (400/500 = error)
        setTemplateStatus({
          checked: true,
          exists: false,
          statusCode: statusCode || 0,
          statusMessage: statusMessage,
        });
      }

      setIsCheckingStatus(false);
    },
    []
  );

  /**
   * Checks if user biometric template exists on server
   */
  const checkTemplateStatus = useCallback(async () => {
    console.log('BiometricOptInScreen - Checking biometric template status');
    setIsCheckingStatus(true);

    try {
      const syncResponse: RDNASyncResponse = await rdnaIDVService.checkIDVUserBiometricTemplateStatus();
      console.log('BiometricOptInScreen - CheckIDVUserBiometricTemplateStatus sync response successful');
      // The actual result will come via onIDVCheckUserBiometricTemplateStatus event
    } catch (error) {
      console.error('BiometricOptInScreen - CheckIDVUserBiometricTemplateStatus sync error:', error);

      const result: RDNASyncResponse = error as RDNASyncResponse;
      const errorMessage = RDNASyncUtils.getErrorMessage(result);

      setTemplateStatus({
        checked: true,
        exists: false,
        statusCode: 0,
        statusMessage: `Error: ${errorMessage}`,
      });
      setIsCheckingStatus(false);

      Alert.alert(
        'Template Status Check Error',
        `Failed to check template status: ${errorMessage}`,
        [{ text: 'OK' }]
      );
    }
  }, []);

Implement Opt-In Initiation

  /**
   * Initiates biometric opt-in workflow
   * SDK will check LDA availability and show password dialog if needed
   */
  const handleOptIn = async () => {
    console.log('BiometricOptInScreen - Starting biometric opt-in');
    setIsOptingIn(true);
    setPasswordError('');
    setAttemptsLeft(3);

    try {
      const password = ''; // Empty string - SDK will check LDA
      const challengeMode = 6; // Biometric opt-in challenge mode

      console.log('BiometricOptInScreen - Calling getIDVPasswordForBiometricOptIn');
      const syncResponse: RDNASyncResponse = await rdnaIDVService.getIDVPasswordForBiometricOptIn(
        password,
        challengeMode
      );
      console.log('BiometricOptInScreen - GetIDVPasswordForBiometricOptIn sync response successful');

      // SDK will now:
      // 1. Check LDA availability
      // 2. Show LDA prompt if available, or password dialog if not
      // 3. Trigger GetPasswordVerification event (idvWorkflow: 10)
    } catch (error) {
      console.error('BiometricOptInScreen - GetIDVPasswordForBiometricOptIn sync error:', error);

      const result: RDNASyncResponse = error as RDNASyncResponse;
      const errorMessage = RDNASyncUtils.getErrorMessage(result);

      Alert.alert('Opt-In Error', `Failed to initiate opt-in: ${errorMessage}`, [{ text: 'OK' }]);
      setIsOptingIn(false);
    }
  };

BiometricOptInScreen UI Preview

Biometric Opt-In Screen

The BiometricOptInScreen shows template status check, opt-in button (enabled when no template exists), and loading states for status check and opt-in process.

Implement Event Handler Chaining

Event handler chaining allows screen-specific logic while preserving global handlers from SDKIDVEventProvider:

  /**
   * Handler for GetPasswordVerification event (idvWorkflow: 10)
   * Chains with global handler from SDKIDVEventProvider
   */
  const handleGetPasswordVerification = useCallback(
    (data: RDNAIDVGetPasswordVerificationData, originalHandler?) => {
      console.log('BiometricOptInScreen - GetPasswordVerification event received');

      const errorCode = data.error?.longErrorCode;

      if (errorCode === 0) {
        // Password validated successfully (or LDA succeeded)
        console.log('BiometricOptInScreen - Password verified successfully for opt-in');
        setPasswordError('');
        // Keep dialog open - it will close when selfie process starts
      } else {
        // Password validation failed
        console.log('BiometricOptInScreen - Password verification failed:', data.error?.errorString);
        setPasswordError(data.error?.errorString || 'Password verification failed');
        setIsPasswordSubmitting(false);

        // Update attempts left
        if (data.attemptsLeft !== undefined) {
          setAttemptsLeft(data.attemptsLeft);
        }

        // Show password dialog if not already visible
        if (!showPasswordDialog) {
          setShowPasswordDialog(true);
        }
      }

      // Call original global handler
      if (originalHandler) {
        originalHandler(data);
      }
    },
    [showPasswordDialog]
  );

  /**
   * Handler for GetSelfieProcessStartConfirmation event (idvWorkflow: 10)
   * Closes password dialog as authentication is complete
   * Chains with global handler from SDKIDVEventProvider
   */
  const handleGetSelfieProcessStartConfirmation = useCallback(
    (data: RDNAIDVGetSelfieProcessStartConfirmationData, originalHandler?) => {
      console.log('BiometricOptInScreen - GetSelfieProcessStartConfirmation event received');

      // Close password dialog as authentication is complete
      setShowPasswordDialog(false);
      setPasswordError('');

      // Call original global handler to show selfie screen
      if (originalHandler) {
        originalHandler(data);
      }
    },
    []
  );

Implement Captured Frame Confirmation with Parent Navigator

  /**
   * Handler for OptInCapturedFrameConfirmation event
   * Pops selfie screen from parent navigator and shows modal
   */
  const handleOptInCapturedFrameConfirmation = useCallback(
    (data: RDNAIDVOptInCapturedFrameConfirmationData) => {
      console.log('BiometricOptInScreen - Opt-in captured frame confirmation received');

      setShowPasswordDialog(false);
      setIsPasswordSubmitting(false);

      // Mark flow as active
      isOptInFlowActiveRef.current = true;

      // Pop IDVSelfieProcessStart screen from root stack if it's on top
      const parentNav = navigation.getParent();
      if (parentNav) {
        const state = parentNav.getState();
        const currentRoute = state.routes[state.index];

        if (currentRoute?.name === 'IDVSelfieProcessStart') {
          console.log('BiometricOptInScreen - Popping IDVSelfieProcessStart screen from parent navigator');
          parentNav.goBack();
        } else {
          console.log('BiometricOptInScreen - No IDVSelfieProcessStart screen on top (recapture case)');
        }
      }

      // Show modal immediately with captured image
      setCapturedImageModal({
        visible: true,
        imageBase64: data.capturedImage,
      });
    },
    [navigation]
  );

Implement Conditional Modal Close with Main Thread Execution

  /**
   * Handler for captured frame actions (Approve/Recapture/Cancel)
   * Implements conditional modal close (keep open for recapture)
   */
  const handleCapturedFrameAction = (action: number) => {
    console.log('BiometricOptInScreen - Captured frame action:', action);

    // Close modal for Approve/Cancel, but KEEP IT OPEN for Recapture
    if (action !== IDVStatus.RDNA_IDV_RECAPTURE) {
      setCapturedImageModal(prev => ({
        ...prev,
        visible: false,
      }));
    }

    // Use setTimeout to ensure API call happens on main thread
    setTimeout(async () => {
      try {
        const syncResponse = await rdnaIDVService.setIDVBiometricOptInConfirmation(action);
        console.log('BiometricOptInScreen - Opt-in confirmation successful');

        // For recapture (action: 1), new captured frame event will fire
        // For approve/cancel (action: 0/2), status event will fire
      } catch (error) {
        console.error('BiometricOptInScreen - Opt-in confirmation error:', error);

        // Reset ref on error
        isOptInFlowActiveRef.current = false;

        setCapturedImageModal({
          visible: false,
          imageBase64: '',
        });

        const result = error as RDNASyncResponse;
        const errorMessage = RDNASyncUtils.getErrorMessage(result);
        Alert.alert('Confirmation Error', errorMessage);
      }
    }, 0);
  };

Implement Opt-In Status Response Handler

  /**
   * Handler for opt-in status response event
   * Resets flow state and displays result
   */
  const handleBiometricOptInStatusResponse = useCallback((data: RDNAIDVBiometricOptInStatusData) => {
    console.log('BiometricOptInScreen - Biometric opt-in status response received');

    setTimeout(() => {
      setIsOptingIn(false);

      // Reset ref
      isOptInFlowActiveRef.current = false;

      const errorCode = data.error?.longErrorCode;
      const statusCode = data.status?.statusCode;
      const statusMessage = data.status?.statusMessage;

      // First check: error code
      if (errorCode !== 0) {
        Alert.alert('Opt-In Failed', data.error?.errorString);
        return;
      }

      // Second check: status code
      if (statusCode === 100) {
        Alert.alert(
          'Opt-In Successful',
          `${statusMessage}\n\nYour biometric template has been created on the server.`,
          [{ text: 'OK', onPress: () => checkTemplateStatus() }]
        );
      } else {
        Alert.alert('Opt-In Failed', statusMessage || 'Failed to complete biometric opt-in');
      }
    }, 300);
  }, [checkTemplateStatus]);

  /**
   * Handler for password dialog submission
   */
  const handlePasswordSubmit = (password: string) => {
    console.log('BiometricOptInScreen - Password submitted');
    setIsPasswordSubmitting(true);
    setPasswordError('');

    // Password will be validated by SDK, GetPasswordVerification event will fire
    // We don't need to call any API here - just wait for the event
  };

  /**
   * Handler for password dialog cancellation
   */
  const handlePasswordCancel = () => {
    console.log('BiometricOptInScreen - Password dialog cancelled');
    setShowPasswordDialog(false);
    setPasswordError('');
    setIsPasswordSubmitting(false);
    setIsOptingIn(false);

    // Reset ref on cancel
    isOptInFlowActiveRef.current = false;
  };

Implement Event Handler Lifecycle

Register event handlers with proper lifecycle management:

  /**
   * Register event handlers on component mount
   * Uses useEffect (not useFocusEffect) to ensure handlers persist during native UI workflows
   */
  useEffect(() => {
    console.log('BiometricOptInScreen - Component mounted, registering event handlers');

    const eventManager = rdnaIDVService.getEventManager();

    // Get original global handlers for chaining
    const originalPasswordHandler = eventManager.getPasswordVerificationHandler?.();
    const originalSelfieStartHandler = eventManager.getSelfieProcessStartConfirmationHandler?.();

    // Register chained handlers
    eventManager.setPasswordVerificationHandler((data) =>
      handleGetPasswordVerification(data, originalPasswordHandler)
    );

    eventManager.setSelfieProcessStartConfirmationHandler((data) =>
      handleGetSelfieProcessStartConfirmation(data, originalSelfieStartHandler)
    );

    eventManager.setCheckUserBiometricTemplateStatusHandler(handleCheckUserBiometricTemplateStatusResponse);
    eventManager.setOptInCapturedFrameConfirmationHandler(handleOptInCapturedFrameConfirmation);
    eventManager.setBiometricOptInStatusHandler(handleBiometricOptInStatusResponse);

    // Cleanup: remove handlers on unmount
    return () => {
      console.log('BiometricOptInScreen - Component unmounting, cleaning up event handlers');
      eventManager.setPasswordVerificationHandler(undefined);
      eventManager.setSelfieProcessStartConfirmationHandler(undefined);
      eventManager.setCheckUserBiometricTemplateStatusHandler(undefined);
      eventManager.setOptInCapturedFrameConfirmationHandler(undefined);
      eventManager.setBiometricOptInStatusHandler(undefined);
    };
  }, [
    handleGetPasswordVerification,
    handleGetSelfieProcessStartConfirmation,
    handleCheckUserBiometricTemplateStatusResponse,
    handleOptInCapturedFrameConfirmation,
    handleBiometricOptInStatusResponse
  ]);

  /**
   * Focus effect with ref guard to prevent duplicate API calls
   */
  useFocusEffect(
    useCallback(() => {
      console.log('BiometricOptInScreen - Screen focused');

      // Reset ref if modal not visible (user navigated away)
      if (isOptInFlowActiveRef.current && !capturedImageModal.visible) {
        console.log('BiometricOptInScreen - Resetting ref (navigated away)');
        isOptInFlowActiveRef.current = false;
      }

      // Skip reset if flow active
      if (isOptInFlowActiveRef.current) {
        console.log('BiometricOptInScreen - Opt-in flow active, skipping reset');
        return;
      }

      // Reset UI state
      setIsOptingIn(false);
      setIsCheckingStatus(false);
      setShowPasswordDialog(false);
      setPasswordError('');
      setIsPasswordSubmitting(false);

      // Check template status
      checkTemplateStatus();
    }, [checkTemplateStatus, capturedImageModal.visible])
  );

Implement UI with Captured Frame Modal

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Biometric Opt-In</Text>
      <Text style={styles.subtitle}>
        Create your biometric template on the server
      </Text>

      {/* Template Status Display */}
      {isCheckingStatus ? (
        <View style={styles.statusContainer}>
          <ActivityIndicator size="large" color="#3498db" />
          <Text style={styles.statusText}>Checking template status...</Text>
        </View>
      ) : templateStatus.checked ? (
        <View style={[
          styles.statusContainer,
          templateStatus.exists ? styles.statusWarning : styles.statusSuccess
        ]}>
          <Text style={styles.statusIcon}>
            {templateStatus.exists ? '⚠️' : '✅'}
          </Text>
          <Text style={styles.statusText}>
            {templateStatus.exists
              ? 'Template already exists on the server'
              : 'No template found - you can opt-in'}
          </Text>
          <Text style={styles.statusMessage}>
            {templateStatus.statusMessage}
          </Text>
          {templateStatus.exists && (
            <Text style={styles.helpText}>
              You already have a biometric template. Use "Biometric Opt-Out" to delete it first.
            </Text>
          )}
        </View>
      ) : null}

      {/* Opt-In Button */}
      <View style={styles.buttonContainer}>
        <Button
          title={isOptingIn ? 'Processing...' : 'Opt In to Biometric Authentication'}
          onPress={handleOptIn}
          disabled={templateStatus.exists || isCheckingStatus || isOptingIn}
          loading={isOptingIn}
        />
      </View>

      {/* Password Validation Dialog */}
      <PasswordValidationDialog
        visible={showPasswordDialog}
        title="Biometric Opt-In Authentication"
        message="Please verify your password to enable biometric storage"
        attemptsLeft={attemptsLeft}
        errorMessage={passwordError}
        isSubmitting={isPasswordSubmitting}
        onSubmitPassword={handlePasswordSubmit}
        onCancel={handlePasswordCancel}
      />

      {/* Captured Image Modal */}
      <Modal
        visible={capturedImageModal.visible}
        transparent={true}
        animationType="fade"
      >
        <View style={styles.modalOverlay}>
          <View style={styles.imageModalContainer}>
            <Text style={styles.modalTitle}>Confirm Your Selfie</Text>
            <Text style={styles.modalSubtitle}>
              Please review your captured image
            </Text>

            <Image
              source={{ uri: `data:image/jpeg;base64,${capturedImageModal.imageBase64}` }}
              style={styles.capturedImage}
              resizeMode="contain"
            />

            <View style={styles.modalButtonContainer}>
              <TouchableOpacity
                style={[styles.modalButton, styles.approveButton]}
                onPress={() => handleCapturedFrameAction(IDVStatus.RDNA_IDV_APPROVED)}
              >
                <Text style={styles.modalButtonText}>✓ Approve</Text>
              </TouchableOpacity>

              <TouchableOpacity
                style={[styles.modalButton, styles.recaptureButton]}
                onPress={() => handleCapturedFrameAction(IDVStatus.RDNA_IDV_RECAPTURE)}
              >
                <Text style={styles.modalButtonText}>↻ Recapture</Text>
              </TouchableOpacity>

              <TouchableOpacity
                style={[styles.modalButton, styles.cancelButton]}
                onPress={() => handleCapturedFrameAction(IDVStatus.RDNA_IDV_CANCEL)}
              >
                <Text style={styles.modalButtonText}>✕ Cancel</Text>
              </TouchableOpacity>
            </View>
          </View>
        </View>
      </Modal>

      {/* Info Section */}
      <View style={styles.infoContainer}>
        <Text style={styles.infoTitle}>How it works:</Text>
        <Text style={styles.infoText}>
          1. We check if you already have a template
        </Text>
        <Text style={styles.infoText}>
          2. SDK checks LDA (fingerprint/face/PIN) availability
        </Text>
        <Text style={styles.infoText}>
          3. If LDA unavailable or cancelled, enter password
        </Text>
        <Text style={styles.infoText}>
          4. You capture a selfie with liveness detection
        </Text>
        <Text style={styles.infoText}>
          5. We create your biometric template on the server
        </Text>
      </View>
    </View>
  );
};

// ... styles similar to README

export default BiometricOptInScreen;

Captured Frame Confirmation Modal UI Preview

Biometric Opt-In Captured Frame Confirmation Modal

The Captured Frame Confirmation modal displays the captured selfie image with three action buttons: Approve (creates template), Recapture (retakes selfie, modal stays open), and Cancel (aborts flow).

Key Points

Overview

Implement the biometric opt-out screen with LDA fallback and password dialog. This screen is simpler than opt-in because it doesn't require selfie capture or modal confirmation.

Complete BiometricOptOutScreen Implementation

Create BiometricOptOutScreen.tsx:

// src/tutorial/screens/idv/biometricOptOut/BiometricOptOutScreen.tsx

import React, { useState, useCallback, useEffect } from 'react';
import { View, Text, StyleSheet, Alert, ActivityIndicator } from 'react-native';
import { useFocusEffect } from '@react-navigation/native';
import rdnaIDVService from '../../../../uniken/services/idv/rdnaIDVService';
import type { RDNASyncResponse } from '../../../../uniken/types/rdnaEvents';
import type {
  RDNAIDVCheckUserBiometricTemplateStatusData,
  RDNAIDVBiometricOptOutStatusData,
  RDNAIDVGetPasswordVerificationData,
} from '../../../../uniken/types/idv/rdnaIDVEvents';
import { RDNASyncUtils } from '../../../../uniken/types/rdnaEvents';
import { Button } from '../../components/Button';
import { PasswordValidationDialog } from '../../../../uniken/components/modals';

const BiometricOptOutScreen: React.FC<{ navigation: any }> = ({ navigation }) => {
  // Template status state
  const [templateStatus, setTemplateStatus] = useState<{
    checked: boolean;
    exists: boolean;
    statusCode: number;
    statusMessage: string;
  }>({
    checked: false,
    exists: false,
    statusCode: 0,
    statusMessage: '',
  });

  // Loading states
  const [isCheckingStatus, setIsCheckingStatus] = useState(false);
  const [isOptingOut, setIsOptingOut] = useState(false);

  // Password dialog state
  const [showPasswordDialog, setShowPasswordDialog] = useState(false);
  const [passwordError, setPasswordError] = useState('');
  const [isPasswordSubmitting, setIsPasswordSubmitting] = useState(false);
  const [attemptsLeft, setAttemptsLeft] = useState(3);

  /**
   * Handles check user biometric template status response event
   * Status Codes:
   * - 100 = Template present (user can opt-out)
   * - 600 = Template does not exist (user should NOT opt-out)
   * - 400/500 = Error
   */
  const handleCheckUserBiometricTemplateStatusResponse = useCallback(
    (data: RDNAIDVCheckUserBiometricTemplateStatusData) => {
      console.log('BiometricOptOutScreen - Check user biometric template status response received');

      const errorCode = data.error?.longErrorCode;
      const statusCode = data.status?.statusCode;
      const statusMessage = data.status?.statusMessage || 'Unknown status';

      // First check: error code
      if (errorCode !== 0) {
        setTemplateStatus({
          checked: true,
          exists: false,
          statusCode: statusCode || 0,
          statusMessage: `Error: ${data.error?.errorString}`,
        });
        setIsCheckingStatus(false);
        return;
      }

      // Second check: status code
      if (statusCode === 100) {
        // Template exists - CAN opt-out
        setTemplateStatus({
          checked: true,
          exists: true,
          statusCode: statusCode,
          statusMessage: statusMessage,
        });
      } else if (statusCode === 600) {
        // Template does not exist - should NOT opt-out
        setTemplateStatus({
          checked: true,
          exists: false,
          statusCode: statusCode,
          statusMessage: statusMessage,
        });
      } else {
        // Other status codes (400/500 = error)
        setTemplateStatus({
          checked: true,
          exists: false,
          statusCode: statusCode || 0,
          statusMessage: statusMessage,
        });
      }

      setIsCheckingStatus(false);
    },
    []
  );

  /**
   * Checks if user biometric template exists on server
   */
  const checkTemplateStatus = useCallback(async () => {
    console.log('BiometricOptOutScreen - Checking biometric template status');
    setIsCheckingStatus(true);

    try {
      const syncResponse: RDNASyncResponse = await rdnaIDVService.checkIDVUserBiometricTemplateStatus();
      console.log('BiometricOptOutScreen - CheckIDVUserBiometricTemplateStatus sync response successful');
      // The actual result will come via onIDVCheckUserBiometricTemplateStatus event
    } catch (error) {
      console.error('BiometricOptOutScreen - CheckIDVUserBiometricTemplateStatus sync error:', error);

      const result: RDNASyncResponse = error as RDNASyncResponse;
      const errorMessage = RDNASyncUtils.getErrorMessage(result);

      setTemplateStatus({
        checked: true,
        exists: false,
        statusCode: 0,
        statusMessage: `Error: ${errorMessage}`,
      });
      setIsCheckingStatus(false);

      Alert.alert(
        'Template Status Check Error',
        `Failed to check template status: ${errorMessage}`,
        [{ text: 'OK' }]
      );
    }
  }, []);

  /**
   * Initiates biometric opt-out workflow
   * SDK will check LDA availability and show password dialog if needed
   */
  const handleOptOut = async () => {
    console.log('BiometricOptOutScreen - Starting biometric opt-out');
    setIsOptingOut(true);
    setPasswordError('');
    setAttemptsLeft(3);

    try {
      const password = ''; // Empty string - SDK will check LDA
      const challengeMode = 7; // Biometric opt-out challenge mode

      console.log('BiometricOptOutScreen - Calling getIDVPasswordForBiometricOptOut');
      const syncResponse: RDNASyncResponse = await rdnaIDVService.getIDVPasswordForBiometricOptOut(
        password,
        challengeMode
      );
      console.log('BiometricOptOutScreen - GetIDVPasswordForBiometricOptOut sync response successful');

      // SDK will now:
      // 1. Check LDA availability
      // 2. Show LDA prompt if available, or password dialog if not
      // 3. Trigger GetPasswordVerification event (idvWorkflow: 11)
      // 4. If valid: Delete template directly (no selfie capture)
      // 5. Trigger RDNAIDVBiometricOptOutStatusResponse event (challengeMode: 7)
    } catch (error) {
      console.error('BiometricOptOutScreen - GetIDVPasswordForBiometricOptOut sync error:', error);

      const result: RDNASyncResponse = error as RDNASyncResponse;
      const errorMessage = RDNASyncUtils.getErrorMessage(result);

      Alert.alert('Opt-Out Error', `Failed to initiate opt-out: ${errorMessage}`, [{ text: 'OK' }]);
      setIsOptingOut(false);
    }
  };

  /**
   * Handler for GetPasswordVerification event (idvWorkflow: 11)
   * Chains with global handler from SDKIDVEventProvider
   */
  const handleGetPasswordVerification = useCallback(
    (data: RDNAIDVGetPasswordVerificationData, originalHandler?) => {
      console.log('BiometricOptOutScreen - GetPasswordVerification event received');

      const errorCode = data.error?.longErrorCode;

      if (errorCode === 0) {
        // Password validated successfully (or LDA succeeded)
        console.log('BiometricOptOutScreen - Password verified successfully for opt-out');
        setPasswordError('');
        setShowPasswordDialog(false);
        // SDK will now delete template directly (no selfie capture)
      } else {
        // Password validation failed
        console.log('BiometricOptOutScreen - Password verification failed:', data.error?.errorString);
        setPasswordError(data.error?.errorString || 'Password verification failed');
        setIsPasswordSubmitting(false);

        // Update attempts left
        if (data.attemptsLeft !== undefined) {
          setAttemptsLeft(data.attemptsLeft);
        }

        // Show password dialog if not already visible
        if (!showPasswordDialog) {
          setShowPasswordDialog(true);
        }
      }

      // Call original global handler
      if (originalHandler) {
        originalHandler(data);
      }
    },
    [showPasswordDialog]
  );

  /**
   * Handler for opt-out status response event
   * Resets flow state and displays result
   */
  const handleBiometricOptOutStatusResponse = useCallback((data: RDNAIDVBiometricOptOutStatusData) => {
    console.log('BiometricOptOutScreen - Biometric opt-out status response received');

    setTimeout(() => {
      setIsOptingOut(false);
      setShowPasswordDialog(false);
      setIsPasswordSubmitting(false);

      const errorCode = data.error?.longErrorCode;
      const statusCode = data.status?.statusCode;
      const statusMessage = data.status?.statusMessage;

      // First check: error code
      if (errorCode !== 0) {
        Alert.alert('Opt-Out Failed', data.error?.errorString);
        return;
      }

      // Second check: status code
      if (statusCode === 100) {
        Alert.alert(
          'Opt-Out Successful',
          `${statusMessage}\n\nYour biometric template has been deleted from the server.`,
          [{ text: 'OK', onPress: () => checkTemplateStatus() }]
        );
      } else {
        Alert.alert('Opt-Out Failed', statusMessage || 'Failed to complete biometric opt-out');
      }
    }, 300);
  }, [checkTemplateStatus]);

  /**
   * Handler for password dialog submission
   */
  const handlePasswordSubmit = (password: string) => {
    console.log('BiometricOptOutScreen - Password submitted');
    setIsPasswordSubmitting(true);
    setPasswordError('');

    // Password will be validated by SDK, GetPasswordVerification event will fire
  };

  /**
   * Handler for password dialog cancellation
   */
  const handlePasswordCancel = () => {
    console.log('BiometricOptOutScreen - Password dialog cancelled');
    setShowPasswordDialog(false);
    setPasswordError('');
    setIsPasswordSubmitting(false);
    setIsOptingOut(false);
  };

  /**
   * Register event handlers on component mount
   */
  useEffect(() => {
    console.log('BiometricOptOutScreen - Component mounted, registering event handlers');

    const eventManager = rdnaIDVService.getEventManager();

    // Get original global handler for chaining
    const originalPasswordHandler = eventManager.getPasswordVerificationHandler?.();

    // Register chained handler
    eventManager.setPasswordVerificationHandler((data) =>
      handleGetPasswordVerification(data, originalPasswordHandler)
    );

    eventManager.setCheckUserBiometricTemplateStatusHandler(handleCheckUserBiometricTemplateStatusResponse);
    eventManager.setBiometricOptOutStatusHandler(handleBiometricOptOutStatusResponse);

    // Cleanup: remove handlers on unmount
    return () => {
      console.log('BiometricOptOutScreen - Component unmounting, cleaning up event handlers');
      eventManager.setPasswordVerificationHandler(undefined);
      eventManager.setCheckUserBiometricTemplateStatusHandler(undefined);
      eventManager.setBiometricOptOutStatusHandler(undefined);
    };
  }, [
    handleGetPasswordVerification,
    handleCheckUserBiometricTemplateStatusResponse,
    handleBiometricOptOutStatusResponse
  ]);

  /**
   * Focus effect to reset UI state and check template
   */
  useFocusEffect(
    useCallback(() => {
      console.log('BiometricOptOutScreen - Screen focused');

      // Reset UI state
      setIsOptingOut(false);
      setIsCheckingStatus(false);
      setShowPasswordDialog(false);
      setPasswordError('');
      setIsPasswordSubmitting(false);

      // Check template status
      checkTemplateStatus();
    }, [checkTemplateStatus])
  );

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Biometric Opt-Out</Text>
      <Text style={styles.subtitle}>
        Delete your biometric template from the server
      </Text>

      {/* Template Status Display */}
      {isCheckingStatus ? (
        <View style={styles.statusContainer}>
          <ActivityIndicator size="large" color="#3498db" />
          <Text style={styles.statusText}>Checking template status...</Text>
        </View>
      ) : templateStatus.checked ? (
        <View style={[
          styles.statusContainer,
          templateStatus.exists ? styles.statusSuccess : styles.statusWarning
        ]}>
          <Text style={styles.statusIcon}>
            {templateStatus.exists ? '✅' : '⚠️'}
          </Text>
          <Text style={styles.statusText}>
            {templateStatus.exists
              ? 'Template found on the server'
              : 'No template found on the server'}
          </Text>
          <Text style={styles.statusMessage}>
            {templateStatus.statusMessage}
          </Text>
          {!templateStatus.exists && (
            <Text style={styles.helpText}>
              You don't have a biometric template. Use "Biometric Opt-In" to create one first.
            </Text>
          )}
        </View>
      ) : null}

      {/* Opt-Out Button */}
      <View style={styles.buttonContainer}>
        <Button
          title={isOptingOut ? 'Processing...' : 'Opt Out of Biometric Authentication'}
          onPress={handleOptOut}
          disabled={!templateStatus.exists || isCheckingStatus || isOptingOut}
          loading={isOptingOut}
        />
      </View>

      {/* Password Validation Dialog */}
      <PasswordValidationDialog
        visible={showPasswordDialog}
        title="Biometric Opt-Out Authentication"
        message="Please verify your password to delete biometric template"
        attemptsLeft={attemptsLeft}
        errorMessage={passwordError}
        isSubmitting={isPasswordSubmitting}
        onSubmitPassword={handlePasswordSubmit}
        onCancel={handlePasswordCancel}
      />

      {/* Info Section */}
      <View style={styles.infoContainer}>
        <Text style={styles.infoTitle}>How it works:</Text>
        <Text style={styles.infoText}>
          1. We check if you have a template on the server
        </Text>
        <Text style={styles.infoText}>
          2. SDK checks LDA (fingerprint/face/PIN) availability
        </Text>
        <Text style={styles.infoText}>
          3. If LDA unavailable or cancelled, enter password
        </Text>
        <Text style={styles.infoText}>
          4. We delete your biometric template from the server
        </Text>
        <Text style={styles.infoText}>
          5. Your biometric data is completely removed
        </Text>
      </View>
    </View>
  );
};

// ... styles similar to BiometricOptInScreen

export default BiometricOptOutScreen;

BiometricOptOutScreen UI Preview

Biometric Opt-Out Screen

The BiometricOptOutScreen shows template status check, opt-out button (enabled when template exists), and loading states. No selfie capture or modal confirmation is needed.

Key Points

Add Routes to Drawer Navigator

Update DrawerNavigator.tsx:

// src/tutorial/navigation/DrawerNavigator.tsx

import { BiometricOptInScreen, BiometricOptOutScreen } from '../screens/idv';

export type DrawerParamList = {
  // ... existing routes
  BiometricOptIn: undefined;
  BiometricOptOut: undefined;
};

// Inside DrawerNavigator component:
<Drawer.Screen
  name="BiometricOptIn"
  component={BiometricOptInScreen}
  options={{
    drawerLabel: 'Biometric Opt-In',
    drawerItemStyle: { display: 'none' }, // Hide from drawer menu
  }}
/>

<Drawer.Screen
  name="BiometricOptOut"
  component={BiometricOptOutScreen}
  options={{
    drawerLabel: 'Biometric Opt-Out',
    drawerItemStyle: { display: 'none' }, // Hide from drawer menu
  }}
/>

Add Menu Items to Drawer Content

Update DrawerContent.tsx:

// src/tutorial/screens/components/DrawerContent.tsx

{/* Biometric Opt-In Menu Item */}
<TouchableOpacity
  style={styles.menuItem}
  onPress={() => props.navigation.navigate('BiometricOptIn')}
>
  <Text style={styles.menuText}>🔐 Biometric Opt-In</Text>
</TouchableOpacity>

{/* Biometric Opt-Out Menu Item */}
<TouchableOpacity
  style={styles.menuItem}
  onPress={() => props.navigation.navigate('BiometricOptOut')}
>
  <Text style={styles.menuText}>🗑️ Biometric Opt-Out</Text>
</TouchableOpacity>

The drawer menu now includes both biometric opt-in and opt-out menu items:

Drawer Menu with Biometric Opt-In and Opt-Out Items

The drawer menu includes "🔐 Biometric Opt-In" and "🗑️ Biometric Opt-Out" menu items for biometric template management.

Update Index Exports

Update src/tutorial/screens/idv/index.ts:

export { default as BiometricOptInScreen } from './biometricOptIn/BiometricOptInScreen';
export { default as BiometricOptOutScreen } from './biometricOptOut/BiometricOptOutScreen';

After implementing the screens, let's understand what REL-ID plugin APIs are called and what screen-level events are handled for each flow.

Biometric Opt-In Flow

REL-ID Plugin APIs Called:

  1. getIDVPasswordForBiometricOptIn(password, challengeMode)
    • When: User clicks "Opt In" button
    • Parameters:
      • password: Empty string "" (SDK handles LDA/password internally)
      • challengeMode: 6 (biometric opt-in)
    • Purpose: Initiates opt-in flow with LDA-first authentication
  2. setIDVBiometricOptInConfirmation(status)
    • When: User confirms captured frame action
    • Parameters:
      • status: 0 (Approve), 1 (Recapture), or 2 (Cancel)
    • Purpose: Sends user decision to SDK for template creation

Screen-Level Events Handled:

The event sequence depends on whether LDA is available and successful:

Branch A: LDA Available & Successful

1. onGetSelfieProcessStartConfirmation (idvWorkflow: 10)
   - Closes password dialog if open
   - Native selfie screen appears

2. RDNAIDVOptInCapturedFrameConfirmation
   - Receives captured frame (base64 image)
   - Pops selfie screen from parent navigator
   - Shows captured frame modal

3. onBiometricOptInStatus (challengeMode: 6)
   - Final template creation result
   - Status code 100 = success

Total: 3 events (GetPasswordVerification is skipped when LDA succeeds)

Branch B: LDA Cancelled OR LDA Unavailable

1. onGetPasswordVerification (idvWorkflow: 10)
   - Validates entered password
   - Shows error if incorrect

2. onGetSelfieProcessStartConfirmation (idvWorkflow: 10)
   - Closes password dialog
   - Native selfie screen appears

3. RDNAIDVOptInCapturedFrameConfirmation
   - Receives captured frame (base64 image)
   - Pops selfie screen from parent navigator
   - Shows captured frame modal

4. onBiometricOptInStatus (challengeMode: 6)
   - Final template creation result
   - Status code 100 = success

Total: 4 events (includes GetPasswordVerification for password fallback)

Biometric Opt-Out Flow

REL-ID Plugin APIs Called:

  1. getIDVPasswordForBiometricOptOut(password, challengeMode)
    • When: User clicks "Opt Out" button
    • Parameters:
      • password: Empty string "" (SDK handles LDA/password internally)
      • challengeMode: 7 (biometric opt-out)
    • Purpose: Initiates opt-out flow with LDA-first authentication

Screen-Level Events Handled:

The event sequence depends on whether LDA is available and successful:

Branch A: LDA Available & Successful

1. onBiometricOptOutStatus (challengeMode: 7)
   - SDK deletes template directly after LDA success
   - Status code 100 = success

Total: 1 event (GetPasswordVerification is skipped when LDA succeeds)

Branch B: LDA Cancelled OR LDA Unavailable

1. onGetPasswordVerification (idvWorkflow: 11)
   - Validates entered password
   - Shows error if incorrect
   - Closes password dialog on success

2. onBiometricOptOutStatus (challengeMode: 7)
   - SDK deletes template after password validation
   - Status code 100 = success

Total: 2 events (includes GetPasswordVerification for password fallback)

API & Event Comparison Table

Aspect

Opt-In Flow

Opt-Out Flow

APIs Called

2 APIs

1 API

Challenge Mode

6

7

IDV Workflow

10

11

Events (LDA Success)

3 events

1 event

Events (Password Fallback)

4 events

2 events

Selfie Capture

✅ Required

❌ Not required

GetPasswordVerification

Only if LDA fails/cancelled

Only if LDA fails/cancelled

User Interaction

Approve/Recapture/Cancel frame

Just authentication

Complete Flow Walkthroughs

Test both biometric opt-in and opt-out flows:

Opt-In Flow Walkthrough

Step 1: Login and Navigate

1. Launch the app
2. Complete user login (activated customer)
3. Navigate to Dashboard
4. Open drawer menu (hamburger icon)
5. Select "🔐 Biometric Opt-In"

Step 2: Template Status Check

1. Screen opens and automatically checks template status
2. Observe loading indicator: "Checking template status..."
3. Template status displayed:
   - ✅ If no template (status code 600):
     - Green banner with "No template found - you can opt-in"
     - "Opt In to Biometric Authentication" button enabled
   - ⚠️ If template exists (status code 100):
     - Yellow banner with "Template already exists on the server"
     - Button disabled (grey)
     - Help text: "Use Biometric Opt-Out to delete it first"

Step 3: Start Opt-In (if no template)

1. Click "Opt In to Biometric Authentication" button
2. SDK checks LDA availability:

   A. LDA Available:
      - LDA prompt appears (fingerprint/face/PIN)
      - If LDA succeeds → Proceeds to Step 4
      - If LDA cancelled → Password dialog appears → Enter password → Proceeds to Step 4

   B. LDA Not Available:
      - Password dialog appears immediately
      - Enter password
      - Click "Verify & Continue"
      - Proceeds to Step 4

Step 4: Password Verification (if LDA failed/cancelled/unavailable)

1. PasswordValidationDialog shows:
   - Title: "Biometric Opt-In Authentication"
   - Message: "Please verify your password to enable biometric storage"
   - Password input field (auto-focused)
   - Attempts counter (3/2/1 with color coding)
2. Enter password
3. Click "Verify & Continue"
4. GetPasswordVerification event fires (idvWorkflow: 10)
   - If valid: Proceeds to Step 5
   - If invalid: Error shown, dialog stays open, attempts decremented

Step 5: Selfie Capture

1. GetSelfieProcessStartConfirmation event fires (idvWorkflow: 10)
2. Password dialog closes automatically
3. IDVSelfieProcessStartConfirmationScreen appears
4. Click "Start Selfie Capture" button
5. Native camera opens with Aware FaceCapture
6. Liveness detection guides user
7. User captures selfie
8. Camera closes

Step 6: Captured Frame Confirmation

1. RDNAIDVOptInCapturedFrameConfirmation event fires
2. Parent navigator pops IDVSelfieProcessStart screen
3. isOptInFlowActiveRef.current = true
4. Captured frame modal appears with base64 image preview
5. Three buttons shown:
   - ✓ Approve (action: 0)
   - ↻ Recapture (action: 1)
   - ✕ Cancel (action: 2)

User choices:

A. Approve:
   - Modal closes immediately
   - setIDVBiometricOptInConfirmation(0) called
   - SDK creates template on server
   - Proceeds to Step 7

B. Recapture:
   - Modal STAYS OPEN
   - setIDVBiometricOptInConfirmation(1) called
   - SDK re-opens selfie screen
   - Returns to Step 5 (new capture updates same modal)

C. Cancel:
   - Modal closes immediately
   - setIDVBiometricOptInConfirmation(2) called
   - SDK aborts opt-in
   - Status event fires with cancellation

Step 7: View Opt-In Result

1. RDNAIDVBiometricOptInStatusResponse event fires (challengeMode: 6)
2. isOptInFlowActiveRef.current = false
3. 300ms delay
4. Result alert appears:

   Success (statusCode: 100):
   - Title: "Opt-In Successful"
   - Message: "[status message]\n\nYour biometric template has been created on the server."
   - Button: "OK" → Refreshes template status

   Failure (statusCode: other):
   - Title: "Opt-In Failed"
   - Message: "[error message]"
   - Button: "OK"

Opt-Out Flow Walkthrough

Step 1: Login and Navigate

1. Launch the app
2. Complete user login (activated customer)
3. Navigate to Dashboard
4. Open drawer menu (hamburger icon)
5. Select "🗑️ Biometric Opt-Out"

Step 2: Template Status Check

1. Screen opens and automatically checks template status
2. Observe loading indicator: "Checking template status..."
3. Template status displayed:
   - ✅ If template exists (status code 100):
     - Green banner with "Template found on the server"
     - "Opt Out of Biometric Authentication" button enabled
   - ⚠️ If no template (status code 600):
     - Yellow banner with "No template found on the server"
     - Button disabled (grey)
     - Help text: "Use Biometric Opt-In to create one first"

Step 3: Start Opt-Out (if template exists)

1. Click "Opt Out of Biometric Authentication" button
2. SDK checks LDA availability:

   A. LDA Available:
      - LDA prompt appears (fingerprint/face/PIN)
      - If LDA succeeds → Proceeds to Step 4 (no password dialog)
      - If LDA cancelled → Password dialog appears → Enter password → Proceeds to Step 4

   B. LDA Not Available:
      - Password dialog appears immediately
      - Enter password
      - Click "Verify & Continue"
      - Proceeds to Step 4

Step 4: Password Verification (if LDA failed/cancelled/unavailable)

1. PasswordValidationDialog shows:
   - Title: "Biometric Opt-Out Authentication"
   - Message: "Please verify your password to delete biometric template"
   - Password input field (auto-focused)
   - Attempts counter (3/2/1 with color coding)
2. Enter password
3. Click "Verify & Continue"
4. GetPasswordVerification event fires (idvWorkflow: 11)
   - If valid: Password dialog closes, SDK deletes template directly (NO selfie)
   - If invalid: Error shown, dialog stays open, attempts decremented

Step 5: View Opt-Out Result

1. RDNAIDVBiometricOptOutStatusResponse event fires (challengeMode: 7)
2. 300ms delay
3. Result alert appears:

   Success (statusCode: 100):
   - Title: "Opt-Out Successful"
   - Message: "[status message]\n\nYour biometric template has been deleted from the server."
   - Button: "OK" → Refreshes template status

   Failure (statusCode: other):
   - Title: "Opt-Out Failed"
   - Message: "[error message]"
   - Button: "OK"

Testing Scenarios

Scenario 1: Opt-In with LDA Success

✅ Template check returns 600 (no template)
✅ Button enabled
✅ User clicks opt-in button
✅ LDA prompt appears
✅ User authenticates with fingerprint/face
✅ GetPasswordVerification event (idvWorkflow: 10, error: 0)
✅ Selfie capture screen appears
✅ User captures selfie
✅ Captured frame modal shows with image
✅ User clicks "Approve"
✅ Template created on server
✅ Success alert: "Opt-In Successful"
✅ Template status refreshed (now shows 100)

Scenario 2: Opt-In with LDA Cancelled → Password Fallback

✅ Template check returns 600
✅ User clicks opt-in button
✅ LDA prompt appears
❌ User cancels LDA prompt
✅ Password dialog appears automatically (fallback)
✅ User enters password
✅ Password validates successfully
✅ GetPasswordVerification event (idvWorkflow: 10, error: 0)
✅ Password dialog closes
✅ Selfie capture proceeds normally
✅ Opt-in completes successfully

Scenario 3: Opt-In with LDA Unavailable → Direct Password

✅ Template check returns 600
✅ User clicks opt-in button
❌ LDA not available on device
✅ Password dialog appears immediately (no LDA prompt)
✅ User enters password
✅ Password validates successfully
✅ Selfie capture proceeds
✅ Opt-in completes successfully

Scenario 4: Opt-In with Recapture

✅ Template check, LDA/password succeed
✅ Selfie captured
✅ Captured frame modal shows
❌ User clicks "Recapture" (wants to retake)
✅ Modal STAYS OPEN (doesn't close)
✅ Selfie screen reopens
✅ User captures new selfie
✅ Modal updates with new image (same modal, not reopened)
✅ User clicks "Approve" on new image
✅ Opt-in completes successfully

Scenario 5: Opt-Out with LDA Success

✅ Template check returns 100 (template exists)
✅ Button enabled
✅ User clicks opt-out button
✅ LDA prompt appears
✅ User authenticates with fingerprint/face
✅ GetPasswordVerification event (idvWorkflow: 11, error: 0)
✅ Template deleted immediately (NO selfie capture)
✅ Success alert: "Opt-Out Successful"
✅ Template status refreshed (now shows 600)

Scenario 6: Opt-Out with Password Fallback

✅ Template check returns 100
✅ User clicks opt-out button
✅ LDA prompt appears
❌ User cancels LDA
✅ Password dialog appears
✅ User enters password
✅ Password validates
✅ Template deleted
✅ Success alert shown

Scenario 7: Password Incorrect

✅ Opt-in/opt-out initiated
✅ Password dialog appears
❌ User enters wrong password
✅ GetPasswordVerification event (error: non-zero)
❌ Error message shown in dialog
✅ Password field cleared automatically
✅ Attempts counter decrements (3 → 2)
✅ Attempts counter color changes (green → orange → red)
✅ Dialog stays open
✅ User can retry with correct password

Verification Checklist

Use this checklist to verify your implementation:

Opt-In Flow:

Opt-Out Flow:

Common:

🎉 Congratulations!

You've successfully implemented the REL-ID IDV Post-Login Server Biometric Opt-In & Opt-Out Flow in your React Native application!

What You've Accomplished

✅ PasswordValidationDialog Component: Built reusable password dialog with LDA fallback support

✅ Biometric Opt-In Flow: Implemented complete template creation with selfie capture (workflow 10, challenge mode 6)

✅ Biometric Opt-Out Flow: Implemented complete template deletion (workflow 11, challenge mode 7)

✅ LDA Fallback Pattern: Mastered LDA-first authentication with seamless password fallback

✅ Event Handler Chaining: Preserved global handlers while adding screen-specific logic

✅ Conditional Modal Behavior: Implemented smart modal close for recapture UX

✅ Parent Navigator Pattern: Navigated across Stack + Drawer hierarchies correctly

✅ Flow State Management: Used refs to prevent duplicate API calls and handle remounting

✅ Main Thread Execution: Ensured React Native native module calls execute properly

✅ Production-Ready Implementation: Built robust, user-friendly biometric template management

Key Takeaways

  1. LDA-First Approach: Always try LDA before password for better UX
  2. Reusable Components: PasswordValidationDialog serves both flows
  3. Challenge Modes: 6 for opt-in, 7 for opt-out
  4. Workflows: 10 (opt-in with selfie), 11 (opt-out without selfie)
  5. Handler Chaining: Preserve global handlers while adding screen logic
  6. Conditional Modal: Keep modal open for recapture to prevent flashing
  7. Parent Navigator: Use navigation.getParent().goBack() for cross-hierarchy navigation
  8. Flow State Refs: Prevent duplicate API calls during active flows
  9. Main Thread: Wrap native calls in setTimeout(() => {}, 0)
  10. Ref Reset: Reset at all completion points to avoid blocking

Next Steps

Now that you've mastered biometric template management, consider:

  1. Explore Server Biometric Authentication: Use created templates for authentication
  2. Implement Template Analytics: Track opt-in/opt-out rates
  3. Add Settings Screen: Centralized biometric management UI
  4. Optimize UX: Add animations, better messaging, retry logic
  5. Security Enhancements: Implement attempt limits, lockout policies
  6. User Education: In-app guides explaining biometric template benefits

Resources

🔐 Thank you for completing the REL-ID IDV Post-Login Server Biometric Opt-In & Opt-Out Flow codelab!

You're now equipped to implement production-ready biometric template management for activated customers, giving users complete control over their biometric data with LDA-first authentication, password fallback, handler chaining, and robust error handling across complex navigation hierarchies and native UI workflows.