feat(reward): 新增客户端奖励中心
This commit is contained in:
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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user