From 4ff20007f5401d4403684b2538e33a2b75e213fc Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 8 Feb 2018 17:07:20 +0000 Subject: [PATCH] [fcm] Add iOS completion handlers --- .../messaging/RNFirebaseMessaging.m | 91 +++++++++++-- lib/modules/messaging/Message.js | 122 ++++++++++++++++++ lib/modules/messaging/index.js | 45 ++----- lib/modules/messaging/types.js | 60 +++++++++ 4 files changed, 275 insertions(+), 43 deletions(-) create mode 100644 lib/modules/messaging/Message.js create mode 100644 lib/modules/messaging/types.js diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index 180e7d54..1faed23c 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -17,6 +17,7 @@ @import UserNotifications; @interface RNFirebaseMessaging () +@property (nonatomic, strong) NSMutableDictionary *callbackHandlers; @end #endif @@ -53,6 +54,9 @@ RCT_EXPORT_MODULE() // Set static instance for use from AppDelegate theRNFirebaseMessaging = self; + + // Initialise callback handlers dictionary + _callbackHandlers = [NSMutableDictionary dictionary]; } // ******************************************************* @@ -63,7 +67,7 @@ RCT_EXPORT_MODULE() // Listen for background messages - (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo { BOOL isFromBackground = (RCTSharedApplication().applicationState == UIApplicationStateInactive); - NSDictionary *message = [self parseUserInfo:userInfo clickAction:nil openedFromTray:isFromBackground]; + NSDictionary *message = [self parseUserInfo:userInfo messageType:@"RemoteNotification" clickAction:nil openedFromTray:isFromBackground]; [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; } @@ -72,11 +76,11 @@ RCT_EXPORT_MODULE() - (void)didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { BOOL isFromBackground = (RCTSharedApplication().applicationState == UIApplicationStateInactive); - NSDictionary *message = [self parseUserInfo:userInfo clickAction:nil openedFromTray:isFromBackground]; + NSDictionary *message = [self parseUserInfo:userInfo messageType:@"RemoteNotificationHandler" clickAction:nil openedFromTray:isFromBackground]; + + [_callbackHandlers setObject:[completionHandler copy] forKey:message[@"messageId"]]; [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; - - // TODO: FetchCompletionHandler? } // Listen for permission response @@ -107,7 +111,9 @@ RCT_EXPORT_MODULE() - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler { - NSDictionary *message = [self parseUNNotification:notification openedFromTray:false]; + NSDictionary *message = [self parseUNNotification:notification messageType:@"PresentNotification" openedFromTray:false]; + + [_callbackHandlers setObject:[completionHandler copy] forKey:message[@"messageId"]]; [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; @@ -123,12 +129,11 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response #else withCompletionHandler:(void(^)())completionHandler { #endif - NSDictionary *userInfo = [self parseUNNotification:response.notification openedFromTray:true]; + NSDictionary *message = [self parseUNNotification:response.notification messageType:@"NotificationResponse" openedFromTray:true]; - [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:userInfo]; + [_callbackHandlers setObject:[completionHandler copy] forKey:message[@"messageId"]]; - // TODO: Validate this - completionHandler(); + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; } #endif @@ -210,7 +215,7 @@ RCT_EXPORT_METHOD(getBadge: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromise RCT_EXPORT_METHOD(getInitialMessage:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){ NSDictionary *notification = [self bridge].launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; if (notification) { - NSDictionary *message = [self parseUserInfo:notification clickAction:nil openedFromTray:true]; + NSDictionary *message = [self parseUserInfo:notification messageType:@"InitialMessage" clickAction:nil openedFromTray:true]; resolve(message); } else { resolve(nil); @@ -260,6 +265,60 @@ RCT_EXPORT_METHOD(unsubscribeFromTopic: (NSString*) topic) { [[FIRMessaging messaging] unsubscribeFromTopic:topic]; } +// Response handler methods + +RCT_EXPORT_METHOD(finishNotificationResponse: (NSString*) messageId) { + void(^callbackHandler)() = [_callbackHandlers objectForKey:messageId]; + if (!callbackHandler) { + NSLog(@"There is no callback handler for messageId: %@", messageId); + return; + } + callbackHandler(); + [_callbackHandlers removeObjectForKey:messageId]; +} + +RCT_EXPORT_METHOD(finishPresentNotification: (NSString*) messageId + result: (NSString*) result) { + void(^callbackHandler)(UNNotificationPresentationOptions) = [_callbackHandlers objectForKey:messageId]; + if (!callbackHandler) { + NSLog(@"There is no callback handler for messageId: %@", messageId); + return; + } + UNNotificationPresentationOptions options; + if ([result isEqualToString:@"UNNotificationPresentationOptionAll"]) { + options = UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound; + } else if ([result isEqualToString:@"UNNotificationPresentationOptionNone"]) { + options = UNNotificationPresentationOptionNone; + } else { + NSLog(@"Invalid result for PresentNotification: %@", result); + return; + } + callbackHandler(options); + [_callbackHandlers removeObjectForKey:messageId]; +} + +RCT_EXPORT_METHOD(finishRemoteNotification: (NSString*) messageId + result: (NSString*) result) { + void(^callbackHandler)(UIBackgroundFetchResult) = [_callbackHandlers objectForKey:messageId]; + if (!callbackHandler) { + NSLog(@"There is no callback handler for messageId: %@", messageId); + return; + } + UIBackgroundFetchResult fetchResult; + if ([result isEqualToString:@"UIBackgroundFetchResultNewData"]) { + fetchResult = UIBackgroundFetchResultNewData; + } else if ([result isEqualToString:@"UIBackgroundFetchResultNoData"]) { + fetchResult = UIBackgroundFetchResultNoData; + } else if ([result isEqualToString:@"UIBackgroundFetchResultFailed"]) { + fetchResult = UIBackgroundFetchResultFailed; + } else { + NSLog(@"Invalid result for PresentNotification: %@", result); + return; + } + callbackHandler(fetchResult); + [_callbackHandlers removeObjectForKey:messageId]; +} + // ** Start internals ** - (NSDictionary*)parseFIRMessagingRemoteMessage:(FIRMessagingRemoteMessage *)remoteMessage @@ -307,6 +366,7 @@ RCT_EXPORT_METHOD(unsubscribeFromTopic: (NSString*) topic) { data[k1] = appData[k1]; } } + message[@"messageType"] = @"RemoteMessage"; message[@"data"] = data; message[@"openedFromTray"] = @(false); @@ -314,14 +374,16 @@ RCT_EXPORT_METHOD(unsubscribeFromTopic: (NSString*) topic) { } - (NSDictionary*)parseUNNotification:(UNNotification *)notification + messageType:(NSString *)messageType openedFromTray:(bool)openedFromTray { NSDictionary *userInfo = notification.request.content.userInfo; NSString *clickAction = notification.request.content.categoryIdentifier; - return [self parseUserInfo:userInfo clickAction:clickAction openedFromTray:openedFromTray]; + return [self parseUserInfo:userInfo messageType:messageType clickAction:clickAction openedFromTray:openedFromTray]; } - (NSDictionary*)parseUserInfo:(NSDictionary *)userInfo + messageType:(NSString *) messageType clickAction:(NSString *) clickAction openedFromTray:(bool)openedFromTray { NSMutableDictionary *message = [[NSMutableDictionary alloc] init]; @@ -383,6 +445,12 @@ RCT_EXPORT_METHOD(unsubscribeFromTopic: (NSString*) topic) { if (!notif[@"clickAction"] && clickAction) { notif[@"clickAction"] = clickAction; } + + // Generate a message ID if one was not present in the notification + // This is used for resolving click handlers + if (!message[@"messageId"]) { + message[@"messageId"] = [[NSUUID UUID] UUIDString]; + } message[@"data"] = data; message[@"notification"] = notif; @@ -406,3 +474,4 @@ RCT_EXPORT_METHOD(unsubscribeFromTopic: (NSString*) topic) { @implementation RNFirebaseMessaging @end #endif + diff --git a/lib/modules/messaging/Message.js b/lib/modules/messaging/Message.js new file mode 100644 index 00000000..19fc8f7a --- /dev/null +++ b/lib/modules/messaging/Message.js @@ -0,0 +1,122 @@ +/** + * @flow + * Message representation wrapper + */ +import { Platform } from 'react-native'; +import { getNativeModule } from '../../utils/native'; +import { + MessageType, + RemoteNotificationResult, + WillPresentNotificationResult, +} from './types'; +import type Messaging from './'; +import type { + MessageTypeType, + NativeMessage, + Notification, + RemoteNotificationResultType, + WillPresentNotificationResultType, +} from './types'; + +/** + * @class Message + */ +export default class Message { + _finished: boolean; + _messaging: Messaging; + _message: NativeMessage; + + constructor(messaging: Messaging, message: NativeMessage) { + this._messaging = messaging; + this._message = message; + } + + get collapseKey(): ?string { + return this._message.collapseKey; + } + + get data(): { [string]: string } { + return this._message.data; + } + + get from(): ?string { + return this._message.from; + } + + get messageId(): ?string { + return this._message.messageId; + } + + get messageType(): ?MessageTypeType { + return this._message.messageType; + } + + get openedFromTray(): boolean { + return this._message.openedFromTray; + } + + get notification(): ?Notification { + return this._message.notification; + } + + get sentTime(): ?number { + return this._message.sentTime; + } + + get to(): ?string { + return this._message.to; + } + + get ttl(): ?number { + return this._message.ttl; + } + + finish( + result?: RemoteNotificationResultType | WillPresentNotificationResultType + ): void { + if (Platform.OS !== 'ios') { + return; + } + + if (!this._finished) { + this._finished = true; + + switch (this.messageType) { + case MessageType.NotificationResponse: + getNativeModule(this._messaging).finishNotificationResponse( + this.messageId + ); + break; + + case MessageType.PresentNotification: + if ( + result && + !Object.values(WillPresentNotificationResult).includes(result) + ) { + throw new Error(`Invalid WillPresentNotificationResult: ${result}`); + } + getNativeModule(this._messaging).finishPresentNotification( + this.messageId, + result || WillPresentNotificationResult.None + ); + break; + + case MessageType.RemoteNotificationHandler: + if ( + result && + !Object.values(RemoteNotificationResult).includes(result) + ) { + throw new Error(`Invalid RemoteNotificationResult: ${result}`); + } + getNativeModule(this._messaging).finishRemoteNotification( + this.messageId, + result || RemoteNotificationResult.NoData + ); + break; + + default: + break; + } + } + } +} diff --git a/lib/modules/messaging/index.js b/lib/modules/messaging/index.js index 83a48f68..d411afc4 100644 --- a/lib/modules/messaging/index.js +++ b/lib/modules/messaging/index.js @@ -8,37 +8,10 @@ import { getLogger } from '../../utils/log'; import ModuleBase from '../../utils/ModuleBase'; import { getNativeModule } from '../../utils/native'; import { isFunction, isObject } from '../../utils'; +import Message from './Message'; import type App from '../core/firebase-app'; - -type Notification = { - body: string, - bodyLocalizationArgs?: string[], - bodyLocalizationKey?: string, - clickAction?: string, - color?: string, - icon?: string, - link?: string, - sound: string, - subtitle?: string, - tag?: string, - title: string, - titleLocalizationArgs?: string[], - titleLocalizationKey?: string, -}; - -type Message = { - collapseKey?: string, - data: { [string]: string }, - from?: string, - messageId: string, - messageType?: string, - openedFromTray: boolean, - notification?: Notification, - sentTime?: number, - to?: string, - ttl?: number, -}; +import type { NativeMessage } from './types'; type OnMessage = Message => any; @@ -105,8 +78,9 @@ export default class Messaging extends ModuleBase { } onMessage(nextOrObserver: OnMessage | OnMessageObserver): () => any { - let listener; + let listener: Message => any; if (isFunction(nextOrObserver)) { + // $FlowBug: Not coping with the overloaded method signature listener = nextOrObserver; } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { listener = nextOrObserver.next; @@ -118,11 +92,18 @@ export default class Messaging extends ModuleBase { // TODO: iOS finish getLogger(this).info('Creating onMessage listener'); - SharedEventEmitter.addListener('onMessage', listener); + + const wrappedListener = async (nativeMessage: NativeMessage) => { + const message = new Message(this, nativeMessage); + await listener(message); + message.finish(); + }; + + SharedEventEmitter.addListener('onMessage', wrappedListener); return () => { getLogger(this).info('Removing onMessage listener'); - SharedEventEmitter.removeListener('onMessage', listener); + SharedEventEmitter.removeListener('onMessage', wrappedListener); }; } diff --git a/lib/modules/messaging/types.js b/lib/modules/messaging/types.js new file mode 100644 index 00000000..5a34c8af --- /dev/null +++ b/lib/modules/messaging/types.js @@ -0,0 +1,60 @@ +/** + * @flow + */ + +export const MessageType = { + InitialMessage: 'InitialMessage', + NotificationResponse: 'NotificationResponse', + PresentNotification: 'PresentNotification', + RemoteMessage: 'RemoteMessage', + RemoteNotification: 'RemoteNotification', + RemoteNotificationHandler: 'RemoteNotificationHandler', +}; + +export const RemoteNotificationResult = { + NewData: 'UIBackgroundFetchResultNewData', + NoData: 'UIBackgroundFetchResultNoData', + ResultFailed: 'UIBackgroundFetchResultFailed', +}; + +export const WillPresentNotificationResult = { + All: 'UNNotificationPresentationOptionAll', + None: 'UNNotificationPresentationOptionNone', +}; + +export type MessageTypeType = $Values; +export type RemoteNotificationResultType = $Values< + typeof RemoteNotificationResult +>; +export type WillPresentNotificationResultType = $Values< + typeof WillPresentNotificationResult +>; + +export type Notification = { + body: string, + bodyLocalizationArgs?: string[], + bodyLocalizationKey?: string, + clickAction?: string, + color?: string, + icon?: string, + link?: string, + sound: string, + subtitle?: string, + tag?: string, + title: string, + titleLocalizationArgs?: string[], + titleLocalizationKey?: string, +}; + +export type NativeMessage = { + collapseKey?: string, + data: { [string]: string }, + from?: string, + messageId: string, + messageType?: MessageTypeType, + openedFromTray: boolean, + notification?: Notification, + sentTime?: number, + to?: string, + ttl?: number, +};