Compare commits

..

2 Commits

Author SHA1 Message Date
ivan
4e90e6f030 优化:增加gg归因上报 2026-03-24 18:45:27 +08:00
ivan
d766f9c45e 打包:修改adjust的归因数据格式 2026-03-24 16:29:45 +08:00
21 changed files with 457 additions and 80 deletions

View File

@ -37,6 +37,7 @@ configurations.all {
android {
namespace "com.petsheroai.app"
compileSdk 36
buildFeatures { buildConfig = true }
ndkVersion flutter.ndkVersion
compileOptions {
@ -58,6 +59,8 @@ android {
targetSdk 36
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
// Facebook SDK true线 false
buildConfigField "boolean", "FACEBOOK_DEBUG_LOGS", (localProperties.getProperty("facebook.debug") ?: "true")
}
signingConfigs {
@ -82,8 +85,10 @@ android {
}
dependencies {
implementation 'com.facebook.android:facebook-core:18.0.0'
implementation 'com.google.android.gms:play-services-ads-identifier:18.1.0'
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'
// Play Billing Library 6.0.1+ (use 7.1.1 for in_app_purchase plugin compatibility)
implementation 'com.android.billingclient:billing:7.1.1'

View File

@ -27,5 +27,16 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<!-- Facebook SDKAdjust 归因 + 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>
</manifest>

View File

@ -1,5 +1,24 @@
package com.petsheroai.app
import android.os.Bundle
import com.facebook.FacebookSdk
import com.facebook.LoggingBehavior
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)
}
}

View 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>

69
docs/app_startup.md Normal file
View File

@ -0,0 +1,69 @@
# 应用启动与数据加载流程
对应代码:`lib/main.dart``lib/core/auth/auth_service.dart``lib/core/referrer/referrer_service.dart``lib/app.dart``lib/features/home/home_screen.dart`
## 总览(时序)
```mermaid
sequenceDiagram
participant Main as main()
participant Ref as ReferrerService
participant Adj as Adjust SDK
participant Auth as AuthService.init
participant API as 后端 API
participant UI as App / HomeScreen
Main->>Adj: initSdk + attributionCallback
Main->>Ref: await init() 竞速归因并缓存 digest
Main->>Auth: init() 不 await后台执行
Main->>UI: runApp
Auth->>Auth: delay 2s
Auth->>Ref: getReferrer() 读缓存
Auth->>API: fast_login(digest=crest)
Auth->>API: referrer(android_adjust)
Auth->>API: referrer(gg)
Auth->>API: common_info
Auth->>UI: loginComplete / isLoginComplete
UI->>API: HomeScreen._loadCategories 等 loginComplete 后
```
## 1. `main()` 中顺序(在 `runApp` 之前)
| 步骤 | 说明 |
|------|------|
| `WidgetsFlutterBinding.ensureInitialized()` | Flutter 绑定 |
| `_initAdjust()` | `Adjust.initSdk`,注册 `attributionCallback`(回调里会 `ReferrerService.receiveAttributionFromCallback` |
| `_initFacebookAppEvents()` | Facebook `activateApp` |
| **`await ReferrerService.init()`** | 与 `getAttributionWithTimeout`、归因回调**竞速**,得到用于 **fast_login**`crest`Adjust Base64 优先,否则 Android Play Install Referrer并写入内存缓存 |
| `SystemChrome.setSystemUIOverlayStyle` | 状态栏样式 |
| **`AuthService.init()`** | **不 await**,与 UI 首帧并行执行快速登录链路 |
| `runApp(const App())` | 构建根组件 |
| Android | `GooglePlayPurchaseService.startPendingPurchaseListener()` |
| `AuthService.loginComplete.then(...)` | 登录 Future 完成后跑谷歌支付补单 `runOrderRecovery` |
## 2. `AuthService.init()`(后台异步)
1. **固定等待 2 秒**(缓解冷启动网络未就绪)。
2. 取 **deviceId**、**sign**MD5(deviceId) 大写)。
3. **`ReferrerService.getReferrer()`**:通常直接命中 `init()` 阶段缓存,作为 **fast_login**`digest`
4. **`POST /v1/user/fast_login`**:最多重试 3 次;成功则设置 **token**、**userId**、积分与资料字段、首次安装时 Adjust register 等。
5. **`_reportBothReferrersAndRefreshCommonInfo`**(需已登录 uid
- 分别计算 **Adjust 专用 digest**、**Play Install Referrergg**`getAdjustReferrerDigest` / `getGgReferrerDigest`)。
- **顺序**调用两次 **`POST /v1/user/referrer`**`accolade=android_adjust``accolade=gg`**无论业务成功失败,均等待响应返回**后继续。
- 两次都结束后 **`GET /v1/user/common_info`** **一次**
- 在 **`_saveCommonInfoToState`** 中解析 `surge`**extConfig**`need_wait``items``safe_area``lucky` 等)。
- 若 **`need_wait``items`** 相对应用前快照发生**结构性变化**,则 **`UserState.requestHomeFullReload()`**(首页完整重走分类 + 任务加载)。
6. **`finally`**`loginComplete` 完成、`isLoginComplete = true`,去掉全屏登录等待遮罩。
## 3. 首屏 UI 与首页数据
- **`App`**`FutureBuilder` 监听 `AuthService.loginComplete`,未完成时叠一层半透明 + `CircularProgressIndicator`
- **`HomeScreen`**`initState`**`_loadCategories()`** 会先 **`await AuthService.loginComplete`**,再请求分类接口;根据当前 **`UserState.needShowVideoMenu`** 决定是否追加 **pets** 分类;选中非 pets 时再拉视频任务列表。
- 监听 **`UserState.needShowVideoMenu` / `extConfigItems` / `isLoginComplete`** → `setState` 刷新展示。
- 监听 **`UserState.homeReloadNonce`** → 再次 **`_loadCategories()`**,用于 **need_wait 等变化**后重新走「加载分类 → 加载任务」。
## 4. 与主页文档的关系
主页如何根据 **extConfig** 渲染分类栏与列表,见 [home.md](home.md) 与 [extConfig.md](extConfig.md)。

View File

@ -19,10 +19,12 @@
## 数据流简述
1. 登录后请求 `common_info`,在 `AuthService._saveCommonInfoToState` 中解析 `data.surge`
1. **应用启动与登录、归因、common_info 的完整顺序**见 [app_startup.md](app_startup.md)。
2. 登录成功后:两次 `POST /v1/user/referrer``android_adjust``gg`)均返回后,再 **`GET /v1/user/common_info` 一次**;在 `AuthService._saveCommonInfoToState` 中解析 `data.surge`
- 写入 `lucky` 等;
- 解析 `need_wait``items`,通过 `UserState.setExtConfig(needShowVideoMenuValue: needWait, items: items)` 写入。
2. 主页 `HomeScreen` 监听 `UserState.needShowVideoMenu``UserState.extConfigItems`,据此决定:
- 若 `need_wait``items` 相对之前发生结构性变化,会触发 `UserState.requestHomeFullReload()`,首页重走「加载分类 → 加载任务」。
3. 主页 `HomeScreen` 监听 `UserState.needShowVideoMenu``UserState.extConfigItems``homeReloadNonce` 等,据此决定:
- 是否渲染顶部分类栏;
- 当前列表是来自 extConfig.items 还是来自视频任务接口。
3. extConfig 的 **items** 单项字段:`image``cost``title``detail`用于展示卡片并作为生图参数taskType / ext
4. extConfig 的 **items** 单项字段:`image``cost``title``detail`用于展示卡片并作为生图参数taskType / ext

View File

@ -18,6 +18,12 @@
@import device_info_plus;
#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>)
#import <flutter_native_splash/FlutterNativeSplashPlugin.h>
#else
@ -89,6 +95,7 @@
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
[AdjustSdk registerWithRegistrar:[registry registrarForPlugin:@"AdjustSdk"]];
[FPPDeviceInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPDeviceInfoPlusPlugin"]];
[FacebookAppEventsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FacebookAppEventsPlugin"]];
[FlutterNativeSplashPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterNativeSplashPlugin"]];
[GalPlugin registerWithRegistrar:[registry registrarForPlugin:@"GalPlugin"]];
[FLTImagePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTImagePickerPlugin"]];

View File

@ -1,9 +1,21 @@
import 'dart:async';
import 'package:adjust_sdk/adjust.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';
/// Adjust docs/adjuest.md
import '../config/facebook_config.dart';
/// Adjust + Facebook App Events
/// Adjust docs/adjuest.md
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 tier4999 = 'aht1ve'; // 49.99
@ -57,56 +69,75 @@ abstract final class AdjustEvents {
Adjust.trackEvent(AdjustEvent(eventToken));
}
/// Facebookfire-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) {
_track(eventToken);
}
///
static void trackFirstPurchase() {
static void trackFirstPurchase(double amount) {
_track(firstPurchase);
_trackFb(
'FirstRecharge amount=$amount',
() => _fb
.logEvent(name: 'FirstRecharge', parameters: {'amount': amount}));
}
///
static void trackOrderAbnormal() {
_track(orderAbnormal);
}
/// trackFirstPurchase
static void trackPurchase() {
_track(purchase);
_trackFb('payment_failed', () => _fb.logEvent(name: 'payment_failed'));
}
/// fast_login
static void trackRegister() {
_track(register);
_trackFb('CompletedRegistration',
() => _fb.logCompletedRegistration(registrationMethod: 'device'));
}
/// PetsHero AI Monthly VIP
static void trackMonthlyVip() {
_track(monthlyVip);
_trackFb('Subscribe monthly_vip',
() => _fb.logSubscribe(orderId: 'monthly_vip'));
}
/// PetsHero AI Weekly VIP
static void trackWeeklyVip() {
_track(weeklyVip);
_trackFb(
'Subscribe weekly_vip', () => _fb.logSubscribe(orderId: 'weekly_vip'));
}
static const String _keyRegisterDate = 'adjust_register_date';
/// Purchase first purchase
static Future<void> trackPurchaseSuccess() async {
static Future<void> trackPurchaseSuccess(double amount) async {
_track(purchase);
final prefs = await SharedPreferences.getInstance();
final registerDate = prefs.getString(_keyRegisterDate);
if (registerDate != null &&
registerDate == DateTime.now().toIso8601String().substring(0, 10)) {
_track(firstPurchase);
final isFirstPurchase = registerDate != null &&
registerDate == DateTime.now().toIso8601String().substring(0, 10);
if (isFirstPurchase) {
trackFirstPurchase(amount);
}
_trackFb(
'Purchase',
() => _fb.logPurchase(
amount: amount,
currency: 'USD',
));
}
///
static void trackPaymentFailed() {
_track(orderAbnormal);
trackOrderAbnormal();
}
}

View File

@ -2,6 +2,9 @@ import 'package:flutter/foundation.dart';
/// petsHeroAI API
abstract final class ApiConfig {
/// true release debug/info线 false
static const bool debugLogs = true;
/// AES
static const String aesKey = 'liyP4LkMfP68XvCt';

View File

@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:math';
import 'package:encrypt/encrypt.dart';
@ -29,8 +30,10 @@ abstract final class ApiCrypto {
}
/// Base64
/// 使 Random.secure()
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);
}

View File

@ -82,7 +82,7 @@ void _logLong(String text) {
/// { [ JSON } ] 1000
void logWithEmbeddedJson(Object? msg) {
if (!kDebugMode) return;
if (!kDebugMode && !ApiConfig.debugLogs) return;
if (msg is! String) {
_proxyLog.d(msg);
@ -265,7 +265,7 @@ class ProxyClient {
final loyaltyIndexEnc = ApiCrypto.encrypt(v2BodyEncoded);
final proxyBody = {
ProxyKeys.heroClass: ApiConfig.appId,
ProxyKeys.heroClass: 'petsHeroAI',
ProxyKeys.petSpecies: petSpeciesEnc,
ProxyKeys.powerLevel: powerLevelEnc,
ProxyKeys.questRank: questRankEnc,
@ -278,9 +278,10 @@ class ProxyClient {
ProxyKeys.accuracyVal: ApiCrypto.randomBase64(),
ProxyKeys.dirPath: ApiCrypto.randomBase64(),
};
_log('加密后的请求体: ${jsonEncode(proxyBody)}');
final url = '$_baseUrl${ApiConfig.proxyPath}';
_log('真实请求URL: $url');
final response = await http.post(
Uri.parse(url),
headers: {'Content-Type': 'application/json'},

View File

@ -19,7 +19,8 @@ abstract final class UserApi {
path: '/v1/user/fast_login',
method: 'POST',
queryParams: {
if (crest != null) 'crest': crest,
// if (crest != null) 'crest': crest,
"sentinel": "HAndroid",
'portal': ApiConfig.packageName,
if (accolade != null) 'accolade': accolade,
},

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:crypto/crypto.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
@ -16,6 +17,27 @@ import '../log/app_logger.dart';
import '../referrer/referrer_service.dart';
import '../user/user_state.dart';
/// common_info /
class _HomeExtSnapshot {
_HomeExtSnapshot(this.needWait, this.items);
final bool? needWait;
final List<dynamic>? items;
factory _HomeExtSnapshot.capture() {
final raw = UserState.extConfigItems.value;
return _HomeExtSnapshot(
UserState.needShowVideoMenu.value,
raw == null ? null : List<dynamic>.from(raw),
);
}
static bool needsFullHomeReload(_HomeExtSnapshot before, _HomeExtSnapshot after) {
if (before.needWait != after.needWait) return true;
return !const DeepCollectionEquality().equals(before.items, after.items);
}
}
/// APP
class AuthService {
AuthService._();
@ -85,7 +107,9 @@ class AuthService {
final realm = data['realm'] as String?;
if (realm != null && realm.isNotEmpty) UserState.setAvatar(realm);
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?;
if (navigate != null) UserState.setNavigate(navigate);
@ -162,6 +186,7 @@ class AuthService {
if (res == null) return;
_logMsg('init: 登录结果 code=${res.code} msg=${res.msg}');
_logMsg('init: 登录响应 data=${res.data}');
if (res.isSuccess && res.data != null) {
final data = res.data as Map<String, dynamic>?;
@ -206,45 +231,11 @@ class AuthService {
UserState.setNavigate(countryCode);
}
// 3. digest Adjust install referrer
// 3. android_adjustgg common_infoextConfig /
try {
final referrerRes = await UserApi.referrer(
sentinel: ApiConfig.appId,
asset: uid!,
digest: crest ?? '',
origin: deviceId,
accolade: 'android_adjust',
);
if (referrerRes.isSuccess) {
_logMsg('referrer 上报成功');
} else {
_logMsg(
'referrer 上报失败: code=${referrerRes.code} msg=${referrerRes.msg}');
}
await _reportBothReferrersAndRefreshCommonInfo(uid!, deviceId);
} catch (e) {
_logMsg('referrer 请求异常: $e');
}
// 4. surge
try {
final commonRes = await UserApi.getCommonInfo(
sentinel: ApiConfig.appId,
asset: uid,
);
if (commonRes.isSuccess && commonRes.data != null) {
final commonData = commonRes.data as Map<String, dynamic>?;
if (commonData != null) {
_saveCommonInfoToState(commonData);
_logMsg('common_info 已保存到全局');
}
_logMsg('common_info 响应:');
logWithEmbeddedJson(json.encode(commonRes.data));
} else {
_logMsg(
'common_info 失败: code=${commonRes.code} msg=${commonRes.msg}');
}
} catch (e) {
_logMsg('common_info 请求异常: $e');
_logMsg('referrer/common_info 流程异常: $e');
}
} else {
_logMsg('init: 登录失败');
@ -259,4 +250,66 @@ class AuthService {
}
}
}
static bool _applyCommonInfoAndDidHomeStructureChange(ApiResponse commonRes) {
if (!commonRes.isSuccess || commonRes.data == null) return false;
final commonData = commonRes.data as Map<String, dynamic>?;
if (commonData == null) return false;
final before = _HomeExtSnapshot.capture();
_saveCommonInfoToState(commonData);
final after = _HomeExtSnapshot.capture();
final changed = _HomeExtSnapshot.needsFullHomeReload(before, after);
if (changed) {
_logMsg('common_info 已更新need_wait/items 相对上次有结构性变化');
}
_logMsg('common_info 响应已应用');
return changed;
}
/// android_adjustgg common_info extConfig
static Future<void> _reportBothReferrersAndRefreshCommonInfo(
String uid,
String deviceId,
) async {
final adjustDigest = await ReferrerService.getAdjustReferrerDigest();
final ggDigest = await ReferrerService.getGgReferrerDigest();
final rAdjust = await UserApi.referrer(
sentinel: ApiConfig.appId,
asset: uid,
digest: adjustDigest,
origin: deviceId,
accolade: 'android_adjust',
);
if (rAdjust.isSuccess) {
_logMsg('referrer(android_adjust) 成功');
} else {
_logMsg(
'referrer(android_adjust) 失败: code=${rAdjust.code} msg=${rAdjust.msg}');
}
final rGg = await UserApi.referrer(
sentinel: ApiConfig.appId,
asset: uid,
digest: ggDigest,
origin: deviceId,
accolade: 'gg',
);
if (rGg.isSuccess) {
_logMsg('referrer(gg) 成功');
} else {
_logMsg('referrer(gg) 失败: code=${rGg.code} msg=${rGg.msg}');
}
final commonRes = await UserApi.getCommonInfo(
sentinel: ApiConfig.appId,
asset: uid,
);
if (_applyCommonInfoAndDidHomeStructureChange(commonRes)) {
UserState.requestHomeFullReload();
} else if (!commonRes.isSuccess) {
_logMsg(
'common_info 失败: code=${commonRes.code} msg=${commonRes.msg}');
}
}
}

View 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;
}

View File

@ -1,8 +1,10 @@
import 'package:flutter/foundation.dart';
import 'package:logger/logger.dart';
import '../api/api_config.dart';
///
/// release warning/error
/// release warning/errorApiConfig.debugLogs=true
///
/// 使:
/// final _log = AppLogger('GenerateVideo');
@ -25,7 +27,7 @@ class AppLogger {
printEmojis: true,
dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart,
),
level: kReleaseMode ? Level.warning : Level.trace,
level: (kDebugMode || ApiConfig.debugLogs) ? Level.trace : Level.warning,
);
return _logger!;
}

View File

@ -1,32 +1,108 @@
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:play_install_referrer/play_install_referrer.dart';
/// referrer ch/crest
/// Adjust fallback 使 Play Install Referrer
class ReferrerService {
ReferrerService._();
static String? _cachedReferrer;
static String _referrerSource = 'gg';
static final Completer<String?> _completer = Completer<String?>();
/// referrerAndroid 使 Google Play Install ReferreriOS
/// 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_adjustPlay 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 {
if (_cachedReferrer != null) return _cachedReferrer;
if (_completer.isCompleted) return _completer.future;
if (defaultTargetPlatform != TargetPlatform.android) {
_cachedReferrer = '';
if (!_completer.isCompleted) _completer.complete('');
return '';
var digest = '';
try {
// getAttributionWithTimeout attribution
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 {
final details = await PlayInstallReferrer.installReferrer;
_cachedReferrer = details.installReferrer ?? '';
} catch (_) {
_cachedReferrer = '';
if (digest.isEmpty) {
_referrerSource = 'gg';
if (defaultTargetPlatform == TargetPlatform.android) {
try {
final details = await PlayInstallReferrer.installReferrer;
digest = details.installReferrer ?? '';
} catch (_) {
digest = '';
}
}
}
_cachedReferrer = digest;
if (!_completer.isCompleted) _completer.complete(_cachedReferrer);
return _cachedReferrer;
}
@ -35,4 +111,27 @@ class ReferrerService {
static Future<void> init() async {
await getReferrer();
}
/// Adjust digestBase64 JSON /v1/user/referrer accolade=android_adjust
static Future<String> getAdjustReferrerDigest() async {
try {
final attr = await Adjust.getAttribution();
final raw = _attributionToDigest(attr);
if (raw.isEmpty) return '';
return base64Encode(utf8.encode(raw));
} catch (_) {
return '';
}
}
/// Google Play Install Referrer /v1/user/referrer accolade=gg
static Future<String> getGgReferrerDigest() async {
if (defaultTargetPlatform != TargetPlatform.android) return '';
try {
final details = await PlayInstallReferrer.installReferrer;
return details.installReferrer ?? '';
} catch (_) {
return '';
}
}
}

View File

@ -21,6 +21,13 @@ class UserState {
/// extConfig.items common_info surge.items
static final ValueNotifier<List<dynamic>?> extConfigItems = ValueNotifier<List<dynamic>?>(null);
/// Home need_wait
static final ValueNotifier<int> homeReloadNonce = ValueNotifier<int>(0);
static void requestHomeFullReload() {
homeReloadNonce.value = homeReloadNonce.value + 1;
}
static void setCredits(int? value) {
credits.value = value;
}

View File

@ -39,6 +39,7 @@ class _HomeScreenState extends State<HomeScreen> {
UserState.needShowVideoMenu.addListener(_onExtConfigChanged);
UserState.extConfigItems.addListener(_onExtConfigChanged);
AuthService.isLoginComplete.addListener(_onExtConfigChanged);
UserState.homeReloadNonce.addListener(_onHomeReloadNonce);
_loadCategories();
if (widget.isActive) refreshAccount();
}
@ -48,6 +49,7 @@ class _HomeScreenState extends State<HomeScreen> {
UserState.needShowVideoMenu.removeListener(_onExtConfigChanged);
UserState.extConfigItems.removeListener(_onExtConfigChanged);
AuthService.isLoginComplete.removeListener(_onExtConfigChanged);
UserState.homeReloadNonce.removeListener(_onHomeReloadNonce);
super.dispose();
}
@ -55,6 +57,11 @@ class _HomeScreenState extends State<HomeScreen> {
if (mounted) setState(() {});
}
void _onHomeReloadNonce() {
if (!mounted) return;
_loadCategories();
}
@override
void didUpdateWidget(covariant HomeScreen oldWidget) {
super.didUpdateWidget(oldWidget);

View File

@ -280,7 +280,8 @@ class _RechargeScreenState extends State<RechargeScreen>
}
_showSnackBar(
context, 'Order created. Complete payment in the page.');
AdjustEvents.trackPurchaseSuccess();
AdjustEvents.trackPurchaseSuccess(
(AdjustEvents.parsePrice(item.actualAmount) ?? 0).toDouble());
}
} else {
if (mounted) {
@ -288,7 +289,8 @@ class _RechargeScreenState extends State<RechargeScreen>
_showSnackBar(
context, 'Order created. Awaiting payment confirmation.');
}
AdjustEvents.trackPurchaseSuccess();
AdjustEvents.trackPurchaseSuccess(
(AdjustEvents.parsePrice(item.actualAmount) ?? 0).toDouble());
}
} catch (e) {
if (mounted) {
@ -456,7 +458,8 @@ class _RechargeScreenState extends State<RechargeScreen>
if (mounted) {
_showSnackBar(context, 'Purchase completed.');
}
AdjustEvents.trackPurchaseSuccess();
AdjustEvents.trackPurchaseSuccess(
(AdjustEvents.parsePrice(item.actualAmount) ?? 0).toDouble());
} else {
_showSnackBar(
context,

View File

@ -1,12 +1,15 @@
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';
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';
@ -15,7 +18,9 @@ import 'features/recharge/google_play_purchase_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
_initAdjust();
ReferrerService.init();
_initFacebookAppEvents();
// Adjust ReferrerService Adjust.getAttributionWithTimeout
await ReferrerService.init();
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: AppColors.surface,
@ -23,7 +28,7 @@ void main() async {
statusBarBrightness: Brightness.light,
),
);
// loginComplete
// Adjust
AuthService.init();
runApp(const App());
// purchaseStream queryPastPurchases
@ -31,7 +36,8 @@ void main() async {
GooglePlayPurchaseService.startPendingPurchaseListener();
}
// completePurchase
AuthService.loginComplete.then((_) => GooglePlayPurchaseService.runOrderRecovery());
AuthService.loginComplete
.then((_) => GooglePlayPurchaseService.runOrderRecovery());
}
void _initAdjust() {
@ -40,16 +46,29 @@ void _initAdjust() {
appToken,
kDebugMode ? AdjustEnvironment.sandbox : AdjustEnvironment.production,
);
if (kDebugMode) {
// config.fbAppId = FacebookConfig.appId;
if (kDebugMode || ApiConfig.debugLogs) {
config.logLevel = AdjustLogLevel.verbose;
}
config.attributionCallback = _onAdjustAttribution;
Adjust.initSdk(config);
}
final _fbAppEvents = FacebookAppEvents();
void _initFacebookAppEvents() {
// activateAppFacebook 广
_fbAppEvents.activateApp();
if (FacebookConfig.debugLogs) {
AppLogger('FB').d('activateApp 已上报');
}
}
final _adjustLog = AppLogger('Adjust');
void _onAdjustAttribution(AdjustAttribution attribution) {
// ReferrerService getAttributionWithTimeout
ReferrerService.receiveAttributionFromCallback(attribution);
_adjustLog.d('归因信息: '
'trackerToken=${attribution.trackerToken}, '
'trackerName=${attribution.trackerName}, '

View File

@ -1,7 +1,7 @@
name: pets_hero_ai
description: PetsHero AI Application.
publish_to: 'none'
version: 1.1.1+12
version: 1.1.11+22
environment:
sdk: '>=3.0.0 <4.0.0'
@ -9,7 +9,9 @@ environment:
dependencies:
flutter:
sdk: flutter
collection: ^1.19.0
adjust_sdk: ^5.5.1
facebook_app_events: ^0.26.0
cupertino_icons: ^1.0.6
play_install_referrer: ^0.5.0
flutter_lucide: ^1.8.2