import Foundation @MainActor public final class XuqmSDK: NSObject { public static let shared = XuqmSDK() private(set) var config: SDKConfig? private(set) var tokenStore: TokenStore? public private(set) var currentUserId: String? private var userSig: String? private var cachedDeviceToken: String? private var lastInitializedAppKey: String? private var isInitialized = false private var initContinuations: [CheckedContinuation] = [] private override init() { super.init() } /// Auto-initialize from the embedded license file (Bundle.main/xuqm/license.xuqm). /// Validates the bundle ID / package name against the license file. public func autoInitialize(debug: Bool = false) throws { guard let file = LicenseFileReader.read() 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 // Resume any waiting continuations let continuations = initContinuations initContinuations.removeAll() for cont in continuations { cont.resume() } if config.autoRegisterPush { Task { @MainActor in try? await PushSDK.shared.start() } } } public func requireConfig() -> SDKConfig { guard let config else { fatalError("XuqmSDK not initialized. Call XuqmSDK.shared.initialize() first.") } return config } public var initialized: Bool { isInitialized } /// Wait for initialization to complete. 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 ImSDK.shared.login(userId, userSig) } catch { // IM login failed; silently ignored per facade pattern } if let cachedDeviceToken { do { try await PushSDK.shared.registerToken(cachedDeviceToken, userId: userId) } catch { // Push registration failed } } } public func logout() async { ImSDK.shared.disconnect() if let userId = currentUserId { do { try await PushSDK.shared.unregisterToken(userId: userId) } catch { // Push unregistration 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 PushSDK.shared.registerToken(token, userId: userId) } } } } public enum XuqmSDKError: Error { case noLicenseFile case packageMismatch(license: String, local: String) }