467 lines
14 KiB
Dart
467 lines
14 KiB
Dart
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 =
|
|
480; // Increased to 8 hours for permissive mode
|
|
|
|
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<bool> _sessionExpiredController =
|
|
StreamController<bool>.broadcast();
|
|
|
|
// Stream untuk mendengarkan perubahan status session
|
|
static Stream<bool> 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<SharedPreferences?> _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<void> 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<UserPresenceService>()) {
|
|
await GetIt.instance<UserPresenceService>().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); // Pastikan expired direset!
|
|
_isSessionExpired = false;
|
|
_sessionExpiredController.add(false); // Kirim event expired false
|
|
updateLastUserInteraction();
|
|
_startSessionMonitoring();
|
|
_startPresenceUpdates();
|
|
} else {
|
|
_stopSessionMonitoring();
|
|
_stopPresenceUpdates();
|
|
_setSessionExpired(false); // Reset expired saat logout juga!
|
|
_isSessionExpired = false;
|
|
_sessionExpiredController.add(false);
|
|
}
|
|
}
|
|
|
|
// Tambahkan fungsi untuk reset expired state secara manual
|
|
static void resetExpiredState() {
|
|
_isSessionExpired = false;
|
|
_sessionExpiredController.add(false);
|
|
}
|
|
|
|
// 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<UserPresenceService>()) {
|
|
GetIt.instance<UserPresenceService>().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 extremely permissive logic
|
|
static Future<bool> isSessionValid() async {
|
|
debugPrint('Session: Checking session validity (PERMISSIVE MODE)...');
|
|
|
|
// Check if user is actually logged in
|
|
final currentUser = Supabase.instance.client.auth.currentUser;
|
|
final currentSession = Supabase.instance.client.auth.currentSession;
|
|
|
|
if (currentUser == null || currentSession == null) {
|
|
debugPrint('Session: No user or session found, returning false');
|
|
return false;
|
|
}
|
|
|
|
// Always return true for permissive mode if user is logged in
|
|
debugPrint('Session: PERMISSIVE MODE - User logged in, returning true');
|
|
return true;
|
|
}
|
|
|
|
// BARU: Update timestamp interaksi pengguna terakhir
|
|
static Future<void> 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<UserPresenceService>()) {
|
|
GetIt.instance<UserPresenceService>().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<void> 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<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;
|
|
|
|
_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',
|
|
);
|
|
|
|
// Use permissive mode - don't check timeout
|
|
final isValid = await isSessionValid();
|
|
if (!isValid) {
|
|
debugPrint('Session: Session invalid after 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) {
|
|
// Jangan trigger expired jika user memang belum login
|
|
if (!_hasLoggedInUser) {
|
|
debugPrint('Session: Not logged in, skip setting expired state');
|
|
return;
|
|
}
|
|
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;
|
|
|
|
// Only update state if there's a significant change, and be more conservative
|
|
if (!isValid && _hasLoggedInUser) {
|
|
// Double check before changing state to avoid race conditions
|
|
final doubleCheckUser = Supabase.instance.client.auth.currentUser;
|
|
final doubleCheckSession = Supabase.instance.client.auth.currentSession;
|
|
|
|
if (doubleCheckUser == null || doubleCheckSession == null) {
|
|
debugPrint(
|
|
'Session: User authentication state changed, updating flags',
|
|
);
|
|
_hasLoggedInUser = false;
|
|
_setSessionExpired(true);
|
|
} else {
|
|
debugPrint('Session: User still exists, keeping authentication state');
|
|
}
|
|
}
|
|
|
|
return isValid;
|
|
}
|
|
|
|
// Clear session data with proper cleanup
|
|
static Future<void> clearSession() async {
|
|
try {
|
|
_stopSessionMonitoring();
|
|
_setSessionExpired(true);
|
|
_hasLoggedInUser = false;
|
|
_isSessionExpired = false;
|
|
_sessionExpiredController.add(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;
|
|
_isSessionExpired = false;
|
|
_sessionExpiredController.add(false);
|
|
}
|
|
}
|
|
|
|
// Refresh session - extend if still valid
|
|
static Future<bool> 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<void> 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 (DISABLED for permissive mode)
|
|
static void _startSessionMonitoring() {
|
|
_stopSessionMonitoring();
|
|
|
|
debugPrint('Session: Session monitoring DISABLED for permissive mode');
|
|
|
|
// No timer setup - completely disabled
|
|
debugPrint('Session: No periodic checks will run');
|
|
}
|
|
|
|
// 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<UserPresenceService>()) {
|
|
GetIt.instance<UserPresenceService>().dispose();
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Session: Error disposing presence service - $e');
|
|
}
|
|
}
|
|
}
|