优化:Facebook统计问题,设备ID获取bug
This commit is contained in:
parent
4e90e6f030
commit
4ef714453e
@ -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")
|
||||
}
|
||||
|
||||
@ -34,6 +34,12 @@
|
||||
<meta-data
|
||||
android:name="com.facebook.sdk.ClientToken"
|
||||
android:value="@string/facebook_client_token" />
|
||||
<!-- 关闭自动 App 事件(含隐式内购 fb_mobile_purchase),避免与 Dart 内 logPurchase 重复计数。
|
||||
会话与显式埋点仍由应用代码负责(如 main.dart activateApp、AdjustEvents.trackPurchaseSuccess)。
|
||||
若开发者后台「自动记录应用事件」与本地冲突,以控制台设置为准。 -->
|
||||
<meta-data
|
||||
android:name="com.facebook.sdk.AutoLogAppEventsEnabled"
|
||||
android:value="false" />
|
||||
<!-- Facebook SDK 调试:Codeless 事件调试时启用 -->
|
||||
<meta-data
|
||||
android:name="com.facebook.sdk.CodelessDebugLogEnabled"
|
||||
|
||||
@ -30,6 +30,9 @@
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<!-- 关闭 Facebook SDK 自动 App 事件(含隐式购买),与 Android AutoLogAppEventsEnabled=false 对齐 -->
|
||||
<key>FacebookAutoLogAppEventsEnabled</key>
|
||||
<false/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
|
||||
41
lib/app.dart
41
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<App> createState() => _AppState();
|
||||
}
|
||||
|
||||
class _AppState extends State<App> {
|
||||
class _AppState extends State<App> 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(
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,19 +59,43 @@ 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<String> _getDeviceId() async {
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
final android = await deviceInfo.androidInfo;
|
||||
return android.id;
|
||||
case TargetPlatform.iOS:
|
||||
final ios = await deviceInfo.iosInfo;
|
||||
return ios.identifierForVendor ?? 'ios-unknown';
|
||||
default:
|
||||
return 'device-${DateTime.now().millisecondsSinceEpoch}';
|
||||
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 DeviceInfoPlugin().iosInfo;
|
||||
final idfv = ios.identifierForVendor;
|
||||
if (idfv != null && idfv.isNotEmpty) return idfv;
|
||||
return _persistedFallbackDeviceId();
|
||||
default:
|
||||
return _persistedFallbackDeviceId();
|
||||
}
|
||||
}
|
||||
|
||||
static 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);
|
||||
_logMsg('_getDeviceId: 已生成并持久化 fallback id');
|
||||
return id;
|
||||
}
|
||||
|
||||
/// 计算 sign:MD5(deviceId) 大写 32 位
|
||||
@ -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) {
|
||||
|
||||
31
lib/core/auth/auth_token_store.dart
Normal file
31
lib/core/auth/auth_token_store.dart
Normal file
@ -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<String?> read() async {
|
||||
final p = await SharedPreferences.getInstance();
|
||||
return p.getString(_prefsKey);
|
||||
}
|
||||
|
||||
static Future<void> write(String token) async {
|
||||
final p = await SharedPreferences.getInstance();
|
||||
await p.setString(_prefsKey, token);
|
||||
}
|
||||
|
||||
static Future<void> clear() async {
|
||||
final p = await SharedPreferences.getInstance();
|
||||
await p.remove(_prefsKey);
|
||||
}
|
||||
|
||||
/// 启动时调用:若有上次 token,写入 [ApiClient],`fast_login` 等请求会带 `knight`
|
||||
static Future<void> restoreToApiClient() async {
|
||||
final t = await read();
|
||||
if (t != null && t.isNotEmpty) {
|
||||
ApiClient.instance.setUserToken(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -266,6 +266,8 @@ class _RechargeScreenState extends State<RechargeScreen>
|
||||
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<RechargeScreen>
|
||||
),
|
||||
);
|
||||
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<RechargeScreen>
|
||||
}
|
||||
}
|
||||
|
||||
/// 三方支付 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<void> poll(int index) async {
|
||||
if (index >= delays.length) return;
|
||||
@ -326,6 +339,7 @@ class _RechargeScreenState extends State<RechargeScreen>
|
||||
RechargeScreen._log.d('订单轮询 orderId=$orderId status=$status');
|
||||
if (status == 'SUCCESS' || status == 'FAILED' || status == 'CANCELED') {
|
||||
if (status == 'SUCCESS') {
|
||||
await AdjustEvents.trackPurchaseSuccess(purchaseAmount);
|
||||
refreshAccount();
|
||||
}
|
||||
return;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user