blob: 0e0c6fb40fd763df3e1e251bdcb943062d257507 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'api_client_provider.dart';
/// All possible authentication states for the app.
///
/// Using a sealed-like enum keeps the router redirect logic exhaustive and
/// avoids stringly-typed checks throughout the codebase.
enum AuthStatus {
/// Initial state while the app checks whether a stored token exists.
loading,
/// A valid token was found in secure storage; the user is logged in.
authenticated,
/// No token exists or it has been purged (e.g. after a 401 response).
unauthenticated,
}
/// Immutable snapshot of the auth state passed through the provider graph.
///
/// Keeping this as a value object (rather than a mutable notifier field)
/// makes it safe to pass into go_router's redirect callback and to compare
/// with `==` in tests.
class AuthState {
const AuthState({required this.status});
final AuthStatus status;
/// Convenience constructors reduce noise at call sites.
const AuthState.loading() : status = AuthStatus.loading;
const AuthState.authenticated() : status = AuthStatus.authenticated;
const AuthState.unauthenticated() : status = AuthStatus.unauthenticated;
bool get isLoading => status == AuthStatus.loading;
bool get isAuthenticated => status == AuthStatus.authenticated;
bool get isUnauthenticated => status == AuthStatus.unauthenticated;
@override
String toString() => 'AuthState(${status.name})';
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AuthState &&
runtimeType == other.runtimeType &&
status == other.status;
@override
int get hashCode => status.hashCode;
}
/// Notifier that owns the mutable [AuthState] and exposes mutation methods
/// for login / logout.
///
/// [AsyncNotifier] is used because the initial state check is async (it reads
/// the secure token store). Downstream consumers can call [login] and
/// [logout] to drive route redirects via the router's [refreshListenable].
class AuthStateNotifier extends AsyncNotifier<AuthState> {
@override
Future<AuthState> build() async {
// Determine whether a token already exists on app startup. This drives
// the initial route decision inside the go_router redirect callback.
final storage = ref.read(tokenStorageProvider);
final token = await storage.readToken();
return token != null && token.isNotEmpty
? const AuthState.authenticated()
: const AuthState.unauthenticated();
}
/// Called after a successful login; persists [token] and updates state.
Future<void> login(String token) async {
final storage = ref.read(tokenStorageProvider);
await storage.writeToken(token);
state = const AsyncData(AuthState.authenticated());
}
/// Called on explicit logout or after [_UnauthorizedInterceptor] purges the
/// token. Clears the stored token and moves to the unauthenticated state.
Future<void> logout() async {
final storage = ref.read(tokenStorageProvider);
await storage.deleteToken();
state = const AsyncData(AuthState.unauthenticated());
}
}
/// The single source of truth for authentication status, consumed by the
/// router's redirect callback and any widget that needs to gate on auth.
final authStateProvider =
AsyncNotifierProvider<AuthStateNotifier, AuthState>(
AuthStateNotifier.new,
);
|