From b0315487911d02c8cfb6dda618c6774aee74e331 Mon Sep 17 00:00:00 2001 From: Santo Shakil Date: Wed, 13 May 2026 22:52:43 +0600 Subject: [PATCH] fix(mobile): don't block app open on slow validateAccessToken (#28405) * fix(mobile): don't block app open on slow validateAccessToken AuthGuard.onNavigation was async so auto_route awaited the body through validateAccessToken's OS timeout. now it's sync and the validate runs in bg. kicks to login on 401. * fix(mobile): handle re-login race in AuthGuard validate if user logs out + logs back in during a slow validate, the old 401 was logging them out again. now we check the token hasn't changed before redirecting, and dedupe in-flight calls. --------- Co-authored-by: Alex --- mobile/lib/routing/auth_guard.dart | 61 +++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/mobile/lib/routing/auth_guard.dart b/mobile/lib/routing/auth_guard.dart index eaa821c0eb..2fc27be4f4 100644 --- a/mobile/lib/routing/auth_guard.dart +++ b/mobile/lib/routing/auth_guard.dart @@ -15,37 +15,62 @@ class AuthGuard extends AutoRouteGuard { final ApiService _apiService; final AuthService _authService; final _log = Logger("AuthGuard"); + bool _validateInFlight = false; AuthGuard(this._apiService, this._authService); @override - void onNavigation(NavigationResolver resolver, StackRouter router) async { - resolver.next(true); - + void onNavigation(NavigationResolver resolver, StackRouter router) { + // Synchronously check for the access token. auto_route awaits async + // guards, so we keep this function fully sync and validate the token in + // the background — otherwise a slow validateAccessToken() request would + // block the route transition for as long as the OS-level HTTP timeout. try { - // Look in the store for an access token Store.get(StoreKey.accessToken); - - // Validate the access token with the server - final res = await _apiService.authenticationApi.validateAccessToken(); - if (res == null || res.authStatus != true) { - // If the access token is invalid, take user back to login - _log.fine('User token is invalid. Redirecting to login'); - unawaited(router.replaceAll([const LoginRoute()]).then((_) => _authService.clearLocalData())); - } } on StoreKeyNotFoundException catch (_) { - // If there is no access token, take us to the login page _log.warning('No access token in the store.'); + resolver.next(false); unawaited(router.replaceAll([const LoginRoute()])); return; + } + + resolver.next(true); + unawaited(_validateAccessTokenInBackground(router)); + } + + Future _validateAccessTokenInBackground(StackRouter router) async { + if (_validateInFlight) { + return; + } + final token = Store.tryGet(StoreKey.accessToken); + if (token == null) { + return; + } + _validateInFlight = true; + try { + final res = await _apiService.authenticationApi.validateAccessToken(); + if (res == null || res.authStatus != true) { + // Token may have changed during validation (user logged out + logged in + // again); only act if it still applies to the current session. + if (Store.tryGet(StoreKey.accessToken) != token) { + return; + } + _log.fine('User token is invalid. Redirecting to login'); + await router.replaceAll([const LoginRoute()]); + await _authService.clearLocalData(); + } } on ApiException catch (e) { - // On an unauthorized request, take us to the login page - if (e.code == HttpStatus.unauthorized) { - _log.warning("Unauthorized access token."); - unawaited(router.replaceAll([const LoginRoute()]).then((_) => _authService.clearLocalData())); + if (e.code != HttpStatus.unauthorized) { return; } + if (Store.tryGet(StoreKey.accessToken) != token) { + return; + } + _log.warning("Unauthorized access token."); + await router.replaceAll([const LoginRoute()]); + await _authService.clearLocalData(); } catch (e) { - // Otherwise, this is not fatal, but we still log the warning _log.warning('Error validating access token from server: $e'); + } finally { + _validateInFlight = false; } } }