This codelab demonstrates how to implement Session Management flow using the rdna_client Flutter plugin. Session management provides critical security features including automatic session timeout handling, idle session warnings with extension capabilities, and seamless session lifecycle management to prevent unexpected user logouts.

What You'll Learn

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-session-management folder in the repository you cloned earlier

What You'll Need

The sample app provides a complete session management implementation. Let's examine the key components:

Component

Purpose

Sample App Reference

Session Provider

Global session state management

lib/uniken/providers/session_provider.dart

Session Modal

UI with countdown and extension

lib/uniken/components/modals/session_modal.dart

Event Handling

Extended event manager

lib/uniken/services/rdna_event_manager.dart

Session Types

Dart class definitions

rdna_client/lib/rdna_struct.dart

Session Management Event Types

The RELID SDK triggers three main session management events:

Event Type

Description

User Action Required

onSessionTimeout

Hard session timeout - session already expired

User must acknowledge and app navigates to home

onSessionTimeOutNotification

Idle session warning - session will expire soon

User can extend session or let it expire

onSessionExtensionResponse

Response from session extension API call

Handle success/failure of extension attempt

Session Management Flow Architecture

The session management flow follows this pattern:

  1. SDK monitors session activity based on gateway configuration
  2. Idle Warning: onSessionTimeOutNotification triggers with countdown and extension option
  3. Extension Request: User can call extendSessionIdleTimeout() API
  4. Extension Response: onSessionExtensionResponse provides success/failure result
  5. Hard Timeout: onSessionTimeout forces app navigation when session expires

Define Dart classes for comprehensive session timeout handling:

// rdna_client/lib/rdna_struct.dart (excerpts)

/// RDNA Session Response Data
/// Used for both session timeout notifications and extension responses
class SessionResponse {
  final String userID;
  final String message;                    // Session timeout/extension message
  final int timeLeftInSeconds;             // Countdown timer value
  final int sessionCanBeExtended;          // 1 = can extend, 0 = cannot extend
  final Info info;                         // Additional session information

  SessionResponse({
    required this.userID,
    required this.message,
    required this.timeLeftInSeconds,
    required this.sessionCanBeExtended,
    required this.info,
  });

  factory SessionResponse.fromJson(Map<String, dynamic> json) {
    return SessionResponse(
      userID: json['userID'] ?? '',
      message: json['message'] ?? '',
      timeLeftInSeconds: json['timeLeftInSeconds'] ?? 0,
      sessionCanBeExtended: json['sessionCanBeExtended'] ?? 0,
      info: Info.fromJson(json['info'] ?? {}),
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'userID': userID,
      'message': message,
      'timeLeftInSeconds': timeLeftInSeconds,
      'sessionCanBeExtended': sessionCanBeExtended,
      'info': info.toJson(),
    };
  }
}

class Info {
  final int sessionType;
  final String currentWorkFlow;

  Info({
    required this.sessionType,
    required this.currentWorkFlow,
  });

  factory Info.fromJson(Map<String, dynamic> json) {
    return Info(
      sessionType: json['sessionType'] ?? 0,
      currentWorkFlow: json['currentWorkFlow'] ?? '',
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'sessionType': sessionType,
      'currentWorkFlow': currentWorkFlow,
    };
  }
}

// Session Management Callback Types
typedef RDNASessionTimeoutCallback = void Function(String);
typedef RDNASessionTimeoutNotificationCallback = void Function(SessionResponse);
typedef RDNASessionExtensionResponseCallback = void Function(SessionResponse);

Understanding Session Timeout Types

Session management handles two distinct scenarios:

Session Type

Trigger

User Options

Implementation

Hard Timeout

Session already expired

Close button only

Navigate to home screen

Idle Warning

Session expiring soon

Extend or Close

API call or natural expiry

Extend your existing event manager to handle session management events:

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

class RdnaEventManager {
  // Add session management callback properties
  RDNASessionTimeoutCallback? _sessionTimeoutHandler;
  RDNASessionTimeoutNotificationCallback? _sessionTimeoutNotificationHandler;
  RDNASessionExtensionResponseCallback? _sessionExtensionResponseHandler;

  void _registerEventListeners() {
    // ... existing listeners ...

    // Add session management event listeners
    _listeners.add(
      _rdnaClient.on(RdnaClient.onSessionTimeout, _onSessionTimeout),
    );
    _listeners.add(
      _rdnaClient.on(RdnaClient.onSessionTimeOutNotification, _onSessionTimeOutNotification),
    );
    _listeners.add(
      _rdnaClient.on(RdnaClient.onSessionExtensionResponse, _onSessionExtensionResponse),
    );
  }

  /// Handles session timeout events for mandatory sessions
  void _onSessionTimeout(dynamic sessionData) {
    print('RdnaEventManager - Session timeout event received');

    // The session timeout data is a plain string message
    final message = sessionData as String;
    print('RdnaEventManager - Session timeout message: $message');

    if (_sessionTimeoutHandler != null) {
      _sessionTimeoutHandler!(message);
    }
  }

  /// Handles session timeout notification events for idle sessions
  void _onSessionTimeOutNotification(dynamic notificationData) {
    print('RdnaEventManager - Session timeout notification event received');

    // Cast to SessionResponse
    final sessionResponse = notificationData as SessionResponse;
    print('RdnaEventManager - Notification data: ${sessionResponse.userID}, TimeLeft: ${sessionResponse.timeLeftInSeconds}s');

    if (_sessionTimeoutNotificationHandler != null) {
      _sessionTimeoutNotificationHandler!(sessionResponse);
    }
  }

  /// Handles session extension response events
  void _onSessionExtensionResponse(dynamic extensionData) {
    print('RdnaEventManager - Session extension response event received');

    // Cast to SessionResponse
    final sessionResponse = extensionData as SessionResponse;
    print('RdnaEventManager - Extension data: ${sessionResponse.userID}, TimeLeft: ${sessionResponse.timeLeftInSeconds}s');

    if (_sessionExtensionResponseHandler != null) {
      _sessionExtensionResponseHandler!(sessionResponse);
    }
  }

  // Handler setter methods

  /// Sets the handler for session timeout events (hard timeout)
  void setSessionTimeoutHandler(RDNASessionTimeoutCallback? callback) {
    _sessionTimeoutHandler = callback;
  }

  /// Sets the handler for session timeout notification events (idle timeout warning)
  void setSessionTimeoutNotificationHandler(RDNASessionTimeoutNotificationCallback? callback) {
    _sessionTimeoutNotificationHandler = callback;
  }

  /// Sets the handler for session extension response events
  void setSessionExtensionResponseHandler(RDNASessionExtensionResponseCallback? callback) {
    _sessionExtensionResponseHandler = callback;
  }
}

Key features of session event handling:

Add session extension capability to your RELID service:

// lib/uniken/services/rdna_service.dart (addition)

class RdnaService {
  /// Extends the idle session timeout
  ///
  /// Extends the current idle session timeout when the session is eligible for extension.
  /// Should be called in response to onSessionTimeOutNotification events when sessionCanBeExtended = 1.
  /// After calling this method, the SDK will trigger an onSessionExtensionResponse event with the result.
  ///
  /// ## Returns
  /// RDNASyncResponse containing sync response (check error.longErrorCode for immediate feedback)
  ///
  /// ## Events Triggered
  /// - `onSessionExtensionResponse`: Triggered after API call
  ///
  /// ## Response Validation Logic
  /// 1. Check error.longErrorCode: 0 = success, > 0 = error
  /// 2. An onSessionExtensionResponse event will be triggered with detailed response
  /// 3. The extension success/failure will be determined by the async event response
  ///
  /// ## Example
  /// ```dart
  /// final response = await rdnaService.extendSessionIdleTimeout();
  /// if (response.error?.longErrorCode == 0) {
  ///   print('Extension successful');
  /// }
  /// ```
  Future<RDNASyncResponse> extendSessionIdleTimeout() async {
    print('RdnaService - Extending session idle timeout');

    final responseString = await _rdnaClient.extendSessionIdleTimeout();
    print('RdnaService - ExtendSessionIdleTimeout response received: $responseString');

    // Parse JSON response to extract error
    Map<String, dynamic> responseMap;
    if (responseString is String) {
      responseMap = json.decode(responseString) as Map<String, dynamic>;
    } else {
      responseMap = responseString as Map<String, dynamic>;
    }

    // Parse nested error JSON string
    RDNAError? error;
    if (responseMap.containsKey('error') && responseMap['error'] is String) {
      final errorJson = responseMap['error'] as String;
      final errorMap = json.decode(errorJson) as Map<String, dynamic>;
      error = RDNAError.fromJson(errorMap);
    } else {
      error = RDNAError(longErrorCode: 0, shortErrorCode: 0, errorString: 'Success');
    }

    final response = RDNASyncResponse(
      error: error,
      response: responseMap['response'],
    );

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

    return response;
  }
}

