打包:修改adjust的归因数据格式
This commit is contained in:
parent
cacb32c25f
commit
d766f9c45e
@ -37,6 +37,7 @@ configurations.all {
|
|||||||
android {
|
android {
|
||||||
namespace "com.petsheroai.app"
|
namespace "com.petsheroai.app"
|
||||||
compileSdk 36
|
compileSdk 36
|
||||||
|
buildFeatures { buildConfig = true }
|
||||||
ndkVersion flutter.ndkVersion
|
ndkVersion flutter.ndkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@ -58,6 +59,8 @@ android {
|
|||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
|
// Facebook SDK 调试:正式包调试时设为 true,上线前改回 false
|
||||||
|
buildConfigField "boolean", "FACEBOOK_DEBUG_LOGS", (localProperties.getProperty("facebook.debug") ?: "true")
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
@ -82,8 +85,10 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation 'com.facebook.android:facebook-core:18.0.0'
|
||||||
implementation 'com.google.android.gms:play-services-ads-identifier:18.1.0'
|
implementation 'com.google.android.gms:play-services-ads-identifier:18.1.0'
|
||||||
implementation 'com.android.installreferrer:installreferrer:2.2'
|
implementation 'com.android.installreferrer:installreferrer:2.2'
|
||||||
|
implementation 'com.adjust.sdk:adjust-android-meta-referrer:5.5.1'
|
||||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||||
// Play Billing Library 6.0.1+ (use 7.1.1 for in_app_purchase plugin compatibility)
|
// Play Billing Library 6.0.1+ (use 7.1.1 for in_app_purchase plugin compatibility)
|
||||||
implementation 'com.android.billingclient:billing:7.1.1'
|
implementation 'com.android.billingclient:billing:7.1.1'
|
||||||
|
|||||||
@ -27,5 +27,16 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
<!-- Facebook SDK:Adjust 归因 + App Events 埋点 -->
|
||||||
|
<meta-data
|
||||||
|
android:name="com.facebook.sdk.ApplicationId"
|
||||||
|
android:value="@string/facebook_app_id" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.facebook.sdk.ClientToken"
|
||||||
|
android:value="@string/facebook_client_token" />
|
||||||
|
<!-- Facebook SDK 调试:Codeless 事件调试时启用 -->
|
||||||
|
<meta-data
|
||||||
|
android:name="com.facebook.sdk.CodelessDebugLogEnabled"
|
||||||
|
android:value="true" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@ -1,5 +1,24 @@
|
|||||||
package com.petsheroai.app
|
package com.petsheroai.app
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.facebook.FacebookSdk
|
||||||
|
import com.facebook.LoggingBehavior
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
class MainActivity: FlutterActivity()
|
class MainActivity : FlutterActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
// 必须在 super.onCreate 之前初始化,否则 facebook_app_events 插件注册时会因 SDK 未初始化而崩溃
|
||||||
|
FacebookSdk.sdkInitialize(applicationContext)
|
||||||
|
// Facebook SDK 调试日志:需 buildConfigField FACEBOOK_DEBUG_LOGS=true
|
||||||
|
if (BuildConfig.FACEBOOK_DEBUG_LOGS) {
|
||||||
|
try {
|
||||||
|
FacebookSdk.setIsDebugEnabled(true)
|
||||||
|
FacebookSdk.addLoggingBehavior(LoggingBehavior.APP_EVENTS)
|
||||||
|
FacebookSdk.addLoggingBehavior(LoggingBehavior.REQUESTS)
|
||||||
|
FacebookSdk.addLoggingBehavior(LoggingBehavior.DEVELOPER_ERRORS)
|
||||||
|
} catch (_: Exception) { /* ignore */ }
|
||||||
|
}
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
6
android/app/src/main/res/values/strings.xml
Normal file
6
android/app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Facebook App Events: 在 Facebook 开发者后台获取 Client Token 后替换 -->
|
||||||
|
<string name="facebook_app_id">1684216162986495</string>
|
||||||
|
<string name="facebook_client_token">df562f4ff186aaac5ff56ba190020ffd</string>
|
||||||
|
</resources>
|
||||||
@ -18,6 +18,12 @@
|
|||||||
@import device_info_plus;
|
@import device_info_plus;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if __has_include(<facebook_app_events/FacebookAppEventsPlugin.h>)
|
||||||
|
#import <facebook_app_events/FacebookAppEventsPlugin.h>
|
||||||
|
#else
|
||||||
|
@import facebook_app_events;
|
||||||
|
#endif
|
||||||
|
|
||||||
#if __has_include(<flutter_native_splash/FlutterNativeSplashPlugin.h>)
|
#if __has_include(<flutter_native_splash/FlutterNativeSplashPlugin.h>)
|
||||||
#import <flutter_native_splash/FlutterNativeSplashPlugin.h>
|
#import <flutter_native_splash/FlutterNativeSplashPlugin.h>
|
||||||
#else
|
#else
|
||||||
@ -89,6 +95,7 @@
|
|||||||
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
|
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
|
||||||
[AdjustSdk registerWithRegistrar:[registry registrarForPlugin:@"AdjustSdk"]];
|
[AdjustSdk registerWithRegistrar:[registry registrarForPlugin:@"AdjustSdk"]];
|
||||||
[FPPDeviceInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPDeviceInfoPlusPlugin"]];
|
[FPPDeviceInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPDeviceInfoPlusPlugin"]];
|
||||||
|
[FacebookAppEventsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FacebookAppEventsPlugin"]];
|
||||||
[FlutterNativeSplashPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterNativeSplashPlugin"]];
|
[FlutterNativeSplashPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterNativeSplashPlugin"]];
|
||||||
[GalPlugin registerWithRegistrar:[registry registrarForPlugin:@"GalPlugin"]];
|
[GalPlugin registerWithRegistrar:[registry registrarForPlugin:@"GalPlugin"]];
|
||||||
[FLTImagePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTImagePickerPlugin"]];
|
[FLTImagePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTImagePickerPlugin"]];
|
||||||
|
|||||||
@ -1,9 +1,21 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:adjust_sdk/adjust.dart';
|
import 'package:adjust_sdk/adjust.dart';
|
||||||
import 'package:adjust_sdk/adjust_event.dart';
|
import 'package:adjust_sdk/adjust_event.dart';
|
||||||
|
import 'package:facebook_app_events/facebook_app_events.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
/// Adjust 事件埋点(识别码见 docs/adjuest.md)
|
import '../config/facebook_config.dart';
|
||||||
|
|
||||||
|
/// 事件埋点:Adjust + Facebook App Events 双通道上报
|
||||||
|
/// Adjust 识别码见 docs/adjuest.md
|
||||||
abstract final class AdjustEvents {
|
abstract final class AdjustEvents {
|
||||||
|
static final _fb = FacebookAppEvents();
|
||||||
|
static final _fbLog = Logger(
|
||||||
|
printer: PrettyPrinter(methodCount: 0, lineLength: 120),
|
||||||
|
level: FacebookConfig.debugLogs ? Level.trace : Level.off,
|
||||||
|
);
|
||||||
// 购买档位(充值页选择档位时上报)
|
// 购买档位(充值页选择档位时上报)
|
||||||
static const String tier1999 = 'm0r9u9'; // 19.99
|
static const String tier1999 = 'm0r9u9'; // 19.99
|
||||||
static const String tier4999 = 'aht1ve'; // 49.99
|
static const String tier4999 = 'aht1ve'; // 49.99
|
||||||
@ -57,56 +69,75 @@ abstract final class AdjustEvents {
|
|||||||
Adjust.trackEvent(AdjustEvent(eventToken));
|
Adjust.trackEvent(AdjustEvent(eventToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 上报 Facebook(fire-and-forget,不阻塞主流程)
|
||||||
|
static void _trackFb(String eventDesc, Future<void> Function() fn) {
|
||||||
|
if (FacebookConfig.debugLogs) _fbLog.w('FB ↑ $eventDesc');
|
||||||
|
unawaited(fn());
|
||||||
|
}
|
||||||
|
|
||||||
/// 购买档位(用户点击某档位购买时)
|
/// 购买档位(用户点击某档位购买时)
|
||||||
static void trackTier(String eventToken) {
|
static void trackTier(String eventToken) {
|
||||||
_track(eventToken);
|
_track(eventToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 首日充值
|
/// 首日充值
|
||||||
static void trackFirstPurchase() {
|
static void trackFirstPurchase(double amount) {
|
||||||
_track(firstPurchase);
|
_track(firstPurchase);
|
||||||
|
_trackFb(
|
||||||
|
'FirstRecharge amount=$amount',
|
||||||
|
() => _fb
|
||||||
|
.logEvent(name: 'FirstRecharge', parameters: {'amount': amount}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 支付失败
|
/// 支付失败
|
||||||
static void trackOrderAbnormal() {
|
static void trackOrderAbnormal() {
|
||||||
_track(orderAbnormal);
|
_track(orderAbnormal);
|
||||||
}
|
_trackFb('payment_failed', () => _fb.logEvent(name: 'payment_failed'));
|
||||||
|
|
||||||
/// 支付成功(若为首日充值需额外调用 trackFirstPurchase)
|
|
||||||
static void trackPurchase() {
|
|
||||||
_track(purchase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 注册(首次 fast_login 成功)
|
/// 注册(首次 fast_login 成功)
|
||||||
static void trackRegister() {
|
static void trackRegister() {
|
||||||
_track(register);
|
_track(register);
|
||||||
|
_trackFb('CompletedRegistration',
|
||||||
|
() => _fb.logCompletedRegistration(registrationMethod: 'device'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PetsHero AI Monthly VIP
|
/// PetsHero AI Monthly VIP
|
||||||
static void trackMonthlyVip() {
|
static void trackMonthlyVip() {
|
||||||
_track(monthlyVip);
|
_track(monthlyVip);
|
||||||
|
_trackFb('Subscribe monthly_vip',
|
||||||
|
() => _fb.logSubscribe(orderId: 'monthly_vip'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PetsHero AI Weekly VIP
|
/// PetsHero AI Weekly VIP
|
||||||
static void trackWeeklyVip() {
|
static void trackWeeklyVip() {
|
||||||
_track(weeklyVip);
|
_track(weeklyVip);
|
||||||
|
_trackFb(
|
||||||
|
'Subscribe weekly_vip', () => _fb.logSubscribe(orderId: 'weekly_vip'));
|
||||||
}
|
}
|
||||||
|
|
||||||
static const String _keyRegisterDate = 'adjust_register_date';
|
static const String _keyRegisterDate = 'adjust_register_date';
|
||||||
|
|
||||||
/// 支付成功时调用:上报 Purchase,若为首日充值则同时上报 first purchase
|
/// 支付成功时调用:上报 Purchase,若为首日充值则同时上报 first purchase
|
||||||
static Future<void> trackPurchaseSuccess() async {
|
static Future<void> trackPurchaseSuccess(double amount) async {
|
||||||
_track(purchase);
|
_track(purchase);
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final registerDate = prefs.getString(_keyRegisterDate);
|
final registerDate = prefs.getString(_keyRegisterDate);
|
||||||
if (registerDate != null &&
|
final isFirstPurchase = registerDate != null &&
|
||||||
registerDate == DateTime.now().toIso8601String().substring(0, 10)) {
|
registerDate == DateTime.now().toIso8601String().substring(0, 10);
|
||||||
_track(firstPurchase);
|
if (isFirstPurchase) {
|
||||||
|
trackFirstPurchase(amount);
|
||||||
}
|
}
|
||||||
|
_trackFb(
|
||||||
|
'Purchase',
|
||||||
|
() => _fb.logPurchase(
|
||||||
|
amount: amount,
|
||||||
|
currency: 'USD',
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 支付失败时调用
|
/// 支付失败时调用
|
||||||
static void trackPaymentFailed() {
|
static void trackPaymentFailed() {
|
||||||
_track(orderAbnormal);
|
trackOrderAbnormal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,9 @@ import 'package:flutter/foundation.dart';
|
|||||||
|
|
||||||
/// petsHeroAI API 配置
|
/// petsHeroAI API 配置
|
||||||
abstract final class ApiConfig {
|
abstract final class ApiConfig {
|
||||||
|
/// 调试日志:true 时 release 包也输出 debug/info(正式包调试用,上线前改 false)
|
||||||
|
static const bool debugLogs = true;
|
||||||
|
|
||||||
/// AES 密钥
|
/// AES 密钥
|
||||||
static const String aesKey = 'liyP4LkMfP68XvCt';
|
static const String aesKey = 'liyP4LkMfP68XvCt';
|
||||||
|
|
||||||
@ -12,7 +15,7 @@ abstract final class ApiConfig {
|
|||||||
static const String packageName = 'com.petsheroai.app';
|
static const String packageName = 'com.petsheroai.app';
|
||||||
|
|
||||||
/// 预发环境域名
|
/// 预发环境域名
|
||||||
static const String preBaseUrl = 'https://pre-ai.petsheroai.xyz';
|
static const String preBaseUrl = 'https://ai.petsheroai.xyz';
|
||||||
//'https://ai.petsheroai.xyz'; //'https://pre-ai.petsheroai.xyz';
|
//'https://ai.petsheroai.xyz'; //'https://pre-ai.petsheroai.xyz';
|
||||||
|
|
||||||
/// 生产环境域名
|
/// 生产环境域名
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:encrypt/encrypt.dart';
|
import 'package:encrypt/encrypt.dart';
|
||||||
|
|
||||||
@ -29,8 +30,10 @@ abstract final class ApiCrypto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 生成随机 Base64 字符串(用于噪音字段)
|
/// 生成随机 Base64 字符串(用于噪音字段)
|
||||||
|
/// 使用 Random.secure() 保证密码学安全随机性
|
||||||
static String randomBase64([int byteLength = 16]) {
|
static String randomBase64([int byteLength = 16]) {
|
||||||
final bytes = List<int>.generate(byteLength, (_) => DateTime.now().millisecondsSinceEpoch % 256);
|
final random = Random.secure();
|
||||||
|
final bytes = List<int>.generate(byteLength, (_) => random.nextInt(256));
|
||||||
return base64Encode(bytes);
|
return base64Encode(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -82,7 +82,7 @@ void _logLong(String text) {
|
|||||||
|
|
||||||
/// 遇到 { 或 [ 视为 JSON 开始,直到与之匹配的 } 或 ] 结束;格式化后单条不超过 1000 字,分条时按行切分保持对齐。
|
/// 遇到 { 或 [ 视为 JSON 开始,直到与之匹配的 } 或 ] 结束;格式化后单条不超过 1000 字,分条时按行切分保持对齐。
|
||||||
void logWithEmbeddedJson(Object? msg) {
|
void logWithEmbeddedJson(Object? msg) {
|
||||||
if (!kDebugMode) return;
|
if (!kDebugMode && !ApiConfig.debugLogs) return;
|
||||||
|
|
||||||
if (msg is! String) {
|
if (msg is! String) {
|
||||||
_proxyLog.d(msg);
|
_proxyLog.d(msg);
|
||||||
@ -265,7 +265,7 @@ class ProxyClient {
|
|||||||
final loyaltyIndexEnc = ApiCrypto.encrypt(v2BodyEncoded);
|
final loyaltyIndexEnc = ApiCrypto.encrypt(v2BodyEncoded);
|
||||||
|
|
||||||
final proxyBody = {
|
final proxyBody = {
|
||||||
ProxyKeys.heroClass: ApiConfig.appId,
|
ProxyKeys.heroClass: 'petsHeroAI',
|
||||||
ProxyKeys.petSpecies: petSpeciesEnc,
|
ProxyKeys.petSpecies: petSpeciesEnc,
|
||||||
ProxyKeys.powerLevel: powerLevelEnc,
|
ProxyKeys.powerLevel: powerLevelEnc,
|
||||||
ProxyKeys.questRank: questRankEnc,
|
ProxyKeys.questRank: questRankEnc,
|
||||||
@ -278,9 +278,10 @@ class ProxyClient {
|
|||||||
ProxyKeys.accuracyVal: ApiCrypto.randomBase64(),
|
ProxyKeys.accuracyVal: ApiCrypto.randomBase64(),
|
||||||
ProxyKeys.dirPath: ApiCrypto.randomBase64(),
|
ProxyKeys.dirPath: ApiCrypto.randomBase64(),
|
||||||
};
|
};
|
||||||
|
_log('加密后的请求体: ${jsonEncode(proxyBody)}');
|
||||||
|
|
||||||
final url = '$_baseUrl${ApiConfig.proxyPath}';
|
final url = '$_baseUrl${ApiConfig.proxyPath}';
|
||||||
|
_log('真实请求URL: $url');
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
Uri.parse(url),
|
Uri.parse(url),
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
|||||||
@ -19,7 +19,8 @@ abstract final class UserApi {
|
|||||||
path: '/v1/user/fast_login',
|
path: '/v1/user/fast_login',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
queryParams: {
|
queryParams: {
|
||||||
if (crest != null) 'crest': crest,
|
// if (crest != null) 'crest': crest,
|
||||||
|
"sentinel": "HAndroid",
|
||||||
'portal': ApiConfig.packageName,
|
'portal': ApiConfig.packageName,
|
||||||
if (accolade != null) 'accolade': accolade,
|
if (accolade != null) 'accolade': accolade,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -85,7 +85,8 @@ class AuthService {
|
|||||||
final realm = data['realm'] as String?;
|
final realm = data['realm'] as String?;
|
||||||
if (realm != null && realm.isNotEmpty) UserState.setAvatar(realm);
|
if (realm != null && realm.isNotEmpty) UserState.setAvatar(realm);
|
||||||
final terminal = data['terminal'] as String?;
|
final terminal = data['terminal'] as String?;
|
||||||
if (terminal != null && terminal.isNotEmpty) UserState.setUserName(terminal);
|
if (terminal != null && terminal.isNotEmpty)
|
||||||
|
UserState.setUserName(terminal);
|
||||||
final navigate = data['navigate'] as String?;
|
final navigate = data['navigate'] as String?;
|
||||||
if (navigate != null) UserState.setNavigate(navigate);
|
if (navigate != null) UserState.setNavigate(navigate);
|
||||||
|
|
||||||
@ -162,6 +163,7 @@ class AuthService {
|
|||||||
if (res == null) return;
|
if (res == null) return;
|
||||||
|
|
||||||
_logMsg('init: 登录结果 code=${res.code} msg=${res.msg}');
|
_logMsg('init: 登录结果 code=${res.code} msg=${res.msg}');
|
||||||
|
_logMsg('init: 登录响应 data=${res.data}');
|
||||||
|
|
||||||
if (res.isSuccess && res.data != null) {
|
if (res.isSuccess && res.data != null) {
|
||||||
final data = res.data as Map<String, dynamic>?;
|
final data = res.data as Map<String, dynamic>?;
|
||||||
@ -213,7 +215,7 @@ class AuthService {
|
|||||||
asset: uid!,
|
asset: uid!,
|
||||||
digest: crest ?? '',
|
digest: crest ?? '',
|
||||||
origin: deviceId,
|
origin: deviceId,
|
||||||
accolade: 'android_adjust',
|
accolade: ReferrerService.referrerSource,
|
||||||
);
|
);
|
||||||
if (referrerRes.isSuccess) {
|
if (referrerRes.isSuccess) {
|
||||||
_logMsg('referrer 上报成功');
|
_logMsg('referrer 上报成功');
|
||||||
|
|||||||
27
lib/core/config/facebook_config.dart
Normal file
27
lib/core/config/facebook_config.dart
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/// Facebook SDK 配置
|
||||||
|
///
|
||||||
|
/// 用于 Adjust Meta Install Referrer 归因与 Facebook App Events 埋点。
|
||||||
|
abstract final class FacebookConfig {
|
||||||
|
/// Facebook 调试日志:true 时 Dart 层会打印所有 FB 事件(正式包调试用,上线前改 false)
|
||||||
|
static const bool debugLogs = true;
|
||||||
|
|
||||||
|
/// Facebook 应用 ID(应用编号)
|
||||||
|
static const String appId = '1684216162986495';
|
||||||
|
|
||||||
|
/// Facebook Client Token(客户端口令)
|
||||||
|
///
|
||||||
|
/// 在 Facebook 开发者后台获取:应用 → 设置 → 高级 → 客户端口令
|
||||||
|
static const String clientToken = '';
|
||||||
|
|
||||||
|
/// 安装引荐来源解密密钥
|
||||||
|
///
|
||||||
|
/// 用于解密 Facebook App Install Ads 的加密 referrer。
|
||||||
|
/// 请在 Adjust 控制台填写此密钥以启用解密
|
||||||
|
/// (路径:App Settings → Partner setup → Meta/Facebook)。
|
||||||
|
static const String installReferrerDecryptionKey =
|
||||||
|
'068aff9bac7e8846b94e9fc73d51c7a5ab7c8ac39fe9a2b16d0ff8b74f98f';
|
||||||
|
|
||||||
|
/// 应用密钥(App Secret)仅用于服务端,勿放入客户端。
|
||||||
|
static bool get hasClientToken => clientToken.isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,8 +1,10 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
|
import '../api/api_config.dart';
|
||||||
|
|
||||||
/// 统一应用日志,提升可读性:时间戳、级别、标签、格式化输出。
|
/// 统一应用日志,提升可读性:时间戳、级别、标签、格式化输出。
|
||||||
/// 在 release 下仅输出 warning/error,避免泄露信息。
|
/// release 下默认仅输出 warning/error;ApiConfig.debugLogs=true 时放开全部级别。
|
||||||
///
|
///
|
||||||
/// 使用示例:
|
/// 使用示例:
|
||||||
/// final _log = AppLogger('GenerateVideo');
|
/// final _log = AppLogger('GenerateVideo');
|
||||||
@ -25,7 +27,7 @@ class AppLogger {
|
|||||||
printEmojis: true,
|
printEmojis: true,
|
||||||
dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart,
|
dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart,
|
||||||
),
|
),
|
||||||
level: kReleaseMode ? Level.warning : Level.trace,
|
level: (kDebugMode || ApiConfig.debugLogs) ? Level.trace : Level.warning,
|
||||||
);
|
);
|
||||||
return _logger!;
|
return _logger!;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,32 +1,108 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:adjust_sdk/adjust_attribution.dart';
|
||||||
|
import 'package:adjust_sdk/adjust.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:play_install_referrer/play_install_referrer.dart';
|
import 'package:play_install_referrer/play_install_referrer.dart';
|
||||||
|
|
||||||
/// 安装来源 referrer 服务(用于 ch/crest 渠道参数)
|
/// 归因信息服务(优先从 Adjust 获取,fallback 使用 Play Install Referrer)
|
||||||
class ReferrerService {
|
class ReferrerService {
|
||||||
ReferrerService._();
|
ReferrerService._();
|
||||||
|
|
||||||
static String? _cachedReferrer;
|
static String? _cachedReferrer;
|
||||||
|
static String _referrerSource = 'gg';
|
||||||
static final Completer<String?> _completer = Completer<String?>();
|
static final Completer<String?> _completer = Completer<String?>();
|
||||||
|
|
||||||
/// 获取 referrer,Android 使用 Google Play Install Referrer,iOS 返回空
|
/// attribution 回调的 Completer,与 getAttributionWithTimeout 竞速,谁先返回用谁
|
||||||
|
static final Completer<AdjustAttribution?> _attributionCallbackCompleter =
|
||||||
|
Completer<AdjustAttribution?>();
|
||||||
|
|
||||||
|
static const int _adjustTimeoutMs = 15000;
|
||||||
|
|
||||||
|
/// 由 Adjust attributionCallback 调用,首次安装时归因往往通过此回调返回(晚于 getAttributionWithTimeout)
|
||||||
|
static void receiveAttributionFromCallback(AdjustAttribution attribution) {
|
||||||
|
if (!_attributionCallbackCompleter.isCompleted) {
|
||||||
|
_attributionCallbackCompleter.complete(attribution);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 归因来源:Adjust 为 android_adjust,Play Install Referrer 为 gg
|
||||||
|
static String get referrerSource => _referrerSource;
|
||||||
|
|
||||||
|
/// JSON 可编码的 costAmount:保留字段,非有限数字用字符串避免 encode 抛错
|
||||||
|
static Object? _jsonEncodableCostAmount(num? v) {
|
||||||
|
if (v == null) return null;
|
||||||
|
if (v is double && !v.isFinite) return v.toString();
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将完整 AdjustAttribution 序列化为 JSON(供 digest / Base64),不省略任何键
|
||||||
|
static String _attributionToDigest(AdjustAttribution attr) {
|
||||||
|
final map = <String, dynamic>{
|
||||||
|
'trackerToken': attr.trackerToken,
|
||||||
|
'trackerName': attr.trackerName,
|
||||||
|
'network': attr.network,
|
||||||
|
'campaign': attr.campaign,
|
||||||
|
'adgroup': attr.adgroup,
|
||||||
|
'creative': attr.creative,
|
||||||
|
'clickLabel': attr.clickLabel,
|
||||||
|
'costType': attr.costType,
|
||||||
|
'costAmount': _jsonEncodableCostAmount(attr.costAmount),
|
||||||
|
'costCurrency': attr.costCurrency,
|
||||||
|
'jsonResponse': attr.jsonResponse,
|
||||||
|
'fbInstallReferrer': attr.fbInstallReferrer,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
return jsonEncode(map);
|
||||||
|
} catch (_) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取 referrer/digest,优先从 Adjust 归因获取,fallback 使用 Play Install Referrer
|
||||||
|
/// 同时监听 attributionCallback,首次安装时归因往往通过回调返回(可能晚于 getAttributionWithTimeout)
|
||||||
static Future<String?> getReferrer() async {
|
static Future<String?> getReferrer() async {
|
||||||
if (_cachedReferrer != null) return _cachedReferrer;
|
if (_cachedReferrer != null) return _cachedReferrer;
|
||||||
if (_completer.isCompleted) return _completer.future;
|
if (_completer.isCompleted) return _completer.future;
|
||||||
|
|
||||||
if (defaultTargetPlatform != TargetPlatform.android) {
|
var digest = '';
|
||||||
_cachedReferrer = '';
|
try {
|
||||||
if (!_completer.isCompleted) _completer.complete('');
|
// 竞速:getAttributionWithTimeout 与 attribution 回调,谁先返回用谁
|
||||||
return '';
|
final attribution = await Future.any<AdjustAttribution?>([
|
||||||
|
(() async {
|
||||||
|
try {
|
||||||
|
return await Adjust.getAttributionWithTimeout(_adjustTimeoutMs);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
_attributionCallbackCompleter.future,
|
||||||
|
]);
|
||||||
|
if (attribution != null) {
|
||||||
|
final raw = _attributionToDigest(attribution);
|
||||||
|
if (raw.isNotEmpty) {
|
||||||
|
_referrerSource = 'android_adjust';
|
||||||
|
digest = base64Encode(utf8.encode(raw));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
digest = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (digest.isEmpty) {
|
||||||
final details = await PlayInstallReferrer.installReferrer;
|
_referrerSource = 'gg';
|
||||||
_cachedReferrer = details.installReferrer ?? '';
|
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||||
} catch (_) {
|
try {
|
||||||
_cachedReferrer = '';
|
final details = await PlayInstallReferrer.installReferrer;
|
||||||
|
digest = details.installReferrer ?? '';
|
||||||
|
} catch (_) {
|
||||||
|
digest = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_cachedReferrer = digest;
|
||||||
if (!_completer.isCompleted) _completer.complete(_cachedReferrer);
|
if (!_completer.isCompleted) _completer.complete(_cachedReferrer);
|
||||||
return _cachedReferrer;
|
return _cachedReferrer;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -280,7 +280,8 @@ class _RechargeScreenState extends State<RechargeScreen>
|
|||||||
}
|
}
|
||||||
_showSnackBar(
|
_showSnackBar(
|
||||||
context, 'Order created. Complete payment in the page.');
|
context, 'Order created. Complete payment in the page.');
|
||||||
AdjustEvents.trackPurchaseSuccess();
|
AdjustEvents.trackPurchaseSuccess(
|
||||||
|
(AdjustEvents.parsePrice(item.actualAmount) ?? 0).toDouble());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@ -288,7 +289,8 @@ class _RechargeScreenState extends State<RechargeScreen>
|
|||||||
_showSnackBar(
|
_showSnackBar(
|
||||||
context, 'Order created. Awaiting payment confirmation.');
|
context, 'Order created. Awaiting payment confirmation.');
|
||||||
}
|
}
|
||||||
AdjustEvents.trackPurchaseSuccess();
|
AdjustEvents.trackPurchaseSuccess(
|
||||||
|
(AdjustEvents.parsePrice(item.actualAmount) ?? 0).toDouble());
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@ -456,7 +458,8 @@ class _RechargeScreenState extends State<RechargeScreen>
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
_showSnackBar(context, 'Purchase completed.');
|
_showSnackBar(context, 'Purchase completed.');
|
||||||
}
|
}
|
||||||
AdjustEvents.trackPurchaseSuccess();
|
AdjustEvents.trackPurchaseSuccess(
|
||||||
|
(AdjustEvents.parsePrice(item.actualAmount) ?? 0).toDouble());
|
||||||
} else {
|
} else {
|
||||||
_showSnackBar(
|
_showSnackBar(
|
||||||
context,
|
context,
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
import 'package:adjust_sdk/adjust_attribution.dart';
|
import 'package:adjust_sdk/adjust_attribution.dart';
|
||||||
import 'package:adjust_sdk/adjust_config.dart';
|
import 'package:adjust_sdk/adjust_config.dart';
|
||||||
import 'package:adjust_sdk/adjust.dart';
|
import 'package:adjust_sdk/adjust.dart';
|
||||||
|
import 'package:facebook_app_events/facebook_app_events.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'app.dart';
|
import 'app.dart';
|
||||||
|
import 'core/api/api_config.dart';
|
||||||
import 'core/auth/auth_service.dart';
|
import 'core/auth/auth_service.dart';
|
||||||
|
import 'core/config/facebook_config.dart';
|
||||||
import 'core/log/app_logger.dart';
|
import 'core/log/app_logger.dart';
|
||||||
import 'core/referrer/referrer_service.dart';
|
import 'core/referrer/referrer_service.dart';
|
||||||
import 'core/theme/app_colors.dart';
|
import 'core/theme/app_colors.dart';
|
||||||
@ -15,7 +18,9 @@ import 'features/recharge/google_play_purchase_service.dart';
|
|||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
_initAdjust();
|
_initAdjust();
|
||||||
ReferrerService.init();
|
_initFacebookAppEvents();
|
||||||
|
// 等待 Adjust 归因(ReferrerService 会调用 Adjust.getAttributionWithTimeout)
|
||||||
|
await ReferrerService.init();
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
const SystemUiOverlayStyle(
|
const SystemUiOverlayStyle(
|
||||||
statusBarColor: AppColors.surface,
|
statusBarColor: AppColors.surface,
|
||||||
@ -23,7 +28,7 @@ void main() async {
|
|||||||
statusBarBrightness: Brightness.light,
|
statusBarBrightness: Brightness.light,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// 先启动登录,确保首次构建时 loginComplete 可被监听
|
// Adjust 初始化后执行登录,确保登录时归因数据已就绪
|
||||||
AuthService.init();
|
AuthService.init();
|
||||||
runApp(const App());
|
runApp(const App());
|
||||||
// 尽早订阅 purchaseStream,否则未确认订单不会出现在 queryPastPurchases 中,补单会为空
|
// 尽早订阅 purchaseStream,否则未确认订单不会出现在 queryPastPurchases 中,补单会为空
|
||||||
@ -31,7 +36,8 @@ void main() async {
|
|||||||
GooglePlayPurchaseService.startPendingPurchaseListener();
|
GooglePlayPurchaseService.startPendingPurchaseListener();
|
||||||
}
|
}
|
||||||
// 登录完成后执行谷歌支付补单(未核销订单上报服务端并 completePurchase)
|
// 登录完成后执行谷歌支付补单(未核销订单上报服务端并 completePurchase)
|
||||||
AuthService.loginComplete.then((_) => GooglePlayPurchaseService.runOrderRecovery());
|
AuthService.loginComplete
|
||||||
|
.then((_) => GooglePlayPurchaseService.runOrderRecovery());
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initAdjust() {
|
void _initAdjust() {
|
||||||
@ -40,16 +46,29 @@ void _initAdjust() {
|
|||||||
appToken,
|
appToken,
|
||||||
kDebugMode ? AdjustEnvironment.sandbox : AdjustEnvironment.production,
|
kDebugMode ? AdjustEnvironment.sandbox : AdjustEnvironment.production,
|
||||||
);
|
);
|
||||||
if (kDebugMode) {
|
// config.fbAppId = FacebookConfig.appId;
|
||||||
|
if (kDebugMode || ApiConfig.debugLogs) {
|
||||||
config.logLevel = AdjustLogLevel.verbose;
|
config.logLevel = AdjustLogLevel.verbose;
|
||||||
}
|
}
|
||||||
config.attributionCallback = _onAdjustAttribution;
|
config.attributionCallback = _onAdjustAttribution;
|
||||||
Adjust.initSdk(config);
|
Adjust.initSdk(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final _fbAppEvents = FacebookAppEvents();
|
||||||
|
|
||||||
|
void _initFacebookAppEvents() {
|
||||||
|
// activateApp:应用启动事件,Facebook 用于统计与广告归因
|
||||||
|
_fbAppEvents.activateApp();
|
||||||
|
if (FacebookConfig.debugLogs) {
|
||||||
|
AppLogger('FB').d('activateApp 已上报');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final _adjustLog = AppLogger('Adjust');
|
final _adjustLog = AppLogger('Adjust');
|
||||||
|
|
||||||
void _onAdjustAttribution(AdjustAttribution attribution) {
|
void _onAdjustAttribution(AdjustAttribution attribution) {
|
||||||
|
// 注入 ReferrerService,与 getAttributionWithTimeout 竞速,首次安装时归因多由此回调返回
|
||||||
|
ReferrerService.receiveAttributionFromCallback(attribution);
|
||||||
_adjustLog.d('归因信息: '
|
_adjustLog.d('归因信息: '
|
||||||
'trackerToken=${attribution.trackerToken}, '
|
'trackerToken=${attribution.trackerToken}, '
|
||||||
'trackerName=${attribution.trackerName}, '
|
'trackerName=${attribution.trackerName}, '
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
name: pets_hero_ai
|
name: pets_hero_ai
|
||||||
description: PetsHero AI Application.
|
description: PetsHero AI Application.
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.1.1+12
|
version: 1.1.11+22
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
@ -10,6 +10,7 @@ dependencies:
|
|||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
adjust_sdk: ^5.5.1
|
adjust_sdk: ^5.5.1
|
||||||
|
facebook_app_events: ^0.26.0
|
||||||
cupertino_icons: ^1.0.6
|
cupertino_icons: ^1.0.6
|
||||||
play_install_referrer: ^0.5.0
|
play_install_referrer: ^0.5.0
|
||||||
flutter_lucide: ^1.8.2
|
flutter_lucide: ^1.8.2
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user