FunyMeeAI/lib/core/auth/auth_service.dart

200 lines
7.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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_IDandroid_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) {
final t = data.avatar!.trim();
UserState.setAvatar(t.isEmpty ? null : t);
}
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);
}
}