DEV Community

Arshan Nawaz
Arshan Nawaz

Posted on

React Native - Google Play Billing (Subscriptions) Complete Guide

Naming: Google Pay vs Google Play Billing

Term What it is When you use it
Google Play Billing In-app purchases & subscriptions sold inside an Android app via Google Play Digital goods, Premium tiers, unlockable features (required by Google for most digital content)
Google Pay Wallet / card wallet for one-time payments (often via Stripe / PaymentRequest) Physical goods, services, web checkout — not the same as Play subscriptions
Google payments profile Merchant / tax / payout setup inside Play Console Required before you can sell anything on Play — often mislabeled “Google Pay”

This guide covers Google Play Billing (store subscriptions). If you need a Google Pay button for one-time charges, see Appendix A.


Table of contents

  1. Architecture overview
  2. Prerequisites checklist
  3. Play Console setup
  4. RevenueCat dashboard setup
  5. Product ID conventions (Android)
  6. Install client dependency
  7. Environment variables
  8. Server: public config endpoint
  9. Server: sync + webhook
  10. Client: types
  11. Client: RevenueCat service layer
  12. Client: device store lock
  13. Client: ownership alerts
  14. Client: purchase hook (full flow)
  15. Client: restore purchases
  16. Client: change plan (monthly ↔ yearly)
  17. Client: trial eligibility
  18. Client: identify user on login / logout
  19. Client: manage / cancel subscription
  20. UI / UX rules
  21. End-to-end purchase sequence
  22. Error handling matrix
  23. Testing (license testers)
  24. Production gotchas
  25. iOS parallel (optional)
  26. File layout template
  27. Copy-paste checklists
  28. Appendix A — Google Pay (Wallet) vs this guide
  29. Appendix B — Glossary

1. Architecture overview

┌─────────────┐     purchaseProduct      ┌──────────────────┐
│  React Native│ ───────────────────────► │ Google Play      │
│  + RevenueCat│ ◄─────────────────────── │ Billing Library  │
│  SDK         │     CustomerInfo         └────────┬─────────┘
└──────┬───────┘                                   │
       │ POST /billing/sync                        │ receipt
       ▼                                           ▼
┌─────────────┐     webhook (HMAC)        ┌──────────────────┐
│ Your API    │ ◄──────────────────────── │ RevenueCat       │
│ (source of  │     REST validation       │ cloud            │
│  truth for  │ ────────────────────────► └──────────────────┘
│  entitlements)│
└─────────────┘
Enter fullscreen mode Exit fullscreen mode

Rules of thumb

  • Play owns money. Your app never stores card numbers.
  • RevenueCat validates receipts and sends webhooks.
  • Your backend is source of truth for “is this app user Premium?” after sync/webhook.
  • Client SDK is trusted for UI (show/hide paywall) but not for authorizing sensitive APIs alone — always check server entitlement for gated actions.

2. Prerequisites checklist

  • [ ] Google Play Developer account (one-time fee)
  • [ ] App created in Play Console with package name (e.g. com.example.myapp)
  • [ ] Payments profile verified (tax + merchant) — can take 2–5 business days
  • [ ] Bank account + tax forms for payouts (before first real earnings)
  • [ ] RevenueCat account + project
  • [ ] Backend that can expose HTTPS webhook URL
  • [ ] Expo / RN app that ships as Play-installed build for real Billing tests (sideload often fails Billing)

3. Play Console setup

3.1 Payments profile

  1. Play Console → Settings → Payments profile
  2. Create / verify merchant profile (country, individual or organization)
  3. Complete tax info (e.g. W-8BEN for non-US) before first US sale
  4. Add bank account for payouts
  5. Confirm Order management is accessible

3.2 License testers (free test purchases)

  1. Play Console → Settings → License testing
  2. Add Gmail accounts that will test
  3. Those accounts must also be opted into your Internal testing track later

3.3 Create subscriptions

  1. Monetize → Subscriptions → Create subscription
  2. Create one subscription product per cadence you sell, e.g.:
    • Product: premium_monthly with base plan monthly
    • Product: premium_yearly with base plan yearly
  3. For each base plan: price, billing period, free trial / intro offer
  4. Activate products (draft products are invisible to testers)

3.4 App signing

Production purchases require Play App Signing enabled before real money flows.


