Implementing Paywall Screen in Mobile Application
Paywall—only screen directly impacting app revenue. Yet typical implementation is rushed—two days before release. Result: static screen with UILabel and UIButton, product loading takes 2–3 seconds, doesn't work offline, no A/B testing.
Critical Detail: Product Loading
StoreKit 2 Product.products(for:) and BillingClient.queryProductDetailsAsync() are async calls to Apple/Google servers. In sandbox, sometimes 3–5 seconds. In production—usually faster, but not instant. Loading products only on Paywall open—user sees spinner or blank screen.
Correct solution: prefetch products on app start in AppDelegate.didFinishLaunching / Application onCreate, cache in memory via ProductsCache singleton. Paywall opens with ready data. Cache invalidation—on SKPaymentTransactionObserver.paymentQueue(_:updatedTransactions:) or BillingClient.BillingClientStateListener.onBillingSetupFinished.
StoreKit 2 on iOS 15+:
// Prefetch on launch
Task {
ProductsCache.shared.products = try? await Product.products(for: productIDs)
}
// Paywall opens with cached data
let products = ProductsCache.shared.products ?? []
Paywall Screen Structure
Minimal required elements:
- Value proposition—concrete what user gets (not "premium access", list features with icons).
- Plan variants (monthly / yearly / lifetime) with highlighted recommended.
- CTA button with amount and period.
- Restore Purchases link (mandatory by App Store Guidelines 3.1.1).
- Terms of Use / Privacy Policy links (mandatory for subscription apps).
- Trial badge ("7 days free") if available.
Trial offer. StoreKit 2 introductoryOffer—check via product.subscription?.isEligibleForIntroOffer (async, needs authorized user). If eligible—show trial CTA. If not (already redeemed)—show standard price without trial messaging, else user expects trial and gets angry on first charge.
Animation and Design Affecting Conversion
Plan switching (monthly ↔ yearly) with price recalc animation—withAnimation(.spring()) in SwiftUI / animateContentChange in Compose. On yearly selection, show "Save 40%" with struck-through 12-month price. This A/B tested—sometimes "2 months free" converts better than discount percentage.
Background gradients, images, Lottie animations—load before Paywall open (Prefetch) to avoid lag. iOS Paywall often presented modally with presentationDetents (half-sheet)—higher conversion than full-screen for some categories.
Purchase Handling
// 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("Could not verify purchase")
}
case .userCancelled:
break // silent, no error
case .pending:
showPendingMessage() // waiting confirmation (Ask to Buy)
}
userCancelled—don't show error. User closed themselves—aggressive retry annoying and leads to 1-star reviews.
A/B Testing via Remote Config
Paywall—prime A/B test candidate. Firebase Remote Config or RevenueCat Experiments: different prices, trial lengths, visual designs. Changes without new release. Minimal: Paywall configured via JSON from Remote Config (variant_id, trial_days, highlighted_plan), client renders by config.
Timeline Estimates
Paywall with product prefetch, full purchase handling, restore, trial offer, Remote Config A/B: 2–3 working days with ready StoreKit/Play Billing setup.







