import 'dart:async'; import 'dart:developer' as developer; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core/network/api_response.dart'; import '../data/repositories/auth_repository.dart'; export '../data/repositories/auth_repository.dart' show CheckAccountForRegisterResult, LoginResult; // ── Enums ────────────────────────────────────────────────── enum AuthInputMethod { email, phone } enum LoginMode { password, code } // ── State ────────────────────────────────────────────────── class AuthState { final AuthInputMethod inputMethod; final LoginMode loginMode; final bool isLoading; final String? errorMessage; final int codeCooldown; final bool isLoggedIn; /// 注册流程暂存 final String pendingEmail; final String pendingPassword; final String pendingInviteCode; /// 忘记密码流程暂存 final bool resetMode; final String resetEmail; final String resetCode; const AuthState({ this.inputMethod = AuthInputMethod.email, this.loginMode = LoginMode.password, this.isLoading = false, this.errorMessage, this.codeCooldown = 0, this.isLoggedIn = false, this.pendingEmail = '', this.pendingPassword = '', this.pendingInviteCode = '', this.resetMode = false, this.resetEmail = '', this.resetCode = '', }); AuthState copyWith({ AuthInputMethod? inputMethod, LoginMode? loginMode, bool? isLoading, String? errorMessage, int? codeCooldown, bool? isLoggedIn, String? pendingEmail, String? pendingPassword, String? pendingInviteCode, bool? resetMode, String? resetEmail, String? resetCode, }) => AuthState( inputMethod: inputMethod ?? this.inputMethod, loginMode: loginMode ?? this.loginMode, isLoading: isLoading ?? this.isLoading, errorMessage: errorMessage, codeCooldown: codeCooldown ?? this.codeCooldown, isLoggedIn: isLoggedIn ?? this.isLoggedIn, pendingEmail: pendingEmail ?? this.pendingEmail, pendingPassword: pendingPassword ?? this.pendingPassword, pendingInviteCode: pendingInviteCode ?? this.pendingInviteCode, resetMode: resetMode ?? this.resetMode, resetEmail: resetEmail ?? this.resetEmail, resetCode: resetCode ?? this.resetCode, ); } // ── Notifier ─────────────────────────────────────────────── class AuthNotifier extends Notifier { AuthRepository get _repo => ref.read(authRepositoryProvider); Timer? _countdownTimer; @override AuthState build() { ref.onDispose(() => _countdownTimer?.cancel()); Future.microtask(_checkToken); return const AuthState(); } Future _checkToken() async { final loggedIn = await _repo.isLoggedIn; state = state.copyWith(isLoggedIn: loggedIn); } void setInputMethod(AuthInputMethod method) => state = state.copyWith(inputMethod: method); void setLoginMode(LoginMode mode) => state = state.copyWith(loginMode: mode); void clearError() => state = state.copyWith(errorMessage: null); // ── 发送验证码(带60s倒计时)────────────────────────────── /// 发送登录验证码(先发 `/uc/check-password` 再发码,与 Web 一致) Future sendLoginCode({ required String email, required String password, }) async { state = state.copyWith(isLoading: true, errorMessage: null); try { await _repo.checkLoginPassword(email: email, password: password); await _repo.sendLoginCode(email); state = state.copyWith(isLoading: false); startCountdown(); return true; } catch (e) { state = state.copyWith( isLoading: false, errorMessage: _loginCredentialCheckError(e), ); return false; } } /// 发送注册验证码(先 `/uc/check-account`:已注册则拦截,与 Web 一致) Future sendRegisterCode({required String email}) async { state = state.copyWith(isLoading: true, errorMessage: null); try { final check = await _repo.checkAccountForRegister(email); if (check == CheckAccountForRegisterResult.alreadyRegistered) { state = state.copyWith( isLoading: false, errorMessage: 'errAccountAlreadyRegistered', ); return false; } await _repo.sendRegisterCode(email); state = state.copyWith(isLoading: false); startCountdown(); return true; } catch (e) { state = state.copyWith( isLoading: false, errorMessage: _parseError(e), ); return false; } } /// 发送重置密码验证码 Future sendResetCode({required String email}) async { state = state.copyWith(isLoading: true, errorMessage: null); try { await _repo.sendResetCode(email); state = state.copyWith(isLoading: false); startCountdown(); return true; } catch (e) { state = state.copyWith( isLoading: false, errorMessage: _parseError(e), ); return false; } } void startCountdown() { _countdownTimer?.cancel(); state = state.copyWith(codeCooldown: 60); _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { final remaining = state.codeCooldown - 1; state = state.copyWith(codeCooldown: remaining); if (remaining <= 0) { timer.cancel(); _countdownTimer = null; } }); } // ── 登录(密码+验证码)────────────────────────────────── /// [vtype] 2=邮箱验证码, 3=谷歌验证码 Future login({ required String email, required String password, required String code, String vtype = '2', }) async { state = state.copyWith(isLoading: true, errorMessage: null); try { await _repo.loginWithPassword( email: email, password: password, code: code, vtype: vtype, ); state = state.copyWith(isLoading: false, isLoggedIn: true); return true; } catch (e) { state = state.copyWith( isLoading: false, errorMessage: _parseError(e), ); return false; } } // ── 注册流程 ────────────────────────────────────────────── /// 暂存注册信息(步骤一) void setPendingRegister({ required String email, required String password, String inviteCode = '', }) { state = state.copyWith( pendingEmail: email, pendingPassword: password, pendingInviteCode: inviteCode, ); } /// 提交注册(步骤二:填完验证码后调用) Future register({required String code}) async { state = state.copyWith(isLoading: true, errorMessage: null); try { await _repo.registerWithEmail( email: state.pendingEmail, password: state.pendingPassword, code: code, inviteCode: state.pendingInviteCode.isEmpty ? null : state.pendingInviteCode, ); state = state.copyWith(isLoading: false); return true; } catch (e) { state = state.copyWith( isLoading: false, errorMessage: _parseError(e), ); return false; } } // ── 忘记密码流程 ────────────────────────────────────────── /// 暂存重置密码信息 void setPendingReset({ required String email, required String code, }) { state = state.copyWith( resetMode: true, resetEmail: email, resetCode: code, ); } /// 重置密码 Future resetPassword({required String password}) async { state = state.copyWith(isLoading: true, errorMessage: null); try { await _repo.resetPassword( email: state.resetEmail, password: password, code: state.resetCode, ); state = state.copyWith( isLoading: false, resetMode: false, resetEmail: '', resetCode: '', ); return true; } catch (e) { state = state.copyWith( isLoading: false, errorMessage: _parseError(e), ); return false; } } // ── 退出登录 ─────────────────────────────────────────────── Future logout() async { await _repo.logout(); state = state.copyWith(isLoggedIn: false); } /// 仅清除本地登录态,不请求 logout 接口(用于 session 过期场景) Future clearLocalSession() async { await _repo.clearSession(); state = state.copyWith(isLoggedIn: false); } /// 与 Web 一致:校验失败时统一提示,不区分账号或密码 String _loginCredentialCheckError(Object e) { if (e is DioException && e.error is ApiException) { return 'errLoginCredentialWrong'; } if (e is ApiException) { return 'errLoginCredentialWrong'; } return _parseError(e); } String _parseError(Object e) { developer.log('Auth error: $e', name: 'AUTH', error: e); // DioException 包裹的 ApiException if (e is DioException) { final responseData = e.response?.data; developer.log('Response data: $responseData', name: 'AUTH'); if (e.error is ApiException) { return (e.error as ApiException).message; } // 直接从 response body 取 message/msg if (responseData is Map) { final msg = responseData['message'] as String? ?? responseData['msg'] as String?; if (msg != null && msg.isNotEmpty) return msg; } return e.message ?? 'errNetworkError'; } if (e is ApiException) return e.message; return e.toString(); } } // ── Providers ────────────────────────────────────────────── final authProvider = NotifierProvider( AuthNotifier.new, ); /// 供 GoRouter 使用的登录状态 Provider final isLoggedInProvider = Provider((ref) { return ref.watch(authProvider).isLoggedIn; });