DOCS LLMs

Swift SDK

Swift SDK

The official Swift SDK for LicenseSeat provides a comprehensive, type-safe API for managing software licenses on Apple platforms.

Installation

Swift Package Manager

Add to your Package.swift:

dependencies: [
    .package(url: "https://github.com/licenseseat/licenseseat-swift.git", from: "0.3.1")
]

Then add the dependency to your target:

.target(
    name: "YourApp",
    dependencies: [
        .product(name: "LicenseSeat", package: "licenseseat-swift")
    ]
)

Xcode

  1. File > Add Package Dependencies
  2. Enter: https://github.com/licenseseat/licenseseat-swift.git
  3. Select your version requirements and add to your target

Quick Start

import LicenseSeat

// 1. Configure at app launch
LicenseSeatStore.shared.configure(
    apiKey: "pk_live_xxxxxxxx",
    productSlug: "your-product"  // Required
)

// 2. Activate a license
let license = try await LicenseSeatStore.shared.activate("USER-LICENSE-KEY")
print("Device ID: \(license.deviceId)")

// 3. Check status
switch LicenseSeatStore.shared.status {
case .active(let details):
    print("Licensed! Device: \(details.device)")
case .inactive:
    print("No license activated")
default:
    break
}

Configuration

Basic Configuration

LicenseSeatStore.shared.configure(
    apiKey: "pk_live_xxxxxxxx",
    productSlug: "your-product"
)

Advanced Configuration

let config = LicenseSeatConfig(
    apiBaseUrl: "https://licenseseat.com/api/v1",  // v1 API endpoint
    apiKey: "pk_live_xxxxxxxx",
    productSlug: "your-product",                   // Required for all operations
    storagePrefix: "myapp_",
    autoValidateInterval: 3600,                    // Re-validate every hour
    maxRetries: 3,
    retryDelay: 1,
    offlineFallbackMode: .networkOnly,             // Offline fallback strategy
    maxOfflineDays: 7,                             // 7-day grace period
    maxClockSkewMs: 300000,                        // 5-minute clock tolerance
    debug: true
)

let licenseSeat = LicenseSeat(config: config)

Or using the builder-style API:

LicenseSeatStore.shared.configure(
    apiKey: "pk_live_xxxxxxxx",
    productSlug: "your-product"
) { config in
    config.autoValidateInterval = 3600
    config.offlineFallbackMode = .networkOnly
    config.maxOfflineDays = 7
    config.debug = true
}

Configuration Options

Option Type Default Description
apiBaseUrl String https://licenseseat.com/api/v1 v1 API endpoint
apiKey String? nil Your publishable API key
productSlug String? nil Required. Product identifier
storagePrefix String licenseseat_ Prefix for cache keys
deviceIdentifier String? Auto-generated Custom device ID
autoValidateInterval TimeInterval 3600 (1 hour) Background validation interval
networkRecheckInterval TimeInterval 30 Offline connectivity check interval
maxRetries Int 3 API retry attempts
retryDelay TimeInterval 1 Base retry delay (exponential backoff)
offlineFallbackMode OfflineFallbackMode .networkOnly Offline fallback strategy
offlineTokenRefreshInterval TimeInterval 259200 (72 hours) Offline token refresh interval
maxOfflineDays Int 0 Grace period for offline use
maxClockSkewMs TimeInterval 300000 (5 min) Clock tamper tolerance
debug Bool false Enable debug logging

Offline Fallback Modes

Mode Description
.networkOnly Falls back to offline validation only for network errors (timeouts, connectivity issues, 5xx). Business logic errors (4xx) immediately invalidate. Recommended.
.always Always attempts offline validation on any failure.

License Lifecycle

Activation

do {
    let license = try await LicenseSeatStore.shared.activate("USER-LICENSE-KEY")
    print("Activated: \(license.licenseKey)")
    print("Device ID: \(license.deviceId)")
    print("Activation ID: \(license.activationId)")
} catch let error as APIError {
    print("Activation failed: \(error.code ?? "unknown") - \(error.message)")
}

With options:

let license = try await LicenseSeatStore.shared.activate(
    "USER-LICENSE-KEY",
    options: ActivationOptions(
        deviceId: "custom-device-id",
        deviceName: "User's MacBook Pro",
        metadata: ["version": "2.0.0", "environment": "production"]
    )
)

Deactivation

try await LicenseSeatStore.shared.deactivate()

Validation

let result = try await LicenseSeatStore.shared.validate(licenseKey: "USER-LICENSE-KEY")

