import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# License Types

The public types backing [`Octet.start(...)`](/docs/v3.0/api-reference/octet-start/) and `OctetSdk.licenseStatus`. See [License & Activation](/docs/v3.0/concepts/license-activation/) for the timeline model.

## `LicenseStatus`

Snapshot of the SDK's current license state. Read synchronously from `OctetSdk.licenseStatus`.

<Tabs groupId="platform">
  <TabItem value="swift" label="Swift (iOS)">

```swift
public struct LicenseStatus: Sendable, Equatable {
    public let state: LicenseState
    public let activatedAt: Date?
    public let hardStopAt: Date?
    public let daysUntilHardStop: Int?
    public let tier: String
    public let upgradeRecommended: Bool       // soft version hint; false until backend gating is on
    public let minSupportedVersion: String?   // soft version hint; nil until backend gating is on
}
```

  </TabItem>
  <TabItem value="kotlin" label="Kotlin (Android)">

```kotlin
data class LicenseStatus(
    val state: LicenseState,
    val activatedAt: Instant?,
    val hardStopAt: Instant?,
    val daysUntilHardStop: Int?,
    val tier: String,
    val upgradeRecommended: Boolean,      // soft version hint; false until backend gating is on
    val minSupportedVersion: String?,     // soft version hint; null until backend gating is on
)
```

  </TabItem>
</Tabs>

| Field | Meaning |
|---|---|
| `state` | Coarse state that drives in-app UI. See `LicenseState` below. |
| `activatedAt` | First successful activation timestamp. `nil` pre-activation, or in `INVALID`. |
| `hardStopAt` | The license's `exp`. Octet maintains active licenses, so this value advances as the license renews. Do not design around a fixed cutoff. Identical for every device that activates this license. |
| `daysUntilHardStop` | `ceil((hardStopAt - now) / 1 day)`. Useful for "X days left" banners. |
| `tier` | Tier carried in the license token. |
| `upgradeRecommended` | Soft version hint, added in 1.2. `true` when the backend reports this SDK version as behind the recommended floor. `false` until backend version gating is enabled. |
| `minSupportedVersion` | Soft version hint, added in 1.2. The lowest SDK version the backend supports, or `nil`/`null` until gating is enabled. Pair it with `upgradeRecommended` to prompt an in-app update. |

Android: in 3.0, `LicenseStatus` gained three trailing constructor parameters for Octet's internal use, each with a default. Source that calls its constructor or `copy` compiles unchanged, with positional or named arguments. Code compiled against 2.0.0 that calls the constructor or `copy` must be recompiled.

## `LicenseState`

Octet maintains active licenses, so a license in normal use stays in `ACTIVE`. `RENEWAL_RECOMMENDED`, `GRACE_PERIOD`, and `EXPIRED` cover prolonged-offline and revocation handling, not an age-based trial clock.

<Tabs groupId="platform">
  <TabItem value="swift" label="Swift (iOS)">

```swift
public enum LicenseState: String, Sendable {
    case notActivated       = "NOT_ACTIVATED"
    case active             = "ACTIVE"
    case renewalRecommended = "RENEWAL_RECOMMENDED"
    case gracePeriod        = "GRACE_PERIOD"
    case expired            = "EXPIRED"
    case invalid            = "INVALID"
}
```

  </TabItem>
  <TabItem value="kotlin" label="Kotlin (Android)">

```kotlin
enum class LicenseState {
    NOT_ACTIVATED,
    ACTIVE,
    RENEWAL_RECOMMENDED,
    GRACE_PERIOD,
    EXPIRED,
    INVALID,
}
```

  </TabItem>
</Tabs>

| State | Meaning |
|---|---|
| `NOT_ACTIVATED` | License verified, never activated. |
| `ACTIVE` | Activated and current. The normal steady state for a maintained license. |
| `RENEWAL_RECOMMENDED` | Activated, nearing the license `exp`. Maintained licenses renew automatically. Surface a banner only if you want to. |
| `GRACE_PERIOD` | Cached activation past its `exp` but within the offline tolerance. The device could not reach the backend to refresh. SDK keeps working. |
| `EXPIRED` | Cached activation past the offline tolerance with no backend reachable to refresh. SDK refused to start. Age alone no longer expires a maintained license. |
| `INVALID` | Signature failed, server rejected, or malformed. SDK refused to start. |

:::caution

`LicenseState` drives in-app UI only. The activation token enforces the cryptographic gate, so a forged `state = ACTIVE` has no effect on it.

:::

## `LicenseError`

Thrown by `Octet.start(...)` for every license-related failure. The SDK guarantees one of these subtypes. It never throws a raw `Error` / `Exception` for license reasons.

<Tabs groupId="platform">
  <TabItem value="swift" label="Swift (iOS)">

```swift
public enum LicenseError: Error, Sendable, Equatable {
    case malformedKey
    case noActivation
    case expired
    case activationWindowClosed
    case revoked
    case network(message: String)
    case serverRejected(httpStatus: Int, reason: String)
    case upgradeRequired(minVersion: String, message: String)
    case bootstrapFailed(reason: BootstrapReason)
}
```

  </TabItem>
  <TabItem value="kotlin" label="Kotlin (Android)">

```kotlin
sealed class LicenseError(message: String) : Exception(message) {
    object MalformedKey            : LicenseError(...)
    object NoActivation            : LicenseError(...)
    object Expired                 : LicenseError(...)
    object ActivationWindowClosed  : LicenseError(...)
    object Revoked                 : LicenseError(...)
    data class Network(override val cause: Throwable) : LicenseError(...)
    data class ServerRejected(val httpStatus: Int, val reason: String) : LicenseError(...)
    data class UpgradeRequired(val minVersion: String, val message: String) : LicenseError(...)
    data class BootstrapFailed(
        val reason: BootstrapReason,
        val upgradeUrl: String? = null,   // set for NewDevicesBlocked
    ) : LicenseError(...)
}
```

  </TabItem>
