Files
pangolin/client/lib/state/auth_provider.dart
T
wangjia ba53a0d478
ci-pangolin / Lint — shellcheck (push) Has been cancelled
ci-pangolin / OpenAPI Sync Check (push) Has been cancelled
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Has been cancelled
ci-pangolin / Flutter — analyze + test (push) Has been cancelled
feat(client): P1 真实数据接入地基(#6 6A)
- api_config.dart:API 基址单源(消除各处重复 _kApiUrl)
- api_client.dart:统一鉴权 HTTP 客户端,401→AuthNotifier.refresh→重试一次,
  错误统一 AuthApiException;+ 单测覆盖 401 刷新重试
- 模型 me/device/plan/usage_point(对齐后端 snake_case 契约)
- account_api.dart:/v1/me、/v1/plans、/v1/me/devices(列表+删)、/v1/usage、
  /v1/redeem、/v1/ads/unlock 封装
- auth_provider:注入 AuthApi + refresh();account_providers 装配 ApiClient/AccountApi
- 顺带修复 onboarding 提交遗留的测试 stub(_NullTokenStore 缺 isOnboarded/markOnboarded)

flutter analyze 0 error;114 tests passed。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 23:32:57 +08:00

84 lines
2.9 KiB
Dart

// auth_provider.dart — 认证状态(JWT 令牌生命周期)
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/api_config.dart';
import '../services/auth_api.dart';
import '../services/token_store.dart';
// ── 状态 ────────────────────────────────────────────────────────────
class AuthState {
const AuthState({this.accessToken, this.isLoading = false});
final String? accessToken;
final bool isLoading;
bool get isLoggedIn => accessToken != null && accessToken!.isNotEmpty;
AuthState copyWith({String? accessToken, bool? isLoading}) => AuthState(
accessToken: accessToken ?? this.accessToken,
isLoading: isLoading ?? this.isLoading,
);
}
// ── 状态机 ───────────────────────────────────────────────────────────
class AuthNotifier extends StateNotifier<AuthState> {
AuthNotifier(this._store, {AuthApi? api})
: _api = api ?? AuthApi(baseUrl: kApiBaseUrl),
super(const AuthState(isLoading: true)) {
_loadFromStore();
}
final TokenStore _store;
final AuthApi _api;
Future<void> _loadFromStore() async {
try {
final token = await _store.loadAccessToken();
state = AuthState(accessToken: token);
} catch (_) {
// FlutterSecureStorage 在测试环境 / 未初始化时抛异常,视为未登录。
state = const AuthState();
}
}
/// 登录 / 注册成功后保存令牌。
Future<void> saveTokens(AuthTokens tokens) async {
await _store.saveTokens(
access: tokens.accessToken,
refresh: tokens.refreshToken,
);
state = AuthState(accessToken: tokens.accessToken);
}
/// 用存储的 refresh token 刷新令牌(供 ApiClient 在 401 时调用)。
/// 成功写入新令牌并更新状态返回 true;无 refresh token / 刷新失败返回 false。
Future<bool> refresh() async {
final rt = await _store.loadRefreshToken();
if (rt == null || rt.isEmpty) return false;
try {
await saveTokens(await _api.refresh(rt));
return true;
} catch (_) {
return false;
}
}
/// 退出登录:清除本地令牌。
Future<void> logout() async {
await _store.clear();
state = const AuthState();
}
}
// ── Providers ────────────────────────────────────────────────────────
/// 可在测试中 override,注入 stub TokenStore(避免 FlutterSecureStorage 平台依赖)。
final tokenStoreProvider = Provider<TokenStore>((_) => const TokenStore());
final authProvider =
StateNotifierProvider<AuthNotifier, AuthState>(
(ref) => AuthNotifier(ref.watch(tokenStoreProvider)),
);