285 lines
27 KiB
TypeScript
285 lines
27 KiB
TypeScript
// The SDK table `storebound scan` detects: which third-party SDKs a mobile app or game ships, from its dependency files
|
|
// (CocoaPods, Swift Package Manager, Carthage, Gradle and version catalogs, npm, pub, Unity packages and External
|
|
// Dependency Manager XML, Godot addons, Cordova plugins), and what each one means for the store declarations.
|
|
//
|
|
// Maintained by hand. Every entry names the SDK's own data disclosure page (`docs`): that page is the source for the
|
|
// data types listed in `data`, which are the types the SDK collects by default as its vendor documents them for the
|
|
// Play Data safety form and Apple's App Privacy. The app's own configuration can change them (for example
|
|
// non-personalized ads only, analytics collection off), which is why scan proposes and the owner decides.
|
|
// Checked 2026-10-11. When a vendor changes its disclosure, update the entry and the date.
|
|
import type { PlayPurpose } from '../config';
|
|
|
|
export type SdkCategory = 'ads' | 'analytics' | 'attribution' | 'crash' | 'auth' | 'payments' | 'external-payments' | 'push';
|
|
export type Eco = 'cocoapods' | 'spm' | 'carthage' | 'gradle' | 'npm' | 'pub' | 'unity' | 'godot';
|
|
|
|
export interface SdkData {
|
|
/** Play Data safety type id (PSL_...). The App Privacy type follows from it (declarations.ts APPLE_PRIVACY). */
|
|
type: string;
|
|
collect: PlayPurpose[];
|
|
/** Sent to the SDK vendor for its own use (Play "shared"; Apple "third-party advertising" when for ads). */
|
|
share?: PlayPurpose[];
|
|
}
|
|
|
|
export interface SdkDef {
|
|
id: string;
|
|
name: string;
|
|
category: SdkCategory;
|
|
/** Can track across apps (reads IDFA / the Android advertising id, or links data with other companies' data):
|
|
* needs App Tracking Transparency on iOS before it does. */
|
|
tracking: boolean;
|
|
/** Login provider this SDK adds (4.8 Login Services). */
|
|
login?: 'google' | 'facebook' | 'apple' | 'firebase';
|
|
/** Matchers per ecosystem. cocoapods: pod name (or its prefix before "/"); spm: a fragment of the repository URL or
|
|
* the package identity; gradle: "group:artifact" (or "group:" for any artifact of the group); npm / pub: package
|
|
* name; unity: UPM package id or a folder under Assets/; godot: a folder under addons/. */
|
|
match: Partial<Record<Eco, string[]>>;
|
|
data: SdkData[];
|
|
/** Vendor's data disclosure / privacy page: the source for `data`. */
|
|
docs: string;
|
|
/** iOS: first version that ships Apple's privacy manifest (only where the vendor's release notes say so). */
|
|
manifestSince?: string;
|
|
/** Ad SDKs: on Google's Families Self-Certified Ads SDK list (support.google.com/googleplay/android-developer/answer/9859768,
|
|
* checked 2026-10-11), from `since` (the Android SDK version the list names). false = not on the list. */
|
|
families?: { certified: boolean; since?: string; note?: string };
|
|
/** Android: the SDK's own manifest declares com.google.android.gms.permission.AD_ID, so it reaches the merged
|
|
* manifest without the app declaring it (Play "Advertising ID" page names Google Mobile Ads as an example). */
|
|
mergesAdId?: boolean;
|
|
}
|
|
|
|
const ADS_DATA: SdkData[] = [
|
|
{ type: 'PSL_DEVICE_ID', collect: ['advertising', 'analytics', 'fraud'], share: ['advertising'] },
|
|
{ type: 'PSL_APPROX_LOCATION', collect: ['advertising', 'analytics', 'fraud'], share: ['advertising'] },
|
|
{ type: 'PSL_USER_INTERACTION', collect: ['advertising', 'analytics'], share: ['advertising'] },
|
|
{ type: 'PSL_PERFORMANCE_DIAGNOSTICS', collect: ['analytics'] },
|
|
];
|
|
const ANALYTICS_DATA: SdkData[] = [
|
|
{ type: 'PSL_DEVICE_ID', collect: ['analytics'] },
|
|
{ type: 'PSL_USER_INTERACTION', collect: ['analytics'] },
|
|
];
|
|
const CRASH_DATA: SdkData[] = [
|
|
{ type: 'PSL_CRASH_LOGS', collect: ['analytics'] },
|
|
{ type: 'PSL_PERFORMANCE_DIAGNOSTICS', collect: ['analytics'] },
|
|
];
|
|
const LOGIN_DATA: SdkData[] = [
|
|
{ type: 'PSL_NAME', collect: ['account'] },
|
|
{ type: 'PSL_EMAIL', collect: ['account'] },
|
|
{ type: 'PSL_USER_ACCOUNT', collect: ['account'] },
|
|
];
|
|
|
|
export const SDKS: SdkDef[] = [
|
|
// ---- ads (tracking-capable: they read the advertising id when the user allows it) ----
|
|
{ id: 'admob', name: 'Google AdMob', category: 'ads', tracking: true, data: ADS_DATA, manifestSince: '11.2.0', mergesAdId: true,
|
|
families: { certified: true, since: '19.0.0' },
|
|
docs: 'https://developers.google.com/admob/android/privacy/play-data-disclosure',
|
|
match: { cocoapods: ['Google-Mobile-Ads-SDK'], spm: ['swift-package-manager-google-mobile-ads'], gradle: ['com.google.android.gms:play-services-ads'],
|
|
npm: ['react-native-google-mobile-ads', '@capacitor-community/admob', 'admob-plus-cordova', 'expo-ads-admob'], pub: ['google_mobile_ads'],
|
|
unity: ['com.google.ads.mobile', 'GoogleMobileAds'], godot: ['admob'] } },
|
|
{ id: 'applovin', name: 'AppLovin MAX', category: 'ads', tracking: true, data: ADS_DATA,
|
|
families: { certified: false, note: 'AppLovin left the program; Families apps had to move off it by 2023-05-31' },
|
|
docs: 'https://support.axon.ai/en/max/android/overview/data-and-privacy',
|
|
match: { cocoapods: ['AppLovinSDK'], spm: ['applovin-ios-sdk'], gradle: ['com.applovin:applovin-sdk'], npm: ['react-native-applovin-max'],
|
|
pub: ['applovin_max'], unity: ['com.applovin.mediation.ads', 'MaxSdk'] } },
|
|
{ id: 'levelplay', name: 'Unity LevelPlay (ironSource)', category: 'ads', tracking: true, data: ADS_DATA,
|
|
families: { certified: true, since: '7.2.1', note: 'listed as ironSource; every mediated network must be on the list too' },
|
|
docs: 'https://developers.is.com/ironsource-mobile/general/ironsource-mobile-sdk-data-disclosure/',
|
|
match: { cocoapods: ['IronSourceSDK', 'IronSourcePod'], spm: ['LevelPlay-Swift-Package'], gradle: ['com.unity3d.ads-mediation:mediation-sdk', 'com.ironsource.sdk:mediationsdk'],
|
|
npm: ['ironsource-mediation', 'react-native-ironsource-mediation'], pub: ['unity_levelplay_mediation', 'ironsource_mediation'],
|
|
unity: ['com.unity.services.levelplay', 'IronSource', 'LevelPlay'] } },
|
|
{ id: 'unity-ads', name: 'Unity Ads', category: 'ads', tracking: true, data: ADS_DATA,
|
|
families: { certified: true, since: '4.0.1' },
|
|
docs: 'https://docs.unity.com/en-us/grow/ads/privacy/google-data-safety',
|
|
match: { cocoapods: ['UnityAds'], gradle: ['com.unity3d.ads:unity-ads'], pub: ['unity_ads_plugin'], unity: ['com.unity.ads'], npm: ['react-native-unity-ads'] } },
|
|
{ id: 'meta-an', name: 'Meta Audience Network', category: 'ads', tracking: true, data: ADS_DATA,
|
|
families: { certified: false, note: 'not on the list' },
|
|
docs: 'https://developers.facebook.com/docs/audience-network/',
|
|
match: { cocoapods: ['FBAudienceNetwork'], gradle: ['com.facebook.android:audience-network-sdk'], npm: ['react-native-fbads'],
|
|
pub: ['facebook_audience_network'], unity: ['AudienceNetwork'] } },
|
|
// ---- attribution (tracking by design) ----
|
|
{ id: 'appsflyer', name: 'AppsFlyer', category: 'attribution', tracking: true,
|
|
data: [{ type: 'PSL_DEVICE_ID', collect: ['analytics', 'advertising', 'fraud'], share: ['advertising'] }, { type: 'PSL_USER_INTERACTION', collect: ['analytics', 'advertising'] }],
|
|
docs: 'https://www.appsflyer.com/legal/services-privacy-policy/',
|
|
match: { cocoapods: ['AppsFlyerFramework'], spm: ['AppsFlyerFramework'], gradle: ['com.appsflyer:af-android-sdk'], npm: ['react-native-appsflyer', 'cordova-plugin-appsflyer-sdk'],
|
|
pub: ['appsflyer_sdk'], unity: ['appsflyer-unity-plugin', 'AppsFlyer'] } },
|
|
{ id: 'adjust', name: 'Adjust', category: 'attribution', tracking: true,
|
|
data: [{ type: 'PSL_DEVICE_ID', collect: ['analytics', 'advertising', 'fraud'], share: ['advertising'] }, { type: 'PSL_USER_INTERACTION', collect: ['analytics', 'advertising'] }],
|
|
docs: 'https://help.adjust.com/en/article/google-play-data-safety',
|
|
match: { cocoapods: ['Adjust'], spm: ['ios_sdk'], gradle: ['com.adjust.sdk:adjust-android'], npm: ['react-native-adjust'], pub: ['adjust_sdk'], unity: ['com.adjust.sdk', 'Adjust'] } },
|
|
// ---- analytics ----
|
|
{ id: 'firebase-analytics', name: 'Firebase Analytics', category: 'analytics', tracking: false, manifestSince: '10.22.0',
|
|
data: [{ type: 'PSL_DEVICE_ID', collect: ['analytics'] }, { type: 'PSL_USER_INTERACTION', collect: ['analytics'] }, { type: 'PSL_APPROX_LOCATION', collect: ['analytics'] }],
|
|
docs: 'https://firebase.google.com/docs/android/play-data-disclosure',
|
|
match: { cocoapods: ['FirebaseAnalytics', 'Firebase/Analytics'], spm: ['firebase-ios-sdk#FirebaseAnalytics'], gradle: ['com.google.firebase:firebase-analytics'],
|
|
npm: ['@react-native-firebase/analytics', '@capacitor-firebase/analytics', '@capacitor-community/firebase-analytics'], pub: ['firebase_analytics'],
|
|
unity: ['com.google.firebase.analytics', 'FirebaseAnalytics'] } },
|
|
{ id: 'gameanalytics', name: 'GameAnalytics', category: 'analytics', tracking: false, data: ANALYTICS_DATA,
|
|
docs: 'https://gameanalytics.com/privacy-policy',
|
|
match: { cocoapods: ['GA-SDK-IOS', 'GameAnalytics'], gradle: ['com.gameanalytics.sdk:gameanalytics-android'], unity: ['com.gameanalytics.sdk', 'GameAnalytics'],
|
|
pub: ['gameanalytics_sdk'], npm: ['react-native-gameanalytics'] } },
|
|
{ id: 'amplitude', name: 'Amplitude', category: 'analytics', tracking: false, data: ANALYTICS_DATA,
|
|
docs: 'https://amplitude.com/security-and-privacy',
|
|
match: { cocoapods: ['Amplitude', 'AmplitudeSwift'], spm: ['Amplitude-iOS', 'Amplitude-Swift'], gradle: ['com.amplitude:'], npm: ['@amplitude/analytics-react-native', '@amplitude/react-native'],
|
|
pub: ['amplitude_flutter'], unity: ['Amplitude'] } },
|
|
{ id: 'mixpanel', name: 'Mixpanel', category: 'analytics', tracking: false, data: ANALYTICS_DATA,
|
|
docs: 'https://docs.mixpanel.com/docs/privacy/protecting-user-data',
|
|
match: { cocoapods: ['Mixpanel', 'Mixpanel-swift'], spm: ['mixpanel-swift', 'mixpanel-iphone'], gradle: ['com.mixpanel.android:mixpanel-android'], npm: ['mixpanel-react-native'],
|
|
pub: ['mixpanel_flutter'], unity: ['com.mixpanel.unity', 'Mixpanel'] } },
|
|
// ---- crash reporting ----
|
|
{ id: 'crashlytics', name: 'Firebase Crashlytics', category: 'crash', tracking: false, data: CRASH_DATA, manifestSince: '10.22.0',
|
|
docs: 'https://firebase.google.com/docs/android/play-data-disclosure',
|
|
match: { cocoapods: ['FirebaseCrashlytics', 'Firebase/Crashlytics'], spm: ['firebase-ios-sdk#FirebaseCrashlytics'], gradle: ['com.google.firebase:firebase-crashlytics'],
|
|
npm: ['@react-native-firebase/crashlytics', '@capacitor-firebase/crashlytics'], pub: ['firebase_crashlytics'], unity: ['com.google.firebase.crashlytics', 'FirebaseCrashlytics'] } },
|
|
{ id: 'sentry', name: 'Sentry', category: 'crash', tracking: false, data: CRASH_DATA, manifestSince: '8.21.0',
|
|
docs: 'https://docs.sentry.io/platforms/android/data-management/data-collected/',
|
|
match: { cocoapods: ['Sentry'], spm: ['sentry-cocoa'], gradle: ['io.sentry:sentry-android'], npm: ['@sentry/react-native', '@sentry/capacitor'], pub: ['sentry_flutter'], unity: ['io.sentry.unity'] } },
|
|
// ---- sign-in ----
|
|
{ id: 'google-signin', name: 'Google Sign-In', category: 'auth', tracking: false, login: 'google', data: LOGIN_DATA, manifestSince: '7.1.0',
|
|
docs: 'https://developers.google.com/identity/sign-in/ios/start-integrating',
|
|
match: { cocoapods: ['GoogleSignIn'], spm: ['GoogleSignIn-iOS'], gradle: ['com.google.android.gms:play-services-auth', 'com.google.android.libraries.identity.googleid:googleid'],
|
|
npm: ['@react-native-google-signin/google-signin', '@codetrix-studio/capacitor-google-auth', 'expo-google-sign-in'], pub: ['google_sign_in'], unity: ['com.google.signin', 'GoogleSignIn'] } },
|
|
{ id: 'facebook-login', name: 'Facebook Login', category: 'auth', tracking: true, login: 'facebook', data: LOGIN_DATA, manifestSince: '17.0.0',
|
|
docs: 'https://developers.facebook.com/docs/facebook-login/',
|
|
match: { cocoapods: ['FBSDKLoginKit', 'FacebookLogin'], spm: ['facebook-ios-sdk'], gradle: ['com.facebook.android:facebook-login', 'com.facebook.android:facebook-android-sdk'],
|
|
npm: ['react-native-fbsdk-next', '@capacitor-community/facebook-login'], pub: ['flutter_facebook_auth'], unity: ['com.facebook.sdk', 'FacebookSDK'] } },
|
|
{ id: 'apple-signin', name: 'Sign in with Apple', category: 'auth', tracking: false, login: 'apple', data: [{ type: 'PSL_NAME', collect: ['account'] }, { type: 'PSL_EMAIL', collect: ['account'] }],
|
|
docs: 'https://developer.apple.com/sign-in-with-apple/',
|
|
match: { npm: ['expo-apple-authentication', '@invertase/react-native-apple-authentication', '@capacitor-community/apple-sign-in', 'cordova-plugin-sign-in-with-apple'],
|
|
pub: ['sign_in_with_apple', 'the_apple_sign_in'], unity: ['com.lupidan.apple-signin-unity', 'AppleAuth'] } },
|
|
{ id: 'firebase-auth', name: 'Firebase Authentication', category: 'auth', tracking: false, login: 'firebase', data: LOGIN_DATA, manifestSince: '10.22.0',
|
|
docs: 'https://firebase.google.com/docs/android/play-data-disclosure',
|
|
match: { cocoapods: ['FirebaseAuth', 'Firebase/Auth'], spm: ['firebase-ios-sdk#FirebaseAuth'], gradle: ['com.google.firebase:firebase-auth'],
|
|
npm: ['@react-native-firebase/auth', '@capacitor-firebase/authentication'], pub: ['firebase_auth'], unity: ['com.google.firebase.auth', 'FirebaseAuth'] } },
|
|
// ---- payments ----
|
|
{ id: 'play-billing', name: 'Google Play Billing', category: 'payments', tracking: false, data: [{ type: 'PSL_PURCHASE_HISTORY', collect: ['functionality'] }],
|
|
docs: 'https://developer.android.com/google/play/billing',
|
|
match: { gradle: ['com.android.billingclient:billing'] } },
|
|
{ id: 'revenuecat', name: 'RevenueCat', category: 'payments', tracking: false, data: [{ type: 'PSL_PURCHASE_HISTORY', collect: ['functionality', 'analytics'] }, { type: 'PSL_USER_ACCOUNT', collect: ['functionality'] }],
|
|
docs: 'https://www.revenuecat.com/docs/platform-resources/apple-platform-resources/apple-app-privacy',
|
|
match: { cocoapods: ['RevenueCat', 'Purchases'], spm: ['purchases-ios'], gradle: ['com.revenuecat.purchases:purchases'], npm: ['react-native-purchases', '@revenuecat/purchases-capacitor', 'cordova-plugin-purchases'],
|
|
pub: ['purchases_flutter'], unity: ['com.revenuecat.purchases-unity', 'RevenueCat'] } },
|
|
{ id: 'iap-wrapper', name: 'In-app purchase plugin', category: 'payments', tracking: false, data: [{ type: 'PSL_PURCHASE_HISTORY', collect: ['functionality'] }],
|
|
docs: 'https://developer.apple.com/storekit/',
|
|
match: { npm: ['react-native-iap', 'expo-iap', 'expo-in-app-purchases', 'cordova-plugin-purchase', '@capgo/native-purchases'], pub: ['in_app_purchase'], unity: ['com.unity.purchasing'] } },
|
|
// ---- payments outside the stores (fine for physical goods and services; digital goods need in-app purchase) ----
|
|
{ id: 'stripe', name: 'Stripe', category: 'external-payments', tracking: false, data: [{ type: 'PSL_CREDIT_DEBIT_BANK_ACCOUNT_NUMBER', collect: ['functionality', 'fraud'] }, { type: 'PSL_PURCHASE_HISTORY', collect: ['functionality'] }],
|
|
docs: 'https://stripe.com/privacy',
|
|
match: { cocoapods: ['Stripe', 'StripePaymentSheet'], spm: ['stripe-ios'], gradle: ['com.stripe:stripe-android'], npm: ['@stripe/stripe-react-native', '@capacitor-community/stripe'], pub: ['flutter_stripe'] } },
|
|
{ id: 'paypal', name: 'PayPal / Braintree', category: 'external-payments', tracking: false, data: [{ type: 'PSL_PURCHASE_HISTORY', collect: ['functionality'] }],
|
|
docs: 'https://www.paypal.com/us/legalhub/privacy-full',
|
|
match: { cocoapods: ['Braintree', 'PayPalCheckout', 'PayPal'], spm: ['braintree_ios', 'paypal-ios'], gradle: ['com.braintreepayments.api:', 'com.paypal.checkout:', 'com.paypal.android:'],
|
|
npm: ['react-native-paypal', 'react-native-braintree-dropin-ui'], pub: ['flutter_braintree', 'flutter_paypal'] } },
|
|
// ---- push ----
|
|
{ id: 'fcm', name: 'Firebase Cloud Messaging', category: 'push', tracking: false, data: [{ type: 'PSL_DEVICE_ID', collect: ['functionality'] }], manifestSince: '10.22.0',
|
|
docs: 'https://firebase.google.com/docs/android/play-data-disclosure',
|
|
match: { cocoapods: ['FirebaseMessaging', 'Firebase/Messaging'], spm: ['firebase-ios-sdk#FirebaseMessaging'], gradle: ['com.google.firebase:firebase-messaging'],
|
|
npm: ['@react-native-firebase/messaging', '@capacitor-firebase/messaging'], pub: ['firebase_messaging'], unity: ['com.google.firebase.messaging', 'FirebaseMessaging'] } },
|
|
{ id: 'onesignal', name: 'OneSignal', category: 'push', tracking: false, data: [{ type: 'PSL_USER_INTERACTION', collect: ['analytics'] }, { type: 'PSL_PURCHASE_HISTORY', collect: ['analytics'] }],
|
|
docs: 'https://documentation.onesignal.com/docs/google-play-data-safety-requirements',
|
|
match: { cocoapods: ['OneSignalXCFramework', 'OneSignal'], spm: ['OneSignal-XCFramework'], gradle: ['com.onesignal:OneSignal'], npm: ['react-native-onesignal', 'onesignal-cordova-plugin', 'onesignal-expo-plugin'],
|
|
pub: ['onesignal_flutter'], unity: ['com.onesignal.unity', 'OneSignal'] } },
|
|
{ id: 'push-plugin', name: 'Push notifications plugin', category: 'push', tracking: false, data: [],
|
|
docs: 'https://developer.apple.com/documentation/usernotifications',
|
|
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: Pick<SdkDef, 'match'>): boolean {
|
|
const list = s.match[d.eco] ?? [];
|
|
const n = d.name;
|
|
return list.some(m => {
|
|
switch (d.eco) {
|
|
case 'cocoapods': return n === m || n.startsWith(`${m}/`);
|
|
case 'gradle': return m.endsWith(':') ? n.startsWith(m) : n === m;
|
|
case 'spm': {
|
|
const [repo, product] = m.split('#') as [string, string | undefined];
|
|
if (product) return n.toLowerCase() === product.toLowerCase();
|
|
return n.toLowerCase().includes(repo.toLowerCase());
|
|
}
|
|
case 'unity': return n === m || n.toLowerCase() === m.toLowerCase();
|
|
default: return n === m;
|
|
}
|
|
});
|
|
}
|
|
|
|
/** "1.2.3" vs "1.10" numerically; non-numeric parts count as 0. Ranges like "^11.2.0" or "~> 11.0" use their numbers. */
|
|
export function versionAtLeast(v: string, min: string): boolean | null {
|
|
const nums = (s: string) => (/\d+(\.\d+)*/.exec(s)?.[0] ?? '').split('.').filter(Boolean).map(Number);
|
|
const a = nums(v), b = nums(min);
|
|
if (!a.length) return null;
|
|
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
const x = a[i] ?? 0, y = b[i] ?? 0;
|
|
if (x !== y) return x > y;
|
|
}
|
|
return true;
|
|
}
|