import Foundation /// Decrypted content of the init config file (xuqm/config.xuqm). /// Contains the appKey and optional server URL needed to bootstrap the SDK. /// This is separate from XuqmLicenseSDK's LicenseFile (device activation). public struct ConfigFileData: Codable, 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)? private override init() { super.init() } /// Auto-initialize from the embedded config file (xuqm/config.xuqm). /// Reads and decrypts the config file directly — no external hooks needed. public func autoInitialize(debug: Bool = false) throws { guard let file = ConfigFileReader.read() else { throw XuqmSDKError.noConfigFile } let bundleId = Bundle.main.bundleIdentifier ?? "" if let configBundle = file.iosBundleId, !configBundle.isEmpty, configBundle != bundleId { throw XuqmSDKError.packageMismatch(license: configBundle, local: bundleId) } if let baseUrl = file.baseUrl { SDKEndpoints.configure(apiBaseURL: baseUrl) } if let serverUrl = file.serverUrl { SDKEndpoints.configure(imWebSocketURL: serverUrl + "/ws/im") } 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 noConfigFile case noLicenseFile case packageMismatch(license: String, local: String) }