| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352 |
- 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<AuthState> {
- AuthRepository get _repo => ref.read(authRepositoryProvider);
- Timer? _countdownTimer;
- @override
- AuthState build() {
- ref.onDispose(() => _countdownTimer?.cancel());
- Future.microtask(_checkToken);
- return const AuthState();
- }
- Future<void> _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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<void> logout() async {
- await _repo.logout();
- state = state.copyWith(isLoggedIn: false);
- }
- /// 仅清除本地登录态,不请求 logout 接口(用于 session 过期场景)
- Future<void> 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<String, dynamic>) {
- 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, AuthState>(
- AuthNotifier.new,
- );
- /// 供 GoRouter 使用的登录状态 Provider
- final isLoggedInProvider = Provider<bool>((ref) {
- return ref.watch(authProvider).isLoggedIn;
- });
|