Во время интеграционного тестирования IAP перед релизом мы столкнулись с типичной, но очень запутанной проблемой:
- Пользователь нажимает «Продолжить покупку»
- Кнопка переходит в состояние «Обработка»
- Через несколько секунд возвращается в «Продолжить покупку»
- Окно подтверждения покупки App Store не появляется
В этой статье — полный путь диагностики, ключевые логи, корневая причина и практическая стратегия исправления перед выпуском.
1. Симптом
Список товаров на экране покупки загружался нормально, но после нажатия кнопки покупки:
- Вызывался
purchase() - Системный диалог не появлялся
- UI в итоге возвращался в состояние «не куплено»
На первый взгляд казалось, что поток покупки вообще не стартует, но логи показали другую картину.
2. Первая проверка: действительно ли выполняется поток покупки
Мы добавили логи во все критические точки потока:
- Параметры нажатия кнопки
- Вход в
purchaseSelectedProduct()и веткиguard - До/после
product.purchase() Transaction.updatesrefreshEntitlements()Transaction.currentEntitlementsTransaction.latest(for:)
Результат:
purchase()возвращал.success- Верификация транзакции проходила,
transaction.finish()также выполнялся - Но
entitledIDsоставался пустым
Это означает: проблема была не в том, что «покупка не стартовала», а в том, что «после покупки права не распознавались».
3. Ключевые находки при углублённом анализе
После расширения логирования появились решающие сигналы:
- Количество итераций
Transaction.currentEntitlementsбыло0 Transaction.latest(for: monthly)содержал валидную запись, но уже истёкшую- Важнее всего: до покупки в
Transaction.unfinishedбыли старые транзакции - Текущий
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-чеклист перед релизом
- Перед каждым тестом проверяйте статус подписки sandbox-аккаунта (истекла или нет)
- Логируйте количество
unfinishedна критических путях - Тестируйте три сценария: первая покупка / продление после истечения / восстановление покупок
- Проверяйте согласованность
currentEntitlementsиlatest - Если наблюдаете «нет попапа», сначала проверьте попадание в старый транзакционный путь
7. Вывод
«Нажали купить, но попап не появился» не обязательно означает проблему UI или тайминга вызова.
В StoreKit 2 состояние транзакций, очередь unfinished и жизненный цикл подписки напрямую влияют на финальное поведение.
Добавление этих логов и шагов очистки до релиза значительно снижает количество IAP-проблем, которые сложно воспроизвести в продакшене.