Purchase

Handle in-app purchases with proper transaction management. This guide covers the complete purchase flow from setup to completion.

Purchase Flow Overview#

A complete purchase flow follows these steps:

  1. Setup Listeners - Register callbacks before any purchase
  2. Fetch Products - Get available products from the store
  3. Request Purchase - Initiate the purchase UI
  4. Handle Listener Callback - Receive success or error from listener
  5. Verify Purchase - Validate with your backend or IAPKit
  6. Finish Transaction - Complete the transaction

Setup Purchase Listeners#

Register purchase listeners before initializing the store connection or making purchase requests. These listeners handle successful purchases and errors.

swift
import OpenIap
import SwiftUI

@MainActor
class PurchaseManager: ObservableObject {
    private let iapStore = OpenIapStore()

    init() {
        setupListeners()
    }

    private func setupListeners() {
        // 1. Setup purchase success callback
        iapStore.onPurchaseSuccess = { [weak self] purchase in
            print("Purchase received: \(purchase.productId)")
            Task {
                await self?.handlePurchase(purchase)
            }
        }

        // 2. Setup error callback
        iapStore.onPurchaseError = { [weak self] error in
            print("Purchase error: \(error.localizedDescription)")
            self?.handlePurchaseError(error)
        }

        // 3. Initialize connection
        Task {
            do {
                try await iapStore.initConnection()
                guard iapStore.isConnected else {
                    print("Store connection failed")
                    return
                }
                print("Store connection established")
            } catch {
                print("Failed to connect: \(error.localizedDescription)")
            }
        }
    }
}

Request Purchase#

After setting up listeners, you can request purchases. The purchase request triggers the native store UI (App Store / Google Play).

Consumable / Non-Consumable Products#

swift
import OpenIap

@MainActor
func purchaseProduct(productId: String) async {
    let iapStore = OpenIapStore()

    do {
        // Request purchase - result delivered to onPurchaseSuccess
        _ = try await iapStore.requestPurchase(
            sku: productId,
            type: .inApp,  // .inApp for consumables/non-consumables
            autoFinish: false  // We'll finish manually after verification
        )
    } catch {
        print("Purchase request failed: \(error.localizedDescription)")
    }
}

// Example usage
await purchaseProduct(productId: "com.app.coins_100")

Verify Purchase with Your Backend#

Always verify purchases with a trusted verifier. Client-side store state alone can be bypassed. Use your networking layer to send purchase data to your own backend, or use the IAPKit section below when you want OpenIAP's managed validation backend to do that work. The generated verifyPurchase API accepts platform verification options, not a Purchase object plus a server URL.

swift
import OpenIap

func verifyOnServer(_ purchase: PurchaseIOS) async -> Bool {
    do {
        // yourBackend is your authenticated networking client.
        return try await yourBackend.verifyApplePurchase(
            productId: purchase.productId,
            jws: purchase.purchaseToken ?? ""
        )
    } catch {
        print("Verification error: \(error.localizedDescription)")
        return false
    }
}

Verify Purchase with IAPKit#

Don't want to implement store receipt verification yourself? IAPKit is a hosted purchase verification service that validates App Store, Google Play, Amazon Appstore, and Meta Horizon purchases for you. Use verifyPurchaseWithProvider with the 'iapkit' provider and pass the platform-specific token or receipt payload. Fire OS and Vega OS use iapkit.amazon with the Amazon receipt id, and no app-owned Amazon RVS server is required. If your own backend serves protected paid resources, have that backend authenticate the user and query IAPKit before serving them; direct app-to-IAPKit calls are fine for in-app or local feature unlocks, but they cannot authorize backend resources by themselves. In either case, require the returned store-verified productId to be present and match the product your app expected; isValid alone is not enough.

For Amazon, include expectedProductId in the verification payload. Amazon App Tester receipts require enabling Allow Amazon App Tester / RVS Cloud Sandbox in the IAPKit project before passing sandbox: true. Handled Amazon results report exactly 'Sandbox' or 'Production' in environment; require the value expected by the build.

swift
import OpenIap

func verifyWithIapkit(_ purchase: PurchaseIOS) async -> Bool {
    do {
        let result = try await OpenIapModule.shared.verifyPurchaseWithProvider(
            VerifyPurchaseWithProviderProps(
                provider: .iapkit,
                iapkit: RequestVerifyPurchaseWithIapkitProps(
                    apiKey: Bundle.main.object(forInfoDictionaryKey: "IAPKitAPIKey") as? String,
                    apple: RequestVerifyPurchaseWithIapkitAppleProps(jws: purchase.purchaseToken ?? "")
                )
            )
        )

        if let verified = result.iapkit,
           verified.isValid,
           let verifiedProductId = verified.productId,
           verifiedProductId == purchase.productId {
            print("IAPKit verified: \(verified.state.rawValue)")
            return true
        }

        print("IAPKit verification failed")
        return false
    } catch {
        print("IAPKit verification error: \(error.localizedDescription)")
        return false
    }
}

