feat(mobile-app): 添加钱包生成状态轮询和UI显示

功能:
- 创建 WalletStatusProvider 管理钱包生成状态
- 每5秒轮询钱包状态直到就绪或失败
- 在个人资料页面显示钱包生成状态
  - 钱包生成中:显示"账号创建审核中"和进度指示器
  - 钱包生成失败:显示失败状态和重试按钮
  - 钱包已就绪:显示序列号
- 在 AccountService 添加 retryWalletGeneration API 调用
- 页面初始化时自动检查并启动轮询
- 页面销毁时自动停止轮询

实现细节:
- 使用 Riverpod 状态管理
- 轮询间隔:5秒
- 自动停止轮询条件:钱包就绪或失败
- 支持手动重试钱包生成

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-20 19:59:02 -08:00
parent ceee3167cb
commit ed6c08914c
3 changed files with 358 additions and 22 deletions

View File

@ -522,6 +522,33 @@ class AccountService {
}
}
/// (POST /user/wallet/retry)
///
///
Future<void> retryWalletGeneration() async {
debugPrint('$_tag retryWalletGeneration() - 开始手动重试钱包生成');
try {
debugPrint('$_tag retryWalletGeneration() - 调用 POST /user/wallet/retry');
final response = await _apiClient.post('/user/wallet/retry');
debugPrint('$_tag retryWalletGeneration() - API 响应状态码: ${response.statusCode}');
if (response.statusCode == 200) {
debugPrint('$_tag retryWalletGeneration() - 重试请求已提交');
} else {
debugPrint('$_tag retryWalletGeneration() - 请求失败,状态码: ${response.statusCode}');
throw ApiException('重试钱包生成失败: ${response.statusCode}');
}
} on ApiException catch (e) {
debugPrint('$_tag retryWalletGeneration() - API 异常: $e');
rethrow;
} catch (e, stackTrace) {
debugPrint('$_tag retryWalletGeneration() - 未知异常: $e');
debugPrint('$_tag retryWalletGeneration() - 堆栈: $stackTrace');
throw ApiException('重试钱包生成失败: $e');
}
}
/// (GET /me)
///
///

View File

@ -0,0 +1,184 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/di/injection_container.dart';
import '../../../../core/services/account_service.dart';
import '../../../../core/storage/secure_storage.dart';
import '../../../../core/storage/storage_keys.dart';
///
enum WalletGenerationStatus {
unknown, //
generating, //
ready, //
failed, //
}
///
class WalletStatusState {
final WalletGenerationStatus status;
final String? errorMessage;
final DateTime? lastChecked;
final bool isPolling;
const WalletStatusState({
this.status = WalletGenerationStatus.unknown,
this.errorMessage,
this.lastChecked,
this.isPolling = false,
});
WalletStatusState copyWith({
WalletGenerationStatus? status,
String? errorMessage,
DateTime? lastChecked,
bool? isPolling,
}) {
return WalletStatusState(
status: status ?? this.status,
errorMessage: errorMessage,
lastChecked: lastChecked ?? this.lastChecked,
isPolling: isPolling ?? this.isPolling,
);
}
bool get isGenerating => status == WalletGenerationStatus.generating;
bool get isReady => status == WalletGenerationStatus.ready;
bool get isFailed => status == WalletGenerationStatus.failed;
}
///
class WalletStatusNotifier extends StateNotifier<WalletStatusState> {
final AccountService _accountService;
final SecureStorage _secureStorage;
Timer? _pollingTimer;
WalletStatusNotifier(this._accountService, this._secureStorage)
: super(const WalletStatusState());
///
///
/// 5
Future<void> startPolling() async {
if (state.isPolling) {
debugPrint('[WalletStatusProvider] Already polling, skipping');
return;
}
debugPrint('[WalletStatusProvider] Starting wallet status polling');
state = state.copyWith(isPolling: true);
//
await _checkWalletStatus();
// 5
_pollingTimer?.cancel();
_pollingTimer = Timer.periodic(const Duration(seconds: 5), (_) async {
await _checkWalletStatus();
//
if (state.isReady || state.isFailed) {
stopPolling();
}
});
}
///
void stopPolling() {
debugPrint('[WalletStatusProvider] Stopping wallet status polling');
_pollingTimer?.cancel();
_pollingTimer = null;
state = state.copyWith(isPolling: false);
}
///
Future<void> _checkWalletStatus() async {
try {
debugPrint('[WalletStatusProvider] Checking wallet status...');
//
final userSerialNum =
await _secureStorage.read(key: StorageKeys.userSerialNum);
if (userSerialNum == null) {
debugPrint(
'[WalletStatusProvider] No userSerialNum found, stopping polling');
stopPolling();
return;
}
// API
final walletInfo = await _accountService.getWalletInfo(userSerialNum);
debugPrint(
'[WalletStatusProvider] Wallet status: ${walletInfo.status}');
//
WalletGenerationStatus newStatus;
if (walletInfo.isReady) {
newStatus = WalletGenerationStatus.ready;
//
await _secureStorage.write(
key: StorageKeys.isWalletReady,
value: 'true',
);
debugPrint('[WalletStatusProvider] Wallet is ready, marked in storage');
} else if (walletInfo.isGenerating) {
newStatus = WalletGenerationStatus.generating;
} else if (walletInfo.isFailed) {
newStatus = WalletGenerationStatus.failed;
} else {
newStatus = WalletGenerationStatus.unknown;
}
state = state.copyWith(
status: newStatus,
lastChecked: DateTime.now(),
errorMessage: null,
);
} catch (e) {
debugPrint('[WalletStatusProvider] Error checking wallet status: $e');
state = state.copyWith(
errorMessage: e.toString(),
lastChecked: DateTime.now(),
);
}
}
///
Future<void> retryWalletGeneration() async {
try {
debugPrint('[WalletStatusProvider] Manually retrying wallet generation');
// API
await _accountService.retryWalletGeneration();
//
state = state.copyWith(
status: WalletGenerationStatus.generating,
errorMessage: null,
);
if (!state.isPolling) {
await startPolling();
}
} catch (e) {
debugPrint('[WalletStatusProvider] Error retrying wallet generation: $e');
state = state.copyWith(errorMessage: e.toString());
}
}
@override
void dispose() {
_pollingTimer?.cancel();
super.dispose();
}
}
/// Provider
final walletStatusProvider =
StateNotifierProvider<WalletStatusNotifier, WalletStatusState>((ref) {
final accountService = ref.watch(accountServiceProvider);
final secureStorage = ref.watch(secureStorageProvider);
return WalletStatusNotifier(accountService, secureStorage);
});

