Skip to main content
skill CX Codex

rc-plan-changes — Claude Code skill: what it does, how to install

Guides subscription upgrades, downgrades, and plan changes for RevenueCat on Android with proper replacement modes.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add RevenueCat/ai-toolkit --skill rc-plan-changes -a claude-code
Open on GitHub

What rc-plan-changes does

This skill guides your coding agent through subscription switches on Android using RevenueCat. It configures PurchaseParams.Builder with GoogleProductChangeInfo for upgrades, downgrades, and trial conversions when a user already holds an active Google Play subscription.

The workflow maps out GoogleReplacementMode choices. You apply WITH_TIME_PRORATION for standard upgrades or CHARGE_PRORATED_PRICE to charge immediately during free trials. Google rejects DEFERRED for upgrades, so the skill restricts that mode strictly to downgrades.

Kotlin examples show how to slice currentProductId using substringBefore(":") from activeSubscriptions to drop the base plan suffix. The setup assumes Google Play Billing Library 7 or newer via the RevenueCat SDK.

What it helps with

  • switch an active monthly subscription to annual with time proration
  • configure deferred downgrades that apply at next renewal
  • parse the raw product identifier from activeSubscriptions cleanly
  • prevent unpaid premium access during active trial upgrades

Example prompts for the agent

  • Configure an upgrade from monthly to annual subscription using RevenueCat on Android
  • Implement a deferred subscription downgrade with GoogleProductChangeInfo in Kotlin
  • How do I pass oldProductId properly to PurchaseParams when changing plans?

How to install rc-plan-changes

1Skills CLI (Vercel)

npx skills add RevenueCat/ai-toolkit --skill rc-plan-changes -a claude-code

2Manually: put the files in place

~/.claude/skills/rc-plan-changes/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/rc-plan-changes/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about rc-plan-changes

How to install rc-plan-changes in Claude Code?

Run npx skills add RevenueCat/ai-toolkit --skill rc-plan-changes -a claude-code in your terminal.

What does the rc-plan-changes skill do?

It provides instructions and Kotlin patterns for changing Google Play subscriptions via RevenueCat. The skill explains how to pick replacement modes and configure GoogleProductChangeInfo.

Why should DEFERRED mode be avoided for upgrades?

Google Play rejects deferred upgrade requests. The DEFERRED replacement mode is valid only for downgrades that take effect at the next renewal.

Source text

SKILL.md · MIT
---
name: rc-plan-changes
description: Use this skill when implementing subscription upgrades, downgrades, and plan switches via RevenueCat on Android. Covers GoogleProductChangeInfo, picking a Google replacement mode, and letting the SDK manage linkedPurchaseToken chaining.
license: Apache-2.0; see LICENSE
metadata:
  author: RevenueCat
  source: revenuecat-handbook chapter 7
  keywords:
  - android
  - revenuecat
  - upgrade
  - downgrade
  - google-product-change-info
  - replacement-mode
---

# Plan Changes on Android with RevenueCat

You use this skill when a user already has an active Google Play subscription and you need to move them to a different SKU (upgrade, downgrade, cross-grade, or trial conversion). RevenueCat exposes Google's replacement modes through a single `PurchaseParams` builder and resolves the `linkedPurchaseToken` chain server side so you do not write token chaining code.

## Phase 1: Preconditions

Confirm the following before invoking a plan change:

- The user has exactly one active Google Play subscription you intend to replace.
- You have a fresh `CustomerInfo` from `Purchases.sharedInstance.awaitCustomerInfo()` or a cached value from a recent listener callback.
- You have the target `Package` resolved from `offerings.current` (see `fetch-offerings` skill).
- Google Play Billing Library 7+ is on the classpath via the RevenueCat SDK.

Skip this skill if the user has no active subscription. For a fresh purchase, use `make-purchase` instead.

## Phase 2: Plan (pick a replacement mode)

`GoogleReplacementMode` maps one to one onto Google's billing modes. Pick based on the user intent:

| Scenario | Mode | Billing effect |
|---|---|---|
| Standard upgrade (monthly to annual) | `WITH_TIME_PRORATION` | Immediate switch, remaining time credited |
| Upgrade, keep the existing billing date | `CHARGE_PRORATED_PRICE` | Immediate switch, prorated charge now |
| Switch to or from a prepaid plan | `CHARGE_FULL_PRICE` | Immediate switch, full charge now |
| Upgrade during an active free trial | `CHARGE_PRORATED_PRICE` | Immediate switch, prorated charge now |
| Downgrade (annual to monthly) | `DEFERRED` | Switch applies at next renewal |

Do not default to `WITHOUT_PRORATION` for trial upgrades. `WITHOUT_PRORATION` applies the new plan immediately but charges nothing until the next renewal, which gives the user free premium access they did not pay for. Use `CHARGE_PRORATED_PRICE` to charge the upgrade price on the spot.

If you set no mode, `PurchaseParams` defaults to `WITHOUT_PRORATION`. Set the mode explicitly every time.

`DEFERRED` is valid only for downgrades. Google rejects deferred upgrades.

## Phase 3: Execute

Derive `currentProductId` from `CustomerInfo`. Hardcoded SKUs break when a user has migrated between plans.

```kotlin
val customerInfo = Purchases.sharedInstance.awaitCustomerInfo()

// activeSubscriptions entries are "productId:basePlanId", strip the base plan suffix
val currentProductId = customerInfo.activeSubscriptions
    .firstOrNull()
    ?.substringBefore(":")
    ?: return  // nothing active, route to make-purchase instead

val newPackage = offerings.current
    ?.availablePackages
    ?.firstOrNull { it.identifier == "premium_annual_package" }
    ?: return

val params = PurchaseParams.Builder(activity, newPackage)
    .googleProductChangeInfo(
        GoogleProductChangeInfo(
            oldProductId = currentProductId,
            replacementMode = GoogleReplacementMode.WITH_TIME_PRORATION,
        )
    )
    .build()

try {
    val result = Purchases.sharedInstance.awaitPurchase(params)
    // result.customerInfo reflects the new subscription
} catch (e: PurchasesTransactionException) {
    if (!e.userCancelled) showError(e.error.message)
}
```

Notes on `oldProductId`:

- Pass the subscription product ID only. If you pass `"basic_monthly:monthly_plan"`, the SDK strips `:monthly_plan` for you, but the intent is clearer when you slice it yourself.
- `CustomerInfo.activeSubscriptions` uses the `productId:basePlanId` shape. `substringBefore(":")` gives you the correct value.

## Phase 4: Verify

After the suspending call returns, read the updated `CustomerInfo`:

- `customerInfo.activeSubscriptions` now contains the new `productId:basePlanId`.
- `customerInfo.entitlements["pro"]?.isActive` stays `true` across the switch; do not gate UI on the SKU string.
- For `DEFERRED` mode, `activeSubscriptions` still reports the old product until the next renewal. RevenueCat tracks the pending switch server side and flips the entitlement after Google sends the renewal RTDN.

You do not write `linkedPurchaseToken` traversal code. RevenueCat resolves the chain, marks the old token as replaced, and attributes both tokens to the same App User ID. Client code reads entitlements and trusts them.

## Common mistakes

| Mistake | Fix |
|---|---|
| Hardcoding `oldProductId` as a constant | Derive it from `customerInfo.activeSubscriptions.firstOrNull()?.substringBefore(":")` |
| Passing `"productId:basePlanId"` as `oldProductId` | Slice off the base plan with `substringBefore(":")` |
| Using `WITHOUT_PRORATION` for a trial upgrade | Use `CHARGE_PRORATED_PRICE` so the user is charged now |
| Using `DEFERRED` for an upgrade | `DEFERRED` is downgrade only; Google rejects deferred upgrades |
| Writing backend code to follow `linkedPurchaseToken` | RevenueCat does this server side, delete the code |
| Reading the SKU to decide UI state | Read `entitlements[...]?.isActive` instead |

## References

- [Full chapter](https://www.revenuecat.com/guides/revenuecat-android-sdk/subscription-upgrades-and-downgrades)

© RevenueCat · MIT · RevenueCat/ai-toolkit

Similar skills

Everything for Mobile →

From the same set33

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor