feat(reward): 新增客户端奖励中心

This commit is contained in:
Zhou Weidong committed 2026-11-13 13:43:38 +08:00
1 parent 67670d46b1
commit bea08110c9
68 files changed
+3987 -332

No files matched your search

@@ -1,5 +1,6 @@
// Author: fruit.com.cn
import 'dart:async';
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart';
@@ -58,6 +59,10 @@ class GoogleAuthService {
static const String _serverClientId =
'11115008580-dbqe47fhp5560nhrocrppiq7hcnlvl1s.apps.googleusercontent.com';
/// 授权等待上限:插件在账号被撤销后可能永不返回,必须有超时兜底,
/// 否则页面 loading 状态会一直卡住、按钮再也点不动。
static const Duration _authTimeout = Duration(seconds: 60);
final GoogleSignIn _signIn;
bool _initialized = false;
@@ -68,22 +73,41 @@ class GoogleAuthService {
if (!_signIn.supportsAuthenticate()) {
return const GoogleSignInOutcome.unavailable();
}
final account = await _signIn.authenticate();
// 先清掉插件缓存的旧账号:用户可能在 Google 侧撤销过授权或切换过账号,
// 残留状态会让 authenticate 直接返回失效账号或干脆不弹授权页。
await _clearStaleAccount();
final account = await _signIn.authenticate().timeout(_authTimeout);
final idToken = account.authentication.idToken;
if (idToken == null || idToken.isEmpty) {
return const GoogleSignInOutcome.failed();
}
return GoogleSignInOutcome.success(idToken);
} on TimeoutException {
await _clearStaleAccount();
return const GoogleSignInOutcome.failed();
} on GoogleSignInException catch (error) {
if (error.code == GoogleSignInExceptionCode.canceled) {
return const GoogleSignInOutcome.canceled();
}
// 失败多半是插件缓存了已失效的账号(用户手动撤销授权、换账号),
// 清掉本地状态,下次点击才能重新拉起授权页。
await _clearStaleAccount();
return const GoogleSignInOutcome.failed();
} catch (_) {
await _clearStaleAccount();
return const GoogleSignInOutcome.failed();
}
}
/// 清掉插件缓存的 Google 账号;失败不影响登录结果,异常直接吞掉。
Future<void> _clearStaleAccount() async {
try {
await _signIn.signOut();
} catch (_) {
// 忽略:清缓存失败不应阻断页面恢复
}
}
/// 插件要求先 initialize 才能 authenticate;只做一次。
Future<void> _ensureInitialized() async {
if (_initialized) return;
@@ -0,0 +1,108 @@
// Author: fruit.com.cn
import 'package:flutter/foundation.dart';
import '../models/reward.dart';
import '../remote/api_exception.dart';
import '../repositories/reward_repository.dart';
/// 奖励中心状态与签到、领取动作。
class RewardController extends ChangeNotifier {
RewardController(this._repository);
final RewardRepository _repository;
RewardOverview? overview;
bool loading = false;
bool checkingIn = false;
String? claimingTaskId;
String? error;
String? actionError;
bool _disposed = false;
String _locale = 'id';
Future<void> load(String locale) async {
if (_disposed || loading) return;
_locale = locale;
loading = true;
error = null;
_notify();
try {
overview = await _repository.fetchOverview(locale);
} on ApiException catch (exception) {
error = exception.message;
} catch (_) {
error = 'rewardLoadFailed';
} finally {
loading = false;
_notify();
}
}
/// 签到成功后刷新总览,确保签到格和余额来自服务端事实。
Future<RewardActionResult?> checkIn() async {
if (_disposed || checkingIn) return null;
checkingIn = true;
actionError = null;
_notify();
try {
final result = await _repository.checkIn();
await _reload();
return result;
} on ApiException catch (exception) {
actionError = exception.message;
return null;
} catch (_) {
actionError = 'rewardActionFailed';
return null;
} finally {
checkingIn = false;
_notify();
}
}
/// 领取任务奖励;同一时刻只允许一个领取动作。
Future<RewardActionResult?> claim(String taskId) async {
if (_disposed || claimingTaskId != null) return null;
claimingTaskId = taskId;
actionError = null;
_notify();
try {
final result = await _repository.claim(taskId);
await _reload();
return result;
} on ApiException catch (exception) {
actionError = exception.message;
return null;
} catch (_) {
actionError = 'rewardActionFailed';
return null;
} finally {
claimingTaskId = null;
_notify();
}
}
void reset() {
overview = null;
loading = false;
checkingIn = false;
claimingTaskId = null;
error = null;
actionError = null;
_notify();
}
Future<void> _reload() async {
overview = await _repository.fetchOverview(_locale);
}
void _notify() {
if (!_disposed) notifyListeners();
}
@override
void dispose() {
_disposed = true;
super.dispose();
}
}