Welcome to the REL-ID Additional Device Activation codelab! This tutorial builds upon the foundational MFA implementation to add sophisticated device onboarding capabilities using REL-ID Verify's push notification system.

What You'll Build

In this codelab, you'll enhance your existing MFA application with:

What You'll Learn

By completing this codelab, you'll master:

  1. Advanced SDK Event Handling: Managing addNewDeviceOptions events and device activation flows
  2. REL-ID Verify Workflows: Implementing automatic push notification-based device approval
  3. Fallback Strategies: Building robust alternative activation methods for various user scenarios
  4. Notification Systems: Creating comprehensive server notification management with user interactions
  5. Enhanced Navigation: Integrating drawer navigation with notification access points
  6. Production Patterns: Implementing error handling, status management, and user experience optimizations

Prerequisites

Before starting this codelab, ensure you have:

Get the Code from GitHub

The code to get started is stored in a GitHub repository.

You can clone the repository using the following command:

git clone https://github.com/uniken-public/codelab-flutter.git

Navigate to the relid-MFA-additional-device-activation folder in the repository you cloned earlier

Codelab Architecture Overview

This codelab extends your MFA application with three core device activation components:

  1. VerifyAuthScreen: Automatic REL-ID Verify activation with real-time status updates
  2. GetNotificationsScreen: Server notification management with interactive action modals
  3. Enhanced Event Handling: addNewDeviceOptions event processing and navigation coordination

Before implementing device activation screens, let's understand the key plugin events and APIs that power the additional device activation workflow.

Device Activation Event Flow

The device activation process follows this event-driven pattern:

User Completes MFA on Primary Device → SDK Detects New Device On Secondary Device → addNewDeviceOptions Event → VerifyAuthScreen →
Push Notifications Sent → User Approves the Notification On  Primary Device → Continue MFA Flow -> Device Activated

Core Device Activation Types

Add these Dart definitions to understand device activation data structures:

// rdna_client/lib/rdna_struct.dart (device activation type definitions)

/**
 * Device activation options data structure
 * Triggered when SDK detects unregistered device during authentication
 */
class RDNAAddNewDeviceOptions {
  String? userId; //user name
  List<String>? newDeviceOptions; //list of authentication type for new device activation
  List<RDNAChallengeInfo>? challengeInfo;

  RDNAAddNewDeviceOptions({
    this.userId,
    this.newDeviceOptions,
    this.challengeInfo,
  });

  factory RDNAAddNewDeviceOptions.fromJson(Map<String, dynamic> json) =>
      RDNAAddNewDeviceOptions(
        userId: json["userID"] == null ? null : json["userID"],
        newDeviceOptions: json["newDeviceOptions"] == null
            ? null
            : List<String>.from(json["newDeviceOptions"].map((x) => x)),
        challengeInfo: json["challengeInfo"] == null
            ? null
            : List<RDNAChallengeInfo>.from(json["challengeInfo"]
                .map((x) => RDNAChallengeInfo.fromJson(x))),
      );

  Map<String, dynamic> toJson() => {
        "userID": userId == null ? null : userId,
        "newDeviceOptions": newDeviceOptions == null
            ? null
            : List<dynamic>.from(newDeviceOptions!.map((x) => x)),
        "challengeInfo": challengeInfo == null
            ? null
            : List<dynamic>.from(challengeInfo!.map((x) => x.toJson())),
      };
}

/**
 * Notification Body Structure
 * Localized content for notification
 */
class RDNANotificationBody {
  String? lng;
  String? subject;
  String? message;
  Map<String, dynamic>? label;

  RDNANotificationBody({this.lng, this.subject, this.message, this.label});

  factory RDNANotificationBody.fromJson(Map<String, dynamic> json) =>
      RDNANotificationBody(
        lng: json['lng'],
        subject: json['subject'],
        message: json['message'],
        label: json['label'] != null
            ? Map<String, dynamic>.from(json['label'])
            : null,
      );

  Map<String, dynamic> toJson() => {
        'lng': lng,
        'subject': subject,
        'message': message,
        'label': label,
      };
}

/**
 * Notification Action Structure
 * Available actions for notification
 */
class RDNANotificationAction {
  String? label;
  String? action;
  String? authlevel;

  RDNANotificationAction({this.label, this.action, this.authlevel});

  factory RDNANotificationAction.fromJson(Map<String, dynamic> json) =>
      RDNANotificationAction(
        label: json['label'],
        action: json['action'],
        authlevel: json['authlevel'],
      );

  Map<String, dynamic> toJson() => {
        'label': label,
        'action': action,
        'authlevel': authlevel,
      };
}

/**
 * Notification Item Structure
 * Individual notification structure from API response
 */
class RDNANotification {
  String? notificationUuid;
  String? createTs;
  String? expiryTimestamp;
  int? createTsEpoch;
  int? expiryTimestampEpoch;
  List<RDNANotificationBody>? body;
  List<RDNANotificationAction>? actions;
  String? actionPerformed;
  bool? dsRequired;

  RDNANotification({
    this.notificationUuid,
    this.createTs,
    this.expiryTimestamp,
    this.createTsEpoch,
    this.expiryTimestampEpoch,
    this.body,
    this.actions,
    this.actionPerformed,
    this.dsRequired,
  });

  factory RDNANotification.fromJson(Map<String, dynamic> json) =>
      RDNANotification(
        notificationUuid: json['notification_uuid'],
        createTs: json['create_ts'],
        expiryTimestamp: json['expiry_timestamp'],
        createTsEpoch: json['create_ts_epoch'],
        expiryTimestampEpoch: json['expiry_timestamp_epoch'],
        body: json['body'] != null
            ? List<RDNANotificationBody>.from(
                json['body'].map((x) => RDNANotificationBody.fromJson(x)))
            : null,
        actions: json['actions'] != null
            ? List<RDNANotificationAction>.from(
                json['actions'].map((x) => RDNANotificationAction.fromJson(x)))
            : null,
        actionPerformed: json['action_performed'],
        dsRequired: json['ds_required'],
      );

  Map<String, dynamic> toJson() => {
        'notification_uuid': notificationUuid,
        'create_ts': createTs,
        'expiry_timestamp': expiryTimestamp,
        'create_ts_epoch': createTsEpoch,
        'expiry_timestamp_epoch': expiryTimestampEpoch,
        'body': body?.map((x) => x.toJson()).toList(),
        'actions': actions?.map((x) => x.toJson()).toList(),
        'action_performed': actionPerformed,
        'ds_required': dsRequired,
      };
}

/**
 * Get Notifications Response Structure
 * Response structure for notifications API
 */
class RDNAGetNotificationsResponse {
  List<RDNANotification>? notifications;
  String? start;
  String? count;
  String? total;

  RDNAGetNotificationsResponse({
    this.notifications,
    this.start,
    this.count,
    this.total,
  });

  factory RDNAGetNotificationsResponse.fromJson(Map<String, dynamic> json) =>
      RDNAGetNotificationsResponse(
        notifications: json['notifications'] != null
            ? List<RDNANotification>.from(
                json['notifications'].map((x) => RDNANotification.fromJson(x)))
            : null,
        start: json['start'],
        count: json['count'],
        total: json['total'],
      );

