One-time purchases, trials and subscriptions over StoreKit 2, for macOS 26 and iOS 26, with a simulated store for testing them.
Early. Non-consumables, trials, auto-renewable subscriptions with their offers, and non-renewing subscriptions. No consumables. The API may still move before 1.0, and it moved here: see Breaking below.
.package(url: "https://github.com/Kosikowski/swift-storekit.git", .upToNextMinor(from: "0.3.0"))This release adds subscriptions. Before anything was built, each StoreKit behaviour the design depends on was measured against real StoreKit, on macOS 26.6 and in the iOS 27 simulator. Seven of the answers changed the design. For example, the listing cannot decide who is subscribed. And at the end of every period, StoreKit says for a moment that the subscription has ended. A subscriber must never be locked out by that.
Auto-renewable subscriptions
static let catalogue: Catalogue = [
.subscription(monthly, in: membership, level: 2),
.subscription(yearly, in: membership, level: 2),
.subscription(plus, in: membership, level: 1),
]
let membership = store.standing.subscription(in: Shop.membership) // .unknown, .none, .active or .inactive- Access follows Apple's rule: subscribed, or in a grace period. Billing retry is reported, but grants no access.
HeldSubscriptiongives the state, the renewal, the offer in force, who owns it, and the date access ends. - The status decides, not the listing. The iOS simulator lists a subscription in billing retry, and both platforms list nothing for a moment at each renewal.
- A lapse at the end of a period is believed only if it lasts. For up to 0.7 s at every renewal, StoreKit says the subscription has ended. On iOS this looks exactly like a real lapse.
- Plan changes are read by comparison. A downgrade comes back from
purchase()as a success with the old plan still held. The store compares what it asked for with what it got, and returns.planChangeScheduled(to:at:). - Status changes are heard as they happen. Expiries, cancellations and grace periods send no transaction, so the App Store front also listens to
Status.updates. ManageSubscriptionsButtonshows Apple's sheet on iOS. On the Mac, in a Mac Catalyst app, and for an iPhone or iPad app running on a Mac, it opens the App Store's subscriptions page instead. The store reads again when the app becomes active.
Offers
StoreProduct.subscriptioncarries each offer's terms as StoreKit states them, so a paywall never writes prices itself.- Introductory eligibility has four states. StoreKit's own answer never changes during a process, so the App Store front corrects it using the group's transactions.
- Win-back offers are the ones Apple allows on the account's own status:
winBackOffers(in:). - Promotional offers and the introductory override are signed by the app's
OfferSigning, usually its server. The package holds no key and never buys without a signature. It never asks the signer for someone who has never subscribed. - If StoreKit did not apply an offer, the purchase returns
.offerNotApplied. PurchaseOptionscarries the offer, a billing plan, and an app account token.
The rest of what StoreKit sells subscriptions with
- Non-renewing subscriptions:
.nonRenewing(_:lasting:stacking:). StoreKit gives them no expiry, so the store works out each period from the purchase dates. - 12-month commitments billed monthly: billing plans on a purchase and on a product, and where a subscription stands in its commitment.
- Purchases asked for outside the app (
PurchaseIntent): kept inrequestedPurchasesuntil the app buys or dismisses them. - Subscription bundles, from the 27 SDK.
- Apple's messages:
.storeMessages(deferredWhile:showing:)holds price-rise, billing and win-back sheets while the app asks it to. - Purchases made in Apple's views:
takePurchase(_:of:)andtakeRedemption(_:). In the iOS simulator, an unlock bought inProductViewis announced nowhere else.
Testing
The simulated store renews on its own clock. It goes into a grace period, then billing retry, then expiry, and by default it reproduces the moment at each renewal. A test runs a year of monthly renewals, and the subscriber is never locked out. It also covers offers, commitments, non-renewing subscriptions and requested purchases.
- Scenarios gain
subscribed=,cancelled=,grace=,retry=,lapsed=,period=,renewal=,intro=,winback=,promo=andsignatures=. - The debug panel gains a line for each group, and controls to match.
StoreKitConfigurationchecks subscriptions and offers in the.storekitfile against the catalogue.
Not measured
Xcode's test environment could not produce purchase intents, the 12-month commitment, Apple's messages or subscription bundles. These are built on Apple's documented API. Family Sharing, a renewal while the app is closed, and a promotional offer signed with a real key need the sandbox. None of these has been run by hand in the sandbox for this release. The roadmap's known limits lists them.
Verified
- CI, with Xcode 26.6: every push runs
make check, which covers:- the tests in debug and release;
- the iOS and Mac Catalyst builds;
- the release check, of the package and of the Demo's Release build.
- The hosted lane: real StoreKit on macOS, with Xcode 26.6. It passes. That test environment renews when told to fail a charge, and gives no grace period when told to give one. The package reports what StoreKit said, and the suite expects this with Xcode 26.6 only (spike).
- The same suite with Xcode 27, on the Mac and in the iOS 27 simulator: run locally, along with the UI tests.
Breaking
ProductAccessgains.subscribedand.nonRenewing.PurchaseCompletiongains.subscribed,.offerNotApplied,.planChangeScheduledand.nonRenewing. Exhaustive switches must handle them.- The ports have new requirements, which matter to a store front or fake an app writes itself.
ProductPurchasingandPurchaseCommandingtakeoptions:, andPurchaseCommandinggainsdismissRequestedPurchase(_:).PurchaseStateProvidinggainsrequestedPurchases,introductoryOffer(for:)andwinBackOffers(in:). Calls topurchase(_:)andpurchase(_:confirmation:)compile as before.
Why each of these was chosen, and what was rejected, is in decisions D35–D59 in docs/10-decisions.md. Start with subscriptions and offers.