Implementing In‑App Purchases with StoreKit2 in iOS SwiftUI Apps
This comprehensive guide walks through using Apple’s StoreKit2 framework to define products, fetch localized product data, display items, handle purchases, verify transactions, manage receipts, support refunds, differentiate consumables from non‑consumables, and implement auto‑renewable subscriptions in a SwiftUI iOS application.
Overview
This article explains how to use Apple’s StoreKit2 framework to implement in‑app purchases (IAP) in a SwiftUI iOS app, covering product definition, StoreKit configuration files, fetching localized product information, displaying products, purchasing, receipt handling, consumables, non‑consumables, refunds, and subscriptions.
Getting Started
Create a multi‑platform Xcode project, add the StoreHelper package, enable the In‑App Purchase capability, and include StoreKit2 in the project.
Define products in Products.storekit and list their identifiers in Products.plist. Example plist snippet is shown.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Products</key>
<array>
<string>com.rarcher.nonconsumable.flowers-large</string>
<string>com.rarcher.nonconsumable.flowers-small</string>
<string>com.rarcher.nonconsumable.roses-large</string>
<string>com.rarcher.nonconsumable.chocolates-small</string>
<string>com.rarcher.consumable.plant-installation</string>
<string>com.rarcher.subscription.vip.gold</string>
<string>com.rarcher.subscription.vip.silver</string>
<string>com.rarcher.subscription.vip.bronze</string>
</array>
</dict>
</plist>Fetching Products from the App Store
Call StoreHelper.requestProductsFromAppStore(productIds:) which uses Product.products(for:) to retrieve localized Product objects. The result is stored in a @Published products array.
Displaying Products
Products are shown in SwiftUI views such as PriceView with a purchase button that triggers PriceViewModel.purchase(product:).
struct PriceView: View {
@EnvironmentObject var storeHelper: StoreHelper
@State private var canMakePayments: Bool = false
@Binding var purchaseState: PurchaseState
var productId: ProductId
var price: String
var product: Product
var body: some View {
let priceViewModel = PriceViewModel(storeHelper: storeHelper, purchaseState: $purchaseState)
HStack {
Button(action: {
withAnimation { purchaseState = .inProgress }
Task.init { await priceViewModel.purchase(product: product) }
}) {
PriceButtonText(price: price, disabled: !canMakePayments)
}
.disabled(!canMakePayments)
}
.onAppear { canMakePayments = AppStore.canMakePayments }
}
}Purchasing and Transaction Verification
The purchase flow calls StoreHelper.purchase(_:), which invokes product.purchase(). StoreKit2 automatically validates the transaction using JSON Web Signature (JWS). The result is mapped to a PurchaseState enum (e.g., .purchased, .pending, .failed).
@MainActor
public func purchase(_ product: Product, options: Set<Product.PurchaseOption> = []) async throws -> (transaction: Transaction?, purchaseState: PurchaseState) {
guard hasStarted else { return (nil, .notStarted) }
guard AppStore.canMakePayments else { return (nil, .userCannotMakePayments) }
guard purchaseState != .inProgress else { throw StoreException.purchaseInProgressException }
purchaseState = .inProgress
guard let result = try? await product.purchase(options: options) else {
purchaseState = .failed
throw StoreException.purchaseException
}
// handle result cases (success, userCancelled, pending, etc.)
...
}Receipt Handling and Offline Support
StoreKit2 stores receipts in a SQLite database ( receipts.db). The app can read the receipt file path via Bundle.main.appStoreReceiptURL. For offline scenarios, purchased product IDs are persisted locally (e.g., in UserDefaults or Keychain) to act as a backup when the App Store is unavailable.
Refunds (iOS 15+)
iOS 15 introduces an in‑app refund request sheet. Use .refundRequestSheet(for:isPresented:) to present the UI and handle the result.
Consumables vs. Non‑Consumables
Consumable purchases are not recorded in the receipt; the app must track usage (e.g., via Keychain). Non‑consumable purchases can be verified with product.latestTransaction and Transaction.currentEntitlement(for:). Helper structs such as ConsumableProductId and KeychainHelper manage consumable counts.
Subscriptions
Auto‑renewable subscriptions follow the naming convention com.{developer}.subscription.{group}.{product}. The app determines the highest‑level active subscription in a group by examining product.subscription.status. Subscription info is wrapped in a SubscriptionInfo struct containing product, group, latest transaction, renewal info, and status.
public struct SubscriptionInfo: Hashable {
public var product: Product?
public var subscriptionGroup: String?
public var latestVerifiedTransaction: Transaction?
public var verifiedSubscriptionRenewalInfo: Product.SubscriptionInfo.RenewalInfo?
public var subscriptionStatus: Product.SubscriptionInfo.Status?
}Users can upgrade subscriptions, view details, and manage them via .manageSubscriptionsSheet(isPresented:) (iOS 15+).
Key Swift Code Samples
@available(iOS 15.0, macOS 12.0, *)
public class StoreHelper: ObservableObject {
// Core logic for product fetching, purchasing, receipt handling, etc.
@Published private(set) var products: [Product]?
@Published private(set) var purchasedProducts: [ProductId] = []
// ... other properties and methods ...
} struct PropertyFile {
static func read(filename: String) -> [String: AnyObject]? {
if let path = Bundle.main.path(forResource: filename, ofType: "plist"),
let contents = NSDictionary(contentsOfFile: path) as? [String: AnyObject] {
return contents
}
return nil
}
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
