- 为 Android、Flutter、iOS 和 RN SDK 的 Jenkinsfile 添加模块化版本控制 - 引入版本升级策略选择(major/minor/patch)和自定义版本号功能 - 实现多模块独立版本管理和选择性构建发布 - 更新 iOS SDK Package.swift 以支持独立模块化库 - 修改 iOS SDK podspec 文件以适应新的标签命名约定 - 优化 Jenkins 构建流程以支持按需选择特定模块进行构建和发布 - 修复 iOS 测试中的可选类型转换问题以提高代码健壮性
167 行
5.2 KiB
Swift
167 行
5.2 KiB
Swift
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<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)?
|
|
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)
|
|
}
|