  Map<String, dynamic> toJson() => {
        'notifications': notifications?.map((v) => v.toJson()).toList(),
        'start': start,
        'count': count,
        'total': total,
      };
}

/**
 * Update Notification Response Structure
 * Response data structure for notification update
 */
class RDNAUpdateNotificationResponse {
  int? statusCode;
  String? message;
  String? notificationUuid;
  bool? isDsVerified;

  RDNAUpdateNotificationResponse({
    this.statusCode,
    this.message,
    this.notificationUuid,
    this.isDsVerified,
  });

  factory RDNAUpdateNotificationResponse.fromJson(Map<String, dynamic> json) =>
      RDNAUpdateNotificationResponse(
        statusCode: json["status_code"],
        message: json["message"],
        notificationUuid: json["notification_uuid"],
        isDsVerified: json["is_ds_verified"],
      );

  Map<String, dynamic> toJson() => {
        "status_code": statusCode,
        "message": message,
        "notification_uuid": notificationUuid,
        "is_ds_verified": isDsVerified,
      };
}

Device Activation Callback Types

Define callback types for device activation events:

// Callback type definitions for device activation events
typedef RDNAAddNewDeviceOptionsCallback = void Function(RDNAAddNewDeviceOptions);
typedef RDNAGetNotificationsCallback = void Function(RDNAResponseMetaData);
typedef RDNAUpdateNotificationCallback = void Function(RDNAResponseMetaData);

Understanding addNewDeviceOptions Event

The addNewDeviceOptions event is the cornerstone of device activation:

When It Triggers

REL-ID Verify Workflow

REL-ID Verify enables secure device-to-device approval:

  1. Push Notification Sent: SDK sends approval request to user's registered devices
  2. User Receives Notification: Registered device shows activation approval request
  3. User Approves/Rejects: User makes decision on registered device
  4. Response Processed: New device receives approval status
  5. Activation Completed: Device registration finalized, MFA flow continues

Enhance your existing RdnaService with device activation APIs. These methods handle REL-ID Verify workflows and notification management.

Adding Device Activation APIs

Extend your RdnaService class with these device activation methods:

// lib/uniken/services/rdna_service.dart (device activation additions)

/**
 * Performs REL-ID Verify authentication for device activation
 * Sends push notifications to registered devices for approval
 * @param verifyAuthStatus User's decision (true = proceed with verification, false = cancel)
 * @returns Future<RDNASyncResponse>
 */
Future<RDNASyncResponse> performVerifyAuth(bool verifyAuthStatus) async {
  try {
    print('RdnaService - Performing verify auth with status: $verifyAuthStatus');

    final response = await _rdnaClient.performVerifyAuth(verifyAuthStatus);

    if (response.error?.longErrorCode == 0) {
      print('RdnaService - PerformVerifyAuth sync response success, waiting for async events');
      return response;
    } else {
      print('RdnaService - PerformVerifyAuth sync response error: ${response.error}');
      throw response;
    }
  } catch (error) {
    print('RdnaService - PerformVerifyAuth error: $error');
    rethrow;
  }
}

/**
 * Initiates fallback device activation flow
 * Alternative method when REL-ID Verify is not available/accessible
 * @returns Future<RDNASyncResponse>
 */
Future<RDNASyncResponse> fallbackNewDeviceActivationFlow() async {
  try {
    print('RdnaService - Starting fallback new device activation flow');

    final response = await _rdnaClient.fallbackNewDeviceActivationFlow();

    if (response.error?.longErrorCode == 0) {
      print('RdnaService - FallbackNewDeviceActivationFlow sync response success, alternative activation started');
      return response;
    } else {
      print('RdnaService - FallbackNewDeviceActivationFlow sync response error: ${response.error}');
      throw response;
    }
  } catch (error) {
    print('RdnaService - FallbackNewDeviceActivationFlow error: $error');
    rethrow;
  }
}

/**
 * Retrieves server notifications for the current user
 * Loads all pending notifications with actions
 * @param recordCount Number of records to fetch (0 = all active notifications)
 * @param startIndex Index to begin fetching from (must be >= 1)
 * @param startDate Start date filter (optional)
 * @param endDate End date filter (optional)
 * @returns Future<RDNASyncResponse>
 */
Future<RDNASyncResponse> getNotifications({
  int recordCount = 0,
  int startIndex = 1,
  String startDate = '',
  String endDate = '',
}) async {
  try {
    print('RdnaService - Fetching notifications with recordCount: $recordCount, startIndex: $startIndex');

    final response = await _rdnaClient.getNotifications(
      recordCount,    // recordCount
      '',             // enterpriseID (optional)
      startIndex,     // startIndex
      startDate,      // startDate (optional)
      endDate,        // endDate (optional)
    );

    if (response.error?.longErrorCode == 0) {
      print('RdnaService - GetNotifications sync response success, waiting for onGetNotifications event');
      return response;
    } else {
      print('RdnaService - GetNotifications sync response error: ${response.error}');
      throw response;
    }
  } catch (error) {
    print('RdnaService - GetNotifications error: $error');
    rethrow;
  }
}

/**
 * Updates a notification with user action
 * Processes user decision on notification actions
 * @param notificationId Notification identifier (UUID)
 * @param response Action response value selected by user
 * @returns Future<RDNASyncResponse>
 */
Future<RDNASyncResponse> updateNotification(
  String notificationId,
  String response,
) async {
  try {
    print('RdnaService - Updating notification: $notificationId with response: $response');

    final syncResponse = await _rdnaClient.updateNotification(
      notificationId,  // notificationId
      response,        // response
    );

    if (syncResponse.error?.longErrorCode == 0) {
      print('RdnaService - UpdateNotification sync response success, waiting for onUpdateNotification event');
      return syncResponse;
    } else {
      print('RdnaService - UpdateNotification sync response error: ${syncResponse.error}');
      throw syncResponse;
    }
  } catch (error) {
    print('RdnaService - UpdateNotification error: $error');
    rethrow;
  }
}

Understanding Device Activation APIs

performVerifyAuth API

fallbackNewDeviceActivationFlow API

getNotifications API

updateNotification API

API Response Pattern

All device activation APIs follow the established REL-ID SDK pattern:

  1. Immediate Sync Response: Indicates if API call was accepted by SDK
  2. Success Check: longErrorCode == 0 means API call succeeded
  3. Async Event Processing: Actual results delivered via SDK events
  4. Error Handling: Sync errors thrown as exceptions

Enhance your existing event manager to handle device activation events. Add support for addNewDeviceOptions, notification retrieval, and notification updates.

Adding Device Activation Event Handlers

Extend your RdnaEventManager class with device activation event handling:

// lib/uniken/services/rdna_event_manager.dart (device activation additions)

class RdnaEventManager {
  // Add device activation event handlers
  RDNAAddNewDeviceOptionsCallback? _addNewDeviceOptionsHandler;
  RDNAGetNotificationsCallback? _getNotificationsHandler;
  RDNAUpdateNotificationCallback? _updateNotificationHandler;