</Tabs>

| Case | When | Fix |
|---|---|---|
| `MalformedKey` | Local PASETO signature or structural failure on the license token the SDK obtained through attested bootstrap. | Confirm the SDK is a supported version. Contact support if it persists. |
| `NoActivation` | No cached activation, and offline (the SDK can't reach the license backend to bootstrap). | Retry when network returns. |
| `Expired` | Cached activation past the offline grace with the backend unreachable to refresh. | Reconnect so the SDK can re-activate. |
| `ActivationWindowClosed` | Server returned `403 activation_window_closed`. Not returned under the current maintained-license model. Retained for compatibility. | Contact support. |
| `Revoked` | Server returned `403 revoked` (admin revoke). | Contact support. |
| `Network` | Transient network failure during activation. `message` (iOS) or `cause` (Android) carries the underlying error for diagnostics. | Retry. Do not parse the message for control flow. |
| `ServerRejected` | Any other HTTP rejection (`app_blocked`, `ip_blocked`, ...). | Inspect `reason`. |
| `UpgradeRequired` | Backend rejected this SDK version as out of support (version gating, added in 1.2). | Update the SDK to `minVersion` or later. |
| `BootstrapFailed` | Attested bootstrap failed. `reason` carries a `BootstrapReason`. Added in 2.0. | See the `BootstrapReason` table below. |

## `BootstrapReason`

Carried by `bootstrapFailed`. Each case except `sandboxNotPermitted` maps to a typed `detail` code the license backend returns for `POST /v1/bootstrap`, identically on both platforms. `sandboxNotPermitted` is raised by the SDK before any request is sent. See [Attested bootstrap](/docs/v3.0/concepts/attested-bootstrap/).

| Reason | Meaning |
|---|---|
| `attestationInvalid` | The App Attest object / key-attestation chain did not verify against the platform root. |
| `appNotRegistered` | The app identity is not registered to any account. |
| `appDisputed` | The app identity is contested and not yet resolved. |
| `accountDisabled` | The owning account is disabled. |
| `bootstrapReplay` | The bootstrap challenge was already used, or was not one the server issued. |
| `deviceLimitReached` | The license's device cap, set by Octet, is reached. Licenses have no cap by default. Devices that already activated are unaffected. |
| `newDevicesBlocked` | Added in 3.0. The account accepts no new devices. Devices that already activated are unaffected. The error carries a server-signed upgrade link, `upgradeUrl`. |
| `pendingRateLimit` | A newly registered app is still rate-limited. |
| `seedInvalid` | The runtime seed (web / desktop channel) did not verify. |
| `sandboxTokenInvalid` | The sandbox bypass token did not verify. |
| `sandboxNotPermitted` | A sandbox token was presented from a store build. Raised locally by the SDK, never sent. |

### Handling `newDevicesBlocked`

Send the user to `upgradeUrl`. The link is server-signed, so open it exactly as received and never build one yourself. On iOS the link is the case's associated value, `.newDevicesBlocked(upgradeUrl:)`. On Android it is `LicenseError.BootstrapFailed.upgradeUrl`, set when `reason` is `BootstrapReason.NewDevicesBlocked`. The value is `nil` / `null` only if the server sent no link.

On Android, a `when (reason)` over `BootstrapReason` without an `else` branch no longer compiles against 3.0. Add the new case or an `else` branch.

<Tabs groupId="platform">
  <TabItem value="swift" label="Swift (iOS)">

```swift
do {
    let sdk = try await Octet.start(config: config)
} catch LicenseError.bootstrapFailed(reason: .newDevicesBlocked(let upgradeUrl)) {
    if let link = upgradeUrl, let url = URL(string: link) {
        await UIApplication.shared.open(url)   // open exactly as received
    }
}
```

  </TabItem>
  <TabItem value="kotlin" label="Kotlin (Android)">

```kotlin
try {
    val sdk = Octet.start(context = applicationContext, config = config)
} catch (e: LicenseError.BootstrapFailed) {
    when (e.reason) {
        BootstrapReason.NewDevicesBlocked ->
            e.upgradeUrl?.let { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it))) }
        else -> showStartupError(e)
    }
}
```

  </TabItem>
</Tabs>

## Reading the status at runtime

<Tabs groupId="platform">
  <TabItem value="swift" label="Swift (iOS)">

```swift
guard let status = sdk.licenseStatus else { return }
switch status.state {
case .renewalRecommended:
    break  // maintained licenses renew automatically
case .gracePeriod:
    showBanner("Connect to the internet to refresh the license")
case .expired, .invalid:
    showBlocker()
case .active, .notActivated:
    break  // happy path
}
```

  </TabItem>
  <TabItem value="kotlin" label="Kotlin (Android)">

```kotlin
val status = sdk.licenseStatus ?: return
when (status.state) {
    LicenseState.RENEWAL_RECOMMENDED -> { /* maintained licenses renew automatically */ }
    LicenseState.GRACE_PERIOD ->
        showBanner("Connect to the internet to refresh the license")
    LicenseState.EXPIRED, LicenseState.INVALID ->
        showBlocker()
    LicenseState.ACTIVE, LicenseState.NOT_ACTIVATED -> {
        // happy path
    }
}
```

  </TabItem>
</Tabs>

## See also

- [License & Activation](/docs/v3.0/concepts/license-activation/). The timeline model and activation flow.
- [`Octet.start(...)`](/docs/v3.0/api-reference/octet-start/). The call that throws `LicenseError`.
- [Troubleshooting FAQ](/docs/v3.0/troubleshooting/faq/). What to do when activation fails.
