Во время интеграционного тестирования 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-проблем, которые сложно воспроизвести в продакшене.