Skip to content

Keylight.manager

import SwiftUI
import KeylightSDK
@MainActor
enum Licensing {
static let manager = try! Keylight.manager(
sdkKey: "sdk_live_...", // from your Keylight dashboard
tenantId: "acme", // assigned at signup
productId: "widget", // set in the dashboard
keyPrefix: "ACME", // 4-char prefix from the dashboard
trustedPublicKeyBase64: "...", // shown once in the dashboard
trialDurationDays: 14,
branding: BrandingConfig(
appName: "Widget",
purchaseURL: URL(string: "https://acme.example.com/buy")!,
supportEmail: "support@acme.example.com",
tintColor: .orange
)
)
}

The factory builds the KeylightConfiguration, the KeylightProvider (with the Keylight origin hardcoded), and the @MainActor LicenseManager in one call. The sdkKey authenticates your app against your Keylight account - rotate it from the dashboard if it’s ever compromised.

Keep your Licensing.manager reference internal (the default) or private within your app module. Do not re-export it as public:

// ✅ good - only your app's own code can touch it
@MainActor
enum Licensing {
static let manager = try! Keylight.manager(...)
}
// ❌ bad - anything linking your module can call activate/deactivate
public let sharedLicenseManager = try! Keylight.manager(...)

This matters when your app loads plugins, embeds scripting, or is itself a framework other modules link against. A public manager lets any linked code call manager.activate(key:) with a forged key or swap state to .licensed. For a standalone app with no plugin surface it is mostly theoretical, but the rule is free to follow.

License and trial state is persisted in a device-bound encrypted file by default (storage: .encryptedFile(), SDK 0.8.0+). The blob is sealed with a key derived from the device identity, your tenantId, and productId, and the Keychain is left untouched — so macOS shows no Keychain permission prompt on first launch.

The factory above uses this default, which is the right choice for almost every new app. To opt into a different backend, pass storage: to the factory:

static let manager = try! Keylight.manager(
// …same parameters as above…
storage: .encryptedFile(keychainMirror: true) // file authoritative + Keychain recovery copy
// or: storage: .keychain // legacy: Keychain authoritative, file as fallback
)

It backs the encrypted file with a composite store — the file stays authoritative and the Keychain is a synchronized recovery copy:

  • Writes go to both the file and the Keychain.
  • Reads try the file first; on a miss they fall back to the Keychain, and if the value is found there they heal the file from it (copy it back). The next read hits the file.

That read-fallback-and-heal is what turns it into a one-time migration path, not just a backup.

The .keychain backend keeps the Keychain authoritative (pre-0.8.0 behavior) with the encrypted file as a crash-recovery fallback. Prefer .encryptedFile(keychainMirror: true) for migrations — it keeps the popup-free file as the source of truth while still recovering Keychain state.

@main
struct WidgetApp: App {
@StateObject private var manager = Licensing.manager
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(manager)
.task { await manager.checkOnLaunch() }
.onChange(of: scenePhase) { phase in
if phase == .active {
Task { await manager.refreshIfNeeded() }
}
}
}
}
}
struct ContentView: View {
@EnvironmentObject var manager: LicenseManager
var body: some View {
if manager.isEntitled {
ProFeaturesView()
} else {
UpgradePromptView()
}
}
}

Build your own upgrade/activation UI, or drop in the bundled LicensePromptView(manager:).

Anywhere in your view tree, read manager.isEntitled (which is true for both .trial and .licensed states) to decide whether to expose paid functionality. For state-specific UI (e.g. showing remaining trial days), switch on manager.state directly.

LicenseManager posts Notification.Name.keylightLicenseDidChange after any state transition. Subscribe if you need to invalidate caches or refresh remote data when entitlement changes.

NotificationCenter.default.addObserver(
forName: .keylightLicenseDidChange,
object: nil,
queue: .main
) { _ in
// react to entitlement change
}

Activation and deactivation are driven by LicensePromptView, which calls manager.activate(key:) and manager.deactivate() under the hood. You can also call them directly if you build your own paywall UI.