328 lines
11 KiB
Dart
328 lines
11 KiB
Dart
// ============================================================
|
||
// AuthProvider — 认证状态 Riverpod Provider
|
||
//
|
||
// 管理 App 全局登录状态,替代原有的 ValueNotifier<AuthResult?> 模式。
|
||
// 所有页面通过 ref.watch(authStateProvider) 感知认证状态变化。
|
||
// ============================================================
|
||
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import '../../domain/entities/auth_user.dart';
|
||
import '../../domain/entities/auth_session.dart';
|
||
import '../../domain/repositories/auth_repository.dart';
|
||
import '../../data/repositories/auth_repository_impl.dart';
|
||
|
||
// ── Repository Provider ───────────────────────────────────────
|
||
|
||
final authRepositoryProvider = Provider<IAuthRepository>((ref) {
|
||
return AuthRepositoryImpl();
|
||
});
|
||
|
||
// ── Auth State ────────────────────────────────────────────────
|
||
|
||
class AuthState {
|
||
final AuthUser? user;
|
||
final String? accessToken;
|
||
final String? refreshToken;
|
||
final bool isLoading;
|
||
final String? error;
|
||
|
||
const AuthState({
|
||
this.user,
|
||
this.accessToken,
|
||
this.refreshToken,
|
||
this.isLoading = false,
|
||
this.error,
|
||
});
|
||
|
||
bool get isAuthenticated => user != null && accessToken != null;
|
||
|
||
const AuthState.initial() : this(isLoading: false);
|
||
|
||
AuthState copyWith({
|
||
AuthUser? user,
|
||
String? accessToken,
|
||
String? refreshToken,
|
||
bool? isLoading,
|
||
String? error,
|
||
bool clearUser = false,
|
||
bool clearError = false,
|
||
}) {
|
||
return AuthState(
|
||
user: clearUser ? null : (user ?? this.user),
|
||
accessToken: clearUser ? null : (accessToken ?? this.accessToken),
|
||
refreshToken: clearUser ? null : (refreshToken ?? this.refreshToken),
|
||
isLoading: isLoading ?? this.isLoading,
|
||
error: clearError ? null : (error ?? this.error),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── Auth Notifier ─────────────────────────────────────────────
|
||
|
||
class AuthNotifier extends Notifier<AuthState> {
|
||
@override
|
||
AuthState build() => const AuthState.initial();
|
||
|
||
IAuthRepository get _repo => ref.read(authRepositoryProvider);
|
||
|
||
/// 从 AuthSession 更新状态
|
||
void _setSession(AuthSession session) {
|
||
state = AuthState(
|
||
user: session.user,
|
||
accessToken: session.accessToken,
|
||
refreshToken: session.refreshToken,
|
||
);
|
||
}
|
||
|
||
/// Session 过期时由 ApiClient 回调触发(配合 main.dart 中的监听)
|
||
void onSessionExpired() {
|
||
state = const AuthState.initial();
|
||
}
|
||
|
||
// ── 会话恢复 ───────────────────────────────────────────────
|
||
|
||
Future<bool> restoreSession() async {
|
||
state = state.copyWith(isLoading: true, clearError: true);
|
||
try {
|
||
final session = await _repo.restoreSession();
|
||
if (session != null) {
|
||
_setSession(session);
|
||
return true;
|
||
}
|
||
state = state.copyWith(isLoading: false, clearUser: true);
|
||
return false;
|
||
} catch (e) {
|
||
state = state.copyWith(isLoading: false, clearUser: true);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// ── 密码登录 ──────────────────────────────────────────────
|
||
|
||
Future<void> loginByPassword({
|
||
required String identifier,
|
||
required String password,
|
||
String? deviceInfo,
|
||
}) async {
|
||
state = state.copyWith(isLoading: true, clearError: true);
|
||
try {
|
||
final session = await _repo.loginByPassword(
|
||
identifier: identifier,
|
||
password: password,
|
||
deviceInfo: deviceInfo,
|
||
);
|
||
_setSession(session);
|
||
} catch (e) {
|
||
state = state.copyWith(isLoading: false, error: _extractMessage(e));
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
// ── SMS 验证码登录 ────────────────────────────────────────
|
||
|
||
Future<void> loginByPhone({
|
||
required String phone,
|
||
required String smsCode,
|
||
String? deviceInfo,
|
||
}) async {
|
||
state = state.copyWith(isLoading: true, clearError: true);
|
||
try {
|
||
final session = await _repo.loginByPhone(
|
||
phone: phone,
|
||
smsCode: smsCode,
|
||
deviceInfo: deviceInfo,
|
||
);
|
||
_setSession(session);
|
||
} catch (e) {
|
||
state = state.copyWith(isLoading: false, error: _extractMessage(e));
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
// ── 邮件验证码登录 ────────────────────────────────────────
|
||
|
||
Future<void> loginByEmail({required String email, required String emailCode}) async {
|
||
state = state.copyWith(isLoading: true, clearError: true);
|
||
try {
|
||
final session = await _repo.loginByEmail(email: email, emailCode: emailCode);
|
||
_setSession(session);
|
||
} catch (e) {
|
||
state = state.copyWith(isLoading: false, error: _extractMessage(e));
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
// ── 第三方登录 ────────────────────────────────────────────
|
||
|
||
Future<void> loginByWechat({required String code, String? referralCode}) async {
|
||
state = state.copyWith(isLoading: true, clearError: true);
|
||
try {
|
||
final session = await _repo.loginByWechat(code: code, referralCode: referralCode);
|
||
_setSession(session);
|
||
} catch (e) {
|
||
state = state.copyWith(isLoading: false, error: _extractMessage(e));
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
Future<void> loginByAlipay({required String authCode, String? referralCode}) async {
|
||
state = state.copyWith(isLoading: true, clearError: true);
|
||
try {
|
||
final session = await _repo.loginByAlipay(authCode: authCode, referralCode: referralCode);
|
||
_setSession(session);
|
||
} catch (e) {
|
||
state = state.copyWith(isLoading: false, error: _extractMessage(e));
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
Future<void> loginByGoogle({required String idToken, String? referralCode}) async {
|
||
state = state.copyWith(isLoading: true, clearError: true);
|
||
try {
|
||
final session = await _repo.loginByGoogle(idToken: idToken, referralCode: referralCode);
|
||
_setSession(session);
|
||
} catch (e) {
|
||
state = state.copyWith(isLoading: false, error: _extractMessage(e));
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
Future<void> loginByApple({
|
||
required String identityToken,
|
||
String? displayName,
|
||
String? referralCode,
|
||
}) async {
|
||
state = state.copyWith(isLoading: true, clearError: true);
|
||
try {
|
||
final session = await _repo.loginByApple(
|
||
identityToken: identityToken,
|
||
displayName: displayName,
|
||
referralCode: referralCode,
|
||
);
|
||
_setSession(session);
|
||
} catch (e) {
|
||
state = state.copyWith(isLoading: false, error: _extractMessage(e));
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
// ── 注册 ──────────────────────────────────────────────────
|
||
|
||
Future<void> register({
|
||
required String phone,
|
||
required String smsCode,
|
||
required String password,
|
||
String? nickname,
|
||
String? referralCode,
|
||
}) async {
|
||
state = state.copyWith(isLoading: true, clearError: true);
|
||
try {
|
||
final session = await _repo.register(
|
||
phone: phone,
|
||
smsCode: smsCode,
|
||
password: password,
|
||
nickname: nickname,
|
||
referralCode: referralCode,
|
||
);
|
||
_setSession(session);
|
||
} catch (e) {
|
||
state = state.copyWith(isLoading: false, error: _extractMessage(e));
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
Future<void> registerByEmail({
|
||
required String email,
|
||
required String emailCode,
|
||
required String password,
|
||
String? nickname,
|
||
String? referralCode,
|
||
}) async {
|
||
state = state.copyWith(isLoading: true, clearError: true);
|
||
try {
|
||
final session = await _repo.registerByEmail(
|
||
email: email,
|
||
emailCode: emailCode,
|
||
password: password,
|
||
nickname: nickname,
|
||
referralCode: referralCode,
|
||
);
|
||
_setSession(session);
|
||
} catch (e) {
|
||
state = state.copyWith(isLoading: false, error: _extractMessage(e));
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
// ── 登出 ──────────────────────────────────────────────────
|
||
|
||
Future<void> logout() async {
|
||
await _repo.logout();
|
||
state = const AuthState.initial();
|
||
}
|
||
|
||
// ── 验证码 ────────────────────────────────────────────────
|
||
|
||
Future<int> sendSmsCode(String phone, SmsCodeType type) =>
|
||
_repo.sendSmsCode(phone, type);
|
||
|
||
Future<int> sendEmailCode(String email, EmailCodeType type) =>
|
||
_repo.sendEmailCode(email, type);
|
||
|
||
// ── 推荐码验证 ────────────────────────────────────────────
|
||
|
||
Future<bool> validateReferralCode(String code) =>
|
||
_repo.validateReferralCode(code);
|
||
|
||
// ── 密码管理 ──────────────────────────────────────────────
|
||
|
||
Future<void> resetPassword({
|
||
required String phone,
|
||
required String smsCode,
|
||
required String newPassword,
|
||
}) =>
|
||
_repo.resetPassword(phone: phone, smsCode: smsCode, newPassword: newPassword);
|
||
|
||
Future<void> resetPasswordByEmail({
|
||
required String email,
|
||
required String emailCode,
|
||
required String newPassword,
|
||
}) =>
|
||
_repo.resetPasswordByEmail(
|
||
email: email,
|
||
emailCode: emailCode,
|
||
newPassword: newPassword,
|
||
);
|
||
|
||
Future<void> changePassword({
|
||
required String oldPassword,
|
||
required String newPassword,
|
||
}) =>
|
||
_repo.changePassword(oldPassword: oldPassword, newPassword: newPassword);
|
||
|
||
Future<String> getAlipayAuthString() => _repo.getAlipayAuthString();
|
||
|
||
// ── 私有工具 ──────────────────────────────────────────────
|
||
|
||
String _extractMessage(Object e) {
|
||
final str = e.toString();
|
||
// DioException 格式: "DioException [...]: message"
|
||
final match = RegExp(r'message["\s:]+([^"}\n]+)').firstMatch(str);
|
||
return match?.group(1)?.trim() ?? str;
|
||
}
|
||
}
|
||
|
||
// ── Provider 导出 ─────────────────────────────────────────────
|
||
|
||
final authProvider = NotifierProvider<AuthNotifier, AuthState>(AuthNotifier.new);
|
||
|
||
/// 便捷选择器:当前登录用户(null = 未登录)
|
||
final currentUserProvider = Provider<AuthUser?>((ref) {
|
||
return ref.watch(authProvider).user;
|
||
});
|
||
|
||
/// 便捷选择器:是否已登录
|
||
final isAuthenticatedProvider = Provider<bool>((ref) {
|
||
return ref.watch(authProvider).isAuthenticated;
|
||
});
|