Open
Description
Hey, I am using the access token for my api's which has the expiration time of 15 mins, So after 15 mins if I again opened the app, sometimes auto refresh is working, some times it is not working and as a result, 403 error in apis coming some times, there is no manual refresh session we have done anywhere every auto refresh is being done by the supabase sdk itself, still instead of refreshing the old token, sign out event is emitted resulting in logging out the user.
This is the massive issue for us!
await sb.Supabase.initialize(
url: ApiKeys.supabaseUrl,
anonKey: ApiKeys.supabaseAnonKey,
authOptions: sb.FlutterAuthClientOptions(
autoRefreshToken: true,
localStorage:
CustomSecureStorage(persistSessionKey: LocalStorage.session),
),
);
class CustomSecureStorage extends sb.LocalStorage {
late FlutterSecureStorage _storage;
final String persistSessionKey;
CustomSecureStorage({required this.persistSessionKey});
@override
Future<void> initialize() async {
WidgetsFlutterBinding.ensureInitialized();
_storage = const FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
resetOnError: false,
),
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock,
),
);
debugPrint('🔥 337ssd: Secure storage initialized');
// No need for specific initialization in secure storage
}
@override
Future<bool> hasAccessToken() async {
final exists = await _storage.containsKey(key: persistSessionKey);
debugPrint('🔍 337ssd: Has access token: $exists');
return exists;
}
@override
Future<String?> accessToken() async {
final token = await _storage.read(key: persistSessionKey);
prints('🔑 337ssd: Retrieved access token: $token');
return token;
}
@override
Future<void> persistSession(String persistSessionString) async {
prints('💾 337ssd: Storing session in secure storage');
await _storage.write(key: persistSessionKey, value: persistSessionString);
}
@override
Future<void> removePersistedSession() async {
prints('🗑️ 337ssd: Removing session from secure storage');
await _storage.delete(key: persistSessionKey);
}
Future<void> clearSecureStorage() async {
await _storage.delete(key: persistSessionKey);
debugPrint('🗑️ 337ssd: All secure storage data cleared');
}
}
class TokenExpiryManager with WidgetsBindingObserver {
static final TokenExpiryManager _instance = TokenExpiryManager._internal();
factory TokenExpiryManager() => _instance;
static final Completer<void> _initializationCompleter = Completer<void>();
static Future<void> get initialized => _initializationCompleter.future;
final _supabase = sb.Supabase.instance.client;
final homeController = Get.find<HomeController>();
static bool _isInitialized = false;
static Future<void>? _initializationFuture;
sb.SupabaseClient get supabase => _supabase;
int attemptCount = 0;
bool isFirstTime = false;
bool isMinimizedAndNotRefreshed = false;
// Add these variables
Completer<void>? _tokenRefreshCompleter;
bool _isRefreshing = false;
bool _isTokenValid = false;
// Add this getter to check status
bool get isRefreshingToken => _isRefreshing;
bool get isTokenValid => _isTokenValid;
// Add this method to wait for refresh
Future<void> waitForTokenRefresh() async {
if (isFirstTime) {
if (_isRefreshing && _tokenRefreshCompleter != null) {
await _tokenRefreshCompleter!.future;
}
}
}
StreamSubscription<sb.AuthState>? _authStateSubscription;
TokenExpiryManager._internal(); // Empty internal constructor
static Future<TokenExpiryManager> initialize() async {
if (!_isInitialized) {
_initializationFuture ??= _instance._initialize();
await _initializationFuture;
_isInitialized = true;
// Add observer only once
WidgetsBinding.instance.removeObserver(_instance);
WidgetsBinding.instance.addObserver(_instance);
}
return _instance;
}
Future<void> _initialize() async {
debugPrint('55ssd TokenExpiryManager initializing...');
isFirstTime = true;
await _authStateSubscription?.cancel();
_authStateSubscription = _supabase.auth.onAuthStateChange
.listen(_handleAuthStateChange, cancelOnError: false);
_initializationCompleter.complete();
debugPrint('55ssd TokenExpiryManager initialization complete');
// Check session status on initialization
}
void _handleAuthStateChange(sb.AuthState data) async {
final session = data.session;
final event = data.event;
final currentSession = _supabase.auth.currentSession;
final user = await LocalStorage.getUserModel();
final token = await LocalStorage.getRefreshToken();
prints('55ssd 73ssd inside listenToAuthChanges session: $session');
prints('55ssd 73ssd inside listenToAuthChanges event: $event');
prints(
'55ssd 73ssd inside listenToAuthChanges currentSession: $currentSession');
prints(
'55ssd 73ssd inside listenToAuthChanges isExpired: ${session?.isExpired}');
prints(
'55ssd 73ssd inside listenToAuthChanges refreshToken: ${await LocalStorage.getRefreshToken()}');
callSaveTestLog(
'Entered Starting point of app: Session: $session, Event: $event',
"Last saved token:$token",
'${user?.firstName ?? 777}');
if (session != null) {
switch (event) {
case sb.AuthChangeEvent.tokenRefreshed:
_isRefreshing = true;
_tokenRefreshCompleter = Completer<void>();
try {
debugPrint('Token refresh started');
await _saveSessionToLocal(session);
_isTokenValid = true;
isFirstTime = false;
isMinimizedAndNotRefreshed = false;
_tokenRefreshCompleter?.complete();
} catch (e) {
_tokenRefreshCompleter?.completeError(e);
isFirstTime = false;
_isTokenValid = false;
isMinimizedAndNotRefreshed = false;
} finally {
_isRefreshing = false;
isFirstTime = false;
_tokenRefreshCompleter = null;
isMinimizedAndNotRefreshed = false;
}
break;
case sb.AuthChangeEvent.initialSession:
try {
debugPrint('Initial session - fresh launch');
await _saveSessionToLocal(session);
isFirstTime = false;
_isTokenValid = true;
isMinimizedAndNotRefreshed = false;
} catch (e) {
debugPrint('Error saving initial session: $e');
isFirstTime = false;
_isTokenValid = false;
isMinimizedAndNotRefreshed = false;
}
break;
case sb.AuthChangeEvent.signedOut:
debugPrint('User signed out');
break;
case sb.AuthChangeEvent.userDeleted:
debugPrint('User account deleted');
// await _clearLocalSession();
break;
default:
debugPrint('Auth event: $event');
}
} else {
_isTokenValid = false;
debugPrint('73ssd session is $session, no details found');
}
}
void logout() async {
await _supabase.auth.signOut();
}
callSaveTestLog(String testLog, String currentToken, String name) async {
try {
final response =
await OnBoardingGQLQueries.saveTestLog(testLog, currentToken, name);
debugPrint('73ssd Test log saved successfully: $response');
final metadata = response['metadata'];
final success = response['success'];
if (success != null && success == true) {
debugPrint('73ssd Test log saved successfully: $metadata');
} else {
debugPrint('73ssd Failed to save test log: $metadata');
}
} catch (e) {
debugPrint('73ssd Error saving test log: $e');
}
}
Future<void> _saveSessionToLocal(sb.Session? session,
// ignore: unused_element_parameter
[String? enteredLocation]) async {
// final user = await LocalStorage.getUserModel();
try {
if (session != null) {
await LocalStorage.setAllTokenData(
token: session.accessToken,
refreshToken: session.refreshToken!,
expiresIn: session.expiresIn?.toString() ?? '',
expiresAt: session.expiresAt.toString(),
);
debugPrint(
'Token saved locally: AccessToken and RefreshToken updated.');
} else {
debugPrint('No session available to save.');
}
} catch (e) {
debugPrint('73ssd Error saving session to local storage: $e');
}
}
bool _hasResumedOnce = false;
@override
void didChangeAppLifecycleState(AppLifecycleState state) async {
final user = await LocalStorage.getUserModel();
final token = await LocalStorage.getRefreshToken();
final homeController = Get.find<HomeController>();
debugPrint(
'55ssd :App lifecycle state condition: ${homeController.enablePrivacyMode.value == true}');
if (state == AppLifecycleState.paused &&
homeController.enablePrivacyMode.value == true) {
isMinimizedAndNotRefreshed = true;
final currentSession = _supabase.auth.currentSession;
debugPrint('55ssd :App paused: Checking session');
callSaveTestLog(
'User minimized the app with subscription canceled status: ${_authStateSubscription == null} session: $currentSession',
"Last saved token : $token",
'${user?.firstName ?? 777}',
);
}
if (state == AppLifecycleState.resumed) {
debugPrint('55ssd :App resumed');
isMinimizedAndNotRefreshed = false;
if (_hasResumedOnce) return;
_hasResumedOnce = true;
final currentSession = _supabase.auth.currentSession;
callSaveTestLog(
'User resumed the app with currentSession: $currentSession',
"Last saved token : $token",
'${user?.firstName ?? 777}',
);
if (user != null) {
final context = AppGlobalKey.navKey.currentContext;
if (context != null) {
prints('73ssd inside Phoenix.rebirth');
Phoenix.rebirth(context);
callSaveTestLog(
'Restarting the app from code level: $currentSession',
"Last saved token : $token",
'${user.firstName ?? 777}',
);
}
if (_authStateSubscription == null) {
debugPrint('73ssd inside if _authStateSubscription == null');
callSaveTestLog(
'Again initialising the app: $currentSession',
"Last saved token : $token",
'${user.firstName ?? 777}',
);
await _initialize();
}
}
_hasResumedOnce = false;
}
}
void dispose() {
_authStateSubscription?.cancel();
WidgetsBinding.instance.removeObserver(this);
debugPrint('TokenExpiryManager disposed');
}
Future<String> getApiToken() async {
final session = _supabase.auth.currentSession;
final user = await LocalStorage.getUserModel();
try {
if (session != null && user != null) {
// await _saveSessionToLocal(session, 'getApiToken');
prints('73ssd inside getApiToken session: ${session.accessToken}');
return session.accessToken;
} else if (session == null && user != null) {
final token = await LocalStorage.getGraphQLApiToken();
prints('73ssd inside local storage token: $token');
return token ?? ApiKeys.unAuthToken;
} else {
return ApiKeys.unAuthToken;
}
} catch (e) {
debugPrint('Error getting API token: $e');
return ApiKeys.unAuthToken;
}
}
}