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-07-13 (updated: 2-product launch model; Founders = Pro+ intro offer; prices locked; sandbox tester creds staged in vault)
Current IAP / StoreKit implementation state
| Component | Status | Notes |
|---|---|---|
Product ID constants (StoreKitManager.ProductID) |
REAL — matches .storekit | app.raxx.ios.pro.monthly, app.raxx.ios.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() returns Set<String> of active product IDs |
| Restore purchases UI | STUB | currentEntitlements() is implemented; the BillingView hook to surface it to the user is not yet wired. |
Transaction.finish() after purchase |
REAL | Called after both purchase and updates listener verify |
| StoreKit Configuration file | REAL | ios/Raxx/Resources/RaxxProducts.storekit — 2 launch products defined (pro.monthly, proplus.monthly with Founders intro) |
| Scheme wiring (local testing) | REAL | Raxx.xcscheme has storeKitConfigurationFileReference set for both TestAction and LaunchAction |
| App Store Connect product records | PROVISIONED (MISSING_METADATA) | 2 launch products in ASC; prices need to be set; review screenshot needed for READY_TO_SUBMIT |
| Sandbox tester accounts | CREDENTIALS STAGED — ASC DASHBOARD STEP REQUIRED | Credentials in vault at /Raxx/iOS/Sandbox/; operator must create accounts in ASC UI (see §2) |
| Server-side transaction verification | NOT IMPLEMENTED | See §6 — gaps tracked in sub-cards #3685/#3686/#3687 |
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). Server-side JWS verification and entitlement persistence (sub-cards #3685–#3687) are the remaining gap for full billing SOP.
0. App Store Connect product record status
App record
- App: Raxx by MooseQuest
- Bundle ID:
app.raxx.ios(MooseQuest LLC org team CM62W3T483) - ASC App ID:
6778650155
Active launch products (as of 2026-07-13)
| Product ID | ASC ID | Type | Sub group | Tier | Period | Price (locked) | ASC state |
|---|---|---|---|---|---|---|---|
app.raxx.ios.pro.monthly |
6781305735 |
Auto-renewable | Raxx Pro Subscriptions (22164259) |
Pro | 1 month | $39.00/mo | MISSING_METADATA |
app.raxx.ios.proplus.monthly |
6781305906 |
Auto-renewable | Raxx Pro Subscriptions (22164259) |
Pro+ / Founders | 1 month | $79.00/mo (standard); $29.00/mo × 6 periods (Founders intro) | MISSING_METADATA |
Retired products (no longer in code):
- app.raxx.ios.founders.6mo (ASC ID 6781305235) — non-renewing; retired per issue #3889. The Founders offer is now modelled as the Pro+ monthly subscription with a pay-as-you-go introductory offer. The ASC non-renewing record must be deactivated in the ASC dashboard — operator action required.
- app.raxx.ios.pro.annual (ASC ID 6781305904) — excluded from launch scope (monthly-only, 2026-06-28). Draft record exists in ASC; do not activate until annual pricing is decided.
Prices — locked, pending ASC dashboard entry
Prices are locked per project_pricing_tiers_locked:
- Pro monthly: $39.00/mo
- Pro+ monthly (standard): $79.00/mo
- Founders introductory offer on Pro+: $29.00/mo × 6 periods (pay-as-you-go), then auto-renews at $79.00/mo
These values are confirmed in RaxxProducts.storekit (the Simulator reference file). The ASC
records currently have no price set — an operator dashboard step is needed:
ASC → Apps → Raxx → Subscriptions → Raxx Pro Subscriptions → [product] → Pricing → Set Base Price
Founders introductory offer — ASC configuration required
The Founders offer is a StoreKit introductory offer on app.raxx.ios.proplus.monthly. It must
be configured in ASC dashboard (not yet done):
- ASC → Apps → Raxx → Subscriptions → Raxx Pro Subscriptions → Raxx Pro+ Monthly
- Under Subscription Prices, click + Add Introductory Offer
- Set: - Type: Pay As You Go - Price: $29.00/month (USD) - Number of periods: 6 - Duration per period: 1 month
- Save.
When the offer is live in ASC, BillingView automatically shows the Founders card to eligible
users (first-time Pro+ subscribers). The offer display copy is generated by
StoreKitManager.foundersIntroDisplayString(introDisplayPrice:periodCount:productDisplayPrice:).
State: MISSING_METADATA — what it means
MISSING_METADATA does NOT block sandbox testing. Products in this state are fully functional
in the Apple sandbox environment. What MISSING_METADATA blocks is submission for App Review.
To move each product to READY_TO_SUBMIT:
1. Set a base price (see above).
2. Upload a review screenshot: ASC → In-App Purchases → [product] → Review Information → Screenshot. The screenshot should show the billing/paywall screen in the app. A Simulator screenshot is acceptable.
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 active,
Product.products() returns the two Raxx launch products and product.purchase() completes
with a locally-signed VerificationResult — no Apple account, no network, no charges.
How to verify it is active
- Xcode menu: Product → Scheme → Edit Scheme
- Select the Run action (left sidebar)
- Options tab
- Confirm StoreKit Configuration shows
RaxxProducts.storekit
The scheme already wires this for both the Run and Test actions. No manual step needed.
To test against the real Apple sandbox (on device)
Set StoreKit Configuration to None for the Run action. With None, the app contacts the
real App Store sandbox and Apple's payment sheet appears. This requires a sandbox tester account
(§2 below) and a physical device.
Products in RaxxProducts.storekit
| Product ID | Type | Price (Simulator) | Introductory offer |
|---|---|---|---|
app.raxx.ios.pro.monthly |
Auto-renewable, 1-month | $39.00 | None |
app.raxx.ios.proplus.monthly |
Auto-renewable, 1-month | $79.00 | Pay As You Go: $29.00 × 6 periods |
Note: The prices in the .storekit file are simulation values only. The Simulator does not
charge anything. The ASC base prices ($39/$79) are the authoritative prices that appear on a
real device.
Sandbox renewal cadence (Simulator / StoreKit local config)
| Subscription period | Simulator duration |
|---|---|
| 1 month | ~2 minutes |
| 1 year | ~3 minutes |
Override via Debug → StoreKit → Manage Transactions in Xcode.
2. App Store Connect sandbox tester setup
The ASC REST API does not support CREATE for sandbox testers. Confirmed 2026-06-17:
"The resource 'sandboxTesters' does not allow 'CREATE'. Allowed operations are: GET_COLLECTION, UPDATE".
Sandbox tester creation requires the ASC dashboard with the operator's Apple ID + 2FA.
Credentials are staged in Infisical at /Raxx/iOS/Sandbox/. Retrieve them via vault before
starting. Do NOT paste them here.
Tester accounts to create
| Vault key prefix | Purpose | |
|---|---|---|
IAP_TESTER_FRESH_* |
iap-test-fresh@raxx.app |
Happy path: first-time Pro subscriber, clean slate |
IAP_TESTER_PROPLUS_* |
iap-test-proplus@raxx.app |
Pro+ Founders intro offer test |
For the #3688 E2E, the minimum is one: iap-test-fresh@raxx.app.
Creation steps
- Go to
appstoreconnect.apple.com— sign in withkris@moosequest.net+ 2FA. - Navigate to Users and Access → Sandbox → Testers.
- Click + (Add Sandbox Tester).
- Fill in the form:
| Field | Value |
|---|---|
| First name | IAP |
| Last name | TestFresh |
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 to the tester address.
- Check
ops@raxx.app(Google Workspace inbox) for the verification link. Complete it. The account is not usable until the email is verified. - Repeat for
iap-test-proplus@raxx.appusing theIAP_TESTER_PROPLUS_*vault keys.
Reset purchase history (between test runs)
ASC → Users and Access → Sandbox → Testers → [tester] → Manage → Clear Purchase History
This resets sandbox renewal eligibility. Use it between test sessions to start fresh.
3. Testing a sandbox purchase on a real iPhone
Apple IAP does NOT function in the Simulator. The Simulator uses the local StoreKit config file (see §1). A physical iPhone is required to exercise the real Apple sandbox payment sheet.
Device setup
- On the iPhone: system-level Apple ID can remain signed in — Apple's sandbox tester sign-in happens inside the app's payment sheet, not at the system level. Just ensure the system-level Apple ID is NOT a sandbox tester email (those accounts are for in-app prompts only).
- Disable the StoreKit configuration in the scheme before building: Edit Scheme → Run → Options → StoreKit Configuration → None
- Build and install: Cmd+R with the device selected in Xcode.
Purchase flow
- Open Raxx. Sign in with the operator passkey.
- Navigate to the billing screen (Dashboard → tap your plan tier → Billing, or similar nav path).
- Tap Subscribe on any tier card.
- Apple's payment sheet appears. When it prompts for Apple ID, enter the sandbox tester
credentials from vault (
IAP_TESTER_FRESH_EMAIL/IAP_TESTER_FRESH_PASSWORD). - The sandbox sheet shows
[Environment: Sandbox]in the header area. - Confirm the purchase. No real charge occurs.
Verifying the purchase
// In BillingViewModel or wherever you gate entitlement:
let entitled = await storeKitManager.currentEntitlements()
assert(entitled.contains("app.raxx.ios.pro.monthly"))
In Xcode with the device connected: Debug → StoreKit → Manage Transactions shows all sandbox transactions even for device-based purchases.
Sandbox renewal cadence on device
| Real duration | Sandbox duration on device |
|---|---|
| 1 month | ~5 minutes |
| 6 months | ~30 minutes |
| 1 year | ~1 hour |
Apple caps renewals at 12 per subscription. Clear purchase history in ASC to reset.
4. Managing and resetting sandbox state
Cancelling a sandbox subscription (device)
Settings → [sandbox tester name] → Subscriptions → Raxx [tier] → Cancel Subscription
This appears in iOS Settings only after a sandbox purchase with that account.
Xcode Transaction Manager (Simulator or device-connected)
Debug → StoreKit → Manage Transactions
From here you can: - View all purchases in the current session - Refund a transaction (simulates an Apple refund notification) - Expire a subscription (accelerates the ~5 min sandbox period) - Reset all purchase history for the Simulator
5. CI / automated testing
The GitHub Actions ios-ci.yml workflow runs xcodebuild test with the Raxx scheme. The scheme
has storeKitConfigurationFileReference pointing to RaxxProducts.storekit. In CI:
Product.products(for:)resolves against the local.storekitfile — no Apple network callStoreKitManagerTests.testStorekitFileProductIDsMatchCodeConstantscatches ID drift between Swift constants and the.storekitfile- Actual
product.purchase()calls are NOT triggered in unit tests (require user interaction)
The .storekit file is plain JSON tracked in git. No secrets needed for CI runs.
6. Server-side transaction verification (gap — sub-cards #3685–#3687)
The client-side purchase flow is complete. What's missing is the backend pipeline:
POST /api/subscriptions/apple/notifications
This endpoint must:
1. Parse the signedPayload JWS from Apple's server-to-server notification.
2. Verify the JWS signature against Apple Root CA G3.
3. Decode originalTransactionId, notificationType, productId, expiresDate.
4. Handle SUBSCRIBED, DID_RENEW, EXPIRED, DID_FAIL_TO_RENEW, REFUND, REVOKE.
5. Return 200 on success (Apple retries on non-200).
Sub-cards:
- #3685 — JWS verify
- #3686 — persist (apple_iap_subscriptions + apple_iap_notification_log)
- #3687 — entitlement + reconciliation
Until these are merged and FLAG_APPLE_IAP=1 is set in staging, only the client-side half of
the iOS billing SOP is testable. The on-device runbook at
docs/ops/runbooks/ios-iap-e2e-device-runbook.md covers the client-side taps only and is
self-contained for the #3688 device verification.
Notification URL configuration
Once sub-cards ship, set in ASC: Apps → Raxx → App Information → App Store Server Notifications
- Sandbox URL:
https://api-staging.raxx.app/api/subscriptions/apple/notifications - Production URL: leave blank until prod E2E is separately authorized
7. Pre-flight checklist for a sandbox purchase test
[ ] StoreKit Configuration = NONE in scheme (Run → Options → StoreKit Conf = None)
(Enable .storekit ONLY for Simulator / unit test work; NONE for real sandbox on device)
[ ] Device is running the Raxx app built from Xcode (Cmd+R, target = your device)
[ ] Sandbox tester account created and email-verified in ASC (§2)
Credentials at /Raxx/iOS/Sandbox/ in vault
[ ] Device system-level Apple ID is not the sandbox tester email
[ ] Xcode console open (Window → Devices and Simulators → your device → Open Console)
to watch StoreKit log output during the purchase
[ ] For server-side verification (optional for #3688 device E2E):
Sub-cards #3685/#3686/#3687 merged, FLAG_APPLE_IAP=1 in staging, WAF skip rule deployed
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 - On-device E2E runbook (#3688):
docs/ops/runbooks/ios-iap-e2e-device-runbook.md - Apple — StoreKit testing in Xcode:
https://developer.apple.com/documentation/storekit/testing_in_xcode_and_the_sandbox_environment - Apple — App Store Server Notifications v2:
https://developer.apple.com/documentation/appstoreservernotifications - Apple — Sandbox tester management:
https://developer.apple.com/documentation/storekit/testing_in_the_sandbox_environment