Finish Transaction#

Always finish transactions after verification. This step is critical - unfinished transactions cause issues on both platforms.

Transaction Types#

TypeisConsumableBehavior
ConsumabletrueProduct can be purchased again (coins, gems, etc.)
Non-ConsumablefalseOne-time purchase, cannot be bought again (premium unlock)
SubscriptionfalseRecurring purchase, managed by the store
swift
import OpenIap

// Complete purchase flow
@MainActor
func handlePurchase(_ purchase: PurchaseIOS) async {
    let iapStore = OpenIapStore()

    // 1. Verify on server
    let isValid = await verifyIOSPurchase(purchase)
    guard isValid else {
        print("Invalid purchase")
        return
    }

    // 2. Grant the product to user
    await grantProductToUser(productId: purchase.productId)

    // 3. Finish the transaction (CRITICAL: Android auto-refunds after 3 days!)
    // - isConsumable: true = consume (can buy again)
    // - isConsumable: false = acknowledge only (one-time purchase)
    let isConsumable = purchase.productId.contains("consumable")
    do {
        try await iapStore.finishTransaction(purchase: purchase, isConsumable: isConsumable)
        print("Transaction finished successfully")
    } catch {
        print("Failed to finish transaction: \(error.localizedDescription)")
    }
}

Complete Example#

Here's a complete implementation combining all steps:

swift
import OpenIap
import SwiftUI

@MainActor
class PurchaseManager: ObservableObject {
    static let shared = PurchaseManager()

    @Published var products: [ProductIOS] = []
    @Published var isProcessing = false

    private let iapStore = OpenIapStore()

    init() {
        setupListeners()
        Task {
            do {
                try await iapStore.initConnection()
                guard iapStore.isConnected else {
                    print("Store connection failed")
                    return
                }
                try await iapStore.fetchProducts(
                    skus: ["com.app.premium", "com.app.coins_100"],
                    type: .inApp
                )
                products = iapStore.iosProducts
            } catch {
                print("Failed to fetch products: \(error.localizedDescription)")
            }
        }
    }

    private func setupListeners() {
        iapStore.onPurchaseSuccess = { [weak self] purchase in
            guard let iosPurchase = purchase.asIOS() else { return }
            Task { @MainActor in
                await self?.handlePurchase(iosPurchase)
            }
        }

        iapStore.onPurchaseError = { [weak self] error in
            Task { @MainActor in
                self?.isProcessing = false
                print("Purchase error: \(error.localizedDescription)")
            }
        }
    }

    func purchase(_ productId: String) async {
        isProcessing = true
        do {
            _ = try await iapStore.requestPurchase(
                sku: productId,
                type: .inApp,
                autoFinish: false
            )
        } catch {
            isProcessing = false
            print("Purchase request failed: \(error.localizedDescription)")
        }
    }

    private func handlePurchase(_ purchase: PurchaseIOS) async {
        defer { isProcessing = false }

        // Step 1: Verify
        let isValid = await verifyIOSPurchase(purchase)
        guard isValid else {
            print("Verification failed")
            return
        }

        // Step 2: Grant product
        await grantProductToUser(productId: purchase.productId)

        // Step 3: Finish
        do {
            let isConsumable = purchase.productId.contains("coins")
            try await iapStore.finishTransaction(
                purchase: purchase,
                isConsumable: isConsumable
            )
            print("Purchase completed!")
        } catch {
            print("Failed to finish: \(error.localizedDescription)")
        }
    }
}

Troubleshooting#

Common Issues#

IssueCauseSolution
Purchase replays on launchTransaction not finishedCall finishTransaction() after verification
Android purchase refundedNot acknowledged within 3 daysFinish transaction immediately after verification
Cannot repurchase consumableNot consumedPass isConsumable: true to finishTransaction()
Listener not calledListener set up after purchaseAlways set up listeners before any purchase request

Handling Pending Purchases#

Check for pending (unfinished) purchases on app launch to complete interrupted transactions:

swift
@MainActor
func checkPendingPurchases() async {
    let iapStore = OpenIapStore()

    do {
        try await iapStore.getAvailablePurchases()

        for purchase in iapStore.availablePurchases {
            // Process each pending purchase
            if let iosPurchase = purchase.asIOS() {
                await handlePurchase(iosPurchase)
            }
        }
    } catch {
        print("Failed to get pending purchases: \(error.localizedDescription)")
    }
}

Native References#