Purchase Testing

Test Google Play Billing, RevenueCat purchases, restores, and entitlement state.

Android purchase testing uses Google Play license testers and a Play testing track. Treat it as a Play Console flow first, then a RevenueCat and Supabase verification flow.

What you are testing

The complete path is:

License tester installs Play-delivered build
  -> tester buys a RevenueCat package through Google Play Billing
  -> RevenueCat sends PLAY_STORE webhook
  -> revenuecat-webhook upserts public.subscriptions
  -> EntitlementController refreshes and unlocks PremiumGate

This is different from Apple sandbox testing. You do not create App Store sandbox users. You add Google accounts as Play license testers, upload the app to a testing track, and install through the track opt-in link.

Prerequisites

  • A Play Console app with package name matching the Android applicationId.
  • Internal testing track with an uploaded .aab.
  • Tester Google accounts added as license testers and track testers.
  • Active Play products: pro_monthly, pro_yearly, and/or pro_lifetime.
  • RevenueCat Android offering with packages attached to entitlement pro.
  • REVENUECAT_ANDROID_KEY in local.properties.
  • Supabase revenuecat-webhook deployed and configured in RevenueCat.
  • A Supabase user signed in on the device before purchasing.

Purchases are account-bound to the Supabase user UUID. If RevenueCat sends an anonymous or non-UUID app_user_id, the webhook ignores the event and the app stays locked.

Run a purchase test

Build and upload

Create a release bundle and upload it to Internal testing:

./gradlew bundleRelease

Use the .aab from:

app/build/outputs/bundle/release/app-release.aab

Install as a tester

Add the tester to the Internal testing track, open the opt-in link with the same Google account, and install the app from Play. A direct adb install build is useful for UI development, but it is not the reliable path for real Play Billing validation.

Sign in to Supabase

Create or use a Supabase account in the app. PurchasesController logs RevenueCat in with that user's UUID, which becomes the webhook app_user_id.

Open the paywall

Use Upgrade to Pro from Home or the subscription row under Me. Confirm the offering packages appear. Annual is selected by default when present; otherwise the first sorted package is selected.

Complete a sandbox purchase

Buy a monthly, yearly, or lifetime product with the license tester account. The paywall closes on RevenueCat purchase success, then EntitlementController polls Supabase while the webhook writes the row.

Confirm Pro content unlocks

Open Showcase -> Guides. The guide body should unlock once public.subscriptions has an entitled row for the signed-in user.

Verify the Supabase row

Run this query in the Supabase SQL editor:

select user_id, provider, product_id, product_type, status, period_start, period_end
from public.subscriptions
where user_id = '<supabase-user-uuid>';

Expected values for a Google Play purchase:

ColumnExpected value
user_idThe Supabase UUID used as RevenueCat app_user_id
providerplay_store
product_idThe Play product id, such as pro_monthly
product_typemonth, year, or one_time
statustrialing, active, canceled, or expired
period_startPurchase timestamp
period_endExpiration, grace-period expiration, or far-future lifetime date

For support/debugging, also inspect subscription_id, customer_id, last_order_no, and last_paid_at.

Restore and manage

Use the paywall restore button to call RevenueCat restore. If restored CustomerInfo has active entitlement pro, the paywall closes. The ManageSubscriptionScreen also exposes restore, the RevenueCat management URL when available, and a Google Play subscriptions fallback URL.

Promo-code testing uses RedeemCodeScreen, which opens:

https://play.google.com/redeem?code=<code>

After a redeemed code produces a RevenueCat event, verify the same subscriptions row.

Local webhook smoke test

For backend validation without going through Play, serve functions locally:

supabase functions serve --env-file supabase/functions/.env

Then send a signed RevenueCat-shaped event:

curl -i \
  -X POST http://localhost:54321/functions/v1/revenuecat-webhook \
  -H "Authorization: ${REVENUECAT_WEBHOOK_AUTH}" \
  -H "Content-Type: application/json" \
  --data '{
    "api_version": "1.0",
    "event": {
      "type": "INITIAL_PURCHASE",
      "store": "PLAY_STORE",
      "app_user_id": "11111111-1111-1111-1111-111111111111",
      "product_id": "pro_monthly",
      "period_type": "NORMAL",
      "purchased_at_ms": 1782940800000,
      "expiration_at_ms": 1785532800000,
      "transaction_id": "gpa.test",
      "original_transaction_id": "gpa.test"
    }
  }'

For a deployed smoke test, replace the URL with:

https://<ref>.supabase.co/functions/v1/revenuecat-webhook

The response body should be success when the auth header matches and the event is accepted.

Common failures

SymptomLikely causeFix
Paywall says purchases are unavailableREVENUECAT_ANDROID_KEY is emptyAdd the Android SDK key to local.properties and rebuild
Offering is emptyProducts inactive, no current offering, wrong package name, or app not on a testing trackVerify Play products, RevenueCat offering, package name, and Internal testing upload
Purchase UI does not openTester installed a side-loaded build or device lacks Play servicesInstall through Play test opt-in on a Play-services device/emulator
Webhook returns 401RevenueCat Authorization does not match REVENUECAT_WEBHOOK_AUTHUpdate the dashboard header or Supabase secret
Webhook returns 500Edge secret missing or database write failedCheck Supabase function logs and the subscriptions schema
RevenueCat event arrives but app stays lockedapp_user_id is anonymous/non-UUID or row is for another Supabase userSign in before purchase and confirm RevenueCat logged in with the Supabase UUID
Different platform purchase disappearedShorter cross-provider event tried to take overThe webhook intentionally preserves the row with the later period_end

Verify

  • License tester installed a Play-delivered build.
  • Paywall packages render from RevenueCat.
  • Purchase, restore, and redeem paths were exercised.
  • RevenueCat delivered a PLAY_STORE webhook.
  • public.subscriptions has the expected provider = play_store row.
  • Showcase guides unlock from Supabase entitlement state.

On this page