Qaf · Billing research · September 16, 2026

How users upgrade from Pro to Max on web, iOS and Android

Do upgraders pay the difference or the full new price? Each platform answers differently. This report covers what Stripe, Apple and Google actually do, which of those behaviors we can control, and a recommended policy for Qaf.

Short answer

  1. Best practice is an immediate upgrade with credit for unused time. The user gets Max now, and whatever they already paid for Pro counts toward it. No platform makes them pay the full price for time they already paid for.
  2. Web and Android can charge "just the difference." On web, Stripe (through Autumn) charges the prorated difference and keeps the same renewal date. On Android, we get the same result by passing CHARGE_PRORATED_PRICE.
  3. iOS can't. Apple handles this itself and we can't configure it. Apple refunds the unused part of Pro to the original payment method, charges the full Max price, and starts a new billing year from today. The user's net cost is about the same, but the card shows a full charge plus a separate refund.
  4. Users upgrade on the platform where they subscribed. A store subscription can't be changed from another platform. Every paywall has to detect where the current plan is billed and send the user there.
  5. Two setup mistakes would double-bill people. On iOS, Max must go in the same subscription group as Pro, ranked above it. On Android, the purchase call must include the old subscription. Our current native code (purchasePackage(pkg)) doesn't include it yet.

What each platform does

Web · Stripe via Autumn

Pays the prorated difference now

Autumn's default for upgrades is prorate_immediately: it credits unused Pro time, charges Max for the rest of the period, and bills the net amount right away. The renewal date stays the same, and the plan renews at the full Max price.

iOS · App Store

Refund of unused Pro, full Max charge

If Max is ranked above Pro in the same group, Apple upgrades the user immediately. It refunds the prorated Pro amount, charges the full Max price, and makes today the new renewal date. There's no code or setting that changes this.

Android · Google Play

Our choice: we pick a "replacement mode"

We choose the mode in the purchase call. CHARGE_PRORATED_PRICE charges the difference now and keeps the billing date, which matches web. Google recommends it for upgrading to a more expensive tier.

Side-by-side

Web (Stripe/Autumn)iOS (App Store)Android (Play)
Upgrade timingImmediateImmediateImmediate (with recommended mode)
What's charged todayMax price minus unused Pro, for the rest of the current periodFull Max price. Unused Pro is refunded separately to the original payment methodSame as web (CHARGE_PRORATED_PRICE)
Renewal dateUnchangedResets to the upgrade dateUnchanged
Who controls itUs (proration_behavior)Apple only. We control the level rankingUs (replacement mode per purchase)
Downgrade Max → ProAutumn schedules it for period endTakes effect at next renewal (automatic)Use DEFERRED so it takes effect at renewal
Double-subscription riskLow. Autumn swaps the plan on the same customerHigh if Max is in a different subscription groupHigh if the old purchase isn't included in the call

Worked example: an annual subscriber

Most Qaf subscribers are annual, so this is the case that matters. Adjust the inputs below; the Max price is only a placeholder until pricing is set. Store amounts are before tax and rounding, and Apple and Google calculate the exact figures themselves.

Web & Android

$87.50
charged today. Renews at $249.99 on the original renewal date, 7 months from now.

iOS

$249.99
charged today, with about $58.33 refunded to the original payment method. Net about $191.66, which buys a full 12 months of Max from today.

Measured per month of Max, both approaches cost about the same; Apple just sells a longer period. The two things to handle on iOS are how it looks and what support hears: the user sees a large charge and a refund that arrives a few days later. Paywall copy should explain that before they tap.

Recommended policy for Qaf

1. Pricing rule: credit unused time and switch now

2. Web (Stripe via Autumn)

3. iOS (App Store via RevenueCat)

4. Android (Google Play via RevenueCat)

5. Cross-platform: upgrade where you subscribed

A subscription can only be changed through the system that bills it: we can't upgrade an Apple subscription from Stripe, or the reverse. ChatGPT and Claude both tell users to manage a plan where they bought it and to cancel before switching platforms, because starting a second subscription elsewhere double-bills them.

Pro billed throughUser opens upgrade onWhat we show
Web (Stripe)WebDirect upgrade (Autumn attach)
Web (Stripe)iOS / Android"Your plan is billed on qaf.ai. Upgrade there." Hide store Max purchase buttons so we don't create a second subscription. On iOS, a clickable link to web checkout is allowed only on the US storefront, where the Epic ruling is still under appeal. Elsewhere, use plain text.
App StoreiOSStore upgrade sheet
App StoreWeb / Android"Your plan is billed by Apple. Upgrade in the Qaf iPhone app." No Stripe checkout.
Google PlayAndroidPlay purchase with the old subscription and replacement mode passed in
Google PlayWeb / iOS"Your plan is billed by Google Play. Upgrade in the Qaf Android app."
Don't offer "cancel your store plan and buy Max on web"We can't refund App Store purchases, and Play refunds are limited, so the user would pay for both until the store plan runs out. Only suggest it to users who ask to move platforms, and say that clearly.

6. Trials: most current Pro subscribers

90% of conversions start as a 7-day annual trial, so at any moment many "Pro" users are still in their trial. Recommendation: a trial user who upgrades switches to a Max trial with the remaining days, and pays for Max when the trial ends. Nobody should be charged before the trial ends.

Implementation checklist

#ItemWhere
1Max monthly and annual in the existing subscription group, level 1; Pro at level 2App Store Connect
2New Play subscription qaf.max with monthly and annual base plans, plus trial offer if wantedPlay Console
3max entitlement in RevenueCat. Gate by highest active tier so an overlapping trial and paid product can't downgrade someoneRevenueCat + packages/billing gating
4Pass GoogleProductChangeInfo when an active Play subscription exists (apps/native/src/billing/revenuecat-client.ts:470 currently calls purchasePackage(pkg) with nothing else)Native
5Read the store the plan is billed through (RC store, Autumn/Stripe) and show the matching row of the cross-platform tableWeb + native paywalls
6Show the amount due today and the renewal amount and date before confirming, with different copy for each platformWeb + native paywalls, 13 locales
7max plan in Autumn. Confirm upgrade (prorate), downgrade (scheduled) and trial behavior in sandboxAutumn
8Sandbox matrix: paid monthly/annual × trialing × each platform, checking for double subscriptions and entitlement flickerQA
9Analytics: billing_upgrade_started/completed with from_plan, to_plan, store, amount_today, preferably fired server-side from webhooksServer
10Help Center article "Upgrading to Max" with the three billing explanations and the "upgrade where you subscribed" ruleapps/help-center

Sources