diff --git a/Jenkinsfile b/Jenkinsfile index 2832c19..3c9d57b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -2,15 +2,34 @@ pipeline { agent any parameters { - string(name: 'VERSION', defaultValue: '', description: '发布版本号(留空自动递增)') + // ── 版本升级策略 ───────────────────────────────────────────────────── + choice(name: 'VERSION_BUMP', defaultValue: 'patch', + description: '版本升级策略: major(1.0.0→2.0.0), minor(1.0.0→1.1.0), patch(1.0.0→1.0.1)') + booleanParam(name: 'CUSTOM_VERSION', defaultValue: false, + description: '勾选后使用下方自定义版本号(忽略VERSION_BUMP)') + + // ── 模块自定义版本号(仅CUSTOM_VERSION=true时生效)─────────────────── + string(name: 'CORE_VERSION', defaultValue: '', description: 'XuqmCoreSDK 自定义版本号(仅CUSTOM_VERSION=true时生效)') + string(name: 'IM_VERSION', defaultValue: '', description: 'XuqmImSDK 自定义版本号') + string(name: 'PUSH_VERSION', defaultValue: '', description: 'XuqmPushSDK 自定义版本号') + string(name: 'UPDATE_VERSION', defaultValue: '', description: 'XuqmUpdateSDK 自定义版本号') + string(name: 'LICENSE_VERSION', defaultValue: '', description: 'XuqmLicenseSDK 自定义版本号') + string(name: 'FILE_VERSION', defaultValue: '', description: 'XuqmFileSDK 自定义版本号') + string(name: 'WEBVIEW_VERSION', defaultValue: '', description: 'XuqmWebViewSDK 自定义版本号') + + // ── 模块选择 ──────────────────────────────────────────────────────── + text(name: 'MODULES', defaultValue: 'core\nim\npush\nupdate\nlicense\nfile\nwebview', + description: '要发布的模块,每行一个。可选: core im push update license file webview') + + // ── 构建选项 ──────────────────────────────────────────────────────── booleanParam(name: 'RUN_TESTS', defaultValue: true, description: '是否运行单元测试') booleanParam(name: 'PUBLISH_SPM', defaultValue: true, description: '发布 SPM 版本(打 Git Tag)') booleanParam(name: 'PUBLISH_PODS', defaultValue: false, description: '发布到 CocoaPods 私有 Spec Repo') } environment { - GIT_URL = 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK.git' - SPEC_REPO = 'https://xuqinmin.com/xuqinmin12/xuqm-specs.git' + GIT_URL = 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK.git' + SPEC_REPO = 'https://xuqinmin.com/xuqinmin12/xuqm-specs.git' } options { @@ -23,18 +42,98 @@ pipeline { stage('Checkout') { steps { checkout([ - $class: 'GitSCM', - branches: [[name: 'main']], - extensions: [[$class: 'CleanBeforeCheckout']], - userRemoteConfigs: scm.userRemoteConfigs + $class: 'GitSCM', + branches: [[name: 'main']], + extensions: [[$class: 'CleanBeforeCheckout']], + userRemoteConfigs: scm.userRemoteConfigs ]) script { env.GIT_COMMIT_SHORT = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() - if (params.VERSION?.trim()) { - env.PUBLISH_VERSION = params.VERSION.trim() - sh "sed -i '' 's/s.version.*= .*/s.version = \"${env.PUBLISH_VERSION}\"/' XuqmSDK.podspec" - } else { - env.PUBLISH_VERSION = sh(script: 'grep "s.version" XuqmSDK.podspec | head -1 | sed "s/.*= *\\"\\(.*\\)\\".*/\\1/"', returnStdout: true).trim() + + // ── 解析选中的模块 ────────────────────────────────────── + def allValid = ['core', 'im', 'push', 'update', 'license', 'file', 'webview'] + def requested = params.MODULES + .tokenize('\n, ') + .collect { it.trim().toLowerCase() } + .findAll { it } + def invalid = requested - allValid + if (invalid) { + error "Unknown module(s): ${invalid}. Valid values: ${allValid}" + } + env.SELECTED_MODULES = requested.join(',') + echo "Selected modules: ${env.SELECTED_MODULES}" + + // ── 模块 → podspec/tag 映射 ──────────────────────────── + def moduleMap = [ + 'core': [podspec: 'XuqmCoreSDK', tagPrefix: 'core'], + 'im': [podspec: 'XuqmImSDK', tagPrefix: 'im'], + 'push': [podspec: 'XuqmPushSDK', tagPrefix: 'push'], + 'update': [podspec: 'XuqmUpdateSDK', tagPrefix: 'update'], + 'license': [podspec: 'XuqmLicenseSDK', tagPrefix: 'license'], + 'file': [podspec: 'XuqmFileSDK', tagPrefix: 'file'], + 'webview': [podspec: 'XuqmWebViewSDK', tagPrefix: 'webview'], + ] + env.MODULE_MAP = groovy.json.JsonOutput.toJson(moduleMap) + } + } + } + + stage('Resolve Versions') { + steps { + script { + def moduleMap = new groovy.json.JsonSlurper().parseText(env.MODULE_MAP) + def modules = env.SELECTED_MODULES.split(',') + def customVersions = [ + 'core': params.CORE_VERSION?.trim(), + 'im': params.IM_VERSION?.trim(), + 'push': params.PUSH_VERSION?.trim(), + 'update': params.UPDATE_VERSION?.trim(), + 'license': params.LICENSE_VERSION?.trim(), + 'file': params.FILE_VERSION?.trim(), + 'webview': params.WEBVIEW_VERSION?.trim(), + ] + + for (mod in modules) { + def podspecFile = "${moduleMap[mod].podspec}.podspec" + def currentVer = sh( + script: "grep \"s.version\" ${podspecFile} | head -1 | sed \"s/.*= *\\\"\\(.*\\)\\\".*/\\1/\"", + returnStdout: true + ).trim() + + def newVer = '' + if (params.CUSTOM_VERSION && customVersions[mod]) { + // 使用自定义版本号 + newVer = customVersions[mod] + } else if (!params.CUSTOM_VERSION) { + // 根据 VERSION_BUMP 策略自动升级版本号 + def parts = currentVer.tokenize('.') + while (parts.size() < 3) { parts.add('0') } + switch (params.VERSION_BUMP) { + case 'major': + parts[0] = (parts[0].toInteger() + 1).toString() + parts[1] = '0' + parts[2] = '0' + break + case 'minor': + parts[1] = (parts[1].toInteger() + 1).toString() + parts[2] = '0' + break + case 'patch': + default: + parts[2] = (parts[2].toInteger() + 1).toString() + break + } + newVer = parts.join('.') + } + + if (newVer && newVer != currentVer) { + echo "Bumping ${moduleMap[mod].podspec}: ${currentVer} → ${newVer}" + sh "sed -i '' 's/s.version.*= .*/s.version = \"${newVer}\"/' ${podspecFile}" + env["VERSION_${mod}"] = newVer + } else { + echo "Keeping ${moduleMap[mod].podspec} at ${currentVer}" + env["VERSION_${mod}"] = currentVer + } } } } @@ -43,10 +142,15 @@ pipeline { stage('Build Info') { steps { script { - echo "🚀 构建分支: ${params.BRANCH}" - echo "🏷️ 发布版本: ${env.PUBLISH_VERSION}" - echo "🔨 Git Commit: ${env.GIT_COMMIT_SHORT}" - echo "⚠️ 注意:此任务应在 Jenkins2 (https://xuqinmin12.eicp.vip/) 上执行" + def modules = env.SELECTED_MODULES.split(',') + def moduleMap = new groovy.json.JsonSlurper().parseText(env.MODULE_MAP) + echo "Git Commit : ${env.GIT_COMMIT_SHORT}" + echo "Bump Strategy: ${params.CUSTOM_VERSION ? 'Custom' : params.VERSION_BUMP}" + echo "Modules (${modules.size()}):" + for (mod in modules) { + def ver = env["VERSION_${mod}"] + echo " ${mod.padRight(10)} → ${moduleMap[mod].podspec} v${ver}" + } } } } @@ -72,37 +176,76 @@ pipeline { stage('Pod Spec Lint') { when { expression { return params.PUBLISH_PODS } } steps { - sh 'pod spec lint XuqmSDK.podspec --allow-warnings' + script { + def moduleMap = new groovy.json.JsonSlurper().parseText(env.MODULE_MAP) + def modules = env.SELECTED_MODULES.split(',') + // 按依赖顺序 lint:先 core,再其他模块 + def ordered = ['core'] + modules.findAll { it != 'core' } + for (mod in ordered) { + def podspec = "${moduleMap[mod].podspec}.podspec" + if (fileExists(podspec)) { + echo "Linting ${podspec}..." + sh "pod spec lint ${podspec} --allow-warnings" + } + } + } } } - stage('Publish SPM (Git Tag)') { - when { - expression { return params.PUBLISH_SPM } - } + stage('Publish SPM (Git Tags)') { + when { expression { return params.PUBLISH_SPM } } steps { withCredentials([usernamePassword(credentialsId: 'gitlab-credentials', passwordVariable: 'GIT_PASS', usernameVariable: 'GIT_USER')]) { - sh """ - git config user.email "jenkins@xuqm.com" - git config user.name "Jenkins CI" - git add XuqmSDK.podspec || true - git diff --cached --quiet || git commit -m "ci: bump version to ${env.PUBLISH_VERSION}" - git tag -f "${env.PUBLISH_VERSION}" - git push https://${GIT_USER}:${GIT_PASS}@xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK.git HEAD:main --tags - """ + script { + def moduleMap = new groovy.json.JsonSlurper().parseText(env.MODULE_MAP) + def modules = env.SELECTED_MODULES.split(',') + def tagsToPush = [] + + for (mod in modules) { + def ver = env["VERSION_${mod}"] + if (ver) { + def tag = "${moduleMap[mod].tagPrefix}/${ver}" + tagsToPush.push(tag) + } + } + + if (tagsToPush) { + sh """ + git config user.email "jenkins@xuqm.com" + git config user.name "Jenkins CI" + git add *.podspec || true + git diff --cached --quiet || git commit -m "ci: bump versions [${env.SELECTED_MODULES}]" + """ + for (tag in tagsToPush) { + sh "git tag -f '${tag}'" + } + sh """ + git push https://${GIT_USER}:${GIT_PASS}@xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK.git HEAD:main --tags -f + """ + echo "Published tags: ${tagsToPush.join(', ')}" + } + } } } } stage('Publish CocoaPods') { - when { - expression { return params.PUBLISH_PODS } - } + when { expression { return params.PUBLISH_PODS } } steps { - sh """ - pod repo add xuqm-specs ${SPEC_REPO} 2>/dev/null || true - pod repo push xuqm-specs XuqmSDK.podspec --allow-warnings - """ + script { + def moduleMap = new groovy.json.JsonSlurper().parseText(env.MODULE_MAP) + def modules = env.SELECTED_MODULES.split(',') + sh "pod repo add xuqm-specs ${SPEC_REPO} 2>/dev/null || true" + // 按依赖顺序发布:先 core,再其他模块 + def ordered = ['core'] + modules.findAll { it != 'core' } + for (mod in ordered) { + def podspec = "${moduleMap[mod].podspec}.podspec" + if (fileExists(podspec)) { + echo "Publishing ${podspec}..." + sh "pod repo push xuqm-specs ${podspec} --allow-warnings" + } + } + } } } } @@ -110,17 +253,14 @@ pipeline { post { success { script { - echo "✅ iOS SDK v${env.PUBLISH_VERSION} 构建成功 (Commit: ${env.GIT_COMMIT_SHORT})" - if (params.PUBLISH_SPM) { - echo "📦 SPM 版本已发布: ${env.PUBLISH_VERSION}" - } - if (params.PUBLISH_PODS) { - echo "📦 CocoaPods 版本已发布到私有 Spec Repo" - } + def modules = env.SELECTED_MODULES?.split(',') ?: [] + def moduleMap = new groovy.json.JsonSlurper().parseText(env.MODULE_MAP) + def summary = modules.collect { "${it}=${env["VERSION_${it}"]}" }.join(', ') + echo "iOS SDK published — ${summary} (Commit: ${env.GIT_COMMIT_SHORT})" } } failure { - echo "❌ iOS SDK 构建失败,请检查日志" + echo "iOS SDK build failed — please check the logs" } } } diff --git a/Package.swift b/Package.swift index e525654..a4ba93a 100644 --- a/Package.swift +++ b/Package.swift @@ -5,21 +5,67 @@ let package = Package( name: "XuqmSDK", platforms: [.iOS(.v16), .macOS(.v13)], products: [ - .library(name: "XuqmSDK", targets: ["XuqmSDK"]), + // Individual modules — use these for per-module consumption + .library(name: "XuqmCoreSDK", targets: ["XuqmCoreSDK"]), + .library(name: "XuqmImSDK", targets: ["XuqmImSDK"]), + .library(name: "XuqmPushSDK", targets: ["XuqmPushSDK"]), + .library(name: "XuqmUpdateSDK", targets: ["XuqmUpdateSDK"]), + .library(name: "XuqmLicenseSDK", targets: ["XuqmLicenseSDK"]), + .library(name: "XuqmFileSDK", targets: ["XuqmFileSDK"]), .library(name: "XuqmWebViewSDK", targets: ["XuqmWebViewSDK"]), + // Convenience — backward compatible, includes all modules + .library(name: "XuqmSDK", targets: ["XuqmSDK"]), ], targets: [ .target( - name: "XuqmSDK", - path: "Sources/XuqmSDK", + name: "XuqmCoreSDK", + path: "Sources/XuqmCoreSDK", + swiftSettings: [.enableExperimentalFeature("StrictConcurrency")] + ), + .target( + name: "XuqmImSDK", + dependencies: ["XuqmCoreSDK", "XuqmFileSDK"], + path: "Sources/XuqmImSDK", + swiftSettings: [.enableExperimentalFeature("StrictConcurrency")] + ), + .target( + name: "XuqmPushSDK", + dependencies: ["XuqmCoreSDK"], + path: "Sources/XuqmPushSDK", + swiftSettings: [.enableExperimentalFeature("StrictConcurrency")] + ), + .target( + name: "XuqmUpdateSDK", + dependencies: ["XuqmCoreSDK"], + path: "Sources/XuqmUpdateSDK", + swiftSettings: [.enableExperimentalFeature("StrictConcurrency")] + ), + .target( + name: "XuqmLicenseSDK", + dependencies: ["XuqmCoreSDK"], + path: "Sources/XuqmLicenseSDK", + swiftSettings: [.enableExperimentalFeature("StrictConcurrency")] + ), + .target( + name: "XuqmFileSDK", + dependencies: ["XuqmCoreSDK"], + path: "Sources/XuqmFileSDK", swiftSettings: [.enableExperimentalFeature("StrictConcurrency")] ), .target( name: "XuqmWebViewSDK", - dependencies: ["XuqmSDK"], path: "Sources/XuqmWebViewSDK", swiftSettings: [.enableExperimentalFeature("StrictConcurrency")] ), + .target( + name: "XuqmSDK", + dependencies: [ + "XuqmCoreSDK", "XuqmImSDK", "XuqmPushSDK", + "XuqmUpdateSDK", "XuqmLicenseSDK", "XuqmFileSDK", + ], + path: "Sources/XuqmSDK", + swiftSettings: [.enableExperimentalFeature("StrictConcurrency")] + ), .testTarget( name: "XuqmSDKTests", dependencies: ["XuqmSDK"], diff --git a/Sources/XuqmSDK/Core/ApiClient.swift b/Sources/XuqmCoreSDK/ApiClient.swift similarity index 100% rename from Sources/XuqmSDK/Core/ApiClient.swift rename to Sources/XuqmCoreSDK/ApiClient.swift diff --git a/Sources/XuqmSDK/Core/CommonApiClient.swift b/Sources/XuqmCoreSDK/CommonApiClient.swift similarity index 100% rename from Sources/XuqmSDK/Core/CommonApiClient.swift rename to Sources/XuqmCoreSDK/CommonApiClient.swift diff --git a/Sources/XuqmSDK/Core/SDKConfig.swift b/Sources/XuqmCoreSDK/SDKConfig.swift similarity index 79% rename from Sources/XuqmSDK/Core/SDKConfig.swift rename to Sources/XuqmCoreSDK/SDKConfig.swift index dfeced6..2d509a5 100644 --- a/Sources/XuqmSDK/Core/SDKConfig.swift +++ b/Sources/XuqmCoreSDK/SDKConfig.swift @@ -33,7 +33,7 @@ public extension SDKConfig { extension SDKConfig { } -enum SDKEndpoints { - static let apiBaseURL = URL(string: "https://dev.xuqinmin.com")! - static let imWebSocketURL = URL(string: "wss://dev.xuqinmin.com/ws/im")! +public enum SDKEndpoints { + public static let apiBaseURL = URL(string: "https://dev.xuqinmin.com")! + public static let imWebSocketURL = URL(string: "wss://dev.xuqinmin.com/ws/im")! } diff --git a/Sources/XuqmSDK/Core/TokenStore.swift b/Sources/XuqmCoreSDK/TokenStore.swift similarity index 94% rename from Sources/XuqmSDK/Core/TokenStore.swift rename to Sources/XuqmCoreSDK/TokenStore.swift index 288199e..0195ac8 100644 --- a/Sources/XuqmSDK/Core/TokenStore.swift +++ b/Sources/XuqmCoreSDK/TokenStore.swift @@ -4,6 +4,8 @@ public final class TokenStore: @unchecked Sendable { private let key = "com.xuqm.sdk.token" + public init() {} + public func save(_ token: String) { UserDefaults.standard.set(token, forKey: key) } diff --git a/Sources/XuqmSDK/Core/XuqmSDK.swift b/Sources/XuqmCoreSDK/XuqmSDK.swift similarity index 54% rename from Sources/XuqmSDK/Core/XuqmSDK.swift rename to Sources/XuqmCoreSDK/XuqmSDK.swift index edc015f..8169507 100644 --- a/Sources/XuqmSDK/Core/XuqmSDK.swift +++ b/Sources/XuqmCoreSDK/XuqmSDK.swift @@ -1,28 +1,74 @@ 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() - private(set) var config: SDKConfig? - private(set) var tokenStore: TokenStore? + public private(set) var config: SDKConfig? + public private(set) var tokenStore: TokenStore? public private(set) var currentUserId: String? private var userSig: String? - private var cachedDeviceToken: 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)? + public var licenseReader: (@Sendable () throws -> LicenseFileData?)? + 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. + /// Auto-initialize from the embedded license file using the registered licenseReader hook. public func autoInitialize(debug: Bool = false) throws { - guard let file = LicenseFileReader.read() else { + guard let reader = licenseReader else { + throw XuqmSDKError.noLicenseFile + } + guard let file = try reader() else { throw XuqmSDKError.noLicenseFile } let bundleId = Bundle.main.bundleIdentifier ?? "" @@ -42,17 +88,12 @@ public final class XuqmSDK: NSObject { 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() - } - } + onInitialize?(config) } public func requireConfig() -> SDKConfig { @@ -64,7 +105,6 @@ public final class XuqmSDK: NSObject { public var initialized: Bool { isInitialized } - /// Wait for initialization to complete. public func awaitInitialization() async { guard !isInitialized else { return } await withCheckedContinuation { cont in @@ -80,28 +120,26 @@ public final class XuqmSDK: NSObject { self.userSig = userSig do { - try await ImSDK.shared.login(userId, userSig) + try await onLogin?(userId, userSig) } catch { - // IM login failed; silently ignored per facade pattern + // Module login failed; silently ignored per facade pattern } if let cachedDeviceToken { do { - try await PushSDK.shared.registerToken(cachedDeviceToken, userId: userId) + try await onDeviceToken?(cachedDeviceToken, userId) } catch { - // Push registration failed + // Device token registration failed } } } public func logout() async { - ImSDK.shared.disconnect() - if let userId = currentUserId { do { - try await PushSDK.shared.unregisterToken(userId: userId) + try await onLogout?(userId) } catch { - // Push unregistration failed + // Module logout failed } } @@ -116,7 +154,7 @@ public final class XuqmSDK: NSObject { self.cachedDeviceToken = token Task { @MainActor in if let userId = self.currentUserId { - try? await PushSDK.shared.registerToken(token, userId: userId) + try? await self.onDeviceToken?(token, userId) } } } diff --git a/Sources/XuqmSDK/File/FileSDK.swift b/Sources/XuqmFileSDK/FileSDK.swift similarity index 99% rename from Sources/XuqmSDK/File/FileSDK.swift rename to Sources/XuqmFileSDK/FileSDK.swift index 40637aa..a2b7fe5 100644 --- a/Sources/XuqmSDK/File/FileSDK.swift +++ b/Sources/XuqmFileSDK/FileSDK.swift @@ -1,4 +1,5 @@ import Foundation +import XuqmCoreSDK import UniformTypeIdentifiers public struct FileUploadResult: Codable, Sendable { diff --git a/Sources/XuqmSDK/IM/ImClient.swift b/Sources/XuqmImSDK/ImClient.swift similarity index 99% rename from Sources/XuqmSDK/IM/ImClient.swift rename to Sources/XuqmImSDK/ImClient.swift index 09d6778..4d3f52d 100644 --- a/Sources/XuqmSDK/IM/ImClient.swift +++ b/Sources/XuqmImSDK/ImClient.swift @@ -1,4 +1,5 @@ import Foundation +import XuqmCoreSDK public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked Sendable { diff --git a/Sources/XuqmSDK/IM/ImSDK.swift b/Sources/XuqmImSDK/ImSDK.swift similarity index 99% rename from Sources/XuqmSDK/IM/ImSDK.swift rename to Sources/XuqmImSDK/ImSDK.swift index 503ea22..f51cc36 100644 --- a/Sources/XuqmSDK/IM/ImSDK.swift +++ b/Sources/XuqmImSDK/ImSDK.swift @@ -1,3 +1,5 @@ +import XuqmCoreSDK +import XuqmFileSDK import Foundation public protocol ConversationDelegate: AnyObject { diff --git a/Sources/XuqmSDK/IM/ImTypes.swift b/Sources/XuqmImSDK/ImTypes.swift similarity index 90% rename from Sources/XuqmSDK/IM/ImTypes.swift rename to Sources/XuqmImSDK/ImTypes.swift index b0ac3da..95883c4 100644 --- a/Sources/XuqmSDK/IM/ImTypes.swift +++ b/Sources/XuqmImSDK/ImTypes.swift @@ -1,4 +1,5 @@ import Foundation +import XuqmCoreSDK public enum ImConnectionState: String, Sendable { case connected @@ -53,6 +54,38 @@ public struct ImMessage: Codable, Sendable { public let revoked: Bool? public let createdAt: Int64 public let editedAt: Int64? + + public init( + id: String, + appKey: String, + fromUserId: String, + fromId: String?, + toId: String, + chatType: ChatType, + msgType: MsgType, + content: String, + status: MsgStatus, + mentionedUserIds: String?, + groupReadCount: Int?, + revoked: Bool?, + createdAt: Int64, + editedAt: Int64? + ) { + self.id = id + self.appKey = appKey + self.fromUserId = fromUserId + self.fromId = fromId + self.toId = toId + self.chatType = chatType + self.msgType = msgType + self.content = content + self.status = status + self.mentionedUserIds = mentionedUserIds + self.groupReadCount = groupReadCount + self.revoked = revoked + self.createdAt = createdAt + self.editedAt = editedAt + } } public struct PageResult: Decodable, Sendable { diff --git a/Sources/XuqmSDK/License/DeviceInfoProvider.swift b/Sources/XuqmLicenseSDK/DeviceInfoProvider.swift similarity index 98% rename from Sources/XuqmSDK/License/DeviceInfoProvider.swift rename to Sources/XuqmLicenseSDK/DeviceInfoProvider.swift index bdc87cb..436cefa 100644 --- a/Sources/XuqmSDK/License/DeviceInfoProvider.swift +++ b/Sources/XuqmLicenseSDK/DeviceInfoProvider.swift @@ -1,3 +1,4 @@ +import XuqmCoreSDK #if canImport(UIKit) import UIKit #endif diff --git a/Sources/XuqmSDK/License/LicenseFileCrypto.swift b/Sources/XuqmLicenseSDK/LicenseFileCrypto.swift similarity index 99% rename from Sources/XuqmSDK/License/LicenseFileCrypto.swift rename to Sources/XuqmLicenseSDK/LicenseFileCrypto.swift index 1482bbd..3e3460f 100644 --- a/Sources/XuqmSDK/License/LicenseFileCrypto.swift +++ b/Sources/XuqmLicenseSDK/LicenseFileCrypto.swift @@ -1,4 +1,5 @@ import Foundation +import XuqmCoreSDK import CryptoKit import CommonCrypto diff --git a/Sources/XuqmSDK/License/LicenseFileReader.swift b/Sources/XuqmLicenseSDK/LicenseFileReader.swift similarity index 86% rename from Sources/XuqmSDK/License/LicenseFileReader.swift rename to Sources/XuqmLicenseSDK/LicenseFileReader.swift index ecdc51d..9a50f42 100644 --- a/Sources/XuqmSDK/License/LicenseFileReader.swift +++ b/Sources/XuqmLicenseSDK/LicenseFileReader.swift @@ -1,8 +1,9 @@ import Foundation +import XuqmCoreSDK -enum LicenseFileReader { +public enum LicenseFileReader { - static func read() -> LicenseFile? { + public static func read() -> LicenseFile? { guard let url = Bundle.main.url(forResource: "license", withExtension: "xuqm", subdirectory: "xuqm"), let encrypted = try? String(contentsOf: url, encoding: .utf8) else { return nil diff --git a/Sources/XuqmSDK/License/LicenseModels.swift b/Sources/XuqmLicenseSDK/LicenseModels.swift similarity index 52% rename from Sources/XuqmSDK/License/LicenseModels.swift rename to Sources/XuqmLicenseSDK/LicenseModels.swift index 6b4108c..fdecf04 100644 --- a/Sources/XuqmSDK/License/LicenseModels.swift +++ b/Sources/XuqmLicenseSDK/LicenseModels.swift @@ -1,16 +1,41 @@ import Foundation +import XuqmCoreSDK -struct LicenseFile: Codable { - let appKey: String - let appName: String? - let companyName: String? - let packageName: String? - let iosBundleId: String? - let harmonyBundleName: String? - let baseUrl: String? - let serverUrl: String? - let issuedAt: String? - let expiresAt: String? +public struct LicenseFile: 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 + } } public struct LicenseUserInfo: Sendable { diff --git a/Sources/XuqmSDK/License/LicenseSDK.swift b/Sources/XuqmLicenseSDK/LicenseSDK.swift similarity index 99% rename from Sources/XuqmSDK/License/LicenseSDK.swift rename to Sources/XuqmLicenseSDK/LicenseSDK.swift index 88a73d1..88ccedd 100644 --- a/Sources/XuqmSDK/License/LicenseSDK.swift +++ b/Sources/XuqmLicenseSDK/LicenseSDK.swift @@ -1,4 +1,5 @@ import Foundation +import XuqmCoreSDK public final class LicenseSDK: @unchecked Sendable { diff --git a/Sources/XuqmSDK/License/LicenseStore.swift b/Sources/XuqmLicenseSDK/LicenseStore.swift similarity index 99% rename from Sources/XuqmSDK/License/LicenseStore.swift rename to Sources/XuqmLicenseSDK/LicenseStore.swift index 4a566e6..49b2972 100644 --- a/Sources/XuqmSDK/License/LicenseStore.swift +++ b/Sources/XuqmLicenseSDK/LicenseStore.swift @@ -1,4 +1,5 @@ import Foundation +import XuqmCoreSDK import Security final class LicenseStore { diff --git a/Sources/XuqmSDK/Push/PushSDK.swift b/Sources/XuqmPushSDK/PushSDK.swift similarity index 99% rename from Sources/XuqmSDK/Push/PushSDK.swift rename to Sources/XuqmPushSDK/PushSDK.swift index 488d34c..44e7a64 100644 --- a/Sources/XuqmSDK/Push/PushSDK.swift +++ b/Sources/XuqmPushSDK/PushSDK.swift @@ -1,4 +1,5 @@ import Foundation +import XuqmCoreSDK import UserNotifications #if canImport(UIKit) import UIKit diff --git a/Sources/XuqmSDK/XuqmModuleRegistration.swift b/Sources/XuqmSDK/XuqmModuleRegistration.swift new file mode 100644 index 0000000..3da08ae --- /dev/null +++ b/Sources/XuqmSDK/XuqmModuleRegistration.swift @@ -0,0 +1,75 @@ +@_exported import XuqmCoreSDK +@_exported import XuqmImSDK +@_exported import XuqmPushSDK +@_exported import XuqmUpdateSDK +@_exported import XuqmLicenseSDK +@_exported import XuqmFileSDK + +import Foundation + +/// Auto-registers all feature module hooks with XuqmSDK.shared. +/// Import `XuqmSDK` to get the full SDK experience (backward compatible). +extension XuqmSDK { + + /// Register all module hooks. Called automatically when any module is first used. + public func registerModuleHooks() { + // License reader hook + licenseReader = { + guard let file = LicenseFileReader.read() else { return nil } + return LicenseFileData( + appKey: file.appKey, + appName: file.appName, + companyName: file.companyName, + packageName: file.packageName, + iosBundleId: file.iosBundleId, + harmonyBundleName: file.harmonyBundleName, + baseUrl: file.baseUrl, + serverUrl: file.serverUrl, + issuedAt: file.issuedAt, + expiresAt: file.expiresAt + ) + } + + // Initialize hook — auto-start push if configured + onInitialize = { @MainActor config in + if config.autoRegisterPush { + Task { @MainActor in + try? await PushSDK.shared.start() + } + } + } + + // Login hook — connect IM and register push token + onLogin = { @MainActor userId, userSig in + try await ImSDK.shared.login(userId, userSig) + if let token = XuqmSDK.shared.cachedDeviceToken { + try await PushSDK.shared.registerToken(token, userId: userId) + } + } + + // Logout hook — disconnect IM and unregister push + onLogout = { @MainActor userId in + ImSDK.shared.disconnect() + try await PushSDK.shared.unregisterToken(userId: userId) + } + + // Device token hook — register with push service + onDeviceToken = { @MainActor token, userId in + try await PushSDK.shared.registerToken(token, userId: userId) + } + } +} + +/// Convenience initializer that auto-registers module hooks. +/// This provides backward compatibility for code that does `import XuqmSDK`. +@MainActor +public enum XuqmSDKSetup { + private static var hooksRegistered = false + + /// Ensure module hooks are registered. Idempotent. + public static func ensureHooksRegistered() { + guard !hooksRegistered else { return } + hooksRegistered = true + XuqmSDK.shared.registerModuleHooks() + } +} diff --git a/Sources/XuqmSDK/Update/UpdateSDK.swift b/Sources/XuqmUpdateSDK/UpdateSDK.swift similarity index 98% rename from Sources/XuqmSDK/Update/UpdateSDK.swift rename to Sources/XuqmUpdateSDK/UpdateSDK.swift index 255b46a..9952a1b 100644 --- a/Sources/XuqmSDK/Update/UpdateSDK.swift +++ b/Sources/XuqmUpdateSDK/UpdateSDK.swift @@ -1,4 +1,5 @@ import Foundation +import XuqmCoreSDK #if canImport(UIKit) import UIKit #endif diff --git a/Tests/XuqmSDKTests/SmokeTests.swift b/Tests/XuqmSDKTests/SmokeTests.swift index 133a960..1a51be5 100644 --- a/Tests/XuqmSDKTests/SmokeTests.swift +++ b/Tests/XuqmSDKTests/SmokeTests.swift @@ -30,11 +30,11 @@ final class SmokeTests: XCTestCase { msgType: .text, content: "Hello", status: .sent, - mentionedUserIds: nil, - groupReadCount: nil, + mentionedUserIds: nil as String?, + groupReadCount: nil as Int?, revoked: false, createdAt: 1714300000000, - editedAt: nil + editedAt: nil as Int64? ) let data = try JSONEncoder().encode(msg) let decoded = try JSONDecoder().decode(ImMessage.self, from: data) diff --git a/XuqmCoreSDK.podspec b/XuqmCoreSDK.podspec new file mode 100644 index 0000000..6e1cd24 --- /dev/null +++ b/XuqmCoreSDK.podspec @@ -0,0 +1,12 @@ +Pod::Spec.new do |s| + s.name = 'XuqmCoreSDK' + s.version = '0.1.0' + s.summary = 'XuqmGroup iOS SDK — Core module' + s.homepage = 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK' + s.license = { :type => 'MIT' } + s.author = { 'XuqmGroup' => 'dev@xuqm.com' } + s.source = { :git => 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK.git', :tag => "core/#{s.version}" } + s.ios.deployment_target = '16.0' + s.swift_version = '5.9' + s.source_files = 'Sources/XuqmCoreSDK/**/*.swift' +end diff --git a/XuqmFileSDK.podspec b/XuqmFileSDK.podspec new file mode 100644 index 0000000..ebd509c --- /dev/null +++ b/XuqmFileSDK.podspec @@ -0,0 +1,13 @@ +Pod::Spec.new do |s| + s.name = 'XuqmFileSDK' + s.version = '0.1.0' + s.summary = 'XuqmGroup iOS SDK — File module' + s.homepage = 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK' + s.license = { :type => 'MIT' } + s.author = { 'XuqmGroup' => 'dev@xuqm.com' } + s.source = { :git => 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK.git', :tag => "file/#{s.version}" } + s.ios.deployment_target = '16.0' + s.swift_version = '5.9' + s.source_files = 'Sources/XuqmFileSDK/**/*.swift' + s.dependency 'XuqmCoreSDK' +end diff --git a/XuqmImSDK.podspec b/XuqmImSDK.podspec new file mode 100644 index 0000000..7d4317a --- /dev/null +++ b/XuqmImSDK.podspec @@ -0,0 +1,14 @@ +Pod::Spec.new do |s| + s.name = 'XuqmImSDK' + s.version = '0.1.0' + s.summary = 'XuqmGroup iOS SDK — IM module' + s.homepage = 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK' + s.license = { :type => 'MIT' } + s.author = { 'XuqmGroup' => 'dev@xuqm.com' } + s.source = { :git => 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK.git', :tag => "im/#{s.version}" } + s.ios.deployment_target = '16.0' + s.swift_version = '5.9' + s.source_files = 'Sources/XuqmImSDK/**/*.swift' + s.dependency 'XuqmCoreSDK' + s.dependency 'XuqmFileSDK' +end diff --git a/XuqmLicenseSDK.podspec b/XuqmLicenseSDK.podspec new file mode 100644 index 0000000..34cfe7e --- /dev/null +++ b/XuqmLicenseSDK.podspec @@ -0,0 +1,13 @@ +Pod::Spec.new do |s| + s.name = 'XuqmLicenseSDK' + s.version = '0.1.0' + s.summary = 'XuqmGroup iOS SDK — License module' + s.homepage = 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK' + s.license = { :type => 'MIT' } + s.author = { 'XuqmGroup' => 'dev@xuqm.com' } + s.source = { :git => 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK.git', :tag => "license/#{s.version}" } + s.ios.deployment_target = '16.0' + s.swift_version = '5.9' + s.source_files = 'Sources/XuqmLicenseSDK/**/*.swift' + s.dependency 'XuqmCoreSDK' +end diff --git a/XuqmPushSDK.podspec b/XuqmPushSDK.podspec new file mode 100644 index 0000000..0bc842b --- /dev/null +++ b/XuqmPushSDK.podspec @@ -0,0 +1,13 @@ +Pod::Spec.new do |s| + s.name = 'XuqmPushSDK' + s.version = '0.1.0' + s.summary = 'XuqmGroup iOS SDK — Push module' + s.homepage = 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK' + s.license = { :type => 'MIT' } + s.author = { 'XuqmGroup' => 'dev@xuqm.com' } + s.source = { :git => 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK.git', :tag => "push/#{s.version}" } + s.ios.deployment_target = '16.0' + s.swift_version = '5.9' + s.source_files = 'Sources/XuqmPushSDK/**/*.swift' + s.dependency 'XuqmCoreSDK' +end diff --git a/XuqmSDK.podspec b/XuqmSDK.podspec index 8cff31f..fd9d094 100644 --- a/XuqmSDK.podspec +++ b/XuqmSDK.podspec @@ -1,12 +1,18 @@ Pod::Spec.new do |s| s.name = 'XuqmSDK' s.version = '0.1.1' - s.summary = 'XuqmGroup iOS SDK — IM, Push Notifications, Version Management' + s.summary = 'XuqmGroup iOS SDK — convenience podspec (includes all modules)' s.homepage = 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK' s.license = { :type => 'MIT' } s.author = { 'XuqmGroup' => 'dev@xuqm.com' } - s.source = { :git => 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK.git', :tag => s.version.to_s } + s.source = { :git => 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK.git', :tag => "sdk/#{s.version}" } s.ios.deployment_target = '16.0' s.swift_version = '5.9' s.source_files = 'Sources/XuqmSDK/**/*.swift' + s.dependency 'XuqmCoreSDK' + s.dependency 'XuqmImSDK' + s.dependency 'XuqmPushSDK' + s.dependency 'XuqmUpdateSDK' + s.dependency 'XuqmLicenseSDK' + s.dependency 'XuqmFileSDK' end diff --git a/XuqmUpdateSDK.podspec b/XuqmUpdateSDK.podspec new file mode 100644 index 0000000..c74e7cc --- /dev/null +++ b/XuqmUpdateSDK.podspec @@ -0,0 +1,13 @@ +Pod::Spec.new do |s| + s.name = 'XuqmUpdateSDK' + s.version = '0.1.0' + s.summary = 'XuqmGroup iOS SDK — Update module' + s.homepage = 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK' + s.license = { :type => 'MIT' } + s.author = { 'XuqmGroup' => 'dev@xuqm.com' } + s.source = { :git => 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK.git', :tag => "update/#{s.version}" } + s.ios.deployment_target = '16.0' + s.swift_version = '5.9' + s.source_files = 'Sources/XuqmUpdateSDK/**/*.swift' + s.dependency 'XuqmCoreSDK' +end diff --git a/XuqmWebViewSDK.podspec b/XuqmWebViewSDK.podspec index b2f47dc..020eb04 100644 --- a/XuqmWebViewSDK.podspec +++ b/XuqmWebViewSDK.podspec @@ -5,9 +5,8 @@ Pod::Spec.new do |s| s.homepage = 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK' s.license = { :type => 'MIT' } s.author = { 'XuqmGroup' => 'dev@xuqm.com' } - s.source = { :git => 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK.git', :tag => s.version.to_s } + s.source = { :git => 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK.git', :tag => "webview/#{s.version}" } s.ios.deployment_target = '16.0' s.swift_version = '5.9' s.source_files = 'Sources/XuqmWebViewSDK/**/*.swift' - s.dependency 'XuqmSDK' end