/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import #import #import #import #import "RCTPushNotificationPlugins.h" NSString *const RCTRemoteNotificationReceived = @"RemoteNotificationReceived"; static NSString *const kLocalNotificationReceived = @"LocalNotificationReceived"; static NSString *const kRemoteNotificationsRegistered = @"RemoteNotificationsRegistered"; static NSString *const kRemoteNotificationRegistrationFailed = @"RemoteNotificationRegistrationFailed"; static NSString *const kErrorUnableToRequestPermissions = @"E_UNABLE_TO_REQUEST_PERMISSIONS"; static UNNotification *kInitialNotification = nil; @interface RCTPushNotificationManager () @property (nonatomic, strong) NSMutableDictionary *remoteNotificationCallbacks; @end @implementation RCTConvert (UNNotificationContent) + (UNNotificationContent *)UNNotificationContent:(id)json { NSDictionary *details = [self NSDictionary:json]; BOOL isSilent = [RCTConvert BOOL:details[@"isSilent"]]; UNMutableNotificationContent *content = [UNMutableNotificationContent new]; content.title = [RCTConvert NSString:details[@"alertTitle"]]; content.body = [RCTConvert NSString:details[@"alertBody"]]; content.userInfo = [RCTConvert NSDictionary:details[@"userInfo"]]; content.categoryIdentifier = [RCTConvert NSString:details[@"category"]]; if (details[@"applicationIconBadgeNumber"]) { content.badge = [RCTConvert NSNumber:details[@"applicationIconBadgeNumber"]]; } if (!isSilent) { NSString *soundName = [RCTConvert NSString:details[@"soundName"]]; content.sound = soundName ? [UNNotificationSound soundNamed:details[@"soundName"]] : [UNNotificationSound defaultSound]; } return content; } + (NSDictionary *)NSDictionaryForNotification: (JS::NativePushNotificationManagerIOS::Notification &)notification { // Note: alertAction is not set, as it is no longer relevant with UNNotification NSMutableDictionary *notificationDict = [NSMutableDictionary new]; notificationDict[@"alertTitle"] = notification.alertTitle(); notificationDict[@"alertBody"] = notification.alertBody(); notificationDict[@"userInfo"] = notification.userInfo(); notificationDict[@"category"] = notification.category(); if (notification.fireIntervalSeconds()) { notificationDict[@"fireIntervalSeconds"] = @(*notification.fireIntervalSeconds()); } 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(); } } return notificationDict; } @end @implementation RCTConvert (UIBackgroundFetchResult) RCT_ENUM_CONVERTER( UIBackgroundFetchResult, (@{ @"UIBackgroundFetchResultNewData" : @(UIBackgroundFetchResultNewData), @"UIBackgroundFetchResultNoData" : @(UIBackgroundFetchResultNoData), @"UIBackgroundFetchResultFailed" : @(UIBackgroundFetchResultFailed), }), UIBackgroundFetchResultNoData, integerValue) @end @implementation RCTPushNotificationManager /** For delivered notifications */ static NSDictionary *RCTFormatUNNotification(UNNotification *notification) { NSMutableDictionary *formattedLocalNotification = [NSMutableDictionary dictionary]; if (notification.date) { formattedLocalNotification[@"fireDate"] = RCTFormatNotificationDateFromNSDate(notification.date); } [formattedLocalNotification addEntriesFromDictionary:RCTFormatUNNotificationContent(notification.request.content)]; return formattedLocalNotification; } /** For scheduled notification requests */ static NSDictionary *RCTFormatUNNotificationRequest(UNNotificationRequest *request) { NSMutableDictionary *formattedLocalNotification = [NSMutableDictionary dictionary]; if (request.trigger) { NSDate *triggerDate = nil; if ([request.trigger isKindOfClass:[UNTimeIntervalNotificationTrigger class]]) { triggerDate = [(UNTimeIntervalNotificationTrigger *)request.trigger nextTriggerDate]; } else if ([request.trigger isKindOfClass:[UNCalendarNotificationTrigger class]]) { triggerDate = [(UNCalendarNotificationTrigger *)request.trigger nextTriggerDate]; } if (triggerDate) { formattedLocalNotification[@"fireDate"] = RCTFormatNotificationDateFromNSDate(triggerDate); } } [formattedLocalNotification addEntriesFromDictionary:RCTFormatUNNotificationContent(request.content)]; return formattedLocalNotification; } static NSDictionary *RCTFormatUNNotificationContent(UNNotificationContent *content) { // Note: soundName is not set because this can't be read from UNNotificationSound. // Note: alertAction is no longer relevant with UNNotification NSMutableDictionary *formattedLocalNotification = [NSMutableDictionary dictionary]; formattedLocalNotification[@"alertTitle"] = RCTNullIfNil(content.title); formattedLocalNotification[@"alertBody"] = RCTNullIfNil(content.body); formattedLocalNotification[@"userInfo"] = RCTNullIfNil(RCTJSONClean(content.userInfo)); formattedLocalNotification[@"category"] = content.categoryIdentifier; formattedLocalNotification[@"applicationIconBadgeNumber"] = content.badge; formattedLocalNotification[@"remote"] = @NO; return formattedLocalNotification; } static NSString *RCTFormatNotificationDateFromNSDate(NSDate *date) { NSDateFormatter *formatter = [NSDateFormatter new]; [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"]; return [formatter stringFromDate:date]; } static BOOL IsNotificationRemote(UNNotification *notification) { return [notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]; } RCT_EXPORT_MODULE() - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); } - (void)startObserving { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleLocalNotificationReceived:) name:kLocalNotificationReceived object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleRemoteNotificationReceived:) name:RCTRemoteNotificationReceived object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleRemoteNotificationsRegistered:) name:kRemoteNotificationsRegistered object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleRemoteNotificationRegistrationError:) name:kRemoteNotificationRegistrationFailed object:nil]; } - (void)stopObserving { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (NSArray *)supportedEvents { return @[ @"localNotificationReceived", @"remoteNotificationReceived", @"remoteNotificationsRegistered", @"remoteNotificationRegistrationError" ]; } + (void)didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { NSMutableString *hexString = [NSMutableString string]; NSUInteger deviceTokenLength = deviceToken.length; const unsigned char *bytes = reinterpret_cast(deviceToken.bytes); for (NSUInteger i = 0; i < deviceTokenLength; i++) { [hexString appendFormat:@"%02x", bytes[i]]; } [[NSNotificationCenter defaultCenter] postNotificationName:kRemoteNotificationsRegistered object:self userInfo:@{@"deviceToken" : [hexString copy]}]; } + (void)didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { [[NSNotificationCenter defaultCenter] postNotificationName:kRemoteNotificationRegistrationFailed object:self userInfo:@{@"error" : error}]; } + (void)didReceiveNotification:(UNNotification *)notification { BOOL const isRemoteNotification = IsNotificationRemote(notification); if (isRemoteNotification) { NSDictionary *userInfo = @{@"notification" : notification.request.content.userInfo}; [[NSNotificationCenter defaultCenter] postNotificationName:RCTRemoteNotificationReceived object:self userInfo:userInfo]; } else { [[NSNotificationCenter defaultCenter] postNotificationName:kLocalNotificationReceived object:self userInfo:RCTFormatUNNotification(notification)]; } } + (void)didReceiveRemoteNotification:(NSDictionary *)notification fetchCompletionHandler:(RCTRemoteNotificationCallback)completionHandler { NSDictionary *userInfo = completionHandler ? @{@"notification" : notification, @"completionHandler" : completionHandler} : @{@"notification" : notification}; [[NSNotificationCenter defaultCenter] postNotificationName:RCTRemoteNotificationReceived object:self userInfo:userInfo]; } + (void)setInitialNotification:(UNNotification *)notification { kInitialNotification = notification; } - (void)invalidate { [super invalidate]; kInitialNotification = nil; } - (void)dealloc { kInitialNotification = nil; } - (void)handleLocalNotificationReceived:(NSNotification *)notification { [self sendEventWithName:@"localNotificationReceived" body:notification.userInfo]; } - (void)handleRemoteNotificationReceived:(NSNotification *)notification { NSMutableDictionary *remoteNotification = [NSMutableDictionary dictionaryWithDictionary:notification.userInfo[@"notification"]]; RCTRemoteNotificationCallback completionHandler = notification.userInfo[@"completionHandler"]; NSString *notificationId = [[NSUUID UUID] UUIDString]; remoteNotification[@"notificationId"] = notificationId; remoteNotification[@"remote"] = @YES; if (completionHandler) { if (!self.remoteNotificationCallbacks) { // Lazy initialization self.remoteNotificationCallbacks = [NSMutableDictionary dictionary]; } self.remoteNotificationCallbacks[notificationId] = completionHandler; } [self sendEventWithName:@"remoteNotificationReceived" body:remoteNotification]; } - (void)handleRemoteNotificationsRegistered:(NSNotification *)notification { [self sendEventWithName:@"remoteNotificationsRegistered" body:notification.userInfo]; } - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification { NSError *error = notification.userInfo[@"error"]; NSDictionary *errorDetails = @{ @"message" : error.localizedDescription, @"code" : @(error.code), @"details" : error.userInfo, }; [self sendEventWithName:@"remoteNotificationRegistrationError" body:errorDetails]; } RCT_EXPORT_METHOD(onFinishRemoteNotification : (NSString *)notificationId fetchResult : (NSString *)fetchResult) { UIBackgroundFetchResult result = [RCTConvert UIBackgroundFetchResult:fetchResult]; RCTRemoteNotificationCallback completionHandler = self.remoteNotificationCallbacks[notificationId]; if (!completionHandler) { RCTLogError(@"There is no completion handler with notification id: %@", notificationId); return; } completionHandler(result); [self.remoteNotificationCallbacks removeObjectForKey:notificationId]; } /** * Update the application icon badge number on the home screen */ RCT_EXPORT_METHOD(setApplicationIconBadgeNumber : (double)number) { RCTSharedApplication().applicationIconBadgeNumber = number; } /** * Get the current application icon badge number on the home screen */ RCT_EXPORT_METHOD(getApplicationIconBadgeNumber : (RCTResponseSenderBlock)callback) { callback(@[ @(RCTSharedApplication().applicationIconBadgeNumber) ]); } RCT_EXPORT_METHOD(requestPermissions : (JS::NativePushNotificationManagerIOS::SpecRequestPermissionsPermission &)permissions resolve : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject) { if (RCTRunningInAppExtension()) { reject( kErrorUnableToRequestPermissions, nil, RCTErrorWithMessage(@"Requesting push notifications is currently unavailable in an app extension")); return; } // Add a listener to make sure that startObserving has been called [self addListener:@"remoteNotificationsRegistered"]; UNAuthorizationOptions options = UNAuthorizationOptionNone; if (permissions.alert()) { options |= UNAuthorizationOptionAlert; } if (permissions.badge()) { options |= UNAuthorizationOptionBadge; } if (permissions.sound()) { options |= UNAuthorizationOptionSound; } [UNUserNotificationCenter.currentNotificationCenter requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError *_Nullable error) { if (error != NULL) { reject(@"-1", @"Error - Push authorization request failed.", error); } else { dispatch_async(dispatch_get_main_queue(), ^{ [RCTSharedApplication() registerForRemoteNotifications]; [UNUserNotificationCenter.currentNotificationCenter getNotificationSettingsWithCompletionHandler:^( UNNotificationSettings *_Nonnull settings) { resolve(RCTPromiseResolveValueForUNNotificationSettings(settings)); }]; }); } }]; } RCT_EXPORT_METHOD(abandonPermissions) { [RCTSharedApplication() unregisterForRemoteNotifications]; } RCT_EXPORT_METHOD(checkPermissions : (RCTResponseSenderBlock)callback) { if (RCTRunningInAppExtension()) { callback(@[ RCTSettingsDictForUNNotificationSettings(NO, NO, NO, NO, NO, NO, UNAuthorizationStatusNotDetermined) ]); return; } [UNUserNotificationCenter.currentNotificationCenter getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *_Nonnull settings) { callback(@[ RCTPromiseResolveValueForUNNotificationSettings(settings) ]); }]; } static inline NSDictionary *RCTPromiseResolveValueForUNNotificationSettings(UNNotificationSettings *_Nonnull settings) { return RCTSettingsDictForUNNotificationSettings( settings.alertSetting == UNNotificationSettingEnabled, settings.badgeSetting == UNNotificationSettingEnabled, settings.soundSetting == UNNotificationSettingEnabled, settings.criticalAlertSetting == UNNotificationSettingEnabled, settings.lockScreenSetting == UNNotificationSettingEnabled, settings.notificationCenterSetting == UNNotificationSettingEnabled, settings.authorizationStatus); } static inline NSDictionary *RCTSettingsDictForUNNotificationSettings( BOOL alert, BOOL badge, BOOL sound, BOOL critical, BOOL lockScreen, BOOL notificationCenter, UNAuthorizationStatus authorizationStatus) { return @{ @"alert" : @(alert), @"badge" : @(badge), @"sound" : @(sound), @"critical" : @(critical), @"lockScreen" : @(lockScreen), @"notificationCenter" : @(notificationCenter), @"authorizationStatus" : @(authorizationStatus) }; } RCT_EXPORT_METHOD(presentLocalNotification : (JS::NativePushNotificationManagerIOS::Notification &)notification) { NSDictionary *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]; } RCT_EXPORT_METHOD(scheduleLocalNotification : (JS::NativePushNotificationManagerIOS::Notification &)notification) { NSDictionary *notificationDict = [RCTConvert NSDictionaryForNotification:notification]; UNNotificationContent *content = [RCTConvert UNNotificationContent:notificationDict]; UNNotificationTrigger *trigger = nil; if (notificationDict[@"fireDate"]) { NSDate *fireDate = [RCTConvert NSDate:notificationDict[@"fireDate"]] ?: [NSDate date]; NSCalendar *calendar = [NSCalendar currentCalendar]; NSDateComponents *components = [calendar components:(NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond) fromDate:fireDate]; trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:NO]; } else if (notificationDict[@"fireIntervalSeconds"]) { trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:[notificationDict[@"fireIntervalSeconds"] doubleValue] repeats:NO]; } UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:[[NSUUID UUID] UUIDString] content:content trigger:trigger]; UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center addNotificationRequest:request withCompletionHandler:nil]; } RCT_EXPORT_METHOD(cancelAllLocalNotifications) { [[UNUserNotificationCenter currentNotificationCenter] getPendingNotificationRequestsWithCompletionHandler:^(NSArray *requests) { NSMutableArray *notificationIdentifiersToCancel = [NSMutableArray new]; for (UNNotificationRequest *request in requests) { [notificationIdentifiersToCancel addObject:request.identifier]; } [[UNUserNotificationCenter currentNotificationCenter] removePendingNotificationRequestsWithIdentifiers:notificationIdentifiersToCancel]; }]; } RCT_EXPORT_METHOD(cancelLocalNotifications : (NSDictionary *)userInfo) { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center getPendingNotificationRequestsWithCompletionHandler:^(NSArray *_Nonnull requests) { NSMutableArray *notificationIdentifiersToCancel = [NSMutableArray new]; for (UNNotificationRequest *request in requests) { NSDictionary *notificationInfo = request.content.userInfo; // Note: we do this with a loop instead of just `isEqualToDictionary:` // because we only require that all specified userInfo values match the // notificationInfo values - notificationInfo may contain additional values // which we don't care about. __block BOOL shouldCancel = YES; [userInfo enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { if (![notificationInfo[key] isEqual:obj]) { shouldCancel = NO; *stop = YES; } }]; if (shouldCancel) { [notificationIdentifiersToCancel addObject:request.identifier]; } } [center removePendingNotificationRequestsWithIdentifiers:notificationIdentifiersToCancel]; }]; } RCT_EXPORT_METHOD(getInitialNotification : (RCTPromiseResolveBlock)resolve reject : (__unused RCTPromiseRejectBlock)reject) { // 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 (kInitialNotification) { NSDictionary *notificationDict = RCTFormatUNNotificationContent(kInitialNotification.request.content); if (IsNotificationRemote(kInitialNotification)) { // For backwards compatibility, remote notifications only returns a userInfo dict. NSMutableDictionary *userInfoCopy = [notificationDict[@"userInfo"] mutableCopy]; userInfoCopy[@"remote"] = @YES; resolve(userInfoCopy); } else { // For backwards compatibility, local notifications return the notification. resolve(notificationDict); } return; } resolve((id)kCFNull); } RCT_EXPORT_METHOD(getScheduledLocalNotifications : (RCTResponseSenderBlock)callback) { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center getPendingNotificationRequestsWithCompletionHandler:^(NSArray *_Nonnull requests) { NSMutableArray *formattedScheduledLocalNotifications = [NSMutableArray new]; for (UNNotificationRequest *request in requests) { [formattedScheduledLocalNotifications addObject:RCTFormatUNNotificationRequest(request)]; } callback(@[ formattedScheduledLocalNotifications ]); }]; } RCT_EXPORT_METHOD(removeAllDeliveredNotifications) { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center removeAllDeliveredNotifications]; } RCT_EXPORT_METHOD(removeDeliveredNotifications : (NSArray *)identifiers) { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center removeDeliveredNotificationsWithIdentifiers:identifiers]; } RCT_EXPORT_METHOD(getDeliveredNotifications : (RCTResponseSenderBlock)callback) { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center getDeliveredNotificationsWithCompletionHandler:^(NSArray *_Nonnull notifications) { NSMutableArray *formattedNotifications = [NSMutableArray new]; for (UNNotification *notification in notifications) { [formattedNotifications addObject:RCTFormatUNNotification(notification)]; } callback(@[ formattedNotifications ]); }]; } RCT_EXPORT_METHOD(getAuthorizationStatus : (RCTResponseSenderBlock)callback) { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *_Nonnull settings) { callback(@[ @(settings.authorizationStatus) ]); }]; } - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { return std::make_shared(params); } @end Class RCTPushNotificationManagerCls(void) { return RCTPushNotificationManager.class; }