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

added interactive notifications 💬

parent 37492496
......@@ -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
......@@ -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 (
<View style={styles.container}>
<Text style={styles.welcome}>
......
......@@ -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
......@@ -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<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;
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(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