RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Articles/Dev Tools
Dev Tools/2026-05-02Intermediate

Sandbox Testing for Rork Subscriptions — The Apple and Google Pitfalls Indie Devs Hit Most

A practical walkthrough of validating Rork-generated subscription code in Apple and Google sandbox environments, covering the specific pitfalls solo developers hit most often before going live.

Rork515subscriptions9sandboxStoreKit 216Google Play Billing4testing5

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 .storekit file 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.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-07-11
Verifying StoreKit 2 Signed Transactions in a Cloudflare Worker
If you trust whatever the device says about its purchase state, a tampered entitlement walks right through. This walks through verifying StoreKit 2 signed transactions (JWS) in a Cloudflare Worker so you grant entitlements without trusting the client, with working code.
Dev Tools2026-06-28
What Rork's Browser Live Simulator Can Verify — and What Belongs on a Real Device
Rork's in-browser live simulator collapses the build-test loop dramatically, but it can't judge everything. Here's a clear, item-by-item split between what the simulator is enough for and what you must move to a real device through Companion.
Dev Tools2026-05-27
Three-Layer StoreKit 2 Entitlement Sync for Rork Apps: Launch, Background Refresh, and Restore
When you wire StoreKit 2 subscriptions into a Rork-generated app, Transaction.updates alone leaves gaps. Here is the three-layer sync I run across six wallpaper apps — launch-time re-evaluation, Background App Refresh, and Restore Purchase — including measured refresh rates and the AdMob revenue I recovered.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →