iOS In-App Purchases Implementation (Consumable)
Consumable IAP — coins, crystals, lives, charges — purchases that spend and buy again. Unlike non-consumable, Apple does not restore them: coins purchased and spent a year ago cannot be returned via restoreCompletedTransactions. Virtual currency balance is entirely developer's responsibility.
Main Problem: Double Crediting
Consumable transaction must be processed exactly once. Most common bug — credit currency in paymentQueue(_:updatedTransactions:) and call finishTransaction in same method. If app crashes after crediting but before finishTransaction — Apple re-delivers transaction on next launch, and user gets coins twice.
Correct order with server architecture:
- Get transaction in
.purchasedstate - Send
transactionIdentifier+ receipt to your server - Server idempotently credits currency (checks
transactionIdentifierin DB — if exists, doesn't credit again) - After successful server response —
finishTransaction
Without idempotency on server double crediting on crash or unstable network is inevitable.
StoreKit 2 and Consumables
In StoreKit 2 consumable transactions don't appear in Transaction.currentEntitlements — because they have no "active" state. They appear in Transaction.all (full history), but only after finish() if transactionID known.
let result = try await product.purchase()
if case .success(let verification) = result,
case .verified(let transaction) = verification {
// Send to server for crediting
let credited = await creditOnServer(transactionId: transaction.id,
receiptData: receiptData)
if credited {
await transaction.finish()
}
// If server unavailable — don't finish,
// transaction comes again on next launch
}
Offline Scenario
For games without permanent backend — local balance storage in Keychain with server verification on next online session. In this case don't finish transaction until confirmation. But if user never goes online — need timeout and local fallback, otherwise App Review rejects (guideline 3.1.1 requires purchased content be accessible).
Testing Edge Cases
In Xcode StoreKit Testing (StoreKitTest framework) can simulate transaction failures:
let session = try SKTestSession(configurationFileNamed: "Products")
session.simulateAskToBuyInSandbox = false
// Force error for testing retry logic
try session.failTransactionsEnabled = true
Must cover: purchase with no internet, crash between crediting and finish(), restart after crash, attempt to buy with already unfinished transaction in queue.
Timeline — 2–3 days: StoreKit 2 integration, server part with idempotent crediting, Sandbox tests.







