88 lines
3.4 KiB
Dart
88 lines
3.4 KiB
Dart
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<void> 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<void> 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;
|