Реализация Paywall-экрана в мобильном приложении
Paywall — единственный экран, от которого напрямую зависит revenue приложения. При этом типичная реализация делается в последнюю очередь, за два дня до релиза. Итог: статичный экран с UILabel и UIButton, который грузит продукты 2–3 секунды, не работает в офлайне и не поддаётся A/B тестированию.
Критичная деталь: загрузка продуктов
StoreKit 2 Product.products(for:) и BillingClient.queryProductDetailsAsync() — асинхронные запросы к серверам Apple/Google. В sandbox они иногда занимают 3–5 секунд. В продакшне — обычно быстрее, но не мгновенно. Если загружать продукты только при открытии Paywall — пользователь видит спиннер или пустой экран.
Правильное решение: prefetch продуктов при старте приложения в AppDelegate.didFinishLaunching / Application onCreate, кешировать в памяти через ProductsCache singleton. Paywall открывается с уже готовыми данными. Cache invalidation — при SKPaymentTransactionObserver.paymentQueue(_:updatedTransactions:) или через BillingClient.BillingClientStateListener.onBillingSetupFinished.
StoreKit 2 на iOS 15+:
// Prefetch при запуске
Task {
ProductsCache.shared.products = try? await Product.products(for: productIDs)
}
// Paywall открывается с cached данными
let products = ProductsCache.shared.products ?? []
Структура Paywall-экрана
Минимально необходимые элементы:
- Value proposition — конкретно что получает пользователь (не «премиум доступ», а список фич с иконками).
- Варианты планов (месячный / годовой / lifetime) с выделенным recommended планом.
- CTA кнопка с суммой и периодом.
- Restore Purchases ссылка (обязательна по App Store Guidelines 3.1.1).
- Terms of Use / Privacy Policy ссылки (обязательны для subscription apps).
- Trial badge («7 дней бесплатно») если есть introductory offer.
Trial offer. StoreKit 2 introductoryOffer — проверяем через product.subscription?.isEligibleForIntroOffer (async, требует авторизованного пользователя). Если eligible — показываем trial CTA. Если нет (already redeemed) — показываем стандартную цену без trial-messaging, иначе пользователь ожидает trial и злится при первом списании.
Анимация и дизайн, влияющие на конверсию
Переключение между планами (месячный ↔ годовой) с анимацией пересчёта цены — withAnimation(.spring()) в SwiftUI / animateContentChange в Compose. При выборе годового плана показываем «Экономия 40%» с зачёркнутой ценой за 12 месяцев. Это A/B тестируется — иногда «2 месяца бесплатно» конвертирует лучше, чем процент скидки.
Фоновые градиенты, изображения, Lottie-анимации — загружаются до открытия Paywall (Prefetch), чтобы не было лагов при показе. На iOS Paywall часто показывается модально с presentationDetents (half-sheet) — это повышает конверсию по сравнению с full-screen для некоторых категорий.
Обработка покупки
// StoreKit 2
let result = try await product.purchase()
switch result {
case .success(let verification):
switch verification {
case .verified(let transaction):
await transaction.finish()
await EntitlementManager.shared.refresh()
dismiss()
case .unverified:
showError("Не удалось верифицировать покупку")
}
case .userCancelled:
break // тихо, не показываем ошибку
case .pending:
showPendingMessage() // покупка ожидает подтверждения (Ask to Buy)
}
userCancelled — не показываем ошибку. Пользователь сам закрыл — агрессивный retry раздражает и ведёт к 1-звёздочным отзывам.
A/B тестирование через Remote Config
Paywall — главный кандидат для A/B тестов. Firebase Remote Config или RevenueCat Experiments: разные цены, разные trial длительности, разный visual дизайн. Изменения без релиза новой версии. Минимальная реализация: Paywall конфигурируется через JSON с Remote Config (variant_id, trial_days, highlighted_plan), клиент рендерит по конфигу.
Ориентиры по срокам
Paywall с prefetch продуктов, полной обработкой покупки, restore, trial offer и Remote Config A/B конфигурацией — 2–3 рабочих дня при готовом StoreKit/Play Billing setup.







