Реалізація 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 відкривається з кешованими даними
let products = ProductsCache.shared.products ?? []
Структура Paywall-екрана
Мінімально необхідні елементи:
- Value proposition — конкретно що отримує користувач (не «премиум доступ», а список фіч з іконками).
- Варіанти планів (місячний / річний / lifetime) з виділеним рекомендованим.
- 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.







