优化:将应用端的配置改用JSON形式更加灵活
This commit is contained in:
parent
83c1c56c36
commit
e77f5f45b7
@ -21,6 +21,19 @@
|
|||||||
],
|
],
|
||||||
"devDependencies": []
|
"devDependencies": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "shared_preferences",
|
||||||
|
"version": "2.5.4",
|
||||||
|
"dependencies": [
|
||||||
|
"flutter",
|
||||||
|
"shared_preferences_android",
|
||||||
|
"shared_preferences_foundation",
|
||||||
|
"shared_preferences_linux",
|
||||||
|
"shared_preferences_platform_interface",
|
||||||
|
"shared_preferences_web",
|
||||||
|
"shared_preferences_windows"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "play_install_referrer",
|
"name": "play_install_referrer",
|
||||||
"version": "0.5.0",
|
"version": "0.5.0",
|
||||||
@ -37,6 +50,16 @@
|
|||||||
"in_app_purchase_platform_interface"
|
"in_app_purchase_platform_interface"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "in_app_purchase",
|
||||||
|
"version": "3.2.3",
|
||||||
|
"dependencies": [
|
||||||
|
"flutter",
|
||||||
|
"in_app_purchase_android",
|
||||||
|
"in_app_purchase_platform_interface",
|
||||||
|
"in_app_purchase_storekit"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "facebook_app_events",
|
"name": "facebook_app_events",
|
||||||
"version": "0.26.0",
|
"version": "0.26.0",
|
||||||
@ -101,6 +124,64 @@
|
|||||||
"vector_math"
|
"vector_math"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "shared_preferences_windows",
|
||||||
|
"version": "2.4.1",
|
||||||
|
"dependencies": [
|
||||||
|
"file",
|
||||||
|
"flutter",
|
||||||
|
"path",
|
||||||
|
"path_provider_platform_interface",
|
||||||
|
"path_provider_windows",
|
||||||
|
"shared_preferences_platform_interface"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shared_preferences_web",
|
||||||
|
"version": "2.4.3",
|
||||||
|
"dependencies": [
|
||||||
|
"flutter",
|
||||||
|
"flutter_web_plugins",
|
||||||
|
"shared_preferences_platform_interface",
|
||||||
|
"web"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shared_preferences_platform_interface",
|
||||||
|
"version": "2.4.1",
|
||||||
|
"dependencies": [
|
||||||
|
"flutter",
|
||||||
|
"plugin_platform_interface"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shared_preferences_linux",
|
||||||
|
"version": "2.4.1",
|
||||||
|
"dependencies": [
|
||||||
|
"file",
|
||||||
|
"flutter",
|
||||||
|
"path",
|
||||||
|
"path_provider_linux",
|
||||||
|
"path_provider_platform_interface",
|
||||||
|
"shared_preferences_platform_interface"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shared_preferences_foundation",
|
||||||
|
"version": "2.5.6",
|
||||||
|
"dependencies": [
|
||||||
|
"flutter",
|
||||||
|
"shared_preferences_platform_interface"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shared_preferences_android",
|
||||||
|
"version": "2.4.21",
|
||||||
|
"dependencies": [
|
||||||
|
"flutter",
|
||||||
|
"shared_preferences_platform_interface"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "in_app_purchase_platform_interface",
|
"name": "in_app_purchase_platform_interface",
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
@ -114,6 +195,16 @@
|
|||||||
"version": "1.19.1",
|
"version": "1.19.1",
|
||||||
"dependencies": []
|
"dependencies": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "in_app_purchase_storekit",
|
||||||
|
"version": "0.4.8+1",
|
||||||
|
"dependencies": [
|
||||||
|
"collection",
|
||||||
|
"flutter",
|
||||||
|
"in_app_purchase_platform_interface",
|
||||||
|
"json_annotation"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "meta",
|
"name": "meta",
|
||||||
"version": "1.17.0",
|
"version": "1.17.0",
|
||||||
@ -195,6 +286,70 @@
|
|||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"dependencies": []
|
"dependencies": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "path_provider_windows",
|
||||||
|
"version": "2.3.0",
|
||||||
|
"dependencies": [
|
||||||
|
"ffi",
|
||||||
|
"flutter",
|
||||||
|
"path",
|
||||||
|
"path_provider_platform_interface"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "path_provider_platform_interface",
|
||||||
|
"version": "2.1.2",
|
||||||
|
"dependencies": [
|
||||||
|
"flutter",
|
||||||
|
"platform",
|
||||||
|
"plugin_platform_interface"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "path",
|
||||||
|
"version": "1.9.1",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "file",
|
||||||
|
"version": "7.0.1",
|
||||||
|
"dependencies": [
|
||||||
|
"meta",
|
||||||
|
"path"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "flutter_web_plugins",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": [
|
||||||
|
"flutter"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "plugin_platform_interface",
|
||||||
|
"version": "2.1.8",
|
||||||
|
"dependencies": [
|
||||||
|
"meta"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "path_provider_linux",
|
||||||
|
"version": "2.2.1",
|
||||||
|
"dependencies": [
|
||||||
|
"ffi",
|
||||||
|
"flutter",
|
||||||
|
"path",
|
||||||
|
"path_provider_platform_interface",
|
||||||
|
"xdg_directories"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "json_annotation",
|
||||||
|
"version": "4.11.0",
|
||||||
|
"dependencies": [
|
||||||
|
"meta"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "js",
|
"name": "js",
|
||||||
"version": "0.7.2",
|
"version": "0.7.2",
|
||||||
@ -224,152 +379,15 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "term_glyph",
|
"name": "ffi",
|
||||||
"version": "1.2.2",
|
"version": "2.2.0",
|
||||||
"dependencies": []
|
"dependencies": []
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "path",
|
|
||||||
"version": "1.9.1",
|
|
||||||
"dependencies": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "plugin_platform_interface",
|
|
||||||
"version": "2.1.8",
|
|
||||||
"dependencies": [
|
|
||||||
"meta"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "in_app_purchase",
|
|
||||||
"version": "3.2.3",
|
|
||||||
"dependencies": [
|
|
||||||
"flutter",
|
|
||||||
"in_app_purchase_android",
|
|
||||||
"in_app_purchase_platform_interface",
|
|
||||||
"in_app_purchase_storekit"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "in_app_purchase_storekit",
|
|
||||||
"version": "0.4.8+1",
|
|
||||||
"dependencies": [
|
|
||||||
"collection",
|
|
||||||
"flutter",
|
|
||||||
"in_app_purchase_platform_interface",
|
|
||||||
"json_annotation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "json_annotation",
|
|
||||||
"version": "4.11.0",
|
|
||||||
"dependencies": [
|
|
||||||
"meta"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "shared_preferences",
|
|
||||||
"version": "2.5.4",
|
|
||||||
"dependencies": [
|
|
||||||
"flutter",
|
|
||||||
"shared_preferences_android",
|
|
||||||
"shared_preferences_foundation",
|
|
||||||
"shared_preferences_linux",
|
|
||||||
"shared_preferences_platform_interface",
|
|
||||||
"shared_preferences_web",
|
|
||||||
"shared_preferences_windows"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "shared_preferences_windows",
|
|
||||||
"version": "2.4.1",
|
|
||||||
"dependencies": [
|
|
||||||
"file",
|
|
||||||
"flutter",
|
|
||||||
"path",
|
|
||||||
"path_provider_platform_interface",
|
|
||||||
"path_provider_windows",
|
|
||||||
"shared_preferences_platform_interface"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "shared_preferences_platform_interface",
|
|
||||||
"version": "2.4.1",
|
|
||||||
"dependencies": [
|
|
||||||
"flutter",
|
|
||||||
"plugin_platform_interface"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "shared_preferences_linux",
|
|
||||||
"version": "2.4.1",
|
|
||||||
"dependencies": [
|
|
||||||
"file",
|
|
||||||
"flutter",
|
|
||||||
"path",
|
|
||||||
"path_provider_linux",
|
|
||||||
"path_provider_platform_interface",
|
|
||||||
"shared_preferences_platform_interface"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "shared_preferences_web",
|
|
||||||
"version": "2.4.3",
|
|
||||||
"dependencies": [
|
|
||||||
"flutter",
|
|
||||||
"flutter_web_plugins",
|
|
||||||
"shared_preferences_platform_interface",
|
|
||||||
"web"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "flutter_web_plugins",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"dependencies": [
|
|
||||||
"flutter"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "shared_preferences_foundation",
|
|
||||||
"version": "2.5.6",
|
|
||||||
"dependencies": [
|
|
||||||
"flutter",
|
|
||||||
"shared_preferences_platform_interface"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "file",
|
|
||||||
"version": "7.0.1",
|
|
||||||
"dependencies": [
|
|
||||||
"meta",
|
|
||||||
"path"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "path_provider_platform_interface",
|
|
||||||
"version": "2.1.2",
|
|
||||||
"dependencies": [
|
|
||||||
"flutter",
|
|
||||||
"platform",
|
|
||||||
"plugin_platform_interface"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "platform",
|
"name": "platform",
|
||||||
"version": "3.1.6",
|
"version": "3.1.6",
|
||||||
"dependencies": []
|
"dependencies": []
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "path_provider_linux",
|
|
||||||
"version": "2.2.1",
|
|
||||||
"dependencies": [
|
|
||||||
"ffi",
|
|
||||||
"flutter",
|
|
||||||
"path",
|
|
||||||
"path_provider_platform_interface",
|
|
||||||
"xdg_directories"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "xdg_directories",
|
"name": "xdg_directories",
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
@ -379,27 +397,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "ffi",
|
"name": "term_glyph",
|
||||||
"version": "2.2.0",
|
"version": "1.2.2",
|
||||||
"dependencies": []
|
"dependencies": []
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "path_provider_windows",
|
|
||||||
"version": "2.3.0",
|
|
||||||
"dependencies": [
|
|
||||||
"ffi",
|
|
||||||
"flutter",
|
|
||||||
"path",
|
|
||||||
"path_provider_platform_interface"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "shared_preferences_android",
|
|
||||||
"version": "2.4.21",
|
|
||||||
"dependencies": [
|
|
||||||
"flutter",
|
|
||||||
"shared_preferences_platform_interface"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"configVersion": 1
|
"configVersion": 1
|
||||||
|
|||||||
21
LICENSE
Normal file
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 框架。**接口请求已按原始字段名写好**,不同应用只需**修改映射表**即可接入不同后端。
|
通用代理 API 框架。**接口请求已按原始字段名写好**,不同应用只需**修改映射表**即可接入不同后端。
|
||||||
|
|
||||||
|
**新建换皮应用**:请阅读 [docs/create_new_skin_app.md](docs/create_new_skin_app.md)(`skin_config.json` + `ClientBootstrap` 全流程)。
|
||||||
|
|
||||||
## 设计思路
|
## 设计思路
|
||||||
|
|
||||||
- **业务层**:统一使用**原始字段名**(canonical),如 `deviceId`、`userId`、`credits`、`userToken`
|
- **业务层**:统一使用**原始字段名**(canonical),如 `deviceId`、`userId`、`credits`、`userToken`
|
||||||
|
|||||||
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>` 提供强类型返回值
|
- **统一响应**:`EntityResponse<T>` 提供强类型返回值
|
||||||
|
|
||||||
|
### 换皮应用:从零搭建
|
||||||
|
|
||||||
|
从零创建一个新的换皮应用(`skin_config.json`、`ClientBootstrap`、`main` 初始化顺序、Android/iOS 要点等),请阅读 **[《创建新换皮应用 — 步骤清单》](create_new_skin_app.md)**。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 快速开始
|
## 2. 快速开始
|
||||||
|
|||||||
160
docs/create_new_skin_app.md
Normal file
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` 从零创建并完成一个换皮应用。
|
本文档说明如何使用 `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 框架。
|
/// 通用代理 API 框架。
|
||||||
///
|
///
|
||||||
/// 换皮应用只需:
|
/// 换皮应用推荐:
|
||||||
/// 1. 实现 [AppConfig] 提供 appId、aesKey、baseUrl 等
|
/// 1. 在 assets 中放置 `skin_config.json`(见 [SkinConfig])
|
||||||
/// 2. 在 main 中调用 ApiClient.init(yourConfig)
|
/// 2. `await ClientBootstrap.initFromAsset('assets/skin_config.json');`
|
||||||
/// 3. 通过 ApiClient.instance.proxy 发起请求
|
/// 3. `await ClientBootstrap.initAnalytics();`(可选)
|
||||||
|
/// 4. 通过 [ApiClient.instance.proxy] 发起请求
|
||||||
|
///
|
||||||
|
/// 亦可手写 [AppConfig] 并 `ApiClient.init(config)`。
|
||||||
library;
|
library;
|
||||||
|
|
||||||
export 'src/api/api_client.dart';
|
export 'src/api/api_client.dart';
|
||||||
export 'src/api/api_crypto.dart';
|
export 'src/api/api_crypto.dart';
|
||||||
export 'src/api/api_response.dart';
|
export 'src/api/api_response.dart';
|
||||||
export 'src/api/proxy_client.dart';
|
export 'src/api/proxy_client.dart';
|
||||||
|
export 'src/bootstrap/client_bootstrap.dart';
|
||||||
export 'src/config/app_config.dart';
|
export 'src/config/app_config.dart';
|
||||||
export 'src/config/attribution_config.dart';
|
export 'src/config/attribution_config.dart';
|
||||||
|
export 'src/config/skin_config.dart';
|
||||||
export 'src/config/field_mapping.dart';
|
export 'src/config/field_mapping.dart';
|
||||||
export 'src/config/default_field_mapping.dart';
|
export 'src/config/default_field_mapping.dart';
|
||||||
export 'src/entities/entities.dart';
|
export 'src/entities/entities.dart';
|
||||||
export 'src/log/app_logger.dart';
|
export 'src/log/app_logger.dart';
|
||||||
|
export 'src/log/sdk_reminder_log.dart';
|
||||||
export 'src/services/adjust_service.dart';
|
export 'src/services/adjust_service.dart';
|
||||||
|
export 'src/services/analytics_attribution_callbacks.dart';
|
||||||
export 'src/services/analytics_service.dart';
|
export 'src/services/analytics_service.dart';
|
||||||
export 'src/services/auth_service.dart';
|
export 'src/services/auth_service.dart';
|
||||||
export 'src/services/facebook_service.dart';
|
export 'src/services/facebook_service.dart';
|
||||||
|
|||||||
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 {
|
class ProxyKeysConfig {
|
||||||
const ProxyKeysConfig({
|
const ProxyKeysConfig({
|
||||||
/// appId 明文字段名
|
/// 代理请求体最外层「应用明文」字段的 **键名**(值由框架写入 [AppConfig.appName],与后端约定一致)
|
||||||
this.appIdField = 'hero_class',
|
this.appIdField = 'hero_class',
|
||||||
|
|
||||||
/// 原始 path 字段名
|
/// 原始 path 字段名
|
||||||
@ -35,7 +35,7 @@ class ProxyKeysConfig {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
/// appId 明文字段名
|
/// 代理请求体应用明文字段键名(值 = appName)
|
||||||
final String appIdField;
|
final String appIdField;
|
||||||
|
|
||||||
/// 原始 path 字段名
|
/// 原始 path 字段名
|
||||||
@ -65,13 +65,19 @@ abstract class AppConfig {
|
|||||||
/// 应用名称
|
/// 应用名称
|
||||||
String get appName;
|
String get appName;
|
||||||
|
|
||||||
/// 应用标识(代理请求 hero_class)
|
/// 业务侧应用标识(如 fast_login 的 query `app`、与后台约定的 app id)
|
||||||
String get appId;
|
String get appId;
|
||||||
|
|
||||||
/// 应用包名
|
/// 应用包名(渠道包名 / 请求头映射等)
|
||||||
String get packageName;
|
String get packageName;
|
||||||
|
|
||||||
/// AES 密钥
|
/// fast_login 等接口在 **iOS** 上使用的 `app` 参数(常见 `HIOS`)
|
||||||
|
String get backendAppTypeIOS => 'HIOS';
|
||||||
|
|
||||||
|
/// fast_login 等接口在 **Android** 上使用的 `app` 参数(常见 `HAndroid`)
|
||||||
|
String get backendAppTypeAndroid => 'HAndroid';
|
||||||
|
|
||||||
|
/// AES-128 密钥字符串(当前crypto实现为 16 字符)
|
||||||
String get aesKey;
|
String get aesKey;
|
||||||
|
|
||||||
/// 预发环境域名
|
/// 预发环境域名
|
||||||
|
|||||||
@ -57,8 +57,8 @@ abstract class AttributionCallbacks {
|
|||||||
Future<String?> getFacebookReferrer();
|
Future<String?> getFacebookReferrer();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 默认归因实现
|
/// 占位用空归因(单测等)。生产环境请使用 [AnalyticsAttributionCallbacks]。
|
||||||
class DefaultAttributionCallbacks implements AttributionCallbacks {
|
class StubAttributionCallbacks implements AttributionCallbacks {
|
||||||
@override
|
@override
|
||||||
Future<String?> getReferrer() async => '';
|
Future<String?> getReferrer() async => '';
|
||||||
|
|
||||||
|
|||||||
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 'package:play_install_referrer/play_install_referrer.dart';
|
||||||
|
|
||||||
import '../log/app_logger.dart';
|
import '../log/app_logger.dart';
|
||||||
|
import '../log/sdk_reminder_log.dart';
|
||||||
|
|
||||||
typedef AdjustAttributionCallback = void Function(
|
typedef AdjustAttributionCallback = void Function(
|
||||||
AdjustAttribution attribution);
|
AdjustAttribution attribution);
|
||||||
@ -29,14 +30,19 @@ class AdjustService {
|
|||||||
|
|
||||||
static void init(adj.AdjustConfig config,
|
static void init(adj.AdjustConfig config,
|
||||||
{AdjustAttributionCallback? onAttribution}) {
|
{AdjustAttributionCallback? onAttribution}) {
|
||||||
_config = config;
|
try {
|
||||||
_attributionCallback = onAttribution;
|
_config = config;
|
||||||
config.attributionCallback = _onAttribution;
|
_attributionCallback = onAttribution;
|
||||||
adj.Adjust.initSdk(config);
|
config.attributionCallback = _onAttribution;
|
||||||
_log.d('Adjust initialized');
|
adj.Adjust.initSdk(config);
|
||||||
|
_log.d('Adjust initialized');
|
||||||
|
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
_log.d('Ensure Adjust App Token matches your Adjust dashboard');
|
_log.d('Ensure Adjust App Token matches your Adjust dashboard');
|
||||||
|
}
|
||||||
|
} catch (e, st) {
|
||||||
|
_config = null;
|
||||||
|
SdkReminderLog.adjust('Adjust.initSdk 失败: $e\n$st');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,16 +56,34 @@ class AdjustService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void trackEvent(String eventToken) {
|
static void trackEvent(String eventToken) {
|
||||||
adj.Adjust.trackEvent(AdjustEvent(eventToken));
|
if (_config == null) {
|
||||||
_log.d('Track event: $eventToken');
|
SdkReminderLog.adjust(
|
||||||
|
'无法上报事件(token: $eventToken):SDK 未成功初始化。',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
adj.Adjust.trackEvent(AdjustEvent(eventToken));
|
||||||
|
_log.d('Track event: $eventToken');
|
||||||
|
} catch (e) {
|
||||||
|
SdkReminderLog.adjust('trackEvent 异常: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void trackRevenueEvent(
|
static void trackRevenueEvent(
|
||||||
String eventToken, double revenue, String currency) {
|
String eventToken, double revenue, String currency) {
|
||||||
final event = AdjustEvent(eventToken);
|
if (_config == null) {
|
||||||
event.setRevenue(revenue, currency);
|
SdkReminderLog.adjust('无法上报收入事件:SDK 未成功初始化。');
|
||||||
adj.Adjust.trackEvent(event);
|
return;
|
||||||
_log.d('Track revenue event: $eventToken, revenue: $revenue $currency');
|
}
|
||||||
|
try {
|
||||||
|
final event = AdjustEvent(eventToken);
|
||||||
|
event.setRevenue(revenue, currency);
|
||||||
|
adj.Adjust.trackEvent(event);
|
||||||
|
_log.d('Track revenue event: $eventToken, revenue: $revenue $currency');
|
||||||
|
} catch (e) {
|
||||||
|
SdkReminderLog.adjust('trackRevenueEvent 异常: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<AttributionData?> getAttribution() async {
|
static Future<AttributionData?> getAttribution() async {
|
||||||
|
|||||||
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 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import '../config/attribution_config.dart';
|
import '../config/attribution_config.dart';
|
||||||
|
import '../log/sdk_reminder_log.dart';
|
||||||
import 'adjust_service.dart';
|
import 'adjust_service.dart';
|
||||||
import 'facebook_service.dart';
|
import 'facebook_service.dart';
|
||||||
|
|
||||||
@ -63,9 +64,9 @@ abstract class AnalyticsService {
|
|||||||
static void _initAdjust() {
|
static void _initAdjust() {
|
||||||
final adjustConfig = _config?.adjustConfig;
|
final adjustConfig = _config?.adjustConfig;
|
||||||
if (adjustConfig == null) {
|
if (adjustConfig == null) {
|
||||||
if (_config?.debugLogs ?? false) {
|
SdkReminderLog.adjust(
|
||||||
debugPrint('[Analytics] Adjust not configured, skipping');
|
'未配置 appToken,已跳过 Adjust SDK 初始化。',
|
||||||
}
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,10 +92,8 @@ abstract class AnalyticsService {
|
|||||||
if (_config?.debugLogs ?? false) {
|
if (_config?.debugLogs ?? false) {
|
||||||
debugPrint('[Analytics] Adjust initialized: ${adjustConfig.appToken}');
|
debugPrint('[Analytics] Adjust initialized: ${adjustConfig.appToken}');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, st) {
|
||||||
if (_config?.debugLogs ?? false) {
|
SdkReminderLog.adjust('初始化失败: $e\n$st');
|
||||||
debugPrint('[Analytics] Adjust init failed: $e');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,23 +118,20 @@ abstract class AnalyticsService {
|
|||||||
|
|
||||||
static Future<void> _initFacebook() async {
|
static Future<void> _initFacebook() async {
|
||||||
final fbConfig = _config?.facebookConfig;
|
final fbConfig = _config?.facebookConfig;
|
||||||
final packageName = _config?.packageName;
|
if (fbConfig == null) {
|
||||||
if (fbConfig == null || packageName == null) {
|
SdkReminderLog.facebook(
|
||||||
if (_config?.debugLogs ?? false) {
|
'未配置有效 App ID,已跳过 Facebook SDK 初始化。',
|
||||||
debugPrint('[Analytics] Facebook not configured, skipping');
|
);
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await FacebookService.init(fbConfig, packageName: packageName);
|
await FacebookService.init(fbConfig);
|
||||||
if (_config?.debugLogs ?? false) {
|
if (_config?.debugLogs ?? false) {
|
||||||
debugPrint('[Analytics] Facebook initialized: ${fbConfig.appId}');
|
debugPrint('[Analytics] Facebook initialized: ${fbConfig.appId}');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, st) {
|
||||||
if (_config?.debugLogs ?? false) {
|
SdkReminderLog.facebook('初始化失败: $e\n$st');
|
||||||
debugPrint('[Analytics] Facebook init failed: $e');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,9 +142,7 @@ abstract class AnalyticsService {
|
|||||||
debugPrint('[Analytics] Adjust track: $eventToken');
|
debugPrint('[Analytics] Adjust track: $eventToken');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (_config?.debugLogs ?? false) {
|
SdkReminderLog.adjust('trackEvent 失败: $e');
|
||||||
debugPrint('[Analytics] Adjust track error: $e');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,13 +2,13 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import '../api/api_client.dart';
|
import '../api/api_client.dart';
|
||||||
import '../api/proxy_client.dart';
|
import '../api/proxy_client.dart';
|
||||||
import '../config/attribution_config.dart';
|
import '../config/attribution_config.dart';
|
||||||
import '../entities/user_entities.dart';
|
import '../entities/user_entities.dart';
|
||||||
import 'adjust_service.dart';
|
import 'adjust_service.dart';
|
||||||
|
import 'analytics_attribution_callbacks.dart';
|
||||||
import 'user_api.dart';
|
import 'user_api.dart';
|
||||||
|
|
||||||
/// 认证服务回调
|
/// 认证服务回调
|
||||||
@ -46,8 +46,17 @@ abstract class FrameworkAuthService {
|
|||||||
/// 登录完成后的 Future,需鉴权接口应 await 此 Future 再请求
|
/// 登录完成后的 Future,需鉴权接口应 await 此 Future 再请求
|
||||||
static Future<void> get loginComplete => _loginFuture ?? Future<void>.value();
|
static Future<void> get loginComplete => _loginFuture ?? Future<void>.value();
|
||||||
|
|
||||||
/// 初始化认证服务
|
/// 初始化认证服务。
|
||||||
static void init(AuthServiceCallbacks callbacks) {
|
///
|
||||||
|
/// 默认注册 [AnalyticsAttributionCallbacks](与 Adjust 缓存一致)。可传入
|
||||||
|
/// [attributionCallbacks] 覆盖(例如自定义 referer 格式)。
|
||||||
|
static void init(
|
||||||
|
AuthServiceCallbacks callbacks, {
|
||||||
|
AttributionCallbacks? attributionCallbacks,
|
||||||
|
}) {
|
||||||
|
AttributionService.init(
|
||||||
|
attributionCallbacks ?? AnalyticsAttributionCallbacks(),
|
||||||
|
);
|
||||||
_callbacks = callbacks;
|
_callbacks = callbacks;
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
}
|
}
|
||||||
@ -63,7 +72,9 @@ abstract class FrameworkAuthService {
|
|||||||
}) async {
|
}) async {
|
||||||
if (!_isInitialized || _callbacks == null) {
|
if (!_isInitialized || _callbacks == null) {
|
||||||
throw StateError(
|
throw StateError(
|
||||||
'AuthService not initialized. Call AuthService.init(callbacks) first.');
|
'FrameworkAuthService not initialized. '
|
||||||
|
'Call FrameworkAuthService.init(callbacks) first.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (_loginFuture != null) return _loginFuture!;
|
if (_loginFuture != null) return _loginFuture!;
|
||||||
final completer = Completer<void>();
|
final completer = Completer<void>();
|
||||||
@ -118,8 +129,10 @@ abstract class FrameworkAuthService {
|
|||||||
await Future<void>.delayed(Duration(seconds: retryDelaySeconds));
|
await Future<void>.delayed(Duration(seconds: retryDelaySeconds));
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
final appType =
|
final cfg = ApiClient.instance.config;
|
||||||
defaultTargetPlatform == TargetPlatform.iOS ? 'HIOS' : 'HAndroid';
|
final appType = defaultTargetPlatform == TargetPlatform.iOS
|
||||||
|
? cfg.backendAppTypeIOS
|
||||||
|
: cfg.backendAppTypeAndroid;
|
||||||
res = await UserApi.fastLogin(
|
res = await UserApi.fastLogin(
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
sign: sign,
|
sign: sign,
|
||||||
@ -192,21 +205,24 @@ abstract class FrameworkAuthService {
|
|||||||
// 上报 Adjust 归因
|
// 上报 Adjust 归因
|
||||||
final adjustReferer = await AttributionService.getAdjustReferrer();
|
final adjustReferer = await AttributionService.getAdjustReferrer();
|
||||||
if (adjustReferer != null && adjustReferer.isNotEmpty) {
|
if (adjustReferer != null && adjustReferer.isNotEmpty) {
|
||||||
|
final adjustType = defaultTargetPlatform == TargetPlatform.iOS
|
||||||
|
? 'ios_adjust'
|
||||||
|
: 'android_adjust';
|
||||||
try {
|
try {
|
||||||
final rAdjust = await UserApi.referrer(
|
final rAdjust = await UserApi.referrer(
|
||||||
app: config.appId,
|
app: config.appId,
|
||||||
userId: uid,
|
userId: uid,
|
||||||
referer: adjustReferer,
|
referer: adjustReferer,
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
type: 'android_adjust',
|
type: adjustType,
|
||||||
);
|
);
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'[AuthService] referrer(android_adjust): ${rAdjust.isSuccess ? "成功" : "失败"}');
|
'[AuthService] referrer($adjustType): ${rAdjust.isSuccess ? "成功" : "失败"}');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
debugPrint('[AuthService] referrer(android_adjust): 异常 $e');
|
debugPrint('[AuthService] referrer($adjustType): 异常 $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,52 +1,61 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:facebook_app_events/facebook_app_events.dart';
|
import 'package:facebook_app_events/facebook_app_events.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import '../config/attribution_config.dart';
|
import '../config/attribution_config.dart';
|
||||||
import '../log/app_logger.dart';
|
import '../log/app_logger.dart';
|
||||||
|
import '../log/sdk_reminder_log.dart';
|
||||||
|
|
||||||
|
/// 与原生插件约定,勿与宿主包名绑定(参见 Android `ClientProxyFrameworkPlugin`)。
|
||||||
|
const String kFacebookSdkChannelName = 'client_proxy_framework/facebook_sdk';
|
||||||
|
|
||||||
|
/// Facebook App Events:MethodChannel 由框架插件注册;未配置或超时不阻塞应用。
|
||||||
class FacebookService {
|
class FacebookService {
|
||||||
FacebookService._();
|
FacebookService._();
|
||||||
|
|
||||||
static final _log = AppLogger('Facebook');
|
static final _log = AppLogger('Facebook');
|
||||||
static final _fb = FacebookAppEvents();
|
static final _fb = FacebookAppEvents();
|
||||||
static MethodChannel? _channel;
|
static MethodChannel? _channel;
|
||||||
static bool _debugLogs = false;
|
|
||||||
static bool _isInitialized = false;
|
static bool _isInitialized = false;
|
||||||
static void Function()? _onInitialized;
|
static void Function()? _onInitialized;
|
||||||
|
|
||||||
|
static const Duration _nativeInitTimeout = Duration(seconds: 8);
|
||||||
|
|
||||||
static bool get isInitialized => _isInitialized;
|
static bool get isInitialized => _isInitialized;
|
||||||
|
|
||||||
static void setOnInitialized(void Function() callback) {
|
static void setOnInitialized(void Function() callback) {
|
||||||
_onInitialized = callback;
|
_onInitialized = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> init(FacebookConfig config,
|
static Future<void> init(FacebookConfig config) async {
|
||||||
{required String packageName}) async {
|
_log.d('Facebook App Events init appId: ${config.appId}');
|
||||||
_debugLogs = config.debugLogs;
|
|
||||||
|
|
||||||
_log.d('Facebook App Events init with appId: ${config.appId}');
|
|
||||||
_log.d('FacebookService.init start, packageName: $packageName');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_log.d('Creating MethodChannel...');
|
_channel ??= MethodChannel(kFacebookSdkChannelName);
|
||||||
_channel = MethodChannel('$packageName/facebook_sdk');
|
|
||||||
|
|
||||||
_log.d('Setting method call handler...');
|
|
||||||
_channel!.setMethodCallHandler((call) async {
|
_channel!.setMethodCallHandler((call) async {
|
||||||
_log.d('MethodCall received: ${call.method}');
|
_log.d('MethodCall: ${call.method}');
|
||||||
if (call.method == 'onFacebookSdkInitialized') {
|
if (call.method == 'onFacebookSdkInitialized') {
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
_log.d('Facebook App Events initialized (from native callback)');
|
|
||||||
_onInitialized?.call();
|
_onInitialized?.call();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_log.d('Invoking waitForFacebookSdkInit on native...');
|
await _channel!
|
||||||
await _channel!.invokeMethod('waitForFacebookSdkInit');
|
.invokeMethod<void>('waitForFacebookSdkInit')
|
||||||
_log.d('waitForFacebookSdkInit completed');
|
.timeout(_nativeInitTimeout);
|
||||||
|
_log.d('waitForFacebookSdkInit finished');
|
||||||
|
} on TimeoutException catch (e, st) {
|
||||||
|
SdkReminderLog.facebook(
|
||||||
|
'原生初始化超时 ($e),已跳过。请检查是否集成框架 Android/iOS 插件、`AndroidManifest` / Info.plist 中 Facebook 配置是否完整。\n$st',
|
||||||
|
);
|
||||||
|
} on MissingPluginException catch (e) {
|
||||||
|
SdkReminderLog.facebook(
|
||||||
|
'未注册原生插件 ($e)。请执行 flutter clean && flutter pub get 后重新构建。',
|
||||||
|
);
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
_log.w('FacebookService.init failed: $e\n$st');
|
SdkReminderLog.facebook('初始化异常: $e\n$st');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,18 +64,34 @@ class FacebookService {
|
|||||||
String currency = 'USD',
|
String currency = 'USD',
|
||||||
String? orderId,
|
String? orderId,
|
||||||
}) {
|
}) {
|
||||||
_fb.logPurchase(amount: amount, currency: currency);
|
try {
|
||||||
|
_fb.logPurchase(amount: amount, currency: currency);
|
||||||
|
} catch (e) {
|
||||||
|
SdkReminderLog.facebook('logPurchase 失败: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void logSubscribe(String planId) {
|
static void logSubscribe(String planId) {
|
||||||
_fb.logSubscribe(orderId: planId);
|
try {
|
||||||
|
_fb.logSubscribe(orderId: planId);
|
||||||
|
} catch (e) {
|
||||||
|
SdkReminderLog.facebook('logSubscribe 失败: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void logRegister({String registrationMethod = 'device'}) {
|
static void logRegister({String registrationMethod = 'device'}) {
|
||||||
_fb.logCompletedRegistration(registrationMethod: registrationMethod);
|
try {
|
||||||
|
_fb.logCompletedRegistration(registrationMethod: registrationMethod);
|
||||||
|
} catch (e) {
|
||||||
|
SdkReminderLog.facebook('logRegister 失败: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void logEvent(String name, {Map<String, dynamic>? parameters}) {
|
static void logEvent(String name, {Map<String, dynamic>? parameters}) {
|
||||||
_fb.logEvent(name: name, parameters: parameters);
|
try {
|
||||||
|
_fb.logEvent(name: name, parameters: parameters);
|
||||||
|
} catch (e) {
|
||||||
|
SdkReminderLog.facebook('logEvent 失败: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,7 @@ abstract final class PaymentApi {
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
entityFactory: PaymentProductsResponse.fromJson,
|
entityFactory: PaymentProductsResponse.fromJson,
|
||||||
queryParams: {
|
queryParams: {
|
||||||
'app': app ?? 'HAndroid',
|
'app': app ?? ApiClient.instance.config.backendAppTypeIOS,
|
||||||
'pkg': pkg ?? ApiClient.instance.config.packageName,
|
'pkg': pkg ?? ApiClient.instance.config.packageName,
|
||||||
if (shield != null) 'shield': shield,
|
if (shield != null) 'shield': shield,
|
||||||
if (country != null) 'country': country,
|
if (country != null) 'country': country,
|
||||||
|
|||||||
@ -6,6 +6,15 @@ publish_to: 'none'
|
|||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
plugin:
|
||||||
|
platforms:
|
||||||
|
android:
|
||||||
|
package: com.funymee.client_proxy_framework
|
||||||
|
pluginClass: ClientProxyFrameworkPlugin
|
||||||
|
ios:
|
||||||
|
pluginClass: ClientProxyFrameworkPlugin
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user