Skip to main content

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
}
info

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:

GuestFree user
AccountNone. No e-mail, no credentials — an identity the SDK minted locallyA real account, created through sign-up
Signs in withloginAsGuest(context)login(context)
session.isGuesttruefalse
session.displayNamenulltheir e-mail
subscriptionsempty — nothing to showa KapeSubscription whose tier reflects the free tier
Can it be recovered on another device?NoYes, 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.

info

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.

info

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 */ }
}