4. RevenueCat dashboard setup

  1. Create a project (one project for sandbox + production is fine; purchases are tagged sandbox vs production)
  2. Add Android app with the same package name as Play (com.example.myapp)
  3. Upload Play service account JSON so RevenueCat can validate receipts
  4. Products → Import from Google Play
  5. Create an entitlement e.g. premium
  6. Attach monthly + yearly products to that entitlement
  7. Create an offering (e.g. default) with packages if you use offerings UI
  8. Copy keys:
    • Android public SDK key (goog_…)
    • iOS public key if needed (appl_…)
    • Secret API key for server (sk_…)
  9. Configure webhook → your POST https://api.example.com/v1/billing/revenuecat/webhook with HMAC secret

5. Product ID conventions (Android)

Google Play uses subscriptionId:basePlanId when talking to Billing / RevenueCat:

Plan Example product ID
Monthly premium_monthly:monthly
Yearly premium_yearly:yearly

Keep these IDs identical across:

  • Play Console
  • RevenueCat products
  • Server env / /billing/config
  • Client purchase calls

Never hard-code product IDs only in the app — serve them from the backend so you can rotate without shipping a binary.


6. Install client dependency

npx expo install react-native-purchases
# or
npm install react-native-purchases
Enter fullscreen mode Exit fullscreen mode

Typical version family: react-native-purchases 10.x.

No separate “Google Pay” native module is required. Play Billing permission / Billing Library is pulled in by the Purchases SDK at native build time.

Expo: use a dev client / EAS build, not Expo Go, for real purchases.


7. Environment variables

Server (recommended)

# Webhook HMAC from RevenueCat dashboard
REVENUECAT_WEBHOOK_HMAC_SECRET=whsec_replace_me

# Server-side REST
REVENUECAT_SECRET_API_KEY=sk_replace_me
REVENUECAT_PROJECT_ID=projreplace_me

# Public SDK keys (safe to return to clients)
REVENUECAT_PUBLIC_IOS_API_KEY=appl_replace_me
REVENUECAT_PUBLIC_ANDROID_API_KEY=goog_replace_me

# Catalog
REVENUECAT_ENTITLEMENT_ID=premium
REVENUECAT_OFFERING_ID=default
REVENUECAT_PRO_MONTHLY_PRODUCT_ID=premium_monthly:monthly
REVENUECAT_PRO_YEARLY_PRODUCT_ID=premium_yearly:yearly
Enter fullscreen mode Exit fullscreen mode

Client

Prefer no hard-coded RevenueCat keys in the app binary. Fetch from:

GET /billing/config (public / skip-auth is OK for public keys only).


8. Server: public config endpoint

Expose everything the client needs to configure the SDK and show prices:

GET /v1/billing/config
Enter fullscreen mode Exit fullscreen mode

Example JSON response:

