MIF_E31222656/lib/services/session_manager.dart

291 lines
9.1 KiB
Dart

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<bool> _sessionExpiredController =
StreamController<bool>.broadcast();
// Stream untuk mendengarkan perubahan status session
static Stream<bool> get sessionExpiredStream =>
_sessionExpiredController.stream;
// Initialize session management
static Future<void> 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<void> 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<void> onAppBackground() async {
debugPrint('Session: App entering background');
_isAppInBackground = true;
await updateLastActiveTime();
_startSessionMonitoring();
}
// Called when app comes to foreground
static Future<void> 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<bool> 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<void> 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<bool> 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();
}
}