import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:patbond_flutter/features/auth/auth_models.dart'; import 'package:uuid/uuid.dart'; /// 认证状态机:unknown(启动恢复中,停留 Splash)→ authenticated / unauthenticated。 enum AuthStatus { unknown, authenticated, unauthenticated } /// token 持久化抽象。生产实现走系统安全存储(Keychain / Keystore), /// 测试注入内存实现。token 绝不写入 SharedPreferences(开发计划 §4.2)。 abstract class TokenStore { Future read(String key); Future write(String key, String value); Future delete(String key); } /// 基于 flutter_secure_storage 的生产实现。 class SecureTokenStore implements TokenStore { const SecureTokenStore([this._storage = const FlutterSecureStorage()]); final FlutterSecureStorage _storage; @override Future read(String key) => _storage.read(key: key); @override Future write(String key, String value) => _storage.write(key: key, value: value); @override Future delete(String key) => _storage.delete(key: key); } /// 会话管理:token 的内存副本 + 安全存储持久化 + 认证状态广播。 /// /// 状态变更只通过 [saveSession](登录/注册成功)、[markAuthenticated] / /// [markUnauthenticated](Splash 恢复裁决)与 [clearSession](登出/会话失效)。 /// TokenRefresher 轮换 token 用 [updateTokens],不改状态—— /// Splash 需要在最短停留时间后才切页(12 号组装稿 §7)。 class SessionManager extends ChangeNotifier { SessionManager({required this._store, this._uuid = const Uuid()}); static const _accessTokenKey = 'patbond_access_token'; static const _refreshTokenKey = 'patbond_refresh_token'; static const _accessExpiresKey = 'patbond_access_token_expires_at'; static const _refreshExpiresKey = 'patbond_refresh_token_expires_at'; static const _userIdKey = 'patbond_user_id'; static const _deviceIdKey = 'patbond_device_id'; final TokenStore _store; final Uuid _uuid; AuthStatus _status = AuthStatus.unknown; String? _accessToken; String? _refreshToken; String? _userId; String? _deviceId; bool _loaded = false; AuthStatus get status => _status; String? get accessToken => _accessToken; String? get refreshToken => _refreshToken; String? get userId => _userId; /// 设备标识:首次生成 UUID 并持久化,跨会话稳定。 String? get deviceId => _deviceId; /// 从安全存储载入 token 到内存(幂等)。存储读取失败按无会话处理, /// 不阻塞启动。 Future loadFromStorage() async { if (_loaded) return; _accessToken = await _readOrNull(_accessTokenKey); _refreshToken = await _readOrNull(_refreshTokenKey); _userId = await _readOrNull(_userIdKey); _deviceId = await _readOrNull(_deviceIdKey); if (_deviceId == null) { _deviceId = _uuid.v4(); await _writeQuietly(_deviceIdKey, _deviceId!); } _loaded = true; } /// 登录/注册成功:持久化整套 token 并进入已认证态。 Future saveSession(AuthTokens tokens) async { await updateTokens(tokens); _status = AuthStatus.authenticated; notifyListeners(); } /// 仅写入/轮换 token,不改变认证状态(刷新流程用)。 Future updateTokens(AuthTokens tokens) async { _accessToken = tokens.accessToken; _refreshToken = tokens.refreshToken; _userId = tokens.userId; _loaded = true; await _writeQuietly(_accessTokenKey, tokens.accessToken); await _writeQuietly(_refreshTokenKey, tokens.refreshToken); await _writeQuietly( _accessExpiresKey, tokens.accessTokenExpiresAt.toIso8601String(), ); await _writeQuietly( _refreshExpiresKey, tokens.refreshTokenExpiresAt.toIso8601String(), ); await _writeQuietly(_userIdKey, tokens.userId); } /// Splash 恢复成功后的显式切态。 void markAuthenticated() { _status = AuthStatus.authenticated; notifyListeners(); } /// 无本地会话或用户选择改用账号登录。 void markUnauthenticated() { _status = AuthStatus.unauthenticated; notifyListeners(); } /// 清除会话(登出 / refresh 失效)。设备标识保留。 Future clearSession() async { _accessToken = null; _refreshToken = null; _userId = null; _status = AuthStatus.unauthenticated; notifyListeners(); for (final key in const [ _accessTokenKey, _refreshTokenKey, _accessExpiresKey, _refreshExpiresKey, _userIdKey, ]) { try { await _store.delete(key); } catch (error) { debugPrint('清除会话存储失败($key):$error'); } } } Future _readOrNull(String key) async { try { return await _store.read(key); } catch (error) { debugPrint('读取安全存储失败($key):$error'); return null; } } Future _writeQuietly(String key, String value) async { try { await _store.write(key, value); } catch (error) { debugPrint('写入安全存储失败($key):$error'); } } }