🎯 Learning Path:

  1. Complete REL-ID Complete Activation & Login Flow Codelab
  2. Complete REL-ID Data Signing Flow Codelab
  3. You are here → Add IDV Server Biometric Step-Up (Workflow 16)

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.

What You'll Build

You'll enhance your existing data signing app with:

What You'll Learn

By completing this codelab, you'll master:

  1. IDV Workflow 16 Integration: Implementing authLevel = 4 with authenticatorType = 1 for server biometric
  2. GoRouter Navigation Pattern: Route state access for cross-hierarchy screen management
  3. Selfie Screen Detection: Detecting and closing IDV selfie screen in provider-level handler
  4. Screen Transition Timing: Adding 300ms delay after selfie closure for smooth UI updates
  5. Error Priority Checking: Implementing proper error handling (error code first, then status code)
  6. Provider-Level Architecture: Understanding why data signing uses provider-level vs screen-level handlers

Prerequisites

Before starting, ensure you have:

Get the Code

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.

What is IDV Workflow 16?

IDV Workflow 16 is the server biometric step-up authentication flow specifically designed for data signing operations. Unlike other IDV workflows, workflow 16:

IDV Workflow Comparison

IDV 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

Why Provider-Level Handler?

Unlike notifications (workflow 9) which use screen-level handlers, data signing uses a provider-level handler because:

  1. No Modal Management: Data signing has no modal to close (unlike notification action modal)
  2. Centralized State: All data signing state lives in SDKEventProvider
  3. Clean Architecture: One place for all data signing event handling
  4. Reusability: Can be used from any screen that implements data signing
  5. Simpler Implementation: No need for handler chaining or focus effects

Server Biometric Event Flow

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

When to Use Server Biometric (AuthLevel 4)?

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.

Provider-Level Handler Pattern

We'll modify sdk_event_provider.dart to add three key pieces:

  1. StateProvider: For cross-screen communication between provider and screen
  2. Error Priority Processing: Check errors before showing results
  3. Selfie Screen Auto-Closure: Logic to detect and close selfie screen via GoRouter

Key Technical Concepts

1. GoRouter Pattern

Why GoRouter?

// Check current route
final currentLocation = appRouter.routeInformationProvider.value.uri.toString();

if (currentLocation.contains('idv-selfie-start')) {
  appRouter.goNamed('dataSigningInputScreen', extra: sessionData);
}

2. StateProvider for Cross-Screen Communication

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

3. Error Priority Checking

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

4. Screen Transition Delay

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

Implementation Flow

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

Files You'll Modify

In this codelab, you'll modify only one file in the provider:

File

Purpose

Changes

lib/uniken/providers/sdk_event_provider.dart

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.

Add StateProvider Declaration

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

Why This StateProvider?

The dataSigningStepUpResultProvider provides:

Verify Import

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.

What's Next?

Now that we have the StateProvider ready, we'll implement the handler that:

  1. Detects if selfie screen is active
  2. Closes it if present
  3. Waits 300ms for transition
  4. Sets the StateProvider with the result

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.

Add Data Signing Response Handler

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

Register the Handler

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

Understanding the Implementation

Part 1: Route Detection

final currentLocation = appRouter.routeInformationProvider.value.uri.toString();

if (currentLocation.contains('idv-selfie-start')) {
  // Selfie screen is active
}

Key Points:

Part 2: Conditional Navigation and Delay

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

Part 3: StateProvider Update

ref.read(dataSigningStepUpResultProvider.notifier).state = data;

What Happens Next:

  1. StateProvider state changes
  2. DataSigningInputScreen watches this provider
  3. Screen's build method rebuilds
  4. Screen processes the result with error priority checking
  5. Screen clears state back to null

Complete Event Flow

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

Common Scenarios

Scenario 1: Server Biometric Success

Scenario 2: Server Biometric Error

Scenario 3: Password/LDA Success

Scenario 4: Password/LDA Error

What's Next?

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

Check Screen Implementation

Open lib/tutorial/screens/data_signing/data_signing_input_screen.dart and verify the following patterns are present.

Pattern 1: Import StateProvider

