← All shorts
70iOS 15+2 min read

StoreKit 2: Purchases Without the Ceremony

StoreKit 2 replaces payment queue observers and receipt parsing with async/await and signed transaction values.

Original StoreKit spreads a purchase across a products request delegate, a payment queue observer, and manual receipt parsing to figure out what a user actually owns — three separate callback surfaces for one linear flow.

#The old way

import StoreKit

final class LegacyStore: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        guard let product = response.products.first else { return }
        SKPaymentQueue.default().add(SKPayment(product: product))
    }

    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions where transaction.transactionState == .purchased {
            SKPaymentQueue.default().finishTransaction(transaction)
        }
    }
}

#The new way

import StoreKit

final class Store {
    func purchase(_ product: Product) async throws {
        let result = try await product.purchase()

        switch result {
        case .success(let verification):
            let transaction = try checkVerified(verification)
            await transaction.finish()
        case .userCancelled, .pending:
            break
        @unknown default:
            break
        }
    }

    private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
        switch result {
        case .unverified:
            throw StoreError.failedVerification
        case .verified(let safe):
            return safe
        }
    }
}

enum StoreError: Error {
    case failedVerification
}

#Why it matters

  • Product.purchase() is a single async call — no payment queue observer, no delegate methods split across the app
  • VerificationResult gives cryptographic verification of the transaction locally, no server round trip needed just to check it's legitimate
  • Transaction.currentEntitlements replaces manual receipt parsing for checking what a user currently owns
  • Transaction.updates is an AsyncSequence you can iterate to react to renewals, refunds, and cross-device purchases as they happen

#Gotcha

You still must call transaction.finish() after delivering the purchased content, or StoreKit keeps redelivering that transaction on every launch — it's easy to assume the async call returning means the purchase is fully closed out. Also, handling .unverified isn't optional to skip: a jailbroken device or a replayed receipt can produce an unverified transaction, and treating it as valid anyway defeats the point of StoreKit 2's local verification. Exact Product and Transaction API shapes have grown since iOS 15; confirm current parameter names against Apple's docs before shipping.

storekitiosin-app-purchase

Related shorts