🎯 Learning Path:

  1. Complete REL-ID Complete Activation & Login Flow Codelab
  2. Complete REL-ID Additional Device Activation Flow Codelab
  3. You are here → Device Management Implementation (Post-Login)

Welcome to the REL-ID Device Management codelab! This tutorial builds upon your existing MFA implementation to add comprehensive device management capabilities using REL-ID SDK's device management APIs.

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. Device Listing API Integration: Fetching registered devices with cooling period information
  2. Device Update Operations: Implementing rename and delete with proper JSON payloads
  3. Cooling Period Management: Detecting and handling server-enforced cooling periods
  4. Current Device Protection: Validating and preventing current device deletion
  5. Event-Driven Architecture: Handling onGetRegistredDeviceDetails and onUpdateDeviceDetails events
  6. Three-Layer Error Handling: Comprehensive error detection and user feedback
  7. Real-time Synchronization: Auto-refresh with pull-to-refresh and navigation-based updates
  8. GoRouter Navigation Integration: Post-login device management access

Prerequisites

Before starting this codelab, ensure you have:

Get the Code from GitHub

The code to get started can be found in a GitHub repository.

You can clone the repository using the following command:

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

Navigate to the relid-device-management folder in the repository you cloned earlier

Codelab Architecture Overview

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

  1. DeviceManagementScreen: Device list with pull-to-refresh and cooling period detection in drawer navigation
  2. DeviceDetailScreen: Device details with rename and delete operations
  3. RenameDeviceDialog: Modal dialog for device renaming with validation

Before implementing device management screens, let's understand the key SDK events and APIs that power the device lifecycle management workflow.

Device Management Event Flow

The device management process follows this event-driven pattern:

User Logs In → Navigate to Device Management →
getRegisteredDeviceDetails() Called → onGetRegistredDeviceDetails Event →
Device List Displayed with Cooling Period Check →
User Taps Device → Navigate to Detail Screen →
User Renames/Deletes → updateDeviceDetails() Called →
onUpdateDeviceDetails Event → Success/Error Feedback →
Navigate Back → Device List Auto-Refreshes

Core Device Management APIs and Events

The REL-ID SDK provides these APIs and events for device management:

API/Event

Type

Description

User Action Required

getRegisteredDeviceDetails(userID)

API

Fetch all registered devices with cooling period info

System calls automatically

onGetRegistredDeviceDetails

Event

Receives device list with metadata

System processes response

updateDeviceDetails(userID, payload)

API

Rename or delete device with JSON payload

User taps action button

onUpdateDeviceDetails

Event

Update operation result with status codes

System handles response

Device Operation Types

The updateDeviceDetails() API supports two operation types via the status field in JSON payload:

Operation Type

Status Value

Description

devName Value

Rename Device

"Update"

Update device name

New device name string

Delete Device

"Delete"

Remove device from account

NA or Empty string ""

Device List Response Structure

The onGetRegistredDeviceDetails event returns this data structure:

class RDNAStatusGetRegisteredDeviceDetails {
  RDNAError? error;                      // API-level error (longErrorCode)
  PArgs? pArgs;                          // Response arguments
}

class PArgs {
  Response? response;
}

class Response {
  int? statusCode;                       // 100=success, 146=cooling period
  String? statusMsg;                     // Status message
  ResponseData? responseData;
}

class ResponseData {
  dynamic response;                      // Contains RDNAGetRegisteredDeviceDetailsResponse
}

class RDNAGetRegisteredDeviceDetailsResponse {
  List<RDNADeviceDetails>? device;       // Array of devices
  int? deviceManagementCoolingPeriodEndTimestamp;  // Cooling period end
}

class RDNADeviceDetails {
  String? devUuid;                       // Device unique identifier
  String? devName;                       // Device display name
  String? status;                        // "ACTIVE" or other status
  bool? currentDevice;                   // true if this is the current device
  int? lastAccessedTsEpoch;             // Last access timestamp (milliseconds)
  int? createdTsEpoch;                  // Creation timestamp (milliseconds)
  String? lastAccessedTs;               // Last access timestamp (formatted string)
  String? createdTs;                    // Creation timestamp (formatted string)
  String? appUuid;                      // Application identifier
  int? devBind;                         // Device binding status
}

Cooling Period Management

Cooling periods are server-enforced timeouts between device operations:

Status Code

Meaning

Cooling Period Active

Actions Allowed

StatusCode = 100

Success

No

All actions enabled

StatusCode = 146

Cooling period active

Yes

All actions disabled

Current Device Protection

The currentDevice flag identifies the active device:

currentDevice Value

Delete Button

Rename Button

Reason

true

❌ Disabled/Hidden

✅ Enabled

Cannot delete active device

false

✅ Enabled

✅ Enabled

Can delete non-current devices

Update Device JSON Payload Structure

The updateDeviceDetails() API requires a complete device object in JSON format:

Rename Operation Example:

{
  "device": [{
    "devUUID": "DEVICE_UUID_HERE",
    "devName": "My New Device Name",
    "status": "Update",
    "lastAccessedTs": "2025-10-09T11:39:49UTC",
    "lastAccessedTsEpoch": 1760009989000,
    "createdTs": "2025-10-09T11:38:34UTC",
    "createdTsEpoch": 1760009914000,
    "appUuid": "6b72172f-3e51-4ea9-b217-2f3e51aea9c3",
    "currentDevice": true,
    "devBind": 0
  }]
}

Delete Operation Example:

{
  "device": [{
    "devUUID": "DEVICE_UUID_HERE",
    "devName": "",
    "status": "Delete",
    "lastAccessedTs": "2025-10-09T11:39:49UTC",
    "lastAccessedTsEpoch": 1760009989000,
    "createdTs": "2025-10-09T11:38:34UTC",
    "createdTsEpoch": 1760009914000,
    "appUuid": "6b72172f-3e51-4ea9-b217-2f3e51aea9c3",
    "currentDevice": false,
    "devBind": 0
  }]
}

Three-Layer Error Handling

Device management implements comprehensive error detection:

Layer

