diff --git a/packages/react-native/Libraries/PushNotificationIOS/NativePushNotificationManagerIOS.js b/packages/react-native/Libraries/PushNotificationIOS/NativePushNotificationManagerIOS.js index 102ce795e77a04..0df32a8b3792ea 100644 --- a/packages/react-native/Libraries/PushNotificationIOS/NativePushNotificationManagerIOS.js +++ b/packages/react-native/Libraries/PushNotificationIOS/NativePushNotificationManagerIOS.js @@ -20,17 +20,45 @@ type Permissions = {| type Notification = {| +alertTitle?: ?string, - // Actual type: string | number - +fireDate?: ?number, +alertBody?: ?string, - +alertAction?: ?string, +userInfo?: ?Object, + /** + * Identifier for the notification category. See the [Apple documentation](https://developer.apple.com/documentation/usernotifications/declaring_your_actionable_notification_types) + * for more details. + */ +category?: ?string, - // Actual type: 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' - +repeatInterval?: ?string, + /** + * Actual type: string | number + * + * Schedule notifications using EITHER `fireDate` or `fireIntervalSeconds`. + * If both are specified, `fireDate` takes precedence. + * If you use `presentLocalNotification`, both will be ignored + * and the notification will be shown immediately. + */ + +fireDate?: ?number, + /** + * Seconds from now to display the notification. + * + * Schedule notifications using EITHER `fireDate` or `fireIntervalSeconds`. + * If both are specified, `fireDate` takes precedence. + * If you use `presentLocalNotification`, both will be ignored + * and the notification will be shown immediately. + */ + +fireIntervalSeconds?: ?number, + /** Badge count to display on the app icon. */ +applicationIconBadgeNumber?: ?number, + /** Whether to silence the notification sound. */ +isSilent?: ?boolean, + /** + * Custom notification sound to play. Write-only: soundName will be null when + * accessing already created notifications using getScheduledLocalNotifications + * or getDeliveredNotifications. + */ +soundName?: ?string, + /** DEPRECATED. This was used for iOS's legacy UILocalNotification. */ + +alertAction?: ?string, + /** DEPRECATED. Use `fireDate` or `fireIntervalSeconds` instead. */ + +repeatInterval?: ?string, |}; export interface Spec extends TurboModule { diff --git a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.h b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.h index fc912b4bef5cb8..f6fdb8c4e7e660 100644 --- a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.h +++ b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.h @@ -16,9 +16,6 @@ typedef void (^RCTRemoteNotificationCallback)(UIBackgroundFetchResult result); #endif // [macOS] #if !TARGET_OS_UIKITFORMAC -#if !TARGET_OS_OSX // [macOS] -+ (void)didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings; -#endif // [macOS] + (void)didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken; + (void)didReceiveRemoteNotification:(NSDictionary *)notification; #if !TARGET_OS_OSX // [macOS] diff --git a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm index 7d575e2fe01de7..c0c08ab746c853 100644 --- a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm +++ b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm @@ -25,88 +25,66 @@ static NSString *const kErrorUnableToRequestPermissions = @"E_UNABLE_TO_REQUEST_PERMISSIONS"; #if !TARGET_OS_UIKITFORMAC -@implementation RCTConvert (NSCalendarUnit) - -RCT_ENUM_CONVERTER( - NSCalendarUnit, - (@{ - @"year" : @(NSCalendarUnitYear), - @"month" : @(NSCalendarUnitMonth), - @"week" : @(NSCalendarUnitWeekOfYear), - @"day" : @(NSCalendarUnitDay), - @"hour" : @(NSCalendarUnitHour), - @"minute" : @(NSCalendarUnitMinute) - }), - 0, - integerValue) - -@end @interface RCTPushNotificationManager () @property (nonatomic, strong) NSMutableDictionary *remoteNotificationCallbacks; @end -@implementation RCTConvert (UILocalNotification) +@implementation RCTConvert (UNNotificationContent) -#if !TARGET_OS_OSX // [macOS] -+ (UILocalNotification *)UILocalNotification:(id)json ++ (UNNotificationContent *)UNNotificationContent:(id)json { NSDictionary *details = [self NSDictionary:json]; BOOL isSilent = [RCTConvert BOOL:details[@"isSilent"]]; - UILocalNotification *notification = [UILocalNotification new]; - notification.alertTitle = [RCTConvert NSString:details[@"alertTitle"]]; - notification.fireDate = [RCTConvert NSDate:details[@"fireDate"]] ?: [NSDate date]; - notification.alertBody = [RCTConvert NSString:details[@"alertBody"]]; - notification.alertAction = [RCTConvert NSString:details[@"alertAction"]]; - notification.userInfo = [RCTConvert NSDictionary:details[@"userInfo"]]; - notification.category = [RCTConvert NSString:details[@"category"]]; - notification.repeatInterval = [RCTConvert NSCalendarUnit:details[@"repeatInterval"]]; + 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"]) { - notification.applicationIconBadgeNumber = [RCTConvert NSInteger:details[@"applicationIconBadgeNumber"]]; + content.badge = [RCTConvert NSNumber:details[@"applicationIconBadgeNumber"]]; } if (!isSilent) { - notification.soundName = [RCTConvert NSString:details[@"soundName"]] ?: UILocalNotificationDefaultSoundName; + NSString *soundName = [RCTConvert NSString:details[@"soundName"]]; + content.sound = + soundName ? [UNNotificationSound soundNamed:details[@"soundName"]] : [UNNotificationSound defaultSound]; } - return notification; + + return content; } -#else // [macOS -+ (NSUserNotification *)NSUserNotification:(id)json + ++ (NSDictionary *)NSDictionaryForNotification: + (JS::NativePushNotificationManagerIOS::Notification &)notification { - NSDictionary *details = [self NSDictionary:json]; - BOOL isSilent = [RCTConvert BOOL:details[@"isSilent"]]; - NSUserNotification *notification = [NSUserNotification new]; - notification.deliveryDate = [RCTConvert NSDate:details[@"fireDate"]] ?: [NSDate date]; - notification.informativeText = [RCTConvert NSString:details[@"alertBody"]]; - NSString *title = [RCTConvert NSString:details[@"alertTitle"]]; - if (title) { - notification.title = title; - } - NSString *actionButtonTitle = [RCTConvert NSString:details[@"alertAction"]]; - if (actionButtonTitle) { - notification.actionButtonTitle = actionButtonTitle; + // 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()); } - notification.userInfo = [RCTConvert NSDictionary:details[@"userInfo"]]; - - NSCalendarUnit calendarUnit = [RCTConvert NSCalendarUnit:details[@"repeatInterval"]]; - if (calendarUnit > 0) { - NSDateComponents *dateComponents = [NSDateComponents new]; - [dateComponents setValue:1 forComponent:calendarUnit]; - notification.deliveryRepeatInterval = dateComponents; + if (notification.fireDate()) { + notificationDict[@"fireDate"] = @(*notification.fireDate()); } - if (!isSilent) { - notification.soundName = [RCTConvert NSString:details[@"soundName"]] ?: NSUserNotificationDefaultSoundName; + if (notification.applicationIconBadgeNumber()) { + notificationDict[@"applicationIconBadgeNumber"] = @(*notification.applicationIconBadgeNumber()); } - - NSString *identifier = [RCTConvert NSString:details[@"identifier"]]; - if (identifier == nil) { - identifier = [[NSUUID UUID] UUIDString]; + if (notification.isSilent()) { + notificationDict[@"isSilent"] = @(*notification.isSilent()); + if ([notificationDict[@"isSilent"] isEqualToNumber:@(NO)]) { + notificationDict[@"soundName"] = notification.soundName(); + } } - notification.identifier = identifier; - return notification; + return notificationDict; } -#endif // macOS] + +@end #if !TARGET_OS_OSX // [macOS] +@implementation RCTConvert (UIBackgroundFetchResult) + RCT_ENUM_CONVERTER( UIBackgroundFetchResult, (@{ @@ -116,9 +94,9 @@ + (NSUserNotification *)NSUserNotification:(id)json }), UIBackgroundFetchResultNoData, integerValue) -#endif // [macOS] @end +#endif // [macOS] #else @interface RCTPushNotificationManager () @end @@ -126,8 +104,10 @@ @interface RCTPushNotificationManager () @implementation RCTPushNotificationManager -#if !TARGET_OS_UIKITFORMAC && !TARGET_OS_OSX // [macOS] +#if !TARGET_OS_UIKITFORMAC +#if !TARGET_OS_OSX // [macOS] +/** DEPRECATED. UILocalNotification was deprecated in iOS 10. Please don't add new callsites. */ static NSDictionary *RCTFormatLocalNotification(UILocalNotification *notification) { NSMutableDictionary *formattedLocalNotification = [NSMutableDictionary dictionary]; @@ -146,33 +126,7 @@ @implementation RCTPushNotificationManager formattedLocalNotification[@"remote"] = @NO; return formattedLocalNotification; } - -static NSDictionary *RCTFormatUNNotification(UNNotification *notification) -{ - NSMutableDictionary *formattedNotification = [NSMutableDictionary dictionary]; - UNNotificationContent *content = notification.request.content; - - formattedNotification[@"identifier"] = notification.request.identifier; - - if (notification.date) { - NSDateFormatter *formatter = [NSDateFormatter new]; - [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"]; - NSString *dateString = [formatter stringFromDate:notification.date]; - formattedNotification[@"date"] = dateString; - } - - formattedNotification[@"title"] = RCTNullIfNil(content.title); - formattedNotification[@"body"] = RCTNullIfNil(content.body); - formattedNotification[@"category"] = RCTNullIfNil(content.categoryIdentifier); - formattedNotification[@"thread-id"] = RCTNullIfNil(content.threadIdentifier); - formattedNotification[@"userInfo"] = RCTNullIfNil(RCTJSONClean(content.userInfo)); - - return formattedNotification; -} - -#endif // TARGET_OS_UIKITFORMAC -#if TARGET_OS_OSX // [macOS - +#else // [macOS static NSDictionary *RCTFormatUserNotification(NSUserNotification *notification) { NSMutableDictionary *formattedUserNotification = [NSMutableDictionary dictionary]; @@ -192,6 +146,60 @@ @implementation RCTPushNotificationManager } #endif // macOS] +/** 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]; +} + +#endif // TARGET_OS_UIKITFORMAC + RCT_EXPORT_MODULE() - (dispatch_queue_t)methodQueue @@ -235,12 +243,6 @@ - (void)stopObserving ]; } -#if !TARGET_OS_OSX // [macOS] -+ (void)didRegisterUserNotificationSettings:(__unused UIUserNotificationSettings *)notificationSettings -{ -} -#endif // [macOS] - + (void)didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { NSMutableString *hexString = [NSMutableString string]; @@ -278,16 +280,16 @@ + (void)didReceiveRemoteNotification:(NSDictionary *)notification object:self userInfo:userInfo]; } +#endif // [macOS] +#if !TARGET_OS_OSX // [macOS] + (void)didReceiveLocalNotification:(UILocalNotification *)notification { [[NSNotificationCenter defaultCenter] postNotificationName:kLocalNotificationReceived object:self userInfo:RCTFormatLocalNotification(notification)]; } - -#else // [macOS - +#else // [macOS + (void)didReceiveUserNotification:(NSUserNotification *)notification { NSString *notificationName = notification.isRemote ? RCTRemoteNotificationReceived : kLocalNotificationReceived; @@ -296,7 +298,6 @@ + (void)didReceiveUserNotification:(NSUserNotification *)notification object:self userInfo:userInfo]; } - #endif // macOS] - (void)handleLocalNotificationReceived:(NSNotification *)notification @@ -306,15 +307,13 @@ - (void)handleLocalNotificationReceived:(NSNotification *)notification - (void)handleRemoteNotificationReceived:(NSNotification *)notification { +#if !TARGET_OS_OSX // [macOS] NSMutableDictionary *remoteNotification = [NSMutableDictionary dictionaryWithDictionary:notification.userInfo[@"notification"]]; -#if !TARGET_OS_OSX // [macOS] RCTRemoteNotificationCallback completionHandler = notification.userInfo[@"completionHandler"]; -#endif // [macOS] NSString *notificationId = [[NSUUID UUID] UUIDString]; remoteNotification[@"notificationId"] = notificationId; remoteNotification[@"remote"] = @YES; -#if !TARGET_OS_OSX // [macOS] if (completionHandler) { if (!self.remoteNotificationCallbacks) { // Lazy initialization @@ -322,9 +321,9 @@ - (void)handleRemoteNotificationReceived:(NSNotification *)notification } self.remoteNotificationCallbacks[notificationId] = completionHandler; } -#endif // [macOS] [self sendEventWithName:@"remoteNotificationReceived" body:remoteNotification]; +#endif // [macOS] } - (void)handleRemoteNotificationsRegistered:(NSNotification *)notification @@ -343,9 +342,9 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification [self sendEventWithName:@"remoteNotificationRegistrationError" body:errorDetails]; } -#if !TARGET_OS_OSX // [macOS] RCT_EXPORT_METHOD(onFinishRemoteNotification : (NSString *)notificationId fetchResult : (NSString *)fetchResult) { +#if !TARGET_OS_OSX // [macOS] UIBackgroundFetchResult result = [RCTConvert UIBackgroundFetchResult:fetchResult]; RCTRemoteNotificationCallback completionHandler = self.remoteNotificationCallbacks[notificationId]; if (!completionHandler) { @@ -354,8 +353,8 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification } completionHandler(result); [self.remoteNotificationCallbacks removeObjectForKey:notificationId]; -} #endif // [macOS] +} /** * Update the application icon badge number on the home screen @@ -401,33 +400,19 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification // Add a listener to make sure that startObserving has been called [self addListener:@"remoteNotificationsRegistered"]; -#if !TARGET_OS_OSX // [macOS - UIUserNotificationType types = UIUserNotificationTypeNone; + UNAuthorizationOptions options = UNAuthorizationOptionNone; if (permissions.alert()) { - types |= UIUserNotificationTypeAlert; + options |= UNAuthorizationOptionAlert; } if (permissions.badge()) { - types |= UIUserNotificationTypeBadge; + options |= UNAuthorizationOptionBadge; } if (permissions.sound()) { - types |= UIUserNotificationTypeSound; + options |= UNAuthorizationOptionSound; } -#else - NSRemoteNotificationType types = NSRemoteNotificationTypeNone; - - if (permissions.alert()) { - types |= NSRemoteNotificationTypeAlert; - } - if (permissions.badge()) { - types |= NSRemoteNotificationTypeBadge; - } - if (permissions.sound()) { - types |= NSRemoteNotificationTypeSound; - } -#endif // macOS] [UNUserNotificationCenter.currentNotificationCenter - requestAuthorizationWithOptions:types + requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError *_Nullable error) { if (error != NULL) { reject(@"-1", @"Error - Push authorization request failed.", error); @@ -496,108 +481,87 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification }; } -#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]; + 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]; } -#endif // macOS] -#if !TARGET_OS_OSX // [macOS] RCT_EXPORT_METHOD(scheduleLocalNotification : (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(); - } + 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]; } - [RCTSharedApplication() scheduleLocalNotification:[RCTConvert UILocalNotification:notificationDict]]; -} -#else // [macOS -RCT_EXPORT_METHOD(scheduleLocalNotification:(NSUserNotification *)notification) -{ - [[NSUserNotificationCenter defaultUserNotificationCenter] scheduleNotification:notification]; + + UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:[[NSUUID UUID] UUIDString] + content:content + trigger:trigger]; + + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center addNotificationRequest:request withCompletionHandler:nil]; } -#endif // macOS] RCT_EXPORT_METHOD(cancelAllLocalNotifications) { -#if !TARGET_OS_OSX // [macOS] - [RCTSharedApplication() cancelAllLocalNotifications]; -#else // [macOS - for (NSUserNotification *notif in [NSUserNotificationCenter defaultUserNotificationCenter].scheduledNotifications) { - [[NSUserNotificationCenter defaultUserNotificationCenter] removeScheduledNotification:notif]; - } -#endif // macOS] + [[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) { -#if !TARGET_OS_OSX // [macOS] - for (UILocalNotification *notification in RCTSharedApplication().scheduledLocalNotifications) { -#else // [macOS - for (NSUserNotification *notification in [NSUserNotificationCenter defaultUserNotificationCenter].scheduledNotifications) { -#endif // macOS] - __block BOOL matchesAll = YES; - NSDictionary *notificationInfo = notification.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. - [userInfo enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { - if (![notificationInfo[key] isEqual:obj]) { - matchesAll = NO; - *stop = YES; + 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]; } - }]; -#if !TARGET_OS_OSX // [macOS] - if (matchesAll) { - [RCTSharedApplication() cancelLocalNotification:notification]; - } -#else // [macOS - if ([notification.identifier isEqualToString:userInfo[@"identifier"]] || matchesAll) { - [[NSUserNotificationCenter defaultUserNotificationCenter] removeScheduledNotification:notification]; } -#endif // macOS] - } + + [center removePendingNotificationRequestsWithIdentifiers:notificationIdentifiersToCancel]; + }]; } RCT_EXPORT_METHOD(getInitialNotification @@ -631,50 +595,30 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification RCT_EXPORT_METHOD(getScheduledLocalNotifications : (RCTResponseSenderBlock)callback) { -#if !TARGET_OS_OSX // [macOS] - NSArray *scheduledLocalNotifications = RCTSharedApplication().scheduledLocalNotifications; -#endif // [macOS] - NSMutableArray *formattedScheduledLocalNotifications = [NSMutableArray new]; -#if !TARGET_OS_OSX // [macOS] - for (UILocalNotification *notification in scheduledLocalNotifications) { - [formattedScheduledLocalNotifications addObject:RCTFormatLocalNotification(notification)]; - } -#else // [macOS - for (NSUserNotification *notification in [NSUserNotificationCenter defaultUserNotificationCenter].scheduledNotifications) { - [formattedScheduledLocalNotifications addObject:RCTFormatUserNotification(notification)]; - } -#endif // macOS] - callback(@[ formattedScheduledLocalNotifications ]); + 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) { -#if !TARGET_OS_OSX // [macOS] UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center removeAllDeliveredNotifications]; -#else // [macOS - [[NSUserNotificationCenter defaultUserNotificationCenter] removeAllDeliveredNotifications]; -#endif // macOS] } RCT_EXPORT_METHOD(removeDeliveredNotifications : (NSArray *)identifiers) { -#if !TARGET_OS_OSX // [macOS] UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center removeDeliveredNotificationsWithIdentifiers:identifiers]; -#else // [macOS - NSArray *notificationsToRemove = [[NSUserNotificationCenter defaultUserNotificationCenter].deliveredNotifications filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSUserNotification* evaluatedObject, NSDictionary * _Nullable bindings) { - return [identifiers containsObject:evaluatedObject.identifier]; - }]]; - for (NSUserNotification *notification in notificationsToRemove) { - [[NSUserNotificationCenter defaultUserNotificationCenter] removeDeliveredNotification:notification]; - } -#endif // macOS] } RCT_EXPORT_METHOD(getDeliveredNotifications : (RCTResponseSenderBlock)callback) { -#if !TARGET_OS_OSX // [macOS] UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center getDeliveredNotificationsWithCompletionHandler:^(NSArray *_Nonnull notifications) { NSMutableArray *formattedNotifications = [NSMutableArray new]; @@ -684,13 +628,6 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification } callback(@[ formattedNotifications ]); }]; -#else // [macOS - NSMutableArray *formattedNotifications = [NSMutableArray new]; - for (NSUserNotification *notification in [NSUserNotificationCenter defaultUserNotificationCenter].deliveredNotifications) { - [formattedNotifications addObject:RCTFormatUserNotification(notification)]; - } - callback(@[formattedNotifications]); -#endif // macOS] } RCT_EXPORT_METHOD(getAuthorizationStatus : (RCTResponseSenderBlock)callback) diff --git a/packages/react-native/Libraries/Text/Text/RCTTextView.m b/packages/react-native/Libraries/Text/Text/RCTTextView.m index 9ac677b4e90b3d..f8934d1a353b49 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextView.m +++ b/packages/react-native/Libraries/Text/Text/RCTTextView.m @@ -37,7 +37,16 @@ - (BOOL)canBecomeKeyView @end +#endif // macOS] + +#if !TARGET_OS_OSX // [macOS] +@interface RCTTextView () + +@property (nonatomic, nullable) UIEditMenuInteraction *editMenuInteraction API_AVAILABLE(ios(16.0)); +#else // [macOS @interface RCTTextView () +// macOS] + @end #endif // macOS] @@ -358,6 +367,10 @@ - (void)enableContextMenu { _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)]; + if (@available(iOS 16.0, *)) { + _editMenuInteraction = [[UIEditMenuInteraction alloc] initWithDelegate:self]; + [self addInteraction:_editMenuInteraction]; + } [self addGestureRecognizer:_longPressGestureRecognizer]; } @@ -369,8 +382,16 @@ - (void)disableContextMenu - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture { - // TODO: Adopt showMenuFromRect (necessary for UIKitForMac) #if !TARGET_OS_UIKITFORMAC + if (@available(iOS 16.0, *)) { + CGPoint location = [gesture locationInView:self]; + UIEditMenuConfiguration *config = [UIEditMenuConfiguration configurationWithIdentifier:nil sourcePoint:location]; + if (_editMenuInteraction) { + [_editMenuInteraction presentEditMenuWithConfiguration:config]; + } + return; + } + // TODO: Adopt showMenuFromRect (necessary for UIKitForMac) UIMenuController *menuController = [UIMenuController sharedMenuController]; if (menuController.isMenuVisible) { diff --git a/packages/react-native/React/Base/RCTUtils.m b/packages/react-native/React/Base/RCTUtils.m index 2ba69e0d0b6720..f741dcf613b020 100644 --- a/packages/react-native/React/Base/RCTUtils.m +++ b/packages/react-native/React/Base/RCTUtils.m @@ -305,14 +305,14 @@ static void RCTUnsafeExecuteOnMainQueueOnceSync(dispatch_once_t *onceToken, disp void RCTComputeScreenScale() { dispatch_once(&onceTokenScreenScale, ^{ - screenScale = [UIScreen mainScreen].scale; + screenScale = [UITraitCollection currentTraitCollection].displayScale; }); } CGFloat RCTScreenScale() { RCTUnsafeExecuteOnMainQueueOnceSync(&onceTokenScreenScale, ^{ - screenScale = [UIScreen mainScreen].scale; + screenScale = [UITraitCollection currentTraitCollection].displayScale; }); return screenScale; @@ -600,12 +600,20 @@ BOOL RCTRunningInAppExtension(void) return nil; } - // TODO: replace with a more robust solution - for (UIWindow *window in RCTSharedApplication().windows) { - if (window.keyWindow) { - return window; + for (UIScene *scene in RCTSharedApplication().connectedScenes) { + if (scene.activationState != UISceneActivationStateForegroundActive || + ![scene isKindOfClass:[UIWindowScene class]]) { + continue; + } + UIWindowScene *windowScene = (UIWindowScene *)scene; + + for (UIWindow *window in windowScene.windows) { + if (window.isKeyWindow) { + return window; + } } } + return nil; } @@ -630,12 +638,10 @@ BOOL RCTForceTouchAvailable(void) static BOOL forceSupported; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - forceSupported = - [UITraitCollection class] && [UITraitCollection instancesRespondToSelector:@selector(forceTouchCapability)]; + forceSupported = [UITraitCollection currentTraitCollection].forceTouchCapability == UIForceTouchCapabilityAvailable; }); - return forceSupported && - (RCTKeyWindow() ?: [UIView new]).traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable; + return forceSupported; } #endif // [macOS] diff --git a/packages/react-native/React/CoreModules/RCTAlertController.m b/packages/react-native/React/CoreModules/RCTAlertController.m index d7b1b9da52f323..30a41c6f246369 100644 --- a/packages/react-native/React/CoreModules/RCTAlertController.m +++ b/packages/react-native/React/CoreModules/RCTAlertController.m @@ -23,17 +23,7 @@ @implementation RCTAlertController - (UIWindow *)alertWindow { if (_alertWindow == nil) { - _alertWindow = [self getUIWindowFromScene]; - - if (_alertWindow == nil) { - UIWindow *keyWindow = RCTSharedApplication().keyWindow; - if (keyWindow) { - _alertWindow = [[UIWindow alloc] initWithFrame:keyWindow.bounds]; - } else { - // keyWindow is nil, so we cannot create and initialize _alertWindow - NSLog(@"Unable to create alert window: keyWindow is nil"); - } - } + _alertWindow = [[UIWindow alloc] initWithWindowScene:RCTKeyWindow().windowScene]; if (_alertWindow) { _alertWindow.rootViewController = [UIViewController new]; @@ -78,19 +68,6 @@ - (void)hide _alertWindow = nil; } - -- (UIWindow *)getUIWindowFromScene -{ - if (@available(iOS 13.0, *)) { - for (UIScene *scene in RCTSharedApplication().connectedScenes) { - if (scene.activationState == UISceneActivationStateForegroundActive && - [scene isKindOfClass:[UIWindowScene class]]) { - return [[UIWindow alloc] initWithWindowScene:(UIWindowScene *)scene]; - } - } - } - return nil; -} #endif // [macOS] @end diff --git a/packages/react-native/React/CoreModules/RCTDevLoadingView.mm b/packages/react-native/React/CoreModules/RCTDevLoadingView.mm index 4d7d3725003f48..b278913ebf289a 100644 --- a/packages/react-native/React/CoreModules/RCTDevLoadingView.mm +++ b/packages/react-native/React/CoreModules/RCTDevLoadingView.mm @@ -139,15 +139,15 @@ - (void)showMessage:(NSString *)message color:(RCTUIColor *)color backgroundColo dispatch_async(dispatch_get_main_queue(), ^{ self->_showDate = [NSDate date]; + if (!self->_window && !RCTRunningInTestEnvironment()) { #if !TARGET_OS_OSX // [macOS] - CGSize screenSize = [UIScreen mainScreen].bounds.size; + UIWindow *window = RCTKeyWindow(); + CGFloat windowWidth = window.bounds.size.width; - UIWindow *window = RCTSharedApplication().keyWindow; - self->_window = - [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, screenSize.width, window.safeAreaInsets.top + 10)]; - self->_label = - [[UILabel alloc] initWithFrame:CGRectMake(0, window.safeAreaInsets.top - 10, screenSize.width, 20)]; + self->_window = [[UIWindow alloc] initWithWindowScene:window.windowScene]; + self->_window.frame = CGRectMake(0, 0, windowWidth, window.safeAreaInsets.top + 10); + self->_label = [[UILabel alloc] initWithFrame:CGRectMake(0, window.safeAreaInsets.top - 10, windowWidth, 20)]; [self->_window addSubview:self->_label]; self->_window.windowLevel = UIWindowLevelStatusBar + 1; @@ -186,17 +186,11 @@ - (void)showMessage:(NSString *)message color:(RCTUIColor *)color backgroundColo #else // [macOS self->_label.stringValue = message; self->_label.textColor = color; + self->_label.backgroundColor = backgroundColor; [self->_window orderFront:nil]; #endif // macOS] -#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \ - __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0 - if (@available(iOS 13.0, *)) { - UIWindowScene *scene = (UIWindowScene *)RCTSharedApplication().connectedScenes.anyObject; - self->_window.windowScene = scene; - } -#endif }); } diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm index eddff908cfaf88..5ded35c6dedce9 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm @@ -27,6 +27,12 @@ using namespace facebook::react; +@interface RCTParagraphComponentView () + +@property (nonatomic, nullable) UIEditMenuInteraction *editMenuInteraction API_AVAILABLE(ios(16.0)); + +@end + @implementation RCTParagraphComponentView { ParagraphShadowNode::ConcreteState::Shared _state; ParagraphAttributes _paragraphAttributes; @@ -223,19 +229,36 @@ - (void)enableContextMenu { _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)]; + + if (@available(iOS 16.0, *)) { + _editMenuInteraction = [[UIEditMenuInteraction alloc] initWithDelegate:self]; + [self addInteraction:_editMenuInteraction]; + } [self addGestureRecognizer:_longPressGestureRecognizer]; } - (void)disableContextMenu { [self removeGestureRecognizer:_longPressGestureRecognizer]; + if (@available(iOS 16.0, *)) { + [self removeInteraction:_editMenuInteraction]; + _editMenuInteraction = nil; + } _longPressGestureRecognizer = nil; } - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture { - // TODO: Adopt showMenuFromRect (necessary for UIKitForMac) #if !TARGET_OS_UIKITFORMAC + if (@available(iOS 16.0, *)) { + CGPoint location = [gesture locationInView:self]; + UIEditMenuConfiguration *config = [UIEditMenuConfiguration configurationWithIdentifier:nil sourcePoint:location]; + if (_editMenuInteraction) { + [_editMenuInteraction presentEditMenuWithConfiguration:config]; + } + return; + } + // TODO: Adopt showMenuFromRect (necessary for UIKitForMac) UIMenuController *menuController = [UIMenuController sharedMenuController]; if (menuController.isMenuVisible) { diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index e19a16c9fdd165..02991f8a8850e6 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -255,7 +255,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & if (oldViewProps.shouldRasterize != newViewProps.shouldRasterize) { self.layer.shouldRasterize = newViewProps.shouldRasterize; #if !TARGET_OS_OSX // [macOS] - self.layer.rasterizationScale = newViewProps.shouldRasterize ? [UIScreen mainScreen].scale : 1.0; + self.layer.rasterizationScale = newViewProps.shouldRasterize ? self.traitCollection.displayScale : 1.0; #else // [macOS self.layer.rasterizationScale = 1.0; #endif // macOS] diff --git a/packages/react-native/React/UIUtils/RCTUIUtils.m b/packages/react-native/React/UIUtils/RCTUIUtils.m index fdac0db4c06432..834dd103f77ac0 100644 --- a/packages/react-native/React/UIUtils/RCTUIUtils.m +++ b/packages/react-native/React/UIUtils/RCTUIUtils.m @@ -15,8 +15,7 @@ RCTDimensions RCTGetDimensions(CGFloat fontScale) UIScreen *mainScreen = UIScreen.mainScreen; CGSize screenSize = mainScreen.bounds.size; - UIView *mainWindow; - mainWindow = RCTKeyWindow(); + UIView *mainWindow = RCTKeyWindow(); // We fallback to screen size if a key window is not found. CGSize windowSize = mainWindow ? mainWindow.bounds.size : screenSize; diff --git a/packages/react-native/React/Views/RCTViewManager.m b/packages/react-native/React/Views/RCTViewManager.m index b2bd47511ce1e6..4dc0f45004614e 100644 --- a/packages/react-native/React/Views/RCTViewManager.m +++ b/packages/react-native/React/Views/RCTViewManager.m @@ -235,7 +235,7 @@ - (RCTShadowView *)shadowView { view.layer.shouldRasterize = json ? [RCTConvert BOOL:json] : defaultView.layer.shouldRasterize; view.layer.rasterizationScale = - view.layer.shouldRasterize ? [UIScreen mainScreen].scale : defaultView.layer.rasterizationScale; + view.layer.shouldRasterize ? view.traitCollection.displayScale : defaultView.layer.rasterizationScale; } #endif // [macOS] diff --git a/packages/rn-tester/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.m b/packages/rn-tester/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.m index 266502d69ca6d1..c7e36bf0232c26 100644 --- a/packages/rn-tester/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.m +++ b/packages/rn-tester/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.m @@ -240,14 +240,15 @@ - (NSString *)_fileNameForSelector:(SEL)selector if (0 < identifier.length) { fileName = [fileName stringByAppendingFormat:@"_%@", identifier]; } - CGFloat scale; // [macOS -#if !TARGET_OS_OSX - scale = [[UIScreen mainScreen] scale]; -#else +#if !TARGET_OS_OSX // [macOS] + UITraitCollection *currentTraitCollection = [UITraitCollection currentTraitCollection]; + if (currentTraitCollection.displayScale > 1.0) { + fileName = [fileName stringByAppendingFormat:@"@%.fx", currentTraitCollection.displayScale]; +#else // [macOS scale = [[NSScreen mainScreen] backingScaleFactor]; -#endif - if (scale > 1.0) { // macOS] + if (scale > 1.0) { fileName = [fileName stringByAppendingFormat:@"@%.fx", scale]; +#endif // macOS] } fileName = [fileName stringByAppendingPathExtension:@"png"]; return fileName; diff --git a/packages/rn-tester/RCTTest/FBSnapshotTestCase/UIImage+Compare.m b/packages/rn-tester/RCTTest/FBSnapshotTestCase/UIImage+Compare.m index 964efed23099cb..9aa90fa83c6e0d 100644 --- a/packages/rn-tester/RCTTest/FBSnapshotTestCase/UIImage+Compare.m +++ b/packages/rn-tester/RCTTest/FBSnapshotTestCase/UIImage+Compare.m @@ -45,14 +45,13 @@ - (BOOL)compareWithImage:(UIImage *)image (CGBitmapInfo)kCGImageAlphaPremultipliedLast); #if !TARGET_OS_OSX // [macOS] - CGFloat scaleFactor = [UIScreen mainScreen].scale; + CGFloat scaleFactor = [UITraitCollection currentTraitCollection].displayScale; #else // [macOS // The compareWithImage: method is used for integration test snapshot image comparison. // The _snapshotView: method that creates snapshot images that are *not* scaled for the screen. // By not using the screen scale factor in this method the test results are machine independent. CGFloat scaleFactor = 1; #endif // macOS] - CGContextScaleCTM(referenceImageContext, scaleFactor, scaleFactor); CGContextScaleCTM(imageContext, scaleFactor, scaleFactor); diff --git a/packages/rn-tester/RNTester/AppDelegate.mm b/packages/rn-tester/RNTester/AppDelegate.mm index 7f98f5bcaa85a4..f40b76bd051bd7 100644 --- a/packages/rn-tester/RNTester/AppDelegate.mm +++ b/packages/rn-tester/RNTester/AppDelegate.mm @@ -338,15 +338,6 @@ - (void)registerPaperComponents:(NSArray *)components #if !TARGET_OS_TV && !TARGET_OS_UIKITFORMAC -#if !TARGET_OS_OSX // [macOS] -// Required to register for notifications -- (void)application:(__unused UIApplication *)application - didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings -{ - [RCTPushNotificationManager didRegisterUserNotificationSettings:notificationSettings]; -} -#endif // [macOS] - // Required for the remoteNotificationsRegistered event. - (void)application:(__unused RCTUIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken