Під час інтеграційного тестування IAP перед релізом ми зіткнулися з типовою, але дуже заплутаною проблемою:

  • Користувач натискає «Продовжити покупку»
  • Кнопка переходить у стан «Обробка»
  • Через кілька секунд повертається до «Продовжити покупку»
  • Вікно підтвердження покупки App Store не з’являється

У цій статті — повний шлях діагностики, ключові логи, коренева причина та практична стратегія виправлення перед випуском.

1. Симптом

Список товарів на екрані покупки завантажувався нормально, але після натискання кнопки покупки:

  • Викликався purchase()
  • Системний діалог не з’являвся
  • UI зрештою повертався в стан «не куплено»

На перший погляд здавалося, що потік покупки взагалі не стартує, але логи показали іншу картину.

2. Перша перевірка: чи справді виконується потік покупки

Ми додали логи в усі критичні точки потоку:

  • Параметри натискання кнопки
  • Вхід у purchaseSelectedProduct() і гілки guard
  • До/після product.purchase()
  • Transaction.updates
  • refreshEntitlements()
  • Transaction.currentEntitlements
  • Transaction.latest(for:)

Результат:

  • purchase() повертав .success
  • Верифікація транзакції проходила, transaction.finish() також виконувався
  • Але entitledIDs залишався порожнім

Це означає: проблема була не в тому, що «покупка не стартувала», а в тому, що «після покупки права не розпізнавалися».

3. Ключові знахідки після поглибленого аналізу

Після розширення логування з’явилися вирішальні сигнали:

  1. Кількість ітерацій Transaction.currentEntitlements була 0
  2. Transaction.latest(for: monthly) містив валідний запис, але вже прострочений
  3. Найважливіше: до покупки в Transaction.unfinished були старі транзакції
  4. Поточний purchase() повертав транзакцію зі старого ланцюжка, а не з нового інтерактивного потоку покупки

Це пояснює, чому «попап не з’являється»:

  • StoreKit спочатку повертає стару оброблювану транзакцію (unfinished / історичний ланцюжок)
  • Код завершує її через finish()
  • Запит виглядає «успішним», але не проходить через новий шлях підтвердження, який очікує користувач

4. Підсумок кореневої причини

Це не одна точка відмови, а поєднання двох факторів:

  • Незавершені транзакції (unfinished transaction) втручаються в потік покупки
  • Sandbox-ланцюжок підписки перебуває у простроченому стані (навіть за наявності latest transaction це не обов’язково активний entitlement)

Фінальна поведінка: попапа немає, видимий успіх, entitlement порожні.

5. Стратегія виправлення (перевірена на практиці)

5.1 Проактивно очищати unfinished transactions перед покупкою

Під час запуску застосунку і перед покупкою скануйте Transaction.unfinished та виконуйте finish() для verified-транзакцій, щоб старі транзакції не перехоплювали новий потік.

5.2 Зберегти й посилити діагностичні логи (рекомендовано до релізу)

До випуску варто тримати увімкненими:

  • Snapshot unfinished transaction
  • subscription status (state, renewal info)
  • Кількість ітерацій currentEntitlements
  • Деталі transaction/expiration із latest(for:)

5.3 Використовувати «двоканальну» перевірку прав (опційно)

Якщо в деяких середовищах currentEntitlements тимчасово порожній, можна використовувати latest(for:) як fallback (з обов’язковою перевіркою активного статусу).

6. IAP-чекліст перед релізом

  1. Перед кожним тестом перевіряйте стан підписки sandbox-акаунта (прострочена чи ні)
  2. Логуйте кількість unfinished на критичних шляхах
  3. Тестуйте три сценарії: перша покупка / продовження після завершення терміну / відновлення покупок
  4. Перевіряйте узгодженість між currentEntitlements і latest
  5. Якщо спостерігаєте «немає попапа», спершу перевірте потрапляння в старий транзакційний шлях

7. Висновок

«Натиснув купити, але попап не з’явився» — це не обов’язково проблема UI або таймінгу виклику.
У StoreKit 2 стан транзакцій, черга unfinished і життєвий цикл підписки напряму впливають на фінальну поведінку.

Додавання цих логів і кроків очищення до релізу суттєво зменшує кількість IAP-проблем, які важко відтворити в продакшені.