Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Native local notifications in macOS (is this already possible?) #2122

Open
michelcrypt4d4mus opened this issue May 17, 2024 · 19 comments
Open
Labels
enhancement New feature or request Needs: Triage 🔍

Comments

@michelcrypt4d4mus
Copy link

michelcrypt4d4mus commented May 17, 2024

Summary

I'm trying to figure out how to send notifications to the Notification Center in macOS similar to what Notifee or PushNotificationsIOS or expo do. Apologies if this is already a thing that one can do in react-native-macos but I've been poking around and haven't seen anything to this effect for macOS; is it something that is supported (or planned to be supported) in some way?

I stumbled across this page about IReactNotificationService but I'm not really sure what to do with it - is this an objective-C interface?

Motivation

Many (most?) native applications make use of the Notification Center for all kinds of things. Seems like it would be good for react native apps to be able to do the same.

Basic Example

No response

Open Questions

No response

@michelcrypt4d4mus michelcrypt4d4mus added the enhancement New feature or request label May 17, 2024
@michelcrypt4d4mus michelcrypt4d4mus changed the title Native local notifications in macOS (is this already possible) Native local notifications in macOS (is this already possible?) May 17, 2024
@Saadnajmi
Copy link
Collaborator

Saadnajmi commented May 17, 2024

PushNotificationsIOS should work? At least, whenever I have touched, I've made sure the code compiles on macOS :)

@michelcrypt4d4mus
Copy link
Author

michelcrypt4d4mus commented May 17, 2024

I tried it out with mixed results

edit: actually at that time i may have also had expo installed... let me try again now that i have transitioned away from expo and towards react-native-macos.

@Saadnajmi
Copy link
Collaborator

Oh sorry, I meant the PushNotificationIOS that ships with React Native, AKA:
https://github.com/microsoft/react-native-macos/tree/main/packages/react-native/Libraries/PushNotificationIOS

@michelcrypt4d4mus
Copy link
Author

ah ok - so you are saying this fork of react-native did not break PushNotificationsIOS out into a separate package (unlike the master fork)?

@michelcrypt4d4mus
Copy link
Author

if that's so then is this deprecated guide to push notifications still a good reference for how to handle notifications in macOS?

for what it's worth i'm only interested in local machine notifications, not remote push notifications. not sure if that makes any difference / makes things easier or harder.

@michelcrypt4d4mus
Copy link
Author

michelcrypt4d4mus commented May 17, 2024

I'm trying to follow (more or less) those deprecated instructions... When I go to add

#import <RCTPushNotificationManager.h>

(or #import <React/RCTPushNotificationManager.h>) to my AppDelegate.h file Xcode informs me the file can't be found, so I did some digging around in the project directory that was generated by react-native-macos and I noticed that CocoaPods doesn't seem to be picking up the PushNotificationsIOS headers and code?

Bear with me here - i'm a very experienced developer but I have fully 0 experience with Xcode or objective-C and it's been years since i did anything in C or C++ - but when I went looking for the RCTPushNotificationManager.h I did find it in the node_modules directory:

./node_modules/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.h
./node_modules/react-native-macos/Libraries/PushNotificationIOS/RCTPushNotificationManager.h

Given that my AppDelegate.h file can currently find RCTAppDelegate.h I checked out where that file is as well, thinking I could just adjust the path of my #import and get the notifications header in there... but I noticed that RCTAppDelegate.h has also apparently been configured by CocoaPods, so it appears in 4 places in the hierarchy:

./macos/Pods/Headers/Public/React-RCTAppDelegate/RCTAppDelegate.h
./macos/Pods/Headers/Private/React-RCTAppDelegate/RCTAppDelegate.h
./node_modules/react-native/Libraries/AppDelegate/RCTAppDelegate.h
./node_modules/react-native-macos/Libraries/AppDelegate/RCTAppDelegate.h

That lead me to Podfile.lock. I don't fully understand what CocoaPods is doing but it seems like it's setting up node_modules code to be accessed by Xcode, so I was surprised to see that my Podfile.lock file showed all kinds of react-native-macos code being setup but made no reference at all to PushNotificaitonsIOS:

DEPENDENCIES:
  - boost (from `../node_modules/react-native-macos/third-party-podspecs/boost.podspec`)
  - DoubleConversion (from `../node_modules/react-native-macos/third-party-podspecs/DoubleConversion.podspec`)
  - FBLazyVector (from `../node_modules/react-native-macos/Libraries/FBLazyVector`)
  - FBReactNativeSpec (from `../node_modules/react-native-macos/React/FBReactNativeSpec`)
  - glog (from `../node_modules/react-native-macos/third-party-podspecs/glog.podspec`)
  - RCT-Folly (from `../node_modules/react-native-macos/third-party-podspecs/RCT-Folly.podspec`)
  - RCTRequired (from `../node_modules/react-native-macos/Libraries/RCTRequired`)
  - RCTTypeSafety (from `../node_modules/react-native-macos/Libraries/TypeSafety`)
  - React (from `../node_modules/react-native-macos/`)
  - React-callinvoker (from `../node_modules/react-native-macos/ReactCommon/callinvoker`)
  - React-Codegen (from `build/generated/ios`)
  - React-Core (from `../node_modules/react-native-macos/`)
  - React-Core/RCTWebSocket (from `../node_modules/react-native-macos/`)
  - React-CoreModules (from `../node_modules/react-native-macos/React/CoreModules`)
  - React-cxxreact (from `../node_modules/react-native-macos/ReactCommon/cxxreact`)
  - React-jsc (from `../node_modules/react-native-macos/ReactCommon/jsc`)
  - React-jsi (from `../node_modules/react-native-macos/ReactCommon/jsi`)
  - React-jsiexecutor (from `../node_modules/react-native-macos/ReactCommon/jsiexecutor`)
  - React-jsinspector (from `../node_modules/react-native-macos/ReactCommon/jsinspector`)
  - React-logger (from `../node_modules/react-native-macos/ReactCommon/logger`)
  - react-native-menubar-extra (from `../node_modules/react-native-menubar-extra`)
  - React-perflogger (from `../node_modules/react-native-macos/ReactCommon/reactperflogger`)
  - React-RCTActionSheet (from `../node_modules/react-native-macos/Libraries/ActionSheetIOS`)
  - React-RCTAnimation (from `../node_modules/react-native-macos/Libraries/NativeAnimation`)
  - React-RCTAppDelegate (from `../node_modules/react-native-macos/Libraries/AppDelegate`)
  - React-RCTBlob (from `../node_modules/react-native-macos/Libraries/Blob`)
  - React-RCTImage (from `../node_modules/react-native-macos/Libraries/Image`)
  - React-RCTLinking (from `../node_modules/react-native-macos/Libraries/LinkingIOS`)
  - React-RCTNetwork (from `../node_modules/react-native-macos/Libraries/Network`)
  - React-RCTSettings (from `../node_modules/react-native-macos/Libraries/Settings`)
  - React-RCTText (from `../node_modules/react-native-macos/Libraries/Text`)
  - React-RCTVibration (from `../node_modules/react-native-macos/Libraries/Vibration`)
  - React-runtimeexecutor (from `../node_modules/react-native-macos/ReactCommon/runtimeexecutor`)
  - ReactCommon/turbomodule/core (from `../node_modules/react-native-macos/ReactCommon`)
  - Yoga (from `../node_modules/react-native-macos/ReactCommon/yoga`)

(That's just a piece of the file; can post the whole thing.) Now I don't really know what I'm doing here but it seems like somehow CocoaPods is not properly setting up PushNotificationsIOS when I pod install for reasons I can't divine. Or am I totally off base?

@michelcrypt4d4mus
Copy link
Author

Digging around in the Podfile I saw it references react_native_pods.rb from this repo... looking in that file I see no reference to PushNotificationsIOS though I do see all the other node libraries that seem to drive react-native-macos listed.

@michelcrypt4d4mus
Copy link
Author

I added the line

pod 'React-RCTPushNotification', :path => "#{prefix}/Libraries/PushNotificationIOS"

to react_native_pods.rb and after running pod install Xcode was able to successfully #import <React/RCTPushNotificationManager.h>... Haven't actually tried using the library but my first question is whether maybe React-RCTPushNotification was left out of the list of pods for a good reason?

@Saadnajmi
Copy link
Collaborator

Upstream React Native still has it too, I guess they never got around to deleting it, and I kept the macOS support not realizing it was actually meant to be ejected. There has been active development in this code path by Meta, so interesting it is supposed to be ejected..

@michelcrypt4d4mus
Copy link
Author

yeah it's just marked as deprecated for now.

I'm trying to follow the react 0.71 docs on push notifications and adjust them to work with macOS instead of iOS based off of the RNTester/AppDelegate.mm code.

So far after adding a couple of methods (⬇️) to my AppDelegate.mm I've at least gotten it to build and successfully requestPermissions() though so far haven't had any luck actually sending notifications... If you see something obvious I'm missing here I'd be grateful for the helping hand; it's hard to exactly map the iOS methods onto macOS ones.

- (void)userNotificationCenter:(NSUserNotificationCenter *)center
        didDeliverNotification:(NSUserNotification *)notification {
}

- (void)userNotificationCenter:(NSUserNotificationCenter *)center
       didActivateNotification:(NSUserNotification *)notification {
    [RCTPushNotificationManager didReceiveUserNotification:notification];
}

- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center
     shouldPresentNotification:(NSUserNotification *)notification {
    return YES;
}

@michelcrypt4d4mus
Copy link
Author

michelcrypt4d4mus commented May 17, 2024

So after more fiddling around I ended up in pretty much the same place I did on the issue I opened that I linked earlier in PushNotificationIOS (but not exactly):

  1. permissions can be requested and retrieved just fine; my app appears in the notifications control panel.
  2. The app can subscribe to and act on fake remote notifications triggered by react native's DeviceEventEmitter but these are unrelated to the official macOS Notification Center (which is what i actually care about)
  3. calling presentLocalNotification() results in nothing appearing in the macOS notification center.
  4. calling scheduleLocalNotification() results in nothing in the notification center and nothing can be retrieved from the notification center queue with getScheduledLocalNotifications()

the difference between the PushNotificationIOS test and this one is that with the new community PushNotificationIOS if i scheduled local notifications i was able to pull them out of some queue, though when the time for them to be delivered would arrive they would just disappear into the void. in this case it seems they just disappear straight into the void with no time in the queue.

poking around the objective-C implementation of presentLocalNotification() in this repo i see that it theoretically should call out the apple's addNotificationRequest:withCompletionHandler... but that's as far as I got.

any advice or tips on why this might not be working or how one might debug this appreciated.

edit: the additions i ended up making to my AppDelegate.mm file were these:

- (void)userNotificationCenter:(NSUserNotificationCenter *)center didDeliverNotification:(NSUserNotification *)notification {
}


- (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification {
    [RCTPushNotificationManager didReceiveUserNotification:notification];
}


- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification {
    return YES;
}


// Required for the remoteNotificationsRegistered event.
- (void)application:(__unused RCTUIApplication *)application
    didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
  [RCTPushNotificationManager didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}


// Required for the registrationError event.
- (void)application:(NSApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
  [RCTPushNotificationManager didFailToRegisterForRemoteNotificationsWithError:error];
}


// Required for the localNotification event.
- (void)application:(NSApplication *)application didReceiveLocalNotification:(NSUserNotification *)notification {
  [RCTPushNotificationManager didReceiveUserNotification:notification];
}


// Required for the remoteNotificationReceived event.
- (void)application:(__unused RCTUIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification
{
  [RCTPushNotificationManager didReceiveRemoteNotification:notification];
}

I also tried to change AppDelegate.h to:

@interface AppDelegate : RCTAppDelegate <UNUserNotificationCenterDelegate>

@michelcrypt4d4mus
Copy link
Author

I found something pretty confusing... if i look in this repo at the obj-c definition of presentLocalNotification() i see code that is notably quite different from what i have in node_modules/react-native-macos/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm. maybe that's normal because the code in this repo has not been released yet or something? but i found it odd and given the fact that i had to manually intervene to get CocoaPods to even find the package I was a bit concerned.

this repo:

RCT_EXPORT_METHOD(presentLocalNotification : (JS::NativePushNotificationManagerIOS::Notification &)notification)
{
  NSDictionary<NSString *, id> *notificationDict = [RCTConvert NSDictionaryForNotification:notification];
  UNNotificationContent *content = [RCTConvert UNNotificationContent:notificationDict];
  UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.1
                                                                                                  repeats:NO];
  UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:[[NSUUID UUID] UUIDString]
                                                                        content:content
                                                                        trigger:trigger];

  UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
  [center addNotificationRequest:request withCompletionHandler:nil];
}

local node_modules/react-native-macos/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm:

#if !TARGET_OS_OSX // [macOS]
RCT_EXPORT_METHOD(presentLocalNotification : (JS::NativePushNotificationManagerIOS::Notification &)notification)
{
  NSMutableDictionary *notificationDict = [NSMutableDictionary new];
  notificationDict[@"alertTitle"] = notification.alertTitle();
  notificationDict[@"alertBody"] = notification.alertBody();
  notificationDict[@"alertAction"] = notification.alertAction();
  notificationDict[@"userInfo"] = notification.userInfo();
  notificationDict[@"category"] = notification.category();
  notificationDict[@"repeatInterval"] = notification.repeatInterval();
  if (notification.fireDate()) {
    notificationDict[@"fireDate"] = @(*notification.fireDate());
  }
  if (notification.applicationIconBadgeNumber()) {
    notificationDict[@"applicationIconBadgeNumber"] = @(*notification.applicationIconBadgeNumber());
  }
  if (notification.isSilent()) {
    notificationDict[@"isSilent"] = @(*notification.isSilent());
    if ([notificationDict[@"isSilent"] isEqualToNumber:@(NO)]) {
      notificationDict[@"soundName"] = notification.soundName();
    }
  }
  [RCTSharedApplication() presentLocalNotificationNow:[RCTConvert UILocalNotification:notificationDict]];
}
#else // [macOS
RCT_EXPORT_METHOD(presentLocalNotification:(NSUserNotification *)notification)
{
  [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification];
}
#endif // macOS]

is this expected?

@Saadnajmi
Copy link
Collaborator

@michelcrypt4d4mus what version of RNM are you using? The main branch probably matches what will come with 0.74, if not with what is already in 0.73 (you can check by looking at the same file on the branch 0.73-stable)

@michelcrypt4d4mus
Copy link
Author

react-native-macos-0.71.36, as specified in the docs

@Saadnajmi
Copy link
Collaborator

Ah yeah.. those docs are out of date. Use the latest if you can, which would be 0.73.
Also, I found official docs about the whole PushNotificationIOS thing, and it seems this library will be removed from React Native: https://reactnative.dev/blog/2024/04/22/release-0.74#api-changes-to-pushnotificationios-deprecated

When that happens (with 0.75), we'd probably have to move the macOS implementation to the community module to keep support.

@AdrianFahrbach
Copy link

@michelcrypt4d4mus I didn't thoroughly read through all of your posts here and this may not be the solution that you are looking for, but I solved this (and other native connections) through a different approach.

I'm very bad at Objective-C, so I went with an EventEmitter that allows the JS and the Swift parts to talk to each other. Once you got that setup you don't need to hassle with getting the whole module into React, since you can do everything in Swift and it is a bit easier to find resources on that. Not a perfect solution, but a good one if you are coming from JS.

You can take a look at my EventEmitter Swift class here.
I'm listening to notification events in my AppDelegate and also send the result of the permissions check back to React to display an error message to the user.
Sending and listening to events is pretty easy as shown here. I'm sending my notification events here.

Also keep in mind that your notifications need a unique ID, otherwise MacOS will not deliver your notification if a notification with the same ID is already in your notification center.

@michelcrypt4d4mus
Copy link
Author

thanks. in my initial post i mentioned "The app can subscribe to and act on fake remote notifications triggered by react native's DeviceEventEmitter" - so yeah, i think my experience was that listening to system events worked OK (I think - but am not sure - that DeviceEventEmitter is firing "real" system events and not just javascript / RN "events") but getting notifications into the formal macOS "Notification Center" was my real goal.

@Saadnajmi
Copy link
Collaborator

Thanks for the discussion here. I learned a bit about how people use React Native macOS, and a good place to put better macOS notification support :)

@michelcrypt4d4mus
Copy link
Author

ultimately i ended up firing my "real" notifications from a background process.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request Needs: Triage 🔍
Projects
None yet
Development

No branches or pull requests

3 participants