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:
hailin 2026-03-04 20:12:50 -08:00
parent 332a8dafe8
commit 4957b2ef85
29 changed files with 1739 additions and 93 deletions

View File

@ -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/403Token
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 = '未知错误']);
}

View File

@ -0,0 +1,17 @@
// ============================================================
// UseCase
//
// UseCase
//
// Type
// Params 使 NoParams
// ============================================================
abstract class UseCase<Type, Params> {
Future<Type> call(Params params);
}
///
class NoParams {
const NoParams();
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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,
);
}
}

View File

@ -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)';
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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,
);
}
}

View File

@ -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,
);
}
}

View File

@ -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,
);

View File

@ -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));

View File

@ -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,

View File

@ -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,
);

View File

@ -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;
});

View File

@ -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);
}
}

View File

@ -0,0 +1,48 @@
// ============================================================
// CouponProvider Riverpod Providers
// ============================================================
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/services/coupon_service.dart';
/// CouponApiService ProviderDI
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);
});

View File

@ -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'];
});

View File

@ -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'];
});

View File

@ -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();
});

View File

@ -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'];
});

View File

@ -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);
});

View File

@ -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);

View File

@ -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'];
});

View File

@ -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'];
});

View File

@ -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);

View File

@ -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'];
});

View File

@ -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,
);
}

View File

@ -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: