diff --git a/README.md b/README.md index d85e05681883c4b34fc34641ee0ddf169767b1ec..641a32944696cbdd980648fa823c8c3069e1a223 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,6 @@ Example: let localNotification = NotificationsIOS.localNotification({ alertBody: "Local notificiation!", alertTitle: "Local Notification Title", - alertAction: "Click here to open", soundName: "chime.aiff", category: "SOME_CATEGORY", userInfo: { } @@ -338,7 +337,7 @@ Notification object contains: - **`fireDate`**- The date and time when the system should deliver the notification (optinal - default is immidiate dispatch). - `alertBody`- The message displayed in the notification alert. - `alertTitle`- The title of the notification, displayed in the notifications center. -- `alertAction`- The "action" displayed beneath an actionable notification. +- `alertAction`- The "action" displayed beneath an actionable notification on the lockscreen (e.g. "Slide to **open**"). Note that Apple no longer shows this in iOS 10. - `soundName`- The sound played when the notification is fired (optional). - `category`- The category of this notification, required for [interactive notifications](#interactive--actionable-notifications-ios-only) (optional). - `userInfo`- An optional object containing additional notification data. @@ -357,8 +356,9 @@ NotificationsAndroid.localNotification({ Upon notification opening (tapping by the device user), all data fields will be delivered as-is). -### Cancel Local Notification -The `NotificationsIOS.localNotification()` and `NotificationsAndroid.localNotification()` methods return unique `notificationId` values, which can be used in order to cancel specific local notifications. You can cancel local notification by calling `NotificationsIOS.cancelLocalNotification(notificationId)` or `NotificationsAndroid.cancelLocalNotification(notificationId)`. +### Cancel Scheduled Local Notifications + +The `NotificationsIOS.localNotification()` and `NotificationsAndroid.localNotification()` methods return unique `notificationId` values, which can be used in order to cancel specific local notifications that were scheduled for delivery on `fireDate` and have not yet been delivered. You can cancel local notification by calling `NotificationsIOS.cancelLocalNotification(notificationId)` or `NotificationsAndroid.cancelLocalNotification(notificationId)`. Example (iOS): @@ -366,7 +366,6 @@ Example (iOS): let someLocalNotification = NotificationsIOS.localNotification({ alertBody: "Local notificiation!", alertTitle: "Local Notification Title", - alertAction: "Click here to open", soundName: "chime.aiff", category: "SOME_CATEGORY", userInfo: { } @@ -375,12 +374,26 @@ let someLocalNotification = NotificationsIOS.localNotification({ NotificationsIOS.cancelLocalNotification(someLocalNotification); ``` -### Cancel All Local Notifications (iOS-only!) +To cancel all local notifications (**iOS only!**), use `cancelAllLocalNotifications()`: ```javascript NotificationsIOS.cancelAllLocalNotifications(); ``` +### Cancel Delivered Local Notifications (iOS 10+ only) + +To dismiss notifications from the notification center that have already been shown to the user, call `NotificationsIOS.removeDeliveredNotifications([notificationId])`: + +```javascript +let someLocalNotification = NotificationsIOS.localNotification({...}); + +NotificationsIOS.removeDeliveredNotifications([someLocalNotification]); +``` + +Call `removeAllDeliveredNotifications()` to dismiss all delivered notifications +(note that this will dismiss push notifications in addition to local +notifications). + --- ## Managed Notifications (iOS only) @@ -440,6 +453,28 @@ Now the server should push the notification a bit differently- background instea --- +## Remove notifications (iOS only) + +### getDeliveredNotifications + +`PushNotification.getDeliveredNotifications(callback: (notifications: Array) => void)` + +Provides you with a list of the app’s notifications that are still displayed in Notification Center. + +### removeDeliveredNotifications + +`PushNotification.removeDeliveredNotifications(identifiers: Array)` + +Removes the specified notifications from Notification Center. + +### removeAllDeliveredNotifications + +`PushNotification.removeAllDeliveredNotifications()` + +Removes all delivered notifications from Notification Center. + +--- + ## PushKit API (iOS only) The PushKit framework provides the classes for your iOS apps to receive background pushes from remote servers. it has better support for background notifications compared to regular push notifications with `content-available: 1`. More info in [iOS PushKit documentation](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Reference/PushKit_Framework/). diff --git a/RNNotifications/RNNotifications.m b/RNNotifications/RNNotifications.m index 0ce06e32c3671a7536f58ae3ea2ee35081a248bc..3c12a9dbcb5db7580cf4cc4cd81abecaf8dda8c5 100644 --- a/RNNotifications/RNNotifications.m +++ b/RNNotifications/RNNotifications.m @@ -15,6 +15,9 @@ #import "RCTUtils.h" #endif #import "RNNotificationsBridgeQueue.h" +#import + +#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending) NSString* const RNNotificationCreateAction = @"CREATE"; NSString* const RNNotificationClearAction = @"CLEAR"; @@ -107,6 +110,61 @@ RCT_ENUM_CONVERTER(UIUserNotificationActionBehavior, (@{ } @end +@implementation RCTConvert (UNNotificationRequest) ++ (UNNotificationRequest *)UNNotificationRequest:(id)json withId:(NSString*)notificationId +{ + NSDictionary *details = [self NSDictionary:json]; + + UNMutableNotificationContent *content = [UNMutableNotificationContent new]; + content.body = [RCTConvert NSString:details[@"alertBody"]]; + content.title = [RCTConvert NSString:details[@"alertTitle"]]; + content.sound = [RCTConvert NSString:details[@"soundName"]] + ? [UNNotificationSound soundNamed:[RCTConvert NSString:details[@"soundName"]]] + : [UNNotificationSound defaultSound]; + content.userInfo = [RCTConvert NSDictionary:details[@"userInfo"]] ?: @{}; + content.categoryIdentifier = [RCTConvert NSString:details[@"category"]]; + + NSDate *triggerDate = [RCTConvert NSDate:details[@"fireDate"]]; + UNCalendarNotificationTrigger *trigger = nil; + if (triggerDate != nil) { + NSDateComponents *triggerDateComponents = [[NSCalendar currentCalendar] + components:NSCalendarUnitYear + + NSCalendarUnitMonth + NSCalendarUnitDay + + NSCalendarUnitHour + NSCalendarUnitMinute + + NSCalendarUnitSecond + NSCalendarUnitTimeZone + fromDate:triggerDate]; + trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:triggerDateComponents + repeats:NO]; + } + + return [UNNotificationRequest requestWithIdentifier:notificationId + content:content trigger:trigger]; +} +@end + +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[@"fireDate"] = dateString; + } + + formattedNotification[@"alertTitle"] = RCTNullIfNil(content.title); + formattedNotification[@"alertBody"] = RCTNullIfNil(content.body); + formattedNotification[@"category"] = RCTNullIfNil(content.categoryIdentifier); + formattedNotification[@"thread-id"] = RCTNullIfNil(content.threadIdentifier); + formattedNotification[@"userInfo"] = RCTNullIfNil(RCTJSONClean(content.userInfo)); + + return formattedNotification; +} + @implementation RNNotifications RCT_EXPORT_MODULE() @@ -521,25 +579,35 @@ RCT_EXPORT_METHOD(consumeBackgroundQueue) RCT_EXPORT_METHOD(localNotification:(NSDictionary *)notification withId:(NSString *)notificationId) { - UILocalNotification* localNotification = [RCTConvert UILocalNotification:notification]; - NSMutableArray* userInfo = localNotification.userInfo.mutableCopy; - [userInfo setValue:notificationId forKey:@"__id"]; - localNotification.userInfo = userInfo; - - if ([notification objectForKey:@"fireDate"] != nil) { - [[UIApplication sharedApplication] scheduleLocalNotification:localNotification]; + if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"10")) { + UNNotificationRequest* localNotification = [RCTConvert UNNotificationRequest:notification withId:notificationId]; + [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:localNotification withCompletionHandler:nil]; } else { - [[UIApplication sharedApplication] presentLocalNotificationNow:localNotification]; + UILocalNotification* localNotification = [RCTConvert UILocalNotification:notification]; + NSMutableArray* userInfo = localNotification.userInfo.mutableCopy; + [userInfo setValue:notificationId forKey:@"__id"]; + localNotification.userInfo = userInfo; + + if ([notification objectForKey:@"fireDate"] != nil) { + [[UIApplication sharedApplication] scheduleLocalNotification:localNotification]; + } else { + [[UIApplication sharedApplication] presentLocalNotificationNow:localNotification]; + } } } RCT_EXPORT_METHOD(cancelLocalNotification:(NSString *)notificationId) { - for (UILocalNotification* notification in [UIApplication sharedApplication].scheduledLocalNotifications) { - NSDictionary* notificationInfo = notification.userInfo; + if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"10")) { + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center removePendingNotificationRequestsWithIdentifiers:@[notificationId]]; + } else { + for (UILocalNotification* notification in [UIApplication sharedApplication].scheduledLocalNotifications) { + NSDictionary* notificationInfo = notification.userInfo; - if ([[notificationInfo objectForKey:@"__id"] isEqualToString:notificationId]) { - [[UIApplication sharedApplication] cancelLocalNotification:notification]; + if ([[notificationInfo objectForKey:@"__id"] isEqualToString:notificationId]) { + [[UIApplication sharedApplication] cancelLocalNotification:notification]; + } } } } @@ -552,7 +620,7 @@ RCT_EXPORT_METHOD(cancelAllLocalNotifications) RCT_EXPORT_METHOD(isRegisteredForRemoteNotifications:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { BOOL ans; - + if (TARGET_IPHONE_SIMULATOR) { ans = [[[UIApplication sharedApplication] currentUserNotificationSettings] types] != 0; } @@ -572,4 +640,39 @@ RCT_EXPORT_METHOD(checkPermissions:(RCTPromiseResolveBlock) resolve }); } +#if !TARGET_OS_TV + +RCT_EXPORT_METHOD(removeAllDeliveredNotifications) +{ + if ([UNUserNotificationCenter class]) { + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center removeAllDeliveredNotifications]; + } +} + +RCT_EXPORT_METHOD(removeDeliveredNotifications:(NSArray *)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 * _Nonnull notifications) { + NSMutableArray *formattedNotifications = [NSMutableArray new]; + + for (UNNotification *notification in notifications) { + [formattedNotifications addObject:RCTFormatUNNotification(notification)]; + } + callback(@[formattedNotifications]); + }]; + } +} + +#endif !TARGET_OS_TV + @end diff --git a/index.ios.js b/index.ios.js index f189676483d8a09477a88b5afaebb30201b77231..d5ac85686d636caa6e814b8d0b4716d0eb3b19dc 100644 --- a/index.ios.js +++ b/index.ios.js @@ -216,4 +216,39 @@ export default class NotificationsIOS { static checkPermissions() { return NativeRNNotifications.checkPermissions(); } + + /** + * Remove all delivered notifications from Notification Center + */ + static removeAllDeliveredNotifications() { + return NativeRNNotifications.removeAllDeliveredNotifications(); + } + + /** + * Removes the specified notifications from Notification Center + * + * @param identifiers Array of notification identifiers + */ + static removeDeliveredNotifications(identifiers: Array) { + return NativeRNNotifications.removeDeliveredNotifications(identifiers); + } + + /** + * Provides you with a list of the app’s notifications that are still displayed in Notification Center + * + * @param callback Function which receive an array of delivered notifications + * + * A delivered notification is an object containing: + * + * - `identifier` : The identifier of this notification. + * - `alertBody` : The message displayed in the notification alert. + * - `alertTitle` : The message title displayed in the notification. + * - `category` : The category of this notification, if has one. + * - `userInfo` : An optional object containing additional notification data. + * - `thread-id` : The thread identifier of this notification, if has one. + * - `fireDate` : The date and time when the system should deliver the notification. if not specified, the notification will be dispatched immediately. + */ + static getDeliveredNotifications(callback: (notifications: Array) => void) { + return NativeRNNotifications.getDeliveredNotifications(callback); + } } diff --git a/notification.ios.js b/notification.ios.js index 016d4e2971ac4bf09889897d4fee4011d64d05db..4042a964eae2735ae6c294b0146af019fc5fcd61 100644 --- a/notification.ios.js +++ b/notification.ios.js @@ -5,6 +5,7 @@ export default class IOSNotification { _badge: number; _category: string; _type: string; // regular / managed + _thread: string; constructor(notification: Object) { this._data = {}; @@ -21,6 +22,7 @@ export default class IOSNotification { this._badge = notification.aps.badge; this._category = notification.managedAps.category; this._type = "managed"; + this._thread = notification.aps["thread-id"]; } else if ( notification.aps && notification.aps.alert) { @@ -30,6 +32,7 @@ export default class IOSNotification { this._badge = notification.aps.badge; this._category = notification.aps.category; this._type = "regular"; + this._thread = notification.aps["thread-id"]; } Object.keys(notification).filter(key => key !== "aps").forEach(key => { @@ -60,4 +63,8 @@ export default class IOSNotification { getType(): ?string { return this._type; } + + getThread(): ?string { + return this._thread; + } } diff --git a/test/index.ios.spec.js b/test/index.ios.spec.js index aa1f72009595ca49494f44990d4b36b1854f33a0..c7ccc17b915764d2957bdc4858a53053f9d87f07 100644 --- a/test/index.ios.spec.js +++ b/test/index.ios.spec.js @@ -30,11 +30,15 @@ describe("NotificationsIOS", () => { nativeCancelAllLocalNotifications, nativeSetBadgesCount, nativeIsRegisteredForRemoteNotifications, - nativeCheckPermissions; + nativeCheckPermissions, + nativeRemoveAllDeliveredNotifications, + nativeRemoveDeliveredNotifications, + nativeGetDeliveredNotifications; let NotificationsIOS, NotificationAction, NotificationCategory; let someHandler = () => {}; let constantGuid = "some-random-uuid"; + let identifiers = ["some-random-uuid", "other-random-uuid"]; /*eslint-enable indent*/ before(() => { @@ -53,6 +57,9 @@ describe("NotificationsIOS", () => { nativeSetBadgesCount = sinon.spy(); nativeIsRegisteredForRemoteNotifications = sinon.spy(); nativeCheckPermissions = sinon.spy(); + nativeRemoveAllDeliveredNotifications = sinon.spy(); + nativeRemoveDeliveredNotifications = sinon.spy(); + nativeGetDeliveredNotifications = sinon.spy(); let libUnderTest = proxyquire("../index.ios", { "uuid": { @@ -71,7 +78,10 @@ describe("NotificationsIOS", () => { cancelAllLocalNotifications: nativeCancelAllLocalNotifications, setBadgesCount: nativeSetBadgesCount, isRegisteredForRemoteNotifications: nativeIsRegisteredForRemoteNotifications, - checkPermissions: nativeCheckPermissions + checkPermissions: nativeCheckPermissions, + removeAllDeliveredNotifications: nativeRemoveAllDeliveredNotifications, + removeDeliveredNotifications: nativeRemoveDeliveredNotifications, + getDeliveredNotifications: nativeGetDeliveredNotifications } }, NativeAppEventEmitter: { @@ -112,6 +122,9 @@ describe("NotificationsIOS", () => { nativeCancelAllLocalNotifications.reset(); nativeIsRegisteredForRemoteNotifications.reset(); nativeCheckPermissions.reset(); + nativeRemoveAllDeliveredNotifications.reset(); + nativeRemoveDeliveredNotifications.reset(); + nativeGetDeliveredNotifications.reset(); }); after(() => { @@ -129,6 +142,9 @@ describe("NotificationsIOS", () => { nativeCancelAllLocalNotifications = null; nativeIsRegisteredForRemoteNotifications = null; nativeCheckPermissions = null; + nativeRemoveAllDeliveredNotifications = null; + nativeRemoveDeliveredNotifications = null; + nativeGetDeliveredNotifications = null; NotificationsIOS = null; NotificationAction = null; @@ -306,7 +322,6 @@ describe("NotificationsIOS", () => { }); }); - describe("Is registered for remote notifications ", () => { it("should call native is registered for remote notifications", () => { NotificationsIOS.isRegisteredForRemoteNotifications(); @@ -322,4 +337,29 @@ describe("NotificationsIOS", () => { }); }); + + describe("Remove all delivered notifications", () => { + it("should call native remove all delivered notifications method", () => { + NotificationsIOS.removeAllDeliveredNotifications(); + + expect(nativeRemoveAllDeliveredNotifications).to.have.been.calledWith(); + }); + }); + + describe("Remove delivered notifications", () => { + it("should call native remove delivered notifications method", () => { + NotificationsIOS.removeDeliveredNotifications(identifiers); + + expect(nativeRemoveDeliveredNotifications).to.have.been.calledWith(identifiers); + }); + }); + + describe("Get delivered notifications", () => { + it("should call native get delivered notifications method", () => { + const callback = (notifications) => console.log(notifications); + NotificationsIOS.getDeliveredNotifications(callback); + + expect(nativeGetDeliveredNotifications).to.have.been.calledWith(callback); + }); + }); }); diff --git a/test/notification.ios.spec.js b/test/notification.ios.spec.js index 0620108aac126f52c057af4d75d65a7bf7673cac..b19a901f9093eef5024914e83310eb551f1c1818 100644 --- a/test/notification.ios.spec.js +++ b/test/notification.ios.spec.js @@ -4,7 +4,7 @@ import IOSNotification from "../notification.ios"; describe("iOS Notification Object", () => { let notification; - let someBadgeCount = 123, someSound = "someSound", someCategory = "some_notification_category"; + let someBadgeCount = 123, someSound = "someSound", someCategory = "some_notification_category", someThread = "thread-1"; describe("for a regular iOS push notification", () => { let regularNativeNotifications = [ @@ -17,7 +17,8 @@ describe("iOS Notification Object", () => { }, badge: someBadgeCount, sound: someSound, - category: someCategory + category: someCategory, + "thread-id": someThread }, key1: "value1", key2: "value2" @@ -33,7 +34,8 @@ describe("iOS Notification Object", () => { }, badge: someBadgeCount, sound: someSound, - category: someCategory + category: someCategory, + "thread-id": someThread }, key1: "value1", key2: "value2" @@ -65,6 +67,10 @@ describe("iOS Notification Object", () => { expect(notification.getCategory()).to.equal(someCategory); }); + it("should return the thread", () => { + expect(notification.getThread()).to.equal("thread-1"); + }); + it("should return the custom data", () => { expect(notification.getData()).to.deep.equal({ key1: "value1", key2: "value2" }); });