优化:将应用端的配置改用JSON形式更加灵活

This commit is contained in:
ivan 2026-03-26 16:19:03 +08:00
parent 83c1c56c36
commit e77f5f45b7
36 changed files with 1247 additions and 234 deletions

View File

@ -21,6 +21,19 @@
], ],
"devDependencies": [] "devDependencies": []
}, },
{
"name": "shared_preferences",
"version": "2.5.4",
"dependencies": [
"flutter",
"shared_preferences_android",
"shared_preferences_foundation",
"shared_preferences_linux",
"shared_preferences_platform_interface",
"shared_preferences_web",
"shared_preferences_windows"
]
},
{ {
"name": "play_install_referrer", "name": "play_install_referrer",
"version": "0.5.0", "version": "0.5.0",
@ -37,6 +50,16 @@
"in_app_purchase_platform_interface" "in_app_purchase_platform_interface"
] ]
}, },
{
"name": "in_app_purchase",
"version": "3.2.3",
"dependencies": [
"flutter",
"in_app_purchase_android",
"in_app_purchase_platform_interface",
"in_app_purchase_storekit"
]
},
{ {
"name": "facebook_app_events", "name": "facebook_app_events",
"version": "0.26.0", "version": "0.26.0",
@ -101,6 +124,64 @@
"vector_math" "vector_math"
] ]
}, },
{
"name": "shared_preferences_windows",
"version": "2.4.1",
"dependencies": [
"file",
"flutter",
"path",
"path_provider_platform_interface",
"path_provider_windows",
"shared_preferences_platform_interface"
]
},
{
"name": "shared_preferences_web",
"version": "2.4.3",
"dependencies": [
"flutter",
"flutter_web_plugins",
"shared_preferences_platform_interface",
"web"
]
},
{
"name": "shared_preferences_platform_interface",
"version": "2.4.1",
"dependencies": [
"flutter",
"plugin_platform_interface"
]
},
{
"name": "shared_preferences_linux",
"version": "2.4.1",
"dependencies": [
"file",
"flutter",
"path",
"path_provider_linux",
"path_provider_platform_interface",
"shared_preferences_platform_interface"
]
},
{
"name": "shared_preferences_foundation",
"version": "2.5.6",
"dependencies": [
"flutter",
"shared_preferences_platform_interface"
]
},
{
"name": "shared_preferences_android",
"version": "2.4.21",
"dependencies": [
"flutter",
"shared_preferences_platform_interface"
]
},
{ {
"name": "in_app_purchase_platform_interface", "name": "in_app_purchase_platform_interface",
"version": "1.4.0", "version": "1.4.0",
@ -114,6 +195,16 @@
"version": "1.19.1", "version": "1.19.1",
"dependencies": [] "dependencies": []
}, },
{
"name": "in_app_purchase_storekit",
"version": "0.4.8+1",
"dependencies": [
"collection",
"flutter",
"in_app_purchase_platform_interface",
"json_annotation"
]
},
{ {
"name": "meta", "name": "meta",
"version": "1.17.0", "version": "1.17.0",
@ -195,6 +286,70 @@
"version": "1.4.1", "version": "1.4.1",
"dependencies": [] "dependencies": []
}, },
{
"name": "path_provider_windows",
"version": "2.3.0",
"dependencies": [
"ffi",
"flutter",
"path",
"path_provider_platform_interface"
]
},
{
"name": "path_provider_platform_interface",
"version": "2.1.2",
"dependencies": [
"flutter",
"platform",
"plugin_platform_interface"
]
},
{
"name": "path",
"version": "1.9.1",
"dependencies": []
},
{
"name": "file",
"version": "7.0.1",
"dependencies": [
"meta",
"path"
]
},
{
"name": "flutter_web_plugins",
"version": "0.0.0",
"dependencies": [
"flutter"
]
},
{
"name": "plugin_platform_interface",
"version": "2.1.8",
"dependencies": [
"meta"
]
},
{
"name": "path_provider_linux",
"version": "2.2.1",
"dependencies": [
"ffi",
"flutter",
"path",
"path_provider_platform_interface",
"xdg_directories"
]
},
{
"name": "json_annotation",
"version": "4.11.0",
"dependencies": [
"meta"
]
},
{ {
"name": "js", "name": "js",
"version": "0.7.2", "version": "0.7.2",
@ -224,152 +379,15 @@
] ]
}, },
{ {
"name": "term_glyph", "name": "ffi",
"version": "1.2.2", "version": "2.2.0",
"dependencies": [] "dependencies": []
}, },
{
"name": "path",
"version": "1.9.1",
"dependencies": []
},
{
"name": "plugin_platform_interface",
"version": "2.1.8",
"dependencies": [
"meta"
]
},
{
"name": "in_app_purchase",
"version": "3.2.3",
"dependencies": [
"flutter",
"in_app_purchase_android",
"in_app_purchase_platform_interface",
"in_app_purchase_storekit"
]
},
{
"name": "in_app_purchase_storekit",
"version": "0.4.8+1",
"dependencies": [
"collection",
"flutter",
"in_app_purchase_platform_interface",
"json_annotation"
]
},
{
"name": "json_annotation",
"version": "4.11.0",
"dependencies": [
"meta"
]
},
{
"name": "shared_preferences",
"version": "2.5.4",
"dependencies": [
"flutter",
"shared_preferences_android",
"shared_preferences_foundation",
"shared_preferences_linux",
"shared_preferences_platform_interface",
"shared_preferences_web",
"shared_preferences_windows"
]
},
{
"name": "shared_preferences_windows",
"version": "2.4.1",
"dependencies": [
"file",
"flutter",
"path",
"path_provider_platform_interface",
"path_provider_windows",
"shared_preferences_platform_interface"
]
},
{
"name": "shared_preferences_platform_interface",
"version": "2.4.1",
"dependencies": [
"flutter",
"plugin_platform_interface"
]
},
{
"name": "shared_preferences_linux",
"version": "2.4.1",
"dependencies": [
"file",
"flutter",
"path",
"path_provider_linux",
"path_provider_platform_interface",
"shared_preferences_platform_interface"
]
},
{
"name": "shared_preferences_web",
"version": "2.4.3",
"dependencies": [
"flutter",
"flutter_web_plugins",
"shared_preferences_platform_interface",
"web"
]
},
{
"name": "flutter_web_plugins",
"version": "0.0.0",
"dependencies": [
"flutter"
]
},
{
"name": "shared_preferences_foundation",
"version": "2.5.6",
"dependencies": [
"flutter",
"shared_preferences_platform_interface"
]
},
{
"name": "file",
"version": "7.0.1",
"dependencies": [
"meta",
"path"
]
},
{
"name": "path_provider_platform_interface",
"version": "2.1.2",
"dependencies": [
"flutter",
"platform",
"plugin_platform_interface"
]
},
{ {
"name": "platform", "name": "platform",
"version": "3.1.6", "version": "3.1.6",
"dependencies": [] "dependencies": []
}, },
{
"name": "path_provider_linux",
"version": "2.2.1",
"dependencies": [
"ffi",
"flutter",
"path",
"path_provider_platform_interface",
"xdg_directories"
]
},
{ {
"name": "xdg_directories", "name": "xdg_directories",
"version": "1.1.0", "version": "1.1.0",
@ -379,27 +397,9 @@
] ]
}, },
{ {
"name": "ffi", "name": "term_glyph",
"version": "2.2.0", "version": "1.2.2",
"dependencies": [] "dependencies": []
},
{
"name": "path_provider_windows",
"version": "2.3.0",
"dependencies": [
"ffi",
"flutter",
"path",
"path_provider_platform_interface"
]
},
{
"name": "shared_preferences_android",
"version": "2.4.21",
"dependencies": [
"flutter",
"shared_preferences_platform_interface"
]
} }
], ],
"configVersion": 1 "configVersion": 1

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 FunyMee
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -2,6 +2,8 @@
通用代理 API 框架。**接口请求已按原始字段名写好**,不同应用只需**修改映射表**即可接入不同后端。 通用代理 API 框架。**接口请求已按原始字段名写好**,不同应用只需**修改映射表**即可接入不同后端。
**新建换皮应用**:请阅读 [docs/create_new_skin_app.md](docs/create_new_skin_app.md)`skin_config.json` + `ClientBootstrap` 全流程)。
## 设计思路 ## 设计思路
- **业务层**:统一使用**原始字段名**canonical`deviceId``userId``credits``userToken` - **业务层**:统一使用**原始字段名**canonical`deviceId``userId``credits``userToken`

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

View File

@ -0,0 +1,2 @@
#Thu Mar 26 15:16:08 CST 2026
gradle.version=8.9

View File

51
android/build.gradle Normal file
View File

@ -0,0 +1,51 @@
group = "com.funymee.client_proxy_framework"
version = "1.0-SNAPSHOT"
buildscript {
ext.kotlin_version = "2.1.0"
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.6.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: "com.android.library"
apply plugin: "kotlin-android"
android {
namespace = "com.funymee.client_proxy_framework"
compileSdk = 35
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
minSdk = 24
}
lintOptions {
disable "InvalidPackage"
}
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version")
implementation("com.facebook.android:facebook-android-sdk:18.0.0")
}

1
android/settings.gradle Normal file
View File

@ -0,0 +1 @@
rootProject.name = "client_proxy_framework"

View File

@ -0,0 +1 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />

View File

@ -0,0 +1,49 @@
package com.funymee.client_proxy_framework
import android.app.Application
import com.facebook.appevents.AppEventsLogger
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
/** Facebook App Events在引擎侧注册固定 Channel供 Dart 触发 activateApp。 */
class ClientProxyFrameworkPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private var channel: MethodChannel? = null
private var applicationContext: android.content.Context? = null
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
applicationContext = binding.applicationContext
val ch = MethodChannel(binding.binaryMessenger, CHANNEL_NAME)
channel = ch
ch.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel?.setMethodCallHandler(null)
channel = null
applicationContext = null
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"waitForFacebookSdkInit" -> {
try {
val ctx = applicationContext
val app = ctx?.applicationContext as? Application
if (app != null) {
AppEventsLogger.activateApp(app)
}
result.success(true)
channel?.invokeMethod("onFacebookSdkInitialized", null)
} catch (e: Exception) {
result.error("FB_ACTIVATE_APP", e.message, null)
}
}
else -> result.notImplemented()
}
}
companion object {
const val CHANNEL_NAME = "client_proxy_framework/facebook_sdk"
}
}

View File

@ -11,6 +11,10 @@
- **强类型实体**:返回强类型实体类,开发者直接使用映射后的字段 - **强类型实体**:返回强类型实体类,开发者直接使用映射后的字段
- **统一响应**`EntityResponse<T>` 提供强类型返回值 - **统一响应**`EntityResponse<T>` 提供强类型返回值
### 换皮应用:从零搭建
从零创建一个新的换皮应用(`skin_config.json``ClientBootstrap``main` 初始化顺序、Android/iOS 要点等),请阅读 **[《创建新换皮应用 — 步骤清单》](create_new_skin_app.md)**。
--- ---
## 2. 快速开始 ## 2. 快速开始

160
docs/create_new_skin_app.md Normal file
View File