Check

Error Source

Example

Layer 1

data.error?.longErrorCode != 0

API-level errors

Network timeout, invalid userID

Layer 2

pArgs.response?.statusCode != 100

Status codes

146 (cooling period), validation errors

Layer 3

try-catch blocks

SDK/Network failures

Connection refused, SDK errors

Event Handler Cleanup Pattern

Device management screens use proper event handler cleanup:

// DeviceManagementScreen - dispose cleanup
@override
void dispose() {
  print('DeviceManagementScreen - Screen disposed, cleaning up event handlers');
  // Reset handler to prevent memory leaks
  _rdnaService.getEventManager().setGetRegisteredDeviceDetailsHandler(null);
  super.dispose();
}

// DeviceDetailScreen - dispose cleanup
@override
void dispose() {
  print('DeviceDetailScreen - Component disposing, cleaning up event handlers');
  // Reset handler to prevent memory leaks
  _rdnaService.getEventManager().setUpdateDeviceDetailsHandler(null);
  super.dispose();
}

Let's implement the device management APIs in your service layer following established REL-ID SDK patterns.

Step 1: Add getRegisteredDeviceDetails API

Add this method to

lib/uniken/services/rdna_service.dart

:

/// Gets registered device details for the current user
///
/// This method fetches all devices registered to the user's account. It follows the sync+async pattern:
/// the method returns a sync response, then triggers an onGetRegistredDeviceDetails event with device data.
///
/// ## Parameters
/// - [userId]: The user ID to fetch device details for
///
/// ## Returns
/// RDNASyncResponse containing sync response (error.longErrorCode: 0 = success)
///
/// ## Events Triggered
/// - `onGetRegistredDeviceDetails`: Event with device list data
///
/// ## Response Validation Logic
/// 1. Check error.longErrorCode: 0 = success, > 0 = error
/// 2. An onGetRegistredDeviceDetails event will be triggered with device list
/// 3. Async events will be handled by event listeners
/// 4. StatusCode 146 indicates cooling period is active for device management actions
///
/// ## Example
/// ```dart
/// final response = await rdnaService.getRegisteredDeviceDetails(userId);
/// if (response.error?.longErrorCode == 0) {
///   print('Device details requested, waiting for onGetRegistredDeviceDetails event');
/// }
/// ```
///
/// ## See Also
/// - [Get Registered Devices Documentation](https://developer.uniken.com/docs/get-registered-devices)
Future<RDNASyncResponse> getRegisteredDeviceDetails(String userId) async {
  print('RdnaService - Fetching registered device details for user: $userId');

  final response = await _rdnaClient.getRegisteredDeviceDetails(userId);

  print('RdnaService - GetRegisteredDeviceDetails sync response received');
  print('RdnaService - Sync response:');
  print('  Long Error Code: ${response.error?.longErrorCode}');
  print('  Short Error Code: ${response.error?.shortErrorCode}');

  return response;
}

Step 2: Add updateDeviceDetails API

Add this method to

lib/uniken/services/rdna_service.dart

:

/// Updates device details (rename or delete)
///
/// This method updates device information including renaming or deleting a device.
/// It follows the sync+async pattern: the method returns a sync response,
/// then triggers an onUpdateDeviceDetails event with operation result.
///
/// ## SDK Payload Format
/// ```json
/// {
///   "device": [{
///     "devUUID": "device-uuid",
///     "devName": "new-device-name",
///     "status": "Update" | "Delete"
///   }]
/// }
/// ```
///
/// ## Parameters
/// - [userId]: The user ID who owns the device
/// - [device]: Complete device object with all fields
/// - [newDevName]: The new device name (for rename) or empty string (for delete)
/// - [operationType]: Operation type: 0 = rename (status="Update"), 1 = delete (status="Delete")
///
/// ## Returns
/// RDNASyncResponse containing sync response (error.longErrorCode: 0 = success)
///
/// ## Events Triggered
/// - `onUpdateDeviceDetails`: Event with operation result
///
/// ## Response Validation Logic
/// 1. Check error.longErrorCode: 0 = success, > 0 = error
/// 2. An onUpdateDeviceDetails event will be triggered with operation result
/// 3. Async events will be handled by event listeners
/// 4. StatusCode 100 indicates successful operation
/// 5. StatusCode 146 indicates cooling period is active (operation not allowed)
///
/// ## Example
/// ```dart
/// // Rename device
/// final response = await rdnaService.updateDeviceDetails(
///   userId,
///   deviceObject,
///   'My New Phone',
///   0 // rename
/// );
/// if (response.error?.longErrorCode == 0) {
///   print('Device rename requested, waiting for onUpdateDeviceDetails event');
/// }
///
/// // Delete device
/// final response = await rdnaService.updateDeviceDetails(
///   userId,
///   deviceObject,
///   '',
///   1 // delete
/// );
/// ```
///
/// ## See Also
/// - [Update Device Details Documentation](https://developer.uniken.com/docs/update-device-details)
Future<RDNASyncResponse> updateDeviceDetails(
  String userId,
  RDNADeviceDetails device,
  String newDevName,
  int operationType
) async {
  final operation = operationType == 0 ? 'rename' : 'delete';
  print('RdnaService - Updating device details ($operation) for user: $userId');
  print('RdnaService - Device UUID: ${device.devUuid}');
  if (operationType == 0) {
    print('RdnaService - New device name: $newDevName');
  }

  // SDK expects JSON string payload with device array format
  // Status field: "Update" for rename, "Delete" for delete
  // IMPORTANT: Must include ALL device fields (matching Cordova implementation)
  final status = operationType == 0 ? 'Update' : 'Delete';

  final payload = jsonEncode({
    'device': [{
      'devUUID': device.devUuid,
      'devName': newDevName,
      'status': status,
      'lastAccessedTs': device.lastAccessedTs,
      'lastAccessedTsEpoch': device.lastAccessedTsEpoch,
      'createdTs': device.createdTs,
      'createdTsEpoch': device.createdTsEpoch,
      'appUuid': device.appUuid,
      'currentDevice': device.currentDevice,
      'devBind': device.devBind
    }]
  });

  print('RdnaService - JSON payload: $payload');

  final response = await _rdnaClient.updateDeviceDetails(userId, payload);

  print('RdnaService - UpdateDeviceDetails sync response received');
  print('RdnaService - Sync response:');
  print('  Long Error Code: ${response.error?.longErrorCode}');
  print('  Short Error Code: ${response.error?.shortErrorCode}');

  return response;
}

Step 3: Verify Service Layer Integration

Ensure these imports exist in

lib/uniken/services/rdna_service.dart

:

import 'dart:convert'; // For jsonEncode
import 'package:rdna_client/rdna_client.dart';
import 'package:rdna_client/rdna_struct.dart';

Verify your service class exports all methods:

class RdnaService {
  // Existing MFA methods...

  // ✅ New device management methods
  Future<RDNASyncResponse> getRegisteredDeviceDetails(String userId) async { /* ... */ }
  Future<RDNASyncResponse> updateDeviceDetails(String userId, RDNADeviceDetails device, String newDevName, int operationType) async { /* ... */ }
}

Now let's enhance your event manager to handle device management events and callbacks.

Step 1: Verify Event Manager Setup

The event manager already includes device management handlers in

lib/uniken/services/rdna_event_manager.dart

:

// Device Management Callbacks (already included in event manager)
typedef RDNAGetRegisteredDeviceDetailsCallback = void Function(RDNAStatusGetRegisteredDeviceDetails);
typedef RDNAUpdateDeviceDetailsCallback = void Function(RDNAStatusUpdateDeviceDetails);

class RdnaEventManager {
  // Device Management Handlers
  RDNAGetRegisteredDeviceDetailsCallback? _getRegisteredDeviceDetailsHandler;
  RDNAUpdateDeviceDetailsCallback? _updateDeviceDetailsHandler;

  // Event listeners are automatically registered in _registerEventListeners()
  void _registerEventListeners() {
    // ... existing listeners ...

    // Device Management Event Listeners
    _listeners.add(
      _rdnaClient.on(RdnaClient.onGetRegistredDeviceDetails, _onGetRegistredDeviceDetails),
    );
    _listeners.add(
      _rdnaClient.on(RdnaClient.onUpdateDeviceDetails, _onUpdateDeviceDetails),
    );
  }

  /// Handles registered device details response events
  void _onGetRegistredDeviceDetails(dynamic deviceDetailsData) {
    print('RdnaEventManager - Get registered device details event received');

    // Cast to RDNAStatusGetRegisteredDeviceDetails and pass to handler
    final statusData = deviceDetailsData as RDNAStatusGetRegisteredDeviceDetails;

    print('RdnaEventManager - Get registered device details data:');
    print('  Error Code: ${statusData.errCode}');
    final deviceResponse = statusData.pArgs?.response?.responseData?.response is RDNAGetRegisteredDeviceDetailsResponse
        ? statusData.pArgs?.response?.responseData?.response as RDNAGetRegisteredDeviceDetailsResponse
        : null;
    print('  Device Count: ${deviceResponse?.device?.length ?? 0}');
    print('  Status Code: ${statusData.pArgs?.response?.statusCode}');
    print('  Status Msg: ${statusData.pArgs?.response?.statusMsg}');

    if (_getRegisteredDeviceDetailsHandler != null) {
      _getRegisteredDeviceDetailsHandler!(statusData);
    }
  }

  /// Handles update device details response events (rename/delete)
  void _onUpdateDeviceDetails(dynamic updateDeviceData) {
    print('RdnaEventManager - Update device details event received');

    // Cast to RDNAStatusUpdateDeviceDetails and pass to handler
    final statusData = updateDeviceData as RDNAStatusUpdateDeviceDetails;

    print('RdnaEventManager - Update device details data:');
    print('  Error Code: ${statusData.errCode}');
    print('  Status Code: ${statusData.pArgs?.response?.statusCode}');
    print('  Status Msg: ${statusData.pArgs?.response?.statusMsg}');

    if (_updateDeviceDetailsHandler != null) {
      _updateDeviceDetailsHandler!(statusData);
    }
  }

  /// Sets the handler for get registered device details events
  void setGetRegisteredDeviceDetailsHandler(RDNAGetRegisteredDeviceDetailsCallback? callback) {
    _getRegisteredDeviceDetailsHandler = callback;
  }

  /// Sets the handler for update device details events
  void setUpdateDeviceDetailsHandler(RDNAUpdateDeviceDetailsCallback? callback) {
    _updateDeviceDetailsHandler = callback;
  }
}

Create the DeviceManagementScreen that displays the device list with pull-to-refresh, cooling period detection, and auto-refresh capabilities.

Step 1: Create DeviceManagementScreen File

The file already exists at:

lib/tutorial/screens/device_management/device_management_screen.dart

Step 2: Understanding DeviceManagementScreen Implementation

The complete implementation is in the reference app:

class DeviceManagementScreen extends ConsumerStatefulWidget {
  final String? userID;
  final RDNAUserLoggedIn? sessionData;

  const DeviceManagementScreen({
    super.key,
    this.userID,
    this.sessionData,
  });

  @override
  ConsumerState<DeviceManagementScreen> createState() => _DeviceManagementScreenState();
}

class _DeviceManagementScreenState extends ConsumerState<DeviceManagementScreen> {
  final RdnaService _rdnaService = RdnaService.getInstance();

  List<RDNADeviceDetails> _devices = [];
  bool _isLoading = true;
  bool _isRefreshing = false;
  int? _coolingPeriodEndTimestamp;
  String _coolingPeriodMessage = '';
  bool _isCoolingPeriodActive = false;

