StoreKit 2 exposes the purchase flow to SwiftUI through view modifiers: hooks for start and
completion, tasks that observe entitlements, and a purchase environment value for custom
paywalls.
Purchase Lifecycle Hooks
onInAppPurchaseStart and onInAppPurchaseCompletion fire around any purchase started by a
StoreKit view in the hierarchy. Completion is the place to dismiss the paywall.
SubscriptionStoreView(groupID: "598392E1")
.onInAppPurchaseStart { product in
print(product.displayName)
}
.onInAppPurchaseCompletion { product, result in
dismiss()
}
Observing Subscription Status
subscriptionStatusTask watches one subscription group and reruns its closure on every change.
It delivers an array of statuses, not one value, because family sharing can grant access
alongside the user's own purchase.
.subscriptionStatusTask(for: "598392E1") { taskState in
if let value = taskState.value {
isPro = !value
.filter { $0.state != .revoked && $0.state != .expired }
.isEmpty
} else {
isPro = false
}
}
Entitlements and Products
currentEntitlementTask is the equivalent for consumable and non-consumable purchases. It hands
you an optional VerificationResult, optional because the product may never have been bought.
.currentEntitlementTask(for: "1232") { taskState in
if let verification = taskState.transaction,
let transaction = try? verification.payloadValue {
isPro = transaction.revocationDate == nil
} else {
isPro = false
}
}
storeProductTask(for:) loads one product and reruns when it changes, useful for showing a
price outside a StoreKit view.
Custom Paywalls
For a hand-built paywall, start purchases through the purchase environment value. It works on
all Apple platforms, so the paywall view stays reusable.
@Environment(\.purchase) private var purchase
Button {
Task {
if let result = try? await purchase(product) {
await store.process(result)
dismiss()
}
}
} label: {
VStack {
Text(verbatim: product.displayName).font(.headline)
Text(verbatim: product.displayPrice)
}
}