优化:禁止截屏字段语义优化
This commit is contained in:
parent
f14474a39f
commit
7192c7275e
@ -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';
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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"],
|
||||||
|
|||||||
135
lib/src/services/analytics_events.dart
Normal file
135
lib/src/services/analytics_events.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
lib/src/services/payment_flow/analytics_payment_sink.dart
Normal file
26
lib/src/services/payment_flow/analytics_payment_sink.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user