  void registerEventListeners() {
    // ... existing MFA and MTD listeners ...

    // Register device activation event listeners
    _listeners.add(
      _rdnaClient.on('addNewDeviceOptions', _onAddNewDeviceOptions),
    );
    _listeners.add(
      _rdnaClient.on('getNotifications', _onGetNotifications),
    );
    _listeners.add(
      _rdnaClient.on('updateNotification', _onUpdateNotification),
    );
  }
}

Device Activation Event Implementations

Add these event handler methods to your event manager:

/**
 * Handles device activation options event
 * Triggered when SDK detects unregistered device during authentication
 */
void _onAddNewDeviceOptions(dynamic response) {
  print("RdnaEventManager - Add new device options event received");

  try {
    final addNewDeviceOptionsData = response as RDNAAddNewDeviceOptions;
    print("RdnaEventManager - UserID: ${addNewDeviceOptionsData.userId}");
    print("RdnaEventManager - Available options: ${addNewDeviceOptionsData.newDeviceOptions?.length ?? 0}");
    print("RdnaEventManager - Challenge info count: ${addNewDeviceOptionsData.challengeInfo?.length ?? 0}");

    // Log each activation option for debugging
    addNewDeviceOptionsData.newDeviceOptions?.asMap().forEach((index, option) {
      print("RdnaEventManager - Option ${index + 1}: $option");
    });

    if (_addNewDeviceOptionsHandler != null) {
      _addNewDeviceOptionsHandler!(addNewDeviceOptionsData);
    }
  } catch (error) {
    print("RdnaEventManager - Failed to parse add new device options: $error");
  }
}

/**
 * Handles get notifications response
 * Triggered after getNotifications API call completes
 */
void _onGetNotifications(dynamic response) {
  print("RdnaEventManager - Get notifications event received");

  try {
    final getNotificationsData = response as RDNAResponseMetaData;

    print("RdnaEventManager - Get notifications data: errCode=${getNotificationsData.error?.longErrorCode}");

    if (_getNotificationsHandler != null) {
      _getNotificationsHandler!(getNotificationsData);
    }
  } catch (error) {
    print("RdnaEventManager - Failed to parse get notifications: $error");
  }
}

/**
 * Handles update notification response
 * Triggered after updateNotification API call completes
 */
void _onUpdateNotification(dynamic response) {
  print("RdnaEventManager - Update notification event received");

  try {
    final updateNotificationData = response as RDNAResponseMetaData;

    print("RdnaEventManager - Update notification data: errCode=${updateNotificationData.error?.longErrorCode}");

    if (_updateNotificationHandler != null) {
      _updateNotificationHandler!(updateNotificationData);
    }
  } catch (error) {
    print("RdnaEventManager - Failed to parse update notification: $error");
  }
}

Event Handler Registration Methods

Add public methods for setting device activation event handlers:

// Public setter methods for device activation event handlers
void setAddNewDeviceOptionsHandler(RDNAAddNewDeviceOptionsCallback? callback) {
  _addNewDeviceOptionsHandler = callback;
}

void setGetNotificationsHandler(RDNAGetNotificationsCallback? callback) {
  _getNotificationsHandler = callback;
}

void setUpdateNotificationHandler(RDNAUpdateNotificationCallback? callback) {
  _updateNotificationHandler = callback;
}

// Enhanced cleanup method to clear device activation handlers
void clearDeviceActivationHandlers() {
  _addNewDeviceOptionsHandler = null;
  _getNotificationsHandler = null;
  _updateNotificationHandler = null;
}

// Enhanced cleanup method to clear all handlers
void cleanup() {
  // Clear existing MFA handlers
  clearActivationHandlers();

  // Clear device activation handlers
  clearDeviceActivationHandlers();

  // Clear existing MTD handlers
  clearMTDHandlers();

  // Remove all event listeners
  for (final listener in _listeners) {
    listener.cancel();
  }
  _listeners.clear();
}

Understanding Device Activation Events

addNewDeviceOptions Event

getNotifications Event

updateNotification Event

Event Manager Integration Pattern

The device activation events integrate with existing event management:

// Example of comprehensive event setup in SDKEventProvider
@override
void initState() {
  super.initState();

  final eventManager = ref.read(rdnaServiceProvider).getEventManager();

  // Existing MFA event handlers
  eventManager.setGetUserHandler(_handleGetUser);
  eventManager.setGetPasswordHandler(_handleGetPassword);
  // ... other MFA handlers ...

  // Device activation event handlers
  eventManager.setAddNewDeviceOptionsHandler(_handleAddNewDeviceOptions);
  eventManager.setGetNotificationsHandler(_handleGetNotifications);
  eventManager.setUpdateNotificationHandler(_handleUpdateNotification);
}

@override
void dispose() {
  final eventManager = ref.read(rdnaServiceProvider).getEventManager();
  eventManager.cleanup();
  super.dispose();
}

Create the VerifyAuthScreen that handles REL-ID Verify device activation with automatic push notification processing and fallback options.

VerifyAuthScreen Component Structure

// lib/tutorial/screens/mfa/verify_auth_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../navigation/app_router.dart';
import '../../../uniken/services/rdna_service.dart';
import '../../../rdna_client/lib/rdna_struct.dart';
import '../components/status_banner.dart';
import '../components/custom_button.dart';
import '../components/close_button.dart' as custom;

/**
 * Verify Auth Screen Component
 */
class VerifyAuthScreen extends ConsumerStatefulWidget {
  final RDNAAddNewDeviceOptions? eventData;
  final String? title;
  final String? subtitle;
  final RDNAAddNewDeviceOptions? responseData;

  const VerifyAuthScreen({
    Key? key,
    this.eventData,
    this.title,
    this.subtitle,
    this.responseData,
  }) : super(key: key);

  @override
  ConsumerState<VerifyAuthScreen> createState() => _VerifyAuthScreenState();
}

class _VerifyAuthScreenState extends ConsumerState<VerifyAuthScreen> {
  bool _isProcessing = false;
  String? _error;
  Map<String, dynamic>? _activationData;

