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
- File > Add Package Dependencies
- Enter:
https://github.com/licenseseat/licenseseat-swift.git - 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
- On activation, the SDK fetches a signed offline token from the server
- The token contains license data, entitlements, and an Ed25519 signature
- When offline, the SDK verifies the signature locally
- 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
- JavaScript SDK - For web applications
- Offline Licensing - Air-gapped validation
- API Reference - Direct API access