How do you test purchases: StoreKit configuration files, sandbox accounts and TestFlight, and what can each not prove?
Predict first
What does the parsed configuration say about ids and price per 30 days?
import Foundation
let json = """
{"identifier":"A1","nonRenewingSubscriptions":[],
"products":[{"displayPrice":"4.99","productID":"com.x.lifetime","referenceName":"Lifetime","type":"NonConsumable"},
{"displayPrice":"0.99","productID":"com.x.coins","referenceName":"Coins","type":"Consumable"}],
"settings":{},
"subscriptionGroups":[{"id":"G1","name":"Pro","subscriptions":[
{"productID":"com.x.pro.monthly","referenceName":"Monthly","type":"RecurringSubscription","displayPrice":"6.99","recurringSubscriptionPeriod":"P1M","groupNumber":2,"subscriptionGroupID":"G1"},
{"productID":"com.x.pro.annual","referenceName":"Annual","type":"RecurringSubscription","displayPrice":"59.99","recurringSubscriptionPeriod":"P1Y","groupNumber":1,"subscriptionGroupID":"G1"}]}],
"version":{"major":4,"minor":0}}
"""
struct Item: Decodable { var productID: String; var type: String; var displayPrice: String; var recurringSubscriptionPeriod: String?; var groupNumber: Int? }
struct Group: Decodable { var name: String; var subscriptions: [Item] }
struct Config: Decodable { var products: [Item]; var subscriptionGroups: [Group] }
let cfg = try! JSONDecoder().decode(Config.self, from: Data(json.utf8))
let all = cfg.products + cfg.subscriptionGroups.flatMap(\.subscriptions)
print("ids unique:", Set(all.map(\.productID)).count == all.count)
for g in cfg.subscriptionGroups {
for s in g.subscriptions.sorted(by: { $0.groupNumber! < $1.groupNumber! }) {
let days: [String: Int] = ["P1W": 7, "P1M": 30, "P1Y": 365]
let per = Double(s.displayPrice)! / Double(days[s.recurringSubscriptionPeriod!]!) * 30
print("\(g.name) level \(s.groupNumber!): \(s.productID) \(s.recurringSubscriptionPeriod!) = \(String(format: "%.2f", per)) per 30 days")
}
}
print("types:", Set(all.map(\.type)).sorted())
- ids unique true; Pro level 1 annual 4.93 and level 2 monthly 6.99 per 30 days; types listed
- ids unique false
- annual costs more per month than monthly
Mental model. Config file: a flight simulator you can crash on purpose; sandbox: a training flight with a real tower; TestFlight: the same with a passenger list; production: the first real flight.
Three layers, from fast and local to real. (1) A StoreKit configuration file (.storekit, JSON edited in Xcode) defines products, groups, prices and offers locally, so you can run in the simulator or on a device without App Store Connect; Xcode's Transaction Manager lets you speed up time (a monthly renewal in minutes), expire subscriptions, refund, revoke, make purchases fail, and simulate Ask to Buy or billing problems. The framework StoreKitTest (SKTestSession) drives the same engine from unit tests and UI tests: you can buy, expire, refund and force errors programmatically. Locally signed transactions use a test certificate, not Apple's, so a server expecting Apple's chain must be configured for tests. (2) The sandbox is the real App Store test environment with real product ids and sandbox Apple IDs; renewals are accelerated (about 5 minutes for a month, a few hours for a year; check Apple's table) and capped, and sandbox transactions come with the sandbox environment flag. (3) TestFlight uses the sandbox payment system, so purchases are free and subscriptions renew quickly; production behaviour only appears after release.
The check parses a small configuration (using the key names I found in Xcode's editor and StoreKitTest binaries; the file itself was not opened in Xcode), verifies product ids are unique, and compares plans by price per 30 days: annual level 1 is 4.93 per 30 days, monthly level 2 is 6.99. Honest limits: SKTestSession needs an XCTest host, so I could not run it from a command-line tool (a bare process aborts when it asks XCTest for its configuration), and none of the sandbox or TestFlight behaviour was exercised here. Typical gaps even after all three: real payment failures and billing retry timing, regional pricing, family sharing across real Apple IDs, and refunds that Apple decides. Keep a short manual script for the first production release, with a small real purchase you refund.
59.99 over 365 days scaled to 30 days is 4.93.
Common traps
- Testing only the happy purchase. Renewal, expiry, refund, pending and failed payment are the paths that lose money.
- Forgetting the config file is selected only for the scheme. Confirm the scheme's StoreKit configuration, or the app silently talks to the sandbox.
They might follow up with
- How do I test on CI? Unit-test your entitlement logic as a pure function and use
SKTestSessioninside an XCTest bundle on a simulator. - Can the config file hold offers? Yes: introductory, promotional, code and win-back offers can be modelled, so test eligibility paths.
Related questions
- 28.5.1 How do you design a paywall that is clear, localised and legal, and how do you run experiments on it?
- 28.3.1 How do you compute what a user is entitled to, and which transactions are excluded from…
- 28.2.1 What are the subscription renewal states, how do grace period and billing retry differ, and who should keep…
- 25.3.2 How does App Review work, what causes common rejections, and what are export compliance, the privacy labels…
← How do you design a paywall that is clear, localised and legal, and…