import 'dart:async'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; class SessionManager { static const String _lastActiveTimeKey = 'last_active_time'; static const String _sessionStateKey = 'session_state'; static const int _sessionTimeoutMinutes = 15; static Timer? _sessionCheckTimer; static bool _isCheckingSession = false; static bool _isAppInBackground = false; static bool _isSessionExpired = false; static final StreamController _sessionExpiredController = StreamController.broadcast(); // Stream untuk mendengarkan perubahan status session static Stream get sessionExpiredStream => _sessionExpiredController.stream; // Initialize session management static Future initializeSession() async { try { // Check if user is authenticated first final currentUser = Supabase.instance.client.auth.currentUser; if (currentUser == null) { debugPrint('Session: No authenticated user found'); _setSessionExpired(true); return; } await updateLastActiveTime(); _isAppInBackground = false; _setSessionExpired(false); debugPrint( 'Session: Initialized successfully for user: ${currentUser.email}', ); } catch (e) { debugPrint('Session: Error initializing - $e'); _setSessionExpired(true); } } // Update last active time with better error handling static Future updateLastActiveTime() async { try { // Only update if user is authenticated final currentUser = Supabase.instance.client.auth.currentUser; if (currentUser == null) { debugPrint('Session: Cannot update activity - user not authenticated'); return; } final prefs = await SharedPreferences.getInstance(); final now = DateTime.now().millisecondsSinceEpoch; await prefs.setInt(_lastActiveTimeKey, now); // Store session state await prefs.setString(_sessionStateKey, 'active'); debugPrint( 'Session: Activity updated at ${DateTime.fromMillisecondsSinceEpoch(now)}', ); } catch (e) { debugPrint('Session: Error updating activity - $e'); } } // Called when app goes to background static Future onAppBackground() async { debugPrint('Session: App entering background'); _isAppInBackground = true; await updateLastActiveTime(); _startSessionMonitoring(); } // Called when app comes to foreground static Future onAppForeground() async { debugPrint('Session: App entering foreground'); if (!_isAppInBackground) return; // Skip if already in foreground _isAppInBackground = false; try { // First check if user is authenticated final currentUser = Supabase.instance.client.auth.currentUser; if (currentUser == null) { debugPrint( 'Session: No authenticated user when returning to foreground', ); _stopSessionMonitoring(); return; } final isValid = await isSessionValid(); if (!isValid) { debugPrint('Session: Expired while in background'); await clearSession(); // Notify UI that session has expired with a slight delay to ensure app is ready Future.delayed(const Duration(milliseconds: 500), () { _setSessionExpired(true); }); } else { debugPrint('Session: Still valid after background'); await updateLastActiveTime(); } } catch (e) { debugPrint('Session: Error during foreground transition - $e'); } finally { _stopSessionMonitoring(); // Always stop background monitoring } } // Check if session is valid with improved logic static Future isSessionValid() async { try { // First check if user is authenticated via Supabase final currentUser = Supabase.instance.client.auth.currentUser; final currentSession = Supabase.instance.client.auth.currentSession; if (currentUser == null || currentSession == null) { debugPrint('Session: No valid Supabase session found'); // Don't trigger session expired notification for unauthenticated users return false; } // Check if session token is expired final sessionExpiry = currentSession.expiresAt; if (sessionExpiry != null && sessionExpiry <= DateTime.now().millisecondsSinceEpoch ~/ 1000) { debugPrint('Session: Supabase session token expired'); _setSessionExpired(true); return false; } // Check our custom activity timeout final prefs = await SharedPreferences.getInstance(); final lastActiveTime = prefs.getInt(_lastActiveTimeKey); if (lastActiveTime == null) { debugPrint('Session: No activity timestamp found'); // Don't trigger session expired for missing timestamps return false; } final lastActive = DateTime.fromMillisecondsSinceEpoch(lastActiveTime); final now = DateTime.now(); // Validate timestamps if (lastActive.isAfter(now)) { debugPrint('Session: Invalid timestamp detected (future date)'); // Don't trigger session expired for invalid timestamps return false; } final difference = now.difference(lastActive); final differenceInMinutes = difference.inMinutes; // Check timeout - only timeout if app has been inactive for too long final isValid = differenceInMinutes < _sessionTimeoutMinutes; if (!isValid) { debugPrint( 'Session: Timeout after $differenceInMinutes minutes of inactivity', ); _setSessionExpired(true); } else { _setSessionExpired(false); debugPrint( 'Session: Valid - last active $differenceInMinutes minutes ago', ); } return isValid; } catch (e) { debugPrint('Session: Error checking validity - $e'); // Don't trigger session expired for errors return false; } } // Set session expired state and notify listeners static void _setSessionExpired(bool value) { if (_isSessionExpired != value) { _isSessionExpired = value; _sessionExpiredController.add(value); } } // Get session status for UI components static bool get isExpired => _isSessionExpired; // Check if user is properly authenticated static bool get isAuthenticated { final currentUser = Supabase.instance.client.auth.currentUser; final currentSession = Supabase.instance.client.auth.currentSession; return currentUser != null && currentSession != null && !_isSessionExpired; } // Clear session data with proper cleanup static Future clearSession() async { try { _stopSessionMonitoring(); _setSessionExpired(true); // Clear local preferences final prefs = await SharedPreferences.getInstance(); await prefs.remove(_lastActiveTimeKey); await prefs.remove(_sessionStateKey); // Sign out from Supabase await Supabase.instance.client.auth.signOut(); debugPrint('Session: Cleared and signed out successfully'); } catch (e) { debugPrint('Session: Error during cleanup - $e'); // Even if there's an error, mark session as expired _setSessionExpired(true); } } // Refresh session - extend if still valid static Future refreshSession() async { try { final isValid = await isSessionValid(); if (isValid) { await updateLastActiveTime(); return true; } return false; } catch (e) { debugPrint('Session: Error refreshing - $e'); return false; } } // Start session monitoring (check every minute when app is in background) static void _startSessionMonitoring() { if (!_isAppInBackground) { debugPrint('Session: Monitoring not needed - app in foreground'); return; } _stopSessionMonitoring(); // Stop any existing timer _sessionCheckTimer = Timer.periodic( const Duration(minutes: 1), // Check every minute (timer) async { if (_isCheckingSession || !_isAppInBackground) return; _isCheckingSession = true; try { final isValid = await isSessionValid(); if (!isValid) { debugPrint('Session: Expired during background monitoring'); await clearSession(); timer.cancel(); // Stop monitoring if session expired } } catch (e) { debugPrint('Session: Error in background monitoring - $e'); } finally { _isCheckingSession = false; } }, ); debugPrint('Session: Background monitoring started'); } // Stop session monitoring static void _stopSessionMonitoring() { if (_sessionCheckTimer != null) { _sessionCheckTimer!.cancel(); _sessionCheckTimer = null; debugPrint('Session: Monitoring stopped'); } } // Get current session timeout static int getSessionTimeout() { return _sessionTimeoutMinutes; } // Dispose resources static void dispose() { _stopSessionMonitoring(); _sessionExpiredController.close(); } }