Реализация In-App Purchases (одноразовые покупки) для iOS
Non-consumable IAP — категория, где каждая ошибка в логике восстановления покупок превращается в жалобу в App Store и чарджбек. Пользователь купил «безлимитный режим» или «убрать рекламу», переустановил приложение — и не получил своё. Поддержка Apple не поможет: восстановление non-consumable это ответственность разработчика.
Что чаще всего идёт не так
Самая распространённая ошибка — вызывать SKPaymentQueue.default().restoreCompletedTransactions() только по нажатию кнопки «Восстановить». Правильно: при каждом запуске проверять originalTransaction через SKReceiptRefreshRequest или серверную валидацию. Без этого пользователь, вернувшийся через полгода с новым iPhone, окажется без оплаченного контента.
Второй кейс — неправильная обработка SKPaymentTransactionObserver. Если updatedTransactions не вызывает finishTransaction(_:) для всех состояний (.purchased, .restored, .failed), транзакция зависает в очереди и при следующем запуске приложения повторно триггерит observer. Видел проекты, где это приводило к двойному открытию paid-контента после рестарта.
Как устроена правильная реализация
Архитектура non-consumable IAP в 2024 году строится вокруг StoreKit 2 (iOS 15+) или StoreKit 1 с поддержкой iOS 13–14.
StoreKit 2 радикально упрощает код:
// Запрос продуктов
let products = try await Product.products(for: ["com.app.premium_unlock"])
// Покупка
let result = try await products.first?.purchase()
switch result {
case .success(let verification):
switch verification {
case .verified(let transaction):
// unlock контент
await transaction.finish()
case .unverified:
// receipt подделан — не разблокируем
break
}
case .pending:
// SCA или родительский контроль — ждём
break
case .userCancelled:
break
}
Transaction.currentEntitlements — async sequence, который при каждом запуске приложения возвращает все активные покупки. Итерируем его в @main или в AppDelegate.applicationDidFinishLaunching и восстанавливаем состояние без кнопки «Восстановить».
Для iOS 13–14 остаётся StoreKit 1 с SKPaymentTransactionObserver. Там нужен отдельный ReceiptValidator — либо локальная верификация через openssl (сложно, но без сетевых запросов), либо серверная через Apple /verifyReceipt endpoint (deprecated с 2023, но работает). Рекомендую серверную: локальная требует embedding Apple root certificate и корректной ASN.1-парсинга.
Серверная валидация
Для приложений с бэкендом: при покупке клиент отправляет appStoreReceiptURL на сервер, сервер запрашивает Apple Sandbox/Production и сохраняет original_transaction_id в базе. При восстановлении на новом устройстве — запрос к своему API по apple_id пользователя.
Без этого невозможно реализовать «покупка на одном устройстве, доступ на другом» в рамках одного Apple ID — а пользователи этого ожидают.
Тестирование
В Xcode Simulator StoreKit работает через локальный .storekit файл — можно тестировать без реальных продуктов. Для device-тестирования нужен Sandbox Account в App Store Connect. Важно проверять сценарий: покупка → удаление → переустановка → восстановление. Этот путь ломается чаще всего.
Сроки реализации — 2–3 дня: настройка продуктов в App Store Connect, интеграция StoreKit 2 с fallback на StoreKit 1, покрытие тестами на Sandbox, прохождение ревью (App Review требует кнопку «Restore Purchases» в интерфейсе).







