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 — only when the SDK can restore (storeKit2, billing configured). Under a provider that owns restoring, or before billing is ready, the tap goes straight toonPaywallRestoreFailed(see below).- The SDK reads the previously-purchased products from the native store — StoreKit 2 on iOS (read on the device; each restored transaction is then sent to AppDNA’s
/billing/verifyin the background, which the restore does not wait for),BillingClient.queryPurchasesAsync(SUBS)+(INAPP)on Android, where it also sends the owned purchases to AppDNA’s/billing/restore. - 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, unless its On failure action is set to Dismiss.providerNotAvailableis also the intended answer underbillingProvider: 'revenueCat'/ Adapty (see below). - Restore errors arrive via
onPaywallRestoreFailed(paywallId, error)— again a message string. Common causes: network failure, store outage, or a provider that cannot restore. A restore that finds nothing is not an error — it completes with an empty list. The paywall stays visible.
billingProvider is 'revenueCat' / Adapty (the provider owns purchases and restores) or 'none', or the provider is not linked — onPaywallRestoreFailed fires immediately with the refusal message (“RevenueCat: purchases are made by RevenueCat in your app”, “Adapty: purchases are made by Adapty in your app” or “No billing provider configured”) and onPaywallRestoreStarted is not fired (on both platforms). The callback carries no error code, so branch on your configured provider. Under a provider this is the intended answer, not a misconfiguration: restore through your provider (e.g. RevenueCat’s restorePurchases). A paywall plan tap in the same setups calls onPaywallPurchaseFailed with errorType: 'providerNotAvailable' and the tapped productId, and no store sheet opens — start the purchase with your provider from that callback (recipe).
A purchase or restore tap before billing is ready — before configure() has set billing up, or after AppDNA.shutdown() — is not providerNotAvailable: onPaywallPurchaseFailed fires with errorType: 'unknown' and onPaywallRestoreFailed with the message “AppDNA SDK not configured yet — call configure() first”, and no Started callback fires. Do not start a provider purchase for it; let the user retry once the SDK is configured.
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.
Template Variables
Any paywall text an author writes in the Console — a section title or subtitle, a feature row, a CTA label — can reference live values:{{var | fallback}} supplies text to use when the reference resolves to nothing.
Two differences from onboarding are worth knowing, because the syntax looks identical:
- On a paywall,
{{responses.…}}is addressed through the step (responses.step_id.field_id). The two-part form that works in an onboarding flow does not resolve here — use{{input.field_id}}, which searches every step for that field. - A paywall reference that cannot be resolved and has no fallback renders as an empty string, so
a mistyped variable leaves a gap rather than showing raw
{{...}}to a paying user. Onboarding renders the literal instead, which makes the typo visible while authoring.
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

