iOS IAP sandbox testing
System: Raxx iOS app — StoreKit 2 subscription billing
Owner: ios-agent / operator
Companion: docs/ops/runbooks/billing-test-tooling.md §7 (Apple IAP overview)
ADR: ADR-0007 — docs/architecture/adr/0007-ios-subscription-billing-iap.md
Last reviewed: 2026-06-17 (updated: ASC product records provisioned via API)
Current IAP / StoreKit implementation state
| Component | Status | Notes |
|---|---|---|
Product ID constants (StoreKitManager.ProductID) |
REAL — matches .storekit | app.raxx.ios.{founders.6mo,pro.monthly,pro.annual,proplus.monthly} |
Product.products() fetch |
REAL | Loads from local StoreKit config in dev/test; from App Store in production |
product.purchase() flow |
REAL | StoreKit 2 native; handles .success, .userCancelled, .pending |
Transaction.updates listener |
REAL | Wired in StoreKitManager.init; cancelled in deinit — no receipts lost |
Transaction.currentEntitlements |
REAL | StoreKitManager.currentEntitlements() added; returns Set<String> of active product IDs |
| Restore purchases UI | STUB | currentEntitlements() is implemented; UI hook in BillingView is not yet wired. File a follow-up. |
Transaction.finish() after purchase |
REAL | Called after both purchase and updates listener verify |
| StoreKit Configuration file | REAL | ios/Raxx/Resources/RaxxProducts.storekit — all 4 products defined |
| Scheme wiring (local testing) | REAL | Raxx.xcscheme has storeKitConfigurationFileReference set for both TestAction and LaunchAction |
| App Store Connect product records | PROVISIONED (MISSING_METADATA) | All 4 records created via ASC API 2026-06-17 — see §0 below for IDs + remaining steps |
| Sandbox tester accounts | NOT YET CREATED | ASC API does not support CREATE for sandbox testers — operator dashboard step required (see §2) |
| Server-side transaction verification | NOT IMPLEMENTED | See §6 below — this is the primary gap blocking full billing SOP |
The iOS app can initiate, complete, and observe purchases end-to-end in the Simulator (StoreKit local config) and in the Apple sandbox (sandbox tester on device). What it cannot do yet is notify the backend — the server-side endpoint and JWS verification logic are not built.
0. App Store Connect product record status (provisioned 2026-06-17)
All four IAP product records were created via the ASC REST API (Admin role key from vault at
/Raxx/iOS/AppStoreConnect/) on 2026-06-17. ASC IDs are stored in vault at /Raxx/iOS/Sandbox/.
App record
- App: Raxx by MooseQuest
- Bundle ID:
app.raxx.ios(org-canonical; already registered) - ASC App ID:
6778650155
IAP records created
| Product ID | ASC ID | Type | State | Price |
|---|---|---|---|---|
app.raxx.ios.founders.6mo |
6781305235 |
Non-renewing subscription | MISSING_METADATA |
$29.00 USD — price schedule set via API |
app.raxx.ios.pro.monthly |
6781305735 |
Auto-renewable (1 month) | MISSING_METADATA |
NOT SET — price pending BLR decision |
app.raxx.ios.pro.annual |
6781305904 |
Auto-renewable (1 year) | MISSING_METADATA |
NOT SET — price pending BLR decision |
app.raxx.ios.proplus.monthly |
6781305906 |
Auto-renewable (1 month) | MISSING_METADATA |
NOT SET — price pending BLR decision |
Auto-renewable subscriptions are in subscription group "Raxx Pro Subscriptions" (group ID 22164259).
English (US) localizations are set on all four products and the group.
State: MISSING_METADATA — what it means
MISSING_METADATA is expected at this stage. Apple requires a review screenshot (a screenshot of the in-app purchase flow within the app) before a product can move to READY_TO_SUBMIT. This is dashboard-only: no ASC API endpoint accepts screenshot uploads for IAP products.
To complete the metadata: 1. Build Raxx on device or Simulator with the billing screen visible. 2. Take a screenshot of the billing/paywall screen showing one of the IAP products. 3. In ASC dashboard: Apps → Raxx → In-App Purchases → [product] → Review Information → Screenshot → Upload. 4. Repeat for all four products (one screenshot per product, can reuse the same billing screen image).
After screenshot upload, each product moves to READY_TO_SUBMIT and can be attached to an App Store version submission.
Pricing decision needed (operator/BLR)
The three Pro and Pro+ products exist in ASC with no price set. Setting a price requires a confirmed dollar amount, which is pending BLR research per the project pricing memory (only Free + Founders $29 are locked). Once BLR delivers the Pro/Pro+ price recommendation:
- In ASC dashboard: Apps → Raxx → Subscriptions → Raxx Pro Subscriptions → [product] → Pricing → Set Base Price
- Or via ASC API
POST /v1/subscriptionPricePointsusing the subscription's price point ID (the same pattern used for Founders).
Do NOT invent prices. This is a blocked operator/BLR decision.
Server notification URL — deferred until #3629
The sandbox and production server notification URLs in ASC are not set. Setting them to a non-existent endpoint would cause Apple to begin delivering unhandled POSTs. They will be configured once #3629 (Raptor POST /api/subscriptions/apple/notifications) ships:
- Sandbox URL:
https://api.raxx.app/api/subscriptions/apple/notifications - Production URL:
https://api.raxx.app/api/subscriptions/apple/notifications
Configure in: ASC dashboard → Apps → Raxx → App Information → App Store Server Notifications.
1. StoreKit Configuration file (local Simulator testing)
What it is
ios/Raxx/Resources/RaxxProducts.storekit is a local product catalogue that Xcode's StoreKit testing framework reads instead of contacting the live App Store. When it is active, Product.products() returns the four Raxx products with placeholder prices, and product.purchase() completes immediately with a locally-signed VerificationResult — no Apple account, no network, no charges.
How to enable it in Xcode
The scheme (ios/Raxx.xcodeproj/xcshareddata/xcschemes/Raxx.xcscheme) already wires the configuration file for both the TestAction (unit tests) and the LaunchAction (run in Simulator). No manual step is needed unless you start from a clean Xcode open.
To verify it is active:
1. Xcode menu: Product → Scheme → Edit Scheme
2. Select the Run action (left sidebar)
3. Options tab
4. Confirm StoreKit Configuration shows RaxxProducts.storekit
Repeat for the Test action.
To temporarily disable local testing (to test against the real App Store sandbox or production):
- Set StoreKit Configuration to None for the relevant action.
Products defined in RaxxProducts.storekit
| Product ID | Type | Sandbox price | Display name |
|---|---|---|---|
app.raxx.ios.founders.6mo |
Non-renewing subscription | $0.00 (local) | Raxx Founders |
app.raxx.ios.pro.monthly |
Auto-renewable, 1-month period | $0.00 (local) | Raxx Pro Monthly |
app.raxx.ios.pro.annual |
Auto-renewable, 1-year period | $0.00 (local) | Raxx Pro Annual |
app.raxx.ios.proplus.monthly |
Auto-renewable, 1-month period | $0.00 (local) | Raxx Pro+ Monthly |
Real prices are pending BLR research and must be set in App Store Connect when the ASC product records are created (Phase D in ios/README.md). The .storekit file uses no price because the local framework does not charge.
Sandbox renewal cadence (Simulator / StoreKit local config)
When using the StoreKit configuration file in the Simulator, subscription periods are compressed:
| Subscription period | Simulator duration |
|---|---|
| 1 month | ~2 minutes (Simulator default) |
| 1 year | ~3 minutes |
You can override this in the Xcode StoreKit Transaction Manager (Debug → StoreKit → Manage Transactions). The Simulator resets all local purchase state when the app is uninstalled or when you use Reset Eligibility in the Transaction Manager.
2. App Store Connect sandbox tester setup (operator action required)
The ASC API allows GET and UPDATE on sandbox testers but does NOT allow CREATE. Confirmed
via API: "The resource 'sandboxTesters' does not allow 'CREATE'. Allowed operations are:
GET_COLLECTION, UPDATE". Sandbox testers must be created in the ASC dashboard by the operator
(requires Apple ID + 2FA).
The intended credentials for all three required testers are pre-staged in Infisical at
/Raxx/iOS/Sandbox/ so you can copy-paste them during dashboard creation without generating
new passwords:
| Vault key | Value |
|---|---|
IAP_TESTER_FRESH_EMAIL |
iap-test-fresh@raxx.app |
IAP_TESTER_FRESH_PASSWORD |
retrieve from vault |
IAP_TESTER_PRO_MONTHLY_EMAIL |
iap-test-pro-monthly@raxx.app |
IAP_TESTER_PRO_MONTHLY_PASSWORD |
retrieve from vault |
IAP_TESTER_FOUNDERS_EMAIL |
iap-test-founders@raxx.app |
IAP_TESTER_FOUNDERS_PASSWORD |
retrieve from vault |
Steps
- Go to
appstoreconnect.apple.com(operator's Apple ID + 2FA) - Navigate to Users and Access → Sandbox → Testers
- Click + (Add Sandbox Tester)
- Fill in the fields using vault credentials from
/Raxx/iOS/Sandbox/: - First name:IAP| Last name:TestFresh(or match the scenario label) - Email: from vault (IAP_TESTER_FRESH_EMAIL) - Password: from vault (IAP_TESTER_FRESH_PASSWORD) - Secret question + answer: choose any; store answer in vault at/Raxx/iOS/Sandbox/IAP_TESTER_FRESH_SECRET_ANSWER- Date of birth: any past date (e.g. 1990-01-15) - Store region: United States - Click Save
- Apple sends a verification email. Check
ops@raxx.app(Google Workspace) for the verification link. Complete it. The tester is not usable until the email is verified. - Repeat for
iap-test-pro-monthly@raxx.appandiap-test-founders@raxx.app.
Incoming mail to @raxx.app routes through Google Workspace and is accessible via ops@raxx.app.
Minimum sandbox testers to create before running the billing SOP
| Tester | Purpose | Credentials in vault |
|---|---|---|
iap-test-fresh@raxx.app |
Happy path: first-time subscriber, no prior purchase history | IAP_TESTER_FRESH_* |
iap-test-pro-monthly@raxx.app |
Renewal cycle: subscribe, observe ~5-min renewal, cancel | IAP_TESTER_PRO_MONTHLY_* |
iap-test-founders@raxx.app |
Non-renewing subscription: one-time Founders purchase | IAP_TESTER_FOUNDERS_* |
3. Testing a sandbox purchase on a real iPhone
Apple IAP does NOT work in the Simulator — product.purchase() returns immediately with a locally-signed transaction from the StoreKit config file. To exercise the real Apple sandbox payment sheet (the one that appears on actual devices), you need a physical iPhone.
One-time device setup
- On the iPhone: Settings → [your name] → Sign Out (sign out of the real Apple ID). - You do NOT need to sign out of the App Store system-level to use a sandbox tester. The sandbox tester sign-in happens inside the app purchase flow, not at the system level. - Signing out avoids inadvertently charging the real Apple ID if the wrong account is used.
- Do NOT sign IN to the system-level Apple ID as a sandbox tester. Apple explicitly says not to do this — sandbox testers are for in-app purchase prompts only.
Purchase flow
- Build and install Raxx on the device via Xcode USB (Cmd+R) with the device selected.
- Disable the StoreKit configuration file first: Edit Scheme → Run → Options → StoreKit Configuration → None
- With
None, the app talks to the real Apple sandbox instead of the local file. - Open Raxx → navigate to the subscription screen (Dashboard → tap your plan → Billing).
- Tap Subscribe on any tier.
- Apple's payment sheet appears. When it prompts for Apple ID, enter the sandbox tester credentials (
iap-test-fresh@raxx.app+ stored password from Infisical). - The sandbox payment sheet shows
[Environment: Sandbox]in the top region of the sheet. - Complete the purchase.
The purchase goes through with no real charge. The tester's sandbox account reflects the purchase in ASC → Sandbox → Testers → [tester] → Manage.
Verifying the purchase in the app
After a successful purchase:
// In BillingViewModel or wherever you gate entitlement:
let entitled = await storeKitManager.currentEntitlements()
// entitled contains the product ID just purchased
assert(entitled.contains("app.raxx.ios.pro.monthly"))
You can also inspect the purchase in Xcode's Debug → StoreKit → Manage Transactions (even for device-based purchases, when the device is connected).
Sandbox renewal cadence on device
| Real duration | Sandbox duration on device |
|---|---|
| 1 week | ~3 minutes |
| 1 month | ~5 minutes |
| 2 months | ~10 minutes |
| 3 months | ~15 minutes |
| 6 months | ~30 minutes |
| 1 year | ~1 hour |
Apple caps sandbox renewals at 12 per subscription before it stops renewing. Reset via ASC → Sandbox → Testers → [tester] → Clear Purchase History.
4. Managing and resetting sandbox state
Cancelling a sandbox subscription (device)
- On the device: Settings → [sandbox tester name] → Subscriptions (appears only after a sandbox purchase with that account).
- Tap the subscription → Cancel Subscription.
Resetting purchase history for a tester
In App Store Connect: - Users and Access → Sandbox → Testers → [tester] → Manage → Clear Purchase History
This allows the same tester account to be reused for a clean-slate test. The tester's prior transactions are cleared from Apple's sandbox environment; locally-stored Transaction.currentEntitlements clears when the app re-launches against the sandbox.
Xcode Transaction Manager (Simulator)
In Xcode with a Simulator session running: - Debug → StoreKit → Manage Transactions
From here you can: - View all purchases made in the current Simulator session - Refund a transaction (simulates an Apple refund notification) - Expire a subscription - Reset all purchase history for the Simulator
5. CI / automated testing
The GitHub Actions ios-ci.yml workflow runs xcodebuild test on macos-14 with the Raxx scheme, which now includes storeKitConfigurationFileReference pointing to RaxxProducts.storekit. This means:
Product.products(for:)in unit tests resolves against the local fileStoreKitManagerTests.testStorekitFileProductIDsMatchCodeConstantscatches any drift between the Swift constants and the.storekitfile- Actual purchase calls (
product.purchase()) are NOT triggered in unit tests — they require user interaction - The CI run does not touch Apple's sandbox; it is entirely local
The .storekit file is a plain JSON file tracked in git. No secrets, no Apple credentials needed for the CI run.
6. Server-side transaction verification (gap — blocking full billing SOP)
This is the primary gap. The iOS billing SOP cannot be fully exercised until this is built.
What is required (ADR-0007 compliance checklist item)
After a purchase completes on the device, Apple sends a server-to-server notification (v2) to a backend endpoint. Raptor must implement:
POST /api/subscriptions/apple/notifications
This endpoint must:
1. Parse the signed payload: Apple sends a signedPayload JWT in the request body — a JWS (JSON Web Signature) using the ES256 algorithm, signed with Apple's private key.
2. Verify the JWS signature against Apple's public key (fetched from https://appleid.apple.com/auth/keys — Apple rotates these; use a short-lived cache).
3. Extract originalTransactionId from the decoded payload and store it. This is the durable identifier across all renewals (not the rolling transactionId).
4. Handle notification types:
- SUBSCRIBED — new subscription; set subscription active
- DID_RENEW — renewal succeeded; extend current_period_end
- EXPIRED — subscription lapsed; revoke entitlement
- DID_FAIL_TO_RENEW — billing retry in flight; set grace period
- REFUND — Apple refunded; revoke entitlement
- REVOKE — Family Sharing revocation
5. Respond 200 if successfully processed; Apple retries on non-200.
Sandbox vs production notification URL
The Apple sandbox sends notifications to a different URL than production. You configure both in App Store Connect:
- Sandbox notification URL:
https://api.raxx.app/api/subscriptions/apple/notifications(same endpoint, but Apple sandbox POSTs to it when purchases are made with sandbox credentials) - Production notification URL: same URL
Apple distinguishes sandbox from production by the environment field in the decoded JWS payload (SANDBOX vs PRODUCTION). The endpoint should handle both.
When testing locally or on staging, the sandbox can be pointed at a staging URL. Set it in App Store Connect → your app → App Information → App Store Server Notifications → Sandbox URL:
https://raxx-api-staging-<hash>.herokuapp.com/api/subscriptions/apple/notifications
What to verify after a sandbox purchase (once the endpoint exists)
# Check Raptor staging logs for the Apple notification
heroku logs --app raxx-api-staging --num 50 | grep -i "apple\|iap\|storekit\|notification"
# Verify the subscription row was written
heroku pg:psql --app raxx-api-staging -c \
"SELECT id, subscription_source, plan_tier, status, original_transaction_id
FROM billing_subscription_mirror
WHERE subscription_source = 'ios'
ORDER BY created_at DESC LIMIT 5;"
What to file
A feature-developer card is needed:
- Title: feat(backend): Apple App Store Server Notifications v2 endpoint + JWS verification
- Scope: Raptor — new route POST /api/subscriptions/apple/notifications, JWS verification using Apple's public key endpoint, original_transaction_id storage, notification type dispatch
- Unblocks: iOS billing E2E test SOP item B-4 and above
Until this is built, only the client-side (device purchase flow) half of the iOS billing SOP can be exercised.
7. Pre-flight checklist for a sandbox purchase test
[ ] StoreKit Configuration file is DISABLED in scheme (Run → Options → StoreKit Conf = None)
(Enable it ONLY for Simulator / unit test work; disable for real sandbox on device)
[ ] Device is running the Raxx app built from Xcode (Cmd+R, target = your device)
[ ] Sandbox tester account verified in App Store Connect (email verification completed)
[ ] Sandbox tester email + password retrieved from Infisical at /Raxx/iOS/Sandbox/
[ ] Device Apple ID: personal Apple ID signed OUT at system level
(Or leave signed in — the purchase sheet will prompt for sandbox credentials separately;
just confirm you're not using the personal Apple ID account during the purchase flow)
[ ] Xcode console open (Window → Devices and Simulators → your device → Open Console)
to watch StoreKit log output during the purchase
[ ] If testing server-side notifications: Raptor staging is deployed + notification
endpoint is live (see §6 — NOT yet available as of 2026-06-17)
References
- ADR-0007:
docs/architecture/adr/0007-ios-subscription-billing-iap.md - StoreKit config file:
ios/Raxx/Resources/RaxxProducts.storekit - Xcode scheme:
ios/Raxx.xcodeproj/xcshareddata/xcschemes/Raxx.xcscheme - StoreKit 2 manager:
ios/Raxx/Features/Billing/StoreKitManager.swift - Billing screen:
ios/Raxx/Features/Billing/BillingView.swift - Billing test tooling (Stripe + Apple overview):
docs/ops/runbooks/billing-test-tooling.md - iOS README (install + Phase D IAP checklist):
ios/README.md - Apple documentation:
- StoreKit testing in Xcode:
https://developer.apple.com/documentation/storekit/testing_in_xcode_and_the_sandbox_environment - App Store Server Notifications v2:
https://developer.apple.com/documentation/appstoreservernotifications - App Store Server API (transaction lookup):
https://developer.apple.com/documentation/appstoreserverapi - Sandbox tester management:
https://developer.apple.com/documentation/storekit/testing_in_the_sandbox_environment