Important Session Extension Logic

When handling session extension, two response layers must be considered:

Response Layer

Purpose

Success Criteria

Failure Handling

Sync Response

API call validation

error.longErrorCode == 0

Immediate rejection

Async Event

Extension result

onSessionExtensionResponse event fired

Display error message

Note: The sync response only indicates the API call was accepted. The actual extension success/failure is communicated through the onSessionExtensionResponse event.

Create a Riverpod StateNotifier to manage session state across your application:

// lib/uniken/providers/session_provider.dart

/// Session state managed by SessionProvider
class SessionState {
  final bool isSessionModalVisible;
  final String? sessionTimeoutMessage;              // Hard timeout message
  final SessionResponse? sessionTimeoutNotificationData;  // Idle timeout data
  final bool isProcessing;

  const SessionState({
    this.isSessionModalVisible = false,
    this.sessionTimeoutMessage,
    this.sessionTimeoutNotificationData,
    this.isProcessing = false,
  });

  SessionState copyWith({
    bool? isSessionModalVisible,
    String? sessionTimeoutMessage,
    SessionResponse? sessionTimeoutNotificationData,
    bool? isProcessing,
    bool clearTimeoutMessage = false,
    bool clearNotificationData = false,
  }) {
    return SessionState(
      isSessionModalVisible: isSessionModalVisible ?? this.isSessionModalVisible,
      sessionTimeoutMessage: clearTimeoutMessage ? null : (sessionTimeoutMessage ?? this.sessionTimeoutMessage),
      sessionTimeoutNotificationData: clearNotificationData ? null : (sessionTimeoutNotificationData ?? this.sessionTimeoutNotificationData),
      isProcessing: isProcessing ?? this.isProcessing,
    );
  }
}

/// Session Management Provider
///
/// Manages global session timeout state and provides session management
/// functionality using Riverpod.
class SessionNotifier extends StateNotifier<SessionState> {
  final RdnaService _rdnaService;
  final RdnaEventManager _eventManager;
  String _currentOperation = 'none';  // Operation tracking

  SessionNotifier(this._rdnaService, this._eventManager)
      : super(const SessionState()) {
    _setupEventHandlers();
  }

  /// Sets up session event handlers
  void _setupEventHandlers() {
    print('SessionProvider - Setting up session event handlers');

    // Session timeout event (hard timeout - mandatory)
    _eventManager.setSessionTimeoutHandler((String message) {
      print('SessionProvider - Session timeout received: $message');
      _showSessionTimeout(message);
    });

    // Session timeout notification event (idle timeout warning)
    _eventManager.setSessionTimeoutNotificationHandler((dynamic data) {
      print('SessionProvider - Session timeout notification received');
      final sessionResponse = data as SessionResponse;
      _showSessionTimeoutNotification(sessionResponse);
    });

    // Session extension response event
    _eventManager.setSessionExtensionResponseHandler((dynamic data) {
      print('SessionProvider - Session extension response received');
      final sessionResponse = data as SessionResponse;
      _handleSessionExtensionResponse(sessionResponse);
    });
  }

  /// Shows session timeout modal (hard timeout)
  void _showSessionTimeout(String message) {
    print('SessionProvider - Session timed out, showing modal');

    state = state.copyWith(
      isSessionModalVisible: true,
      sessionTimeoutMessage: message,
      clearNotificationData: true,
      isProcessing: false,
    );
    _currentOperation = 'none';
  }

  /// Shows session timeout notification modal (idle timeout warning)
  void _showSessionTimeoutNotification(SessionResponse data) {
    print('SessionProvider - Showing session timeout notification modal');

    state = state.copyWith(
      isSessionModalVisible: true,
      sessionTimeoutNotificationData: data,
      clearTimeoutMessage: true,
      isProcessing: false,
    );
    _currentOperation = 'none';
  }

