2026-04-21 22:07:29 +08:00
|
|
|
import Foundation
|
2026-04-30 19:23:22 +08:00
|
|
|
import UserNotifications
|
|
|
|
|
#if canImport(UIKit)
|
|
|
|
|
import UIKit
|
|
|
|
|
#endif
|
2026-04-21 22:07:29 +08:00
|
|
|
|
|
|
|
|
public enum PushVendor: String {
|
|
|
|
|
case apns = "APNS"
|
|
|
|
|
case fcm = "FCM"
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 22:57:55 +08:00
|
|
|
public struct PushMessage: Sendable {
|
|
|
|
|
public let title: String?
|
|
|
|
|
public let body: String?
|
|
|
|
|
public let payload: [String: Any]
|
|
|
|
|
public let rawUserInfo: [AnyHashable: Any]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public protocol PushMessageDelegate: AnyObject, Sendable {
|
|
|
|
|
func pushSDK(_ sdk: PushSDK, didReceiveMessage message: PushMessage)
|
|
|
|
|
func pushSDK(_ sdk: PushSDK, didTapNotification message: PushMessage)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 22:07:29 +08:00
|
|
|
@MainActor
|
2026-04-30 19:23:22 +08:00
|
|
|
public final class PushSDK: NSObject, UNUserNotificationCenterDelegate {
|
2026-04-21 22:07:29 +08:00
|
|
|
|
|
|
|
|
public static let shared = PushSDK()
|
2026-05-02 22:57:55 +08:00
|
|
|
public weak var delegate: PushMessageDelegate?
|
2026-04-30 19:23:22 +08:00
|
|
|
private override init() {
|
|
|
|
|
super.init()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@MainActor
|
|
|
|
|
public func requestAuthorization(options: UNAuthorizationOptions = [.alert, .badge, .sound]) async throws -> Bool {
|
|
|
|
|
let granted = try await UNUserNotificationCenter.current().requestAuthorization(options: options)
|
|
|
|
|
#if canImport(UIKit)
|
|
|
|
|
if granted {
|
|
|
|
|
await MainActor.run {
|
|
|
|
|
UIApplication.shared.registerForRemoteNotifications()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
return granted
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func registerDeviceToken(_ deviceToken: Data, userId: String) async throws {
|
|
|
|
|
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
|
|
|
|
|
try await registerToken(token, userId: userId)
|
|
|
|
|
}
|
2026-04-21 22:07:29 +08:00
|
|
|
|
|
|
|
|
public func registerToken(_ token: String, userId: String, vendor: PushVendor = .apns) async throws {
|
|
|
|
|
let config = XuqmSDK.shared.requireConfig()
|
|
|
|
|
let _: EmptyResponse = try await ApiClient.shared.request(
|
|
|
|
|
path: "/api/push/register",
|
|
|
|
|
method: "POST",
|
|
|
|
|
queryItems: [
|
|
|
|
|
URLQueryItem(name: "appId", value: config.appId),
|
|
|
|
|
URLQueryItem(name: "userId", value: userId),
|
|
|
|
|
URLQueryItem(name: "vendor", value: vendor.rawValue),
|
|
|
|
|
URLQueryItem(name: "token", value: token),
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-04-30 19:23:22 +08:00
|
|
|
|
2026-05-01 21:27:39 +08:00
|
|
|
public func registerFcmToken(_ token: String, userId: String) async throws {
|
|
|
|
|
try await registerToken(token, userId: userId, vendor: .fcm)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public var isFcmAvailable: Bool {
|
|
|
|
|
#if canImport(FirebaseMessaging)
|
|
|
|
|
return true
|
|
|
|
|
#else
|
|
|
|
|
return false
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func unregisterToken(userId: String, vendor: PushVendor = .apns) async throws {
|
|
|
|
|
let config = XuqmSDK.shared.requireConfig()
|
|
|
|
|
let _: EmptyResponse = try await ApiClient.shared.request(
|
|
|
|
|
path: "/api/push/unregister",
|
2026-05-02 22:57:55 +08:00
|
|
|
method: "DELETE",
|
2026-05-01 21:27:39 +08:00
|
|
|
queryItems: [
|
|
|
|
|
URLQueryItem(name: "appId", value: config.appId),
|
|
|
|
|
URLQueryItem(name: "userId", value: userId),
|
|
|
|
|
URLQueryItem(name: "vendor", value: vendor.rawValue),
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 19:23:22 +08:00
|
|
|
public func userNotificationCenter(
|
|
|
|
|
_ center: UNUserNotificationCenter,
|
|
|
|
|
willPresent notification: UNNotification,
|
|
|
|
|
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
|
|
|
|
) {
|
2026-05-02 22:57:55 +08:00
|
|
|
let message = parseMessage(notification.request.content)
|
|
|
|
|
if let message = message {
|
|
|
|
|
delegate?.pushSDK(self, didReceiveMessage: message)
|
|
|
|
|
}
|
2026-04-30 19:23:22 +08:00
|
|
|
completionHandler([.banner, .sound, .badge])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func userNotificationCenter(
|
|
|
|
|
_ center: UNUserNotificationCenter,
|
|
|
|
|
didReceive response: UNNotificationResponse,
|
|
|
|
|
withCompletionHandler completionHandler: @escaping () -> Void
|
|
|
|
|
) {
|
2026-05-02 22:57:55 +08:00
|
|
|
let message = parseMessage(response.notification.request.content)
|
|
|
|
|
if let message = message {
|
|
|
|
|
delegate?.pushSDK(self, didTapNotification: message)
|
|
|
|
|
}
|
2026-04-30 19:23:22 +08:00
|
|
|
completionHandler()
|
|
|
|
|
}
|
2026-05-02 22:57:55 +08:00
|
|
|
|
|
|
|
|
private func parseMessage(_ content: UNNotificationContent) -> PushMessage? {
|
|
|
|
|
let userInfo = content.userInfo
|
|
|
|
|
let aps = userInfo["aps"] as? [String: Any]
|
|
|
|
|
let alert = aps?["alert"] as? [String: Any]
|
|
|
|
|
let title = content.title.isEmpty ? (alert?["title"] as? String) : content.title
|
|
|
|
|
let body = content.body.isEmpty ? (alert?["body"] as? String) : content.body
|
|
|
|
|
return PushMessage(
|
|
|
|
|
title: title,
|
|
|
|
|
body: body,
|
|
|
|
|
payload: userInfo.compactMapKeys { $0 as? String },
|
|
|
|
|
rawUserInfo: userInfo
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private extension Dictionary {
|
|
|
|
|
func compactMapKeys<T: Hashable>(_ transform: (Key) -> T?) -> [T: Value] {
|
|
|
|
|
var result: [T: Value] = [:]
|
|
|
|
|
for (key, value) in self {
|
|
|
|
|
if let newKey = transform(key) {
|
|
|
|
|
result[newKey] = value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
2026-04-21 22:07:29 +08:00
|
|
|
}
|