Skip to content

Add proration mode for subscription upgrades / downgrades #3

@proggeramlug

Description

@proggeramlug

Today `js_play_billing_purchase(productId)` only supports buying a brand-new subscription — there's no way to upgrade an existing subscription to a higher tier or downgrade to a lower one. Apps that offer multiple subscription tiers will hit this immediately.

Play Billing API

`BillingFlowParams` has a `SubscriptionUpdateParams` builder that takes the current subscription's purchase token plus a `replacementMode` enum:

  • `CHARGE_FULL_PRICE` — charge the new price, no proration
  • `CHARGE_PRORATED_PRICE` — charge the difference for the rest of the period
  • `WITHOUT_PRORATION` — effective on next renewal
  • `WITH_TIME_PRORATION` — extend the period based on credit
  • `DEFERRED` — change applies on next renewal, no charge now

Each mode has different UX implications. Apps need to expose the choice, or pick one based on policy.

Proposed API

Add a third optional parameter to `purchase`:

```typescript
export type ReplacementMode =
| "chargeFullPrice"
| "chargeProratedPrice"
| "withoutProration"
| "withTimeProration"
| "deferred";

export type PurchaseOptions = {
offerToken?: string;
/** Pass to upgrade / downgrade an existing subscription. */
replaceFor?: {
oldPurchaseToken: string;
replacementMode: ReplacementMode;
};
};

export declare function js_play_billing_purchase(
productId: string,
options?: PurchaseOptions,
): Promise;
```

(This obsoletes the simpler signature in #2 — they should land together. Or land #2 first as a milestone.)

Why deferred

Subscription upgrades have non-trivial UX implications (the user is changing their plan, not buying a new one). v0.1.x intentionally only covers the new-purchase case so apps can ship one-tier subscriptions. Multi-tier apps need this, but they're a smaller subset.

Work

  • FFI declaration: pass options as a JSON string and parse on the Kotlin side (lets us add fields without touching the JNI signature) — or pick a flat `(productId, offerToken, oldToken, replacementMode)` shape and accept "" for absence. Decide which is cleaner.
  • Rust: encode/forward the options object to Kotlin.
  • Kotlin: parse, build `SubscriptionUpdateParams`, attach to `BillingFlowParams`.
  • Document the upgrade flow + the proration semantics in README.
  • Cross-platform note: StoreKit upgrades/downgrades work differently (Product.subscription.subscriptionGroupID + Transaction.upgrade); the cross-platform README example needs updating to show both flows side by side.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions