优化:将应用端的配置改用JSON形式更加灵活
This commit is contained in:
parent
83c1c56c36
commit
e77f5f45b7
@ -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
|
||||
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 FunyMee
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@ -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`
|
||||
|
||||
BIN
android/.gradle/8.9/checksums/checksums.lock
Normal file
BIN
android/.gradle/8.9/checksums/checksums.lock
Normal file
Binary file not shown.
BIN
android/.gradle/8.9/checksums/md5-checksums.bin
Normal file
BIN
android/.gradle/8.9/checksums/md5-checksums.bin
Normal file
Binary file not shown.
BIN
android/.gradle/8.9/checksums/sha1-checksums.bin
Normal file
BIN
android/.gradle/8.9/checksums/sha1-checksums.bin
Normal file
Binary file not shown.
BIN
android/.gradle/8.9/fileChanges/last-build.bin
Normal file
BIN
android/.gradle/8.9/fileChanges/last-build.bin
Normal file
Binary file not shown.
BIN
android/.gradle/8.9/fileHashes/fileHashes.lock
Normal file
BIN
android/.gradle/8.9/fileHashes/fileHashes.lock
Normal file
Binary file not shown.
0
android/.gradle/8.9/gc.properties
Normal file
0
android/.gradle/8.9/gc.properties
Normal file
BIN
android/.gradle/buildOutputCleanup/buildOutputCleanup.lock
Normal file
BIN
android/.gradle/buildOutputCleanup/buildOutputCleanup.lock
Normal file
Binary file not shown.
2
android/.gradle/buildOutputCleanup/cache.properties
Normal file
2
android/.gradle/buildOutputCleanup/cache.properties
Normal file
@ -0,0 +1,2 @@
|
||||
#Thu Mar 26 15:16:08 CST 2026
|
||||
gradle.version=8.9
|
||||
0
android/.gradle/vcs-1/gc.properties
Normal file
0
android/.gradle/vcs-1/gc.properties
Normal file
51
android/build.gradle
Normal file
51
android/build.gradle
Normal file
@ -0,0 +1,51 @@
|
||||
group = "com.funymee.client_proxy_framework"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = "2.1.0"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.6.0")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.android.library"
|
||||
apply plugin: "kotlin-android"
|
||||
|
||||
android {
|
||||
namespace = "com.funymee.client_proxy_framework"
|
||||
compileSdk = 35
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable "InvalidPackage"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version")
|
||||
implementation("com.facebook.android:facebook-android-sdk:18.0.0")
|
||||
}
|
||||
1
android/settings.gradle
Normal file
1
android/settings.gradle
Normal file
@ -0,0 +1 @@
|
||||
rootProject.name = "client_proxy_framework"
|
||||
1
android/src/main/AndroidManifest.xml
Normal file
1
android/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,10 @@
|
||||
- **强类型实体**:返回强类型实体类,开发者直接使用映射后的字段
|
||||
- **统一响应**:`EntityResponse<T>` 提供强类型返回值
|
||||
|
||||
### 换皮应用:从零搭建
|
||||
|
||||
从零创建一个新的换皮应用(`skin_config.json`、`ClientBootstrap`、`main` 初始化顺序、Android/iOS 要点等),请阅读 **[《创建新换皮应用 — 步骤清单》](create_new_skin_app.md)**。
|
||||
|
||||
---
|
||||
|
||||
## 2. 快速开始
|
||||
|
||||
160
docs/create_new_skin_app.md
Normal file
160
docs/create_new_skin_app.md
Normal file
@ -0,0 +1,160 @@
|
||||
# 创建新换皮应用 — 步骤清单
|
||||
|
||||
本文说明基于 `client_proxy_framework` **从零搭一个换皮应用**的推荐流程。配置以 **`skin_config.json`** 为主;更细的字段说明见 [`new_app_config_template.md`](new_app_config_template.md),原生侧见 [`sdk_integration_guide.md`](sdk_integration_guide.md)。更长篇的上下文与业务对接见 [`skin_app_development_guide.md`](skin_app_development_guide.md)。
|
||||
|
||||
***
|
||||
|
||||
## 1. 目录与仓库布局
|
||||
|
||||
建议将新应用与框架放在**同级目录**,便于 path 依赖:
|
||||
|
||||
```text
|
||||
Workplace/
|
||||
├── client_proxy_framework/ # 本框架
|
||||
└── your_skin_app/ # 新换皮应用
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 2. 创建 Flutter 工程
|
||||
|
||||
```bash
|
||||
cd Workplace
|
||||
flutter create your_skin_app
|
||||
cd your_skin_app
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 3. 接入框架依赖
|
||||
|
||||
在应用根目录 **`pubspec.yaml`** 中增加:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
client_proxy_framework:
|
||||
path: ../client_proxy_framework
|
||||
```
|
||||
|
||||
换皮应用若实现自有登录回调(设备 ID、签名等),通常还需要:
|
||||
|
||||
```yaml
|
||||
device_info_plus: ^11.1.0 # 常见:读 Android id / iOS identifierForVendor
|
||||
crypto: ^3.0.3 # 常见:与后端约定的 MD5 签名
|
||||
```
|
||||
|
||||
执行:
|
||||
|
||||
```bash
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
框架已通过插件注册 **Adjust、Facebook、IAP、Play Install Referrer** 等依赖,宿主 **无需** 再单独声明 `adjust_sdk`、`facebook_app_events`,除非你有特殊版本要求。
|
||||
|
||||
***
|
||||
|
||||
## 4. 准备 `skin_config.json`
|
||||
|
||||
1. 在应用内创建目录 **`assets/`**。
|
||||
2. 复制框架内的示例为起点:
|
||||
- 源文件:框架仓库 [`lib/src/config/skin_config.example.json`](../lib/src/config/skin_config.example.json)
|
||||
- 目标:`assets/skin_config.json`
|
||||
3. 按后端交付的换皮参数填写 JSON,主要块包括:
|
||||
- **`app`**:`name`、`id`(业务 appId)、`packageName`(渠道包名)
|
||||
- **`backend`**:`iosAppType`、`androidAppType`(如 `HIOS` / `HAndroid`,与 fast_login 约定一致)
|
||||
- **`api`**:预发/生产 URL、`proxyPath`、**16 字符** `aesKey`、`debugBaseUrlOverride`、`alwaysUsePreBaseUrl`(长期连预发时可 `true`)
|
||||
- **`proxyKeys` / `v2`**:与后端约定的代理字段名、V2 包装路径与噪音键
|
||||
- **`fieldMapping`**:原始字段 → V2 字段(可很大,整块放在 JSON)
|
||||
- **`analytics`**:Adjust / Facebook(占位符会在运行时跳过初始化,并在控制台提醒)
|
||||
- **`adjustEvents`**:可选,逻辑名 → Adjust 事件 Token
|
||||
|
||||
4. 在 **`pubspec.yaml`** 注册资源:
|
||||
|
||||
```yaml
|
||||
flutter:
|
||||
assets:
|
||||
- assets/skin_config.json
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 5. 应用入口 `main.dart`(推荐顺序)
|
||||
|
||||
异步初始化与 **归因缓存** 须在启动登录前完成,推荐顺序如下:
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:client_proxy_framework/client_proxy_framework.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await ClientBootstrap.initFromAsset('assets/skin_config.json');
|
||||
await ClientBootstrap.initAnalytics();
|
||||
await AnalyticsService.initAttribution();
|
||||
|
||||
runApp(MyApp(title: ClientBootstrap.skin.appName));
|
||||
|
||||
// 若使用 FrameworkAuthService 自动登录,在 runApp 之后或之前按产品要求调用
|
||||
// MyAuthService.init(); // 内部:FrameworkAuthService.init(你的 AuthServiceCallbacks); start();
|
||||
}
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- **`ClientBootstrap.initFromAsset`**:加载 JSON 并 `ApiClient.init`。
|
||||
- **`ClientBootstrap.initAnalytics`**:Adjust / Facebook(未配置或占位则降级,不卡死)。
|
||||
- **`AnalyticsService.initAttribution`**:预取归因,供登录时 referer;框架内 **`FrameworkAuthService.init` 默认已注册** 与 Adjust 缓存一致的 **`AnalyticsAttributionCallbacks`**,应用侧一般**不用再写归因回调类**。
|
||||
|
||||
***
|
||||
|
||||
## 6. Android 配置
|
||||
|
||||
1. **包名**:`android/app/build.gradle.kts`(或 `build.gradle`)中的 `applicationId`、`namespace`,与 JSON 里 `app.packageName` 一致;Kotlin 目录与 `MainActivity` 包名同步修改。
|
||||
2. **`MainActivity`**:使用默认 **`FlutterActivity`** 即可,**不要**再为 Facebook 单独写 `{packageName}/facebook_sdk` 的 MethodChannel;框架插件使用固定通道 **`client_proxy_framework/facebook_sdk`**。
|
||||
3. **`AndroidManifest.xml`**:按 Adjust / Facebook 官方要求配置 `meta-data`(App Token、环境、Facebook AppId、ClientToken 等),并与 `strings.xml` 等引用一致。细节见 [`sdk_integration_guide.md`](sdk_integration_guide.md)。
|
||||
4. **应用级 `Application` 子类**:一般不再需要仅为 Facebook 挂自定义 `Application`;若历史工程仍有,可评估删除后以默认 `Application` 运行。
|
||||
|
||||
***
|
||||
|
||||
## 7. iOS 配置
|
||||
|
||||
1. Xcode 中 **Bundle Identifier** 与 `skin_config.json` 的 `packageName` / 业务约定一致。
|
||||
2. **`Info.plist`**:Facebook、Adjust 等键值与控制台一致;参见 [`sdk_integration_guide.md`](sdk_integration_guide.md)。
|
||||
3. 工程根目录执行 **`pod install`** 后编译。
|
||||
|
||||
***
|
||||
|
||||
## 8. 登录与业务状态(应用侧最少代码)
|
||||
|
||||
框架负责:代理请求加解密、字段映射、**归因 referer 默认实现**、Adjust/Facebook 初始化(可配置)。
|
||||
|
||||
应用侧通常仍需:
|
||||
|
||||
1. 实现 **`AuthServiceCallbacks`**:`getDeviceId`、`computeSign`、`onLoginSuccess` / `onCommonInfoLoaded` / `onLoginFailed`(例如写入本地 `UserState`、路由)。
|
||||
2. 调用 **`FrameworkAuthService.init(你的回调)`**,再 **`await FrameworkAuthService.start()`**(可参考示例工程里的薄封装 `AuthService.init()`)。
|
||||
|
||||
无需再实现 **`AttributionCallbacks`**,除非要自定义 referer 格式;此时用 **`FrameworkAuthService.init(callbacks, attributionCallbacks: 你的实现)`**。
|
||||
|
||||
***
|
||||
|
||||
## 9. 自检与上线前
|
||||
|
||||
1. **调试运行**:观察控制台是否出现 `[client_proxy_framework]` 的 SDK 配置提醒;占位 Token 会跳过对应 SDK,**不要**带着 TODO 配置上生产。
|
||||
2. **接口**:确认 `debug` / `release` 下 `api.alwaysUsePreBaseUrl`、`debugBaseUrlOverride` 是否符合预期。
|
||||
3. **发布**:补齐 Adjust/Facebook 真实参数;完成签名、隐私政策、应用商店物料等(不在本文范围)。
|
||||
|
||||
***
|
||||
|
||||
## 10. 相关文档索引
|
||||
|
||||
| 文档 | 用途 |
|
||||
|------|------|
|
||||
| [`new_app_config_template.md`](new_app_config_template.md) | 按表逐项填写换皮参数(可对照填入 JSON) |
|
||||
| [`sdk_integration_guide.md`](sdk_integration_guide.md) | 原生工程 Adjust / Facebook 等集成要点 |
|
||||
| [`skin_app_development_guide.md`](skin_app_development_guide.md) | 更长流程、业务/UI 后续对接说明 |
|
||||
| 框架 `lib/src/config/skin_config.example.json` | JSON 最小结构示例 |
|
||||
|
||||
若本清单与旧文档冲突,**以本清单与当前框架代码为准**。
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
本文档说明如何使用 `client_proxy_framework` 从零创建并完成一个换皮应用。
|
||||
|
||||
> **推荐优先阅读**:[《创建新换皮应用 — 步骤清单》](create_new_skin_app.md) — 与当前框架实现(JSON 配置、`ClientBootstrap`、固定 Facebook Channel、默认归因回调等)保持同步的精简步骤。
|
||||
|
||||
***
|
||||
|
||||
> **重要说明**:本指南**仅完成数据框架的对接**,包括:
|
||||
|
||||
24
ios/Classes/ClientProxyFrameworkPlugin.swift
Normal file
24
ios/Classes/ClientProxyFrameworkPlugin.swift
Normal file
@ -0,0 +1,24 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
public class ClientProxyFrameworkPlugin: NSObject, FlutterPlugin {
|
||||
private var channel: FlutterMethodChannel?
|
||||
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let ch = FlutterMethodChannel(
|
||||
name: "client_proxy_framework/facebook_sdk",
|
||||
binaryMessenger: registrar.messenger())
|
||||
let instance = ClientProxyFrameworkPlugin()
|
||||
instance.channel = ch
|
||||
registrar.addMethodCallDelegate(instance, channel: ch)
|
||||
}
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
if call.method == "waitForFacebookSdkInit" {
|
||||
result(true)
|
||||
channel?.invokeMethod("onFacebookSdkInitialized", arguments: nil)
|
||||
} else {
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
}
|
||||
17
ios/client_proxy_framework.podspec
Normal file
17
ios/client_proxy_framework.podspec
Normal file
@ -0,0 +1,17 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'client_proxy_framework'
|
||||
s.version = '1.0.0'
|
||||
s.summary = 'Client proxy framework (Facebook bridge)'
|
||||
s.description = <<-DESC
|
||||
Registers MethodChannel for Facebook SDK init handshake used by Dart.
|
||||
DESC
|
||||
s.homepage = 'https://example.com'
|
||||
s.license = { :type => 'MIT', :file => '../LICENSE' }
|
||||
s.author = { 'FunyMee' => 'dev@example.com' }
|
||||
s.source = { :path => '.' }
|
||||
s.source_files = 'Classes/**/*'
|
||||
s.dependency 'Flutter'
|
||||
s.platform = :ios, '13.0'
|
||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
|
||||
s.swift_version = '5.0'
|
||||
end
|
||||
@ -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';
|
||||
|
||||
56
lib/src/bootstrap/client_bootstrap.dart
Normal file
56
lib/src/bootstrap/client_bootstrap.dart
Normal file
@ -0,0 +1,56 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../api/api_client.dart';
|
||||
import '../config/skin_config.dart';
|
||||
import '../services/analytics_service.dart';
|
||||
|
||||
/// 换皮应用统一入口:加载 JSON、初始化 ApiClient 与 Analytics。
|
||||
abstract final class ClientBootstrap {
|
||||
static SkinConfig? _skin;
|
||||
|
||||
/// 当前皮肤配置([initFromAsset] / [initFromJson] 成功后可用)。
|
||||
static SkinConfig get skin {
|
||||
final s = _skin;
|
||||
if (s == null) {
|
||||
throw StateError(
|
||||
'ClientBootstrap not initialized. Call initFromAsset or initFromJson first.',
|
||||
);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
static SkinConfig? get skinOrNull => _skin;
|
||||
|
||||
/// 从 Asset 加载 [SkinConfig] 并完成 [ApiClient]、[AnalyticsService] 初始化。
|
||||
///
|
||||
/// 不会注册归因回调或启动登录;宿主在 `initAttribution`、
|
||||
/// `FrameworkAuthService.init`、`start` 中自行接入。
|
||||
static Future<SkinConfig> initFromAsset(String assetPath) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
final raw = await rootBundle.loadString(assetPath);
|
||||
return initFromJsonString(raw);
|
||||
}
|
||||
|
||||
static Future<SkinConfig> initFromJsonString(String json) async {
|
||||
final skin = SkinConfig.fromJsonString(json);
|
||||
_apply(skin);
|
||||
return skin;
|
||||
}
|
||||
|
||||
static Future<SkinConfig> initFromJson(Map<String, dynamic> map) async {
|
||||
final skin = SkinConfig.fromJson(map);
|
||||
_apply(skin);
|
||||
return skin;
|
||||
}
|
||||
|
||||
static void _apply(SkinConfig skin) {
|
||||
_skin = skin;
|
||||
ApiClient.init(skin);
|
||||
}
|
||||
|
||||
/// 初始化归因 SDK(Adjust / Facebook);须在 [initFromAsset] 之后调用。
|
||||
static Future<void> initAnalytics() async {
|
||||
await AnalyticsService.init(skin.buildAnalyticsConfig());
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
/// 预发环境域名
|
||||
|
||||
@ -57,8 +57,8 @@ abstract class AttributionCallbacks {
|
||||
Future<String?> getFacebookReferrer();
|
||||
}
|
||||
|
||||
/// 默认归因实现
|
||||
class DefaultAttributionCallbacks implements AttributionCallbacks {
|
||||
/// 占位用空归因(单测等)。生产环境请使用 [AnalyticsAttributionCallbacks]。
|
||||
class StubAttributionCallbacks implements AttributionCallbacks {
|
||||
@override
|
||||
Future<String?> getReferrer() async => '';
|
||||
|
||||
|
||||
393
lib/src/config/skin_config.dart
Normal file
393
lib/src/config/skin_config.dart
Normal file
@ -0,0 +1,393 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../log/sdk_reminder_log.dart';
|
||||
import '../services/analytics_service.dart';
|
||||
import 'app_config.dart';
|
||||
import 'attribution_config.dart';
|
||||
import 'default_field_mapping.dart';
|
||||
import 'field_mapping.dart';
|
||||
|
||||
/// JSON 换皮配置(单文件描述 API、归因、字段映射等)。
|
||||
///
|
||||
/// 在宿主 `pubspec.yaml` 中注册 assets 后使用:
|
||||
/// `await ClientBootstrap.initFromAsset('assets/skin_config.json');`
|
||||
///
|
||||
/// 顶层结构示例:`lib/src/config/skin_config.example.json`。
|
||||
class SkinConfig implements AppConfig {
|
||||
SkinConfig._({
|
||||
required this.appName,
|
||||
required this.appId,
|
||||
required this.packageName,
|
||||
required this.backendAppTypeIOS,
|
||||
required this.backendAppTypeAndroid,
|
||||
required this.preBaseUrl,
|
||||
required this.prodBaseUrl,
|
||||
required this.proxyPath,
|
||||
required this.debugBaseUrlOverride,
|
||||
required this.alwaysUsePreBaseUrl,
|
||||
required this.aesKey,
|
||||
required this.proxyKeys,
|
||||
required this.v2LevelField,
|
||||
required this.v2LevelFixedValue,
|
||||
required this.v2SanctumPath,
|
||||
required this.v2NoiseKeys,
|
||||
required this.fieldMapping,
|
||||
required Map<String, String> adjustEvents,
|
||||
this.analyticsJson,
|
||||
}) : _adjustEvents = adjustEvents;
|
||||
|
||||
final Map<String, dynamic>? analyticsJson;
|
||||
final Map<String, String> _adjustEvents;
|
||||
|
||||
/// 从 JSON 文本解析(如 `rootBundle.loadString` 的结果)。
|
||||
factory SkinConfig.fromJsonString(String source) {
|
||||
final dynamic decoded = jsonDecode(source);
|
||||
if (decoded is! Map<String, dynamic>) {
|
||||
throw FormatException('skin_config root must be a JSON object');
|
||||
}
|
||||
return SkinConfig.fromJson(decoded);
|
||||
}
|
||||
|
||||
factory SkinConfig.fromJson(Map<String, dynamic> json) {
|
||||
final app = _map(json['app'], 'app');
|
||||
final api = _map(json['api'], 'api');
|
||||
|
||||
final name = app['name'] as String?;
|
||||
final id = app['id'] as String?;
|
||||
final pkg = app['packageName'] as String?;
|
||||
if (name == null || name.isEmpty) {
|
||||
throw FormatException('app.name is required');
|
||||
}
|
||||
if (id == null || id.isEmpty) {
|
||||
throw FormatException('app.id is required');
|
||||
}
|
||||
if (pkg == null || pkg.isEmpty) {
|
||||
throw FormatException('app.packageName is required');
|
||||
}
|
||||
|
||||
final pre = api['preBaseUrl'] as String?;
|
||||
final prod = api['prodBaseUrl'] as String?;
|
||||
final proxyPath = api['proxyPath'] as String?;
|
||||
final aes = api['aesKey'] as String?;
|
||||
if (pre == null || pre.isEmpty) {
|
||||
throw FormatException('api.preBaseUrl is required');
|
||||
}
|
||||
if (prod == null || prod.isEmpty) {
|
||||
throw FormatException('api.prodBaseUrl is required');
|
||||
}
|
||||
if (proxyPath == null || proxyPath.isEmpty) {
|
||||
throw FormatException('api.proxyPath is required');
|
||||
}
|
||||
if (aes == null || aes.isEmpty) {
|
||||
throw FormatException('api.aesKey is required (16 chars for current crypto)');
|
||||
}
|
||||
|
||||
final backend = json['backend'];
|
||||
String iosType = 'HIOS';
|
||||
String androidType = 'HAndroid';
|
||||
if (backend is Map<String, dynamic>) {
|
||||
iosType = backend['iosAppType'] as String? ?? iosType;
|
||||
androidType = backend['androidAppType'] as String? ?? androidType;
|
||||
}
|
||||
|
||||
final debugOverride = api['debugBaseUrlOverride'] as String?;
|
||||
final alwaysPre = api['alwaysUsePreBaseUrl'] as bool? ?? false;
|
||||
|
||||
final proxyKeys = _proxyKeysFromJson(json['proxyKeys']);
|
||||
final v2 = json['v2'];
|
||||
String v2Level = 'arsenal';
|
||||
int v2Fixed = 4;
|
||||
List<String> sanctum = const [
|
||||
'vault',
|
||||
'tome',
|
||||
'codex',
|
||||
'grimoire',
|
||||
'sanctum',
|
||||
];
|
||||
List<String> v2Noise = const [
|
||||
'roar',
|
||||
'clash',
|
||||
'thunder',
|
||||
'rumble',
|
||||
'howl',
|
||||
'growl',
|
||||
];
|
||||
if (v2 is Map<String, dynamic>) {
|
||||
v2Level = v2['levelField'] as String? ?? v2Level;
|
||||
v2Fixed = (v2['levelFixedValue'] as num?)?.toInt() ?? v2Fixed;
|
||||
final path = v2['sanctumPath'];
|
||||
if (path is List) {
|
||||
sanctum = path.map((e) => e.toString()).toList();
|
||||
}
|
||||
final nk = v2['noiseKeys'];
|
||||
if (nk is List) {
|
||||
v2Noise = nk.map((e) => e.toString()).toList();
|
||||
}
|
||||
}
|
||||
|
||||
FieldMapping mapping;
|
||||
final fmRaw = json['fieldMapping'];
|
||||
if (fmRaw == null) {
|
||||
mapping = petsHeroAIFieldMapping;
|
||||
} else if (fmRaw is Map<String, dynamic>) {
|
||||
if (fmRaw.isEmpty) {
|
||||
mapping = petsHeroAIFieldMapping;
|
||||
} else {
|
||||
final m = <String, String>{};
|
||||
for (final e in fmRaw.entries) {
|
||||
m[e.key.toString()] = e.value.toString();
|
||||
}
|
||||
mapping = FieldMapping(m);
|
||||
}
|
||||
} else {
|
||||
throw FormatException('fieldMapping must be a JSON object');
|
||||
}
|
||||
|
||||
final eventsRaw = json['adjustEvents'];
|
||||
final events = <String, String>{};
|
||||
if (eventsRaw is Map<String, dynamic>) {
|
||||
for (final e in eventsRaw.entries) {
|
||||
final v = e.value;
|
||||
if (v != null && v.toString().isNotEmpty) {
|
||||
events[e.key.toString()] = v.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SkinConfig._(
|
||||
appName: name,
|
||||
appId: id,
|
||||
packageName: pkg,
|
||||
backendAppTypeIOS: iosType,
|
||||
backendAppTypeAndroid: androidType,
|
||||
preBaseUrl: pre,
|
||||
prodBaseUrl: prod,
|
||||
proxyPath: proxyPath,
|
||||
debugBaseUrlOverride: debugOverride,
|
||||
alwaysUsePreBaseUrl: alwaysPre,
|
||||
aesKey: aes,
|
||||
proxyKeys: proxyKeys,
|
||||
v2LevelField: v2Level,
|
||||
v2LevelFixedValue: v2Fixed,
|
||||
v2SanctumPath: sanctum,
|
||||
v2NoiseKeys: v2Noise,
|
||||
fieldMapping: mapping,
|
||||
adjustEvents: events,
|
||||
analyticsJson: json['analytics'] as Map<String, dynamic>?,
|
||||
);
|
||||
}
|
||||
|
||||
static Map<String, dynamic> _map(dynamic v, String name) {
|
||||
if (v is! Map<String, dynamic>) {
|
||||
throw FormatException('$name must be a JSON object');
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
static ProxyKeysConfig _proxyKeysFromJson(dynamic v) {
|
||||
if (v == null) return const ProxyKeysConfig();
|
||||
if (v is! Map<String, dynamic>) {
|
||||
throw FormatException('proxyKeys must be a JSON object');
|
||||
}
|
||||
List<String> noise = const [
|
||||
'billing_addr',
|
||||
'utm_term',
|
||||
'cluster_id',
|
||||
'lsn_value',
|
||||
'accuracy_val',
|
||||
'dir_path',
|
||||
];
|
||||
final nk = v['noiseKeys'];
|
||||
if (nk is List && nk.isNotEmpty) {
|
||||
noise = nk.map((e) => e.toString()).toList();
|
||||
}
|
||||
return ProxyKeysConfig(
|
||||
appIdField: v['appIdField'] as String? ?? 'hero_class',
|
||||
pathField: v['pathField'] as String? ?? 'pet_species',
|
||||
methodField: v['methodField'] as String? ?? 'power_level',
|
||||
headerField: v['headerField'] as String? ?? 'quest_rank',
|
||||
paramsField: v['paramsField'] as String? ?? 'battle_score',
|
||||
bodyField: v['bodyField'] as String? ?? 'loyalty_index',
|
||||
noiseKeys: noise,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
final String appName;
|
||||
|
||||
@override
|
||||
final String appId;
|
||||
|
||||
@override
|
||||
final String packageName;
|
||||
|
||||
@override
|
||||
final String backendAppTypeIOS;
|
||||
|
||||
@override
|
||||
final String backendAppTypeAndroid;
|
||||
|
||||
@override
|
||||
final String preBaseUrl;
|
||||
|
||||
@override
|
||||
final String prodBaseUrl;
|
||||
|
||||
@override
|
||||
final String proxyPath;
|
||||
|
||||
@override
|
||||
final String? debugBaseUrlOverride;
|
||||
|
||||
/// 为 true 时 [baseUrl] 始终为 [preBaseUrl](便于长期连预发调试)。
|
||||
final bool alwaysUsePreBaseUrl;
|
||||
|
||||
@override
|
||||
String get baseUrl {
|
||||
if (alwaysUsePreBaseUrl) return preBaseUrl;
|
||||
if (!kDebugMode) return prodBaseUrl;
|
||||
return debugBaseUrlOverride ?? preBaseUrl;
|
||||
}
|
||||
|
||||
@override
|
||||
String get proxyUrl => '$baseUrl$proxyPath';
|
||||
|
||||
@override
|
||||
final String aesKey;
|
||||
|
||||
@override
|
||||
final ProxyKeysConfig proxyKeys;
|
||||
|
||||
@override
|
||||
final String v2LevelField;
|
||||
|
||||
@override
|
||||
final int v2LevelFixedValue;
|
||||
|
||||
@override
|
||||
final List<String> v2SanctumPath;
|
||||
|
||||
@override
|
||||
final List<String> v2NoiseKeys;
|
||||
|
||||
@override
|
||||
final FieldMapping fieldMapping;
|
||||
|
||||
@override
|
||||
Map<String, dynamic> get v2FixedValues => {v2LevelField: v2LevelFixedValue};
|
||||
|
||||
/// 语义化 Adjust 事件名 → Dashboard token;无配置则为空 map。
|
||||
Map<String, String> get adjustEventTokens =>
|
||||
Map<String, String>.unmodifiable(_adjustEvents);
|
||||
|
||||
/// 按 [adjustEvents] 里的逻辑名上报 Adjust(若 token 不存在则忽略)。
|
||||
void trackAdjustEvent(String logicalName) {
|
||||
final token = _adjustEvents[logicalName];
|
||||
if (token != null && token.isNotEmpty) {
|
||||
AnalyticsService.trackEvent(token);
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建 [AnalyticsService.init] 所需配置(从 JSON `analytics` 节读取)。
|
||||
AnalyticsConfig buildAnalyticsConfig({bool debugLogsFallback = false}) {
|
||||
final root = analyticsJson;
|
||||
bool debugLogs = debugLogsFallback;
|
||||
AdjustConfig? adjustCfg;
|
||||
FacebookConfig? fbCfg;
|
||||
|
||||
if (root != null) {
|
||||
debugLogs = root['debugLogs'] as bool? ?? debugLogs;
|
||||
|
||||
final adj = root['adjust'];
|
||||
if (adj is Map<String, dynamic>) {
|
||||
final token = adj['appToken'] as String?;
|
||||
if (token != null && _isUsableSecret(token)) {
|
||||
adjustCfg = AdjustConfig(
|
||||
appToken: token.trim(),
|
||||
environment: _parseAdjustEnv(adj['environment'] as String?),
|
||||
logLevel: _parseAdjustLogLevel(adj['logLevel'] as String?),
|
||||
fbAppId: adj['fbAppId'] as String?,
|
||||
);
|
||||
} else if (token != null && token.trim().isNotEmpty) {
|
||||
SdkReminderLog.adjust('appToken 为占位或无效值,已跳过 Adjust。');
|
||||
}
|
||||
}
|
||||
|
||||
final fb = root['facebook'];
|
||||
if (fb is Map<String, dynamic>) {
|
||||
final id = fb['appId'] as String? ?? '';
|
||||
final clientTok = fb['clientToken'] as String? ?? '';
|
||||
final idOk = _isUsableSecret(id);
|
||||
final ct = clientTok.trim();
|
||||
final ctOk = ct.isEmpty || _isUsableSecret(ct);
|
||||
if (idOk && ctOk) {
|
||||
fbCfg = FacebookConfig(
|
||||
appId: id.trim(),
|
||||
clientToken: ct.isEmpty ? null : ct,
|
||||
debugLogs: fb['debugLogs'] as bool? ?? false,
|
||||
);
|
||||
} else if (id.trim().isNotEmpty || clientTok.trim().isNotEmpty) {
|
||||
SdkReminderLog.facebook(
|
||||
'App ID / Client Token 为占位或无效,已跳过 Facebook。',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return AnalyticsConfig(
|
||||
packageName: packageName,
|
||||
adjustConfig: adjustCfg,
|
||||
facebookConfig: fbCfg,
|
||||
platformAttributionConfig: _parsePlatformAttribution(root),
|
||||
debugLogs: debugLogs,
|
||||
);
|
||||
}
|
||||
|
||||
static AdjustEnv _parseAdjustEnv(String? raw) {
|
||||
switch (raw?.toLowerCase()) {
|
||||
case 'sandbox':
|
||||
return AdjustEnv.sandbox;
|
||||
case 'production':
|
||||
default:
|
||||
return AdjustEnv.production;
|
||||
}
|
||||
}
|
||||
|
||||
static AdjustLogLevel _parseAdjustLogLevel(String? raw) {
|
||||
switch (raw?.toLowerCase()) {
|
||||
case 'verbose':
|
||||
return AdjustLogLevel.verbose;
|
||||
case 'off':
|
||||
default:
|
||||
return AdjustLogLevel.off;
|
||||
}
|
||||
}
|
||||
|
||||
/// 排除模板占位符,避免带着假 token 初始化 SDK。
|
||||
static bool _isUsableSecret(String? raw) {
|
||||
if (raw == null) return false;
|
||||
final s = raw.trim();
|
||||
if (s.isEmpty) return false;
|
||||
final lower = s.toLowerCase();
|
||||
if (lower.startsWith('todo')) return false;
|
||||
if (lower.contains('your_')) return false;
|
||||
if (lower.contains('example.com')) return false;
|
||||
if (lower == 'changeme') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static PlatformAttributionConfig? _parsePlatformAttribution(
|
||||
Map<String, dynamic>? root,
|
||||
) {
|
||||
if (root == null) return null;
|
||||
final p = root['platformAttribution'];
|
||||
if (p is Map<String, dynamic>) {
|
||||
return PlatformAttributionConfig(
|
||||
enabled: p['enabled'] as bool? ?? true,
|
||||
);
|
||||
}
|
||||
return const PlatformAttributionConfig();
|
||||
}
|
||||
}
|
||||
57
lib/src/config/skin_config.example.json
Normal file
57
lib/src/config/skin_config.example.json
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "Example App",
|
||||
"id": "com.example.app",
|
||||
"packageName": "com.example.app"
|
||||
},
|
||||
"backend": {
|
||||
"iosAppType": "HIOS",
|
||||
"androidAppType": "HAndroid"
|
||||
},
|
||||
"api": {
|
||||
"preBaseUrl": "https://pre-api.example.com",
|
||||
"prodBaseUrl": "https://api.example.com",
|
||||
"proxyPath": "/v1/proxy",
|
||||
"debugBaseUrlOverride": null,
|
||||
"alwaysUsePreBaseUrl": false,
|
||||
"aesKey": "1234567890123456"
|
||||
},
|
||||
"proxyKeys": {
|
||||
"appIdField": "hero_class",
|
||||
"pathField": "pet_species",
|
||||
"methodField": "power_level",
|
||||
"headerField": "quest_rank",
|
||||
"paramsField": "battle_score",
|
||||
"bodyField": "loyalty_index",
|
||||
"noiseKeys": ["billing_addr", "utm_term", "cluster_id", "lsn_value", "accuracy_val", "dir_path"]
|
||||
},
|
||||
"v2": {
|
||||
"levelField": "arsenal",
|
||||
"levelFixedValue": 4,
|
||||
"sanctumPath": ["vault", "tome", "codex", "grimoire", "sanctum"],
|
||||
"noiseKeys": ["roar", "clash", "thunder", "rumble", "howl", "growl"]
|
||||
},
|
||||
"fieldMapping": {
|
||||
"code": "helm",
|
||||
"msg": "rampart",
|
||||
"data": "sidekick"
|
||||
},
|
||||
"analytics": {
|
||||
"debugLogs": false,
|
||||
"adjust": {
|
||||
"appToken": "your_adjust_app_token",
|
||||
"environment": "sandbox",
|
||||
"logLevel": "off",
|
||||
"fbAppId": null
|
||||
},
|
||||
"facebook": {
|
||||
"appId": "your_facebook_app_id",
|
||||
"clientToken": "your_client_token",
|
||||
"debugLogs": false
|
||||
}
|
||||
},
|
||||
"adjustEvents": {
|
||||
"register": "abc123",
|
||||
"purchase": "def456"
|
||||
}
|
||||
}
|
||||
33
lib/src/log/sdk_reminder_log.dart
Normal file
33
lib/src/log/sdk_reminder_log.dart
Normal file
@ -0,0 +1,33 @@
|
||||
import 'dart:developer' as developer;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// 第三方 SDK 未配置或降级时的提醒(不抛异常、不阻塞启动)。
|
||||
abstract final class SdkReminderLog {
|
||||
static final Set<String> _onceKeys = {};
|
||||
|
||||
static void debug(String message) {
|
||||
debugPrint('[client_proxy_framework] $message');
|
||||
}
|
||||
|
||||
/// 每条 [key] 仅输出一次,避免刷屏。
|
||||
static void once(String key, String message) {
|
||||
if (!_onceKeys.add(key)) return;
|
||||
final full = '[client_proxy_framework] $message '
|
||||
'(正式上线前请在 skin_config.json / 原生侧完成配置)';
|
||||
debugPrint(full);
|
||||
developer.log(full, name: 'client_proxy_framework');
|
||||
}
|
||||
|
||||
static void adjust(String detail) {
|
||||
once('adjust', 'Adjust: $detail');
|
||||
}
|
||||
|
||||
static void facebook(String detail) {
|
||||
once('facebook', 'Facebook: $detail');
|
||||
}
|
||||
|
||||
static void playReferrer(String detail) {
|
||||
once('play_referrer', 'Play Install Referrer: $detail');
|
||||
}
|
||||
}
|
||||
@ -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<AttributionData?> getAttribution() async {
|
||||
|
||||
59
lib/src/services/analytics_attribution_callbacks.dart
Normal file
59
lib/src/services/analytics_attribution_callbacks.dart
Normal file
@ -0,0 +1,59 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../config/attribution_config.dart';
|
||||
import 'adjust_service.dart';
|
||||
import 'analytics_service.dart';
|
||||
|
||||
/// 基于 [AnalyticsService] 已缓存的 Adjust 归因数据,为 fast_login / 后端提供 referer 串。
|
||||
///
|
||||
/// 须在 [AnalyticsService.init] 与 [AnalyticsService.initAttribution] 完成之后再启动
|
||||
/// [FrameworkAuthService.start](宿主 `main` 中建议保持该顺序)。
|
||||
final class AnalyticsAttributionCallbacks implements AttributionCallbacks {
|
||||
@override
|
||||
Future<String?> getReferrer() async {
|
||||
final attribution = AnalyticsService.getAttribution();
|
||||
if (attribution != null) {
|
||||
return _attributionToPayload(attribution);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getAdjustReferrer() async => getReferrer();
|
||||
|
||||
@override
|
||||
Future<String?> getPlatformReferrer() async => null;
|
||||
|
||||
@override
|
||||
Future<String?> getFacebookReferrer() async {
|
||||
final attribution = AnalyticsService.getAttribution();
|
||||
if (attribution != null && attribution.fbInstallReferrer != null) {
|
||||
return attribution.fbInstallReferrer;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String _attributionToPayload(AttributionData data) {
|
||||
final map = <String, dynamic>{
|
||||
'trackerToken': data.trackerToken,
|
||||
'trackerName': data.trackerName,
|
||||
'network': data.network,
|
||||
'campaign': data.campaign,
|
||||
'adgroup': data.adgroup,
|
||||
'creative': data.creative,
|
||||
'clickLabel': data.clickLabel,
|
||||
'costType': data.costType,
|
||||
'costAmount': _jsonEncodableCostAmount(data.costAmount),
|
||||
'costCurrency': data.costCurrency,
|
||||
'jsonResponse': data.jsonResponse,
|
||||
'fbInstallReferrer': data.fbInstallReferrer,
|
||||
};
|
||||
return base64Encode(utf8.encode(jsonEncode(map)));
|
||||
}
|
||||
|
||||
static Object? _jsonEncodableCostAmount(double? v) {
|
||||
if (v == null) return null;
|
||||
if (v.isNaN || !v.isFinite) return v.toString();
|
||||
return v;
|
||||
}
|
||||
}
|
||||
@ -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<void> _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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<void> get loginComplete => _loginFuture ?? Future<void>.value();
|
||||
|
||||
/// 初始化认证服务
|
||||
static void init(AuthServiceCallbacks callbacks) {
|
||||
/// 初始化认证服务。
|
||||
///
|
||||
/// 默认注册 [AnalyticsAttributionCallbacks](与 Adjust 缓存一致)。可传入
|
||||
/// [attributionCallbacks] 覆盖(例如自定义 referer 格式)。
|
||||
static void init(
|
||||
AuthServiceCallbacks callbacks, {
|
||||
AttributionCallbacks? attributionCallbacks,
|
||||
}) {
|
||||
AttributionService.init(
|
||||
attributionCallbacks ?? AnalyticsAttributionCallbacks(),
|
||||
);
|
||||
_callbacks = callbacks;
|
||||
_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<void>();
|
||||
@ -118,8 +129,10 @@ abstract class FrameworkAuthService {
|
||||
await Future<void>.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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<void> 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<void> 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<void>('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<String, dynamic>? parameters}) {
|
||||
_fb.logEvent(name: name, parameters: parameters);
|
||||
try {
|
||||
_fb.logEvent(name: name, parameters: parameters);
|
||||
} catch (e) {
|
||||
SdkReminderLog.facebook('logEvent 失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user