Skip to content
All essays
iOSMarch 27, 202514 min

In-App Purchases with StoreKit: Complete Guide

Monetize your iOS apps with StoreKit. Consumables, subscriptions, and best practices.

Ü
Ümit Uz
Mobile & Full Stack Developer

StoreKit enables in-app purchases in iOS apps. Learn to implement consumables, non-consumables, and subscriptions.

StoreKit Setup

Configure Products in App Store Connect

  1. 1Go to App Store Connect
  2. 2Select your app
  3. 3Navigate to "Features" > "In-App Purchases"
  4. 4Create products:

- Consumable: Items that can be purchased multiple times (coins, gems) - Non-Consumable: One-time purchase (remove ads, premium features) - Auto-Renewable Subscription: Recurring payments (premium access) - Non-Renewing Subscription: Time-limited access

StoreKit 2 Implementation

Product Model

swift
import StoreKit

enum ProductType {
    case consumable
    case nonConsumable
    case subscription
}

struct StoreProduct: Identifiable {
    let id: String
    let type: ProductType
    let product: Product
}

class StoreManager: ObservableObject {
    @Published private(set) var products: [StoreProduct] = []
    @Published private(set) var purchasedProductIDs: Set<String> = []

    private let productIds: [String] = [
        "com.yourapp.premium",
        "com.yourapp.consumable100",
        "com.yourapp.subscription.monthly"
    ]

    init() {
        Task {
            await loadProducts()
            await updatePurchasedProducts()
        }
    }

    func loadProducts() async {
        do {
            let storeProducts = try await Product.products(for: productIds)

            self.products = storeProducts.map { product in
                let type: ProductType
                switch product.type {
                case .consumable:
                    type = .consumable
                case .nonConsumable:
                    type = .nonConsumable
                case .autoRenewable:
                    type = .subscription
                default:
                    type = .nonConsumable
                }

                return StoreProduct(id: product.id, type: type, product: product)
            }
        } catch {
            print("Failed to load products: \(error)")
        }
    }
}

Purchasing Products

Purchase Flow

swift
extension StoreManager {
    func purchase(_ product: Product) async throws -> Transaction? {
        let result = try await product.purchase()

        switch result {
        case .success(let verification):
            let transaction = try checkVerified(verification)
            await updatePurchasedProducts()
            await transaction.finish()
            return transaction

        case .userCancelled:
            print("User cancelled purchase")
            return nil

        case .pending:
            print("Purchase pending")
            return nil

        @unknown default:
            return nil
        }
    }

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

enum StoreError: Error {
    case failedVerification
    case productNotFound
}

Subscription Management

Check Subscription Status

swift
extension StoreManager {
    func checkSubscriptionStatus() async -> Bool {
        for product in products where product.type == .subscription {
            if await isPurchased(product.id) {
                return true
            }
        }
        return false
    }

    func isPurchased(_ productId: String) async -> Bool {
        purchasedProductIDs.contains(productId)
    }

    func updatePurchasedProducts() async {
        var purchasedIDs = Set<String>()

        for await result in Transaction.updates {
            do {
                let transaction = try checkVerified(result)
                purchasedIDs.insert(transaction.productID)

                if transaction.revocationDate == nil {
                    // Transaction is valid
                } else {
                    // Transaction was refunded
                    purchasedIDs.remove(transaction.productID)
                }
            } catch {
                print("Transaction verification failed")
            }
        }

        self.purchasedProductIDs = purchasedIDs
    }
}

Restore Purchases

swift
extension StoreManager {
    func restorePurchases() async {
        try? await AppStore.sync()
        await updatePurchasedProducts()
    }
}

UI Implementation

Store View

swift
struct StoreView: View {
    @StateObject private var storeManager = StoreManager()
    @State private var isLoading = false

    var body: some View {
        NavigationView {
            List {
                ForEach(storeManager.products) { product in
                    ProductRow(product: product) {
                        Task {
                            await purchaseProduct(product)
                        }
                    }
                }
            }
            .navigationTitle("Store")
            .task {
                await storeManager.loadProducts()
            }
        }
    }

    private func purchaseProduct(_ storeProduct: StoreProduct) async {
        isLoading = true
        defer { isLoading = false }

        do {
            if let transaction = try await storeManager.purchase(storeProduct.product) {
                print("Purchase successful: \(transaction.productID)")
            }
        } catch {
            print("Purchase failed: \(error)")
        }
    }
}

Product Row

swift
struct ProductRow: View {
    let product: StoreProduct
    let action: () -> Void

    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(product.product.displayName)
                    .font(.headline)

                Text(product.product.description)
                    .font(.caption)
                    .foregroundColor(.secondary)

                if product.type == .subscription {
                    Text("Subscription")
                        .font(.caption2)
                        .padding(4)
                        .background(Color.blue.opacity(0.2))
                        .cornerRadius(4)
                }
            }

            Spacer()

            Button(action: action) {
                VStack(spacing: 4) {
                    Text(product.product.displayPrice)
                        .font(.headline)

                    if product.type == .subscription {
                        Text(product.product.subscription?.subscriptionPeriod.rawValue ?? "")
                            .font(.caption2)
                    }
                }
                .foregroundColor(.white)
                .padding(.horizontal, 16)
                .padding(.vertical, 8)
                .background(Color.blue)
                .cornerRadius(10)
            }
        }
        .padding(.vertical, 8)
    }
}

Receipt Validation

Server-Side Validation

swift
extension StoreManager {
    func validateReceipt() async throws {
        guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL else {
            throw StoreError.receiptNotFound
        }

        let receiptData = try Data(contentsOf: appStoreReceiptURL)
        let base64Receipt = receiptData.base64EncodedString()

        // Send to your server for validation
        try await validateReceiptWithServer(base64Receipt)
    }

    private func validateReceiptWithServer(_ receipt: String) async throws {
        // Implement server-side validation
    }
}

enum StoreError: Error {
    case receiptNotFound
    case failedVerification
    case productNotFound
}

Best Practices

  1. 1Testing: Test thoroughly with StoreKit testing in Xcode
  2. 2Pricing: Use localized pricing with price formatting
  3. 3Validation: Always validate receipts server-side
  4. 4Restore: Provide restore purchases option
  5. 5UI: Make purchase flow intuitive
  6. 6Error Handling: Handle all error cases gracefully
  7. 7Subscriptions: Provide clear subscription terms
  8. 8Analytics: Track purchase events and conversions

Testing Purchases

StoreKit Configuration File

xml
<!-- Create a StoreKit configuration file in Xcode -->
<StoreKitConfiguration>
    <Products>
        <Product>
            <ProductID>com.yourapp.premium</ProductID>
            <Type>NonConsumable</Type>
            <Price>9.99</Price>
        </Product>
        <Product>
            <ProductID>com.yourapp.consumable100</ProductID>
            <Type>Consumable</Type>
            <Price>0.99</Price>
        </Product>
    </Products>
</StoreKitConfiguration>

StoreKit provides a complete monetization solution for iOS apps!

Related essays

Next essay
Widget Development in Swift: iOS Widgets Guide