diff --git a/RNNotifications/RNNotifications.m b/RNNotifications/RNNotifications.m index cdbffd19f502bce4ab4d7ddcb8b1fd82fe6107c6..67d24b599499eb0ae735a076bc0564ffc982d11f 100644 --- a/RNNotifications/RNNotifications.m +++ b/RNNotifications/RNNotifications.m @@ -3,7 +3,7 @@ #import "RCTBridge.h" #import "RCTEventDispatcher.h" #import "RNNotifications.h" - +#import "RCTConvert.h" #import "RCTUtils.h" NSString *const RNNotificationCreateAction = @"CREATE"; @@ -13,13 +13,48 @@ NSString *const RNNotificationReceivedForeground = @"RNNotificationReceivedForeg NSString *const RNNotificationReceivedBackground = @"RNNotificationReceivedBackground"; NSString *const RNNotificationOpened = @"RNNotificationOpened"; +/* + * Enum Converters for Interactive Notifications + */ +@implementation RCTConvert (UIUserNotificationActivationMode) +RCT_ENUM_CONVERTER(UIUserNotificationActivationMode, (@{ + @"foreground": @(UIUserNotificationActivationModeForeground), + @"background": @(UIUserNotificationActivationModeBackground) + }), UIUserNotificationActivationModeForeground, integerValue) +@end + +@implementation RCTConvert (UIUserNotificationActionContext) +RCT_ENUM_CONVERTER(UIUserNotificationActionContext, (@{ + @"default": @(UIUserNotificationActionContextDefault), + @"minimal": @(UIUserNotificationActionContextMinimal) + }), UIUserNotificationActionContextDefault, integerValue) +@end + +@implementation RCTConvert (UIUserNotificationActionBehavior) +/* iOS 9 only */ +RCT_ENUM_CONVERTER(UIUserNotificationActionBehavior, (@{ + @"default": @(UIUserNotificationActionBehaviorDefault), + @"textInput": @(UIUserNotificationActionBehaviorTextInput) + }), UIUserNotificationActionBehaviorDefault, integerValue) +@end + @implementation RNNotifications RCT_EXPORT_MODULE() @synthesize bridge = _bridge; -static NSString* username; +NSMutableDictionary *actionCallbacks; + +- (id)init +{ + if (self = [super init]) { + actionCallbacks = [[NSMutableDictionary alloc] init]; + return self; + } else { + return nil; + } +} - (void)dealloc { @@ -47,7 +82,7 @@ static NSString* username; } /* - * API Methods + * Public Methods */ + (void)didReceiveRemoteNotification:(NSDictionary *)notification { @@ -176,6 +211,48 @@ static NSString* username; return [NSString stringWithFormat:@"%@.%@", [[NSBundle mainBundle] bundleIdentifier], notificationId]; } ++ (UIMutableUserNotificationAction *)parseAction:(NSDictionary *)json +{ + UIMutableUserNotificationAction* action =[[UIMutableUserNotificationAction alloc] init]; + action.activationMode = [RCTConvert UIUserNotificationActivationMode:json[@"activationMode"]]; + action.behavior = [RCTConvert UIUserNotificationActionBehavior:json[@"behavior"]]; + action.authenticationRequired = [RCTConvert BOOL:json[@"authenticationRequired"]]; + action.destructive = [RCTConvert BOOL:json[@"destructive"]]; + action.title = json[@"title"]; + action.identifier = json[@"identifier"]; + + return action; +} + ++ (UIMutableUserNotificationCategory *)parseCategory:(NSDictionary *)json +{ + UIMutableUserNotificationCategory* category = [[UIMutableUserNotificationCategory alloc] init]; + category.identifier = json[@"identifier"]; + + // category actions + NSMutableArray* actions = [[NSMutableArray alloc] init]; + for (NSDictionary* actionJson in [RCTConvert NSArray:json[@"actions"]]) { + [actions addObject:[self parseAction:actionJson]]; + } + + [category setActions:actions forContext:[RCTConvert UIUserNotificationActionContext:json[@"context"]]]; + + return category; +} + ++ (void)updateNotificationCategories:(NSArray *)json +{ + NSMutableSet* categories = [[NSMutableSet alloc] init]; + for (NSDictionary* categoryJson in json) { + [categories addObject:[self parseCategory:categoryJson]]; + } + + UIUserNotificationType types = (UIUserNotificationType) (UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert); + UIUserNotificationSettings* settings = [UIUserNotificationSettings settingsForTypes:types categories:categories]; + + [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; +} + - (void)handleNotificationReceivedForeground:(NSNotification *)notification { [_bridge.eventDispatcher sendDeviceEventWithName:@"notificationReceivedForeground" body:notification.userInfo]; @@ -194,15 +271,9 @@ static NSString* username; /* * React Native exported methods */ - -RCT_EXPORT_METHOD(dispatchLocalNotificationFromNotification:(NSDictionary *)notification) -{ - [RNNotifications dispatchLocalNotificationFromNotification:notification]; -} - -RCT_EXPORT_METHOD(clearNotificationFromNotificationsCenter:(NSString *)notificationId) +RCT_EXPORT_METHOD(updateNotificationCategories:(NSArray *)json) { - [RNNotifications clearNotificationFromNotificationsCenter:notificationId]; + [RNNotifications updateNotificationCategories:json]; } @end diff --git a/example/index.ios.js b/example/index.ios.js index 38afc940a07021009e03850171bea8f0e6b42cc1..ffc76cd5a750c7c93365fe8d19eb48648a7348f4 100644 --- a/example/index.ios.js +++ b/example/index.ios.js @@ -12,7 +12,7 @@ import React, { PushNotificationIOS } from 'react-native'; -import NotificationsIOS from 'react-native-notifications'; +import NotificationsIOS, { NotificationAction, NotificationCategory } from 'react-native-notifications'; class NotificationsExampleApp extends Component { @@ -25,6 +25,38 @@ class NotificationsExampleApp extends Component { NotificationsIOS.addEventListener('notificationOpened', this.onNotificationOpened.bind(this)); } + componentDidMount() { + PushNotificationIOS.requestPermissions(); + + let upvoteAction = new NotificationAction({ + activationMode: "background", + title: String.fromCodePoint(0x1F44D), + identifier: "UPVOTE_ACTION" + }, (action) => { + console.log("ACTION RECEIVED"); + console.log(action); + }); + + let replyAction = new NotificationAction({ + activationMode: "background", + title: "Reply", + behavior: "textInput", + authenticationRequired: true, + identifier: "REPLY_ACTION" + }, (action) => { + console.log("ACTION RECEIVED"); + console.log(action); + }); + + let cat = new NotificationCategory({ + identifier: "SOME_CATEGORY", + actions: [upvoteAction, replyAction], + context: "default" + }); + + NotificationsIOS.setCategories([cat]); + } + onPushRegistered(deviceToken) { console.log("Device Token Received: " + deviceToken); } @@ -42,8 +74,6 @@ class NotificationsExampleApp extends Component { } render() { - PushNotificationIOS.requestPermissions(); - return ( diff --git a/example/ios/NotificationsExampleApp/AppDelegate.m b/example/ios/NotificationsExampleApp/AppDelegate.m index 379709d4b9291f4a4caeb2c6aeb1cc940688c6b6..7f36e0c4fd203683c6486485216e53e42e9d4563 100644 --- a/example/ios/NotificationsExampleApp/AppDelegate.m +++ b/example/ios/NotificationsExampleApp/AppDelegate.m @@ -69,6 +69,8 @@ } + + // Required for the notification event. - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification { @@ -82,5 +84,17 @@ } +// Required for the notification actions. +- (void)application:(UIApplication *)application handleActionWithIdentifier:(NSString *)identifier forLocalNotification:(UILocalNotification *)notification withResponseInfo:(NSDictionary *)responseInfo completionHandler:(void (^)())completionHandler +{ + [RNNotifications application:application handleActionWithIdentifier:identifier forLocalNotification:notification withResponseInfo:responseInfo completionHandler:completionHandler]; +} + +- (void)application:(UIApplication *)application handleActionWithIdentifier:(NSString *)identifier forRemoteNotification:(NSDictionary *)userInfo withResponseInfo:(NSDictionary *)responseInfo completionHandler:(void (^)())completionHandler +{ + [RNNotifications application:application handleActionWithIdentifier:identifier forRemoteNotification:userInfo withResponseInfo:responseInfo completionHandler:completionHandler]; +} + + @end diff --git a/index.ios.js b/index.ios.js index f1968e8377d07885c02ff129b95bdf69a9a90c7f..768c52c7582899f86dc3da093a75ce1257491a34 100644 --- a/index.ios.js +++ b/index.ios.js @@ -5,15 +5,29 @@ "use strict"; import { NativeModules, DeviceEventEmitter } from "react-native"; import Map from "core-js/library/es6/map"; -let NativeRNNotifications = NativeModules.RNNotifications; // eslint-disable-line no-unused-vars +const NativeRNNotifications = NativeModules.RNNotifications; // eslint-disable-line no-unused-vars import IOSNotification from "./notification.ios"; export const DEVICE_NOTIFICATION_RECEIVED_FOREGROUND_EVENT = "notificationReceivedForeground"; export const DEVICE_NOTIFICATION_RECEIVED_BACKGROUND_EVENT = "notificationReceivedBackground"; export const DEVICE_NOTIFICATION_OPENED_EVENT = "notificationOpened"; +export const DEVICE_NOTIFICATION_ACTION_RECEIVED = "notificationActionReceived"; let _notificationHandlers = new Map(); +export class NotificationAction { + constructor(options: Object, handler: Function) { + this.options = options; + this.handler = handler; + } +} + +export class NotificationCategory { + constructor(options: Object) { + this.options = options; + } +} + export default class NotificationsIOS { /** * Attaches a listener to remote notification events while the app is running @@ -55,4 +69,35 @@ export default class NotificationsIOS { _notificationHandlers.delete(handler); } } + + static _actionHandlerGenerator(identifier: string, handler: Function) { + return (action) => { + if (action.identifier === identifier) { + handler(action); + } + }; + } + + /** + * Sets the notification categories + */ + /* eslint-disable no-unused-vars */ + static setCategories(categories: Array) { + let notificationCategories = []; + + if (categories) { + notificationCategories = categories.map(category => { + return Object.assign({}, category.options, { + actions: category.options.actions.map(action => { + // subscribe to action event + DeviceEventEmitter.addListener(DEVICE_NOTIFICATION_ACTION_RECEIVED, this._actionHandlerGenerator(action.options.identifier, action.handler)); + + return action.options; + }) + }); + }); + } + + NativeRNNotifications.updateNotificationCategories(notificationCategories); + } } diff --git a/test/index.ios.spec.js b/test/index.ios.spec.js index ec520c585a93da7fcb12b32912a247a536f162d8..e97e6a0e044c02d52377826fba7324aa94cc3d1e 100644 --- a/test/index.ios.spec.js +++ b/test/index.ios.spec.js @@ -3,6 +3,8 @@ let expect = require("chai").use(require("sinon-chai")).expect; import proxyquire from "proxyquire"; import sinon from "sinon"; +/* eslint-disable no-unused-vars */ + describe("NotificationsIOS", () => { let deviceEvents = [ "notificationReceivedForeground", @@ -10,75 +12,129 @@ describe("NotificationsIOS", () => { "notificationOpened" ]; - let addEventListenerSpy, removeEventListenerSpy; - let notificationIOS; + let nativeAddEventListener, nativeRemoveEventListener, nativeUpdateNotificationCategories; + let NotificationIOS, NotificationAction, NotificationCategory; let someHandler = () => {}; before(() => { - addEventListenerSpy = sinon.spy(); - removeEventListenerSpy = sinon.spy(); - notificationIOS = proxyquire("../index.ios", { + nativeAddEventListener = sinon.spy(); + nativeRemoveEventListener = sinon.spy(); + nativeUpdateNotificationCategories = sinon.spy(); + + let libUnderTest = proxyquire("../index.ios", { "react-native": { NativeModules: { - RNNotifications: { } + RNNotifications: { + updateNotificationCategories: nativeUpdateNotificationCategories + } }, DeviceEventEmitter: { addListener: (...args) => { - addEventListenerSpy(...args); + nativeAddEventListener(...args); - return { remove: removeEventListenerSpy }; + return { remove: nativeRemoveEventListener }; } }, "@noCallThru": true } - }).default; + }); + + NotificationIOS = libUnderTest.default; + NotificationAction = libUnderTest.NotificationAction; + NotificationCategory = libUnderTest.NotificationCategory; }); afterEach(() => { - addEventListenerSpy.reset(); - removeEventListenerSpy.reset(); + nativeAddEventListener.reset(); + nativeRemoveEventListener.reset(); + nativeUpdateNotificationCategories.reset(); }); after(() => { - addEventListenerSpy = null; - removeEventListenerSpy = null; - notificationIOS = null; + nativeAddEventListener = null; + nativeRemoveEventListener = null; + nativeUpdateNotificationCategories = null; + NotificationIOS = null; + NotificationAction = null; + NotificationCategory = null; }); describe("Add Event Listener", () => { deviceEvents.forEach(event => { it(`should subscribe the given handler to device event: ${event}`, () => { - notificationIOS.addEventListener(event, someHandler); + NotificationIOS.addEventListener(event, someHandler); - expect(addEventListenerSpy).to.have.been.calledWith(event, sinon.match.func); + expect(nativeAddEventListener).to.have.been.calledWith(event, sinon.match.func); }); }); it("should not subscribe to unknown device events", () => { - notificationIOS.addEventListener("someUnsupportedEvent", someHandler); + NotificationIOS.addEventListener("someUnsupportedEvent", someHandler); - expect(addEventListenerSpy).to.not.have.been.called; + expect(nativeAddEventListener).to.not.have.been.called; }); }); describe("Remove Event Listener", () => { deviceEvents.forEach(event => { it(`should unsubscribe the given handler from device event: ${event}`, () => { - notificationIOS.addEventListener(event, someHandler); - notificationIOS.removeEventListener(event, someHandler); + NotificationIOS.addEventListener(event, someHandler); + NotificationIOS.removeEventListener(event, someHandler); - expect(removeEventListenerSpy).to.have.been.calledOnce; + expect(nativeRemoveEventListener).to.have.been.calledOnce; }); }); it("should not unsubscribe to unknown device events", () => { let someUnsupportedEvent = "someUnsupportedEvent"; - notificationIOS.addEventListener(someUnsupportedEvent, someHandler); - notificationIOS.removeEventListener(someUnsupportedEvent, someHandler); + NotificationIOS.addEventListener(someUnsupportedEvent, someHandler); + NotificationIOS.removeEventListener(someUnsupportedEvent, someHandler); - expect(removeEventListenerSpy).to.not.have.been.called; + expect(nativeRemoveEventListener).to.not.have.been.called; }); }); - // TODO: Test handle notification with IOSNotification object + describe("Update notification categories", () => { + let someAction, someCategory; + + let actionOpts = { + activationMode: "foreground", + title: "someAction", + behavior: "default", + identifier: "SOME_ACTION" + }; + + beforeEach(() => { + someAction = new NotificationAction(actionOpts, () => {}); + + someCategory = new NotificationCategory({ + identifier: "SOME_CATEGORY", + actions: [someAction], + context: "default" + }); + }); + + it("should call native update categories with array of categories", () => { + NotificationIOS.setCategories([someCategory]); + + expect(nativeUpdateNotificationCategories).to.have.been.calledWith([{ + identifier: "SOME_CATEGORY", + actions: [actionOpts], + context: "default" + }]); + }); + + it("should call native update categories with empty array if no categories specified", () => { + NotificationIOS.setCategories(); + + expect(nativeUpdateNotificationCategories).to.have.been.calledWith([]); + }); + + it("should subscribe to 'notificationActionReceived' event for each action identifier", () => { + NotificationIOS.setCategories([someCategory]); + + expect(nativeAddEventListener).to.have.been.calledOnce; + expect(nativeAddEventListener).to.have.been.calledWith("notificationActionReceived", sinon.match.func); + }); + }); });