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.
105 lines
3.6 KiB
Swift
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
|
|
)
|
|
}
|
|
}
|