Skip to content

4.0.0: subscription status, purchase options and the rest of StoreKit 2 - #12

Merged
Ramiz69 merged 5 commits into
mainfrom
release/4.0.0
Oct 19, 2026
Merged

Ramiz69 merged 5 commits into
mainfrom
release/4.0.0

Conversation

@Ramiz69

@Ramiz69 Ramiz69 commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

What

PurchaseKit surfaced a fraction of StoreKit 2. This release covers what was missing:

  • Subscription status: subscriptionStatuses(inGroup:) / subscriptionStatus(inGroup:) and a live
    subscriptionStatusUpdates stream. They report state, renewal date, auto-renew product, grace period,
    billing retry and expiration reason. isEligibleForIntroOffer(inGroup:) is new too.
  • PurchaseOptions: quantity, promotional offers signed as JWS, win-back offers, the intro-eligibility JWS
    and Ask to Buy simulation, alongside appAccountToken. There is one purchase path,
    purchase(productID:options:). The short forms are protocol conveniences.
  • Richer values:
    • StoreProduct gains currency, subscription period and offers.
    • StoreTransaction gains product type, ownership (family sharing), environment, reason, offer,
      revocation reason and type, billing plan, storefront and price.
    • New: AppPurchaseInfo (original app version), the storefront country, and the manage-subscriptions,
      refund-request and offer-code sheets on iOS, Catalyst and visionOS.
  • Fixes:
    • Subscription bundles and suites (OS 27) count as active subscriptions.
    • Missing products are fetched in one request instead of one per entitlement.
    • An uncached requestProducts no longer re-announces owned products as new purchases.
    • unavailableProductIDs exposes identifiers StoreKit did not return.

Why it is a major release

  • ProductType and PurchasesError are now RawRepresentable structs instead of enums. A switch over
    them needs a default branch, and ProductType.unknown is gone: a kind with no constant in this build
    arrives as itself, with StoreKit's raw value. In exchange, kinds and error codes StoreKit adds later
    (as OS 27 added subscription bundles and suites) are no longer breaking changes.
  • PurchasesError follows the URLError / CocoaError layout: a code, plus productID, offerID and
    underlyingError. Match it with catch PurchasesError.purchaseCancelled or error.code == .purchasePending.
    StoreKit failures a caller can act on now have their own codes. Before, they escaped untranslated.
  • PurchasesProtocol / ServerVerifiedPurchasesProtocol requirements changed, so mocks must be updated.
  • StoreProduct's public initializer requires currencyCode, and StoreTransaction's requires productType.

MigratingTo4 in the DocC catalog shows each break with before-and-after code.

New open-ended kinds (offer type, ownership, environment, revocation, billing plan, subscription state) use the
same struct pattern. The two types that would clash with StoreKit's top-level typealiases are prefixed:
StoreSubscriptionStatus and StoreSubscriptionPeriod.

Distribution

  • The product no longer forces type: .static, so Xcode picks static or dynamic linking per client.
  • .spi.yml tells Swift Package Index to host the DocC documentation, and the README badges come from the index.

Verified locally (Xcode 27.1 / Swift 6.4)

  • swift test: 97 tests in 11 suites pass.
  • xcodebuild build succeeds for iOS, Mac Catalyst, watchOS, tvOS and visionOS with zero warnings.
    build-for-testing succeeds for visionOS.
  • Builds with all six upcoming Swift 7 features and -warnings-as-errors.
  • DocC from a fresh derived-data path: zero warnings.

CI runs Xcode 26.6, so this run is the check that the kit still builds with the OS 26 SDK. OS 27 product types
are recognised by raw value, and the 26.4 / 26.5 fields sit behind availability checks.

Calls into live StoreKit remain untested, as before. StoreKitTest was tried: it does not run in a SwiftPM test target (StoreKitInternalError.invalidApp), and it needs a host app plus an iOS simulator, where OS 27's StoreKitTest is too unreliable for CI. It was left out of this release.

…tions

The value types surfaced a fraction of what StoreKit hands over. A paywall could
not say "3 days free, then $4.99 a month" without reaching into the raw Product,
an app could not tell a family-shared entitlement from its own purchase, and a
subscription's renewal state was not representable at all.

New types:
- StoreSubscriptionStatus: state, renewal date, auto-renew product, grace period,
  billing retry and expiration reason, with best(of:) to pick the status that
  grants the most access when Family Sharing yields several.
- SubscriptionOffer and StoreSubscriptionPeriod: introductory, promotional and
  win-back offers with price, payment mode and period.
- PurchaseOptions: quantity, promotional offers signed as JWS, win-back offers,
  the intro-eligibility JWS and Ask to Buy simulation, next to appAccountToken.
- AppPurchaseInfo, StoreEnvironment and RefundRequestStatus.

StoreProduct gains currencyCode, the subscription period and its offers.
StoreTransaction gains product type, ownership, environment, reason, offer,
revocation reason and type, billing plan, storefront and price.

Two names are prefixed with Store - StoreSubscriptionStatus and
StoreSubscriptionPeriod - because StoreKit declares top-level typealiases with
the plain names from OS 18.4, and any file importing both modules failed with
"ambiguous for type lookup" the moment it named one.

Breaking, and therefore 4.0.0:
- ProductType gains subscriptionBundle and subscriptionSuite (OS 27). They used
  to arrive as unknown. isAutoRenewableSubscription covers all three subscription
  kinds.
- PurchasesError gains a case for each StoreKit failure a caller can act on, and
  mapping(_:) translates StoreKitError and Product.PurchaseError into them.
  Before, those errors escaped untranslated.
- StoreProduct's public initializer requires currencyCode.

Every new open-ended kind - offer type, ownership, environment, revocation,
billing plan, subscription state - is a RawRepresentable struct rather than an
enum, the pattern StoreKit itself uses. A value a later StoreKit adds then comes
through as itself, and recognising it later is not another breaking release.
ProductType and PurchasesError stay enums because callers switch over them.

The kit still builds with the OS 26 SDK that CI uses: the OS 27 product types are
recognised by raw value instead of by symbol, and the 26.4 and 26.5 fields are
read behind availability checks.
PurchasesManager could answer whether a subscription was active and nothing else.
It now also reports renewal status - one-off through subscriptionStatuses(inGroup:)
and live through subscriptionStatusUpdates, fed by a second listener that holds
self weakly like the transaction listener does - along with intro-offer
eligibility, how the app itself was obtained (appPurchaseInfo()), and the
storefront country.

Purchasing has one path, purchase(productID:options:), and its visionOS scene
variant. purchase(productID:) and purchase(productID:appAccountToken:) are now
protocol extension conveniences that build the options and land there, so a
stand-in implements one purchase method instead of two and the two entry points
cannot drift apart. A win-back offer ID that is not on the product fails by name
before the sheet appears, rather than letting the purchase go through at full
price.

StoreKit errors from purchase, restore, product lookup, status and the app
transaction are translated into PurchasesError.

On iOS, Mac Catalyst and visionOS the manager wraps the manage-subscriptions,
refund-request and offer-code sheets. They are not protocol requirements, since
they present system UI and decide nothing a stand-in would need to fake.

Behaviour fixes along the way:
- Subscription bundles and suites count as active subscriptions. Comparing
  against autoRenewable alone made a bundle subscriber look unsubscribed.
- Products missing from the cache are fetched with one Product.products(for:)
  call per refresh instead of one request per entitlement.
- hasEntitlement(for:) asks Transaction.currentEntitlements(for:) about the one
  product rather than walking every entitlement the customer has.
- An uncached requestProducts no longer resets isPurchased on products the cache
  already held, which made the refresh announce every owned product as a new
  purchase.
- unavailableProductIDs lists configured identifiers StoreKit did not return,
  which used to show up only as a paywall one row short.

Breaking: PurchasesProtocol requires purchase(productID:options:),
subscriptionStatuses(inGroup:), isEligibleForIntroOffer(inGroup:) and
appPurchaseInfo(), and ServerVerifiedPurchasesProtocol no longer requires
purchase(productID:appAccountToken:). Existing mocks have to be updated.
MigratingTo4 lists every break with before-and-after code: the new ProductType
cases, the PurchasesError mapping table, what a mock now has to implement, the
currencyCode argument, and bundles appearing among active subscriptions. A
Subscriptions article covers renewal status, grace-period and billing-retry UI,
intro eligibility, promotional offers signed on the server, and win-back offers.
Each new public type has a symbol page.

Corrections to what was there:
- The landing page listed iOS 15 / macOS 12 as requirements. The package has
  deployed to OS 26 since 2.0.0.
- GettingStarted showed purchase returning a StoreProduct and a mock built that
  way. It has returned a (product, transaction) tuple since 3.0.0.
- The Requirements and Notes sections came after Topics, so DocC read them as
  task groups and warned about every line.

README asks for 4.0.0.
Both were enums, so every value StoreKit adds is a breaking release: OS 27's
subscription bundles and suites were one reason this release is a major. Like
the other open-ended kinds, they are now RawRepresentable structs. A kind or
error code added later is not a break. The price is a default branch in client
switches.

ProductType's raw values are StoreKit's own. A kind this build has no constant
for arrives as itself instead of being collapsed into a guess. That makes
.unknown unnecessary, and it is gone. StoreTransaction's initializer now
requires productType, since .unknown was only ever there to be a default.

PurchasesError follows the URLError / CocoaError layout: a Code, plus the
product ID, offer ID or underlying StoreKit error that goes with it. Callers
write `catch PurchasesError.purchaseCancelled` (matched through `~=`) or
`error.code == .purchasePending`, and the StoreKit error that a code was
translated from is kept in underlyingError instead of being thrown away.
…Index

The product forced `type: .static`. That came from the package template, not
from any need, and it takes the decision away from the app. An app that links
the kit into several of its own dynamic frameworks gets a copy in each, with
duplicate class warnings at run time. Leaving the type out lets Xcode pick
static or dynamic per client, which is what Swift Package Manager recommends.
Source compatibility is unaffected.

.spi.yml tells Swift Package Index to build and host the DocC documentation
for every release. The README badges now come from the index as well: they
report the Swift versions and platforms the index has actually built, rather
than hand-written values. The old ones still said "swift 6.2".
@Ramiz69
Ramiz69 merged commit b563917 into main Oct 19, 2026
1 check passed
@Ramiz69
Ramiz69 deleted the release/4.0.0 branch October 19, 2026 13:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant