# Upgrade/downgrade path without re-subscribing

- **Pattern:** `ab-000417` (`app-store-iap-subscriptions.subscription-lifecycle.upgrade-downgrade`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000417
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000417)

## 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`:

```kotlin
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:
  1. 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.
  2. On Android (Play Billing Library 5+):
     ```kotlin
     val updateParams = SubscriptionUpdateParams.newBuilder()
       .setOldPurchaseToken(currentPurchaseToken)
       .setSubscriptionReplacementMode(SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE)
       .build()
     ```
  3. 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: user-experience, error-resilience

HTML version: https://auditbuffet.com/patterns/ab-000417
