feat(push): 添加多厂商推送集成支持
- 实现了华为 HMS 推送服务集成 - 实现了小米推送服务集成 - 实现了 OPPO 推送服务集成 - 实现了 vivo 推送服务集成 - 实现了荣耀推送服务集成 - 实现了 FCM 推送服务集成 - 添加了统一的厂商推送接口和检测机制 - 添加了推送配置 API 和存储管理 - 添加了推送令牌管理和设备注册功能 - 添加了模拟器环境的推送测试用例
这个提交包含在:
父节点
44bb4c2ebe
当前提交
6867091c04
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户