Upgrade/downgrade path without re-subscribing
Why it matters
Manually calling cancelSubscription() followed by purchaseSubscription() to move a Monthly subscriber to Annual creates a coverage gap, risks double-charging the user during the transition, and can trigger Play Store purchase rejections because Google expects proration via SubscriptionUpdateParams. The error-resilience cost is high: failed upgrades strand users between plans with unclear entitlement state and drive refund requests. Both platforms ship a first-class upgrade/downgrade path — bypassing it is the failure mode here.
Severity rationale
Low because it only affects users actively changing tiers, though each affected user risks a double charge.
Remediation
Use the platform's native upgrade/downgrade mechanism. On iOS, place all subscription products in the same Subscription Group in App Store Connect — Apple handles proration when the user purchases a higher tier from the same group. On Android, pass the existing purchase token through SubscriptionUpdateParams:
val updateParams = SubscriptionUpdateParams.newBuilder()
.setOldPurchaseToken(currentPurchaseToken)
.setSubscriptionReplacementMode(SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE)
.build()
For RevenueCat, call purchasePackage() with the target package in the same subscription group — the SDK routes to the platform upgrade path automatically.
Detection
-
ID:
upgrade-downgrade -
Severity:
low -
What to look for: Count all relevant instances and enumerate each. If the app offers multiple subscription tiers (e.g., Basic and Pro, or Monthly and Annual), check how plan changes are handled. Look for: (1) Whether the app shows a plan comparison or upgrade prompt to existing subscribers. (2) Whether tapping "Upgrade to Annual" for an existing Monthly subscriber triggers the platform's upgrade flow rather than a new subscription purchase. For iOS StoreKit 2:
product.subscribe(options: Product.SubscriptionInfo.RenewalInfo.UpgradeSubscriptionOption(...)). For RevenueCat: purchasing a package in the same subscription group as the current subscription will automatically trigger an upgrade/downgrade through the platform. The critical failure is an app that cancels the current subscription and starts a new one manually, which can cause a gap in coverage, duplicate charges, or rejected purchases from the store. Also check for a downgrade path — can a Pro subscriber move to Basic without losing access or contacting support? -
Pass criteria: App has a single subscription tier (skip), or multi-tier subscriptions use the platform's upgrade/downgrade mechanism (same subscription group on iOS,
setSubscriptionUpdateParamsAsync()on Android) rather than cancel-and-resubscribe. At least 1 implementation must be verified. -
Fail criteria: Multi-tier subscriptions present but the upgrade path explicitly cancels the current subscription before starting a new one; or no upgrade path exists (users must cancel and resubscribe to change plans).
-
Skip (N/A) when: App has only a single subscription tier or no subscription at all.
-
Detail on fail:
"UpgradeScreen.tsx calls cancelSubscription() followed by purchaseSubscription() for plan changes — this creates a gap in access and may cause double charges"or"No upgrade/downgrade path exists — plan change instructions tell users to cancel and resubscribe manually" -
Remediation: Use the platform's built-in upgrade/downgrade mechanism:
- On iOS, place all subscription products in the same subscription group in App Store Connect. When a subscriber purchases a higher-tier product in the same group, Apple handles the proration automatically.
- On Android (Play Billing Library 5+):
val updateParams = SubscriptionUpdateParams.newBuilder() .setOldPurchaseToken(currentPurchaseToken) .setSubscriptionReplacementMode(SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE) .build() - For RevenueCat: simply call
purchasePackage()with the target package — RevenueCat handles upgrade/downgrade within the same subscription group
-
Cross-reference: For related patterns and deeper analysis, see the corresponding checks in other AuditBuffet audits covering this domain.
Taxons
History
- 2026-04-18·v1.0.0·Initial import from app-store-iap-subscriptions·automated