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
- 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.
- 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. - 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.
- 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.
- 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
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
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
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 timing | Immediate | Immediate | Immediate (with recommended mode) |
| What's charged today | Max price minus unused Pro, for the rest of the current period | Full Max price. Unused Pro is refunded separately to the original payment method | Same as web (CHARGE_PRORATED_PRICE) |
| Renewal date | Unchanged | Resets to the upgrade date | Unchanged |
| Who controls it | Us (proration_behavior) | Apple only. We control the level ranking | Us (replacement mode per purchase) |
| Downgrade Max → Pro | Autumn schedules it for period end | Takes effect at next renewal (automatic) | Use DEFERRED so it takes effect at renewal |
| Double-subscription risk | Low. Autumn swaps the plan on the same customer | High if Max is in a different subscription group | High 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
iOS
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
- Upgrades take effect immediately on every platform. A user who hits a limit and upgrades should get Max right then. Our upgrade-drivers data shows 76% of purchases happen within an hour of the first paywall view, so a delayed upgrade loses the moment.
- Never charge full price and drop the unused Pro time (e.g. Stripe
proration_behavior: nonewith an immediate reset). Users see that as paying twice, and it leads to refund requests and chargebacks. - Don't delay upgrades until renewal (
DEFERRED, orWITHOUT_PRORATIONfor a paid Pro user). An annual user who upgrades in month 2 would wait 10 months for Max or get it free until renewal. - Downgrades take effect at the end of the paid period on all three platforms. This is Apple's fixed behavior,
DEFERREDon Play, and Autumn's default schedule on web.
2. Web (Stripe via Autumn)
- Call
billing.attachwith the Max plan and leave the defaultprorate_immediately. Autumn charges the saved card straight away without sending the user through a new checkout. - Before confirming, show the exact amount due today and the next renewal (Stripe's invoice preview, or Autumn's preview if it exposes one): "Pay $87.50 today · Renews at $249.99 on Mar 3, 2027."
- Keep the same billing interval: Pro annual → Max annual, Pro monthly → Max monthly. Switching to annual at the same time is fine, but it resets the billing date and bills right away (Stripe applies credit, then charges the new annual price).
- Handle failed payments. If the saved card is declined, keep the user on Pro and show a clear error. Don't leave them with a half-applied Max plan.
3. iOS (App Store via RevenueCat)
- Put Max products in the existing Pro subscription group. Max monthly and Max annual go at level 1, Pro monthly and Pro annual at level 2. With a separate group, Apple lets a user hold both subscriptions and bills both.
- No purchase-code changes are needed. Buying a Max package while Pro is active triggers Apple's upgrade sheet.
- Staying in the same group also keeps a subscriber's "one year of paid service" count, so year-two subscribers stay at Apple's 15% rate after upgrading (Apple: upgrades, downgrades and crossgrades within a group "don't affect the one year of paid service").
- Paywall copy for current Pro subscribers: "You'll be charged $249.99 today for a year of Max. Apple refunds the unused part of your Pro plan to your original payment method."
- Upgrades during a free trial: Apple keeps the introductory offer active alongside the upgraded product (per RevenueCat), so two products in the group show as active. Entitlement logic must choose the highest tier, and this needs a sandbox test before launch.
4. Android (Google Play via RevenueCat)
- Create Max as its own subscription product (e.g.
qaf.maxwith monthly and annual base plans), not as a base plan of Pro, so each tier's price and offers are managed separately. - Pass the current subscription in the purchase call.
react-native-purchasesaccepts it aspurchasePackage(pkg, null, {oldProductIdentifier, replacementMode}). Without it, Play treats Max as a second, separate subscription and bills both. Google Play community threads show this exact problem with Claude Pro → Max. - Choose the mode for each case:
- Paid Pro → Max:
CHARGE_PRORATED_PRICE. It charges the difference, keeps the renewal date, and matches web. - Max → Pro, or monthly ↔ annual on the same tier:
DEFERRED. This requires Google server notifications to be connected to RevenueCat. - Pro trial → Max: see the trial section below.
- Paid Pro → Max:
CHARGE_PRORATED_PRICEonly works when the new plan costs more per unit of time. Pro monthly → Max annual still qualifies if Max annual ÷ 12 is more than Pro monthly; otherwise the purchase fails. Validate this in the client before calling Play.
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 through | User opens upgrade on | What we show |
|---|---|---|
| Web (Stripe) | Web | Direct 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 Store | iOS | Store upgrade sheet |
| App Store | Web / Android | "Your plan is billed by Apple. Upgrade in the Qaf iPhone app." No Stripe checkout. |
| Google Play | Android | Play purchase with the old subscription and replacement mode passed in |
| Google Play | Web / iOS | "Your plan is billed by Google Play. Upgrade in the Qaf Android app." |
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.
- Android
WITHOUT_PRORATIONdoes exactly this: Max starts now and the first charge is at the Max price when the trial ends. (Google's docs suggestCHARGE_PRORATED_PRICE, which ends the trial and charges immediately. Skip that here.) - iOSApple sets this behavior and it can't be changed. The intro offer stays active alongside the upgrade. Confirm in sandbox what is charged and when, then write the copy to match.
- WebConfirm in an Autumn sandbox how
attachtreats a trialing customer (keeps the trial end, or charges now). If it charges immediately, pass the remaining trial explicitly, or only show the Max trial offer to trialing users.
Implementation checklist
| # | Item | Where |
|---|---|---|
| 1 | Max monthly and annual in the existing subscription group, level 1; Pro at level 2 | App Store Connect |
| 2 | New Play subscription qaf.max with monthly and annual base plans, plus trial offer if wanted | Play Console |
| 3 | max entitlement in RevenueCat. Gate by highest active tier so an overlapping trial and paid product can't downgrade someone | RevenueCat + packages/billing gating |
| 4 | Pass GoogleProductChangeInfo when an active Play subscription exists (apps/native/src/billing/revenuecat-client.ts:470 currently calls purchasePackage(pkg) with nothing else) | Native |
| 5 | Read the store the plan is billed through (RC store, Autumn/Stripe) and show the matching row of the cross-platform table | Web + native paywalls |
| 6 | Show the amount due today and the renewal amount and date before confirming, with different copy for each platform | Web + native paywalls, 13 locales |
| 7 | max plan in Autumn. Confirm upgrade (prorate), downgrade (scheduled) and trial behavior in sandbox | Autumn |
| 8 | Sandbox matrix: paid monthly/annual × trialing × each platform, checking for double subscriptions and entitlement flicker | QA |
| 9 | Analytics: billing_upgrade_started/completed with from_plan, to_plan, store, amount_today, preferably fired server-side from webhooks | Server |
| 10 | Help Center article "Upgrading to Max" with the three billing explanations and the "upgrade where you subscribed" rule | apps/help-center |
Sources
- Apple: Auto-renewable subscription information (upgrade: prorated refund, full charge, "date of upgrade is the renewal date")
- Apple: Auto-renewable subscriptions (levels, and changes within a group don't affect one year of paid service)
- Apple: Handling Subscriptions Billing
- Google: ReplacementMode reference · Subscription replacements codelab
- RevenueCat: Upgrades, Downgrades & Management (Apple intro-offer overlap, Play modes) · Play plan-change guide
- Stripe: Prorations · Update a subscription
- Autumn: Proration · Subscription lifecycle
- Peers: OpenAI: managing your subscription across devices · Claude: paid plan billing FAQs · Play community: Claude Pro→Max double subscription
- Link-out rules: TechCrunch, Apr 29 2026 (US commission-free link-outs still allowed while Apple appeals to the Supreme Court)