The AppDNA SDK supports server-driven paywalls that are configured remotely and rendered natively using Jetpack Compose. Paywalls handle the full purchase flow, from presenting product options to completing the transaction.
Present a Paywall
Present a paywall by passing the current Activity, the paywall ID, and an optional context:
import ai.appdna.sdk.AppDNA
import ai.appdna.sdk.paywall.PaywallContext
AppDNA.presentPaywall(
activity = this,
id = "premium_paywall",
context = PaywallContext(placement = "settings"),
listener = paywallListener
)
Paywall Module
The AppDNA.paywall module provides direct access to paywall functionality:
| Method | Description |
|---|
present(activity, paywallId, context?) | Presents the paywall |
setDelegate(delegate: AppDNAPaywallDelegate?) | Sets the paywall delegate |
Paywall Context
Pass additional context to control paywall behavior and tracking:
import ai.appdna.sdk.paywall.PaywallContext
val context = PaywallContext(
placement = "settings",
experiment = "paywall_test",
variant = "b"
)
AppDNA.paywall.present(
activity = this,
paywallId = "premium_paywall",
context = context
)
PaywallContext Properties
| Property | Type | Description |
|---|
placement | String | Where the paywall was triggered (e.g., “settings”, “onboarding”) |
experiment | String? | Experiment identifier for A/B testing |
variant | String? | Variant identifier within the experiment |
The placement property is used for analytics segmentation. Use descriptive placement names to track which entry points convert best.
Paywall Delegate
Implement AppDNAPaywallDelegate to receive paywall lifecycle callbacks:
import ai.appdna.sdk.paywall.AppDNAPaywallDelegate
import ai.appdna.sdk.paywall.PaywallAction
import ai.appdna.sdk.billing.TransactionInfo
class MyPaywallDelegate : AppDNAPaywallDelegate {
override fun onPaywallPresented(paywallId: String) {
Log.d("Paywall", "Paywall presented: $paywallId")
}
override fun onPaywallAction(paywallId: String, action: PaywallAction) {
Log.d("Paywall", "Action: $action on paywall: $paywallId")
}
override fun onPaywallPurchaseStarted(paywallId: String, productId: String) {
Log.d("Paywall", "Purchase started: $productId")
}
override fun onPaywallPurchaseCompleted(
paywallId: String,
productId: String,
transaction: TransactionInfo
) {
Log.d("Paywall", "Purchase completed: $productId")
// Dismiss paywall or navigate to premium content
}
override fun onPaywallPurchaseFailed(paywallId: String, error: Exception) {
Log.e("Paywall", "Purchase failed on paywall: $paywallId", error)
}
override fun onPaywallDismissed(paywallId: String) {
Log.d("Paywall", "Paywall dismissed: $paywallId")
}
}
// Set the delegate
AppDNA.paywall.setDelegate(MyPaywallDelegate())
Paywall Action
The PaywallAction enum represents user interactions within the paywall:
| Value | Description |
|---|
CTA_TAPPED | User tapped the primary call-to-action button |
FEATURE_SELECTED | User selected a feature to view details |
PLAN_CHANGED | User switched between plan options |
LINK_TAPPED | User tapped a link (e.g., terms, privacy) |
CUSTOM | Custom action defined in the paywall config |
Dismiss Reason
The DismissReason enum indicates how the paywall was closed:
| Value | Description |
|---|
PURCHASED | Paywall dismissed after a successful purchase |
DISMISSED | User explicitly dismissed the paywall |
TAPPED_OUTSIDE | User tapped outside the paywall to close it |
PROGRAMMATIC | Paywall was dismissed programmatically |
Auto-Tracked Events
The paywall module automatically tracks the following events:
| Event | Triggered When |
|---|
paywall_view | Paywall is presented to the user |
paywall_close | Paywall is dismissed |
purchase_started | User initiates a purchase from the paywall |
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 |
| Image | Full-width or sized image with optional corner radius |
| Spacer | Configurable vertical spacing between sections |
| Testimonial | Quote with author name, role, and optional avatar |
| Countdown | Urgency countdown timer with configurable expiry behavior |
| Legal | Terms of service, privacy policy, and subscription terms |
| Divider | Horizontal separator line with optional label |
| Sticky footer | Fixed bottom bar with CTA and price summary |
| Card | Rounded card container for grouping related content |
| Carousel | Horizontally scrollable content cards (features, reviews) |
| Timeline | Step-by-step vertical timeline (e.g., trial-to-paid flow) |
| Icon grid | Grid layout of icons with labels (feature highlights) |
| Comparison table | Side-by-side plan comparison (free vs. premium columns) |
| Promo input | Text field for entering promotional or coupon codes |
| Toggle | On/off toggle for add-on options (e.g., annual billing) |
| Reviews carousel | Horizontally 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:
| Style | Description |
|---|
HORIZONTAL_CARDS | Side-by-side cards, one per plan. Best for 2-3 plans. |
VERTICAL_STACK | Stacked rows, each showing plan name, price, and badge. |
TOGGLE_SWITCH | Two-option toggle (e.g., Monthly / Annual). |
SEGMENTED_CONTROL | Native segmented control for 2-4 plan options. |
RADIO_LIST | Radio button list with plan details per row. |
CAROUSEL | Horizontally scrollable plan cards with snap behavior. |
COMPARISON_GRID | Feature comparison grid with checkmarks per plan column. |
PILL_SELECTOR | Rounded pill buttons arranged horizontally. |
TIER_BLOCKS | Large stacked blocks with feature lists per tier. |
MINIMAL_TEXT | Text-only plan names with prices, no cards or borders. |
FEATURED_HIGHLIGHT | One plan visually promoted (larger, badge, glow). |
ACCORDION | Expandable/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
When a paywall includes a Promo input section, implement the promo code delegate method to validate codes against your backend:
class MyPaywallDelegate : AppDNAPaywallDelegate {
// ... other callbacks ...
override fun onPromoCodeSubmit(
paywallId: String,
code: String,
completion: (PromoCodeResult) -> Unit
) {
// Validate the code with your backend
lifecycleScope.launch {
try {
val discount = MyAPI.validatePromoCode(code)
completion(PromoCodeResult.Valid(
discount = discount.percentage,
label = discount.label
))
} catch (e: Exception) {
completion(PromoCodeResult.Invalid(
message = "This code is not valid or has expired."
))
}
}
}
}
| Result Case | Description |
|---|
Valid(discount, label) | Code accepted — the SDK updates displayed prices with the discount |
Invalid(message) | Code rejected — the SDK shows the error message in the promo input |
When a paywall includes Toggle sections (e.g., for annual billing or add-on options), the toggle state is automatically included in the purchase metadata passed to onPaywallPurchaseCompleted:
override fun onPaywallPurchaseCompleted(
paywallId: String,
productId: String,
transaction: TransactionInfo
) {
val toggles = transaction.metadata["toggles"] as? Map<String, Boolean>
val annualBilling = toggles?.get("annual_billing") ?: false
val addonSelected = toggles?.get("premium_addon") ?: false
}
Purchase completion and failure events are tracked by the billing module, not the paywall module. See the Billing documentation for the full list of purchase-related events.
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.
Paywalls are rendered using Jetpack Compose. The SDK handles the full UI lifecycle, including product loading, plan selection, and purchase flow. Your app only needs to set the delegate and handle callbacks.
Next Steps