🎯 Learning Path:
Welcome to the REL-ID IDV Server Biometric Step-Up for Data Signing codelab! This tutorial shows you how to add high-security biometric authentication with liveness detection to your existing data signing implementation.
You'll enhance your existing data signing app with:
By completing this codelab, you'll master:
authLevel = 4 with authenticatorType = 1 for server biometricBefore starting, ensure you have:
Navigate to the relid-IDV-post-login-data-signing folder in your cloned repository:
cd relid-IDV-post-login-data-signing
Before implementing, let's understand what makes IDV Workflow 16 unique and why server biometric step-up requires special handling.
IDV Workflow 16 is the server biometric step-up authentication flow specifically designed for data signing operations. Unlike other IDV workflows, workflow 16:
authenticateUserAndSignData() is called with authLevel = 4 and authenticatorType = 1IDV Workflow | Scenario | When Used | Handler Location |
16 | Data Signing Step-Up Auth | High-security data signing | Provider-level (SDKEventProvider) |
9 | Notification Step-Up Auth | Sensitive notification actions | Screen-level (GetNotificationsScreen) |
10 | Biometric Opt-In | User enrolling in biometric | Standalone flow |
6 | Post-Login KYC | Know Your Customer verification | Standalone flow |
Unlike notifications (workflow 9) which use screen-level handlers, data signing uses a provider-level handler because:
User Submits Form (authLevel=4, authenticatorType=1) →
authenticateUserAndSignData(payload, 4, 1, reason) →
SDK Returns Sync Response →
SDK Triggers: getIDVSelfieProcessStartConfirmation (idvWorkflow: 16) →
[Global Handler] Navigate to IDV selfie screen →
User Starts Selfie Capture →
SDK Captures Selfie with Liveness Detection →
SDK Verifies Biometric on Server →
SDK Signs Data →
SDK Triggers: onAuthenticateUserAndSignData →
[Provider Handler] Close selfie screen if present →
[Provider Handler] Wait 300ms for transition →
[Provider Handler] Check errors (priority: error code → status code) →
[Provider Handler] Navigate to result screen or show error
Use authLevel = 4 with authenticatorType = 1 for:
Let's understand the technical architecture we'll implement for IDV workflow 16 in sdk_event_provider.dart.
We'll modify sdk_event_provider.dart to add three key pieces:
Why GoRouter?
appRouter// Check current route
final currentLocation = appRouter.routeInformationProvider.value.uri.toString();
if (currentLocation.contains('idv-selfie-start')) {
appRouter.goNamed('dataSigningInputScreen', extra: sessionData);
}
Why StateProvider?
// In sdk_event_provider.dart
final dataSigningStepUpResultProvider = StateProvider<AuthenticateUserAndSignData?>((ref) => null);
// Provider handler sets result
ref.read(dataSigningStepUpResultProvider.notifier).state = response;
// Screen watches for changes
final stepUpResult = ref.watch(dataSigningStepUpResultProvider);
Priority 1: Check error.shortErrorCode != 0
Priority 2: Check status.statusCode != 100
// PRIORITY 1: Check error code first
if (response.error?.shortErrorCode != 0) {
showErrorDialog('Failed', errorMessage);
return; // Stop processing
}
// PRIORITY 2: Check status code
if (response.status?.statusCode != 100) {
showErrorDialog('Failed', statusMessage);
return; // Stop processing
}
// SUCCESS: Both checks passed
navigateToResults();
Why 300ms?
if (selfieScreenDetected) {
// Navigate back to data signing screen
appRouter.goNamed('dataSigningInputScreen', extra: sessionData);
// Wait for transition
Future.delayed(const Duration(milliseconds: 300), () {
// Set provider state for screen to process
ref.read(dataSigningStepUpResultProvider.notifier).state = response;
});
}
onAuthenticateUserAndSignData Event Triggered →
↓
_handleDataSigningResponse Called →
↓
Check if IDV selfie screen is active (route detection) →
↓
If Active:
├─ Navigate back with appRouter.goNamed()
├─ Wait 300ms for transition
└─ Set StateProvider with result
↓
If Not Active:
└─ Set StateProvider immediately
↓
DataSigningInputScreen watches StateProvider:
├─ Check error.shortErrorCode != 0 (Priority 1)
├─ Check status.statusCode != 100 (Priority 2)
└─ If success: Navigate to result screen
In this codelab, you'll modify only one file in the provider:
File | Purpose | Changes |
| Central event handler | Add StateProvider, error priority processing, selfie auto-closure |
The screen file (data_signing_input_screen.dart) should already have the watcher implementation from the base codelab. If not, you'll need to add the provider watching pattern.
Let's start by adding the StateProvider that enables communication between the provider-level handler and the screen.
Open lib/uniken/providers/sdk_event_provider.dart and locate the StateProvider declarations section (after the session data provider, around line 66).
Find this section:
/// Provider for session data
///
/// This provider holds the most recent user login session data.
/// It's updated by the SDK event handler when onUserLoggedIn fires.
final sessionDataProvider =
StateProvider<RDNAUserLoggedIn?>((ref) => null);
Add this below it (if not already present):
/// StateProvider for data signing step-up authentication result
///
/// Stores the onAuthenticateUserAndSignData event response data for server biometric step-up (idvWorkflow 16).
/// The DataSigningInputScreen watches this provider to close selfie screen and show result.
///
/// **How it works:**
/// 1. User submits data signing with server biometric (authLevel=4, authenticatorType=1)
/// 2. Selfie screen appears (idvWorkflow=16)
/// 3. User cancels/completes → onAuthenticateUserAndSignData event fires
/// 4. Provider handler sets: `ref.read(dataSigningStepUpResultProvider.notifier).state = response`
/// 5. DataSigningInputScreen watches this provider → detects change
/// 6. Screen closes selfie screen if present
/// 7. Screen shows result or navigates to result screen, then clears state back to null
final dataSigningStepUpResultProvider = StateProvider<AuthenticateUserAndSignData?>((ref) => null);
The dataSigningStepUpResultProvider provides:
Ensure you have the correct import for the data class:
import 'package:rdna_client/rdna_struct.dart';
This import provides the AuthenticateUserAndSignData class used by the StateProvider.
Now that we have the StateProvider ready, we'll implement the handler that:
Let's move to the next step to add the event handler.
Now let's implement the main handler that processes data signing responses with selfie screen auto-closure.
In sdk_event_provider.dart, locate the _SDKEventProviderWidgetState class and find the event handler setup section (around line 140-180).
Add this handler function in the class (before the build() method):
/// Handler for onAuthenticateUserAndSignData event (IDV Workflow 16)
///
/// This handler processes data signing responses from the SDK, with special handling
/// for server biometric step-up authentication (idvWorkflow 16).
///
/// **Flow**:
/// 1. Check if IDV selfie screen is active
/// 2. If active: Close screen, wait 300ms, set StateProvider
/// 3. If not active: Set StateProvider immediately
/// 4. DataSigningInputScreen watches StateProvider and processes result
void _handleDataSigningResponse(AuthenticateUserAndSignData data) {
print('SDKEventProvider - Data signing response received');
print(' Error Code: ${data.error?.shortErrorCode}');
print(' Status Code: ${data.status?.statusCode}');
// Safely display signature (handle null or short strings)
final signature = data.payloadSignature;
final signaturePreview = signature != null && signature.length > 20
? '${signature.substring(0, 20)}...'
: signature ?? 'null';
print(' Payload Signature: $signaturePreview');
// Get session data from provider
final sessionData = ref.read(sessionDataProvider);
// Check if we need to close selfie screen
// (IDV workflow 16 triggers selfie screen, which may still be active)
final currentLocation = appRouter.routeInformationProvider.value.uri.toString();
print('SDKEventProvider - Current route: $currentLocation');
// If selfie screen is active, navigate back to DataSigningInputScreen
// Check for IDV selfie screen routes (hyphen-separated format)
if (currentLocation.contains('idv-selfie-start')) {
print('SDKEventProvider - IDV selfie screen detected, navigating back to DataSigningInputScreen');
// Navigate back to input screen
appRouter.goNamed('dataSigningInputScreen', extra: sessionData);
// Delay setting provider state to allow screen transition (300ms)
// This is critical for IDV workflow 16 where selfie screen needs to close smoothly
Future.delayed(const Duration(milliseconds: 300), () {
print('SDKEventProvider - Setting provider state after transition');
ref.read(dataSigningStepUpResultProvider.notifier).state = data;
});
} else {
print('SDKEventProvider - No selfie screen detected, DataSigningInputScreen should already be active');
// No delay needed - set provider state immediately
// DataSigningInputScreen is already active and will watch the provider state
ref.read(dataSigningStepUpResultProvider.notifier).state = data;
}
}
Now find the _setupSDKEventHandlers() method (around line 145) and add the handler registration:
Locate this method:
/// Setup SDK event handlers
void _setupSDKEventHandlers() {
final eventManager = _rdnaService.getEventManager();
// ... existing handler registrations ...
}
Add this registration in the method:
// Register data signing response handler (IDV Workflow 16)
eventManager.setDataSigningResponseHandler(_handleDataSigningResponse);
print('SDKEventProvider - Data signing response handler registered');
final currentLocation = appRouter.routeInformationProvider.value.uri.toString();
if (currentLocation.contains('idv-selfie-start')) {
// Selfie screen is active
}
Key Points:
appRouter is the global GoRouter instancerouteInformationProvider.value.uri.toString() gets current route path'idv-selfie-start' (hyphen-separated route name)if (currentLocation.contains('idv-selfie-start')) {
// Navigate back to input screen
appRouter.goNamed('dataSigningInputScreen', extra: sessionData);
// Wait 300ms for smooth transition
Future.delayed(const Duration(milliseconds: 300), () {
ref.read(dataSigningStepUpResultProvider.notifier).state = data;
});
} else {
// No delay needed - set state immediately
ref.read(dataSigningStepUpResultProvider.notifier).state = data;
}
Why This Pattern?:
ref.read(dataSigningStepUpResultProvider.notifier).state = data;
What Happens Next:
1. User completes biometric capture
↓
2. SDK triggers onAuthenticateUserAndSignData event
↓
3. _handleDataSigningResponse called with response
↓
4. Check current route → '/idv-selfie-start'
↓
5. Navigate back: appRouter.goNamed('dataSigningInputScreen')
↓
6. Wait 300ms for screen transition
↓
7. Set StateProvider: dataSigningStepUpResultProvider = response
↓
8. DataSigningInputScreen detects change (watching provider)
↓
9. Screen processes result with error priority checking
↓
10. Screen clears state: dataSigningStepUpResultProvider = null
Scenario 1: Server Biometric Success
/idv-selfie-start → Navigate back → 300ms delay → Set StateProvider → Screen shows resultsScenario 2: Server Biometric Error
/idv-selfie-start → Navigate back → 300ms delay → Set StateProvider → Screen shows errorScenario 3: Password/LDA Success
/data-signing-input → No navigation → Set StateProvider immediately → Screen shows resultsScenario 4: Password/LDA Error
/data-signing-input → No navigation → Set StateProvider immediately → Screen shows errorYour provider-level handler is complete! In the next step, we'll verify the screen-level watcher implementation that processes results from the StateProvider.
Now let's verify that the DataSigningInputScreen has the proper watcher implementation to process results from the StateProvider.
Open lib/tutorial/screens/data_signing/data_signing_input_screen.dart and verify the following patterns are present.
Verify this import exists (around line 31):
import '../../../uniken/providers/sdk_event_provider.dart';
Verify this code exists in the build method (around line 365):
@override
Widget build(BuildContext context) {
// Watch for data signing step-up result from provider (IDV workflow 16)
// This handles server biometric step-up authentication result
final stepUpResult = ref.watch(dataSigningStepUpResultProvider);
// Process result if available (after selfie screen closes)
if (stepUpResult != null) {
// Schedule result processing for after current build completes
WidgetsBinding.instance.addPostFrameCallback((_) {
_handleDataSigningStepUpResult(stepUpResult);
});
}
return Scaffold(
// ... rest of build
);
}
Key Points:
ref.watch(dataSigningStepUpResultProvider): Watches for state changesWidgetsBinding.instance.addPostFrameCallback: Processes result after build completesstepUpResult != nullVerify this handler method exists (around line 293):
/// Handles data signing step-up result from provider (IDV workflow 16)
///
/// This method is called when the provider-level handler sets the result after
/// server biometric step-up authentication completes.
///
/// **Pattern:**
/// 1. Clear provider state (prevent infinite loop)
/// 2. Stop loading state
/// 3. Check error code FIRST (priority 1)
/// 4. Check status code (priority 2)
/// 5. Navigate to result screen on success
void _handleDataSigningStepUpResult(AuthenticateUserAndSignData response) {
print('DataSigningInputScreen - Processing step-up result from provider');
print(' Error Code: ${response.error?.shortErrorCode}');
print(' Status Code: ${response.status?.statusCode}');
// CRITICAL: Clear provider state first to prevent re-processing
ref.read(dataSigningStepUpResultProvider.notifier).state = null;
// Stop loading state
if (mounted) {
setState(() => _isLoading = false);
}
// PRIORITY 1: Check error code first
if (response.error?.shortErrorCode != 0) {
print('DataSigningInputScreen - Step-up error detected');
print(' Error Code: ${response.error?.shortErrorCode}');
print(' Error Message: ${response.error?.errorString}');
if (mounted) {
final errorMessage = response.error?.errorString ??
DataSigningService.getErrorMessage(response.error?.shortErrorCode ?? -1);
_showErrorDialog('Data signing failed: $errorMessage');
}
return; // Stop processing
}
// PRIORITY 2: Check status code
if (response.status?.statusCode != 100) {
print('DataSigningInputScreen - Step-up status error');
print(' Status Code: ${response.status?.statusCode}');
print(' Status Message: ${response.status?.statusMessage}');
if (mounted) {
_showErrorDialog('Data signing failed with status: ${response.status?.statusCode}');
}
return; // Stop processing
}
// SUCCESS: Both error and status checks passed
print('DataSigningInputScreen - Step-up successful, navigating to result screen');
// Navigate to results screen with the response data
if (mounted) {
context.push('/data-signing-result', extra: response);
}
}
Key Features:
ref.read(dataSigningStepUpResultProvider.notifier).state = null prevents infinite loopDataSigningService.getErrorMessage() for readable errors1. Provider sets StateProvider state
↓
2. Screen's build() method triggers (watching provider)
↓
3. stepUpResult != null detected
↓
4. Schedule callback: WidgetsBinding.instance.addPostFrameCallback
↓
5. After build completes, callback fires:
↓
6. _handleDataSigningStepUpResult(response) called
↓
7. Clear StateProvider (set to null)
↓
8. Stop loading state
↓
9. Check error.shortErrorCode != 0 (Priority 1)
↓
10. Check status.statusCode != 100 (Priority 2)
↓
11. If success: Navigate to result screen
If error: Show error dialog
If the screen doesn't have this implementation yet, you need to add:
Screen Implementation:
Your implementation is complete! Let's test it to verify everything works correctly across all scenarios.
Let's verify your IDV workflow 16 implementation works correctly across all scenarios.
Expected Logs:
flutter: SDKEventProvider - Data signing response received
flutter: Error Code: 0
flutter: Status Code: 100
flutter: SDKEventProvider - Current route: /idv-selfie-start
flutter: SDKEventProvider - IDV selfie screen detected, navigating back to DataSigningInputScreen
flutter: SDKEventProvider - Setting provider state after transition
flutter: DataSigningInputScreen - Processing step-up result from provider
flutter: Error Code: 0
flutter: Status Code: 100
flutter: DataSigningInputScreen - Step-up successful, navigating to result screen
Expected Error Messages:
Expected Logs:
flutter: SDKEventProvider - Data signing response received
flutter: Error Code: 0
flutter: Status Code: 100
flutter: SDKEventProvider - Current route: /data-signing-input
flutter: SDKEventProvider - No selfie screen detected, DataSigningInputScreen should already be active
flutter: DataSigningInputScreen - Processing step-up result from provider
Core Functionality:
Error Handling:
Other Authentication Methods:
Edge Cases:
Issue: Selfie screen doesn't close
appRouter is the correct global instanceIssue: Alert appears behind selfie screen
Issue: Results appear for error responses
Issue: Password/LDA delayed unnecessarily
Issue: Infinite loop (screen keeps rebuilding)
You've successfully added IDV Server Biometric Step-Up Authentication (Workflow 16) to your Flutter data signing implementation!
authLevel = 4 with server biometricStateProvider for Cross-Screen Communication:
// In sdk_event_provider.dart
final dataSigningStepUpResultProvider = StateProvider<AuthenticateUserAndSignData?>((ref) => null);
// Provider sets result
ref.read(dataSigningStepUpResultProvider.notifier).state = data;
// Screen watches for changes
final stepUpResult = ref.watch(dataSigningStepUpResultProvider);
GoRouter for Screen Navigation:
// Check current route
final currentLocation = appRouter.routeInformationProvider.value.uri.toString();
if (currentLocation.contains('idv-selfie-start')) {
// Navigate back
appRouter.goNamed('dataSigningInputScreen', extra: sessionData);
}
Error Priority Checking:
// Priority 1: Error code
if (response.error?.shortErrorCode != 0) {
showErrorDialog('Failed', errorMessage);
return;
}
// Priority 2: Status code
if (response.status?.statusCode != 100) {
showErrorDialog('Failed', statusMessage);
return;
}
Conditional Delay for Smooth Transitions:
if (currentLocation.contains('idv-selfie-start')) {
appRouter.goNamed('dataSigningInputScreen', extra: sessionData);
Future.delayed(const Duration(milliseconds: 300), () {
ref.read(dataSigningStepUpResultProvider.notifier).state = data;
});
} else {
ref.read(dataSigningStepUpResultProvider.notifier).state = data;
}
Before deploying, ensure:
flutter build --releaseExplore More REL-ID Features:
Enhance Your Implementation:
🔐 Congratulations! You've mastered IDV Server Biometric Step-Up Authentication for Data Signing with REL-ID SDK!
Your Flutter app now provides high-security biometric authentication with liveness detection for sensitive data signing operations, with a smooth, professional user experience!