Skip to content

Subscriptions and Vulture Pro

As of 2026-10-19 (vulture-ios 3176c4cc4, vulture-android 85fd9cc6).

Vulture is free, with an optional subscription, Vulture Pro. It is sold monthly and yearly with an introductory free trial. Both platforms sell it through RevenueCat and check one entitlement. The plan for sharing one subscription and one coupon across platforms is in Cross-platform subscriptions.

App Store (iOS, iPadOS, Mac)Google Play (Android)
Release productsbe.fousa.vulture.subscriptions.monthly, be.fousa.vulture.subscriptions.yearly (two products)Subscription vulture_pro with base plans monthly and yearly. RevenueCat names them vulture_pro:monthly and vulture_pro:yearly
Debug buildbe.fousa.vulture.debug.subscriptions.monthly, …yearly (the separate debug app)None: be.fousa.vulture.debug is not on Play, so the debug build has no store
Defined invulture-ios/Packages/Preferences/Sources/Preferences/Vulture.swiftvulture-android/data/billing/src/main/kotlin/be/fousa/vulture/data/billing/SubscriptionProducts.kt, set-up steps in vulture-android/docs/billing-setup.md
Fallback when the offering is emptyAsks the store for the two product ids (fetchKnownProducts())Asks for vulture_pro, which returns both base plans (knownProductIds)
plan analytics valueThe last . component of the product id: monthly or yearlyThe last . or : component: monthly or yearly (SubscriptionOffer.analyticsPlan)
SDKRevenueCat with StoreKit 2 (Modules/App/Sources/Services/Subscriptions/AppStoreSubscription.swift)RevenueCat purchases (:data:billing)
ItemValue
ProjectOne Vulture project. The Android app was added to the project that already held the iOS app (billing-setup.md §3)
Entitlementall. Every Pro feature checks it. iOS: entitlements["all"] in Modules/App/Sources/Services/Subscriptions/PurchaserInfo+Active.swift. Android: SubscriptionProducts.ENTITLEMENT_ID
TrialiOS treats periodType == .trial on all as a trial (isTrialActive)
OfferingThe project’s current offering. Neither app names an offering id
Packages$rc_monthly and $rc_annual, each with the iOS product and the Play base plan attached
Offering metadatashow_paywall_on_first_launch (Boolean): shows the paywall once after the first launch. One switch for both platforms. iOS: AppStoreSubscription.firstLaunchPaywallMetadataKey. Android: SubscriptionProducts.FIRST_LAUNCH_PAYWALL_METADATA_KEY, FirstLaunchPaywall
Other remote configNone. Apart from this metadata key, nothing is switched remotely. iOS’s isFreeTierEnabled is compiled in

API keys. The values are not reproduced here.

PlatformKeyWhere
iOSPublic appl_… SDK keysHard-coded in vulture-ios/Packages/Preferences/Sources/Preferences/Vulture.swift (purchasesAPIKey). Debug and Release have different keys, so the debug app is a separate RevenueCat app
AndroidPublic goog_… SDK keyFrom local.properties or the CI secret REVENUECAT_API_KEY, written into BuildConfig.REVENUECAT_API_KEY (vulture-android/data/billing/build.gradle.kts)

RevenueCat ties a subscription to an app user id, not to a store.

PlatformApp user idWhere
iOSThe CloudKit user record name of iCloud.be.fousa.vulture.cloud, set with Purchases.logIn(recordName). With no iCloud account, RevenueCat’s anonymous idAppStoreSubscription.identifyUser() / identify(with:)
AndroidThe Firebase uid after Google sign-in, set with logIn(uid). On sign-out or account deletion, logOut(), back to a new anonymous customer. A purchase made before sign-in is aliased into the uidvulture-android/app/src/main/kotlin/be/fousa/vulture/account/SubscriptionIdentitySync.kt, decision P8-D14

The two ids never match today. A subscription bought on one platform therefore does not unlock the other. Android’s copy must not promise cross-platform access (P8-D8). The fix is for iOS to log in with the Firebase uid once it joins the Firebase sync: see ios-changes A4 and Sync schema §7.

The Firebase ID token also carries RevenueCat’s custom claim revenueCatEntitlements. When it contains all, the sync storage cap is lifted (vulture-android/firebase/firestore.rules, sync-schema §6).

