Present your first paywall

Register a placement and present a Superwall paywall from shared code.

Beta

The KMP SDK is in beta and its API may change between releases.

Register a placement

Paywalls are presented by registering a placement. You do not tell the SDK to show a paywall. You tell it a placement occurred, and your campaign on the dashboard decides what happens.

Superwall.register(placement = "campaign_trigger")

That one line is a complete integration. Whether a paywall appears, which one, and to whom, is all configured on the dashboard without shipping an app update.

The placement name must match one you have added to a campaign on the dashboard. A name that is not in any campaign resolves to PaywallSkippedReason.PlacementNotFound.

Gate a feature behind it

Pass a feature closure to run code that should only happen if the user is entitled to it:

Superwall.register(placement = "campaign_trigger") {
    launchTheFeature()
}

When the closure runs depends on the paywall's feature-gating behavior: immediately when no paywall shows, or after a purchase or restore when the placement is gated. Feature gating covers the rules.

Pass parameters

params are usable in audience filters and on the paywall itself:

Superwall.register(
    placement = "campaign_trigger",
    params = mapOf(
        "source" to "onboarding",
        "isTrialEligible" to true,
    ),
)

Values may be String, Boolean, Long, Double, List, Map, or Set. Anything else is stringified.

Observe what happened

A PaywallPresentationHandler reports on the presentation. Set only the closures you care about; unset ones are never invoked.

import com.superwall.sdk.kmp.PaywallPresentationHandler
import com.superwall.sdk.kmp.models.results.PaywallResult
import com.superwall.sdk.kmp.models.results.PaywallSkippedReason

val handler = PaywallPresentationHandler()

handler.onPresent { info ->
    println("Presented ${info.name}")
}

handler.onDismiss { info, result ->
    when (result) {
        is PaywallResult.Purchased -> println("Purchased ${result.productId}")
        is PaywallResult.Restored -> println("Restored")
        is PaywallResult.Declined -> println("Declined")
    }
}

handler.onSkip { reason ->
    when (reason) {
        is PaywallSkippedReason.Holdout -> println("Holdout: ${reason.experiment.id}")
        is PaywallSkippedReason.NoAudienceMatch -> println("No audience match")
        is PaywallSkippedReason.PlacementNotFound -> println("Placement not found")
    }
}

handler.onError { error ->
    println("Paywall error: $error")
}

Superwall.register(
    placement = "campaign_trigger",
    handler = handler,
) {
    launchTheFeature()
}

onSkip is not a failure path. A holdout or an unmatched audience filter means your campaign worked as configured. The user was not meant to see a paywall.

All handler closures and the feature closure are delivered on the main thread, on both platforms. You can touch UI from them directly.

Custom callbacks

onCustomCallback is the one closure that returns a value. A paywall can request an action from your app and branch on the result:

import com.superwall.sdk.kmp.models.callbacks.CustomCallbackResult

handler.onCustomCallback { callback ->
    when (callback.name) {
        "validate_email" -> {
            val email = callback.variables?.get("email") as? String
            if (isValidEmail(email)) {
                CustomCallbackResult.success(mapOf("validated" to true))
            } else {
                CustomCallbackResult.failure(mapOf("error" to "Invalid email"))
            }
        }
        else -> CustomCallbackResult.failure()
    }
}

It is a suspend closure, so you can do real work in it. When it is unset, the SDK responds with CustomCallbackResult.failure().

Check before you register

getPresentationResult previews what registering would do without presenting anything, which is useful for adjusting UI ahead of time, like hiding an upgrade button for users who would not see a paywall:

val result = Superwall.getPresentationResult(placement = "campaign_trigger")

Controlling the paywall

A few members act on the presented paywall:

Superwall.dismiss()                        // suspend; resumes once dismissed
Superwall.isPaywallPresented               // Boolean
Superwall.latestPaywallInfo                // PaywallInfo?
Superwall.togglePaywallSpinner(isHidden = true)

Preloading

Paywalls preload by default. To take over the timing, disable it and preload yourself:

Superwall.configure(
    apiKey = "pk_your_api_key",
    options = SuperwallOptions(paywalls = PaywallOptions(shouldPreload = false)),
)

// Later
Superwall.preloadAllPaywalls()
Superwall.preloadPaywalls(placementNames = setOf("campaign_trigger"))

Next, manage your users.

How is this guide?

On this page