StoreKit 2 is the Swift-native purchase API: fetch Product values, run purchase(), verify
the signed transaction, and read Transaction.currentEntitlements for what the user owns.
Project Setup
Add the in-app purchase capability in Signing and Capabilities. Then create a StoreKit configuration file (File > New > File), local-only or synced with App Store Connect, and select it in the scheme's run options. That lets Xcode test purchases with no network.
Fetching and Buying
Product.products(for:) fetches by identifier. Each Product carries displayName and
displayPrice, ready to render. purchase() returns success, pending, or userCancelled.
import StoreKit
@MainActor final class Store: ObservableObject {
@Published private(set) var products: [Product] = []
@Published private(set) var activeTransactions: Set<StoreKit.Transaction> = []
func fetchProducts() async {
products = (try? await Product.products(for: ["123456789", "987654321"])) ?? []
}
func purchase(_ product: Product) async throws {
let result = try await product.purchase()
switch result {
case .success(let verificationResult):
if let transaction = try? verificationResult.payloadValue {
activeTransactions.insert(transaction)
await transaction.finish()
}
case .userCancelled, .pending:
break
@unknown default:
break
}
}
}
A success wraps the transaction in VerificationResult; payloadValue unwraps it or throws
when the App Store signature does not check out. Unlock the purchased feature first, then call
finish(). Finishing before unlocking can lose a paid purchase.
Pending Transactions
With ask to buy, the transaction arrives later, after a parent approves. Observe
Transaction.updates from app launch so no transaction is missed.
init() {
updates = Task {
for await update in StoreKit.Transaction.updates {
if let transaction = try? update.payloadValue {
await fetchActiveTransactions()
await transaction.finish()
}
}
}
}
deinit { updates?.cancel() }
Entitlements Replace Restore
Transaction.currentEntitlements streams every active subscription and non-refunded purchase,
including ones made on another device. Refreshing it whenever the scene becomes active removes
the need for a restore-purchases flow.
func fetchActiveTransactions() async {
var active: Set<StoreKit.Transaction> = []
for await entitlement in StoreKit.Transaction.currentEntitlements {
if let transaction = try? entitlement.payloadValue {
active.insert(transaction)
}
}
activeTransactions = active
}
// In the App: .task(id: scenePhase) { if scenePhase == .active { await store.fetchActiveTransactions() } }