google_auth_provider.dart 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import 'dart:developer' as developer;
  2. import 'package:dio/dio.dart';
  3. import 'package:flutter_riverpod/flutter_riverpod.dart';
  4. import '../core/network/api_response.dart';
  5. import '../data/services/auth_service.dart';
  6. import '../core/network/dio_client.dart';
  7. // ── State ──────────────────────────────────────────────────
  8. class GoogleAuthState {
  9. /// 秘钥加载中
  10. final bool isSecretLoading;
  11. /// 发送验证码中
  12. final bool isSendingCode;
  13. /// 提交绑定中
  14. final bool isSubmitting;
  15. final String? errorMessage;
  16. final int codeCooldown;
  17. /// TOTP 秘钥
  18. final String secret;
  19. /// otpauth:// URI(用于生成 QR 码)
  20. final String otpauthUrl;
  21. /// 脱敏邮箱(从用户信息获取)
  22. final String maskedEmail;
  23. const GoogleAuthState({
  24. this.isSecretLoading = false,
  25. this.isSendingCode = false,
  26. this.isSubmitting = false,
  27. this.errorMessage,
  28. this.codeCooldown = 0,
  29. this.secret = '',
  30. this.otpauthUrl = '',
  31. this.maskedEmail = '',
  32. });
  33. GoogleAuthState copyWith({
  34. bool? isSecretLoading,
  35. bool? isSendingCode,
  36. bool? isSubmitting,
  37. String? errorMessage,
  38. int? codeCooldown,
  39. String? secret,
  40. String? otpauthUrl,
  41. String? maskedEmail,
  42. }) =>
  43. GoogleAuthState(
  44. isSecretLoading: isSecretLoading ?? this.isSecretLoading,
  45. isSendingCode: isSendingCode ?? this.isSendingCode,
  46. isSubmitting: isSubmitting ?? this.isSubmitting,
  47. errorMessage: errorMessage,
  48. codeCooldown: codeCooldown ?? this.codeCooldown,
  49. secret: secret ?? this.secret,
  50. otpauthUrl: otpauthUrl ?? this.otpauthUrl,
  51. maskedEmail: maskedEmail ?? this.maskedEmail,
  52. );
  53. }
  54. // ── Notifier ───────────────────────────────────────────────
  55. class GoogleAuthNotifier extends Notifier<GoogleAuthState> {
  56. AuthService get _service => AuthService(ref.read(dioClientProvider));
  57. @override
  58. GoogleAuthState build() => const GoogleAuthState();
  59. void clearError() => state = state.copyWith(errorMessage: null);
  60. /// 页面加载时获取秘钥 — GET uc/google/sendgoogle
  61. /// 同时获取用户邮箱用于脱敏显示
  62. Future<void> fetchSecret() async {
  63. state = state.copyWith(isSecretLoading: true, errorMessage: null);
  64. try {
  65. final results = await Future.wait([
  66. _service.getGoogleSecret(),
  67. _service.getMyInfo(),
  68. ]);
  69. final secretData = results[0];
  70. final userInfo = results[1];
  71. final email = userInfo['email']?.toString() ?? '';
  72. state = state.copyWith(
  73. isSecretLoading: false,
  74. secret: secretData['secret']?.toString() ?? '',
  75. otpauthUrl: secretData['link']?.toString() ?? '',
  76. maskedEmail: _maskEmail(email),
  77. );
  78. } catch (e) {
  79. state = state.copyWith(
  80. isSecretLoading: false,
  81. errorMessage: _parseError(e),
  82. );
  83. }
  84. }
  85. /// 发送邮箱验证码 — POST uc/googleAuth/email/code
  86. Future<bool> sendEmailCode() async {
  87. state = state.copyWith(isSendingCode: true, errorMessage: null);
  88. try {
  89. await _service.sendGoogleAuthEmailCode();
  90. state = state.copyWith(isSendingCode: false);
  91. _startCountdown();
  92. return true;
  93. } catch (e) {
  94. state = state.copyWith(
  95. isSendingCode: false,
  96. errorMessage: _parseError(e),
  97. );
  98. return false;
  99. }
  100. }
  101. /// 绑定谷歌验证器 — POST uc/google/googleAuthWithSms
  102. Future<bool> bindGoogleAuth({
  103. required String googleCode,
  104. required String emailCode,
  105. }) async {
  106. state = state.copyWith(isSubmitting: true, errorMessage: null);
  107. try {
  108. await _service.bindGoogleAuth(
  109. codes: googleCode,
  110. vcode: emailCode,
  111. secret: state.secret,
  112. vtype: '2',
  113. );
  114. state = state.copyWith(isSubmitting: false);
  115. return true;
  116. } catch (e) {
  117. state = state.copyWith(
  118. isSubmitting: false,
  119. errorMessage: _parseError(e),
  120. );
  121. return false;
  122. }
  123. }
  124. void _startCountdown() {
  125. state = state.copyWith(codeCooldown: 60);
  126. Future.doWhile(() async {
  127. await Future.delayed(const Duration(seconds: 1));
  128. final remaining = state.codeCooldown - 1;
  129. state = state.copyWith(codeCooldown: remaining);
  130. return remaining > 0;
  131. });
  132. }
  133. /// 邮箱脱敏:user@gmail.com → u**@gmail.com
  134. String _maskEmail(String email) {
  135. if (email.isEmpty) return '';
  136. final parts = email.split('@');
  137. if (parts.length != 2) return email;
  138. final name = parts[0];
  139. if (name.length <= 1) return '$name**@${parts[1]}';
  140. return '${name[0]}**@${parts[1]}';
  141. }
  142. String _parseError(Object e) {
  143. developer.log('GoogleAuth error: $e', name: 'GOOGLE_AUTH', error: e);
  144. if (e is DioException) {
  145. if (e.error is ApiException) {
  146. return (e.error as ApiException).message;
  147. }
  148. final responseData = e.response?.data;
  149. if (responseData is Map<String, dynamic>) {
  150. final msg = responseData['message'] as String? ??
  151. responseData['msg'] as String?;
  152. if (msg != null && msg.isNotEmpty) return msg;
  153. }
  154. return e.message ?? 'errNetworkError';
  155. }
  156. if (e is ApiException) return e.message;
  157. return e.toString();
  158. }
  159. }
  160. // ── Providers ──────────────────────────────────────────────
  161. final googleAuthProvider =
  162. NotifierProvider<GoogleAuthNotifier, GoogleAuthState>(
  163. GoogleAuthNotifier.new,
  164. );