microsoft / react-native-macos

A framework for building native macOS apps with React.
https://microsoft.github.io/react-native-windows/
MIT License
3.35k stars 128 forks source link

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

Open michelcrypt4d4mus opened 1 month ago

michelcrypt4d4mus commented 1 month ago

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

Saadnajmi commented 1 month ago

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

michelcrypt4d4mus commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

@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 commented 1 month ago

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

Saadnajmi commented 1 month ago

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 commented 1 month ago

@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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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