Tracking Subscription State

Observe whether a user is on a paid plan from shared Kotlin code.

Beta

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

Superwall tracks subscription state for you. But there are times you need to know directly whether a user is on a paid plan, either to show different UI or to unlock something without a placement.

Read it synchronously

import com.superwall.sdk.kmp.models.entitlements.SubscriptionStatus

when (val status = Superwall.subscriptionStatus) {
    is SubscriptionStatus.Active -> showPro(status.entitlements)
    is SubscriptionStatus.Inactive -> showFree()
    is SubscriptionStatus.Unknown -> showLoading()
}

SubscriptionStatus has three states:

StateMeaning
UnknownNot yet determined, typically before configuration completes
Active(Set<Entitlement>)The user has one or more active entitlements
InactiveThe user has no active entitlements

There is also a convenience boolean:

if (Superwall.subscriptionStatus.isActive) { /* ... */ }

Treat Unknown as its own case, not as "not subscribed". Showing free-tier UI during Unknown will flash the wrong state at paying users on cold start.

Observe changes

subscriptionStatusFlow is a StateFlow, so it always has a current value and emits on change:

import kotlinx.coroutines.launch

scope.launch {
    Superwall.subscriptionStatusFlow.collect { status ->
        when (status) {
            is SubscriptionStatus.Active -> showPro(status.entitlements)
            is SubscriptionStatus.Inactive -> showFree()
            is SubscriptionStatus.Unknown -> showLoading()
        }
    }
}

Two naming details to watch for if you are coming from the Android SDK.

The flow is Superwall.subscriptionStatusFlow. The plain Superwall.subscriptionStatus is a synchronous property, not a flow. On the Android SDK, subscriptionStatus is the flow.

And this flow is guard-exempt: you can collect it before configure, where it is seeded with SubscriptionStatus.Unknown and attached to the native source once configuration completes. You do not have to sequence collection behind configuration.

This is a plain StateFlow, which means it delivers on your collector's context and does not force emissions onto the main thread. Collect it from a main-dispatched scope (collectAsState, viewModelScope, lifecycleScope) and updating UI from the collector is safe. Collect it on Dispatchers.IO and it is not.

The SDK's own KDoc says emissions arrive on the main thread; that is true of the common case, not a guarantee the flow enforces.

With Compose Multiplatform

@Composable
fun ContentScreen() {
    val status by Superwall.subscriptionStatusFlow.collectAsState()

    when (val current = status) {
        is SubscriptionStatus.Active -> PremiumContent(current.entitlements)
        is SubscriptionStatus.Inactive -> FreeContent()
        is SubscriptionStatus.Unknown -> LoadingIndicator()
    }
}

Setting it yourself

If you configured with a PurchaseController, you own subscription state and must set it:

Superwall.subscriptionStatus = SubscriptionStatus.Active(entitlements)

Only set this when you have a PurchaseController. Without one, Superwall manages the value and writing to it will fight the SDK.

Detailed purchase history

CustomerInfo carries more than SubscriptionStatus does, including full transaction history that merges device and web purchases:

val info = Superwall.getCustomerInfo()

info.subscriptions       // List<SubscriptionTransaction>
info.nonSubscriptions    // List<NonSubscriptionTransaction>
info.entitlements        // List<Entitlement>
info.userId              // String

Each SubscriptionTransaction includes productId, purchaseDate, expirationDate, willRenew, isActive, isInGracePeriod, and more.

Observe changes with customerInfoFlow:

scope.launch {
    Superwall.customerInfoFlow.collect { info ->
        render(info)
    }
}

Customer info works on both platforms. If you are reading the SDK's own KDoc, note that the @platform iOS annotations on customerInfoFlow and getCustomerInfo are out of date. They describe a limitation that no longer applies. On Android the flow is fed by the native customerInfoDidChange delegate hook, and getCustomerInfo() calls straight through to superwall-android 2.8.0.

customerInfoFlow has no replay, so a new collector gets nothing until the next change. Use getCustomerInfo() for the current value.

Restoring purchases

import com.superwall.sdk.kmp.models.results.RestorationResult

when (val result = Superwall.restorePurchases()) {
    is RestorationResult.Restored -> println("Restored")
    is RestorationResult.Failed -> println("Restore failed: ${result.error}")
}

Restoration failure stays in the return type and does not throw. Restored means the restore completed without errors, not that the user necessarily has an active subscription.

How is this guide?

On this page