import Foundation import XuqmCoreSDK public final class LicenseSDK: @unchecked Sendable { public static let shared = LicenseSDK() private static let defaultBaseUrl = "https://auth.dev.xuqinmin.com/" private static let statusOk = "ok" private static let statusDenied = "denied" private static let cacheWindowMs: Double = 10 * 60 * 1000 private var appKey: String? private var baseUrl: String = defaultBaseUrl private var deviceName: String? private let store = LicenseStore() private var initialized = false private init() {} /// Manual initialization. Most apps only need to place the license file at /// assets/xuqm/license.xuqm (bundle) and call checkLicense() — auto-init handles the rest. public func initialize(appKey: String, baseUrl: String? = nil, deviceName: String? = nil) { let url = normalize(baseUrl ?? Self.defaultBaseUrl) if store.appKey != nil && store.appKey != appKey { store.clear() } self.appKey = appKey self.baseUrl = url self.deviceName = deviceName ?? DeviceInfoProvider.getDeviceName() store.appKey = appKey initialized = true } /// Check license validity. Returns cached result within 10-minute window. /// On network error, falls back to last cached OK status. public func checkLicense(userInfo: LicenseUserInfo? = nil) async -> LicenseResult { if !initialized { await XuqmSDK.shared.awaitInitialization() tryAutoInitialize() } guard initialized, let appKey else { return .error("LicenseSDK not initialized") } let cachedStatus = store.status let cachedTime = store.statusTime if cachedStatus == Self.statusOk && (Date().timeIntervalSince1970 * 1000 - cachedTime) < Self.cacheWindowMs { return .success("Cached") } let deviceId = DeviceInfoProvider.getDeviceId(store: store) let payload = userInfo.map { UserInfoPayload(userId: $0.userId, name: $0.name, email: $0.email, phone: $0.phone) } do { if let token = store.token { let req = VerifyRequest(appKey: appKey, deviceId: deviceId, token: token, userInfo: payload) if let resp = try? await post(VerifyResponse.self, path: "api/license/verify", body: req), resp.valid { persistStatus(Self.statusOk) return .success("Verified") } store.token = nil } let req = RegisterRequest( appKey: appKey, deviceId: deviceId, deviceName: deviceName ?? DeviceInfoProvider.getDeviceName(), deviceModel: DeviceInfoProvider.getDeviceModel(), deviceVendor: DeviceInfoProvider.getDeviceVendor(), osVersion: DeviceInfoProvider.getOsVersion(), userInfo: payload ) let resp = try await post(RegisterResponse.self, path: "api/license/register", body: req) if resp.success, let token = resp.token { store.token = token persistStatus(Self.statusOk) return .success("Registered") } persistStatus(Self.statusDenied) return .error(resp.message ?? "Registration denied") } catch { if cachedStatus == Self.statusOk { return .success("Offline - cached ok") } return .error(error.localizedDescription) } } public func getStatus() async -> LicenseStatus { if !initialized { await XuqmSDK.shared.awaitInitialization() tryAutoInitialize() } switch store.status { case Self.statusOk: return .ok case Self.statusDenied: return .denied default: return .unknown } } public func getDeviceId() async -> String? { if !initialized { await XuqmSDK.shared.awaitInitialization() tryAutoInitialize() } if !initialized { return nil } return store.deviceId } public func clear() { store.clear() } @discardableResult private func tryAutoInitialize() -> Bool { guard let file = LicenseFileReader.read(), !file.appKey.isEmpty else { return false } initialize(appKey: file.appKey, baseUrl: file.baseUrl) return true } private func persistStatus(_ status: String) { store.status = status store.statusTime = Date().timeIntervalSince1970 * 1000 } private func normalize(_ url: String) -> String { let s = url.trimmingCharacters(in: .whitespaces) return s.hasSuffix("/") ? s : s + "/" } private func post(_ type: T.Type, path: String, body: B) async throws -> T { guard let url = URL(string: baseUrl + path) else { throw URLError(.badURL) } var req = URLRequest(url: url) req.httpMethod = "POST" req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.httpBody = try JSONEncoder().encode(body) let (data, response) = try await URLSession.shared.data(for: req) guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else { throw URLError(.badServerResponse) } guard let wrapper = try? JSONDecoder().decode(LicenseResponseWrapper.self, from: data), let result = wrapper.data else { throw URLError(.cannotDecodeContentData) } return result } } private struct LicenseResponseWrapper: Decodable { let data: U? }