🎯 Learning Path:
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.
In this codelab, you'll implement two complete biometric template management workflows:
Biometric Opt-In Flow (Template Creation):
Biometric Opt-Out Flow (Template Deletion):
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:
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
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
For biometric opt-in and opt-out, you'll use these specific values:
Flow | Workflow | Challenge Mode | Events |
Opt-In |
|
| GetPasswordVerification (10) |
Opt-Out |
|
| GetPasswordVerification (11) |
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 |
Ensure you have completed these requirements:
# Required tools
Node.js >= 16
npm or yarn
React Native CLI
Xcode (for iOS) or Android Studio (for Android)
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
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)
The project includes pre-configured Aware FaceCapture library for opt-in:
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.
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;
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';

The PasswordValidationDialog shows password input field, attempts counter with color coding (green/orange/red), error messages, and action buttons for verify and cancel.
Implement biometric opt-in APIs in the IDV service layer. These APIs handle template creation workflow with LDA fallback and selfie capture.
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);
}
});
});
}
}
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();
Implement biometric opt-out APIs in the IDV service layer. These APIs handle template deletion workflow with LDA fallback (no selfie capture).
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();
Add TypeScript interfaces for opt-in and opt-out status events. These events are triggered by the SDK after template operations complete.
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;
};
}
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;
};
}
data.status directly, no JSON parsing needederror.longErrorCode before status.statusCodeImplement 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.
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();
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.
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
};
/**
* 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' }]
);
}
}, []);
/**
* 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);
}
};

The BiometricOptInScreen shows template status check, opt-in button (enabled when no template exists), and loading states for status check and opt-in process.
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);
}
},
[]
);
/**
* 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]
);
/**
* 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);
};
/**
* 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;
};
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])
);
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;

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).
navigation.getParent().goBack() to dismiss selfie screenImplement 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.
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;

The BiometricOptOutScreen shows template status check, opt-out button (enabled when template exists), and loading states. No selfie capture or modal confirmation is needed.
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
}}
/>
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:

The drawer menu includes "🔐 Biometric Opt-In" and "🗑️ Biometric Opt-Out" menu items for biometric template management.
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.
REL-ID Plugin APIs Called:
getIDVPasswordForBiometricOptIn(password, challengeMode)password: Empty string "" (SDK handles LDA/password internally)challengeMode: 6 (biometric opt-in)setIDVBiometricOptInConfirmation(status)status: 0 (Approve), 1 (Recapture), or 2 (Cancel)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)
REL-ID Plugin APIs Called:
getIDVPasswordForBiometricOptOut(password, challengeMode)password: Empty string "" (SDK handles LDA/password internally)challengeMode: 7 (biometric opt-out)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)
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 |
Test both biometric opt-in and opt-out flows:
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"
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"
✅ 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)
✅ 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
✅ 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
✅ 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
✅ 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)
✅ 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
✅ 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
Use this checklist to verify your implementation:
Opt-In Flow:
Opt-Out Flow:
Common:
You've successfully implemented the REL-ID IDV Post-Login Server Biometric Opt-In & Opt-Out Flow in your React Native application!
✅ 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
navigation.getParent().goBack() for cross-hierarchy navigationsetTimeout(() => {}, 0)Now that you've mastered biometric template management, consider:
🔐 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.