197 lines
7.0 KiB
Dart
197 lines
7.0 KiB
Dart
import 'dart:async';
|
||
import 'dart:convert';
|
||
import 'dart:math';
|
||
|
||
import 'package:android_id/android_id.dart';
|
||
import 'package:client_proxy_framework/client_proxy_framework.dart';
|
||
import 'package:crypto/crypto.dart' show md5;
|
||
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 '../user/user_state.dart';
|
||
|
||
const _prefsKeyFallbackDeviceId = 'persisted_device_id';
|
||
|
||
Future<String> _persistedFallbackDeviceId() async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
var id = prefs.getString(_prefsKeyFallbackDeviceId);
|
||
if (id != null && id.isNotEmpty) return id;
|
||
final random = Random.secure();
|
||
final bytes = List<int>.generate(16, (_) => random.nextInt(256));
|
||
id = base64UrlEncode(bytes).replaceAll('=', '');
|
||
await prefs.setString(_prefsKeyFallbackDeviceId, id);
|
||
return id;
|
||
}
|
||
|
||
class AppAuthCallbacks implements AuthServiceCallbacks {
|
||
/// 与 app_client 一致:Android 用 Settings.Secure.ANDROID_ID(android_id 包,非 Build.ID);
|
||
/// iOS 用 identifierForVendor;失败或其它平台用 SharedPreferences 持久化随机 id。
|
||
@override
|
||
Future<String> getDeviceId() async {
|
||
switch (defaultTargetPlatform) {
|
||
case TargetPlatform.android:
|
||
final androidId = await const AndroidId().getId();
|
||
if (androidId != null && androidId.isNotEmpty) {
|
||
return androidId;
|
||
}
|
||
return _persistedFallbackDeviceId();
|
||
case TargetPlatform.iOS:
|
||
final ios = await DeviceInfoPlugin().iosInfo;
|
||
final idfv = ios.identifierForVendor;
|
||
if (idfv != null && idfv.isNotEmpty) return idfv;
|
||
return _persistedFallbackDeviceId();
|
||
default:
|
||
return _persistedFallbackDeviceId();
|
||
}
|
||
}
|
||
|
||
@override
|
||
String computeSign(String deviceId) {
|
||
return md5.convert(utf8.encode(deviceId)).toString().toUpperCase();
|
||
}
|
||
|
||
@override
|
||
void onLoginSuccess(FastLoginResponse data) {
|
||
UserState.applyLogin(
|
||
userId: data.userId,
|
||
credits: data.credits,
|
||
avatar: data.avatar,
|
||
userName: data.userName,
|
||
);
|
||
unawaited(
|
||
AnalyticsEvents.trackRegisterIfNeeded(
|
||
firstRegister: data.firstRegister == true,
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
void onCommonInfoLoaded(CommonInfoResponse data) {
|
||
if (data.credits != null) UserState.setCredits(data.credits!);
|
||
if (data.avatar != null) UserState.setAvatar(data.avatar!);
|
||
if (data.userName != null) UserState.setUserName(data.userName!);
|
||
}
|
||
|
||
@override
|
||
void onLoginFailed(String msg) {
|
||
debugPrint('[AuthService] Login failed: $msg');
|
||
}
|
||
}
|
||
|
||
/// 应用侧登录封装;Adjust 归因桥已由框架 [FrameworkAuthService.init] 默认注册。
|
||
class AuthService {
|
||
static final _authCallbacks = AppAuthCallbacks();
|
||
|
||
static var _screenSecureListenerBound = false;
|
||
|
||
/// [ExtConfigRuntime.data] 初次为 `null` 时触发的 `_applyScreenSecure(null)` 与后续
|
||
/// common_info 触发的 `_applyScreenSecure(…)` 均为异步:若前者后完成会错误地关闭防护,故用序号丢弃过期任务。
|
||
static int _screenSecureApplyGeneration = 0;
|
||
|
||
static Future<void> init() async {
|
||
FrameworkAuthService.init(_authCallbacks);
|
||
_bindScreenSecureToExtConfig();
|
||
await FrameworkAuthService.start();
|
||
}
|
||
|
||
/// 与 [app_client] `AuthService.runWithNativeMediaPicker` 一致:系统相册/相机返回后若开启防截屏,
|
||
/// 部分机型会黑屏;选图前后临时关闭防护,结束后再按 [ExtConfigRuntime] 恢复。
|
||
static Future<T> runWithNativeMediaPicker<T>(Future<T> Function() action) async {
|
||
if (defaultTargetPlatform != TargetPlatform.android &&
|
||
defaultTargetPlatform != TargetPlatform.iOS) {
|
||
return await action();
|
||
}
|
||
try {
|
||
await ScreenSecure.disableScreenshotBlock();
|
||
await ScreenSecure.disableScreenRecordBlock();
|
||
} on ScreenSecureException catch (e) {
|
||
debugPrint('[AuthService] native media picker: disable failed: ${e.message}');
|
||
}
|
||
try {
|
||
return await action();
|
||
} finally {
|
||
unawaited(_applyScreenSecure(ExtConfigRuntime.data.value));
|
||
}
|
||
}
|
||
|
||
static void _bindScreenSecureToExtConfig() {
|
||
if (_screenSecureListenerBound) return;
|
||
_screenSecureListenerBound = true;
|
||
void listener() {
|
||
unawaited(_applyScreenSecure(ExtConfigRuntime.data.value));
|
||
}
|
||
|
||
ExtConfigRuntime.data.addListener(listener);
|
||
// 勿在此处同步调用 listener():`data == null` 的 disable 若晚于 common_info 的 enable 完成,会把防截屏关掉。
|
||
}
|
||
|
||
/// [ExtConfigData]:`screen` / `safe_area` 等键在 [ExtConfigKeySchema] 下解析为 [ExtConfigData.forbidScreenshot];
|
||
/// 为 `true` 时启用系统截屏/录屏防护(与 app_client `safe_area` 语义对齐)。
|
||
static Future<void> _applyScreenSecure(ExtConfigData? ext) async {
|
||
if (defaultTargetPlatform != TargetPlatform.android &&
|
||
defaultTargetPlatform != TargetPlatform.iOS) {
|
||
return;
|
||
}
|
||
final gen = ++_screenSecureApplyGeneration;
|
||
final block = ext?.forbidScreenshot == true;
|
||
try {
|
||
await ScreenSecure.init(screenshotBlock: false, screenRecordBlock: false);
|
||
if (gen != _screenSecureApplyGeneration) return;
|
||
if (block) {
|
||
await ScreenSecure.enableScreenshotBlock();
|
||
await ScreenSecure.enableScreenRecordBlock();
|
||
} else {
|
||
await ScreenSecure.disableScreenshotBlock();
|
||
await ScreenSecure.disableScreenRecordBlock();
|
||
}
|
||
if (gen != _screenSecureApplyGeneration) return;
|
||
if (kDebugMode && block) {
|
||
debugPrint('[AuthService] ScreenSecure: enabled (forbidScreenshot)');
|
||
}
|
||
} on ScreenSecureException catch (e) {
|
||
debugPrint('[AuthService] ScreenSecure: ${e.message}');
|
||
}
|
||
}
|
||
|
||
static Future<void> get loginComplete => FrameworkAuthService.loginComplete;
|
||
|
||
/// 登录流程是否已结束(含 fast_login + common_info 链路);用于遮罩,勿用 [loginComplete] 的 Future(首帧可能为 null)。
|
||
static ValueNotifier<bool> get isLoginComplete =>
|
||
FrameworkAuthService.isLoginComplete;
|
||
|
||
/// 在 **登录成功**([UserState.userId] 非空)且 [isLoginComplete] 为 `true` 之后执行 [onReady]。
|
||
///
|
||
/// 若登录流程已结束但无用户(失败),调用一次 [onFailed]。若 [isLoginComplete] 已为 `true`,同步执行。
|
||
///
|
||
/// 返回在 [State.dispose] 中调用的取消函数,避免界面已销毁仍监听。
|
||
static VoidCallback whenLoginSucceeded({
|
||
required VoidCallback onReady,
|
||
VoidCallback? onFailed,
|
||
}) {
|
||
void runOnce() {
|
||
final uid = UserState.userId.value;
|
||
if (uid != null && uid.isNotEmpty) {
|
||
onReady();
|
||
} else {
|
||
onFailed?.call();
|
||
}
|
||
}
|
||
|
||
void listener() {
|
||
if (!isLoginComplete.value) return;
|
||
isLoginComplete.removeListener(listener);
|
||
runOnce();
|
||
}
|
||
|
||
if (isLoginComplete.value) {
|
||
runOnce();
|
||
return () {};
|
||
}
|
||
|
||
isLoginComplete.addListener(listener);
|
||
return () => isLoginComplete.removeListener(listener);
|
||
}
|
||
}
|