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:
- Setup Listeners - Register callbacks before any purchase
- Fetch Products - Get available products from the store
- Request Purchase - Initiate the purchase UI
- Handle Listener Callback - Receive success or error from listener
- Verify Purchase - Validate with your backend or IAPKit
- 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.
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#
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.
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.
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#
| Type | isConsumable | Behavior |
|---|---|---|
| Consumable | true | Product can be purchased again (coins, gems, etc.) |
| Non-Consumable | false | One-time purchase, cannot be bought again (premium unlock) |
| Subscription | false | Recurring purchase, managed by the store |
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:
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#
| Issue | Cause | Solution |
|---|---|---|
| Purchase replays on launch | Transaction not finished | Call finishTransaction() after verification |
| Android purchase refunded | Not acknowledged within 3 days | Finish transaction immediately after verification |
| Cannot repurchase consumable | Not consumed | Pass isConsumable: true to finishTransaction() |
| Listener not called | Listener set up after purchase | Always set up listeners before any purchase request |
Handling Pending Purchases#
Check for pending (unfinished) purchases on app launch to complete interrupted transactions:
@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#
- Apple · In-App Purchase overview
- Apple · StoreKit 2 In-App Purchase
- Apple · StoreKit Transaction
- Google · Integrate the Google Play Billing Library
- Google · BillingClient reference