react-native-qpush 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +439 -2
- package/android/build.gradle +77 -0
- package/android/gradle.properties +5 -0
- package/android/proguard-rules.pro +5 -0
- package/android/src/main/AndroidManifest.xml +29 -0
- package/android/src/main/java/com/qpush/reactnative/QPushDeviceInfo.kt +96 -0
- package/android/src/main/java/com/qpush/reactnative/QPushEventBus.kt +153 -0
- package/android/src/main/java/com/qpush/reactnative/QPushFcm.kt +108 -0
- package/android/src/main/java/com/qpush/reactnative/QPushHttp.kt +92 -0
- package/android/src/main/java/com/qpush/reactnative/QPushImages.kt +103 -0
- package/android/src/main/java/com/qpush/reactnative/QPushLog.kt +39 -0
- package/android/src/main/java/com/qpush/reactnative/QPushMessagingService.kt +77 -0
- package/android/src/main/java/com/qpush/reactnative/QPushNotificationOpenActivity.kt +121 -0
- package/android/src/main/java/com/qpush/reactnative/QPushNotificationRenderer.kt +256 -0
- package/android/src/main/java/com/qpush/reactnative/QPushPayload.kt +114 -0
- package/android/src/main/java/com/qpush/reactnative/QPushPushModule.kt +368 -0
- package/android/src/main/java/com/qpush/reactnative/QPushPushPackage.kt +15 -0
- package/android/src/main/java/com/qpush/reactnative/QPushStore.kt +108 -0
- package/android/src/main/res/values/qpush_strings.xml +6 -0
- package/app.plugin.js +1 -0
- package/ios/Extension/QPushNotificationServiceExtension.swift +261 -0
- package/ios/Extension/QPushPushExtension.podspec +34 -0
- package/ios/QPushPush.m +62 -0
- package/ios/QPushPush.swift +186 -0
- package/ios/QPushPushBridge.h +5 -0
- package/ios/QPushPushCore.swift +457 -0
- package/ios/QPushPushHooks.m +237 -0
- package/ios/QPushPushSupport.swift +249 -0
- package/ios_nse_template/Info.plist +38 -0
- package/ios_nse_template/NotificationService.entitlements +10 -0
- package/ios_nse_template/NotificationService.swift +44 -0
- package/lib/NativeQPushPush.d.ts +44 -0
- package/lib/NativeQPushPush.js +4 -0
- package/lib/QPush.d.ts +117 -0
- package/lib/QPush.js +206 -0
- package/lib/core/QPushCore.d.ts +148 -0
- package/lib/core/QPushCore.js +1387 -0
- package/lib/core/constants.d.ts +37 -0
- package/lib/core/constants.js +40 -0
- package/lib/core/events.d.ts +11 -0
- package/lib/core/events.js +40 -0
- package/lib/core/http.d.ts +41 -0
- package/lib/core/http.js +123 -0
- package/lib/core/logger.d.ts +18 -0
- package/lib/core/logger.js +43 -0
- package/lib/core/operationQueue.d.ts +117 -0
- package/lib/core/operationQueue.js +264 -0
- package/lib/core/operations.d.ts +80 -0
- package/lib/core/operations.js +88 -0
- package/lib/core/payload.d.ts +20 -0
- package/lib/core/payload.js +187 -0
- package/lib/core/platform.d.ts +72 -0
- package/lib/core/platform.js +7 -0
- package/lib/core/state.d.ts +51 -0
- package/lib/core/state.js +74 -0
- package/lib/core/storage.d.ts +34 -0
- package/lib/core/storage.js +68 -0
- package/lib/core/subscription.d.ts +67 -0
- package/lib/core/subscription.js +98 -0
- package/lib/core/tags.d.ts +39 -0
- package/lib/core/tags.js +123 -0
- package/lib/core/url.d.ts +17 -0
- package/lib/core/url.js +64 -0
- package/lib/core/uuid.d.ts +2 -0
- package/lib/core/uuid.js +18 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +15 -0
- package/lib/notification.d.ts +67 -0
- package/lib/notification.js +73 -0
- package/lib/reactNativePlatform.d.ts +3 -0
- package/lib/reactNativePlatform.js +164 -0
- package/lib/types.d.ts +157 -0
- package/lib/types.js +31 -0
- package/package.json +70 -4
- package/plugin/withQPush.js +330 -0
- package/react-native-qpush.podspec +30 -0
- package/react-native.config.js +18 -0
- package/src/NativeQPushPush.ts +57 -0
- package/src/QPush.ts +292 -0
- package/src/core/QPushCore.ts +1515 -0
- package/src/core/constants.ts +42 -0
- package/src/core/events.ts +34 -0
- package/src/core/http.ts +146 -0
- package/src/core/logger.ts +42 -0
- package/src/core/operationQueue.ts +324 -0
- package/src/core/operations.ts +166 -0
- package/src/core/payload.ts +175 -0
- package/src/core/platform.ts +84 -0
- package/src/core/state.ts +127 -0
- package/src/core/storage.ts +76 -0
- package/src/core/subscription.ts +133 -0
- package/src/core/tags.ts +116 -0
- package/src/core/url.ts +58 -0
- package/src/core/uuid.ts +13 -0
- package/src/index.ts +23 -0
- package/src/notification.ts +113 -0
- package/src/reactNativePlatform.ts +172 -0
- package/src/types.ts +174 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
import UserNotifications
|
|
4
|
+
|
|
5
|
+
/// Native singleton shared by the React Native module and the Objective-C
|
|
6
|
+
/// hooks (app delegate swizzling + UNUserNotificationCenter delegate proxy in
|
|
7
|
+
/// QPushPushHooks.m, which call the `@objc` class methods below at runtime).
|
|
8
|
+
///
|
|
9
|
+
/// All mutable state is confined to the main queue.
|
|
10
|
+
@objc(QPushPushCore)
|
|
11
|
+
public final class QPushPushCore: NSObject {
|
|
12
|
+
static let shared = QPushPushCore()
|
|
13
|
+
|
|
14
|
+
static let clickEvent = "QPushPushClick"
|
|
15
|
+
static let foregroundWillDisplayEvent = "QPushPushForegroundWillDisplay"
|
|
16
|
+
static let pushTokenEvent = "QPushPushToken"
|
|
17
|
+
static let supportedEvents = [clickEvent, foregroundWillDisplayEvent, pushTokenEvent]
|
|
18
|
+
|
|
19
|
+
/// JS must answer `foregroundWillDisplay` within this time, else the notification is shown.
|
|
20
|
+
private static let foregroundResponseTimeout: TimeInterval = 1.0
|
|
21
|
+
/// After `preventDefault()`, JS may still call `display()` during this window.
|
|
22
|
+
private static let preventedExpiry: TimeInterval = 25.0
|
|
23
|
+
private static let registrationTimeout: TimeInterval = 30.0
|
|
24
|
+
private static let maxPendingEvents = 50
|
|
25
|
+
|
|
26
|
+
private struct PendingDisplay {
|
|
27
|
+
let completion: (UNNotificationPresentationOptions) -> Void
|
|
28
|
+
let options: UNNotificationPresentationOptions
|
|
29
|
+
var prevented: Bool
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private weak var emitter: QPushPush?
|
|
33
|
+
private var jsReady = false
|
|
34
|
+
private var pendingEvents: [(name: String, body: [String: Any])] = []
|
|
35
|
+
private var pendingDisplays: [String: PendingDisplay] = [:]
|
|
36
|
+
private var registrationWaiters: [([String: Any]) -> Void] = []
|
|
37
|
+
private var lastToken: String?
|
|
38
|
+
private var backgroundTasks: [Int: UIBackgroundTaskIdentifier] = [:]
|
|
39
|
+
|
|
40
|
+
private override init() {
|
|
41
|
+
super.init()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// MARK: - Module lifecycle
|
|
45
|
+
|
|
46
|
+
func attach(_ module: QPushPush) {
|
|
47
|
+
onMain {
|
|
48
|
+
self.emitter = module
|
|
49
|
+
self.jsReady = false
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func setJsReady() {
|
|
54
|
+
onMain {
|
|
55
|
+
self.jsReady = true
|
|
56
|
+
let events = self.pendingEvents
|
|
57
|
+
self.pendingEvents.removeAll()
|
|
58
|
+
for event in events {
|
|
59
|
+
self.emit(event.name, event.body)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private func canSendToJs() -> Bool {
|
|
65
|
+
guard jsReady, let emitter = emitter else { return false }
|
|
66
|
+
return emitter.hasListeners
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Emits now when JS is listening, otherwise queues (cold-start clicks).
|
|
70
|
+
func emit(_ name: String, _ body: [String: Any]) {
|
|
71
|
+
onMain {
|
|
72
|
+
if self.canSendToJs(), let emitter = self.emitter {
|
|
73
|
+
emitter.sendEvent(withName: name, body: body)
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
self.pendingEvents.append((name: name, body: body))
|
|
77
|
+
if self.pendingEvents.count > QPushPushCore.maxPendingEvents {
|
|
78
|
+
self.pendingEvents.removeFirst()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// MARK: - APNs registration (called from the swizzled app delegate)
|
|
84
|
+
|
|
85
|
+
@objc(handleDeviceToken:)
|
|
86
|
+
public static func handleDeviceToken(_ deviceToken: Data) {
|
|
87
|
+
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
|
|
88
|
+
shared.didRegister(token: token)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@objc(handleRegistrationError:)
|
|
92
|
+
public static func handleRegistrationError(_ error: NSError) {
|
|
93
|
+
shared.didFailToRegister(error)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private func didRegister(token: String) {
|
|
97
|
+
onMain {
|
|
98
|
+
QPushLog.debug("APNs token received")
|
|
99
|
+
let changed = self.lastToken != token
|
|
100
|
+
self.lastToken = token
|
|
101
|
+
self.resolveRegistration(["token": token])
|
|
102
|
+
if changed { self.emit(QPushPushCore.pushTokenEvent, ["token": token]) }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private func didFailToRegister(_ error: NSError) {
|
|
107
|
+
onMain {
|
|
108
|
+
QPushLog.error("APNs registration failed: \(error.localizedDescription)")
|
|
109
|
+
self.resolveRegistration([
|
|
110
|
+
"token": NSNull(),
|
|
111
|
+
"notificationTypes": -10,
|
|
112
|
+
"error": error.localizedDescription,
|
|
113
|
+
])
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private func resolveRegistration(_ result: [String: Any]) {
|
|
118
|
+
let waiters = registrationWaiters
|
|
119
|
+
registrationWaiters.removeAll()
|
|
120
|
+
for waiter in waiters { waiter(result) }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Calls `registerForRemoteNotifications`; resolves with the hex token or an error.
|
|
124
|
+
func registerForPush(_ completion: @escaping ([String: Any]) -> Void) {
|
|
125
|
+
onMain {
|
|
126
|
+
var finished = false
|
|
127
|
+
let waiter: ([String: Any]) -> Void = { result in
|
|
128
|
+
if finished { return }
|
|
129
|
+
finished = true
|
|
130
|
+
completion(result)
|
|
131
|
+
}
|
|
132
|
+
self.registrationWaiters.append(waiter)
|
|
133
|
+
UIApplication.shared.registerForRemoteNotifications()
|
|
134
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + QPushPushCore.registrationTimeout) {
|
|
135
|
+
guard !finished else { return }
|
|
136
|
+
let result: [String: Any] = [
|
|
137
|
+
"error": "Timed out waiting for the APNs token",
|
|
138
|
+
"token": QPushPushCore.orNull(self.lastToken),
|
|
139
|
+
]
|
|
140
|
+
waiter(result)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// MARK: - Permission
|
|
146
|
+
|
|
147
|
+
static func permissionString(_ status: UNAuthorizationStatus) -> String {
|
|
148
|
+
switch status {
|
|
149
|
+
case .authorized:
|
|
150
|
+
return "granted"
|
|
151
|
+
case .denied:
|
|
152
|
+
return "denied"
|
|
153
|
+
case .notDetermined:
|
|
154
|
+
return "notDetermined"
|
|
155
|
+
case .provisional:
|
|
156
|
+
return "provisional"
|
|
157
|
+
default:
|
|
158
|
+
if #available(iOS 14.0, *), status == .ephemeral {
|
|
159
|
+
return "ephemeral"
|
|
160
|
+
}
|
|
161
|
+
return "notDetermined"
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
func permissionStatus(_ completion: @escaping (String) -> Void) {
|
|
166
|
+
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
|
167
|
+
completion(QPushPushCore.permissionString(settings.authorizationStatus))
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private static func isGranted(_ status: String) -> Bool {
|
|
172
|
+
return status == "granted" || status == "provisional" || status == "ephemeral"
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
func requestPermission(fallbackToSettings: Bool, completion: @escaping (Bool) -> Void) {
|
|
176
|
+
let center = UNUserNotificationCenter.current()
|
|
177
|
+
center.getNotificationSettings { settings in
|
|
178
|
+
switch settings.authorizationStatus {
|
|
179
|
+
case .notDetermined, .provisional:
|
|
180
|
+
center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
|
|
181
|
+
if let error = error { QPushLog.warn("requestAuthorization failed: \(error.localizedDescription)") }
|
|
182
|
+
if granted {
|
|
183
|
+
DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() }
|
|
184
|
+
}
|
|
185
|
+
completion(granted)
|
|
186
|
+
}
|
|
187
|
+
case .denied:
|
|
188
|
+
if fallbackToSettings {
|
|
189
|
+
self.openSettingsThenCheck(completion)
|
|
190
|
+
} else {
|
|
191
|
+
completion(false)
|
|
192
|
+
}
|
|
193
|
+
default:
|
|
194
|
+
completion(true)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
func registerForProvisionalAuthorization(_ completion: @escaping (Bool) -> Void) {
|
|
200
|
+
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge, .provisional]) { granted, _ in
|
|
201
|
+
if granted {
|
|
202
|
+
DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() }
|
|
203
|
+
}
|
|
204
|
+
completion(granted)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
func canRequestPermission(_ completion: @escaping (Bool) -> Void) {
|
|
209
|
+
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
|
210
|
+
completion(settings.authorizationStatus == .notDetermined || settings.authorizationStatus == .provisional)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// Opens the app's notification settings and resolves when the user comes back.
|
|
215
|
+
private func openSettingsThenCheck(_ completion: @escaping (Bool) -> Void) {
|
|
216
|
+
DispatchQueue.main.async {
|
|
217
|
+
let urlString: String
|
|
218
|
+
if #available(iOS 16.0, *) {
|
|
219
|
+
urlString = UIApplication.openNotificationSettingsURLString
|
|
220
|
+
} else {
|
|
221
|
+
urlString = UIApplication.openSettingsURLString
|
|
222
|
+
}
|
|
223
|
+
guard let url = URL(string: urlString) else {
|
|
224
|
+
completion(false)
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
var observer: NSObjectProtocol?
|
|
228
|
+
observer = NotificationCenter.default.addObserver(
|
|
229
|
+
forName: UIApplication.didBecomeActiveNotification,
|
|
230
|
+
object: nil,
|
|
231
|
+
queue: .main
|
|
232
|
+
) { _ in
|
|
233
|
+
if let token = observer { NotificationCenter.default.removeObserver(token) }
|
|
234
|
+
observer = nil
|
|
235
|
+
self.permissionStatus { status in completion(QPushPushCore.isGranted(status)) }
|
|
236
|
+
}
|
|
237
|
+
UIApplication.shared.open(url, options: [:]) { opened in
|
|
238
|
+
if !opened {
|
|
239
|
+
if let token = observer { NotificationCenter.default.removeObserver(token) }
|
|
240
|
+
observer = nil
|
|
241
|
+
completion(false)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// MARK: - Foreground presentation (called from the UNUserNotificationCenter proxy)
|
|
248
|
+
|
|
249
|
+
@objc(handleWillPresent:completionHandler:)
|
|
250
|
+
public static func handleWillPresent(
|
|
251
|
+
_ notification: UNNotification,
|
|
252
|
+
completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
|
253
|
+
) {
|
|
254
|
+
shared.willPresent(notification, completion: completionHandler)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
static func defaultPresentationOptions() -> UNNotificationPresentationOptions {
|
|
258
|
+
if #available(iOS 14.0, *) {
|
|
259
|
+
return [.banner, .list, .sound, .badge]
|
|
260
|
+
}
|
|
261
|
+
return [.alert, .sound, .badge]
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private func willPresent(_ notification: UNNotification, completion: @escaping (UNNotificationPresentationOptions) -> Void) {
|
|
265
|
+
let userInfo = notification.request.content.userInfo
|
|
266
|
+
// Receipt fallback for apps without the Notification Service Extension (idempotent).
|
|
267
|
+
if let notificationId = QPushPushCore.notificationId(userInfo) {
|
|
268
|
+
QPushHTTP.reportReceived(notificationId: notificationId)
|
|
269
|
+
}
|
|
270
|
+
let options = QPushPushCore.defaultPresentationOptions()
|
|
271
|
+
onMain {
|
|
272
|
+
guard self.canSendToJs() else {
|
|
273
|
+
completion(options)
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
let displayId = UUID().uuidString
|
|
277
|
+
self.pendingDisplays[displayId] = PendingDisplay(completion: completion, options: options, prevented: false)
|
|
278
|
+
self.emit(QPushPushCore.foregroundWillDisplayEvent, [
|
|
279
|
+
"displayId": displayId,
|
|
280
|
+
"rawPayload": QPushPushCore.jsonSafe(userInfo),
|
|
281
|
+
])
|
|
282
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + QPushPushCore.foregroundResponseTimeout) {
|
|
283
|
+
self.finishDisplay(displayId, show: true, onlyIfNotPrevented: true)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
func displayForeground(_ displayId: String) {
|
|
289
|
+
onMain { self.finishDisplay(displayId, show: true, onlyIfNotPrevented: false) }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
func preventForeground(_ displayId: String) {
|
|
293
|
+
onMain {
|
|
294
|
+
guard var pending = self.pendingDisplays[displayId] else { return }
|
|
295
|
+
pending.prevented = true
|
|
296
|
+
self.pendingDisplays[displayId] = pending
|
|
297
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + QPushPushCore.preventedExpiry) {
|
|
298
|
+
self.finishDisplay(displayId, show: false, onlyIfNotPrevented: false)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private func finishDisplay(_ displayId: String, show: Bool, onlyIfNotPrevented: Bool) {
|
|
304
|
+
guard let pending = pendingDisplays[displayId] else { return }
|
|
305
|
+
if onlyIfNotPrevented && pending.prevented { return }
|
|
306
|
+
pendingDisplays.removeValue(forKey: displayId)
|
|
307
|
+
pending.completion(show ? pending.options : [])
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// MARK: - Click (called from the UNUserNotificationCenter proxy)
|
|
311
|
+
|
|
312
|
+
@objc(handleDidReceive:completionHandler:)
|
|
313
|
+
public static func handleDidReceive(_ response: UNNotificationResponse, completionHandler: @escaping () -> Void) {
|
|
314
|
+
shared.didReceive(response, completion: completionHandler)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private func didReceive(_ response: UNNotificationResponse, completion: @escaping () -> Void) {
|
|
318
|
+
let userInfo = response.notification.request.content.userInfo
|
|
319
|
+
guard let qpush = userInfo["qpush"] as? [String: Any],
|
|
320
|
+
let notificationId = QPushPushCore.notificationId(userInfo)
|
|
321
|
+
else {
|
|
322
|
+
completion()
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
var actionId: String?
|
|
326
|
+
switch response.actionIdentifier {
|
|
327
|
+
case UNNotificationDefaultActionIdentifier:
|
|
328
|
+
actionId = nil
|
|
329
|
+
case UNNotificationDismissActionIdentifier:
|
|
330
|
+
completion()
|
|
331
|
+
return
|
|
332
|
+
default:
|
|
333
|
+
actionId = response.actionIdentifier
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let buttons = qpush["buttons"] as? [[String: Any]] ?? []
|
|
337
|
+
let buttonUrl = buttons.first { ($0["id"] as? String) == actionId && actionId != nil }?["url"] as? String
|
|
338
|
+
let url = (buttonUrl?.isEmpty == false ? buttonUrl : nil) ?? (qpush["url"] as? String)
|
|
339
|
+
|
|
340
|
+
QPushHTTP.reportOpened(notificationId: notificationId, actionId: actionId)
|
|
341
|
+
emit(QPushPushCore.clickEvent, [
|
|
342
|
+
"rawPayload": QPushPushCore.jsonSafe(userInfo),
|
|
343
|
+
"actionId": QPushPushCore.orNull(actionId),
|
|
344
|
+
"url": QPushPushCore.orNull(url),
|
|
345
|
+
])
|
|
346
|
+
|
|
347
|
+
if let url = url, !url.isEmpty, QPushStorage.shared.autoOpenUrls, let target = URL(string: url) {
|
|
348
|
+
DispatchQueue.main.async {
|
|
349
|
+
UIApplication.shared.open(target, options: [:], completionHandler: nil)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
completion()
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// MARK: - Notification center helpers
|
|
356
|
+
|
|
357
|
+
func clearAll() {
|
|
358
|
+
UNUserNotificationCenter.current().removeAllDeliveredNotifications()
|
|
359
|
+
onMain { self.setBadge(0) }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
func removeNotification(_ notificationId: String) {
|
|
363
|
+
let center = UNUserNotificationCenter.current()
|
|
364
|
+
center.getDeliveredNotifications { notifications in
|
|
365
|
+
let identifiers = notifications
|
|
366
|
+
.filter { QPushPushCore.notificationId($0.request.content.userInfo) == notificationId }
|
|
367
|
+
.map { $0.request.identifier }
|
|
368
|
+
if !identifiers.isEmpty { center.removeDeliveredNotifications(withIdentifiers: identifiers) }
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
func removeGroupedNotifications(_ groupKey: String) {
|
|
373
|
+
let center = UNUserNotificationCenter.current()
|
|
374
|
+
center.getDeliveredNotifications { notifications in
|
|
375
|
+
let identifiers = notifications
|
|
376
|
+
.filter { $0.request.content.threadIdentifier == groupKey }
|
|
377
|
+
.map { $0.request.identifier }
|
|
378
|
+
if !identifiers.isEmpty { center.removeDeliveredNotifications(withIdentifiers: identifiers) }
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/// Clears the badge and returns the previous value.
|
|
383
|
+
func clearBadge(_ completion: @escaping (Int) -> Void) {
|
|
384
|
+
onMain {
|
|
385
|
+
let previous = UIApplication.shared.applicationIconBadgeNumber
|
|
386
|
+
if previous > 0 { self.setBadge(0) }
|
|
387
|
+
completion(previous)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private func setBadge(_ value: Int) {
|
|
392
|
+
if #available(iOS 16.0, *) {
|
|
393
|
+
UNUserNotificationCenter.current().setBadgeCount(value) { error in
|
|
394
|
+
if let error = error { QPushLog.warn("setBadgeCount failed: \(error.localizedDescription)") }
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
UIApplication.shared.applicationIconBadgeNumber = value
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// MARK: - Background tasks (lets the JS queue flush after backgrounding)
|
|
402
|
+
|
|
403
|
+
func beginBackgroundTask(_ completion: @escaping (Int) -> Void) {
|
|
404
|
+
onMain {
|
|
405
|
+
var identifier = UIBackgroundTaskIdentifier.invalid
|
|
406
|
+
identifier = UIApplication.shared.beginBackgroundTask(withName: "QPushFlush") {
|
|
407
|
+
self.endBackgroundTask(identifier.rawValue)
|
|
408
|
+
}
|
|
409
|
+
if identifier == .invalid {
|
|
410
|
+
completion(-1)
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
self.backgroundTasks[identifier.rawValue] = identifier
|
|
414
|
+
completion(identifier.rawValue)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
func endBackgroundTask(_ taskId: Int) {
|
|
419
|
+
onMain {
|
|
420
|
+
guard let identifier = self.backgroundTasks.removeValue(forKey: taskId) else { return }
|
|
421
|
+
UIApplication.shared.endBackgroundTask(identifier)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// MARK: - Utilities
|
|
426
|
+
|
|
427
|
+
/// `value` or `NSNull` (serialized as `null` by the bridge).
|
|
428
|
+
static func orNull(_ value: String?) -> Any {
|
|
429
|
+
if let value = value { return value }
|
|
430
|
+
return NSNull()
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
static func notificationId(_ userInfo: [AnyHashable: Any]) -> String? {
|
|
434
|
+
guard let qpush = userInfo["qpush"] as? [String: Any],
|
|
435
|
+
let notificationId = qpush["id"] as? String,
|
|
436
|
+
!notificationId.isEmpty
|
|
437
|
+
else { return nil }
|
|
438
|
+
return notificationId
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/// `userInfo` with string keys only (what the RN bridge can serialize).
|
|
442
|
+
static func jsonSafe(_ userInfo: [AnyHashable: Any]) -> [String: Any] {
|
|
443
|
+
var result: [String: Any] = [:]
|
|
444
|
+
for (key, value) in userInfo {
|
|
445
|
+
if let key = key as? String { result[key] = value }
|
|
446
|
+
}
|
|
447
|
+
return result
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private func onMain(_ block: @escaping () -> Void) {
|
|
451
|
+
if Thread.isMainThread {
|
|
452
|
+
block()
|
|
453
|
+
} else {
|
|
454
|
+
DispatchQueue.main.async(execute: block)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// Captures the APNs token and notification callbacks without requiring the
|
|
2
|
+
// developer to edit the AppDelegate (same approach as OneSignal):
|
|
3
|
+
//
|
|
4
|
+
// 1. +load swizzles -[UIApplication setDelegate:]. When UIKit installs the app
|
|
5
|
+
// delegate (inside UIApplicationMain, before didFinishLaunching), the
|
|
6
|
+
// delegate class gets didRegisterForRemoteNotificationsWithDeviceToken: and
|
|
7
|
+
// didFailToRegisterForRemoteNotificationsWithError: hooks that call QPush
|
|
8
|
+
// first and then the app's own implementation (if any).
|
|
9
|
+
// 2. -[UNUserNotificationCenter setDelegate:] is swizzled so a proxy owned by
|
|
10
|
+
// QPush always stays the delegate. Whatever delegate the app or another
|
|
11
|
+
// library sets is kept as the proxy's forward target: QPush notifications
|
|
12
|
+
// are handled by QPush, everything else is forwarded untouched.
|
|
13
|
+
//
|
|
14
|
+
// The Swift side (`QPushPushCore`) is reached through the Objective-C runtime
|
|
15
|
+
// so this file does not depend on the generated "-Swift.h" header name.
|
|
16
|
+
|
|
17
|
+
#import <Foundation/Foundation.h>
|
|
18
|
+
#import <UIKit/UIKit.h>
|
|
19
|
+
#import <UserNotifications/UserNotifications.h>
|
|
20
|
+
#import <objc/message.h>
|
|
21
|
+
#import <objc/runtime.h>
|
|
22
|
+
|
|
23
|
+
#pragma mark - Swift entry points
|
|
24
|
+
|
|
25
|
+
/// Class methods of the Swift `QPushPushCore` (`@objc` names), looked up at
|
|
26
|
+
/// runtime. Declared here only so the selectors are known to the compiler.
|
|
27
|
+
@protocol QPushPushCoreEntryPoints
|
|
28
|
+
+ (void)handleDeviceToken:(NSData *)deviceToken;
|
|
29
|
+
+ (void)handleRegistrationError:(NSError *)error;
|
|
30
|
+
+ (void)handleWillPresent:(UNNotification *)notification
|
|
31
|
+
completionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler;
|
|
32
|
+
+ (void)handleDidReceive:(UNNotificationResponse *)response completionHandler:(void (^)(void))completionHandler;
|
|
33
|
+
@end
|
|
34
|
+
|
|
35
|
+
static Class QPushCoreClass(void) {
|
|
36
|
+
static Class cls = Nil;
|
|
37
|
+
if (cls == Nil) {
|
|
38
|
+
cls = NSClassFromString(@"QPushPushCore");
|
|
39
|
+
}
|
|
40
|
+
return cls;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static BOOL QPushCoreSend1(SEL selector, id argument) {
|
|
44
|
+
Class cls = QPushCoreClass();
|
|
45
|
+
if (cls == Nil || ![cls respondsToSelector:selector]) {
|
|
46
|
+
return NO;
|
|
47
|
+
}
|
|
48
|
+
((void (*)(id, SEL, id))objc_msgSend)(cls, selector, argument);
|
|
49
|
+
return YES;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static BOOL QPushCoreSend2(SEL selector, id first, id second) {
|
|
53
|
+
Class cls = QPushCoreClass();
|
|
54
|
+
if (cls == Nil || ![cls respondsToSelector:selector]) {
|
|
55
|
+
return NO;
|
|
56
|
+
}
|
|
57
|
+
((void (*)(id, SEL, id, id))objc_msgSend)(cls, selector, first, second);
|
|
58
|
+
return YES;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static BOOL QPushIsQPushNotification(UNNotification *notification) {
|
|
62
|
+
id qpush = notification.request.content.userInfo[@"qpush"];
|
|
63
|
+
return [qpush isKindOfClass:[NSDictionary class]];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#pragma mark - UNUserNotificationCenter delegate proxy
|
|
67
|
+
|
|
68
|
+
@interface QPushNotificationCenterProxy : NSObject <UNUserNotificationCenterDelegate>
|
|
69
|
+
@property (nonatomic, weak) id<UNUserNotificationCenterDelegate> forwardDelegate;
|
|
70
|
+
+ (instancetype)shared;
|
|
71
|
+
@end
|
|
72
|
+
|
|
73
|
+
@implementation QPushNotificationCenterProxy
|
|
74
|
+
|
|
75
|
+
+ (instancetype)shared {
|
|
76
|
+
static QPushNotificationCenterProxy *instance = nil;
|
|
77
|
+
static dispatch_once_t onceToken;
|
|
78
|
+
dispatch_once(&onceToken, ^{
|
|
79
|
+
instance = [[QPushNotificationCenterProxy alloc] init];
|
|
80
|
+
});
|
|
81
|
+
return instance;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
- (BOOL)respondsToSelector:(SEL)aSelector {
|
|
85
|
+
if ([super respondsToSelector:aSelector]) {
|
|
86
|
+
return YES;
|
|
87
|
+
}
|
|
88
|
+
id forward = self.forwardDelegate;
|
|
89
|
+
return forward != nil && [forward respondsToSelector:aSelector];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
- (id)forwardingTargetForSelector:(SEL)aSelector {
|
|
93
|
+
id forward = self.forwardDelegate;
|
|
94
|
+
if (forward != nil && [forward respondsToSelector:aSelector]) {
|
|
95
|
+
return forward;
|
|
96
|
+
}
|
|
97
|
+
return [super forwardingTargetForSelector:aSelector];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
|
101
|
+
willPresentNotification:(UNNotification *)notification
|
|
102
|
+
withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler {
|
|
103
|
+
if (QPushIsQPushNotification(notification) &&
|
|
104
|
+
QPushCoreSend2(@selector(handleWillPresent:completionHandler:), notification, completionHandler)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
id<UNUserNotificationCenterDelegate> forward = self.forwardDelegate;
|
|
108
|
+
if (forward != nil && [forward respondsToSelector:_cmd]) {
|
|
109
|
+
[forward userNotificationCenter:center willPresentNotification:notification withCompletionHandler:completionHandler];
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
completionHandler(UNNotificationPresentationOptionNone);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
|
116
|
+
didReceiveNotificationResponse:(UNNotificationResponse *)response
|
|
117
|
+
withCompletionHandler:(void (^)(void))completionHandler {
|
|
118
|
+
if (QPushIsQPushNotification(response.notification) &&
|
|
119
|
+
QPushCoreSend2(@selector(handleDidReceive:completionHandler:), response, completionHandler)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
id<UNUserNotificationCenterDelegate> forward = self.forwardDelegate;
|
|
123
|
+
if (forward != nil && [forward respondsToSelector:_cmd]) {
|
|
124
|
+
[forward userNotificationCenter:center didReceiveNotificationResponse:response withCompletionHandler:completionHandler];
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
completionHandler();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@end
|
|
131
|
+
|
|
132
|
+
#pragma mark - Swizzling
|
|
133
|
+
|
|
134
|
+
static IMP QPushOriginalAppSetDelegate = NULL;
|
|
135
|
+
static IMP QPushOriginalCenterSetDelegate = NULL;
|
|
136
|
+
static NSMutableSet<NSString *> *QPushHookedDelegateClasses = nil;
|
|
137
|
+
|
|
138
|
+
static void QPushCenterSetDelegate(id self, SEL _cmd, id<UNUserNotificationCenterDelegate> delegate) {
|
|
139
|
+
QPushNotificationCenterProxy *proxy = [QPushNotificationCenterProxy shared];
|
|
140
|
+
if (delegate != (id)proxy) {
|
|
141
|
+
proxy.forwardDelegate = delegate;
|
|
142
|
+
}
|
|
143
|
+
((void (*)(id, SEL, id))QPushOriginalCenterSetDelegate)(self, _cmd, proxy);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static void QPushInstallNotificationCenterProxy(void) {
|
|
147
|
+
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
|
148
|
+
id current = center.delegate;
|
|
149
|
+
if (current != (id)[QPushNotificationCenterProxy shared]) {
|
|
150
|
+
// Goes through QPushCenterSetDelegate: `current` becomes the forward target.
|
|
151
|
+
center.delegate = current;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// Adds (or wraps) `selector` on `cls`; QPush's block runs first, then the original.
|
|
156
|
+
static void QPushHookTokenSuccess(Class cls) {
|
|
157
|
+
SEL selector = @selector(application:didRegisterForRemoteNotificationsWithDeviceToken:);
|
|
158
|
+
Method existing = class_getInstanceMethod(cls, selector);
|
|
159
|
+
__block IMP original = existing ? method_getImplementation(existing) : NULL;
|
|
160
|
+
IMP replacement = imp_implementationWithBlock(^(id receiver, UIApplication *application, NSData *deviceToken) {
|
|
161
|
+
QPushCoreSend1(@selector(handleDeviceToken:), deviceToken);
|
|
162
|
+
if (original != NULL) {
|
|
163
|
+
((void (*)(id, SEL, UIApplication *, NSData *))original)(receiver, selector, application, deviceToken);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
const char *types = existing ? method_getTypeEncoding(existing) : "v@:@@";
|
|
167
|
+
if (!class_addMethod(cls, selector, replacement, types)) {
|
|
168
|
+
// The class implements it itself: replace it and keep its implementation.
|
|
169
|
+
original = method_setImplementation(class_getInstanceMethod(cls, selector), replacement);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
static void QPushHookTokenFailure(Class cls) {
|
|
174
|
+
SEL selector = @selector(application:didFailToRegisterForRemoteNotificationsWithError:);
|
|
175
|
+
Method existing = class_getInstanceMethod(cls, selector);
|
|
176
|
+
__block IMP original = existing ? method_getImplementation(existing) : NULL;
|
|
177
|
+
IMP replacement = imp_implementationWithBlock(^(id receiver, UIApplication *application, NSError *error) {
|
|
178
|
+
QPushCoreSend1(@selector(handleRegistrationError:), error);
|
|
179
|
+
if (original != NULL) {
|
|
180
|
+
((void (*)(id, SEL, UIApplication *, NSError *))original)(receiver, selector, application, error);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
const char *types = existing ? method_getTypeEncoding(existing) : "v@:@@";
|
|
184
|
+
if (!class_addMethod(cls, selector, replacement, types)) {
|
|
185
|
+
original = method_setImplementation(class_getInstanceMethod(cls, selector), replacement);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
static void QPushHookAppDelegateClass(Class cls) {
|
|
190
|
+
if (cls == Nil) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
NSString *name = NSStringFromClass(cls);
|
|
194
|
+
@synchronized([QPushNotificationCenterProxy class]) {
|
|
195
|
+
if (QPushHookedDelegateClasses == nil) {
|
|
196
|
+
QPushHookedDelegateClasses = [NSMutableSet set];
|
|
197
|
+
}
|
|
198
|
+
if ([QPushHookedDelegateClasses containsObject:name]) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
[QPushHookedDelegateClasses addObject:name];
|
|
202
|
+
}
|
|
203
|
+
QPushHookTokenSuccess(cls);
|
|
204
|
+
QPushHookTokenFailure(cls);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
static void QPushAppSetDelegate(id self, SEL _cmd, id<UIApplicationDelegate> delegate) {
|
|
208
|
+
// Hook before UIKit caches which delegate methods are implemented.
|
|
209
|
+
if (delegate != nil) {
|
|
210
|
+
QPushHookAppDelegateClass(object_getClass(delegate));
|
|
211
|
+
}
|
|
212
|
+
((void (*)(id, SEL, id))QPushOriginalAppSetDelegate)(self, _cmd, delegate);
|
|
213
|
+
// Must be in place before didFinishLaunching returns to receive the
|
|
214
|
+
// response of the notification that launched the app.
|
|
215
|
+
QPushInstallNotificationCenterProxy();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
@interface QPushPushHooks : NSObject
|
|
219
|
+
@end
|
|
220
|
+
|
|
221
|
+
@implementation QPushPushHooks
|
|
222
|
+
|
|
223
|
+
+ (void)load {
|
|
224
|
+
static dispatch_once_t onceToken;
|
|
225
|
+
dispatch_once(&onceToken, ^{
|
|
226
|
+
Method centerSetDelegate = class_getInstanceMethod([UNUserNotificationCenter class], @selector(setDelegate:));
|
|
227
|
+
if (centerSetDelegate != NULL) {
|
|
228
|
+
QPushOriginalCenterSetDelegate = method_setImplementation(centerSetDelegate, (IMP)QPushCenterSetDelegate);
|
|
229
|
+
}
|
|
230
|
+
Method appSetDelegate = class_getInstanceMethod([UIApplication class], @selector(setDelegate:));
|
|
231
|
+
if (appSetDelegate != NULL) {
|
|
232
|
+
QPushOriginalAppSetDelegate = method_setImplementation(appSetDelegate, (IMP)QPushAppSetDelegate);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
@end
|