Repository navigation
4.0.0: subscription status, purchase options and the rest of StoreKit 2 - #12
Merged
Merged
Conversation
…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".
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
PurchaseKit surfaced a fraction of StoreKit 2. This release covers what was missing:
subscriptionStatuses(inGroup:)/subscriptionStatus(inGroup:)and a livesubscriptionStatusUpdatesstream. 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 JWSand Ask to Buy simulation, alongside
appAccountToken. There is one purchase path,purchase(productID:options:). The short forms are protocol conveniences.StoreProductgains currency, subscription period and offers.StoreTransactiongains product type, ownership (family sharing), environment, reason, offer,revocation reason and type, billing plan, storefront and price.
AppPurchaseInfo(original app version), the storefront country, and the manage-subscriptions,refund-request and offer-code sheets on iOS, Catalyst and visionOS.
requestProductsno longer re-announces owned products as new purchases.unavailableProductIDsexposes identifiers StoreKit did not return.Why it is a major release
ProductTypeandPurchasesErrorare nowRawRepresentablestructs instead of enums. Aswitchoverthem needs a
defaultbranch, andProductType.unknownis gone: a kind with no constant in this buildarrives 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.
PurchasesErrorfollows theURLError/CocoaErrorlayout: acode, plusproductID,offerIDandunderlyingError. Match it withcatch PurchasesError.purchaseCancelledorerror.code == .purchasePending.StoreKit failures a caller can act on now have their own codes. Before, they escaped untranslated.
PurchasesProtocol/ServerVerifiedPurchasesProtocolrequirements changed, so mocks must be updated.StoreProduct's public initializer requirescurrencyCode, andStoreTransaction's requiresproductType.MigratingTo4in 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:
StoreSubscriptionStatusandStoreSubscriptionPeriod.Distribution
type: .static, so Xcode picks static or dynamic linking per client..spi.ymltells 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 buildsucceeds for iOS, Mac Catalyst, watchOS, tvOS and visionOS with zero warnings.build-for-testingsucceeds for visionOS.-warnings-as-errors.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.