Реалізація In-App Purchases (расходуемые покупки) для iOS
Consumable IAP — монети, кристали, життя, заряди — покупки, які витрачаються і купуються знову. На відміну від non-consumable, Apple їх не відновлює: куплені монети, витрачені рік тому, не вернути через restoreCompletedTransactions. Відповідальність за балансування віртуальної валюти повністю на розробнику.
Головна проблема: подвійне начисленние
Consumable-транзакція повинна бути обработана рівно один раз. Найпоширеніший баг — начислювати валюту в paymentQueue(_:updatedTransactions:) та викликати finishTransaction в тому ж методі. Якщо приложение крашнеться після начислення, але до finishTransaction — Apple повторно доставить транзакцію при наступному запуску, та користувач отримає монети двічі.
Правильний порядок при серверній архітектурі:
- Отримуємо транзакцію в
.purchasedстані - Відправляємо
transactionIdentifier+ receipt на свій сервер - Сервер ідемпотентно начислює валюту (перевіряє
transactionIdentifierу БД — якщо вже є, не начислює повторно) - Після успішної відповіді сервера —
finishTransaction
Без ідемпотентності на сервері подвійне начисленння при краху або нестабільній мережі неизбежне.
StoreKit 2 та consumables
У StoreKit 2 consumable-транзакції не потрапляють в Transaction.currentEntitlements — тому що у них немає «активного» стану. Вони з'являються в Transaction.all (повна історія), але тільки після finish() якщо transactionID відомий.
let result = try await product.purchase()
if case .success(let verification) = result,
case .verified(let transaction) = verification {
// Відправляємо на сервер для начислення
let credited = await creditOnServer(transactionId: transaction.id,
receiptData: receiptData)
if credited {
await transaction.finish()
}
// Якщо сервер недоступний — не завершуємо,
// транзакція прийде знову при наступному запуску
}
Офлайн-сценарій
Для ігор без постійного бекенду — локальне зберігання баланса в Keychain з серверною верифікацією при наступній онлайн-сесії. При цьому транзакцію не завершуємо до підтвердження. Але якщо користувач ніколи не виходить онлайн — потрібен таймаут та локальний fallback, інакше App Review це відклонить (гайдлайн 3.1.1 вимагає, щоб куплений контент був доступний).
Тестування edge cases
В Xcode StoreKit Testing (StoreKitTest framework) можна імітувати збої транзакцій:
let session = try SKTestSession(configurationFileNamed: "Products")
session.simulateAskToBuyInSandbox = false
// Форсуємо помилку для тестування retry-логіки
try session.failTransactionsEnabled = true
Обов'язково покриваємо: покупка при відсутності інтернету, краш між начисленням та finish(), перезавантаження після краху, спроба купити при уже незавершеній транзакції у черзі.
Терміни — 2–3 дні: інтеграція StoreKit 2, серверна частина з ідемпотентним начисленням, тести на Sandbox.







