Files
guangya_flutter/lib/providers/auth_provider.dart
T
2026-07-18 10:23:34 +08:00

352 lines
11 KiB
Dart

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<String, dynamic>? 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<String, dynamic>? 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<String, dynamic>? json, List<String> 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<String, dynamic>) {
final found =
_findStringDeep(entry.value as Map<String, dynamic>, keys);
if (found != null) return found;
}
}
return null;
}
static int? _findInt64Deep(
Map<String, dynamic>? json, List<String> 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<AuthState> {
final GuangyaAPI _api = GuangyaAPI(
deviceID: StorageManager.get<String>(StorageKeys.deviceID),
);
Timer? _qrPollingTimer;
Timer? _countdownTimer;
GuangyaAPI get api => _api;
AuthNotifier() : super(const AuthState()) {
_api.accessToken = '';
tryRestoreSession();
}
Future<void> tryRestoreSession() async {
final access = StorageManager.get<String>(StorageKeys.accessToken) ?? '';
final refresh = StorageManager.get<String>(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<void> 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<void> 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<void> 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<void> 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<void> signOut() async {
_api.clearTokens();
await _clearTokens();
_qrPollingTimer?.cancel();
_countdownTimer?.cancel();
state = const AuthState(isLoading: false);
}
// ── Token persistence ─────────────────────────────────────────────
Future<void> _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<void> _clearTokens() async {
await StorageManager.delete(StorageKeys.accessToken);
await StorageManager.delete(StorageKeys.refreshToken);
}
@override
void dispose() {
_qrPollingTimer?.cancel();
_countdownTimer?.cancel();
super.dispose();
}
}
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>(
(ref) => AuthNotifier(),
);