Skip to main content
Migrate Already selling? Move your customers to Keylight without re-issuing a single key.
Keylight
Blog
swift macos license-keys

How to Add License Keys to a Swift macOS App

3 min read Nicolas Demanez — Founder

This is a practical walk-through of adding license keys to a Swift macOS app. By the end you will have a trial, an activation flow, entitlement checks, and the right UI for each state. The examples use the KeylightSDK, but the shape of the integration applies to any signed-key licensing system.

Step 1: Create the manager at startup

Licensing needs exactly one owner that lives for the whole app lifecycle. In a SwiftUI app, that is your @main type. Create the manager there:

import SwiftUI
import KeylightSDK

@main
struct MyApp: App {
    let licensing = try! Keylight.manager(
        sdkKey: "sdk_live_...",
        tenantId: "acme",
        productId: "myapp",
        keyPrefix: "ACME",
        trustedPublicKeyBase64: "lk_pub_MCowBQYDK2VwAy...",
        trialDurationDays: 14,
        branding: .init(
            appName: "My App",
            purchaseURL: URL(string: "https://acme.example.com/buy")!,
            supportEmail: "[email protected]",
            tintColor: .blue
        )
    )

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(licensing)
        }
    }
}

Two credentials go in the bundle: the SDK key and the trusted public key. Neither is a secret that hurts you if extracted — the public key only verifies signatures, and the SDK key is scoped to your product. Your Stripe secret and the signing private key never leave the server.

Step 2: Resolve the license on launch

When the app starts, you need to know what the customer is entitled to. One call does it:

await licensing.checkOnLaunch()

checkOnLaunch() verifies the cached signed license locally — instantly, with no network — then schedules a background online revalidation if one is due. It does not block your UI on the network. After it runs, licensing.state holds the answer.

Step 3: Branch on the license state

A signed-key SDK collapses licensing into a small set of states. Switch on them once, in a place every view can react to:

switch licensing.state {
case .licensed:
    // Customer has a valid paid license
    enablePaidFeatures()

case .trial(let daysLeft):
    // Inside the evaluation window — show the real product
    enablePaidFeatures()
    showTrialBanner(daysLeft: daysLeft)

case .expired:
    // Trial ran out, or a subscription lapsed
    showRenewalPrompt()

case .invalid:
    // No valid license — needs activation
    showActivationSheet()
}

Note that you do not write a separate trial system. The trial is just state == .trial(daysLeft:), and during it you unlock the same features a paying customer sees — the point of a trial is to show the real app. When it runs out, the state becomes .expired.

Step 4: The activation flow

When a customer has bought a key and needs to enter it — or pastes one into your registration window — call activate(key:):

func activate(_ enteredKey: String) async {
    await licensing.activate(key: enteredKey)

    switch licensing.state {
    case .licensed:
        dismissActivationSheet()
    case .invalid:
        // activationError explains why — bad key, or device limit reached
        showError(licensing.activationError)
    default:
        break
    }
}

activate(key:) verifies the key’s signature, registers this Mac against the key’s device-activation limit, and updates state. If the key is already at its device limit, activation is refused and activationError carries the reason, so you can prompt the customer to free a device or upgrade.

Step 5: Gate the paid features

For a single paid tier — the common case — state == .licensed (or .trial) is the whole gate. Resolve it once and let your views read the result:

var isPaid: Bool {
    switch licensing.state {
    case .licensed, .trial: return true
    case .expired, .invalid: return false
    }
}

Button("Export") { export() }
    .disabled(!isPaid)

If your app has multiple tiers or add-ons, the signed license carries which plan and features the customer bought, so a feature gate is a check against the verified license rather than a network call at the point of use. The exact entitlement-inspection API depends on your licensing SDK — keep the check declarative and close to the feature it guards.

Putting it together

The full integration is small: create the manager once, checkOnLaunch() at startup, switch on state, and activate(key:) for entered keys. The licensing SDK handles the cryptography, the offline verification, the periodic revalidation, and the device bookkeeping; you write the UI.

For the conceptual background on how the signed keys themselves work, see software license keys explained. If you would like this guide extended — subscriptions, multiple products, AppKit instead of SwiftUI — send us your feedback and we will cover it.

Frequently asked

How do I add license keys to a Swift macOS app?+

Add a licensing SDK, create a manager once at app startup with your SDK key and public key, call checkOnLaunch() to resolve the license, and drive your UI from the resulting license state.

Where should the license manager live in a SwiftUI app?+

Create it once on your @main App type so it lives for the whole app lifecycle, and expose it to views — for example as an @Observable object passed through the environment.

Do I need to handle the trial separately from the paid license?+

No. A good SDK models the trial as one of the license states, so the same switch handles trial, licensed, and expired without separate code paths.

Ready to ship?

Create your account and start licensing your apps in under a minute. Free forever tier included.

Start Free