316 lines
11 KiB
Dart
316 lines
11 KiB
Dart
import 'dart:async';
|
||
import 'dart:convert';
|
||
|
||
import 'package:collection/collection.dart';
|
||
import 'package:crypto/crypto.dart';
|
||
import 'package:device_info_plus/device_info_plus.dart';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:screen_secure/screen_secure.dart';
|
||
import 'package:shared_preferences/shared_preferences.dart';
|
||
|
||
import '../adjust/adjust_events.dart';
|
||
import '../api/api_client.dart';
|
||
import '../api/api_config.dart';
|
||
import '../api/proxy_client.dart';
|
||
import '../api/services/user_api.dart';
|
||
import '../log/app_logger.dart';
|
||
import '../referrer/referrer_service.dart';
|
||
import '../user/user_state.dart';
|
||
|
||
/// 用于判断 common_info 是否改变了首页结构(分类栏 / 列表数据源)
|
||
class _HomeExtSnapshot {
|
||
_HomeExtSnapshot(this.needWait, this.items);
|
||
|
||
final bool? needWait;
|
||
final List<dynamic>? items;
|
||
|
||
factory _HomeExtSnapshot.capture() {
|
||
final raw = UserState.extConfigItems.value;
|
||
return _HomeExtSnapshot(
|
||
UserState.needShowVideoMenu.value,
|
||
raw == null ? null : List<dynamic>.from(raw),
|
||
);
|
||
}
|
||
|
||
static bool needsFullHomeReload(_HomeExtSnapshot before, _HomeExtSnapshot after) {
|
||
if (before.needWait != after.needWait) return true;
|
||
return !const DeepCollectionEquality().equals(before.items, after.items);
|
||
}
|
||
}
|
||
|
||
/// 认证服务:APP 启动时执行快速登录
|
||
class AuthService {
|
||
AuthService._();
|
||
|
||
static final _log = AppLogger('AuthService');
|
||
|
||
static Future<void>? _loginFuture;
|
||
|
||
/// 登录是否已完成,用于 UI 控制(如登录中隐藏页面加载指示器)
|
||
static final ValueNotifier<bool> isLoginComplete = ValueNotifier(false);
|
||
|
||
/// 登录完成后的 Future,需鉴权接口应 await 此 Future 再请求
|
||
static Future<void> get loginComplete => _loginFuture ?? Future<void>.value();
|
||
|
||
static void _logMsg(String msg) {
|
||
_log.d(msg);
|
||
}
|
||
|
||
/// 获取设备 ID(Android: androidId, iOS: identifierForVendor, Web: fallback)
|
||
static Future<String> _getDeviceId() async {
|
||
final deviceInfo = DeviceInfoPlugin();
|
||
switch (defaultTargetPlatform) {
|
||
case TargetPlatform.android:
|
||
final android = await deviceInfo.androidInfo;
|
||
return android.id;
|
||
case TargetPlatform.iOS:
|
||
final ios = await deviceInfo.iosInfo;
|
||
return ios.identifierForVendor ?? 'ios-unknown';
|
||
default:
|
||
return 'device-${DateTime.now().millisecondsSinceEpoch}';
|
||
}
|
||
}
|
||
|
||
/// 计算 sign:MD5(deviceId) 大写 32 位
|
||
static String _computeSign(String deviceId) {
|
||
final bytes = utf8.encode(deviceId);
|
||
final digest = md5.convert(bytes);
|
||
return digest.toString().toUpperCase();
|
||
}
|
||
|
||
/// 根据 extConfig.safe_area 启用/禁用系统截屏防护
|
||
static Future<void> _applyScreenSecure(bool? safeArea) async {
|
||
if (defaultTargetPlatform != TargetPlatform.android &&
|
||
defaultTargetPlatform != TargetPlatform.iOS) {
|
||
return;
|
||
}
|
||
try {
|
||
await ScreenSecure.init(screenshotBlock: false, screenRecordBlock: false);
|
||
if (safeArea == true) {
|
||
await ScreenSecure.enableScreenshotBlock();
|
||
await ScreenSecure.enableScreenRecordBlock();
|
||
_logMsg('safe_area=true: 已启用截屏/录屏防护');
|
||
} else {
|
||
await ScreenSecure.disableScreenshotBlock();
|
||
await ScreenSecure.disableScreenRecordBlock();
|
||
_logMsg('safe_area=$safeArea: 已关闭截屏/录屏防护');
|
||
}
|
||
} on ScreenSecureException catch (e) {
|
||
_logMsg('ScreenSecure 设置失败: ${e.message}');
|
||
}
|
||
}
|
||
|
||
/// 将 common_info 响应保存到全局,并解析 surge 中的 lucky(是否开启三方支付)
|
||
static void _saveCommonInfoToState(Map<String, dynamic> data) {
|
||
final reveal = data['reveal'] as int?;
|
||
if (reveal != null) UserState.setCredits(reveal);
|
||
final realm = data['realm'] as String?;
|
||
if (realm != null && realm.isNotEmpty) UserState.setAvatar(realm);
|
||
final terminal = data['terminal'] as String?;
|
||
if (terminal != null && terminal.isNotEmpty) {
|
||
UserState.setUserName(terminal);
|
||
}
|
||
final navigate = data['navigate'] as String?;
|
||
if (navigate != null) UserState.setNavigate(navigate);
|
||
|
||
final surgeStr = data['surge'] as String?;
|
||
if (surgeStr != null && surgeStr.isNotEmpty) {
|
||
try {
|
||
final surge = json.decode(surgeStr) as Map<String, dynamic>?;
|
||
if (surge != null) {
|
||
final enable = surge['lucky'] as bool?;
|
||
UserState.setEnableThirdPartyPayment(enable);
|
||
// extConfig:need_wait = 是否展示 Video 菜单,safe_area = 是否防止截屏,items = 图片列表(见 docs/extConfig.md)
|
||
final needWait = surge['need_wait'] as bool?;
|
||
final safeArea = surge['safe_area'] as bool?;
|
||
final items = surge['items'] as List<dynamic>?;
|
||
UserState.setExtConfig(
|
||
needShowVideoMenuValue: needWait,
|
||
safeAreaValue: safeArea,
|
||
items: items,
|
||
);
|
||
_applyScreenSecure(safeArea);
|
||
}
|
||
} catch (e) {
|
||
_logMsg('surge JSON 解析失败: $e');
|
||
}
|
||
}
|
||
}
|
||
|
||
/// APP 启动时调用快速登录
|
||
/// 启动时网络可能未就绪,会延迟后重试
|
||
static Future<void> init() async {
|
||
if (_loginFuture != null) return _loginFuture!;
|
||
final completer = Completer<void>();
|
||
_loginFuture = completer.future;
|
||
|
||
_logMsg('init: 开始快速登录');
|
||
const maxRetries = 3;
|
||
const retryDelay = Duration(seconds: 2);
|
||
|
||
try {
|
||
// 等待网络就绪(浏览器能访问但 App 报错时,多为启动时网络未初始化)
|
||
await Future<void>.delayed(const Duration(seconds: 2));
|
||
|
||
final deviceId = await _getDeviceId();
|
||
_logMsg('init: deviceId=$deviceId');
|
||
|
||
final sign = _computeSign(deviceId);
|
||
_logMsg('init: sign=$sign');
|
||
|
||
final crest = await ReferrerService.getReferrer();
|
||
if (crest != null && crest.isNotEmpty) {
|
||
_logMsg('init: crest(referrer)=$crest');
|
||
}
|
||
|
||
ApiResponse? res;
|
||
for (var i = 0; i < maxRetries; i++) {
|
||
if (i > 0) {
|
||
_logMsg('init: 第 ${i + 1} 次重试,等待 ${retryDelay.inSeconds}s...');
|
||
await Future<void>.delayed(retryDelay);
|
||
}
|
||
try {
|
||
res = await UserApi.fastLogin(
|
||
origin: deviceId,
|
||
resolution: sign,
|
||
digest: crest ?? '',
|
||
crest: crest,
|
||
);
|
||
break;
|
||
} catch (e) {
|
||
_logMsg('init: 第 ${i + 1} 次请求失败: $e');
|
||
if (i == maxRetries - 1) rethrow;
|
||
}
|
||
}
|
||
|
||
if (res == null) return;
|
||
|
||
_logMsg('init: 登录结果 code=${res.code} msg=${res.msg}');
|
||
_logMsg('init: 登录响应 data=${res.data}');
|
||
|
||
if (res.isSuccess && res.data != null) {
|
||
final data = res.data as Map<String, dynamic>?;
|
||
final token = data?['reevaluate'] as String?;
|
||
if (token != null && token.isNotEmpty) {
|
||
ApiClient.instance.setUserToken(token);
|
||
_logMsg('init: 已设置 userToken');
|
||
} else {
|
||
_logMsg('init: 响应中无 reevaluate (userToken)');
|
||
}
|
||
final prefs = await SharedPreferences.getInstance();
|
||
final hadLoggedIn = prefs.getBool('adjust_has_logged_in') ?? false;
|
||
if (!hadLoggedIn) {
|
||
AdjustEvents.trackRegister();
|
||
await prefs.setBool('adjust_has_logged_in', true);
|
||
await prefs.setString(
|
||
'adjust_register_date',
|
||
DateTime.now().toIso8601String().substring(0, 10),
|
||
);
|
||
_logMsg('init: 首次登录,已上报 register');
|
||
}
|
||
final credits = data?['reveal'] as int?;
|
||
if (credits != null) {
|
||
UserState.setCredits(credits);
|
||
_logMsg('init: 已同步积分 $credits');
|
||
}
|
||
final uid = data?['asset'] as String?;
|
||
if (uid != null && uid.isNotEmpty) {
|
||
UserState.setUserId(uid);
|
||
_logMsg('init: 已设置 userId');
|
||
}
|
||
final avatarUrl = data?['realm'] as String?;
|
||
if (avatarUrl != null && avatarUrl.isNotEmpty) {
|
||
UserState.setAvatar(avatarUrl);
|
||
}
|
||
final name = data?['terminal'] as String?;
|
||
if (name != null && name.isNotEmpty) {
|
||
UserState.setUserName(name);
|
||
}
|
||
final countryCode = data?['navigate'] as String?;
|
||
if (countryCode != null) {
|
||
UserState.setNavigate(countryCode);
|
||
}
|
||
|
||
// 3. 归因:android_adjust、gg 各上报一次(均等待响应);再拉一次 common_info,extConfig 影响首页时重载分类/任务
|
||
try {
|
||
await _reportBothReferrersAndRefreshCommonInfo(uid!, deviceId);
|
||
} catch (e) {
|
||
_logMsg('referrer/common_info 流程异常: $e');
|
||
}
|
||
} else {
|
||
_logMsg('init: 登录失败');
|
||
}
|
||
} catch (e, st) {
|
||
_logMsg('init: 异常 $e');
|
||
_logMsg('init: 堆栈 $st');
|
||
} finally {
|
||
if (!completer.isCompleted) {
|
||
completer.complete();
|
||
isLoginComplete.value = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
static bool _applyCommonInfoAndDidHomeStructureChange(ApiResponse commonRes) {
|
||
if (!commonRes.isSuccess || commonRes.data == null) return false;
|
||
final commonData = commonRes.data as Map<String, dynamic>?;
|
||
if (commonData == null) return false;
|
||
final before = _HomeExtSnapshot.capture();
|
||
_saveCommonInfoToState(commonData);
|
||
final after = _HomeExtSnapshot.capture();
|
||
final changed = _HomeExtSnapshot.needsFullHomeReload(before, after);
|
||
if (changed) {
|
||
_logMsg('common_info 已更新,need_wait/items 相对上次有结构性变化');
|
||
}
|
||
_logMsg('common_info 响应已应用');
|
||
return changed;
|
||
}
|
||
|
||
/// 依次上报 android_adjust、gg(均等待服务端返回,成败都继续);结束后统一拉 common_info 更新 extConfig
|
||
static Future<void> _reportBothReferrersAndRefreshCommonInfo(
|
||
String uid,
|
||
String deviceId,
|
||
) async {
|
||
final adjustDigest = await ReferrerService.getAdjustReferrerDigest();
|
||
final ggDigest = await ReferrerService.getGgReferrerDigest();
|
||
|
||
final rAdjust = await UserApi.referrer(
|
||
sentinel: ApiConfig.appId,
|
||
asset: uid,
|
||
digest: adjustDigest,
|
||
origin: deviceId,
|
||
accolade: 'android_adjust',
|
||
);
|
||
if (rAdjust.isSuccess) {
|
||
_logMsg('referrer(android_adjust) 成功');
|
||
} else {
|
||
_logMsg(
|
||
'referrer(android_adjust) 失败: code=${rAdjust.code} msg=${rAdjust.msg}');
|
||
}
|
||
|
||
final rGg = await UserApi.referrer(
|
||
sentinel: ApiConfig.appId,
|
||
asset: uid,
|
||
digest: ggDigest,
|
||
origin: deviceId,
|
||
accolade: 'gg',
|
||
);
|
||
if (rGg.isSuccess) {
|
||
_logMsg('referrer(gg) 成功');
|
||
} else {
|
||
_logMsg('referrer(gg) 失败: code=${rGg.code} msg=${rGg.msg}');
|
||
}
|
||
|
||
final commonRes = await UserApi.getCommonInfo(
|
||
sentinel: ApiConfig.appId,
|
||
asset: uid,
|
||
);
|
||
if (_applyCommonInfoAndDidHomeStructureChange(commonRes)) {
|
||
UserState.requestHomeFullReload();
|
||
} else if (!commonRes.isSuccess) {
|
||
_logMsg(
|
||
'common_info 失败: code=${commonRes.code} msg=${commonRes.msg}');
|
||
}
|
||
}
|
||
}
|