Skip to main content

Subscriptions

Before using any features that require an entitlement (such as VPN), the SDK needs to know which subscription to operate on. Call activateSubscription(for:) on the handle after authentication to select the correct subscription.

Observing Subscription Details​

KapeAuthClient.subscriptions publishes the signed-in user's subscriptions (including expired ones) as a read-only, self-maintained AnyPublisher<[KapeSubscription]?, Never> — there is no method to call, and no separate refresh step. It is nil until the first fetch for the current session resolves, and [] once resolved for an account with none — including a guest session, which has no subscriptions at all (see below).

Each KapeSubscription exposes status, source, tier, currentTermEnd/nextBillingAt/cancelledAt (Date?), billingPeriod, lastAutoBillPaymentFailed (Bool — the last automatic renewal payment failed), isBusiness (Bool — a business account, whose subscription management is restricted), and isActive (a convenience over status). array.active returns the first subscription that currently grants access.

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:

let label = switch subscription.billingPeriod {
case .day(let count): "Every \(count) day(s)"
case .week(let count): "Every \(count) week(s)"
case .month(let count): "Every \(count) month(s)"
case .year(let count): "Every \(count) year(s)"
case .unknown: "Unknown"
}

count is also readable directly off any KapeBillingPeriod when the unit does not matter — it is nil 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 [] (never nil, so UI gated on "has the fetch resolved yet?" does not wait for a fetch that will never come), and ?.active is therefore nil.

if let session = await auth.currentAuthenticationSession(), session.isGuest {
// 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()login(presenting:)
session.isGuesttruefalse
session.displayNameniltheir e-mail
subscriptions[] — 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.

Activating a Subscription​

Pass a predefined product type to select a subscription by its entitlement:

import KapeCore

// Activate using the built-in VPN product type
try await handle.activateSubscription(for: .vpn)

The .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
try await handle.activateSubscription(for: .custom(values: ["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​

activateSubscription(for:) is called once after a successful login, before initiating a VPN connection:

// 1. Authenticate
try await auth.login(presenting: viewController)

// 2. Activate the subscription
try await handle.activateSubscription(for: .vpn)

// 3. Connect to VPN
try await vpn.connect(to: locationId)