From c8ab44cf0b1e63ac7224147f3df1f414a29b65a0 Mon Sep 17 00:00:00 2001 From: Phillip Pan Date: Fri, 26 Jan 2024 15:33:02 -0800 Subject: [PATCH] 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 --- .../RCTPushNotificationManager.h | 2 + .../RCTPushNotificationManager.mm | 45 +++++++++++++++---- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.h b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.h index 8423a81ad3d30d..15bdd59e86e132 100644 --- a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.h +++ b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.h @@ -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; diff --git a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm index 6ebe188674fac4..a0d41469b6e5c5 100644 --- a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm +++ b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm @@ -168,6 +168,11 @@ @implementation RCTPushNotificationManager 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 @@ + (void)didFailToRegisterForRemoteNotificationsWithError:(NSError *)error + (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 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification : (RCTPromiseResolveBlock)resolve reject : (__unused RCTPromiseRejectBlock)reject) { - NSMutableDictionary *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 *notificationDict = + RCTFormatUNNotificationContent(self.initialNotification.request.content); + if (IsNotificationRemote(self.initialNotification)) { + NSMutableDictionary *notificationDictCopy = [notificationDict mutableCopy]; + notificationDictCopy[@"remote"] = @YES; + resolve(notificationDictCopy); + } else { + resolve(notificationDict); + } + return; + } + + NSMutableDictionary *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)