Mobile Development 11 min read

Migrating to StoreKit 2: Practical Lessons and Code Guide for iOS In‑App Purchases

This article explains why and how to upgrade a production iOS app from StoreKit 1 to StoreKit 2, outlines the new purchase flow, compares the two APIs, shares real‑world issues encountered during migration, and provides Swift code snippets and work‑arounds for each challenge.

Liulishuo Tech Team
Liulishuo Tech Team
Liulishuo Tech Team
Migrating to StoreKit 2: Practical Lessons and Code Guide for iOS In‑App Purchases

Background

Apple introduced StoreKit 2 at WWDC 2021 (iOS 15). After more than a year of API changes and bug fixes, StoreKit 2 became stable in iOS 16, prompting the team at Liulishuo to adopt it for their app.

Why StoreKit 1 Was Problematic

Key pain points with StoreKit 1 included:

Loss of applicationUsername across devices, making it hard to correlate Apple orders with Liulishuo Pay orders.

Complex SDK design requiring developers to understand products, transactions, payments, receipts, refreshes, and often a dedicated server.

Refunds could only be processed through Apple, with no testable notification flow.

No way to proactively fetch transaction history or link receipts to internal order IDs.

Manual receipt parsing using the legacy C‑based OpenSSL library.

Difficulties sharing transaction data across multiple Apple devices.

Given the growing share of revenue from In‑App Purchases and increasingly complex mobile commerce scenarios, Apple finally released StoreKit 2 in 2021.

How to Support StoreKit 2

Liulishuo mainly uses Non‑renewing subscriptions . The complete StoreKit 1 purchase flow is:

iOS fetches product details via StoreKit.

iOS sends the product ID to Liulishuo Pay service to create an order and obtain an order number.

iOS initiates the payment with StoreKit, setting applicationUsername to the order number.

User completes payment; iOS uploads the receipt to the backend for verification, including the context information.

Backend validates the receipt, delivers the content, and callbacks the iOS client.

iOS receives the verification result and marks the transaction complete.

StoreKit 2 simplifies many of these steps. Example code for fetching product details:

# StoreKit 1
let productsRequest = SKProductsRequest(productIdentifiers: identifiers)
productsRequest.delegate = self
productsRequest.start()
self.productsRequest = productsRequest

extension StoreKitService: SKProductsRequestDelegate {
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        // handle product info
    }
}

# StoreKit 2
let storeProducts = try await Product.request(with: identifiers)

When creating an order with StoreKit 2, the iOS client receives an additional appAccountToken (a UUID) that is persisted in the transaction and can be used for later refund or re‑order handling.

# StoreKit 1
func purchase(_ product: SKProduct) {
    let payment = SKPayment(product: product)
    SKPaymentQueue.default().add(payment)
}

extension StoreKitService: SKPaymentTransactionObserver {
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        // handle states
    }
}

# StoreKit 2
func purchase(_ product: Product) async throws -> Transaction? {
    let result = try await product.purchase(options: [.appAccountToken(yourAppToken)])
    switch result {
    case .success(let verification):
        // handle success
        return verification
    case .userCancelled, .pending:
        // handle other cases
        break
    default:
        break
    }
    return nil
}

Receipt verification also changes: StoreKit 1 required uploading the raw receipt data to the server. StoreKit 2 stores transaction information in a JWS token and performs verification on‑device, so the client only needs to send the transactionId to the backend.

Issues Encountered During Migration

Problem 1: Transaction.updates is unavailable on iOS 15.4 and earlier.

Solution: Use the legacy SKPaymentTransactionObserver on those versions.

Problem 2: StoreKit 2 does not provide a direct callback for purchases initiated from the App Store.

Solution: Continue using StoreKit 1 APIs for those events.

Problem 3: No API to retrieve the localized currency symbol.

if let price = product.price.formatted(), let displayPrice = product.displayPrice {
    let currencySymbol = displayPrice.replacingOccurrences(of: price, with: "")
    // StoreKit 2 does not expose currencySymbol directly
}

Problem 4: Compatibility between StoreKit 1 and StoreKit 2.

Solution: Detect the device iOS version (≥ 15.0) at runtime and initialize the appropriate IAP service. Tests showed StoreKit 1 can be upgraded to StoreKit 2 without issues.

Problem 5: Supporting user refunds.

Solution: Use Transaction.beginRefundRequest in StoreKit 2; it works in the sandbox, and the backend receives Apple’s refund notification.

Problem 6: Order synchronization.

Solution: StoreKit 2 automatically syncs transactions across devices. For manual sync, call await AppStore.sync(), but it is usually unnecessary. Still provide a “Restore Purchases” UI to satisfy App Store review.

Problem 7: Retrieving Apple order numbers for verification.

Solution: StoreKit 2 exposes the Apple order ID, but it cannot be tested in the sandbox environment.

Outlook

StoreKit 2 resolves most of the shortcomings of StoreKit 1, introducing a cleaner API, built‑in receipt verification, and better transaction continuity across devices. However, developers must still handle edge cases such as pre‑iOS 15.4 devices, missing currency symbols, and sandbox limitations. For a complete migration, consult Apple’s official documentation: https://developer.apple.com/documentation/storekit/.

mobile developmentiOSSwiftIn-App Purchasepayment integrationStoreKit2
Liulishuo Tech Team
Written by

Liulishuo Tech Team

Help everyone become a global citizen!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.