if result.valid {
    print("License is valid")
    print("Plan: \(result.license.planKey)")
    print("Status: \(result.license.status)")

    // Check entitlements from validation
    for entitlement in result.license.activeEntitlements {
        print("Entitlement: \(entitlement.key)")
        if let expiresAt = entitlement.expiresAt {
            print("  Expires: \(expiresAt)")
        }
    }
} else {
    print("Invalid: \(result.code ?? "unknown")")
    print("Message: \(result.message ?? "")")
}

Status Checking

Get Current Status

switch LicenseSeatStore.shared.status {
case .inactive(let message):
    print("No license: \(message)")
    showActivationScreen()

case .pending(let message):
    print("Validating: \(message)")
    showLoadingIndicator()

case .active(let details):
    print("Licensed to: \(details.license)")
    print("Device: \(details.device)")
    enableFeatures()

case .offlineValid(let details):
    print("Valid offline until next sync")
    enableFeatures()
    showOfflineBanner()

case .invalid(let message):
    print("Invalid: \(message)")
    showErrorScreen(message)

case .offlineInvalid(let message):
    print("Expired offline: \(message)")
    showRenewalScreen()
}

Status Types

Status Description
.inactive No license activated
.pending License pending validation
.active License is valid (online)
.offlineValid License is valid (offline check)
.invalid License is invalid
.offlineInvalid License invalid (offline, e.g., expired)

Entitlements

Check feature access based on license entitlements:

let status = LicenseSeatStore.shared.entitlement("premium-features")

switch status.reason {
case nil where status.active:
    enablePremiumFeatures()
case .expired:
    showRenewalPrompt(expiresAt: status.expiresAt)
case .notFound:
    showUpgradePrompt()
case .noLicense:
    showActivationPrompt()
default:
    disablePremiumFeatures()
}

// Access entitlement details
if let entitlement = status.entitlement {
    print("Entitlement key: \(entitlement.key)")
    if let metadata = entitlement.metadata {
        print("Metadata: \(metadata)")
    }
}

Reactive Entitlement Monitoring

LicenseSeatStore.shared.entitlementPublisher(for: "api-access")
    .receive(on: DispatchQueue.main)
    .sink { status in
        apiAccessEnabled = status.active
        if let expiresAt = status.expiresAt {
            scheduleExpirationWarning(at: expiresAt)
        }
    }
    .store(in: &cancellables)

SwiftUI Integration

Property Wrappers

The SDK provides property wrappers for reactive SwiftUI apps:

import SwiftUI
import LicenseSeat

@main
struct MyApp: App {
    init() {
        LicenseSeatStore.shared.configure(
            apiKey: ProcessInfo.processInfo.environment["LICENSESEAT_API_KEY"] ?? "",
            productSlug: "my-app"
        )
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    @LicenseState private var status           // Auto-updates on license changes
    @EntitlementState("pro") private var hasPro // Feature flag

    var body: some View {
        switch status {
        case .active, .offlineValid:
            MainAppView()
                .environment(\.proEnabled, hasPro)
        case .inactive:
            ActivationView()
        case .invalid(let message):
            ErrorView(message: message)
        case .pending:
            ProgressView("Validating...")
        case .offlineInvalid:
            ExpiredView()
        }
    }
}

struct ActivationView: View {
    @State private var licenseKey = ""
    @State private var isLoading = false
    @State private var error: String?

    var body: some View {
        Form {
            TextField("License Key", text: $licenseKey)

            Button("Activate") {
                Task {
                    isLoading = true
                    defer { isLoading = false }
                    do {
                        try await LicenseSeatStore.shared.activate(licenseKey)
                    } catch let apiError as APIError {
                        self.error = apiError.message
                    } catch {
                        self.error = error.localizedDescription
                    }
                }
            }
            .disabled(licenseKey.isEmpty || isLoading)

            if let error {
                Text(error).foregroundColor(.red)
            }
        }
    }
}

UIKit / AppKit Integration

Use Combine publishers for reactive updates:

import LicenseSeat
import Combine

class LicenseManager: ObservableObject {
    @Published var isLicensed = false
    @Published var hasProFeatures = false

    private var cancellables = Set<AnyCancellable>()

    init() {
        LicenseSeatStore.shared.configure(
            apiKey: "pk_live_xxxxxxxx",
            productSlug: "your-product"
        )

        // React to license status changes
        LicenseSeatStore.shared.$status
            .receive(on: DispatchQueue.main)
            .sink { [weak self] status in
                switch status {
                case .active, .offlineValid:
                    self?.isLicensed = true
                default:
                    self?.isLicensed = false
                }
            }
            .store(in: &cancellables)

        // Monitor specific entitlements
        LicenseSeatStore.shared.entitlementPublisher(for: "pro-features")
            .map { $0.active }
            .receive(on: DispatchQueue.main)
            .assign(to: &$hasProFeatures)
    }

