diff --git a/lib/client_proxy_framework.dart b/lib/client_proxy_framework.dart index 4909ad7..9a1f8d5 100644 --- a/lib/client_proxy_framework.dart +++ b/lib/client_proxy_framework.dart @@ -29,6 +29,7 @@ export 'src/log/sdk_reminder_log.dart'; export 'src/media/video_thumbnail_cache.dart'; export 'src/services/adjust_service.dart'; export 'src/services/analytics_attribution_callbacks.dart'; +export 'src/services/analytics_events.dart'; export 'src/services/analytics_service.dart'; export 'src/services/auth_service.dart'; export 'src/services/facebook_service.dart'; diff --git a/lib/src/config/ext_config_key_schema.dart b/lib/src/config/ext_config_key_schema.dart index 31806b6..4790b71 100644 --- a/lib/src/config/ext_config_key_schema.dart +++ b/lib/src/config/ext_config_key_schema.dart @@ -16,7 +16,7 @@ import 'dart:convert'; class ExtConfigKeySchema { const ExtConfigKeySchema({ required this.showVideoMenuKeys, - required this.allowScreenshotKeys, + required this.forbidScreenshotKeys, required this.blockScreenshotKeys, required this.allowThirdPartyPaymentKeys, required this.privacyUrlKeys, @@ -44,10 +44,11 @@ class ExtConfigKeySchema { /// 是否展示顶部 Video/分类 Tab,并把 items 固定为最后一格 Tab。 final List showVideoMenuKeys; - /// 为 `true` 时表示**允许**截屏(直接读取这些键的布尔值)。 - final List allowScreenshotKeys; + /// 线网布尔为 `true` 时表示**禁止**截屏的键(默认 `screen`)。 + /// 与 [blockScreenshotKeys] 二选一:[forbidScreenshotKeys] 优先(首个存在的键即生效)。 + final List forbidScreenshotKeys; - /// 为 `true` 时表示**禁止**截屏(与 [allowScreenshotKeys] 互斥推导:本键为 true ⇒ 不允许截屏)。 + /// 备选用键:`true` 同样表示禁止截屏(默认 `safe_area`),仅当 [forbidScreenshotKeys] 未命中任何键时使用。 final List blockScreenshotKeys; /// 是否允许第三方支付。 @@ -125,7 +126,7 @@ class ExtConfigKeySchema { factory ExtConfigKeySchema.defaults() { return const ExtConfigKeySchema( showVideoMenuKeys: ['go_run', 'need_wait'], - allowScreenshotKeys: ['screen'], + forbidScreenshotKeys: ['screen'], blockScreenshotKeys: ['safe_area'], allowThirdPartyPaymentKeys: ['san_fang', 'lucky'], privacyUrlKeys: ['privacy'], @@ -178,10 +179,26 @@ class ExtConfigKeySchema { return rootList(itemKeys, k, fallback); } + /// `skin_config.extConfig.keys`:优先 [primary],为空则回退 [legacy](兼容旧键名),再不行用 [fallback]。 + List mergedRootList( + String primary, + String legacy, + List fallback, + ) { + final p = rootList(keyMap, primary, const []); + if (p.isNotEmpty) return p; + final l = rootList(keyMap, legacy, const []); + if (l.isNotEmpty) return l; + return fallback; + } + return ExtConfigKeySchema( showVideoMenuKeys: rootList(keyMap, 'showVideoMenu', d.showVideoMenuKeys), - allowScreenshotKeys: - rootList(keyMap, 'allowScreenshot', d.allowScreenshotKeys), + forbidScreenshotKeys: mergedRootList( + 'forbidScreenshot', + 'allowScreenshot', + d.forbidScreenshotKeys, + ), blockScreenshotKeys: rootList(keyMap, 'blockScreenshot', d.blockScreenshotKeys), allowThirdPartyPaymentKeys: rootList( diff --git a/lib/src/config/ext_config_models.dart b/lib/src/config/ext_config_models.dart index a31536b..082a0d2 100644 --- a/lib/src/config/ext_config_models.dart +++ b/lib/src/config/ext_config_models.dart @@ -14,7 +14,7 @@ import 'field_mapping.dart'; class ExtConfigData { const ExtConfigData({ this.showVideoMenu, - this.allowScreenshot, + this.forbidScreenshot, this.allowThirdPartyPayment, this.privacyUrl, this.agreementUrl, @@ -22,18 +22,16 @@ class ExtConfigData { }); final bool? showVideoMenu; - final bool? allowScreenshot; + + /// 是否**禁止**截屏(逻辑值)。 + /// + /// 线网键(如 `screen`、`safe_area`)为 `true` 时本字段为 `true`;未命中相关键时为 `null`。 + final bool? forbidScreenshot; final bool? allowThirdPartyPayment; final String? privacyUrl; final String? agreementUrl; final List items; - bool? get shouldPreventCapture { - final a = allowScreenshot; - if (a == null) return null; - return !a; - } - static ExtConfigData empty() => const ExtConfigData(); static ExtConfigData parse( @@ -68,13 +66,14 @@ class ExtConfigData { }) { final showVideo = _readBoolFromKeys(map, schema.showVideoMenuKeys); - bool? allowShot; - allowShot = _readBoolFromKeys(map, schema.allowScreenshotKeys); - if (allowShot == null && schema.blockScreenshotKeys.isNotEmpty) { + bool? forbidScreenshot; + final fromPrimary = _readBoolFromKeys(map, schema.forbidScreenshotKeys); + if (fromPrimary != null) { + forbidScreenshot = fromPrimary; + } else if (schema.blockScreenshotKeys.isNotEmpty) { for (final k in schema.blockScreenshotKeys) { if (!map.containsKey(k)) continue; - final block = _readBool(map, k) == true; - allowShot = !block; + forbidScreenshot = _readBool(map, k) == true; break; } } @@ -131,7 +130,7 @@ class ExtConfigData { return ExtConfigData( showVideoMenu: showVideo, - allowScreenshot: allowShot, + forbidScreenshot: forbidScreenshot, allowThirdPartyPayment: third, privacyUrl: privacy, agreementUrl: agreement, diff --git a/lib/src/config/skin_config.example.json b/lib/src/config/skin_config.example.json index 4f50bca..f5a6c25 100644 --- a/lib/src/config/skin_config.example.json +++ b/lib/src/config/skin_config.example.json @@ -61,7 +61,7 @@ "extConfig": { "keys": { "showVideoMenu": ["go_run", "need_wait"], - "allowScreenshot": ["screen"], + "forbidScreenshot": ["screen"], "blockScreenshot": ["safe_area"], "allowThirdPartyPayment": ["san_fang", "lucky"], "privacyUrl": ["privacy"], diff --git a/lib/src/services/analytics_events.dart b/lib/src/services/analytics_events.dart new file mode 100644 index 0000000..089109e --- /dev/null +++ b/lib/src/services/analytics_events.dart @@ -0,0 +1,135 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../bootstrap/client_bootstrap.dart'; +import '../config/skin_config.dart'; +import '../entities/payment_entities.dart'; +import 'analytics_service.dart'; +import 'facebook_service.dart'; +import 'payment_flow/payment_flow_models.dart'; + +/// 宿主侧业务埋点(Adjust token 来自 `skin_config.json` 的 `adjustEvents` + Facebook App Events)。 +/// +/// 须在 [ClientBootstrap.initFromAsset] / [ClientBootstrap.initFromJson] 与 [ClientBootstrap.initAnalytics] 之后调用。 +/// 行为对齐常见 app 线(如 app_client [AdjustEvents]):档位价、首日充值标记、支付成功/失败。 +abstract final class AnalyticsEvents { + AnalyticsEvents._(); + + static const _prefsRegisterDayKey = + 'client_proxy_framework_skin_analytics_register_day'; + + /// 历史宿主键(FunyMee 曾用),仅用于读取兼容。 + static const _prefsLegacyRegisterDayKey = 'funymee_analytics_register_date'; + + static SkinConfig get _skin => ClientBootstrap.skin; + + /// 从金额文案解析数字(如 `"¥19.99"` / `"\$9.99"`)。 + static num? parsePrice(String? amount) { + if (amount == null || amount.trim().isEmpty) return null; + final m = RegExp(r'[\d.]+').firstMatch(amount); + if (m == null) return null; + return num.tryParse(m.group(0)!); + } + + static String? _tierLogicalNameForPrice(num price) { + final s = price.toStringAsFixed(2); + switch (s) { + case '5.99': + return 'price_599'; + case '9.99': + return 'price_999'; + case '19.99': + return 'price_1999'; + case '49.99': + return 'price_4999'; + case '99.99': + return 'price_9999'; + default: + return null; + } + } + + /// 用户点击某档位发起支付前(按价格匹配 `adjustEvents.price_*`,否则 `purchase`)。 + static void trackTierSelection(PaymentProductItem item) { + final p = parsePrice(item.actualAmount); + if (p == null) return; + final name = _tierLogicalNameForPrice(p); + if (name != null) { + _skin.trackAdjustEvent(name); + } else { + _skin.trackAdjustEvent('purchase'); + } + } + + /// [FastLoginResponse.firstRegister] 为 true 时调用。 + static Future trackRegisterIfNeeded({required bool firstRegister}) async { + if (!firstRegister) return; + _skin.trackAdjustEvent('register'); + AnalyticsService.trackRegister(); + final prefs = await SharedPreferences.getInstance(); + await prefs.setString( + _prefsRegisterDayKey, + DateTime.now().toIso8601String().substring(0, 10), + ); + if (kDebugMode) { + debugPrint('[AnalyticsEvents] register + first-day marker set'); + } + } + + /// 支付成功:Adjust `purchase`;若注册当日则 `firstPurchase`;Facebook [AnalyticsService.trackPurchase]。 + static Future trackPurchaseSuccess(double amount) async { + _skin.trackAdjustEvent('purchase'); + final prefs = await SharedPreferences.getInstance(); + final regDate = prefs.getString(_prefsRegisterDayKey) ?? + prefs.getString(_prefsLegacyRegisterDayKey); + final today = DateTime.now().toIso8601String().substring(0, 10); + if (regDate != null && regDate == today) { + _skin.trackAdjustEvent('firstPurchase'); + } + if (amount > 0) { + AnalyticsService.trackPurchase(amount: amount, currency: 'USD'); + } + } + + /// 使用 [PaymentProductItem.actualAmount] 解析金额后 [trackPurchaseSuccess]。 + static Future trackPurchaseSuccessForProduct( + PaymentProductItem? item) async { + final raw = parsePrice(item?.actualAmount); + final amount = raw?.toDouble() ?? 0.0; + await trackPurchaseSuccess(amount); + } + + /// 支付失败 / 下单失败(Adjust `paymentFailed` + FB `payment_failed`)。 + static void trackPaymentFailed() { + _skin.trackAdjustEvent('paymentFailed'); + FacebookService.logEvent('payment_failed'); + } + + /// 根据支付编排结果 [PaymentSettlement] 自动埋点(成功 → [trackPurchaseSuccessForProduct];失败 → [trackPaymentFailed])。 + /// + /// 取消、超时、[PaymentFlowOutcomeType.nativePendingHostVerification] 不打点。 + static void trackPaymentSettlement( + PaymentSettlement settlement, { + PaymentProductItem? product, + }) { + switch (settlement.type) { + case PaymentFlowOutcomeType.success: + unawaited(trackPurchaseSuccessForProduct(product)); + break; + case PaymentFlowOutcomeType.failure: + trackPaymentFailed(); + break; + case PaymentFlowOutcomeType.cancelled: + case PaymentFlowOutcomeType.timeout: + case PaymentFlowOutcomeType.nativePendingHostVerification: + break; + } + } + + /// Facebook 自定义事件。 + static void trackCustomEvent(String name, {Map? parameters}) { + FacebookService.logEvent(name, parameters: parameters); + } +} diff --git a/lib/src/services/facebook_service.dart b/lib/src/services/facebook_service.dart index ad18559..6907698 100644 --- a/lib/src/services/facebook_service.dart +++ b/lib/src/services/facebook_service.dart @@ -94,4 +94,13 @@ class FacebookService { SdkReminderLog.facebook('logEvent 失败: $e'); } } + + /// 手动上报应用激活(当 `AndroidManifest` / `Info.plist` 关闭自动 App Events 时常用)。 + static void activateApp() { + try { + _fb.activateApp(); + } catch (e) { + SdkReminderLog.facebook('activateApp 失败: $e'); + } + } } diff --git a/lib/src/services/payment_flow/analytics_payment_sink.dart b/lib/src/services/payment_flow/analytics_payment_sink.dart new file mode 100644 index 0000000..9ad491e --- /dev/null +++ b/lib/src/services/payment_flow/analytics_payment_sink.dart @@ -0,0 +1,26 @@ +import '../../entities/payment_entities.dart'; +import '../analytics_events.dart'; +import 'payment_flow_models.dart'; +import 'payment_settlement_sink.dart'; + +/// 在 [inner] 收到结果前写入 Adjust / Facebook 支付相关埋点(见 [AnalyticsEvents.trackPaymentSettlement])。 +/// +/// 宿主只实现 UI / 刷新逻辑的 [PaymentSettlementSink],用本类包一层即可。 +final class PaymentSettlementSinkWithAnalytics implements PaymentSettlementSink { + PaymentSettlementSinkWithAnalytics({ + required this.inner, + this.analyticsProduct, + }); + + final PaymentSettlementSink inner; + final PaymentProductItem? analyticsProduct; + + @override + void onPaymentSettled(PaymentSettlement settlement) { + AnalyticsEvents.trackPaymentSettlement( + settlement, + product: analyticsProduct, + ); + inner.onPaymentSettled(settlement); + } +} diff --git a/lib/src/services/payment_flow/payment_flow.dart b/lib/src/services/payment_flow/payment_flow.dart index 10c6fd7..762e79f 100644 --- a/lib/src/services/payment_flow/payment_flow.dart +++ b/lib/src/services/payment_flow/payment_flow.dart @@ -1,6 +1,7 @@ /// 支付编排(宿主策略 + 框架编排):档位列表、三方建单/轮询、Google Play 内购收口。 library payment_flow; +export 'analytics_payment_sink.dart'; export 'payment_checkout_launcher.dart'; export 'payment_flow_catalog.dart'; export 'payment_flow_models.dart';