195 行
6.3 KiB
Swift
195 行
6.3 KiB
Swift
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?
|
||
public private(set) var currentNickname: String?
|
||
public private(set) var currentAvatar: String?
|
||
|
||
private var userSig: String?
|
||
public private(set) var cachedDeviceToken: String?
|
||
private var lastInitializedAppKey: String?
|
||
private var isInitialized = false
|
||
private var initContinuations: [CheckedContinuation<Void, Never>] = []
|
||
|
||
// 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
|
||
}
|
||
|
||
/// 设置用户信息(登录后调用)
|
||
/// 用于灰度发布、精准推送等场景
|
||
/// - Parameters:
|
||
/// - userId: 用户唯一标识
|
||
/// - nickname: 用户昵称(可选)
|
||
/// - avatar: 用户头像 URL(可选)
|
||
public func setUserInfo(userId: String, nickname: String? = nil, avatar: String? = nil) {
|
||
self.currentUserId = userId
|
||
self.currentNickname = nickname
|
||
self.currentAvatar = avatar
|
||
}
|
||
|
||
/// 清除用户信息(登出时调用)
|
||
public func clearUserInfo() {
|
||
self.currentUserId = nil
|
||
self.currentNickname = nil
|
||
self.currentAvatar = 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)
|
||
}
|