iOS In-App Purchases Implementation (Non-Consumable)
Non-consumable IAP — category where every error in purchase restoration logic turns into App Store complaint and chargeback. User bought "unlimited mode" or "remove ads", reinstalled app — and didn't get their purchase. Apple Support won't help: purchase restoration is developer's responsibility.
What Usually Goes Wrong
Most common mistake — calling SKPaymentQueue.default().restoreCompletedTransactions() only on "Restore" button click. Correct: check originalTransaction on every launch via SKReceiptRefreshRequest or server validation. Without this, user returning after half a year with new iPhone gets no paid content.
Second case — incorrect SKPaymentTransactionObserver handling. If updatedTransactions doesn't call finishTransaction(_:) for all states (.purchased, .restored, .failed), transaction hangs in queue and re-triggers observer on next app launch. Seen projects where this caused double unlock of paid content after restart.
Correct Implementation Architecture
Non-consumable IAP in 2024 built around StoreKit 2 (iOS 15+) or StoreKit 1 with iOS 13–14 support.
StoreKit 2 radically simplifies code:
// Request products
let products = try await Product.products(for: ["com.app.premium_unlock"])
// Purchase
let result = try await products.first?.purchase()
switch result {
case .success(let verification):
switch verification {
case .verified(let transaction):
// unlock content
await transaction.finish()
case .unverified:
// receipt forged — don't unlock
break
}
case .pending:
// SCA or parental control — wait
break
case .userCancelled:
break
}
Transaction.currentEntitlements — async sequence that on every app launch returns all active purchases. Iterate through it in @main or AppDelegate.applicationDidFinishLaunching and restore state without "Restore" button.
For iOS 13–14 StoreKit 1 remains with SKPaymentTransactionObserver. Need separate ReceiptValidator — either local via openssl (complex, no network requests), or server via Apple /verifyReceipt endpoint (deprecated since 2023, but works). Recommend server: local requires embedding Apple root certificate and correct ASN.1 parsing.
Server Validation
For apps with backend: on purchase client sends appStoreReceiptURL to server, server requests Apple Sandbox/Production and saves original_transaction_id in DB. On restore on new device — request to your API by user apple_id.
Impossible to implement "purchase on one device, access on another" within single Apple ID without this — and users expect this.
Testing
In Xcode Simulator StoreKit works via local .storekit file — test without real products. For device testing need Sandbox Account in App Store Connect. Important to check: purchase → deletion → reinstall → restore. This path breaks most often.
Timeline implementation — 2–3 days: product setup in App Store Connect, StoreKit 2 integration with StoreKit 1 fallback, test coverage on Sandbox, App Review (requires "Restore Purchases" button in interface).







