StoreKit for in-app purchases

Sofia Martinez Jan 2026
1 tab
import StoreKit

class StoreKitManager: NSObject, ObservableObject {
    static let shared = StoreKitManager()

    @Published var products: [SKProduct] = []
    @Published var isPurchasing = false

    private var productIDs: Set<String> = [
        "com.myapp.premium.monthly",
        "com.myapp.premium.yearly",
        "com.myapp.coin.pack.small"
    ]

    private var completionHandlers: [String: (Result<SKPaymentTransaction, Error>) -> Void] = [:]

    override private init() {
        super.init()
        SKPaymentQueue.default().add(self)
    }

    deinit {
        SKPaymentQueue.default().remove(self)
    }

    // MARK: - Fetch Products
    func fetchProducts() {
        let request = SKProductsRequest(productIdentifiers: productIDs)
        request.delegate = self
        request.start()
    }

    // MARK: - Purchase Product
    func purchase(_ product: SKProduct, completion: @escaping (Result<SKPaymentTransaction, Error>) -> Void) {
        guard SKPaymentQueue.canMakePayments() else {
            completion(.failure(StoreError.cannotMakePayments))
            return
        }

        completionHandlers[product.productIdentifier] = completion
        isPurchasing = true

        let payment = SKPayment(product: product)
        SKPaymentQueue.default().add(payment)
    }

    // MARK: - Restore Purchases
    func restorePurchases(completion: @escaping (Result<Void, Error>) -> Void) {
        SKPaymentQueue.default().restoreCompletedTransactions()
    }

    // MARK: - Check Subscription Status
    func hasActiveSubscription() -> Bool {
        // Check receipt for active subscription
        guard let receiptURL = Bundle.main.appStoreReceiptURL,
              let receiptData = try? Data(contentsOf: receiptURL) else {
            return false
        }

        // In production, validate receipt with your server
        // This is simplified for example
        return receiptData.count > 0
    }
}

// MARK: - SKProductsRequestDelegate
extension StoreKitManager: SKProductsRequestDelegate {
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        DispatchQueue.main.async {
            self.products = response.products

            // Log invalid product IDs
            for invalidID in response.invalidProductIdentifiers {
                print("Invalid product ID: \(invalidID)")
            }
        }
    }

    func request(_ request: SKRequest, didFailWithError error: Error) {
        print("Product request failed: \(error)")
    }
}

// MARK: - SKPaymentTransactionObserver
extension StoreKitManager: SKPaymentTransactionObserver {
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch transaction.transactionState {
            case .purchased:
                handlePurchased(transaction)
            case .failed:
                handleFailed(transaction)
            case .restored:
                handleRestored(transaction)
            case .deferred, .purchasing:
                break
            @unknown default:
                break
            }
        }
    }

    private func handlePurchased(_ transaction: SKPaymentTransaction) {
        // Deliver content
        deliverPurchase(for: transaction.payment.productIdentifier)

        // Complete transaction
        SKPaymentQueue.default().finishTransaction(transaction)

        // Call completion handler
        if let handler = completionHandlers[transaction.payment.productIdentifier] {
            handler(.success(transaction))
            completionHandlers.removeValue(forKey: transaction.payment.productIdentifier)
        }

        isPurchasing = false
    }

    private func handleFailed(_ transaction: SKPaymentTransaction) {
        if let error = transaction.error as? SKError {
            if error.code != .paymentCancelled {
                print("Purchase failed: \(error.localizedDescription)")
            }

            if let handler = completionHandlers[transaction.payment.productIdentifier] {
                handler(.failure(error))
                completionHandlers.removeValue(forKey: transaction.payment.productIdentifier)
            }
        }

        SKPaymentQueue.default().finishTransaction(transaction)
        isPurchasing = false
    }

    private func handleRestored(_ transaction: SKPaymentTransaction) {
        deliverPurchase(for: transaction.payment.productIdentifier)
        SKPaymentQueue.default().finishTransaction(transaction)
    }

    private func deliverPurchase(for productID: String) {
        // Unlock content based on product ID
        UserDefaults.standard.set(true, forKey: productID)
        print("Content delivered for: \(productID)")
    }
}

enum StoreError: Error {
    case cannotMakePayments
    case noProductsFound
}
1 file · swift Explain with highlit

StoreKit enables selling digital goods and subscriptions within iOS apps. I request product info from App Store Connect with product identifiers, then display prices in the user's currency. Purchase flows use SKPaymentQueue to add transactions, which get validated on Apple's servers. Transaction observers handle purchase success, failure, and restoration. For subscriptions, I check receipt validation to verify active status. StoreKit 2 simplifies with async/await APIs and automatic receipt validation. App Store Server API handles server-side validation. I test purchases with sandbox accounts before production. Proper error handling addresses network issues, cancelled purchases, and pending transactions.