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) => {
// await someAsyncCall();
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
//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
......@@ -372,7 +376,7 @@ class App extends Component {
body: "My Notification Message", // as FCM payload (required)
sound: "default", // 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
number: 10, // Android only
ticker: "My Notification Ticker", // Android only
......@@ -416,6 +420,28 @@ class App extends Component {
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()
.then( () => {
//Deleted instance id successfully
......@@ -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.
### 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
#### Why do you build another local notification
......
......@@ -25,6 +25,26 @@ declare module "react-native-fcm" {
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 {
collapse_key: string;
opened_from_tray: boolean;
......@@ -44,6 +64,8 @@ declare module "react-native-fcm" {
};
local_notification?: boolean;
_notificationType: string;
_actionIdentifier?: string;
_userText?: string;
finish(type?: string): void;
[key: string]: any;
}
......@@ -83,6 +105,23 @@ declare module "react-native-fcm" {
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 {
static requestPermissions(): Promise<void>;
static getFCMToken(): Promise<string>;
......@@ -109,6 +148,8 @@ declare module "react-native-fcm" {
static enableDirectChannel(): void
static isDirectChannelEstablished(): Promise<boolean>
static getAPNSToken(): Promise<string>
static setNotificationCategories(categories: NotificationCategory[]): void;
}
export default FCM;
......
......@@ -26,6 +26,26 @@ export const NotificationType = {
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 FCM = {};
......@@ -174,4 +194,12 @@ FCM.send = (senderId, payload) => {
RNFIRMessaging.send(senderId, payload);
};
FCM.setNotificationCategories = (categories) => {
if (Platform.OS === 'ios') {
RNFIRMessaging.setNotificationCategories(categories);
}
}
export default FCM;
export {};
......@@ -131,6 +131,94 @@ RCT_ENUM_CONVERTER(UNNotificationPresentationOptions, (@{
@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 ()
@property (nonatomic, strong) NSMutableDictionary *notificationCallbacks;
@end
......@@ -169,7 +257,21 @@ RCT_EXPORT_MODULE();
if (response.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
......@@ -210,6 +312,14 @@ RCT_EXPORT_MODULE();
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)
{
[[FIRMessaging messaging] setShouldEstablishDirectChannel:@YES];
......@@ -230,6 +340,8 @@ RCT_EXPORT_METHOD(getInitialNotification:(RCTPromiseResolveBlock)resolve rejecte
}
}
RCT_EXPORT_METHOD(getAPNSToken:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{
NSData * deviceToken = [FIRMessaging messaging].APNSToken;
......@@ -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)
{
dispatch_async(dispatch_get_main_queue(), ^{
......@@ -493,6 +619,9 @@ RCT_EXPORT_METHOD(finishNotificationResponse: (NSString *)completionHandlerId){
[self sendEventWithName:FCMNotificationReceived body:data];
if (initialNotificationActionResponse) {
initialNotificationActionResponse = nil;
}
}
- (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