Commit 66da4eb0 authored by Libin Lu's avatar Libin Lu Committed by GitHub

Merge pull request #773 from krystofcelba/ios-notification-actions

Add support for notification actions on iOS
parents 24f887fd aa3c559e
...@@ -312,6 +312,10 @@ FCM.on(FCMEvent.Notification, async (notif) => { ...@@ -312,6 +312,10 @@ FCM.on(FCMEvent.Notification, async (notif) => {
// await someAsyncCall(); // await someAsyncCall();
if(Platform.OS ==='ios'){ if(Platform.OS ==='ios'){
if (notif._actionIdentifier === 'com.myapp.MyCategory.Confirm') {
// handle notification action here
// the text from user is in notif._userText if type of the action is NotificationActionType.TextInput
}
//optional //optional
//iOS requires developers to call completionHandler to end notification process. If you do not call it your background remote notifications could be throttled, to read more about it see https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623013-application. //iOS requires developers to call completionHandler to end notification process. If you do not call it your background remote notifications could be throttled, to read more about it see https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623013-application.
//This library handles it for you automatically with default behavior (for remote notification, finish with NoData; for WillPresent, finish depend on "show_in_foreground"). However if you want to return different result, follow the following code to override //This library handles it for you automatically with default behavior (for remote notification, finish with NoData; for WillPresent, finish depend on "show_in_foreground"). However if you want to return different result, follow the following code to override
...@@ -372,7 +376,7 @@ class App extends Component { ...@@ -372,7 +376,7 @@ class App extends Component {
body: "My Notification Message", // as FCM payload (required) body: "My Notification Message", // as FCM payload (required)
sound: "default", // as FCM payload sound: "default", // as FCM payload
priority: "high", // as FCM payload priority: "high", // as FCM payload
click_action: "ACTION", // as FCM payload click_action: "com.myapp.MyCategory", // as FCM payload - this is used as category identifier on iOS.
badge: 10, // as FCM payload IOS only, set 0 to clear badges badge: 10, // as FCM payload IOS only, set 0 to clear badges
number: 10, // Android only number: 10, // Android only
ticker: "My Notification Ticker", // Android only ticker: "My Notification Ticker", // Android only
...@@ -416,6 +420,28 @@ class App extends Component { ...@@ -416,6 +420,28 @@ class App extends Component {
my_custom_data_2: 'my_custom_field_value_2' my_custom_data_2: 'my_custom_field_value_2'
}); });
// Call this somewhere at initialization to register types of your actionable notifications. See https://goo.gl/UanU9p.
FCM.setNotificationCategories([
{
id: 'com.myapp.MyCategory',
actions: [
{
type: NotificationActionType.Default, // or NotificationActionType.TextInput
id: 'com.myapp.MyCategory.Confirm',
title: 'Confirm', // Use with NotificationActionType.Default
textInputButtonTitle: 'Send', // Use with NotificationActionType.TextInput
textInputPlaceholder: 'Message', // Use with NotificationActionType.TextInput
// Available options: NotificationActionOption.None, NotificationActionOption.AuthenticationRequired, NotificationActionOption.Destructive and NotificationActionOption.Foreground.
options: NotificationActionOption.AuthenticationRequired, // single or array
},
],
intentIdentifiers: [],
// Available options: NotificationCategoryOption.None, NotificationCategoryOption.CustomDismissAction and NotificationCategoryOption.AllowInCarPlay.
// On iOS >= 11.0 there is also NotificationCategoryOption.PreviewsShowTitle and NotificationCategoryOption.PreviewsShowSubtitle.
options: [NotificationCategoryOption.CustomDismissAction, NotificationCategoryOption.PreviewsShowTitle], // single or array
},
]);
FCM.deleteInstanceId() FCM.deleteInstanceId()
.then( () => { .then( () => {
//Deleted instance id successfully //Deleted instance id successfully
...@@ -543,6 +569,26 @@ FCM.send('984XXXXXXXXX', { ...@@ -543,6 +569,26 @@ FCM.send('984XXXXXXXXX', {
The `Data Object` is message data comprising as many key-value pairs of the message's payload as are needed (ensure that the value of each pair in the data object is a `string`). Your `Sender ID` is a unique numerical value generated when you created your Firebase project, it is available in the `Cloud Messaging` tab of the Firebase console `Settings` pane. The sender ID is used to identify each app server that can send messages to the client app. The `Data Object` is message data comprising as many key-value pairs of the message's payload as are needed (ensure that the value of each pair in the data object is a `string`). Your `Sender ID` is a unique numerical value generated when you created your Firebase project, it is available in the `Cloud Messaging` tab of the Firebase console `Settings` pane. The sender ID is used to identify each app server that can send messages to the client app.
### Sending remote notifications with category on iOS
If you want to send notification which will have actions as you defined in app it's important to correctly set it's `category` (`click_action`) property. It's also good to set `"content-available" : 1` so app will gets enough time to handle actions in background.
So the fcm payload should look like this:
```javascript
{
"to": "some_device_token",
"content_available": true,
"notification": {
"title": "Alarm",
"subtitle": "First Alarm",
"body": "First Alarm",
"click_action": "com.myapp.MyCategory" // The id of notification category which you defined with FCM.setNotificationCategories
},
"data": {
"extra": "juice"
}
}
```
## Q & A ## Q & A
#### Why do you build another local notification #### Why do you build another local notification
......
...@@ -25,6 +25,26 @@ declare module "react-native-fcm" { ...@@ -25,6 +25,26 @@ declare module "react-native-fcm" {
const Local = "local_notification"; const Local = "local_notification";
} }
export enum NotificationCategoryOption {
CustomDismissAction = 'UNNotificationCategoryOptionCustomDismissAction',
AllowInCarPlay = 'UNNotificationCategoryOptionAllowInCarPlay',
PreviewsShowTitle = 'UNNotificationCategoryOptionHiddenPreviewsShowTitle',
PreviewsShowSubtitle = 'UNNotificationCategoryOptionHiddenPreviewsShowSubtitle',
None = 'UNNotificationCategoryOptionNone'
}
export enum NotificationActionOption {
AuthenticationRequired = 'UNNotificationActionOptionAuthenticationRequired',
Destructive = 'UNNotificationActionOptionDestructive',
Foreground = 'UNNotificationActionOptionForeground',
None = 'UNNotificationActionOptionNone'
}
export enum NotificationActionType {
Default = 'UNNotificationActionTypeDefault',
TextInput = 'UNNotificationActionTypeTextInput',
}
export interface Notification { export interface Notification {
collapse_key: string; collapse_key: string;
opened_from_tray: boolean; opened_from_tray: boolean;
...@@ -44,6 +64,8 @@ declare module "react-native-fcm" { ...@@ -44,6 +64,8 @@ declare module "react-native-fcm" {
}; };
local_notification?: boolean; local_notification?: boolean;
_notificationType: string; _notificationType: string;
_actionIdentifier?: string;
_userText?: string;
finish(type?: string): void; finish(type?: string): void;
[key: string]: any; [key: string]: any;
} }
...@@ -83,6 +105,23 @@ declare module "react-native-fcm" { ...@@ -83,6 +105,23 @@ declare module "react-native-fcm" {
remove(): void; remove(): void;
} }
export interface NotificationAction {
type: NotificationActionType;
id: string;
title?: string;
textInputButtonTitle?: string;
textInputPlaceholder?: string;
options: NotificationActionOption | NotificationActionOption[];
}
export interface NotificationCategory {
id: string;
actions: NotificationAction[];
intentIdentifiers: string[];
hiddenPreviewsBodyPlaceholder?: string;
options?: NotificationCategoryOption | NotificationCategoryOption[];
}
export class FCM { export class FCM {
static requestPermissions(): Promise<void>; static requestPermissions(): Promise<void>;
static getFCMToken(): Promise<string>; static getFCMToken(): Promise<string>;
...@@ -109,6 +148,8 @@ declare module "react-native-fcm" { ...@@ -109,6 +148,8 @@ declare module "react-native-fcm" {
static enableDirectChannel(): void static enableDirectChannel(): void
static isDirectChannelEstablished(): Promise<boolean> static isDirectChannelEstablished(): Promise<boolean>
static getAPNSToken(): Promise<string> static getAPNSToken(): Promise<string>
static setNotificationCategories(categories: NotificationCategory[]): void;
} }
export default FCM; export default FCM;
......
...@@ -26,6 +26,26 @@ export const NotificationType = { ...@@ -26,6 +26,26 @@ export const NotificationType = {
Local: 'local_notification' Local: 'local_notification'
}; };
export const NotificationCategoryOption = {
CustomDismissAction: 'UNNotificationCategoryOptionCustomDismissAction',
AllowInCarPlay: 'UNNotificationCategoryOptionAllowInCarPlay',
PreviewsShowTitle: 'UNNotificationCategoryOptionHiddenPreviewsShowTitle',
PreviewsShowSubtitle: 'UNNotificationCategoryOptionHiddenPreviewsShowSubtitle',
None: 'UNNotificationCategoryOptionNone'
};
export const NotificationActionOption = {
AuthenticationRequired: 'UNNotificationActionOptionAuthenticationRequired',
Destructive: 'UNNotificationActionOptionDestructive',
Foreground: 'UNNotificationActionOptionForeground',
None: 'UNNotificationActionOptionNone',
};
export const NotificationActionType = {
Default: 'UNNotificationActionTypeDefault',
TextInput: 'UNNotificationActionTypeTextInput',
};
const RNFIRMessaging = NativeModules.RNFIRMessaging; const RNFIRMessaging = NativeModules.RNFIRMessaging;
const FCM = {}; const FCM = {};
...@@ -174,4 +194,12 @@ FCM.send = (senderId, payload) => { ...@@ -174,4 +194,12 @@ FCM.send = (senderId, payload) => {
RNFIRMessaging.send(senderId, payload); RNFIRMessaging.send(senderId, payload);
}; };
FCM.setNotificationCategories = (categories) => {
if (Platform.OS === 'ios') {
RNFIRMessaging.setNotificationCategories(categories);
}
}
export default FCM; export default FCM;
export {};
...@@ -131,6 +131,94 @@ RCT_ENUM_CONVERTER(UNNotificationPresentationOptions, (@{ ...@@ -131,6 +131,94 @@ RCT_ENUM_CONVERTER(UNNotificationPresentationOptions, (@{
@end @end
@implementation RCTConvert (UNNotificationAction)
typedef NS_ENUM(NSUInteger, UNNotificationActionType) {
UNNotificationActionTypeDefault,
UNNotificationActionTypeTextInput
};
+ (UNNotificationAction *) UNNotificationAction:(id)json {
NSDictionary<NSString *, id> *details = [self NSDictionary:json];
NSString *identifier = [RCTConvert NSString: details[@"id"]];
NSString *title = [RCTConvert NSString: details[@"title"]];
UNNotificationActionOptions options = [RCTConvert UNNotificationActionOptions: details[@"options"]];
UNNotificationActionType type = [RCTConvert UNNotificationActionType:details[@"type"]];
if (type == UNNotificationActionTypeTextInput) {
NSString *textInputButtonTitle = [RCTConvert NSString: details[@"textInputButtonTitle"]];
NSString *textInputPlaceholder = [RCTConvert NSString: details[@"textInputPlaceholder"]];
return [UNTextInputNotificationAction actionWithIdentifier:identifier title:title options:options textInputButtonTitle:textInputButtonTitle textInputPlaceholder:textInputPlaceholder];
}
return [UNNotificationAction actionWithIdentifier:identifier
title:title
options:options];
}
RCT_ENUM_CONVERTER(UNNotificationActionType, (@{
@"UNNotificationActionTypeDefault": @(UNNotificationActionTypeDefault),
@"UNNotificationActionTypeTextInput": @(UNNotificationActionTypeTextInput),
}), UNNotificationActionTypeDefault, integerValue)
RCT_MULTI_ENUM_CONVERTER(UNNotificationActionOptions, (@{
@"UNNotificationActionOptionAuthenticationRequired": @(UNNotificationActionOptionAuthenticationRequired),
@"UNNotificationActionOptionDestructive": @(UNNotificationActionOptionDestructive),
@"UNNotificationActionOptionForeground": @(UNNotificationActionOptionForeground),
@"UNNotificationActionOptionNone": @(UNNotificationActionOptionNone),
}), UNNotificationActionOptionNone, integerValue)
@end
@implementation RCTConvert (UNNotificationCategory)
+ (UNNotificationCategory *) UNNotificationCategory:(id)json {
NSDictionary<NSString *, id> *details = [self NSDictionary:json];
NSString *identifier = [RCTConvert NSString: details[@"id"]];
NSMutableArray *actions = [[NSMutableArray alloc] init];
for (NSDictionary *actionDict in details[@"actions"]) {
[actions addObject:[RCTConvert UNNotificationAction:actionDict]];
}
NSArray<NSString *> *intentIdentifiers = [RCTConvert NSStringArray:details[@"intentIdentifiers"]];
NSString *hiddenPreviewsBodyPlaceholder = [RCTConvert NSString:details[@"hiddenPreviewsBodyPlaceholder"]];
UNNotificationCategoryOptions options = [RCTConvert UNNotificationCategoryOptions: details[@"options"]];
if (hiddenPreviewsBodyPlaceholder) {
if (@available(iOS 11.0, *)) {
return [UNNotificationCategory categoryWithIdentifier:identifier actions:actions intentIdentifiers:intentIdentifiers hiddenPreviewsBodyPlaceholder:hiddenPreviewsBodyPlaceholder options:options];
}
}
return [UNNotificationCategory categoryWithIdentifier:identifier actions:actions intentIdentifiers:intentIdentifiers options:options];
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpartial-availability"
RCT_MULTI_ENUM_CONVERTER(UNNotificationCategoryOptions, (@{
@"UNNotificationCategoryOptionNone": @(UNNotificationCategoryOptionNone),
@"UNNotificationCategoryOptionCustomDismissAction": @(UNNotificationCategoryOptionCustomDismissAction),
@"UNNotificationCategoryOptionAllowInCarPlay": @(UNNotificationCategoryOptionAllowInCarPlay),
@"UNNotificationCategoryOptionHiddenPreviewsShowTitle": @(UNNotificationCategoryOptionHiddenPreviewsShowTitle),
@"UNNotificationCategoryOptionHiddenPreviewsShowSubtitle": @(UNNotificationCategoryOptionHiddenPreviewsShowSubtitle),
}), UNNotificationCategoryOptionNone, integerValue)
#pragma clang diagnostic pop
@end
static NSDictionary *initialNotificationActionResponse;
@interface RNFIRMessaging () @interface RNFIRMessaging ()
@property (nonatomic, strong) NSMutableDictionary *notificationCallbacks; @property (nonatomic, strong) NSMutableDictionary *notificationCallbacks;
@end @end
...@@ -169,7 +257,21 @@ RCT_EXPORT_MODULE(); ...@@ -169,7 +257,21 @@ RCT_EXPORT_MODULE();
if (response.actionIdentifier) { if (response.actionIdentifier) {
[data setValue:response.actionIdentifier forKey:@"_actionIdentifier"]; [data setValue:response.actionIdentifier forKey:@"_actionIdentifier"];
} }
[[NSNotificationCenter defaultCenter] postNotificationName:FCMNotificationReceived object:self userInfo:@{@"data": data, @"completionHandler": completionHandler}];
if ([response isKindOfClass:UNTextInputNotificationResponse.class]) {
[data setValue:[(UNTextInputNotificationResponse *)response userText] forKey:@"_userText"];
}
NSDictionary *userInfo = @{@"data": data, @"completionHandler": completionHandler};
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (data[@"_actionIdentifier"] && ![data[@"_actionIdentifier"] isEqualToString:UNNotificationDefaultActionIdentifier]) {
initialNotificationActionResponse = userInfo;
}
});
[[NSNotificationCenter defaultCenter] postNotificationName:FCMNotificationReceived object:self userInfo:userInfo];
} }
+ (void)willPresentNotification:(UNNotification *)notification withCompletionHandler:(nonnull RCTWillPresentNotificationCallback)completionHandler + (void)willPresentNotification:(UNNotification *)notification withCompletionHandler:(nonnull RCTWillPresentNotificationCallback)completionHandler
...@@ -210,6 +312,14 @@ RCT_EXPORT_MODULE(); ...@@ -210,6 +312,14 @@ RCT_EXPORT_MODULE();
return self; return self;
} }
-(void) addListener:(NSString *)eventName {
[super addListener:eventName];
if([eventName isEqualToString:FCMNotificationReceived] && initialNotificationActionResponse) {
[[NSNotificationCenter defaultCenter] postNotificationName:FCMNotificationReceived object:self userInfo:[initialNotificationActionResponse copy]];
}
}
RCT_EXPORT_METHOD(enableDirectChannel) RCT_EXPORT_METHOD(enableDirectChannel)
{ {
[[FIRMessaging messaging] setShouldEstablishDirectChannel:@YES]; [[FIRMessaging messaging] setShouldEstablishDirectChannel:@YES];
...@@ -230,6 +340,8 @@ RCT_EXPORT_METHOD(getInitialNotification:(RCTPromiseResolveBlock)resolve rejecte ...@@ -230,6 +340,8 @@ RCT_EXPORT_METHOD(getInitialNotification:(RCTPromiseResolveBlock)resolve rejecte
} }
} }
RCT_EXPORT_METHOD(getAPNSToken:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) RCT_EXPORT_METHOD(getAPNSToken:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{ {
NSData * deviceToken = [FIRMessaging messaging].APNSToken; NSData * deviceToken = [FIRMessaging messaging].APNSToken;
...@@ -416,6 +528,20 @@ RCT_EXPORT_METHOD(getScheduledLocalNotifications:(RCTPromiseResolveBlock)resolve ...@@ -416,6 +528,20 @@ RCT_EXPORT_METHOD(getScheduledLocalNotifications:(RCTPromiseResolveBlock)resolve
} }
} }
RCT_EXPORT_METHOD(setNotificationCategories:(NSArray *)categories)
{
if([UNUserNotificationCenter currentNotificationCenter] != nil) {
NSMutableSet *categoriesSet = [[NSMutableSet alloc] init];
for(NSDictionary *categoryDict in categories) {
UNNotificationCategory *category = [RCTConvert UNNotificationCategory:categoryDict];
[categoriesSet addObject:category];
}
[[UNUserNotificationCenter currentNotificationCenter] setNotificationCategories:categoriesSet];
}
}
RCT_EXPORT_METHOD(setBadgeNumber: (NSInteger) number) RCT_EXPORT_METHOD(setBadgeNumber: (NSInteger) number)
{ {
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
...@@ -493,6 +619,9 @@ RCT_EXPORT_METHOD(finishNotificationResponse: (NSString *)completionHandlerId){ ...@@ -493,6 +619,9 @@ RCT_EXPORT_METHOD(finishNotificationResponse: (NSString *)completionHandlerId){
[self sendEventWithName:FCMNotificationReceived body:data]; [self sendEventWithName:FCMNotificationReceived body:data];
if (initialNotificationActionResponse) {
initialNotificationActionResponse = nil;
}
} }
- (void)sendDataMessageFailure:(NSNotification *)notification - (void)sendDataMessageFailure:(NSNotification *)notification
......
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