From 26fb9133058a2254b2e7064a3388f634a01206c1 Mon Sep 17 00:00:00 2001 From: Simone Gauli Date: Sat, 2 May 2026 20:30:50 +0200 Subject: [PATCH 1/4] feat: fix foreground attempt --- ios/NitroNotificationLoader.mm | 20 ++++++++++++++++++++ ios/NotificationHub.swift | 4 ++-- ios/NotificationsAppDelegateSubscriber.swift | 7 +++++++ 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 ios/NitroNotificationLoader.mm diff --git a/ios/NitroNotificationLoader.mm b/ios/NitroNotificationLoader.mm new file mode 100644 index 0000000..a37d4af --- /dev/null +++ b/ios/NitroNotificationLoader.mm @@ -0,0 +1,20 @@ +#import + +/// Forward-declare the Swift entry point generated by the NitroNotification Swift module. +/// This is declared in NitroNotification-Swift.h but we avoid importing that header +/// to stay away from the C++ umbrella chain. +extern "C" void NitroNotification_setup(void); + +@interface NitroNotificationLoader : NSObject +@end + +@implementation NitroNotificationLoader + ++ (void)load { + // Touch NotificationHub.shared via the Swift @_cdecl shim so the + // UNUserNotificationCenterDelegate is registered before any foreground + // notification can arrive — without requiring an import in the app delegate. + NitroNotification_setup(); +} + +@end diff --git a/ios/NotificationHub.swift b/ios/NotificationHub.swift index aa1a7b6..2567db8 100644 --- a/ios/NotificationHub.swift +++ b/ios/NotificationHub.swift @@ -57,7 +57,7 @@ final class NotificationHub: NSObject, UNUserNotificationCenterDelegate { func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, - withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void + withCompletionHandler completionHandler: @escaping @Sendable (Int) -> Void ) { var options: UNNotificationPresentationOptions = [] if foregroundAlert { options.insert(.banner) } @@ -65,7 +65,7 @@ final class NotificationHub: NSObject, UNUserNotificationCenterDelegate { if foregroundSound { options.insert(.sound) } let payload = payloadFrom(notification.request.content) onNotificationReceived?(payload) - completionHandler(options) + completionHandler(Int(options.rawValue)) } func userNotificationCenter( diff --git a/ios/NotificationsAppDelegateSubscriber.swift b/ios/NotificationsAppDelegateSubscriber.swift index 7d93dbc..2dd9af7 100644 --- a/ios/NotificationsAppDelegateSubscriber.swift +++ b/ios/NotificationsAppDelegateSubscriber.swift @@ -4,3 +4,10 @@ import Foundation /// Post this notification with userInfo ["deviceToken": Data] from your AppDelegate. public let NitroNotificationTokenKey = "NitroNotificationDeviceToken" public let NitroNotificationTokenNotification = Notification.Name("NitroNotificationDidRegisterToken") + +/// Called from NitroNotificationLoader.mm via @_cdecl so the ObjC +load +/// method can bootstrap NotificationHub without importing the C++ umbrella. +@_cdecl("NitroNotification_setup") +public func nitroNotificationSetup() { + _ = NotificationHub.shared +} From 33887b7036dca4f733671b7dc29892fb1756d224 Mon Sep 17 00:00:00 2001 From: Simone Gauli Date: Sat, 2 May 2026 21:50:56 +0200 Subject: [PATCH 2/4] feat: add a dummy relay and start maestro tests impl --- .gitignore | 3 + example/.maestro/background_notification.yaml | 33 ++++++++ example/.maestro/foreground_notification.yaml | 25 ++++++ example/.maestro/send_push.js | 10 +++ example/src/App.tsx | 30 +++---- package.json | 6 +- tools/push-relay.ts | 82 +++++++++++++++++++ 7 files changed, 174 insertions(+), 15 deletions(-) create mode 100644 example/.maestro/background_notification.yaml create mode 100644 example/.maestro/foreground_notification.yaml create mode 100644 example/.maestro/send_push.js create mode 100644 tools/push-relay.ts diff --git a/.gitignore b/.gitignore index 67f3212..b406a51 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,6 @@ android/generated # React Native Nitro Modules nitrogen/ + +# Push relay credentials (real-device APNs — never commit) +.env.push diff --git a/example/.maestro/background_notification.yaml b/example/.maestro/background_notification.yaml new file mode 100644 index 0000000..f63e2f7 --- /dev/null +++ b/example/.maestro/background_notification.yaml @@ -0,0 +1,33 @@ +appId: nitronotification.example +--- +- launchApp: + clearState: false + +- assertVisible: + text: Nitro Notifications + timeout: 10000 + +- pressKey: Home + +- evalScript: ${new Promise(r => setTimeout(r, 1500))} + +- runScript: + file: send_push.js + env: + PUSH_TITLE: Background Test + PUSH_BODY: Tap to open the app + +- assertVisible: + text: Background Test + timeout: 8000 + +- tapOn: + text: Background Test + +- assertVisible: + id: last-event-text + timeout: 8000 + +- assertVisible: + text: 'Tapped: Background Test' + timeout: 8000 diff --git a/example/.maestro/foreground_notification.yaml b/example/.maestro/foreground_notification.yaml new file mode 100644 index 0000000..fc7d96e --- /dev/null +++ b/example/.maestro/foreground_notification.yaml @@ -0,0 +1,25 @@ +appId: nitronotification.example +--- +- launchApp: + clearState: false + +- extendedWaitUntil: + visible: + text: Nitro Notifications + timeout: 10000 + +- runScript: + file: send_push.js + env: + PUSH_TITLE: Foreground Test + PUSH_BODY: Notification received in foreground + +- extendedWaitUntil: + visible: + id: last-event-text + timeout: 8000 + +- extendedWaitUntil: + visible: + text: "Received: Foreground Test — Notification received in foreground" + timeout: 8000 diff --git a/example/.maestro/send_push.js b/example/.maestro/send_push.js new file mode 100644 index 0000000..08b8fd7 --- /dev/null +++ b/example/.maestro/send_push.js @@ -0,0 +1,10 @@ +// Maestro runScript helper - calls local push-relay +const title = typeof PUSH_TITLE !== 'undefined' ? PUSH_TITLE : 'Test'; +const body = typeof PUSH_BODY !== 'undefined' ? PUSH_BODY : 'Test body'; +const port = typeof PUSH_RELAY_PORT !== 'undefined' ? PUSH_RELAY_PORT : '8765'; +const payload = JSON.stringify({ title, body }); + +output.result = http.post(`http://127.0.0.1:${port}/push`, { + headers: { 'Content-Type': 'application/json' }, + body: payload, +}); diff --git a/example/src/App.tsx b/example/src/App.tsx index ccb64e7..c51d963 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -40,19 +40,6 @@ export default function App() { }); }; - useEffect(() => { - (async () => { - const status = await Notifications.getPermissionStatus(); - setPermStatus(status); - if (status === 'granted') { - setupNotifications(); - const t = await Notifications.getDevicePushToken(); - setToken(t); - api.login(t); - } - })(); - }, []); - const handleRequestPermissions = async () => { const status = await Notifications.requestPermissions(); setPermStatus(status); @@ -121,6 +108,19 @@ export default function App() { Alert.alert('Copied'); }; + useEffect(() => { + (async () => { + const status = await Notifications.getPermissionStatus(); + setPermStatus(status); + if (status === 'granted') { + setupNotifications(); + const t = await Notifications.getDevicePushToken(); + setToken(t); + api.login(t); + } + })(); + }, []); + return ( Nitro Notifications @@ -151,7 +151,9 @@ export default function App() {