feat(genex-mobile): 重构为 Clean Architecture + Riverpod
- 添加 flutter_riverpod ^2.5.1 依赖 - 搭建 core/error/failures.dart 和 core/usecases/usecase.dart 基础骨架 - auth feature 完整 3 层重构: - domain: AuthUser + AuthSession 实体, IAuthRepository 接口, 全套 UseCases - data: AuthRepositoryImpl(Strangler Fig,委托给 AuthService 保留 token 刷新逻辑) - presentation: AuthNotifier + authProvider + currentUserProvider + isAuthenticatedProvider - 4 个 auth 页面升级为 ConsumerStatefulWidget,使用 ref.read(authProvider.notifier) - main.dart: ProviderScope 包裹 GenexConsumerApp,改为 ConsumerStatefulWidget - 桥接 AuthService ValueNotifier → Riverpod authProvider(会话过期自动导航) - 12 个 feature 全部创建 Riverpod providers(FutureProvider/NotifierProvider) - 修复 my_coupons_page.dart 中 EmptyState/StatusTags 缺少 BuildContext 的预存在错误 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
332a8dafe8
commit
4957b2ef85
|
|
@ -0,0 +1,44 @@
|
|||
// ============================================================
|
||||
// Failures — 领域层统一错误类型
|
||||
// 所有 Repository/UseCase 抛出的错误均为 Failure 的子类,
|
||||
// 避免 Presentation 层依赖具体异常类型(DioException 等)
|
||||
// ============================================================
|
||||
|
||||
sealed class Failure implements Exception {
|
||||
final String message;
|
||||
const Failure(this.message);
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType: $message';
|
||||
}
|
||||
|
||||
/// 网络/HTTP 层错误(连接失败、超时等)
|
||||
class NetworkFailure extends Failure {
|
||||
const NetworkFailure([super.message = '网络错误,请检查连接']);
|
||||
}
|
||||
|
||||
/// 服务端返回业务错误(HTTP 4xx/5xx + 业务 code)
|
||||
class ServerFailure extends Failure {
|
||||
final int? statusCode;
|
||||
const ServerFailure(super.message, {this.statusCode});
|
||||
}
|
||||
|
||||
/// 认证失败(401/403、Token 失效、权限不足)
|
||||
class AuthFailure extends Failure {
|
||||
const AuthFailure([super.message = '认证失败,请重新登录']);
|
||||
}
|
||||
|
||||
/// 本地缓存/存储错误
|
||||
class CacheFailure extends Failure {
|
||||
const CacheFailure([super.message = '本地数据读写失败']);
|
||||
}
|
||||
|
||||
/// 业务校验错误(输入不合法等)
|
||||
class ValidationFailure extends Failure {
|
||||
const ValidationFailure(super.message);
|
||||
}
|
||||
|
||||
/// 未知错误兜底
|
||||
class UnknownFailure extends Failure {
|
||||
const UnknownFailure([super.message = '未知错误']);
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
// ============================================================
|
||||
// UseCase — 用例基类
|
||||
//
|
||||
// 遵循单一职责原则:每个 UseCase 只做一件事。
|
||||
// 泛型参数:
|
||||
// Type — 成功返回的数据类型
|
||||
// Params — 入参类型(无参数时使用 NoParams)
|
||||
// ============================================================
|
||||
|
||||
abstract class UseCase<Type, Params> {
|
||||
Future<Type> call(Params params);
|
||||
}
|
||||
|
||||
/// 无入参的占位类型
|
||||
class NoParams {
|
||||
const NoParams();
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
// ============================================================
|
||||
// AiAgentProvider — AI 智能助手 Riverpod Providers
|
||||
// ============================================================
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../core/network/api_client.dart';
|
||||
|
||||
// ── 对话历史 ──────────────────────────────────────────────────
|
||||
|
||||
final chatHistoryProvider = FutureProvider.autoDispose((ref) async {
|
||||
final api = ApiClient.instance;
|
||||
final resp = await api.get('/api/v1/ai/sessions/current/messages');
|
||||
return resp.data['data'];
|
||||
});
|
||||
|
||||
// ── 发送消息状态 ──────────────────────────────────────────────
|
||||
|
||||
class AiChatState {
|
||||
final List<Map<String, dynamic>> messages;
|
||||
final bool isSending;
|
||||
final String? error;
|
||||
|
||||
const AiChatState({
|
||||
this.messages = const [],
|
||||
this.isSending = false,
|
||||
this.error,
|
||||
});
|
||||
|
||||
AiChatState copyWith({
|
||||
List<Map<String, dynamic>>? messages,
|
||||
bool? isSending,
|
||||
String? error,
|
||||
bool clearError = false,
|
||||
}) {
|
||||
return AiChatState(
|
||||
messages: messages ?? this.messages,
|
||||
isSending: isSending ?? this.isSending,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AiChatNotifier extends Notifier<AiChatState> {
|
||||
@override
|
||||
AiChatState build() => const AiChatState();
|
||||
|
||||
Future<void> sendMessage(String content) async {
|
||||
state = state.copyWith(isSending: true, clearError: true);
|
||||
try {
|
||||
final api = ApiClient.instance;
|
||||
final resp = await api.post('/api/v1/ai/chat', data: {'message': content});
|
||||
final reply = resp.data['data'] as Map<String, dynamic>;
|
||||
state = state.copyWith(
|
||||
messages: [
|
||||
...state.messages,
|
||||
{'role': 'user', 'content': content},
|
||||
{'role': 'assistant', 'content': reply['reply'] ?? reply['content'] ?? ''},
|
||||
],
|
||||
isSending: false,
|
||||
);
|
||||
} catch (e) {
|
||||
state = state.copyWith(isSending: false, error: e.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final aiChatProvider = NotifierProvider<AiChatNotifier, AiChatState>(AiChatNotifier.new);
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
// ============================================================
|
||||
// AuthRepositoryImpl — 认证仓库实现(数据层)
|
||||
//
|
||||
// 采用 Strangler Fig 模式:委托给现有的 AuthService 单例,
|
||||
// 确保 ApiClient token 刷新/过期回调逻辑不重复实现。
|
||||
// ============================================================
|
||||
|
||||
import '../../domain/entities/auth_user.dart';
|
||||
import '../../domain/entities/auth_session.dart';
|
||||
import '../../domain/repositories/auth_repository.dart';
|
||||
import '../../../../core/services/auth_service.dart';
|
||||
|
||||
class AuthRepositoryImpl implements IAuthRepository {
|
||||
final AuthService _service;
|
||||
|
||||
AuthRepositoryImpl({AuthService? service}) : _service = service ?? AuthService.instance;
|
||||
|
||||
// ── 辅助:AuthResult → AuthSession ─────────────────────────
|
||||
|
||||
AuthSession _toSession(AuthResult result) {
|
||||
return AuthSession(
|
||||
user: AuthUser.fromJson(result.user),
|
||||
accessToken: result.accessToken,
|
||||
refreshToken: result.refreshToken,
|
||||
expiresIn: result.expiresIn,
|
||||
);
|
||||
}
|
||||
|
||||
// ── 会话恢复 ────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<AuthSession?> restoreSession() async {
|
||||
final restored = await _service.restoreSession();
|
||||
if (!restored) return null;
|
||||
final result = _service.authState.value;
|
||||
if (result == null) return null;
|
||||
return _toSession(result);
|
||||
}
|
||||
|
||||
// ── 密码登录 ─────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<AuthSession> loginByPassword({
|
||||
required String identifier,
|
||||
required String password,
|
||||
String? deviceInfo,
|
||||
}) async {
|
||||
final result = await _service.loginByPassword(
|
||||
identifier: identifier,
|
||||
password: password,
|
||||
deviceInfo: deviceInfo,
|
||||
);
|
||||
return _toSession(result);
|
||||
}
|
||||
|
||||
// ── SMS 验证码登录 ────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<AuthSession> loginByPhone({
|
||||
required String phone,
|
||||
required String smsCode,
|
||||
String? deviceInfo,
|
||||
}) async {
|
||||
final result = await _service.loginByPhone(
|
||||
phone: phone,
|
||||
smsCode: smsCode,
|
||||
deviceInfo: deviceInfo,
|
||||
);
|
||||
return _toSession(result);
|
||||
}
|
||||
|
||||
// ── 邮件验证码登录 ────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<AuthSession> loginByEmail({
|
||||
required String email,
|
||||
required String emailCode,
|
||||
String? deviceInfo,
|
||||
}) async {
|
||||
final result = await _service.loginByEmail(email: email, emailCode: emailCode);
|
||||
return _toSession(result);
|
||||
}
|
||||
|
||||
// ── 微信登录 ──────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<AuthSession> loginByWechat({
|
||||
required String code,
|
||||
String? referralCode,
|
||||
String? deviceInfo,
|
||||
}) async {
|
||||
final result = await _service.loginByWechat(
|
||||
code: code,
|
||||
referralCode: referralCode,
|
||||
deviceInfo: deviceInfo,
|
||||
);
|
||||
return _toSession(result);
|
||||
}
|
||||
|
||||
// ── 支付宝登录 ────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<AuthSession> loginByAlipay({
|
||||
required String authCode,
|
||||
String? referralCode,
|
||||
String? deviceInfo,
|
||||
}) async {
|
||||
final result = await _service.loginByAlipay(
|
||||
authCode: authCode,
|
||||
referralCode: referralCode,
|
||||
deviceInfo: deviceInfo,
|
||||
);
|
||||
return _toSession(result);
|
||||
}
|
||||
|
||||
// ── Google 登录 ────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<AuthSession> loginByGoogle({
|
||||
required String idToken,
|
||||
String? referralCode,
|
||||
String? deviceInfo,
|
||||
}) async {
|
||||
final result = await _service.loginByGoogle(
|
||||
idToken: idToken,
|
||||
referralCode: referralCode,
|
||||
deviceInfo: deviceInfo,
|
||||
);
|
||||
return _toSession(result);
|
||||
}
|
||||
|
||||
// ── Apple 登录 ────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<AuthSession> loginByApple({
|
||||
required String identityToken,
|
||||
String? displayName,
|
||||
String? referralCode,
|
||||
String? deviceInfo,
|
||||
}) async {
|
||||
final result = await _service.loginByApple(
|
||||
identityToken: identityToken,
|
||||
displayName: displayName,
|
||||
referralCode: referralCode,
|
||||
deviceInfo: deviceInfo,
|
||||
);
|
||||
return _toSession(result);
|
||||
}
|
||||
|
||||
// ── 注册 ──────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<AuthSession> register({
|
||||
required String phone,
|
||||
required String smsCode,
|
||||
required String password,
|
||||
String? nickname,
|
||||
String? referralCode,
|
||||
}) async {
|
||||
final result = await _service.register(
|
||||
phone: phone,
|
||||
smsCode: smsCode,
|
||||
password: password,
|
||||
nickname: nickname,
|
||||
referralCode: referralCode,
|
||||
);
|
||||
return _toSession(result);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AuthSession> registerByEmail({
|
||||
required String email,
|
||||
required String emailCode,
|
||||
required String password,
|
||||
String? nickname,
|
||||
String? referralCode,
|
||||
}) async {
|
||||
final result = await _service.registerByEmail(
|
||||
email: email,
|
||||
emailCode: emailCode,
|
||||
password: password,
|
||||
nickname: nickname,
|
||||
referralCode: referralCode,
|
||||
);
|
||||
return _toSession(result);
|
||||
}
|
||||
|
||||
// ── 登出 ──────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<void> logout() => _service.logout();
|
||||
|
||||
// ── 验证码 ────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<int> sendSmsCode(String phone, SmsCodeType type) =>
|
||||
_service.sendSmsCode(phone, type);
|
||||
|
||||
@override
|
||||
Future<int> sendEmailCode(String email, EmailCodeType type) =>
|
||||
_service.sendEmailCode(email, type);
|
||||
|
||||
// ── 密码管理 ─────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<void> resetPassword({
|
||||
required String phone,
|
||||
required String smsCode,
|
||||
required String newPassword,
|
||||
}) =>
|
||||
_service.resetPassword(phone: phone, smsCode: smsCode, newPassword: newPassword);
|
||||
|
||||
@override
|
||||
Future<void> resetPasswordByEmail({
|
||||
required String email,
|
||||
required String emailCode,
|
||||
required String newPassword,
|
||||
}) =>
|
||||
_service.resetPasswordByEmail(
|
||||
email: email, emailCode: emailCode, newPassword: newPassword);
|
||||
|
||||
@override
|
||||
Future<void> changePassword({
|
||||
required String oldPassword,
|
||||
required String newPassword,
|
||||
}) =>
|
||||
_service.changePassword(oldPassword: oldPassword, newPassword: newPassword);
|
||||
|
||||
@override
|
||||
Future<void> changePhone({required String newPhone, required String newSmsCode}) =>
|
||||
_service.changePhone(newPhone: newPhone, newSmsCode: newSmsCode);
|
||||
|
||||
// ── 支付宝工具 ────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<String> getAlipayAuthString() => _service.getAlipayAuthString();
|
||||
|
||||
// ── 推荐码 ────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<bool> validateReferralCode(String code) =>
|
||||
_service.validateReferralCode(code);
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// ============================================================
|
||||
// AuthSession — 登录会话实体(用户 + 双 Token)
|
||||
// ============================================================
|
||||
|
||||
import 'auth_user.dart';
|
||||
|
||||
class AuthSession {
|
||||
final AuthUser user;
|
||||
final String accessToken;
|
||||
final String refreshToken;
|
||||
final int expiresIn;
|
||||
|
||||
const AuthSession({
|
||||
required this.user,
|
||||
required this.accessToken,
|
||||
required this.refreshToken,
|
||||
required this.expiresIn,
|
||||
});
|
||||
|
||||
AuthSession copyWith({
|
||||
AuthUser? user,
|
||||
String? accessToken,
|
||||
String? refreshToken,
|
||||
int? expiresIn,
|
||||
}) {
|
||||
return AuthSession(
|
||||
user: user ?? this.user,
|
||||
accessToken: accessToken ?? this.accessToken,
|
||||
refreshToken: refreshToken ?? this.refreshToken,
|
||||
expiresIn: expiresIn ?? this.expiresIn,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
// ============================================================
|
||||
// AuthUser — 认证域用户实体
|
||||
// 纯 Dart 类,不依赖任何框架
|
||||
// ============================================================
|
||||
|
||||
class AuthUser {
|
||||
final String id;
|
||||
final String? phone;
|
||||
final String? email;
|
||||
final String? nickname;
|
||||
final String? avatar;
|
||||
final String? kycStatus;
|
||||
final bool isActive;
|
||||
|
||||
const AuthUser({
|
||||
required this.id,
|
||||
this.phone,
|
||||
this.email,
|
||||
this.nickname,
|
||||
this.avatar,
|
||||
this.kycStatus,
|
||||
this.isActive = true,
|
||||
});
|
||||
|
||||
factory AuthUser.fromJson(Map<String, dynamic> json) {
|
||||
return AuthUser(
|
||||
id: json['id']?.toString() ?? '',
|
||||
phone: json['phone'] as String?,
|
||||
email: json['email'] as String?,
|
||||
nickname: json['nickname'] as String?,
|
||||
avatar: json['avatar'] as String?,
|
||||
kycStatus: json['kycStatus'] as String?,
|
||||
isActive: json['isActive'] as bool? ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
if (phone != null) 'phone': phone,
|
||||
if (email != null) 'email': email,
|
||||
if (nickname != null) 'nickname': nickname,
|
||||
if (avatar != null) 'avatar': avatar,
|
||||
if (kycStatus != null) 'kycStatus': kycStatus,
|
||||
'isActive': isActive,
|
||||
};
|
||||
|
||||
AuthUser copyWith({
|
||||
String? id,
|
||||
String? phone,
|
||||
String? email,
|
||||
String? nickname,
|
||||
String? avatar,
|
||||
String? kycStatus,
|
||||
bool? isActive,
|
||||
}) {
|
||||
return AuthUser(
|
||||
id: id ?? this.id,
|
||||
phone: phone ?? this.phone,
|
||||
email: email ?? this.email,
|
||||
nickname: nickname ?? this.nickname,
|
||||
avatar: avatar ?? this.avatar,
|
||||
kycStatus: kycStatus ?? this.kycStatus,
|
||||
isActive: isActive ?? this.isActive,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) || (other is AuthUser && other.id == id);
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => 'AuthUser(id=$id, phone=$phone, email=$email)';
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
// ============================================================
|
||||
// IAuthRepository — 认证仓库接口(域层)
|
||||
//
|
||||
// 实现位于 data/ 层,域层不依赖任何具体实现。
|
||||
// ============================================================
|
||||
|
||||
import '../entities/auth_session.dart';
|
||||
import '../../../../core/services/auth_service.dart' show SmsCodeType, EmailCodeType;
|
||||
|
||||
export '../../../../core/services/auth_service.dart' show SmsCodeType, EmailCodeType;
|
||||
|
||||
abstract class IAuthRepository {
|
||||
// ── 会话恢复 ────────────────────────────────────────────────
|
||||
/// 从安全存储恢复上次会话;返回 null 表示无会话
|
||||
Future<AuthSession?> restoreSession();
|
||||
|
||||
// ── 密码登录 ─────────────────────────────────────────────────
|
||||
Future<AuthSession> loginByPassword({
|
||||
required String identifier,
|
||||
required String password,
|
||||
String? deviceInfo,
|
||||
});
|
||||
|
||||
// ── SMS 验证码登录 ────────────────────────────────────────────
|
||||
Future<AuthSession> loginByPhone({
|
||||
required String phone,
|
||||
required String smsCode,
|
||||
String? deviceInfo,
|
||||
});
|
||||
|
||||
// ── 邮件验证码登录 ────────────────────────────────────────────
|
||||
Future<AuthSession> loginByEmail({
|
||||
required String email,
|
||||
required String emailCode,
|
||||
String? deviceInfo,
|
||||
});
|
||||
|
||||
// ── 第三方登录 ────────────────────────────────────────────────
|
||||
Future<AuthSession> loginByWechat({
|
||||
required String code,
|
||||
String? referralCode,
|
||||
String? deviceInfo,
|
||||
});
|
||||
|
||||
Future<AuthSession> loginByAlipay({
|
||||
required String authCode,
|
||||
String? referralCode,
|
||||
String? deviceInfo,
|
||||
});
|
||||
|
||||
Future<AuthSession> loginByGoogle({
|
||||
required String idToken,
|
||||
String? referralCode,
|
||||
String? deviceInfo,
|
||||
});
|
||||
|
||||
Future<AuthSession> loginByApple({
|
||||
required String identityToken,
|
||||
String? displayName,
|
||||
String? referralCode,
|
||||
String? deviceInfo,
|
||||
});
|
||||
|
||||
// ── 注册 ──────────────────────────────────────────────────────
|
||||
Future<AuthSession> register({
|
||||
required String phone,
|
||||
required String smsCode,
|
||||
required String password,
|
||||
String? nickname,
|
||||
String? referralCode,
|
||||
});
|
||||
|
||||
Future<AuthSession> registerByEmail({
|
||||
required String email,
|
||||
required String emailCode,
|
||||
required String password,
|
||||
String? nickname,
|
||||
String? referralCode,
|
||||
});
|
||||
|
||||
// ── 登出 ──────────────────────────────────────────────────────
|
||||
Future<void> logout();
|
||||
|
||||
// ── 验证码 ────────────────────────────────────────────────────
|
||||
Future<int> sendSmsCode(String phone, SmsCodeType type);
|
||||
Future<int> sendEmailCode(String email, EmailCodeType type);
|
||||
|
||||
// ── 密码管理 ─────────────────────────────────────────────────
|
||||
Future<void> resetPassword({
|
||||
required String phone,
|
||||
required String smsCode,
|
||||
required String newPassword,
|
||||
});
|
||||
|
||||
Future<void> resetPasswordByEmail({
|
||||
required String email,
|
||||
required String emailCode,
|
||||
required String newPassword,
|
||||
});
|
||||
|
||||
Future<void> changePassword({
|
||||
required String oldPassword,
|
||||
required String newPassword,
|
||||
});
|
||||
|
||||
Future<void> changePhone({
|
||||
required String newPhone,
|
||||
required String newSmsCode,
|
||||
});
|
||||
|
||||
// ── 支付宝工具 ────────────────────────────────────────────────
|
||||
Future<String> getAlipayAuthString();
|
||||
|
||||
// ── 推荐码 ────────────────────────────────────────────────────
|
||||
Future<bool> validateReferralCode(String code);
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
// ============================================================
|
||||
// Auth Misc UseCases — 登出、Session恢复、验证码、密码管理
|
||||
// ============================================================
|
||||
|
||||
import '../entities/auth_session.dart';
|
||||
import '../repositories/auth_repository.dart';
|
||||
|
||||
// ── Session 恢复 ─────────────────────────────────────────────
|
||||
|
||||
class RestoreSessionUseCase {
|
||||
final IAuthRepository _repository;
|
||||
const RestoreSessionUseCase(this._repository);
|
||||
|
||||
Future<AuthSession?> call() => _repository.restoreSession();
|
||||
}
|
||||
|
||||
// ── 登出 ──────────────────────────────────────────────────────
|
||||
|
||||
class LogoutUseCase {
|
||||
final IAuthRepository _repository;
|
||||
const LogoutUseCase(this._repository);
|
||||
|
||||
Future<void> call() => _repository.logout();
|
||||
}
|
||||
|
||||
// ── SMS 验证码 ────────────────────────────────────────────────
|
||||
|
||||
class SendSmsCodeParams {
|
||||
final String phone;
|
||||
final SmsCodeType type;
|
||||
const SendSmsCodeParams({required this.phone, required this.type});
|
||||
}
|
||||
|
||||
class SendSmsCodeUseCase {
|
||||
final IAuthRepository _repository;
|
||||
const SendSmsCodeUseCase(this._repository);
|
||||
|
||||
Future<int> call(SendSmsCodeParams params) =>
|
||||
_repository.sendSmsCode(params.phone, params.type);
|
||||
}
|
||||
|
||||
// ── 邮件验证码 ────────────────────────────────────────────────
|
||||
|
||||
class SendEmailCodeParams {
|
||||
final String email;
|
||||
final EmailCodeType type;
|
||||
const SendEmailCodeParams({required this.email, required this.type});
|
||||
}
|
||||
|
||||
class SendEmailCodeUseCase {
|
||||
final IAuthRepository _repository;
|
||||
const SendEmailCodeUseCase(this._repository);
|
||||
|
||||
Future<int> call(SendEmailCodeParams params) =>
|
||||
_repository.sendEmailCode(params.email, params.type);
|
||||
}
|
||||
|
||||
// ── 重置密码(手机) ──────────────────────────────────────────
|
||||
|
||||
class ResetPasswordParams {
|
||||
final String phone;
|
||||
final String smsCode;
|
||||
final String newPassword;
|
||||
const ResetPasswordParams({
|
||||
required this.phone,
|
||||
required this.smsCode,
|
||||
required this.newPassword,
|
||||
});
|
||||
}
|
||||
|
||||
class ResetPasswordUseCase {
|
||||
final IAuthRepository _repository;
|
||||
const ResetPasswordUseCase(this._repository);
|
||||
|
||||
Future<void> call(ResetPasswordParams params) => _repository.resetPassword(
|
||||
phone: params.phone,
|
||||
smsCode: params.smsCode,
|
||||
newPassword: params.newPassword,
|
||||
);
|
||||
}
|
||||
|
||||
// ── 重置密码(邮件)──────────────────────────────────────────
|
||||
|
||||
class ResetPasswordByEmailParams {
|
||||
final String email;
|
||||
final String emailCode;
|
||||
final String newPassword;
|
||||
const ResetPasswordByEmailParams({
|
||||
required this.email,
|
||||
required this.emailCode,
|
||||
required this.newPassword,
|
||||
});
|
||||
}
|
||||
|
||||
class ResetPasswordByEmailUseCase {
|
||||
final IAuthRepository _repository;
|
||||
const ResetPasswordByEmailUseCase(this._repository);
|
||||
|
||||
Future<void> call(ResetPasswordByEmailParams params) =>
|
||||
_repository.resetPasswordByEmail(
|
||||
email: params.email,
|
||||
emailCode: params.emailCode,
|
||||
newPassword: params.newPassword,
|
||||
);
|
||||
}
|
||||
|
||||
// ── 修改密码 ──────────────────────────────────────────────────
|
||||
|
||||
class ChangePasswordParams {
|
||||
final String oldPassword;
|
||||
final String newPassword;
|
||||
const ChangePasswordParams({required this.oldPassword, required this.newPassword});
|
||||
}
|
||||
|
||||
class ChangePasswordUseCase {
|
||||
final IAuthRepository _repository;
|
||||
const ChangePasswordUseCase(this._repository);
|
||||
|
||||
Future<void> call(ChangePasswordParams params) => _repository.changePassword(
|
||||
oldPassword: params.oldPassword,
|
||||
newPassword: params.newPassword,
|
||||
);
|
||||
}
|
||||
|
||||
// ── 换绑手机 ──────────────────────────────────────────────────
|
||||
|
||||
class ChangePhoneParams {
|
||||
final String newPhone;
|
||||
final String newSmsCode;
|
||||
const ChangePhoneParams({required this.newPhone, required this.newSmsCode});
|
||||
}
|
||||
|
||||
class ChangePhoneUseCase {
|
||||
final IAuthRepository _repository;
|
||||
const ChangePhoneUseCase(this._repository);
|
||||
|
||||
Future<void> call(ChangePhoneParams params) => _repository.changePhone(
|
||||
newPhone: params.newPhone,
|
||||
newSmsCode: params.newSmsCode,
|
||||
);
|
||||
}
|
||||
|
||||
// ── 推荐码验证 ────────────────────────────────────────────────
|
||||
|
||||
class ValidateReferralCodeUseCase {
|
||||
final IAuthRepository _repository;
|
||||
const ValidateReferralCodeUseCase(this._repository);
|
||||
|
||||
Future<bool> call(String code) => _repository.validateReferralCode(code);
|
||||
}
|
||||
|
||||
// ── 支付宝授权字符串 ──────────────────────────────────────────
|
||||
|
||||
class GetAlipayAuthStringUseCase {
|
||||
final IAuthRepository _repository;
|
||||
const GetAlipayAuthStringUseCase(this._repository);
|
||||
|
||||
Future<String> call() => _repository.getAlipayAuthString();
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
// ============================================================
|
||||
// Login UseCases — 各种登录方式的用例
|
||||
// ============================================================
|
||||
|
||||
import '../entities/auth_session.dart';
|
||||
import '../repositories/auth_repository.dart';
|
||||
|
||||
// ── 密码登录 ────────────────────────────────────────────────
|
||||
|
||||
class LoginByPasswordParams {
|
||||
final String identifier;
|
||||
final String password;
|
||||
final String? deviceInfo;
|
||||
const LoginByPasswordParams({
|
||||
required this.identifier,
|
||||
required this.password,
|
||||
this.deviceInfo,
|
||||
});
|
||||
}
|
||||
|
||||
class LoginByPasswordUseCase {
|
||||
final IAuthRepository _repository;
|
||||
const LoginByPasswordUseCase(this._repository);
|
||||
|
||||
Future<AuthSession> call(LoginByPasswordParams params) {
|
||||
return _repository.loginByPassword(
|
||||
identifier: params.identifier,
|
||||
password: params.password,
|
||||
deviceInfo: params.deviceInfo,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── SMS 验证码登录 ────────────────────────────────────────────
|
||||
|
||||
class LoginByPhoneParams {
|
||||
final String phone;
|
||||
final String smsCode;
|
||||
final String? deviceInfo;
|
||||
const LoginByPhoneParams({
|
||||
required this.phone,
|
||||
required this.smsCode,
|
||||
this.deviceInfo,
|
||||
});
|
||||
}
|
||||
|
||||
class LoginByPhoneUseCase {
|
||||
final IAuthRepository _repository;
|
||||
const LoginByPhoneUseCase(this._repository);
|
||||
|
||||
Future<AuthSession> call(LoginByPhoneParams params) {
|
||||
return _repository.loginByPhone(
|
||||
phone: params.phone,
|
||||
smsCode: params.smsCode,
|
||||
deviceInfo: params.deviceInfo,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 邮件验证码登录 ────────────────────────────────────────────
|
||||
|
||||
class LoginByEmailParams {
|
||||
final String email;
|
||||
final String emailCode;
|
||||
const LoginByEmailParams({required this.email, required this.emailCode});
|
||||
}
|
||||
|
||||
class LoginByEmailUseCase {
|
||||
final IAuthRepository _repository;
|
||||
const LoginByEmailUseCase(this._repository);
|
||||
|
||||
Future<AuthSession> call(LoginByEmailParams params) {
|
||||
return _repository.loginByEmail(email: params.email, emailCode: params.emailCode);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 微信登录 ──────────────────────────────────────────────────
|
||||
|
||||
class LoginByWechatParams {
|
||||
final String code;
|
||||
final String? referralCode;
|
||||
const LoginByWechatParams({required this.code, this.referralCode});
|
||||
}
|
||||
|
||||
class LoginByWechatUseCase {
|
||||
final IAuthRepository _repository;
|
||||
const LoginByWechatUseCase(this._repository);
|
||||
|
||||
Future<AuthSession> call(LoginByWechatParams params) {
|
||||
return _repository.loginByWechat(code: params.code, referralCode: params.referralCode);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 支付宝登录 ────────────────────────────────────────────────
|
||||
|
||||
class LoginByAlipayParams {
|
||||
final String authCode;
|
||||
final String? referralCode;
|
||||
const LoginByAlipayParams({required this.authCode, this.referralCode});
|
||||
}
|
||||
|
||||
class LoginByAlipayUseCase {
|
||||
final IAuthRepository _repository;
|
||||
const LoginByAlipayUseCase(this._repository);
|
||||
|
||||
Future<AuthSession> call(LoginByAlipayParams params) {
|
||||
return _repository.loginByAlipay(authCode: params.authCode, referralCode: params.referralCode);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Google 登录 ────────────────────────────────────────────────
|
||||
|
||||
class LoginByGoogleParams {
|
||||
final String idToken;
|
||||
final String? referralCode;
|
||||
const LoginByGoogleParams({required this.idToken, this.referralCode});
|
||||
}
|
||||
|
||||
class LoginByGoogleUseCase {
|
||||
final IAuthRepository _repository;
|
||||
const LoginByGoogleUseCase(this._repository);
|
||||
|
||||
Future<AuthSession> call(LoginByGoogleParams params) {
|
||||
return _repository.loginByGoogle(idToken: params.idToken, referralCode: params.referralCode);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Apple 登录 ────────────────────────────────────────────────
|
||||
|
||||
class LoginByAppleParams {
|
||||
final String identityToken;
|
||||
final String? displayName;
|
||||
final String? referralCode;
|
||||
const LoginByAppleParams({
|
||||
required this.identityToken,
|
||||
this.displayName,
|
||||
this.referralCode,
|
||||
});
|
||||
}
|
||||
|
||||
class LoginByAppleUseCase {
|
||||
final IAuthRepository _repository;
|
||||
const LoginByAppleUseCase(this._repository);
|
||||
|
||||
Future<AuthSession> call(LoginByAppleParams params) {
|
||||
return _repository.loginByApple(
|
||||
identityToken: params.identityToken,
|
||||
displayName: params.displayName,
|
||||
referralCode: params.referralCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
// ============================================================
|
||||
// Register UseCases
|
||||
// ============================================================
|
||||
|
||||
import '../entities/auth_session.dart';
|
||||
import '../repositories/auth_repository.dart';
|
||||
|
||||
class RegisterParams {
|
||||
final String phone;
|
||||
final String smsCode;
|
||||
final String password;
|
||||
final String? nickname;
|
||||
final String? referralCode;
|
||||
const RegisterParams({
|
||||
required this.phone,
|
||||
required this.smsCode,
|
||||
required this.password,
|
||||
this.nickname,
|
||||
this.referralCode,
|
||||
});
|
||||
}
|
||||
|
||||
class RegisterUseCase {
|
||||
final IAuthRepository _repository;
|
||||
const RegisterUseCase(this._repository);
|
||||
|
||||
Future<AuthSession> call(RegisterParams params) {
|
||||
return _repository.register(
|
||||
phone: params.phone,
|
||||
smsCode: params.smsCode,
|
||||
password: params.password,
|
||||
nickname: params.nickname,
|
||||
referralCode: params.referralCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RegisterByEmailParams {
|
||||
final String email;
|
||||
final String emailCode;
|
||||
final String password;
|
||||
final String? nickname;
|
||||
final String? referralCode;
|
||||
const RegisterByEmailParams({
|
||||
required this.email,
|
||||
required this.emailCode,
|
||||
required this.password,
|
||||
this.nickname,
|
||||
this.referralCode,
|
||||
});
|
||||
}
|
||||
|
||||
class RegisterByEmailUseCase {
|
||||
final IAuthRepository _repository;
|
||||
const RegisterByEmailUseCase(this._repository);
|
||||
|
||||
Future<AuthSession> call(RegisterByEmailParams params) {
|
||||
return _repository.registerByEmail(
|
||||
email: params.email,
|
||||
emailCode: params.emailCode,
|
||||
password: params.password,
|
||||
nickname: params.nickname,
|
||||
referralCode: params.referralCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,24 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../../../app/theme/app_colors.dart';
|
||||
import '../../../../app/theme/app_typography.dart';
|
||||
import '../../../../app/theme/app_spacing.dart';
|
||||
import '../../../../shared/widgets/genex_button.dart';
|
||||
import '../../../../shared/widgets/countdown_button.dart';
|
||||
import '../../../../core/services/auth_service.dart';
|
||||
import '../../../../core/services/auth_service.dart' show SmsCodeType;
|
||||
import '../../../../app/i18n/app_localizations.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
|
||||
/// A1. 忘记密码 - 手机号验证 → 输入验证码 → 设置新密码 → 成功
|
||||
class ForgotPasswordPage extends StatefulWidget {
|
||||
class ForgotPasswordPage extends ConsumerStatefulWidget {
|
||||
const ForgotPasswordPage({super.key});
|
||||
|
||||
@override
|
||||
State<ForgotPasswordPage> createState() => _ForgotPasswordPageState();
|
||||
ConsumerState<ForgotPasswordPage> createState() => _ForgotPasswordPageState();
|
||||
}
|
||||
|
||||
class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
|
||||
class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
|
||||
int _step = 0; // 0: 输入账号, 1: 验证码, 2: 新密码, 3: 成功
|
||||
final _phoneController = TextEditingController();
|
||||
final _codeController = TextEditingController();
|
||||
|
|
@ -27,8 +29,6 @@ class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
|
|||
bool _loading = false;
|
||||
String? _errorMessage;
|
||||
|
||||
final _authService = AuthService.instance;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_phoneController.dispose();
|
||||
|
|
@ -55,7 +55,7 @@ class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
|
|||
}
|
||||
setState(() => _errorMessage = null);
|
||||
try {
|
||||
await _authService.sendSmsCode(phone, SmsCodeType.resetPassword);
|
||||
await ref.read(authProvider.notifier).sendSmsCode(phone, SmsCodeType.resetPassword);
|
||||
setState(() => _step = 1);
|
||||
} on DioException catch (e) {
|
||||
setState(() => _errorMessage = _extractError(e));
|
||||
|
|
@ -88,7 +88,7 @@ class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
|
|||
|
||||
setState(() { _loading = true; _errorMessage = null; });
|
||||
try {
|
||||
await _authService.resetPassword(
|
||||
await ref.read(authProvider.notifier).resetPassword(
|
||||
phone: _phoneController.text.trim(),
|
||||
smsCode: _codeController.text.trim(),
|
||||
newPassword: password,
|
||||
|
|
@ -106,7 +106,7 @@ class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
|
|||
/* ── Step 1: 重新发送 ── */
|
||||
Future<void> _handleResend() async {
|
||||
try {
|
||||
await _authService.sendSmsCode(
|
||||
await ref.read(authProvider.notifier).sendSmsCode(
|
||||
_phoneController.text.trim(),
|
||||
SmsCodeType.resetPassword,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,22 +1,24 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../../../app/theme/app_colors.dart';
|
||||
import '../../../../app/theme/app_typography.dart';
|
||||
import '../../../../app/theme/app_spacing.dart';
|
||||
import '../../../../shared/widgets/genex_button.dart';
|
||||
import '../../../../shared/widgets/countdown_button.dart';
|
||||
import '../../../../core/services/auth_service.dart';
|
||||
import '../../../../core/services/auth_service.dart' show SmsCodeType;
|
||||
import '../../../../app/i18n/app_localizations.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
|
||||
/// A1. 登录页 - 密码登录 / 验证码快捷登录
|
||||
class LoginPage extends StatefulWidget {
|
||||
class LoginPage extends ConsumerStatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
State<LoginPage> createState() => _LoginPageState();
|
||||
ConsumerState<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMixin {
|
||||
class _LoginPageState extends ConsumerState<LoginPage> with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
final _phoneController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
|
|
@ -26,8 +28,6 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||
bool _loading = false;
|
||||
String? _errorMessage;
|
||||
|
||||
final _authService = AuthService.instance;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -75,7 +75,7 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||
|
||||
setState(() { _loading = true; _errorMessage = null; });
|
||||
try {
|
||||
await _authService.loginByPassword(
|
||||
await ref.read(authProvider.notifier).loginByPassword(
|
||||
identifier: identifier,
|
||||
password: password,
|
||||
);
|
||||
|
|
@ -98,7 +98,7 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||
}
|
||||
setState(() => _errorMessage = null);
|
||||
try {
|
||||
await _authService.sendSmsCode(phone, SmsCodeType.login);
|
||||
await ref.read(authProvider.notifier).sendSmsCode(phone, SmsCodeType.login);
|
||||
} on DioException catch (e) {
|
||||
_showError(_extractError(e));
|
||||
rethrow;
|
||||
|
|
@ -121,7 +121,7 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||
|
||||
setState(() { _loading = true; _errorMessage = null; });
|
||||
try {
|
||||
await _authService.loginByPhone(phone: phone, smsCode: code);
|
||||
await ref.read(authProvider.notifier).loginByPhone(phone: phone, smsCode: code);
|
||||
if (mounted) Navigator.pushReplacementNamed(context, '/main');
|
||||
} on DioException catch (e) {
|
||||
_showError(_extractError(e));
|
||||
|
|
|
|||
|
|
@ -1,26 +1,28 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../../../app/theme/app_colors.dart';
|
||||
import '../../../../app/theme/app_typography.dart';
|
||||
import '../../../../app/theme/app_spacing.dart';
|
||||
import '../../../../shared/widgets/genex_button.dart';
|
||||
import '../../../../shared/widgets/countdown_button.dart';
|
||||
import '../../../../core/services/auth_service.dart';
|
||||
import '../../../../core/services/auth_service.dart' show SmsCodeType, EmailCodeType;
|
||||
import '../../../../app/i18n/app_localizations.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
|
||||
/// A1. 手机号注册页
|
||||
///
|
||||
/// 3步流程: 输入手机号+验证码 → 设置密码 → 完成注册
|
||||
class RegisterPage extends StatefulWidget {
|
||||
class RegisterPage extends ConsumerStatefulWidget {
|
||||
final bool isEmail;
|
||||
|
||||
const RegisterPage({super.key, this.isEmail = false});
|
||||
|
||||
@override
|
||||
State<RegisterPage> createState() => _RegisterPageState();
|
||||
ConsumerState<RegisterPage> createState() => _RegisterPageState();
|
||||
}
|
||||
|
||||
class _RegisterPageState extends State<RegisterPage> {
|
||||
class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
final _accountController = TextEditingController();
|
||||
final _codeController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
|
|
@ -32,8 +34,6 @@ class _RegisterPageState extends State<RegisterPage> {
|
|||
// null=未验证, true=有效, false=无效
|
||||
bool? _referralCodeValid;
|
||||
|
||||
final _authService = AuthService.instance;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -55,7 +55,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
|||
return;
|
||||
}
|
||||
try {
|
||||
final resp = await _authService.validateReferralCode(code);
|
||||
final resp = await ref.read(authProvider.notifier).validateReferralCode(code);
|
||||
setState(() => _referralCodeValid = resp);
|
||||
} catch (_) {
|
||||
setState(() => _referralCodeValid = false);
|
||||
|
|
@ -82,9 +82,9 @@ class _RegisterPageState extends State<RegisterPage> {
|
|||
setState(() => _errorMessage = null);
|
||||
try {
|
||||
if (widget.isEmail) {
|
||||
await _authService.sendEmailCode(account, EmailCodeType.register);
|
||||
await ref.read(authProvider.notifier).sendEmailCode(account, EmailCodeType.register);
|
||||
} else {
|
||||
await _authService.sendSmsCode(account, SmsCodeType.register);
|
||||
await ref.read(authProvider.notifier).sendSmsCode(account, SmsCodeType.register);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
setState(() => _errorMessage = _extractError(e));
|
||||
|
|
@ -123,14 +123,14 @@ class _RegisterPageState extends State<RegisterPage> {
|
|||
try {
|
||||
final referralCode = _referralCodeController.text.trim();
|
||||
if (widget.isEmail) {
|
||||
await _authService.registerByEmail(
|
||||
await ref.read(authProvider.notifier).registerByEmail(
|
||||
email: account,
|
||||
emailCode: code,
|
||||
password: password,
|
||||
referralCode: referralCode.isNotEmpty ? referralCode : null,
|
||||
);
|
||||
} else {
|
||||
await _authService.register(
|
||||
await ref.read(authProvider.notifier).register(
|
||||
phone: account,
|
||||
smsCode: code,
|
||||
password: password,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart' show defaultTargetPlatform, TargetPlatform;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:fluwx/fluwx.dart';
|
||||
import 'package:tobias/tobias.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
|
|
@ -9,7 +10,7 @@ import '../../../../app/theme/app_typography.dart';
|
|||
import '../../../../app/theme/app_spacing.dart';
|
||||
import '../../../../shared/widgets/genex_button.dart';
|
||||
import '../../../../app/i18n/app_localizations.dart';
|
||||
import '../../../../core/services/auth_service.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
|
||||
/// A1. 欢迎页 - 品牌展示 + 注册/登录入口
|
||||
///
|
||||
|
|
@ -32,14 +33,14 @@ import '../../../../core/services/auth_service.dart';
|
|||
/// 支付宝: GET /auth/alipay/auth-string → Tobias().auth(authStr) → Map(result 含 auth_code) → POST /auth/alipay
|
||||
/// Google: google_sign_in.signIn → GoogleSignInAuthentication.idToken → POST /auth/google
|
||||
/// Apple: sign_in_with_apple.getAppleIDCredential → identityToken → POST /auth/apple
|
||||
class WelcomePage extends StatefulWidget {
|
||||
class WelcomePage extends ConsumerStatefulWidget {
|
||||
const WelcomePage({super.key});
|
||||
|
||||
@override
|
||||
State<WelcomePage> createState() => _WelcomePageState();
|
||||
ConsumerState<WelcomePage> createState() => _WelcomePageState();
|
||||
}
|
||||
|
||||
class _WelcomePageState extends State<WelcomePage> {
|
||||
class _WelcomePageState extends ConsumerState<WelcomePage> {
|
||||
bool _wechatLoading = false;
|
||||
bool _alipayLoading = false;
|
||||
bool _googleLoading = false;
|
||||
|
|
@ -105,7 +106,7 @@ class _WelcomePageState extends State<WelcomePage> {
|
|||
setState(() => _wechatLoading = false);
|
||||
if ((resp.errCode ?? -1) != 0 || resp.code == null) return;
|
||||
try {
|
||||
await AuthService.instance.loginByWechat(code: resp.code!);
|
||||
await ref.read(authProvider.notifier).loginByWechat(code: resp.code!);
|
||||
if (mounted) Navigator.pushReplacementNamed(context, '/main');
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
|
|
@ -164,7 +165,7 @@ class _WelcomePageState extends State<WelcomePage> {
|
|||
// 2. 传给 Tobias().auth(authString) 拉起支付宝 App
|
||||
// 3. 用户授权后 result['result'] 中含 auth_code(同旧流程)
|
||||
// 4. auth_code → POST /auth/alipay(不变)
|
||||
final authString = await AuthService.instance.getAlipayAuthString();
|
||||
final authString = await ref.read(authProvider.notifier).getAlipayAuthString();
|
||||
final result = await _tobias.auth(authString);
|
||||
|
||||
final status = result['resultStatus']?.toString() ?? '';
|
||||
|
|
@ -184,7 +185,7 @@ class _WelcomePageState extends State<WelcomePage> {
|
|||
}
|
||||
|
||||
// 将 auth_code 发送后端,后端完成 token 换取 + 用户信息获取
|
||||
await AuthService.instance.loginByAlipay(authCode: authCode);
|
||||
await ref.read(authProvider.notifier).loginByAlipay(authCode: authCode);
|
||||
if (mounted) Navigator.pushReplacementNamed(context, '/main');
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
|
|
@ -247,7 +248,7 @@ class _WelcomePageState extends State<WelcomePage> {
|
|||
if (idToken == null) throw Exception('未获取到 ID Token');
|
||||
|
||||
// 将 idToken 发送后端验证(Google tokeninfo API 或 JWKS 本地验证)
|
||||
await AuthService.instance.loginByGoogle(idToken: idToken);
|
||||
await ref.read(authProvider.notifier).loginByGoogle(idToken: idToken);
|
||||
if (mounted) Navigator.pushReplacementNamed(context, '/main');
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
|
|
@ -309,7 +310,7 @@ class _WelcomePageState extends State<WelcomePage> {
|
|||
// - 后端用 Apple JWKS 验证 identityToken 签名
|
||||
// - 提取 sub 作为用户唯一标识(后续登录同一用户)
|
||||
// - 首次注册时用 email + displayName 初始化用户资料
|
||||
await AuthService.instance.loginByApple(
|
||||
await ref.read(authProvider.notifier).loginByApple(
|
||||
identityToken: identityToken,
|
||||
displayName: displayName.isNotEmpty ? displayName : null,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,327 @@
|
|||
// ============================================================
|
||||
// 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;
|
||||
});
|
||||
|
|
@ -71,6 +71,7 @@ class _MyCouponsPageState extends State<MyCouponsPage>
|
|||
// Example: show empty state for expired tab
|
||||
if (filter == CouponStatus.expired) {
|
||||
return EmptyState.noCoupons(
|
||||
context,
|
||||
onBrowse: () {
|
||||
// noop - market tab accessible from bottom nav
|
||||
},
|
||||
|
|
@ -164,7 +165,7 @@ class _MyCouponCard extends StatelessWidget {
|
|||
Text('${context.t('walletCoupons.faceValue')} \$${faceValue.toStringAsFixed(0)}',
|
||||
style: AppTypography.bodySmall),
|
||||
const SizedBox(width: 8),
|
||||
_statusWidget,
|
||||
_buildStatusWidget(context),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
@ -206,16 +207,16 @@ class _MyCouponCard extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget get _statusWidget {
|
||||
Widget _buildStatusWidget(BuildContext context) {
|
||||
switch (status) {
|
||||
case CouponStatus.active:
|
||||
return StatusTags.active();
|
||||
return StatusTags.active(context);
|
||||
case CouponStatus.pending:
|
||||
return StatusTags.pending();
|
||||
return StatusTags.pending(context);
|
||||
case CouponStatus.expired:
|
||||
return StatusTags.expired();
|
||||
return StatusTags.expired(context);
|
||||
case CouponStatus.used:
|
||||
return StatusTags.used();
|
||||
return StatusTags.used(context);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
// ============================================================
|
||||
// CouponProvider — 券功能 Riverpod Providers
|
||||
// ============================================================
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../core/services/coupon_service.dart';
|
||||
|
||||
/// CouponApiService Provider(DI 入口)
|
||||
final couponServiceProvider = Provider<CouponApiService>((ref) {
|
||||
return CouponApiService();
|
||||
});
|
||||
|
||||
// ── Featured Coupons ──────────────────────────────────────────
|
||||
|
||||
final featuredCouponsProvider = FutureProvider.autoDispose((ref) async {
|
||||
return ref.read(couponServiceProvider).getFeaturedCoupons();
|
||||
});
|
||||
|
||||
// ── My Holdings ───────────────────────────────────────────────
|
||||
|
||||
class HoldingsParams {
|
||||
final String? status;
|
||||
final int page;
|
||||
final int limit;
|
||||
const HoldingsParams({this.status, this.page = 1, this.limit = 20});
|
||||
}
|
||||
|
||||
final myHoldingsProvider =
|
||||
FutureProvider.autoDispose.family<dynamic, HoldingsParams>((ref, params) async {
|
||||
return ref.read(couponServiceProvider).getMyHoldings(
|
||||
status: params.status,
|
||||
page: params.page,
|
||||
limit: params.limit,
|
||||
);
|
||||
});
|
||||
|
||||
// ── Holdings Summary ──────────────────────────────────────────
|
||||
|
||||
final holdingsSummaryProvider = FutureProvider.autoDispose((ref) async {
|
||||
return ref.read(couponServiceProvider).getHoldingsSummary();
|
||||
});
|
||||
|
||||
// ── Coupon Detail ─────────────────────────────────────────────
|
||||
|
||||
final couponDetailProvider =
|
||||
FutureProvider.autoDispose.family<dynamic, String>((ref, couponId) async {
|
||||
return ref.read(couponServiceProvider).getCouponDetail(couponId);
|
||||
});
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
// ============================================================
|
||||
// IssuerProvider — 发行方 Riverpod Providers
|
||||
// ============================================================
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../core/network/api_client.dart';
|
||||
|
||||
final issuerInfoProvider = FutureProvider.autoDispose((ref) async {
|
||||
final api = ApiClient.instance;
|
||||
final resp = await api.get('/api/v1/issuers/me');
|
||||
return resp.data['data'] as Map<String, dynamic>?;
|
||||
});
|
||||
|
||||
final issuerCouponsProvider = FutureProvider.autoDispose((ref) async {
|
||||
final api = ApiClient.instance;
|
||||
final resp = await api.get('/api/v1/issuers/me/coupons');
|
||||
return resp.data['data'];
|
||||
});
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
// ============================================================
|
||||
// MerchantProvider — 商家 Riverpod Providers
|
||||
// ============================================================
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../core/network/api_client.dart';
|
||||
|
||||
final merchantInfoProvider = FutureProvider.autoDispose((ref) async {
|
||||
final api = ApiClient.instance;
|
||||
final resp = await api.get('/api/v1/merchants/me');
|
||||
return resp.data['data'] as Map<String, dynamic>?;
|
||||
});
|
||||
|
||||
final merchantRedemptionsProvider = FutureProvider.autoDispose((ref) async {
|
||||
final api = ApiClient.instance;
|
||||
final resp = await api.get('/api/v1/merchants/me/redemptions');
|
||||
return resp.data['data'];
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// ============================================================
|
||||
// MessageProvider — 通知/消息 Riverpod Providers
|
||||
// ============================================================
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../core/services/notification_service.dart';
|
||||
|
||||
final notificationServiceProvider = Provider<NotificationService>((ref) {
|
||||
return NotificationService();
|
||||
});
|
||||
|
||||
// ── 通知列表 ──────────────────────────────────────────────────
|
||||
|
||||
final notificationsProvider =
|
||||
FutureProvider.autoDispose.family<NotificationListResponse, NotificationType?>(
|
||||
(ref, type) async {
|
||||
return ref.read(notificationServiceProvider).getNotifications(type: type);
|
||||
});
|
||||
|
||||
// ── 未读数量 ──────────────────────────────────────────────────
|
||||
|
||||
final unreadCountProvider = FutureProvider.autoDispose((ref) async {
|
||||
return ref.read(notificationServiceProvider).getUnreadCount();
|
||||
});
|
||||
|
||||
// ── 公告列表 ──────────────────────────────────────────────────
|
||||
|
||||
final announcementsProvider = FutureProvider.autoDispose((ref) async {
|
||||
return ref.read(notificationServiceProvider).getAnnouncements();
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// ============================================================
|
||||
// ProfileProvider — 用户资料 Riverpod Providers
|
||||
// ============================================================
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../core/network/api_client.dart';
|
||||
|
||||
// ── 用户资料 ──────────────────────────────────────────────────
|
||||
|
||||
final userProfileProvider = FutureProvider.autoDispose((ref) async {
|
||||
final api = ApiClient.instance;
|
||||
final resp = await api.get('/api/v1/users/me');
|
||||
return resp.data['data'] as Map<String, dynamic>?;
|
||||
});
|
||||
|
||||
// ── KYC 状态 ──────────────────────────────────────────────────
|
||||
|
||||
final kycStatusProvider = FutureProvider.autoDispose((ref) async {
|
||||
final api = ApiClient.instance;
|
||||
final resp = await api.get('/api/v1/users/kyc/status');
|
||||
return resp.data['data'] as Map<String, dynamic>?;
|
||||
});
|
||||
|
||||
// ── 支付方式 ──────────────────────────────────────────────────
|
||||
|
||||
final paymentMethodsProvider = FutureProvider.autoDispose((ref) async {
|
||||
final api = ApiClient.instance;
|
||||
final resp = await api.get('/api/v1/users/payment-methods');
|
||||
return resp.data['data'];
|
||||
});
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
// ============================================================
|
||||
// ReferralProvider — 推荐/邀请系统 Riverpod Providers
|
||||
// ============================================================
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../core/services/referral_service.dart';
|
||||
|
||||
final referralServiceProvider = Provider<ReferralService>((ref) {
|
||||
return ReferralService.instance;
|
||||
});
|
||||
|
||||
/// 当前用户推荐信息(自动缓存 5 分钟)
|
||||
final myReferralInfoProvider = FutureProvider.autoDispose((ref) async {
|
||||
return ref.read(referralServiceProvider).getMyInfo();
|
||||
});
|
||||
|
||||
/// 直接推荐列表
|
||||
final directReferralsProvider =
|
||||
FutureProvider.autoDispose.family<List<Map<String, dynamic>>, int>(
|
||||
(ref, offset) async {
|
||||
return ref.read(referralServiceProvider).getDirectReferrals(offset: offset);
|
||||
});
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
// ============================================================
|
||||
// RedeemProvider — 核销 Riverpod Providers
|
||||
// ============================================================
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../core/network/api_client.dart';
|
||||
|
||||
class RedeemState {
|
||||
final bool isLoading;
|
||||
final Map<String, dynamic>? result;
|
||||
final String? error;
|
||||
|
||||
const RedeemState({this.isLoading = false, this.result, this.error});
|
||||
|
||||
RedeemState copyWith({
|
||||
bool? isLoading,
|
||||
Map<String, dynamic>? result,
|
||||
String? error,
|
||||
bool clearError = false,
|
||||
}) {
|
||||
return RedeemState(
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
result: result ?? this.result,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RedeemNotifier extends Notifier<RedeemState> {
|
||||
@override
|
||||
RedeemState build() => const RedeemState();
|
||||
|
||||
Future<void> redeemCoupon(String holdingId) async {
|
||||
state = state.copyWith(isLoading: true, clearError: true);
|
||||
try {
|
||||
final api = ApiClient.instance;
|
||||
final resp = await api.post('/api/v1/redemptions', data: {'holdingId': holdingId});
|
||||
state = state.copyWith(isLoading: false, result: resp.data['data'] as Map<String, dynamic>?);
|
||||
} catch (e) {
|
||||
state = state.copyWith(isLoading: false, error: e.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final redeemProvider = NotifierProvider<RedeemNotifier, RedeemState>(RedeemNotifier.new);
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// ============================================================
|
||||
// SupportProvider — 客服/工单 Riverpod Providers
|
||||
// ============================================================
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../core/network/api_client.dart';
|
||||
|
||||
final supportTicketsProvider = FutureProvider.autoDispose((ref) async {
|
||||
final api = ApiClient.instance;
|
||||
final resp = await api.get('/api/v1/support/tickets');
|
||||
return resp.data['data'];
|
||||
});
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// ============================================================
|
||||
// TradingProvider — 交易 Riverpod Providers
|
||||
// ============================================================
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../core/network/api_client.dart';
|
||||
|
||||
// ── 市场列表 ──────────────────────────────────────────────────
|
||||
|
||||
final marketListProvider = FutureProvider.autoDispose((ref) async {
|
||||
final api = ApiClient.instance;
|
||||
final resp = await api.get('/api/v1/trading/markets');
|
||||
return resp.data['data'];
|
||||
});
|
||||
|
||||
// ── 订单簿 ────────────────────────────────────────────────────
|
||||
|
||||
final orderBookProvider =
|
||||
FutureProvider.autoDispose.family<dynamic, String>((ref, marketId) async {
|
||||
final api = ApiClient.instance;
|
||||
final resp = await api.get('/api/v1/trading/orderbook/$marketId');
|
||||
return resp.data['data'];
|
||||
});
|
||||
|
||||
// ── 我的挂单 ──────────────────────────────────────────────────
|
||||
|
||||
final myOpenOrdersProvider = FutureProvider.autoDispose((ref) async {
|
||||
final api = ApiClient.instance;
|
||||
final resp = await api.get('/api/v1/trading/orders', queryParameters: {'status': 'OPEN'});
|
||||
return resp.data['data'];
|
||||
});
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
// ============================================================
|
||||
// TransferProvider — 转赠 Riverpod Providers
|
||||
// ============================================================
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../core/network/api_client.dart';
|
||||
|
||||
class TransferState {
|
||||
final bool isLoading;
|
||||
final bool isSuccess;
|
||||
final String? error;
|
||||
|
||||
const TransferState({this.isLoading = false, this.isSuccess = false, this.error});
|
||||
|
||||
TransferState copyWith({bool? isLoading, bool? isSuccess, String? error, bool clearError = false}) {
|
||||
return TransferState(
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
isSuccess: isSuccess ?? this.isSuccess,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TransferNotifier extends Notifier<TransferState> {
|
||||
@override
|
||||
TransferState build() => const TransferState();
|
||||
|
||||
Future<void> transfer({
|
||||
required String holdingId,
|
||||
required String recipientPhone,
|
||||
int quantity = 1,
|
||||
}) async {
|
||||
state = state.copyWith(isLoading: true, clearError: true, isSuccess: false);
|
||||
try {
|
||||
final api = ApiClient.instance;
|
||||
await api.post('/api/v1/transfers', data: {
|
||||
'holdingId': holdingId,
|
||||
'recipientPhone': recipientPhone,
|
||||
'quantity': quantity,
|
||||
});
|
||||
state = state.copyWith(isLoading: false, isSuccess: true);
|
||||
} catch (e) {
|
||||
state = state.copyWith(isLoading: false, error: e.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final transferProvider = NotifierProvider<TransferNotifier, TransferState>(TransferNotifier.new);
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
// ============================================================
|
||||
// WalletProvider — 钱包 Riverpod Providers
|
||||
//
|
||||
// 钱包 API 直接通过 ApiClient 调用,无单独 Service 类。
|
||||
// ============================================================
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../core/network/api_client.dart';
|
||||
|
||||
final apiClientProvider = Provider<ApiClient>((ref) {
|
||||
return ApiClient.instance;
|
||||
});
|
||||
|
||||
// ── 钱包余额 ──────────────────────────────────────────────────
|
||||
|
||||
final walletBalanceProvider = FutureProvider.autoDispose((ref) async {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final resp = await api.get('/api/v1/wallet/balance');
|
||||
return resp.data['data'] as Map<String, dynamic>?;
|
||||
});
|
||||
|
||||
// ── 交易记录 ──────────────────────────────────────────────────
|
||||
|
||||
class TransactionParams {
|
||||
final int page;
|
||||
final int pageSize;
|
||||
final String? type;
|
||||
const TransactionParams({this.page = 1, this.pageSize = 20, this.type});
|
||||
}
|
||||
|
||||
final transactionRecordsProvider =
|
||||
FutureProvider.autoDispose.family<dynamic, TransactionParams>((ref, params) async {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final resp = await api.get('/api/v1/wallet/transactions', queryParameters: {
|
||||
'page': params.page,
|
||||
'pageSize': params.pageSize,
|
||||
if (params.type != null) 'type': params.type,
|
||||
});
|
||||
return resp.data['data'];
|
||||
});
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:fluwx/fluwx.dart';
|
||||
import 'app/theme/app_theme.dart';
|
||||
import 'app/main_shell.dart';
|
||||
|
|
@ -14,6 +15,7 @@ import 'features/auth/presentation/pages/login_page.dart';
|
|||
import 'features/auth/presentation/pages/welcome_page.dart';
|
||||
import 'features/auth/presentation/pages/register_page.dart';
|
||||
import 'features/auth/presentation/pages/forgot_password_page.dart';
|
||||
import 'features/auth/presentation/providers/auth_provider.dart';
|
||||
import 'features/coupons/presentation/pages/coupon_detail_page.dart';
|
||||
import 'features/coupons/presentation/pages/order_confirm_page.dart';
|
||||
import 'features/coupons/presentation/pages/payment_page.dart';
|
||||
|
|
@ -44,27 +46,13 @@ Future<void> main() async {
|
|||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// ── 微信 SDK 初始化 (fluwx 5.x) ──────────────────────────────────────
|
||||
// WECHAT_APP_ID 在构建时通过 --dart-define 注入,例如:
|
||||
// flutter build apk --dart-define=WECHAT_APP_ID=wx0000000000000000
|
||||
// flutter build ipa --dart-define=WECHAT_APP_ID=wx0000000000000000
|
||||
//
|
||||
// 未传入 WECHAT_APP_ID 时(本地开发 / CI 未配置),跳过初始化,
|
||||
// WelcomePage 中点击微信按钮会提示「微信未安装」(isWeChatInstalled=false)。
|
||||
//
|
||||
// universalLink: iOS Universal Links 地址,需在微信开放平台填写并配置
|
||||
// apple-app-site-association 文件(路径: https://www.gogenex.com/wechat/apple-app-site-association)
|
||||
// 详见: https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Universal_Links/Universal_Links.html
|
||||
//
|
||||
// fluwx 5.x 迁移说明:
|
||||
// 旧版 (3.x): 顶层函数 registerWxApi(appId: ..., universalLink: ...)
|
||||
// 新版 (5.x): 实例方法 Fluwx().registerApi(appId: ..., universalLink: ...)
|
||||
// 注册只需执行一次(app 启动时),Fluwx 底层 MethodChannel 为单例,
|
||||
// 后续在 WelcomePage 中新建 Fluwx() 实例可共享同一注册状态。
|
||||
const wechatAppId = String.fromEnvironment('WECHAT_APP_ID', defaultValue: '');
|
||||
if (wechatAppId.isNotEmpty) {
|
||||
await Fluwx().registerApi(appId: wechatAppId, universalLink: 'https://www.gogenex.com/wechat/');
|
||||
await Fluwx().registerApi(
|
||||
appId: wechatAppId,
|
||||
universalLink: 'https://www.gogenex.com/wechat/',
|
||||
);
|
||||
}
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
// 初始化升级服务(走 Nginx 反向代理 → Kong 网关)
|
||||
UpdateService().initialize(UpdateConfig.selfHosted(
|
||||
|
|
@ -81,64 +69,73 @@ Future<void> main() async {
|
|||
// 恢复用户语言偏好(无选择时跟随系统语言)
|
||||
await LocaleManager.init();
|
||||
|
||||
// 从安全存储恢复上次登录的 Token(若存在则自动进入 /main)
|
||||
await AuthService.instance.restoreSession();
|
||||
// 从安全存储恢复上次登录的 Token
|
||||
// ProviderScope 外提前恢复,确保 initialRoute 能正确判断
|
||||
final isLoggedIn = await AuthService.instance.restoreSession();
|
||||
|
||||
runApp(const GenexConsumerApp());
|
||||
runApp(
|
||||
ProviderScope(
|
||||
child: GenexConsumerApp(initiallyLoggedIn: isLoggedIn),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Genex Mobile - 券的生命周期管理平台
|
||||
///
|
||||
/// 持仓/交易所/消息/个人中心
|
||||
/// 持有/接收/转赠/交易/核销数字券
|
||||
///
|
||||
/// 国际化:首次启动跟随系统语言,用户可在设置中切换
|
||||
class GenexConsumerApp extends StatefulWidget {
|
||||
const GenexConsumerApp({super.key});
|
||||
class GenexConsumerApp extends ConsumerStatefulWidget {
|
||||
final bool initiallyLoggedIn;
|
||||
|
||||
const GenexConsumerApp({super.key, required this.initiallyLoggedIn});
|
||||
|
||||
@override
|
||||
State<GenexConsumerApp> createState() => _GenexConsumerAppState();
|
||||
ConsumerState<GenexConsumerApp> createState() => _GenexConsumerAppState();
|
||||
}
|
||||
|
||||
class _GenexConsumerAppState extends State<GenexConsumerApp> {
|
||||
// Navigator Key — 用于在无 BuildContext 时执行命令式导航(如 Session 过期后跳 /)
|
||||
class _GenexConsumerAppState extends ConsumerState<GenexConsumerApp> {
|
||||
final _navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
// 上一帧的登录状态,用于检测 Session 过期(已登录 → 未登录)
|
||||
bool _wasLoggedIn = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
LocaleManager.userLocale.addListener(_onLocaleChanged);
|
||||
_wasLoggedIn = AuthService.instance.isLoggedIn;
|
||||
AuthService.instance.authState.addListener(_onAuthStateChanged);
|
||||
|
||||
// 监听 AuthService ValueNotifier → 同步到 Riverpod authProvider
|
||||
// ApiClient 的 Token 刷新/过期回调仍走 AuthService,此处桥接到 Riverpod
|
||||
AuthService.instance.authState.addListener(_onLegacyAuthChanged);
|
||||
|
||||
// 启动时已登录,同步初始状态到 Riverpod authProvider
|
||||
if (widget.initiallyLoggedIn) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(authProvider.notifier).restoreSession();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
LocaleManager.userLocale.removeListener(_onLocaleChanged);
|
||||
AuthService.instance.authState.removeListener(_onAuthStateChanged);
|
||||
AuthService.instance.authState.removeListener(_onLegacyAuthChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onLocaleChanged() {
|
||||
setState(() {});
|
||||
}
|
||||
void _onLocaleChanged() => setState(() {});
|
||||
|
||||
/// Token 过期时由 ApiClient 拦截器触发 AuthService._clearAuth(),
|
||||
/// 进而将 authState 置 null,此监听器感知变化后导航回欢迎页。
|
||||
void _onAuthStateChanged() {
|
||||
final isLoggedIn = AuthService.instance.isLoggedIn;
|
||||
if (_wasLoggedIn && !isLoggedIn) {
|
||||
// Session 过期(非主动登出):清空路由栈,回到欢迎页
|
||||
/// AuthService ValueNotifier 变化时同步到 Riverpod authProvider
|
||||
void _onLegacyAuthChanged() {
|
||||
if (!mounted) return;
|
||||
final legacyResult = AuthService.instance.authState.value;
|
||||
final riverpodState = ref.read(authProvider);
|
||||
|
||||
if (legacyResult == null && riverpodState.isAuthenticated) {
|
||||
// Token 双重过期:AuthService 已清空,通知 Riverpod 同步
|
||||
ref.read(authProvider.notifier).onSessionExpired();
|
||||
_navigatorKey.currentState?.pushNamedAndRemoveUntil('/', (_) => false);
|
||||
}
|
||||
_wasLoggedIn = isLoggedIn;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isAuthenticated = ref.watch(isAuthenticatedProvider);
|
||||
|
||||
return MaterialApp(
|
||||
title: 'Genex',
|
||||
theme: AppTheme.light,
|
||||
|
|
@ -164,8 +161,8 @@ class _GenexConsumerAppState extends State<GenexConsumerApp> {
|
|||
return LocaleManager.userLocale.value;
|
||||
},
|
||||
|
||||
// 启动路由:已有保存的 Token → 直接进主界面;否则显示欢迎页
|
||||
initialRoute: AuthService.instance.isLoggedIn ? '/main' : '/',
|
||||
// 启动路由:已有保存的 Token → 直接进主界面;否则欢迎页
|
||||
initialRoute: (widget.initiallyLoggedIn || isAuthenticated) ? '/main' : '/',
|
||||
onGenerateRoute: _generateRoute,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ dependencies:
|
|||
tobias: ^5.0.0
|
||||
google_sign_in: ^6.2.1
|
||||
sign_in_with_apple: ^6.1.0
|
||||
flutter_riverpod: ^2.5.1
|
||||
fpdart: ^1.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
Loading…
Reference in New Issue