Skip to content

Commit

Permalink
Merge pull request #773 from krystofcelba/ios-notification-actions
Browse files Browse the repository at this point in the history
Add support for notification actions on iOS
  • Loading branch information
Libin Lu authored Feb 5, 2018
2 parents 24f887f + aa3c559 commit 66da4eb
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 3 deletions.
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
41 changes: 41 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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>;
Expand All @@ -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;
Expand Down
28 changes: 28 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down Expand Up @@ -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 {};
133 changes: 131 additions & 2 deletions ios/RNFIRMessaging.m
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,94 @@ + (UILocalNotification *)UILocalNotification:(id)json

@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
Expand All @@ -144,7 +232,7 @@ @implementation RNFIRMessaging
}

+ (BOOL)requiresMainQueueSetup {
return YES;
return YES;
}

+ (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo fetchCompletionHandler:(nonnull RCTRemoteNotificationCallback)completionHandler {
Expand All @@ -169,7 +257,21 @@ + (void)didReceiveNotificationResponse:(UNNotificationResponse *)response withCo
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
Expand Down Expand Up @@ -210,6 +312,14 @@ - (instancetype)init {
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];
Expand All @@ -230,6 +340,8 @@ - (instancetype)init {
}
}



RCT_EXPORT_METHOD(getAPNSToken:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{
NSData * deviceToken = [FIRMessaging messaging].APNSToken;
Expand Down Expand Up @@ -416,6 +528,20 @@ - (void)applicationReceivedRemoteMessage:(FIRMessagingRemoteMessage *)remoteMess
}
}

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(), ^{
Expand Down Expand Up @@ -493,6 +619,9 @@ - (void)handleNotificationReceived:(NSNotification *)notification

[self sendEventWithName:FCMNotificationReceived body:data];

if (initialNotificationActionResponse) {
initialNotificationActionResponse = nil;
}
}

- (void)sendDataMessageFailure:(NSNotification *)notification
Expand Down

0 comments on commit 66da4eb

Please sign in to comment.