import 'dart:async'; import 'package:flutter_riverpod/legacy.dart'; import '../core/storage/storage_manager.dart'; import '../api/guangya_api.dart'; class AuthState { final bool isSignedIn; final bool isLoading; final bool isRefreshing; final Map? userInfo; final String? errorMessage; final String phoneNumber; final String verificationCode; final String captchaToken; final String verificationID; final int codeCountdown; final String qrPayload; final String qrToken; final String qrStatus; const AuthState({ this.isSignedIn = false, this.isLoading = true, this.isRefreshing = false, this.userInfo, this.errorMessage, this.phoneNumber = '+86 ', this.verificationCode = '', this.captchaToken = '', this.verificationID = '', this.codeCountdown = 0, this.qrPayload = '', this.qrToken = '', this.qrStatus = '等待生成二维码', }); AuthState copyWith({ bool? isSignedIn, bool? isLoading, bool? isRefreshing, Map? userInfo, bool clearUserInfo = false, String? errorMessage, bool clearError = false, String? phoneNumber, String? verificationCode, String? captchaToken, String? verificationID, int? codeCountdown, String? qrPayload, String? qrToken, String? qrStatus, }) { return AuthState( isSignedIn: isSignedIn ?? this.isSignedIn, isLoading: isLoading ?? this.isLoading, isRefreshing: isRefreshing ?? this.isRefreshing, userInfo: clearUserInfo ? null : (userInfo ?? this.userInfo), errorMessage: clearError ? null : (errorMessage ?? this.errorMessage), phoneNumber: phoneNumber ?? this.phoneNumber, verificationCode: verificationCode ?? this.verificationCode, captchaToken: captchaToken ?? this.captchaToken, verificationID: verificationID ?? this.verificationID, codeCountdown: codeCountdown ?? this.codeCountdown, qrPayload: qrPayload ?? this.qrPayload, qrToken: qrToken ?? this.qrToken, qrStatus: qrStatus ?? this.qrStatus, ); } // ── Derived getters ──────────────────────────────────────────── String get userName => _findStringDeep(userInfo, ['nickname', 'name', 'username']) ?? '光鸭用户'; String get memberLevel => _findStringDeep(userInfo, ['vipName', 'memberName', 'memberLevelName']) ?? '普通会员'; int? get capacity => _findInt64Deep(userInfo, ['capacity', 'totalCapacity']); int? get usedCapacity => _findInt64Deep(userInfo, ['usedCapacity', 'usedSpace']); String get capacityText { if (capacity == null) return '空间信息暂不可用'; return '${_formatBytes(usedCapacity ?? 0)} / ${_formatBytes(capacity!)}'; } // ── Static helpers ───────────────────────────────────────────── static String? _findStringDeep( Map? json, List keys) { if (json == null) return null; for (final key in keys) { final v = json[key]; if (v != null && v.toString().isNotEmpty) return v.toString(); } for (final entry in json.entries) { if (entry.value is Map) { final found = _findStringDeep(entry.value as Map, keys); if (found != null) return found; } } return null; } static int? _findInt64Deep( Map? json, List keys) { if (json == null) return null; for (final key in keys) { final v = json[key]; if (v != null) return v is int ? v : int.tryParse(v.toString()); } return null; } static String _formatBytes(int bytes) { if (bytes < 1024) return '$bytes B'; if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; if (bytes < 1024 * 1024 * 1024) { return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; } return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB'; } } class AuthNotifier extends StateNotifier { final GuangyaAPI _api = GuangyaAPI( deviceID: StorageManager.get(StorageKeys.deviceID), ); Timer? _qrPollingTimer; Timer? _countdownTimer; GuangyaAPI get api => _api; AuthNotifier() : super(const AuthState()) { _api.accessToken = ''; tryRestoreSession(); } Future tryRestoreSession() async { final access = StorageManager.get(StorageKeys.accessToken) ?? ''; final refresh = StorageManager.get(StorageKeys.refreshToken); _api.accessToken = access; _api.refreshTokenValue = refresh; if (access.isNotEmpty || refresh != null) { try { await _api.refreshAccessToken(); await _saveTokens(); state = state.copyWith(isSignedIn: true); await loadAccount(); } catch (_) { state = state.copyWith(isSignedIn: false); _api.clearTokens(); await _clearTokens(); } } state = state.copyWith(isLoading: false); } Future loadAccount() async { try { final userInfo = await _api.userInfo(); state = state.copyWith(userInfo: userInfo); } catch (e) { state = state.copyWith(errorMessage: e.toString()); } } // ── SMS Login ───────────────────────────────────────────────────── void updatePhoneNumber(String value) { state = state.copyWith(phoneNumber: value); } void updateVerificationCode(String value) { state = state.copyWith(verificationCode: value); } Future sendVerificationCode() async { state = state.copyWith(clearError: true); final cleanPhone = state.phoneNumber.replaceAll(RegExp(r'[^\d]'), ''); if (cleanPhone.length < 8) { state = state.copyWith(errorMessage: '请输入有效的手机号'); return; } try { final initResult = await _api.loginSMSInit(cleanPhone); final captcha = AuthState._findStringDeep(initResult, ['captcha_token', 'captchaToken']); if (captcha == null) { if (AuthState._findStringDeep(initResult, ['url', 'verify_url']) != null) { throw Exception('需要完成验证码验证后再发送短信'); } throw Exception('获取验证码令牌失败'); } state = state.copyWith(captchaToken: captcha); await _api.loginSMSSend(cleanPhone, captchaToken: captcha); _startCountdown(); } catch (e) { state = state.copyWith(errorMessage: e.toString()); } } void _startCountdown() { state = state.copyWith(codeCountdown: 60); _countdownTimer?.cancel(); _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { final next = state.codeCountdown - 1; if (next <= 0) { timer.cancel(); state = state.copyWith(codeCountdown: 0); } else { state = state.copyWith(codeCountdown: next); } }); } Future verifySMSCode() async { state = state.copyWith(clearError: true); try { final verifyResult = await _api.loginSMSVerify( state.verificationID, state.verificationCode); final vToken = AuthState._findStringDeep( verifyResult, ['verification_token', 'verificationToken']); final code = AuthState._findStringDeep(verifyResult, ['code']); if (vToken == null || code == null) throw Exception('验证码验证失败'); await _api.loginSMSSignIn( code: code, verificationToken: vToken, username: state.phoneNumber.replaceAll(RegExp(r'[^\d]'), ''), captchaToken: state.captchaToken, ); state = state.copyWith(isSignedIn: true); await _saveTokens(); await loadAccount(); } catch (e) { state = state.copyWith(errorMessage: e.toString()); } } // ── QR Login ────────────────────────────────────────────────────── Future initQRLogin() async { state = state.copyWith(clearError: true); try { final result = await _api.loginQRInit(); final token = AuthState._findStringDeep( result, ['device_code', 'deviceCode']) ?? ''; final payload = AuthState._findStringDeep( result, ['verification_uri_complete', 'verificationUriComplete', 'qr_url', 'qrUrl', 'url', 'verification_uri', 'verificationUri', 'qrcode', 'qrCode', 'code', 'qr_payload', 'qrPayload']) ?? token; state = state.copyWith( qrPayload: payload, qrToken: token, qrStatus: '请使用光鸭APP扫描二维码', ); _startQRPolling(); } catch (e) { state = state.copyWith(errorMessage: e.toString()); } } void _startQRPolling() { _qrPollingTimer?.cancel(); _qrPollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) async { if (state.qrToken.isEmpty) { timer.cancel(); return; } try { final result = await _api.loginQRPoll(state.qrToken); final accessToken = AuthState._findStringDeep( result, ['access_token', 'accessToken']); if (accessToken != null) { timer.cancel(); state = state.copyWith(isSignedIn: true); await _saveTokens(); await loadAccount(); } else { final error = AuthState._findStringDeep(result, ['error']); String? newStatus; if (error == 'slow_down') { newStatus = '轮询过快,请稍候'; } else if (error == 'authorization_pending') { newStatus = '等待扫码确认…'; } if (newStatus != null) { state = state.copyWith(qrStatus: newStatus); } } } catch (_) { // Continue polling on transient errors } }); } // ── Sign out ────────────────────────────────────────────────────── Future signOut() async { _api.clearTokens(); await _clearTokens(); _qrPollingTimer?.cancel(); _countdownTimer?.cancel(); state = const AuthState(isLoading: false); } // ── Token persistence ───────────────────────────────────────────── Future _saveTokens() async { await StorageManager.set(StorageKeys.accessToken, _api.accessToken); if (_api.refreshTokenValue != null) { await StorageManager.set(StorageKeys.refreshToken, _api.refreshTokenValue!); } if (_api.tokenExpiresAt != null) { await StorageManager.set( StorageKeys.tokenExpiresAt, _api.tokenExpiresAt!.millisecondsSinceEpoch, ); } await StorageManager.set(StorageKeys.deviceID, _api.deviceID); } Future _clearTokens() async { await StorageManager.delete(StorageKeys.accessToken); await StorageManager.delete(StorageKeys.refreshToken); } @override void dispose() { _qrPollingTimer?.cancel(); _countdownTimer?.cancel(); super.dispose(); } } final authProvider = StateNotifierProvider( (ref) => AuthNotifier(), );