learning_ai_common_plat/packages/swift-platform-sdk/Sources/BLLicenseClient.swift
saravanakumardb1 77d6ff328f fix(swift-sdk): URL-encode license key + add request tracing to kill switch
BLLicenseClient.checkStatus: percent-encode key before inserting into URL
path to prevent malformed URLs with special characters.

BLKillSwitchClient: add X-Product-Id and X-Request-Id headers for
consistency with BLPlatformClient request tracing pattern.
2026-02-28 22:52:00 -08:00

105 lines
3.6 KiB
Swift

// 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
)
}
}