import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// Tracks the user's authentication state so the [GoRouter] can decide /// whether to allow access to protected routes (such as the groups /// flow) or redirect to the sign-in screen. /// /// The router is wired to refresh whenever [notifyListeners] is called, /// which means any place that mutates the auth state (login, logout, /// registration completion) just has to call [refresh] to keep the /// navigation layer in sync. class AuthState extends ChangeNotifier { /// SharedPreferences key used to persist the auth token returned by /// the backend after a successful sign-in. static const String _tokenKey = 'token'; bool _isLoggedIn = false; bool _initialized = false; /// The route the user originally tried to visit before being sent to /// the sign-in screen. After a successful login, the redirect logic /// reads and clears this so the user lands where they were heading. String? _intendedRoute; bool get isLoggedIn => _isLoggedIn; String? get intendedRoute => _intendedRoute; /// Loads the current auth state from disk. Safe to call multiple /// times; subsequent calls just re-read the token value. Future initialize() async { final prefs = await SharedPreferences.getInstance(); _update(prefs.getString(_tokenKey) != null); _initialized = true; } /// Re-reads the auth state from disk and notifies listeners if it /// has changed. Call this from places that mutate the persisted /// token (e.g. after the login API call or after the user logs out). Future refresh() async { final prefs = await SharedPreferences.getInstance(); _update(prefs.getString(_tokenKey) != null); } void _update(bool loggedIn) { if (_isLoggedIn == loggedIn) return; _isLoggedIn = loggedIn; // NOTE: we intentionally do NOT clear _intendedRoute here. The // router's redirect callback reads it on the next build frame via // [consumeIntendedRoute], which is responsible for clearing it // after it has been consumed. notifyListeners(); } /// Called by the router's redirect callback when a protected route /// is hit by an unauthenticated user. Stores the route the user was /// trying to visit and returns the sign-in path the router should /// navigate to. String? requireAuth(String attemptedRoute) { if (_isLoggedIn) return null; // Avoid stacking redirects when the user is already on the // sign-in / sign-up screens. if (attemptedRoute.startsWith('/onboarding/')) return null; _intendedRoute = attemptedRoute; return '/onboarding/landing'; } /// Returns the previously captured intended route so the router can /// navigate the user there after a successful sign-in. The value is /// consumed (cleared) once read to avoid loops. String? consumeIntendedRoute() { if (!_isLoggedIn) return null; final route = _intendedRoute; _intendedRoute = null; return route; } } /// Global singleton so widgets and the router can share the same /// instance without needing to plumb a provider through the app. final AuthState authState = AuthState(); /// Whether the auth state has finished its first read from disk. /// The router waits for this before evaluating auth-gated redirects /// so we don't briefly bounce users to the sign-in screen on cold /// start before their persisted token has been loaded. bool get isAuthInitialized => authState._initialized;