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 PremiumGateThis 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/orpro_lifetime. - RevenueCat Android offering with packages attached to entitlement
pro. REVENUECAT_ANDROID_KEYinlocal.properties.- Supabase
revenuecat-webhookdeployed 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 bundleReleaseUse the .aab from:
app/build/outputs/bundle/release/app-release.aabInstall 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:
| Column | Expected value |
|---|---|
user_id | The Supabase UUID used as RevenueCat app_user_id |
provider | play_store |
product_id | The Play product id, such as pro_monthly |
product_type | month, year, or one_time |
status | trialing, active, canceled, or expired |
period_start | Purchase timestamp |
period_end | Expiration, 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/.envThen 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-webhookThe response body should be success when the auth header matches and the event
is accepted.
Common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Paywall says purchases are unavailable | REVENUECAT_ANDROID_KEY is empty | Add the Android SDK key to local.properties and rebuild |
| Offering is empty | Products inactive, no current offering, wrong package name, or app not on a testing track | Verify Play products, RevenueCat offering, package name, and Internal testing upload |
| Purchase UI does not open | Tester installed a side-loaded build or device lacks Play services | Install through Play test opt-in on a Play-services device/emulator |
| Webhook returns 401 | RevenueCat Authorization does not match REVENUECAT_WEBHOOK_AUTH | Update the dashboard header or Supabase secret |
| Webhook returns 500 | Edge secret missing or database write failed | Check Supabase function logs and the subscriptions schema |
| RevenueCat event arrives but app stays locked | app_user_id is anonymous/non-UUID or row is for another Supabase user | Sign in before purchase and confirm RevenueCat logged in with the Supabase UUID |
| Different platform purchase disappeared | Shorter cross-provider event tried to take over | The 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_STOREwebhook. -
public.subscriptionshas the expectedprovider = play_storerow. - Showcase guides unlock from Supabase entitlement state.