  @override
  void initState() {
    super.initState();

    // Auto-call performVerifyAuth after frame renders
    if (widget.responseData != null) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        _processActivationData();
      });
    }
  }

  /**
   * Handle close button - direct resetAuthState call
   */
  Future<void> _handleClose() async {
    try {
      print('VerifyAuthScreen - Calling resetAuthState');
      final rdnaService = ref.read(rdnaServiceProvider);
      await rdnaService.resetAuthState();
      print('VerifyAuthScreen - ResetAuthState successful');
    } catch (error) {
      print('VerifyAuthScreen - ResetAuthState error: $error');
    }
  }

  /**
   * Process activation data and auto-start verify auth
   */
  Future<void> _processActivationData() async {
    if (widget.responseData == null) return;

    try {
      final data = widget.responseData!;
      setState(() {
        _activationData = {
          'userID': data.userId,
          'options': data.newDeviceOptions ?? [],
        };
      });

      print('VerifyAuthScreen - Processed activation data: $_activationData');

      // Automatically call performVerifyAuth(true) when data is processed
      await _handleVerifyAuth(true);
    } catch (error) {
      print('VerifyAuthScreen - Failed to process activation data: $error');
      setState(() {
        _error = 'Failed to process activation data';
      });
    }
  }

  /**
   * Handle REL-ID Verify authentication
   */
  Future<void> _handleVerifyAuth(bool proceed) async {
    if (_isProcessing) return;

    setState(() {
      _isProcessing = true;
      _error = null;
    });

    try {
      print('VerifyAuthScreen - Performing verify auth: $proceed');

      final rdnaService = ref.read(rdnaServiceProvider);
      final syncResponse = await rdnaService.performVerifyAuth(proceed);

      print('VerifyAuthScreen - PerformVerifyAuth sync response successful, waiting for async events');
      print('VerifyAuthScreen - Sync response received: ${syncResponse.error?.longErrorCode}');

      if (proceed) {
        // Log success message for approval
        print('VerifyAuthScreen - REL-ID Verify notification has been sent to registered devices');
      }

    } catch (error) {
      // This catch block handles sync response errors (rejected promises)
      print('VerifyAuthScreen - PerformVerifyAuth sync error: $error');

      final result = error as RDNASyncResponse;
      final errorMessage = result.error?.errorString ?? 'Verification failed';

      setState(() {
        _error = errorMessage;
      });
    } finally {
      if (mounted) {
        setState(() {
          _isProcessing = false;
        });
      }
    }
  }

  /**
   * Handle fallback new device activation flow
   */
  Future<void> _handleFallbackFlow() async {
    if (_isProcessing) return;

    setState(() {
      _isProcessing = true;
      _error = null;
    });

    try {
      print('VerifyAuthScreen - Initiating fallback new device activation flow');

      final rdnaService = ref.read(rdnaServiceProvider);
      final syncResponse = await rdnaService.fallbackNewDeviceActivationFlow();

      print('VerifyAuthScreen - FallbackNewDeviceActivationFlow sync response successful, waiting for async events');
      print('VerifyAuthScreen - Sync response received: ${syncResponse.error?.longErrorCode}');

      // Log success message for fallback initiation
      print('VerifyAuthScreen - Alternative device activation process has been initiated');

    } catch (error) {
      // This catch block handles sync response errors (rejected promises)
      print('VerifyAuthScreen - FallbackNewDeviceActivationFlow sync error: $error');

      final result = error as RDNASyncResponse;
      final errorMessage = result.error?.errorString ?? 'Fallback activation failed';

      setState(() {
        _error = errorMessage;
      });
    } finally {
      if (mounted) {
        setState(() {
          _isProcessing = false;
        });
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF8F9FA),
      body: SafeArea(
        child: SingleChildScrollView(
          child: Column(
            children: [
              // Close Button
              Align(
                alignment: Alignment.topRight,
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: custom.CloseButton(
                    onPressed: _handleClose,
                    disabled: _isProcessing,
                  ),
                ),
              ),

              Padding(
                padding: const EdgeInsets.all(20.0),
                child: Column(
                  children: [
                    const SizedBox(height: 40),

                    Text(
                      widget.title ?? 'Additional Device Activation',
                      style: const TextStyle(
                        fontSize: 28,
                        fontWeight: FontWeight.bold,
                        color: Color(0xFF2C3E50),
                      ),
                      textAlign: TextAlign.center,
                    ),

                    const SizedBox(height: 8),

                    Text(
                      widget.subtitle ?? 'Activate this device for secure access',
                      style: const TextStyle(
                        fontSize: 16,
                        color: Color(0xFF7F8C8D),
                      ),
                      textAlign: TextAlign.center,
                    ),

                    const SizedBox(height: 30),

                    // Error Display
                    if (_error != null)
                      Padding(
                        padding: const EdgeInsets.only(bottom: 20.0),
                        child: StatusBanner(
                          type: StatusBannerType.error,
                          message: _error!,
                        ),
                      ),

                    // Processing Status
                    if (_isProcessing)
                      const Padding(
                        padding: EdgeInsets.only(bottom: 20.0),
                        child: StatusBanner(
                          type: StatusBannerType.info,
                          message: 'Processing device activation...',
                        ),
                      ),

                    // Activation Information
                    if (_activationData != null) ...[
                      // Processing Message
                      Container(
                        decoration: BoxDecoration(
                          color: const Color(0xFFE3F2FD),
                          borderRadius: BorderRadius.circular(12),
                          border: const Border(
                            left: BorderSide(
                              color: Color(0xFF2196F3),
                              width: 4,
                            ),
                          ),
                        ),
                        padding: const EdgeInsets.all(20),
                        margin: const EdgeInsets.only(bottom: 20),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            const Text(
                              'REL-ID Verify Authentication',
                              style: TextStyle(
                                fontSize: 18,
                                fontWeight: FontWeight.bold,
                                color: Color(0xFF1976D2),
                              ),
                            ),
                            const SizedBox(height: 8),
                            const Text(
                              'REL-ID Verify notification has been sent to your registered devices. Please approve it to activate this device.',
                              style: TextStyle(
                                fontSize: 16,
                                color: Color(0xFF1565C0),
                                height: 1.5,
                              ),
                            ),
                          ],
                        ),
                      ),

                      // Fallback Option
                      Container(
                        decoration: BoxDecoration(
                          color: const Color(0xFFF5F5F5),
                          borderRadius: BorderRadius.circular(12),
                          border: Border.all(
                            color: const Color(0xFFE0E0E0),
                          ),
                        ),
                        padding: const EdgeInsets.all(20),
                        child: Column(
                          children: [
                            const Text(
                              'Device Not Handy?',
                              style: TextStyle(
                                fontSize: 18,
                                fontWeight: FontWeight.bold,
                                color: Color(0xFF2C3E50),
                              ),
                              textAlign: TextAlign.center,
                            ),
                            const SizedBox(height: 8),
                            const Text(
                              'If you don\'t have access to your registered devices, you can use an alternative activation method.',
                              style: TextStyle(
                                fontSize: 14,
                                color: Color(0xFF7F8C8D),
                                height: 1.4,
                              ),
                              textAlign: TextAlign.center,
                            ),
                            const SizedBox(height: 16),
                            CustomButton(
                              title: 'Activate using fallback method',
                              onPressed: _handleFallbackFlow,
                              loading: _isProcessing,
                              variant: ButtonVariant.outline,
                            ),
                          ],
                        ),
                      ),
                    ],
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Key VerifyAuthScreen Features

Automatic Activation Flow

Fallback Integration

User Experience Enhancements

The following image showcases screen from the sample application:

Device Activtion Verify Screen

Create the GetNotificationsScreen that automatically loads server notifications and provides interactive action modals for user responses.

GetNotificationsScreen Component Implementation

Due to the complexity and size of GetNotificationsScreen, I'll show the key implementation patterns here. The complete file follows Flutter's StatefulWidget pattern with Riverpod for state management.

// lib/tutorial/screens/notification/get_notifications_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../uniken/services/rdna_service.dart';
import '../../../rdna_client/lib/rdna_struct.dart';
import '../components/status_banner.dart';
import '../components/custom_button.dart';

/**
 * Get Notifications Screen Component
 */
class GetNotificationsScreen extends ConsumerStatefulWidget {
  final String userID;
  final String sessionID;
  final int sessionType;
  final String jwtToken;
  final String? loginTime;
  final String? userRole;
  final String? currentWorkFlow;

  const GetNotificationsScreen({
    Key? key,
    required this.userID,
    required this.sessionID,
    required this.sessionType,
    required this.jwtToken,
    this.loginTime,
    this.userRole,
    this.currentWorkFlow,
  }) : super(key: key);

  @override
  ConsumerState<GetNotificationsScreen> createState() => _GetNotificationsScreenState();
}

class _GetNotificationsScreenState extends ConsumerState<GetNotificationsScreen> {
  List<RDNANotification> _notifications = [];
  bool _isLoading = true;
  bool _isRefreshing = false;
  String? _error;
  RDNANotification? _selectedNotification;
  String? _selectedAction;
  bool _isProcessingAction = false;
  bool _showActionModal = false;

  @override
  void initState() {
    super.initState();

    // Set up notification event handlers
    final eventManager = ref.read(rdnaServiceProvider).getEventManager();
    eventManager.setGetNotificationsHandler(_handleGetNotificationsResponse);
    eventManager.setUpdateNotificationHandler(_handleUpdateNotificationResponse);

    // Auto-load notifications when screen loads
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _loadNotifications();
    });
  }

  @override
  void dispose() {
    // Cleanup handlers on unmount
    final eventManager = ref.read(rdnaServiceProvider).getEventManager();
    eventManager.setGetNotificationsHandler(null);
    eventManager.setUpdateNotificationHandler(null);
    super.dispose();
  }

  /**
   * Handle notifications received from onGetNotifications event
   */
  void _handleGetNotificationsResponse(RDNAResponseMetaData data) {
    print('GetNotificationsScreen - Received notifications event');

    setState(() {
      _isLoading = false;
      _isRefreshing = false;
    });

    // Check if this is the standard response format
    if (data.error?.longErrorCode == 0 && data.responseData?.response != null) {
      final response = data.responseData!.response as RDNAGetNotificationsResponse;
      final notificationList = response.notifications ?? [];

      print('GetNotificationsScreen - Received notifications: ${notificationList.length}');

      setState(() {
        _notifications = notificationList;
      });
    } else {
      // Error or empty response
      print('GetNotificationsScreen - Error or empty response: ${data.error?.errorString}');
      setState(() {
        _notifications = [];
        _error = data.error?.errorString ?? 'Failed to load notifications';
      });
    }
  }

  /**
   * Handle update notification response from onUpdateNotification event
   */
  void _handleUpdateNotificationResponse(RDNAResponseMetaData data) {
    print('GetNotificationsScreen - Received update notification event');

    setState(() {
      _isProcessingAction = false;
    });

    // Check for errors first
    if (data.error?.longErrorCode != 0) {
      final errorMessage = data.error?.errorString ?? 'Failed to update notification';
      print('GetNotificationsScreen - Update notification error: ${data.error}');

      _showAlert(
        'Update Failed',
        errorMessage,
      );
      return;
    }

    // Check response status
    if (data.responseData?.response != null) {
      final response = data.responseData!.response as RDNAUpdateNotificationResponse;

      if (response.statusCode == 100) {
        print('GetNotificationsScreen - Update notification success');
        setState(() {
          _showActionModal = false;
        });
        _loadNotifications();
      } else {
        final statusMessage = response.message ?? 'Unknown error occurred';
        print('GetNotificationsScreen - Update notification status error: $statusMessage');

        _showAlert(
          'Update Failed',
          statusMessage,
          onDismiss: () {
            setState(() {
              _showActionModal = false;
            });
            _loadNotifications();
          },
        );
      }
    }
  }

  /**
   * Load notifications from server
   */
  Future<void> _loadNotifications() async {
    try {
      setState(() {
        _error = null;
      });

      print('GetNotificationsScreen - Loading notifications for user: ${widget.userID}');

      final rdnaService = ref.read(rdnaServiceProvider);
      await rdnaService.getNotifications();

      print('GetNotificationsScreen - GetNotifications API called, waiting for response');

    } catch (error) {
      print('GetNotificationsScreen - Error loading notifications: $error');

      setState(() {
        _isLoading = false;
        _isRefreshing = false;
        _error = 'Failed to load notifications. Please try again.';
      });
    }
  }

  /**
   * Handle pull-to-refresh
   */
  Future<void> _handleRefresh() async {
    setState(() {
      _isRefreshing = true;
    });
    await _loadNotifications();
  }

  /**
   * Open action modal for notification
   */
  void _openActionModal(RDNANotification notification) {
    if (notification.actions == null || notification.actions!.isEmpty) {
      _showAlert('No Actions', 'This notification has no available actions.');
      return;
    }

    if (notification.actionPerformed != null && notification.actionPerformed!.isNotEmpty) {
      _showAlert('Already Processed', 'This notification has already been processed.');
      return;
    }

    setState(() {
      _selectedNotification = notification;
      _selectedAction = null;
      _showActionModal = true;
    });
  }

  /**
   * Execute the notification action
   */
  Future<void> _executeNotificationAction() async {
    if (_selectedNotification == null || _selectedAction == null) return;

    setState(() {
      _isProcessingAction = true;
    });

    try {
      print('GetNotificationsScreen - Processing notification action: ${_selectedNotification!.notificationUuid}');

      final rdnaService = ref.read(rdnaServiceProvider);
      await rdnaService.updateNotification(
        _selectedNotification!.notificationUuid!,
        _selectedAction!,
      );

      print('GetNotificationsScreen - UpdateNotification API called, waiting for response');

    } catch (error) {
      print('GetNotificationsScreen - Error processing action: $error');

      setState(() {
        _isProcessingAction = false;
      });

      _showAlert(
        'Error',
        'Failed to process notification action. Please try again.',
      );
    }
  }

  /**
   * Show alert dialog
   */
  void _showAlert(String title, String message, {VoidCallback? onDismiss}) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(title),
        content: Text(message),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
              if (onDismiss != null) {
                onDismiss();
              }
            },
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF5F5F5),
      body: Column(
        children: [
          // Header
          Container(
            color: Colors.white,
            padding: const EdgeInsets.only(
              left: 20,
              right: 20,
              top: 60,
              bottom: 20,
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text(
                  'Notifications',
                  style: TextStyle(
                    fontSize: 24,
                    fontWeight: FontWeight.bold,
                    color: Color(0xFF333333),
                  ),
                ),
                const SizedBox(height: 8),
                const Text(
                  'Manage your REL-ID notifications',
                  style: TextStyle(
                    fontSize: 16,
                    color: Color(0xFF666666),
                  ),
                ),
                const SizedBox(height: 8),
                Text(
                  'User: ${widget.userID}',
                  style: const TextStyle(
                    fontSize: 14,
                    color: Color(0xFF007AFF),
                    fontWeight: FontWeight.w500,
                  ),
                ),
              ],
            ),
          ),

          // Content
          Expanded(
            child: _isLoading && !_isRefreshing
                ? const Center(
                    child: StatusBanner(
                      type: StatusBannerType.info,
                      message: 'Loading notifications...',
                    ),
                  )
                : RefreshIndicator(
                    onRefresh: _handleRefresh,
                    child: _notifications.isEmpty
                        ? _buildEmptyState()
                        : ListView.builder(
                            padding: const EdgeInsets.all(20),
                            itemCount: _notifications.length,
                            itemBuilder: (context, index) {
                              return _buildNotificationItem(_notifications[index]);
                            },
                          ),
                  ),
          ),

          // Action Modal
          if (_showActionModal) _buildActionModal(),
        ],
      ),
    );
  }

  Widget _buildEmptyState() {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Text(
            'No Notifications',
            style: TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.w600,
              color: Color(0xFF333333),
            ),
          ),
          const SizedBox(height: 8),
          const Text(
            'You don\'t have any notifications at the moment.',
            style: TextStyle(
              fontSize: 14,
              color: Color(0xFF666666),
            ),
            textAlign: TextAlign.center,
          ),
          const SizedBox(height: 20),
          CustomButton(
            title: 'Refresh',
            onPressed: _loadNotifications,
            variant: ButtonVariant.outline,
          ),
        ],
      ),
    );
  }

  Widget _buildNotificationItem(RDNANotification notification) {
    final primaryBody = notification.body?.first;
    final subject = primaryBody?.subject ?? 'No Subject';
    final message = primaryBody?.message ?? 'No Message';
    final actionCount = notification.actions?.length ?? 0;
    final actionStatus = notification.actionPerformed ?? 'Pending';

    return GestureDetector(
      onTap: () => _openActionModal(notification),
      child: Container(
        margin: const EdgeInsets.only(bottom: 12),
        padding: const EdgeInsets.all(16),
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(12),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withOpacity(0.05),
              blurRadius: 2,
              offset: const Offset(0, 1),
            ),
          ],
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Expanded(
                  child: Text(
                    subject,
                    style: const TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.w600,
                      color: Color(0xFF333333),
                    ),
                  ),
                ),
                Text(
                  _formatTimestamp(notification.createTs ?? ''),
                  style: const TextStyle(
                    fontSize: 12,
                    color: Color(0xFF8E8E93),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 8),
            Text(
              message,
              style: const TextStyle(
                fontSize: 14,
                color: Color(0xFF666666),
                height: 1.4,
              ),
              maxLines: 3,
              overflow: TextOverflow.ellipsis,
            ),
            const SizedBox(height: 12),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  '$actionCount action${actionCount != 1 ? 's' : ''} available',
                  style: const TextStyle(
                    fontSize: 12,
                    color: Color(0xFF8E8E93),
                  ),
                ),
                Text(
                  actionStatus,
                  style: const TextStyle(
                    fontSize: 12,
                    color: Color(0xFF8E8E93),
                  ),
                ),
              ],
            ),
            if (notification.expiryTimestamp != null)
              Padding(
                padding: const EdgeInsets.only(top: 8),
                child: Text(
                  'Expires: ${_formatTimestamp(notification.expiryTimestamp!)}',
                  style: const TextStyle(
                    fontSize: 12,
                    color: Color(0xFF8E8E93),
                  ),
                ),
              ),
          ],
        ),
      ),
    );
  }

  Widget _buildActionModal() {
    return Container(
      color: Colors.black.withOpacity(0.5),
      child: Center(
        child: Container(
          margin: const EdgeInsets.all(20),
          padding: const EdgeInsets.all(20),
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(12),
          ),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  const Text(
                    'Notification Actions',
                    style: TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.w600,
                      color: Color(0xFF333333),
                    ),
                  ),
                  if (!_isProcessingAction)
                    GestureDetector(
                      onTap: () {
                        setState(() {
                          _showActionModal = false;
                        });
                      },
                      child: Container(
                        width: 30,
                        height: 30,
                        decoration: BoxDecoration(
                          color: const Color(0xFFF0F0F0),
                          borderRadius: BorderRadius.circular(15),
                        ),
                        child: const Center(
                          child: Text(
                            '×',
                            style: TextStyle(
                              fontSize: 20,
                              color: Color(0xFF666666),
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                        ),
                      ),
                    ),
                ],
              ),
              const SizedBox(height: 20),
              if (_selectedNotification != null) ...[
                Text(
                  _selectedNotification!.body?.first?.subject ?? 'No Subject',
                  style: const TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.w600,
                    color: Color(0xFF333333),
                  ),
                ),
                const SizedBox(height: 8),
                Text(
                  _selectedNotification!.body?.first?.message ?? 'No Message',
                  style: const TextStyle(
                    fontSize: 14,
                    color: Color(0xFF666666),
                    height: 1.4,
                  ),
                ),
                const SizedBox(height: 20),
                const Text(
                  'Select an action:',
                  style: TextStyle(
                    fontSize: 14,
                    fontWeight: FontWeight.w600,
                    color: Color(0xFF333333),
                  ),
                ),
                const SizedBox(height: 12),
                ...(_selectedNotification!.actions ?? []).map((action) {
                  final isSelected = _selectedAction == action.action;
                  return GestureDetector(
                    onTap: _isProcessingAction
                        ? null
                        : () {
                            setState(() {
                              _selectedAction = action.action;
                            });
                          },
                    child: Container(
                      margin: const EdgeInsets.only(bottom: 8),
                      padding: const EdgeInsets.all(12),
                      decoration: BoxDecoration(
                        color: isSelected ? const Color(0xFFF0F8FF) : null,
                        borderRadius: BorderRadius.circular(8),
                        border: Border.all(
                          color: isSelected ? const Color(0xFF007AFF) : const Color(0xFFE0E0E0),
                        ),
                      ),
                      child: Row(
                        children: [
                          Container(
                            width: 20,
                            height: 20,
                            decoration: BoxDecoration(
                              shape: BoxShape.circle,
                              border: Border.all(
                                color: isSelected ? const Color(0xFF007AFF) : const Color(0xFFCCCCCC),
                                width: 2,
                              ),
                            ),
                            child: isSelected
                                ? Center(
                                    child: Container(
                                      width: 10,
                                      height: 10,
                                      decoration: const BoxDecoration(
                                        shape: BoxShape.circle,
                                        color: Color(0xFF007AFF),
                                      ),
                                    ),
                                  )
                                : null,
                          ),
                          const SizedBox(width: 12),
                          Expanded(
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text(
                                  action.label ?? 'Action',
                                  style: const TextStyle(
                                    fontSize: 14,
                                    fontWeight: FontWeight.w500,
                                    color: Color(0xFF333333),
                                  ),
                                ),
                                if (action.authlevel != null)
                                  Text(
                                    action.authlevel!,
                                    style: const TextStyle(
                                      fontSize: 12,
                                      color: Color(0xFF666666),
                                    ),
                                  ),
                              ],
                            ),
                          ),
                        ],
                      ),
                    ),
                  );
                }).toList(),
                const SizedBox(height: 20),
                CustomButton(
                  title: _isProcessingAction ? 'Processing...' : 'Submit Action',
                  onPressed: _selectedAction == null || _isProcessingAction
                      ? null
                      : _executeNotificationAction,
                  variant: ButtonVariant.primary,
                ),
                if (!_isProcessingAction)
                  Padding(
                    padding: const EdgeInsets.only(top: 10),
                    child: CustomButton(
                      title: 'Cancel',
                      onPressed: () {
                        setState(() {
                          _showActionModal = false;
                        });
                      },
                      variant: ButtonVariant.outline,
                    ),
                  ),
              ],
            ],
          ),
        ),
      ),
    );
  }

  String _formatTimestamp(String timestamp) {
    try {
      final date = DateTime.parse(timestamp.replaceAll('UTC', 'Z'));
      return '${date.day}/${date.month}/${date.year} ${date.hour}:${date.minute.toString().padLeft(2, '0')}';
    } catch (e) {
      return timestamp;
    }
  }
}