    func activate(_ key: String) async throws {
        try await LicenseSeatStore.shared.activate(key)
    }

    func deactivate() async throws {
        try await LicenseSeatStore.shared.deactivate()
    }
}

Event System

Subscribe to SDK events for analytics, UI updates, or custom logic:

// Subscribe with closure (returns AnyCancellable)
let cancellable = licenseSeat.on("activation:success") { data in
    print("License activated!")
    Analytics.track("license_activated")
}

// Unsubscribe by cancelling
cancellable.cancel()

// Or use Combine publishers
licenseSeat.eventPublisher
    .filter { $0.name.hasPrefix("validation:") }
    .sink { event in
        switch event.name {
        case "validation:success":
            updateUI()
        case "validation:offline-success":
            showOfflineBanner()
        case "license:revoked":
            lockFeatures()
        default:
            break
        }
    }
    .store(in: &cancellables)

Available Events

Event Description
activation:start/success/error License activation lifecycle
validation:start/success/failed/error Online validation
validation:offline-success/offline-failed Offline validation
deactivation:start/success/error License deactivation
license:loaded Cached license loaded at startup
license:revoked License revoked by server
offlineToken:verified Offline token signature verified
offlineToken:verificationFailed Offline token verification failed
autovalidation:cycle Auto-validation cycle triggered
network:online/offline Connectivity changes
sdk:reset SDK state cleared

Offline Validation

The SDK provides seamless offline support with Ed25519 cryptographic verification:

LicenseSeatStore.shared.configure(
    apiKey: "pk_live_xxxxxxxx",
    productSlug: "your-product"
) { config in
    config.offlineFallbackMode = .networkOnly     // Network-first fallback mode
    config.maxOfflineDays = 7                     // 7-day grace period
    config.offlineTokenRefreshInterval = 259200   // Refresh every 72 hours
}

How Offline Validation Works

  1. On activation, the SDK fetches a signed offline token from the server
  2. The token contains license data, entitlements, and an Ed25519 signature
  3. When offline, the SDK verifies the signature locally
  4. Clock tamper detection prevents users from bypassing expiration

Security Features

  • Ed25519 Signatures: Offline licenses are cryptographically signed
  • Clock Tamper Detection: Detects system clock manipulation
  • Constant-Time Comparison: Prevents timing attacks on license keys
  • Grace Period: Configurable offline validity period

API Response Format

The v1 API uses Stripe-style conventions with object fields identifying response types.

Activation Response

{
  "object": "activation",
  "id": 12345,
  "device_id": "mac_abc123",
  "device_name": "User's MacBook",
  "license_key": "LICENSE-KEY",
  "activated_at": "2025-01-15T10:30:00Z",
  "license": {
    "object": "license",
    "key": "LICENSE-KEY",
    "status": "active",
    "mode": "hardware_locked",
    "plan_key": "pro",
    "seat_limit": 5,
    "active_seats": 1,
    "active_entitlements": [
      {"key": "premium", "expires_at": null, "metadata": null}
    ],
    "product": {"slug": "my-app", "name": "My App"}
  }
}

Deactivation Response

{
  "object": "deactivation",
  "activation_id": 12345,
  "deactivated_at": "2025-01-15T12:00:00Z"
}

Error Handling

do {
    try await LicenseSeatStore.shared.activate("INVALID-KEY")
} catch let error as APIError {
    print("Error code: \(error.code ?? "unknown")")
    print("Message: \(error.message)")
    print("Details: \(error.details ?? [:])")
}

Common Error Codes

Code Description
license_not_found License key doesn't exist
license_expired License has expired
license_suspended License has been suspended
seat_limit_exceeded No available seats
device_mismatch Device ID doesn't match activation
product_mismatch License not valid for this product

Platform Support

Platform Minimum Version Notes
macOS 12.0+ Full support including hardware UUID
iOS 13.0+ Full support
tvOS 13.0+ Full support
watchOS 8.0+ Core features (no Network.framework)

Testing Authentication

Test that your API key is configured correctly:

do {
    try await LicenseSeatStore.shared.testAuth()
    print("API key is valid")
} catch {
    print("Auth test failed: \(error)")
}

Debug Report

Generate a diagnostic report for support:

let report = LicenseSeatStore.shared.debugReport()
print(report)
// Contains: SDK version, status, license key prefix (redacted), timestamps, etc.

Reset SDK

Clear all cached data and reset to initial state:

LicenseSeatStore.shared.reset()

Next Steps