From c6449734f97a4ccdd879a882e3909dda9217983b Mon Sep 17 00:00:00 2001 From: ivan Date: Thu, 26 Mar 2026 10:39:39 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E8=B7=91=E9=80=9A?= =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dart_tool/package_config.json | 144 ++++ .dart_tool/package_graph.json | 229 +++++- .flutter-plugins-dependencies | 1 + docs/README.md | 707 ++++++++++++++++++ docs/new_app_config_template.md | 186 +++++ docs/payment_flow.md | 247 +++--- docs/sdk_integration_guide.md | 317 ++++++++ docs/skin_app_development_guide.md | 869 ++++++++++++++++++++++ lib/client_proxy_framework.dart | 8 + lib/src/api/proxy_client.dart | 104 ++- lib/src/config/app_config.dart | 80 +- lib/src/config/attribution_config.dart | 90 +++ lib/src/config/default_field_mapping.dart | 10 + lib/src/config/field_mapping.dart | 28 +- lib/src/entities/entities.dart | 5 + lib/src/entities/entity.dart | 12 + lib/src/entities/feedback_entities.dart | 52 ++ lib/src/entities/image_entities.dart | 402 ++++++++++ lib/src/entities/payment_entities.dart | 214 ++++++ lib/src/entities/user_entities.dart | 292 ++++++++ lib/src/log/app_logger.dart | 153 ++++ lib/src/services/adjust_service.dart | 207 ++++++ lib/src/services/analytics_service.dart | 208 ++++++ lib/src/services/auth_service.dart | 245 ++++++ lib/src/services/facebook_service.dart | 72 ++ lib/src/services/feedback_api.dart | 39 + lib/src/services/image_api.dart | 124 ++- lib/src/services/payment_api.dart | 53 +- lib/src/services/payment_service.dart | 362 +++++++++ lib/src/services/user_api.dart | 32 +- pubspec.lock | 192 ++++- pubspec.yaml | 6 + 32 files changed, 5501 insertions(+), 189 deletions(-) create mode 100644 .flutter-plugins-dependencies create mode 100644 docs/README.md create mode 100644 docs/new_app_config_template.md create mode 100644 docs/sdk_integration_guide.md create mode 100644 docs/skin_app_development_guide.md create mode 100644 lib/src/config/attribution_config.dart create mode 100644 lib/src/entities/entities.dart create mode 100644 lib/src/entities/entity.dart create mode 100644 lib/src/entities/feedback_entities.dart create mode 100644 lib/src/entities/image_entities.dart create mode 100644 lib/src/entities/payment_entities.dart create mode 100644 lib/src/entities/user_entities.dart create mode 100644 lib/src/services/adjust_service.dart create mode 100644 lib/src/services/analytics_service.dart create mode 100644 lib/src/services/auth_service.dart create mode 100644 lib/src/services/facebook_service.dart create mode 100644 lib/src/services/feedback_api.dart create mode 100644 lib/src/services/payment_service.dart diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json index a97cfbe..35ee0f4 100644 --- a/.dart_tool/package_config.json +++ b/.dart_tool/package_config.json @@ -1,6 +1,12 @@ { "configVersion": 2, "packages": [ + { + "name": "adjust_sdk", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/adjust_sdk-5.5.1", + "packageUri": "lib/", + "languageVersion": "2.12" + }, { "name": "args", "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/args-2.7.0", @@ -55,12 +61,36 @@ "packageUri": "lib/", "languageVersion": "2.18" }, + { + "name": "facebook_app_events", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/facebook_app_events-0.26.0", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "ffi", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/ffi-2.2.0", + "packageUri": "lib/", + "languageVersion": "3.7" + }, + { + "name": "file", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/file-7.0.1", + "packageUri": "lib/", + "languageVersion": "3.0" + }, { "name": "flutter", "rootUri": "file:///Users/sven/flutter/packages/flutter", "packageUri": "lib/", "languageVersion": "3.9" }, + { + "name": "flutter_web_plugins", + "rootUri": "file:///Users/sven/flutter/packages/flutter_web_plugins", + "packageUri": "lib/", + "languageVersion": "3.9" + }, { "name": "http", "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/http-1.6.0", @@ -73,12 +103,42 @@ "packageUri": "lib/", "languageVersion": "3.4" }, + { + "name": "in_app_purchase", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/in_app_purchase-3.2.3", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "in_app_purchase_android", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/in_app_purchase_android-0.4.0+8", + "packageUri": "lib/", + "languageVersion": "3.9" + }, + { + "name": "in_app_purchase_platform_interface", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/in_app_purchase_platform_interface-1.4.0", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "in_app_purchase_storekit", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/in_app_purchase_storekit-0.4.8+1", + "packageUri": "lib/", + "languageVersion": "3.9" + }, { "name": "js", "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/js-0.7.2", "packageUri": "lib/", "languageVersion": "3.7" }, + { + "name": "json_annotation", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/json_annotation-4.11.0", + "packageUri": "lib/", + "languageVersion": "3.9" + }, { "name": "logger", "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/logger-2.7.0", @@ -103,12 +163,90 @@ "packageUri": "lib/", "languageVersion": "3.4" }, + { + "name": "path_provider_linux", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1", + "packageUri": "lib/", + "languageVersion": "2.19" + }, + { + "name": "path_provider_platform_interface", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/path_provider_platform_interface-2.1.2", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "path_provider_windows", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "platform", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/platform-3.1.6", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "play_install_referrer", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/play_install_referrer-0.5.0", + "packageUri": "lib/", + "languageVersion": "3.7" + }, + { + "name": "plugin_platform_interface", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/plugin_platform_interface-2.1.8", + "packageUri": "lib/", + "languageVersion": "3.0" + }, { "name": "pointycastle", "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/pointycastle-3.9.1", "packageUri": "lib/", "languageVersion": "3.2" }, + { + "name": "shared_preferences", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/shared_preferences-2.5.4", + "packageUri": "lib/", + "languageVersion": "3.9" + }, + { + "name": "shared_preferences_android", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.21", + "packageUri": "lib/", + "languageVersion": "3.9" + }, + { + "name": "shared_preferences_foundation", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6", + "packageUri": "lib/", + "languageVersion": "3.9" + }, + { + "name": "shared_preferences_linux", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "shared_preferences_platform_interface", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/shared_preferences_platform_interface-2.4.1", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "shared_preferences_web", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "shared_preferences_windows", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1", + "packageUri": "lib/", + "languageVersion": "3.3" + }, { "name": "sky_engine", "rootUri": "file:///Users/sven/flutter/bin/cache/pkg/sky_engine", @@ -151,6 +289,12 @@ "packageUri": "lib/", "languageVersion": "3.4" }, + { + "name": "xdg_directories", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/xdg_directories-1.1.0", + "packageUri": "lib/", + "languageVersion": "3.3" + }, { "name": "client_proxy_framework", "rootUri": "../", diff --git a/.dart_tool/package_graph.json b/.dart_tool/package_graph.json index 953be45..20839de 100644 --- a/.dart_tool/package_graph.json +++ b/.dart_tool/package_graph.json @@ -7,14 +7,51 @@ "name": "client_proxy_framework", "version": "1.0.0", "dependencies": [ + "adjust_sdk", "crypto", "encrypt", + "facebook_app_events", "flutter", "http", - "logger" + "in_app_purchase", + "in_app_purchase_android", + "logger", + "play_install_referrer", + "shared_preferences" ], "devDependencies": [] }, + { + "name": "play_install_referrer", + "version": "0.5.0", + "dependencies": [ + "flutter" + ] + }, + { + "name": "in_app_purchase_android", + "version": "0.4.0+8", + "dependencies": [ + "collection", + "flutter", + "in_app_purchase_platform_interface" + ] + }, + { + "name": "facebook_app_events", + "version": "0.26.0", + "dependencies": [ + "flutter" + ] + }, + { + "name": "adjust_sdk", + "version": "5.5.1", + "dependencies": [ + "flutter", + "meta" + ] + }, { "name": "logger", "version": "2.7.0", @@ -65,8 +102,16 @@ ] }, { - "name": "clock", - "version": "1.1.2", + "name": "in_app_purchase_platform_interface", + "version": "1.4.0", + "dependencies": [ + "flutter", + "plugin_platform_interface" + ] + }, + { + "name": "collection", + "version": "1.19.1", "dependencies": [] }, { @@ -74,6 +119,11 @@ "version": "1.17.0", "dependencies": [] }, + { + "name": "clock", + "version": "1.1.2", + "dependencies": [] + }, { "name": "typed_data", "version": "1.4.0", @@ -90,11 +140,6 @@ "js" ] }, - { - "name": "collection", - "version": "1.19.1", - "dependencies": [] - }, { "name": "asn1lib", "version": "1.6.5", @@ -187,6 +232,174 @@ "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", + "dependencies": [ + "meta", + "path" + ] + }, + { + "name": "ffi", + "version": "2.2.0", + "dependencies": [] + }, + { + "name": "path_provider_windows", + "version": "2.3.0", + "dependencies": [ + "ffi", + "flutter", + "path", + "path_provider_platform_interface" + ] + }, + { + "name": "shared_preferences_android", + "version": "2.4.21", + "dependencies": [ + "flutter", + "shared_preferences_platform_interface" + ] } ], "configVersion": 1 diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies new file mode 100644 index 0000000..9e2c851 --- /dev/null +++ b/.flutter-plugins-dependencies @@ -0,0 +1 @@ +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"adjust_sdk","path":"/Users/sven/.pub-cache/hosted/pub.dev/adjust_sdk-5.5.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"facebook_app_events","path":"/Users/sven/.pub-cache/hosted/pub.dev/facebook_app_events-0.26.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"in_app_purchase_storekit","path":"/Users/sven/.pub-cache/hosted/pub.dev/in_app_purchase_storekit-0.4.8+1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/Users/sven/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"adjust_sdk","path":"/Users/sven/.pub-cache/hosted/pub.dev/adjust_sdk-5.5.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"facebook_app_events","path":"/Users/sven/.pub-cache/hosted/pub.dev/facebook_app_events-0.26.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"in_app_purchase_android","path":"/Users/sven/.pub-cache/hosted/pub.dev/in_app_purchase_android-0.4.0+8/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"play_install_referrer","path":"/Users/sven/.pub-cache/hosted/pub.dev/play_install_referrer-0.5.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_android","path":"/Users/sven/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.21/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"in_app_purchase_storekit","path":"/Users/sven/.pub-cache/hosted/pub.dev/in_app_purchase_storekit-0.4.8+1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/Users/sven/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"path_provider_linux","path":"/Users/sven/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"/Users/sven/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false}],"windows":[{"name":"path_provider_windows","path":"/Users/sven/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"/Users/sven/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false}],"web":[{"name":"shared_preferences_web","path":"/Users/sven/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"adjust_sdk","dependencies":[]},{"name":"facebook_app_events","dependencies":[]},{"name":"in_app_purchase","dependencies":["in_app_purchase_android","in_app_purchase_storekit"]},{"name":"in_app_purchase_android","dependencies":[]},{"name":"in_app_purchase_storekit","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"play_install_referrer","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]}],"date_created":"2026-03-25 14:45:33.520021","version":"3.41.4","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..6239937 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,707 @@ +# Client Proxy Framework 使用指南 + +## 1. 框架概述 + +`client_proxy_framework` 是一个通用代理 API 框架,用于 Flutter 移动应用与后端 API 的通信。框架已封装好请求/响应的加密解密、字段映射等逻辑,换皮应用只需配置少量参数即可使用。 + +### 核心特性 + +- **自动加解密**:请求体和响应体自动进行 AES 加密/解密 +- **字段映射**:框架自动将原始字段名转换为 V2 字段名,响应自动转换回来 +- **强类型实体**:返回强类型实体类,开发者直接使用映射后的字段 +- **统一响应**:`EntityResponse` 提供强类型返回值 + +--- + +## 2. 快速开始 + +### 2.1 添加依赖 + +在 `pubspec.yaml` 中添加: + +```yaml +dependencies: + client_proxy_framework: + path: ../client_proxy_framework +``` + +### 2.2 实现配置 + +创建一个类继承 `AppConfig`: + +```dart +import 'package:client_proxy_framework/client_proxy_framework.dart'; + +class MyAppConfig extends AppConfig { + @override + String get appId => 'YourAppId'; + + @override + String get packageName => 'com.yourapp.package'; + + @override + String get aesKey => 'your-16-char-key'; + + @override + String get preBaseUrl => 'https://pre-api.example.com'; + + @override + String get prodBaseUrl => 'https://api.example.com'; + + @override + String get proxyPath => '/quester/defender/summoner'; +} +``` + +### 2.3 初始化 + +在 `main.dart` 中初始化: + +```dart +void main() { + ApiClient.init(MyAppConfig()); + runApp(const MyApp()); +} +``` + +--- + +## 3. 设计理念 + +### 3.1 请求流程 + +``` +调用层 (原始字段) + ↓ +ProxyClient.request() + ↓ +字段映射 (原始 → V2) + ↓ +AES 加密 + ↓ +发送请求 +``` + +### 3.2 响应流程 + +``` +接收响应 + ↓ +AES 解密 + ↓ +字段映射 (V2 → 原始) + ↓ +转换为实体类 + ↓ +调用层 (实体类) +``` + +--- + +## 4. 配置项详解 + +### AppConfig 配置 + +| 配置项 | 必填 | 说明 | +|--------|------|------| +| `appId` | 是 | 应用标识,对应代理请求的 `hero_class` | +| `packageName` | 是 | 应用包名,如 `com.example.app` | +| `aesKey` | 是 | AES 加密密钥,长度需为 16 字符 | +| `preBaseUrl` | 是 | 预发环境域名 | +| `prodBaseUrl` | 是 | 生产环境域名 | +| `proxyPath` | 是 | 代理入口路径 | +| `debugBaseUrlOverride` | 否 | 调试时本地代理地址 | +| `fieldMapping` | 否 | 字段映射表,默认使用 `petsHeroAIFieldMapping` | + +--- + +## 5. API 服务 + +所有 API 方法使用**原始字段名**,返回**强类型实体**。 + +### 5.1 UserApi - 用户相关 + +#### 5.1.1 fastLogin - 设备快速登录 + +```dart +final res = await UserApi.fastLogin( + deviceId: '设备ID', + sign: 'MD5(deviceId)大写', + referer: '归因来源', // 可选 + ch: '渠道号', // 可选 + type: '类型', // 可选 +); + +if (res.isSuccess) { + final loginInfo = res.data!; + final token = loginInfo.userToken; + final credits = loginInfo.credits; + final userId = loginInfo.userId; +} +``` + +**返回实体**: `FastLoginResponse` +- `userToken`: 用户 Token +- `userId`: 用户 ID +- `credits`: 积分 +- `avatar`: 头像 +- `userName`: 用户名 +- `countryCode`: 国家码 +- `isVip`: 是否 VIP +- 等等... + +#### 5.1.2 getCommonInfo - 获取用户通用信息 + +```dart +final res = await UserApi.getCommonInfo( + app: '应用ID', + userId: '用户ID', // 可选 +); + +if (res.isSuccess) { + final info = res.data!; +} +``` + +**返回实体**: `CommonInfoResponse` + +#### 5.1.3 getAccount - 获取账户信息 + +```dart +final res = await UserApi.getAccount( + app: '应用ID', + userId: '用户ID', // 可选 +); + +if (res.isSuccess) { + final account = res.data!; + final credits = account.credits; + final isVip = account.isVip; +} +``` + +**返回实体**: `AccountResponse` +- `credits`: 积分 +- `avatar`: 头像 +- `userName`: 用户名 +- `isVip`: 是否 VIP +- `freeTimes`: 免费次数 + +--- + +### 5.2 PaymentApi - 支付相关 + +#### 5.2.1 getGooglePayActivities - 获取 Android 商品列表 + +```dart +final res = await PaymentApi.getGooglePayActivities( + app: '应用ID', // 可选 + country: '国家', // 可选 +); + +if (res.isSuccess) { + final products = res.data!.productList; + for (final product in products ?? []) { + final id = product.productId; + final price = product.actualAmount; + final bonus = product.bonus; + } +} +``` + +**返回实体**: `PaymentProductsResponse` +- `productList`: 商品列表 + - `productId`: 商品 ID + - `activityId`: 活动 ID + - `actualAmount`: 实际金额 + - `originAmount`: 原价 + - `bonus`: 赠送积分 + - `title`: 标题 + +#### 5.2.2 getPaymentMethods - 获取支付方式列表 + +```dart +final res = await PaymentApi.getPaymentMethods( + activityId: '活动ID', + country: '国家', // 可选 +); + +if (res.isSuccess) { + final methods = res.data!.paymentMethods; + for (final method in methods ?? []) { + final pm = method.paymentMethod; + final name = method.name; + final isRecommend = method.recommend; + } +} +``` + +**返回实体**: `PaymentMethodsResponse` +- `paymentMethods`: 支付方式列表 + - `paymentMethod`: 支付方式 + - `subPaymentMethod`: 子支付方式 + - `name`: 显示名称 + - `icon`: 图标 + - `recommend`: 是否推荐 + +#### 5.2.3 createPayment - 创建支付订单 + +```dart +final res = await PaymentApi.createPayment( + app: '应用ID', + userId: '用户ID', + activityId: '活动ID', + paymentMethod: '支付方式', + paymentType: '支付子类型', // 可选 +); + +if (res.isSuccess) { + final order = res.data!; + final orderId = order.orderId; + final payUrl = order.payUrl; +} +``` + +**返回实体**: `CreatePaymentResponse` +- `orderId`: 订单 ID +- `payUrl`: 支付链接 +- `status`: 状态 + +#### 5.2.4 getOrderDetail - 获取订单详情 + +```dart +final res = await PaymentApi.getOrderDetail( + userId: '用户ID', + orderId: '订单ID', +); + +if (res.isSuccess) { + final order = res.data!; + final status = order.status; +} +``` + +**返回实体**: `OrderDetailResponse` +- `orderId`: 订单 ID +- `status`: 状态 +- `amount`: 金额 + +#### 5.2.5 googlepay - Google Pay 回调 + +```dart +final res = await PaymentApi.googlepay( + signature: '购买签名', + purchaseData: '购买数据', + orderId: '订单ID', + userId: '用户ID', +); + +if (res.isSuccess) { + final result = res.data!; + final status = result.status; +} +``` + +**返回实体**: `GooglePayCallbackResponse` +- `orderId`: 订单 ID +- `status`: 状态 +- `creditsAdded`: 是否已加积分 + +--- + +### 5.3 ImageApi - 图片/视频相关 + +#### 5.3.1 getCategoryList - 获取分类列表 + +```dart +final res = await ImageApi.getCategoryList(); + +if (res.isSuccess) { + final categories = res.data!.categories; +} +``` + +**返回实体**: `CategoryListResponse` +- `categories`: 分类列表 + - `id`: ID + - `name`: 名称 + - `icon`: 图标 + +#### 5.3.2 getImg2VideoTasks - 获取任务列表 + +```dart +final res = await ImageApi.getImg2VideoTasks( + categoryId: 123, // 可选 +); + +if (res.isSuccess) { + final tasks = res.data!.tasks; +} +``` + +**返回实体**: `TasksResponse` + +#### 5.3.3 getProgress - 查询任务进度 + +```dart +final res = await ImageApi.getProgress( + app: '应用ID', + taskId: '任务ID', + userId: '用户ID', // 可选 +); + +if (res.isSuccess) { + final progress = res.data!; + final status = progress.status; + final resultUrl = progress.resultUrl; +} +``` + +**返回实体**: `ProgressResponse` +- `taskId`: 任务 ID +- `status`: 状态 +- `progress`: 进度 (0-100) +- `resultUrl`: 结果 URL + +#### 5.3.4 createTask - 创建任务 + +```dart +final res = await ImageApi.createTask( + userId: '用户ID', + prompt: '提示词', // 可选 + resolution: '1024x1024', // 可选 + imgUrl: '图片URL', // 可选 + allowance: false, // 可选 +); + +if (res.isSuccess) { + final taskId = res.data!.taskId; +} +``` + +**返回实体**: `CreateTaskResponse` +- `taskId`: 任务 ID +- `status`: 状态 + +#### 5.3.5 getMyTasks - 获取我的任务列表 + +```dart +final res = await ImageApi.getMyTasks( + app: '应用ID', + page: '1', // 可选 + pageSize: '10', // 可选 + cursor: '游标', // 可选 +); + +if (res.isSuccess) { + final tasks = res.data!.tasks; + final total = res.data!.total; +} +``` + +**返回实体**: `MyTasksResponse` +- `tasks`: 任务列表 +- `total`: 总数 +- `cursor`: 游标 + +#### 5.3.6 getCreditsPageInfo - 获取积分页面信息 + +```dart +final res = await ImageApi.getCreditsPageInfo( + app: '应用ID', + userId: '用户ID', // 可选 + ch: '渠道', // 可选 +); + +if (res.isSuccess) { + final info = res.data!; + final credits = info.credits; + final freeTimes = info.freeTimes; + final isVip = info.isVip; +} +``` + +**返回实体**: `CreditsPageInfoResponse` +- `credits`: 积分 +- `freeTimes`: 免费次数 +- `isVip`: 是否 VIP +- `vipExpireTime`: VIP 过期时间 + +--- + +### 5.4 FeedbackApi - 举报/反馈相关 + +#### 5.4.1 getUploadPresignedUrl - 获取上传预签名 URL + +```dart +final res = await FeedbackApi.getUploadPresignedUrl( + fileName: 'image.jpg', +); + +if (res.isSuccess) { + final result = res.data!; + final uploadUrl = result.uploadUrl; + final filePath = result.filePath; +} +``` + +**返回实体**: `FeedbackUploadPresignedUrlResponse` +- `uploadUrl`: 上传 URL +- `filePath`: 文件路径 + +#### 5.4.2 submit - 提交反馈 + +```dart +final res = await FeedbackApi.submit( + fileUrls: ['https://...'], + content: '反馈内容', + contentType: 'text/plain', +); + +if (res.isSuccess) { + final success = res.data!.success; +} +``` + +**返回实体**: `SubmitFeedbackResponse` +- `success`: 是否成功 +- `feedbackId`: 反馈 ID + +--- + +## 6. 响应处理 + +### 6.1 EntityResponse + +所有返回实体类的方法都使用 `EntityResponse`: + +```dart +final res = await UserApi.fastLogin(...); + +// 检查成功 +if (res.isSuccess) { + // 访问实体 + final data = res.data!; + final token = data.userToken; +} else { + // 处理错误 + print('Error: ${res.msg}'); +} +``` + +### EntityResponse 属性 + +| 属性 | 类型 | 说明 | +|------|------|------| +| `code` | int | 响应码,0 表示成功 | +| `msg` | String | 响应消息 | +| `data` | T? | 实体数据 | +| `isSuccess` | bool | 便捷属性,code == 0 时为 true | + +--- + +## 7. 设置用户 Token + +登录成功后,调用以下方法设置用户 Token: + +```dart +// 登录成功后 +final res = await UserApi.fastLogin(...); +if (res.isSuccess) { + ApiClient.instance.setUserToken(res.data!.userToken); +} + +// 登出时 +ApiClient.instance.setUserToken(null); +``` + +--- + +## 8. 调试模式 + +框架会自动根据 `kDebugMode` 选择环境: + +- **调试模式**:使用 `preBaseUrl`(或 `debugBaseUrlOverride`) +- **发布模式**:使用 `prodBaseUrl` + +--- + +## 9. 字段映射 + +框架自动处理字段映射,调用层使用原始字段名。 + +### 请求字段映射(原始 → V2) + +| 原始字段 | V2 字段 | +|----------|---------| +| app | sentinel | +| userId | asset | +| deviceId | origin | +| sign | resolution | +| referer | digest | +| activityId | warrior | +| country | vambrace | +| paymentMethod | resource | +| paymentType | ceremony | +| orderId | federation | +| signature | sample | +| purchaseData | merchant | +| taskId | tree | +| prompt | ledger | +| resolution | guild | +| srcImgUrls | commission | +| fileName1 | gateway | +| contentType | pauldron | +| expectedSize | stronghold | + +### 响应字段映射(V2 → 原始) + +| V2 字段 | 原始字段 | +|---------|----------| +| helm | code/响应码 | +| rampart | msg | +| sidekick | data | +| summon | productList | +| renew | paymentMethods | +| reveal | credits | +| reevaluate | userToken | + +--- + +## 10. 代码示例 + +### 完整登录流程 + +```dart +import 'package:client_proxy_framework/client_proxy_framework.dart'; + +class AuthService { + static Future login() async { + final deviceId = await getDeviceId(); + final sign = md5(deviceId).toUpperCase(); + + final res = await UserApi.fastLogin( + deviceId: deviceId, + sign: sign, + referer: 'organic', + ); + + if (res.isSuccess) { + final loginInfo = res.data!; + ApiClient.instance.setUserToken(loginInfo.userToken!); + UserState.setUserId(loginInfo.userId!); + UserState.setCredits(loginInfo.credits ?? 0); + return true; + } + return false; + } +} +``` + +### 完整支付流程 + +```dart +class PaymentService { + // 1. 获取商品列表 + static Future> getProducts() async { + final res = await PaymentApi.getGooglePayActivities(); + return res.data?.productList ?? []; + } + + // 2. 获取支付方式 + static Future> getPaymentMethods(String activityId) async { + final res = await PaymentApi.getPaymentMethods(activityId: activityId); + return res.data?.paymentMethods ?? []; + } + + // 3. 创建订单 + static Future createOrder({ + required String userId, + required String activityId, + required String paymentMethod, + }) async { + final res = await PaymentApi.createPayment( + app: ApiClient.instance.config.appId, + userId: userId, + activityId: activityId, + paymentMethod: paymentMethod, + ); + if (res.isSuccess) { + return res.data?.orderId; + } + return null; + } + + // 4. Google Pay 回调 + static Future verifyGooglePay({ + required String signature, + required String purchaseData, + required String orderId, + required String userId, + }) async { + final res = await PaymentApi.googlepay( + signature: signature, + purchaseData: purchaseData, + orderId: orderId, + userId: userId, + ); + return res.isSuccess; + } +} +``` + +--- + +## 11. 常见问题 + +### Q: 如何修改字段映射? + +A: 覆盖 `AppConfig.fieldMapping`: + +```dart +@override +FieldMapping get fieldMapping => const FieldMapping({ + 'deviceId': 'origin', + 'userId': 'asset', + // ... 其他字段 +}); +``` + +### Q: 响应 data 为空怎么办? + +A: 检查以下几点: +1. `ApiClient.init()` 是否已调用 +2. 网络是否正常 +3. `appId`、`aesKey` 等配置是否正确 + +--- + +## 12. 文件结构 + +``` +lib/ +├── client_proxy_framework.dart # 入口文件 +└── src/ + ├── api/ + │ ├── api_client.dart # 全局客户端 + │ ├── api_crypto.dart # 加解密工具 + │ ├── api_response.dart # 响应对象 + │ └── proxy_client.dart # 代理请求客户端(含实体转换) + ├── config/ + │ ├── app_config.dart # 应用配置抽象类 + │ ├── default_field_mapping.dart # 默认字段映射 + │ └── field_mapping.dart # 字段映射类 + ├── entities/ + │ ├── entity.dart # 实体基类 + │ ├── user_entities.dart # 用户相关实体 + │ ├── payment_entities.dart # 支付相关实体 + │ ├── image_entities.dart # 图片/视频实体 + │ └── feedback_entities.dart # 反馈实体 + ├── log/ + │ └── app_logger.dart # 日志工具 + └── services/ + ├── user_api.dart # 用户 API + ├── payment_api.dart # 支付 API + ├── image_api.dart # 图片/视频 API + └── feedback_api.dart # 反馈 API +``` diff --git a/docs/new_app_config_template.md b/docs/new_app_config_template.md new file mode 100644 index 0000000..b18b878 --- /dev/null +++ b/docs/new_app_config_template.md @@ -0,0 +1,186 @@ +# 新应用配置模板 + +创建新应用时,请填写以下配置信息。 + +*** + +## 一、应用基本信息 + +| 配置项 | 值 | 说明 | +| ------------- | ------------------ | ------------------- | +| 应用名称 | `填写应用名称` | 如:FunyMee AI | +| Android 包名 | `填写 Android 包名` | 如:com.funymeeai.app | +| iOS Bundle ID | `填写 iOS Bundle ID` | 如:com.funymeeai.app | +| 应用 ID(appId) | `填写 appId` | 后台接口参数用 | + +*** + +## 二、服务器配置 + +| 配置项 | 值 | 说明 | +| --------- | ------------- | ------------------------------- | +| 预发布环境 URL | `填写预发布环境 URL` | 如: | +| 生产环境 URL | `填写生产环境 URL` | 如: | +| 代理接口路径 | `填写代理接口路径` | 如:/v1/proxy | + +*** + +## 三、代理请求体字段配置 + +| 配置项 | 值 | 说明 | +| ------------------- | --------- | ----------------------------------------------------- | +| appId 明文 | `填写字段名` | 如:hero\_class | +| 原始path | `填写字段名` | 如:pet\_species | +| "POST"或"GET" | `填写字段名` | 如:power\_level | +| 映射后的Header字段名构造JSON | `填写字段名` | 如:quest\_rank | +| 映射后的URL参数字段名构造JSON | `填写字段名` | 如:battle\_score | +| V2包装后的业务数据 | `填写字段名` | 如:loyalty\_index | +| 噪音字段名列表 | `填写字段名列表` | 如:\['billing\_addr', 'utm\_term', 'cluster\_id', ...] | + +*** + +## 四、V2 包装配置 + +| 配置项 | 值 | 说明 | +| ----------- | --------- | ---------------------------------------------------------- | +| V2 层级 | `填写字段名` | 如:arsenal | +| V2 层级值 | `填写值` | 如:4 | +| V2 层级路径 | `填写路径` | 如:\['vault', 'tome', 'codex', 'grimoire', 'sanctum'] | +| 噪音字段名列表 | `填写字段名列表` | 如:\['roar', 'clash', 'thunder', 'rumble', 'howl', 'growl'] | + +*** + +## 五、安全配置 + +| 配置项 | 值 | 说明 | +| ------ | ---------------- | --------------- | +| AES 密钥 | `填写 16 字节 AES 密钥` | 用于请求加密,AES-128 需要 16 字符 | + +> **密钥长度说明**: +> - AES-128:16 字节(16 个字符) +> - AES-256:32 字节(32 个字符) +> +> 当前框架使用 AES-128-ECB 模式,密钥固定为 16 字符。 + +*** + +## 六、Adjust SDK 配置 + +| 配置项 | 值 | 说明 | +| ----------- | ------------------------ | -------------------------- | +| App Token | `填写 Adjust App Token` | Adjust Dashboard 获取 | +| Environment | `sandbox` / `production` | 测试用 sandbox,正式用 production | + +### Adjust 事件 Token(如有自定义) + +| 事件名称 | 事件 Token | +| --------- | ---------- | +| 首充 | `填写 token` | +| 购买 | `填写 token` | +| 注册 | `填写 token` | +| $5.99 档位 | `填写 token` | +| $9.99 档位 | `填写 token` | +| $19.99 档位 | `填写 token` | +| $49.99 档位 | `填写 token` | +| $99.99 档位 | `填写 token` | + +*** + +## 七、Facebook SDK 配置 + +| 配置项 | 值 | 说明 | +| ------------ | -------------------- | ----------------------------- | +| App ID | `填写 Facebook App ID` | Facebook Developer Console 获取 | +| Client Token | `填写 Client Token` | Facebook Developer Console 获取 | + +> MethodChannel 名称:框架自动使用 `{packageName}/facebook_sdk`,无需配置 + +*** + +## 八、字段映射配置(可选) + +如有特殊字段映射需求请填写,默认使用框架默认映射。 + +| 原始字段名 | 映射后字段名 | 说明 | +| ------ | ------ | ------ | +|
|
|
| + +*** + +## 九、其他配置(如有) + +| 配置项 | 值 | 说明 | +| ----------- | ----------- | ---------- | +| 调试基础 URL 覆盖 | `填写调试用 URL` | 仅调试时使用,可留空 | +|
|
|
| + +*** + +## 十、填写完成后的配置示例 + +```dart +// app_config.dart +class NewAppConfig extends AppConfig { + @override + String get appId => 'your_app_id'; + + @override + String get packageName => 'com.example.app'; + + @override + String get aesKey => 'your_16_char_key'; // AES-128 需要 16 字符 + + @override + String get preBaseUrl => 'https://pre-api.example.com'; + + @override + String get prodBaseUrl => 'https://api.example.com'; + + @override + String get proxyPath => '/v1/proxy'; + + @override + ProxyKeysConfig get proxyKeys => const ProxyKeysConfig( + 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'], + ); + + @override + String get v2LevelField => 'arsenal'; + + @override + int get v2LevelFixedValue => 4; + + @override + List get v2SanctumPath => + const ['vault', 'tome', 'codex', 'grimoire', 'sanctum']; + + @override + List get v2NoiseKeys => + const ['roar', 'clash', 'thunder', 'rumble', 'howl', 'growl']; +} + +// main.dart +await AnalyticsService.init( + AnalyticsConfig( + packageName: 'com.example.app', + adjustConfig: AdjustConfig( + appToken: 'your_adjust_app_token', + environment: AdjustEnv.sandbox, + ), + facebookConfig: FacebookConfig( + appId: 'your_facebook_app_id', + clientToken: 'your_facebook_client_token', + ), + ), +); +``` + +*** + +**填写完成后发送给我,我将根据此配置创建完整的应用代码。** diff --git a/docs/payment_flow.md b/docs/payment_flow.md index 5febf83..0de55d2 100644 --- a/docs/payment_flow.md +++ b/docs/payment_flow.md @@ -14,13 +14,13 @@ ├─ enableThirdPartyPayment === true 且已登录 │ │ │ ├─ getPaymentMethods(activityId) 获取支付方式 - │ ├─ 弹窗选择支付方式(_PaymentMethodDialog) + │ ├─ 弹窗选择支付方式 │ ├─ createPayment 创建订单 │ │ - │ ├─ 若选中的是 Google Pay(resource/ceremony == "GooglePay") + │ ├─ 若选中的是 Google Pay │ │ ├─ 调起 Google Play 内购 - │ │ ├─ 拿到 serverVerificationData - │ │ └─ POST /v1/payment/googlepay 回调验证 + │ │ ├─ 拿到 purchaseData + signature + │ │ └─ googlepay 回调验证 │ │ │ └─ 否则(其他支付方式) │ └─ 打开 payUrl 在外部浏览器完成支付 @@ -46,55 +46,82 @@ ### 4.1 接口 -- **Android**: `GET /v1/payment/getGooglePayActivities` -- **iOS**: `GET /v1/payment/getApplePayActivities` +```dart +// Android +final res = await PaymentApi.getGooglePayActivities( + app: '应用ID', // 可选 + country: '国家', // 可选 +); -### 4.2 商品字段映射 +// iOS +final res = await PaymentApi.getApplePayActivities( + app: '应用ID', // 可选 + country: '国家', // 可选 +); +``` -| 字段(API) | 字段(客户端映射) | 说明 | -|-------------|-------------------|------| -| helm | code / productId | Google Play 商品 ID | -| warrior | activityId | 活动 ID,用于创建订单 | -| guardian | actualAmount | 实际金额 | -| curriculum | originAmount | 原价(带划线)| -| forge | bonus | 赠送积分 | -| glossary | title | 标题 | +### 4.2 返回实体 -### 4.3 代码入口 +```dart +class PaymentProductsResponse { + List? productList; +} -文件:`lib/features/recharge/recharge_screen.dart` -- `_fetchActivities()`: 获取商品列表 -- `_onBuy()`: 用户点击购买入口 +class PaymentProductItem { + String? productId; // 商品 ID (对应 helm) + String? activityId; // 活动 ID (对应 warrior) + String? actualAmount; // 实际金额 (对应 guardian) + String? originAmount; // 原价 (对应 curriculum) + int? bonus; // 赠送积分 (对应 forge) + String? title; // 标题 (对应 glossary) +} +``` --- ## 5. 第三方支付流程 -### 5.1 步骤 +### 5.1 获取支付方式 -1. **获取支付方式**: `POST /v1/payment/get-payment-methods` - - 参数: `warrior` (activityId), `vambrace` (可选,国家) +```dart +final res = await PaymentApi.getPaymentMethods( + activityId: '活动ID', + country: '国家', // 可选 +); -2. **弹窗选择**: 展示支付方式列表(`_PaymentMethodSheet`),包含: - - `resource`: 支付方式(如 GOOGLEPAY) - - `ceremony`: 子支付方式 - - `name`: 显示名称 - - `icon`: 图标 URL - - `recommend`: 是否推荐 +if (res.isSuccess) { + final methods = res.data!.paymentMethods; + // methods 包含: + // - paymentMethod: 支付方式 (如 GOOGLEPAY) + // - subPaymentMethod: 子支付方式 + // - name: 显示名称 + // - icon: 图标 URL + // - recommend: 是否推荐 +} +``` -3. **创建订单**: `POST /v1/payment/createPayment` - - 参数: `sentinel`, `asset`(userId), `warrior`(activityId), `resource`, `ceremony` - - 返回: `federation`(订单ID), `convert`(支付URL) +### 5.2 创建订单 -4. **支付方式分支**: - - **Google Pay**: 调用 `GooglePlayPurchaseService.launchPurchaseAndReturnData()` → 调起内购 → 调用 `PaymentApi.googlepay()` 回调验证 - - **其他方式**: 使用 `url_launcher` 打开 `convert` 支付链接 +```dart +final res = await PaymentApi.createPayment( + app: '应用ID', + userId: '用户ID', + activityId: '活动ID', + paymentMethod: '支付方式', + paymentType: '支付子类型', // 可选 +); -### 5.2 代码位置 +if (res.isSuccess) { + final order = res.data!; + final orderId = order.orderId; // 订单 ID + final payUrl = order.payUrl; // 支付链接 +} +``` -- 入口: `recharge_screen.dart` → `_runThirdPartyPayment()` -- 创建订单: `_createOrderAndOpenUrl()` -- Google Pay 判断: `_isGooglePay()` +### 5.3 支付分支 + +- **Google Pay**: 调起内购 → 获取 purchaseData/signature → 调用 `googlepay` 回调 +- **其他方式**: 打开 `payUrl` 在外部浏览器 --- @@ -102,44 +129,78 @@ 仅 Android,且不经过 `getPaymentMethods` 和 `createPayment`(三方支付关闭时): -1. 调用 `createPayment`(resource=GooglePay, ceremony=GooglePay) -2. 调起 Google Play 内购 -3. 回调验证 +```dart +// 1. 创建订单(直接走谷歌支付) +final createRes = await PaymentApi.createPayment( + app: '应用ID', + userId: '用户ID', + activityId: '活动ID', + paymentMethod: 'GooglePay', + paymentType: 'GooglePay', +); -### 代码位置 +// 2. 调起 Google Play 内购 +final purchaseResult = await GooglePlayPurchaseService.launchPurchaseAndReturnData( + productId: '商品ID', +); -- `recharge_screen.dart` → `_runGooglePay()` +// 3. 回调验证 +if (purchaseResult != null) { + final res = await PaymentApi.googlepay( + signature: purchaseResult.payload.signature, + purchaseData: purchaseResult.payload.purchaseData, + orderId: orderId ?? purchaseResult.orderId, + userId: userId, + ); +} +``` --- ## 7. Google Play 内购统一入口 -### 7.1 核心方法 +### 7.1 调起内购 -`GooglePlayPurchaseService.launchPurchaseAndReturnData(productId)` +```dart +final result = await GooglePlayPurchaseService.launchPurchaseAndReturnData( + productId: '商品ID', +); -- 调起 Google Play 内购 -- 返回 `GooglePayPurchaseResult` 包含: - - `orderId`: Google 订单号 - - `payload.purchaseData`: purchaseData(用于 merchant) - - `payload.signature`: 签名(用于 sample) - - `purchaseDetails`: PurchaseDetails 对象 +if (result != null) { + // result.orderId: Google 订单号 + // result.payload.purchaseData: 用于 merchant + // result.payload.signature: 用于 signature + // result.purchaseDetails: PurchaseDetails 对象 +} +``` ### 7.2 回调验证 -`PaymentApi.googlepay(sample, merchant, federation, asset)` +```dart +final res = await PaymentApi.googlepay( + signature: result.payload.signature, + purchaseData: result.payload.purchaseData, + orderId: orderId, + userId: userId, +); -- `sample`: 签名 -- `merchant`: purchaseData -- `federation`: 订单ID -- `asset`: userId +if (res.isSuccess) { + // 核销订单 + await GooglePlayPurchaseService.completeAndConsumePurchase( + result.purchaseDetails, + ); +} +``` -### 7.3 核销 +### 7.3 响应实体 -`GooglePlayPurchaseService.completeAndConsumePurchase(purchaseDetails)` - -- 执行 `completePurchase` -- 执行 `consumePurchase`(Android) +```dart +class GooglePayCallbackResponse { + String? orderId; + String? status; // SUCCESS / FAILED + bool? creditsAdded; // 是否已加积分 +} +``` --- @@ -161,29 +222,47 @@ 3. 补单成功后刷新账户 -### 8.3 存储映射 - -使用 `SharedPreferences` 存储 `googleOrderId → federation` 映射: -- `saveFederationForGoogleOrderId()` -- `getFederationForGoogleOrderId()` -- `removeFederationForGoogleOrderId()` - --- ## 9. API 汇总 -| 接口 | 方法 | 说明 | -|------|------|------| -| `/v1/payment/getGooglePayActivities` | GET | 获取 Android 商品列表 | -| `/v1/payment/getApplePayActivities` | GET | 获取 iOS 商品列表 | -| `/v1/payment/get-payment-methods` | POST | 获取支付方式列表 | -| `/v1/payment/createPayment` | POST | 创建支付订单 | -| `/v1/payment/getOrderDetail` | GET | 查询订单状态(轮询)| -| `/v1/payment/googlepay` | POST | Google Pay 回调验证 | +| 接口 | 方法 | 返回实体 | +|------|------|----------| +| `getGooglePayActivities` | GET | `PaymentProductsResponse` | +| `getApplePayActivities` | GET | `PaymentProductsResponse` | +| `getPaymentMethods` | POST | `PaymentMethodsResponse` | +| `createPayment` | POST | `CreatePaymentResponse` | +| `getOrderDetail` | GET | `OrderDetailResponse` | +| `googlepay` | POST | `GooglePayCallbackResponse` | --- -## 10. 代码文件位置 +## 10. 字段映射说明 + +框架自动完成字段映射,调用层使用原始字段名。 + +### 请求 → 响应 字段对照 + +| 业务含义 | 请求字段 | 响应字段 | +|----------|----------|----------| +| 应用 ID | app | - | +| 用户 ID | userId | - | +| 活动 ID | activityId | - | +| 支付方式 | paymentMethod | - | +| 支付子类型 | paymentType | - | +| 订单 ID | orderId | orderId | +| 购买签名 | signature | - | +| 购买数据 | purchaseData | - | +| 商品 ID | - | productId | +| 实际金额 | - | actualAmount | +| 原价 | - | originAmount | +| 赠送积分 | - | bonus | +| 支付链接 | - | payUrl | +| 订单状态 | - | status | + +--- + +## 11. 代码文件位置 | 功能 | 文件路径 | |------|----------| @@ -197,14 +276,14 @@ --- -## 11. 常见问题 +## 12. 常见问题 -### 11.1 商品未找到 +### 12.1 商品未找到 -- 原因: 客户端 `helm` (productId) 与 Google Play 后台「产品 ID」不一致 -- 排查: 检查 `docs/google_pay_product_not_found.md` +- 原因: 客户端 `productId` 与 Google Play 后台「产品 ID」不一致 +- 排查: 检查 Play 后台产品 ID 配置 -### 11.2 补单 +### 12.2 补单 - 未确认订单可能不会出现在 `queryPastPurchases` 中 - 应用启动时订阅 `purchaseStream` 接收重新下发 @@ -212,9 +291,9 @@ --- -## 12. 注意事项 +## 13. 注意事项 - 所有 Google Play 内购统一使用 `launchPurchaseAndReturnData()` 方法 - 回调验证成功后必须调用 `completePurchase` + `consumePurchase` -- 支付 URL 打开方式取决于 `createPayment` 返回的 `convert` 字段 +- 支付 URL 打开方式取决于 `createPayment` 返回的 `payUrl` 字段 - 订单状态轮询: 间隔 1/3/7/15/31/63 秒 diff --git a/docs/sdk_integration_guide.md b/docs/sdk_integration_guide.md new file mode 100644 index 0000000..67650be --- /dev/null +++ b/docs/sdk_integration_guide.md @@ -0,0 +1,317 @@ +# 第三方 SDK 集成配置指南 + +本文档说明换皮应用如何配置 Adjust、Facebook App Events 和 Google Play 内购三大 SDK。 + +--- + +## 配置文件 + +请填写以下配置文件(放在项目根目录或 `docs/` 目录下): + +```json +{ + "adjust": { + "app_token": "your_adjust_app_token", + "environment": "sandbox", + "event_tokens": { + "tier_599": "event_token_599", + "tier_999": "event_token_999", + "tier_1999": "event_token_1999", + "tier_4999": "event_token_4999", + "tier_9999": "event_token_9999", + "first_purchase": "event_token_first", + "purchase": "event_token_purchase", + "register": "event_token_register" + } + }, + "facebook": { + "app_id": "your_facebook_app_id", + "client_token": "your_facebook_client_token" + }, + "google_play": { + "product_ids": { + "tier_599": "com.example.product.599", + "tier_999": "com.example.product.999", + "tier_1999": "com.example.product.1999", + "tier_4999": "com.example.product.4999", + "tier_9999": "com.example.product.9999" + } + } +} +``` + +--- + +## 一、Adjust SDK 配置 + +### 1.1 所需信息 + +| 字段 | 说明 | 获取位置 | +|------|------|----------| +| `app_token` | Adjust App Token | Adjust Dashboard → App Settings | +| `environment` | `sandbox`(测试)或 `production`(正式) | 根据构建环境选择 | + +### 1.2 Android 配置 + +> **重要**:Facebook SDK 必须在 Flutter 引擎启动前初始化。以下步骤确保初始化顺序正确: +> 1. `Application.onCreate()` → 初始化 Facebook SDK +> 2. `MainActivity.configureFlutterEngine()` → 缓存 FlutterEngine +> 3. `Application` 通过 `FlutterEngineCache` 通知 Dart 层 + +**1. AndroidManifest.xml** - 添加权限和 Adjust App Token: +```xml + + + + + + + + + + + + + +``` + +**2. res/values/strings.xml** - 添加 Facebook 配置: +```xml + + your_facebook_app_id + your_facebook_client_token + +``` + +**3. app/build.gradle** - 添加 Facebook 依赖: +```groovy +dependencies { + implementation 'com.facebook.android:facebook-core:18.0.0' +} +``` + +**4. 创建 Application 类** - Facebook SDK 初始化(**必须**): + +创建 `android/app/src/main/kotlin//App.kt`: +```kotlin +package com.your.package.name + +import android.app.Application +import android.os.Handler +import android.os.Looper +import com.facebook.FacebookSdk +import com.facebook.LoggingBehavior +import com.facebook.appevents.AppEventsLogger +import io.flutter.embedding.engine.FlutterEngineCache +import io.flutter.plugin.common.MethodChannel + +class App : Application() { + companion object { + const val CHANNEL = "com.your.package.name/facebook_sdk" + const val ENGINE_ID = "main" + } + + override fun onCreate() { + super.onCreate() + + FacebookSdk.sdkInitialize(this) + FacebookSdk.setIsDebugEnabled(true) + FacebookSdk.addLoggingBehavior(LoggingBehavior.APP_EVENTS) + + // 通知 Dart 层 Facebook SDK 已初始化 + Handler(Looper.getMainLooper()).postDelayed({ + val engine = FlutterEngineCache.getInstance().get(ENGINE_ID) + if (engine != null) { + MethodChannel(engine.dartExecutor.binaryMessenger, CHANNEL) + .invokeMethod("onFacebookSdkInitialized", null) + } + }, 100) + } +} +``` + +**5. 更新 MainActivity.kt** - 处理 SDK 初始化回调: +```kotlin +package com.your.package.name + +import android.os.Handler +import android.os.Looper +import com.facebook.appevents.AppEventsLogger +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.embedding.engine.FlutterEngineCache +import io.flutter.plugin.common.MethodChannel + +class MainActivity : FlutterActivity() { + companion object { + const val CHANNEL = "com.your.package.name/facebook_sdk" + const val ENGINE_ID = "main" + } + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + + AppEventsLogger.activateApp(application) + + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) + .setMethodCallHandler { call, _ -> + if (call.method == "waitForFacebookSdkInit") { + AppEventsLogger.activateApp(application) + } + } + + // 缓存 FlutterEngine 供 Application 类使用 + Handler(Looper.getMainLooper()).postDelayed({ + FlutterEngineCache.getInstance().put(ENGINE_ID, flutterEngine) + }, 100) + } +} +``` + +**6. 更新 AndroidManifest.xml** - 使用 Application 类: +```xml + +``` + +**注意**:Facebook SDK 必须在 Flutter 引擎启动前初始化,使用 Application 类 + MethodChannel 确保时序正确。 + +### 1.3 iOS 配置 + +**1. Info.plist** - 添加配置: +```xml +AdjustAppToken +your_adjust_app_token +AdjustEnvironment +sandbox + +FacebookAppID +your_facebook_app_id +FacebookClientToken +your_facebook_client_token +FacebookDisplayName +Your App Name +``` + +--- + +## 二、Facebook App Events 配置 + +### 2.1 所需信息 + +| 字段 | 说明 | 获取位置 | +|------|------|----------| +| `app_id` | Facebook 应用 ID | Facebook Developer Console → Your App → Settings → Basic | +| `client_token` | 客户端口令 | Facebook Developer Console → Your App → Settings → Advanced | + +### 2.2 配置步骤 + +按 **1.1** 和 **1.2** 中的 Android/iOS 配置说明添加 `facebook_app_id` 和 `facebook_client_token`。 + +--- + +## 三、Google Play 内购配置 + +### 3.1 所需信息 + +| 字段 | 说明 | 获取位置 | +|------|------|----------| +| `product_ids` | Google Play Console 商品 ID | Google Play Console → Monetize → Products → Subscriptions/In-app products | + +### 3.2 Android 配置 + +**1. AndroidManifest.xml** - 添加权限: +```xml + +``` + +**2. app/build.gradle** - 添加依赖: +```groovy +dependencies { + implementation 'com.android.billingclient:billing-ktx:6.0.0' +} +``` + +--- + +## 四、框架初始化代码 + +在 `main.dart` 中初始化: + +```dart +import 'package:client_proxy_framework/client_proxy_framework.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // 1. 初始化 API 客户端 + ApiClient.init(YourAppConfig()); + + // 2. 初始化分析服务 + AnalyticsService.init( + AnalyticsConfig( + adjustConfig: AdjustConfig( + appToken: 'your_adjust_app_token', + environment: AdjustEnv.sandbox, // 上线改为 AdjustEnv.production + logLevel: AdjustLogLevel.verbose, + ), + facebookConfig: FacebookConfig( + appId: 'your_facebook_app_id', + debugLogs: true, + ), + debugLogs: true, + ), + ); + + // 3. 启动应用 + runApp(const YourApp()); + + // 4. 初始化认证服务 + await AuthService.init(); +} +``` + +--- + +## 五、事件埋点示例 + +```dart +// Adjust 事件埋点 +AnalyticsService.trackEvent('your_event_token'); + +// Facebook 购买事件 +AnalyticsService.trackPurchase(amount: 9.99, currency: 'USD'); + +// Facebook 注册事件 +AnalyticsService.trackRegister(); + +// Facebook 订阅事件 +AnalyticsService.trackSubscribe('monthly_vip'); +``` + +--- + +## 六、获取 SDK 配置信息的位置 + +### Adjust +- Dashboard: https://dashboard.adjust.com +- App Settings → App Tokens + +### Facebook +- Developer Console: https://developers.facebook.com +- Settings → Basic (App ID) +- Settings → Advanced → Client Token + +### Google Play +- Play Console: https://play.google.com/console +- Monetize → Products → Subscriptions/In-app products diff --git a/docs/skin_app_development_guide.md b/docs/skin_app_development_guide.md new file mode 100644 index 0000000..5c5fc15 --- /dev/null +++ b/docs/skin_app_development_guide.md @@ -0,0 +1,869 @@ +# 换皮应用开发完整流程指南 + +本文档说明如何使用 `client_proxy_framework` 从零创建并完成一个换皮应用。 + +*** + +> **重要说明**:本指南**仅完成数据框架的对接**,包括: +> +> - API 请求封装与字段映射 +> - 第三方 SDK(Adjust、Facebook、Google Play)集成 +> - 用户认证与归因追踪 +> +> **UI 组件需要另行对接**,包括: +> +> - 首页、生成页、个人中心等业务页面 +> - 主题配色、字体、图标等 UI 样式 +> - 业务组件和交互逻辑 +> +> 详见本文档最后一章 [后续开发指引](#后续开发指引)。 + +*** + +## 目录 + +1. [准备阶段:创建项目目录](#1-准备阶段创建项目目录) +2. [填写配置模板](#2-填写配置模板) +3. [创建 Flutter 项目](#3-创建-flutter-项目) +4. [集成框架](#4-集成框架) +5. [配置应用信息](#5-配置应用信息) +6. [配置第三方-sdk](#6-配置第三方-sdk) +7. [实现业务代码](#7-实现业务代码) +8. [调试与测试](#8-调试与测试) +9. [上线准备](#9-上线准备) +10. [后续开发指引](#10-后续开发指引) + +*** + +## 1. 准备阶段:创建项目目录 + +### 1.1 创建项目目录 + +在合适的位置创建项目目录: + +```bash +# 请将 your_app_name 替换为实际的应用名称 +mkdir your_app_name +cd your_app_name +``` + +### 1.2 复制配置模板 + +从框架目录复制配置模板到项目的 `docs` 目录下: + +```bash +mkdir docs +cp ../client_proxy_framework/docs/new_app_config_template.md docs/ +cp ../client_proxy_framework/docs/sdk_integration_guide.md docs/ +``` + +复制后的目录结构: + +``` +your_app_name/ +├── docs/ +│ ├── new_app_config_template.md # 应用配置模板 +│ └── sdk_integration_guide.md # SDK 集成指南 +└── ... +``` + +### 1.3 打开配置模板 + +使用编辑器打开配置模板文件: + +```bash +# 使用 VSCode +code docs/new_app_config_template.md + +# 或使用其他编辑器 +open docs/new_app_config_template.md +``` + +### 1.4 下一步 + +> **请先完成配置模板的填写**,再继续执行后续步骤。 +> +> 配置模板包含以下信息: +> - 应用基本信息(appId、packageName、AES 密钥等) +> - Adjust 配置(App Token、Event Tokens) +> - Facebook 配置(App ID、Client Token) +> - Google Play 配置(商品 ID) + +填写完成后,继续执行第 3 步。 + +*** + +## 2. 填写配置模板 + +请参考 `docs/new_app_config_template.md` 文件,填写以下配置信息: + +### 2.1 应用基本信息 + +| 配置项 | 占位符 | 填写值 | +|-------|-------|-------| +| 应用名称 | `填写应用名称` | | +| Android 包名 | `填写 Android 包名` | | +| iOS Bundle ID | `填写 iOS Bundle ID` | | +| 应用 ID(appId) | `填写 appId` | | + +### 2.2 服务器配置 + +| 配置项 | 占位符 | 填写值 | +|-------|-------|-------| +| 预发布环境 URL | `填写预发布环境 URL` | | +| 生产环境 URL | `填写生产环境 URL` | | +| 代理接口路径 | `填写代理接口路径` | | + +### 2.3 Adjust SDK 配置 + +| 配置项 | 占位符 | 填写值 | +|-------|-------|-------| +| App Token | `填写 Adjust App Token` | | +| Environment | `sandbox` / `production` | | + +### 2.4 Facebook SDK 配置 + +| 配置项 | 占位符 | 填写值 | +|-------|-------|-------| +| App ID | `填写 Facebook App ID` | | +| Client Token | `填写 Client Token` | | + +### 2.5 AES 密钥 + +| 配置项 | 占位符 | 填写值 | +|-------|-------|-------| +| AES 密钥 | `填写 16 字符 AES-128 密钥` | | + +> 框架使用 AES-128-ECB 模式,密钥固定为 16 字符。 + +### 2.6 下一步 + +> **配置填写完成后,继续执行第 3 步。** +> +> 如果配置复杂或有任何疑问,请将填写好的配置发送给我进行确认。 + +*** + +## 3. 创建 Flutter 项目 + +### 3.1 在项目目录下执行 + +确保你在之前创建的项目目录下执行: + +```bash +# 如果不在项目目录下,先进入 +cd your_app_name +``` + +### 3.2 创建 Flutter 项目 + +```bash +# 请将 com.yourcompany 替换为你的组织包名 +flutter create --org com.yourcompany your_app_name +cd your_app_name +``` + +### 3.3 复制配置模板到项目 + +```bash +mkdir docs +cp ../client_proxy_framework/docs/new_app_config_template.md docs/ +cp ../client_proxy_framework/docs/sdk_integration_guide.md docs/ +``` + +### 3.4 下一步 + +> 继续执行 [第 4 步:集成框架](#4-集成框架) + +*** + +## 4. 集成框架 + +### 4.1 添加依赖 + +编辑 `pubspec.yaml`: + +```yaml +dependencies: + flutter: + sdk: flutter + + # 框架依赖 + client_proxy_framework: + path: ../client_proxy_framework # 或使用 pub 发布的版本 + + # 框架需要的额外依赖 + http: ^1.2.2 + encrypt: ^5.0.3 + crypto: ^3.0.3 + logger: ^2.0.2 + shared_preferences: ^2.2.2 + device_info_plus: ^11.1.0 +``` + +### 4.2 目录结构 + +建议按以下结构组织代码: + +``` +lib/ +├── main.dart # 入口文件 +├── app.dart # MaterialApp +├── core/ +│ ├── config/ +│ │ └── app_config.dart # 应用配置类 +│ ├── auth/ +│ │ └── auth_service.dart # 认证服务实现 +│ ├── user/ +│ │ └── user_state.dart # 用户状态管理 +│ └── theme/ +│ ├── app_colors.dart # 主题配色 +│ ├── app_typography.dart # 字体样式 +│ └── app_spacing.dart # 间距常量 +├── features/ +│ ├── home/ # 首页模块 +│ ├── create/ # 生成页模块 +│ ├── profile/ # 个人中心模块 +│ └── recharge/ # 充值页模块 +└── shared/ + ├── tab_selector_scope.dart # Tab 导航作用域 + └── widgets/ # 共享组件 +``` + +*** + +## 5. 配置应用信息 + +### 5.1 创建应用配置类 + +创建 `lib/core/config/app_config.dart`: + +```dart +import 'package:client_proxy_framework/client_proxy_framework.dart'; + +class YourAppConfig implements AppConfig { + @override + String get appId => 'your_app_id'; + + @override + String get packageName => 'com.yourcompany.yourapp'; + + @override + String get aesKey => 'your_16_char_key'; // AES-128 需要 16 字符 + + @override + String get preBaseUrl => 'https://pre-api.example.com'; + + @override + String get prodBaseUrl => 'https://api.example.com'; + + @override + String get proxyPath => '/v1/proxy'; + + @override + FieldMapping? get fieldMapping => DefaultFieldMapping.instance; +} +``` + +### 5.2 创建用户状态管理 + +创建 `lib/core/user/user_state.dart`: + +```dart +import 'package:flutter/foundation.dart'; + +class UserState { + static String? _userId; + static int _credits = 0; + static String? _avatar; + static String? _userName; + static String? _countryCode; + + static String? get userId => _userId; + static int get credits => _credits; + static String? get avatar => _avatar; + static String? get userName => _userName; + static String? get countryCode => _countryCode; + + static void setUserId(String id) => _userId = id; + static void setCredits(int credits) => _credits = credits; + static void setAvatar(String avatar) => _avatar = avatar; + static void setUserName(String name) => _userName = name; + static void setCountryCode(String code) => _countryCode = code; +} +``` + +*** + +## 6. 配置第三方 SDK + +### 6.1 填写配置信息 + +参考 [SDK 集成配置指南](sdk_integration_guide.md),获取并填写以下信息: + +| SDK | 必需信息 | +| ----------- | ---------------------- | +| Adjust | App Token, Environment | +| Facebook | App ID, Client Token | +| Google Play | 无(商品 ID 从后端获取) | + +### 6.2 Android 配置 + +#### 6.2.1 创建 strings.xml + +创建 `android/app/src/main/res/values/strings.xml`: + +```xml + + your_facebook_app_id + your_facebook_client_token + +``` + +#### 6.2.2 更新 AndroidManifest.xml + +在 `` 标签之前添加: + +```xml + + + + + + + +``` + +#### 6.2.3 创建 Application 类 + +创建 `android/app/src/main/kotlin/com/yourcompany/yourapp/App.kt`: + +```kotlin +package com.yourcompany.yourapp + +import android.app.Application +import android.os.Handler +import android.os.Looper +import com.facebook.FacebookSdk +import com.facebook.LoggingBehavior +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.embedding.engine.FlutterEngineCache +import io.flutter.plugin.common.MethodChannel + +class App : Application() { + companion object { + const val CHANNEL = "com.yourcompany.yourapp/facebook_sdk" + const val ENGINE_ID = "main" + } + + override fun onCreate() { + super.onCreate() + + FacebookSdk.sdkInitialize(this) + FacebookSdk.setIsDebugEnabled(true) + FacebookSdk.addLoggingBehavior(LoggingBehavior.APP_EVENTS) + + Handler(Looper.getMainLooper()).postDelayed({ + val engine = FlutterEngineCache.getInstance().get(ENGINE_ID) + if (engine != null) { + MethodChannel(engine.dartExecutor.binaryMessenger, CHANNEL) + .invokeMethod("onFacebookSdkInitialized", null) + } + }, 100) + } +} +``` + +#### 6.2.4 创建 MainActivity + +创建 `android/app/src/main/kotlin/com/yourcompany/yourapp/MainActivity.kt`: + +```kotlin +package com.yourcompany.yourapp + +import android.os.Handler +import android.os.Looper +import com.facebook.appevents.AppEventsLogger +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.embedding.engine.FlutterEngineCache +import io.flutter.plugin.common.MethodChannel + +class MainActivity : FlutterActivity() { + companion object { + const val CHANNEL = "com.yourcompany.yourapp/facebook_sdk" + const val ENGINE_ID = "main" + } + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + + AppEventsLogger.activateApp(application) + + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) + .setMethodCallHandler { call, result -> + when (call.method) { + "waitForFacebookSdkInit" -> { + AppEventsLogger.activateApp(application) + result.success(true) // 必须调用 result.success() 否则 Dart 端会卡住 + } + "onFacebookSdkInitialized" -> { + result.success(true) + } + else -> { + result.notImplemented() + } + } + } + + Handler(Looper.getMainLooper()).postDelayed({ + FlutterEngineCache.getInstance().put(ENGINE_ID, flutterEngine) + }, 100) + } +} +``` + +> **注意**:`result.success()` 必须被调用,否则 Dart 端的 `await` 会一直等待响应,导致应用卡住。 + +#### 6.2.5 更新 AndroidManifest.xml 使用 Application + +```xml + +``` + +### 6.3 iOS 配置 + +#### 6.3.1 更新 Info.plist + +在 `ios/Runner/Info.plist` 中添加: + +```xml +AdjustAppToken +your_adjust_app_token +AdjustEnvironment +sandbox + +FacebookAppID +your_facebook_app_id +FacebookClientToken +your_facebook_client_token +FacebookDisplayName +Your App Name +``` + +*** + +## 7. 实现业务代码 + +### 7.1 创建认证服务实现 + +创建 `lib/core/auth/auth_service.dart`: + +```dart +import 'dart:convert'; +import 'package:client_proxy_framework/client_proxy_framework.dart'; +import 'package:crypto/crypto.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/foundation.dart'; + +import '../user/user_state.dart'; + +/// 归因回调实现 +class AppAttributionCallbacks implements AttributionCallbacks { + @override + Future getReferrer() async { + final attribution = AnalyticsService.getAttribution(); + if (attribution != null) { + return _attributionToJson(attribution); + } + return ''; + } + + String _attributionToJson(AttributionData data) { + final map = { + 'trackerToken': data.trackerToken, + 'trackerName': data.trackerName, + 'network': data.network, + 'campaign': data.campaign, + 'adgroup': data.adgroup, + 'creative': data.creative, + 'clickLabel': data.clickLabel, + 'costType': data.costType, + 'costAmount': _jsonEncodableCostAmount(data.costAmount), + 'costCurrency': data.costCurrency, + 'jsonResponse': data.jsonResponse, + 'fbInstallReferrer': data.fbInstallReferrer, + }; + return base64Encode(utf8.encode(jsonEncode(map))); + } + + Object? _jsonEncodableCostAmount(double? v) { + if (v == null) return null; + if (v.isNaN || !v.isFinite) return v.toString(); + return v; + } + + @override + Future getAdjustReferrer() async => getReferrer(); + + @override + Future getPlatformReferrer() async => null; +} + +/// 认证回调实现 +class AppAuthCallbacks implements AuthServiceCallbacks { + @override + Future getDeviceId() async { + final deviceInfo = DeviceInfoPlugin(); + switch (defaultTargetPlatform) { + case TargetPlatform.android: + final android = await deviceInfo.androidInfo; + return android.id; + case TargetPlatform.iOS: + final ios = await deviceInfo.iosInfo; + return ios.identifierForVendor ?? 'ios-unknown'; + default: + return 'device-${DateTime.now().millisecondsSinceEpoch}'; + } + } + + @override + String computeSign(String deviceId) { + return md5.convert(utf8.encode(deviceId)).toString().toUpperCase(); + } + + @override + void onLoginSuccess(FastLoginResponse data) { + if (data.userId != null) UserState.setUserId(data.userId!); + if (data.credits != null) UserState.setCredits(data.credits!); + if (data.avatar != null) UserState.setAvatar(data.avatar!); + if (data.userName != null) UserState.setUserName(data.userName!); + } + + @override + void onCommonInfoLoaded(CommonInfoResponse data) { + if (data.credits != null) UserState.setCredits(data.credits!); + if (data.avatar != null) UserState.setAvatar(data.avatar!); + if (data.userName != null) UserState.setUserName(data.userName!); + } + + @override + void onLoginFailed(String msg) { + debugPrint('[AuthService] Login failed: $msg'); + } +} + +/// 认证服务 +class AuthService { + static final _authCallbacks = AppAuthCallbacks(); + static final _attributionCallbacks = AppAttributionCallbacks(); + + static Future init() async { + AttributionService.init(_attributionCallbacks); + FrameworkAuthService.init(_authCallbacks); + await FrameworkAuthService.start(); + } + + static Future get loginComplete => FrameworkAuthService.loginComplete; +} +``` + +### 7.2 创建 main.dart + +创建 `lib/main.dart`: + +```dart +import 'package:flutter/material.dart'; +import 'package:client_proxy_framework/client_proxy_framework.dart'; + +import 'app.dart'; +import 'core/auth/auth_service.dart'; +import 'core/config/app_config.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // 1. 初始化 API 客户端 + ApiClient.init(YourAppConfig()); + + // 2. 初始化分析服务 + await AnalyticsService.init( + AnalyticsConfig( + packageName: 'com.yourcompany.yourapp', // 包名,MethodChannel 使用 + adjustConfig: AdjustConfig( + appToken: 'your_adjust_app_token', + environment: AdjustEnv.sandbox, // 上线改为 production + ), + facebookConfig: FacebookConfig( + appId: 'your_facebook_app_id', + clientToken: 'your_facebook_client_token', + ), + ), + ); + + // 3. 提前获取归因 + await AnalyticsService.initAttribution(); + + // 4. 启动应用 + runApp(const App()); + + // 5. 初始化认证 + AuthService.init(); +} +``` + +### 7.3 创建 App 组件 + +创建 `lib/app.dart`: + +```dart +import 'package:flutter/material.dart'; + +import 'home_screen.dart'; + +class App extends StatelessWidget { + const App({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Your App Name', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), + useMaterial3: true, + ), + home: const HomeScreen(), + ); + } +} +``` + +*** + +## 8. 调试与测试 + +### 8.1 运行调试版本 + +```bash +flutter run +``` + +### 8.2 检查日志 + +运行时应看到以下日志: + +``` +D/Adjust (xxxxx): Adjust SDK started +I/flutter (xxxxx): [AuthService] start: deviceId=xxx +I/flutter (xxxxx): Facebook App Events initialized (from native callback) +``` + +### 8.3 常见问题排查 + +| 问题 | 解决方案 | +| --------------------------------- | ---------------------------------------- | +| Adjust 日志显示 "SANDBOX" | 正常,测试环境会显示 | +| Facebook 报错 "must be initialized" | 检查 AndroidManifest.xml 和 Application 类配置 | +| 归因未上报 | 检查 initAttribution() 是否在 main() 中调用 | + +*** + +## 9. 上线准备 + +### 9.1 修改环境配置 + +#### main.dart + +```dart +// Adjust 改为 production +adjustConfig: AdjustConfig( + appToken: 'your_production_app_token', + environment: AdjustEnv.production, // 修改这里 +), + +// Facebook 关闭调试日志 +facebookConfig: FacebookConfig( + appId: 'your_facebook_app_id', + clientToken: 'your_facebook_client_token', + debugLogs: false, // 关闭调试日志 +), +``` + +#### Android + +```xml + + +``` + +#### iOS + +```xml + +AdjustEnvironment +production +``` + +### 9.2 构建发布版本 + +```bash +# Android +flutter build apk --release +flutter build appbundle --release + +# iOS +flutter build ios --release +``` + +### 9.3 检查清单 + +- [ ] Adjust App Token 已替换为生产环境 +- [ ] Adjust Environment 已改为 production +- [ ] Facebook 调试日志已关闭 +- [ ] Facebook Client Token 已更新(生产环境) +- [ ] 应用名称和包名正确 +- [ ] 所有第三方 SDK 配置完成 + +*** + +## 附录:框架 API 快速参考 + +### 分析服务 + +```dart +// 初始化 +await AnalyticsService.init(config); + +// 埋点 +AnalyticsService.trackEvent('event_token'); +AnalyticsService.trackPurchase(amount: 9.99, currency: 'USD'); +AnalyticsService.trackRegister(); +AnalyticsService.trackSubscribe('monthly_vip'); + +// 获取归因 +final attribution = AnalyticsService.getAttribution(); +``` + +### 认证服务 + +```dart +// 获取登录完成状态 +await AuthService.loginComplete; + +// 发起登录 +await AuthService.init(); +``` + +### API 请求 + +```dart +// 获取代理客户端 +final proxy = ApiClient.instance.proxy; + +// GET 请求 +final res = await proxy.request( + path: '/v1/endpoint', + method: 'GET', +); + +// POST 请求 +final res = await proxy.request( + path: '/v1/endpoint', + method: 'POST', + body: {'key': 'value'}, +); + +// 请求带字段映射的实体 +final res = await proxy.requestEntity( + path: '/v1/entity', + method: 'GET', + entityFactory: YourEntity.fromJson, +); +``` + +*** + +## 10. 后续开发指引 + +完成本指南后,数据框架已就绪。接下来需要开发 **UI 业务层**: + +### 10.1 框架提供的 API 服务 + +| 服务类 | 用途 | +| ------------- | ------------- | +| `ImageApi` | 图片/视频生成相关 API | +| `PaymentApi` | 支付相关 API | +| `FeedbackApi` | 举报/反馈相关 API | +| `UserApi` | 用户账户相关 API | + +### 10.2 框架提供的实体类 + +参考 `client_proxy_framework/lib/src/entities/` 目录: + +| 实体类 | 用途 | +| ------------------ | ------------ | +| `UserEntities` | 用户、登录、通用信息响应 | +| `ImageEntities` | 分类、任务、生成结果 | +| `PaymentEntities` | 商品、订单、支付结果 | +| `FeedbackEntities` | 举报反馈相关 | + +### 10.3 典型的 UI 开发流程 + +```dart +// 1. 在业务页面中使用框架 API +class HomeScreen extends StatefulWidget { + @override + void initState() { + super.initState(); + _loadData(); + } + + Future _loadData() async { + // 等待登录完成(如果需要) + await AuthService.loginComplete; + + // 调用框架 API 获取数据 + final res = await ImageApi.getCategoryList(); + if (res.isSuccess) { + // 处理数据... + } + } +} + +// 2. 使用 UserState 管理状态 +UserState.setCredits(credits); +UserState.setUserId(userId); + +// 3. 调用分析服务埋点 +AnalyticsService.trackPurchase(amount: 9.99, currency: 'USD'); +AnalyticsService.trackEvent('your_event_token'); +``` + +### 10.4 建议的 UI 开发顺序 + +1. **首页(HomeScreen)** - 展示分类和任务卡片 +2. **生成页(CreateScreen)** - 创建生成任务 +3. **个人中心(ProfileScreen)** - 用户信息和设置 +4. **充值页(RechargeScreen)** - 内购支付流程 + +### 10.5 参考实现 + +参考 `app_client_1` 项目中的 UI 实现: + +- `lib/features/home/home_screen.dart` - 首页实现 +- `lib/shared/widgets/` - 共享组件(按钮、卡片等) +- `lib/core/theme/` - 主题配置 + diff --git a/lib/client_proxy_framework.dart b/lib/client_proxy_framework.dart index ca35411..304a8b6 100644 --- a/lib/client_proxy_framework.dart +++ b/lib/client_proxy_framework.dart @@ -11,9 +11,17 @@ export 'src/api/api_crypto.dart'; export 'src/api/api_response.dart'; export 'src/api/proxy_client.dart'; export 'src/config/app_config.dart'; +export 'src/config/attribution_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/services/adjust_service.dart'; +export 'src/services/analytics_service.dart'; +export 'src/services/auth_service.dart'; +export 'src/services/facebook_service.dart'; +export 'src/services/feedback_api.dart'; export 'src/services/image_api.dart'; export 'src/services/payment_api.dart'; +export 'src/services/payment_service.dart'; export 'src/services/user_api.dart'; diff --git a/lib/src/api/proxy_client.dart b/lib/src/api/proxy_client.dart index c02ebc9..1c0142e 100644 --- a/lib/src/api/proxy_client.dart +++ b/lib/src/api/proxy_client.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import '../config/app_config.dart'; +import '../entities/entity.dart'; import '../log/app_logger.dart'; import 'api_crypto.dart'; import 'api_response.dart'; @@ -51,7 +52,6 @@ void _log(Object? msg) { _logLong(str); } -/// 从 json 中按路径取嵌套值,如 ['vault','tome','codex','grimoire','sanctum'] dynamic _getByPath(Map json, List path) { dynamic current = json; for (final key in path) { @@ -61,26 +61,27 @@ dynamic _getByPath(Map json, List path) { return current; } +/// 实体工厂函数类型 +typedef EntityFactory = T Function(Map); + /// 代理请求客户端 class ProxyClient { ProxyClient({ required this.config, this.baseUrlOverride, this.userToken, - }) : _crypto = ApiCrypto(aesKey: config.aesKey); + }) : _crypto = ApiCrypto(aesKey: config.aesKey); final AppConfig config; final String? baseUrlOverride; final ApiCrypto _crypto; - /// 用户 Token,登录后由 [ApiClient.setUserToken] 设置 String? userToken; String get _baseUrl => baseUrlOverride ?? config.baseUrl; Map _buildV2Wrapper(Map sanctum) { final result = Map.from(config.v2FixedValues); - // 构建嵌套:vault.tome.codex.grimoire.sanctum Map current = result; final path = config.v2SanctumPath; for (var i = 0; i < path.length - 1; i++) { @@ -96,9 +97,9 @@ class ProxyClient { return result; } - /// 发送代理请求 + /// 发送代理请求(返回原始字段的 Map) /// - /// [headers]、[queryParams]、[body] 使用**原始字段名**(canonical), + /// [headers]、[queryParams]、[body] 使用**原始字段名**, /// 框架会按 [AppConfig.fieldMapping] 转为 V2 字段名后发送。 /// 响应 data 会自动从 V2 转回原始字段名。 Future request({ @@ -113,10 +114,10 @@ class ProxyClient { var headersMap = Map.from(headers ?? {}); if (config.packageName.isNotEmpty) { - headersMap['pkg'] = config.packageName; + headersMap[mapping.headerPackageNameField] = config.packageName; } if (userToken != null && userToken!.isNotEmpty) { - headersMap['User_token'] = userToken!; + headersMap[mapping.headerUserTokenField] = userToken!; } headersMap = mapping.mapRequest(headersMap); @@ -135,15 +136,15 @@ class ProxyClient { final logStr = '========== 原始入参 ===========\npath: $path\nmethod: $method\nqueryParams: $paramsEncoded\nbody(sanctum): ${jsonEncode(sanctum)}'; - _log(logStr); + logWithEmbeddedJson(logStr); final proxyBody = { - pk.heroClass: config.appId, - pk.petSpecies: _crypto.encrypt(path), - pk.powerLevel: _crypto.encrypt(method), - pk.questRank: _crypto.encrypt(headersEncoded), - pk.battleScore: _crypto.encrypt(paramsEncoded), - pk.loyaltyIndex: _crypto.encrypt(v2BodyEncoded), + pk.appIdField: config.appName, + pk.pathField: _crypto.encrypt(path), + pk.methodField: _crypto.encrypt(method), + pk.headerField: _crypto.encrypt(headersEncoded), + pk.paramsField: _crypto.encrypt(paramsEncoded), + pk.bodyField: _crypto.encrypt(v2BodyEncoded), }; for (final key in pk.noiseKeys) { @@ -152,6 +153,7 @@ class ProxyClient { final url = '$_baseUrl${config.proxyPath}'; + logWithEmbeddedJson('========== 实际请求体 ===========\n${jsonEncode(proxyBody)}'); final response = await http.post( Uri.parse(url), headers: {'Content-Type': 'application/json'}, @@ -161,25 +163,66 @@ class ProxyClient { return _parseResponse(response); } + /// 发送代理请求并返回实体 + /// + /// [headers]、[queryParams]、[body] 使用**原始字段名**。 + /// [entityFactory] 用于将映射后的 data 转换为实体对象。 + Future> requestEntity({ + required String path, + required String method, + required EntityFactory entityFactory, + Map? headers, + Map? queryParams, + Map? body, + }) async { + final response = await request( + path: path, + method: method, + headers: headers, + queryParams: queryParams, + body: body, + ); + + if (response.isSuccess && response.data is Map) { + final entity = entityFactory(response.data as Map); + return EntityResponse( + code: response.code, + msg: response.msg, + data: entity, + ); + } + + return EntityResponse( + code: response.code, + msg: response.msg, + data: null, + ); + } + ApiResponse _parseResponse(http.Response response) { try { final decrypted = _crypto.decrypt(response.body); final json = jsonDecode(decrypted) as Map; - _log('========== 响应 ===========\n${jsonEncode(json)}'); + logWithEmbeddedJson('========== 响应 ===========\n${jsonEncode(json)}'); + final mapping = config.fieldMapping; final sanctum = _getByPath(json, config.v2SanctumPath); + final codeField = mapping.responseCodeField; + final msgField = mapping.responseMsgField; + final dataField = mapping.responseDataField; + final code = sanctum is Map - ? (sanctum[config.responseCodeField] as int? ?? -1) - : (json[config.responseCodeField] as int? ?? -1); + ? (sanctum[codeField] as int? ?? -1) + : (json[codeField] as int? ?? -1); final msg = sanctum is Map - ? (sanctum[config.responseMsgField] as String? ?? '') - : (json[config.responseMsgField] as String? ?? ''); + ? (sanctum[msgField] as String? ?? '') + : (json[msgField] as String? ?? ''); var data = sanctum is Map - ? sanctum[config.responseDataField] - : json[config.responseDataField]; + ? sanctum[dataField] + : json[dataField]; if (data is Map) { - data = config.fieldMapping.mapResponse(data); + data = mapping.mapResponse(data); } return ApiResponse(code: code, msg: msg, data: data); @@ -188,3 +231,18 @@ class ProxyClient { } } } + +/// 带泛型实体的响应 +class EntityResponse { + EntityResponse({ + required this.code, + this.msg = '', + this.data, + }); + + final int code; + final String msg; + final T? data; + + bool get isSuccess => code == 0; +} diff --git a/lib/src/config/app_config.dart b/lib/src/config/app_config.dart index 343d951..cece1b8 100644 --- a/lib/src/config/app_config.dart +++ b/lib/src/config/app_config.dart @@ -6,28 +6,54 @@ import 'field_mapping.dart'; /// 代理请求体字段名配置 class ProxyKeysConfig { const ProxyKeysConfig({ - this.heroClass = 'hero_class', - this.petSpecies = 'pet_species', - this.powerLevel = 'power_level', - this.questRank = 'quest_rank', - this.battleScore = 'battle_score', - this.loyaltyIndex = 'loyalty_index', + /// appId 明文字段名 + this.appIdField = 'hero_class', + + /// 原始 path 字段名 + this.pathField = 'pet_species', + + /// "POST" 或 "GET" 字段名 + this.methodField = 'power_level', + + /// 映射后的 Header 字段名(构造 JSON) + this.headerField = 'quest_rank', + + /// 映射后的 URL 参数字段名(构造 JSON) + this.paramsField = 'battle_score', + + /// V2 包装后的业务数据字段名 + this.bodyField = 'loyalty_index', + + /// 噪音字段名列表 this.noiseKeys = const [ 'billing_addr', 'utm_term', 'cluster_id', 'lsn_value', 'accuracy_val', - 'dir_path', + 'dir_path' ], }); - final String heroClass; - final String petSpecies; - final String powerLevel; - final String questRank; - final String battleScore; - final String loyaltyIndex; + /// appId 明文字段名 + final String appIdField; + + /// 原始 path 字段名 + final String pathField; + + /// "POST" 或 "GET" 字段名 + final String methodField; + + /// 映射后的 Header 字段名(构造 JSON) + final String headerField; + + /// 映射后的 URL 参数字段名(构造 JSON) + final String paramsField; + + /// V2 包装后的业务数据字段名 + final String bodyField; + + /// 噪音字段名列表 final List noiseKeys; } @@ -36,6 +62,9 @@ class ProxyKeysConfig { abstract class AppConfig { AppConfig(); + /// 应用名称 + String get appName; + /// 应用标识(代理请求 hero_class) String get appId; @@ -69,7 +98,7 @@ abstract class AppConfig { /// 代理请求体字段名 ProxyKeysConfig get proxyKeys => const ProxyKeysConfig(); - /// V2 包装:sanctum 的嵌套路径,如 ['vault','tome','codex','grimoire','sanctum'] + /// V2 层级路径 List get v2SanctumPath => const ['vault', 'tome', 'codex', 'grimoire', 'sanctum']; @@ -77,23 +106,16 @@ abstract class AppConfig { List get v2NoiseKeys => const ['roar', 'clash', 'thunder', 'rumble', 'howl', 'growl']; - /// V2 包装:固定值(如 arsenal: 4) - Map get v2FixedValues => const {'arsenal': 4}; + /// V2 层级字段名 + String get v2LevelField => 'arsenal'; - /// 响应 code 字段名 - String get responseCodeField => 'helm'; + /// V2 层级固定值 + int get v2LevelFixedValue => 4; - /// 响应 msg 字段名 - String get responseMsgField => 'rampart'; - - /// 响应 data 字段名 - String get responseDataField => 'sidekick'; - - /// 请求头:包名字段名 - String get headerPackageNameField => 'portal'; - - /// 请求头:用户 token 字段名 - String get headerUserTokenField => 'knight'; + /// V2 层级固定值 + Map get v2FixedValues { + return {v2LevelField: v2LevelFixedValue}; + } /// 字段映射表(原始字段 ↔ V2 字段) /// 换皮应用若后端 V2 字段名不同,覆盖此方法返回自己的映射 diff --git a/lib/src/config/attribution_config.dart b/lib/src/config/attribution_config.dart new file mode 100644 index 0000000..2a32f6a --- /dev/null +++ b/lib/src/config/attribution_config.dart @@ -0,0 +1,90 @@ +import 'dart:async'; + +/// Adjust SDK 环境 +enum AdjustEnv { + sandbox, + production, +} + +/// Adjust SDK 日志级别 +enum AdjustLogLevel { + off, + verbose, +} + +/// Adjust SDK 配置 +class AdjustConfig { + const AdjustConfig({ + required this.appToken, + this.environment = AdjustEnv.production, + this.logLevel = AdjustLogLevel.off, + this.fbAppId, + }); + + final String appToken; + final AdjustEnv environment; + final AdjustLogLevel logLevel; + final String? fbAppId; +} + +/// Facebook SDK 配置 +class FacebookConfig { + const FacebookConfig({ + required this.appId, + this.clientToken, + this.debugLogs = false, + }); + + final String appId; + final String? clientToken; + final bool debugLogs; +} + +/// 原生平台归因配置 +class PlatformAttributionConfig { + const PlatformAttributionConfig({ + this.enabled = true, + }); + + final bool enabled; +} + +/// 归因回调接口 +abstract class AttributionCallbacks { + Future getReferrer(); + Future getAdjustReferrer(); + Future getPlatformReferrer(); +} + +/// 默认归因实现 +class DefaultAttributionCallbacks implements AttributionCallbacks { + @override + Future getReferrer() async => ''; + + @override + Future getAdjustReferrer() async => null; + + @override + Future getPlatformReferrer() async => null; +} + +/// 归因服务 +abstract class AttributionService { + static AttributionCallbacks? _callbacks; + + static void init(AttributionCallbacks callbacks) { + _callbacks = callbacks; + } + + static AttributionCallbacks get callbacks { + if (_callbacks == null) { + throw StateError('AttributionService not initialized'); + } + return _callbacks!; + } + + static Future getReferrer() => callbacks.getReferrer(); + static Future getAdjustReferrer() => callbacks.getAdjustReferrer(); + static Future getPlatformReferrer() => + callbacks.getPlatformReferrer(); +} diff --git a/lib/src/config/default_field_mapping.dart b/lib/src/config/default_field_mapping.dart index 7f2e102..028f367 100644 --- a/lib/src/config/default_field_mapping.dart +++ b/lib/src/config/default_field_mapping.dart @@ -9,6 +9,11 @@ const FieldMapping petsHeroAIFieldMapping = FieldMapping({ 'pkg': 'portal', 'User_token': 'knight', + // === 响应字段 === + 'code': 'helm', + 'msg': 'rampart', + 'data': 'sidekick', + // === 通用 query === 'app': 'sentinel', 'userId': 'asset', @@ -87,4 +92,9 @@ const FieldMapping petsHeroAIFieldMapping = FieldMapping({ 'img': 'revenue', 'url': 'digitize', 'launchImgUrl': 'launchImgUrl', + + // === 反馈 === + 'fileName': 'layer', + 'fileUrls': 'inventory', + 'content': 'cloak', }); diff --git a/lib/src/config/field_mapping.dart b/lib/src/config/field_mapping.dart index 42d5cc2..46ec3ed 100644 --- a/lib/src/config/field_mapping.dart +++ b/lib/src/config/field_mapping.dart @@ -16,12 +16,18 @@ class FieldMapping { ); } + /// 获取映射后的字段名,如果不存在则返回原始字段名 + String mapField(String original) => mapping[original] ?? original; + + /// 获取反向映射的字段名(V2 → 原始),如果不存在则返回原始字段名 + String reverseMapField(String v2) => _inverse[v2] ?? v2; + /// 将 Map 的 key 从原始名转为后端名(请求) Map mapRequest(Map input) { if (input.isEmpty) return input; final out = {}; for (final e in input.entries) { - final key = mapping[e.key] ?? e.key; + final key = mapField(e.key); out[key] = _mapRequestValue(e.value); } return out; @@ -40,10 +46,9 @@ class FieldMapping { /// 将 Map 的 key 从后端名转为原始名(响应) Map mapResponse(Map input) { if (input.isEmpty) return input; - final inv = _inverse; final out = {}; for (final e in input.entries) { - final key = inv[e.key] ?? e.key; + final key = reverseMapField(e.key); out[key] = _mapResponseValue(e.value); } return out; @@ -58,4 +63,21 @@ class FieldMapping { } return value; } + + // === 响应字段 === + /// 响应 code 字段名 + String get responseCodeField => mapField('code'); + + /// 响应 msg 字段名 + String get responseMsgField => mapField('msg'); + + /// 响应 data 字段名 + String get responseDataField => mapField('data'); + + // === 请求头字段 === + /// 请求头:包名字段名 + String get headerPackageNameField => mapField('pkg'); + + /// 请求头:用户 token 字段名 + String get headerUserTokenField => mapField('User_token'); } diff --git a/lib/src/entities/entities.dart b/lib/src/entities/entities.dart new file mode 100644 index 0000000..2a3ef54 --- /dev/null +++ b/lib/src/entities/entities.dart @@ -0,0 +1,5 @@ +export 'entity.dart'; +export 'feedback_entities.dart'; +export 'image_entities.dart'; +export 'payment_entities.dart'; +export 'user_entities.dart'; diff --git a/lib/src/entities/entity.dart b/lib/src/entities/entity.dart new file mode 100644 index 0000000..13d2e4e --- /dev/null +++ b/lib/src/entities/entity.dart @@ -0,0 +1,12 @@ +/// 实体基类 +abstract class Entity { + Entity(); + + /// 从 Map 创建实体 + factory Entity.fromJson(Map json) { + throw UnimplementedError('Subclass must implement fromJson'); + } + + /// 转换为 Map + Map toJson(); +} diff --git a/lib/src/entities/feedback_entities.dart b/lib/src/entities/feedback_entities.dart new file mode 100644 index 0000000..a493755 --- /dev/null +++ b/lib/src/entities/feedback_entities.dart @@ -0,0 +1,52 @@ +import 'entity.dart'; + +/// 反馈预签名上传 URL 响应 +class FeedbackUploadPresignedUrlResponse extends Entity { + FeedbackUploadPresignedUrlResponse({ + this.uploadUrl, + this.filePath, + }); + + final String? uploadUrl; + final String? filePath; + + @override + factory FeedbackUploadPresignedUrlResponse.fromJson( + Map json) { + return FeedbackUploadPresignedUrlResponse( + uploadUrl: json['uploadUrl'] as String?, + filePath: json['filePath'] as String?, + ); + } + + @override + Map toJson() => { + 'uploadUrl': uploadUrl, + 'filePath': filePath, + }; +} + +/// 提交反馈响应 +class SubmitFeedbackResponse extends Entity { + SubmitFeedbackResponse({ + this.success, + this.feedbackId, + }); + + final bool? success; + final String? feedbackId; + + @override + factory SubmitFeedbackResponse.fromJson(Map json) { + return SubmitFeedbackResponse( + success: json['success'] as bool?, + feedbackId: json['feedbackId'] as String?, + ); + } + + @override + Map toJson() => { + 'success': success, + 'feedbackId': feedbackId, + }; +} diff --git a/lib/src/entities/image_entities.dart b/lib/src/entities/image_entities.dart new file mode 100644 index 0000000..a28671d --- /dev/null +++ b/lib/src/entities/image_entities.dart @@ -0,0 +1,402 @@ +import 'entity.dart'; + +/// 分类项 +class CategoryItem extends Entity { + CategoryItem({ + this.id, + this.name, + this.icon, + }); + + final int? id; + final String? name; + final String? icon; + + @override + factory CategoryItem.fromJson(Map json) { + return CategoryItem( + id: json['id'] as int?, + name: json['name'] as String?, + icon: json['icon'] as String?, + ); + } + + @override + Map toJson() => { + 'id': id, + 'name': name, + 'icon': icon, + }; +} + +/// 分类列表响应 +class CategoryListResponse extends Entity { + CategoryListResponse({ + this.categories, + }); + + final List? categories; + + @override + factory CategoryListResponse.fromJson(Map json) { + final list = json['categories'] as List?; + return CategoryListResponse( + categories: list + ?.map((e) => CategoryItem.fromJson(e as Map)) + .toList(), + ); + } + + @override + Map toJson() => { + 'categories': categories?.map((e) => e.toJson()).toList(), + }; +} + +/// 任务项 +class TaskItem extends Entity { + TaskItem({ + this.id, + this.name, + this.imageUrl, + this.categoryId, + }); + + final String? id; + final String? name; + final String? imageUrl; + final int? categoryId; + + @override + factory TaskItem.fromJson(Map json) { + return TaskItem( + id: json['id'] as String?, + name: json['name'] as String?, + imageUrl: json['imageUrl'] as String?, + categoryId: json['categoryId'] as int?, + ); + } + + @override + Map toJson() => { + 'id': id, + 'name': name, + 'imageUrl': imageUrl, + 'categoryId': categoryId, + }; +} + +/// 任务列表响应 +class TasksResponse extends Entity { + TasksResponse({ + this.tasks, + }); + + final List? tasks; + + @override + factory TasksResponse.fromJson(Map json) { + final list = json['tasks'] as List?; + return TasksResponse( + tasks: list + ?.map((e) => TaskItem.fromJson(e as Map)) + .toList(), + ); + } + + @override + Map toJson() => { + 'tasks': tasks?.map((e) => e.toJson()).toList(), + }; +} + +/// 推荐提示词项 +class PromptRecommendItem extends Entity { + PromptRecommendItem({ + this.prompt, + this.icon, + }); + + final String? prompt; + final String? icon; + + @override + factory PromptRecommendItem.fromJson(Map json) { + return PromptRecommendItem( + prompt: json['prompt'] as String?, + icon: json['icon'] as String?, + ); + } + + @override + Map toJson() => { + 'prompt': prompt, + 'icon': icon, + }; +} + +/// 推荐提示词响应 +class PromptRecommendsResponse extends Entity { + PromptRecommendsResponse({ + this.recommends, + }); + + final List? recommends; + + @override + factory PromptRecommendsResponse.fromJson(Map json) { + final list = json['recommends'] as List?; + return PromptRecommendsResponse( + recommends: list + ?.map((e) => PromptRecommendItem.fromJson(e as Map)) + .toList(), + ); + } + + @override + Map toJson() => { + 'recommends': recommends?.map((e) => e.toJson()).toList(), + }; +} + +/// 任务进度响应 +class ProgressResponse extends Entity { + ProgressResponse({ + this.taskId, + this.status, + this.progress, + this.resultUrl, + }); + + final String? taskId; + final String? status; + final int? progress; + final String? resultUrl; + + @override + factory ProgressResponse.fromJson(Map json) { + return ProgressResponse( + taskId: json['taskId'] as String?, + status: json['status'] as String?, + progress: json['progress'] as int?, + resultUrl: json['resultUrl'] as String?, + ); + } + + @override + Map toJson() => { + 'taskId': taskId, + 'status': status, + 'progress': progress, + 'resultUrl': resultUrl, + }; +} + +/// 姿态模板项 +class PoseTemplateItem extends Entity { + PoseTemplateItem({ + this.id, + this.name, + this.imageUrl, + }); + + final String? id; + final String? name; + final String? imageUrl; + + @override + factory PoseTemplateItem.fromJson(Map json) { + return PoseTemplateItem( + id: json['id'] as String?, + name: json['name'] as String?, + imageUrl: json['imageUrl'] as String?, + ); + } + + @override + Map toJson() => { + 'id': id, + 'name': name, + 'imageUrl': imageUrl, + }; +} + +/// 姿态模板列表响应 +class PoseTemplatesResponse extends Entity { + PoseTemplatesResponse({ + this.templates, + }); + + final List? templates; + + @override + factory PoseTemplatesResponse.fromJson(Map json) { + final list = json['templates'] as List?; + return PoseTemplatesResponse( + templates: list + ?.map((e) => PoseTemplateItem.fromJson(e as Map)) + .toList(), + ); + } + + @override + Map toJson() => { + 'templates': templates?.map((e) => e.toJson()).toList(), + }; +} + +/// 预签名上传 URL 响应 +class UploadPresignedUrlResponse extends Entity { + UploadPresignedUrlResponse({ + this.uploadUrl, + this.filePath, + }); + + final String? uploadUrl; + final String? filePath; + + @override + factory UploadPresignedUrlResponse.fromJson(Map json) { + return UploadPresignedUrlResponse( + uploadUrl: json['uploadUrl'] as String?, + filePath: json['filePath'] as String?, + ); + } + + @override + Map toJson() => { + 'uploadUrl': uploadUrl, + 'filePath': filePath, + }; +} + +/// 创建任务响应 +class CreateTaskResponse extends Entity { + CreateTaskResponse({ + this.taskId, + this.status, + }); + + final String? taskId; + final String? status; + + @override + factory CreateTaskResponse.fromJson(Map json) { + return CreateTaskResponse( + taskId: json['taskId'] as String?, + status: json['status'] as String?, + ); + } + + @override + Map toJson() => { + 'taskId': taskId, + 'status': status, + }; +} + +/// 我的任务项 +class MyTaskItem extends Entity { + MyTaskItem({ + this.taskId, + this.status, + this.progress, + this.resultUrl, + this.createTime, + this.type, + }); + + final String? taskId; + final String? status; + final int? progress; + final String? resultUrl; + final String? createTime; + final String? type; + + @override + factory MyTaskItem.fromJson(Map json) { + return MyTaskItem( + taskId: json['taskId'] as String?, + status: json['status'] as String?, + progress: json['progress'] as int?, + resultUrl: json['resultUrl'] as String?, + createTime: json['createTime'] as String?, + type: json['type'] as String?, + ); + } + + @override + Map toJson() => { + 'taskId': taskId, + 'status': status, + 'progress': progress, + 'resultUrl': resultUrl, + 'createTime': createTime, + 'type': type, + }; +} + +/// 我的任务列表响应 +class MyTasksResponse extends Entity { + MyTasksResponse({ + this.tasks, + this.total, + this.cursor, + }); + + final List? tasks; + final int? total; + final String? cursor; + + @override + factory MyTasksResponse.fromJson(Map json) { + final list = json['tasks'] as List?; + return MyTasksResponse( + tasks: list + ?.map((e) => MyTaskItem.fromJson(e as Map)) + .toList(), + total: json['total'] as int?, + cursor: json['cursor'] as String?, + ); + } + + @override + Map toJson() => { + 'tasks': tasks?.map((e) => e.toJson()).toList(), + 'total': total, + 'cursor': cursor, + }; +} + +/// 积分页面信息响应 +class CreditsPageInfoResponse extends Entity { + CreditsPageInfoResponse({ + this.credits, + this.freeTimes, + this.vipExpireTime, + this.isVip, + }); + + final int? credits; + final int? freeTimes; + final String? vipExpireTime; + final bool? isVip; + + @override + factory CreditsPageInfoResponse.fromJson(Map json) { + return CreditsPageInfoResponse( + credits: json['credits'] as int?, + freeTimes: json['freeTimes'] as int?, + vipExpireTime: json['vipExpireTime'] as String?, + isVip: json['isVip'] as bool?, + ); + } + + @override + Map toJson() => { + 'credits': credits, + 'freeTimes': freeTimes, + 'vipExpireTime': vipExpireTime, + 'isVip': isVip, + }; +} diff --git a/lib/src/entities/payment_entities.dart b/lib/src/entities/payment_entities.dart new file mode 100644 index 0000000..cf65e2a --- /dev/null +++ b/lib/src/entities/payment_entities.dart @@ -0,0 +1,214 @@ +import 'entity.dart'; + +/// 支付商品项 +class PaymentProductItem extends Entity { + PaymentProductItem({ + this.productId, + this.activityId, + this.actualAmount, + this.originAmount, + this.bonus, + this.title, + }); + + final String? productId; + final String? activityId; + final String? actualAmount; + final String? originAmount; + final int? bonus; + final String? title; + + @override + factory PaymentProductItem.fromJson(Map json) { + return PaymentProductItem( + productId: json['productId'] as String?, + activityId: json['activityId'] as String?, + actualAmount: json['actualAmount'] as String?, + originAmount: json['originAmount'] as String?, + bonus: json['bonus'] as int?, + title: json['title'] as String?, + ); + } + + @override + Map toJson() => { + 'productId': productId, + 'activityId': activityId, + 'actualAmount': actualAmount, + 'originAmount': originAmount, + 'bonus': bonus, + 'title': title, + }; +} + +/// 支付商品列表响应 +class PaymentProductsResponse extends Entity { + PaymentProductsResponse({ + this.productList, + }); + + final List? productList; + + @override + factory PaymentProductsResponse.fromJson(Map json) { + final list = json['productList'] as List?; + return PaymentProductsResponse( + productList: list + ?.map((e) => PaymentProductItem.fromJson(e as Map)) + .toList(), + ); + } + + @override + Map toJson() => { + 'productList': productList?.map((e) => e.toJson()).toList(), + }; +} + +/// 支付方式项 +class PaymentMethodItem extends Entity { + PaymentMethodItem({ + this.paymentMethod, + this.subPaymentMethod, + this.name, + this.icon, + this.recommend, + }); + + final String? paymentMethod; + final String? subPaymentMethod; + final String? name; + final String? icon; + final bool? recommend; + + @override + factory PaymentMethodItem.fromJson(Map json) { + return PaymentMethodItem( + paymentMethod: json['paymentMethod'] as String?, + subPaymentMethod: json['subPaymentMethod'] as String?, + name: json['name'] as String?, + icon: json['icon'] as String?, + recommend: json['recommend'] as bool?, + ); + } + + @override + Map toJson() => { + 'paymentMethod': paymentMethod, + 'subPaymentMethod': subPaymentMethod, + 'name': name, + 'icon': icon, + 'recommend': recommend, + }; +} + +/// 支付方式列表响应 +class PaymentMethodsResponse extends Entity { + PaymentMethodsResponse({ + this.paymentMethods, + }); + + final List? paymentMethods; + + @override + factory PaymentMethodsResponse.fromJson(Map json) { + final list = json['paymentMethods'] as List?; + return PaymentMethodsResponse( + paymentMethods: list + ?.map((e) => PaymentMethodItem.fromJson(e as Map)) + .toList(), + ); + } + + @override + Map toJson() => { + 'paymentMethods': paymentMethods?.map((e) => e.toJson()).toList(), + }; +} + +/// 创建订单响应 +class CreatePaymentResponse extends Entity { + CreatePaymentResponse({ + this.orderId, + this.payUrl, + this.status, + }); + + final String? orderId; + final String? payUrl; + final String? status; + + @override + factory CreatePaymentResponse.fromJson(Map json) { + return CreatePaymentResponse( + orderId: json['orderId'] as String?, + payUrl: json['payUrl'] as String?, + status: json['status'] as String?, + ); + } + + @override + Map toJson() => { + 'orderId': orderId, + 'payUrl': payUrl, + 'status': status, + }; +} + +/// 订单详情响应 +class OrderDetailResponse extends Entity { + OrderDetailResponse({ + this.orderId, + this.status, + this.amount, + }); + + final String? orderId; + final String? status; + final String? amount; + + @override + factory OrderDetailResponse.fromJson(Map json) { + return OrderDetailResponse( + orderId: json['orderId'] as String?, + status: json['status'] as String?, + amount: json['amount'] as String?, + ); + } + + @override + Map toJson() => { + 'orderId': orderId, + 'status': status, + 'amount': amount, + }; +} + +/// Google Pay 回调响应 +class GooglePayCallbackResponse extends Entity { + GooglePayCallbackResponse({ + this.orderId, + this.status, + this.creditsAdded, + }); + + final String? orderId; + final String? status; + final bool? creditsAdded; + + @override + factory GooglePayCallbackResponse.fromJson(Map json) { + return GooglePayCallbackResponse( + orderId: json['orderId'] as String?, + status: json['status'] as String?, + creditsAdded: json['creditsAdded'] as bool?, + ); + } + + @override + Map toJson() => { + 'orderId': orderId, + 'status': status, + 'creditsAdded': creditsAdded, + }; +} diff --git a/lib/src/entities/user_entities.dart b/lib/src/entities/user_entities.dart new file mode 100644 index 0000000..624f7bf --- /dev/null +++ b/lib/src/entities/user_entities.dart @@ -0,0 +1,292 @@ +import 'entity.dart'; + +/// 登录响应 +class FastLoginResponse extends Entity { + FastLoginResponse({ + this.userToken, + this.userId, + this.credits, + this.avatar, + this.userName, + this.countryCode, + this.extConfig, + this.appFbConfig, + this.usign, + this.creditsRecordUrl, + this.tgId, + this.tgName, + this.email, + this.forcePayCenter, + this.t2IConfig, + this.h5UrlConfig, + this.payCenterUrl, + this.freeBlurTimes, + this.firstRegister, + this.isVip, + this.tags, + this.freeTimes, + this.subScribeValidTime, + }); + + final String? userToken; + final String? userId; + final int? credits; + final String? avatar; + final String? userName; + final String? countryCode; + final String? extConfig; + final String? appFbConfig; + final String? usign; + final String? creditsRecordUrl; + final String? tgId; + final String? tgName; + final String? email; + final bool? forcePayCenter; + final String? t2IConfig; + final String? h5UrlConfig; + final String? payCenterUrl; + final int? freeBlurTimes; + final bool? firstRegister; + final bool? isVip; + final String? tags; + final int? freeTimes; + final String? subScribeValidTime; + + static String? _toString(dynamic value) { + if (value == null) return null; + if (value is String) return value; + if (value is List) return value.join(','); + return value.toString(); + } + + static int? _toInt(dynamic value) { + if (value == null) return null; + if (value is int) return value; + if (value is num) return value.toInt(); + if (value is String && value.isNotEmpty) return int.tryParse(value); + return null; + } + + @override + factory FastLoginResponse.fromJson(Map json) { + return FastLoginResponse( + userToken: _toString(json['userToken']), + userId: _toString(json['userId']), + credits: _toInt(json['credits']), + avatar: _toString(json['avatar']), + userName: _toString(json['userName']), + countryCode: _toString(json['countryCode']), + extConfig: _toString(json['extConfig']), + appFbConfig: _toString(json['appFbConfig']), + usign: _toString(json['usign']), + creditsRecordUrl: _toString(json['creditsRecordUrl']), + tgId: _toString(json['tgId']), + tgName: _toString(json['tgName']), + email: _toString(json['email']), + forcePayCenter: json['forcePayCenter'] is bool + ? json['forcePayCenter'] as bool + : null, + t2IConfig: _toString(json['t2IConfig']), + h5UrlConfig: _toString(json['h5UrlConfig']), + payCenterUrl: _toString(json['payCenterUrl']), + freeBlurTimes: _toInt(json['freeBlurTimes']), + firstRegister: + json['firstRegister'] is bool ? json['firstRegister'] as bool : null, + isVip: json['isVip'] is bool ? json['isVip'] as bool : null, + tags: _toString(json['tags']), + freeTimes: _toInt(json['freeTimes']), + subScribeValidTime: _toString(json['subScribeValidTime']), + ); + } + + @override + Map toJson() => { + 'userToken': userToken, + 'userId': userId, + 'credits': credits, + 'avatar': avatar, + 'userName': userName, + 'countryCode': countryCode, + 'extConfig': extConfig, + 'appFbConfig': appFbConfig, + 'usign': usign, + 'creditsRecordUrl': creditsRecordUrl, + 'tgId': tgId, + 'tgName': tgName, + 'email': email, + 'forcePayCenter': forcePayCenter, + 't2IConfig': t2IConfig, + 'h5UrlConfig': h5UrlConfig, + 'payCenterUrl': payCenterUrl, + 'freeBlurTimes': freeBlurTimes, + 'firstRegister': firstRegister, + 'isVip': isVip, + 'tags': tags, + 'freeTimes': freeTimes, + 'subScribeValidTime': subScribeValidTime, + }; +} + +/// 用户通用信息响应 +class CommonInfoResponse extends Entity { + CommonInfoResponse({ + this.userToken, + this.userId, + this.credits, + this.avatar, + this.userName, + this.countryCode, + this.extConfig, + this.appFbConfig, + this.usign, + this.creditsRecordUrl, + this.tgId, + this.tgName, + this.email, + this.forcePayCenter, + this.t2IConfig, + this.h5UrlConfig, + this.payCenterUrl, + this.freeBlurTimes, + this.firstRegister, + this.isVip, + this.tags, + this.freeTimes, + this.subScribeValidTime, + }); + + final String? userToken; + final String? userId; + final int? credits; + final String? avatar; + final String? userName; + final String? countryCode; + final String? extConfig; + final String? appFbConfig; + final String? usign; + final String? creditsRecordUrl; + final String? tgId; + final String? tgName; + final String? email; + final bool? forcePayCenter; + final String? t2IConfig; + final String? h5UrlConfig; + final String? payCenterUrl; + final int? freeBlurTimes; + final bool? firstRegister; + final bool? isVip; + final String? tags; + final int? freeTimes; + final String? subScribeValidTime; + + static String? _toString(dynamic value) { + if (value == null) return null; + if (value is String) return value; + if (value is List) return value.join(','); + return value.toString(); + } + + static int? _toInt(dynamic value) { + if (value == null) return null; + if (value is int) return value; + if (value is num) return value.toInt(); + if (value is String && value.isNotEmpty) return int.tryParse(value); + return null; + } + + @override + factory CommonInfoResponse.fromJson(Map json) { + return CommonInfoResponse( + userToken: _toString(json['userToken']), + userId: _toString(json['userId']), + credits: _toInt(json['credits']), + avatar: _toString(json['avatar']), + userName: _toString(json['userName']), + countryCode: _toString(json['countryCode']), + extConfig: _toString(json['extConfig']), + appFbConfig: _toString(json['appFbConfig']), + usign: _toString(json['usign']), + creditsRecordUrl: _toString(json['creditsRecordUrl']), + tgId: _toString(json['tgId']), + tgName: _toString(json['tgName']), + email: _toString(json['email']), + forcePayCenter: json['forcePayCenter'] is bool + ? json['forcePayCenter'] as bool + : null, + t2IConfig: _toString(json['t2IConfig']), + h5UrlConfig: _toString(json['h5UrlConfig']), + payCenterUrl: _toString(json['payCenterUrl']), + freeBlurTimes: _toInt(json['freeBlurTimes']), + firstRegister: + json['firstRegister'] is bool ? json['firstRegister'] as bool : null, + isVip: json['isVip'] is bool ? json['isVip'] as bool : null, + tags: _toString(json['tags']), + freeTimes: _toInt(json['freeTimes']), + subScribeValidTime: _toString(json['subScribeValidTime']), + ); + } + + @override + Map toJson() => { + 'userToken': userToken, + 'userId': userId, + 'credits': credits, + 'avatar': avatar, + 'userName': userName, + 'countryCode': countryCode, + 'extConfig': extConfig, + 'appFbConfig': appFbConfig, + 'usign': usign, + 'creditsRecordUrl': creditsRecordUrl, + 'tgId': tgId, + 'tgName': tgName, + 'email': email, + 'forcePayCenter': forcePayCenter, + 't2IConfig': t2IConfig, + 'h5UrlConfig': h5UrlConfig, + 'payCenterUrl': payCenterUrl, + 'freeBlurTimes': freeBlurTimes, + 'firstRegister': firstRegister, + 'isVip': isVip, + 'tags': tags, + 'freeTimes': freeTimes, + 'subScribeValidTime': subScribeValidTime, + }; +} + +/// 用户账户响应 +class AccountResponse extends Entity { + AccountResponse({ + this.credits, + this.avatar, + this.userName, + this.isVip, + this.freeTimes, + }); + + final int? credits; + final String? avatar; + final String? userName; + final bool? isVip; + final int? freeTimes; + + @override + factory AccountResponse.fromJson(Map json) { + return AccountResponse( + credits: json['credits'] as int?, + avatar: json['avatar'] as String?, + userName: json['userName'] as String?, + isVip: json['isVip'] as bool?, + freeTimes: json['freeTimes'] as int?, + ); + } + + @override + Map toJson() => { + 'credits': credits, + 'avatar': avatar, + 'userName': userName, + 'isVip': isVip, + 'freeTimes': freeTimes, + }; +} diff --git a/lib/src/log/app_logger.dart b/lib/src/log/app_logger.dart index 74f809f..1239f5f 100644 --- a/lib/src/log/app_logger.dart +++ b/lib/src/log/app_logger.dart @@ -1,6 +1,159 @@ +import 'dart:convert'; + import 'package:flutter/foundation.dart'; import 'package:logger/logger.dart'; +final _proxyLog = AppLogger('ProxyClient'); + +const int _maxLogChunk = 1000; + +int _findMatchingBracket(String str, int start, String open) { + final close = open == '{' ? '}' : ']'; + int depth = 1; + int i = start + 1; + while (i < str.length) { + final c = str[i]; + if (c == '"') { + i = _skipJsonString(str, i); + if (i < 0) return -1; + continue; + } + if (c == open) { + depth++; + } else if (c == close) { + depth--; + if (depth == 0) return i; + } + i++; + } + return -1; +} + +int _skipJsonString(String str, int start) { + if (start >= str.length || str[start] != '"') return -1; + int i = start + 1; + while (i < str.length) { + final c = str[i++]; + if (c == '\\') { + if (i < str.length) i++; + continue; + } + if (c == '"') return i; + } + return -1; +} + +void _logLong(String text) { + if (text.isEmpty) return; + final lines = text.split('\n'); + if (lines.length == 1 && text.length <= _maxLogChunk) { + _proxyLog.d(text); + return; + } + final buffer = StringBuffer(); + int chunkIndex = 0; + for (final line in lines) { + final lineWithNewline = buffer.isEmpty ? line : '\n$line'; + if (buffer.length + lineWithNewline.length > _maxLogChunk && + buffer.isNotEmpty) { + chunkIndex++; + _proxyLog.d('(part $chunkIndex)\n$buffer'); + buffer.clear(); + buffer.write(line); + } else { + if (buffer.isNotEmpty) buffer.write('\n'); + buffer.write(line); + } + } + if (buffer.isNotEmpty) { + chunkIndex++; + _proxyLog + .d(chunkIndex > 1 ? '(part $chunkIndex)\n$buffer' : buffer.toString()); + } +} + +/// 格式化输出嵌入在字符串中的 JSON,保持缩进对齐。 +void logWithEmbeddedJson(Object? msg) { + if (!kDebugMode) return; + + if (msg is! String) { + _proxyLog.d(msg); + return; + } + + final String str = msg.trim(); + final sb = StringBuffer(); + + void write(String line) { + sb.writeln(line); + } + + void printJson(dynamic value, [int indent = 0]) { + final pad = ' ' * indent; + final padInner = ' ' * (indent + 1); + if (value is Map) { + write('$pad{'); + value.forEach((k, v) { + if (v is Map || v is List) { + write('$padInner"$k":'); + printJson(v, indent + 1); + } else { + write('$padInner"$k": ${json.encode(v)}'); + } + }); + write('$pad}'); + } else if (value is List) { + write('$pad['); + for (final item in value) { + printJson(item, indent + 1); + } + write('$pad]'); + } else { + write('$pad${json.encode(value)}'); + } + } + + int i = 0; + int lastEnd = 0; + + while (i < str.length) { + final c = str[i]; + if (c == '"') { + final next = _skipJsonString(str, i); + if (next > 0) { + i = next; + continue; + } + i++; + continue; + } + if (c == '{' || c == '[') { + final end = _findMatchingBracket(str, i, c); + if (end >= 0) { + final prefix = str.substring(lastEnd, i).trim(); + if (prefix.isNotEmpty) write(prefix); + final jsonStr = str.substring(i, end + 1); + try { + final parsed = json.decode(jsonStr); + printJson(parsed, 0); + } catch (e) { + write(jsonStr); + } + lastEnd = end + 1; + i = end + 1; + continue; + } + } + i++; + } + + final trailing = str.substring(lastEnd).trim(); + if (trailing.isNotEmpty) write(trailing); + + final out = sb.toString().trim(); + if (out.isNotEmpty) _logLong(out); +} + /// 统一应用日志 class AppLogger { AppLogger([this.tag = 'App']); diff --git a/lib/src/services/adjust_service.dart b/lib/src/services/adjust_service.dart new file mode 100644 index 0000000..b0adfd0 --- /dev/null +++ b/lib/src/services/adjust_service.dart @@ -0,0 +1,207 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:adjust_sdk/adjust.dart' as adj; +import 'package:adjust_sdk/adjust_attribution.dart'; +import 'package:adjust_sdk/adjust_config.dart' as adj; +import 'package:adjust_sdk/adjust_event.dart'; +import 'package:flutter/foundation.dart'; +import 'package:play_install_referrer/play_install_referrer.dart'; + +import '../log/app_logger.dart'; + +typedef AdjustAttributionCallback = void Function( + AdjustAttribution attribution); + +class AdjustService { + AdjustService._(); + + static final _log = AppLogger('Adjust'); + static adj.AdjustConfig? _config; + static AdjustAttributionCallback? _attributionCallback; + static String _referrerSource = 'gg'; + static String? _cachedReferrer; + + static const int _adjustTimeoutMs = 15000; + + static final Completer _attributionCallbackCompleter = + Completer(); + + static void init(adj.AdjustConfig config, + {AdjustAttributionCallback? onAttribution}) { + _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'); + } + } + + static void _onAttribution(AdjustAttribution attribution) { + if (!_attributionCallbackCompleter.isCompleted) { + _attributionCallbackCompleter.complete(attribution); + } + _attributionCallback?.call(attribution); + _log.d('Attribution: trackerToken=${attribution.trackerToken}, ' + 'network=${attribution.network}, campaign=${attribution.campaign}'); + } + + static void trackEvent(String eventToken) { + adj.Adjust.trackEvent(AdjustEvent(eventToken)); + _log.d('Track event: $eventToken'); + } + + 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'); + } + + static Future getAttribution() async { + if (_config == null) return null; + + // 尝试获取 Adjust 归因 + try { + final attribution = await Future.any([ + (() async { + try { + return await adj.Adjust.getAttributionWithTimeout(_adjustTimeoutMs); + } catch (_) { + return null; + } + })(), + _attributionCallbackCompleter.future, + ]); + + if (attribution != null) { + _referrerSource = 'android_adjust'; + // Adjust 归因存在时,也获取 Google Play referrer(两者都需要上报) + await _getPlayReferrer(); + return _toAttributionData(attribution); + } + } catch (_) {} + + // 没有 Adjust 归因时,获取 Google Play referrer + await _getPlayReferrer(); + + return null; + } + + static Future _getPlayReferrer() async { + if (defaultTargetPlatform != TargetPlatform.android) return; + + try { + final details = await PlayInstallReferrer.installReferrer; + if (details.installReferrer != null && + details.installReferrer!.isNotEmpty) { + _referrerSource = 'gg'; + _cachedReferrer = details.installReferrer; + } + } catch (_) {} + } + + static Object? _jsonEncodableCostAmount(num? v) { + if (v == null) return null; + if (v is double && !v.isFinite) return v.toString(); + return v; + } + + static String _attributionToDigest(AdjustAttribution attr) { + final map = { + 'trackerToken': attr.trackerToken, + 'trackerName': attr.trackerName, + 'network': attr.network, + 'campaign': attr.campaign, + 'adgroup': attr.adgroup, + 'creative': attr.creative, + 'clickLabel': attr.clickLabel, + 'costType': attr.costType, + 'costAmount': _jsonEncodableCostAmount(attr.costAmount), + 'costCurrency': attr.costCurrency, + 'jsonResponse': attr.jsonResponse, + 'fbInstallReferrer': attr.fbInstallReferrer, + }; + try { + return jsonEncode(map); + } catch (_) { + return ''; + } + } + + static AttributionData _toAttributionData(AdjustAttribution attr) { + return AttributionData( + trackerToken: attr.trackerToken, + trackerName: attr.trackerName, + network: attr.network, + campaign: attr.campaign, + adgroup: attr.adgroup, + creative: attr.creative, + clickLabel: attr.clickLabel, + costType: attr.costType, + costAmount: attr.costAmount?.toDouble(), + costCurrency: attr.costCurrency, + jsonResponse: attr.jsonResponse, + fbInstallReferrer: attr.fbInstallReferrer, + ); + } + + static Future getReferrerDigest() async { + try { + final attr = await adj.Adjust.getAttribution(); + final raw = _attributionToDigest(attr); + if (raw.isEmpty) return ''; + return base64Encode(utf8.encode(raw)); + } catch (_) { + return ''; + } + } + + static Future getPlayReferrerDigest() async { + if (defaultTargetPlatform != TargetPlatform.android) return ''; + try { + final details = await PlayInstallReferrer.installReferrer; + return details.installReferrer ?? ''; + } catch (_) { + return ''; + } + } + + static String get referrerSource => _referrerSource; + + static String? get cachedPlayReferrer => _cachedReferrer; +} + +class AttributionData { + AttributionData({ + this.trackerToken, + this.trackerName, + this.network, + this.campaign, + this.adgroup, + this.creative, + this.clickLabel, + this.costType, + this.costAmount, + this.costCurrency, + this.jsonResponse, + this.fbInstallReferrer, + }); + + final String? trackerToken; + final String? trackerName; + final String? network; + final String? campaign; + final String? adgroup; + final String? creative; + final String? clickLabel; + final String? costType; + final double? costAmount; + final String? costCurrency; + final String? jsonResponse; + final String? fbInstallReferrer; +} diff --git a/lib/src/services/analytics_service.dart b/lib/src/services/analytics_service.dart new file mode 100644 index 0000000..8746573 --- /dev/null +++ b/lib/src/services/analytics_service.dart @@ -0,0 +1,208 @@ +import 'package:adjust_sdk/adjust_config.dart' as adj; +import 'package:adjust_sdk/adjust_attribution.dart' + as adjust_sdk_adjust_attribution; +import 'package:flutter/foundation.dart'; + +import '../config/attribution_config.dart'; +import 'adjust_service.dart'; +import 'facebook_service.dart'; + +typedef AttributionCallback = void Function(AttributionData data); + +class AnalyticsConfig { + const AnalyticsConfig({ + required this.packageName, + this.adjustConfig, + this.facebookConfig, + this.platformAttributionConfig, + this.debugLogs = false, + }); + + final String packageName; + final AdjustConfig? adjustConfig; + final FacebookConfig? facebookConfig; + final PlatformAttributionConfig? platformAttributionConfig; + final bool debugLogs; +} + +abstract class AnalyticsService { + static AnalyticsConfig? _config; + static AttributionCallback? _attributionCallback; + static bool _isInitialized = false; + static AttributionData? _cachedAttribution; + + static Future init(AnalyticsConfig config) async { + _config = config; + _initAdjust(); + await _initFacebook(); + _isInitialized = true; + } + + static void setAttributionCallback(AttributionCallback callback) { + _attributionCallback = callback; + } + + static bool get isInitialized => _isInitialized; + + /// 提前获取并缓存归因数据 + /// 应在 main() 早期调用,确保登录时归因已就绪 + static Future initAttribution() async { + if (_cachedAttribution != null) return; + _cachedAttribution = await AdjustService.getAttribution(); + if (_config?.debugLogs ?? false) { + debugPrint( + '[Analytics] Attribution cached: ${_cachedAttribution?.trackerToken ?? "none"}'); + } + } + + /// 获取归因数据(优先返回缓存) + static AttributionData? getAttribution() { + return _cachedAttribution; + } + + static void _initAdjust() { + final adjustConfig = _config?.adjustConfig; + if (adjustConfig == null) { + if (_config?.debugLogs ?? false) { + debugPrint('[Analytics] Adjust not configured, skipping'); + } + return; + } + + try { + final environment = adjustConfig.environment == AdjustEnv.sandbox + ? adj.AdjustEnvironment.sandbox + : adj.AdjustEnvironment.production; + final sdkConfig = adj.AdjustConfig(adjustConfig.appToken, environment); + + if (adjustConfig.logLevel == AdjustLogLevel.verbose) { + sdkConfig.logLevel = adj.AdjustLogLevel.verbose; + } else { + sdkConfig.logLevel = adj.AdjustLogLevel.suppress; + } + + if (adjustConfig.fbAppId != null) { + sdkConfig.fbAppId = adjustConfig.fbAppId; + } + + sdkConfig.attributionCallback = _onAdjustAttribution; + AdjustService.init(sdkConfig, onAttribution: _onAdjustAttribution); + + if (_config?.debugLogs ?? false) { + debugPrint('[Analytics] Adjust initialized: ${adjustConfig.appToken}'); + } + } catch (e) { + if (_config?.debugLogs ?? false) { + debugPrint('[Analytics] Adjust init failed: $e'); + } + } + } + + static void _onAdjustAttribution( + adjust_sdk_adjust_attribution.AdjustAttribution attribution) { + final data = AttributionData( + trackerToken: attribution.trackerToken, + trackerName: attribution.trackerName, + network: attribution.network, + campaign: attribution.campaign, + adgroup: attribution.adgroup, + creative: attribution.creative, + clickLabel: attribution.clickLabel, + costType: attribution.costType, + costAmount: attribution.costAmount?.toDouble(), + costCurrency: attribution.costCurrency, + jsonResponse: attribution.jsonResponse, + fbInstallReferrer: attribution.fbInstallReferrer, + ); + _attributionCallback?.call(data); + } + + static Future _initFacebook() async { + final fbConfig = _config?.facebookConfig; + final packageName = _config?.packageName; + if (fbConfig == null || packageName == null) { + if (_config?.debugLogs ?? false) { + debugPrint('[Analytics] Facebook not configured, skipping'); + } + return; + } + + try { + await FacebookService.init(fbConfig, packageName: packageName); + if (_config?.debugLogs ?? false) { + debugPrint('[Analytics] Facebook initialized: ${fbConfig.appId}'); + } + } catch (e) { + if (_config?.debugLogs ?? false) { + debugPrint('[Analytics] Facebook init failed: $e'); + } + } + } + + static void trackEvent(String eventToken) { + try { + AdjustService.trackEvent(eventToken); + if (_config?.debugLogs ?? false) { + debugPrint('[Analytics] Adjust track: $eventToken'); + } + } catch (e) { + if (_config?.debugLogs ?? false) { + debugPrint('[Analytics] Adjust track error: $e'); + } + } + } + + static void trackPurchase({ + required double amount, + String currency = 'USD', + String? orderId, + }) { + try { + FacebookService.logPurchase( + amount: amount, currency: currency, orderId: orderId); + if (_config?.debugLogs ?? false) { + debugPrint('[Analytics] Facebook purchase: $amount $currency'); + } + } catch (e) { + if (_config?.debugLogs ?? false) { + debugPrint('[Analytics] Facebook purchase error: $e'); + } + } + } + + static void trackSubscribe(String planId) { + try { + FacebookService.logSubscribe(planId); + if (_config?.debugLogs ?? false) { + debugPrint('[Analytics] Facebook subscribe: $planId'); + } + } catch (e) { + if (_config?.debugLogs ?? false) { + debugPrint('[Analytics] Facebook subscribe error: $e'); + } + } + } + + static void trackRegister() { + try { + FacebookService.logRegister(); + if (_config?.debugLogs ?? false) { + debugPrint('[Analytics] Facebook register'); + } + } catch (e) { + if (_config?.debugLogs ?? false) { + debugPrint('[Analytics] Facebook register error: $e'); + } + } + } + + static String get referrerSource => AdjustService.referrerSource; + + static Future getAdjustReferrerDigest() async { + return AdjustService.getReferrerDigest(); + } + + static Future getPlayReferrerDigest() async { + return AdjustService.getPlayReferrerDigest(); + } +} diff --git a/lib/src/services/auth_service.dart b/lib/src/services/auth_service.dart new file mode 100644 index 0000000..8a3a55d --- /dev/null +++ b/lib/src/services/auth_service.dart @@ -0,0 +1,245 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/foundation.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 'user_api.dart'; + +/// 认证服务回调 +/// 用于在认证流程各阶段通知调用方 +abstract class AuthServiceCallbacks { + /// 获取设备 ID(由调用方实现) + Future getDeviceId(); + + /// 计算签名(由调用方实现) + String computeSign(String deviceId); + + /// 登录成功后回调 + void onLoginSuccess(FastLoginResponse data) {} + + /// 通用信息获取后回调 + void onCommonInfoLoaded(CommonInfoResponse data) {} + + /// 登录失败回调 + void onLoginFailed(String msg) {} +} + +/// APP 启动认证服务 +/// 封装完整的启动登录流程,包括: +/// 1. 快速登录 +/// 2. 归因上报 +/// 3. 获取通用信息 +abstract class FrameworkAuthService { + static AuthServiceCallbacks? _callbacks; + static Future? _loginFuture; + static bool _isInitialized = false; + + /// 登录是否已完成 + static final ValueNotifier isLoginComplete = ValueNotifier(false); + + /// 登录完成后的 Future,需鉴权接口应 await 此 Future 再请求 + static Future get loginComplete => _loginFuture ?? Future.value(); + + /// 初始化认证服务 + static void init(AuthServiceCallbacks callbacks) { + _callbacks = callbacks; + _isInitialized = true; + } + + /// 启动登录流程 + /// [delaySeconds] 启动延迟秒数,默认 2 秒 + /// [maxRetries] 最大重试次数,默认 3 次 + /// [retryDelaySeconds] 重试延迟秒数,默认 2 秒 + static Future start({ + int delaySeconds = 2, + int maxRetries = 3, + int retryDelaySeconds = 2, + }) async { + if (!_isInitialized || _callbacks == null) { + throw StateError( + 'AuthService not initialized. Call AuthService.init(callbacks) first.'); + } + if (_loginFuture != null) return _loginFuture!; + final completer = Completer(); + _loginFuture = completer.future; + + if (kDebugMode) { + debugPrint('[AuthService] start: 开始登录流程'); + } + + try { + await Future.delayed(Duration(seconds: delaySeconds)); + + final deviceId = await _callbacks!.getDeviceId(); + if (kDebugMode) { + debugPrint('[AuthService] start: deviceId=$deviceId'); + } + + final sign = _callbacks!.computeSign(deviceId); + if (kDebugMode) { + debugPrint('[AuthService] start: sign=$sign'); + } + + final referer = await AttributionService.getReferrer(); + if (kDebugMode && referer != null) { + debugPrint('[AuthService] start: referer=$referer'); + } + + // 尝试快速登录 + EntityResponse? res; + for (var i = 0; i < maxRetries; i++) { + if (i > 0) { + if (kDebugMode) { + debugPrint('[AuthService] start: 第 ${i + 1} 次重试...'); + } + await Future.delayed(Duration(seconds: retryDelaySeconds)); + } + try { + res = await UserApi.fastLogin( + deviceId: deviceId, + sign: sign, + referer: referer ?? '', + ); + break; + } catch (e) { + if (kDebugMode) { + debugPrint('[AuthService] start: 第 ${i + 1} 次请求失败: $e'); + } + if (i == maxRetries - 1) rethrow; + } + } + + if (res == null) { + completer.complete(); + return; + } + + if (kDebugMode) { + debugPrint('[AuthService] start: 登录结果 code=${res.code} msg=${res.msg}'); + } + + if (res.isSuccess && res.data != null) { + final loginData = res.data!; + + // 设置 Token + if (loginData.userToken != null && loginData.userToken!.isNotEmpty) { + ApiClient.instance.setUserToken(loginData.userToken!); + if (kDebugMode) { + debugPrint('[AuthService] start: 已设置 userToken'); + } + } + + // 回调登录成功 + _callbacks!.onLoginSuccess(loginData); + + // 上报归因并获取通用信息 + await _reportReferrersAndLoadCommonInfo( + uid: loginData.userId ?? '', + deviceId: deviceId, + ); + } else { + _callbacks!.onLoginFailed(res.msg); + } + } catch (e, st) { + if (kDebugMode) { + debugPrint('[AuthService] start: 异常 $e\n$st'); + } + _callbacks!.onLoginFailed(e.toString()); + } finally { + if (!completer.isCompleted) { + completer.complete(); + isLoginComplete.value = true; + } + } + } + + /// 上报归因并获取通用信息 + static Future _reportReferrersAndLoadCommonInfo({ + required String uid, + required String deviceId, + }) async { + if (uid.isEmpty) return; + + final config = ApiClient.instance.config; + + // 上报 Adjust 归因 + final adjustReferer = await AttributionService.getAdjustReferrer(); + if (adjustReferer != null && adjustReferer.isNotEmpty) { + try { + final rAdjust = await UserApi.referrer( + app: config.appId, + userId: uid, + referer: adjustReferer, + deviceId: deviceId, + type: 'android_adjust', + ); + if (kDebugMode) { + debugPrint( + '[AuthService] referrer(android_adjust): ${rAdjust.isSuccess ? "成功" : "失败"}'); + } + } catch (e) { + if (kDebugMode) { + debugPrint('[AuthService] referrer(android_adjust): 异常 $e'); + } + } + } + + // 上报 Google Play 归因(从 AdjustService 获取缓存的 referrer) + final playReferrer = AdjustService.cachedPlayReferrer; + if (playReferrer != null && playReferrer.isNotEmpty) { + try { + final rGg = await UserApi.referrer( + app: config.appId, + userId: uid, + referer: playReferrer, + deviceId: deviceId, + type: 'gg', + ); + if (kDebugMode) { + debugPrint( + '[AuthService] referrer(gg): ${rGg.isSuccess ? "成功" : "失败"}'); + } + } catch (e) { + if (kDebugMode) { + debugPrint('[AuthService] referrer(gg): 异常 $e'); + } + } + } + + // 获取通用信息 + try { + final commonRes = await UserApi.getCommonInfo( + app: config.appId, + userId: uid, + ); + if (commonRes.isSuccess && commonRes.data != null) { + _callbacks?.onCommonInfoLoaded(commonRes.data!); + if (kDebugMode) { + debugPrint('[AuthService] common_info: 获取成功'); + } + } else if (kDebugMode) { + debugPrint( + '[AuthService] common_info: 失败 code=${commonRes.code} msg=${commonRes.msg}'); + } + } catch (e) { + if (kDebugMode) { + debugPrint('[AuthService] common_info: 异常 $e'); + } + } + } + + /// 解析 extConfig JSON 字符串 + static Map? parseExtConfig(String? extConfigStr) { + if (extConfigStr == null || extConfigStr.isEmpty) return null; + try { + return json.decode(extConfigStr) as Map?; + } catch (_) { + return null; + } + } +} diff --git a/lib/src/services/facebook_service.dart b/lib/src/services/facebook_service.dart new file mode 100644 index 0000000..3c8b34c --- /dev/null +++ b/lib/src/services/facebook_service.dart @@ -0,0 +1,72 @@ +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'; + +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 bool get isInitialized => _isInitialized; + + static void setOnInitialized(void Function() callback) { + _onInitialized = callback; + } + + static Future init(FacebookConfig config, + {required String packageName}) async { + _debugLogs = config.debugLogs; + + _log.d('Facebook App Events init with appId: ${config.appId}'); + _log.d('FacebookService.init start, packageName: $packageName'); + + try { + _log.d('Creating MethodChannel...'); + _channel = MethodChannel('$packageName/facebook_sdk'); + + _log.d('Setting method call handler...'); + _channel!.setMethodCallHandler((call) async { + _log.d('MethodCall received: ${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'); + } catch (e, st) { + _log.w('FacebookService.init failed: $e\n$st'); + } + } + + static void logPurchase({ + required double amount, + String currency = 'USD', + String? orderId, + }) { + _fb.logPurchase(amount: amount, currency: currency); + } + + static void logSubscribe(String planId) { + _fb.logSubscribe(orderId: planId); + } + + static void logRegister({String registrationMethod = 'device'}) { + _fb.logCompletedRegistration(registrationMethod: registrationMethod); + } + + static void logEvent(String name, {Map? parameters}) { + _fb.logEvent(name: name, parameters: parameters); + } +} diff --git a/lib/src/services/feedback_api.dart b/lib/src/services/feedback_api.dart new file mode 100644 index 0000000..2a458d8 --- /dev/null +++ b/lib/src/services/feedback_api.dart @@ -0,0 +1,39 @@ +import '../api/api_client.dart'; +import '../api/proxy_client.dart'; +import '../entities/feedback_entities.dart'; + +/// 举报/反馈相关 API(使用原始字段名) +abstract final class FeedbackApi { + static ProxyClient get _client => ApiClient.instance.proxy; + + /// 获取反馈图片上传预签名 URL + static Future> + getUploadPresignedUrl({ + required String fileName, + }) async { + return _client.requestEntity( + path: '/v1/feedback/upload-presigned-url', + method: 'POST', + entityFactory: FeedbackUploadPresignedUrlResponse.fromJson, + body: {'fileName': fileName}, + ); + } + + /// 提交反馈 + static Future> submit({ + required List fileUrls, + required String content, + required String contentType, + }) async { + return _client.requestEntity( + path: '/v1/feedback/submit', + method: 'POST', + entityFactory: SubmitFeedbackResponse.fromJson, + body: { + 'fileUrls': fileUrls, + 'content': content, + 'contentType': contentType, + }, + ); + } +} diff --git a/lib/src/services/image_api.dart b/lib/src/services/image_api.dart index 70c45ea..2d005df 100644 --- a/lib/src/services/image_api.dart +++ b/lib/src/services/image_api.dart @@ -1,37 +1,43 @@ import '../api/api_client.dart'; -import '../api/api_response.dart'; import '../api/proxy_client.dart'; +import '../entities/image_entities.dart'; -/// 图片/视频相关 API(使用原始字段名) +/// 图片/视频生成相关 API(使用原始字段名) abstract final class ImageApi { static ProxyClient get _client => ApiClient.instance.proxy; /// 获取图转视频分类列表 - static Future getCategoryList() async { - return _client.request( + static Future> getCategoryList() async { + return _client.requestEntity( path: '/v1/image/img2video/categories', method: 'GET', + entityFactory: CategoryListResponse.fromJson, ); } /// 获取图转视频任务列表 - static Future getImg2VideoTasks({int? categoryId}) async { - return _client.request( + static Future> getImg2VideoTasks({ + int? categoryId, + }) async { + return _client.requestEntity( path: '/v1/image/img2video/tasks', method: 'GET', - queryParams: categoryId != null ? {'categoryId': categoryId.toString()} : null, + entityFactory: TasksResponse.fromJson, + queryParams: + categoryId != null ? {'categoryId': categoryId.toString()} : null, ); } /// 获取推荐提示词 - static Future getPromptRecommends({ + static Future> getPromptRecommends({ required String app, String? ch, String? userId, }) async { - return _client.request( + return _client.requestEntity( path: '/v1/image/prompt/recomends', method: 'GET', + entityFactory: PromptRecommendsResponse.fromJson, queryParams: { 'app': app, if (ch != null) 'ch': ch, @@ -40,15 +46,69 @@ abstract final class ImageApi { ); } + /// 创建文生图任务 + static Future> createTxt2Img({ + required String app, + required String prompt, + String? ch, + String? userId, + String? quest, + }) async { + return _client.requestEntity( + path: '/v1/image/txt2img_create', + method: 'POST', + entityFactory: CreateTaskResponse.fromJson, + queryParams: { + 'app': app, + if (ch != null) 'ch': ch, + if (userId != null) 'userId': userId, + }, + body: { + 'declaration': 1, + if (quest != null) 'quest': quest, + 'prompt': prompt, + }, + ); + } + + /// 创建图转视频姿态任务 + static Future> createImg2VideoPose({ + required String app, + required String userId, + String? imgUrl, + String? poseId, + String? templateId, + String? notification, + bool? allowance, + String? cosmos, + }) async { + return _client.requestEntity( + path: '/v1/image/img2video_pose_task', + method: 'POST', + entityFactory: CreateTaskResponse.fromJson, + queryParams: { + 'app': app, + 'userId': userId, + if (imgUrl != null) 'imgUrl': imgUrl, + if (notification != null) 'notification': notification, + if (allowance != null) 'allowance': allowance.toString(), + if (cosmos != null) 'cosmos': cosmos, + if (poseId != null) 'poseId': poseId, + if (templateId != null) 'templateId': templateId, + }, + ); + } + /// 查询任务进度 - static Future getProgress({ + static Future> getProgress({ required String app, required String taskId, String? userId, }) async { - return _client.request( + return _client.requestEntity( path: '/v1/image/progress', method: 'GET', + entityFactory: ProgressResponse.fromJson, queryParams: { 'app': app, 'taskId': taskId, @@ -57,16 +117,39 @@ abstract final class ImageApi { ); } + /// 获取图转视频姿态模板 + static Future> + getImg2VideoPoseTemplates({ + required String app, + String? ch, + String? userId, + int? categoryId, + }) async { + return _client.requestEntity( + path: '/v1/image/img2Video_pose_template', + method: 'GET', + entityFactory: PoseTemplatesResponse.fromJson, + queryParams: { + 'app': app, + if (ch != null) 'ch': ch, + if (userId != null) 'userId': userId, + if (categoryId != null) 'categoryId': categoryId.toString(), + }, + ); + } + /// 获取预签名上传 URL - static Future getUploadPresignedUrl({ + static Future> + getUploadPresignedUrl({ required String fileName1, String? fileName2, required String contentType, required int expectedSize, }) async { - return _client.request( + return _client.requestEntity( path: '/v1/image/upload-presigned-url', method: 'POST', + entityFactory: UploadPresignedUrlResponse.fromJson, body: { 'fileName1': fileName1, 'fileName2': fileName2 ?? '', @@ -77,7 +160,7 @@ abstract final class ImageApi { } /// 创建生图/视频任务 - static Future createTask({ + static Future> createTask({ required String userId, String? resolution, String? srcImgUrls, @@ -88,9 +171,10 @@ abstract final class ImageApi { bool allowance = false, String? ext, }) async { - return _client.request( + return _client.requestEntity( path: '/v1/image/create-task', method: 'POST', + entityFactory: CreateTaskResponse.fromJson, queryParams: {'userId': userId}, body: { if (resolution != null) 'resolution': resolution, @@ -106,15 +190,16 @@ abstract final class ImageApi { } /// 获取我的任务列表 - static Future getMyTasks({ + static Future> getMyTasks({ required String app, String? page, String? pageSize, String? cursor, }) async { - return _client.request( + return _client.requestEntity( path: '/v1/image/my-tasks', method: 'GET', + entityFactory: MyTasksResponse.fromJson, queryParams: { 'app': app, if (page != null) 'page': page, @@ -125,14 +210,15 @@ abstract final class ImageApi { } /// 获取积分页面信息 - static Future getCreditsPageInfo({ + static Future> getCreditsPageInfo({ required String app, String? userId, String? ch, }) async { - return _client.request( + return _client.requestEntity( path: '/v1/image/getCreditsPageInfo', method: 'GET', + entityFactory: CreditsPageInfoResponse.fromJson, queryParams: { 'app': app, if (userId != null) 'userId': userId, diff --git a/lib/src/services/payment_api.dart b/lib/src/services/payment_api.dart index 32316c1..b79f216 100644 --- a/lib/src/services/payment_api.dart +++ b/lib/src/services/payment_api.dart @@ -1,21 +1,23 @@ import '../api/api_client.dart'; -import '../api/api_response.dart'; import '../api/proxy_client.dart'; +import '../entities/payment_entities.dart'; /// 支付相关 API(使用原始字段名) abstract final class PaymentApi { static ProxyClient get _client => ApiClient.instance.proxy; - /// 获取 Google 商品列表 - static Future getGooglePayActivities({ + /// 获取 Google 商品列表(Android) + static Future> + getGooglePayActivities({ String? app, String? shield, String? country, String? pkg, }) async { - return _client.request( + return _client.requestEntity( path: '/v1/payment/getGooglePayActivities', method: 'GET', + entityFactory: PaymentProductsResponse.fromJson, queryParams: { 'app': app ?? ApiClient.instance.config.appId, 'pkg': pkg ?? ApiClient.instance.config.packageName, @@ -25,18 +27,19 @@ abstract final class PaymentApi { ); } - /// 获取 Apple 商品列表 - static Future getApplePayActivities({ + /// 获取 Apple 商品列表(iOS) + static Future> getApplePayActivities({ String? app, String? shield, String? country, String? pkg, }) async { - return _client.request( + return _client.requestEntity( path: '/v1/payment/getApplePayActivities', method: 'GET', + entityFactory: PaymentProductsResponse.fromJson, queryParams: { - 'app': app ?? ApiClient.instance.config.appId, + 'app': app ?? 'HAndroid', 'pkg': pkg ?? ApiClient.instance.config.packageName, if (shield != null) 'shield': shield, if (country != null) 'country': country, @@ -45,13 +48,14 @@ abstract final class PaymentApi { } /// 获取支付方式列表 - static Future getPaymentMethods({ + static Future> getPaymentMethods({ required String activityId, String? country, }) async { - return _client.request( + return _client.requestEntity( path: '/v1/payment/get-payment-methods', method: 'POST', + entityFactory: PaymentMethodsResponse.fromJson, body: { 'activityId': activityId, if (country != null && country.isNotEmpty) 'country': country, @@ -60,7 +64,7 @@ abstract final class PaymentApi { } /// 创建支付订单 - static Future createPayment({ + static Future> createPayment({ required String app, required String userId, required String activityId, @@ -69,32 +73,51 @@ abstract final class PaymentApi { String? lineage, String? armor, }) async { - return _client.request( + return _client.requestEntity( path: '/v1/payment/createPayment', method: 'POST', + entityFactory: CreatePaymentResponse.fromJson, queryParams: {'app': app, 'userId': userId}, body: { 'app': app, 'userId': userId, 'activityId': activityId, 'paymentMethod': paymentMethod, - if (paymentType != null && paymentType.isNotEmpty) 'paymentType': paymentType, + if (paymentType != null && paymentType.isNotEmpty) + 'paymentType': paymentType, if (lineage != null) 'lineage': lineage, if (armor != null) 'armor': armor, }, ); } + /// 获取订单详情 + static Future> getOrderDetail({ + required String userId, + required String orderId, + }) async { + return _client.requestEntity( + path: '/v1/payment/getOrderDetail', + method: 'GET', + entityFactory: OrderDetailResponse.fromJson, + queryParams: { + 'userId': userId, + 'orderId': orderId, + }, + ); + } + /// Google 支付结果回调 - static Future googlepay({ + static Future> googlepay({ required String signature, required String purchaseData, required String orderId, required String userId, }) async { - return _client.request( + return _client.requestEntity( path: '/v1/payment/googlepay', method: 'POST', + entityFactory: GooglePayCallbackResponse.fromJson, queryParams: { 'app': ApiClient.instance.config.appId, 'userId': userId, diff --git a/lib/src/services/payment_service.dart b/lib/src/services/payment_service.dart new file mode 100644 index 0000000..2ee1ec6 --- /dev/null +++ b/lib/src/services/payment_service.dart @@ -0,0 +1,362 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:in_app_purchase_android/billing_client_wrappers.dart'; +import 'package:in_app_purchase_android/in_app_purchase_android.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../log/app_logger.dart'; + +class PaymentResult { + PaymentResult({required this.isSuccess, this.msg = ''}); + + final bool isSuccess; + final String msg; +} + +class GooglePayVerificationPayload { + GooglePayVerificationPayload({ + required this.purchaseData, + required this.signature, + }); + + final String purchaseData; + final String signature; +} + +class UnacknowledgedGooglePayPurchase { + UnacknowledgedGooglePayPurchase({ + required this.orderId, + required this.productId, + required this.payload, + required this.purchaseDetails, + }); + + final String orderId; + final String productId; + final GooglePayVerificationPayload payload; + final PurchaseDetails purchaseDetails; +} + +class GooglePayPurchaseResult { + GooglePayPurchaseResult({ + required this.orderId, + required this.payload, + required this.purchaseDetails, + }); + + final String orderId; + final GooglePayVerificationPayload payload; + final PurchaseDetails purchaseDetails; +} + +typedef OrderRecoveryCallback = Future Function( + String federation, String orderId, String productId); + +class PaymentService { + PaymentService._(); + + static final _log = AppLogger('PaymentService'); + + static const String _kFederationMapKey = + 'google_pay_google_order_to_federation'; + + static final Map _pendingFromStream = {}; + static StreamSubscription>? _pendingStreamSub; + + static OrderRecoveryCallback? _orderRecoveryCallback; + + static void setOrderRecoveryCallback(OrderRecoveryCallback callback) { + _orderRecoveryCallback = callback; + } + + static void startPendingPurchaseListener() { + if (defaultTargetPlatform != TargetPlatform.android) return; + if (_pendingStreamSub != null) return; + final iap = InAppPurchase.instance; + _pendingStreamSub = + iap.purchaseStream.listen((List purchases) { + for (final p in purchases) { + if (p is! GooglePlayPurchaseDetails) continue; + if (!p.pendingCompletePurchase) continue; + final orderId = p.billingClientPurchase.orderId; + if (orderId.isEmpty) continue; + _pendingFromStream[orderId] = p; + _log.d('purchaseStream received pending orderId=$orderId'); + } + }, onError: (e) { + _log.w('purchaseStream error: $e'); + }); + _log.d('Subscribed to purchaseStream'); + } + + static Future saveFederationForGoogleOrderId( + String googleOrderId, String federation) async { + try { + final prefs = await SharedPreferences.getInstance(); + final json = prefs.getString(_kFederationMapKey); + final map = json != null + ? Map.from((jsonDecode(json) as Map) + .map((k, v) => MapEntry(k.toString(), v.toString()))) + : {}; + map[googleOrderId] = federation; + await prefs.setString(_kFederationMapKey, jsonEncode(map)); + } catch (e) { + _log.w('Failed to save federation mapping: $e'); + } + } + + static Future getFederationForGoogleOrderId( + String googleOrderId) async { + try { + final prefs = await SharedPreferences.getInstance(); + final json = prefs.getString(_kFederationMapKey); + if (json == null) return null; + final map = (jsonDecode(json) as Map) + .map((k, v) => MapEntry(k.toString(), v.toString())); + final v = map[googleOrderId]?.toString(); + return (v != null && v.isNotEmpty) ? v : null; + } catch (e) { + _log.w('Failed to read federation mapping: $e'); + return null; + } + } + + static Future removeFederationForGoogleOrderId( + String googleOrderId) async { + try { + final prefs = await SharedPreferences.getInstance(); + final json = prefs.getString(_kFederationMapKey); + if (json == null) return; + final map = Map.from((jsonDecode(json) as Map) + .map((k, v) => MapEntry(k.toString(), v.toString()))); + map.remove(googleOrderId); + await prefs.setString(_kFederationMapKey, jsonEncode(map)); + } catch (e) { + _log.w('Failed to remove federation mapping: $e'); + } + } + + static Future> + getUnacknowledgedPurchases() async { + if (defaultTargetPlatform != TargetPlatform.android) { + return []; + } + final iap = InAppPurchase.instance; + if (!await iap.isAvailable()) { + _log.w('Billing not available'); + return []; + } + try { + startPendingPurchaseListener(); + final androidAddition = + iap.getPlatformAddition(); + final response = await androidAddition.queryPastPurchases(); + if (response.error != null) { + _log.w('queryPastPurchases error: ${response.error!.message}'); + return []; + } + + final list = []; + final orderIdsFromQuery = {}; + for (final p in response.pastPurchases) { + final b = p.billingClientPurchase; + orderIdsFromQuery.add(b.orderId); + list.add(UnacknowledgedGooglePayPurchase( + orderId: b.orderId, + productId: p.productID, + payload: GooglePayVerificationPayload( + purchaseData: b.originalJson, + signature: b.signature, + ), + purchaseDetails: p, + )); + } + + await Future.delayed(const Duration(milliseconds: 1500)); + for (final entry in _pendingFromStream.entries) { + if (orderIdsFromQuery.contains(entry.key)) continue; + final p = entry.value; + if (p is! GooglePlayPurchaseDetails) continue; + final b = p.billingClientPurchase; + list.add(UnacknowledgedGooglePayPurchase( + orderId: b.orderId, + productId: p.productID, + payload: GooglePayVerificationPayload( + purchaseData: b.originalJson, + signature: b.signature, + ), + purchaseDetails: p, + )); + } + + _log.d('Found ${list.length} unacknowledged purchases'); + return list; + } catch (e, st) { + _log.w('Failed to get unacknowledged purchases: $e\n$st'); + return []; + } + } + + static Future completeAndConsumePurchase( + PurchaseDetails purchaseDetails) async { + final iap = InAppPurchase.instance; + try { + iap.completePurchase(purchaseDetails); + _log.d('completePurchase executed'); + if (defaultTargetPlatform == TargetPlatform.android) { + final androidAddition = + iap.getPlatformAddition(); + final result = await androidAddition.consumePurchase(purchaseDetails); + final ok = result.responseCode == BillingResponse.ok; + if (ok) { + _log.d('consumePurchase executed'); + } else { + _log.w('consumePurchase failed: ${result.responseCode}'); + } + return ok; + } + return true; + } catch (e, st) { + _log.w('completePurchase/consumePurchase exception: $e\n$st'); + return false; + } + } + + static Future runOrderRecovery({ + required String userId, + required Future Function( + String federation, String sample, String merchant, String asset) + onPaymentCallback, + }) async { + if (defaultTargetPlatform != TargetPlatform.android) return false; + if (userId.isEmpty) { + _log.d('Order recovery skipped: not logged in'); + return false; + } + final pending = await getUnacknowledgedPurchases(); + if (pending.isEmpty) return false; + + _log.d('Order recovery: ${pending.length} pending'); + bool needRefresh = false; + final iap = InAppPurchase.instance; + + for (final p in pending) { + try { + final federation = await getFederationForGoogleOrderId(p.orderId); + if (federation != null && federation.isNotEmpty) { + final res = await onPaymentCallback( + federation, + p.payload.signature, + p.payload.purchaseData, + userId, + ); + if (res.isSuccess) { + if (await completeAndConsumePurchase(p.purchaseDetails)) { + _pendingFromStream.remove(p.orderId); + await removeFederationForGoogleOrderId(p.orderId); + needRefresh = true; + _log.d('Order recovery success: ${p.orderId}'); + } + } else { + _log.w('Order recovery failed: ${p.orderId}, ${res.msg}'); + } + } else { + if (await completeAndConsumePurchase(p.purchaseDetails)) { + _pendingFromStream.remove(p.orderId); + needRefresh = true; + } + } + } catch (e, st) { + _log.w('Order recovery exception: ${p.orderId}: $e\n$st'); + } + } + return needRefresh; + } + + static Future launchPurchaseAndReturnData( + String productId) async { + _log.d('Purchase request for productId: "$productId"'); + if (defaultTargetPlatform != TargetPlatform.android) { + return null; + } + final iap = InAppPurchase.instance; + if (!await iap.isAvailable()) { + _log.w('Billing not available'); + return null; + } + final response = await iap.queryProductDetails({productId}); + if (response.notFoundIDs.isNotEmpty || response.productDetails.isEmpty) { + _log.w('Product not found: $productId'); + return null; + } + final product = response.productDetails.first; + final completer = Completer(); + StreamSubscription>? sub; + + sub = iap.purchaseStream.listen( + (purchases) { + for (final p in purchases) { + if (p.productID != productId) continue; + if (p.status == PurchaseStatus.purchased || + p.status == PurchaseStatus.restored) { + if (!completer.isCompleted && p is GooglePlayPurchaseDetails) { + final b = p.billingClientPurchase; + _pendingFromStream[b.orderId] = p; + completer.complete(GooglePayPurchaseResult( + orderId: b.orderId, + payload: GooglePayVerificationPayload( + purchaseData: b.originalJson, + signature: b.signature, + ), + purchaseDetails: p, + )); + } + sub?.cancel(); + return; + } + if (p.status == PurchaseStatus.error || + p.status == PurchaseStatus.canceled) { + if (!completer.isCompleted) completer.complete(null); + sub?.cancel(); + return; + } + } + }, + onError: (e) { + if (!completer.isCompleted) completer.complete(null); + sub?.cancel(); + }, + ); + + final success = await iap.buyConsumable( + purchaseParam: PurchaseParam(productDetails: product), + autoConsume: false, + ); + if (!success) { + sub?.cancel(); + return null; + } + return completer.future.timeout( + const Duration(seconds: 120), + onTimeout: () { + sub?.cancel(); + return null; + }, + ); + } + + static Future isAvailable() async { + final iap = InAppPurchase.instance; + return iap.isAvailable(); + } + + static Future> queryProductDetails( + Set productIds) async { + final iap = InAppPurchase.instance; + final response = await iap.queryProductDetails(productIds); + return response.productDetails; + } +} diff --git a/lib/src/services/user_api.dart b/lib/src/services/user_api.dart index c2658f5..2067406 100644 --- a/lib/src/services/user_api.dart +++ b/lib/src/services/user_api.dart @@ -1,23 +1,24 @@ import '../api/api_client.dart'; import '../api/api_response.dart'; import '../api/proxy_client.dart'; +import '../entities/user_entities.dart'; /// 用户相关 API(使用原始字段名) abstract final class UserApi { static ProxyClient get _client => ApiClient.instance.proxy; /// 设备快速登录 - /// 参数:deviceId, sign(MD5(deviceId)大写), referer(归因) - static Future fastLogin({ + static Future> fastLogin({ required String deviceId, required String sign, String? referer, String? ch, String? type, }) async { - return _client.request( + return _client.requestEntity( path: '/v1/user/fast_login', method: 'POST', + entityFactory: FastLoginResponse.fromJson, queryParams: { if (ch != null) 'ch': ch, 'pkg': ApiClient.instance.config.packageName, @@ -57,7 +58,7 @@ abstract final class UserApi { } /// 获取用户通用信息 - static Future getCommonInfo({ + static Future> getCommonInfo({ required String app, String? shield, String? userId, @@ -67,9 +68,10 @@ abstract final class UserApi { String? gauntlet, String? pkg, }) async { - return _client.request( + return _client.requestEntity( path: '/v1/user/common_info', method: 'GET', + entityFactory: CommonInfoResponse.fromJson, queryParams: { 'app': app, if (shield != null) 'shield': shield, @@ -84,12 +86,28 @@ abstract final class UserApi { } /// 获取用户账户信息 - static Future getAccount({ + static Future> getAccount({ + required String app, + String? userId, + }) async { + return _client.requestEntity( + path: '/v1/user/account', + method: 'GET', + entityFactory: AccountResponse.fromJson, + queryParams: { + 'app': app, + if (userId != null) 'userId': userId, + }, + ); + } + + /// 删除账号 + static Future deleteAccount({ required String app, String? userId, }) async { return _client.request( - path: '/v1/user/account', + path: '/v1/user/delete', method: 'GET', queryParams: { 'app': app, diff --git a/pubspec.lock b/pubspec.lock index eb5bd8d..a0c4fd3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + adjust_sdk: + dependency: "direct main" + description: + name: adjust_sdk + sha256: "62408962d8b01e0631f4d83ca7300fa43ea7d3b95ad9784fe7a8477e016987e1" + url: "https://pub.dev" + source: hosted + version: "5.5.1" args: dependency: transitive description: @@ -73,11 +81,40 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.3" + facebook_app_events: + dependency: "direct main" + description: + name: facebook_app_events + sha256: "0e183947c222e153ab70b9d93c08da18800c73019b9455f86bf2a0bfff30d51e" + url: "https://pub.dev" + source: hosted + version: "0.26.0" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" http: dependency: "direct main" description: @@ -94,6 +131,38 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + in_app_purchase: + dependency: "direct main" + description: + name: in_app_purchase + sha256: "5cddd7f463f3bddb1d37a72b95066e840d5822d66291331d7f8f05ce32c24b6c" + url: "https://pub.dev" + source: hosted + version: "3.2.3" + in_app_purchase_android: + dependency: "direct main" + description: + name: in_app_purchase_android + sha256: abb254ae159a5a9d4f867795ecb076864faeba59ce015ab81d4cca380f23df45 + url: "https://pub.dev" + source: hosted + version: "0.4.0+8" + in_app_purchase_platform_interface: + dependency: transitive + description: + name: in_app_purchase_platform_interface + sha256: "1d353d38251da5b9fea6635c0ebfc6bb17a2d28d0e86ea5e083bf64244f1fb4c" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + in_app_purchase_storekit: + dependency: transitive + description: + name: in_app_purchase_storekit + sha256: "1d512809edd9f12ff88fce4596a13a18134e2499013f4d6a8894b04699363c93" + url: "https://pub.dev" + source: hosted + version: "0.4.8+1" js: dependency: transitive description: @@ -102,6 +171,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + url: "https://pub.dev" + source: hosted + version: "4.11.0" logger: dependency: "direct main" description: @@ -134,6 +211,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + play_install_referrer: + dependency: "direct main" + description: + name: play_install_referrer + sha256: "7dd808236d35d15199d5e7a5b55488e4343c1b67c4694902d6b95196b22b19cd" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" pointycastle: dependency: transitive description: @@ -142,6 +267,62 @@ packages: url: "https://pub.dev" source: hosted version: "3.9.1" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8374d6200ab33ac99031a852eba4c8eb2170c4bf20778b3e2c9eccb45384fb41" + url: "https://pub.dev" + source: hosted + version: "2.4.21" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -195,5 +376,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" sdks: - dart: ">=3.9.0-0 <4.0.0" + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 5ad4339..18f7f8f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,3 +13,9 @@ dependencies: encrypt: ^5.0.3 crypto: ^3.0.3 logger: ^2.0.2 + adjust_sdk: ^5.5.1 + facebook_app_events: ^0.26.0 + in_app_purchase: ^3.2.0 + in_app_purchase_android: ^0.4.0+8 + play_install_referrer: ^0.5.0 + shared_preferences: ^2.2.2