Compare commits
No commits in common. "4e90e6f030cf51ea899c512cb253f663d4c0446d" and "cacb32c25f2dbf5334d55bd9b23dbff1ad931c26" have entirely different histories.
4e90e6f030
...
cacb32c25f
@ -37,7 +37,6 @@ configurations.all {
|
||||
android {
|
||||
namespace "com.petsheroai.app"
|
||||
compileSdk 36
|
||||
buildFeatures { buildConfig = true }
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
@ -59,8 +58,6 @@ android {
|
||||
targetSdk 36
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
// Facebook SDK 调试:正式包调试时设为 true,上线前改回 false
|
||||
buildConfigField "boolean", "FACEBOOK_DEBUG_LOGS", (localProperties.getProperty("facebook.debug") ?: "true")
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@ -85,10 +82,8 @@ 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'
|
||||
|
||||
@ -27,16 +27,5 @@
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
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>
|
||||
</manifest>
|
||||
|
||||
@ -1,24 +1,5 @@
|
||||
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() {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
class MainActivity: FlutterActivity()
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
<?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>
|
||||
@ -1,69 +0,0 @@
|
||||
# 应用启动与数据加载流程
|
||||
|
||||
对应代码:`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 Referrer(gg)**(`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)。
|
||||
@ -19,12 +19,10 @@
|
||||
|
||||
## 数据流简述
|
||||
|
||||
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`:
|
||||
1. 登录后请求 `common_info`,在 `AuthService._saveCommonInfoToState` 中解析 `data.surge`:
|
||||
- 写入 `lucky` 等;
|
||||
- 解析 `need_wait`、`items`,通过 `UserState.setExtConfig(needShowVideoMenuValue: needWait, items: items)` 写入。
|
||||
- 若 `need_wait` 或 `items` 相对之前发生结构性变化,会触发 `UserState.requestHomeFullReload()`,首页重走「加载分类 → 加载任务」。
|
||||
3. 主页 `HomeScreen` 监听 `UserState.needShowVideoMenu`、`UserState.extConfigItems`、`homeReloadNonce` 等,据此决定:
|
||||
2. 主页 `HomeScreen` 监听 `UserState.needShowVideoMenu`、`UserState.extConfigItems`,据此决定:
|
||||
- 是否渲染顶部分类栏;
|
||||
- 当前列表是来自 extConfig.items 还是来自视频任务接口。
|
||||
4. extConfig 的 **items** 单项字段:`image`、`cost`、`title`、`detail`,用于展示卡片并作为生图参数(taskType / ext)。
|
||||
3. extConfig 的 **items** 单项字段:`image`、`cost`、`title`、`detail`,用于展示卡片并作为生图参数(taskType / ext)。
|
||||
|
||||
@ -18,12 +18,6 @@
|
||||
@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
|
||||
@ -95,7 +89,6 @@
|
||||
+ (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"]];
|
||||
|
||||
@ -1,21 +1,9 @@
|
||||
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';
|
||||
|
||||
import '../config/facebook_config.dart';
|
||||
|
||||
/// 事件埋点:Adjust + Facebook App Events 双通道上报
|
||||
/// Adjust 识别码见 docs/adjuest.md
|
||||
/// 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
|
||||
@ -69,75 +57,56 @@ abstract final class AdjustEvents {
|
||||
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) {
|
||||
_track(eventToken);
|
||||
}
|
||||
|
||||
/// 首日充值
|
||||
static void trackFirstPurchase(double amount) {
|
||||
static void trackFirstPurchase() {
|
||||
_track(firstPurchase);
|
||||
_trackFb(
|
||||
'FirstRecharge amount=$amount',
|
||||
() => _fb
|
||||
.logEvent(name: 'FirstRecharge', parameters: {'amount': amount}));
|
||||
}
|
||||
|
||||
/// 支付失败
|
||||
static void trackOrderAbnormal() {
|
||||
_track(orderAbnormal);
|
||||
_trackFb('payment_failed', () => _fb.logEvent(name: 'payment_failed'));
|
||||
}
|
||||
|
||||
/// 支付成功(若为首日充值需额外调用 trackFirstPurchase)
|
||||
static void trackPurchase() {
|
||||
_track(purchase);
|
||||
}
|
||||
|
||||
/// 注册(首次 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(double amount) async {
|
||||
static Future<void> trackPurchaseSuccess() async {
|
||||
_track(purchase);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final registerDate = prefs.getString(_keyRegisterDate);
|
||||
final isFirstPurchase = registerDate != null &&
|
||||
registerDate == DateTime.now().toIso8601String().substring(0, 10);
|
||||
if (isFirstPurchase) {
|
||||
trackFirstPurchase(amount);
|
||||
if (registerDate != null &&
|
||||
registerDate == DateTime.now().toIso8601String().substring(0, 10)) {
|
||||
_track(firstPurchase);
|
||||
}
|
||||
_trackFb(
|
||||
'Purchase',
|
||||
() => _fb.logPurchase(
|
||||
amount: amount,
|
||||
currency: 'USD',
|
||||
));
|
||||
}
|
||||
|
||||
/// 支付失败时调用
|
||||
static void trackPaymentFailed() {
|
||||
trackOrderAbnormal();
|
||||
_track(orderAbnormal);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,9 +2,6 @@ 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';
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:encrypt/encrypt.dart';
|
||||
|
||||
@ -30,10 +29,8 @@ abstract final class ApiCrypto {
|
||||
}
|
||||
|
||||
/// 生成随机 Base64 字符串(用于噪音字段)
|
||||
/// 使用 Random.secure() 保证密码学安全随机性
|
||||
static String randomBase64([int byteLength = 16]) {
|
||||
final random = Random.secure();
|
||||
final bytes = List<int>.generate(byteLength, (_) => random.nextInt(256));
|
||||
final bytes = List<int>.generate(byteLength, (_) => DateTime.now().millisecondsSinceEpoch % 256);
|
||||
return base64Encode(bytes);
|
||||
}
|
||||
|
||||
|
||||
@ -82,7 +82,7 @@ void _logLong(String text) {
|
||||
|
||||
/// 遇到 { 或 [ 视为 JSON 开始,直到与之匹配的 } 或 ] 结束;格式化后单条不超过 1000 字,分条时按行切分保持对齐。
|
||||
void logWithEmbeddedJson(Object? msg) {
|
||||
if (!kDebugMode && !ApiConfig.debugLogs) return;
|
||||
if (!kDebugMode) return;
|
||||
|
||||
if (msg is! String) {
|
||||
_proxyLog.d(msg);
|
||||
@ -265,7 +265,7 @@ class ProxyClient {
|
||||
final loyaltyIndexEnc = ApiCrypto.encrypt(v2BodyEncoded);
|
||||
|
||||
final proxyBody = {
|
||||
ProxyKeys.heroClass: 'petsHeroAI',
|
||||
ProxyKeys.heroClass: ApiConfig.appId,
|
||||
ProxyKeys.petSpecies: petSpeciesEnc,
|
||||
ProxyKeys.powerLevel: powerLevelEnc,
|
||||
ProxyKeys.questRank: questRankEnc,
|
||||
@ -278,10 +278,9 @@ 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'},
|
||||
|
||||
@ -19,8 +19,7 @@ abstract final class UserApi {
|
||||
path: '/v1/user/fast_login',
|
||||
method: 'POST',
|
||||
queryParams: {
|
||||
// if (crest != null) 'crest': crest,
|
||||
"sentinel": "HAndroid",
|
||||
if (crest != null) 'crest': crest,
|
||||
'portal': ApiConfig.packageName,
|
||||
if (accolade != null) 'accolade': accolade,
|
||||
},
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
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';
|
||||
@ -17,27 +16,6 @@ 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._();
|
||||
@ -107,9 +85,7 @@ 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);
|
||||
|
||||
@ -186,7 +162,6 @@ 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>?;
|
||||
@ -231,11 +206,45 @@ class AuthService {
|
||||
UserState.setNavigate(countryCode);
|
||||
}
|
||||
|
||||
// 3. 归因:android_adjust、gg 各上报一次(均等待响应);再拉一次 common_info,extConfig 影响首页时重载分类/任务
|
||||
// 3. 归因上报(digest 从 Adjust 取,暂无则用 install referrer)
|
||||
try {
|
||||
await _reportBothReferrersAndRefreshCommonInfo(uid!, deviceId);
|
||||
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}');
|
||||
}
|
||||
} catch (e) {
|
||||
_logMsg('referrer/common_info 流程异常: $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');
|
||||
}
|
||||
} else {
|
||||
_logMsg('init: 登录失败');
|
||||
@ -250,66 +259,4 @@ 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_adjust、gg(均等待服务端返回,成败都继续);结束后统一拉 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}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
/// 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,10 +1,8 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import '../api/api_config.dart';
|
||||
|
||||
/// 统一应用日志,提升可读性:时间戳、级别、标签、格式化输出。
|
||||
/// release 下默认仅输出 warning/error;ApiConfig.debugLogs=true 时放开全部级别。
|
||||
/// 在 release 下仅输出 warning/error,避免泄露信息。
|
||||
///
|
||||
/// 使用示例:
|
||||
/// final _log = AppLogger('GenerateVideo');
|
||||
@ -27,7 +25,7 @@ class AppLogger {
|
||||
printEmojis: true,
|
||||
dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart,
|
||||
),
|
||||
level: (kDebugMode || ApiConfig.debugLogs) ? Level.trace : Level.warning,
|
||||
level: kReleaseMode ? Level.warning : Level.trace,
|
||||
);
|
||||
return _logger!;
|
||||
}
|
||||
|
||||
@ -1,108 +1,32 @@
|
||||
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';
|
||||
|
||||
/// 归因信息服务(优先从 Adjust 获取,fallback 使用 Play Install Referrer)
|
||||
/// 安装来源 referrer 服务(用于 ch/crest 渠道参数)
|
||||
class ReferrerService {
|
||||
ReferrerService._();
|
||||
|
||||
static String? _cachedReferrer;
|
||||
static String _referrerSource = 'gg';
|
||||
static final Completer<String?> _completer = Completer<String?>();
|
||||
|
||||
/// 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)
|
||||
/// 获取 referrer,Android 使用 Google Play Install Referrer,iOS 返回空
|
||||
static Future<String?> getReferrer() async {
|
||||
if (_cachedReferrer != null) return _cachedReferrer;
|
||||
if (_completer.isCompleted) return _completer.future;
|
||||
|
||||
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 = '';
|
||||
if (defaultTargetPlatform != TargetPlatform.android) {
|
||||
_cachedReferrer = '';
|
||||
if (!_completer.isCompleted) _completer.complete('');
|
||||
return '';
|
||||
}
|
||||
|
||||
if (digest.isEmpty) {
|
||||
_referrerSource = 'gg';
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
try {
|
||||
final details = await PlayInstallReferrer.installReferrer;
|
||||
digest = details.installReferrer ?? '';
|
||||
_cachedReferrer = details.installReferrer ?? '';
|
||||
} catch (_) {
|
||||
digest = '';
|
||||
_cachedReferrer = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_cachedReferrer = digest;
|
||||
if (!_completer.isCompleted) _completer.complete(_cachedReferrer);
|
||||
return _cachedReferrer;
|
||||
}
|
||||
@ -111,27 +35,4 @@ class ReferrerService {
|
||||
static Future<void> init() async {
|
||||
await getReferrer();
|
||||
}
|
||||
|
||||
/// 仅 Adjust 归因 digest(Base64 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 '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,13 +21,6 @@ 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;
|
||||
}
|
||||
|
||||
@ -39,7 +39,6 @@ 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();
|
||||
}
|
||||
@ -49,7 +48,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
UserState.needShowVideoMenu.removeListener(_onExtConfigChanged);
|
||||
UserState.extConfigItems.removeListener(_onExtConfigChanged);
|
||||
AuthService.isLoginComplete.removeListener(_onExtConfigChanged);
|
||||
UserState.homeReloadNonce.removeListener(_onHomeReloadNonce);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -57,11 +55,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
void _onHomeReloadNonce() {
|
||||
if (!mounted) return;
|
||||
_loadCategories();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant HomeScreen oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
@ -280,8 +280,7 @@ class _RechargeScreenState extends State<RechargeScreen>
|
||||
}
|
||||
_showSnackBar(
|
||||
context, 'Order created. Complete payment in the page.');
|
||||
AdjustEvents.trackPurchaseSuccess(
|
||||
(AdjustEvents.parsePrice(item.actualAmount) ?? 0).toDouble());
|
||||
AdjustEvents.trackPurchaseSuccess();
|
||||
}
|
||||
} else {
|
||||
if (mounted) {
|
||||
@ -289,8 +288,7 @@ class _RechargeScreenState extends State<RechargeScreen>
|
||||
_showSnackBar(
|
||||
context, 'Order created. Awaiting payment confirmation.');
|
||||
}
|
||||
AdjustEvents.trackPurchaseSuccess(
|
||||
(AdjustEvents.parsePrice(item.actualAmount) ?? 0).toDouble());
|
||||
AdjustEvents.trackPurchaseSuccess();
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
@ -458,8 +456,7 @@ class _RechargeScreenState extends State<RechargeScreen>
|
||||
if (mounted) {
|
||||
_showSnackBar(context, 'Purchase completed.');
|
||||
}
|
||||
AdjustEvents.trackPurchaseSuccess(
|
||||
(AdjustEvents.parsePrice(item.actualAmount) ?? 0).toDouble());
|
||||
AdjustEvents.trackPurchaseSuccess();
|
||||
} else {
|
||||
_showSnackBar(
|
||||
context,
|
||||
|
||||
@ -1,15 +1,12 @@
|
||||
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';
|
||||
@ -18,9 +15,7 @@ import 'features/recharge/google_play_purchase_service.dart';
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
_initAdjust();
|
||||
_initFacebookAppEvents();
|
||||
// 等待 Adjust 归因(ReferrerService 会调用 Adjust.getAttributionWithTimeout)
|
||||
await ReferrerService.init();
|
||||
ReferrerService.init();
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: AppColors.surface,
|
||||
@ -28,7 +23,7 @@ void main() async {
|
||||
statusBarBrightness: Brightness.light,
|
||||
),
|
||||
);
|
||||
// Adjust 初始化后执行登录,确保登录时归因数据已就绪
|
||||
// 先启动登录,确保首次构建时 loginComplete 可被监听
|
||||
AuthService.init();
|
||||
runApp(const App());
|
||||
// 尽早订阅 purchaseStream,否则未确认订单不会出现在 queryPastPurchases 中,补单会为空
|
||||
@ -36,8 +31,7 @@ void main() async {
|
||||
GooglePlayPurchaseService.startPendingPurchaseListener();
|
||||
}
|
||||
// 登录完成后执行谷歌支付补单(未核销订单上报服务端并 completePurchase)
|
||||
AuthService.loginComplete
|
||||
.then((_) => GooglePlayPurchaseService.runOrderRecovery());
|
||||
AuthService.loginComplete.then((_) => GooglePlayPurchaseService.runOrderRecovery());
|
||||
}
|
||||
|
||||
void _initAdjust() {
|
||||
@ -46,29 +40,16 @@ void _initAdjust() {
|
||||
appToken,
|
||||
kDebugMode ? AdjustEnvironment.sandbox : AdjustEnvironment.production,
|
||||
);
|
||||
// config.fbAppId = FacebookConfig.appId;
|
||||
if (kDebugMode || ApiConfig.debugLogs) {
|
||||
if (kDebugMode) {
|
||||
config.logLevel = AdjustLogLevel.verbose;
|
||||
}
|
||||
config.attributionCallback = _onAdjustAttribution;
|
||||
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) {
|
||||
// 注入 ReferrerService,与 getAttributionWithTimeout 竞速,首次安装时归因多由此回调返回
|
||||
ReferrerService.receiveAttributionFromCallback(attribution);
|
||||
_adjustLog.d('归因信息: '
|
||||
'trackerToken=${attribution.trackerToken}, '
|
||||
'trackerName=${attribution.trackerName}, '
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
name: pets_hero_ai
|
||||
description: PetsHero AI Application.
|
||||
publish_to: 'none'
|
||||
version: 1.1.11+22
|
||||
version: 1.1.1+12
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
@ -9,9 +9,7 @@ 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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user