Skip to main content
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
)

Present by Placement

Present a paywall by placement — the SDK uses audience rules configured in the Console to determine which paywall to show:
// Present paywall by placement (audience rules determine which paywall to show)
AppDNA.presentPaywall(placement: "premium_upgrade", from: viewController)

Module Access

Access the paywall module directly:
let paywall = AppDNA.paywall

Module Methods

MethodSignatureDescription
presentpresent(_: String, from: UIViewController, context: PaywallContext?)Present a paywall by ID
setDelegatesetDelegate(_ 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"
)
PropertyTypeDescription
placementStringWhere the paywall is triggered (e.g., “settings”, “onboarding”, “feature_gate”)
experimentString?Experiment identifier, if showing as part of an A/B test
variantString?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)
    func onPromoCodeSubmit(paywallId: String, code: String, completion: @escaping (Bool) -> Void)
    func onPostPurchaseDeepLink(paywallId: String, url: URL)
    func onPostPurchaseNextStep(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:
CaseDescription
.ctaTappedThe main call-to-action button was tapped
.featureSelectedA feature item was selected
.planChangedThe user switched between plan options
.linkTappedA link (e.g., terms, privacy policy) was tapped
.customA custom action defined in the paywall config

DismissReason

The DismissReason enum indicates how the paywall was closed:
CaseDescription
.purchasedDismissed after a successful purchase
.dismissedDismissed by the user via a close button
.tappedOutsideDismissed by tapping outside the paywall
.programmaticDismissed programmatically by your code

Paywall Sections

Paywalls configured in the Console support the following content sections:
SectionDescription
HeaderTitle, subtitle, and optional hero image
Features listList of feature highlights with icons and descriptions
Plan selectionSelectable plan options (e.g., monthly, annual)
CTA buttonPrimary purchase button with dynamic price text
Social proofTestimonials, ratings, or user counts
GuaranteeMoney-back guarantee or free trial messaging
ImageFull-width or sized image with optional corner radius
SpacerConfigurable vertical spacing between sections
TestimonialQuote with author name, role, and optional avatar
CountdownUrgency countdown timer with configurable expiry behavior
LegalTerms of service, privacy policy, and subscription terms
DividerHorizontal separator line with optional label
Sticky footerFixed bottom bar with CTA and price summary
CardRounded card container for grouping related content
CarouselHorizontally scrollable content cards (features, reviews)
TimelineStep-by-step vertical timeline (e.g., trial-to-paid flow)
Icon gridGrid layout of icons with labels (feature highlights)
Comparison tableSide-by-side plan comparison (free vs. premium columns)
Promo inputText field for entering promotional or coupon codes
ToggleOn/off toggle for add-on options (e.g., annual billing)
Reviews carouselHorizontally scrollable user reviews with ratings

Plan Display Styles

The plan selection section supports 12 display styles, configured in the Console. Each style controls how subscription plans are laid out and visually differentiated:
StyleDescription
horizontal_cardsSide-by-side cards, one per plan. Best for 2-3 plans.
vertical_stackStacked rows, each showing plan name, price, and badge.
toggle_switchTwo-option toggle (e.g., Monthly / Annual).
segmented_controlNative segmented control for 2-4 plan options.
radio_listRadio button list with plan details per row.
carouselHorizontally scrollable plan cards with snap behavior.
comparison_gridFeature comparison grid with checkmarks per plan column.
pill_selectorRounded pill buttons arranged horizontally.
tier_blocksLarge stacked blocks with feature lists per tier.
minimal_textText-only plan names with prices, no cards or borders.
featured_highlightOne plan visually promoted (larger, badge, glow).
accordionExpandable/collapsible plan sections with full details.

Card & Badge Customization

Plan cards support per-plan styling configured in the Console:
  • Badge text and color — e.g., “Best Value”, “Most Popular” with custom background
  • Card border and shadow — highlight the selected or recommended plan
  • Save percentage — automatically calculated and displayed on annual plans
  • Trial label — shows “7-day free trial” or custom trial messaging
  • Card background — solid color, gradient, or image per plan card
  • Corner radius — configurable per card

Layout Types

LayoutDescription
"stack"Vertical stack layout (sections arranged top to bottom)
"grid"Grid layout (for feature comparison or multi-plan views)
"carousel"Horizontally scrollable section layout

Rich Media in Paywalls

Paywall sections support rich media content configured in the Console:
  • Lottie animations — animated hero images, feature illustrations, or backgrounds
  • Rive animations — interactive state-machine-driven animations
  • Video — inline video in header or feature sections
  • Haptic feedback — triggered on plan selection and CTA taps
  • Particle effects — confetti or celebration effects on purchase completion
  • Per-section styling — background colors, gradients, images, borders, shadows, and corner radius
See the Rich Media guide for full details on supported formats and configuration.

Promo Code Handling

When a paywall includes a Promo input section, implement the promo code delegate method to validate codes against your backend:
func onPromoCodeSubmit(paywallId: String, code: String, completion: @escaping (Bool) -> Void) {
    // Validate the code and call completion(true) for valid, completion(false) for invalid
    validateCode(code) { isValid in
        completion(isValid)
    }
}

Configuration in Console

Paywalls are created and managed in the AppDNA Console:
  1. Navigate to Monetization > Paywalls.
  2. Create a new paywall or edit an existing one.
  3. Add sections (header, features, plans, CTA, social proof, guarantee).
  4. Link App Store products to the plan options.
  5. Optionally assign the paywall to an experiment for A/B testing.
  6. 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.