StoreKit 2 Purchase Validation and Subscriptions in iOS 16
StoreKit 2 modernizes in-app purchase workflows in iOS 16 by replacing legacy App Store receipts with cryptographically signed JSON Web Signatures (JWS) and Swift concurrency. This article details how StoreKit 2 handles transaction validation directly on-device, tracks auto-renewable subscription statuses using dedicated status models, and monitors real-time entitlement updates without relying on manual receipt-parsing logic.
Cryptographic Purchase Validation with JWS
Legacy StoreKit required retrieving an encrypted receipt file from the application bundle and either decoding it locally via OpenSSL or sending it to Apple's verifyReceipt server. StoreKit 2 eliminates this pattern entirely. Every transaction returned by StoreKit 2 is represented as a signed JWS token.
When your app queries products, transactions, or completes a
purchase, the API wraps the result in an enum called
VerificationResult<T>.
let result = try await product.purchase()
switch result {
case .success(let verification):
switch verification {
case .verified(let transaction):
// Signature verified by Apple's public key; deliver content
await transaction.finish()
case .unverified(let transaction, let verificationError):
// Signature validation failed; do not unlock content
break
}
case .userCancelled, .pending:
break
}StoreKit 2 automatically validates the JWS token against Appleās root
certificate authority on the device. If the signature is intact and the
payload is authentic, it returns
.verified(let transaction). If the payload has been
tampered with or fails certificate validation, it returns
.unverified. Once content is unlocked, calling
await transaction.finish() notifies the App Store that the
entitlement has been granted.
Determining Active Entitlements
To grant users access to non-consumable items and active
subscriptions, iOS 16 relies on
Transaction.currentEntitlements. This asynchronous sequence
queries the latest transaction for every product the user is currently
entitled to, automatically filtering out expired subscriptions and
revoked transactions.
func updateEntitlements() async {
for await verificationResult in Transaction.currentEntitlements {
guard case .verified(let transaction) = verificationResult else {
continue
}
// Grant access based on transaction.productID
}
}If a user revokes access, a subscription lapses, or an item is
refunded, the item is removed from
Transaction.currentEntitlements.
Querying Subscription Status
For auto-renewable subscriptions, StoreKit 2 introduces
Product.SubscriptionInfo.status(for:). This method returns
a list of subscription statuses within a given subscription group,
accounting for Family Sharing and account changes.
Each subscription status exposes two critical components:
- Transaction Information: Provides the latest verified transaction and expiration details.
- Renewal Information: An additional JWS-signed
payload (
RenewalInfo) containing the current state of the subscription.
A subscription's state is evaluated via the
Product.SubscriptionInfo.Status.State enum:
.subscribed: The subscription is active and in good standing..expired: The subscription has lapsed without renewing..inBillingRetryPeriod: Payment failed, and the App Store is actively attempting to collect payment..inGracePeriod: Payment failed, but the user maintains access under an active Billing Grace Period..revoked: Apple Customer Support refunded or revoked the subscription.
if let subscription = product.subscription {
let statuses = try await subscription.status
for status in statuses {
guard case .verified(let renewalInfo) = status.renewalInfo,
case .verified(let transaction) = status.transaction else {
continue
}
switch status.state {
case .subscribed, .inGracePeriod:
// Maintain user access
let willAutoRenew = renewalInfo.willAutoRenew
case .inBillingRetryPeriod:
// Notify user to update payment method
case .expired, .revoked:
// Revoke access
default:
break
}
}
}Listening for Real-Time Lifecycle Updates
StoreKit 2 provides a continuous update stream via
Transaction.updates. This allows apps to react immediately
to out-of-app events, including external renewals, Ask to Buy approvals,
refunds, and subscription changes made via App Store settings.
Task(priority: .background) {
for await verificationResult in Transaction.updates {
guard case .verified(let transaction) = verificationResult else {
continue
}
// Update app state
await transaction.finish()
}
}By combining automatic JWS verification, clear subscription state models, and reactive transaction streams, StoreKit 2 in iOS 16 removes the historical complexities of client-side StoreKit implementation while maintaining high security.