๐ฏ Learning Path:
Welcome to the REL-ID Data Signing codelab! This tutorial builds upon your existing MFA implementation to add secure cryptographic data signing capabilities using REL-ID SDK's authentication and signing infrastructure.
In this codelab, you'll enhance your existing MFA application with:
By completing this codelab, you'll master:
authenticateUserAndSignData() with proper parameter handlingonAuthenticateUserAndSignData callbacks and state updatesBefore starting this codelab, ensure you have:
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-public/codelab-react-native.git
Navigate to the relid-data-signing folder in the repository you cloned earlier
This codelab extends your MFA application with comprehensive data signing functionality:
Before implementing data signing functionality, let's understand the cryptographic concepts and security architecture that powers REL-ID's data signing capabilities.
Data signing is a cryptographic process that creates a digital signature for a piece of data, providing:
REL-ID's data signing implementation follows enterprise security standards:
User Data Input โ Authentication Challenge โ Biometric/LDA/Password Verification โ
Cryptographic Signing โ Signed Payload โ Verification
Data signing is ideal for:
Key security guidelines:
Let's explore the three core APIs that power REL-ID's data signing functionality, understanding their parameters, responses, and integration patterns.
This is the primary API for initiating cryptographic data signing with user authentication.
authenticateUserAndSignData(
payload: string,
authLevel: RDNAAuthLevel,
authenticatorType: RDNAAuthenticatorType,
reason: string
): Promise<RDNASyncResponse>
Parameter | Type | Required | Description |
| string | โ | The data to be cryptographically signed (max 500 characters) |
| RDNAAuthLevel | โ | Authentication security level (0, 1, or 4 only) |
| RDNAAuthenticatorType | โ | Type of authentication method (0 or 1 only) |
| string | โ | Human-readable reason for signing (max 100 characters) |
RDNAAuthLevel | RDNAAuthenticatorType | Supported Authentication | Description |
|
| No Authentication | No authentication required - NOT RECOMMENDED for production |
|
| Device biometric, Device passcode, or Password | Priority: Device biometric โ Device passcode โ Password |
| NOT SUPPORTED | โ SDK will error out | Level 2 is not supported for data signing |
| NOT SUPPORTED | โ SDK will error out | Level 3 is not supported for data signing |
|
| IDV Server Biometric | Maximum security - Any other authenticator type will cause SDK error |
REL-ID data signing supports three authentication modes:
authLevel: RDNAAuthLevel.NONE,
authenticatorType: RDNAAuthenticatorType.NONE
authLevel: RDNAAuthLevel.RDNA_AUTH_LEVEL_1,
authenticatorType: RDNAAuthenticatorType.NONE
authLevel: RDNAAuthLevel.RDNA_AUTH_LEVEL_4,
authenticatorType: RDNAAuthenticatorType.RDNA_IDV_SERVER_BIOMETRIC
RDNA_IDV_SERVER_BIOMETRIC - other types will cause errors// Success Response
{
error: {
longErrorCode: 0,
shortErrorCode: 0,
errorString: ""
},
// Additional response fields...
}
// Error Response
{
error: {
longErrorCode: 123,
shortErrorCode: 45,
errorString: "Authentication failed"
}
}
// Service layer implementation
async signData(request: DataSigningRequest): Promise<void> {
console.log('DataSigningService - Starting data signing process');
try {
const response = await rdnaService.authenticateUserAndSignData(
request.payload,
request.authLevel,
request.authenticatorType,
request.reason
);
console.log('DataSigningService - Data signing initiated successfully');
return Promise.resolve();
} catch (error) {
console.error('DataSigningService - Data signing failed:', error);
throw error;
}
}
This API cleans up authentication state after signing completion or cancellation.
resetAuthenticateUserAndSignDataState(): Promise<RDNASyncResponse>
// Service layer cleanup implementation
async resetState(): Promise<void> {
console.log('DataSigningService - Resetting data signing state');
try {
await rdnaService.resetAuthenticateUserAndSignDataState();
console.log('DataSigningService - State reset successfully');
return Promise.resolve();
} catch (error) {
console.error('DataSigningService - State reset failed:', error);
throw error;
}
}
This callback event delivers the final signing results after authentication completion.
interface DataSigningResponse {
dataPayload: string; // Original payload that was signed
dataPayloadLength: number; // Length of the payload
reason: string; // Reason provided for signing
payloadSignature: string; // Cryptographic signature
dataSignatureID: string; // Unique signature identifier
authLevel: number; // Authentication level used
authenticationType: number; // Authentication type used
status: ResponseStatus; // Operation status
error: ResponseError; // Error details (if any)
}
const handleDataSigningResponse = useCallback((response: RDNADataSigningResponse) => {
console.log('SDKEventProvider - Data signing response received:', response);
// Convert SDK response to internal format
const internalResponse: DataSigningResponse = {
dataPayload: response.dataPayload,
dataPayloadLength: response.dataPayloadLength,
reason: response.reason,
payloadSignature: response.payloadSignature,
dataSignatureID: response.dataSignatureID,
authLevel: response.authLevel,
authenticationType: response.authenticationType,
status: response.status,
error: response.error,
};
// Update state
setSigningResult(internalResponse);
setFormState(prev => ({ ...prev, isLoading: false }));
// Check if signing was successful
if (response.error.shortErrorCode === 0 && response.status.statusCode === 100) {
// Format result for display
const displayData = DataSigningService.formatSigningResultForDisplay(internalResponse);
setResultDisplay(displayData);
console.log('SDKEventProvider - Data signing successful');
} else {
console.error('SDKEventProvider - Data signing failed:', response.error);
// Handle error appropriately
}
}, []);
Success Indicators:
error.shortErrorCode === 0status.statusCode === 100payloadSignature and dataSignatureIDError Handling:
Now let's implement the service layer architecture that provides clean abstraction over REL-ID SDK data signing APIs. This follows the established patterns from your MFA implementation.
First, let's examine the core SDK service implementation. This should already exist in your codelab:
// src/uniken/services/rdnaService.ts
/**
* Authenticates user and signs data payload
*
* This method initiates the data signing flow with step-up authentication.
* It requires user authentication and cryptographically signs the provided
* payload upon successful authentication.
*/
async authenticateUserAndSignData(
payload: string,
authLevel: RDNAAuthLevel,
authenticatorType: RDNAAuthenticatorType,
reason: string
): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
console.log('RdnaService - Initiating data signing:', {
payloadLength: payload.length,
authLevel,
authenticatorType,
reason,
});
RdnaClient.authenticateUserAndSignData(
payload,
authLevel,
authenticatorType,
reason,
response => {
console.log('RdnaService - AuthenticateUserAndSignData sync callback received');
console.log('RdnaService - AuthenticateUserAndSignData sync raw response:', response);
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaService - AuthenticateUserAndSignData sync response success');
resolve(result);
} else {
console.error('RdnaService - AuthenticateUserAndSignData sync response error:', result);
reject(result);
}
}
);
});
}
/**
* Resets the data signing authentication state
*/
async resetAuthenticateUserAndSignDataState(): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
console.log('RdnaService - Resetting data signing authentication state');
RdnaClient.resetAuthenticateUserAndSignDataState(response => {
console.log('RdnaService - ResetAuthenticateUserAndSignDataState sync callback received');
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaService - ResetAuthenticateUserAndSignDataState sync response success');
resolve(result);
} else {
console.error('RdnaService - ResetAuthenticateUserAndSignDataState sync response error:', result);
reject(result);
}
});
});
}
Create a high-level service that combines multiple concerns:
// src/tutorial/services/DataSigningService.ts
import { RDNAAuthLevel, RDNAAuthenticatorType } from 'react-native-rdna-client';
import rdnaService from '../../uniken/services/rdnaService';
import { DropdownDataService } from './DropdownDataService';
import type {
DataSigningRequest,
DataSigningResponse,
DataSigningResultDisplay,
ResultInfoItem,
} from '../types/DataSigningTypes';
/**
* High-level service for data signing operations
* Combines RdnaService and DropdownDataService for complete functionality
*/
class DataSigningServiceClass {
/**
* Get dropdown data service instance
*/
get dropdownService() {
return DropdownDataService;
}
/**
* Get RDNA service instance
*/
get rdnaService() {
return rdnaService;
}
/**
* Initiates data signing with proper enum conversion
*
* @param request Data signing request with display values
* @returns Promise that resolves when sync response is received
*/
async signData(request: DataSigningRequest): Promise<void> {
console.log('DataSigningService - Starting data signing process');
try {
const response = await rdnaService.authenticateUserAndSignData(
request.payload,
request.authLevel,
request.authenticatorType,
request.reason
);
console.log('DataSigningService - Data signing initiated successfully');
return Promise.resolve();
} catch (error) {
console.error('DataSigningService - Data signing failed:', error);
throw error;
}
}
/**
* Submits password for step-up authentication during data signing
*
* @param password User's password
* @param challengeMode Challenge mode from getPassword callback
* @returns Promise that resolves when sync response is received
*/
async submitPassword(password: string, challengeMode: number): Promise<void> {
console.log('DataSigningService - Submitting password for data signing');
try {
const response = await rdnaService.setPassword(password, challengeMode);
console.log('DataSigningService - Password submitted successfully');
return Promise.resolve();
} catch (error) {
console.error('DataSigningService - Password submission failed:', error);
throw error;
}
}
/**
* Resets data signing state (cleanup)
*
* @returns Promise that resolves when state is reset
*/
async resetState(): Promise<void> {
console.log('DataSigningService - Resetting data signing state');
try {
await rdnaService.resetAuthenticateUserAndSignDataState();
console.log('DataSigningService - State reset successfully');
return Promise.resolve();
} catch (error) {
console.error('DataSigningService - State reset failed:', error);
throw error;
}
}
/**
* Convert dropdown values to SDK enums for API call
*/
convertDropdownToEnums(
authLevelDisplay: string,
authenticatorTypeDisplay: string
): {
authLevel: RDNAAuthLevel;
authenticatorType: RDNAAuthenticatorType;
} {
return {
authLevel: DropdownDataService.convertAuthLevelToEnum(authLevelDisplay),
authenticatorType: DropdownDataService.convertAuthenticatorTypeToEnum(authenticatorTypeDisplay),
};
}
/**
* Validates form input before submission
*/
validateSigningInput(
payload: string,
authLevel: string,
authenticatorType: string,
reason: string
): {
isValid: boolean;
errors: string[];
} {
const errors: string[] = [];
// Validate payload
if (!payload || payload.trim().length === 0) {
errors.push('Payload is required');
} else if (payload.length > 500) {
errors.push('Payload must be less than 500 characters');
}
// Validate auth level
if (!authLevel || !DropdownDataService.isValidAuthLevel(authLevel)) {
errors.push('Please select a valid authentication level');
}
// Validate authenticator type
if (!authenticatorType || !DropdownDataService.isValidAuthenticatorType(authenticatorType)) {
errors.push('Please select a valid authenticator type');
}
// Validate reason
if (!reason || reason.trim().length === 0) {
errors.push('Reason is required');
} else if (reason.length > 100) {
errors.push('Reason must be less than 100 characters');
}
return {
isValid: errors.length === 0,
errors,
};
}
/**
* Converts raw data signing response to display format
* Excludes status and error fields as per requirements
*/
formatSigningResultForDisplay(response: DataSigningResponse): DataSigningResultDisplay {
return {
authLevel: response.authLevel?.toString() || 'N/A',
authenticationType: response.authenticationType?.toString() || 'N/A',
dataPayloadLength: response.dataPayloadLength?.toString() || 'N/A',
dataPayload: response.dataPayload || 'N/A',
payloadSignature: response.payloadSignature || 'N/A',
dataSignatureID: response.dataSignatureID || 'N/A',
reason: response.reason || 'N/A',
};
}
/**
* Converts display format to info items for results screen
*/
convertToResultInfoItems(displayData: DataSigningResultDisplay): ResultInfoItem[] {
return [
{
name: 'Payload Signature',
value: displayData.payloadSignature,
},
{
name: 'Data Signature ID',
value: displayData.dataSignatureID,
},
{
name: 'Reason',
value: displayData.reason,
},
{
name: 'Data Payload',
value: displayData.dataPayload,
},
{
name: 'Auth Level',
value: displayData.authLevel,
},
{
name: 'Authentication Type',
value: displayData.authenticationType,
},
{
name: 'Data Payload Length',
value: displayData.dataPayloadLength,
},
];
}
/**
* Gets user-friendly error message for error codes
*/
getErrorMessage(errorCode: number): string {
switch (errorCode) {
case 0:
return 'Success';
case 214:
return 'Authentication method not supported. Please try a different authentication type.';
case 102:
return 'Authentication failed. Please check your credentials and try again.';
case 153:
return 'Operation cancelled by user.';
default:
return `Operation failed with error code: ${errorCode}`;
}
}
}
// Export singleton instance
export const DataSigningService = new DataSigningServiceClass();
Create a service to manage dropdown data and enum conversions:
// src/tutorial/services/DropdownDataService.ts
import { RDNAAuthLevel, RDNAAuthenticatorType } from 'react-native-rdna-client';
import type {
DropdownOption,
AuthLevelDropdownData,
AuthenticatorTypeDropdownData,
} from '../types/DataSigningTypes';
/**
* Service for managing dropdown data and enum conversions
*/
class DropdownDataServiceClass {
/**
* Get authentication level options for dropdown
* Only includes levels supported for data signing: 0, 1, and 4
*/
getAuthLevelOptions(): DropdownOption[] {
return [
{ value: "NONE (0)" },
{ value: "RDNA_AUTH_LEVEL_1 (1)" },
{ value: "RDNA_AUTH_LEVEL_4 (4)" },
// Note: Levels 2 and 3 are NOT SUPPORTED for data signing
];
}
/**
* Get authenticator type options for dropdown
* Only includes types supported for data signing: 0 and 1
*/
getAuthenticatorTypeOptions(): DropdownOption[] {
return [
{ value: "NONE (0)" },
{ value: "RDNA_IDV_SERVER_BIOMETRIC (1)" },
// Note: RDNA_AUTH_PASS (2) and RDNA_AUTH_LDA (3) are NOT SUPPORTED for data signing
];
}
/**
* Convert auth level display value to enum
* Only handles levels supported for data signing
*/
convertAuthLevelToEnum(displayValue: string): RDNAAuthLevel {
switch (displayValue) {
case "NONE (0)":
return RDNAAuthLevel.NONE;
case "RDNA_AUTH_LEVEL_1 (1)":
return RDNAAuthLevel.RDNA_AUTH_LEVEL_1;
case "RDNA_AUTH_LEVEL_4 (4)":
return RDNAAuthLevel.RDNA_AUTH_LEVEL_4;
default:
// Default to Level 4 for maximum security
return RDNAAuthLevel.RDNA_AUTH_LEVEL_4;
}
}
/**
* Convert authenticator type display value to enum
* Only handles types supported for data signing
*/
convertAuthenticatorTypeToEnum(displayValue: string): RDNAAuthenticatorType {
switch (displayValue) {
case "NONE (0)":
return RDNAAuthenticatorType.NONE;
case "RDNA_IDV_SERVER_BIOMETRIC (1)":
return RDNAAuthenticatorType.RDNA_IDV_SERVER_BIOMETRIC;
default:
// Default to biometric for maximum security
return RDNAAuthenticatorType.RDNA_IDV_SERVER_BIOMETRIC;
}
}
/**
* Validate auth level display value
*/
isValidAuthLevel(displayValue: string): boolean {
const validValues = this.getAuthLevelOptions().map(option => option.value);
return validValues.includes(displayValue);
}
/**
* Validate authenticator type display value
*/
isValidAuthenticatorType(displayValue: string): boolean {
const validValues = this.getAuthenticatorTypeOptions().map(option => option.value);
return validValues.includes(displayValue);
}
}
// Export singleton instance
export const DropdownDataService = new DropdownDataServiceClass();
Key error handling strategies in the service layer:
Now let's implement the user interface components for data signing, including form inputs, dropdowns, and result display screens.
This is the primary screen where users input data to be signed:
The following image showcases the data signing input screen from the sample application:

// src/tutorial/screens/dataSigning/DataSigningInputScreen.tsx
import React from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
ScrollView,
StyleSheet,
Alert,
ActivityIndicator,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useDataSigning } from '../../../uniken/providers/SDKEventProvider';
import { DropdownDataService } from '../../services/DropdownDataService';
import AuthLevelDropdown from './components/AuthLevelDropdown';
import AuthenticatorTypeDropdown from './components/AuthenticatorTypeDropdown';
import PasswordChallengeModal from './components/PasswordChallengeModal';
/**
* Main screen for data signing input
*/
const DataSigningInputScreen: React.FC = () => {
const navigation = useNavigation();
const {
formState,
updateFormState,
passwordModalState,
submitDataSigning,
resultDisplay,
} = useDataSigning();
/**
* Handles form submission
*/
const handleSubmit = async () => {
console.log('DataSigningInputScreen - Submit button pressed');
// Validate required fields
if (!formState.payload.trim()) {
Alert.alert('Validation Error', 'Please enter a payload to sign');
return;
}
if (!formState.selectedAuthLevel) {
Alert.alert('Validation Error', 'Please select an authentication level');
return;
}
if (!formState.selectedAuthenticatorType) {
Alert.alert('Validation Error', 'Please select an authenticator type');
return;
}
if (!formState.reason.trim()) {
Alert.alert('Validation Error', 'Please enter a reason for signing');
return;
}
try {
await submitDataSigning();
} catch (error) {
console.error('DataSigningInputScreen - Submit error:', error);
const errorMessage = error instanceof Error ? error.message : 'Failed to submit data signing request. Please try again.';
Alert.alert('Error', errorMessage);
}
};
/**
* Navigates to result screen when signing is complete
*/
React.useEffect(() => {
if (resultDisplay) {
console.log('DataSigningInputScreen - Navigating to results screen');
navigation.navigate('DataSigningResult' as never);
}
}, [resultDisplay, navigation]);
return (
<View style={styles.container}>
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>Data Signing</Text>
<Text style={styles.subtitle}>
Sign your data with cryptographic authentication
</Text>
</View>
{/* Info Section */}
<View style={styles.infoSection}>
<Text style={styles.infoTitle}>How it works:</Text>
<Text style={styles.infoText}>
1. Enter your data payload and select authentication parameters{'\n'}
2. Click "Sign Data" to initiate the signing process{'\n'}
3. Complete authentication when prompted{'\n'}
4. Receive your cryptographically signed data
</Text>
</View>
{/* Form */}
<View style={styles.form}>
{/* Payload Input */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Data Payload *</Text>
<TextInput
style={[styles.textInput, styles.multilineInput]}
placeholder="Enter the data you want to sign..."
value={formState.payload}
onChangeText={(value) => updateFormState({ payload: value })}
multiline={true}
numberOfLines={4}
maxLength={500}
textAlignVertical="top"
editable={!formState.isLoading}
/>
<Text style={styles.charCount}>
{formState.payload.length}/500
</Text>
</View>
{/* Auth Level Dropdown */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Authentication Level *</Text>
<AuthLevelDropdown
selectedValue={formState.selectedAuthLevel}
onValueChange={(value) => updateFormState({ selectedAuthLevel: value })}
enabled={!formState.isLoading}
/>
<Text style={styles.helpText}>
Level 4 is recommended for maximum security
</Text>
</View>
{/* Authenticator Type Dropdown */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Authenticator Type *</Text>
<AuthenticatorTypeDropdown
selectedValue={formState.selectedAuthenticatorType}
onValueChange={(value) => updateFormState({ selectedAuthenticatorType: value })}
enabled={!formState.isLoading}
/>
<Text style={styles.helpText}>
Choose the authentication method for signing
</Text>
</View>
{/* Reason Input */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Signing Reason *</Text>
<TextInput
style={styles.textInput}
placeholder="Enter reason for signing"
value={formState.reason}
onChangeText={(value) => updateFormState({ reason: value })}
maxLength={100}
editable={!formState.isLoading}
/>
<Text style={styles.charCount}>
{formState.reason.length}/100
</Text>
</View>
</View>
{/* Submit Button */}
<TouchableOpacity
style={[styles.submitButton, formState.isLoading && styles.submitButtonDisabled]}
onPress={handleSubmit}
disabled={formState.isLoading}
>
{formState.isLoading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator color="#FFFFFF" size="small" />
<Text style={styles.submitButtonText}>Processing...</Text>
</View>
) : (
<Text style={styles.submitButtonText}>Sign Data</Text>
)}
</TouchableOpacity>
</ScrollView>
{/* Password Challenge Modal */}
<PasswordChallengeModal />
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8f9fa',
},
scrollContent: {
flexGrow: 1,
padding: 20,
},
header: {
marginBottom: 30,
alignItems: 'center',
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#1a1a1a',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: '#666',
textAlign: 'center',
lineHeight: 22,
},
form: {
marginBottom: 30,
},
inputGroup: {
marginBottom: 24,
},
label: {
fontSize: 16,
fontWeight: '600',
color: '#1a1a1a',
marginBottom: 8,
},
textInput: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 16,
fontSize: 16,
backgroundColor: '#fff',
color: '#1a1a1a',
},
multilineInput: {
height: 100,
textAlignVertical: 'top',
},
charCount: {
fontSize: 12,
color: '#888',
textAlign: 'right',
marginTop: 4,
},
helpText: {
fontSize: 12,
color: '#666',
marginTop: 4,
fontStyle: 'italic',
},
submitButton: {
backgroundColor: '#007AFF',
borderRadius: 12,
paddingVertical: 16,
paddingHorizontal: 24,
alignItems: 'center',
justifyContent: 'center',
elevation: 3,
shadowColor: '#007AFF',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
},
submitButtonDisabled: {
backgroundColor: '#ccc',
shadowOpacity: 0,
elevation: 0,
},
submitButtonText: {
color: '#FFFFFF',
fontSize: 18,
fontWeight: '600',
},
loadingContainer: {
flexDirection: 'row',
alignItems: 'center',
},
infoSection: {
marginTop: 10,
marginBottom: 10,
padding: 16,
backgroundColor: '#e8f4fd',
borderRadius: 8,
borderLeftWidth: 4,
borderLeftColor: '#007AFF',
},
infoTitle: {
fontSize: 16,
fontWeight: '600',
color: '#1a1a1a',
marginBottom: 8,
},
infoText: {
fontSize: 14,
color: '#555',
lineHeight: 20,
},
});
export default DataSigningInputScreen;
// src/tutorial/screens/dataSigning/components/AuthLevelDropdown.tsx
import React, { useState } from 'react';
import {
View,
Text,
TouchableOpacity,
Modal,
FlatList,
StyleSheet,
} from 'react-native';
import { DropdownDataService } from '../../../services/DropdownDataService';
interface AuthLevelDropdownProps {
selectedValue: string;
onValueChange: (value: string) => void;
enabled?: boolean;
}
const AuthLevelDropdown: React.FC<AuthLevelDropdownProps> = ({
selectedValue,
onValueChange,
enabled = true,
}) => {
const [isVisible, setIsVisible] = useState(false);
const options = DropdownDataService.getAuthLevelOptions();
const handleSelect = (value: string) => {
onValueChange(value);
setIsVisible(false);
};
const displayValue = selectedValue || 'Select Authentication Level';
return (
<>
<TouchableOpacity
style={[styles.dropdown, !enabled && styles.dropdownDisabled]}
onPress={() => enabled && setIsVisible(true)}
disabled={!enabled}
>
<Text style={[styles.dropdownText, !selectedValue && styles.placeholderText]}>
{displayValue}
</Text>
<Text style={styles.dropdownArrow}>โผ</Text>
</TouchableOpacity>
<Modal
visible={isVisible}
transparent={true}
animationType="fade"
onRequestClose={() => setIsVisible(false)}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Select Authentication Level</Text>
<TouchableOpacity
style={styles.closeButton}
onPress={() => setIsVisible(false)}
>
<Text style={styles.closeButtonText}>โ</Text>
</TouchableOpacity>
</View>
<FlatList
data={options}
keyExtractor={(item) => item.value}
renderItem={({ item }) => (
<TouchableOpacity
style={[
styles.option,
selectedValue === item.value && styles.selectedOption,
]}
onPress={() => handleSelect(item.value)}
>
<Text
style={[
styles.optionText,
selectedValue === item.value && styles.selectedOptionText,
]}
>
{item.value}
</Text>
</TouchableOpacity>
)}
showsVerticalScrollIndicator={false}
/>
</View>
</View>
</Modal>
</>
);
};
const styles = StyleSheet.create({
dropdown: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 16,
backgroundColor: '#fff',
minHeight: 56,
},
dropdownDisabled: {
backgroundColor: '#f5f5f5',
opacity: 0.6,
},
dropdownText: {
fontSize: 16,
color: '#1a1a1a',
flex: 1,
},
placeholderText: {
color: '#999',
},
dropdownArrow: {
fontSize: 12,
color: '#666',
marginLeft: 8,
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
},
modalContent: {
backgroundColor: '#fff',
borderRadius: 12,
width: '90%',
maxHeight: '70%',
elevation: 5,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 4,
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 20,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
modalTitle: {
fontSize: 18,
fontWeight: '600',
color: '#1a1a1a',
},
closeButton: {
padding: 4,
},
closeButtonText: {
fontSize: 18,
color: '#666',
fontWeight: 'bold',
},
option: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
selectedOption: {
backgroundColor: '#e8f4fd',
},
optionText: {
fontSize: 16,
color: '#1a1a1a',
},
selectedOptionText: {
color: '#007AFF',
fontWeight: '600',
},
});
export default AuthLevelDropdown;
The following image showcases the successful data signing results screen from the sample application:

// src/tutorial/screens/dataSigning/DataSigningResultScreen.tsx
import React, { useState } from 'react';
import {
View,
Text,
ScrollView,
TouchableOpacity,
StyleSheet,
Alert,
Clipboard,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useDataSigning } from '../../../uniken/providers/SDKEventProvider';
import { DataSigningService } from '../../services/DataSigningService';
const DataSigningResultScreen: React.FC = () => {
const navigation = useNavigation();
const { resultDisplay, resetState } = useDataSigning();
const [copiedField, setCopiedField] = useState<string | null>(null);
// Convert to result info items for display
const resultItems = resultDisplay
? DataSigningService.convertToResultInfoItems(resultDisplay)
: [];
const handleCopyToClipboard = async (value: string, fieldName: string) => {
try {
await Clipboard.setString(value);
setCopiedField(fieldName);
// Reset the copied state after 2 seconds
setTimeout(() => {
setCopiedField(null);
}, 2000);
console.log(`DataSigningResultScreen - Copied ${fieldName} to clipboard`);
} catch (error) {
console.error('DataSigningResultScreen - Failed to copy to clipboard:', error);
Alert.alert('Error', 'Failed to copy to clipboard');
}
};
const handleSignAnother = async () => {
console.log('DataSigningResultScreen - Sign another button pressed');
try {
await resetState();
navigation.goBack();
} catch (error) {
console.error('DataSigningResultScreen - Failed to reset state:', error);
navigation.goBack();
}
};
const renderResultItem = (item: { name: string; value: string }, index: number) => {
const isSignature = item.name === 'Payload Signature';
const isLongValue = item.value.length > 50;
const displayValue = isLongValue && !isSignature
? `${item.value.substring(0, 50)}...`
: item.value;
return (
<View key={index} style={styles.resultItem}>
<View style={styles.resultItemHeader}>
<Text style={styles.resultLabel}>{item.name}</Text>
{item.value !== 'N/A' && (
<TouchableOpacity
style={styles.copyButton}
onPress={() => handleCopyToClipboard(item.value, item.name)}
>
<Text style={styles.copyButtonText}>
{copiedField === item.name ? 'โ Copied' : '๐ Copy'}
</Text>
</TouchableOpacity>
)}
</View>
<View style={[styles.resultValueContainer, isSignature && styles.signatureContainer]}>
<Text style={[styles.resultValue, isSignature && styles.signatureText]}>
{displayValue}
</Text>
</View>
{isLongValue && !isSignature && (
<TouchableOpacity
style={styles.expandButton}
onPress={() => Alert.alert(item.name, item.value)}
>
<Text style={styles.expandButtonText}>View Full Value</Text>
</TouchableOpacity>
)}
{isSignature && (
<TouchableOpacity
style={styles.expandButton}
onPress={() => Alert.alert('Complete Signature', item.value)}
>
<Text style={styles.expandButtonText}>View Complete Signature</Text>
</TouchableOpacity>
)}
</View>
);
};
if (!resultDisplay) {
return (
<View style={styles.container}>
<View style={styles.errorContainer}>
<Text style={styles.errorText}>No signing results available</Text>
</View>
</View>
);
}
return (
<View style={styles.container}>
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* Success Header */}
<View style={styles.successHeader}>
<View style={styles.successIcon}>
<Text style={styles.successIconText}>โ
</Text>
</View>
<Text style={styles.successTitle}>Data Signing Successful!</Text>
<Text style={styles.successSubtitle}>
Your data has been cryptographically signed
</Text>
</View>
{/* Results Section */}
<View style={styles.resultsSection}>
<Text style={styles.sectionTitle}>Signing Results</Text>
<Text style={styles.sectionSubtitle}>
All values below have been cryptographically verified
</Text>
<View style={styles.resultsContainer}>
{resultItems.map(renderResultItem)}
</View>
</View>
{/* Actions Section */}
<View style={styles.actionsSection}>
<TouchableOpacity
style={[styles.button, styles.primaryButton]}
onPress={handleSignAnother}
>
<Text style={styles.buttonText}>๐ Sign Another Document</Text>
</TouchableOpacity>
</View>
</ScrollView>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8f9fa',
},
scrollContent: {
flexGrow: 1,
padding: 20,
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
errorText: {
fontSize: 18,
color: '#666',
textAlign: 'center',
marginBottom: 20,
},
successHeader: {
alignItems: 'center',
marginBottom: 30,
paddingVertical: 20,
},
successIcon: {
marginBottom: 16,
},
successIconText: {
fontSize: 48,
},
successTitle: {
fontSize: 24,
fontWeight: 'bold',
color: '#1a1a1a',
marginBottom: 8,
textAlign: 'center',
},
successSubtitle: {
fontSize: 16,
color: '#666',
textAlign: 'center',
lineHeight: 22,
},
resultsSection: {
marginBottom: 30,
},
sectionTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#1a1a1a',
marginBottom: 8,
},
sectionSubtitle: {
fontSize: 14,
color: '#666',
marginBottom: 20,
lineHeight: 20,
},
resultsContainer: {
gap: 16,
},
resultItem: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 16,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
resultItemHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
resultLabel: {
fontSize: 16,
fontWeight: '600',
color: '#1a1a1a',
flex: 1,
},
copyButton: {
paddingHorizontal: 12,
paddingVertical: 6,
backgroundColor: '#f0f0f0',
borderRadius: 6,
},
copyButtonText: {
fontSize: 12,
color: '#007AFF',
fontWeight: '500',
},
resultValueContainer: {
backgroundColor: '#f8f9fa',
borderRadius: 8,
padding: 12,
borderWidth: 1,
borderColor: '#e9ecef',
},
signatureContainer: {
backgroundColor: '#fff3cd',
borderColor: '#ffeeba',
},
resultValue: {
fontSize: 14,
color: '#1a1a1a',
lineHeight: 20,
},
signatureText: {
fontFamily: 'monospace',
fontSize: 12,
color: '#856404',
},
expandButton: {
marginTop: 8,
alignSelf: 'flex-start',
paddingHorizontal: 12,
paddingVertical: 6,
backgroundColor: '#e8f4fd',
borderRadius: 6,
},
expandButtonText: {
fontSize: 12,
color: '#007AFF',
fontWeight: '500',
},
actionsSection: {
gap: 12,
},
button: {
borderRadius: 12,
paddingVertical: 16,
paddingHorizontal: 24,
alignItems: 'center',
justifyContent: 'center',
},
primaryButton: {
backgroundColor: '#007AFF',
elevation: 3,
shadowColor: '#007AFF',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
},
buttonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: '600',
},
});
export default DataSigningResultScreen;
Let's implement the React Context-based state management that coordinates between UI components and the REL-ID SDK event system.
The SDKEventProvider manages all data signing state and coordinates between UI and SDK:
// src/uniken/providers/SDKEventProvider.tsx (Data Signing Section)
import React, { createContext, useContext, useEffect, useCallback, ReactNode, useState } from 'react';
import rdnaService from '../services/rdnaService';
import NavigationService from '../../tutorial/navigation/NavigationService';
import type {
RDNADataSigningResponse,
RDNADataSigningPasswordChallengeData
} from '../types/rdnaEvents';
// Import data signing services and types
import { DataSigningService } from '../../tutorial/services/DataSigningService';
import type {
DataSigningFormState,
PasswordModalState,
DataSigningResponse,
DataSigningResultDisplay,
} from '../../tutorial/types/DataSigningTypes';
/**
* SDK Event Context Interface - Data Signing Section
*/
interface SDKEventContextType {
// Data Signing State
formState: DataSigningFormState;
updateFormState: (updates: Partial<DataSigningFormState>) => void;
passwordModalState: PasswordModalState;
updatePasswordModalState: (updates: Partial<PasswordModalState>) => void;
// Data Signing Actions
submitDataSigning: () => Promise<void>;
submitPassword: () => Promise<void>;
cancelPasswordModal: () => Promise<void>;
resetDataSigningState: () => Promise<void>;
// Data Signing Results
signingResult: DataSigningResponse | null;
resultDisplay: DataSigningResultDisplay | null;
}
/**
* SDK Event Provider Component - Data Signing Implementation
*/
export const SDKEventProvider: React.FC<SDKEventProviderProps> = ({ children }) => {
const [currentScreen, setCurrentScreen] = useState<string | null>(null);
// =============================================================================
// DATA SIGNING STATE
// =============================================================================
// Form state for data signing input
const [formState, setFormState] = useState<DataSigningFormState>({
payload: '',
selectedAuthLevel: '',
selectedAuthenticatorType: '',
reason: '',
isLoading: false,
});
// Password modal state for step-up authentication
const [passwordModalState, setPasswordModalState] = useState<PasswordModalState>({
isVisible: false,
password: '',
challengeMode: 0,
attemptsLeft: 3,
authenticationOptions: [],
isLargeModal: false,
keyboardHeight: 0,
responseData: null,
});
// Results from data signing operation
const [signingResult, setSigningResult] = useState<DataSigningResponse | null>(null);
const [resultDisplay, setResultDisplay] = useState<DataSigningResultDisplay | null>(null);
// =============================================================================
// DATA SIGNING EVENT HANDLERS
// =============================================================================
/**
* Handles the final data signing response event
*/
const handleDataSigningResponse = useCallback((response: RDNADataSigningResponse) => {
console.log('SDKEventProvider - Data signing response received:', response);
// Convert SDK response to our internal format
const internalResponse: DataSigningResponse = {
dataPayload: response.dataPayload,
dataPayloadLength: response.dataPayloadLength,
reason: response.reason,
payloadSignature: response.payloadSignature,
dataSignatureID: response.dataSignatureID,
authLevel: response.authLevel,
authenticationType: response.authenticationType,
status: response.status,
error: response.error,
};
// Update state
setSigningResult(internalResponse);
setFormState(prev => ({ ...prev, isLoading: false }));
// Check if signing was successful
if (response.error.shortErrorCode === 0 && response.status.statusCode === 100) {
// Format result for display (excluding status and error)
const displayData = DataSigningService.formatSigningResultForDisplay(internalResponse);
setResultDisplay(displayData);
console.log('SDKEventProvider - Data signing successful');
} else {
console.error('SDKEventProvider - Data signing failed:', response.error);
// Show error dialog
Alert.alert(
'Signing Failed',
DataSigningService.getErrorMessage(response.error.shortErrorCode),
[
{
text: 'Try Again',
onPress: () => {
setFormState(prev => ({ ...prev, isLoading: false }));
},
},
]
);
// Reset state on error
try {
DataSigningService.resetState();
} catch (resetError) {
console.error('SDKEventProvider - Failed to reset state after error:', resetError);
}
}
}, []);
/**
* Handles password challenge during data signing
*/
const handleDataSigningPasswordChallenge = useCallback((challengeData: RDNADataSigningPasswordChallengeData) => {
console.log('SDKEventProvider - Data signing password challenge received:', challengeData);
// Extract authentication options from challenge response
const authOptions = challengeData.challengeResponse?.challengeInfo?.map(
(challenge) => `${challenge.key}: ${challenge.value}`
) || [];
// Update password modal state
setPasswordModalState(prev => ({
...prev,
isVisible: true,
challengeMode: challengeData.challengeMode,
attemptsLeft: challengeData.attemptsLeft,
authenticationOptions: authOptions,
isLargeModal: authOptions.length > 3,
responseData: challengeData,
}));
console.log('SDKEventProvider - Password challenge modal displayed');
}, []);
// =============================================================================
// DATA SIGNING ACTIONS
// =============================================================================
/**
* Updates form state
*/
const updateFormState = useCallback((updates: Partial<DataSigningFormState>) => {
setFormState(prev => ({ ...prev, ...updates }));
}, []);
/**
* Updates password modal state
*/
const updatePasswordModalState = useCallback((updates: Partial<PasswordModalState>) => {
setPasswordModalState(prev => ({ ...prev, ...updates }));
}, []);
/**
* Submits data signing request
*/
const submitDataSigning = useCallback(async () => {
console.log('SDKEventProvider - Submitting data signing request');
// Validate input
const validation = DataSigningService.validateSigningInput(
formState.payload,
formState.selectedAuthLevel,
formState.selectedAuthenticatorType,
formState.reason
);
if (!validation.isValid) {
const errorMessage = validation.errors.join('\n');
Alert.alert('Validation Error', errorMessage);
return;
}
// Convert dropdown values to enums
const { authLevel, authenticatorType } = DataSigningService.convertDropdownToEnums(
formState.selectedAuthLevel,
formState.selectedAuthenticatorType
);
// Create request object
const request: DataSigningRequest = {
payload: formState.payload,
authLevel,
authenticatorType,
reason: formState.reason,
};
// Set loading state
setFormState(prev => ({ ...prev, isLoading: true }));
try {
await DataSigningService.signData(request);
console.log('SDKEventProvider - Data signing request submitted successfully');
} catch (error) {
console.error('SDKEventProvider - Data signing request failed:', error);
setFormState(prev => ({ ...prev, isLoading: false }));
const errorMessage = error instanceof Error ? error.message : 'Failed to initiate data signing';
Alert.alert('Error', errorMessage);
}
}, [formState]);
/**
* Submits password for step-up authentication
*/
const submitPassword = useCallback(async () => {
console.log('SDKEventProvider - Submitting password for data signing');
if (!passwordModalState.password.trim()) {
Alert.alert('Validation Error', 'Please enter your password');
return;
}
try {
await DataSigningService.submitPassword(
passwordModalState.password,
passwordModalState.challengeMode
);
console.log('SDKEventProvider - Password submitted successfully');
// Clear password and keep modal visible for biometric prompt
setPasswordModalState(prev => ({ ...prev, password: '' }));
} catch (error) {
console.error('SDKEventProvider - Password submission failed:', error);
const errorMessage = error instanceof Error ? error.message : 'Password authentication failed';
Alert.alert('Authentication Failed', errorMessage);
// Clear password for retry
setPasswordModalState(prev => ({ ...prev, password: '' }));
}
}, [passwordModalState.password, passwordModalState.challengeMode]);
/**
* Cancels password modal and resets data signing state
*/
const cancelPasswordModal = useCallback(async () => {
console.log('SDKEventProvider - Cancelling password modal');
try {
// Reset data signing state in SDK
await DataSigningService.resetState();
// Hide modal and reset states
setPasswordModalState(prev => ({
...prev,
isVisible: false,
password: '',
}));
setFormState(prev => ({ ...prev, isLoading: false }));
console.log('SDKEventProvider - Password modal cancelled successfully');
} catch (error) {
console.error('SDKEventProvider - Failed to cancel password modal:', error);
// Hide modal anyway
setPasswordModalState(prev => ({
...prev,
isVisible: false,
password: '',
}));
setFormState(prev => ({ ...prev, isLoading: false }));
throw error;
}
}, []);
/**
* Resets all data signing state
*/
const resetDataSigningState = useCallback(async () => {
console.log('SDKEventProvider - Resetting data signing state');
try {
// Reset SDK state
await DataSigningService.resetState();
} catch (error) {
console.error('SDKEventProvider - Failed to reset SDK state:', error);
}
// Reset all local state
setFormState({
payload: '',
selectedAuthLevel: '',
selectedAuthenticatorType: '',
reason: '',
isLoading: false,
});
setPasswordModalState({
isVisible: false,
password: '',
challengeMode: 0,
attemptsLeft: 3,
authenticationOptions: [],
isLargeModal: false,
keyboardHeight: 0,
responseData: null,
});
setSigningResult(null);
setResultDisplay(null);
console.log('SDKEventProvider - Data signing state reset complete');
}, []);
// =============================================================================
// EVENT LISTENER SETUP
// =============================================================================
useEffect(() => {
const eventManager = rdnaService.getEventManager();
// Register data signing event listeners
eventManager.on('onAuthenticateUserAndSignData', handleDataSigningResponse);
eventManager.on('getPasswordStepUpAuthentication', handleDataSigningPasswordChallenge);
console.log('SDKEventProvider - Data signing event listeners registered');
return () => {
eventManager.off('onAuthenticateUserAndSignData', handleDataSigningResponse);
eventManager.off('getPasswordStepUpAuthentication', handleDataSigningPasswordChallenge);
console.log('SDKEventProvider - Data signing event listeners cleaned up');
};
}, [handleDataSigningResponse, handleDataSigningPasswordChallenge]);
// =============================================================================
// CONTEXT VALUE
// =============================================================================
const contextValue: SDKEventContextType = {
// Data Signing State
formState,
updateFormState,
passwordModalState,
updatePasswordModalState,
// Data Signing Actions
submitDataSigning,
submitPassword,
cancelPasswordModal,
resetState: resetDataSigningState,
// Data Signing Results
signingResult,
resultDisplay,
};
return (
<SDKEventContext.Provider value={contextValue}>
{children}
</SDKEventContext.Provider>
);
};
/**
* Custom hook to use data signing functionality
*/
export const useDataSigning = () => {
const context = useContext(SDKEventContext);
if (context === undefined) {
throw new Error('useDataSigning must be used within an SDKEventProvider');
}
return context;
};
The following image showcases the authentication required modal during step-up authentication:

