import 'dart:async'; import 'dart:isolate'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:tugas_akhir_supabase/services/user_presence_service.dart'; class SessionManager { static const String _lastActiveTimeKey = 'last_active_time'; static const String _lastUserInteractionKey = 'last_user_interaction'; static const String _sessionStateKey = 'session_state'; static const int _sessionTimeoutMinutes = 30; static Timer? _sessionCheckTimer; static Timer? _presenceUpdateTimer; static bool _isCheckingSession = false; static bool _isAppInBackground = false; static bool _isSessionExpired = false; static bool _hasLoggedInUser = false; static bool _isAppJustLaunched = true; static final StreamController _sessionExpiredController = StreamController.broadcast(); // Stream untuk mendengarkan perubahan status session static Stream get sessionExpiredStream => _sessionExpiredController.stream; // Getter untuk mendapatkan waktu timeout session dalam menit static int getSessionTimeout() { return _sessionTimeoutMinutes; } // Getter untuk status login static bool get hasLoggedInUser => _hasLoggedInUser; // Metode untuk menghindari blocking pada SharedPreferences static Future _getSafeSharedPreferences() async { try { return await SharedPreferences.getInstance().timeout( const Duration(seconds: 2), onTimeout: () { debugPrint('Session: SharedPreferences timeout'); throw TimeoutException('SharedPreferences timeout'); }, ); } catch (e) { debugPrint('Session: Error getting SharedPreferences - $e'); return null; } } // Initialize session management static Future initializeSession() async { try { _isAppJustLaunched = true; debugPrint('Session: App just launched flag set to true'); Future.delayed(Duration(seconds: 10), () { _isAppJustLaunched = false; debugPrint('Session: App just launched flag set to false after delay'); }); final currentUser = Supabase.instance.client.auth.currentUser; if (currentUser == null) { debugPrint('Session: No authenticated user found'); _hasLoggedInUser = false; return; } _hasLoggedInUser = true; await updateLastUserInteraction(); _isAppInBackground = false; _setSessionExpired(false); // Initialize user presence service try { if (GetIt.instance.isRegistered()) { await GetIt.instance().initialize(); _startPresenceUpdates(); } } catch (e) { debugPrint('Session: Error initializing presence service - $e'); } Future.delayed(Duration(seconds: 5), () { _startSessionMonitoring(); }); debugPrint( 'Session: Initialized successfully for user: ${currentUser.email}', ); } catch (e) { debugPrint('Session: Error initializing - $e'); _hasLoggedInUser = false; } } // Method baru untuk memperbarui status login static void setUserLoggedIn(bool isLoggedIn) { _hasLoggedInUser = isLoggedIn; debugPrint('Session: User login status set to: $isLoggedIn'); if (isLoggedIn) { _setSessionExpired(false); updateLastUserInteraction(); _startSessionMonitoring(); _startPresenceUpdates(); } else { _stopSessionMonitoring(); _stopPresenceUpdates(); } } // Start periodic presence updates static void _startPresenceUpdates() { _stopPresenceUpdates(); // Stop any existing timer // Update presence every 30 seconds _presenceUpdateTimer = Timer.periodic(Duration(seconds: 30), (timer) { if (_hasLoggedInUser && !_isSessionExpired) { try { if (GetIt.instance.isRegistered()) { GetIt.instance().updatePresence(); } } catch (e) { debugPrint('Session: Error updating presence - $e'); } } }); debugPrint('Session: Started presence updates'); } // Stop presence updates static void _stopPresenceUpdates() { _presenceUpdateTimer?.cancel(); _presenceUpdateTimer = null; } // Check if session is valid with improved logic static Future isSessionValid() async { debugPrint('Session: Checking session validity...'); // Jika aplikasi baru saja diluncurkan, asumsikan sesi valid untuk menghindari dialog terlalu dini if (_isAppJustLaunched) { debugPrint('Session: App just launched, assuming session valid'); return true; } // If session is already marked as expired, return false immediately if (_isSessionExpired) { debugPrint('Session: Session already marked as expired'); return false; } if (!_hasLoggedInUser) { debugPrint('Session: No logged in user, skipping session validity check'); return true; } if (_isCheckingSession) { debugPrint('Session: Already checking session, returning current status'); return !_isSessionExpired; } _isCheckingSession = true; try { User? currentUser; Session? currentSession; try { currentUser = Supabase.instance.client.auth.currentUser; currentSession = Supabase.instance.client.auth.currentSession; debugPrint('Session: Supabase user: ${currentUser?.id ?? "null"}'); debugPrint( 'Session: Supabase session: ${currentSession != null ? "exists" : "null"}', ); if (currentSession != null) { final expiresAt = currentSession.expiresAt; final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; debugPrint( 'Session: Token expires at: $expiresAt, current time: $now', ); } } catch (e) { debugPrint('Session: Error accessing Supabase auth - $e'); _isCheckingSession = false; return !_isSessionExpired; } // If no user or session, session is invalid if (currentUser == null || currentSession == null) { debugPrint('Session: No valid Supabase user or session found'); _setSessionExpired(true); _isCheckingSession = false; return false; } SharedPreferences? prefs = await _getSafeSharedPreferences(); final sessionState = prefs?.getString(_sessionStateKey); if (sessionState == null || sessionState == 'inactive') { debugPrint( 'Session: No session state found or inactive, marking as expired', ); _hasLoggedInUser = false; _setSessionExpired(true); _isCheckingSession = false; return false; } // Check if Supabase token is expired final sessionExpiry = currentSession.expiresAt; if (sessionExpiry != null && sessionExpiry <= DateTime.now().millisecondsSinceEpoch ~/ 1000) { debugPrint('Session: Supabase session token expired'); _hasLoggedInUser = false; _setSessionExpired(true); _isCheckingSession = false; return false; } if (prefs == null) { debugPrint( 'Session: Could not access SharedPreferences, assuming valid', ); _isCheckingSession = false; return !_isSessionExpired; } final lastInteractionTime = prefs.getInt(_lastUserInteractionKey); debugPrint( 'Session: Last user interaction time from prefs: $lastInteractionTime', ); if (lastInteractionTime == null) { debugPrint( 'Session: No user interaction timestamp found, setting current time', ); await updateLastUserInteraction(); _isCheckingSession = false; return !_isSessionExpired; } final lastInteraction = DateTime.fromMillisecondsSinceEpoch( lastInteractionTime, ); final now = DateTime.now(); debugPrint('Session: Last user interaction time: $lastInteraction'); debugPrint('Session: Current time: $now'); if (lastInteraction.isAfter(now)) { debugPrint('Session: Invalid timestamp detected (future date)'); await updateLastUserInteraction(); _isCheckingSession = false; return !_isSessionExpired; } final difference = now.difference(lastInteraction); final differenceInMinutes = difference.inMinutes; final differenceInSeconds = difference.inSeconds; debugPrint( 'Session: Time difference: $differenceInMinutes minutes, $differenceInSeconds seconds', ); debugPrint( 'Session: Timeout limit: $_sessionTimeoutMinutes minutes (${_sessionTimeoutMinutes * 60} seconds)', ); final timeoutInSeconds = _sessionTimeoutMinutes * 60; final isValid = differenceInSeconds < timeoutInSeconds; if (!isValid) { debugPrint( 'Session: TIMEOUT - Sesi kedaluwarsa setelah $differenceInMinutes menit ($differenceInSeconds detik) tidak aktif. Batas waktu: $_sessionTimeoutMinutes menit', ); _setSessionExpired(true); } else { _setSessionExpired(false); debugPrint( 'Session: VALID - Terakhir aktif $differenceInMinutes menit ($differenceInSeconds detik) yang lalu. Batas waktu: $_sessionTimeoutMinutes menit', ); } _isCheckingSession = false; return isValid; } catch (e) { debugPrint('Session: Error checking validity - $e'); _isCheckingSession = false; return !_isSessionExpired; } } // BARU: Update timestamp interaksi pengguna terakhir static Future updateLastUserInteraction() async { try { final currentUser = Supabase.instance.client.auth.currentUser; if (currentUser == null) { debugPrint( 'Session: Cannot update interaction - user not authenticated', ); return; } SharedPreferences? prefs = await _getSafeSharedPreferences(); if (prefs == null) return; final now = DateTime.now().millisecondsSinceEpoch; try { await prefs .setInt(_lastUserInteractionKey, now) .timeout( const Duration(seconds: 2), onTimeout: () { debugPrint( 'Session: Timeout setting last user interaction time', ); return false; }, ); await updateLastActiveTime(); // Update presence when user interacts try { if (GetIt.instance.isRegistered()) { GetIt.instance().updatePresence(); } } catch (e) { debugPrint('Session: Error updating presence on interaction - $e'); } debugPrint( 'Session: User interaction recorded at ${DateTime.fromMillisecondsSinceEpoch(now)}', ); } catch (e) { debugPrint( 'Session: Error writing user interaction to SharedPreferences - $e', ); } } catch (e) { debugPrint('Session: Error updating user interaction - $e'); } } // Update last active time with better error handling static Future updateLastActiveTime() async { try { final currentUser = Supabase.instance.client.auth.currentUser; if (currentUser == null) { debugPrint('Session: Cannot update activity - user not authenticated'); return; } SharedPreferences? prefs = await _getSafeSharedPreferences(); if (prefs == null) return; final now = DateTime.now().millisecondsSinceEpoch; try { await prefs .setInt(_lastActiveTimeKey, now) .timeout( const Duration(seconds: 2), onTimeout: () { debugPrint('Session: Timeout setting last active time'); return false; }, ); await prefs .setString(_sessionStateKey, 'active') .timeout( const Duration(seconds: 2), onTimeout: () { debugPrint('Session: Timeout setting session state'); return false; }, ); debugPrint( 'Session: Activity updated at ${DateTime.fromMillisecondsSinceEpoch(now)}', ); } catch (e) { debugPrint('Session: Error writing to SharedPreferences - $e'); } } 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; _isAppInBackground = false; try { final currentUser = Supabase.instance.client.auth.currentUser; if (currentUser == null) { debugPrint( 'Session: No authenticated user when returning to foreground', ); _stopSessionMonitoring(); return; } debugPrint( 'Session: Checking session validity after returning to foreground', ); SharedPreferences? prefs = await _getSafeSharedPreferences(); if (prefs != null) { final lastInteractionTime = prefs.getInt(_lastUserInteractionKey); if (lastInteractionTime != null) { final lastInteraction = DateTime.fromMillisecondsSinceEpoch( lastInteractionTime, ); final now = DateTime.now(); final difference = now.difference(lastInteraction); final differenceInSeconds = difference.inSeconds; final timeoutInSeconds = _sessionTimeoutMinutes * 60; debugPrint('Session: Last user interaction time: $lastInteraction'); debugPrint('Session: Current time: $now'); debugPrint( 'Session: Time difference: ${difference.inMinutes} minutes, $differenceInSeconds seconds', ); debugPrint( 'Session: Timeout limit: $_sessionTimeoutMinutes minutes ($timeoutInSeconds seconds)', ); if (differenceInSeconds >= timeoutInSeconds) { debugPrint( 'Session: TIMEOUT after foreground - Inactive for $differenceInSeconds seconds (limit: $timeoutInSeconds)', ); _setSessionExpired(true); await clearSession(); return; } } } final isValid = await isSessionValid(); if (!isValid) { debugPrint('Session: Expired while in background'); await clearSession(); Future.delayed(const Duration(milliseconds: 500), () { _setSessionExpired(true); }); } else { debugPrint('Session: Still valid after background'); await updateLastActiveTime(); _startSessionMonitoring(); } } catch (e) { debugPrint('Session: Error during foreground transition - $e'); } } // Set session expired state and notify listeners static void _setSessionExpired(bool value) { if (_isSessionExpired != value) { _isSessionExpired = value; debugPrint('Session: Setting expired state to $value'); _sessionExpiredController.add(value); // If session is expired, force clear session if (value) { debugPrint('Session: Session expired, clearing session data'); clearSession(); } } } // Get session status for UI components static bool get isExpired => _isSessionExpired; // Check if user is properly authenticated static bool get isAuthenticated { if (_isSessionExpired) { debugPrint('Session: Session is marked as expired, not authenticated'); return false; } if (!_hasLoggedInUser) { return false; } final currentUser = Supabase.instance.client.auth.currentUser; final currentSession = Supabase.instance.client.auth.currentSession; final isValid = currentUser != null && currentSession != null && !_isSessionExpired; if (!isValid && _hasLoggedInUser) { _hasLoggedInUser = false; _setSessionExpired(true); debugPrint( 'Session: Updated login status to false based on authentication check', ); } return isValid; } // Clear session data with proper cleanup static Future clearSession() async { try { _stopSessionMonitoring(); _setSessionExpired(true); _hasLoggedInUser = false; final prefs = await SharedPreferences.getInstance(); await prefs.remove(_lastActiveTimeKey); await prefs.remove(_lastUserInteractionKey); await prefs.remove(_sessionStateKey); await Supabase.instance.client.auth.signOut(); debugPrint('Session: Cleared and signed out successfully'); } catch (e) { debugPrint('Session: Error during cleanup - $e'); _setSessionExpired(true); _hasLoggedInUser = false; } } // Refresh session - extend if still valid static Future refreshSession() async { try { final isValid = await isSessionValid(); if (isValid) { await updateLastUserInteraction(); return true; } return false; } catch (e) { debugPrint('Session: Error refreshing - $e'); return false; } } // Fungsi baru untuk memberi tahu aplikasi tentang aktivitas penting yang harus memperbarui sesi static Future notifyImportantActivity(String activityType) async { try { debugPrint('Session: Important activity detected: $activityType'); await updateLastUserInteraction(); Timer(Duration(seconds: 2), () { debugPrint('Session: Follow-up check after important activity'); isSessionValid(); }); } catch (e) { debugPrint('Session: Error handling important activity - $e'); } } // Start session monitoring (check every 30 seconds) static void _startSessionMonitoring() { _stopSessionMonitoring(); debugPrint( 'Session: Starting monitoring with timeout: $_sessionTimeoutMinutes minutes', ); _sessionCheckTimer = Timer.periodic(const Duration(seconds: 15), ( timer, ) async { debugPrint('Session: Running periodic check...'); if (_isCheckingSession) { debugPrint('Session: Skipping periodic check (already checking)'); return; } _isCheckingSession = true; try { bool isValid = false; try { isValid = await isSessionValid().timeout( const Duration(seconds: 3), onTimeout: () { debugPrint('Session: Validity check timed out'); return true; }, ); } catch (e) { debugPrint('Session: Error during periodic check - $e'); isValid = true; } if (!isValid) { debugPrint('Session: Expired during periodic check'); await clearSession(); _setSessionExpired(true); _stopSessionMonitoring(); } } catch (e) { debugPrint('Session: Error during periodic check - $e'); } finally { _isCheckingSession = false; } }); debugPrint('Session: Monitoring started'); Future.delayed(Duration(seconds: 5), () async { debugPrint('Session: Immediate check after monitoring start'); await isSessionValid(); }); } // Stop session monitoring static void _stopSessionMonitoring() { _sessionCheckTimer?.cancel(); _sessionCheckTimer = null; _stopPresenceUpdates(); debugPrint('Session: Monitoring stopped'); } // Dispose resources static void dispose() { _sessionCheckTimer?.cancel(); _presenceUpdateTimer?.cancel(); _sessionExpiredController.close(); try { if (GetIt.instance.isRegistered()) { GetIt.instance().dispose(); } } catch (e) { debugPrint('Session: Error disposing presence service - $e'); } } }