// ── License Client ────────────────────────────────────────── // Generic license key activation via platform-service. // Flow: enter key → POST /api/licenses/activate → receive tokens. // Product apps configure with BLPlatformConfig. import Foundation /// License information returned from platform-service. public struct BLLicenseInfo: Codable, Sendable { public let key: String public let plan: String public let status: String public let devicesUsed: Int public let maxDevices: Int public let expiresAt: String? public init(key: String, plan: String, status: String, devicesUsed: Int, maxDevices: Int, expiresAt: String?) { self.key = key self.plan = plan self.status = status self.devicesUsed = devicesUsed self.maxDevices = maxDevices self.expiresAt = expiresAt } } /// Activation result containing tokens + license info. public struct BLActivationResult: Sendable { public let accessToken: String public let refreshToken: String public let license: BLLicenseInfo } /// Generic license client for all ByteLyst iOS apps. /// Handles license key activation and status checking via platform-service. public final class BLLicenseClient { private let config: BLPlatformConfig private let client: BLPlatformClient public init(config: BLPlatformConfig, client: BLPlatformClient) { self.config = config self.client = client } // MARK: - Activate /// Activate a license key on this device. /// Returns activation result with tokens and license info. public func activate(key: String, deviceId: String, deviceName: String) async throws -> BLActivationResult { let body: [String: String] = [ "key": key.uppercased().trimmingCharacters(in: .whitespaces), "deviceId": deviceId, "deviceName": deviceName, "platform": config.platform, ] let (data, _) = try await client.rawRequest(path: "/api/licenses/activate", method: "POST", body: body) struct ActivateResponse: Codable { let accessToken: String let refreshToken: String let license: LicenseDoc } struct LicenseDoc: Codable { let key: String let plan: String let status: String let deviceIds: [String] let maxDevices: Int let expiresAt: String? let userId: String } let result = try JSONDecoder().decode(ActivateResponse.self, from: data) let info = BLLicenseInfo( key: result.license.key, plan: result.license.plan, status: result.license.status, devicesUsed: result.license.deviceIds.count, maxDevices: result.license.maxDevices, expiresAt: result.license.expiresAt ) return BLActivationResult( accessToken: result.accessToken, refreshToken: result.refreshToken, license: info ) } // MARK: - Status /// Check license status without activating. public func checkStatus(key: String) async throws -> BLLicenseInfo { let trimmedKey = key.uppercased().trimmingCharacters(in: .whitespaces) let encodedKey = trimmedKey.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? trimmedKey return try await client.request( path: "/api/licenses/status/\(encodedKey)", responseType: BLLicenseInfo.self ) } }