// src/tutorial/screens/dataSigning/components/PasswordChallengeModal.tsx
import React, { useState, useEffect } from 'react';
import {
Modal,
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Alert,
ScrollView,
KeyboardAvoidingView,
Platform,
} from 'react-native';
import { useDataSigning } from '../../../../uniken/providers/SDKEventProvider';
const PasswordChallengeModal: React.FC = () => {
const {
passwordModalState,
updatePasswordModalState,
submitPassword,
cancelPasswordModal,
} = useDataSigning();
const [isProcessing, setIsProcessing] = useState(false);
const handleSubmit = async () => {
setIsProcessing(true);
try {
await submitPassword();
} catch (error) {
console.error('PasswordChallengeModal - Submit error:', error);
} finally {
setIsProcessing(false);
}
};
const handleCancel = async () => {
setIsProcessing(true);
try {
await cancelPasswordModal();
} catch (error) {
console.error('PasswordChallengeModal - Cancel error:', error);
updatePasswordModalState({ isVisible: false });
} finally {
setIsProcessing(false);
}
};
return (
<Modal
visible={passwordModalState.isVisible}
transparent={true}
animationType="slide"
onRequestClose={handleCancel}
>
<KeyboardAvoidingView
style={styles.overlay}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<View style={styles.modalContainer}>
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>Authentication Required</Text>
<Text style={styles.subtitle}>
Please enter your password to complete data signing
</Text>
</View>
{/* Authentication Options */}
{passwordModalState.authenticationOptions.length > 0 && (
<View style={styles.optionsSection}>
<Text style={styles.optionsTitle}>Available Authentication Methods:</Text>
{passwordModalState.authenticationOptions.map((option, index) => (
<Text key={index} style={styles.optionItem}>โข {option}</Text>
))}
</View>
)}
{/* Password Input */}
<View style={styles.inputSection}>
<Text style={styles.label}>Password *</Text>
<TextInput
style={styles.textInput}
placeholder="Enter your password"
value={passwordModalState.password}
onChangeText={(value) => updatePasswordModalState({ password: value })}
secureTextEntry
autoFocus
editable={!isProcessing}
/>
{passwordModalState.attemptsLeft < 3 && (
<Text style={styles.attemptsText}>
{passwordModalState.attemptsLeft} attempts remaining
</Text>
)}
</View>
{/* Action Buttons */}
<View style={styles.buttonSection}>
<TouchableOpacity
style={[styles.button, styles.submitButton]}
onPress={handleSubmit}
disabled={isProcessing || !passwordModalState.password.trim()}
>
<Text style={styles.submitButtonText}>
{isProcessing ? 'Authenticating...' : 'Authenticate'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.cancelButton]}
onPress={handleCancel}
disabled={isProcessing}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
</View>
</ScrollView>
</View>
</KeyboardAvoidingView>
</Modal>
);
};
// Styles omitted for brevity - include comprehensive styling
export default PasswordChallengeModal;
Let's implement comprehensive testing scenarios to validate your data signing implementation and ensure robust error handling.
Your data signing implementation should handle these key scenarios:
Test Case 1: Successful Data Signing Flow
// Manual Testing Steps:
1. **Input Validation**
- Enter payload: "Test document for signing"
- Select Auth Level: "RDNA_AUTH_LEVEL_4 (4)"
- Select Authenticator Type: "RDNA_IDV_SERVER_BIOMETRIC (1)"
- Enter reason: "Document authorization test"
2. **Form Submission**
- Click "Sign Data" button
- Verify loading state appears
- Check console logs for:
โ
"DataSigningService - Starting data signing process"
โ
"RdnaService - AuthenticateUserAndSignData sync response success"
3. **Password Challenge** (if triggered)
- Verify password modal appears
- Enter correct password
- Click "Authenticate" button
- Check console logs for password submission success
4. **Final Result**
- Verify navigation to results screen
- Check all result fields are populated:
โ
Payload Signature (cryptographic signature)
โ
Data Signature ID (unique identifier)
โ
Original payload and parameters
5. **State Cleanup**
- Click "Sign Another Document"
- Verify return to input screen with clean state
- Check console logs for successful state reset
Expected Console Output Pattern:
SDKEventProvider - Submitting data signing request
DataSigningService - Starting data signing process
RdnaService - Initiating data signing: {payloadLength: 29, authLevel: 4, ...}
RdnaService - AuthenticateUserAndSignData sync response success
[Password challenge if required]
SDKEventProvider - Data signing response received
SDKEventProvider - Data signing successful
Test Case 2: Authentication Failure Handling
// Simulate authentication failure:
1. **Trigger Error Scenario**
- Use invalid credentials during password challenge
- Or disconnect network during signing process
- Or use unsupported authentication type
2. **Verify Error Handling**
โ
Error dialog displayed with user-friendly message
โ
Loading state properly cleared
โ
Form remains editable for retry
โ
Console logs show comprehensive error details
3. **Expected Error Messages**
- "Authentication failed. Please check your credentials and try again." (Code 102)
- "Authentication method not supported. Please try a different type." (Code 214)
- "Operation cancelled by user." (Code 153)
Test Case 3: Network/Connection Errors
// Simulate network issues:
1. **Preparation**
- Start data signing process
- Disable network connection mid-process
- Or simulate server timeout
2. **Verify Recovery**
โ
Appropriate error message shown
โ
State automatically reset
โ
User can retry operation
โ
No stuck loading states
Test Case 4: Cancel Flow Validation
// Test proper state cleanup:
1. **Password Modal Cancellation**
- Start data signing process
- When password modal appears, click "Cancel"
- Verify:
โ
Modal closes immediately
โ
Form returns to editable state
โ
Console shows: "SDKEventProvider - Password modal cancelled successfully"
โ
Console shows: "RdnaService - ResetAuthenticateUserAndSignDataState sync response success"
2. **Navigation Cancellation**
- Start signing process
- Use device back button or navigation gesture
- Verify proper state cleanup occurs
3. **Multiple Reset Calls**
- Verify reset API can be called multiple times safely
- No errors or crashes should occur
Test Case 5: Input Validation
// Test form validation:
1. **Required Field Validation**
- Submit form with empty fields
- Verify appropriate error messages
2. **Character Limit Validation**
- Enter 501 characters in payload field
- Enter 101 characters in reason field
- Verify validation prevents submission
3. **Dropdown Validation**
- Submit without selecting auth level/type
- Verify validation error displayed
Test Case 6: Authentication Level Enforcement
// Test security levels:
1. **Level 4 Authentication (Recommended)**
- Select "RDNA_AUTH_LEVEL_4 (4)"
- Verify biometric challenge is triggered
- Confirm highest security enforcement
2. **Different Auth Types**
- Test each authenticator type option
- Verify appropriate challenge type appears
- Confirm expected authentication flow
Test Case 7: Loading States & User Feedback
// Test UI behavior:
1. **Loading Indicators**
โ
Submit button shows "Processing..." with spinner
โ
Form fields become disabled during processing
โ
Password modal shows processing state
2. **Copy Functionality** (Results Screen)
โ
Copy buttons work for all result fields
โ
"Copied" confirmation appears briefly
โ
Long signatures can be viewed in full
3. **Responsive Design**
โ
UI works on different screen sizes
โ
Keyboard handling works properly
โ
Modal layouts adapt to content size
Use this checklist to ensure comprehensive testing:
Use these console checks during testing:
// Check current state (in debugger)
console.log('Form State:', formState);
console.log('Password Modal:', passwordModalState);
console.log('Signing Result:', signingResult);
// Verify service availability
console.log('DataSigningService:', DataSigningService);
console.log('RdnaService methods:', Object.getOwnPropertyNames(rdnaService));
// Check event listener registration
console.log('Event Manager:', rdnaService.getEventManager());
Monitor these performance indicators:
Let's explore the essential security practices and production considerations for implementing REL-ID data signing in enterprise applications.
Data Sensitivity | Recommended Level | Use Cases | Security Features |
Testing/Development | Level 0 | Testing environments only | โ ๏ธ No authentication - NOT for production |
Public/Standard | Level 1 | Standard documents, general approvals | Device biometric/passcode/password |
Confidential/High | Level 4 | Financial, legal, medical, high-value transactions | Biometric + Step-up Auth |
// โ
GOOD: Production-ready authentication level selection for data signing
const getRecommendedAuthLevel = (dataType: string): RDNAAuthLevel => {
switch (dataType) {
case 'financial_transaction':
case 'legal_document':
case 'medical_record':
case 'high_value_approval':
return RDNAAuthLevel.RDNA_AUTH_LEVEL_4; // Maximum security
case 'general_document':
case 'standard_approval':
return RDNAAuthLevel.RDNA_AUTH_LEVEL_1; // Standard authentication
case 'testing_only':
return RDNAAuthLevel.NONE; // Testing only - never use in production
default:
return RDNAAuthLevel.RDNA_AUTH_LEVEL_4; // Default to maximum security
}
};
// โ
GOOD: Correct authenticator type pairing for data signing
const getCorrectAuthenticatorType = (authLevel: RDNAAuthLevel): RDNAAuthenticatorType => {
switch (authLevel) {
case RDNAAuthLevel.NONE:
case RDNAAuthLevel.RDNA_AUTH_LEVEL_1:
return RDNAAuthenticatorType.NONE; // Let REL-ID choose best available
case RDNAAuthLevel.RDNA_AUTH_LEVEL_4:
return RDNAAuthenticatorType.RDNA_IDV_SERVER_BIOMETRIC; // Required for Level 4
default:
throw new Error('Unsupported authentication level for data signing');
}
};
// โ BAD: Using unsupported combinations
const badCombinations = [
{ authLevel: RDNAAuthLevel.RDNA_AUTH_LEVEL_2 }, // Will cause SDK error!
{ authLevel: RDNAAuthLevel.RDNA_AUTH_LEVEL_3 }, // Will cause SDK error!
{ authLevel: RDNAAuthLevel.RDNA_AUTH_LEVEL_4, authenticatorType: RDNAAuthenticatorType.RDNA_AUTH_PASS }, // Will cause SDK error!
];
// โ
GOOD: Comprehensive state cleanup pattern
const secureStateCleanup = async () => {
try {
// 1. Reset SDK authentication state
await rdnaService.resetAuthenticateUserAndSignDataState();
// 2. Clear sensitive form data
setFormState({
payload: '', // Clear signed data
selectedAuthLevel: '',
selectedAuthenticatorType: '',
reason: '',
isLoading: false,
});
// 3. Reset authentication modal state
setPasswordModalState({
isVisible: false,
password: '', // Clear password immediately
challengeMode: 0,
attemptsLeft: 3,
authenticationOptions: [],
isLargeModal: false,
keyboardHeight: 0,
responseData: null, // Clear challenge data
});
// 4. Clear results (optional - depends on requirements)
setSigningResult(null);
setResultDisplay(null);
console.log('Secure state cleanup completed');
} catch (error) {
console.error('State cleanup failed - potential security risk:', error);
// Force cleanup of local state even if SDK cleanup fails
setFormState(getInitialFormState());
setPasswordModalState(getInitialPasswordState());
}
};
// โ BAD: Incomplete cleanup leaving sensitive data
const badCleanup = () => {
setFormState({ ...formState, isLoading: false }); // Password still in memory!
};
// โ
GOOD: Secure password handling
const handlePasswordSubmission = async (password: string) => {
try {
// Use password immediately
await DataSigningService.submitPassword(password, challengeMode);
// Clear password from all variables immediately
password = ''; // Clear parameter
setPasswordModalState(prev => ({ ...prev, password: '' })); // Clear state
} catch (error) {
// Clear password even on error
password = '';
setPasswordModalState(prev => ({ ...prev, password: '' }));
throw error;
}
};
// โ BAD: Password persists in memory
const badPasswordHandling = async (password: string) => {
await submitPassword(password);
// Password remains in memory and state!
};
// โ
GOOD: Security-aware error handling
const getSecureErrorMessage = (error: any): string => {
const errorCode = error?.error?.shortErrorCode || error?.code;
switch (errorCode) {
case 0:
return 'Operation completed successfully';
case 102:
return 'Authentication failed. Please verify your credentials.';
case 153:
return 'Operation was cancelled by user.';
case 214:
return 'Authentication method not supported. Please try another method.';
default:
// โ
GOOD: Don't expose internal error details
console.error('Internal error details (not shown to user):', error);
return 'Operation failed. Please try again or contact support.';
}
};
// โ BAD: Exposing sensitive error information
const badErrorHandling = (error: any) => {
Alert.alert('Error', JSON.stringify(error)); // Exposes internal details!
};
// โ
GOOD: Robust error recovery with security cleanup
const handleSigningError = async (error: any) => {
console.error('Data signing error:', error);
try {
// 1. Attempt SDK state cleanup
await DataSigningService.resetState();
// 2. Clear sensitive local state
await secureStateCleanup();
// 3. Show user-friendly error
const userMessage = getSecureErrorMessage(error);
Alert.alert('Signing Error', userMessage, [
{ text: 'Try Again', onPress: () => resetToInitialState() },
{ text: 'Cancel', style: 'cancel' }
]);
} catch (cleanupError) {
console.error('Error cleanup failed:', cleanupError);
// Force application to safe state
await forceSecurityReset();
}
};
// โ
GOOD: Comprehensive audit logging
const auditSigningOperation = {
logSigningAttempt: (payload: string, authLevel: number, userId: string) => {
console.log('AUDIT: Data signing initiated', {
timestamp: new Date().toISOString(),
userId,
payloadLength: payload.length,
payloadHash: generateHash(payload), // Hash, not actual payload
authLevel,
sessionId: getCurrentSessionId(),
});
},
logSigningSuccess: (signatureId: string, userId: string) => {
console.log('AUDIT: Data signing successful', {
timestamp: new Date().toISOString(),
userId,
signatureId,
sessionId: getCurrentSessionId(),
});
},
logSigningFailure: (error: any, userId: string) => {
console.log('AUDIT: Data signing failed', {
timestamp: new Date().toISOString(),
userId,
errorCode: error?.error?.shortErrorCode,
sessionId: getCurrentSessionId(),
});
}
};
// โ BAD: No audit logging or logging sensitive data
console.log('Signing payload:', payload); // Exposes sensitive data!
// โ
GOOD: Network security considerations
const networkSecurityConfig = {
// Use HTTPS only
enforceHTTPS: true,
// Implement request timeout
requestTimeout: 30000, // 30 seconds
// Retry with exponential backoff
maxRetries: 3,
baseDelay: 1000,
// Certificate pinning (production)
certificatePinning: true,
};
// Network request with security
const secureNetworkCall = async (requestData: any) => {
try {
// Add request timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), networkSecurityConfig.requestTimeout);
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Add security headers
'X-Requested-With': 'XMLHttpRequest',
},
body: JSON.stringify(requestData),
signal: controller.signal,
});
clearTimeout(timeoutId);
return response;
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request timeout - please check your connection');
}
throw error;
}
};
// โ
GOOD: Secure data handling patterns
const secureDataHandling = {
// Don't store sensitive data unnecessarily
minimizeDataStorage: (payload: string) => {
// Store hash instead of original payload for verification
return {
payloadHash: generateHash(payload),
payloadLength: payload.length,
timestamp: Date.now(),
};
},
// Clear data from memory after use
secureClearData: (dataObject: any) => {
Object.keys(dataObject).forEach(key => {
if (typeof dataObject[key] === 'string') {
dataObject[key] = ''; // Clear string data
} else {
dataObject[key] = null; // Clear object references
}
});
},
// Validate data before processing
validateDataIntegrity: (data: any) => {
// Implement checksum validation
// Verify data hasn't been tampered with
return isValidDataStructure(data) && hasValidChecksum(data);
}
};
// โ BAD: Storing sensitive data unnecessarily
const badDataStorage = {
lastPayload: 'sensitive document content', // Don't do this!
userPasswords: ['password1', 'password2'], // Never do this!
};
Industry | Key Requirements | Implementation Notes |
Financial Services | SOX, PCI DSS, Basel III | Audit logs, data encryption, multi-factor auth |
Healthcare | HIPAA, HITECH | Patient data protection, access controls |
Government | FISMA, FedRAMP | High-security authentication, audit trails |
Legal | eIDAS, ESIGN Act | Legal validity, non-repudiation |
// โ
GOOD: Performance optimization patterns
const performanceOptimizations = {
// Batch signing operations when possible
batchSigning: async (payloads: string[]) => {
// Process multiple documents in single authentication session
return Promise.all(payloads.map(payload => signSinglePayload(payload)));
},
// Implement request deduplication
deduplicateRequests: (requestId: string) => {
// Prevent duplicate signing requests
if (pendingRequests.has(requestId)) {
return pendingRequests.get(requestId);
}
const promise = performSigning(requestId);
pendingRequests.set(requestId, promise);
return promise;
},
// Cache authentication state appropriately
cacheAuthState: (duration: number = 300000) => { // 5 minutes
// Cache authentication for short periods within same session
// Balance security with user experience
}
};
Congratulations! You've successfully implemented a complete, production-ready data signing solution using REL-ID SDK with React Native.
Throughout this codelab, you've built:
You've mastered these essential concepts:
authenticateUserAndSignData(), resetAuthenticateUserAndSignDataState(), and onAuthenticateUserAndSignData callback patternsBefore deploying to production, ensure:
Thank you for completing the REL-ID Data Signing Flow codelab! ๐
You're now equipped to build secure, production-grade data signing features in your Cordova applications using REL-ID SDK's powerful cryptographic capabilities.