Skip to content

0.2.0

Choose a tag to compare

@Kosikowski Kosikowski released this 19 Sep 10:23
· 5 commits to main since this release

One-time purchases and trials over StoreKit 2, for macOS 26 and iOS 26, with a simulated store for testing them.

Early. Non-consumables and trials only. No 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.2.0"))

This release comes from moving the first real app onto the package and reviewing it. The biggest change is how apps and tests reach the simulated store.

An app imports nothing that is empty in release

In 0.1.0, an app wrote #if DEBUG import PurchaseTestKit #endif round its composition root, its previews and its debug panel. Now every name an app uses exists in every build, and in a release build it does nothing:

import PurchaseLaunch

let launch = StoreLaunch.make(catalogue: Shop.catalogue)   // live, or simulated when a debug build is launched with -PurchaseScenario
WindowGroup { ContentView().purchaseStore(launch.store) }
Product Who imports it In a release build
PurchaseCore, PurchaseStoreKit, PurchaseUI the app Everything
PurchaseLaunch (new) the app StoreLaunch.make: the App Store, always. StoreLaunch.preview(catalogue:scenario:) for previews
PurchaseDebugUI the app PurchaseDebugPanel(launch), which draws nothing. PurchaseDebugPanel.isAvailable is false
PurchaseTestKit test targets only. An app that links it does not build The clock, the waits, the .storekit check. The simulated store in debug builds only
PurchaseSimulator (new) nobody, usually Nothing at all: behind #if DEBUG from first line to last
PurchaseDirectDistribution a build sold outside the App Store EverythingOwnedStoreFront

The only #if DEBUG an app may still need is round a Window scene, if the debug panel gets a window of its own.

An app that links the test kit does not build

The test kit now calls Swift Testing, which only a test target can link. So an app that links PurchaseTestKit stops in the linker, in Debug and in Release, even if it uses nothing from it. make demo holds this true with an app built to fail. swift package release-check --app still reads a built app for anything that gets past the linker.

The call that does this is new API in its own right. StoreKitConfiguration.expectNoProblems(against:) records each disagreement between the .storekit file and the catalogue as its own failure, worded as a sentence, at the calling line.

What the first integration asked for

  • A read that finds nothing new is not published, so an app that reads on every activation no longer redraws every gate. A trial running out still is.
  • ProductAccess.isGranted: a Bool? for "owned or on trial". It stays nil until the store has answered.
  • PurchaseStore.clock is public, so an app and the store never disagree about the time.
  • loadProductsIfNeeded(), used by .purchaseStore(_:).
  • PurchaseStore.diagnose().
  • SimulatedStoreFront(catalogue:owned:clock:behaviour:) and Behaviour(purchase:restore:catalogue:), the one-line set-ups every app's tests were writing for themselves.

Breaking

  • PurchaseTestSupport is gone. A test imports only PurchaseTestKit, which re-exports the simulator.
  • PurchaseTestKitError is now ScenarioError.
  • The simulated store moved to PurchaseSimulator. An app that wrote its own composition root imports that module, under its own #if. Most apps switch to StoreLaunch.make and need no #if at all.

Why each of these was chosen, and what was rejected, is in decisions D32–D34 in docs/10-decisions.md. Start with getting started.