iOS Development

StoreKit 2 In-App Purchase: The Modern Way

StoreKit 2 was Apple's complete rewrite of the IAP API, shipping with iOS 15. It's async/await-native, dramatically simpler than the original StoreKit, and the only reasonable choice for new apps in 2026. This is the working pattern we use in the Aether AI apps.

Why StoreKit 2

  • Async/await — no more delegate callbacks, transaction queues, semaphores
  • Built-in receipt validation — JWS-signed transactions you can verify locally OR on a server
  • Better testing — local StoreKit configuration file, no need to deploy to TestFlight to test
  • Cleaner API — half the lines of code for the same functionality

Setup in App Store Connect

  1. Sign Paid Apps agreement
  2. Configure banking and tax info
  3. Create the IAP in App Store Connect — Product ID like com.djenterprises.rdr2.unlimited_ai
  4. For testing: create a Sandbox tester account

Fetching products

import StoreKit

let productIDs: Set<String> = ["com.djenterprises.rdr2.unlimited_ai"]
let products = try await Product.products(for: productIDs)
let unlock = products.first { $0.id == productIDs.first }

Making a purchase

do {
    let result = try await unlock.purchase()
    switch result {
    case .success(let verification):
        let transaction = try checkVerified(verification)
        // Grant entitlement
        await transaction.finish()
    case .userCancelled:
        break
    case .pending:
        // Awaiting parental approval, etc.
        break
    @unknown default:
        break
    }
} catch {
    // Handle error
}

The checkVerified helper extracts the transaction and checks the JWS signature. Apple has a recommended pattern in their sample code.

Transaction listener

On app launch, listen for transactions that arrived while the app was closed (refunds, family sharing, etc.):

Task {
    for await result in Transaction.updates {
        guard case .verified(let txn) = result else { continue }
        // Update entitlement state
        await txn.finish()
    }
}

Restore purchases

// In your restore button handler:
try await AppStore.sync()

Then re-check Transaction.currentEntitlements to see what the user owns.

Testing with StoreKit configuration

Xcode's StoreKit Configuration file lets you simulate IAPs locally — no App Store Connect round-trip required. File → New → StoreKit Configuration, define products, then in your scheme: Run → Options → StoreKit Configuration. Purchases work in the simulator.

Test these scenarios before submission:

  • Successful purchase
  • Failed payment (decline simulation)
  • User cancellation
  • Restore on a fresh install
  • Refund (simulated via Transaction.refund test API)
  • Family sharing (if enabled)

Server-side receipt validation

For most consumer apps, on-device verification via checkVerified is sufficient. For premium / subscription apps where revenue matters more, validate on your backend using Apple's App Store Server API.

The pattern: when the user makes a purchase, send the JWS to your backend, your backend verifies with Apple's API and grants the entitlement in your database. This prevents jailbreak/MITM bypass at the cost of complexity.


For the full iOS app development story including how this fits with Claude API integration, see Shipping the RDR2 Companion From Scratch.

Sources & References
  1. Apple — StoreKit documentation
  2. Apple — WWDC21: Meet StoreKit 2
  3. Apple — App Store Server API
  4. RevenueCat — Subscription IAP best practices