ExisOne Swift SDK

Native Swift package for integrating software licensing into macOS and iOS applications. Uses IOKit for hardware fingerprinting, Security.framework for offline RSA validation, and Swift concurrency (async/await) for all network operations.

Package: ExisOne · Version: 0.7.0 · Platforms: macOS 12+, iOS 15+ · Swift: 5.9+
View on GitHub →

Install

Add the package in Xcode:

  1. File → Add Package Dependencies...
  2. Enter the repository URL:
    https://github.com/exisllc/ExisOne.Swift
  3. Select version 0.7.0 (or "Up to Next Major")
  4. Add ExisOne to your target

Or add it directly to your Package.swift:

dependencies: [
    .package(url: "https://github.com/exisllc/ExisOne.Swift", from: "0.7.0")
]

// In your target:
.target(name: "MyApp", dependencies: [
    .product(name: "ExisOne", package: "ExisOne.Swift")
])

Initialize

import ExisOne

// Simple initialization
let client = try ExisOneClient(
    baseURL: "https://www.exisone.com",
    accessToken: "exo_at_<public>_<secret>"
)

// Full options
let client = try ExisOneClient(options: ExisOneClientOptions(
    baseURL: "https://www.exisone.com",
    accessToken: "exo_at_<public>_<secret>",
    offlinePublicKey: "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
    allowedBaseURLHosts: ["www.exisone.com"],
    timeout: 30
))

// SDK version
print(ExisOneClient.version)  // "0.7.0"

baseURL can also be set via the EXISONE_BASEURL environment variable.

For offline license validation, set offlinePublicKey to your tenant's RSA public key (PEM format). Obtain this from the Crypto Keys page.

Capabilities

Quick Start

import ExisOne

let client = try ExisOneClient(
    baseURL: "https://www.exisone.com",
    accessToken: "exo_at_xxx_yyy"
)

// 1) Generate a hardware fingerprint (store locally)
let hwid = ExisOneHardwareId.generate()

// 2) Activate a license (user enters key & email)
let activation = await client.activate(
    activationKey: "XXXX-XXXX-XXXX-XXXX",
    email: "user@example.com",
    hardwareId: hwid,
    productName: "MyMacApp",
    version: "1.0.0"
)

if !activation.success {
    if activation.errorCode == "version_outdated" {
        print("Please update to \(activation.minimumRequiredVersion ?? "latest")")
    } else {
        print(activation.errorMessage ?? "Activation failed")
    }
}

// 3) Validate on app launch
let result = try await client.validate(
    hardwareId: hwid,
    productName: "MyMacApp",
    activationKey: "XXXX-XXXX-XXXX-XXXX",
    version: "1.0.0"
)

if result.isValid {
    print("Licensed until: \(result.expirationDate ?? Date())")
    print("Features: \(result.features)")

    if result.features.contains("Pro") {
        enableProFeatures()
    }
} else {
    print("Status: \(result.status)")
    showActivationDialog()
}

// 4) Deactivate (same hardware required)
try await client.deactivate(
    activationKey: "XXXX-XXXX-XXXX-XXXX",
    hardwareId: hwid,
    productName: "MyMacApp"
)

// 5) Generate a key (publisher tooling, requires generate permission)
let newKey = try await client.generateActivationKey(
    productName: "MyMacApp",
    email: "customer@example.com",
    planId: 1,
    validityDays: 365
)

// 6) Submit a support ticket
try await client.sendSupportTicket(
    productName: "MyMacApp",
    email: "user@example.com",
    subject: "Help with activation",
    message: "I'm having trouble activating..."
)

Hardware ID

The hardware fingerprint is generated using native macOS APIs with no subprocess calls:

let hwid = ExisOneHardwareId.generate()
// "D7391D68656651079189364C9BB7D4642C97B5B05D10B44FD4CA5B8D4723A682"

The result is a stable 64-character uppercase hex string. The same Mac always produces the same ID.

Offline License Validation

For air-gapped environments, generate offline activation codes from the License Keys page using the customer's Hardware ID.

Setup

let client = try ExisOneClient(options: ExisOneClientOptions(
    baseURL: "https://www.exisone.com",
    accessToken: "your-token",
    offlinePublicKey: """
    -----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
    -----END PUBLIC KEY-----
    """
))

Smart Validation (Recommended)

Auto-detects online vs offline keys. Falls back to offline if the server is unreachable.

let hwid = ExisOneHardwareId.generate()

// Works with both online keys (XXXX-XXXX-XXXX-XXXX) and offline codes
let result = await client.validateSmart(
    keyOrCode: licenseKeyOrOfflineCode,
    hardwareId: hwid,
    productName: "MyMacApp"
)

if result.isValid {
    print("Licensed until: \(result.expirationDate ?? Date())")
    print("Offline mode: \(result.wasOffline)")
    print("Features: \(result.features)")
} else {
    print("Invalid: \(result.errorMessage ?? result.status)")
}

Direct Offline Validation

// Validate completely offline - no server calls
let result = client.validateOffline(
    offlineCode: offlineCode,
    hardwareId: hwid
)

if result.isValid {
    print("Product: \(result.productName ?? "")")
    print("Expires: \(result.expirationDate ?? Date())")
    print("Features: \(result.features)")
} else if result.isExpired {
    print("License expired")
} else if result.hardwareMismatch {
    print("Wrong machine - license bound to different hardware")
} else {
    print("Invalid: \(result.errorMessage ?? "Unknown error")")
}

SwiftUI Integration

Use the SDK in SwiftUI views with the .task modifier:

import SwiftUI
import ExisOne

struct ContentView: View {
    @State private var licenseStatus = "Checking..."
    @State private var features: [String] = []

    var body: some View {
        VStack {
            Text(licenseStatus)
                .font(.headline)

            if features.contains("Pro") {
                ProFeaturesView()
            }
        }
        .task {
            await checkLicense()
        }
    }

    func checkLicense() async {
        guard let client = try? ExisOneClient(
            baseURL: "https://www.exisone.com",
            accessToken: "exo_at_xxx_yyy"
        ) else { return }

        let hwid = ExisOneHardwareId.generate()
        let savedKey = UserDefaults.standard.string(forKey: "licenseKey") ?? ""

        let result = await client.validateSmart(
            keyOrCode: savedKey,
            hardwareId: hwid,
            productName: "MyMacApp"
        )

        if result.isValid {
            licenseStatus = "Licensed"
            features = result.features
        } else {
            licenseStatus = "Not licensed: \(result.status)"
        }
    }
}

macOS App Example

Typical integration pattern for a macOS application:

import ExisOne

class LicenseManager {
    static let shared = LicenseManager()

    private let client: ExisOneClient
    private let hwid: String

    private init() {
        client = try! ExisOneClient(
            baseURL: "https://www.exisone.com",
            accessToken: "exo_at_xxx_yyy"
        )
        hwid = ExisOneHardwareId.generate()
    }

    /// Check license on app launch.
    func validateOnLaunch() async -> ValidationResult {
        let key = storedActivationKey()
        return try! await client.validate(
            hardwareId: hwid,
            productName: "MyMacApp",
            activationKey: key,
            version: Bundle.main.appVersion
        )
    }

    /// Activate with a new key.
    func activate(key: String, email: String) async -> ActivationResult {
        await client.activate(
            activationKey: key,
            email: email,
            hardwareId: hwid,
            productName: "MyMacApp",
            version: Bundle.main.appVersion
        )
    }

    private func storedActivationKey() -> String {
        UserDefaults.standard.string(forKey: "activationKey") ?? ""
    }
}

extension Bundle {
    var appVersion: String {
        infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
    }
}

Error Handling

import ExisOne

do {
    let result = try await client.validate(
        hardwareId: hwid,
        productName: "MyMacApp",
        activationKey: key
    )
} catch let error as ExisOneError {
    switch error {
    case .httpError(let code, let message):
        print("Server error \(code): \(message)")
    case .networkError(let message):
        print("Network issue: \(message)")
    case .insecureBaseURL:
        print("HTTPS required")
    default:
        print(error.localizedDescription)
    }
}

API Reference

// Version
static let version: String  // "0.7.0"

// Hardware
static func generateHardwareId() -> String

// Activation
func activate(
    activationKey: String,
    email: String,
    hardwareId: String,
    productName: String,
    version: String? = nil
) async -> ActivationResult

// Validation (Online)
func validate(
    hardwareId: String,
    productName: String? = nil,
    activationKey: String? = nil,
    version: String? = nil
) async throws -> ValidationResult

// Validation (Offline)
func validateOffline(
    offlineCode: String,
    hardwareId: String
) -> OfflineValidationResult

func validateSmart(
    keyOrCode: String,
    hardwareId: String,
    productName: String? = nil
) async -> SmartValidationResult

// Deactivation
func deactivate(
    activationKey: String,
    hardwareId: String,
    productName: String
) async throws -> Bool

func deactivateSmart(
    keyOrCode: String,
    hardwareId: String,
    productName: String
) async -> DeactivationResult

// Generation (publisher)
func generateActivationKey(
    productName: String,
    email: String,
    planId: Int? = nil,
    validityDays: Int? = nil
) async throws -> String

// Features
func getLicensedFeatures(
    activationKey: String
) async throws -> [String]

// Support
func sendSupportTicket(
    productName: String,
    email: String,
    subject: String,
    message: String
) async throws

Result Types

// All types conform to Sendable for safe use with Swift concurrency

struct ActivationResult {
    let success: Bool
    let errorCode: String?
    let errorMessage: String?
    let serverVersion: String?
    let minimumRequiredVersion: String?
    let licenseData: String?
}

struct ValidationResult {
    let isValid: Bool
    let status: String            // "licensed", "trial", "expired", "invalid", "version_outdated"
    let expirationDate: Date?
    let features: [String]
    let serverVersion: String?
    let minimumRequiredVersion: String?
}

struct OfflineValidationResult {
    let isValid: Bool
    let errorMessage: String?
    let productName: String?
    let productId: Int
    let expirationDate: Date?
    let email: String?
    let features: [String]
    let version: String?
    let isExpired: Bool
    let hardwareMismatch: Bool
}

struct SmartValidationResult {
    let isValid: Bool
    let status: String
    let expirationDate: Date?
    let features: [String]
    let wasOffline: Bool
    let errorMessage: String?
    let productName: String?
    let serverVersion: String?
    let minimumRequiredVersion: String?
}

struct DeactivationResult {
    let success: Bool
    let serverNotified: Bool
    let errorMessage: String?
}

Environment Variables

VariableDescription
EXISONE_BASEURLDefault base URL if not specified in options

Requirements

Common Errors

Changelog

See Also