Enable replying from the push notification (#871)

* Enable replying from the push notification

* Fix Android and iOS flow

* Own review
This commit is contained in:
enahum 2017-08-29 12:16:34 -03:00 committed by GitHub
parent 3373aae455
commit 60087e6b4d
19 changed files with 324 additions and 72 deletions

View file

@ -45,7 +45,12 @@
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<receiver android:name=".NotificationDismissReceiver" />
<service android:name=".NotificationDismissService"
android:enabled="true"
android:exported="false" />
<service android:name=".NotificationReplyService"
android:enabled="true"
android:exported="false" />
</application>
</manifest>

View file

@ -11,10 +11,13 @@ import android.os.Bundle;
import android.os.Build;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.RemoteInput;
import java.util.LinkedHashMap;
import java.util.ArrayList;
import java.lang.reflect.Field;
import com.wix.reactnativenotifications.core.notification.PushNotification;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
import com.wix.reactnativenotifications.core.AppLaunchHelper;
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import com.wix.reactnativenotifications.core.JsIOHelper;
@ -27,18 +30,25 @@ public class CustomPushNotification extends PushNotification {
public static final int MESSAGE_NOTIFICATION_ID = 435345;
public static final String GROUP_KEY_MESSAGES = "mm_group_key_messages";
public static final String NOTIFICATION_ID = "notificationId";
public static final String KEY_TEXT_REPLY = "CAN_REPLY";
public static final String NOTIFICATION_REPLIED_EVENT_NAME = "notificationReplied";
private static LinkedHashMap<String,Integer> channelIdToNotificationCount = new LinkedHashMap<String,Integer>();
private static LinkedHashMap<String,ArrayList<Bundle>> channelIdToNotification = new LinkedHashMap<String,ArrayList<Bundle>>();
private static AppLifecycleFacade lifecycleFacade;
private static Context context;
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
this.context = context;
}
public static void clearNotification(int notificationId) {
public static void clearNotification(int notificationId, String channelId) {
if (notificationId != -1) {
String channelId = String.valueOf(notificationId);
channelIdToNotificationCount.remove(channelId);
channelIdToNotification.remove(channelId);
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationId);
}
}
@ -108,8 +118,9 @@ public class CustomPushNotification extends PushNotification {
title = mContext.getPackageManager().getApplicationLabel(appInfo).toString();
}
int notificationId = bundle.getString("channel_id").hashCode();
String channelId = bundle.getString("channel_id");
String postId = bundle.getString("post_id");
int notificationId = channelId.hashCode();
String message = bundle.getString("message");
String subText = bundle.getString("subText");
String numberString = bundle.getString("badge");
@ -143,11 +154,7 @@ public class CustomPushNotification extends PushNotification {
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), Integer.parseInt(numberString));
}
int numMessages = 0;
Object objCount = channelIdToNotificationCount.get(channelId);
if (objCount != null) {
numMessages = (Integer)objCount;
}
int numMessages = getMessageCountInChannel(channelId);
notification
.setGroupSummary(true)
@ -173,19 +180,40 @@ public class CustomPushNotification extends PushNotification {
style.setBigContentTitle(title);
notification.setStyle(style)
.setContentTitle(summaryTitle);
// .setNumber(numMessages);
}
// Let's add a delete intent when the notification is dismissed
Intent delIntent = new Intent(mContext, NotificationDismissReceiver.class);
Intent delIntent = new Intent(mContext, NotificationDismissService.class);
delIntent.putExtra(NOTIFICATION_ID, notificationId);
PendingIntent deleteIntent = PendingIntent.getBroadcast(mContext, 0, delIntent, 0);
PendingIntent deleteIntent = NotificationIntentAdapter.createPendingNotificationIntent(mContext, delIntent, mNotificationProps);
notification.setDeleteIntent(deleteIntent);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
notification.setGroup(GROUP_KEY_MESSAGES);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && postId != null) {
Intent replyIntent = new Intent(mContext, NotificationReplyService.class);
replyIntent.setAction(KEY_TEXT_REPLY);
replyIntent.putExtra(NOTIFICATION_ID, notificationId);
replyIntent.putExtra("pushNotification", bundle);
PendingIntent replyPendingIntent = PendingIntent.getService(mContext, 103, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY)
.setLabel("Reply")
.build();
Notification.Action replyAction = new Notification.Action.Builder(
R.drawable.ic_notif_action_reply, "Reply", replyPendingIntent)
.addRemoteInput(remoteInput)
.setAllowGeneratedReplies(true)
.build();
notification
.setShowWhen(true)
.addAction(replyAction);
}
Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
if (largeIconResId != 0 && (largeIcon != null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) {
notification.setLargeIcon(largeIconBitmap);
@ -202,6 +230,15 @@ public class CustomPushNotification extends PushNotification {
mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext());
}
public static Integer getMessageCountInChannel(String channelId) {
Object objCount = channelIdToNotificationCount.get(channelId);
if (objCount != null) {
return (Integer)objCount;
}
return 0;
}
private void cancelNotification(Bundle data, int notificationId) {
final String channelId = data.getString("channel_id");

View file

@ -1,14 +0,0 @@
package com.mattermost.rnbeta;
import android.content.Context;
import android.content.Intent;
import android.content.BroadcastReceiver;
public class NotificationDismissReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
CustomPushNotification.clearNotification(notificationId);
}
}

