react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.m

478 lines
18 KiB
Mathematica
Raw Normal View History

/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "RCTPushNotificationManager.h"
#import <UserNotifications/UserNotifications.h>
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTUtils.h>
NSString *const RCTRemoteNotificationReceived = @"RemoteNotificationReceived";
static NSString *const kLocalNotificationReceived = @"LocalNotificationReceived";
static NSString *const kRemoteNotificationsRegistered = @"RemoteNotificationsRegistered";
static NSString *const kRegisterUserNotificationSettings = @"RegisterUserNotificationSettings";
static NSString *const kRemoteNotificationRegistrationFailed = @"RemoteNotificationRegistrationFailed";
static NSString *const kErrorUnableToRequestPermissions = @"E_UNABLE_TO_REQUEST_PERMISSIONS";
#if !TARGET_OS_TV
@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)
+ (UILocalNotification *)UILocalNotification:(id)json
{
NSDictionary<NSString *, id> *details = [self NSDictionary:json];
BOOL isSilent = [RCTConvert BOOL:details[@"isSilent"]];
UILocalNotification *notification = [UILocalNotification new];
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"]];
if (details[@"applicationIconBadgeNumber"]) {
notification.applicationIconBadgeNumber = [RCTConvert NSInteger:details[@"applicationIconBadgeNumber"]];
}
if (!isSilent) {
notification.soundName = [RCTConvert NSString:details[@"soundName"]] ?: UILocalNotificationDefaultSoundName;
}
return notification;
}
RCT_ENUM_CONVERTER(UIBackgroundFetchResult, (@{
@"UIBackgroundFetchResultNewData": @(UIBackgroundFetchResultNewData),
@"UIBackgroundFetchResultNoData": @(UIBackgroundFetchResultNoData),
@"UIBackgroundFetchResultFailed": @(UIBackgroundFetchResultFailed),
}), UIBackgroundFetchResultNoData, integerValue)
@end
#endif //TARGET_OS_TV
@implementation RCTPushNotificationManager
{
RCTPromiseResolveBlock _requestPermissionsResolveBlock;
}
#if !TARGET_OS_TV
static NSDictionary *RCTFormatLocalNotification(UILocalNotification *notification)
{
NSMutableDictionary *formattedLocalNotification = [NSMutableDictionary dictionary];
if (notification.fireDate) {
NSDateFormatter *formatter = [NSDateFormatter new];
[formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"];
NSString *fireDateString = [formatter stringFromDate:notification.fireDate];
formattedLocalNotification[@"fireDate"] = fireDateString;
}
formattedLocalNotification[@"alertAction"] = RCTNullIfNil(notification.alertAction);
formattedLocalNotification[@"alertBody"] = RCTNullIfNil(notification.alertBody);
formattedLocalNotification[@"applicationIconBadgeNumber"] = @(notification.applicationIconBadgeNumber);
formattedLocalNotification[@"category"] = RCTNullIfNil(notification.category);
formattedLocalNotification[@"soundName"] = RCTNullIfNil(notification.soundName);
formattedLocalNotification[@"userInfo"] = RCTNullIfNil(RCTJSONClean(notification.userInfo));
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_TV
RCT_EXPORT_MODULE()
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
#if !TARGET_OS_TV
- (void)startObserving
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleLocalNotificationReceived:)
name:kLocalNotificationReceived
object:nil];
Refactored module access to allow for lazy loading Summary: public The `bridge.modules` dictionary provides access to all native modules, but this API requires that every module is initialized in advance so that any module can be accessed. This diff introduces a better API that will allow modules to be initialized lazily as they are needed, and deprecates `bridge.modules` (modules that use it will still work, but should be rewritten to use `bridge.moduleClasses` or `-[bridge moduleForName/Class:` instead. The rules are now as follows: * Any module that overrides `init` or `setBridge:` will be initialized on the main thread when the bridge is created * Any module that implements `constantsToExport:` will be initialized later when the config is exported (the module itself will be initialized on a background queue, but `constantsToExport:` will still be called on the main thread. * All other modules will be initialized lazily when a method is first called on them. These rules may seem slightly arcane, but they have the advantage of not violating any assumptions that may have been made by existing code - any module written under the original assumption that it would be initialized synchronously on the main thread when the bridge is created should still function exactly the same, but modules that avoid overriding `init` or `setBridge:` will now be loaded lazily. I've rewritten most of the standard modules to take advantage of this new lazy loading, with the following results: Out of the 65 modules included in UIExplorer: * 16 are initialized on the main thread when the bridge is created * A further 8 are initialized when the config is exported to JS * The remaining 41 will be initialized lazily on-demand Reviewed By: jspahrsummers Differential Revision: D2677695 fb-gh-sync-id: 507ae7e9fd6b563e89292c7371767c978e928f33
2015-11-25 11:09:00 +00:00
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleRemoteNotificationReceived:)
name:RCTRemoteNotificationReceived
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleRegisterUserNotificationSettings:)
name:kRegisterUserNotificationSettings
object:nil];
Refactored module access to allow for lazy loading Summary: public The `bridge.modules` dictionary provides access to all native modules, but this API requires that every module is initialized in advance so that any module can be accessed. This diff introduces a better API that will allow modules to be initialized lazily as they are needed, and deprecates `bridge.modules` (modules that use it will still work, but should be rewritten to use `bridge.moduleClasses` or `-[bridge moduleForName/Class:` instead. The rules are now as follows: * Any module that overrides `init` or `setBridge:` will be initialized on the main thread when the bridge is created * Any module that implements `constantsToExport:` will be initialized later when the config is exported (the module itself will be initialized on a background queue, but `constantsToExport:` will still be called on the main thread. * All other modules will be initialized lazily when a method is first called on them. These rules may seem slightly arcane, but they have the advantage of not violating any assumptions that may have been made by existing code - any module written under the original assumption that it would be initialized synchronously on the main thread when the bridge is created should still function exactly the same, but modules that avoid overriding `init` or `setBridge:` will now be loaded lazily. I've rewritten most of the standard modules to take advantage of this new lazy loading, with the following results: Out of the 65 modules included in UIExplorer: * 16 are initialized on the main thread when the bridge is created * A further 8 are initialized when the config is exported to JS * The remaining 41 will be initialized lazily on-demand Reviewed By: jspahrsummers Differential Revision: D2677695 fb-gh-sync-id: 507ae7e9fd6b563e89292c7371767c978e928f33
2015-11-25 11:09:00 +00:00
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleRemoteNotificationsRegistered:)
name:kRemoteNotificationsRegistered
Refactored module access to allow for lazy loading Summary: public The `bridge.modules` dictionary provides access to all native modules, but this API requires that every module is initialized in advance so that any module can be accessed. This diff introduces a better API that will allow modules to be initialized lazily as they are needed, and deprecates `bridge.modules` (modules that use it will still work, but should be rewritten to use `bridge.moduleClasses` or `-[bridge moduleForName/Class:` instead. The rules are now as follows: * Any module that overrides `init` or `setBridge:` will be initialized on the main thread when the bridge is created * Any module that implements `constantsToExport:` will be initialized later when the config is exported (the module itself will be initialized on a background queue, but `constantsToExport:` will still be called on the main thread. * All other modules will be initialized lazily when a method is first called on them. These rules may seem slightly arcane, but they have the advantage of not violating any assumptions that may have been made by existing code - any module written under the original assumption that it would be initialized synchronously on the main thread when the bridge is created should still function exactly the same, but modules that avoid overriding `init` or `setBridge:` will now be loaded lazily. I've rewritten most of the standard modules to take advantage of this new lazy loading, with the following results: Out of the 65 modules included in UIExplorer: * 16 are initialized on the main thread when the bridge is created * A further 8 are initialized when the config is exported to JS * The remaining 41 will be initialized lazily on-demand Reviewed By: jspahrsummers Differential Revision: D2677695 fb-gh-sync-id: 507ae7e9fd6b563e89292c7371767c978e928f33
2015-11-25 11:09:00 +00:00
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleRemoteNotificationRegistrationError:)
name:kRemoteNotificationRegistrationFailed
object:nil];
Refactored module access to allow for lazy loading Summary: public The `bridge.modules` dictionary provides access to all native modules, but this API requires that every module is initialized in advance so that any module can be accessed. This diff introduces a better API that will allow modules to be initialized lazily as they are needed, and deprecates `bridge.modules` (modules that use it will still work, but should be rewritten to use `bridge.moduleClasses` or `-[bridge moduleForName/Class:` instead. The rules are now as follows: * Any module that overrides `init` or `setBridge:` will be initialized on the main thread when the bridge is created * Any module that implements `constantsToExport:` will be initialized later when the config is exported (the module itself will be initialized on a background queue, but `constantsToExport:` will still be called on the main thread. * All other modules will be initialized lazily when a method is first called on them. These rules may seem slightly arcane, but they have the advantage of not violating any assumptions that may have been made by existing code - any module written under the original assumption that it would be initialized synchronously on the main thread when the bridge is created should still function exactly the same, but modules that avoid overriding `init` or `setBridge:` will now be loaded lazily. I've rewritten most of the standard modules to take advantage of this new lazy loading, with the following results: Out of the 65 modules included in UIExplorer: * 16 are initialized on the main thread when the bridge is created * A further 8 are initialized when the config is exported to JS * The remaining 41 will be initialized lazily on-demand Reviewed By: jspahrsummers Differential Revision: D2677695 fb-gh-sync-id: 507ae7e9fd6b563e89292c7371767c978e928f33
2015-11-25 11:09:00 +00:00
}
- (void)stopObserving
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (NSArray<NSString *> *)supportedEvents
{
return @[@"localNotificationReceived",
@"remoteNotificationReceived",
@"remoteNotificationsRegistered",
@"remoteNotificationRegistrationError"];
}
+ (void)didRegisterUserNotificationSettings:(__unused UIUserNotificationSettings *)notificationSettings
{
if ([UIApplication instancesRespondToSelector:@selector(registerForRemoteNotifications)]) {
[RCTSharedApplication() registerForRemoteNotifications];
[[NSNotificationCenter defaultCenter] postNotificationName:kRegisterUserNotificationSettings
object:self
userInfo:@{@"notificationSettings": notificationSettings}];
2015-03-25 00:37:03 +00:00
}
}
+ (void)didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
NSMutableString *hexString = [NSMutableString string];
NSUInteger deviceTokenLength = deviceToken.length;
const unsigned char *bytes = 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)didReceiveRemoteNotification:(NSDictionary *)notification
{
NSDictionary *userInfo = @{@"notification": notification};
[[NSNotificationCenter defaultCenter] postNotificationName:RCTRemoteNotificationReceived
object:self
userInfo:userInfo];
}
+ (void)didReceiveRemoteNotification:(NSDictionary *)notification
fetchCompletionHandler:(RCTRemoteNotificationCallback)completionHandler
{
NSDictionary *userInfo = @{@"notification": notification, @"completionHandler": completionHandler};
[[NSNotificationCenter defaultCenter] postNotificationName:RCTRemoteNotificationReceived
object:self
userInfo:userInfo];
}
+ (void)didReceiveLocalNotification:(UILocalNotification *)notification
{
[[NSNotificationCenter defaultCenter] postNotificationName:kLocalNotificationReceived
object:self
userInfo:RCTFormatLocalNotification(notification)];
}
- (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];
}
- (void)handleRegisterUserNotificationSettings:(NSNotification *)notification
{
if (_requestPermissionsResolveBlock == nil) {
return;
}
UIUserNotificationSettings *notificationSettings = notification.userInfo[@"notificationSettings"];
NSDictionary *notificationTypes = @{
@"alert": @((notificationSettings.types & UIUserNotificationTypeAlert) > 0),
@"sound": @((notificationSettings.types & UIUserNotificationTypeSound) > 0),
@"badge": @((notificationSettings.types & UIUserNotificationTypeBadge) > 0),
};
_requestPermissionsResolveBlock(notificationTypes);
// Clean up listener added in requestPermissions
[self removeListeners:1];
_requestPermissionsResolveBlock = nil;
}
RCT_EXPORT_METHOD(onFinishRemoteNotification:(NSString *)notificationId fetchResult:(UIBackgroundFetchResult)result) {
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
*/
2015-04-08 15:52:48 +00:00
RCT_EXPORT_METHOD(setApplicationIconBadgeNumber:(NSInteger)number)
{
RCTSharedApplication().applicationIconBadgeNumber = number;
}
/**
* Get the current application icon badge number on the home screen
*/
2015-04-08 15:52:48 +00:00
RCT_EXPORT_METHOD(getApplicationIconBadgeNumber:(RCTResponseSenderBlock)callback)
{
callback(@[@(RCTSharedApplication().applicationIconBadgeNumber)]);
}
RCT_EXPORT_METHOD(requestPermissions:(NSDictionary *)permissions
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
if (RCTRunningInAppExtension()) {
reject(kErrorUnableToRequestPermissions, nil, RCTErrorWithMessage(@"Requesting push notifications is currently unavailable in an app extension"));
return;
}
if (_requestPermissionsResolveBlock != nil) {
RCTLogError(@"Cannot call requestPermissions twice before the first has returned.");
return;
}
// Add a listener to make sure that startObserving has been called
[self addListener:@"remoteNotificationsRegistered"];
_requestPermissionsResolveBlock = resolve;
UIUserNotificationType types = UIUserNotificationTypeNone;
if (permissions) {
if ([RCTConvert BOOL:permissions[@"alert"]]) {
types |= UIUserNotificationTypeAlert;
}
if ([RCTConvert BOOL:permissions[@"badge"]]) {
types |= UIUserNotificationTypeBadge;
}
if ([RCTConvert BOOL:permissions[@"sound"]]) {
types |= UIUserNotificationTypeSound;
}
} else {
types = UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound;
}
UIUserNotificationSettings *notificationSettings =
[UIUserNotificationSettings settingsForTypes:types categories:nil];
[RCTSharedApplication() registerUserNotificationSettings:notificationSettings];
}
RCT_EXPORT_METHOD(abandonPermissions)
{
[RCTSharedApplication() unregisterForRemoteNotifications];
}
2015-04-08 15:52:48 +00:00
RCT_EXPORT_METHOD(checkPermissions:(RCTResponseSenderBlock)callback)
{
if (RCTRunningInAppExtension()) {
callback(@[@{@"alert": @NO, @"badge": @NO, @"sound": @NO}]);
return;
}
NSUInteger types = [RCTSharedApplication() currentUserNotificationSettings].types;
callback(@[@{
@"alert": @((types & UIUserNotificationTypeAlert) > 0),
@"badge": @((types & UIUserNotificationTypeBadge) > 0),
@"sound": @((types & UIUserNotificationTypeSound) > 0),
}]);
}
RCT_EXPORT_METHOD(presentLocalNotification:(UILocalNotification *)notification)
{
[RCTSharedApplication() presentLocalNotificationNow:notification];
}
RCT_EXPORT_METHOD(scheduleLocalNotification:(UILocalNotification *)notification)
{
[RCTSharedApplication() scheduleLocalNotification:notification];
}
RCT_EXPORT_METHOD(cancelAllLocalNotifications)
{
[RCTSharedApplication() cancelAllLocalNotifications];
}
RCT_EXPORT_METHOD(cancelLocalNotifications:(NSDictionary<NSString *, id> *)userInfo)
{
for (UILocalNotification *notification in RCTSharedApplication().scheduledLocalNotifications) {
__block BOOL matchesAll = YES;
NSDictionary<NSString *, id> *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;
}
}];
if (matchesAll) {
[RCTSharedApplication() cancelLocalNotification:notification];
}
}
}
RCT_EXPORT_METHOD(getInitialNotification:(RCTPromiseResolveBlock)resolve
reject:(__unused RCTPromiseRejectBlock)reject)
{
NSMutableDictionary<NSString *, id> *initialNotification =
[self.bridge.launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey] mutableCopy];
UILocalNotification *initialLocalNotification =
self.bridge.launchOptions[UIApplicationLaunchOptionsLocalNotificationKey];
if (initialNotification) {
initialNotification[@"remote"] = @YES;
resolve(initialNotification);
} else if (initialLocalNotification) {
resolve(RCTFormatLocalNotification(initialLocalNotification));
} else {
resolve((id)kCFNull);
}
}
RCT_EXPORT_METHOD(getScheduledLocalNotifications:(RCTResponseSenderBlock)callback)
{
NSArray<UILocalNotification *> *scheduledLocalNotifications = RCTSharedApplication().scheduledLocalNotifications;
NSMutableArray<NSDictionary *> *formattedScheduledLocalNotifications = [NSMutableArray new];
for (UILocalNotification *notification in scheduledLocalNotifications) {
[formattedScheduledLocalNotifications addObject:RCTFormatLocalNotification(notification)];
}
callback(@[formattedScheduledLocalNotifications]);
}
RCT_EXPORT_METHOD(removeAllDeliveredNotifications)
{
if ([UNUserNotificationCenter class]) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center removeAllDeliveredNotifications];
}
}
RCT_EXPORT_METHOD(removeDeliveredNotifications:(NSArray<NSString *> *)identifiers)
{
if ([UNUserNotificationCenter class]) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center removeDeliveredNotificationsWithIdentifiers:identifiers];
}
}
RCT_EXPORT_METHOD(getDeliveredNotifications:(RCTResponseSenderBlock)callback)
{
if ([UNUserNotificationCenter class]) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center getDeliveredNotificationsWithCompletionHandler:^(NSArray<UNNotification *> *_Nonnull notifications) {
NSMutableArray<NSDictionary *> *formattedNotifications = [NSMutableArray new];
for (UNNotification *notification in notifications) {
[formattedNotifications addObject:RCTFormatUNNotification(notification)];
}
callback(@[formattedNotifications]);
}];
}
}
#else //TARGET_OS_TV
- (NSArray<NSString *> *)supportedEvents
{
return @[];
}
#endif //TARGET_OS_TV
@end