291 lines
9.1 KiB
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();
|
|
}
|
|
}
|