diff --git a/lib/core/auth/auth_state_manager.dart b/lib/core/auth/auth_state_manager.dart index 65da1f7..afa5423 100644 --- a/lib/core/auth/auth_state_manager.dart +++ b/lib/core/auth/auth_state_manager.dart @@ -86,6 +86,8 @@ class AuthStateManager extends StateNotifier { final Ref _ref; final AuthCacheManager _cacheManager = AuthCacheManager(); + // Prevent overlapping silent-login attempts from multiple triggers + Future? _silentLoginFuture; /// Initialize auth state from storage Future _initialize() async { @@ -330,6 +332,22 @@ class AuthStateManager extends StateNotifier { /// Perform silent auto-login with saved credentials Future silentLogin() async { + // Coalesce concurrent calls (e.g., UI + interceptor retry) + if (_silentLoginFuture != null) { + return await _silentLoginFuture!; + } + final thisAttempt = _performSilentLogin(); + _silentLoginFuture = thisAttempt; + try { + return await thisAttempt; + } finally { + if (identical(_silentLoginFuture, thisAttempt)) { + _silentLoginFuture = null; + } + } + } + + Future _performSilentLogin() async { state = state.copyWith( status: AuthStatus.loading, isLoading: true, @@ -401,7 +419,11 @@ class AuthStateManager extends StateNotifier { /// Handle token invalidation (called by API service) Future onTokenInvalidated() async { - DebugLogger.auth('Auth token invalidated'); + // Avoid spamming logs if multiple requests invalidate at once + final reloginInProgress = _silentLoginFuture != null; + if (!reloginInProgress) { + DebugLogger.auth('Auth token invalidated'); + } // Clear token from storage final storage = _ref.read(optimizedStorageServiceProvider); @@ -418,7 +440,9 @@ class AuthStateManager extends StateNotifier { // Attempt silent re-login if credentials are available final hasCredentials = await storage.getSavedCredentials() != null; if (hasCredentials) { - DebugLogger.auth('Attempting silent re-login after token invalidation'); + if (!reloginInProgress) { + DebugLogger.auth('Attempting silent re-login after token invalidation'); + } await silentLogin(); } } diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index a104916..1e7a747 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -233,7 +233,7 @@ final currentUserProvider = FutureProvider((ref) async { // Helper provider to force refresh auth state - now using unified system final refreshAuthStateProvider = Provider((ref) { // This provider can be invalidated to force refresh the unified auth system - ref.read(refreshAuthProvider)(); + Future.microtask(() => ref.read(authActionsProvider).refresh()); return; }); diff --git a/lib/features/auth/providers/unified_auth_providers.dart b/lib/features/auth/providers/unified_auth_providers.dart index db27048..38f39d8 100644 --- a/lib/features/auth/providers/unified_auth_providers.dart +++ b/lib/features/auth/providers/unified_auth_providers.dart @@ -5,35 +5,53 @@ import '../../../core/providers/app_providers.dart'; /// Unified auth providers using the new auth state manager /// These replace the old auth providers for better efficiency -/// Login action provider (schedules side-effect outside provider build) -final loginActionProvider = Provider.family, Map>( - (ref, credentials) async { - final authManager = ref.read(authStateManagerProvider.notifier); +/// Imperative auth actions wrapper to avoid side-effects during provider build +class AuthActions { + final Ref _ref; + AuthActions(this._ref); - final username = credentials['username']!; - final password = credentials['password']!; - final rememberCredentials = credentials['remember'] == 'true'; + AuthStateManager get _auth => + _ref.read(authStateManagerProvider.notifier); - // Defer the mutation to avoid changing other providers during build - return Future(() => authManager.login( + Future login( + String username, + String password, { + bool rememberCredentials = false, + }) { + // Defer mutation to a microtask to avoid provider-build side-effects + return Future(() => _auth.login( username, password, rememberCredentials: rememberCredentials, )); - }, -); + } -/// Silent login action provider - returns a callback to run later -final silentLoginActionProvider = Provider Function()>((ref) { - final authManager = ref.read(authStateManagerProvider.notifier); - return () => authManager.silentLogin(); -}); + Future loginWithApiKey( + String apiKey, { + bool rememberCredentials = false, + }) { + return Future(() => _auth.loginWithApiKey( + apiKey, + rememberCredentials: rememberCredentials, + )); + } -/// Logout action provider - returns a callback to run later -final logoutActionProvider = Provider Function()>((ref) { - final authManager = ref.read(authStateManagerProvider.notifier); - return () => authManager.logout(); -}); + Future silentLogin() { + return Future(() => _auth.silentLogin()); + } + + Future logout() { + return Future(() => _auth.logout()); + } + + Future refresh() { + return Future(() => _auth.refresh()); + } +} + +final authActionsProvider = Provider((ref) => AuthActions(ref)); + +// Legacy action providers have been replaced by `authActionsProvider` /// Check if saved credentials exist final hasSavedCredentialsProvider2 = FutureProvider((ref) async { @@ -70,11 +88,7 @@ final authStatusProvider = Provider((ref) { return ref.watch(authStateManagerProvider.select((state) => state.status)); }); -/// Helper provider to trigger auth refresh - returns a callback -final refreshAuthProvider = Provider Function()>((ref) { - final authManager = ref.read(authStateManagerProvider.notifier); - return () => authManager.refresh(); -}); +// Use `ref.read(authActionsProvider).refresh()` instead of refresh providers /// Provider to watch for auth state changes and update API service final authApiIntegrationProvider = Provider((ref) { diff --git a/lib/features/auth/views/authentication_page.dart b/lib/features/auth/views/authentication_page.dart index 2ea2ad1..cde14e5 100644 --- a/lib/features/auth/views/authentication_page.dart +++ b/lib/features/auth/views/authentication_page.dart @@ -16,6 +16,7 @@ import '../../../shared/widgets/conduit_components.dart'; import '../../../core/auth/auth_state_manager.dart'; import '../../../core/utils/debug_logger.dart'; import 'package:conduit/l10n/app_localizations.dart'; +import '../providers/unified_auth_providers.dart'; class AuthenticationPage extends ConsumerStatefulWidget { final ServerConfig serverConfig; @@ -71,16 +72,16 @@ class _AuthenticationPageState extends ConsumerState { }); try { - final authManager = ref.read(authStateManagerProvider.notifier); + final actions = ref.read(authActionsProvider); bool success; if (_useApiKey) { - success = await authManager.loginWithApiKey( + success = await actions.loginWithApiKey( _apiKeyController.text.trim(), rememberCredentials: true, // Consistent with credentials method ); } else { - success = await authManager.login( + success = await actions.login( _usernameController.text.trim(), _passwordController.text, rememberCredentials: true, diff --git a/lib/features/profile/views/profile_page.dart b/lib/features/profile/views/profile_page.dart index 2b98878..82571af 100644 --- a/lib/features/profile/views/profile_page.dart +++ b/lib/features/profile/views/profile_page.dart @@ -762,7 +762,7 @@ class ProfilePage extends ConsumerWidget { ); if (confirm) { - await ref.read(logoutActionProvider)(); + await ref.read(authActionsProvider).logout(); } } } diff --git a/lib/main.dart b/lib/main.dart index 38ed275..4ba76dd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -181,7 +181,7 @@ class _ConduitAppState extends ConsumerState { hasSavedCredentialsProvider2.future, ); if (hasCreds) { - await ref.read(silentLoginActionProvider)(); + await ref.read(authActionsProvider).silentLogin(); } } catch (_) { // Ignore errors, fallback to showing unified page