View File

@ -17,6 +17,7 @@ import '../../../../core/services/notification_service.dart';
import '../../../../routes/route_paths.dart';
import '../../../../routes/app_router.dart';
import '../../../auth/presentation/providers/auth_provider.dart';
import '../../../auth/presentation/providers/wallet_status_provider.dart';
import '../widgets/team_tree_widget.dart';
import '../widgets/stacked_cards_widget.dart';
import '../../../authorization/presentation/widgets/stickman_race_widget.dart';
@ -192,6 +193,10 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_loadUnreadNotificationCount();
//
_startAutoRefreshTimer();
//
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkAndStartWalletPolling();
});
}
///
@ -787,6 +792,17 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
}
}
///
Future<void> _checkAndStartWalletPolling() async {
final authState = ref.read(authProvider);
//
if (authState.isAccountCreated && !authState.isWalletReady) {
debugPrint('[ProfilePage] Account created but wallet not ready, starting polling');
await ref.read(walletStatusProvider.notifier).startPolling();
}
}
@override
void dispose() {
_timer?.cancel();
@ -795,6 +811,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_referralDebounceTimer?.cancel();
_authorizationDebounceTimer?.cancel();
_walletDebounceTimer?.cancel();
//
ref.read(walletStatusProvider.notifier).stopPolling();
super.dispose();
}
@ -1382,28 +1400,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
),
),
const SizedBox(height: 3),
Row(
children: [
Text(
'序列号: $_serialNumber',
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
height: 1.5,
color: Color(0xCC5D4037),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: _copySerialNumber,
child: const Icon(
Icons.copy,
size: 16,
color: Color(0xCC5D4037),
),
),
],
),
_buildSerialNumberOrStatus(),
],
),
),
@ -4023,4 +4020,132 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
}
}
}
///
Widget _buildSerialNumberOrStatus() {
final walletStatus = ref.watch(walletStatusProvider);
final authState = ref.watch(authProvider);
//
if (authState.isWalletReady || walletStatus.isReady) {
return Row(
children: [
Text(
'序列号: $_serialNumber',
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
height: 1.5,
color: Color(0xCC5D4037),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: _copySerialNumber,
child: const Icon(
Icons.copy,
size: 16,
color: Color(0xCC5D4037),
),
),
],
);
}
// "账号创建审核中"
if (authState.isAccountCreated && !authState.isWalletReady) {
return Row(
children: [
const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
),
),
const SizedBox(width: 8),
Text(
'账号创建审核中',
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
height: 1.5,
color: Color(0xFFD4AF37),
fontStyle: FontStyle.italic,
),
),
],
);
}
//
if (walletStatus.isFailed) {
return Row(
children: [
const Icon(
Icons.error_outline,
size: 16,
color: Color(0xFFD32F2F),
),
const SizedBox(width: 8),
Text(
'钱包生成失败',
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
height: 1.5,
color: Color(0xFFD32F2F),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () async {
await ref.read(walletStatusProvider.notifier).retryWalletGeneration();
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFD4AF37),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'重试',
style: TextStyle(
fontSize: 12,
fontFamily: 'Inter',
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
),
],
);
}
//
return Row(
children: [
Text(
'序列号: $_serialNumber',
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
height: 1.5,
color: Color(0xCC5D4037),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: _copySerialNumber,
child: const Icon(
Icons.copy,
size: 16,
color: Color(0xCC5D4037),
),
),
],
);
}
}