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.
npx skills add RevenueCat/ai-toolkit --skill rc-plan-changes -a claude-codeWhat 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-code2Manually: put the files in place
~/.claude/skills/rc-plan-changes/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/rc-plan-changes/
1Skills CLI (Vercel)
npx skills add RevenueCat/ai-toolkit --skill rc-plan-changes -a codex2Manually: put the files in place
.agents/skills/rc-plan-changes/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add RevenueCat/ai-toolkit --skill rc-plan-changes -a cursor2Manually: put the files in place
.cursor/skills/rc-plan-changes/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install RevenueCat/ai-toolkit rc-plan-changes2Manually: put the files in place
.github/skills/rc-plan-changes/SKILL.md1Skills CLI (Vercel)
npx skills add RevenueCat/ai-toolkit --skill rc-plan-changes -a gemini-cli2Platform CLI
gemini skills install https://github.com/RevenueCat/ai-toolkit --path revenuecat-play-billing/skills/rc-plan-changesManually: put the files in place
.devin/skills/rc-plan-changes/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add RevenueCat/ai-toolkit --skill rc-plan-changes -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
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