Files
velocity-pay-flutter/lib/auth_state.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;