You ship a subscription feature with Rork, push the build to TestFlight, tap "Subscribe" — and nothing happens. Or the price stays "Loading" forever. Or the purchase goes through but the entitlement never flips on. If you've shipped a paid app, you know exactly the day where this kind of issue eats four hours of your evening.
In my own work shipping Rork-generated apps to the App Store and Google Play, sandbox testing has been the single biggest source of last-minute surprises. The reasons are predictable once you've hit them, but Apple and Google each have their own distinct traps, and they don't overlap much. This article walks through what I actually run before I let a Rork subscription build leave TestFlight or Google Play's internal testing track.
The four scenarios you must reproduce in sandbox
Before any sandbox session, I write down the same four scenarios and refuse to release until each one has been observed end-to-end:
- Successful new purchase — a fresh user buys for the first time
- Trial → paid conversion — the moment the free trial ends and billing starts
- User-initiated cancellation — entitlement should disappear at the right time
- Failed renewal (billing retry / grace period) — card expiry simulation
The fourth one is the one most indie devs skip because it feels like "production-only behavior". You can absolutely reproduce billing retry in both sandboxes, and skipping it is a fast track to refund tickets and angry App Store reviews. The most common bug I see in Rork-generated subscription code is that the app correctly grants entitlement when a purchase succeeds, but never has logic to revoke it when a renewal fails after the grace period ends — the user simply keeps premium access forever, free. That bug is invisible until you exercise the failed-renewal scenario in sandbox.
I keep a tiny markdown table in my notes for each release where I check off all four scenarios across both Apple and Google. It's an embarrassingly low-tech tool, but it stops me from convincing myself that "I tested it" when I really only ran one happy path on iOS. If your subscription state machine isn't fully drawn out yet, work through the subscription entitlement state machine design guide first — sandbox testing is far smoother once the state diagram is in front of you.
Three things that trip you up in Apple's sandbox
The official docs cover creating sandbox testers in App Store Connect well enough. The places I see Rork developers actually lose time are downstream of that:
First, sandbox tester emails cannot be real addresses. Apple won't send the verification mail to a working inbox, so use something like sandbox-rork-001@example.com and stash the password in the Notes field. I keep one tester per supported region so I can check localized pricing.
Second, you must sign in with the sandbox tester under "Settings → App Store → Sandbox Account" on the test device. If your real Apple ID is still active, the purchase sheet either fails to appear or the price stays at "Loading". Many react-native-iap templates that Rork generates don't have proper error handling for the price-fetch failure path, so the UI just sits there silently.
Third, disable any StoreKit Configuration file in your Xcode scheme before building. Local StoreKit testing uses a .storekit config that simulates purchases entirely on-device — useful for unit testing, but if it stays attached to your scheme when you run eas build, the resulting build behaves inconsistently with TestFlight's actual sandbox. The symptom is usually that purchases "succeed" instantly with no real network round-trip, which in turn means your server-side receipt validation never sees the transaction and the entitlement bug only shows up in production. Make "set StoreKit Configuration to None for the Run scheme" part of your release checklist, and audit it explicitly before any TestFlight push.
Detecting sandbox vs production from your code
You'll often want to know, from inside the running app, whether the current transaction is a sandbox one. This matters for routing analytics events to a separate stream and for keeping debug logs out of production telemetry. With StoreKit 2, Transaction.environment is the canonical answer:
import StoreKit
@MainActor
func resolveActiveSubscription() async -> SubscriptionState {
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else { continue }
let env = transaction.environment // .sandbox | .production | .xcode
if env == .sandbox {
// Route sandbox events to a dev sink so they don't pollute prod metrics
Analytics.send("sandbox_entitlement_active", productId: transaction.productID)
}
return SubscriptionState(
productId: transaction.productID,
isSandbox: env == .sandbox,
expiresAt: transaction.expirationDate ?? .distantPast
)
}
return .none
}Expected output: when you sign in with a sandbox tester and trigger a purchase on a TestFlight build, isSandbox returns true. On a real App Store build with a real purchase, it returns false. The reason I prefer this over the older "look at the receipt URL" trick from StoreKit 1 days is that the receipt-URL approach can't be trusted without server-side verification and doesn't distinguish Xcode-local testing. If you've already moved to StoreKit 2, just use environment. For a deeper StoreKit 2 walkthrough, Implementing in-app purchases with StoreKit 2 in Rork Max ties the sandbox detection back into the full purchase flow.
Google Play Billing: license testers and the closed-track requirement
Google's model is structurally different. You add an email under "License testing → Test accounts" in Play Console; when a device signed in with that account installs an internal-testing build, purchases are treated as test purchases and no money changes hands.
The single biggest gotcha here is that license testing only applies to internal testing and closed testing tracks — not to the production track. If you push your AAB straight to the production track during this validation phase, license testers will be billed for real and you'll be filing refunds. When you ship a Rork-generated app to Google Play for the first time, always promote through internal testing first.
Subscription renewal periods are also accelerated in sandbox: a monthly plan renews roughly every 5 minutes during testing, and a yearly plan compresses to about half an hour. That's great for fast iteration on the renewal-failure scenario, but it also means you should leave the app idle (preferably backgrounded) so the renewal cycle actually fires; foregrounded apps sometimes throttle the simulated renewal. There's also a per-account cap of around 50 renewals before the subscription auto-cancels, so if you've been hammering a single tester account for days of debugging, switch to a fresh license tester rather than chasing phantom renewal failures that are really just the cap kicking in. If you're new to the Android side of subscriptions, the Google Play Billing Android subscription implementation guide gives you the full picture and helps you map sandbox observations back to the production behavior they simulate.
What gets harder when RevenueCat sits in the middle
RevenueCat hides a lot of the per-store complexity, which is mostly a feature. The downside, especially in sandbox, is that you lose direct visibility into what the store is actually doing. Two things have made a real difference for me:
First, enable "Show sandbox data" in the RevenueCat dashboard and watch the Customer page in real time while you trigger purchases. Entitlement grants and revocations show up with timestamps, and you can verify that the right product ID is being recorded.
Second, turn on Verbose logging in development builds only. Drop this near your SDK initialization in the Rork-generated code:
import Purchases, { LOG_LEVEL } from 'react-native-purchases';
if (__DEV__) {
// Expected output: noisy [Purchases] - DEBUG lines covering every API call
Purchases.setLogLevel(LOG_LEVEL.VERBOSE);
}
Purchases.configure({
apiKey: process.env.EXPO_PUBLIC_REVENUECAT_KEY!,
});The reason I gate this behind __DEV__ is that verbose logs include user-adjacent identifiers (App User IDs, transaction IDs) that I don't want spilling into production crash reports or Sentry events. Before any release build, double-check that this block is actually compiled out.
If purchases succeed but your app's entitlement state never updates, the RevenueCat purchase status sync fix guide has the typical root causes catalogued. A subtle one I've personally lost time to is product identifier mismatch between App Store Connect, the RevenueCat dashboard, and the Rork-generated productIdentifiers array — capitalization differences that look identical at a glance but cause RevenueCat to silently treat the purchase as belonging to no offering. Always copy-paste the IDs rather than retyping them.
Pre-release checklist
Run through this list before you let any subscription build leave TestFlight or Google Play's internal testing track:
- All four scenarios (new purchase, trial conversion, cancellation, failed renewal) reproduced in sandbox
- Server-side webhooks (App Store Server Notifications V2 and Google's Real-time Developer Notifications) confirmed reaching your backend
- StoreKit Configuration disabled in the Xcode scheme; no
.storekitfile referenced in EAS build - Google Play license testers verified on the internal testing track only
- RevenueCat sandbox transactions visible in the dashboard's Customer view
- Verbose RevenueCat logging compiled out of release builds
The webhook side is where many indie shipped apps quietly fall behind real-time subscription state. If you haven't wired this up yet, handling App Store Server Notifications V2 in your own backend walks through a self-hosted implementation you can lift directly.
Wrap-up — the one thing to do today
Sandbox testing is less about whether you did it and more about how many failure modes you actually reproduced. If you read this and want to act on one thing, run the failed renewal / grace period scenario through your sandbox tester once before the day ends. Most indie developers test happy-path purchase and call it done; the failed-renewal flow is the single biggest source of customer-support tickets after launch, and walking through it once with your own hands changes how confidently you respond when the first real billing-retry email lands in your inbox. Once that path is verified, the rest of the checklist becomes a matter of mechanical confirmation rather than guesswork — and the moment you ship feels much less like a leap of faith.