introduce property on RCTPushNotificationManager to hold notification that launched the app (#42628)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/42628

Changelog: [iOS][Deprecated] Retrieving initial notification requires UNUserNotificationCenterDelegate setup instead of reading UIApplicationLaunchOptionsLocalNotificationKey

# how to migrate:

if you are currently using `getInitialNotification` to check the notification on an app start (warm or cold), you will need to do a migration.

have an object become the delegate of `UNUserNotificationCenterDelegate`. you can use your app's `AppDelegate` to do this.

then, override `userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:`. in the implementation, add these lines:

  if ([response.actionIdentifier isEqualToString:UNNotificationDefaultActionIdentifier]) {
    id module = _reactHost ? [[_reactHost getModuleRegistry] moduleForName:"PushNotificationManager"]
                           : [_bridge moduleForClass:[RCTPushNotificationManager class]];

    if ([module isKindOfClass:[RCTPushNotificationManager class]]) {
      RCTPushNotificationManager *pushNotificationManager = (RCTPushNotificationManager *)module;
      pushNotificationManager.initialLocalNotification = response.notification;
    }
  }

# reasoning:

when you start an app from a push notification, the `UIApplicationLaunchOptionsLocalNotificationKey` in `application:didFinishLaunchingWithOptions:` will contain the notification metadata that started the app. additionally, this was stored on the bridge, which is not compatible with the new architecture. however, `UIApplicationLaunchOptionsLocalNotificationKey` has been deprecated since iOS 10, which this PR aims to address.

apple's supported API to do this is `userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:` which is on `UNUserNotificationCenterDelegate`. this is a significant change from a pull to push model. in order to make this change without having to rearchitect the product layer, we're going to store the notification on the notification native module.

another caveat is that the API follows the delegation pattern, not the listener pattern. that means we can't make our notificaiton native module the delegate here - the app will probably need `UNUserNotificationCenterDelegate` to be a top level object - usually the scope of the app delegate.

in future, i actually think we need to unify this with `handleLocalNotificationReceived:`, but right now there's forked handling between platforms and some product code is only using the pull model, so this is still the minimum change in the product layer.

Reviewed By: ingridwang

Differential Revision: D52897071

fbshipit-source-id: 579578d1b3128c5f7e81249c75cf7655b8e360e2
This commit is contained in:
Phillip Pan
2024-01-26 15:33:02 -08:00
committed by Facebook GitHub Bot
parent 8c4979e7a1
commit c8ab44cf0b
2 changed files with 39 additions and 8 deletions
@@ -11,6 +11,8 @@ extern NSString *const RCTRemoteNotificationReceived;
@interface RCTPushNotificationManager : RCTEventEmitter
@property (nonatomic, nullable, readwrite) UNNotification *initialNotification;
typedef void (^RCTRemoteNotificationCallback)(UIBackgroundFetchResult result);
+ (void)didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken;
@@ -168,6 +168,11 @@ static NSString *RCTFormatNotificationDateFromNSDate(NSDate *date)
return [formatter stringFromDate:date];
}
static BOOL IsNotificationRemote(UNNotification *notification)
{
return [notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]];
}
RCT_EXPORT_MODULE()
- (dispatch_queue_t)methodQueue
@@ -232,7 +237,7 @@ RCT_EXPORT_MODULE()
+ (void)didReceiveNotification:(UNNotification *)notification
{
BOOL const isRemoteNotification = [notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]];
BOOL const isRemoteNotification = IsNotificationRemote(notification);
if (isRemoteNotification) {
NSDictionary *userInfo = @{@"notification" : notification.request.content.userInfo};
[[NSNotificationCenter defaultCenter] postNotificationName:RCTRemoteNotificationReceived
@@ -524,20 +529,44 @@ RCT_EXPORT_METHOD(getInitialNotification
: (RCTPromiseResolveBlock)resolve reject
: (__unused RCTPromiseRejectBlock)reject)
{
NSMutableDictionary<NSString *, id> *initialNotification =
// The user actioned a local or remote notification to launch the app. Notification is represented by UNNotification.
// Set this property in the implementation of
// userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler.
if (self.initialNotification) {
NSDictionary<NSString *, id> *notificationDict =
RCTFormatUNNotificationContent(self.initialNotification.request.content);
if (IsNotificationRemote(self.initialNotification)) {
NSMutableDictionary<NSString *, id> *notificationDictCopy = [notificationDict mutableCopy];
notificationDictCopy[@"remote"] = @YES;
resolve(notificationDictCopy);
} else {
resolve(notificationDict);
}
return;
}
NSMutableDictionary<NSString *, id> *initialRemoteNotification =
[self.bridge.launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey] mutableCopy];
// The user actioned a remote notification to launch the app. This is a fallback that is deprecated
// in the new architecture.
if (initialRemoteNotification) {
initialRemoteNotification[@"remote"] = @YES;
resolve(initialRemoteNotification);
return;
}
UILocalNotification *initialLocalNotification =
self.bridge.launchOptions[UIApplicationLaunchOptionsLocalNotificationKey];
if (initialNotification) {
initialNotification[@"remote"] = @YES;
resolve(initialNotification);
} else if (initialLocalNotification) {
// The user actioned a local notification to launch the app. Notification is represented by UILocalNotification. This
// is deprecated.
if (initialLocalNotification) {
resolve(RCTFormatLocalNotification(initialLocalNotification));
} else {
resolve((id)kCFNull);
return;
}
resolve((id)kCFNull);
}
RCT_EXPORT_METHOD(getScheduledLocalNotifications : (RCTResponseSenderBlock)callback)