Verify this import exists (around line 31):

import '../../../uniken/providers/sdk_event_provider.dart';

Pattern 2: Watch StateProvider in Build Method

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:

Pattern 3: Result Handler Method

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

  1. Clear State First: ref.read(dataSigningStepUpResultProvider.notifier).state = null prevents infinite loop
  2. Priority Checking: Error code checked before status code
  3. Early Return: Stops processing on error (prevents navigation behind alerts)
  4. Mounted Checks: Ensures widget is still mounted before setState/navigation
  5. User-Friendly Messages: Uses DataSigningService.getErrorMessage() for readable errors

Understanding the Screen-Level Flow

1. 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 Implementation is Missing

If the screen doesn't have this implementation yet, you need to add:

  1. Import sdk_event_provider
  2. Add provider watcher in build method
  3. Add _handleDataSigningStepUpResult handler method

Verification Checklist

Screen Implementation:

What's Next?

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.

Test 1: Server Biometric Success Flow

  1. Fill Data Signing Form:
    • Payload: "Transfer $10,000 to account 123456"
    • Auth Level: "RDNA_AUTH_LEVEL_4 (4)"
    • Authenticator Type: "RDNA_IDV_SERVER_BIOMETRIC (1)"
    • Reason: "High-value financial transaction"
  2. Tap "Sign Data" → Loading state activates
  3. Verify Selfie Screen:
    • ✅ IDV selfie screen appears
    • ✅ Screen shows data signing specific guidelines
    • ✅ "Start Selfie Capture" button visible
  4. Complete Biometric Capture:
    • Tap "Start Selfie Capture"
    • Complete selfie with liveness detection
    • Wait for server verification
  5. Verify Auto-Closure:
    • ✅ Selfie screen automatically closes
    • ✅ DataSigningResultScreen appears (not behind selfie screen)
    • ✅ No flicker or UI glitches
    • ✅ Signature details all present

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

Test 2: Server Biometric Error Handling

  1. Trigger Error Scenario:
    • Use same form values as Test 1
    • Cancel biometric capture OR
    • Fail liveness detection
  2. Verify Error Handling:
    • ✅ Selfie screen closes automatically
    • ✅ Alert appears on DataSigningInputScreen (not behind selfie screen)
    • ✅ Alert shows user-friendly error message
    • ✅ Form data preserved for retry

Expected Error Messages:

Test 3: Password/LDA Flow (No Selfie Screen)

  1. Test Password Flow:
    • Auth Level: "RDNA_AUTH_LEVEL_3 (3)"
    • Authenticator Type: "RDNA_AUTH_PASS (2)"
    • Complete password entry
    • ✅ Results appear immediately (no 300ms delay)
    • ✅ No selfie screen involved
  2. Test LDA Flow:
    • Auth Level: "RDNA_AUTH_LEVEL_3 (3)"
    • Authenticator Type: "RDNA_AUTH_LDA (3)"
    • Complete biometric prompt
    • ✅ Results appear immediately
    • ✅ No screen closure logic triggered

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

Test 4: Hot Reload and State Management

  1. Test State Persistence:
    • Start server biometric flow
    • While on selfie screen, perform hot reload (press ‘r')
    • ✅ App recovers gracefully
    • ✅ No state corruption
  2. Test Multiple Submissions:
    • Submit multiple data signing requests rapidly
    • ✅ Each request processed independently
    • ✅ No state leakage between requests

Verification Checklist

Core Functionality:

Error Handling:

Other Authentication Methods:

Edge Cases:

Common Issues & Solutions

Issue: Selfie screen doesn't close

Issue: Alert appears behind selfie screen

Issue: Results appear for error responses

Issue: Password/LDA delayed unnecessarily

Issue: Infinite loop (screen keeps rebuilding)

🎉 You Did It!

You've successfully added IDV Server Biometric Step-Up Authentication (Workflow 16) to your Flutter data signing implementation!

What You Accomplished

Key Implementation Patterns

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

Production Checklist

Before deploying, ensure:

Next Steps

Explore More REL-ID Features:

Enhance Your Implementation:

Additional Resources

🔐 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!