ProFeature is the full list of gates. iOS: vulture-ios/Packages/Preferences/Sources/Preferences/Pro/ProFeature.swift, decided by ProAccessPolicy. Android: vulture-android/data/billing/src/main/kotlin/be/fousa/vulture/data/billing/ProFeature.kt. Each raw value is also the analytics name and the paywall trigger.

ProFeaturePro unlocksStays freeAndroid
severalFlightsMore than one flight on a map: a competition day, logbook flights side by side, CompareOne flight at a timeGated. A batch opened from another app keeps only its first flight
customAirspaceThe pilot’s own airspace files on the map, instead of openAIP’sopenAIP airspace and its warningsGated
debriefEvery part of the Debrief rating and its answers, and Siri’s flight analysisThe Debrief score and its bandGated
thermalAnalysisThe thermal sheet: climb, centring, 3D viewThermals drawn on the mapGated
taskPlanningCreating and editing a taskOpening, flying and sharing an existing task, including a shared oneNever shown: task planning is out of scope for v1
recordingRecording a flight: the in-flight map, its airspace warnings, the Live ActivityResuming a recording that is already runningNever shown: recording is out of scope for v1
skySightSkySight forecasts and the satellite clouds on the maps (with the pilot’s own SkySight account)n/aNever shown: SkySight is out of scope for v1
statisticsThe logbook’s statistics, year by yearn/aGated
uploadsUploading to WeGlide, SkyLines or Charron.linen/aGated
taskOverridePutting another task over the flights on a map, by picking it or dropping itResetting the override; the task a competition day opens withGated
raceRankingThe race ranking of a competition replay, and Replay Dayn/a (the flights themselves are severalFlights)Gated
syncStorageAndroid only. Cloud storage beyond the free 100 MB. The server enforces the cap; this case exists so the account screen’s upsell can open the paywall with it first100 MB of synced filesGated

Free with no gate at all: the logbook, import, one flight on the map with replay and the barogram, competitions (Soaring Spot, Charron.line), openAIP airspace, waypoints, viewing tasks, the Debrief score, connected-service logins, and sync. Sync is iCloud on iOS, and Firebase on Android up to 100 MB.

Rules shared by both apps (iOS CLAUDE.md, “Vulture is free, with Vulture Pro”):

  • Every feature has one choke point, which asks ProAccess and opens the paywall for that ProFeature.
  • Never hide data a lapsed pilot already has. Files stay listed, tasks open, and recordings resume.
  • A gate never switches a synced setting off. Without Pro, openAIP is shown whatever the airspace setting says, and the setting keeps its saved value.

The free tier is always on in Release. iOS can only turn it off in Debug with -VultureHardPaywall (ProAccess.isFreeTierEnabled, Modules/App/Sources/Services/Subscriptions/ProAccess.swift). With the free tier off, the app becomes a hard paywall. Android has ProAccessPolicy.IS_FREE_TIER_ENABLED, which is always on.

Every paywall event carries trigger, which records what put the paywall up (see Analytics events).

Trigger (name)WheniOSAndroid
a ProFeature raw valueThe pilot reached for that feature without Pro. The paywall leads with that feature’s hero image and headlineYesYes
settingsThe Vulture Pro row in SettingsYesYes
firstLaunchOnce after the first launch, when the offering metadata show_paywall_on_first_launch is trueYesYes. Skipped when the app was started by a link
launchThe hard paywall over the app at launch, only when the free tier is offYes (Debug only)No
promotedA purchase started from the App Store product page (promoted in-app purchase)YesNo: Google Play has no promoted purchases

Sources: iOS vulture-ios/Modules/App/Sources/Services/Subscriptions/PaywallTrigger.swift. Android vulture-android/data/billing/src/main/kotlin/be/fousa/vulture/data/billing/PaywallTrigger.kt.

Paywall screens:

  • iOS: vulture-ios/Packages/SettingsScreens/Sources/SettingsScreens/Subscription/ (design A, one PaywallContext per feature, closed by its button and never by a swipe).
  • Android: :feature:paywall. Android has no Restore row in subscription settings (P8-D13), but the paywall footer keeps Restore.