  @override
  void initState() {
    super.initState();
    print('DeviceManagementScreen - Screen initialized');
    // Use post-frame callback to load devices after first frame
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _loadDevices();
    });
  }

  @override
  void dispose() {
    print('DeviceManagementScreen - Screen disposed, cleaning up event handlers');
    // Reset handler to prevent memory leaks
    _rdnaService.getEventManager().setGetRegisteredDeviceDetailsHandler(null);
    super.dispose();
  }

  /// Fetches registered device details from the SDK
  Future<void> _loadDevices() async {
    if (widget.userID == null || widget.userID!.isEmpty) {
      print('DeviceManagementScreen - No userID available');
      if (mounted) {
        _showError('User ID is required to load devices');
        setState(() {
          _isLoading = false;
          _isRefreshing = false;
        });
      }
      return;
    }

    print('DeviceManagementScreen - Loading devices for user: ${widget.userID}');
    if (!_isRefreshing) {
      setState(() => _isLoading = true);
    }

    try {
      // Set up event handler for device details response
      final eventManager = _rdnaService.getEventManager();
      bool handlerCalled = false;

      // Set callback for this screen
      eventManager.setGetRegisteredDeviceDetailsHandler((RDNAStatusGetRegisteredDeviceDetails data) {
        if (handlerCalled) return; // Prevent double handling
        handlerCalled = true;

        print('DeviceManagementScreen - Received device details event');

        // Extract and parse device data
        final parsedResponse = data.pArgs?.response?.responseData?.response;
        final deviceResponse = parsedResponse is RDNAGetRegisteredDeviceDetailsResponse ? parsedResponse : null;
        final deviceList = deviceResponse?.device ?? [];

        print('DeviceManagementScreen - Device count: ${deviceList.length}');
        print('DeviceManagementScreen - Status code: ${data.pArgs?.response?.statusCode}');

        // Check for errors using data.error.longErrorCode (sync error check, no try-catch)
        if (data.error?.longErrorCode != 0) {
          print('DeviceManagementScreen - API error: ${data.error?.longErrorCode}');
          if (mounted) {
            _showError(data.error?.errorString  ?? 'Failed to load devices. Please try again.');
            setState(() {
              _isLoading = false;
              _isRefreshing = false;
            });
          }
          return;
        }

        // Extract additional data
        final coolingPeriodEnd = deviceResponse?.deviceManagementCoolingPeriodEndTimestamp;
        final statusCode = data.pArgs?.response?.statusCode ?? 0;
        final statusMsg = data.pArgs?.response?.statusMsg ?? '';

        if (mounted) {
          setState(() {
            _devices = deviceList;
            _coolingPeriodEndTimestamp = coolingPeriodEnd;
            _coolingPeriodMessage = statusMsg;
            _isCoolingPeriodActive = statusCode == 146;
            _isLoading = false;
            _isRefreshing = false;
          });
        }

        print('DeviceManagementScreen - Devices loaded successfully');
      });

      // Call the API with userID (check sync error, no try-catch)
      final response = await _rdnaService.getRegisteredDeviceDetails(widget.userID!);

      // Check sync response error
      if (response.error?.longErrorCode != 0) {
        print('DeviceManagementScreen - API call failed: ${response.error?.errorString}');
        if (mounted) {
          _showError(response.error?.errorString ?? 'Failed to load devices');
          setState(() {
            _isLoading = false;
            _isRefreshing = false;
          });
        }
      }
    } catch (error) {
      print('DeviceManagementScreen - Unexpected error: $error');
      if (mounted) {
        _showError('Failed to load devices. Please try again.');
        setState(() {
          _isLoading = false;
          _isRefreshing = false;
        });
      }
    }
  }

  /// Handles device item tap
  void _handleDeviceTap(RDNADeviceDetails device) async {
    print('DeviceManagementScreen - Device tapped: ${device.devUuid}');

    // Navigate to DeviceDetailScreen and wait for result
    final result = await context.push('/device-detail', extra: {
      'device': device,
      'userID': widget.userID,
      'isCoolingPeriodActive': _isCoolingPeriodActive,
      'coolingPeriodEndTimestamp': _coolingPeriodEndTimestamp,
      'coolingPeriodMessage': _coolingPeriodMessage,
    });

    // Reload devices when returning from detail screen (in case of rename/delete)
    if (result == true || result == 'refresh') {
      print('DeviceManagementScreen - Reloading devices after detail screen return');
      _loadDevices();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF8F9FA),
      drawer: DrawerContent(
        sessionData: widget.sessionData,
        currentRoute: 'deviceManagementScreen',
      ),
      appBar: AppBar(/* ... */),
      body: Column(
        children: [
          // Cooling Period Banner
          if (_renderCoolingPeriodBanner() != null)
            _renderCoolingPeriodBanner()!,

          // Main Content with Pull-to-Refresh
          Expanded(
            child: _isLoading
                ? const Center(child: CircularProgressIndicator())
                : RefreshIndicator(
                    onRefresh: _onRefresh,
                    child: ListView.builder(
                      itemCount: _devices.length,
                      itemBuilder: (context, index) {
                        return _renderDeviceItem(_devices[index]);
                      },
                    ),
                  ),
          ),
        ],
      ),
    );
  }
}

Key DeviceManagementScreen Features

Auto-Loading and Refresh

Cooling Period Management

Current Device Highlighting

Event Handler Cleanup

The following image showcases the screen from the sample application:

Device Management Screen

Create the DeviceDetailScreen that displays device details and provides rename and delete operations.

Step 1: Create DeviceDetailScreen File

The file already exists at:

lib/tutorial/screens/device_management/device_detail_screen.dart

Step 2: Understanding DeviceDetailScreen Implementation

The complete implementation is in the reference app:

class DeviceDetailScreen extends StatefulWidget {
  final RDNADeviceDetails device;
  final String? userID;
  final bool isCoolingPeriodActive;
  final int? coolingPeriodEndTimestamp;
  final String coolingPeriodMessage;

  const DeviceDetailScreen({
    super.key,
    required this.device,
    required this.userID,
    required this.isCoolingPeriodActive,
    this.coolingPeriodEndTimestamp,
    required this.coolingPeriodMessage,
  });

  @override
  State<DeviceDetailScreen> createState() => _DeviceDetailScreenState();
}

class _DeviceDetailScreenState extends State<DeviceDetailScreen> {
  final RdnaService _rdnaService = RdnaService.getInstance();