Key GetNotificationsScreen Features

Automatic Notification Loading

Interactive Action Modals

Enhanced User Experience

Production Features

The following images showcase screens from the sample application:

Get Notifications Dashboard Menu

Get Notifictions Screen

Get Notifiction Actions Screen

Extend your existing SDKEventProvider to handle device activation events and coordinate navigation for the additional device activation workflow.

Adding Device Activation Event Handlers

Enhance your SDKEventProvider with device activation event handling:

// lib/uniken/providers/sdk_event_provider.dart (device activation additions)

class SDKEventProviderWidget extends ConsumerStatefulWidget {
  final Widget child;

  const SDKEventProviderWidget({Key? key, required this.child}) : super(key: key);

  @override
  ConsumerState<SDKEventProviderWidget> createState() => _SDKEventProviderWidgetState();
}

class _SDKEventProviderWidgetState extends ConsumerState<SDKEventProviderWidget> {
  @override
  void initState() {
    super.initState();
    _registerEventHandlers();
  }

  void _registerEventHandlers() {
    final eventManager = ref.read(rdnaServiceProvider).getEventManager();

    // Existing MFA event handlers
    eventManager.setGetUserHandler(_handleGetUser);
    eventManager.setGetPasswordHandler(_handleGetPassword);
    eventManager.setGetActivationCodeHandler(_handleGetActivationCode);
    eventManager.setGetUserConsentForLDAHandler(_handleGetUserConsentForLDA);
    eventManager.setOnUserLoggedInHandler(_handleUserLoggedIn);
    eventManager.setCredentialsAvailableForUpdateHandler(_handleCredentialsAvailableForUpdate);
    eventManager.setOnUserLoggedOffHandler(_handleUserLoggedOff);

    // Device activation event handlers
    eventManager.setAddNewDeviceOptionsHandler(_handleAddNewDeviceOptions);
    eventManager.setGetNotificationsHandler(_handleGetNotifications);
    eventManager.setUpdateNotificationHandler(_handleUpdateNotification);
  }

