Supported on: iOS SDK
1.0.70+ · Android SDK 1.0.42+ · React Native SDK 1.0.7+ (New Architecture only)Present a Paywall
Present a paywall by its identifier:The paywall ID must match the ID configured in the AppDNA Console under Monetization > Paywalls.
Present by Placement
Pass a placement string inPaywallContext to tag the presentation for analytics and audience targeting:
Routing by placement in app code
To pick the paywall ID based on the placement at runtime, keep a small placement-to-ID map in your app and pass both values topresent():
Module Access
PaywallContext
The
placement value is included in all paywall analytics events, allowing you to measure conversion by placement in the Console.AppDNAPaywallDelegate
Implement the delegate to receive paywall lifecycle callbacks. The same delegate fires for paywalls launched standalone viaAppDNA.paywall.present(...) AND for paywalls launched from inside an onboarding flow (via the paywall_trigger graph node) — register once with AppDNA.paywall.setDelegate(...).
Example Implementation
Restore Purchases
The Restore button is rendered alongside the CTA section (above or below the main subscribe button, controlled by the section’srestore_text and restore_position config in the Console). When the user taps it, the SDK runs:
onPaywallRestoreStarted(paywallId)fires immediately.- The SDK calls the native store API — StoreKit 2 on iOS,
BillingClient.queryPurchasesAsync(SUBS)+(INAPP)on Android — and queries your verification endpoint for previously-purchased products. - On success:
onPaywallRestoreCompleted(paywallId, restoredProductIds)fires, and the SDK emits apurchase_restoredevent automatically. - On failure:
onPaywallRestoreFailed(paywallId, error)fires and the SDK emitspurchase_restore_failed.
Auto-dismiss + onboarding routing
When the paywall is presented from inside an onboarding flow via apaywall_trigger graph node, a successful restore that returns at least one product automatically:
- Dismisses the paywall.
- Refreshes the in-memory entitlement cache.
- Routes the onboarding flow via the trigger’s
on_success_target— same path as a real purchase.
on_dismiss_target only if the user then closes without buying.
For richer transaction details (status, expiry, store), call
AppDNA.billing.getEntitlements() after a successful restore — it returns Entitlement[] with the full verified state. The paywall delegate intentionally returns just product IDs to keep the callback lightweight.Entitlement-aware Paywall Triggers
Onboarding flows can includepaywall_trigger graph nodes that present a paywall mid-flow. Each trigger carries a skip_if_subscribed flag (default true for upsells). When the native SDK reaches the trigger, it checks AppDNA.billing.hasActiveSubscription() first:
- Subscribed +
skip_if_subscribed: true→ paywall is skipped. Flow routes viaon_subscribed_skip_target(falls back toon_success_target, then to the downstream edge). Nopaywall_viewevent is emitted. - Subscribed +
skip_if_subscribed: false→ paywall is shown anyway. Use this for cross-sell / lifetime upgrade paywalls where existing subscribers are the target audience. - Not subscribed → paywall is presented normally.
Refreshing the entitlement cache
When you callAppDNA.identify(...) after sign-in, the native SDK silently refreshes the entitlement cache in the background. The next paywall_trigger gate then reflects the identified user’s store subscriptions instead of the prior anonymous device’s empty state.
If your app completes auth out-of-band (SSO callback, OAuth web flow) without calling identify, force a refresh from TypeScript with:
paywall_trigger will see the fresh state.
Custom Paywall Presentation
If you want a fully custom paywall UI (built in React Native components rather than rendered by the SDK), drive the purchase yourself with the billing module:AppDNA.paywall.present(...). Reserve custom presentation for cases where the rendered paywall cannot meet a hard product requirement.
PaywallAction Values
Theaction argument passed to onPaywallAction is one of:
DismissReason
The native SDK reports a dismiss reason in analytics events; on React Native you receive the lifecycle viaonPaywallDismissed. Reasons include:
Paywall Sections
Paywalls configured in the Console support the following content sections:Plan Display Styles
Configured per paywall as a lowercaseplan_display_style string.
Card & Badge Customization
Plan cards support per-plan styling configured in the Console: badge text and color (e.g., “Best Value”), card border and shadow, auto-calculated save percentage on annual plans, trial label, card background (solid / gradient / image), and per-card corner radius.Layout Types
Rich Media in Paywalls
Paywall sections support: Lottie animations, Rive animations, inline video, haptic feedback on plan selection / CTA, particle effects (confetti) on purchase, and per-section styling (background colors, gradients, images, borders, shadows, corner radius). See the Rich Media guide for full details.Promo Code Handling
Paywalls that include aPromo input section validate codes server-side against the rules configured in the Console (under Monetization > Promo Codes). When the user submits a code:
- The SDK posts it to the Console validation endpoint along with the paywall ID, the active product set, and the user’s identity.
- On a valid code, the matching discount is applied to the displayed plans and an updated price is rendered.
- On an invalid or expired code, the SDK surfaces the rejection inline inside the section.
onPromoCodeSubmit on the paywall delegate:
Promise<boolean>, displays a “Validating…” spinner inside the promo section, and on true re-renders the paywall with the discounted plan prices the Console computed.
Auto-Tracked Events
Error Handling
- Purchase errors arrive via
onPaywallPurchaseFailed(paywallId, error, errorType, productId).erroris the platform’s error message string (localizedDescriptionon iOS,messageon Android) — it is not a JSErrorand has no.messageproperty;String(error)is the whole of it. Branch onerrorType, which is the stable reason code:userCancelled,productNotFound,verificationFailed,networkError,serverError,pending,providerNotAvailable,unknown.productIdnames the plan that failed, or isnullif none was selected. The paywall stays visible so the user can retry. - Restore errors arrive via
onPaywallRestoreFailed(paywallId, error)— again a message string. Common causes: no previous purchases, network failure, store outage. The paywall stays visible.
onPaywallRestoreFailed fires immediately with a providerNotAvailable error and onPaywallRestoreStarted is not fired. This only affects misconfigured hosts; production apps with billingProvider configured in AppDNAOptions never hit this branch.
Testing in Sandbox
- iOS — Sign in to a sandbox Apple ID in Settings > App Store > Sandbox Account. Purchases route through the StoreKit sandbox; receipts validate against Apple’s sandbox endpoint. A StoreKit configuration file in Xcode also works for simulator runs.
- Android — Add the tester’s Google account as a license tester in Google Play Console > Setup > License testing and upload to an internal testing track. Sandbox purchases never charge the card and renewals run on accelerated timelines.
Configuration in Console
- Navigate to Monetization > Paywalls.
- Create or edit a paywall.
- Add sections (header, features, plans, CTA, social proof, guarantee).
- Link App Store and Google Play products to the plan options.
- Optionally assign the paywall to an experiment for A/B testing.
- Publish to make it available via the config bundle.
Full Example
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.Next Steps
- Set up Billing for detailed purchase handling
- Configure Onboarding flows that transition into paywalls
- Learn about Offline Support for paywall config caching

