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 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? _loginFuture; static bool _isInitialized = false; /// 登录是否已完成 static final ValueNotifier isLoginComplete = ValueNotifier(false); /// 最近一次快速登录成功时的 [FastLoginResponse.userId](失败或未登录为空)。 /// /// 宿主在调用需 `userId` 的接口(如生图 `create-task`)前可读取;若为空应提示用户稍后重试。 static String? lastLoggedInUserId; /// 登录完成后的 Future,需鉴权接口应 await 此 Future 再请求 static Future get loginComplete => _loginFuture ?? Future.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 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(); _loginFuture = completer.future; if (kDebugMode) { debugPrint('[AuthService] start: 开始登录流程'); } /// 供 [catch] 上报 Facebook 使用:若在 [getDeviceId] 成功前抛错则为空串。 var deviceIdForFacebookFailure = ''; try { await Future.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? res; for (var i = 0; i < maxRetries; i++) { if (i > 0) { if (kDebugMode) { debugPrint('[AuthService] start: 第 ${i + 1} 次重试...'); } await Future.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 _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? 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 = [ if (adjustMsg.trim().isNotEmpty) 'adjust: ${_truncateForFacebookParam(adjustMsg)}', if (ggMsg.trim().isNotEmpty) 'gg: ${_truncateForFacebookParam(ggMsg)}', ]; final combinedMsg = parts.join(' | '); final params = { '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 = { '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 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 = { 'server_error_code': '$serverCode', 'user_id': uid, 'device_id': did, if (missingUserId) 'register_faild': 'register faild', }; FacebookService.logEvent( facebookLoginFailedEventName, parameters: params, ); } }