  /**
   * Event handler for device activation options
   * Triggered when SDK detects unregistered device during authentication
   */
  void _handleAddNewDeviceOptions(RDNAAddNewDeviceOptions data) {
    print('SDKEventProvider - Add new device options event received for user: ${data.userId}');
    print('SDKEventProvider - Available options: ${data.newDeviceOptions}');
    print('SDKEventProvider - Challenge info count: ${data.challengeInfo?.length ?? 0}');

    // Navigate to VerifyAuthScreen with activation options
    appRouter.goNamed(
      'verifyAuthScreen',
      extra: {
        'eventName': 'addNewDeviceOptions',
        'eventData': data,
        'title': 'Additional Device Activation',
        'subtitle': 'Activate this device for user: ${data.userId}',
        'responseData': data,
      },
    );
  }

  /**
   * Event handler for get notifications response
   * Triggered after getNotifications API call completes
   */
  void _handleGetNotifications(RDNAResponseMetaData data) {
    print('SDKEventProvider - Get notifications event received');

    // The GetNotificationsScreen handles this event directly through its own event subscription
    // No navigation needed here - this is handled by the screen itself
    print('SDKEventProvider - Get notifications event handled by GetNotificationsScreen');
  }

  /**
   * Event handler for update notification response
   * Triggered after updateNotification API call completes
   */
  void _handleUpdateNotification(RDNAResponseMetaData data) {
    print('SDKEventProvider - Update notification event received');

    // The GetNotificationsScreen handles this event directly through its own event subscription
    // No navigation needed here - this is handled by the screen itself
    print('SDKEventProvider - Update notification event handled by GetNotificationsScreen');
  }

