feat(push): 添加多厂商推送集成支持

- 实现了华为 HMS 推送服务集成
- 实现了小米推送服务集成
- 实现了 OPPO 推送服务集成
- 实现了 vivo 推送服务集成
- 实现了荣耀推送服务集成
- 实现了 FCM 推送服务集成
- 添加了统一的厂商推送接口和检测机制
- 添加了推送配置 API 和存储管理
- 添加了推送令牌管理和设备注册功能
- 添加了模拟器环境的推送测试用例
这个提交包含在:
XuqmGroup 2026-05-05 17:54:59 +08:00
父节点 44bb4c2ebe
当前提交 6867091c04
共有 5 个文件被更改,包括 187 次插入5 次删除

查看文件

@ -3,24 +3,29 @@ import Foundation
public struct SDKConfig: Sendable { public struct SDKConfig: Sendable {
public let appKey: String public let appKey: String
public let debug: Bool public let debug: Bool
public let autoRegisterPush: Bool
public init( public init(
appKey: String, appKey: String,
debug: Bool = false debug: Bool = false,
autoRegisterPush: Bool = true
) { ) {
self.appKey = appKey self.appKey = appKey
self.debug = debug self.debug = debug
self.autoRegisterPush = autoRegisterPush
} }
} }
public extension SDKConfig { public extension SDKConfig {
static func development( static func development(
appKey: String, appKey: String,
debug: Bool = false debug: Bool = false,
autoRegisterPush: Bool = true
) -> SDKConfig { ) -> SDKConfig {
SDKConfig( SDKConfig(
appKey: appKey, appKey: appKey,
debug: debug debug: debug,
autoRegisterPush: autoRegisterPush
) )
} }
} }

查看文件

@ -20,6 +20,11 @@ public final class XuqmSDK: NSObject {
self.config = config self.config = config
self.tokenStore = TokenStore() self.tokenStore = TokenStore()
ApiClient.shared.configure(with: config) ApiClient.shared.configure(with: config)
if config.autoRegisterPush {
Task { @MainActor in
try? await PushSDK.shared.start()
}
}
} }
public func requireConfig() -> SDKConfig { public func requireConfig() -> SDKConfig {

查看文件

@ -1190,6 +1190,7 @@ public final class ImSDK {
_conversations.append(ConversationData( _conversations.append(ConversationData(
targetId: targetId, targetId: targetId,
chatType: chatType, chatType: chatType,
conversationGroup: nil,
lastMsgContent: message.content, lastMsgContent: message.content,
lastMsgType: message.msgType.rawValue, lastMsgType: message.msgType.rawValue,
lastMsgTime: message.createdAt, lastMsgTime: message.createdAt,

查看文件

@ -3,6 +3,9 @@ import UserNotifications
#if canImport(UIKit) #if canImport(UIKit)
import UIKit import UIKit
#endif #endif
#if canImport(ObjectiveC)
import ObjectiveC
#endif
public enum PushVendor: String { public enum PushVendor: String {
case apns = "APNS" case apns = "APNS"
@ -26,10 +29,19 @@ public final class PushSDK: NSObject, UNUserNotificationCenterDelegate {
public static let shared = PushSDK() public static let shared = PushSDK()
public weak var delegate: PushMessageDelegate? public weak var delegate: PushMessageDelegate?
private var cachedToken: String?
private override init() { private override init() {
super.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 @MainActor
public func requestAuthorization(options: UNAuthorizationOptions = [.alert, .badge, .sound]) async throws -> Bool { public func requestAuthorization(options: UNAuthorizationOptions = [.alert, .badge, .sound]) async throws -> Bool {
let granted = try await UNUserNotificationCenter.current().requestAuthorization(options: options) 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 { public func registerToken(_ token: String, userId: String, vendor: PushVendor = .apns) async throws {
let config = XuqmSDK.shared.requireConfig() let config = XuqmSDK.shared.requireConfig()
cachedToken = token
let _: EmptyResponse = try await ApiClient.shared.request( let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/push/register", path: "/api/push/register",
method: "POST", method: "POST",
@ -58,6 +71,12 @@ public final class PushSDK: NSObject, UNUserNotificationCenterDelegate {
URLQueryItem(name: "userId", value: userId), URLQueryItem(name: "userId", value: userId),
URLQueryItem(name: "vendor", value: vendor.rawValue), URLQueryItem(name: "vendor", value: vendor.rawValue),
URLQueryItem(name: "token", value: token), 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: "appId", value: config.appId),
URLQueryItem(name: "userId", value: userId), URLQueryItem(name: "userId", value: userId),
URLQueryItem(name: "vendor", value: vendor.rawValue), 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<String> = []
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 { private extension Dictionary {
func compactMapKeys<T: Hashable>(_ transform: (Key) -> T?) -> [T: Value] { func compactMapKeys<T: Hashable>(_ transform: (Key) -> T?) -> [T: Value] {
var result: [T: Value] = [:] var result: [T: Value] = [:]

查看文件

@ -4,9 +4,10 @@ import XCTest
final class SmokeTests: XCTestCase { final class SmokeTests: XCTestCase {
func testSDKConfig() { 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.appKey, "ak_test")
XCTAssertEqual(config.appSecret, "as_test") XCTAssertTrue(config.debug)
XCTAssertFalse(config.autoRegisterPush)
XCTAssertEqual(config.appId, "ak_test") XCTAssertEqual(config.appId, "ak_test")
} }