656 lines
20 KiB
Dart
656 lines
20 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 = 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<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);
|
|
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<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 improved logic
|
|
static Future<bool> 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<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',
|
|
);
|
|
|
|
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<void> 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<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 (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<UserPresenceService>()) {
|
|
GetIt.instance<UserPresenceService>().dispose();
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Session: Error disposing presence service - $e');
|
|
}
|
|
}
|
|
}
|