  /**
   * Enhanced handleUserLoggedIn for device activation support
   * Updated to handle drawer navigation with GetNotifications
   */
  void _handleUserLoggedIn(RDNAUserLoggedIn data) {
    print('SDKEventProvider - User logged in event received for user: ${data.challengeResponse?.userId}');
    print('SDKEventProvider - Session ID: ${data.challengeResponse?.session?.sessionID}');

    // Extract session and JWT information
    final sessionID = data.challengeResponse?.session?.sessionID ?? '';
    final sessionType = data.challengeResponse?.session?.sessionType ?? 0;
    final additionalInfo = data.challengeResponse?.additionalInfo;
    final jwtToken = additionalInfo?.jwtJsonTokenInfo ?? '';
    final userRole = additionalInfo?.idvUserRole;
    final currentWorkFlow = additionalInfo?.currentWorkFlow;

    // Navigate to DrawerNavigator with all session data
    // This now includes access to GetNotifications screen
    appRouter.goNamed(
      'dashboard',
      extra: {
        'userID': data.challengeResponse?.userId ?? '',
        'sessionID': sessionID,
        'sessionType': sessionType,
        'jwtToken': jwtToken,
        'loginTime': DateTime.now().toString(),
        'userRole': userRole,
        'currentWorkFlow': currentWorkFlow,
      },
    );
  }

  @override
  void dispose() {
    final eventManager = ref.read(rdnaServiceProvider).getEventManager();
    eventManager.cleanup();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}

Navigation Router Updates

Update your navigation types to support the new device activation screens:

// lib/tutorial/navigation/app_router.dart (route additions)

final appRouter = GoRouter(
  initialLocation: '/',
  routes: [
    // ... existing routes ...

    // Device Activation Screens
    GoRoute(
      path: '/verify-auth',
      name: 'verifyAuthScreen',
      builder: (context, state) {
        final extra = state.extra as Map<String, dynamic>?;
        return VerifyAuthScreen(
          eventData: extra?['eventData'] as RDNAAddNewDeviceOptions?,
          title: extra?['title'] as String?,
          subtitle: extra?['subtitle'] as String?,
          responseData: extra?['responseData'] as RDNAAddNewDeviceOptions?,
        );
      },
    ),

    // Drawer Navigator (enhanced with GetNotifications)
    GoRoute(
      path: '/dashboard',
      name: 'dashboard',
      builder: (context, state) {
        final extra = state.extra as Map<String, dynamic>?;
        return DashboardScreen(
          userID: extra?['userID'] as String? ?? '',
          sessionID: extra?['sessionID'] as String? ?? '',
          sessionType: extra?['sessionType'] as int? ?? 0,
          jwtToken: extra?['jwtToken'] as String? ?? '',
          loginTime: extra?['loginTime'] as String?,
          userRole: extra?['userRole'] as String?,
          currentWorkFlow: extra?['currentWorkFlow'] as String?,
        );
      },
    ),
  ],
);

Key SDKEventProvider Enhancements

Device Activation Event Integration

Notification Event Handling

Enhanced MFA Integration

Event Flow Coordination

The enhanced SDKEventProvider coordinates these device activation flows:

