531 lines
18 KiB
Dart
531 lines
18 KiB
Dart
import 'dart:async';
|
||
|
||
import 'package:flutter/foundation.dart';
|
||
|
||
import '../api/api_client.dart';
|
||
import '../api/proxy_client.dart';
|
||
import '../config/attribution_config.dart';
|
||
import '../config/ext_config_models.dart';
|
||
import '../config/ext_config_runtime.dart';
|
||
import '../config/video_home_runtime.dart';
|
||
import '../entities/user_entities.dart';
|
||
import 'adjust_service.dart';
|
||
import 'analytics_attribution_callbacks.dart';
|
||
import 'facebook_service.dart';
|
||
import 'login_identity_cache.dart';
|
||
import 'user_api.dart';
|
||
|
||
/// [FrameworkAuthService.start] 中 `fast_login` 在拿不到 Play Install Referrer 时使用的 `referer` 兜底(自然安装)。
|
||
const String _fastLoginPlayReferrerFallback =
|
||
'utm_source=google-play&utm_medium=organic';
|
||
|
||
/// 认证服务回调
|
||
/// 用于在认证流程各阶段通知调用方
|
||
abstract class AuthServiceCallbacks {
|
||
/// 获取设备 ID(由调用方实现)
|
||
Future<String> getDeviceId();
|
||
|
||
/// 计算签名(由调用方实现)
|
||
String computeSign(String deviceId);
|
||
|
||
/// 登录成功后回调
|
||
void onLoginSuccess(FastLoginResponse data) {}
|
||
|
||
/// 通用信息获取后回调
|
||
void onCommonInfoLoaded(CommonInfoResponse data) {}
|
||
|
||
/// 登录失败回调
|
||
void onLoginFailed(String msg) {}
|
||
}
|
||
|
||
/// APP 启动认证服务
|
||
/// 封装完整的启动登录流程,包括:
|
||
/// 1. 快速登录
|
||
/// 2. 归因上报
|
||
/// 3. 获取通用信息
|
||
///
|
||
/// **换皮默认行为**(无需宿主接入):
|
||
/// - `fast_login`:`type` **强制**为 `gg`(Google 归因);`referer` 优先 Google Play Install Referrer,取不到则使用 [_fastLoginPlayReferrerFallback],不再切换 Adjust/Facebook 的 type。
|
||
/// - 登录成功且 `userId` 非空:将 `userId` 与本次 `deviceId` 写入 [LoginIdentityCache]。
|
||
/// - 登录失败(含无响应、异常、`code != 0`、或成功体无 `userId`):上报 Facebook 自定义事件 [facebookLoginFailedEventName],
|
||
/// 参数:`server_error_code`、`user_id`(响应中有则带,否则空串)、`device_id`(本次启动已解析的设备 ID,若尚未取到则为空串);
|
||
/// 未能取得用户 ID 时另带 `register_faild` = `register faild`。
|
||
/// - [UserApi.getCommonInfo] 失败、请求异常,或成功但响应体无 `extConfig` 字符串:上报 Facebook [facebookExtConfigFailedEventName](`user_id`、`device_id`、`server_error_code`、可选 `server_error_msg`)。
|
||
abstract class FrameworkAuthService {
|
||
static AuthServiceCallbacks? _callbacks;
|
||
static Future<void>? _loginFuture;
|
||
static bool _isInitialized = false;
|
||
|
||
/// 登录是否已完成
|
||
static final ValueNotifier<bool> isLoginComplete = ValueNotifier(false);
|
||
|
||
/// 最近一次快速登录成功时的 [FastLoginResponse.userId](失败或未登录为空)。
|
||
///
|
||
/// 宿主在调用需 `userId` 的接口(如生图 `create-task`)前可读取;若为空应提示用户稍后重试。
|
||
static String? lastLoggedInUserId;
|
||
|
||
/// 登录完成后的 Future,需鉴权接口应 await 此 Future 再请求
|
||
static Future<void> get loginComplete => _loginFuture ?? Future<void>.value();
|
||
|
||
/// 初始化认证服务。
|
||
///
|
||
/// 默认注册 [AnalyticsAttributionCallbacks](与 Adjust 缓存一致)。可传入
|
||
/// [attributionCallbacks] 覆盖(例如自定义 referer 格式)。
|
||
static void init(
|
||
AuthServiceCallbacks callbacks, {
|
||
AttributionCallbacks? attributionCallbacks,
|
||
}) {
|
||
AttributionService.init(
|
||
attributionCallbacks ?? AnalyticsAttributionCallbacks(),
|
||
);
|
||
_callbacks = callbacks;
|
||
_isInitialized = true;
|
||
}
|
||
|
||
/// 启动登录流程
|
||
/// [delaySeconds] 启动延迟秒数,默认 2 秒
|
||
/// [maxRetries] 最大重试次数,默认 3 次
|
||
/// [retryDelaySeconds] 重试延迟秒数,默认 2 秒
|
||
static Future<void> start({
|
||
int delaySeconds = 2,
|
||
int maxRetries = 3,
|
||
int retryDelaySeconds = 2,
|
||
}) async {
|
||
if (!_isInitialized || _callbacks == null) {
|
||
throw StateError(
|
||
'FrameworkAuthService not initialized. '
|
||
'Call FrameworkAuthService.init(callbacks) first.',
|
||
);
|
||
}
|
||
if (_loginFuture != null) return _loginFuture!;
|
||
final completer = Completer<void>();
|
||
_loginFuture = completer.future;
|
||
|
||
if (kDebugMode) {
|
||
debugPrint('[AuthService] start: 开始登录流程');
|
||
}
|
||
|
||
/// 供 [catch] 上报 Facebook 使用:若在 [getDeviceId] 成功前抛错则为空串。
|
||
var deviceIdForFacebookFailure = '';
|
||
|
||
try {
|
||
await Future<void>.delayed(Duration(seconds: delaySeconds));
|
||
|
||
final deviceId = await _callbacks!.getDeviceId();
|
||
deviceIdForFacebookFailure = deviceId;
|
||
if (kDebugMode) {
|
||
debugPrint('[AuthService] start: deviceId=$deviceId');
|
||
}
|
||
|
||
final sign = _callbacks!.computeSign(deviceId);
|
||
if (kDebugMode) {
|
||
debugPrint('[AuthService] start: sign=$sign');
|
||
}
|
||
|
||
// fast_login:强制使用 Google 归因类型 `gg`;referer 优先 Play Install Referrer,无则自然安装 UTM 兜底。
|
||
final playReferrer = AdjustService.cachedPlayReferrer;
|
||
final fastLoginReferer = (playReferrer != null && playReferrer.isNotEmpty)
|
||
? playReferrer
|
||
: _fastLoginPlayReferrerFallback;
|
||
const fastLoginType = 'gg';
|
||
|
||
if (kDebugMode) {
|
||
debugPrint(
|
||
'[AuthService] start: fast_login type=$fastLoginType refererLen=${fastLoginReferer.length}',
|
||
);
|
||
}
|
||
|
||
// 尝试快速登录
|
||
EntityResponse<FastLoginResponse>? res;
|
||
for (var i = 0; i < maxRetries; i++) {
|
||
if (i > 0) {
|
||
if (kDebugMode) {
|
||
debugPrint('[AuthService] start: 第 ${i + 1} 次重试...');
|
||
}
|
||
await Future<void>.delayed(Duration(seconds: retryDelaySeconds));
|
||
}
|
||
try {
|
||
final cfg = ApiClient.instance.config;
|
||
final appType = defaultTargetPlatform == TargetPlatform.iOS
|
||
? cfg.backendAppTypeIOS
|
||
: cfg.backendAppTypeAndroid;
|
||
res = await UserApi.fastLogin(
|
||
deviceId: deviceId,
|
||
sign: sign,
|
||
referer: fastLoginReferer,
|
||
app: appType,
|
||
type: fastLoginType,
|
||
);
|
||
break;
|
||
} catch (e) {
|
||
if (kDebugMode) {
|
||
debugPrint('[AuthService] start: 第 ${i + 1} 次请求失败: $e');
|
||
}
|
||
if (i == maxRetries - 1) rethrow;
|
||
}
|
||
}
|
||
|
||
if (res == null) {
|
||
lastLoggedInUserId = null;
|
||
VideoHomeRuntime.reset();
|
||
ExtConfigRuntime.applyCommonInfoFailure();
|
||
_logFacebookLoginFailed(
|
||
serverCode: -1,
|
||
missingUserId: true,
|
||
userIdFromServer: null,
|
||
deviceId: deviceId,
|
||
);
|
||
completer.complete();
|
||
return;
|
||
}
|
||
|
||
if (kDebugMode) {
|
||
debugPrint('[AuthService] start: 登录结果 code=${res.code} msg=${res.msg}');
|
||
}
|
||
|
||
if (res.isSuccess && res.data != null) {
|
||
final loginData = res.data!;
|
||
final uid = loginData.userId?.trim();
|
||
lastLoggedInUserId = uid != null && uid.isNotEmpty ? uid : null;
|
||
|
||
// 设置 Token
|
||
if (loginData.userToken != null && loginData.userToken!.isNotEmpty) {
|
||
ApiClient.instance.setUserToken(loginData.userToken!);
|
||
if (kDebugMode) {
|
||
debugPrint('[AuthService] start: 已设置 userToken');
|
||
}
|
||
}
|
||
|
||
if (uid != null && uid.isNotEmpty) {
|
||
unawaited(
|
||
LoginIdentityCache.writeUserAndDeviceId(
|
||
userId: uid,
|
||
deviceId: deviceId,
|
||
),
|
||
);
|
||
} else {
|
||
_logFacebookLoginFailed(
|
||
serverCode: res.code,
|
||
missingUserId: true,
|
||
userIdFromServer: loginData.userId,
|
||
deviceId: deviceId,
|
||
);
|
||
}
|
||
|
||
// 回调登录成功
|
||
_callbacks!.onLoginSuccess(loginData);
|
||
|
||
// 上报归因并获取通用信息
|
||
await _reportReferrersAndLoadCommonInfo(
|
||
uid: loginData.userId ?? '',
|
||
deviceId: deviceId,
|
||
);
|
||
} else {
|
||
lastLoggedInUserId = null;
|
||
VideoHomeRuntime.reset();
|
||
ExtConfigRuntime.applyCommonInfoFailure();
|
||
_logFacebookLoginFailedFromResponse(res, deviceId: deviceId);
|
||
_callbacks!.onLoginFailed(res.msg);
|
||
}
|
||
} catch (e, st) {
|
||
lastLoggedInUserId = null;
|
||
VideoHomeRuntime.reset();
|
||
ExtConfigRuntime.applyCommonInfoFailure();
|
||
if (kDebugMode) {
|
||
debugPrint('[AuthService] start: 异常 $e\n$st');
|
||
}
|
||
_logFacebookLoginFailed(
|
||
serverCode: -1,
|
||
missingUserId: true,
|
||
userIdFromServer: null,
|
||
deviceId: deviceIdForFacebookFailure,
|
||
);
|
||
_callbacks!.onLoginFailed(e.toString());
|
||
} finally {
|
||
if (!completer.isCompleted) {
|
||
completer.complete();
|
||
isLoginComplete.value = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 上报归因并获取通用信息
|
||
static Future<void> _reportReferrersAndLoadCommonInfo({
|
||
required String uid,
|
||
required String deviceId,
|
||
}) async {
|
||
if (uid.isEmpty) {
|
||
VideoHomeRuntime.reset();
|
||
ExtConfigRuntime.applyCommonInfoFailure();
|
||
if (kDebugMode) {
|
||
debugPrint(
|
||
'[AuthService] common_info: 跳过(userId 为空),已标记 common_info 失败',
|
||
);
|
||
}
|
||
return;
|
||
}
|
||
|
||
final config = ApiClient.instance.config;
|
||
final backendApp = defaultTargetPlatform == TargetPlatform.iOS
|
||
? config.backendAppTypeIOS
|
||
: config.backendAppTypeAndroid;
|
||
|
||
// 上报 Adjust 归因
|
||
var adjustReferrerTried = false;
|
||
var adjustReferrerOk = false;
|
||
var adjustReferrerCode = 0;
|
||
var adjustReferrerMsg = '';
|
||
|
||
final adjustReferer = await AttributionService.getAdjustReferrer();
|
||
if (adjustReferer != null && adjustReferer.isNotEmpty) {
|
||
adjustReferrerTried = true;
|
||
final adjustType = defaultTargetPlatform == TargetPlatform.iOS
|
||
? 'ios_adjust'
|
||
: 'android_adjust';
|
||
try {
|
||
final rAdjust = await UserApi.referrer(
|
||
app: backendApp,
|
||
userId: uid,
|
||
referer: adjustReferer,
|
||
deviceId: deviceId,
|
||
type: adjustType,
|
||
);
|
||
if (rAdjust.isSuccess) {
|
||
adjustReferrerOk = true;
|
||
} else {
|
||
adjustReferrerCode = rAdjust.code;
|
||
adjustReferrerMsg = rAdjust.msg;
|
||
}
|
||
if (kDebugMode) {
|
||
debugPrint(
|
||
'[AuthService] referrer($adjustType): ${rAdjust.isSuccess ? "成功" : "失败"}');
|
||
}
|
||
} catch (e) {
|
||
adjustReferrerCode = -110;
|
||
adjustReferrerMsg = e.toString();
|
||
if (kDebugMode) {
|
||
debugPrint('[AuthService] referrer($adjustType): 异常 $e');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 上报 Google Play 归因(从 AdjustService 获取缓存的 referrer)
|
||
var ggReferrerTried = false;
|
||
var ggReferrerOk = false;
|
||
var ggReferrerCode = 0;
|
||
var ggReferrerMsg = '';
|
||
|
||
final playReferrer = AdjustService.cachedPlayReferrer;
|
||
if (playReferrer != null && playReferrer.isNotEmpty) {
|
||
ggReferrerTried = true;
|
||
try {
|
||
final rGg = await UserApi.referrer(
|
||
app: backendApp,
|
||
userId: uid,
|
||
referer: playReferrer,
|
||
deviceId: deviceId,
|
||
type: 'gg',
|
||
);
|
||
if (rGg.isSuccess) {
|
||
ggReferrerOk = true;
|
||
} else {
|
||
ggReferrerCode = rGg.code;
|
||
ggReferrerMsg = rGg.msg;
|
||
}
|
||
if (kDebugMode) {
|
||
debugPrint(
|
||
'[AuthService] referrer(gg): ${rGg.isSuccess ? "成功" : "失败"}');
|
||
}
|
||
} catch (e) {
|
||
ggReferrerCode = -110;
|
||
ggReferrerMsg = e.toString();
|
||
if (kDebugMode) {
|
||
debugPrint('[AuthService] referrer(gg): 异常 $e');
|
||
}
|
||
}
|
||
}
|
||
|
||
if (adjustReferrerTried &&
|
||
ggReferrerTried &&
|
||
!adjustReferrerOk &&
|
||
!ggReferrerOk) {
|
||
_logFacebookReferrerBothFailed(
|
||
userId: uid,
|
||
deviceId: deviceId,
|
||
adjustCode: adjustReferrerCode,
|
||
adjustMsg: adjustReferrerMsg,
|
||
ggCode: ggReferrerCode,
|
||
ggMsg: ggReferrerMsg,
|
||
);
|
||
}
|
||
|
||
// 获取通用信息
|
||
try {
|
||
final commonRes = await UserApi.getCommonInfo(
|
||
app: backendApp,
|
||
pkg: config.packageName,
|
||
userId: uid,
|
||
deviceId: deviceId,
|
||
);
|
||
if (commonRes.isSuccess && commonRes.data != null) {
|
||
final info = commonRes.data!;
|
||
final extRaw = info.extConfig?.trim();
|
||
final extConfigMissing = extRaw == null || extRaw.isEmpty;
|
||
ExtConfigRuntime.applyCommonInfoSuccess(info);
|
||
_callbacks?.onCommonInfoLoaded(info);
|
||
unawaited(
|
||
VideoHomeRuntime.hydrateAfterCommonInfo(
|
||
userId: uid,
|
||
app: backendApp,
|
||
),
|
||
);
|
||
if (kDebugMode) {
|
||
debugPrint('[AuthService] common_info: 获取成功');
|
||
}
|
||
if (extConfigMissing) {
|
||
_logFacebookExtConfigFailed(
|
||
userId: uid,
|
||
deviceId: deviceId,
|
||
serverCode: 0,
|
||
serverMsg: 'ext_config_missing',
|
||
);
|
||
}
|
||
} else {
|
||
VideoHomeRuntime.reset();
|
||
ExtConfigRuntime.applyCommonInfoFailure();
|
||
_logFacebookExtConfigFailed(
|
||
userId: uid,
|
||
deviceId: deviceId,
|
||
serverCode: commonRes.code,
|
||
serverMsg: commonRes.msg,
|
||
);
|
||
if (kDebugMode) {
|
||
debugPrint(
|
||
'[AuthService] common_info: 失败 code=${commonRes.code} msg=${commonRes.msg}');
|
||
}
|
||
}
|
||
} catch (e) {
|
||
VideoHomeRuntime.reset();
|
||
ExtConfigRuntime.applyCommonInfoFailure();
|
||
_logFacebookExtConfigFailed(
|
||
userId: uid,
|
||
deviceId: deviceId,
|
||
serverCode: -1,
|
||
serverMsg: e.toString(),
|
||
);
|
||
if (kDebugMode) {
|
||
debugPrint('[AuthService] common_info: 异常 $e');
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 解析 extConfig JSON 字符串为 Map(兼容旧代码;结构化解析请用 [ExtConfigData.parse])。
|
||
static Map<String, dynamic>? parseExtConfig(String? extConfigStr) {
|
||
return ExtConfigData.parseRawMap(extConfigStr);
|
||
}
|
||
|
||
/// Facebook 自定义事件名(与产品约定一致:`LoginFaild`)。
|
||
static const String facebookLoginFailedEventName = 'LoginFaild';
|
||
|
||
/// 两次归因上报(Adjust + `gg`)均请求且均失败时上报(与产品约定:`Referer_faild`)。
|
||
static const String facebookReferrerBothFailedEventName = 'Referer_faild';
|
||
|
||
/// `common_info` 失败或响应中无 `extConfig` 时上报(与产品约定:`ExtConfigFaild`)。
|
||
static const String facebookExtConfigFailedEventName = 'ExtConfigFaild';
|
||
|
||
static const int _facebookParamMaxLen = 500;
|
||
|
||
static String _truncateForFacebookParam(String s) {
|
||
final t = s.trim();
|
||
if (t.length <= _facebookParamMaxLen) return t;
|
||
return '${t.substring(0, _facebookParamMaxLen)}…';
|
||
}
|
||
|
||
/// Adjust 与 Google Play 两条 [UserApi.referrer] 都发起且都未成功时调用(不阻塞、失败静默)。
|
||
static void _logFacebookReferrerBothFailed({
|
||
required String userId,
|
||
required String deviceId,
|
||
required int adjustCode,
|
||
required String adjustMsg,
|
||
required int ggCode,
|
||
required String ggMsg,
|
||
}) {
|
||
final parts = <String>[
|
||
if (adjustMsg.trim().isNotEmpty)
|
||
'adjust: ${_truncateForFacebookParam(adjustMsg)}',
|
||
if (ggMsg.trim().isNotEmpty) 'gg: ${_truncateForFacebookParam(ggMsg)}',
|
||
];
|
||
final combinedMsg = parts.join(' | ');
|
||
final params = <String, dynamic>{
|
||
'user_id': userId.trim(),
|
||
'device_id': deviceId.trim(),
|
||
'server_error_code': '$adjustCode/$ggCode',
|
||
if (combinedMsg.isNotEmpty) 'server_error_msg': combinedMsg,
|
||
};
|
||
FacebookService.logEvent(
|
||
facebookReferrerBothFailedEventName,
|
||
parameters: params,
|
||
);
|
||
}
|
||
|
||
/// [UserApi.getCommonInfo] 失败、异常,或成功但缺少 `extConfig` 时调用(不阻塞、失败静默)。
|
||
static void _logFacebookExtConfigFailed({
|
||
required String userId,
|
||
required String deviceId,
|
||
required int serverCode,
|
||
String serverMsg = '',
|
||
}) {
|
||
final msg = serverMsg.trim();
|
||
final params = <String, dynamic>{
|
||
'user_id': userId.trim(),
|
||
'device_id': deviceId.trim(),
|
||
'server_error_code': '$serverCode',
|
||
if (msg.isNotEmpty) 'server_error_msg': _truncateForFacebookParam(msg),
|
||
};
|
||
FacebookService.logEvent(
|
||
facebookExtConfigFailedEventName,
|
||
parameters: params,
|
||
);
|
||
}
|
||
|
||
static void _logFacebookLoginFailedFromResponse(
|
||
EntityResponse<FastLoginResponse> res, {
|
||
required String deviceId,
|
||
}) {
|
||
final uid = res.data?.userId?.trim();
|
||
final missingUserId = uid == null || uid.isEmpty;
|
||
_logFacebookLoginFailed(
|
||
serverCode: res.code,
|
||
missingUserId: missingUserId,
|
||
userIdFromServer: res.data?.userId,
|
||
deviceId: deviceId,
|
||
);
|
||
}
|
||
|
||
/// 登录失败或未拿到用户 ID 时上报 Meta / Facebook App Events(不阻塞、失败静默)。
|
||
///
|
||
/// - [serverCode]:接口 [EntityResponse.code];无响应或异常时为 `-1`。
|
||
/// - [userIdFromServer]:解密后响应里的 `userId`;无则上报空串。
|
||
/// - [deviceId]:本次流程使用的设备 ID;未取到时为空串。
|
||
/// - [missingUserId] 为 `true` 时附带参数 `register_faild` = `register faild`。
|
||
static void _logFacebookLoginFailed({
|
||
required int serverCode,
|
||
required bool missingUserId,
|
||
String? userIdFromServer,
|
||
required String deviceId,
|
||
}) {
|
||
final uid = userIdFromServer?.trim() ?? '';
|
||
final did = deviceId.trim();
|
||
final params = <String, dynamic>{
|
||
'server_error_code': '$serverCode',
|
||
'user_id': uid,
|
||
'device_id': did,
|
||
if (missingUserId) 'register_faild': 'register faild',
|
||
};
|
||
FacebookService.logEvent(
|
||
facebookLoginFailedEventName,
|
||
parameters: params,
|
||
);
|
||
}
|
||
}
|