also looking at this
fix(ios): hand a notification tap that launches the app to the page that asks for it
#255
5 files
+194
-9
| @@ -34,7 +34,6 @@ import ActivityKit | ||
| 34 | 34 | extension Notification.Name { |
| 35 | 35 | static let craftPushToken = Notification.Name("craftPushToken") |
| 36 | 36 | static let craftPushRegistrationError = Notification.Name("craftPushRegistrationError") |
| 37 | static let craftNotificationResponse = Notification.Name("craftNotificationResponse") | |
| 38 | 37 | } |
| 39 | 38 | |
| 40 | 39 | /// Which failed page loads mean the remote origin is out of reach (#252). |
| @@ -104,7 +103,14 @@ final class CraftAppDelegate: NSObject, UIApplicationDelegate, UNUserNotificatio | ||
| 104 | 103 | } |
| 105 | 104 | |
| 106 | 105 | func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { |
| 107 | NotificationCenter.default.post(name: .craftNotificationResponse, object: response.notification.request.content.userInfo) | |
| 106 | // Through the manager, not NotificationCenter. A tap that launches a | |
| 107 | // killed app arrives here before SwiftUI has built the Coordinator, and | |
| 108 | // NotificationCenter keeps nothing for an observer that does not exist | |
| 109 | // yet — so the tap was dropped, and the page opened wherever it opens | |
| 110 | // rather than where the notification pointed. The manager exists from | |
| 111 | // process start and holds the tap until the page is ready, the way it | |
| 112 | // already does for a home-screen shortcut. | |
| 113 | CraftEventManager.shared.handleNotificationResponse(response.notification.request.content.userInfo) | |
| 108 | 114 | completionHandler() |
| 109 | 115 | } |
| 110 | 116 | |
| @@ -144,6 +150,20 @@ class CraftEventManager { | ||
| 144 | 150 | sendToWeb("craftShortcut", data: ["type": shortcut.type]) |
| 145 | 151 | } |
| 146 | 152 | |
| 153 | /// A tap on a notification, carrying the payload it was sent with. | |
| 154 | /// | |
| 155 | /// Keys that are not strings are dropped rather than failing the whole | |
| 156 | /// event: an APNs payload's keys always are, and the page could not index | |
| 157 | /// by anything else anyway. | |
| 158 | func handleNotificationResponse(_ userInfo: [AnyHashable: Any]) { | |
| 159 | var data: [String: Any] = [:] | |
| 160 | for (key, value) in userInfo { | |
| 161 | guard let key = key as? String else { continue } | |
| 162 | data[key] = value | |
| 163 | } | |
| 164 | sendToWeb("craftNotificationResponse", data: data) | |
| 165 | } | |
| 166 | ||
| 147 | 167 | func handleSiriActivity(_ activity: NSUserActivity) -> Bool { |
| 148 | 168 | let prefix = "\(Bundle.main.bundleIdentifier ?? "{{BUNDLE_ID}}")." |
| 149 | 169 | guard activity.activityType.hasPrefix(prefix) else { return false } |
| @@ -635,7 +655,6 @@ struct CraftWebView: UIViewRepresentable { | ||
| 635 | 655 | } |
| 636 | 656 | NotificationCenter.default.addObserver(self, selector: #selector(receivePushToken(_:)), name: .craftPushToken, object: nil) |
| 637 | 657 | NotificationCenter.default.addObserver(self, selector: #selector(receivePushRegistrationError(_:)), name: .craftPushRegistrationError, object: nil) |
| 638 | NotificationCenter.default.addObserver(self, selector: #selector(receiveNotificationResponse(_:)), name: .craftNotificationResponse, object: nil) | |
| 639 | 658 | NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil) |
| 640 | 659 | setupNetworkMonitoring() |
| 641 | 660 | } |
| @@ -3090,6 +3109,49 @@ struct CraftWebView: UIViewRepresentable { | ||
| 3090 | 3109 | }; |
| 3091 | 3110 | })(window.craft); |
| 3092 | 3111 | |
| 3112 | // A notification tap that arrived before anything subscribed. | |
| 3113 | // | |
| 3114 | // On a cold launch the tap is flushed the moment the bridge is | |
| 3115 | // ready, and a page that wires its listener once its router has | |
| 3116 | // hydrated — which is most single-page apps — was not listening | |
| 3117 | // yet, so "open the thing this notification is about" opened the | |
| 3118 | // home screen instead. Deep links hit the same wall (#198) and | |
| 3119 | // are held the same way: state on window, so a second injection | |
| 3120 | // into the same page does not start a fresh buffer, and the first | |
| 3121 | // subscriber is handed whatever it missed. | |
| 3122 | (function installNotificationTapReplay(craft) { | |
| 3123 | var replay = window.__craftNotificationTapReplay; | |
| 3124 | if (!replay) { | |
| 3125 | replay = window.__craftNotificationTapReplay = {undelivered: [], subscribed: false}; | |
| 3126 | window.addEventListener('craftNotificationResponse', function(e) { | |
| 3127 | if (!replay.subscribed) replay.undelivered.push(e.detail); | |
| 3128 | }); | |
| 3129 | } | |
| 3130 | craft._subscribeNotificationTaps = function(callback) { | |
| 3131 | var active = true; | |
| 3132 | var listener = function(e) { callback(e.detail); }; | |
| 3133 | window.addEventListener('craftNotificationResponse', listener); | |
| 3134 | if (!replay.subscribed) { | |
| 3135 | replay.subscribed = true; | |
| 3136 | setTimeout(function() { | |
| 3137 | var pending = replay.undelivered; | |
| 3138 | replay.undelivered = []; | |
| 3139 | if (!active) return; | |
| 3140 | pending.forEach(function(detail) { callback(detail); }); | |
| 3141 | }, 0); | |
| 3142 | } | |
| 3143 | return function() { | |
| 3144 | active = false; | |
| 3145 | window.removeEventListener('craftNotificationResponse', listener); | |
| 3146 | }; | |
| 3147 | }; | |
| 3148 | if (craft.notifications) { | |
| 3149 | craft.notifications.onTap = function(callback) { | |
| 3150 | return craft._subscribeNotificationTaps(callback); | |
| 3151 | }; | |
| 3152 | } | |
| 3153 | })(window.craft); | |
| 3154 | ||
| 3093 | 3155 | (function installCraftMobileContract(craft) { |
| 3094 | 3156 | var legacyShare = craft.share.bind(craft); |
| 3095 | 3157 | var legacyOpenCamera = craft.openCamera.bind(craft); |
| @@ -3590,11 +3652,6 @@ struct CraftWebView: UIViewRepresentable { | ||
| 3590 | 3652 | rejectCallback(claimed, error: message, code: "PUSH_REGISTRATION_ERROR") |
| 3591 | 3653 | } |
| 3592 | 3654 | |
| 3593 | @objc private func receiveNotificationResponse(_ notification: Notification) { | |
| 3594 | let data = notification.object as? [String: Any] ?? [:] | |
| 3595 | sendToWeb("craftNotificationResponse", data: data) | |
| 3596 | } | |
| 3597 | ||
| 3598 | 3655 | // MARK: - Secure Storage (Keychain) |
| 3599 | 3656 | private func secureStore(key: String, value: String) -> Bool { |
| 3600 | 3657 | let data = value.data(using: .utf8)! |