diff --git a/.dart_tool/package_graph.json b/.dart_tool/package_graph.json
index 20839de..288a09f 100644
--- a/.dart_tool/package_graph.json
+++ b/.dart_tool/package_graph.json
@@ -21,6 +21,19 @@
],
"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",
"version": "0.5.0",
@@ -37,6 +50,16 @@
"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",
"version": "0.26.0",
@@ -101,6 +124,64 @@
"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",
"version": "1.4.0",
@@ -114,6 +195,16 @@
"version": "1.19.1",
"dependencies": []
},
+ {
+ "name": "in_app_purchase_storekit",
+ "version": "0.4.8+1",
+ "dependencies": [
+ "collection",
+ "flutter",
+ "in_app_purchase_platform_interface",
+ "json_annotation"
+ ]
+ },
{
"name": "meta",
"version": "1.17.0",
@@ -195,6 +286,70 @@
"version": "1.4.1",
"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",
"version": "0.7.2",
@@ -224,152 +379,15 @@
]
},
{
- "name": "term_glyph",
- "version": "1.2.2",
+ "name": "ffi",
+ "version": "2.2.0",
"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",
"version": "3.1.6",
"dependencies": []
},
- {
- "name": "path_provider_linux",
- "version": "2.2.1",
- "dependencies": [
- "ffi",
- "flutter",
- "path",
- "path_provider_platform_interface",
- "xdg_directories"
- ]
- },
{
"name": "xdg_directories",
"version": "1.1.0",
@@ -379,27 +397,9 @@
]
},
{
- "name": "ffi",
- "version": "2.2.0",
+ "name": "term_glyph",
+ "version": "1.2.2",
"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
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..74f808e
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
index 94b28d6..a33fa24 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
通用代理 API 框架。**接口请求已按原始字段名写好**,不同应用只需**修改映射表**即可接入不同后端。
+**新建换皮应用**:请阅读 [docs/create_new_skin_app.md](docs/create_new_skin_app.md)(`skin_config.json` + `ClientBootstrap` 全流程)。
+
## 设计思路
- **业务层**:统一使用**原始字段名**(canonical),如 `deviceId`、`userId`、`credits`、`userToken`
diff --git a/android/.gradle/8.9/checksums/checksums.lock b/android/.gradle/8.9/checksums/checksums.lock
new file mode 100644
index 0000000..e9a7e47
Binary files /dev/null and b/android/.gradle/8.9/checksums/checksums.lock differ
diff --git a/android/.gradle/8.9/checksums/md5-checksums.bin b/android/.gradle/8.9/checksums/md5-checksums.bin
new file mode 100644
index 0000000..d5c10cd
Binary files /dev/null and b/android/.gradle/8.9/checksums/md5-checksums.bin differ
diff --git a/android/.gradle/8.9/checksums/sha1-checksums.bin b/android/.gradle/8.9/checksums/sha1-checksums.bin
new file mode 100644
index 0000000..11f2a50
Binary files /dev/null and b/android/.gradle/8.9/checksums/sha1-checksums.bin differ
diff --git a/android/.gradle/8.9/dependencies-accessors/gc.properties b/android/.gradle/8.9/dependencies-accessors/gc.properties
new file mode 100644
index 0000000..e69de29
diff --git a/android/.gradle/8.9/fileChanges/last-build.bin b/android/.gradle/8.9/fileChanges/last-build.bin
new file mode 100644
index 0000000..f76dd23
Binary files /dev/null and b/android/.gradle/8.9/fileChanges/last-build.bin differ
diff --git a/android/.gradle/8.9/fileHashes/fileHashes.lock b/android/.gradle/8.9/fileHashes/fileHashes.lock
new file mode 100644
index 0000000..a471423
Binary files /dev/null and b/android/.gradle/8.9/fileHashes/fileHashes.lock differ
diff --git a/android/.gradle/8.9/gc.properties b/android/.gradle/8.9/gc.properties
new file mode 100644
index 0000000..e69de29
diff --git a/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock
new file mode 100644
index 0000000..0300086
Binary files /dev/null and b/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ
diff --git a/android/.gradle/buildOutputCleanup/cache.properties b/android/.gradle/buildOutputCleanup/cache.properties
new file mode 100644
index 0000000..9cdbbcf
--- /dev/null
+++ b/android/.gradle/buildOutputCleanup/cache.properties
@@ -0,0 +1,2 @@
+#Thu Mar 26 15:16:08 CST 2026
+gradle.version=8.9
diff --git a/android/.gradle/vcs-1/gc.properties b/android/.gradle/vcs-1/gc.properties
new file mode 100644
index 0000000..e69de29
diff --git a/android/build.gradle b/android/build.gradle
new file mode 100644
index 0000000..ca5bef5
--- /dev/null
+++ b/android/build.gradle
@@ -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")
+}
diff --git a/android/settings.gradle b/android/settings.gradle
new file mode 100644
index 0000000..07a9d75
--- /dev/null
+++ b/android/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = "client_proxy_framework"
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..94cbbcf
--- /dev/null
+++ b/android/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/android/src/main/kotlin/com/funymee/client_proxy_framework/ClientProxyFrameworkPlugin.kt b/android/src/main/kotlin/com/funymee/client_proxy_framework/ClientProxyFrameworkPlugin.kt
new file mode 100644
index 0000000..e3afb3d
--- /dev/null
+++ b/android/src/main/kotlin/com/funymee/client_proxy_framework/ClientProxyFrameworkPlugin.kt
@@ -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"
+ }
+}
diff --git a/docs/README.md b/docs/README.md
index 6239937..977212e 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -11,6 +11,10 @@
- **强类型实体**:返回强类型实体类,开发者直接使用映射后的字段
- **统一响应**:`EntityResponse` 提供强类型返回值
+### 换皮应用:从零搭建
+
+从零创建一个新的换皮应用(`skin_config.json`、`ClientBootstrap`、`main` 初始化顺序、Android/iOS 要点等),请阅读 **[《创建新换皮应用 — 步骤清单》](create_new_skin_app.md)**。
+
---
## 2. 快速开始
diff --git a/docs/create_new_skin_app.md b/docs/create_new_skin_app.md
new file mode 100644
index 0000000..0885323
--- /dev/null
+++ b/docs/create_new_skin_app.md
@@ -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 最小结构示例 |
+
+若本清单与旧文档冲突,**以本清单与当前框架代码为准**。
diff --git a/docs/skin_app_development_guide.md b/docs/skin_app_development_guide.md
index 5c5fc15..50cfeee 100644
--- a/docs/skin_app_development_guide.md
+++ b/docs/skin_app_development_guide.md
@@ -2,6 +2,8 @@
本文档说明如何使用 `client_proxy_framework` 从零创建并完成一个换皮应用。
+> **推荐优先阅读**:[《创建新换皮应用 — 步骤清单》](create_new_skin_app.md) — 与当前框架实现(JSON 配置、`ClientBootstrap`、固定 Facebook Channel、默认归因回调等)保持同步的精简步骤。
+
***
> **重要说明**:本指南**仅完成数据框架的对接**,包括:
diff --git a/ios/Classes/ClientProxyFrameworkPlugin.swift b/ios/Classes/ClientProxyFrameworkPlugin.swift
new file mode 100644
index 0000000..1bbe7fe
--- /dev/null
+++ b/ios/Classes/ClientProxyFrameworkPlugin.swift
@@ -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)
+ }
+ }
+}
diff --git a/ios/client_proxy_framework.podspec b/ios/client_proxy_framework.podspec
new file mode 100644
index 0000000..142a596
--- /dev/null
+++ b/ios/client_proxy_framework.podspec
@@ -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
diff --git a/lib/client_proxy_framework.dart b/lib/client_proxy_framework.dart
index 304a8b6..5519acc 100644
--- a/lib/client_proxy_framework.dart
+++ b/lib/client_proxy_framework.dart
@@ -1,22 +1,29 @@
/// 通用代理 API 框架。
///
-/// 换皮应用只需:
-/// 1. 实现 [AppConfig] 提供 appId、aesKey、baseUrl 等
-/// 2. 在 main 中调用 ApiClient.init(yourConfig)
-/// 3. 通过 ApiClient.instance.proxy 发起请求
+/// 换皮应用推荐:
+/// 1. 在 assets 中放置 `skin_config.json`(见 [SkinConfig])
+/// 2. `await ClientBootstrap.initFromAsset('assets/skin_config.json');`
+/// 3. `await ClientBootstrap.initAnalytics();`(可选)
+/// 4. 通过 [ApiClient.instance.proxy] 发起请求
+///
+/// 亦可手写 [AppConfig] 并 `ApiClient.init(config)`。
library;
export 'src/api/api_client.dart';
export 'src/api/api_crypto.dart';
export 'src/api/api_response.dart';
export 'src/api/proxy_client.dart';
+export 'src/bootstrap/client_bootstrap.dart';
export 'src/config/app_config.dart';
export 'src/config/attribution_config.dart';
+export 'src/config/skin_config.dart';
export 'src/config/field_mapping.dart';
export 'src/config/default_field_mapping.dart';
export 'src/entities/entities.dart';
export 'src/log/app_logger.dart';
+export 'src/log/sdk_reminder_log.dart';
export 'src/services/adjust_service.dart';
+export 'src/services/analytics_attribution_callbacks.dart';
export 'src/services/analytics_service.dart';
export 'src/services/auth_service.dart';
export 'src/services/facebook_service.dart';
diff --git a/lib/src/bootstrap/client_bootstrap.dart b/lib/src/bootstrap/client_bootstrap.dart
new file mode 100644
index 0000000..ebf0f48
--- /dev/null
+++ b/lib/src/bootstrap/client_bootstrap.dart
@@ -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 initFromAsset(String assetPath) async {
+ WidgetsFlutterBinding.ensureInitialized();
+ final raw = await rootBundle.loadString(assetPath);
+ return initFromJsonString(raw);
+ }
+
+ static Future initFromJsonString(String json) async {
+ final skin = SkinConfig.fromJsonString(json);
+ _apply(skin);
+ return skin;
+ }
+
+ static Future initFromJson(Map map) async {
+ final skin = SkinConfig.fromJson(map);
+ _apply(skin);
+ return skin;
+ }
+
+ static void _apply(SkinConfig skin) {
+ _skin = skin;
+ ApiClient.init(skin);
+ }
+
+ /// 初始化归因 SDK(Adjust / Facebook);须在 [initFromAsset] 之后调用。
+ static Future initAnalytics() async {
+ await AnalyticsService.init(skin.buildAnalyticsConfig());
+ }
+}
diff --git a/lib/src/config/app_config.dart b/lib/src/config/app_config.dart
index cece1b8..0f587c0 100644
--- a/lib/src/config/app_config.dart
+++ b/lib/src/config/app_config.dart
@@ -6,7 +6,7 @@ import 'field_mapping.dart';
/// 代理请求体字段名配置
class ProxyKeysConfig {
const ProxyKeysConfig({
- /// appId 明文字段名
+ /// 代理请求体最外层「应用明文」字段的 **键名**(值由框架写入 [AppConfig.appName],与后端约定一致)
this.appIdField = 'hero_class',
/// 原始 path 字段名
@@ -35,7 +35,7 @@ class ProxyKeysConfig {
],
});
- /// appId 明文字段名
+ /// 代理请求体应用明文字段键名(值 = appName)
final String appIdField;
/// 原始 path 字段名
@@ -65,13 +65,19 @@ abstract class AppConfig {
/// 应用名称
String get appName;
- /// 应用标识(代理请求 hero_class)
+ /// 业务侧应用标识(如 fast_login 的 query `app`、与后台约定的 app id)
String get appId;
- /// 应用包名
+ /// 应用包名(渠道包名 / 请求头映射等)
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;
/// 预发环境域名
diff --git a/lib/src/config/attribution_config.dart b/lib/src/config/attribution_config.dart
index 1e15eca..09ac781 100644
--- a/lib/src/config/attribution_config.dart
+++ b/lib/src/config/attribution_config.dart
@@ -57,8 +57,8 @@ abstract class AttributionCallbacks {
Future getFacebookReferrer();
}
-/// 默认归因实现
-class DefaultAttributionCallbacks implements AttributionCallbacks {
+/// 占位用空归因(单测等)。生产环境请使用 [AnalyticsAttributionCallbacks]。
+class StubAttributionCallbacks implements AttributionCallbacks {
@override
Future getReferrer() async => '';
diff --git a/lib/src/config/skin_config.dart b/lib/src/config/skin_config.dart
new file mode 100644
index 0000000..14f4c45
--- /dev/null
+++ b/lib/src/config/skin_config.dart
@@ -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 adjustEvents,
+ this.analyticsJson,
+ }) : _adjustEvents = adjustEvents;
+
+ final Map? analyticsJson;
+ final Map _adjustEvents;
+
+ /// 从 JSON 文本解析(如 `rootBundle.loadString` 的结果)。
+ factory SkinConfig.fromJsonString(String source) {
+ final dynamic decoded = jsonDecode(source);
+ if (decoded is! Map) {
+ throw FormatException('skin_config root must be a JSON object');
+ }
+ return SkinConfig.fromJson(decoded);
+ }
+
+ factory SkinConfig.fromJson(Map 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) {
+ 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 sanctum = const [
+ 'vault',
+ 'tome',
+ 'codex',
+ 'grimoire',
+ 'sanctum',
+ ];
+ List v2Noise = const [
+ 'roar',
+ 'clash',
+ 'thunder',
+ 'rumble',
+ 'howl',
+ 'growl',
+ ];
+ if (v2 is Map) {
+ 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) {
+ if (fmRaw.isEmpty) {
+ mapping = petsHeroAIFieldMapping;
+ } else {
+ final m = {};
+ 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 = {};
+ if (eventsRaw is Map) {
+ 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?,
+ );
+ }
+
+ static Map _map(dynamic v, String name) {
+ if (v is! Map) {
+ 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) {
+ throw FormatException('proxyKeys must be a JSON object');
+ }
+ List 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 v2SanctumPath;
+
+ @override
+ final List v2NoiseKeys;
+
+ @override
+ final FieldMapping fieldMapping;
+
+ @override
+ Map get v2FixedValues => {v2LevelField: v2LevelFixedValue};
+
+ /// 语义化 Adjust 事件名 → Dashboard token;无配置则为空 map。
+ Map get adjustEventTokens =>
+ Map.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) {
+ 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) {
+ 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? root,
+ ) {
+ if (root == null) return null;
+ final p = root['platformAttribution'];
+ if (p is Map) {
+ return PlatformAttributionConfig(
+ enabled: p['enabled'] as bool? ?? true,
+ );
+ }
+ return const PlatformAttributionConfig();
+ }
+}
diff --git a/lib/src/config/skin_config.example.json b/lib/src/config/skin_config.example.json
new file mode 100644
index 0000000..5e1af37
--- /dev/null
+++ b/lib/src/config/skin_config.example.json
@@ -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"
+ }
+}
diff --git a/lib/src/log/sdk_reminder_log.dart b/lib/src/log/sdk_reminder_log.dart
new file mode 100644
index 0000000..ecf5603
--- /dev/null
+++ b/lib/src/log/sdk_reminder_log.dart
@@ -0,0 +1,33 @@
+import 'dart:developer' as developer;
+
+import 'package:flutter/foundation.dart';
+
+/// 第三方 SDK 未配置或降级时的提醒(不抛异常、不阻塞启动)。
+abstract final class SdkReminderLog {
+ static final Set _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');
+ }
+}
diff --git a/lib/src/services/adjust_service.dart b/lib/src/services/adjust_service.dart
index b0adfd0..75356c2 100644
--- a/lib/src/services/adjust_service.dart
+++ b/lib/src/services/adjust_service.dart
@@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart';
import 'package:play_install_referrer/play_install_referrer.dart';
import '../log/app_logger.dart';
+import '../log/sdk_reminder_log.dart';
typedef AdjustAttributionCallback = void Function(
AdjustAttribution attribution);
@@ -29,14 +30,19 @@ class AdjustService {
static void init(adj.AdjustConfig config,
{AdjustAttributionCallback? onAttribution}) {
- _config = config;
- _attributionCallback = onAttribution;
- config.attributionCallback = _onAttribution;
- adj.Adjust.initSdk(config);
- _log.d('Adjust initialized');
+ try {
+ _config = config;
+ _attributionCallback = onAttribution;
+ config.attributionCallback = _onAttribution;
+ adj.Adjust.initSdk(config);
+ _log.d('Adjust initialized');
- if (kDebugMode) {
- _log.d('Ensure Adjust App Token matches your Adjust dashboard');
+ if (kDebugMode) {
+ _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) {
- adj.Adjust.trackEvent(AdjustEvent(eventToken));
- _log.d('Track event: $eventToken');
+ if (_config == null) {
+ SdkReminderLog.adjust(
+ '无法上报事件(token: $eventToken):SDK 未成功初始化。',
+ );
+ return;
+ }
+ try {
+ adj.Adjust.trackEvent(AdjustEvent(eventToken));
+ _log.d('Track event: $eventToken');
+ } catch (e) {
+ SdkReminderLog.adjust('trackEvent 异常: $e');
+ }
}
static void trackRevenueEvent(
String eventToken, double revenue, String currency) {
- final event = AdjustEvent(eventToken);
- event.setRevenue(revenue, currency);
- adj.Adjust.trackEvent(event);
- _log.d('Track revenue event: $eventToken, revenue: $revenue $currency');
+ if (_config == null) {
+ SdkReminderLog.adjust('无法上报收入事件:SDK 未成功初始化。');
+ return;
+ }
+ 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 getAttribution() async {
diff --git a/lib/src/services/analytics_attribution_callbacks.dart b/lib/src/services/analytics_attribution_callbacks.dart
new file mode 100644
index 0000000..4cab392
--- /dev/null
+++ b/lib/src/services/analytics_attribution_callbacks.dart
@@ -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 getReferrer() async {
+ final attribution = AnalyticsService.getAttribution();
+ if (attribution != null) {
+ return _attributionToPayload(attribution);
+ }
+ return '';
+ }
+
+ @override
+ Future getAdjustReferrer() async => getReferrer();
+
+ @override
+ Future getPlatformReferrer() async => null;
+
+ @override
+ Future getFacebookReferrer() async {
+ final attribution = AnalyticsService.getAttribution();
+ if (attribution != null && attribution.fbInstallReferrer != null) {
+ return attribution.fbInstallReferrer;
+ }
+ return null;
+ }
+
+ static String _attributionToPayload(AttributionData data) {
+ final map = {
+ '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;
+ }
+}
diff --git a/lib/src/services/analytics_service.dart b/lib/src/services/analytics_service.dart
index 8746573..87abc9a 100644
--- a/lib/src/services/analytics_service.dart
+++ b/lib/src/services/analytics_service.dart
@@ -4,6 +4,7 @@ import 'package:adjust_sdk/adjust_attribution.dart'
import 'package:flutter/foundation.dart';
import '../config/attribution_config.dart';
+import '../log/sdk_reminder_log.dart';
import 'adjust_service.dart';
import 'facebook_service.dart';
@@ -63,9 +64,9 @@ abstract class AnalyticsService {
static void _initAdjust() {
final adjustConfig = _config?.adjustConfig;
if (adjustConfig == null) {
- if (_config?.debugLogs ?? false) {
- debugPrint('[Analytics] Adjust not configured, skipping');
- }
+ SdkReminderLog.adjust(
+ '未配置 appToken,已跳过 Adjust SDK 初始化。',
+ );
return;
}
@@ -91,10 +92,8 @@ abstract class AnalyticsService {
if (_config?.debugLogs ?? false) {
debugPrint('[Analytics] Adjust initialized: ${adjustConfig.appToken}');
}
- } catch (e) {
- if (_config?.debugLogs ?? false) {
- debugPrint('[Analytics] Adjust init failed: $e');
- }
+ } catch (e, st) {
+ SdkReminderLog.adjust('初始化失败: $e\n$st');
}
}
@@ -119,23 +118,20 @@ abstract class AnalyticsService {
static Future _initFacebook() async {
final fbConfig = _config?.facebookConfig;
- final packageName = _config?.packageName;
- if (fbConfig == null || packageName == null) {
- if (_config?.debugLogs ?? false) {
- debugPrint('[Analytics] Facebook not configured, skipping');
- }
+ if (fbConfig == null) {
+ SdkReminderLog.facebook(
+ '未配置有效 App ID,已跳过 Facebook SDK 初始化。',
+ );
return;
}
try {
- await FacebookService.init(fbConfig, packageName: packageName);
+ await FacebookService.init(fbConfig);
if (_config?.debugLogs ?? false) {
debugPrint('[Analytics] Facebook initialized: ${fbConfig.appId}');
}
- } catch (e) {
- if (_config?.debugLogs ?? false) {
- debugPrint('[Analytics] Facebook init failed: $e');
- }
+ } catch (e, st) {
+ SdkReminderLog.facebook('初始化失败: $e\n$st');
}
}
@@ -146,9 +142,7 @@ abstract class AnalyticsService {
debugPrint('[Analytics] Adjust track: $eventToken');
}
} catch (e) {
- if (_config?.debugLogs ?? false) {
- debugPrint('[Analytics] Adjust track error: $e');
- }
+ SdkReminderLog.adjust('trackEvent 失败: $e');
}
}
diff --git a/lib/src/services/auth_service.dart b/lib/src/services/auth_service.dart
index cc53562..4ccc019 100644
--- a/lib/src/services/auth_service.dart
+++ b/lib/src/services/auth_service.dart
@@ -2,13 +2,13 @@ import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
import '../api/api_client.dart';
import '../api/proxy_client.dart';
import '../config/attribution_config.dart';
import '../entities/user_entities.dart';
import 'adjust_service.dart';
+import 'analytics_attribution_callbacks.dart';
import 'user_api.dart';
/// 认证服务回调
@@ -46,8 +46,17 @@ abstract class FrameworkAuthService {
/// 登录完成后的 Future,需鉴权接口应 await 此 Future 再请求
static Future get loginComplete => _loginFuture ?? Future.value();
- /// 初始化认证服务
- static void init(AuthServiceCallbacks callbacks) {
+ /// 初始化认证服务。
+ ///
+ /// 默认注册 [AnalyticsAttributionCallbacks](与 Adjust 缓存一致)。可传入
+ /// [attributionCallbacks] 覆盖(例如自定义 referer 格式)。
+ static void init(
+ AuthServiceCallbacks callbacks, {
+ AttributionCallbacks? attributionCallbacks,
+ }) {
+ AttributionService.init(
+ attributionCallbacks ?? AnalyticsAttributionCallbacks(),
+ );
_callbacks = callbacks;
_isInitialized = true;
}
@@ -63,7 +72,9 @@ abstract class FrameworkAuthService {
}) async {
if (!_isInitialized || _callbacks == null) {
throw StateError(
- 'AuthService not initialized. Call AuthService.init(callbacks) first.');
+ 'FrameworkAuthService not initialized. '
+ 'Call FrameworkAuthService.init(callbacks) first.',
+ );
}
if (_loginFuture != null) return _loginFuture!;
final completer = Completer();
@@ -118,8 +129,10 @@ abstract class FrameworkAuthService {
await Future.delayed(Duration(seconds: retryDelaySeconds));
}
try {
- final appType =
- defaultTargetPlatform == TargetPlatform.iOS ? 'HIOS' : 'HAndroid';
+ final cfg = ApiClient.instance.config;
+ final appType = defaultTargetPlatform == TargetPlatform.iOS
+ ? cfg.backendAppTypeIOS
+ : cfg.backendAppTypeAndroid;
res = await UserApi.fastLogin(
deviceId: deviceId,
sign: sign,
@@ -192,21 +205,24 @@ abstract class FrameworkAuthService {
// 上报 Adjust 归因
final adjustReferer = await AttributionService.getAdjustReferrer();
if (adjustReferer != null && adjustReferer.isNotEmpty) {
+ final adjustType = defaultTargetPlatform == TargetPlatform.iOS
+ ? 'ios_adjust'
+ : 'android_adjust';
try {
final rAdjust = await UserApi.referrer(
app: config.appId,
userId: uid,
referer: adjustReferer,
deviceId: deviceId,
- type: 'android_adjust',
+ type: adjustType,
);
if (kDebugMode) {
debugPrint(
- '[AuthService] referrer(android_adjust): ${rAdjust.isSuccess ? "成功" : "失败"}');
+ '[AuthService] referrer($adjustType): ${rAdjust.isSuccess ? "成功" : "失败"}');
}
} catch (e) {
if (kDebugMode) {
- debugPrint('[AuthService] referrer(android_adjust): 异常 $e');
+ debugPrint('[AuthService] referrer($adjustType): 异常 $e');
}
}
}
diff --git a/lib/src/services/facebook_service.dart b/lib/src/services/facebook_service.dart
index 3c8b34c..ad18559 100644
--- a/lib/src/services/facebook_service.dart
+++ b/lib/src/services/facebook_service.dart
@@ -1,52 +1,61 @@
+import 'dart:async';
+
import 'package:facebook_app_events/facebook_app_events.dart';
-import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import '../config/attribution_config.dart';
import '../log/app_logger.dart';
+import '../log/sdk_reminder_log.dart';
+/// 与原生插件约定,勿与宿主包名绑定(参见 Android `ClientProxyFrameworkPlugin`)。
+const String kFacebookSdkChannelName = 'client_proxy_framework/facebook_sdk';
+
+/// Facebook App Events:MethodChannel 由框架插件注册;未配置或超时不阻塞应用。
class FacebookService {
FacebookService._();
static final _log = AppLogger('Facebook');
static final _fb = FacebookAppEvents();
static MethodChannel? _channel;
- static bool _debugLogs = false;
static bool _isInitialized = false;
static void Function()? _onInitialized;
+ static const Duration _nativeInitTimeout = Duration(seconds: 8);
+
static bool get isInitialized => _isInitialized;
static void setOnInitialized(void Function() callback) {
_onInitialized = callback;
}
- static Future init(FacebookConfig config,
- {required String packageName}) async {
- _debugLogs = config.debugLogs;
-
- _log.d('Facebook App Events init with appId: ${config.appId}');
- _log.d('FacebookService.init start, packageName: $packageName');
+ static Future init(FacebookConfig config) async {
+ _log.d('Facebook App Events init appId: ${config.appId}');
try {
- _log.d('Creating MethodChannel...');
- _channel = MethodChannel('$packageName/facebook_sdk');
+ _channel ??= MethodChannel(kFacebookSdkChannelName);
- _log.d('Setting method call handler...');
_channel!.setMethodCallHandler((call) async {
- _log.d('MethodCall received: ${call.method}');
+ _log.d('MethodCall: ${call.method}');
if (call.method == 'onFacebookSdkInitialized') {
_isInitialized = true;
- _log.d('Facebook App Events initialized (from native callback)');
_onInitialized?.call();
}
});
- _log.d('Invoking waitForFacebookSdkInit on native...');
- await _channel!.invokeMethod('waitForFacebookSdkInit');
- _log.d('waitForFacebookSdkInit completed');
+ await _channel!
+ .invokeMethod('waitForFacebookSdkInit')
+ .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) {
- _log.w('FacebookService.init failed: $e\n$st');
+ SdkReminderLog.facebook('初始化异常: $e\n$st');
}
}
@@ -55,18 +64,34 @@ class FacebookService {
String currency = 'USD',
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) {
- _fb.logSubscribe(orderId: planId);
+ try {
+ _fb.logSubscribe(orderId: planId);
+ } catch (e) {
+ SdkReminderLog.facebook('logSubscribe 失败: $e');
+ }
}
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? parameters}) {
- _fb.logEvent(name: name, parameters: parameters);
+ try {
+ _fb.logEvent(name: name, parameters: parameters);
+ } catch (e) {
+ SdkReminderLog.facebook('logEvent 失败: $e');
+ }
}
}
diff --git a/lib/src/services/payment_api.dart b/lib/src/services/payment_api.dart
index b79f216..d793aaf 100644
--- a/lib/src/services/payment_api.dart
+++ b/lib/src/services/payment_api.dart
@@ -39,7 +39,7 @@ abstract final class PaymentApi {
method: 'GET',
entityFactory: PaymentProductsResponse.fromJson,
queryParams: {
- 'app': app ?? 'HAndroid',
+ 'app': app ?? ApiClient.instance.config.backendAppTypeIOS,
'pkg': pkg ?? ApiClient.instance.config.packageName,
if (shield != null) 'shield': shield,
if (country != null) 'country': country,
diff --git a/pubspec.yaml b/pubspec.yaml
index 18f7f8f..702eeb8 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -6,6 +6,15 @@ publish_to: 'none'
environment:
sdk: '>=3.0.0 <4.0.0'
+flutter:
+ plugin:
+ platforms:
+ android:
+ package: com.funymee.client_proxy_framework
+ pluginClass: ClientProxyFrameworkPlugin
+ ios:
+ pluginClass: ClientProxyFrameworkPlugin
+
dependencies:
flutter:
sdk: flutter