2026-04-21 22:07:29 +08:00
|
|
|
import Foundation
|
|
|
|
|
|
|
|
|
|
@MainActor
|
2026-05-01 21:27:39 +08:00
|
|
|
public final class XuqmSDK: NSObject {
|
2026-04-21 22:07:29 +08:00
|
|
|
|
|
|
|
|
public static let shared = XuqmSDK()
|
|
|
|
|
private(set) var config: SDKConfig?
|
|
|
|
|
private(set) var tokenStore: TokenStore?
|
|
|
|
|
|
2026-05-01 21:27:39 +08:00
|
|
|
public private(set) var currentUserId: String?
|
|
|
|
|
|
|
|
|
|
private var userSig: String?
|
|
|
|
|
private var cachedDeviceToken: String?
|
2026-05-07 19:39:48 +08:00
|
|
|
private var lastInitializedAppKey: String?
|
2026-05-22 17:56:52 +08:00
|
|
|
private var isInitialized = false
|
|
|
|
|
private var initContinuations: [CheckedContinuation<Void, Never>] = []
|
2026-05-01 21:27:39 +08:00
|
|
|
|
|
|
|
|
private override init() {
|
|
|
|
|
super.init()
|
|
|
|
|
}
|
2026-04-21 22:07:29 +08:00
|
|
|
|
2026-05-22 17:56:52 +08:00
|
|
|
/// 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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 22:07:29 +08:00
|
|
|
public func initialize(config: SDKConfig) {
|
2026-05-07 19:39:48 +08:00
|
|
|
if let lastInitializedAppKey, lastInitializedAppKey == config.appKey {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-21 22:07:29 +08:00
|
|
|
self.config = config
|
2026-05-07 19:39:48 +08:00
|
|
|
self.lastInitializedAppKey = config.appKey
|
2026-04-21 22:07:29 +08:00
|
|
|
self.tokenStore = TokenStore()
|
|
|
|
|
ApiClient.shared.configure(with: config)
|
2026-05-22 17:56:52 +08:00
|
|
|
isInitialized = true
|
|
|
|
|
// Resume any waiting continuations
|
|
|
|
|
let continuations = initContinuations
|
|
|
|
|
initContinuations.removeAll()
|
|
|
|
|
for cont in continuations {
|
|
|
|
|
cont.resume()
|
|
|
|
|
}
|
2026-05-05 17:54:59 +08:00
|
|
|
if config.autoRegisterPush {
|
|
|
|
|
Task { @MainActor in
|
|
|
|
|
try? await PushSDK.shared.start()
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-21 22:07:29 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func requireConfig() -> SDKConfig {
|
|
|
|
|
guard let config else {
|
|
|
|
|
fatalError("XuqmSDK not initialized. Call XuqmSDK.shared.initialize() first.")
|
|
|
|
|
}
|
|
|
|
|
return config
|
|
|
|
|
}
|
2026-05-01 21:27:39 +08:00
|
|
|
|
2026-05-22 17:56:52 +08:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 21:27:39 +08:00
|
|
|
public func login(userId: String, userSig: String) async {
|
2026-05-07 19:39:48 +08:00
|
|
|
if currentUserId == userId && self.userSig == userSig {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-05-01 21:27:39 +08:00
|
|
|
self.currentUserId = userId
|
|
|
|
|
self.userSig = userSig
|
|
|
|
|
|
|
|
|
|
do {
|
2026-05-02 11:29:50 +08:00
|
|
|
try await ImSDK.shared.login(userId, userSig)
|
2026-05-01 21:27:39 +08:00
|
|
|
} 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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-22 17:56:52 +08:00
|
|
|
}
|
2026-05-01 21:27:39 +08:00
|
|
|
|
2026-05-22 17:56:52 +08:00
|
|
|
public enum XuqmSDKError: Error {
|
|
|
|
|
case noLicenseFile
|
|
|
|
|
case packageMismatch(license: String, local: String)
|
2026-04-21 22:07:29 +08:00
|
|
|
}
|