import Foundation public struct LicenseFileData: Sendable { public let appKey: String public let appName: String? public let companyName: String? public let packageName: String? public let iosBundleId: String? public let harmonyBundleName: String? public let baseUrl: String? public let serverUrl: String? public let issuedAt: String? public let expiresAt: String? public init( appKey: String, appName: String? = nil, companyName: String? = nil, packageName: String? = nil, iosBundleId: String? = nil, harmonyBundleName: String? = nil, baseUrl: String? = nil, serverUrl: String? = nil, issuedAt: String? = nil, expiresAt: String? = nil ) { self.appKey = appKey self.appName = appName self.companyName = companyName self.packageName = packageName self.iosBundleId = iosBundleId self.harmonyBundleName = harmonyBundleName self.baseUrl = baseUrl self.serverUrl = serverUrl self.issuedAt = issuedAt self.expiresAt = expiresAt } } @MainActor public final class XuqmSDK: NSObject { public static let shared = XuqmSDK() public private(set) var config: SDKConfig? public private(set) var tokenStore: TokenStore? public private(set) var currentUserId: String? private var userSig: String? public private(set) var cachedDeviceToken: String? private var lastInitializedAppKey: String? private var isInitialized = false private var initContinuations: [CheckedContinuation] = [] // Module hooks — set by the convenience XuqmSDK module to wire feature modules public var onInitialize: (@MainActor @Sendable (SDKConfig) -> Void)? public var onLogin: (@MainActor @Sendable (String, String) async throws -> Void)? public var onLogout: (@MainActor @Sendable (String) async throws -> Void)? public var onDeviceToken: (@MainActor @Sendable (String, String) async throws -> Void)? public var licenseReader: (@Sendable () throws -> LicenseFileData?)? private override init() { super.init() } /// Auto-initialize from the embedded license file using the registered licenseReader hook. public func autoInitialize(debug: Bool = false) throws { guard let reader = licenseReader else { throw XuqmSDKError.noLicenseFile } guard let file = try reader() else { throw XuqmSDKError.noLicenseFile } let bundleId = Bundle.main.bundleIdentifier ?? "" if let licenseBundle = file.iosBundleId, !licenseBundle.isEmpty, licenseBundle != bundleId { throw XuqmSDKError.packageMismatch(license: licenseBundle, local: bundleId) } let cfg = SDKConfig(appKey: file.appKey, debug: debug) initialize(config: cfg) } public func initialize(config: SDKConfig) { if let lastInitializedAppKey, lastInitializedAppKey == config.appKey { return } self.config = config self.lastInitializedAppKey = config.appKey self.tokenStore = TokenStore() ApiClient.shared.configure(with: config) isInitialized = true let continuations = initContinuations initContinuations.removeAll() for cont in continuations { cont.resume() } onInitialize?(config) } public func requireConfig() -> SDKConfig { guard let config else { fatalError("XuqmSDK not initialized. Call XuqmSDK.shared.initialize() first.") } return config } public var initialized: Bool { isInitialized } public func awaitInitialization() async { guard !isInitialized else { return } await withCheckedContinuation { cont in initContinuations.append(cont) } } public func login(userId: String, userSig: String) async { if currentUserId == userId && self.userSig == userSig { return } self.currentUserId = userId self.userSig = userSig do { try await onLogin?(userId, userSig) } catch { // Module login failed; silently ignored per facade pattern } if let cachedDeviceToken { do { try await onDeviceToken?(cachedDeviceToken, userId) } catch { // Device token registration failed } } } public func logout() async { if let userId = currentUserId { do { try await onLogout?(userId) } catch { // Module logout failed } } tokenStore?.clear() currentUserId = nil userSig = nil cachedDeviceToken = nil } public func registerDeviceToken(_ deviceToken: Data) { let token = deviceToken.map { String(format: "%02x", $0) }.joined() self.cachedDeviceToken = token Task { @MainActor in if let userId = self.currentUserId { try? await self.onDeviceToken?(token, userId) } } } } public enum XuqmSDKError: Error { case noLicenseFile case packageMismatch(license: String, local: String) }