Почему монетизация — это не просто «добавить кнопку купить»

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

App Store предлагает несколько механик монетизации. Разберём каждую честно — с нюансами, которые обычно всплывают уже в продакшне.

Типы покупок: что бывает

Расходуемые (Consumable) — то, что тратится и покупается снова. Монеты в игре, жизни, кредиты на генерацию изображений. Пользователь купил, использовал — покупает снова. Apple берёт 30% (или 15% для малого бизнеса с выручкой до $1M в год).

Нерасходуемые (Non-consumable) — покупается один раз. Убрать рекламу, разблокировать тёмную тему, открыть набор уровней. Важная деталь: Apple требует, чтобы нерасходуемые покупки можно было восстановить на других устройствах. Если этого нет — отклонят при ревью.

Автовозобновляемые подписки (Auto-Renewable Subscriptions) — самая популярная модель для сервисных приложений. Пользователь подписывается, деньги списываются автоматически каждый месяц или год. Отменить можно в любой момент, но до конца периода доступ сохраняется. Apple берёт 30% в первый год, 15% — начиная со второго.

Неавтоматические подписки (Non-Renewing Subscriptions) — подписка на фиксированный срок без автопродления. Редкая механика, обычно используется в специфических случаях: сезонные контент-пакеты, доступ на конкретный период.

StoreKit 2: что изменилось и почему это важно

До StoreKit 2 (появился в iOS 15) работа с покупками была болезненной. Разработчики сами верифицировали receipt на сервере, получали огромный base64-блоб, отправляли его на Apple-эндпоинт, разбирали XML-ответ. Плюс надо было отдельно обрабатывать отмены, возвраты, апгрейды подписки — всё это через разные механизмы.

StoreKit 2 переписан с нуля под Swift Concurrency. Теперь это нативный async/await API, а верификация транзакций встроена прямо в SDK.

Вот как выглядит базовый флоу покупки:

// Загружаем продукты
let products = try await Product.products(for: ["com.yourapp.premium_monthly"])

// Инициируем покупку
let result = try await products.first?.purchase()

switch result {
case .success(let verification):
    switch verification {
    case .verified(let transaction):
        // Транзакция подтверждена — даём доступ
        await transaction.finish()
    case .unverified(_, let error):
        // Что-то пошло не так с подписью
        print(error)
    }
case .pending:
    // Ожидает родительского разрешения (Ask to Buy)
    break
case .userCancelled:
    break
@unknown default:
    break
}

Ключевой момент — VerificationResult. StoreKit 2 сам проверяет подпись транзакции на устройстве. Не нужен сервер для базовой верификации. Для серьёзных приложений серверная верификация всё равно нужна (чтобы синхронизировать состояние между устройствами), но порог входа резко упал.

Слушаем обновления транзакций

Пользователь может восстановить покупку на другом устройстве, отменить подписку, получить возврат — всё это приходит через Transaction.updates. Этот поток надо слушать с момента запуска приложения.

private var updateListenerTask: Task<Void, Error>?

init() {
    updateListenerTask = listenForTransactions()
}

func listenForTransactions() -> Task<Void, Error> {
    return Task.detached {
        for await result in Transaction.updates {
            if case .verified(let transaction) = result {
                await self.handleTransaction(transaction)
                await transaction.finish()
            }
        }
    }
}

Если не слушать этот поток, пользователь может отменить подписку и получить возврат через Apple — а в приложении у него всё ещё будет доступ. Или наоборот: восстановить покупку, а доступ не появится.

Проверка текущего статуса подписки

Это отдельная задача, которую часто делают неправильно. Нельзя просто хранить флаг «подписан» в UserDefaults и считать его правдой. Статус может измениться в любой момент.

func checkSubscriptionStatus() async -> Bool {
    for await result in Transaction.currentEntitlements {
        if case .verified(let transaction) = result {
            if transaction.productType == .autoRenewable &&
               transaction.revocationDate == nil {
                return true
            }
        }
    }
    return false
}

currentEntitlements возвращает все активные транзакции. Проверяем, что нет даты отзыва (значит, не было возврата) — и даём доступ.

Подписки: как выстроить логику

Несколько практических вещей, которые лучше знать заранее.

Introductory offers — вводные предложения. Первый месяц бесплатно, или за полцены, или фиксированная скидка на первые три месяца. Настраивается в App Store Connect. Пользователь может воспользоваться один раз — StoreKit отслеживает это автоматически. Конверсия с бесплатным периодом обычно выше, но и отток после окончания триала — тоже.

Promotional offers — промо-предложения для существующих или бывших подписчиков. Хотите вернуть пользователя, который отписался? Или удержать того, кто собирается уйти? Здесь это и работает. Требует серверной подписи — без бэкенда не обойтись.

Grace period — льготный период при проблеме с оплатой. Если у пользователя не прошёл платёж, Apple несколько дней пытается списать деньги. Всё это время вы можете и должны сохранять доступ. Настраивается в App Store Connect, обрабатывается через isInBillingRetryPeriod в транзакции.

Subscription groups — группы подписок. Если у вас несколько уровней (Basic, Pro, Enterprise), все они должны быть в одной группе. Пользователь может быть подписан только на один уровень внутри группы. Апгрейд и даунгрейд Apple обрабатывает автоматически с пересчётом пропорциональной стоимости.

Restore purchases: обязательная функция

Apple требует наличия кнопки восстановления покупок — особенно для нерасходуемых покупок и неавтоматических подписок. Без неё ждите reject на ревью.

На StoreKit 2 это стало проще:

try await AppStore.sync()

После вызова sync() StoreKit обновляет все транзакции с сервера Apple. Потом снова проверяете currentEntitlements — и даёте доступ.

Один нюанс: sync() может показать системный диалог с запросом пароля Apple ID. Не вызывайте его в фоне или автоматически при запуске — только по нажатию пользователем.

Серверная верификация: когда нужна

Для простого приложения с одной нерасходуемой покупкой серверная часть не нужна. Но если у вас подписки с доступом к контенту или данным — сервер нужен.

Зачем:

  • Синхронизация статуса между устройствами
  • Webhooks от Apple (Server Notifications) — узнаёте об отменах и возвратах в реальном времени
  • Защита от jailbreak-инструментов, которые эмулируют покупки

Apple предоставляет App Store Server API. Основные эндпоинты: getTransactionHistory, getAllSubscriptionStatuses. Авторизация через JWT с приватным ключом из App Store Connect.

Server Notifications v2 присылают события: SUBSCRIBED, DID_RENEW, EXPIRED, REFUND и другие. Настраивается URL в App Store Connect — и Apple будет слать POST-запросы на ваш сервер при каждом изменении статуса.

Ценообразование: несколько честных наблюдений

Apple управляет ценами через ценовые уровни — вы не вбиваете произвольную сумму, а выбираете из предустановленных tier'ов.

Годовые подписки конвертируют лучше, когда скидка заметна. Правило «два месяца в подарок» (десять месяцев по цене года) — стандарт индустрии, и оно работает.

Не ставьте слишком низкую цену на старте. Поднять цену для существующих подписчиков сложно психологически — Apple сохраняет им старую цену до тех пор, пока они не переподпишутся. Лучше начать с нормальной цены и давать introductory offer новым пользователям.

Paywall лучше показывать не при запуске, а в момент, когда пользователь впервые столкнулся с ограничением. Человек уже понял ценность — самое время предложить апгрейд.

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

В Xcode есть StoreKit Testing — можно настроить продукты прямо в конфигурационном файле и тестировать покупки без App Store Connect. Время подписок ускорено: месяц проходит за несколько минут.

Для тестирования в TestFlight используются тестовые аккаунты (Sandbox). Реальных денег не списывается, но поведение идентично продакшну — включая все статусы транзакций.

Обязательно тестируйте сценарий возврата. Сделайте покупку в Sandbox, запросите возврат через App Store Connect — и убедитесь, что приложение корректно забирает доступ. Многие пропускают этот кейс.

Частые ошибки

Не вызывать finish() после обработки транзакции. Незавершённые транзакции будут снова приходить через Transaction.updates при каждом запуске. Это не баг StoreKit — это ожидаемое поведение. Просто не забывайте вызывать finish() после того, как дали пользователю доступ.

Хранить статус подписки только локально. UserDefaults сбрасываются при переустановке. Если пользователь переустановил приложение — он потеряет доступ, хотя подписка активна. Всегда проверяйте currentEntitlements при запуске.

Игнорировать unverified кейс. Некоторые разработчики пишут только .verified и пропускают .unverified. Это нормально — но стоит логировать, чтобы понимать, происходит ли что-то странное в продакшне.

Не обрабатывать Ask to Buy. Если у ребёнка настроен Family Sharing с родительским одобрением покупок — результат будет .pending. Приложение должно корректно показать, что покупка ожидает одобрения, а не просто молчать.

Не подписываться на Transaction.updates при запуске. Если запустить слушатель только в момент открытия экрана покупок — вы пропустите все события, которые произошли, пока приложение было в фоне или закрыто.

Монетизация проектируется с нуля, а не прикручивается потом

Если у вас уже есть мобильное приложение или вы только планируете его запустить, монетизацию нужно закладывать в архитектуру с самого начала. Добавлять IAP после того, как приложение готово, сложнее и дороже: приходится переписывать логику доступа, трогать навигацию, переделывать onboarding.

В REEXY мы помогаем с разработкой iOS-приложений — включая интеграцию StoreKit 2, настройку подписок и серверной части. Если нужна оценка задачи или консультация — напишите через форму на r3xy.ru.

Glавное, что стоит запомнить: StoreKit 2 сильно упростил работу с покупками по сравнению с тем, что было раньше. Встроенная верификация, нативный async/await, удобный Transaction.updates — приятно использовать. Но бизнес-логика — что показывать, когда, как удерживать пользователей — по-прежнему на вас.