  /// Hides session modal
  void hideSessionModal() {
    print('SessionProvider - Hiding session modal');

    state = state.copyWith(
      isSessionModalVisible: false,
      clearTimeoutMessage: true,
      clearNotificationData: true,
      isProcessing: false,
    );
    _currentOperation = 'none';
  }
}

/// Global session provider
final sessionProvider = StateNotifierProvider<SessionNotifier, SessionState>((ref) {
  final rdnaService = RdnaService.getInstance();
  final eventManager = rdnaService.getEventManager();
  return SessionNotifier(rdnaService, eventManager);
});

Session Extension Logic

The provider handles session extension with comprehensive state management:

  /// Handles extend session button press
  Future<void> handleExtendSession(BuildContext context) async {
    print('SessionProvider - User chose to extend session');

    if (_currentOperation != 'none') {
      print('SessionProvider - Operation already in progress, ignoring extend request');
      return;
    }

    state = state.copyWith(isProcessing: true);
    _currentOperation = 'extend';

    // Call extend session API and check sync response
    final response = await _rdnaService.extendSessionIdleTimeout();

    print('SessionProvider - ExtendSessionIdleTimeout sync response received');
    print('  Long Error Code: ${response.error?.longErrorCode}');

    // Check sync response for immediate errors
    if (response.error?.longErrorCode != 0) {
      // Sync error - stop and show error
      print('SessionProvider - Extension sync error: ${response.error?.errorString}');
      state = state.copyWith(isProcessing: false);
      _currentOperation = 'none';

      // Show error alert to user
      if (context.mounted) {
        showDialog(
          context: context,
          builder: (context) => AlertDialog(
            title: const Text('Extension Failed'),
            content: Text(
              'Failed to extend session:\n\n${response.error?.errorString}\n\nError Code: ${response.error?.longErrorCode}',
            ),
            actions: [
              TextButton(
                onPressed: () => Navigator.of(context).pop(),
                child: const Text('OK'),
              ),
            ],
          ),
        );
      }
      return;
    }

    // Sync success - wait for onSessionExtensionResponse event
    print('SessionProvider - Extension sync success, waiting for async event...');
    // Note: Event handler (_handleSessionExtensionResponse) will hide modal
  }

  /// Handles session extension response event from SDK
  void _handleSessionExtensionResponse(SessionResponse data) {
    print('SessionProvider - Extension response event received');

    // Only process if we're currently extending
    if (_currentOperation != 'extend') {
      print('SessionProvider - Extension response received but no extend operation in progress, ignoring');
      return;
    }

    // Event firing indicates success - hide the modal
    print('SessionProvider - Session extension successful (event received)');
    hideSessionModal();
  }

  /// Handles dismiss button press
  void handleDismiss(BuildContext context) {
    print('SessionProvider - User dismissed session modal');

    // Check timeout type BEFORE hiding modal (state will be cleared)
    final hasTimeoutMessage = state.sessionTimeoutMessage != null;

    // Hide modal first
    hideSessionModal();

    // For hard session timeout (mandatory), navigate to home screen
    if (hasTimeoutMessage) {
      print('SessionProvider - Hard session timeout - navigating to home screen');
      appRouter.go('/');
    }
  }

Key features of the session provider:

Create a modal component to display session information and handle user interactions:

// lib/uniken/components/modals/session_modal.dart

class SessionModal extends ConsumerStatefulWidget {
  const SessionModal({Key? key}) : super(key: key);

  @override
  ConsumerState<SessionModal> createState() => _SessionModalState();
}

class _SessionModalState extends ConsumerState<SessionModal> with WidgetsBindingObserver {
  Timer? _countdownTimer;
  int _countdown = 0;
  DateTime? _backgroundTime;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);  // Register lifecycle observer
  }

  @override
  void dispose() {
    _countdownTimer?.cancel();
    WidgetsBinding.instance.removeObserver(this);  // Unregister
    super.dispose();
  }

  void _startCountdown(int timeLeftInSeconds) {
    _countdown = timeLeftInSeconds;
    _countdownTimer?.cancel();

    _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
      if (_countdown > 0) {
        setState(() {
          _countdown--;
        });
      } else {
        timer.cancel();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    final sessionState = ref.watch(sessionProvider);
    final sessionNotifier = ref.read(sessionProvider.notifier);

    // Don't show modal if not visible
    if (!sessionState.isSessionModalVisible) {
      _countdownTimer?.cancel();
      return const SizedBox.shrink();
    }

    // Determine session type
    final isMandatoryTimeout = sessionState.sessionTimeoutMessage != null;
    final isIdleTimeout = sessionState.sessionTimeoutNotificationData != null;

    // Initialize countdown for idle timeout
    if (isIdleTimeout && _countdownTimer == null) {
      final data = sessionState.sessionTimeoutNotificationData;
      if (data != null) {
        _startCountdown(data.timeLeftInSeconds);
      }
    }

    // Get display message
    String displayMessage = 'Session timeout occurred.';
    if (isMandatoryTimeout) {
      displayMessage = sessionState.sessionTimeoutMessage!;
    } else if (isIdleTimeout && sessionState.sessionTimeoutNotificationData != null) {
      displayMessage = sessionState.sessionTimeoutNotificationData!.message;
    }

    // Check if session can be extended
    bool canExtendSession = false;
    if (isIdleTimeout && sessionState.sessionTimeoutNotificationData != null) {
      canExtendSession = sessionState.sessionTimeoutNotificationData!.sessionCanBeExtended == 1;
    }

    // Get modal configuration
    final config = _getModalConfig(isMandatoryTimeout, isIdleTimeout, canExtendSession);

    return PopScope(
      canPop: false,  // Prevent back button dismissal
      child: GestureDetector(
        onTap: () {},  // Absorb taps - prevent background interaction
        child: Material(
          color: Colors.black.withOpacity(0.8),
          child: Center(
            child: Padding(
              padding: const EdgeInsets.all(20),
              child: Container(
                constraints: const BoxConstraints(maxWidth: 400),
                decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.circular(16),
                  boxShadow: [
                    BoxShadow(
                      color: Colors.black.withOpacity(0.3),
                      blurRadius: 8,
                      offset: const Offset(0, 4),
                    ),
                  ],
                ),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    // Header with dynamic color based on timeout type
                    Container(
                      padding: const EdgeInsets.all(20),
                      decoration: BoxDecoration(
                        color: config.headerColor,  // Red or Orange
                        borderRadius: const BorderRadius.only(
                          topLeft: Radius.circular(16),
                          topRight: Radius.circular(16),
                        ),
                      ),
                      child: Column(
                        children: [
                          Text(
                            config.title,
                            style: const TextStyle(
                              fontSize: 20,
                              fontWeight: FontWeight.bold,
                              color: Colors.white,
                            ),
                            textAlign: TextAlign.center,
                          ),
                          const SizedBox(height: 8),
                          Text(
                            config.subtitle,
                            style: TextStyle(
                              fontSize: 14,
                              color: Colors.white.withOpacity(0.8),
                            ),
                            textAlign: TextAlign.center,
                          ),
                        ],
                      ),
                    ),

                    // Content with message and countdown
                    Padding(
                      padding: const EdgeInsets.all(20),
                      child: Column(
                        children: [
                          // Icon and Message
                          Text(
                            config.icon,
                            style: const TextStyle(fontSize: 48),
                          ),
                          const SizedBox(height: 16),
                          Text(
                            displayMessage,
                            style: const TextStyle(
                              fontSize: 16,
                              color: Color(0xFF1f2937),
                              height: 1.5,
                            ),
                            textAlign: TextAlign.center,
                          ),

                          // Countdown display for idle timeout
                          if (isIdleTimeout && _countdown > 0) ...[
                            const SizedBox(height: 20),
                            Container(
                              padding: const EdgeInsets.all(16),
                              decoration: BoxDecoration(
                                color: const Color(0xFFfef3c7),
                                borderRadius: BorderRadius.circular(12),
                                border: Border.all(
                                  color: const Color(0xFFf59e0b),
                                  width: 2,
                                ),
                              ),
                              child: Column(
                                children: [
                                  const Text(
                                    'Time Remaining:',
                                    style: TextStyle(
                                      fontSize: 14,
                                      fontWeight: FontWeight.w600,
                                      color: Color(0xFF92400e),
                                    ),
                                  ),
                                  const SizedBox(height: 8),
                                  Text(
                                    '${(_countdown / 60).floor()}:${(_countdown % 60).toString().padLeft(2, '0')}',
                                    style: const TextStyle(
                                      fontSize: 32,
                                      fontWeight: FontWeight.bold,
                                      color: Color(0xFFd97706),
                                      fontFamily: 'monospace',
                                    ),
                                  ),
                                ],
                              ),
                            ),
                          ],
                        ],
                      ),
                    ),

                    // Action Buttons
                    Container(
                      padding: const EdgeInsets.all(20),
                      decoration: const BoxDecoration(
                        border: Border(
                          top: BorderSide(color: Color(0xFFf3f4f6)),
                        ),
                      ),
                      child: Column(
                        children: [
                          // Hard timeout - only Close option
                          if (isMandatoryTimeout)
                            SizedBox(
                              width: double.infinity,
                              child: ElevatedButton(
                                onPressed: () => sessionNotifier.handleDismiss(context),
                                style: ElevatedButton.styleFrom(
                                  backgroundColor: const Color(0xFF6b7280),
                                  padding: const EdgeInsets.all(16),
                                  shape: RoundedRectangleBorder(
                                    borderRadius: BorderRadius.circular(8),
                                  ),
                                  elevation: 2,
                                ),
                                child: const Text(
                                  'Close',
                                  style: TextStyle(
                                    color: Colors.white,
                                    fontSize: 16,
                                    fontWeight: FontWeight.w600,
                                  ),
                                ),
                              ),
                            ),

                          // Idle timeout - extend or dismiss options
                          if (isIdleTimeout) ...[
                            if (canExtendSession)
                              SizedBox(
                                width: double.infinity,
                                child: ElevatedButton(
                                  onPressed: sessionState.isProcessing
                                      ? null
                                      : () => sessionNotifier.handleExtendSession(context),
                                  style: ElevatedButton.styleFrom(
                                    backgroundColor: const Color(0xFF3b82f6),
                                    padding: const EdgeInsets.all(16),
                                    shape: RoundedRectangleBorder(
                                      borderRadius: BorderRadius.circular(8),
                                    ),
                                    elevation: 2,
                                  ),
                                  child: sessionState.isProcessing
                                      ? Row(
                                          mainAxisAlignment: MainAxisAlignment.center,
                                          children: const [
                                            SizedBox(
                                              width: 20,
                                              height: 20,
                                              child: CircularProgressIndicator(
                                                color: Colors.white,
                                                strokeWidth: 2,
                                              ),
                                            ),
                                            SizedBox(width: 8),
                                            Text(
                                              'Extending...',
                                              style: TextStyle(
                                                color: Colors.white,
                                                fontSize: 16,
                                                fontWeight: FontWeight.w600,
                                              ),
                                            ),
                                          ],
                                        )
                                      : const Text(
                                          'Extend Session',
                                          style: TextStyle(
                                            color: Colors.white,
                                            fontSize: 16,
                                            fontWeight: FontWeight.w600,
                                          ),
                                        ),
                                ),
                              ),
                            const SizedBox(height: 12),
                            SizedBox(
                              width: double.infinity,
                              child: ElevatedButton(
                                onPressed: sessionState.isProcessing
                                    ? null
                                    : () => sessionNotifier.handleDismiss(context),
                                style: ElevatedButton.styleFrom(
                                  backgroundColor: const Color(0xFF6b7280),
                                  padding: const EdgeInsets.all(16),
                                  shape: RoundedRectangleBorder(
                                    borderRadius: BorderRadius.circular(8),
                                  ),
                                  elevation: 2,
                                ),
                                child: const Text(
                                  'Close',
                                  style: TextStyle(
                                    color: Colors.white,
                                    fontSize: 16,
                                    fontWeight: FontWeight.w600,
                                  ),
                                ),
                              ),
                            ),
                          ],
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }

  /// Get modal configuration based on session type
  _ModalConfig _getModalConfig(bool isMandatoryTimeout, bool isIdleTimeout, bool canExtendSession) {
    if (isMandatoryTimeout) {
      return _ModalConfig(
        title: '🔐 Session Expired',
        subtitle: 'Your session has expired. You will be redirected to the home screen.',
        headerColor: const Color(0xFFdc2626),  // Red for hard timeout
        icon: '🔐',
      );
    }

    if (isIdleTimeout) {
      return _ModalConfig(
        title: '⚠️ Session Timeout Warning',
        subtitle: canExtendSession
            ? 'Your session will expire soon. You can extend it or let it timeout.'
            : 'Your session will expire soon.',
        headerColor: const Color(0xFFf59e0b),  // Orange for idle timeout
        icon: '⏱️',
      );
    }

    return _ModalConfig(
      title: '⏰ Session Management',
      subtitle: 'Session timeout notification',
      headerColor: const Color(0xFF6b7280),
      icon: '🔐',
    );
  }
}

class _ModalConfig {
  final String title;
  final String subtitle;
  final Color headerColor;
  final String icon;

  _ModalConfig({
    required this.title,
    required this.subtitle,
    required this.headerColor,
    required this.icon,
  });
}

Background/Foreground Timer Handling

Critical feature for accurate countdown when app goes to background:

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    // Handle app state changes for accurate countdown when app goes to background/foreground
    if (state == AppLifecycleState.paused || state == AppLifecycleState.inactive) {
      // App going to background - record the time
      _backgroundTime = DateTime.now();
      print('SessionModal - App going to background, recording time');
    } else if (state == AppLifecycleState.resumed && _backgroundTime != null) {
      // App returning to foreground - calculate elapsed time
      final elapsedSeconds = DateTime.now().difference(_backgroundTime!).inSeconds;
      print('SessionModal - App returning to foreground, elapsed: ${elapsedSeconds}s');

      // Update countdown based on actual elapsed time
      setState(() {
        _countdown = (_countdown - elapsedSeconds).clamp(0, double.infinity).toInt();
        print('SessionModal - Countdown updated to: $_countdown');
      });

      _backgroundTime = null;
    }
  }

Key features of the session modal:

The following images showcase screens from the sample application:

Session Extend Screen

Session Timeout Screen

Wrap your application with the Riverpod provider scope and session modal overlay:

// lib/main.dart

void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final sessionState = ref.watch(sessionProvider);

    return SDKEventProviderWidget(
      child: MaterialApp.router(
        title: 'REL-ID Session Management Tutorial',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        routerConfig: appRouter,
        builder: (context, child) {
          return Stack(
            children: [
              // Main app content
              child ?? const SizedBox.shrink(),

              // Global Session Management Modal (overlay)
              if (sessionState.isSessionModalVisible)
                const SessionModal(),
            ],
          );
        },
      ),
    );
  }
}

Integration Benefits

The Riverpod provider approach offers several advantages:

Test Scenarios

  1. Hard Session Timeout: Test mandatory session expiration
  2. Idle Session Timeout with Extension: Test warning with successful extension
  3. Extension Failure: Test extension API failure handling
  4. Background/Foreground Timer: Test countdown accuracy across app states
  5. Multiple Session Events: Test handling of rapid session events

Common Session Management Test Scenarios

Session Type

Test Case

Expected Behavior

Validation Points

Hard Timeout

Session expires

Modal with Close button only

Navigate to home screen

Idle Warning

Session expiring soon

Modal with countdown and Extend button

Extension API call works

Extension Success

Extend session API succeeds

Modal dismisses, session continues

No navigation occurs

Extension Failure

Extend session API fails

Error alert, modal remains

User can retry or close

Background Timer

App goes to background during countdown

Timer accurately reflects elapsed time

Countdown resumes correctly

Testing Background/Foreground Accuracy

Critical test for production reliability:

// Test background/foreground timer accuracy
void testBackgroundTimer() {
  // 1. Trigger idle session timeout notification
  // 2. Note the countdown time (e.g., 60 seconds)
  // 3. Background the app for 30 seconds (press home button)
  // 4. Foreground the app (tap app icon)
  // 5. Verify countdown shows ~30 seconds remaining

  print('Testing background timer accuracy');
  print('Initial countdown: $initialCountdown');
  print('Time spent in background: $backgroundTime');
  print('Expected remaining time: ${initialCountdown - backgroundTime}');
  print('Actual remaining time: $currentCountdown');
}

Debugging Session Events

Use these debugging techniques to verify session functionality:

// Verify callback registration
print('Session callbacks:');
print('  timeout: ${eventManager.sessionTimeoutHandler != null}');
print('  notification: ${eventManager.sessionTimeoutNotificationHandler != null}');
print('  extension: ${eventManager.sessionExtensionResponseHandler != null}');

// Log session event data
print('Session timeout notification:');
print('  userID: ${data.userID}');
print('  timeLeft: ${data.timeLeftInSeconds}s');
print('  canExtend: ${data.sessionCanBeExtended == 1}');
print('  message: ${data.message}');

Modal Not Appearing

Cause: Session callbacks not properly registered Solution: Verify ProviderScope wraps your app and provider is initialized

// Correct setup in main.dart
void main() {
  runApp(
    const ProviderScope(  // ✅ Required for Riverpod
      child: MyApp(),
    ),
  );
}

Cause: Event listeners not attached Solution: Check that event registration succeeds in _setupEventHandlers()

Timer Accuracy Issues

Cause: Countdown doesn't account for background time Solution: Implement WidgetsBindingObserver lifecycle handling

// Correct background/foreground handling
class _SessionModalState extends ConsumerState<SessionModal>
    with WidgetsBindingObserver {
  DateTime? _backgroundTime;

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused || state == AppLifecycleState.inactive) {
      _backgroundTime = DateTime.now();
    } else if (state == AppLifecycleState.resumed && _backgroundTime != null) {
      final elapsedSeconds = DateTime.now().difference(_backgroundTime!).inSeconds;
      setState(() {
        _countdown = (_countdown - elapsedSeconds).clamp(0, double.infinity).toInt();
      });
      _backgroundTime = null;
    }
  }
}

Extension API Failures

Cause: Calling extension API when sessionCanBeExtended is false Solution: Check extension eligibility before API call

Future<void> handleExtendSession(BuildContext context) async {
  final data = state.sessionTimeoutNotificationData;
  if (data?.sessionCanBeExtended != 1) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Extension Not Available'),
        content: const Text('This session cannot be extended.'),
      ),
    );
    return;
  }

  // Proceed with extension API call
  await _rdnaService.extendSessionIdleTimeout();
}

Cause: Multiple concurrent extension requests Solution: Use operation tracking to prevent duplicates

String _currentOperation = 'none';

Future<void> handleExtendSession(BuildContext context) async {
  if (_currentOperation != 'none') {
    print('Extension already in progress');
    return;
  }

  _currentOperation = 'extend';
  // ... perform extension
}

Modal Dismissal Issues

Cause: Back button dismissing modal on Android Solution: Use PopScope with canPop: false

return PopScope(
  canPop: false,  // ✅ Prevent default back action
  child: Material(
    // Modal content
  ),
);

Best Practice: Test session management behavior on both iOS and Android devices with different timeout scenarios.

Important Security Guidelines

  1. Never bypass session timeouts - Always respect hard timeout requirements
  2. Limit extension attempts - Implement reasonable limits on extension requests
  3. Log session events securely - Track session management for security analysis
  4. Keep SDK updated - Regular updates include latest session security features
  5. Test thoroughly - Verify session behavior across different usage patterns

Session Extension Guidelines

Extension Scenario

Recommended Action

Implementation

Frequent Extensions

Set reasonable limits

Track extension count per session

Critical Operations

Allow extensions during important tasks

Context-aware extension logic

Inactive Sessions

Enforce timeouts

Don't extend completely idle sessions

Memory and Performance

// Proper cleanup in SessionNotifier
@override
void dispose() {
  print('SessionProvider - Disposing session provider');
  _eventManager.setSessionTimeoutHandler(null);
  _eventManager.setSessionTimeoutNotificationHandler(null);
  _eventManager.setSessionExtensionResponseHandler(null);
  super.dispose();
}

// Proper cleanup in SessionModal
@override
void dispose() {
  _countdownTimer?.cancel();
  WidgetsBinding.instance.removeObserver(this);
  super.dispose();
}

Session Data Protection

Congratulations! You've successfully learned how to implement comprehensive session management functionality with:

Key Security Benefits

Your session management implementation now provides:

Next Steps