auth_provider.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. import 'dart:async';
  2. import 'dart:developer' as developer;
  3. import 'package:dio/dio.dart';
  4. import 'package:flutter_riverpod/flutter_riverpod.dart';
  5. import '../core/network/api_response.dart';
  6. import '../data/repositories/auth_repository.dart';
  7. export '../data/repositories/auth_repository.dart'
  8. show CheckAccountForRegisterResult, LoginResult;
  9. // ── Enums ──────────────────────────────────────────────────
  10. enum AuthInputMethod { email, phone }
  11. enum LoginMode { password, code }
  12. // ── State ──────────────────────────────────────────────────
  13. class AuthState {
  14. final AuthInputMethod inputMethod;
  15. final LoginMode loginMode;
  16. final bool isLoading;
  17. final String? errorMessage;
  18. final int codeCooldown;
  19. final bool isLoggedIn;
  20. /// 注册流程暂存
  21. final String pendingEmail;
  22. final String pendingPassword;
  23. final String pendingInviteCode;
  24. /// 忘记密码流程暂存
  25. final bool resetMode;
  26. final String resetEmail;
  27. final String resetCode;
  28. const AuthState({
  29. this.inputMethod = AuthInputMethod.email,
  30. this.loginMode = LoginMode.password,
  31. this.isLoading = false,
  32. this.errorMessage,
  33. this.codeCooldown = 0,
  34. this.isLoggedIn = false,
  35. this.pendingEmail = '',
  36. this.pendingPassword = '',
  37. this.pendingInviteCode = '',
  38. this.resetMode = false,
  39. this.resetEmail = '',
  40. this.resetCode = '',
  41. });
  42. AuthState copyWith({
  43. AuthInputMethod? inputMethod,
  44. LoginMode? loginMode,
  45. bool? isLoading,
  46. String? errorMessage,
  47. int? codeCooldown,
  48. bool? isLoggedIn,
  49. String? pendingEmail,
  50. String? pendingPassword,
  51. String? pendingInviteCode,
  52. bool? resetMode,
  53. String? resetEmail,
  54. String? resetCode,
  55. }) =>
  56. AuthState(
  57. inputMethod: inputMethod ?? this.inputMethod,
  58. loginMode: loginMode ?? this.loginMode,
  59. isLoading: isLoading ?? this.isLoading,
  60. errorMessage: errorMessage,
  61. codeCooldown: codeCooldown ?? this.codeCooldown,
  62. isLoggedIn: isLoggedIn ?? this.isLoggedIn,
  63. pendingEmail: pendingEmail ?? this.pendingEmail,
  64. pendingPassword: pendingPassword ?? this.pendingPassword,
  65. pendingInviteCode: pendingInviteCode ?? this.pendingInviteCode,
  66. resetMode: resetMode ?? this.resetMode,
  67. resetEmail: resetEmail ?? this.resetEmail,
  68. resetCode: resetCode ?? this.resetCode,
  69. );
  70. }
  71. // ── Notifier ───────────────────────────────────────────────
  72. class AuthNotifier extends Notifier<AuthState> {
  73. AuthRepository get _repo => ref.read(authRepositoryProvider);
  74. Timer? _countdownTimer;
  75. @override
  76. AuthState build() {
  77. ref.onDispose(() => _countdownTimer?.cancel());
  78. Future.microtask(_checkToken);
  79. return const AuthState();
  80. }
  81. Future<void> _checkToken() async {
  82. final loggedIn = await _repo.isLoggedIn;
  83. state = state.copyWith(isLoggedIn: loggedIn);
  84. }
  85. void setInputMethod(AuthInputMethod method) =>
  86. state = state.copyWith(inputMethod: method);
  87. void setLoginMode(LoginMode mode) =>
  88. state = state.copyWith(loginMode: mode);
  89. void clearError() => state = state.copyWith(errorMessage: null);
  90. // ── 发送验证码(带60s倒计时)──────────────────────────────
  91. /// 发送登录验证码(先发 `/uc/check-password` 再发码,与 Web 一致)
  92. Future<bool> sendLoginCode({
  93. required String email,
  94. required String password,
  95. }) async {
  96. state = state.copyWith(isLoading: true, errorMessage: null);
  97. try {
  98. await _repo.checkLoginPassword(email: email, password: password);
  99. await _repo.sendLoginCode(email);
  100. state = state.copyWith(isLoading: false);
  101. startCountdown();
  102. return true;
  103. } catch (e) {
  104. state = state.copyWith(
  105. isLoading: false,
  106. errorMessage: _loginCredentialCheckError(e),
  107. );
  108. return false;
  109. }
  110. }
  111. /// 发送注册验证码(先 `/uc/check-account`:已注册则拦截,与 Web 一致)
  112. Future<bool> sendRegisterCode({required String email}) async {
  113. state = state.copyWith(isLoading: true, errorMessage: null);
  114. try {
  115. final check = await _repo.checkAccountForRegister(email);
  116. if (check == CheckAccountForRegisterResult.alreadyRegistered) {
  117. state = state.copyWith(
  118. isLoading: false,
  119. errorMessage: 'errAccountAlreadyRegistered',
  120. );
  121. return false;
  122. }
  123. await _repo.sendRegisterCode(email);
  124. state = state.copyWith(isLoading: false);
  125. startCountdown();
  126. return true;
  127. } catch (e) {
  128. state = state.copyWith(
  129. isLoading: false,
  130. errorMessage: _parseError(e),
  131. );
  132. return false;
  133. }
  134. }
  135. /// 发送重置密码验证码
  136. Future<bool> sendResetCode({required String email}) async {
  137. state = state.copyWith(isLoading: true, errorMessage: null);
  138. try {
  139. await _repo.sendResetCode(email);
  140. state = state.copyWith(isLoading: false);
  141. startCountdown();
  142. return true;
  143. } catch (e) {
  144. state = state.copyWith(
  145. isLoading: false,
  146. errorMessage: _parseError(e),
  147. );
  148. return false;
  149. }
  150. }
  151. void startCountdown() {
  152. _countdownTimer?.cancel();
  153. state = state.copyWith(codeCooldown: 60);
  154. _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
  155. final remaining = state.codeCooldown - 1;
  156. state = state.copyWith(codeCooldown: remaining);
  157. if (remaining <= 0) {
  158. timer.cancel();
  159. _countdownTimer = null;
  160. }
  161. });
  162. }
  163. // ── 登录(密码+验证码)──────────────────────────────────
  164. /// [vtype] 2=邮箱验证码, 3=谷歌验证码
  165. Future<bool> login({
  166. required String email,
  167. required String password,
  168. required String code,
  169. String vtype = '2',
  170. }) async {
  171. state = state.copyWith(isLoading: true, errorMessage: null);
  172. try {
  173. await _repo.loginWithPassword(
  174. email: email,
  175. password: password,
  176. code: code,
  177. vtype: vtype,
  178. );
  179. state = state.copyWith(isLoading: false, isLoggedIn: true);
  180. return true;
  181. } catch (e) {
  182. state = state.copyWith(
  183. isLoading: false,
  184. errorMessage: _parseError(e),
  185. );
  186. return false;
  187. }
  188. }
  189. // ── 注册流程 ──────────────────────────────────────────────
  190. /// 暂存注册信息(步骤一)
  191. void setPendingRegister({
  192. required String email,
  193. required String password,
  194. String inviteCode = '',
  195. }) {
  196. state = state.copyWith(
  197. pendingEmail: email,
  198. pendingPassword: password,
  199. pendingInviteCode: inviteCode,
  200. );
  201. }
  202. /// 提交注册(步骤二:填完验证码后调用)
  203. Future<bool> register({required String code}) async {
  204. state = state.copyWith(isLoading: true, errorMessage: null);
  205. try {
  206. await _repo.registerWithEmail(
  207. email: state.pendingEmail,
  208. password: state.pendingPassword,
  209. code: code,
  210. inviteCode: state.pendingInviteCode.isEmpty
  211. ? null
  212. : state.pendingInviteCode,
  213. );
  214. state = state.copyWith(isLoading: false);
  215. return true;
  216. } catch (e) {
  217. state = state.copyWith(
  218. isLoading: false,
  219. errorMessage: _parseError(e),
  220. );
  221. return false;
  222. }
  223. }
  224. // ── 忘记密码流程 ──────────────────────────────────────────
  225. /// 暂存重置密码信息
  226. void setPendingReset({
  227. required String email,
  228. required String code,
  229. }) {
  230. state = state.copyWith(
  231. resetMode: true,
  232. resetEmail: email,
  233. resetCode: code,
  234. );
  235. }
  236. /// 重置密码
  237. Future<bool> resetPassword({required String password}) async {
  238. state = state.copyWith(isLoading: true, errorMessage: null);
  239. try {
  240. await _repo.resetPassword(
  241. email: state.resetEmail,
  242. password: password,
  243. code: state.resetCode,
  244. );
  245. state = state.copyWith(
  246. isLoading: false,
  247. resetMode: false,
  248. resetEmail: '',
  249. resetCode: '',
  250. );
  251. return true;
  252. } catch (e) {
  253. state = state.copyWith(
  254. isLoading: false,
  255. errorMessage: _parseError(e),
  256. );
  257. return false;
  258. }
  259. }
  260. // ── 退出登录 ───────────────────────────────────────────────
  261. Future<void> logout() async {
  262. await _repo.logout();
  263. state = state.copyWith(isLoggedIn: false);
  264. }
  265. /// 仅清除本地登录态,不请求 logout 接口(用于 session 过期场景)
  266. Future<void> clearLocalSession() async {
  267. await _repo.clearSession();
  268. state = state.copyWith(isLoggedIn: false);
  269. }
  270. /// 与 Web 一致:校验失败时统一提示,不区分账号或密码
  271. String _loginCredentialCheckError(Object e) {
  272. if (e is DioException && e.error is ApiException) {
  273. return 'errLoginCredentialWrong';
  274. }
  275. if (e is ApiException) {
  276. return 'errLoginCredentialWrong';
  277. }
  278. return _parseError(e);
  279. }
  280. String _parseError(Object e) {
  281. developer.log('Auth error: $e', name: 'AUTH', error: e);
  282. // DioException 包裹的 ApiException
  283. if (e is DioException) {
  284. final responseData = e.response?.data;
  285. developer.log('Response data: $responseData', name: 'AUTH');
  286. if (e.error is ApiException) {
  287. return (e.error as ApiException).message;
  288. }
  289. // 直接从 response body 取 message/msg
  290. if (responseData is Map<String, dynamic>) {
  291. final msg = responseData['message'] as String?
  292. ?? responseData['msg'] as String?;
  293. if (msg != null && msg.isNotEmpty) return msg;
  294. }
  295. return e.message ?? 'errNetworkError';
  296. }
  297. if (e is ApiException) return e.message;
  298. return e.toString();
  299. }
  300. }
  301. // ── Providers ──────────────────────────────────────────────
  302. final authProvider = NotifierProvider<AuthNotifier, AuthState>(
  303. AuthNotifier.new,
  304. );
  305. /// 供 GoRouter 使用的登录状态 Provider
  306. final isLoggedInProvider = Provider<bool>((ref) {
  307. return ref.watch(authProvider).isLoggedIn;
  308. });