Commit 2ade57c9 authored by Lidan Hifi's avatar Lidan Hifi

added interactive notifications 💬

parent 37492496
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
#import "RCTBridge.h" #import "RCTBridge.h"
#import "RCTEventDispatcher.h" #import "RCTEventDispatcher.h"
#import "RNNotifications.h" #import "RNNotifications.h"
#import "RCTConvert.h"
#import "RCTUtils.h" #import "RCTUtils.h"
NSString *const RNNotificationCreateAction = @"CREATE"; NSString *const RNNotificationCreateAction = @"CREATE";
...@@ -13,13 +13,48 @@ NSString *const RNNotificationReceivedForeground = @"RNNotificationReceivedForeg ...@@ -13,13 +13,48 @@ NSString *const RNNotificationReceivedForeground = @"RNNotificationReceivedForeg
NSString *const RNNotificationReceivedBackground = @"RNNotificationReceivedBackground"; NSString *const RNNotificationReceivedBackground = @"RNNotificationReceivedBackground";
NSString *const RNNotificationOpened = @"RNNotificationOpened"; 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 @implementation RNNotifications
RCT_EXPORT_MODULE() RCT_EXPORT_MODULE()
@synthesize bridge = _bridge; @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 - (void)dealloc
{ {
...@@ -47,7 +82,7 @@ static NSString* username; ...@@ -47,7 +82,7 @@ static NSString* username;
} }
/* /*
* API Methods * Public Methods
*/ */
+ (void)didReceiveRemoteNotification:(NSDictionary *)notification + (void)didReceiveRemoteNotification:(NSDictionary *)notification
{ {
...@@ -176,6 +211,48 @@ static NSString* username; ...@@ -176,6 +211,48 @@ static NSString* username;
return [NSString stringWithFormat:@"%@.%@", [[NSBundle mainBundle] bundleIdentifier], notificationId]; 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 - (void)handleNotificationReceivedForeground:(NSNotification *)notification
{ {
[_bridge.eventDispatcher sendDeviceEventWithName:@"notificationReceivedForeground" body:notification.userInfo]; [_bridge.eventDispatcher sendDeviceEventWithName:@"notificationReceivedForeground" body:notification.userInfo];
...@@ -194,15 +271,9 @@ static NSString* username; ...@@ -194,15 +271,9 @@ static NSString* username;
/* /*
* React Native exported methods * React Native exported methods
*/ */
RCT_EXPORT_METHOD(updateNotificationCategories:(NSArray *)json)
RCT_EXPORT_METHOD(dispatchLocalNotificationFromNotification:(NSDictionary *)notification)
{
[RNNotifications dispatchLocalNotificationFromNotification:notification];
}
RCT_EXPORT_METHOD(clearNotificationFromNotificationsCenter:(NSString *)notificationId)
{ {
[RNNotifications clearNotificationFromNotificationsCenter:notificationId]; [RNNotifications updateNotificationCategories:json];
} }
@end @end
...@@ -12,7 +12,7 @@ import React, { ...@@ -12,7 +12,7 @@ import React, {
PushNotificationIOS PushNotificationIOS
} from 'react-native'; } from 'react-native';
import NotificationsIOS from 'react-native-notifications'; import NotificationsIOS, { NotificationAction, NotificationCategory } from 'react-native-notifications';
class NotificationsExampleApp extends Component { class NotificationsExampleApp extends Component {
...@@ -25,6 +25,38 @@ class NotificationsExampleApp extends Component { ...@@ -25,6 +25,38 @@ class NotificationsExampleApp extends Component {
NotificationsIOS.addEventListener('notificationOpened', this.onNotificationOpened.bind(this)); 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) { onPushRegistered(deviceToken) {
console.log("Device Token Received: " + deviceToken); console.log("Device Token Received: " + deviceToken);
} }
...@@ -42,8 +74,6 @@ class NotificationsExampleApp extends Component { ...@@ -42,8 +74,6 @@ class NotificationsExampleApp extends Component {
} }
render() { render() {
PushNotificationIOS.requestPermissions();
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.welcome}> <Text style={styles.welcome}>
......
...@@ -69,6 +69,8 @@ ...@@ -69,6 +69,8 @@
} }
// Required for the notification event. // Required for the notification event.
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification
{ {
...@@ -82,5 +84,17 @@ ...@@ -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 @end
...@@ -5,15 +5,29 @@ ...@@ -5,15 +5,29 @@
"use strict"; "use strict";
import { NativeModules, DeviceEventEmitter } from "react-native"; import { NativeModules, DeviceEventEmitter } from "react-native";
import Map from "core-js/library/es6/map"; 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"; import IOSNotification from "./notification.ios";
export const DEVICE_NOTIFICATION_RECEIVED_FOREGROUND_EVENT = "notificationReceivedForeground"; export const DEVICE_NOTIFICATION_RECEIVED_FOREGROUND_EVENT = "notificationReceivedForeground";
export const DEVICE_NOTIFICATION_RECEIVED_BACKGROUND_EVENT = "notificationReceivedBackground"; export const DEVICE_NOTIFICATION_RECEIVED_BACKGROUND_EVENT = "notificationReceivedBackground";
export const DEVICE_NOTIFICATION_OPENED_EVENT = "notificationOpened"; export const DEVICE_NOTIFICATION_OPENED_EVENT = "notificationOpened";
export const DEVICE_NOTIFICATION_ACTION_RECEIVED = "notificationActionReceived";
let _notificationHandlers = new Map(); 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 { export default class NotificationsIOS {
/** /**
* Attaches a listener to remote notification events while the app is running * Attaches a listener to remote notification events while the app is running
...@@ -55,4 +69,35 @@ export default class NotificationsIOS { ...@@ -55,4 +69,35 @@ export default class NotificationsIOS {
_notificationHandlers.delete(handler); _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<NotificationCategory>) {
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);
}
} }
...@@ -3,6 +3,8 @@ let expect = require("chai").use(require("sinon-chai")).expect; ...@@ -3,6 +3,8 @@ let expect = require("chai").use(require("sinon-chai")).expect;
import proxyquire from "proxyquire"; import proxyquire from "proxyquire";
import sinon from "sinon"; import sinon from "sinon";
/* eslint-disable no-unused-vars */
describe("NotificationsIOS", () => { describe("NotificationsIOS", () => {
let deviceEvents = [ let deviceEvents = [
"notificationReceivedForeground", "notificationReceivedForeground",
...@@ -10,75 +12,129 @@ describe("NotificationsIOS", () => { ...@@ -10,75 +12,129 @@ describe("NotificationsIOS", () => {
"notificationOpened" "notificationOpened"
]; ];
let addEventListenerSpy, removeEventListenerSpy; let nativeAddEventListener, nativeRemoveEventListener, nativeUpdateNotificationCategories;
let notificationIOS; let NotificationIOS, NotificationAction, NotificationCategory;
let someHandler = () => {}; let someHandler = () => {};
before(() => { before(() => {
addEventListenerSpy = sinon.spy(); nativeAddEventListener = sinon.spy();
removeEventListenerSpy = sinon.spy(); nativeRemoveEventListener = sinon.spy();
notificationIOS = proxyquire("../index.ios", { nativeUpdateNotificationCategories = sinon.spy();
let libUnderTest = proxyquire("../index.ios", {
"react-native": { "react-native": {
NativeModules: { NativeModules: {
RNNotifications: { } RNNotifications: {
updateNotificationCategories: nativeUpdateNotificationCategories
}
}, },
DeviceEventEmitter: { DeviceEventEmitter: {
addListener: (...args) => { addListener: (...args) => {
addEventListenerSpy(...args); nativeAddEventListener(...args);
return { remove: removeEventListenerSpy }; return { remove: nativeRemoveEventListener };
} }
}, },
"@noCallThru": true "@noCallThru": true
} }
}).default; });
NotificationIOS = libUnderTest.default;
NotificationAction = libUnderTest.NotificationAction;
NotificationCategory = libUnderTest.NotificationCategory;
}); });
afterEach(() => { afterEach(() => {
addEventListenerSpy.reset(); nativeAddEventListener.reset();
removeEventListenerSpy.reset(); nativeRemoveEventListener.reset();
nativeUpdateNotificationCategories.reset();
}); });
after(() => { after(() => {
addEventListenerSpy = null; nativeAddEventListener = null;
removeEventListenerSpy = null; nativeRemoveEventListener = null;
notificationIOS = null; nativeUpdateNotificationCategories = null;
NotificationIOS = null;
NotificationAction = null;
NotificationCategory = null;
}); });
describe("Add Event Listener", () => { describe("Add Event Listener", () => {
deviceEvents.forEach(event => { deviceEvents.forEach(event => {
it(`should subscribe the given handler to device event: ${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", () => { 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", () => { describe("Remove Event Listener", () => {
deviceEvents.forEach(event => { deviceEvents.forEach(event => {
it(`should unsubscribe the given handler from device event: ${event}`, () => { it(`should unsubscribe the given handler from device event: ${event}`, () => {
notificationIOS.addEventListener(event, someHandler); NotificationIOS.addEventListener(event, someHandler);
notificationIOS.removeEventListener(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", () => { it("should not unsubscribe to unknown device events", () => {
let someUnsupportedEvent = "someUnsupportedEvent"; let someUnsupportedEvent = "someUnsupportedEvent";
notificationIOS.addEventListener(someUnsupportedEvent, someHandler); NotificationIOS.addEventListener(someUnsupportedEvent, someHandler);
notificationIOS.removeEventListener(someUnsupportedEvent, someHandler); NotificationIOS.removeEventListener(someUnsupportedEvent, someHandler);
expect(nativeRemoveEventListener).to.not.have.been.called;
});
});
describe("Update notification categories", () => {
let someAction, someCategory;
let actionOpts = {
activationMode: "foreground",
title: "someAction",
behavior: "default",
identifier: "SOME_ACTION"
};
expect(removeEventListenerSpy).to.not.have.been.called; 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([]);
}); });
// TODO: Test handle notification with IOSNotification object 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);
});
});
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment