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.
Files changed (98) hide show
  1. package/README.md +439 -2
  2. package/android/build.gradle +77 -0
  3. package/android/gradle.properties +5 -0
  4. package/android/proguard-rules.pro +5 -0
  5. package/android/src/main/AndroidManifest.xml +29 -0
  6. package/android/src/main/java/com/qpush/reactnative/QPushDeviceInfo.kt +96 -0
  7. package/android/src/main/java/com/qpush/reactnative/QPushEventBus.kt +153 -0
  8. package/android/src/main/java/com/qpush/reactnative/QPushFcm.kt +108 -0
  9. package/android/src/main/java/com/qpush/reactnative/QPushHttp.kt +92 -0
  10. package/android/src/main/java/com/qpush/reactnative/QPushImages.kt +103 -0
  11. package/android/src/main/java/com/qpush/reactnative/QPushLog.kt +39 -0
  12. package/android/src/main/java/com/qpush/reactnative/QPushMessagingService.kt +77 -0
  13. package/android/src/main/java/com/qpush/reactnative/QPushNotificationOpenActivity.kt +121 -0
  14. package/android/src/main/java/com/qpush/reactnative/QPushNotificationRenderer.kt +256 -0
  15. package/android/src/main/java/com/qpush/reactnative/QPushPayload.kt +114 -0
  16. package/android/src/main/java/com/qpush/reactnative/QPushPushModule.kt +368 -0
  17. package/android/src/main/java/com/qpush/reactnative/QPushPushPackage.kt +15 -0
  18. package/android/src/main/java/com/qpush/reactnative/QPushStore.kt +108 -0
  19. package/android/src/main/res/values/qpush_strings.xml +6 -0
  20. package/app.plugin.js +1 -0
  21. package/ios/Extension/QPushNotificationServiceExtension.swift +261 -0
  22. package/ios/Extension/QPushPushExtension.podspec +34 -0
  23. package/ios/QPushPush.m +62 -0
  24. package/ios/QPushPush.swift +186 -0
  25. package/ios/QPushPushBridge.h +5 -0
  26. package/ios/QPushPushCore.swift +457 -0
  27. package/ios/QPushPushHooks.m +237 -0
  28. package/ios/QPushPushSupport.swift +249 -0
  29. package/ios_nse_template/Info.plist +38 -0
  30. package/ios_nse_template/NotificationService.entitlements +10 -0
  31. package/ios_nse_template/NotificationService.swift +44 -0
  32. package/lib/NativeQPushPush.d.ts +44 -0
  33. package/lib/NativeQPushPush.js +4 -0
  34. package/lib/QPush.d.ts +117 -0
  35. package/lib/QPush.js +206 -0
  36. package/lib/core/QPushCore.d.ts +148 -0
  37. package/lib/core/QPushCore.js +1387 -0
  38. package/lib/core/constants.d.ts +37 -0
  39. package/lib/core/constants.js +40 -0
  40. package/lib/core/events.d.ts +11 -0
  41. package/lib/core/events.js +40 -0
  42. package/lib/core/http.d.ts +41 -0
  43. package/lib/core/http.js +123 -0
  44. package/lib/core/logger.d.ts +18 -0
  45. package/lib/core/logger.js +43 -0
  46. package/lib/core/operationQueue.d.ts +117 -0
  47. package/lib/core/operationQueue.js +264 -0
  48. package/lib/core/operations.d.ts +80 -0
  49. package/lib/core/operations.js +88 -0
  50. package/lib/core/payload.d.ts +20 -0
  51. package/lib/core/payload.js +187 -0
  52. package/lib/core/platform.d.ts +72 -0
  53. package/lib/core/platform.js +7 -0
  54. package/lib/core/state.d.ts +51 -0
  55. package/lib/core/state.js +74 -0
  56. package/lib/core/storage.d.ts +34 -0
  57. package/lib/core/storage.js +68 -0
  58. package/lib/core/subscription.d.ts +67 -0
  59. package/lib/core/subscription.js +98 -0
  60. package/lib/core/tags.d.ts +39 -0
  61. package/lib/core/tags.js +123 -0
  62. package/lib/core/url.d.ts +17 -0
  63. package/lib/core/url.js +64 -0
  64. package/lib/core/uuid.d.ts +2 -0
  65. package/lib/core/uuid.js +18 -0
  66. package/lib/index.d.ts +7 -0
  67. package/lib/index.js +15 -0
  68. package/lib/notification.d.ts +67 -0
  69. package/lib/notification.js +73 -0
  70. package/lib/reactNativePlatform.d.ts +3 -0
  71. package/lib/reactNativePlatform.js +164 -0
  72. package/lib/types.d.ts +157 -0
  73. package/lib/types.js +31 -0
  74. package/package.json +70 -4
  75. package/plugin/withQPush.js +330 -0
  76. package/react-native-qpush.podspec +30 -0
  77. package/react-native.config.js +18 -0
  78. package/src/NativeQPushPush.ts +57 -0
  79. package/src/QPush.ts +292 -0
  80. package/src/core/QPushCore.ts +1515 -0
  81. package/src/core/constants.ts +42 -0
  82. package/src/core/events.ts +34 -0
  83. package/src/core/http.ts +146 -0
  84. package/src/core/logger.ts +42 -0
  85. package/src/core/operationQueue.ts +324 -0
  86. package/src/core/operations.ts +166 -0
  87. package/src/core/payload.ts +175 -0
  88. package/src/core/platform.ts +84 -0
  89. package/src/core/state.ts +127 -0
  90. package/src/core/storage.ts +76 -0
  91. package/src/core/subscription.ts +133 -0
  92. package/src/core/tags.ts +116 -0
  93. package/src/core/url.ts +58 -0
  94. package/src/core/uuid.ts +13 -0
  95. package/src/index.ts +23 -0
  96. package/src/notification.ts +113 -0
  97. package/src/reactNativePlatform.ts +172 -0
  98. 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