@ -0,0 +1,160 @@
# 创建新换皮应用 — 步骤清单
本文说明基于 `client_proxy_framework` **从零搭一个换皮应用**的推荐流程。配置以 **`skin_config.json`** 为主;更细的字段说明见 [`new_app_config_template.md`](new_app_config_template.md),原生侧见 [`sdk_integration_guide.md`](sdk_integration_guide.md)。更长篇的上下文与业务对接见 [`skin_app_development_guide.md`](skin_app_development_guide.md)。
***
## 1. 目录与仓库布局
建议将新应用与框架放在**同级目录**,便于 path 依赖:
```text
Workplace/
├── client_proxy_framework/ # 本框架
└── your_skin_app/ # 新换皮应用
```
***
## 2. 创建 Flutter 工程
```bash
cd Workplace
flutter create your_skin_app
cd your_skin_app
```
***
## 3. 接入框架依赖
在应用根目录 **`pubspec.yaml`** 中增加:
```yaml
dependencies:
flutter:
sdk: flutter
client_proxy_framework:
path: ../client_proxy_framework
```
换皮应用若实现自有登录回调(设备 ID、签名等通常还需要
```yaml
device_info_plus: ^11.1.0 # 常见:读 Android id / iOS identifierForVendor
crypto: ^3.0.3 # 常见:与后端约定的 MD5 签名
```
执行:
```bash
flutter pub get
```
框架已通过插件注册 **Adjust、Facebook、IAP、Play Install Referrer** 等依赖,宿主 **无需** 再单独声明 `adjust_sdk``facebook_app_events`,除非你有特殊版本要求。
***
## 4. 准备 `skin_config.json`
1. 在应用内创建目录 **`assets/`**。
2. 复制框架内的示例为起点:
- 源文件:框架仓库 [`lib/src/config/skin_config.example.json`](../lib/src/config/skin_config.example.json)
- 目标:`assets/skin_config.json`
3. 按后端交付的换皮参数填写 JSON主要块包括
- **`app`**`name``id`(业务 appId`packageName`(渠道包名)
- **`backend`**`iosAppType``androidAppType`(如 `HIOS` / `HAndroid`,与 fast_login 约定一致)
- **`api`**:预发/生产 URL、`proxyPath`、**16 字符** `aesKey``debugBaseUrlOverride``alwaysUsePreBaseUrl`(长期连预发时可 `true`
- **`proxyKeys` / `v2`**与后端约定的代理字段名、V2 包装路径与噪音键
- **`fieldMapping`**:原始字段 → V2 字段(可很大,整块放在 JSON
- **`analytics`**Adjust / Facebook占位符会在运行时跳过初始化并在控制台提醒
- **`adjustEvents`**:可选,逻辑名 → Adjust 事件 Token
4. 在 **`pubspec.yaml`** 注册资源:
```yaml
flutter:
assets:
- assets/skin_config.json
```
***
## 5. 应用入口 `main.dart`(推荐顺序)
异步初始化与 **归因缓存** 须在启动登录前完成,推荐顺序如下:
```dart
import 'package:flutter/material.dart';
import 'package:client_proxy_framework/client_proxy_framework.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await ClientBootstrap.initFromAsset('assets/skin_config.json');
await ClientBootstrap.initAnalytics();
await AnalyticsService.initAttribution();
runApp(MyApp(title: ClientBootstrap.skin.appName));
// 若使用 FrameworkAuthService 自动登录,在 runApp 之后或之前按产品要求调用
// MyAuthService.init(); // 内部FrameworkAuthService.init(你的 AuthServiceCallbacks); start();
}
```
要点:
- **`ClientBootstrap.initFromAsset`**:加载 JSON 并 `ApiClient.init`
- **`ClientBootstrap.initAnalytics`**Adjust / Facebook未配置或占位则降级不卡死
- **`AnalyticsService.initAttribution`**:预取归因,供登录时 referer框架内 **`FrameworkAuthService.init` 默认已注册** 与 Adjust 缓存一致的 **`AnalyticsAttributionCallbacks`**,应用侧一般**不用再写归因回调类**。
***
## 6. Android 配置
1. **包名**`android/app/build.gradle.kts`(或 `build.gradle`)中的 `applicationId``namespace`,与 JSON 里 `app.packageName` 一致Kotlin 目录与 `MainActivity` 包名同步修改。
2. **`MainActivity`**:使用默认 **`FlutterActivity`** 即可,**不要**再为 Facebook 单独写 `{packageName}/facebook_sdk` 的 MethodChannel框架插件使用固定通道 **`client_proxy_framework/facebook_sdk`**。
3. **`AndroidManifest.xml`**:按 Adjust / Facebook 官方要求配置 `meta-data`App Token、环境、Facebook AppId、ClientToken 等),并与 `strings.xml` 等引用一致。细节见 [`sdk_integration_guide.md`](sdk_integration_guide.md)。
4. **应用级 `Application` 子类**:一般不再需要仅为 Facebook 挂自定义 `Application`;若历史工程仍有,可评估删除后以默认 `Application` 运行。
***
## 7. iOS 配置
1. Xcode 中 **Bundle Identifier**`skin_config.json``packageName` / 业务约定一致。
2. **`Info.plist`**Facebook、Adjust 等键值与控制台一致;参见 [`sdk_integration_guide.md`](sdk_integration_guide.md)。
3. 工程根目录执行 **`pod install`** 后编译。
***
## 8. 登录与业务状态(应用侧最少代码)
框架负责:代理请求加解密、字段映射、**归因 referer 默认实现**、Adjust/Facebook 初始化(可配置)。
应用侧通常仍需:
1. 实现 **`AuthServiceCallbacks`**`getDeviceId``computeSign``onLoginSuccess` / `onCommonInfoLoaded` / `onLoginFailed`(例如写入本地 `UserState`、路由)。
2. 调用 **`FrameworkAuthService.init(你的回调)`**,再 **`await FrameworkAuthService.start()`**(可参考示例工程里的薄封装 `AuthService.init()`)。
无需再实现 **`AttributionCallbacks`**,除非要自定义 referer 格式;此时用 **`FrameworkAuthService.init(callbacks, attributionCallbacks: 你的实现)`**。
***
## 9. 自检与上线前
1. **调试运行**:观察控制台是否出现 `[client_proxy_framework]` 的 SDK 配置提醒;占位 Token 会跳过对应 SDK**不要**带着 TODO 配置上生产。
2. **接口**:确认 `debug` / `release``api.alwaysUsePreBaseUrl``debugBaseUrlOverride` 是否符合预期。
3. **发布**:补齐 Adjust/Facebook 真实参数;完成签名、隐私政策、应用商店物料等(不在本文范围)。
***
## 10. 相关文档索引
| 文档 | 用途 |
|------|------|
| [`new_app_config_template.md`](new_app_config_template.md) | 按表逐项填写换皮参数(可对照填入 JSON |
| [`sdk_integration_guide.md`](sdk_integration_guide.md) | 原生工程 Adjust / Facebook 等集成要点 |
| [`skin_app_development_guide.md`](skin_app_development_guide.md) | 更长流程、业务/UI 后续对接说明 |
| 框架 `lib/src/config/skin_config.example.json` | JSON 最小结构示例 |
若本清单与旧文档冲突,**以本清单与当前框架代码为准**。

View File

@ -2,6 +2,8 @@
本文档说明如何使用 `client_proxy_framework` 从零创建并完成一个换皮应用。 本文档说明如何使用 `client_proxy_framework` 从零创建并完成一个换皮应用。
> **推荐优先阅读**[《创建新换皮应用 — 步骤清单》](create_new_skin_app.md) — 与当前框架实现JSON 配置、`ClientBootstrap`、固定 Facebook Channel、默认归因回调等保持同步的精简步骤。
*** ***
> **重要说明**:本指南**仅完成数据框架的对接**,包括: > **重要说明**:本指南**仅完成数据框架的对接**,包括:

View File

@ -0,0 +1,24 @@
import Flutter
import UIKit
public class ClientProxyFrameworkPlugin: NSObject, FlutterPlugin {
private var channel: FlutterMethodChannel?
public static func register(with registrar: FlutterPluginRegistrar) {
let ch = FlutterMethodChannel(
name: "client_proxy_framework/facebook_sdk",
binaryMessenger: registrar.messenger())
let instance = ClientProxyFrameworkPlugin()
instance.channel = ch
registrar.addMethodCallDelegate(instance, channel: ch)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
if call.method == "waitForFacebookSdkInit" {
result(true)
channel?.invokeMethod("onFacebookSdkInitialized", arguments: nil)
} else {
result(FlutterMethodNotImplemented)
}
}
}

View File

@ -0,0 +1,17 @@
Pod::Spec.new do |s|
s.name = 'client_proxy_framework'
s.version = '1.0.0'
s.summary = 'Client proxy framework (Facebook bridge)'
s.description = <<-DESC
Registers MethodChannel for Facebook SDK init handshake used by Dart.
DESC
s.homepage = 'https://example.com'
s.license = { :type => 'MIT', :file => '../LICENSE' }
s.author = { 'FunyMee' => 'dev@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '13.0'
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
s.swift_version = '5.0'
end

View File

@ -1,22 +1,29 @@
/// API /// API
/// ///
/// ///
/// 1. [AppConfig] appIdaesKeybaseUrl /// 1. assets `skin_config.json` [SkinConfig]
/// 2. main ApiClient.init(yourConfig) /// 2. `await ClientBootstrap.initFromAsset('assets/skin_config.json');`
/// 3. ApiClient.instance.proxy /// 3. `await ClientBootstrap.initAnalytics();`
/// 4. [ApiClient.instance.proxy]
///
/// [AppConfig] `ApiClient.init(config)`
library; library;
export 'src/api/api_client.dart'; export 'src/api/api_client.dart';
export 'src/api/api_crypto.dart'; export 'src/api/api_crypto.dart';
export 'src/api/api_response.dart'; export 'src/api/api_response.dart';
export 'src/api/proxy_client.dart'; export 'src/api/proxy_client.dart';
export 'src/bootstrap/client_bootstrap.dart';
export 'src/config/app_config.dart'; export 'src/config/app_config.dart';
export 'src/config/attribution_config.dart'; export 'src/config/attribution_config.dart';
export 'src/config/skin_config.dart';
export 'src/config/field_mapping.dart'; export 'src/config/field_mapping.dart';
export 'src/config/default_field_mapping.dart'; export 'src/config/default_field_mapping.dart';
export 'src/entities/entities.dart'; export 'src/entities/entities.dart';
export 'src/log/app_logger.dart'; export 'src/log/app_logger.dart';
export 'src/log/sdk_reminder_log.dart';
export 'src/services/adjust_service.dart'; export 'src/services/adjust_service.dart';
export 'src/services/analytics_attribution_callbacks.dart';
export 'src/services/analytics_service.dart'; export 'src/services/analytics_service.dart';
export 'src/services/auth_service.dart'; export 'src/services/auth_service.dart';
export 'src/services/facebook_service.dart'; export 'src/services/facebook_service.dart';

View File

@ -0,0 +1,56 @@
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import '../api/api_client.dart';
import '../config/skin_config.dart';
import '../services/analytics_service.dart';
/// JSON ApiClient Analytics
abstract final class ClientBootstrap {
static SkinConfig? _skin;
/// [initFromAsset] / [initFromJson]
static SkinConfig get skin {
final s = _skin;
if (s == null) {
throw StateError(
'ClientBootstrap not initialized. Call initFromAsset or initFromJson first.',
);
}
return s;
}
static SkinConfig? get skinOrNull => _skin;
/// Asset [SkinConfig] [ApiClient][AnalyticsService]
///
/// 宿 `initAttribution`
/// `FrameworkAuthService.init``start`
static Future<SkinConfig> initFromAsset(String assetPath) async {
WidgetsFlutterBinding.ensureInitialized();
final raw = await rootBundle.loadString(assetPath);
return initFromJsonString(raw);
}
static Future<SkinConfig> initFromJsonString(String json) async {
final skin = SkinConfig.fromJsonString(json);
_apply(skin);
return skin;
}
static Future<SkinConfig> initFromJson(Map<String, dynamic> map) async {
final skin = SkinConfig.fromJson(map);
_apply(skin);
return skin;
}
static void _apply(SkinConfig skin) {
_skin = skin;
ApiClient.init(skin);
}
/// SDKAdjust / Facebook [initFromAsset]
static Future<void> initAnalytics() async {
await AnalyticsService.init(skin.buildAnalyticsConfig());
}
}

View File

@ -6,7 +6,7 @@ import 'field_mapping.dart';
/// ///
class ProxyKeysConfig { class ProxyKeysConfig {
const ProxyKeysConfig({ const ProxyKeysConfig({
/// appId /// **** [AppConfig.appName]
this.appIdField = 'hero_class', this.appIdField = 'hero_class',
/// path /// path
@ -35,7 +35,7 @@ class ProxyKeysConfig {
], ],
}); });
/// appId /// = appName
final String appIdField; final String appIdField;
/// path /// path
@ -65,13 +65,19 @@ abstract class AppConfig {
/// ///
String get appName; String get appName;
/// hero_class /// fast_login query `app` app id
String get appId; String get appId;
/// /// /
String get packageName; String get packageName;
/// AES /// fast_login **iOS** 使 `app` `HIOS`
String get backendAppTypeIOS => 'HIOS';
/// fast_login **Android** 使 `app` `HAndroid`
String get backendAppTypeAndroid => 'HAndroid';
/// AES-128 crypto实现为 16
String get aesKey; String get aesKey;
/// ///

View File

@ -57,8 +57,8 @@ abstract class AttributionCallbacks {
Future<String?> getFacebookReferrer(); Future<String?> getFacebookReferrer();
} }
/// /// 使 [AnalyticsAttributionCallbacks]
class DefaultAttributionCallbacks implements AttributionCallbacks { class StubAttributionCallbacks implements AttributionCallbacks {
@override @override
Future<String?> getReferrer() async => ''; Future<String?> getReferrer() async => '';

View File

@ -0,0 +1,393 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import '../log/sdk_reminder_log.dart';
import '../services/analytics_service.dart';
import 'app_config.dart';
import 'attribution_config.dart';
import 'default_field_mapping.dart';
import 'field_mapping.dart';
/// JSON API
///
/// 宿 `pubspec.yaml` assets 使
/// `await ClientBootstrap.initFromAsset('assets/skin_config.json');`
///
/// `lib/src/config/skin_config.example.json`
class SkinConfig implements AppConfig {
SkinConfig._({
required this.appName,
required this.appId,
required this.packageName,
required this.backendAppTypeIOS,
required this.backendAppTypeAndroid,
required this.preBaseUrl,
required this.prodBaseUrl,
required this.proxyPath,
required this.debugBaseUrlOverride,
required this.alwaysUsePreBaseUrl,
required this.aesKey,
required this.proxyKeys,
required this.v2LevelField,
required this.v2LevelFixedValue,
required this.v2SanctumPath,
required this.v2NoiseKeys,
required this.fieldMapping,
required Map<String, String> adjustEvents,
this.analyticsJson,
}) : _adjustEvents = adjustEvents;
final Map<String, dynamic>? analyticsJson;
final Map<String, String> _adjustEvents;
/// JSON `rootBundle.loadString`
factory SkinConfig.fromJsonString(String source) {
final dynamic decoded = jsonDecode(source);
if (decoded is! Map<String, dynamic>) {
throw FormatException('skin_config root must be a JSON object');
}
return SkinConfig.fromJson(decoded);
}
factory SkinConfig.fromJson(Map<String, dynamic> json) {
final app = _map(json['app'], 'app');
final api = _map(json['api'], 'api');
final name = app['name'] as String?;
final id = app['id'] as String?;
final pkg = app['packageName'] as String?;
if (name == null || name.isEmpty) {
throw FormatException('app.name is required');
}
if (id == null || id.isEmpty) {
throw FormatException('app.id is required');
}
if (pkg == null || pkg.isEmpty) {
throw FormatException('app.packageName is required');
}
final pre = api['preBaseUrl'] as String?;
final prod = api['prodBaseUrl'] as String?;
final proxyPath = api['proxyPath'] as String?;
final aes = api['aesKey'] as String?;
if (pre == null || pre.isEmpty) {
throw FormatException('api.preBaseUrl is required');
}
if (prod == null || prod.isEmpty) {
throw FormatException('api.prodBaseUrl is required');
}
if (proxyPath == null || proxyPath.isEmpty) {
throw FormatException('api.proxyPath is required');
}
if (aes == null || aes.isEmpty) {
throw FormatException('api.aesKey is required (16 chars for current crypto)');
}
final backend = json['backend'];
String iosType = 'HIOS';
String androidType = 'HAndroid';
if (backend is Map<String, dynamic>) {
iosType = backend['iosAppType'] as String? ?? iosType;
androidType = backend['androidAppType'] as String? ?? androidType;
}
final debugOverride = api['debugBaseUrlOverride'] as String?;
final alwaysPre = api['alwaysUsePreBaseUrl'] as bool? ?? false;
final proxyKeys = _proxyKeysFromJson(json['proxyKeys']);
final v2 = json['v2'];
String v2Level = 'arsenal';
int v2Fixed = 4;
List<String> sanctum = const [
'vault',
'tome',
'codex',
'grimoire',
'sanctum',
];
List<String> v2Noise = const [
'roar',
'clash',
'thunder',
'rumble',
'howl',
'growl',
];
if (v2 is Map<String, dynamic>) {
v2Level = v2['levelField'] as String? ?? v2Level;
v2Fixed = (v2['levelFixedValue'] as num?)?.toInt() ?? v2Fixed;
final path = v2['sanctumPath'];
if (path is List) {
sanctum = path.map((e) => e.toString()).toList();
}
final nk = v2['noiseKeys'];
if (nk is List) {
v2Noise = nk.map((e) => e.toString()).toList();
}
}
FieldMapping mapping;
final fmRaw = json['fieldMapping'];
if (fmRaw == null) {
mapping = petsHeroAIFieldMapping;
} else if (fmRaw is Map<String, dynamic>) {
if (fmRaw.isEmpty) {
mapping = petsHeroAIFieldMapping;
} else {
final m = <String, String>{};
for (final e in fmRaw.entries) {
m[e.key.toString()] = e.value.toString();
}
mapping = FieldMapping(m);
}
} else {
throw FormatException('fieldMapping must be a JSON object');
}
final eventsRaw = json['adjustEvents'];
final events = <String, String>{};
if (eventsRaw is Map<String, dynamic>) {
for (final e in eventsRaw.entries) {
final v = e.value;
if (v != null && v.toString().isNotEmpty) {
events[e.key.toString()] = v.toString();
}
}
}
return SkinConfig._(
appName: name,
appId: id,
packageName: pkg,
backendAppTypeIOS: iosType,
backendAppTypeAndroid: androidType,
preBaseUrl: pre,
prodBaseUrl: prod,
proxyPath: proxyPath,
debugBaseUrlOverride: debugOverride,
alwaysUsePreBaseUrl: alwaysPre,
aesKey: aes,
proxyKeys: proxyKeys,
v2LevelField: v2Level,
v2LevelFixedValue: v2Fixed,
v2SanctumPath: sanctum,
v2NoiseKeys: v2Noise,
fieldMapping: mapping,
adjustEvents: events,
analyticsJson: json['analytics'] as Map<String, dynamic>?,
);
}
static Map<String, dynamic> _map(dynamic v, String name) {
if (v is! Map<String, dynamic>) {
throw FormatException('$name must be a JSON object');
}
return v;
}
static ProxyKeysConfig _proxyKeysFromJson(dynamic v) {
if (v == null) return const ProxyKeysConfig();
if (v is! Map<String, dynamic>) {
throw FormatException('proxyKeys must be a JSON object');
}
List<String> noise = const [
'billing_addr',
'utm_term',
'cluster_id',
'lsn_value',
'accuracy_val',
'dir_path',
];
final nk = v['noiseKeys'];
if (nk is List && nk.isNotEmpty) {
noise = nk.map((e) => e.toString()).toList();
}
return ProxyKeysConfig(
appIdField: v['appIdField'] as String? ?? 'hero_class',
pathField: v['pathField'] as String? ?? 'pet_species',
methodField: v['methodField'] as String? ?? 'power_level',
headerField: v['headerField'] as String? ?? 'quest_rank',
paramsField: v['paramsField'] as String? ?? 'battle_score',
bodyField: v['bodyField'] as String? ?? 'loyalty_index',
noiseKeys: noise,
);
}
@override
final String appName;
@override
final String appId;
@override
final String packageName;
@override
final String backendAppTypeIOS;
@override
final String backendAppTypeAndroid;
@override
final String preBaseUrl;
@override
final String prodBaseUrl;
@override
final String proxyPath;
@override
final String? debugBaseUrlOverride;
/// true [baseUrl] [preBaseUrl]便
final bool alwaysUsePreBaseUrl;
@override
String get baseUrl {
if (alwaysUsePreBaseUrl) return preBaseUrl;
if (!kDebugMode) return prodBaseUrl;
return debugBaseUrlOverride ?? preBaseUrl;
}
@override
String get proxyUrl => '$baseUrl$proxyPath';
@override
final String aesKey;
@override
final ProxyKeysConfig proxyKeys;
@override
final String v2LevelField;
@override
final int v2LevelFixedValue;
@override
final List<String> v2SanctumPath;
@override
final List<String> v2NoiseKeys;
@override
final FieldMapping fieldMapping;
@override
Map<String, dynamic> get v2FixedValues => {v2LevelField: v2LevelFixedValue};
/// Adjust Dashboard token map
Map<String, String> get adjustEventTokens =>
Map<String, String>.unmodifiable(_adjustEvents);
/// [adjustEvents] Adjust token
void trackAdjustEvent(String logicalName) {
final token = _adjustEvents[logicalName];
if (token != null && token.isNotEmpty) {
AnalyticsService.trackEvent(token);
}
}
/// [AnalyticsService.init] JSON `analytics`
AnalyticsConfig buildAnalyticsConfig({bool debugLogsFallback = false}) {
final root = analyticsJson;
bool debugLogs = debugLogsFallback;
AdjustConfig? adjustCfg;
FacebookConfig? fbCfg;
if (root != null) {
debugLogs = root['debugLogs'] as bool? ?? debugLogs;
final adj = root['adjust'];
if (adj is Map<String, dynamic>) {
final token = adj['appToken'] as String?;
if (token != null && _isUsableSecret(token)) {
adjustCfg = AdjustConfig(
appToken: token.trim(),
environment: _parseAdjustEnv(adj['environment'] as String?),
logLevel: _parseAdjustLogLevel(adj['logLevel'] as String?),
fbAppId: adj['fbAppId'] as String?,
);
} else if (token != null && token.trim().isNotEmpty) {
SdkReminderLog.adjust('appToken 为占位或无效值,已跳过 Adjust。');
}
}
final fb = root['facebook'];
if (fb is Map<String, dynamic>) {
final id = fb['appId'] as String? ?? '';
final clientTok = fb['clientToken'] as String? ?? '';
final idOk = _isUsableSecret(id);
final ct = clientTok.trim();
final ctOk = ct.isEmpty || _isUsableSecret(ct);
if (idOk && ctOk) {
fbCfg = FacebookConfig(
appId: id.trim(),
clientToken: ct.isEmpty ? null : ct,
debugLogs: fb['debugLogs'] as bool? ?? false,
);
} else if (id.trim().isNotEmpty || clientTok.trim().isNotEmpty) {
SdkReminderLog.facebook(
'App ID / Client Token 为占位或无效,已跳过 Facebook。',
);
}
}
}
return AnalyticsConfig(
packageName: packageName,
adjustConfig: adjustCfg,
facebookConfig: fbCfg,
platformAttributionConfig: _parsePlatformAttribution(root),
debugLogs: debugLogs,
);
}
static AdjustEnv _parseAdjustEnv(String? raw) {
switch (raw?.toLowerCase()) {
case 'sandbox':
return AdjustEnv.sandbox;
case 'production':
default:
return AdjustEnv.production;
}
}
static AdjustLogLevel _parseAdjustLogLevel(String? raw) {
switch (raw?.toLowerCase()) {
case 'verbose':
return AdjustLogLevel.verbose;
case 'off':
default:
return AdjustLogLevel.off;
}
}
/// token SDK
static bool _isUsableSecret(String? raw) {
if (raw == null) return false;
final s = raw.trim();
if (s.isEmpty) return false;
final lower = s.toLowerCase();
if (lower.startsWith('todo')) return false;
if (lower.contains('your_')) return false;
if (lower.contains('example.com')) return false;
if (lower == 'changeme') return false;
return true;
}
static PlatformAttributionConfig? _parsePlatformAttribution(
Map<String, dynamic>? root,
) {
if (root == null) return null;
final p = root['platformAttribution'];
if (p is Map<String, dynamic>) {
return PlatformAttributionConfig(
enabled: p['enabled'] as bool? ?? true,
);
}
return const PlatformAttributionConfig();
}
}

View File

@ -0,0 +1,57 @@
{
"app": {
"name": "Example App",
"id": "com.example.app",
"packageName": "com.example.app"
},
"backend": {
"iosAppType": "HIOS",
"androidAppType": "HAndroid"
},
"api": {
"preBaseUrl": "https://pre-api.example.com",
"prodBaseUrl": "https://api.example.com",
"proxyPath": "/v1/proxy",
"debugBaseUrlOverride": null,
"alwaysUsePreBaseUrl": false,
"aesKey": "1234567890123456"
},
"proxyKeys": {
"appIdField": "hero_class",
"pathField": "pet_species",
"methodField": "power_level",
"headerField": "quest_rank",
"paramsField": "battle_score",
"bodyField": "loyalty_index",
"noiseKeys": ["billing_addr", "utm_term", "cluster_id", "lsn_value", "accuracy_val", "dir_path"]
},
"v2": {
"levelField": "arsenal",
"levelFixedValue": 4,
"sanctumPath": ["vault", "tome", "codex", "grimoire", "sanctum"],
"noiseKeys": ["roar", "clash", "thunder", "rumble", "howl", "growl"]
},
"fieldMapping": {
"code": "helm",
"msg": "rampart",
"data": "sidekick"
},
"analytics": {
"debugLogs": false,
"adjust": {
"appToken": "your_adjust_app_token",
"environment": "sandbox",
"logLevel": "off",
"fbAppId": null
},
"facebook": {
"appId": "your_facebook_app_id",
"clientToken": "your_client_token",
"debugLogs": false
}
},
"adjustEvents": {
"register": "abc123",
"purchase": "def456"
}
}

View File

@ -0,0 +1,33 @@
import 'dart:developer' as developer;
import 'package:flutter/foundation.dart';
/// SDK
abstract final class SdkReminderLog {
static final Set<String> _onceKeys = {};
static void debug(String message) {
debugPrint('[client_proxy_framework] $message');
}
/// [key]
static void once(String key, String message) {
if (!_onceKeys.add(key)) return;
final full = '[client_proxy_framework] $message '
'(正式上线前请在 skin_config.json / 原生侧完成配置)';
debugPrint(full);
developer.log(full, name: 'client_proxy_framework');
}
static void adjust(String detail) {
once('adjust', 'Adjust: $detail');
}
static void facebook(String detail) {
once('facebook', 'Facebook: $detail');
}
static void playReferrer(String detail) {
once('play_referrer', 'Play Install Referrer: $detail');
}
}

View File

@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart';
import 'package:play_install_referrer/play_install_referrer.dart'; import 'package:play_install_referrer/play_install_referrer.dart';
import '../log/app_logger.dart'; import '../log/app_logger.dart';
import '../log/sdk_reminder_log.dart';
typedef AdjustAttributionCallback = void Function( typedef AdjustAttributionCallback = void Function(
AdjustAttribution attribution); AdjustAttribution attribution);
@ -29,14 +30,19 @@ class AdjustService {
static void init(adj.AdjustConfig config, static void init(adj.AdjustConfig config,
{AdjustAttributionCallback? onAttribution}) { {AdjustAttributionCallback? onAttribution}) {
_config = config; try {
_attributionCallback = onAttribution; _config = config;
config.attributionCallback = _onAttribution; _attributionCallback = onAttribution;
adj.Adjust.initSdk(config); config.attributionCallback = _onAttribution;
_log.d('Adjust initialized'); adj.Adjust.initSdk(config);
_log.d('Adjust initialized');
if (kDebugMode) { if (kDebugMode) {
_log.d('Ensure Adjust App Token matches your Adjust dashboard'); _log.d('Ensure Adjust App Token matches your Adjust dashboard');
}
} catch (e, st) {
_config = null;
SdkReminderLog.adjust('Adjust.initSdk 失败: $e\n$st');
} }
} }
@ -50,16 +56,34 @@ class AdjustService {
} }
static void trackEvent(String eventToken) { static void trackEvent(String eventToken) {
adj.Adjust.trackEvent(AdjustEvent(eventToken)); if (_config == null) {
_log.d('Track event: $eventToken'); SdkReminderLog.adjust(
'无法上报事件token: $eventTokenSDK 未成功初始化。',
);
return;
}
try {
adj.Adjust.trackEvent(AdjustEvent(eventToken));
_log.d('Track event: $eventToken');
} catch (e) {
SdkReminderLog.adjust('trackEvent 异常: $e');
}
} }
static void trackRevenueEvent( static void trackRevenueEvent(
String eventToken, double revenue, String currency) { String eventToken, double revenue, String currency) {
final event = AdjustEvent(eventToken); if (_config == null) {
event.setRevenue(revenue, currency); SdkReminderLog.adjust('无法上报收入事件SDK 未成功初始化。');
adj.Adjust.trackEvent(event); return;
_log.d('Track revenue event: $eventToken, revenue: $revenue $currency'); }
try {
final event = AdjustEvent(eventToken);
event.setRevenue(revenue, currency);
adj.Adjust.trackEvent(event);
_log.d('Track revenue event: $eventToken, revenue: $revenue $currency');
} catch (e) {
SdkReminderLog.adjust('trackRevenueEvent 异常: $e');
}
} }
static Future<AttributionData?> getAttribution() async { static Future<AttributionData?> getAttribution() async {

View File

@ -0,0 +1,59 @@
import 'dart:convert';
import '../config/attribution_config.dart';
import 'adjust_service.dart';
import 'analytics_service.dart';
/// [AnalyticsService] Adjust fast_login / referer
///
/// [AnalyticsService.init] [AnalyticsService.initAttribution]
/// [FrameworkAuthService.start]宿 `main`
final class AnalyticsAttributionCallbacks implements AttributionCallbacks {
@override
Future<String?> getReferrer() async {
final attribution = AnalyticsService.getAttribution();
if (attribution != null) {
return _attributionToPayload(attribution);
}
return '';
}
@override
Future<String?> getAdjustReferrer() async => getReferrer();
@override
Future<String?> getPlatformReferrer() async => null;
@override
Future<String?> getFacebookReferrer() async {
final attribution = AnalyticsService.getAttribution();
if (attribution != null && attribution.fbInstallReferrer != null) {
return attribution.fbInstallReferrer;
}
return null;
}
static String _attributionToPayload(AttributionData data) {
final map = <String, dynamic>{
'trackerToken': data.trackerToken,
'trackerName': data.trackerName,
'network': data.network,
'campaign': data.campaign,
'adgroup': data.adgroup,
'creative': data.creative,
'clickLabel': data.clickLabel,
'costType': data.costType,
'costAmount': _jsonEncodableCostAmount(data.costAmount),
'costCurrency': data.costCurrency,
'jsonResponse': data.jsonResponse,
'fbInstallReferrer': data.fbInstallReferrer,
};
return base64Encode(utf8.encode(jsonEncode(map)));
}
static Object? _jsonEncodableCostAmount(double? v) {
if (v == null) return null;
if (v.isNaN || !v.isFinite) return v.toString();
return v;
}
}

View File

@ -4,6 +4,7 @@ import 'package:adjust_sdk/adjust_attribution.dart'
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../config/attribution_config.dart'; import '../config/attribution_config.dart';
import '../log/sdk_reminder_log.dart';
import 'adjust_service.dart'; import 'adjust_service.dart';
import 'facebook_service.dart'; import 'facebook_service.dart';
@ -63,9 +64,9 @@ abstract class AnalyticsService {
static void _initAdjust() { static void _initAdjust() {
final adjustConfig = _config?.adjustConfig; final adjustConfig = _config?.adjustConfig;
if (adjustConfig == null) { if (adjustConfig == null) {
if (_config?.debugLogs ?? false) { SdkReminderLog.adjust(
debugPrint('[Analytics] Adjust not configured, skipping'); '未配置 appToken已跳过 Adjust SDK 初始化。',
} );
return; return;
} }
@ -91,10 +92,8 @@ abstract class AnalyticsService {
if (_config?.debugLogs ?? false) { if (_config?.debugLogs ?? false) {
debugPrint('[Analytics] Adjust initialized: ${adjustConfig.appToken}'); debugPrint('[Analytics] Adjust initialized: ${adjustConfig.appToken}');
} }
} catch (e) { } catch (e, st) {
if (_config?.debugLogs ?? false) { SdkReminderLog.adjust('初始化失败: $e\n$st');
debugPrint('[Analytics] Adjust init failed: $e');
}
} }
} }
@ -119,23 +118,20 @@ abstract class AnalyticsService {
static Future<void> _initFacebook() async { static Future<void> _initFacebook() async {
final fbConfig = _config?.facebookConfig; final fbConfig = _config?.facebookConfig;
final packageName = _config?.packageName; if (fbConfig == null) {
if (fbConfig == null || packageName == null) { SdkReminderLog.facebook(
if (_config?.debugLogs ?? false) { '未配置有效 App ID已跳过 Facebook SDK 初始化。',
debugPrint('[Analytics] Facebook not configured, skipping'); );
}
return; return;
} }
try { try {
await FacebookService.init(fbConfig, packageName: packageName); await FacebookService.init(fbConfig);
if (_config?.debugLogs ?? false) { if (_config?.debugLogs ?? false) {
debugPrint('[Analytics] Facebook initialized: ${fbConfig.appId}'); debugPrint('[Analytics] Facebook initialized: ${fbConfig.appId}');
} }
} catch (e) { } catch (e, st) {
if (_config?.debugLogs ?? false) { SdkReminderLog.facebook('初始化失败: $e\n$st');
debugPrint('[Analytics] Facebook init failed: $e');
}
} }
} }
@ -146,9 +142,7 @@ abstract class AnalyticsService {
debugPrint('[Analytics] Adjust track: $eventToken'); debugPrint('[Analytics] Adjust track: $eventToken');
} }
} catch (e) { } catch (e) {
if (_config?.debugLogs ?? false) { SdkReminderLog.adjust('trackEvent 失败: $e');
debugPrint('[Analytics] Adjust track error: $e');
}
} }
} }

View File

@ -2,13 +2,13 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../api/api_client.dart'; import '../api/api_client.dart';
import '../api/proxy_client.dart'; import '../api/proxy_client.dart';
import '../config/attribution_config.dart'; import '../config/attribution_config.dart';
import '../entities/user_entities.dart'; import '../entities/user_entities.dart';
import 'adjust_service.dart'; import 'adjust_service.dart';
import 'analytics_attribution_callbacks.dart';
import 'user_api.dart'; import 'user_api.dart';
/// ///
@ -46,8 +46,17 @@ abstract class FrameworkAuthService {
/// Future await Future /// Future await Future
static Future<void> get loginComplete => _loginFuture ?? Future<void>.value(); static Future<void> get loginComplete => _loginFuture ?? Future<void>.value();
/// ///
static void init(AuthServiceCallbacks callbacks) { ///
/// [AnalyticsAttributionCallbacks] Adjust
/// [attributionCallbacks] referer
static void init(
AuthServiceCallbacks callbacks, {
AttributionCallbacks? attributionCallbacks,
}) {
AttributionService.init(
attributionCallbacks ?? AnalyticsAttributionCallbacks(),
);
_callbacks = callbacks; _callbacks = callbacks;
_isInitialized = true; _isInitialized = true;
} }
@ -63,7 +72,9 @@ abstract class FrameworkAuthService {
}) async { }) async {
if (!_isInitialized || _callbacks == null) { if (!_isInitialized || _callbacks == null) {
throw StateError( throw StateError(
'AuthService not initialized. Call AuthService.init(callbacks) first.'); 'FrameworkAuthService not initialized. '
'Call FrameworkAuthService.init(callbacks) first.',
);
} }
if (_loginFuture != null) return _loginFuture!; if (_loginFuture != null) return _loginFuture!;
final completer = Completer<void>(); final completer = Completer<void>();
@ -118,8 +129,10 @@ abstract class FrameworkAuthService {
await Future<void>.delayed(Duration(seconds: retryDelaySeconds)); await Future<void>.delayed(Duration(seconds: retryDelaySeconds));
} }
try { try {
final appType = final cfg = ApiClient.instance.config;
defaultTargetPlatform == TargetPlatform.iOS ? 'HIOS' : 'HAndroid'; final appType = defaultTargetPlatform == TargetPlatform.iOS
? cfg.backendAppTypeIOS
: cfg.backendAppTypeAndroid;
res = await UserApi.fastLogin( res = await UserApi.fastLogin(
deviceId: deviceId, deviceId: deviceId,
sign: sign, sign: sign,
@ -192,21 +205,24 @@ abstract class FrameworkAuthService {
// Adjust // Adjust
final adjustReferer = await AttributionService.getAdjustReferrer(); final adjustReferer = await AttributionService.getAdjustReferrer();
if (adjustReferer != null && adjustReferer.isNotEmpty) { if (adjustReferer != null && adjustReferer.isNotEmpty) {
final adjustType = defaultTargetPlatform == TargetPlatform.iOS
? 'ios_adjust'
: 'android_adjust';
try { try {
final rAdjust = await UserApi.referrer( final rAdjust = await UserApi.referrer(
app: config.appId, app: config.appId,
userId: uid, userId: uid,
referer: adjustReferer, referer: adjustReferer,
deviceId: deviceId, deviceId: deviceId,
type: 'android_adjust', type: adjustType,
); );
if (kDebugMode) { if (kDebugMode) {
debugPrint( debugPrint(
'[AuthService] referrer(android_adjust): ${rAdjust.isSuccess ? "成功" : "失败"}'); '[AuthService] referrer($adjustType): ${rAdjust.isSuccess ? "成功" : "失败"}');
} }
} catch (e) { } catch (e) {
if (kDebugMode) { if (kDebugMode) {
debugPrint('[AuthService] referrer(android_adjust): 异常 $e'); debugPrint('[AuthService] referrer($adjustType): 异常 $e');
} }
} }
} }

View File

@ -1,52 +1,61 @@
import 'dart:async';
import 'package:facebook_app_events/facebook_app_events.dart'; import 'package:facebook_app_events/facebook_app_events.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../config/attribution_config.dart'; import '../config/attribution_config.dart';
import '../log/app_logger.dart'; import '../log/app_logger.dart';
import '../log/sdk_reminder_log.dart';
/// 宿 Android `ClientProxyFrameworkPlugin`
const String kFacebookSdkChannelName = 'client_proxy_framework/facebook_sdk';
/// Facebook App EventsMethodChannel
class FacebookService { class FacebookService {
FacebookService._(); FacebookService._();
static final _log = AppLogger('Facebook'); static final _log = AppLogger('Facebook');
static final _fb = FacebookAppEvents(); static final _fb = FacebookAppEvents();
static MethodChannel? _channel; static MethodChannel? _channel;
static bool _debugLogs = false;
static bool _isInitialized = false; static bool _isInitialized = false;
static void Function()? _onInitialized; static void Function()? _onInitialized;
static const Duration _nativeInitTimeout = Duration(seconds: 8);
static bool get isInitialized => _isInitialized; static bool get isInitialized => _isInitialized;
static void setOnInitialized(void Function() callback) { static void setOnInitialized(void Function() callback) {
_onInitialized = callback; _onInitialized = callback;
} }
static Future<void> init(FacebookConfig config, static Future<void> init(FacebookConfig config) async {
{required String packageName}) async { _log.d('Facebook App Events init appId: ${config.appId}');
_debugLogs = config.debugLogs;
_log.d('Facebook App Events init with appId: ${config.appId}');
_log.d('FacebookService.init start, packageName: $packageName');
try { try {
_log.d('Creating MethodChannel...'); _channel ??= MethodChannel(kFacebookSdkChannelName);
_channel = MethodChannel('$packageName/facebook_sdk');
_log.d('Setting method call handler...');
_channel!.setMethodCallHandler((call) async { _channel!.setMethodCallHandler((call) async {
_log.d('MethodCall received: ${call.method}'); _log.d('MethodCall: ${call.method}');
if (call.method == 'onFacebookSdkInitialized') { if (call.method == 'onFacebookSdkInitialized') {
_isInitialized = true; _isInitialized = true;
_log.d('Facebook App Events initialized (from native callback)');
_onInitialized?.call(); _onInitialized?.call();
} }
}); });
_log.d('Invoking waitForFacebookSdkInit on native...'); await _channel!
await _channel!.invokeMethod('waitForFacebookSdkInit'); .invokeMethod<void>('waitForFacebookSdkInit')
_log.d('waitForFacebookSdkInit completed'); .timeout(_nativeInitTimeout);
_log.d('waitForFacebookSdkInit finished');
} on TimeoutException catch (e, st) {
SdkReminderLog.facebook(
'原生初始化超时 ($e),已跳过。请检查是否集成框架 Android/iOS 插件、`AndroidManifest` / Info.plist 中 Facebook 配置是否完整。\n$st',
);
} on MissingPluginException catch (e) {
SdkReminderLog.facebook(
'未注册原生插件 ($e)。请执行 flutter clean && flutter pub get 后重新构建。',
);
} catch (e, st) { } catch (e, st) {
_log.w('FacebookService.init failed: $e\n$st'); SdkReminderLog.facebook('初始化异常: $e\n$st');
} }
} }
@ -55,18 +64,34 @@ class FacebookService {
String currency = 'USD', String currency = 'USD',
String? orderId, String? orderId,
}) { }) {
_fb.logPurchase(amount: amount, currency: currency); try {
_fb.logPurchase(amount: amount, currency: currency);
} catch (e) {
SdkReminderLog.facebook('logPurchase 失败: $e');
}
} }
static void logSubscribe(String planId) { static void logSubscribe(String planId) {
_fb.logSubscribe(orderId: planId); try {
_fb.logSubscribe(orderId: planId);
} catch (e) {
SdkReminderLog.facebook('logSubscribe 失败: $e');
}
} }
static void logRegister({String registrationMethod = 'device'}) { static void logRegister({String registrationMethod = 'device'}) {
_fb.logCompletedRegistration(registrationMethod: registrationMethod); try {
_fb.logCompletedRegistration(registrationMethod: registrationMethod);
} catch (e) {
SdkReminderLog.facebook('logRegister 失败: $e');
}
} }
static void logEvent(String name, {Map<String, dynamic>? parameters}) { static void logEvent(String name, {Map<String, dynamic>? parameters}) {
_fb.logEvent(name: name, parameters: parameters); try {
_fb.logEvent(name: name, parameters: parameters);
} catch (e) {
SdkReminderLog.facebook('logEvent 失败: $e');
}
} }
} }

View File

@ -39,7 +39,7 @@ abstract final class PaymentApi {
method: 'GET', method: 'GET',
entityFactory: PaymentProductsResponse.fromJson, entityFactory: PaymentProductsResponse.fromJson,
queryParams: { queryParams: {
'app': app ?? 'HAndroid', 'app': app ?? ApiClient.instance.config.backendAppTypeIOS,
'pkg': pkg ?? ApiClient.instance.config.packageName, 'pkg': pkg ?? ApiClient.instance.config.packageName,
if (shield != null) 'shield': shield, if (shield != null) 'shield': shield,
if (country != null) 'country': country, if (country != null) 'country': country,

View File

@ -6,6 +6,15 @@ publish_to: 'none'
environment: environment:
sdk: '>=3.0.0 <4.0.0' sdk: '>=3.0.0 <4.0.0'
flutter:
plugin:
platforms:
android:
package: com.funymee.client_proxy_framework
pluginClass: ClientProxyFrameworkPlugin
ios:
pluginClass: ClientProxyFrameworkPlugin
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter