diff --git a/README.md b/README.md index e7b4c07d868752872e1e7cc63566fe56a55c2c96..4d165d48a738961f7837251b913cf738d6ab6e12 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/index.d.ts b/index.d.ts index fce43d2dc21b246039043c14b97c75bae5d3ad07..ac9fa5a5ebc028eed2b77ad0fb5d8c25465a6e99 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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; static getFCMToken(): Promise; @@ -109,6 +148,8 @@ declare module "react-native-fcm" { static enableDirectChannel(): void static isDirectChannelEstablished(): Promise static getAPNSToken(): Promise + + static setNotificationCategories(categories: NotificationCategory[]): void; } export default FCM; diff --git a/index.js b/index.js index dd0b3cb00d2081efbf2452b4fa9ce3e89eeeee79..7de6a6e77e8833954f5e35ee6c4e26bc2d954eae 100644 --- a/index.js +++ b/index.js @@ -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 {}; diff --git a/ios/RNFIRMessaging.m b/ios/RNFIRMessaging.m index 8bc4ae34026a7afd0df3f4c71a6607d5b855c005..507b975eacaead3c045099c8157b65d5b3016059 100644 --- a/ios/RNFIRMessaging.m +++ b/ios/RNFIRMessaging.m @@ -131,6 +131,94 @@ RCT_ENUM_CONVERTER(UNNotificationPresentationOptions, (@{ @end +@implementation RCTConvert (UNNotificationAction) + +typedef NS_ENUM(NSUInteger, UNNotificationActionType) { + UNNotificationActionTypeDefault, + UNNotificationActionTypeTextInput +}; + ++ (UNNotificationAction *) UNNotificationAction:(id)json { + NSDictionary *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 *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 *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 @@ -144,7 +232,7 @@ RCT_EXPORT_MODULE(); } + (BOOL)requiresMainQueueSetup { - return YES; + return YES; } + (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo fetchCompletionHandler:(nonnull RCTRemoteNotificationCallback)completionHandler { @@ -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