Raxx · internal docs

internal · gated

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-0007docs/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

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):

  1. ASC → Apps → Raxx → Subscriptions → Raxx Pro Subscriptions → Raxx Pro+ Monthly
  2. Under Subscription Prices, click + Add Introductory Offer
  3. Set: - Type: Pay As You Go - Price: $29.00/month (USD) - Number of periods: 6 - Duration per period: 1 month
  4. 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

  1. Xcode menu: Product → Scheme → Edit Scheme
  2. Select the Run action (left sidebar)
  3. Options tab
  4. 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 Email 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

  1. Go to appstoreconnect.apple.com — sign in with kris@moosequest.net + 2FA.
  2. Navigate to Users and Access → Sandbox → Testers.
  3. Click + (Add Sandbox Tester).
  4. Fill in the form:
Field Value
First name IAP
Last name TestFresh
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
  1. Click Save. Apple sends a verification email to the tester address.
  2. Check ops@raxx.app (Google Workspace inbox) for the verification link. Complete it. The account is not usable until the email is verified.
  3. Repeat for iap-test-proplus@raxx.app using the IAP_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

  1. 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).
  2. Disable the StoreKit configuration in the scheme before building: Edit Scheme → Run → Options → StoreKit Configuration → None
  3. Build and install: Cmd+R with the device selected in Xcode.

Purchase flow

  1. Open Raxx. Sign in with the operator passkey.
  2. Navigate to the billing screen (Dashboard → tap your plan tier → Billing, or similar nav path).
  3. Tap Subscribe on any tier card.
  4. 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).
  5. The sandbox sheet shows [Environment: Sandbox] in the header area.
  6. 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:

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


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