The AppDNA paywall module lets you present server-driven paywalls configured in the AppDNA Console. Paywalls are rendered natively and include built-in purchase handling through the billing module.
Present a Paywall
Present a paywall by its identifier:
AppDNA.presentPaywall(
id: "premium_paywall",
from: viewController,
context: PaywallContext(placement: "settings"),
delegate: self
)
Module Access
Access the paywall module directly:
let paywall = AppDNA.paywall
Module Methods
| Method | Signature | Description |
|---|
present | present(_: String, from: UIViewController, context: PaywallContext?) | Present a paywall by ID |
setDelegate | setDelegate(_ delegate: AppDNAPaywallDelegate?) | Set a delegate for paywall callbacks |
PaywallContext
Provide context about where and why the paywall is being shown:
let context = PaywallContext(
placement: "settings",
experiment: "paywall_test",
variant: "b"
)
| Property | Type | Description |
|---|
placement | String | Where the paywall is triggered (e.g., “settings”, “onboarding”, “feature_gate”) |
experiment | String? | Experiment identifier, if showing as part of an A/B test |
variant | String? | Variant identifier within the experiment |
The placement value is included in all paywall analytics events, allowing you to measure conversion by placement in the Console.
AppDNAPaywallDelegate
Implement the delegate protocol to receive paywall lifecycle callbacks:
protocol AppDNAPaywallDelegate {
func onPaywallPresented(paywallId: String)
func onPaywallAction(paywallId: String, action: PaywallAction)
func onPaywallPurchaseStarted(paywallId: String, productId: String)
func onPaywallPurchaseCompleted(paywallId: String, productId: String, transaction: TransactionInfo)
func onPaywallPurchaseFailed(paywallId: String, error: Error)
func onPaywallDismissed(paywallId: String)
}
Example Implementation
class PaywallHandler: AppDNAPaywallDelegate {
func onPaywallPresented(paywallId: String) {
print("Paywall shown: \(paywallId)")
}
func onPaywallAction(paywallId: String, action: PaywallAction) {
switch action {
case .ctaTapped:
print("CTA tapped")
case .featureSelected:
print("Feature selected")
case .planChanged:
print("Plan changed")
case .linkTapped:
print("Link tapped")
case .custom:
print("Custom action")
}
}
func onPaywallPurchaseStarted(paywallId: String, productId: String) {
print("Purchase started: \(productId)")
}
func onPaywallPurchaseCompleted(
paywallId: String,
productId: String,
transaction: TransactionInfo
) {
print("Purchased \(productId) — txn: \(transaction.transactionId)")
// Paywall auto-dismisses on successful purchase
}
func onPaywallPurchaseFailed(paywallId: String, error: Error) {
print("Purchase failed: \(error.localizedDescription)")
// Paywall stays visible so the user can retry
}
func onPaywallDismissed(paywallId: String) {
print("Paywall dismissed")
}
}
PaywallAction
The PaywallAction enum represents user interactions within the paywall:
| Case | Description |
|---|
.ctaTapped | The main call-to-action button was tapped |
.featureSelected | A feature item was selected |
.planChanged | The user switched between plan options |
.linkTapped | A link (e.g., terms, privacy policy) was tapped |
.custom | A custom action defined in the paywall config |
DismissReason
The DismissReason enum indicates how the paywall was closed:
| Case | Description |
|---|
.purchased | Dismissed after a successful purchase |
.dismissed | Dismissed by the user via a close button |
.tappedOutside | Dismissed by tapping outside the paywall |
.programmatic | Dismissed programmatically by your code |
Paywall Sections
Paywalls configured in the Console support the following content sections:
| Section | Description |
|---|
| Header | Title, subtitle, and optional hero image |
| Features list | List of feature highlights with icons and descriptions |
| Plan selection | Selectable plan options (e.g., monthly, annual) |
| CTA button | Primary purchase button with dynamic price text |
| Social proof | Testimonials, ratings, or user counts |
| Guarantee | Money-back guarantee or free trial messaging |
Layout Types
| Layout | Description |
|---|
"stack" | Vertical stack layout (sections arranged top to bottom) |
"grid" | Grid layout (for feature comparison or multi-plan views) |
Configuration in Console
Paywalls are created and managed in the AppDNA Console:
- Navigate to Monetization > Paywalls.
- Create a new paywall or edit an existing one.
- Add sections (header, features, plans, CTA, social proof, guarantee).
- Link App Store products to the plan options.
- Optionally assign the paywall to an experiment for A/B testing.
- Publish the paywall to make it available via the config bundle.
Ensure the product identifiers used in your paywall match the products configured in App Store Connect. Mismatched identifiers will cause purchase failures.
Full Example
import AppDNASDK
class PremiumGate: AppDNAPaywallDelegate {
private weak var presenter: UIViewController?
init(presenter: UIViewController) {
self.presenter = presenter
AppDNA.paywall.setDelegate(self)
}
func showPremiumPaywall(placement: String) {
guard let vc = presenter else { return }
AppDNA.presentPaywall(
id: "premium_paywall",
from: vc,
context: PaywallContext(placement: placement),
delegate: self
)
}
// MARK: - AppDNAPaywallDelegate
func onPaywallPresented(paywallId: String) {
// Track additional analytics if needed
}
func onPaywallAction(paywallId: String, action: PaywallAction) {
// Handle custom actions
}
func onPaywallPurchaseStarted(paywallId: String, productId: String) {
// Show loading state if desired
}
func onPaywallPurchaseCompleted(
paywallId: String,
productId: String,
transaction: TransactionInfo
) {
// Unlock premium features
unlockPremium()
}
func onPaywallPurchaseFailed(paywallId: String, error: Error) {
// Error is shown in the paywall UI automatically
}
func onPaywallDismissed(paywallId: String) {
// User closed without purchasing
}
}
The paywall module integrates with the billing module for purchase handling. Ensure your billingProvider is configured correctly in AppDNAOptions. See the Billing guide for details.