ReviewOS

also looking at this

craft-native/craft

fix(ios): hand a notification tap that launches the app to the page that asks for it

#255
Merged glennmichael123 wants to merge fix/ios-notification-tap-cold-start into main
5 files +194 -9
packages/ios/templates/CraftApp.swiftmodified+65-8
Changes to packages/ios/templates/CraftApp.swift
@@ -34,7 +34,6 @@ import ActivityKit
3434extension Notification.Name {
3535 static let craftPushToken = Notification.Name("craftPushToken")
3636 static let craftPushRegistrationError = Notification.Name("craftPushRegistrationError")
37 static let craftNotificationResponse = Notification.Name("craftNotificationResponse")
3837}
3938
4039/// Which failed page loads mean the remote origin is out of reach (#252).
@@ -104,7 +103,14 @@ final class CraftAppDelegate: NSObject, UIApplicationDelegate, UNUserNotificatio
104103 }
105104
106105 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)
108114 completionHandler()
109115 }
110116
@@ -144,6 +150,20 @@ class CraftEventManager {
144150 sendToWeb("craftShortcut", data: ["type": shortcut.type])
145151 }
146152
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
147167 func handleSiriActivity(_ activity: NSUserActivity) -> Bool {
148168 let prefix = "\(Bundle.main.bundleIdentifier ?? "{{BUNDLE_ID}}")."
149169 guard activity.activityType.hasPrefix(prefix) else { return false }
@@ -635,7 +655,6 @@ struct CraftWebView: UIViewRepresentable {
635655 }
636656 NotificationCenter.default.addObserver(self, selector: #selector(receivePushToken(_:)), name: .craftPushToken, object: nil)
637657 NotificationCenter.default.addObserver(self, selector: #selector(receivePushRegistrationError(_:)), name: .craftPushRegistrationError, object: nil)
638 NotificationCenter.default.addObserver(self, selector: #selector(receiveNotificationResponse(_:)), name: .craftNotificationResponse, object: nil)
639658 NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
640659 setupNetworkMonitoring()
641660 }
@@ -3090,6 +3109,49 @@ struct CraftWebView: UIViewRepresentable {
30903109 };
30913110 })(window.craft);
30923111
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
30933155 (function installCraftMobileContract(craft) {
30943156 var legacyShare = craft.share.bind(craft);
30953157 var legacyOpenCamera = craft.openCamera.bind(craft);
@@ -3590,11 +3652,6 @@ struct CraftWebView: UIViewRepresentable {
35903652 rejectCallback(claimed, error: message, code: "PUSH_REGISTRATION_ERROR")
35913653 }
35923654
3593 @objc private func receiveNotificationResponse(_ notification: Notification) {
3594 let data = notification.object as? [String: Any] ?? [:]
3595 sendToWeb("craftNotificationResponse", data: data)
3596 }
3597
35983655 // MARK: - Secure Storage (Keychain)
35993656 private func secureStore(key: String, value: String) -> Bool {
36003657 let data = value.data(using: .utf8)!