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