Reference for in-app purchases and subscriptions in React Native apps, with RevenueCat as the integration layer. Use when implementing paywalls or subscriptions, when sandbox purchases behave strangely, when premium status does not arrive after purchase, when webhooks need verification, or when restore and transfer flows are being designed.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add AnilBurcu/claude-code-react-native --skill in-app-purchases --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of In App Purchases?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anilburcu-in-app-purchases)More formats (shields.io, HTML) on the badges page.
---
name: in-app-purchases
description: Reference for in-app purchases and subscriptions in React Native apps, with RevenueCat as the integration layer. Use when implementing paywalls or subscriptions, when sandbox purchases behave strangely, when premium status does not arrive after purchase, when webhooks need verification, or when restore and transfer flows are being designed.
user-invocable: false
---
# Subscriptions without 2 AM incidents
## The architecture rule everything else hangs on
Entitlement truth lives on your server, not in the client. The purchase SDK tells the client "probably premium" fast; the webhook pipeline tells your database "actually premium" reliably. The client renders from server state and treats its local SDK state as an optimistic hint.
Every horror story in this domain is a variation of trusting the client: premium flags in AsyncStorage, entitlement checks that only run at purchase time, or webhook handlers that were never verified against replay.
## RevenueCat wiring that holds up
- Identify the user to the SDK with your own auth user id after sign-in. Anonymous ids merging into identified ids is where "user bought on the wrong account" tickets are born.
- Configure once at startup with the platform key; render from `customerInfo` listeners rather than one-shot fetches, so a purchase completing in the background updates the UI without a restart.
- Decide your transfer policy consciously. "Transfer to new app user id" versus blocking transfers changes what happens when one store account signs into a second app account. Whichever you pick, enforce ownership rules in your own database guards; the SDK setting alone is not a policy.
- Restore purchases must be a visible button (App Store review expects it) and must resolve through the same server-side reconciliation as a fresh purchase.
## Webhooks: the part that must be boring
- Verify the sender on every request: at minimum compare the Authorization header against your configured secret; where the provider offers HMAC-signed webhooks, prefer verifying the signature. An unverified webhook endpoint is an open "make me premium" API.
- If the endpoint runs on a platform that enforces its own auth by default (Supabase Edge Functions and their JWT verification, for example), remember the store's webhook sender cannot present your platform's JWT. Shared-secret endpoints must have platform JWT verification disabled and their own secret check enabled, or every event bounces with 401 and premium "never arrives".
- Deduplicate by event id. Webhook senders retry; your handler will see the same event twice on a bad day.
- Store the environment (SANDBOX vs PRODUCTION) on every event and subscription row, and filter sandbox out of admin dashboards and metrics. Mixed environments make revenue numbers lie.
- Reconcile, don't accumulate: handlers should upsert the subscription to the state the event describes, so replays and out-of-order deliveries converge instead of corrupting.
- A runtime-neutral handler skeleton with the dedupe and transaction contracts spelled out is in [reference.md](reference.md).
## Sandbox behavior that looks like a bug but is not
- Sandbox subscriptions renew on an accelerated clock (a monthly plan renews in minutes, and the speed is adjustable in App Store Connect) and stop after a fixed number of renewals, so expiry during a test session is expected. TestFlight runs its own accelerated, daily-capped renewal schedule; do not read either as production timing.
- Test with multiple store accounts and record which store each test row came from. Cross-platform testing (one user, App Store purchase, Play restore attempt) is where ownership policies show their true behavior.
- iOS: purchases in TestFlight builds use sandbox. A "works in TestFlight, broken in production" report is usually an environment-filtering bug in your pipeline, not a store bug.
- A StoreKit configuration file enables local purchase testing in the simulator with no sandbox account, which is the fastest loop for paywall UI work. Treat your webhook pipeline as unverified until a real sandbox purchase has run end to end; local StoreKit testing is for the client loop, not for proving the server side.
## The purchase flow's unhappy paths, all of which will happen
- Purchase succeeds, network drops before your server hears about it: the client must not be the only messenger. The webhook arrives independently, and the client reconciles on next launch via the customer info listener. Design for "server knows before the client does" and this case disappears.
- Purchase cancelled by the user: not an error. Do not log it as one or your error budget drowns.
- Pending / deferred states (ask-to-buy, slow card processing on Play): render "purchase pending" honestly instead of failing.
- Billing retry and grace periods: decide explicitly whether grace period keeps access (usually yes) and encode it server-side, driven by the webhook event types, not by client guessing.
## Debugging "premium didn't arrive", in order
1. Did the webhook arrive at all? Check the sender's event delivery log first; a 401 there ends the investigation immediately (see the JWT note above).
2. Did the handler verify, dedupe and upsert? Read the handler's log for this event id.
3. Environment mismatch: sandbox event, production query, or the reverse.
4. Identity: which app user id was attached to the purchase? Anonymous-id purchases explain most "bought but not premium" cases.
5. Only after those four: suspect the client's entitlement rendering.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!