  bool _isRenaming = false;
  bool _isDeleting = false;
  late String _currentDeviceName;

  @override
  void initState() {
    super.initState();
    _currentDeviceName = widget.device.devName ?? '';
  }

  @override
  void dispose() {
    print('DeviceDetailScreen - Component disposing, cleaning up event handlers');
    // Reset handler to prevent memory leaks
    _rdnaService.getEventManager().setUpdateDeviceDetailsHandler(null);
    super.dispose();
  }

  /// Unified method to handle device update operations (rename/delete)
  Future<void> _updateDevice({
    required String newName,
    required int operationType, // 0 = rename, 1 = delete
  }) async {
    final isRename = operationType == 0;
    final operation = isRename ? 'rename' : 'delete';

    if (isRename) {
      setState(() => _isRenaming = true);
    } else {
      setState(() => _isDeleting = true);
    }

    try {
      print('DeviceDetailScreen - $operation device: ${widget.device.devUuid}');

      final eventManager = _rdnaService.getEventManager();
      bool handlerCalled = false;

      // Set callback for this operation
      eventManager.setUpdateDeviceDetailsHandler((RDNAStatusUpdateDeviceDetails data) {
        if (handlerCalled) return; // Prevent double handling
        handlerCalled = true;

        print('DeviceDetailScreen - Received update device details event');

        // Check sync error (no try-catch)
        if (data.error?.longErrorCode != 0) {
          print('DeviceDetailScreen - $operation error: ${data.error?.longErrorCode}');
          if (mounted) {
            _showError(data.error?.errorString ?? 'Failed to $operation device. Please try again.');
            setState(() {
              if (isRename) {
                _isRenaming = false;
              } else {
                _isDeleting = false;
              }
            });
          }
          return;
        }

        final statusCode = data.pArgs?.response?.statusCode ?? 0;
        final statusMsg = data.pArgs?.response?.statusMsg ?? '';

        if (statusCode == 100) {
          print('DeviceDetailScreen - $operation successful');
          if (mounted) {
            if (isRename) {
              // Rename success
              setState(() {
                _currentDeviceName = newName;
                _isRenaming = false;
              });
              _showSuccess('Device renamed successfully');

              // Navigate back to device list after success
              Future.delayed(const Duration(milliseconds: 500), () {
                if (mounted) {
                  Navigator.of(context).pop(true); // Return true to trigger refresh
                }
              });
            } else {
              // Delete success
              showDialog(
                context: context,
                builder: (BuildContext context) {
                  return AlertDialog(
                    title: const Text('Success'),
                    content: const Text('Device deleted successfully'),
                    actions: [
                      TextButton(
                        onPressed: () {
                          Navigator.of(context).pop(); // Close dialog
                          Navigator.of(context).pop(true); // Go back to device list with refresh signal
                        },
                        child: const Text('OK'),
                      ),
                    ],
                  );
                },
              );
              setState(() => _isDeleting = false);
            }
          }
        } else if (statusCode == 146) {
          if (mounted) {
            _showError('Device management is currently in cooling period. Please try again later.');
            setState(() {
              if (isRename) {
                _isRenaming = false;
              } else {
                _isDeleting = false;
              }
            });
          }
        } else {
          if (mounted) {
            _showError(statusMsg.isNotEmpty ? statusMsg : 'Failed to $operation device');
            setState(() {
              if (isRename) {
                _isRenaming = false;
              } else {
                _isDeleting = false;
              }
            });
          }
        }
      });

      // Call the API (check sync error, no try-catch)
      final response = await _rdnaService.updateDeviceDetails(
        widget.userID!,
        widget.device,
        newName,
        operationType,
      );

      // Check sync response error
      if (response.error?.longErrorCode != 0) {
        print('DeviceDetailScreen - $operation API call failed: ${response.error?.errorString}');
        if (mounted) {
          _showError(response.error?.errorString ?? 'Failed to $operation device');
          setState(() {
            if (isRename) {
              _isRenaming = false;
            } else {
              _isDeleting = false;
            }
          });
        }
      }
    } catch (error) {
      print('DeviceDetailScreen - $operation unexpected error: $error');
      if (mounted) {
        _showError('Failed to $operation device. Please try again.');
        setState(() {
          if (isRename) {
            _isRenaming = false;
          } else {
            _isDeleting = false;
          }
        });
      }
    }
  }

  /// Handles rename device action
  Future<void> _handleRenameDevice(String newName) async {
    await _updateDevice(newName: newName, operationType: 0);
  }

  /// Handles delete device action
  void _handleDeleteDevice() {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('Delete Device'),
          content: Text('Are you sure you want to delete "$_currentDeviceName"? This action cannot be undone.'),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: const Text('Cancel'),
            ),
            TextButton(
              onPressed: () {
                Navigator.of(context).pop();
                _performDeleteDevice();
              },
              style: TextButton.styleFrom(
                foregroundColor: Colors.red,
              ),
              child: const Text('Delete'),
            ),
          ],
        );
      },
    );
  }

  Future<void> _performDeleteDevice() async {
    await _updateDevice(newName: '', operationType: 1);
  }

  @override
  Widget build(BuildContext context) {
    final isCurrentDevice = widget.device.currentDevice ?? false;

    return Scaffold(
      appBar: AppBar(/* ... */),
      body: SingleChildScrollView(
        child: Column(
          children: [
            // Current Device Banner
            if (isCurrentDevice)
              Container(/* ... Current device banner ... */),

            // Device Details Card
            Container(/* ... Device information ... */),

            // Cooling Period Warning
            if (widget.isCoolingPeriodActive)
              Container(/* ... Cooling period warning ... */),

            // Actions Card with Rename and Delete buttons
            Container(
              child: Column(
                children: [
                  // Rename Button
                  ElevatedButton(
                    onPressed: (widget.isCoolingPeriodActive || _isRenaming || _isDeleting)
                        ? null
                        : () async {
                            final newName = await showDialog<String>(
                              context: context,
                              builder: (BuildContext context) {
                                return RenameDeviceDialog(
                                  currentName: _currentDeviceName,
                                );
                              },
                            );

                            if (newName != null && newName.isNotEmpty && newName != _currentDeviceName) {
                              _handleRenameDevice(newName);
                            }
                          },
                    child: _isRenaming
                        ? const CircularProgressIndicator()
                        : const Text('Rename Device'),
                  ),

                  // Delete Button (only if not current device)
                  if (!isCurrentDevice)
                    ElevatedButton(
                      onPressed: (widget.isCoolingPeriodActive || _isRenaming || _isDeleting)
                          ? null
                          : _handleDeleteDevice,
                      child: _isDeleting
                          ? const CircularProgressIndicator()
                          : const Text('Delete Device'),
                    ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Key DeviceDetailScreen Features

Three-Layer Error Handling

Current Device Protection

Cooling Period Enforcement

Event Handler Cleanup

The following images showcase the screen from the sample application:

Device Management Details Screen

Create the RenameDeviceDialog modal component for device renaming with validation.

Step 1: Create RenameDeviceDialog File

The file already exists at:

lib/tutorial/screens/device_management/rename_device_dialog.dart

Step 2: Understanding RenameDeviceDialog Implementation

The complete implementation is in the reference app:

class RenameDeviceDialog extends StatefulWidget {
  final String currentName;

  const RenameDeviceDialog({
    super.key,
    required this.currentName,
  });

  @override
  State<RenameDeviceDialog> createState() => _RenameDeviceDialogState();
}

class _RenameDeviceDialogState extends State<RenameDeviceDialog> {
  late TextEditingController _controller;
  String _error = '';

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController(text: widget.currentName);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _handleSubmit() {
    final trimmedName = _controller.text.trim();

    if (trimmedName.isEmpty) {
      setState(() => _error = 'Device name cannot be empty');
      return;
    }

    if (trimmedName == widget.currentName) {
      setState(() => _error = 'New name must be different from current name');
      return;
    }

    if (trimmedName.length < 3) {
      setState(() => _error = 'Device name must be at least 3 characters');
      return;
    }

    if (trimmedName.length > 50) {
      setState(() => _error = 'Device name must be less than 50 characters');
      return;
    }

    Navigator.of(context).pop(trimmedName);
  }

  @override
  Widget build(BuildContext context) {
    return Dialog(
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
      ),
      child: Container(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Header
            const Text(
              'Rename Device',
              style: TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
                color: Color(0xFF2C3E50),
              ),
            ),
            const SizedBox(height: 20),

            // Current Name Display
            const Text(
              'Current Name:',
              style: TextStyle(
                fontSize: 14,
                color: Color(0xFF7F8C8D),
                fontWeight: FontWeight.w500,
              ),
            ),
            const SizedBox(height: 6),
            Text(
              widget.currentName,
              style: const TextStyle(
                fontSize: 16,
                color: Color(0xFF2C3E50),
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 16),

            // New Name Input
            const Text(
              'New Name:',
              style: TextStyle(
                fontSize: 14,
                color: Color(0xFF7F8C8D),
                fontWeight: FontWeight.w500,
              ),
            ),
            const SizedBox(height: 6),
            TextField(
              controller: _controller,
              autofocus: true,
              maxLength: 50,
              decoration: InputDecoration(
                hintText: 'Enter new device name',
                errorText: _error.isEmpty ? null : _error,
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(8),
                ),
                counterText: '',
              ),
              onChanged: (value) {
                if (_error.isNotEmpty) {
                  setState(() => _error = '');
                }
              },
              onSubmitted: (_) => _handleSubmit(),
            ),
            const SizedBox(height: 24),

            // Actions
            Row(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                TextButton(
                  onPressed: () => Navigator.of(context).pop(),
                  child: const Text('Cancel'),
                ),
                const SizedBox(width: 12),
                ElevatedButton(
                  onPressed: _handleSubmit,
                  child: const Text('Rename'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Key RenameDeviceDialog Features

Input Validation

User Experience

Loading States

The following images showcase the screen from the sample application:

Device Management Details Screen

Now let's integrate the Device Management screens into your GoRouter for post-login access.

Step 1: Add Device Management to GoRouter

The routes are already configured in

lib/tutorial/navigation/app_router.dart

:

// Device Management Routes
GoRoute(
  path: '/device-management',
  name: 'deviceManagementScreen',
  builder: (context, state) {
    final sessionData = state.extra as RDNAUserLoggedIn?;
    return DeviceManagementScreen(
      userID: sessionData?.userId,
      sessionData: sessionData,
    );
  },
),
GoRoute(
  path: '/device-detail',
  name: 'deviceDetailScreen',
  builder: (context, state) {
    final params = state.extra as Map<String, dynamic>;
    return DeviceDetailScreen(
      device: params['device'] as RDNADeviceDetails,
      userID: params['userID'] as String?,
      isCoolingPeriodActive: params['isCoolingPeriodActive'] as bool,
      coolingPeriodEndTimestamp: params['coolingPeriodEndTimestamp'] as int?,
      coolingPeriodMessage: params['coolingPeriodMessage'] as String,
    );
  },
),

Step 2: Add Device Management Menu to DrawerContent

Modify

lib/tutorial/screens/components/drawer_content.dart

to add menu item:

// Add Device Management menu item to drawer
ListTile(
  leading: const Icon(Icons.devices, color: Color(0xFF2C3E50)),
  title: const Text(
    'Device Management',
    style: TextStyle(
      fontSize: 16,
      fontWeight: FontWeight.w500,
      color: Color(0xFF2C3E50),
    ),
  ),
  selected: currentRoute == 'deviceManagementScreen',
  selectedTileColor: const Color(0xFFE3F2FD),
  onTap: () {
    Navigator.of(context).pop(); // Close drawer
    context.push('/device-management', extra: sessionData);
  },
),

Navigation Patterns

Navigating to Device Management:

// From drawer or other screens
context.push('/device-management', extra: sessionData);

Navigating to Device Detail:

// From device list, passing all required parameters
final result = await context.push('/device-detail', extra: {
  'device': device,
  'userID': userID,
  'isCoolingPeriodActive': isCoolingPeriodActive,
  'coolingPeriodEndTimestamp': coolingPeriodEndTimestamp,
  'coolingPeriodMessage': coolingPeriodMessage,
});

// Handle refresh on return
if (result == true || result == 'refresh') {
  _loadDevices();
}

Returning with Refresh Signal:

// From DeviceDetailScreen after successful operation
Navigator.of(context).pop(true); // Return true to trigger refresh

Let's verify your device management implementation with comprehensive manual testing scenarios.

Test Scenario 1: Successful Device List Display

Steps:

  1. Launch the app and complete MFA login flow successfully
  2. Verify navigation to Dashboard screen
  3. Open drawer menu (☰ button or swipe from left)
  4. Tap "Device Management" menu item
  5. Verify loading indicator displays
  6. Wait for device list to load

Expected Console Logs:

DeviceManagementScreen - Loading devices for user: user@example.com
RdnaService - GetRegisteredDeviceDetails sync response success
DeviceManagementScreen - Received device details event
DeviceManagementScreen - Device count: 3
DeviceManagementScreen - Status code: 100

Expected Result: ✅ Device list displays with:

Test Scenario 2: Pull-to-Refresh

Steps:

  1. Navigate to Device Management screen (following Scenario 1)
  2. Pull down on device list
  3. Release when refresh indicator appears
  4. Verify refresh indicator spins
  5. Wait for device list to reload

Expected Behavior: ✅ Device list refreshes, refresh indicator disappears, updated device data displayed

Test Scenario 3: Successful Device Rename

Steps:

  1. Navigate to Device Management screen
  2. Tap on a device card
  3. Verify navigation to DeviceDetailScreen
  4. Verify device details displayed correctly
  5. Tap "Rename Device" button
  6. Verify rename modal opens with current name pre-filled
  7. Enter new device name (e.g., "My Updated Device")
  8. Tap "Rename" button
  9. Wait for success message

Expected Console Logs:

DeviceDetailScreen - Received update device details event
DeviceDetailScreen - Status code: 100
DeviceDetailScreen - rename successful

Expected Result: ✅ Success snackbar "Device renamed successfully", modal closes, device name updates in UI, navigates back to list

Test Scenario 4: Successful Device Deletion

Steps:

  1. Navigate to Device Management screen
  2. Tap on a NON-CURRENT device card (ensure currentDevice: false)
  3. Verify navigation to DeviceDetailScreen
  4. Scroll to Action Buttons section
  5. Verify "Delete Device" button is visible and enabled
  6. Tap "Delete Device" button
  7. Verify confirmation dialog: "Are you sure you want to delete..."
  8. Tap "Delete" button
  9. Wait for success dialog

Expected Console Logs:

DeviceDetailScreen - Received update device details event
DeviceDetailScreen - Status code: 100
DeviceDetailScreen - delete successful

Expected Result: ✅ Success dialog "Device deleted successfully", navigation back to device list, deleted device no longer appears

Test Scenario 5: Current Device Deletion Prevention

Steps:

  1. Navigate to Device Management screen
  2. Tap on CURRENT device card (has "Current Device" badge)
  3. Verify navigation to DeviceDetailScreen
  4. Verify green banner: "This is your current device"
  5. Scroll to Action Buttons section
  6. Verify "Delete Device" button is NOT visible

Expected Result: ✅ Delete button hidden, info message displayed explaining current device cannot be deleted

Test Scenario 6: Cooling Period Detection and Enforcement

Prerequisites: Perform a device operation (rename or delete) to trigger cooling period

Steps:

  1. Complete Scenario 3 or 4 to trigger cooling period
  2. Navigate back to Device Management screen
  3. Pull to refresh device list
  4. Verify cooling period banner displays with StatusCode 146

Expected Console Logs:

DeviceManagementScreen - Status code: 146
DeviceManagementScreen - Cooling period end: 1760013589000

Expected Result: ✅ Orange/yellow banner displays:

  1. Tap any device card
  2. Navigate to DeviceDetailScreen
  3. Verify cooling period warning banner displays
  4. Verify "Rename Device" button is DISABLED (grayed out)
  5. Verify "Delete Device" button is DISABLED (grayed out)
  6. Attempt to tap disabled button

Expected Result: ✅ Nothing happens, buttons remain disabled during cooling period

Test Scenario 7: Rename Validation

Test Empty Name:

  1. Navigate to DeviceDetailScreen
  2. Open rename dialog
  3. Clear all text from input
  4. Tap "Rename" button

Expected Result: ✅ Error message: "Device name cannot be empty"

Test Same Name:

  1. Open rename dialog
  2. Leave current name unchanged
  3. Tap "Rename" button

Expected Result: ✅ Error message: "New name must be different from current name"

Test Too Short:

  1. Open rename dialog
  2. Enter "AB" (2 characters)
  3. Tap "Rename" button

Expected Result: ✅ Error message: "Device name must be at least 3 characters"

Test Too Long:

  1. Open rename dialog
  2. Enter 51+ characters
  3. Tap "Rename" button

Expected Result: ✅ Error message: "Device name must be less than 50 characters"

Test Scenario 8: Auto-Refresh After Operations

Steps:

  1. Navigate to Device Management screen
  2. Note device list state
  3. Tap device, navigate to detail
  4. Rename device successfully
  5. Navigate back to Device Management screen
  6. Verify device list automatically refreshes
  7. Verify renamed device shows new name

Expected Behavior: ✅ Device list auto-refreshes when returning from detail screen, showing updated device name without manual refresh

Test Scenario 9: Event Handler Cleanup

Steps:

  1. Navigate to Device Management screen
  2. Let devices load completely
  3. Navigate to Dashboard
  4. Check console logs for cleanup message
  5. Navigate back to Device Management
  6. Verify devices load again without issues

Expected Console Logs:

DeviceManagementScreen - Screen disposed, cleaning up event handlers
DeviceManagementScreen - Screen initialized

Expected Result: ✅ Event handlers properly cleaned up and re-registered, no memory leaks or duplicate event handling

Issue 1: Device List Not Loading

Symptoms:

Causes & Solutions:

Cause 1: UserID not passed to DeviceManagementScreen

Solution: Verify GoRouter passes userID via extra parameter
- Check: context.push('/device-management', extra: sessionData)
- Verify widget.userID in DeviceManagementScreen
- Ensure sessionData contains userId field

Cause 2: Event handler not triggered

Solution: Verify event handler registration
- Check: eventManager.setGetRegisteredDeviceDetailsHandler() is called
- Verify handler is set BEFORE API call
- Check console for: "Received device details event"

Cause 3: API call fails silently

Solution: Check sync response error handling
- Verify: rdnaService.getRegisteredDeviceDetails() returns Future
- Check error handling in event callback
- Verify Layer 1 error check: data.error?.longErrorCode != 0

Issue 2: Cooling Period Banner Not Appearing

Symptoms:

Causes & Solutions:

Cause 1: StatusCode check incorrect

Solution: Verify exact status code comparison
- Check: _isCoolingPeriodActive = statusCode == 146
- Verify statusCode is int, not String
- Log statusCode value: print('Status code: $statusCode ${statusCode.runtimeType}')

Cause 2: Banner render logic error

Solution: Check conditional rendering
- Verify: if (_renderCoolingPeriodBanner() != null)
- Ensure _isCoolingPeriodActive state is bool
- Check banner widget is not null

Cause 3: Status code extracted from wrong path

Solution: Verify response structure
- Check: data.pArgs?.response?.statusCode
- Log response structure: print('Response: ${data.pArgs?.response}')
- Verify SDK version matches expected response format

Issue 3: Cannot Delete Non-Current Device

Symptoms:

Causes & Solutions:

Cause 1: currentDevice flag check incorrect

Solution: Verify boolean comparison
- Check: if (device.currentDevice ?? false) { ... }
- Ensure device.currentDevice is bool?
- Log device object: print('Current device: ${device.currentDevice}')

Cause 2: Device object not passed correctly

Solution: Verify navigation parameters
- Check: context.push('/device-detail', extra: {params})
- Verify params map contains device object
- Ensure complete device object passed, not just UUID

Cause 3: Cooling period active

Solution: Check cooling period state
- Verify: isCoolingPeriodActive is false
- Check cooling period end timestamp hasn't expired
- Log cooling period state before operations

Issue 4: Rename Dialog Not Appearing

Symptoms:

Causes & Solutions:

Cause 1: showDialog not awaiting properly

Solution: Verify async/await pattern
- Check: final newName = await showDialog<String>(...)
- Ensure showDialog returns Future<String?>
- Verify RenameDeviceDialog returns value via Navigator.pop()

Cause 2: Dialog widget not rendering

Solution: Verify RenameDeviceDialog component
- Check: showDialog(builder: (context) => RenameDeviceDialog(...))
- Ensure component imported correctly
- Verify Dialog widget structure

Cause 3: Button disabled during cooling period

Solution: Check button disabled logic
- Verify: onPressed: (isCoolingPeriodActive || _isRenaming) ? null : ...
- Ensure cooling period is not active
- Check button enabled state

Issue 5: Event Handler Not Firing

Symptoms:

Causes & Solutions:

Cause 1: Event handler not set before API call

Solution: Verify handler registration timing
- Set handler BEFORE calling API: eventManager.setGetRegisteredDeviceDetailsHandler()
- Verify timing: handler -> API call -> event received
- Check for race conditions

Cause 2: Event name mismatch

Solution: Verify exact event name
- Check: _rdnaClient.on(RdnaClient.onGetRegistredDeviceDetails, ...)
- Note spelling: 'Registred' not 'Registered' (SDK convention)
- Verify event name matches SDK documentation

Cause 3: Handler not cleaned up properly

Solution: Implement proper lifecycle cleanup
- Set handler when needed: eventManager.setHandler(callback)
- Clean up in dispose: eventManager.setHandler(null)
- Verify cleanup runs: Add print in dispose method

Issue 6: Memory Leaks and Duplicate Events

Symptoms:

Causes & Solutions:

Cause 1: Event handlers not cleaned up

Solution: Implement proper cleanup
- Add dispose cleanup: eventManager.setHandler(null)
- Verify cleanup runs on dispose: print('Cleaning up')
- Check handler is null after dispose

Cause 2: Multiple handlers registered

Solution: Remove handlers before setting new ones
- Check existing handler before setting
- Use single handler per screen
- Avoid setting handlers in build methods

Cause 3: Listeners not removed

Solution: Remove native event listeners
- Store listener: final listener = _rdnaClient.on(...)
- Remove on cleanup: _rdnaClient.off(listener)
- Verify listeners array cleared in event manager

Security Considerations

Device Data Handling:

Cooling Period Enforcement:

Error Handling:

User Experience Best Practices

Loading States:

Visual Feedback:

Navigation:

Validation:

Code Organization

File Structure:

lib/
├── uniken/
│   ├── services/
│   │   ├── rdna_service.dart (✅ Add getRegisteredDeviceDetails, updateDeviceDetails)
│   │   └── rdna_event_manager.dart (✅ Add device management event handlers)
│   └── utils/
│       └── connection_profile_parser.dart
└── tutorial/
    ├── navigation/
    │   └── app_router.dart (✅ Add device management routes)
    └── screens/
        ├── device_management/
        │   ├── device_management_screen.dart (✅ NEW)
        │   ├── device_detail_screen.dart (✅ NEW)
        │   └── rename_device_dialog.dart (✅ NEW)
        └── components/
            └── drawer_content.dart (✅ Add device management menu)

Component Responsibilities:

Performance Optimization

Render Optimization:

Memory Management:

Network Optimization:

Testing Checklist

Before deploying to production, verify:

Congratulations! You've successfully implemented comprehensive device management functionality with REL-ID SDK in Flutter!

What You've Accomplished

In this codelab, you learned how to:

Additional Resources