  1. MFA Authentication Flow: User completes username/password → MFA validation
  2. Device Detection: SDK detects unregistered device → triggers addNewDeviceOptions
  3. Automatic Navigation: SDKEventProvider navigates to VerifyAuthScreen with options
  4. Device Activation: User completes REL-ID Verify or fallback activation
  5. MFA Continuation: Flow continues to LDA consent or final authentication
  6. Dashboard Access: User reaches dashboard with drawer navigation including GetNotifications

Event Handler Coordination

The provider uses a layered event handling approach:

Test your device activation implementation to ensure REL-ID Verify workflows, fallback methods, and notification management work correctly across different scenarios.

Device Activation Test Scenarios

Scenario 1: Automatic REL-ID Verify Activation

Test the complete automatic device activation flow:

  1. Prepare Test Environment:
    # Ensure you have multiple physical devices
    # Device A: Already registered with REL-ID
    # Device B: New device for activation testing
    
    # Build and deploy to both devices
    flutter run -d <device-a-id>
    flutter run -d <device-b-id>
    
  2. Execute Test Flow:
    • On Device B (New Device): Complete MFA username
    • Verify Event Trigger: Check console for addNewDeviceOptions event
    • Automatic Navigation: Confirm VerifyAuthScreen loads automatically
    • REL-ID Verify Start: Verify performVerifyAuth(true) called automatically
    • Push Notification: Check Device A receives activation approval request
    • User Approval: On Device A, approve the device activation request
    • Activation Success: Verify Device B completes activation and continues MFA
  3. Expected Console Output:
    SDKEventProvider - Add new device options event received for user: testuser@example.com
    SDKEventProvider - Available options: [verify-auth, fallback]
    VerifyAuthScreen - Auto-starting REL-ID Verify for user: testuser@example.com
    VerifyAuthScreen - PerformVerifyAuth sync response successful
    

Scenario 2: Fallback Activation Method

Test the fallback activation when REL-ID Verify is not accessible:

  1. Test Setup:
    • Use Device B (new device) without accessible registered devices
    • Or simulate scenario where Device A is offline/unreachable
  2. Execute Fallback Test:
    • Start REL-ID Verify: Allow automatic verification to start
    • Use Fallback: Tap "Activate using fallback method" button
    • Fallback Processing: Verify fallbackNewDeviceActivationFlow() called
    • Alternative Method: Complete server-configured alternative activation
  3. Expected Behavior:
    VerifyAuthScreen - Starting fallback activation for user: testuser@example.com
    VerifyAuthScreen - FallbackNewDeviceActivationFlow sync response successful
    

Scenario 3: Notification Management Testing

Test the GetNotificationsScreen functionality:

  1. Access Notifications:
    • Complete device activation and reach dashboard
    • Open drawer navigation menu
    • Tap "Get Notifications" menu item
    • Verify automatic navigation to GetNotificationsScreen
  2. Test Notification Loading:
    • Auto-load: Verify notifications load automatically on screen entry
    • Loading State: Check loading indicator displays during API call
    • Data Display: Confirm notifications appear in chronological order
  3. Test Notification Actions:
    • Select Notification: Tap on notification with available actions
    • Action Modal: Verify modal opens with radio button options
    • Action Selection: Select an action and submit
    • Processing State: Check processing indicator during update
    • Success Feedback: Verify success message and notification status update
  4. Test Pull-to-Refresh:
    • Pull down on notification list
    • Verify refresh indicator appears
    • Confirm getNotifications() API called again

Debug and Troubleshooting

Common Device Activation Issues

  1. addNewDeviceOptions Event Not Triggered:
    // Check if device is already registered
    // Verify MFA flow completion before device detection
    // Ensure proper connection profile configuration
    
  2. REL-ID Verify Push Notifications Not Received:
    • Verify registered device has push notifications enabled
    • Check network connectivity on both devices
    • Confirm REL-ID Verify service configuration
  3. Fallback Activation Fails:
    • Check server configuration for fallback methods
    • Verify network connectivity and SDK configuration
    • Review error logs for specific failure reasons
  4. Notification Loading Issues:
    // Check GetNotificationsScreen event handler setup
    eventManager.setGetNotificationsHandler(_handleGetNotificationsResponse);
    
    // Verify API call execution
    await rdnaService.getNotifications();
    

Testing Best Practices

  1. Use Physical Devices: REL-ID Verify requires real device-to-device communication
  2. Test Network Conditions: Test with different network conditions and connectivity
  3. Error Scenarios: Test error conditions like network failures and server timeouts
  4. User Experience: Test complete user flows from start to finish
  5. Performance: Monitor performance impact on existing MFA flows

Validation Checklist

Production Deployment Considerations

Security Validation

User Experience

Congratulations! You've successfully implemented a comprehensive Additional Device Activation system with REL-ID Verify push notifications, fallback methods, and notification management.

What You've Accomplished

Core Device Activation Features

REL-ID Verify Integration: Automatic push notification-based device activation ✅ VerifyAuthScreen Implementation: Auto-starting activation with real-time status updates ✅ Fallback Activation Methods: Alternative activation when registered devices aren't accessible ✅ GetNotificationsScreen: Server notification management with interactive action processing ✅ Enhanced Navigation: Seamless access to notifications via go_router integration

Key Architectural Patterns Mastered

  1. Event-Driven Device Activation: Seamless integration with existing MFA workflows
  2. Push Notification Workflows: Real device-to-device communication and approval systems
  3. Fallback Strategy Implementation: Robust alternative activation methods for various scenarios
  4. Interactive Notification Management: Server notification retrieval with action processing
  5. Flutter Navigation Patterns: go_router with named routes and typed data passing

Advanced Device Activation Scenarios

Your implementation now handles these production scenarios:

Seamless MFA Integration

Multi-Device Management

Network Resilience

Next Steps and Advanced Features

Potential Enhancements

  1. Advanced Notification Features: Push notification content customization
  2. Biometric Integration: Enhanced biometric authentication during device activation
  3. Admin Dashboard: Administrative interface for managing device activations
  4. Advanced Analytics: Machine learning-based fraud detection during activation
  5. Multi-Tenant Support: Enterprise-grade multi-organization support

Resources for Continued Learning

REL-ID Documentation

Congratulations! 🎉

You've mastered Advanced Device Activation with REL-ID Verify and built a production-ready system that provides:

Your application now provides enterprise-grade device activation capabilities that enhance security while maintaining user convenience. You're ready to deploy this solution in production environments and scale to support thousands of users across multiple devices.

🚀 You're now equipped to build sophisticated device activation workflows that combine security, usability, and reliability!