StoreKit 2, introduced with iOS 15, significantly simplifies in-app purchase implementation. No more complex callback code: welcome to async/await and modern APIs. Here's how to implement it correctly.
StoreKit 2 Basics
StoreKit 2 uses Swift Concurrency (async/await) for all its operations. The code is more readable and easier to debug than the old delegate-based API.
Products are retrieved via Product.products(for:) with their identifiers configured in App Store Connect. Transaction verification is automatic with JWS (JSON Web Signature).
Transaction.currentEntitlements provides an asynchronous stream of all the user's active transactions, making entitlement verification easier.
import StoreKit
class StoreManager: ObservableObject {
@Published var products: [Product] = []
@Published var purchasedProductIDs: Set<String> = []
func loadProducts() async throws {
let productIDs = ["pro_monthly", "pro_yearly", "lifetime"]
products = try await Product.products(for: productIDs)
}
func purchase(_ product: Product) async throws -> Transaction? {
let result = try await product.purchase()
switch result {
case .success(let verification):
let transaction = try checkVerified(verification)
await transaction.finish()
return transaction
case .userCancelled, .pending:
return nil
@unknown default:
return nil
}
}
}Subscription Management
For subscriptions, Product.SubscriptionInfo contains all metadata: period, localized price, trial offer, renewal status.
Check subscription status with Transaction.currentEntitlements and filter by productType == .autoRenewable. This includes active subscriptions, grace period, or expired.
Handle subscription changes (upgrade, downgrade, cancellation) by observing Transaction.updates, an AsyncSequence that emits new transactions.
Restore Purchases
StoreKit 2 simplifies restoration. AppStore.sync() forces synchronization with the Apple server and updates local entitlements.
For most cases, Transaction.currentEntitlements is sufficient. Explicit restoration is only necessary if the user signs in with a new Apple ID.
Always display a 'Restore Purchases' button in your settings. It's required by App Store guidelines.
Server-Side Validation
Although StoreKit 2 automatically verifies transactions with JWS, server validation remains recommended for sensitive content.
App Store Server API v2 allows verifying transactions, getting purchase history, and receiving server-to-server notifications for subscription events.
Store the originTransactionId for each purchase. It's the stable identifier that persists across renewals and restorations.
// Checking subscription status
func checkSubscriptionStatus() async -> Bool {
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else {
continue
}
if transaction.productType == .autoRenewable {
// Check if not expired and not revoked
if transaction.revocationDate == nil,
transaction.expirationDate ?? Date() > Date() {
return true
}
}
}
return false
}Pitfalls to Avoid
Test in Sandbox AND with TestFlight. Behavior sometimes differs between environments. Sandbox transactions expire at accelerated rate (1 month = 5 minutes).
Handle error cases gracefully. Purchases can fail for many reasons: no connection, parental restrictions, payment issues.
Don't forget to call transaction.finish() after granting content. Without this, the transaction remains pending and will be re-delivered on next launch.
Conclusion
StoreKit 2 makes iOS monetization much more accessible. With async/await and automatic verification, you can implement robust in-app purchases in a few hours. Take time to test thoroughly in Sandbox and handle all edge cases for a friction-free user experience.