{
  "revenueCat": {
    "iosApiKey": "appl_…",
    "androidApiKey": "goog_…",
    "entitlementId": "premium",
    "offeringId": "default"
  },
  "plans": {
    "free": {
      "tier": "free",
      "label": "Free",
      "priceLabel": "$0",
      "features": {},
      "limits": {}
    },
    "pro": {
      "tier": "pro",
      "label": "Premium",
      "entitlementId": "premium",
      "monthly": {
        "priceLabel": "$4.99/mo",
        "cadence": "monthly",
        "productId": "premium_monthly:monthly"
      },
      "yearly": {
        "priceLabel": "$39.99/yr",
        "cadence": "yearly",
        "productId": "premium_yearly:yearly"
      },
      "features": {},
      "limits": {}
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

9. Server: sync + webhook

9.1 Client sync (fast path after purchase)

POST /v1/billing/sync
Authorization: Bearer <user JWT>
Enter fullscreen mode Exit fullscreen mode

Body:

{
  "appUserId": "<same id used in Purchases.logIn>",
  "entitlement": "premium",
  "productId": "premium_monthly:monthly",
  "store": "play_store",
  "currentPeriodEnd": "2026-08-19T00:00:00.000Z"
}
Enter fullscreen mode Exit fullscreen mode

Server responsibilities:

  1. Verify the caller is authenticated
  2. Prefer verifying against RevenueCat REST / CustomerInfo for that appUserId
  3. Upsert subscription row for this user
  4. If receipt already linked to another user → return 409 with clear message
  5. Invalidate any session/bootstrap cache for that user

9.2 Webhook (source of truth for renewals / cancels)

POST /v1/billing/revenuecat/webhook
Enter fullscreen mode Exit fullscreen mode
  • Verify X-RevenueCat-Webhook-Signature (HMAC over raw body)
  • Idempotent: store event IDs so retries are safe
  • Handle at least: INITIAL_PURCHASE, RENEWAL, PRODUCT_CHANGE, CANCELLATION, EXPIRATION, BILLING_ISSUE, UNCANCELLATION
  • Only grant access when the configured entitlement is present/active
  • Invalidate session caches after each processed event

10. Client: types

export type BillingConfigResponse = {
  revenueCat: {
    iosApiKey: string | null;
    androidApiKey: string | null;
    entitlementId: string;
    offeringId: string;
  };
  plans: {
    free: { /* … */ };
    pro: {
      entitlementId: string;
      monthly: { priceLabel: string; cadence: string; productId?: string };
      yearly: { priceLabel: string; cadence: string; productId?: string };
      // …
    };
  };
};

export type BillingSyncPayload = {
  appUserId: string;
  entitlement: string;
  productId?: string | null;
  store?: string | null;
  currentPeriodEnd?: string | null;
};

export type BillingPurchaseResult = {
  productId: string | null;
  store: string | null;
  currentPeriodEnd: string | null;
  isActive: boolean;
  willRenew: boolean | null;
};
Enter fullscreen mode Exit fullscreen mode

11. Client: RevenueCat service layer

Create a single module (e.g. src/features/billing/services/purchases.ts) that owns:

Concern Function
Configure once ensureConfigured(config)Purchases.configure({ apiKey })
Identify user logIn(appUserId) / logOut()
Purchase purchaseProduct(productId)
Change plan (Android) purchaseSubscriptionOption + proration
Restore restorePurchases()
Local entitlement getCustomerInfo()entitlements.active[id]
Manage UI showManageSubscriptions()
Trial eligibility platform-specific (see §17)
Errors map RC codes → typed errors

11.1 Custom errors

export class PurchaseUnavailableError extends Error {} // cancel / missing key
export class PaymentPendingError extends Error {}      // family approval
export class AlreadyPurchasedError extends Error {}    // store already owns sub
Enter fullscreen mode Exit fullscreen mode

11.2 Map purchase errors

import { PURCHASES_ERROR_CODE } from 'react-native-purchases';

function rethrowPurchaseError(err: unknown): never {
  const code = (err as { code?: PURCHASES_ERROR_CODE }).code;
  if (code === PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {
    throw new PurchaseUnavailableError('Purchase cancelled.');
  }
  if (code === PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR) {
    throw new PaymentPendingError('Payment pending.');
  }
  if (
    code === PURCHASES_ERROR_CODE.PRODUCT_ALREADY_PURCHASED_ERROR ||
    code === PURCHASES_ERROR_CODE.RECEIPT_ALREADY_IN_USE_ERROR ||
    code === PURCHASES_ERROR_CODE.RECEIPT_IN_USE_BY_OTHER_SUBSCRIBER_ERROR
  ) {
    throw new AlreadyPurchasedError('Subscription already owned on this store account.');
  }
  throw err;
}
Enter fullscreen mode Exit fullscreen mode

11.3 Configure + identify

import Purchases, { LOG_LEVEL } from 'react-native-purchases';
import { Platform } from 'react-native';

let sdkConfigured = false;
let loggedInAppUserId: string | null = null;

function platformApiKey(config: BillingConfigResponse): string | null {
  if (Platform.OS === 'ios') return config.revenueCat.iosApiKey;
  if (Platform.OS === 'android') return config.revenueCat.androidApiKey;
  return null;
}

export async function ensureConfigured(config: BillingConfigResponse): Promise<void> {
  if (sdkConfigured) return;
  if (await Purchases.isConfigured().catch(() => false)) {
    sdkConfigured = true;
    return;
  }
  const apiKey = platformApiKey(config);
  if (!apiKey) throw new PurchaseUnavailableError('API key missing for this platform.');
  Purchases.setLogLevel(__DEV__ ? LOG_LEVEL.DEBUG : LOG_LEVEL.INFO);
  Purchases.configure({ apiKey });
  sdkConfigured = true;
}

export async function logInPurchases(appUserId: string): Promise<void> {
  if (!appUserId || loggedInAppUserId === appUserId) return;
  await Purchases.logIn(appUserId);
  loggedInAppUserId = appUserId;
}

export async function logOutPurchases(): Promise<void> {
  try {
    if (sdkConfigured && loggedInAppUserId) await Purchases.logOut();
  } catch {
    // logOut on anonymous user can throw — ignore
  }
  loggedInAppUserId = null;
}

export async function configurePurchases(config: BillingConfigResponse, appUserId: string) {
  await ensureConfigured(config);
  await logInPurchases(appUserId);
}
Enter fullscreen mode Exit fullscreen mode

Important: configure with API key once per process. Switch users with logIn, not by re-calling configure with a new identity.

11.4 Purchase

export async function purchaseProProduct(
  productId: string,
  entitlementId = 'premium',
): Promise<BillingPurchaseResult> {
  try {
    const { customerInfo, productIdentifier } = await Purchases.purchaseProduct(productId);
    return mapPurchaseResult(productIdentifier, customerInfo, productId, entitlementId);
  } catch (err) {
    rethrowPurchaseError(err);
  }
}
Enter fullscreen mode Exit fullscreen mode

11.5 Connectivity probe (optional but useful)

Before opening the store sheet:

export async function checkConnection(): Promise<boolean> {
  try {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 2000);
    const res = await fetch('https://clients3.google.com/generate_204', {
      method: 'HEAD',
      signal: controller.signal,
    });
    clearTimeout(timeoutId);
    return res.status === 204;
  } catch {
    return false;
  }
}
Enter fullscreen mode Exit fullscreen mode

12. Client: device store lock

Problem: Monthly and yearly are separate Play products. With RevenueCat “Keep with original App User ID”, a second app login on the same Google account can sometimes buy the other SKU while the receipt stays on the first user.

Mitigation: Persist a short-lived device lock in AsyncStorage after any successful Premium detection:

type StorePremiumLock = {
  productIds: string[];
  expiresAt: string; // ISO
  updatedAt: string;
};
Enter fullscreen mode Exit fullscreen mode
  • Write lock after purchase / restore / active entitlement
  • Before purchase: if lock active OR CustomerInfo already owns configured SKUs → block with “Already subscribed on Google Play”
  • Auto-clear when expiresAt passes

13. Client: ownership alerts

Provide two alerts:

  1. Already subscribed on Google Play — store account owns Premium linked to another app login
  2. Subscription on another account — server returned 409 on sync

Both should offer a button that opens:

https://play.google.com/store/account/subscriptions
Enter fullscreen mode Exit fullscreen mode

(iOS parallel: https://apps.apple.com/account/subscriptions)

Detect store ownership errors via RevenueCat codes listed in §11.2.


14. Client: purchase hook (full flow)

Recommended order inside purchase(plan):

  1. Guard: already Premium on server → navigate to Manage, do not open sheet
  2. Guard: purchase already in progress → return
  3. Network check → alert if offline
  4. Resolve productId from /billing/config for monthly/yearly
  5. configurePurchases(config, userId)
  6. Restore first (soft-fail unless AlreadyPurchased)
  7. Read local entitlement — if active, sync + go Manage (skip sheet)
  8. Store lock / CustomerInfo owns SKU → ownership alert
  9. Stop spinner (store UI owns the wait), call purchaseProProduct
  10. On success → POST /billing/sync
  11. Sync 409 → cross-account alert
  12. Sync fails but local entitlement active → toast “Premium is confirming…” → Manage
  13. Success → Welcome / upgrade-success screen

Spinner UX

  • Show spinner for app-owned work only
  • While Play sheet is open: keep CTA disabled, hide spinner so it does not spin behind the modal

Pseudo-code outline

async function purchase(plan: 'monthly' | 'yearly') {
  if (alreadyPro) return navigateManage();
  if (busy) return;

  if (!(await checkConnection())) return alertOffline();

  const productId = plan === 'yearly' ? yearlyId : monthlyId;
  await configurePurchases(config, userId);

  // restore → local entitlement → store lock checks…
  // then:
  setSpinner(false);
  const result = await purchaseProProduct(productId, entitlementId);
  await syncBilling({
    appUserId: userId,
    entitlement: entitlementId,
    productId: result.productId,
    store: 'play_store',
    currentPeriodEnd: result.currentPeriodEnd,
  });
  navigateUpgradeSuccess();
}
Enter fullscreen mode Exit fullscreen mode

15. Client: restore purchases

const info = await Purchases.restorePurchases();
// map entitlement → sync payload → POST /billing/sync
Enter fullscreen mode Exit fullscreen mode

UI cases:

Result UX
Entitlement active Sync → Manage or success
Nothing found “No purchases found”
Store unreachable “Restore failed — check connection”
Sync 409 Cross-account alert

16. Client: change plan (monthly ↔ yearly)

Android (required)

Google Play’s manage-subscriptions UI cannot switch base plans. You must run an in-app replacement purchase:

import { PRORATION_MODE, PRODUCT_CATEGORY } from 'react-native-purchases';

// upgrade monthly → yearly
prorationMode = PRORATION_MODE.IMMEDIATE_WITH_TIME_PRORATION;

// downgrade yearly → monthly
prorationMode = PRORATION_MODE.DEFERRED;

const products = await Purchases.getProducts([newProductId], PRODUCT_CATEGORY.SUBSCRIPTION);
const option = products[0]?.defaultOption ?? products[0]?.subscriptionOptions?.[0];

await Purchases.purchaseSubscriptionOption(option, {
  oldProductIdentifier: oldProductId,
  prorationMode,
});
Enter fullscreen mode Exit fullscreen mode

Fallback if no subscription option: purchaseStoreProduct(product, { oldProductIdentifier, prorationMode }).

iOS

Products in the same subscription grouppurchaseProduct(newProductId) and StoreKit handles replacement.


17. Client: trial eligibility

Never promise “Start free trial” unless you know the store account is eligible.

Platform How
iOS Purchases.checkTrialOrIntroductoryPriceEligibility(ids)
Android getProducts — if any option has freePhase, user is eligible (Play only returns eligible offers)

If status is unknown, treat as ineligible for CTA copy (safer).


18. Client: identify user on login / logout

Event Action
Session bootstrap / login configure + logIn(userId) early
Logout logOut()
Purchase / restore / manage Call configure+logIn as a safe prelude

Use your stable backend user id as RevenueCat appUserId so webhooks and sync line up.


19. Client: manage / cancel subscription

  1. Prefer Purchases.showManageSubscriptions()
  2. Fallback deep link: Play subscriptions URL
  3. Tell users clearly: cancel / payment method changes happen in Google Play, not inside your app settings alone

20. UI / UX rules

  • Trust badge copy: “Billed through Google Play” (not “Google Pay”)
  • One primary CTA on paywall plan screen
  • Disable CTA while purchase pending
  • User cancel → silent return (no error toast)
  • Family / pending payment → dedicated “Approval Request Sent” alert
  • After purchase, unlock Premium only after sync or show confirming state if local entitlement is already true

21. End-to-end purchase sequence

sequenceDiagram
  participant User
  participant App
  participant RC as RevenueCat SDK
  participant Play as Google Play Billing
  participant API as Your Backend
  participant WH as RevenueCat Webhook

  User->>App: Tap Subscribe
  App->>App: Guards + restore + lock checks
  App->>RC: purchaseProduct(sku)
  RC->>Play: Present billing sheet
  User->>Play: Confirm
  Play-->>RC: Receipt
  RC-->>App: CustomerInfo (entitlement active)
  App->>API: POST /billing/sync
  WH-->>API: INITIAL_PURCHASE (async)
  API-->>App: Plan = premium
  App->>User: Upgrade success
Enter fullscreen mode Exit fullscreen mode

22. Error handling matrix

Situation User sees App does
User cancels sheet Nothing Silent return
Offline Connection alert Abort
Already Premium (server) Open Manage
Already owned on Play Already subscribed alert Abort / restore path
Sync 409 Another account alert Abort
Payment pending Approval request alert Abort
Generic purchase failure Purchase failed alert Abort
Sync fails, local entitlement OK Toast “confirming…” Open Manage
SKU not found / not activated Purchase failed Check Play + RC product mapping

23. Testing (license testers)

Setup

  1. License tester Gmail on device
  2. Opt into Internal testing track
  3. Install from Play opt-in link (not random sideload)
  4. Force-stop Play Store → clear cache → reopen (stale catalog)

Happy path

  1. Open paywall → Subscribe
  2. Sheet shows (Test) label
  3. Complete purchase
  4. RevenueCat Customer view shows entitlement
  5. App unlocks Premium
  6. Restore after reinstall works
  7. Cancel in Play → webhook → entitlement drops on refresh

Accelerated renewals

License tester renewals are accelerated (e.g. ~5 minutes ≈ 1 month). Use this to test renewal webhooks — not real calendar months.

First SKU delay

Brand-new products can fail for 2–6 hours while Play propagates. Retry later.


24. Production gotchas

  • [ ] Payments profile + tax + bank before real payouts
  • [ ] Play App Signing on
  • [ ] Products Active in Play Console
  • [ ] Service account JSON uploaded to RevenueCat
  • [ ] Webhook HMAC verified; raw body not re-serialized
  • [ ] Public keys served from /config, not committed secrets
  • [ ] Never run real (non–license-tester) purchases from a debug build against production SKUs
  • [ ] Monthly↔yearly change uses Android replacement purchase, not manage sheet
  • [ ] 409 handling for shared Google accounts / multi-login
  • [ ] Device store lock for Keep-with-original edge cases
  • [ ] usesCleartextTraffic: false and HTTPS API in production builds

25. iOS parallel (optional)

Same architecture; differences:

Topic Android iOS
API key goog_… appl_…
Store string play_store app_store
Product IDs sub:basePlan App Store product id
Plan change Proration / replacement Same subscription group
Trial API freePhase on options Intro eligibility API
Manage URL Play subscriptions App Store subscriptions

26. File layout template

src/
  features/
    billing/
      api/
        billing.api.ts          # getConfig, getMe, sync
      hooks/
        useBillingQueries.ts    # React Query wrappers
      lib/
        storePremiumLock.ts     # device lock
        premiumLifecycle.ts     # map plan → UI state
      services/
        purchases.ts            # RevenueCat wrapper
    monetization/
      hooks/
        useProPurchase.ts
        useProRestore.ts
        useChangePremiumPlan.ts
        usePaywallTrialEligibility.ts
      lib/
        storeOwnershipAlerts.ts
        paywall.ts
      screens/
        PaywallScreen.tsx
        PlanComparisonScreen.tsx
        ChangePlanScreen.tsx
        UpgradeSuccessScreen.tsx
server/
  modules/billing/
    billing.route.js
    revenuecat.controller.js
    revenuecat.service.js
    revenuecat.schema.js
    subscription.model.js
Enter fullscreen mode Exit fullscreen mode

27. Copy-paste checklists

New project — day 1

  • [ ] Play app + package name locked
  • [ ] Start payments profile verification
  • [ ] Create RevenueCat project + Android app
  • [ ] Add react-native-purchases
  • [ ] Add /billing/config, /billing/sync, webhook routes

Before first test purchase

  • [ ] Subscriptions created + activated
  • [ ] Products imported + entitlement mapped in RC
  • [ ] Service account connected
  • [ ] License testers added
  • [ ] Internal track build installed from Play

Before production

  • [ ] Tax + payout methods
  • [ ] Webhook live + HMAC
  • [ ] E2E: buy, restore, cancel, renew (accelerated)
  • [ ] 409 / already-owned UX verified
  • [ ] Plan change (upgrade + downgrade) verified on Android

Appendix A — Google Pay (Wallet) vs this guide

Use this guide when selling digital Premium inside an Android app.

Use Google Pay (Wallet) when:

  • Charging for physical goods / services
  • Integrating Stripe / Braintree / etc. with a Google Pay button
  • Not selling a Play-managed subscription

Typical Google Pay stack (out of scope here):

  • @stripe/stripe-react-nativeGooglePay / Payment Sheet
  • Or native PaymentsClient / Wallet API with gateway tokenization

Do not mix the two for the same digital entitlement — Google’s policies generally require Play Billing for digital in-app unlocks.


Appendix B — Glossary

Term Meaning
SKU / product ID Store catalog identifier
Base plan Android billing period + price under a subscription
Entitlement Logical access flag in RevenueCat (e.g. premium)
Offering Packaged set of products shown to a user cohort
App User ID Your user id passed to Purchases.logIn
CustomerInfo RC snapshot of entitlements + subscriptions
Proration How Play charges when switching plans mid-cycle
License tester Gmail that gets free “(Test)” purchases
Keep with original RC transfer behavior when the same store receipt hits a new App User ID

Quick reference — minimum viable integration

  1. Play subscriptions + payments profile
  2. RevenueCat products → entitlement premium
  3. npm i react-native-purchases
  4. GET /billing/config → public keys + product IDs
  5. configure + logIn(userId)
  6. purchaseProduct(id)POST /billing/sync
  7. Webhook for renewals / expiration
  8. Restore + manage deep link
  9. License-tester E2E before launch

Top comments (0)