scan: remote config, feature flags and remote paywalls not told to review (App Review 2.3.1, Play Deceptive behavior): remote-features finding for both stores, medium, with deep links, unless the review notes mention them; check prints it as a warning, validate carries it; warn only, the templates get a commented example line; code obfuscators a low note, minify alone nothing; store-pitfalls 74

This commit is contained in:
CRTkafa 2026-10-18 01:26:32 +08:00
parent 9ba97b7e29
commit 61cb3338e8
13 changed files with 258 additions and 6 deletions

View file

@ -181,11 +181,80 @@ export const SDKS: SdkDef[] = [
match: { npm: ['@capacitor/push-notifications', 'expo-notifications', '@react-native-community/push-notification-ios', 'react-native-push-notification'], pub: ['flutter_local_notifications'], unity: ['com.unity.mobile.notifications'] } },
];
// ---- remotely switchable features and remote paywalls (App Review 2.3.1, Play Deceptive Behavior) ----
// Not in SDKS: they add no data types of their own to the declarations; scan only asks that App Review hears about
// them. Checked 2026-10-18 against each vendor's install docs.
export interface RemoteSdkDef {
id: string;
name: string;
/** remote-config: feature flags / remote config / experiments; paywall: paywall designs, offers or prices set remotely. */
kind: 'remote-config' | 'paywall';
match: Partial<Record<Eco, string[]>>;
/** Only with this in the own sources (an SDK that also does other things, e.g. PostHog analytics). */
needsCode?: RegExp;
docs: string;
}
export const REMOTE_SDKS: RemoteSdkDef[] = [
{ id: 'firebase-remote-config', name: 'Firebase Remote Config', kind: 'remote-config', docs: 'https://firebase.google.com/docs/remote-config',
match: { cocoapods: ['FirebaseRemoteConfig', 'Firebase/RemoteConfig'], spm: ['firebase-ios-sdk#FirebaseRemoteConfig'], gradle: ['com.google.firebase:firebase-config', 'com.google.firebase:firebase-config-ktx'],
npm: ['@react-native-firebase/remote-config', '@capacitor-firebase/remote-config'], pub: ['firebase_remote_config'], unity: ['com.google.firebase.remote-config', 'FirebaseRemoteConfig'] } },
{ id: 'statsig', name: 'Statsig', kind: 'remote-config', docs: 'https://docs.statsig.com/',
match: { cocoapods: ['Statsig'], spm: ['statsig-kit'], gradle: ['com.statsig:android-sdk'], npm: ['statsig-react-native', 'statsig-js', '@statsig/js-client', '@statsig/react-native-bindings', '@statsig/expo-bindings'], pub: ['statsig'] } },
{ id: 'launchdarkly', name: 'LaunchDarkly', kind: 'remote-config', docs: 'https://docs.launchdarkly.com/sdk',
match: { cocoapods: ['LaunchDarkly'], spm: ['launchdarkly/ios-client-sdk'], gradle: ['com.launchdarkly:launchdarkly-android-client-sdk'],
npm: ['@launchdarkly/react-native-client-sdk', 'launchdarkly-react-native-client-sdk', 'launchdarkly-js-client-sdk', '@launchdarkly/js-client-sdk'], pub: ['launchdarkly_flutter_client_sdk'] } },
{ id: 'growthbook', name: 'GrowthBook', kind: 'remote-config', docs: 'https://docs.growthbook.io/',
match: { cocoapods: ['GrowthBook-IOS'], spm: ['growthbook-swift'], gradle: ['io.growthbook.sdk:GrowthBook'], npm: ['@growthbook/growthbook', '@growthbook/growthbook-react'], pub: ['growthbook_sdk_flutter'] } },
{ id: 'optimizely', name: 'Optimizely', kind: 'remote-config', docs: 'https://docs.developers.optimizely.com/feature-experimentation/docs',
match: { cocoapods: ['OptimizelySwiftSDK'], spm: ['optimizely/swift-sdk'], gradle: ['com.optimizely.ab:android-sdk'], npm: ['@optimizely/react-sdk', '@optimizely/optimizely-sdk', '@optimizely/react-native-sdk'], pub: ['optimizely_flutter_sdk'] } },
{ id: 'posthog-flags', name: 'PostHog feature flags', kind: 'remote-config', docs: 'https://posthog.com/docs/feature-flags',
needsCode: /\b(isFeatureEnabled|getFeatureFlag|getFeatureFlagPayload|useFeatureFlag|useFeatureFlagEnabled|onFeatureFlags|reloadFeatureFlags)\b/,
match: { cocoapods: ['PostHog'], spm: ['posthog-ios'], gradle: ['com.posthog:posthog-android', 'com.posthog.android:posthog'], npm: ['posthog-react-native', 'posthog-js'], pub: ['posthog_flutter'] } },
{ id: 'unleash', name: 'Unleash', kind: 'remote-config', docs: 'https://docs.getunleash.io/',
match: { cocoapods: ['UnleashProxyClientSwift'], spm: ['unleash-proxy-client-swift'], gradle: ['io.getunleash:unleash-android'], npm: ['unleash-proxy-client', '@unleash/proxy-client-react'], pub: ['unleash_proxy_client_flutter'] } },
{ id: 'configcat', name: 'ConfigCat', kind: 'remote-config', docs: 'https://configcat.com/docs/',
match: { cocoapods: ['ConfigCat'], spm: ['configcat/swift-sdk'], gradle: ['com.configcat:configcat-android-client'], npm: ['configcat-js', 'configcat-react', 'configcat-js-ssr', '@configcat/sdk'], pub: ['configcat_client'] } },
{ id: 'amplitude-experiment', name: 'Amplitude Experiment', kind: 'remote-config', docs: 'https://amplitude.com/docs/feature-experiment',
match: { cocoapods: ['AmplitudeExperiment'], spm: ['experiment-ios-client'], gradle: ['com.amplitude:experiment-android-client'], npm: ['@amplitude/experiment-js-client', '@amplitude/experiment-react-native-client'] } },
{ id: 'split', name: 'Split', kind: 'remote-config', docs: 'https://help.split.io/',
match: { cocoapods: ['Split'], spm: ['splitio/ios-client'], gradle: ['io.split.client:android-client'], npm: ['@splitsoftware/splitio', '@splitsoftware/splitio-react-native', '@splitsoftware/splitio-react'], pub: ['splitio'] } },
// paywalls whose design, products or prices are set from a dashboard
{ id: 'revenuecat-paywalls', name: 'RevenueCat Paywalls', kind: 'paywall', docs: 'https://www.revenuecat.com/docs/tools/paywalls',
match: { cocoapods: ['RevenueCatUI'], spm: ['purchases-ios#RevenueCatUI'], gradle: ['com.revenuecat.purchases:purchases-ui'], npm: ['react-native-purchases-ui', '@revenuecat/purchases-capacitor-ui'], pub: ['purchases_ui_flutter'] } },
{ id: 'superwall', name: 'Superwall', kind: 'paywall', docs: 'https://superwall.com/docs',
match: { cocoapods: ['SuperwallKit'], spm: ['Superwall-iOS'], gradle: ['com.superwall.sdk:superwall-android'], npm: ['@superwall/react-native-superwall', 'expo-superwall'], pub: ['superwallkit_flutter'] } },
{ id: 'adapty', name: 'Adapty (paywall builder)', kind: 'paywall', docs: 'https://adapty.io/docs/',
match: { cocoapods: ['Adapty', 'AdaptyUI'], spm: ['AdaptySDK-iOS'], gradle: ['io.adapty:android-sdk', 'io.adapty:android-ui'], npm: ['react-native-adapty', '@adapty/capacitor', '@adapty/react-native-ui'], pub: ['adapty_flutter', 'adapty_ui_flutter'], unity: ['com.adapty.unity', 'AdaptySDK'] } },
{ id: 'qonversion', name: 'Qonversion', kind: 'paywall', docs: 'https://documentation.qonversion.io/',
match: { cocoapods: ['Qonversion'], spm: ['qonversion-ios-sdk'], gradle: ['io.qonversion.android.sdk:sdk', 'io.qonversion:sdk'], npm: ['react-native-qonversion', '@qonversion/capacitor-plugin'], pub: ['qonversion_flutter'] } },
{ id: 'purchasely', name: 'Purchasely', kind: 'paywall', docs: 'https://docs.purchasely.com/',
match: { cocoapods: ['Purchasely'], spm: ['Purchasely-iOS'], gradle: ['io.purchasely:core'], npm: ['react-native-purchasely', '@purchasely/cordova-plugin-purchasely'], pub: ['purchasely_flutter'] } },
{ id: 'apphud', name: 'Apphud', kind: 'paywall', docs: 'https://docs.apphud.com/',
match: { cocoapods: ['ApphudSDK'], spm: ['ApphudSDK'], gradle: ['com.apphud:ApphudSDK-Android'], npm: ['@apphud/react-native-apphud-sdk', 'react-native-apphud-sdk'], pub: ['apphud'] } },
];
/** Remote config / paywall use in the own sources without a listed dependency (the web Firebase SDK, a RevenueCat
* paywall presented from the core package). Line-anchored code, never an import line. */
export const REMOTE_CODE: { id: string; name: string; kind: RemoteSdkDef['kind']; re: RegExp }[] = [
{ id: 'firebase-remote-config', name: 'Firebase Remote Config', kind: 'remote-config', re: /\bgetRemoteConfig\(|\bfetchAndActivate\(|\bremoteConfig\(\)\.|RemoteConfig\.remoteConfig\(\)|FirebaseRemoteConfig\.(getInstance|instance)\b|\bFirebase\.RemoteConfig\b/ },
{ id: 'revenuecat-paywalls', name: 'RevenueCat Paywalls', kind: 'paywall', re: /\bpresentPaywall(IfNeeded)?\b|\bRevenueCatUI\b|\bpaywallFooter\(|\bPaywallView\(/ },
];
/** Code obfuscators that hide what the app does (not plain minify: ProGuard / R8 shrinking is normal on Android). */
export const OBFUSCATORS: { name: string; match: Partial<Record<Eco, string[]>> }[] = [
{ name: 'DexGuard', match: { gradle: ['com.guardsquare.dexguard', 'com.guardsquare:dexguard-gradle-plugin', 'dexguard'] } },
{ name: 'iXGuard', match: { cocoapods: ['iXGuard'] } },
{ name: 'JavaScript obfuscator', match: { npm: ['javascript-obfuscator', 'react-native-obfuscating-transformer', 'obfuscator-io-metro-plugin', 'webpack-obfuscator', 'vite-plugin-javascript-obfuscator', 'rollup-plugin-obfuscator'] } },
{ name: 'Beebyte Obfuscator', match: { unity: ['Beebyte'] } },
{ name: 'SwiftShield', match: { cocoapods: ['SwiftShield'], spm: ['swiftshield'] } },
];
export interface DepRef { eco: Eco; name: string; version: string | null; file: string; line: number }
/** Does a dependency match an SDK? Pods match by name or "Name/Sub"; Gradle by "group:artifact" or a "group:" prefix;
* SPM by URL fragment (or "url#Product" for one product of a multi-product repository, matched on the product). */
export function matchesSdk(d: DepRef, s: SdkDef): boolean {
export function matchesSdk(d: DepRef, s: Pick<SdkDef, 'match'>): boolean {
const list = s.match[d.eco] ?? [];
const n = d.name;
return list.some(m => {