diff --git a/android/app/build.gradle b/android/app/build.gradle index 5424e86..614c07e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -59,6 +59,11 @@ android { targetSdk 36 versionCode flutterVersionCode.toInteger() versionName flutterVersionName + // Flutter 引擎仅提供 arm + x86_64 的 libflutter.so,不再提供 32 位 x86;勿加 x86,否则 32 位模拟器会缺 .so + // Intel/AMD 模拟器请用 **x86_64** 系统镜像(非 x86) + ndk { + abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64' + } // Facebook SDK 调试:正式包调试时设为 true,上线前改回 false buildConfigField "boolean", "FACEBOOK_DEBUG_LOGS", (localProperties.getProperty("facebook.debug") ?: "true") } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e827b43..752a04a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -34,6 +34,12 @@ + + $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + + FacebookAutoLogAppEventsEnabled + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/lib/app.dart b/lib/app.dart index e935d8d..1b10cc1 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,6 +1,9 @@ +import 'package:facebook_app_events/facebook_app_events.dart'; import 'package:flutter/material.dart'; import 'core/auth/auth_service.dart'; +import 'core/config/facebook_config.dart'; +import 'core/log/app_logger.dart'; import 'core/theme/app_colors.dart'; import 'core/theme/app_theme.dart'; import 'core/user/user_state.dart'; @@ -24,9 +27,45 @@ class App extends StatefulWidget { State createState() => _AppState(); } -class _AppState extends State { +class _AppState extends State with WidgetsBindingObserver { NavTab _currentTab = NavTab.home; + static final _fbLog = AppLogger('FB'); + final FacebookAppEvents _fbAppEvents = FacebookAppEvents(); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + // 冷启动时进程已在 resumed,didChangeAppLifecycleState 往往收不到「变为 resumed」,需首帧后再手动打一次 + WidgetsBinding.instance.addPostFrameCallback((_) { + _reportFacebookActivateApp('first_frame'); + }); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + /// `AutoLogAppEventsEnabled=false` 时手动上报 Facebook **安装 + 应用激活**(activateApp) + void _reportFacebookActivateApp(String reason) { + _fbAppEvents.activateApp().then((_) { + if (FacebookConfig.debugLogs) { + _fbLog.d('activateApp(手动: $reason)'); + } + }); + } + + /// 从后台回到前台时触发;冷启动依赖 initState 里的 addPostFrameCallback + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + _reportFacebookActivateApp('lifecycle_resumed'); + } + } + @override Widget build(BuildContext context) { return UserCreditsScope( diff --git a/lib/core/adjust/adjust_events.dart b/lib/core/adjust/adjust_events.dart index bb1aefe..d006579 100644 --- a/lib/core/adjust/adjust_events.dart +++ b/lib/core/adjust/adjust_events.dart @@ -95,7 +95,7 @@ abstract final class AdjustEvents { _trackFb('payment_failed', () => _fb.logEvent(name: 'payment_failed')); } - /// 注册(首次 fast_login 成功) + /// 注册(fast_login 返回 equip=true / firstRegister 时) static void trackRegister() { _track(register); _trackFb('CompletedRegistration', diff --git a/lib/core/auth/auth_service.dart b/lib/core/auth/auth_service.dart index 59ee0fd..91cabd5 100644 --- a/lib/core/auth/auth_service.dart +++ b/lib/core/auth/auth_service.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:math'; +import 'package:android_id/android_id.dart'; import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart'; import 'package:device_info_plus/device_info_plus.dart'; @@ -10,6 +12,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../adjust/adjust_events.dart'; import '../api/api_client.dart'; +import 'auth_token_store.dart'; import '../api/api_config.dart'; import '../api/proxy_client.dart'; import '../api/services/user_api.dart'; @@ -56,21 +59,45 @@ class AuthService { _log.d(msg); } - /// 获取设备 ID(Android: androidId, iOS: identifierForVendor, Web: fallback) + static const _prefsKeyFallbackDeviceId = 'persisted_device_id'; + + /// 设备唯一标识(用于 fast_login origin 等)。 + /// + /// - **Android**:`Settings.Secure.ANDROID_ID`([`android_id`](https://pub.dev/packages/android_id))。\ + /// 注意:`device_info_plus` 的 `AndroidDeviceInfo.id` 实为 **`Build.ID`(ROM 构建号)**,同版本机型大量相同,**不能**当设备 ID。 + /// - **iOS**:`identifierForVendor`(同厂商应用间稳定;卸载同厂商全部应用后会变)。 + /// - **其它 / 读失败**:写入 SharedPreferences 的随机 id,保证进程内稳定且极低碰撞概率。 static Future _getDeviceId() async { - final deviceInfo = DeviceInfoPlugin(); switch (defaultTargetPlatform) { case TargetPlatform.android: - final android = await deviceInfo.androidInfo; - return android.id; + final androidId = await const AndroidId().getId(); + if (androidId != null && androidId.isNotEmpty) { + return androidId; + } + _logMsg('_getDeviceId: ANDROID_ID 为空,使用本地持久化 fallback'); + return _persistedFallbackDeviceId(); case TargetPlatform.iOS: - final ios = await deviceInfo.iosInfo; - return ios.identifierForVendor ?? 'ios-unknown'; + final ios = await DeviceInfoPlugin().iosInfo; + final idfv = ios.identifierForVendor; + if (idfv != null && idfv.isNotEmpty) return idfv; + return _persistedFallbackDeviceId(); default: - return 'device-${DateTime.now().millisecondsSinceEpoch}'; + return _persistedFallbackDeviceId(); } } + static Future _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.generate(16, (_) => random.nextInt(256)); + id = base64UrlEncode(bytes).replaceAll('=', ''); + await prefs.setString(_prefsKeyFallbackDeviceId, id); + _logMsg('_getDeviceId: 已生成并持久化 fallback id'); + return id; + } + /// 计算 sign:MD5(deviceId) 大写 32 位 static String _computeSign(String deviceId) { final bytes = utf8.encode(deviceId); @@ -163,6 +190,12 @@ class AuthService { _logMsg('init: crest(referrer)=$crest'); } + await AuthTokenStore.restoreToApiClient(); + if (ApiClient.instance.proxy.userToken != null && + ApiClient.instance.proxy.userToken!.isNotEmpty) { + _logMsg('init: 已用本地 token 注入请求头,本次 fast_login 将携带 knight'); + } + ApiResponse? res; for (var i = 0; i < maxRetries; i++) { if (i > 0) { @@ -193,20 +226,25 @@ class AuthService { final token = data?['reevaluate'] as String?; if (token != null && token.isNotEmpty) { ApiClient.instance.setUserToken(token); - _logMsg('init: 已设置 userToken'); + await AuthTokenStore.write(token); + _logMsg('init: 已设置 userToken 并写入本地'); } else { - _logMsg('init: 响应中无 reevaluate (userToken)'); + _logMsg('init: 响应中无 reevaluate (userToken),保留原本地 token(若有)'); } - final prefs = await SharedPreferences.getInstance(); - final hadLoggedIn = prefs.getBool('adjust_has_logged_in') ?? false; - if (!hadLoggedIn) { + // equip = 服务端 firstRegister(见 docs/user_login.md),为 true/1 时上报注册事件 + final equipRaw = data?['equip']; + final equipFirstRegister = equipRaw == true || + equipRaw == 1 || + equipRaw == '1' || + (equipRaw is String && equipRaw.toLowerCase() == 'true'); + if (equipFirstRegister) { AdjustEvents.trackRegister(); - await prefs.setBool('adjust_has_logged_in', true); + final prefs = await SharedPreferences.getInstance(); await prefs.setString( 'adjust_register_date', DateTime.now().toIso8601String().substring(0, 10), ); - _logMsg('init: 首次登录,已上报 register'); + _logMsg('init: equip=true 首次注册,已上报 register'); } final credits = data?['reveal'] as int?; if (credits != null) { diff --git a/lib/core/auth/auth_token_store.dart b/lib/core/auth/auth_token_store.dart new file mode 100644 index 0000000..4abc42f --- /dev/null +++ b/lib/core/auth/auth_token_store.dart @@ -0,0 +1,31 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +import '../api/api_client.dart'; + +/// 持久化用户 Token(代理请求头 `knight`,字段来源 fast_login `reevaluate`) +abstract final class AuthTokenStore { + static const _prefsKey = 'user_knight_token'; + + static Future read() async { + final p = await SharedPreferences.getInstance(); + return p.getString(_prefsKey); + } + + static Future write(String token) async { + final p = await SharedPreferences.getInstance(); + await p.setString(_prefsKey, token); + } + + static Future clear() async { + final p = await SharedPreferences.getInstance(); + await p.remove(_prefsKey); + } + + /// 启动时调用:若有上次 token,写入 [ApiClient],`fast_login` 等请求会带 `knight` + static Future restoreToApiClient() async { + final t = await read(); + if (t != null && t.isNotEmpty) { + ApiClient.instance.setUserToken(t); + } + } +} diff --git a/lib/features/profile/profile_screen.dart b/lib/features/profile/profile_screen.dart index e57fe92..818e08b 100644 --- a/lib/features/profile/profile_screen.dart +++ b/lib/features/profile/profile_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_lucide/flutter_lucide.dart'; import 'package:cached_network_image/cached_network_image.dart'; import '../../core/api/api_client.dart'; +import '../../core/auth/auth_token_store.dart'; import '../../core/api/api_config.dart'; import '../../core/api/services/user_api.dart'; import '../../core/user/account_refresh.dart'; @@ -408,7 +409,9 @@ class _DeleteAccountDialogState extends State<_DeleteAccountDialog> { UserState.setAvatar(null); UserState.setUserName(null); UserState.setNavigate(null); + await AuthTokenStore.clear(); ApiClient.instance.setUserToken(null); + if (!mounted) return; Navigator.of(context).pop(); if (widget.parentContext.mounted) { ScaffoldMessenger.of(widget.parentContext).showSnackBar( diff --git a/lib/features/recharge/recharge_screen.dart b/lib/features/recharge/recharge_screen.dart index 4ffc77c..aead116 100644 --- a/lib/features/recharge/recharge_screen.dart +++ b/lib/features/recharge/recharge_screen.dart @@ -266,6 +266,8 @@ class _RechargeScreenState extends State return; } + final purchaseAmount = + (AdjustEvents.parsePrice(item.actualAmount) ?? 0).toDouble(); final payUrl = data?['convert']?.toString(); if (payUrl != null && payUrl.isNotEmpty) { if (mounted) { @@ -276,21 +278,28 @@ class _RechargeScreenState extends State ), ); if (mounted && orderId != null && orderId.isNotEmpty) { - _startOrderPolling(orderId: orderId, userId: userId); + _startOrderPolling( + orderId: orderId, + userId: userId, + purchaseAmount: purchaseAmount, + ); } _showSnackBar( context, 'Order created. Complete payment in the page.'); - AdjustEvents.trackPurchaseSuccess( - (AdjustEvents.parsePrice(item.actualAmount) ?? 0).toDouble()); } } else { if (mounted) { setState(() => _loadingProductId = null); _showSnackBar( context, 'Order created. Awaiting payment confirmation.'); + if (orderId != null && orderId.isNotEmpty) { + _startOrderPolling( + orderId: orderId, + userId: userId, + purchaseAmount: purchaseAmount, + ); + } } - AdjustEvents.trackPurchaseSuccess( - (AdjustEvents.parsePrice(item.actualAmount) ?? 0).toDouble()); } } catch (e) { if (mounted) { @@ -304,8 +313,12 @@ class _RechargeScreenState extends State } } - /// 三方支付 webview 关闭后轮询订单详情,间隔 1/3/7/15/31/63 秒;status 为 SUCCESS|FAILED|CANCELED 或订单不存在或轮询结束则停止;SUCCESS 时刷新用户信息 - void _startOrderPolling({required String orderId, required String userId}) { + /// 三方支付 webview 关闭后轮询订单详情,间隔 1/3/7/15/31/63 秒;status 为 SUCCESS|FAILED|CANCELED 或订单不存在或轮询结束则停止;SUCCESS 时刷新用户信息并上报 Adjust/Facebook 购买 + void _startOrderPolling({ + required String orderId, + required String userId, + required double purchaseAmount, + }) { const delays = [1, 2, 4, 8, 16, 32]; // 累计 1,3,7,15,31,63 秒 Future poll(int index) async { if (index >= delays.length) return; @@ -326,6 +339,7 @@ class _RechargeScreenState extends State RechargeScreen._log.d('订单轮询 orderId=$orderId status=$status'); if (status == 'SUCCESS' || status == 'FAILED' || status == 'CANCELED') { if (status == 'SUCCESS') { + await AdjustEvents.trackPurchaseSuccess(purchaseAmount); refreshAccount(); } return; diff --git a/lib/main.dart b/lib/main.dart index 2a270ba..15f9dee 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,6 @@ import 'package:adjust_sdk/adjust_attribution.dart'; import 'package:adjust_sdk/adjust_config.dart'; import 'package:adjust_sdk/adjust.dart'; -import 'package:facebook_app_events/facebook_app_events.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -9,7 +8,6 @@ import 'package:flutter/services.dart'; import 'app.dart'; import 'core/api/api_config.dart'; import 'core/auth/auth_service.dart'; -import 'core/config/facebook_config.dart'; import 'core/log/app_logger.dart'; import 'core/referrer/referrer_service.dart'; import 'core/theme/app_colors.dart'; @@ -18,7 +16,7 @@ import 'features/recharge/google_play_purchase_service.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); _initAdjust(); - _initFacebookAppEvents(); + // Facebook:安装/启动由 App 内生命周期手动 activateApp(见 app.dart),关闭 AutoLog 后依赖此项 // 等待 Adjust 归因(ReferrerService 会调用 Adjust.getAttributionWithTimeout) await ReferrerService.init(); SystemChrome.setSystemUIOverlayStyle( @@ -46,7 +44,6 @@ void _initAdjust() { appToken, kDebugMode ? AdjustEnvironment.sandbox : AdjustEnvironment.production, ); - // config.fbAppId = FacebookConfig.appId; if (kDebugMode || ApiConfig.debugLogs) { config.logLevel = AdjustLogLevel.verbose; } @@ -54,16 +51,6 @@ void _initAdjust() { Adjust.initSdk(config); } -final _fbAppEvents = FacebookAppEvents(); - -void _initFacebookAppEvents() { - // activateApp:应用启动事件,Facebook 用于统计与广告归因 - _fbAppEvents.activateApp(); - if (FacebookConfig.debugLogs) { - AppLogger('FB').d('activateApp 已上报'); - } -} - final _adjustLog = AppLogger('Adjust'); void _onAdjustAttribution(AdjustAttribution attribution) { diff --git a/pubspec.yaml b/pubspec.yaml index 7bcc418..a45231c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: pets_hero_ai description: PetsHero AI Application. publish_to: 'none' -version: 1.1.11+22 +version: 1.1.13+24 environment: sdk: '>=3.0.0 <4.0.0' @@ -34,6 +34,7 @@ dependencies: webview_flutter: ^4.10.0 screen_secure: ^1.0.3 flutter_native_splash: ^2.4.7 + android_id: ^0.5.1 dev_dependencies: flutter_test: