Расходуемые покупки iOS: StoreKit 2, идемпотентность, защита

Реализация расходуемых покупок в iOS: StoreKit 2 и защита от двойного начисления Мы не раз сталкивались с ситуацией: после интеграции consumable покупок пользователи получали монеты дважды. Причина — классическая ошибка: начисление валюты сразу в `paymentQueue(_:updatedTransactions:)` и вызов `fi

Разработка и поддержка любых видов мобильных приложений:

Информационные и развлекательные мобильные приложения
Новостные приложения, игры, справочники, онлайн-каталоги, погодные, фитнес и здоровье, туристические, образовательные, социальные сети и мессенджеры, квиз, блоги и подкасты, форумы, агрегаторы
Мобильные приложения электронной коммерции
Интернет-магазины, B2B-приложения, маркетплейсы, онлайн-обменники, кэшбэк-сервисы, биржи, дропшиппинг-платформы, программы лояльности, доставка еды и товаров, платежные системы
Мобильные приложения для управления бизнес-процессами
CRM-системы, ERP-системы, управление проектами, инструменты для команды продаж, учет финансов, управление производством, логистика и доставка, управление персоналом, системы мониторинга данных
Мобильные приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, платформы предоставления электронных услуг, платформы кешбека, видеохостинги, тематические порталы, платформы онлайн-бронирования и записи, платформы онлайн-торговли

Это лишь некоторые из типы мобильных приложений, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента.

Услуги, которые мы предлагаем
Показано 1 из 1Все 1734 услуг
Расходуемые покупки iOS: StoreKit 2, идемпотентность, защита
Средний
~2-3 дня

Наши компетенции:

Часто задаваемые вопросы

Последние работы

  • image_mobile-applications_feedme_467_0.webp
    Разработка мобильного приложения для компании FEEDME
    895
  • image_mobile-applications_xoomer_471_0.webp
    Разработка мобильного приложения для компании XOOMER
    782
  • image_mobile-applications_rhl_428_0.webp
    Разработка мобильного приложения для компании RHL
    1216
  • image_mobile-applications_zippy_411_0.webp
    Разработка мобильного приложения для компании ZIPPY
    1079
  • image_mobile-applications_affhome_429_0.webp
    Разработка мобильного приложения для компании Affhome
    1002
  • image_mobile-applications_flavors_409_0.webp
    Разработка мобильного приложения для компании FLAVORS
    597

Реализация расходуемых покупок в iOS: StoreKit 2 и защита от двойного начисления

Мы не раз сталкивались с ситуацией: после интеграции consumable покупок пользователи получали монеты дважды. Причина — классическая ошибка: начисление валюты сразу в paymentQueue(_:updatedTransactions:) и вызов finishTransaction в том же методе. Если приложение крашится между начислением и finish, Apple повторно доставляет транзакцию, и баланс уходит в минус. В одном из проектов с аудиторией в 500K пользователей это приводило к потерям виртуальной валюты в 3% случаев — пока мы не внедрили серверную идемпотентность. Рассказываем, как избежать этой проблемы и реализовать надёжную обработку consumable IAP с использованием StoreKit 2.

Главная проблема: двойное начисление

Consumable-транзакция должна быть обработана ровно один раз. Самый распространённый баг — начислять валюту в paymentQueue(_:updatedTransactions:) и вызывать finishTransaction в том же методе. Если приложение крашнет после начисления, но до finishTransaction, Apple повторно доставит транзакцию при следующем запуске — и пользователь получит монеты дважды. В наших проектах мы используем следующий защищённый порядок при серверной архитектуре:

  1. Получаем транзакцию в .purchased состоянии.
  2. Отправляем transactionIdentifier + receipt на свой сервер.
  3. Сервер идемпотентно начисляет валюту (проверяет transactionIdentifier в БД — если уже есть, не начисляет повторно).
  4. После успешного ответа сервера — вызываем finishTransaction.

Без шага с идемпотентностью на сервере двойное начисление при краше или нестабильной сети неизбежно. Мы протестировали это на нагрузке в 10 000 транзакций — с идемпотентностью не было ни одного задвоения.

Как StoreKit 2 обрабатывает consumable-транзакции?

В StoreKit 2 consumable-транзакции не попадают в Transaction.currentEntitlements — потому что у них нет «активного» состояния. Они появляются в Transaction.all (полная история), но после finish() — только если transactionID известен. Это важное отличие от non-consumable покупок. Пример обработки:

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() } // Если сервер недоступен — не финишируем, // транзакция придёт снова при следующем запуске } 

Как избежать двойного начисления?

Ключевой принцип — идемпотентность на стороне бэкенда. Мы используем transactionIdentifier как уникальный ключ: если ID уже есть в базе, сервер возвращает статус "already credited", и клиент просто завершает транзакцию. Это исключает двойное зачисление даже при многократной доставке одной транзакции. Дополнительно мы настраиваем мониторинг: если количество транзакций с одним ID превышает порог (например, 3), отправляется алерт.

Офлайн-сценарий

Для игр без постоянного бэкенда — локальное хранение баланса в Keychain с серверной верификацией при следующей онлайн-сессии. При этом транзакцию не финишируем до подтверждения. Но если пользователь никогда не выходит в онлайн — нужен таймаут и локальный fallback. Иначе App Review это отклонит (гайдлайн 3.1.1 требует, чтобы купленный контент был доступен). Мы рекомендуем устанавливать таймаут в 30 минут: по истечении — временно зачисляем локально и помечаем для повторной верификации.

Почему серверная верификация надёжнее локальной?

Критерий Локальная (Keychain) Серверная
Защита от взлома Уязвим для jailbreak Высокая, всё на бэкенде
Идемпотентность Сложно гарантировать Простая, через ID транзакции
Офлайн-доступ Доступен сразу Требует сети для верификации
Соответствие гайдлайнам Apple Нужен fallback Соответствует по умолчанию

Тестирование edge cases

В Xcode StoreKit Testing (StoreKitTest framework) можно имитировать сбои транзакций:

let session = try SKTestSession(configurationFileNamed: "Products") session.simulateAskToBuyInSandbox = false // Форсируем ошибку для тестирования retry-логики try session.failTransactionsEnabled = true 

Обязательно покрываем: покупка при отсутствии интернета, краш между начислением и finish(), повторный запуск после краша, попытка купить при уже незавершённой транзакции в очереди. В наших проектах тестовое покрытие клиентской части достигает 90%.

Что входит в нашу работу

  • Анализ текущей архитектуры покупок и выявление рисков двойного начисления.
  • Проектирование серверной логики с идемпотентными эндпоинтами.
  • Интеграция StoreKit 2 (Swift 5.9+, async/await) или StoreKit 1 для совместимости.
  • Настройка продуктов в App Store Connect, включая consumable IAP.
  • Реализация серверной верификации receipt (с помощью verifyReceipt или собственной логики).
  • Покрытие тестами: unit-тесты для серверной части, StoreKitTest для клиента.
  • Документация по обработке транзакций и диаграмма последовательности.
  • Поддержка при публикации в App Store (гарантия успешного прохождения Review).

Сроки реализации

Ориентировочный срок — от 2 до 3 рабочих дней на типовую интеграцию consumable покупок. Если требуется нестандартная серверная логика или поддержка нескольких валют, срок может увеличиться до 5–7 дней. Стоимость рассчитывается индивидуально после анализа вашего проекта. У нас за плечами более 5 успешных проектов с consumable IAP, включая приложения с миллионной аудиторией.

Важно: Для корректной работы обязательно соблюдайте App Store Review Guidelines Section 3.1.1.

Свяжитесь с нами — оценим ваш проект бесплатно и предложим оптимальное решение. Закажите интеграцию уже сегодня и получите защиту от потери виртуальной валюты.

Типичные ошибки при реализации consumable IAP
  • Начисление валюты до завершения серверной верификации.
  • Отсутствие идемпотентности на сервере.
  • Игнорирование краш-сценариев между начислением и finish.
  • Неправильная обработка офлайн-режима (таймауты).

Сравнение подходов к обработке consumable

Подход Сложность Надёжность Скорость внедрения
Только клиент (local) Низкая Низкая (взлом/задвоения) 1 день
Клиент + сервер (идемпотентность) Средняя Высокая 2–3 дня
Сервер + receipt validation Высокая Очень высокая 3–5 дней