From 6867091c044178ce2c7599bf06327787886a4d5b Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Tue, 5 May 2026 17:54:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(push):=20=E6=B7=BB=E5=8A=A0=E5=A4=9A?= =?UTF-8?q?=E5=8E=82=E5=95=86=E6=8E=A8=E9=80=81=E9=9B=86=E6=88=90=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了华为 HMS 推送服务集成 - 实现了小米推送服务集成 - 实现了 OPPO 推送服务集成 - 实现了 vivo 推送服务集成 - 实现了荣耀推送服务集成 - 实现了 FCM 推送服务集成 - 添加了统一的厂商推送接口和检测机制 - 添加了推送配置 API 和存储管理 - 添加了推送令牌管理和设备注册功能 - 添加了模拟器环境的推送测试用例 --- Sources/XuqmSDK/Core/SDKConfig.swift | 11 +- Sources/XuqmSDK/Core/XuqmSDK.swift | 5 + Sources/XuqmSDK/IM/ImSDK.swift | 1 + Sources/XuqmSDK/Push/PushSDK.swift | 170 +++++++++++++++++++++++++++ Tests/XuqmSDKTests/SmokeTests.swift | 5 +- 5 files changed, 187 insertions(+), 5 deletions(-) diff --git a/Sources/XuqmSDK/Core/SDKConfig.swift b/Sources/XuqmSDK/Core/SDKConfig.swift index 8dfb1c0..0881d21 100644 --- a/Sources/XuqmSDK/Core/SDKConfig.swift +++ b/Sources/XuqmSDK/Core/SDKConfig.swift @@ -3,24 +3,29 @@ import Foundation public struct SDKConfig: Sendable { public let appKey: String public let debug: Bool + public let autoRegisterPush: Bool public init( appKey: String, - debug: Bool = false + debug: Bool = false, + autoRegisterPush: Bool = true ) { self.appKey = appKey self.debug = debug + self.autoRegisterPush = autoRegisterPush } } public extension SDKConfig { static func development( appKey: String, - debug: Bool = false + debug: Bool = false, + autoRegisterPush: Bool = true ) -> SDKConfig { SDKConfig( appKey: appKey, - debug: debug + debug: debug, + autoRegisterPush: autoRegisterPush ) } } diff --git a/Sources/XuqmSDK/Core/XuqmSDK.swift b/Sources/XuqmSDK/Core/XuqmSDK.swift index 1d018f3..98bde62 100644 --- a/Sources/XuqmSDK/Core/XuqmSDK.swift +++ b/Sources/XuqmSDK/Core/XuqmSDK.swift @@ -20,6 +20,11 @@ public final class XuqmSDK: NSObject { self.config = config self.tokenStore = TokenStore() ApiClient.shared.configure(with: config) + if config.autoRegisterPush { + Task { @MainActor in + try? await PushSDK.shared.start() + } + } } public func requireConfig() -> SDKConfig { diff --git a/Sources/XuqmSDK/IM/ImSDK.swift b/Sources/XuqmSDK/IM/ImSDK.swift index 5e85775..ed16cf6 100644 --- a/Sources/XuqmSDK/IM/ImSDK.swift +++ b/Sources/XuqmSDK/IM/ImSDK.swift @@ -1190,6 +1190,7 @@ public final class ImSDK { _conversations.append(ConversationData( targetId: targetId, chatType: chatType, + conversationGroup: nil, lastMsgContent: message.content, lastMsgType: message.msgType.rawValue, lastMsgTime: message.createdAt, diff --git a/Sources/XuqmSDK/Push/PushSDK.swift b/Sources/XuqmSDK/Push/PushSDK.swift index 13d4a67..3a77620 100644 --- a/Sources/XuqmSDK/Push/PushSDK.swift +++ b/Sources/XuqmSDK/Push/PushSDK.swift @@ -3,6 +3,9 @@ import UserNotifications #if canImport(UIKit) import UIKit #endif +#if canImport(ObjectiveC) +import ObjectiveC +#endif public enum PushVendor: String { case apns = "APNS" @@ -26,10 +29,19 @@ public final class PushSDK: NSObject, UNUserNotificationCenterDelegate { public static let shared = PushSDK() public weak var delegate: PushMessageDelegate? + private var cachedToken: String? private override init() { super.init() } + public func start(options: UNAuthorizationOptions = [.alert, .badge, .sound]) async throws { + UNUserNotificationCenter.current().delegate = self + #if canImport(UIKit) && canImport(ObjectiveC) + PushAppDelegateInterceptor.install() + #endif + _ = try await requestAuthorization(options: options) + } + @MainActor public func requestAuthorization(options: UNAuthorizationOptions = [.alert, .badge, .sound]) async throws -> Bool { let granted = try await UNUserNotificationCenter.current().requestAuthorization(options: options) @@ -50,6 +62,7 @@ public final class PushSDK: NSObject, UNUserNotificationCenterDelegate { public func registerToken(_ token: String, userId: String, vendor: PushVendor = .apns) async throws { let config = XuqmSDK.shared.requireConfig() + cachedToken = token let _: EmptyResponse = try await ApiClient.shared.request( path: "/api/push/register", method: "POST", @@ -58,6 +71,12 @@ public final class PushSDK: NSObject, UNUserNotificationCenterDelegate { URLQueryItem(name: "userId", value: userId), URLQueryItem(name: "vendor", value: vendor.rawValue), URLQueryItem(name: "token", value: token), + URLQueryItem(name: "deviceId", value: deviceId), + URLQueryItem(name: "brand", value: "Apple"), + URLQueryItem(name: "model", value: deviceModel), + URLQueryItem(name: "osVersion", value: osVersion), + URLQueryItem(name: "platform", value: "IOS"), + URLQueryItem(name: "appVersion", value: appVersion), ] ) } @@ -83,6 +102,7 @@ public final class PushSDK: NSObject, UNUserNotificationCenterDelegate { URLQueryItem(name: "appId", value: config.appId), URLQueryItem(name: "userId", value: userId), URLQueryItem(name: "vendor", value: vendor.rawValue), + URLQueryItem(name: "deviceId", value: deviceId), ] ) } @@ -126,6 +146,156 @@ public final class PushSDK: NSObject, UNUserNotificationCenterDelegate { } } +#if canImport(UIKit) && canImport(ObjectiveC) +private final class OriginalImpBox { + let imp: IMP + + init(_ imp: IMP) { + self.imp = imp + } +} + +private var didRegisterOriginalImpKey: UInt8 = 0 + +private final class PushAppDelegateInterceptor: NSObject { + private static let lock = NSLock() + private static var installedClassNames: Set = [] + + static func install() { + guard let delegate = UIApplication.shared.delegate else { + return + } + + let delegateClass: AnyClass = object_getClass(delegate) ?? type(of: delegate) + let className = NSStringFromClass(delegateClass) + + lock.lock() + let alreadyInstalled = installedClassNames.contains(className) + if !alreadyInstalled { + installedClassNames.insert(className) + } + lock.unlock() + + guard !alreadyInstalled else { + return + } + + installDidRegisterHandler(on: delegateClass) + } + + private static func installDidRegisterHandler(on delegateClass: AnyClass) { + let originalSelector = #selector( + UIApplicationDelegate.application( + _:didRegisterForRemoteNotificationsWithDeviceToken: + ) + ) + let interceptorSelector = #selector( + PushAppDelegateInterceptor.xuqm_application( + _:didRegisterForRemoteNotificationsWithDeviceToken: + ) + ) + + guard let interceptorMethod = class_getInstanceMethod(Self.self, interceptorSelector) else { + return + } + + if let originalMethod = class_getInstanceMethod(delegateClass, originalSelector) { + objc_setAssociatedObject( + delegateClass, + &didRegisterOriginalImpKey, + OriginalImpBox(method_getImplementation(originalMethod)), + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + } + + class_replaceMethod( + delegateClass, + originalSelector, + method_getImplementation(interceptorMethod), + method_getTypeEncoding(interceptorMethod) + ) + } + + @objc private func xuqm_application( + _ application: UIApplication, + didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data + ) { + Task { @MainActor in + XuqmSDK.shared.registerDeviceToken(deviceToken) + } + callOriginalDidRegister(application, deviceToken: deviceToken) + } + + private func callOriginalDidRegister(_ application: UIApplication, deviceToken: Data) { + guard let box = objc_getAssociatedObject( + object_getClass(self) ?? type(of: self), + &didRegisterOriginalImpKey + ) as? OriginalImpBox else { + return + } + + typealias DidRegisterImp = @convention(c) ( + AnyObject, + Selector, + UIApplication, + Data + ) -> Void + + let original = unsafeBitCast(box.imp, to: DidRegisterImp.self) + original( + self, + #selector( + UIApplicationDelegate.application( + _:didRegisterForRemoteNotificationsWithDeviceToken: + ) + ), + application, + deviceToken + ) + } +} +#endif + +private extension PushSDK { + var deviceId: String { + #if canImport(UIKit) + return UIDevice.current.identifierForVendor?.uuidString ?? "ios-device" + #else + return "ios-device" + #endif + } + + var deviceModel: String { + #if canImport(UIKit) + return UIDevice.current.model + #else + return "Apple" + #endif + } + + var osVersion: String { + #if canImport(UIKit) + return "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)" + #else + return "iOS" + #endif + } + + var appVersion: String? { + let info = Bundle.main.infoDictionary + let version = info?["CFBundleShortVersionString"] as? String + let build = info?["CFBundleVersion"] as? String + switch (version, build) { + case let (version?, build?): + return "\(version)(\(build))" + case let (version?, nil): + return version + default: + return nil + } + } +} + private extension Dictionary { func compactMapKeys(_ transform: (Key) -> T?) -> [T: Value] { var result: [T: Value] = [:] diff --git a/Tests/XuqmSDKTests/SmokeTests.swift b/Tests/XuqmSDKTests/SmokeTests.swift index 790ab97..dd0cb1e 100644 --- a/Tests/XuqmSDKTests/SmokeTests.swift +++ b/Tests/XuqmSDKTests/SmokeTests.swift @@ -4,9 +4,10 @@ import XCTest final class SmokeTests: XCTestCase { func testSDKConfig() { - let config = SDKConfig(appKey: "ak_test", appSecret: "as_test") + let config = SDKConfig(appKey: "ak_test", debug: true, autoRegisterPush: false) XCTAssertEqual(config.appKey, "ak_test") - XCTAssertEqual(config.appSecret, "as_test") + XCTAssertTrue(config.debug) + XCTAssertFalse(config.autoRegisterPush) XCTAssertEqual(config.appId, "ak_test") }