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: