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:
| State | Meaning |
|---|---|
Unknown | Not yet determined, typically before configuration completes |
Active(Set<Entitlement>) | The user has one or more active entitlements |
Inactive | The 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 // StringEach 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?