View file

@ -0,0 +1,23 @@
package com.mattermost.rnbeta;
import android.content.Context;
import android.content.Intent;
import android.app.IntentService;
import android.os.Bundle;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
public class NotificationDismissService extends IntentService {
public NotificationDismissService() {
super("notificationDismissService");
}
@Override
protected void onHandleIntent(Intent intent) {
Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
String channelId = bundle.getString("channel_id");
CustomPushNotification.clearNotification(notificationId, channelId);
}
}

View file

@ -0,0 +1,49 @@
package com.mattermost.rnbeta;
import android.content.Context;
import android.content.Intent;
import android.app.NotificationManager;
import android.app.RemoteInput;
import android.os.Bundle;
import android.support.annotation.Nullable;
import com.facebook.react.HeadlessJsTaskService;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.jstasks.HeadlessJsTaskConfig;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
public class NotificationReplyService extends HeadlessJsTaskService {
@Override
protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) {
if (CustomPushNotification.KEY_TEXT_REPLY.equals(intent.getAction())) {
CharSequence message = getReplyMessage(intent);
Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
String channelId = bundle.getString("channel_id");
bundle.putCharSequence("text", message);
bundle.putInt("msg_count", CustomPushNotification.getMessageCountInChannel(channelId));
int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
CustomPushNotification.clearNotification(notificationId, channelId);
return new HeadlessJsTaskConfig(
"notificationReplied",
Arguments.fromBundle(bundle),
5000);
}
return null;
}
private CharSequence getReplyMessage(Intent intent) {
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput != null) {
return remoteInput.getCharSequence(CustomPushNotification.KEY_TEXT_REPLY);
}
return null;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -91,7 +91,7 @@ export default class Badge extends PureComponent {
setTimeout(() => {
this.setNativeProps({
style: {
display: 'flex'
opacity: 1
}
});
}, 100);
@ -125,7 +125,7 @@ export default class Badge extends PureComponent {
>
<View
ref='badgeContainer'
style={[styles.badge, this.props.style, {display: 'none'}]}
style={[styles.badge, this.props.style, {opacity: 0}]}
>
<View style={styles.wrapper}>
{this.renderText()}

View file

@ -5,7 +5,7 @@ import 'babel-polyfill';
import Analytics from 'analytics-react-native';
import Orientation from 'react-native-orientation';
import {Provider} from 'react-redux';
import {Navigation} from 'react-native-navigation';
import {Navigation, NativeEventsReceiver} from 'react-native-navigation';
import {IntlProvider} from 'react-intl';
import {
Alert,
@ -23,6 +23,7 @@ import {General} from 'mattermost-redux/constants';
import {setAppState, setDeviceToken, setServerVersion} from 'mattermost-redux/actions/general';
import {markChannelAsRead} from 'mattermost-redux/actions/channels';
import {logError} from 'mattermost-redux/actions/errors';
import {createPost} from 'mattermost-redux/actions/posts';
import {logout} from 'mattermost-redux/actions/users';
import {close as closeWebSocket} from 'mattermost-redux/actions/websocket';
import {Client, Client4} from 'mattermost-redux/client';
@ -149,6 +150,7 @@ export default class Mattermost {
PushNotifications.configure({
onRegister: this.onRegisterDevice,
onNotification: this.onPushNotification,
onReply: this.onPushNotificationReply,
popInitialNotification: true,
requestPermissions: true
});
@ -158,24 +160,30 @@ export default class Mattermost {
const {dispatch, getState} = store;
const isActive = appState === 'active';
setAppState(isActive)(dispatch, getState);
try {
if (!isActive && !this.inBackgroundSince) {
this.inBackgroundSince = Date.now();
} else if (isActive && this.inBackgroundSince && (Date.now() - this.inBackgroundSince) >= AUTHENTICATION_TIMEOUT) {
this.inBackgroundSince = null;
if (this.mdmEnabled) {
const config = await mattermostManaged.getConfig();
const authNeeded = config.inAppPinCode && config.inAppPinCode === 'true';
if (authNeeded) {
const authenticated = await this.handleAuthentication(config.vendor);
if (!authenticated) {
mattermostManaged.quitApp();
if (isActive && this.shouldRelaunchonActive) {
this.launchApp();
this.shouldRelaunchonActive = false;
} else {
try {
if (!isActive && !this.inBackgroundSince) {
this.inBackgroundSince = Date.now();
} else if (isActive && this.inBackgroundSince && (Date.now() - this.inBackgroundSince) >= AUTHENTICATION_TIMEOUT) {
this.inBackgroundSince = null;
if (this.mdmEnabled) {
const config = await mattermostManaged.getConfig();
const authNeeded = config.inAppPinCode && config.inAppPinCode === 'true';
if (authNeeded) {
const authenticated = await this.handleAuthentication(config.vendor);
if (!authenticated) {
mattermostManaged.quitApp();
}
}
}
}
} catch (error) {
// do nothing
}
} catch (error) {
// do nothing
}
};
@ -347,11 +355,33 @@ export default class Mattermost {
const state = store.getState();
if (state.views.root.hydrationComplete) {
this.unsubscribeFromStore();
this.handleManagedConfig().then((shouldStart) => {
if (shouldStart) {
this.startApp();
}
});
const notification = PushNotifications.getNotification();
if (notification) {
// If we have a notification means that the app was started cause of a reply
// and the app was not sitting in the background nor opened
const {data, text, badge} = notification;
this.onPushNotificationReply(data, text, badge);
PushNotifications.resetNotification();
}
if (Platform.OS === 'android') {
// In case of Android we need to handle the bridge being initialized by HeadlessJS
Promise.resolve(Navigation.isAppLaunched()).then((appLaunched) => {
if (appLaunched) {
this.launchApp(); // App is launched -> show UI
} else {
new NativeEventsReceiver().appLaunched(this.launchApp); // App hasn't been launched yet -> show the UI only when needed.
}
});
} else if (AppState.currentState === 'background') {
// for IOS replying from push notification starts the app in the background
this.shouldRelaunchonActive = true;
this.configurePushNotifications();
this.startFakeApp();
} else {
this.launchApp();
}
}
};
@ -366,6 +396,7 @@ export default class Mattermost {
} else {
prefix = General.PUSH_NOTIFY_ANDROID_REACT_NATIVE;
}
setDeviceToken(`${prefix}:${data.token}`)(dispatch, getState);
this.isConfigured = true;
};
@ -407,6 +438,46 @@ export default class Mattermost {
}
};
onPushNotificationReply = (data, text, badge, completed) => {
const {dispatch, getState} = store;
const state = getState();
const {currentUserId} = state.entities.users;
if (currentUserId) {
// one thing to note is that for android it will reply to the last post in the stack
const rootId = data.root_id || data.post_id;
const post = {
user_id: currentUserId,
channel_id: data.channel_id,
root_id: rootId,
parent_id: rootId,
message: text
};
if (!Client4.getUrl()) {
// Make sure the Client has the server url set
Client4.setUrl(state.entities.general.credentials.url);
}
if (!Client.getToken()) {
// Make sure the Client has the server token set
Client4.setToken(state.entities.general.credentials.token);
}
createPost(post)(dispatch, getState);
markChannelAsRead(data.channel_id)(dispatch, getState);
if (badge >= 0) {
PushNotifications.setApplicationIconBadgeNumber(badge);
}
}
if (completed) {
// You must call to completed(), otherwise the action will not be triggered
completed();
}
};
resetBadgeAndVersion = () => {
const {dispatch, getState} = store;
Client4.serverVersion = '';
@ -426,6 +497,14 @@ export default class Mattermost {
this.startApp('fade');
};
launchApp = () => {
this.handleManagedConfig().then((shouldStart) => {
if (shouldStart) {
this.startApp();
}
});
};
startFakeApp = async () => {
return Navigation.startSingleScreenApp({
screen: {
@ -443,6 +522,10 @@ export default class Mattermost {
};
startApp = (animationType = 'none') => {
if (!this.isConfigured) {
this.configurePushNotifications();
}
Navigation.startSingleScreenApp({
screen: {
screen: 'Root',
@ -457,9 +540,5 @@ export default class Mattermost {
},
animationType
});
if (!this.isConfigured) {
this.configurePushNotifications();
}
};
}

View file

@ -1,13 +1,16 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {AppState} from 'react-native';
import {AppRegistry, AppState} from 'react-native';
import {NotificationsAndroid, PendingNotifications} from 'react-native-notifications';
import Notification from 'react-native-notifications/notification.android';
class PushNotification {
constructor() {
this.onRegister = null;
this.onNotification = null;
this.onReply = null;
this.deviceNotification = null;
NotificationsAndroid.setNotificationReceivedListener((notification) => {
if (notification) {
@ -22,6 +25,21 @@ class PushNotification {
this.handleNotification(data, true);
}
});
AppRegistry.registerHeadlessTask('notificationReplied', () => async (deviceNotification) => {
const notification = new Notification(deviceNotification);
const data = notification.getData();
if (this.onReply && AppState.currentState === 'background') {
this.onReply(data, data.text, parseInt(data.badge, 10) - parseInt(data.msg_count, 10));
} else {
this.deviceNotification = {
data,
text: data.text,
badge: parseInt(data.badge, 10) - parseInt(data.msg_count, 10)
};
}
});
}
handleNotification = (data, userInteraction) => {
@ -41,6 +59,7 @@ class PushNotification {
configure(options) {
this.onRegister = options.onRegister;
this.onNotification = options.onNotification;
this.onReply = options.onReply;
NotificationsAndroid.refreshToken();
NotificationsAndroid.setRegistrationTokenUpdateListener((deviceToken) => {
@ -80,6 +99,14 @@ class PushNotification {
setApplicationIconBadgeNumber(number) {
NotificationsAndroid.setBadgesCount(number);
}
getNotification() {
return this.deviceNotification;
}
resetNotification() {
this.deviceNotification = null;
}
}
export default new PushNotification();

View file

@ -2,12 +2,18 @@
// See License.txt for license information.
import {AppState} from 'react-native';
import NotificationsIOS from 'react-native-notifications';
import NotificationsIOS, {NotificationAction, NotificationCategory} from 'react-native-notifications';
const CATEGORY = 'CAN_REPLY';
const REPLY_ACTION = 'REPLY_ACTION';
let replyCategory;
class PushNotification {
constructor() {
this.onRegister = null;
this.onNotification = null;
this.onReply = null;
NotificationsIOS.addEventListener('notificationReceivedForeground', (notification) => {
const info = {
@ -32,6 +38,20 @@ class PushNotification {
};
this.handleNotification(info, false, true);
});
const replyAction = new NotificationAction({
activationMode: 'background',
title: 'Reply',
behavior: 'textInput',
authenticationRequired: true,
identifier: REPLY_ACTION
}, this.handleReply);
replyCategory = new NotificationCategory({
identifier: CATEGORY,
actions: [replyAction],
context: 'default'
});
}
handleNotification = (data, foreground, userInteraction) => {
@ -48,9 +68,24 @@ class PushNotification {
}
};
handleReply = (action, completed) => {
if (action.identifier === REPLY_ACTION) {
const data = action.notification.getData();
const text = action.text;
const badge = parseInt(action.notification._badge, 10) - 1; //eslint-disable-line no-underscore-dangle
if (this.onReply) {
this.onReply(data, text, badge, completed);
}
} else {
completed();
}
};
configure(options) {
this.onRegister = options.onRegister;
this.onNotification = options.onNotification;
this.onReply = options.onReply;
NotificationsIOS.addEventListener('remoteNotificationsRegistered', (deviceToken) => {
if (this.onRegister) {
@ -59,7 +94,7 @@ class PushNotification {
});
if (options.requestPermissions) {
this.requestPermissions();
this.requestPermissions([replyCategory]);
}
if (options.popInitialNotification) {
@ -67,8 +102,8 @@ class PushNotification {
}
}
requestPermissions = () => {
NotificationsIOS.requestPermissions();
requestPermissions = (permissions) => {
NotificationsIOS.requestPermissions(permissions);
};
localNotificationSchedule(notification) {
@ -91,6 +126,10 @@ class PushNotification {
setApplicationIconBadgeNumber(number) {
NotificationsIOS.setBadgesCount(number);
}
getNotification() {
return null;
}
}
export default new PushNotification();

View file

@ -350,7 +350,13 @@ platform :android do
)
find_replace_string(
path_to_file: './android/app/src/main/java/com/mattermost/rn/NotificationDismissReceiver.java',
path_to_file: './android/app/src/main/java/com/mattermost/rn/NotificationDismissService.java',
old_string: 'package com.mattermost.rnbeta;',
new_string: 'package com.mattermost.rn;'
)
find_replace_string(
path_to_file: './android/app/src/main/java/com/mattermost/rn/NotificationReplyService.java',
old_string: 'package com.mattermost.rnbeta;',
new_string: 'package com.mattermost.rn;'
)

View file

@ -5,7 +5,6 @@
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; };
00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; };
@ -1435,7 +1434,6 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -1453,7 +1451,6 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";

View file

@ -60,4 +60,16 @@
{
[RNNotifications didReceiveLocalNotification:notification];
}
// Required for the notification actions.
- (void)application:(UIApplication *)application handleActionWithIdentifier:(NSString *)identifier forLocalNotification:(UILocalNotification *)notification withResponseInfo:(NSDictionary *)responseInfo completionHandler:(void (^)())completionHandler
{
[RNNotifications 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 handleActionWithIdentifier:identifier forRemoteNotification:userInfo withResponseInfo:responseInfo completionHandler:completionHandler];
}
@end

View file

@ -44,7 +44,7 @@
<key>NSCameraUsageDescription</key>
<string>Take a Photo or Video and upload it to your mattermost instance</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<string/>
<key>NSPhotoLibraryUsageDescription</key>
<string>Upload Photos and Videos to your Mattermost instance</string>
<key>UIAppFonts</key>
@ -57,16 +57,8 @@
<string>MaterialIcons.ttf</string>
<string>Octicons.ttf</string>
<string>Zocial.ttf</string>
<string>Entypo.ttf</string>
<string>EvilIcons.ttf</string>
<string>FontAwesome.ttf</string>
<string>Foundation.ttf</string>
<string>Ionicons.ttf</string>
<string>MaterialCommunityIcons.ttf</string>
<string>MaterialIcons.ttf</string>
<string>Octicons.ttf</string>
<string>SimpleLineIcons.ttf</string>
<string>Zocial.ttf</string>
<string>OpenSans-Bold.ttf</string>
<string>OpenSans-BoldItalic.ttf</string>
<string>OpenSans-ExtraBold.ttf</string>

View file

@ -4560,8 +4560,8 @@ react-native-navigation@1.1.131:
lodash "4.x.x"
react-native-notifications@enahum/react-native-notifications.git:
version "1.1.11"
resolved "https://codeload.github.com/enahum/react-native-notifications/tar.gz/a8450bcbfc6ae46ed4b97f6fe723aac44c8ad598"
version "1.1.15"
resolved "https://codeload.github.com/enahum/react-native-notifications/tar.gz/084d5e928d8149e711c1b6c384f3d87cc16072f9"
dependencies:
core-js "^1.0.0"
uuid "^2.0.3"