Subscriptions
Observing Subscription Status
KapeAuthClient.subscriptions publishes the signed-in user's subscriptions (including expired
ones) as a read-only, self-maintained StateFlow<List<KapeSubscription>?> — there is no method to
call, and no separate refresh step.
import com.kape.platformsdk.auth.active
auth.subscriptions.collect { subs ->
if (subs == null) return@collect // not resolved yet for this session — keep waiting
val active = subs.active // first subscription that currently grants access, or null
}
subscriptions is null until the first fetch for the current session resolves — including right
after sign-out, since there is then no session to resolve for — and an empty list once resolved for
an account with none. The two are deliberately distinguishable: treating "not fetched yet" as
"resolved, zero subscriptions" would make any logic gated on subscription count fire prematurely.
The SDK keeps it fresh on its own: refreshed on sign-in and whenever the app returns to the
foreground (throttled to once per 60 seconds; the underlying API is not cached). Each
KapeSubscription exposes status (KapeSubscriptionStatus — billing status), source
(APP_STORE/PLAY_STORE/WEB/UNKNOWN), tier, currentTermEnd/nextBillingAt/cancelledAt
(Date?), billingPeriod (see below), lastAutoBillPaymentFailed (Boolean — the last automatic
renewal payment failed), isBusiness (Boolean — a business account, whose subscription management
is restricted), and isActive (a convenience over status).
Billing Period
billingPeriod is how often the subscription renews. The unit and the count travel together as one
KapeBillingPeriod, so a count can never be read without the unit it belongs to:
val label = when (val period = sub.billingPeriod) {
is KapeBillingPeriod.Day -> "Every ${period.count} day(s)"
is KapeBillingPeriod.Week -> "Every ${period.count} week(s)"
is KapeBillingPeriod.Month -> "Every ${period.count} month(s)"
is KapeBillingPeriod.Year -> "Every ${period.count} year(s)"
KapeBillingPeriod.Unknown -> "Unknown"
}
count is also readable directly off any KapeBillingPeriod when the unit does not matter — it is
null for Unknown, which is what the SDK reports for a unit this version does not recognise (the
count it came with is not interpretable without one).
Guest Sessions Have No Subscriptions
A guest session has no account behind it, so it has no
subscriptions — and the SDK does not try to fetch any for one. subscriptions resolves straight
to an empty list (never null, so UI gated on "has the fetch resolved yet?" does not wait for a
fetch that will never come), and .active on it is therefore null.
val session = auth.currentAuthenticationSession()
if (session?.isGuest == true) {
// No subscription to show. Gate on entitlements, or offer an account upgrade.
}
This is not an error state and needs no handling beyond not presenting subscription UI. A guest's
access comes from the entitlements your brand's guest tenant grants, not from a KapeSubscription.
Guest vs Free User
These are different things and it is worth keeping them apart:
| Guest | Free user | |
|---|---|---|
| Account | None. No e-mail, no credentials — an identity the SDK minted locally | A real account, created through sign-up |
| Signs in with | loginAsGuest(context) | login(context) |
session.isGuest | true | false |
session.displayName | null | their e-mail |
subscriptions | empty — nothing to show | a KapeSubscription whose tier reflects the free tier |
| Can it be recovered on another device? | No | Yes, by signing in |
So a free user is visible in subscriptions; a guest is not. If you need to tell "this user is
on the free tier" from "this user has no account at all", read session.isGuest — do not infer it
from an empty subscription list, which both produce only if the free tier happens to carry no
subscription record.
Guests and free users are likely to be offered the same VPN locations, since both typically resolve to the same freemium entitlements — but that is a property of your brand's backend configuration, not a guarantee of this SDK. Do not hard-code the assumption that the two see an identical server list.
Selecting a Subscription for Entitlements
Features that require an entitlement (such as VPN) act on one subscription, and the SDK resolves the VPN one itself — on start from the persisted authentication state, and again on every authentication change. Nothing has to be called after login, including from a boot receiver, where nothing has opened your UI.
handle.awaitSdkManager() — which every SDK call already goes through — returns only once that pick has settled, so VPN calls never race it. Settled includes finding nothing, so check for a subscription rather than waiting for one:
handle.awaitSdkManager()
if (handle.activeSubscriptionId.isEmpty()) showNoSubscriptionScreen()
activeSubscriptionId is empty when signed out, when no subscription holds the entitlement, or when the lookup failed — typically no network at boot, retried on the next authentication change. The lookup is bounded by a timeout, so no SDK call waits on it indefinitely.
The VPN entitlement is the default. Pass another to the constructor, or null to turn the automatic pick off and select explicitly:
val handle = KapeSdkHandle(settings, context, autoActivatedEntitlement = null)
Overriding the Automatic Choice
An account with several VPN subscriptions, or a feature on a different entitlement, still selects explicitly. Call activateSubscription(...) to pick the first subscription holding an entitlement, or setActiveSubscriptionId(...) to name one outright.
Activating a Subscription
Pass a predefined product type to select a subscription by its entitlement:
import com.kape.platformsdk.core.KapeProductType
// Activate using the built-in VPN product type
handle.activateSubscription(KapeProductType.Vpn)
activateSubscription(...) is a suspend function. The KapeProductType.Vpn product type matches any subscription that carries the <tenant_id>.vpn entitlement (e.g. xv.vpn).
For custom or multi-product scenarios, pass explicit entitlement IDs:
// Activate using custom entitlement IDs
handle.activateSubscription(KapeProductType.Custom(values = listOf("acme.a", "acme.b")))
A subscription is considered a match if it grants any of the provided entitlements.
If multiple subscriptions match, the SDK automatically selects the first one.
Typical Flow
Logging in is enough — the handle resolves the subscription off the authentication change, and connect waits for it:
// In a coroutine (e.g. viewModelScope.launch):
// 1. Authenticate
when (auth.login(context)) {
is LoginResult.Success -> {
// 2. Connect to VPN — the handle has picked the subscription
vpn.connect(KapeConnectionMode.Auto)
}
else -> { /* handle cancellation or error */ }
}