优化:禁止截屏字段语义优化

This commit is contained in:
ivan 2026-04-14 15:29:21 +08:00
parent f14474a39f
commit 7192c7275e
8 changed files with 210 additions and 22 deletions

View File

@ -29,6 +29,7 @@ export 'src/log/sdk_reminder_log.dart';
export 'src/media/video_thumbnail_cache.dart'; export 'src/media/video_thumbnail_cache.dart';
export 'src/services/adjust_service.dart'; export 'src/services/adjust_service.dart';
export 'src/services/analytics_attribution_callbacks.dart'; export 'src/services/analytics_attribution_callbacks.dart';
export 'src/services/analytics_events.dart';
export 'src/services/analytics_service.dart'; export 'src/services/analytics_service.dart';
export 'src/services/auth_service.dart'; export 'src/services/auth_service.dart';
export 'src/services/facebook_service.dart'; export 'src/services/facebook_service.dart';

View File

@ -16,7 +16,7 @@ import 'dart:convert';
class ExtConfigKeySchema { class ExtConfigKeySchema {
const ExtConfigKeySchema({ const ExtConfigKeySchema({
required this.showVideoMenuKeys, required this.showVideoMenuKeys,
required this.allowScreenshotKeys, required this.forbidScreenshotKeys,
required this.blockScreenshotKeys, required this.blockScreenshotKeys,
required this.allowThirdPartyPaymentKeys, required this.allowThirdPartyPaymentKeys,
required this.privacyUrlKeys, required this.privacyUrlKeys,
@ -44,10 +44,11 @@ class ExtConfigKeySchema {
/// 是否展示顶部 Video/分类 Tab,并把 items 固定为最后一格 Tab。 /// 是否展示顶部 Video/分类 Tab,并把 items 固定为最后一格 Tab。
final List<String> showVideoMenuKeys; final List<String> showVideoMenuKeys;
/// 为 `true` 时表示**允许**截屏(直接读取这些键的布尔值)。 /// 线网布尔为 `true` 时表示**禁止**截屏的键(默认 `screen`)。
final List<String> allowScreenshotKeys; /// 与 [blockScreenshotKeys] 二选一:[forbidScreenshotKeys] 优先(首个存在的键即生效)。
final List<String> forbidScreenshotKeys;
/// 为 `true` 时表示**禁止**截屏(与 [allowScreenshotKeys] 互斥推导:本键为 true ⇒ 不允许截屏)。 /// 备选用键:`true` 同样表示禁止截屏(默认 `safe_area`),仅当 [forbidScreenshotKeys] 未命中任何键时使用。
final List<String> blockScreenshotKeys; final List<String> blockScreenshotKeys;
/// 是否允许第三方支付。 /// 是否允许第三方支付。
@ -125,7 +126,7 @@ class ExtConfigKeySchema {
factory ExtConfigKeySchema.defaults() { factory ExtConfigKeySchema.defaults() {
return const ExtConfigKeySchema( return const ExtConfigKeySchema(
showVideoMenuKeys: ['go_run', 'need_wait'], showVideoMenuKeys: ['go_run', 'need_wait'],
allowScreenshotKeys: ['screen'], forbidScreenshotKeys: ['screen'],
blockScreenshotKeys: ['safe_area'], blockScreenshotKeys: ['safe_area'],
allowThirdPartyPaymentKeys: ['san_fang', 'lucky'], allowThirdPartyPaymentKeys: ['san_fang', 'lucky'],
privacyUrlKeys: ['privacy'], privacyUrlKeys: ['privacy'],
@ -178,10 +179,26 @@ class ExtConfigKeySchema {
return rootList(itemKeys, k, fallback); return rootList(itemKeys, k, fallback);
} }
/// `skin_config.extConfig.keys`:优先 [primary],为空则回退 [legacy](兼容旧键名),再不行用 [fallback]。
List<String> mergedRootList(
String primary,
String legacy,
List<String> fallback,
) {
final p = rootList(keyMap, primary, const <String>[]);
if (p.isNotEmpty) return p;
final l = rootList(keyMap, legacy, const <String>[]);
if (l.isNotEmpty) return l;
return fallback;
}
return ExtConfigKeySchema( return ExtConfigKeySchema(
showVideoMenuKeys: rootList(keyMap, 'showVideoMenu', d.showVideoMenuKeys), showVideoMenuKeys: rootList(keyMap, 'showVideoMenu', d.showVideoMenuKeys),
allowScreenshotKeys: forbidScreenshotKeys: mergedRootList(
rootList(keyMap, 'allowScreenshot', d.allowScreenshotKeys), 'forbidScreenshot',
'allowScreenshot',
d.forbidScreenshotKeys,
),
blockScreenshotKeys: blockScreenshotKeys:
rootList(keyMap, 'blockScreenshot', d.blockScreenshotKeys), rootList(keyMap, 'blockScreenshot', d.blockScreenshotKeys),
allowThirdPartyPaymentKeys: rootList( allowThirdPartyPaymentKeys: rootList(

View File

@ -14,7 +14,7 @@ import 'field_mapping.dart';
class ExtConfigData { class ExtConfigData {
const ExtConfigData({ const ExtConfigData({
this.showVideoMenu, this.showVideoMenu,
this.allowScreenshot, this.forbidScreenshot,
this.allowThirdPartyPayment, this.allowThirdPartyPayment,
this.privacyUrl, this.privacyUrl,
this.agreementUrl, this.agreementUrl,
@ -22,18 +22,16 @@ class ExtConfigData {
}); });
final bool? showVideoMenu; final bool? showVideoMenu;
final bool? allowScreenshot;
/// 是否**禁止**截屏(逻辑值)。
///
/// 线网键(如 `screen`、`safe_area`)为 `true` 时本字段为 `true`;未命中相关键时为 `null`。
final bool? forbidScreenshot;
final bool? allowThirdPartyPayment; final bool? allowThirdPartyPayment;
final String? privacyUrl; final String? privacyUrl;
final String? agreementUrl; final String? agreementUrl;
final List<ExtConfigItem> items; final List<ExtConfigItem> items;
bool? get shouldPreventCapture {
final a = allowScreenshot;
if (a == null) return null;
return !a;
}
static ExtConfigData empty() => const ExtConfigData(); static ExtConfigData empty() => const ExtConfigData();
static ExtConfigData parse( static ExtConfigData parse(
@ -68,13 +66,14 @@ class ExtConfigData {
}) { }) {
final showVideo = _readBoolFromKeys(map, schema.showVideoMenuKeys); final showVideo = _readBoolFromKeys(map, schema.showVideoMenuKeys);
bool? allowShot; bool? forbidScreenshot;
allowShot = _readBoolFromKeys(map, schema.allowScreenshotKeys); final fromPrimary = _readBoolFromKeys(map, schema.forbidScreenshotKeys);
if (allowShot == null && schema.blockScreenshotKeys.isNotEmpty) { if (fromPrimary != null) {
forbidScreenshot = fromPrimary;
} else if (schema.blockScreenshotKeys.isNotEmpty) {
for (final k in schema.blockScreenshotKeys) { for (final k in schema.blockScreenshotKeys) {
if (!map.containsKey(k)) continue; if (!map.containsKey(k)) continue;
final block = _readBool(map, k) == true; forbidScreenshot = _readBool(map, k) == true;
allowShot = !block;
break; break;
} }
} }
@ -131,7 +130,7 @@ class ExtConfigData {
return ExtConfigData( return ExtConfigData(
showVideoMenu: showVideo, showVideoMenu: showVideo,
allowScreenshot: allowShot, forbidScreenshot: forbidScreenshot,
allowThirdPartyPayment: third, allowThirdPartyPayment: third,
privacyUrl: privacy, privacyUrl: privacy,
agreementUrl: agreement, agreementUrl: agreement,

View File

@ -61,7 +61,7 @@
"extConfig": { "extConfig": {
"keys": { "keys": {
"showVideoMenu": ["go_run", "need_wait"], "showVideoMenu": ["go_run", "need_wait"],
"allowScreenshot": ["screen"], "forbidScreenshot": ["screen"],
"blockScreenshot": ["safe_area"], "blockScreenshot": ["safe_area"],
"allowThirdPartyPayment": ["san_fang", "lucky"], "allowThirdPartyPayment": ["san_fang", "lucky"],
"privacyUrl": ["privacy"], "privacyUrl": ["privacy"],

View File

@ -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<void> 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<void> 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<void> 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<String, dynamic>? parameters}) {
FacebookService.logEvent(name, parameters: parameters);
}
}

View File

@ -94,4 +94,13 @@ class FacebookService {
SdkReminderLog.facebook('logEvent 失败: $e'); SdkReminderLog.facebook('logEvent 失败: $e');
} }
} }
/// 手动上报应用激活(当 `AndroidManifest` / `Info.plist` 关闭自动 App Events 时常用)。
static void activateApp() {
try {
_fb.activateApp();
} catch (e) {
SdkReminderLog.facebook('activateApp 失败: $e');
}
}
} }

View File

@ -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);
}
}

View File

@ -1,6 +1,7 @@
/// 支付编排(宿主策略 + 框架编排):档位列表、三方建单/轮询、Google Play 内购收口。 /// 支付编排(宿主策略 + 框架编排):档位列表、三方建单/轮询、Google Play 内购收口。
library payment_flow; library payment_flow;
export 'analytics_payment_sink.dart';
export 'payment_checkout_launcher.dart'; export 'payment_checkout_launcher.dart';
export 'payment_flow_catalog.dart'; export 'payment_flow_catalog.dart';
export 'payment_flow_models.dart'; export 'payment_flow_models.dart';