diff --git a/ios/RNFirebase.xcodeproj/project.pbxproj b/ios/RNFirebase.xcodeproj/project.pbxproj index 9212415b..03109210 100644 --- a/ios/RNFirebase.xcodeproj/project.pbxproj +++ b/ios/RNFirebase.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 168785AD210B584E00E4BD57 /* RCTConvert+UIBackgroundFetchResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 168785AB210B584E00E4BD57 /* RCTConvert+UIBackgroundFetchResult.m */; }; 17AF4F6B1F59CDBF00C02336 /* RNFirebaseLinks.m in Sources */ = {isa = PBXBuildFile; fileRef = 17AF4F6A1F59CDBF00C02336 /* RNFirebaseLinks.m */; }; 27540F9A209F3641001F4AF4 /* RNFirebaseFunctions.m in Sources */ = {isa = PBXBuildFile; fileRef = 27540F99209F3641001F4AF4 /* RNFirebaseFunctions.m */; }; 8300A7AE1F31E143001B16AB /* RNFirebaseDatabaseReference.m in Sources */ = {isa = PBXBuildFile; fileRef = 8300A7AD1F31E143001B16AB /* RNFirebaseDatabaseReference.m */; }; @@ -49,6 +50,8 @@ /* Begin PBXFileReference section */ 134814201AA4EA6300B7C361 /* libRNFirebase.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNFirebase.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 168785AB210B584E00E4BD57 /* RCTConvert+UIBackgroundFetchResult.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RCTConvert+UIBackgroundFetchResult.m"; sourceTree = ""; }; + 168785AC210B584E00E4BD57 /* RCTConvert+UIBackgroundFetchResult.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RCTConvert+UIBackgroundFetchResult.h"; sourceTree = ""; }; 17AF4F691F59CDBF00C02336 /* RNFirebaseLinks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseLinks.h; sourceTree = ""; }; 17AF4F6A1F59CDBF00C02336 /* RNFirebaseLinks.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseLinks.m; sourceTree = ""; }; 27540F98209F361B001F4AF4 /* RNFirebaseFunctions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNFirebaseFunctions.h; sourceTree = ""; }; @@ -123,6 +126,16 @@ name = Products; sourceTree = ""; }; + 168785A0210B50AA00E4BD57 /* converters */ = { + isa = PBXGroup; + children = ( + 168785AC210B584E00E4BD57 /* RCTConvert+UIBackgroundFetchResult.h */, + 168785AB210B584E00E4BD57 /* RCTConvert+UIBackgroundFetchResult.m */, + ); + name = converters; + path = RNFirebase/converters; + sourceTree = ""; + }; 17AF4F681F59CDBF00C02336 /* links */ = { isa = PBXGroup; children = ( @@ -146,6 +159,7 @@ 58B511D21A9E6C8500147676 = { isa = PBXGroup; children = ( + 168785A0210B50AA00E4BD57 /* converters */, 27540F97209F35DF001F4AF4 /* functions */, 83AAA0762063DEC2007EC5F7 /* invites */, 838E372420231E15004DCD3A /* notifications */, @@ -384,6 +398,7 @@ 27540F9A209F3641001F4AF4 /* RNFirebaseFunctions.m in Sources */, 838E372320231DF0004DCD3A /* RNFirebaseInstanceId.m in Sources */, 839D916E1EF3E20B0077C7C8 /* RNFirebaseAdMobRewardedVideo.m in Sources */, + 168785AD210B584E00E4BD57 /* RCTConvert+UIBackgroundFetchResult.m in Sources */, 839D916C1EF3E20B0077C7C8 /* RNFirebaseAdMob.m in Sources */, 17AF4F6B1F59CDBF00C02336 /* RNFirebaseLinks.m in Sources */, 8376F7161F7C149100D45A85 /* RNFirebaseFirestoreCollectionReference.m in Sources */, diff --git a/ios/RNFirebase/converters/RCTConvert+UIBackgroundFetchResult.h b/ios/RNFirebase/converters/RCTConvert+UIBackgroundFetchResult.h new file mode 100644 index 00000000..e0ed190a --- /dev/null +++ b/ios/RNFirebase/converters/RCTConvert+UIBackgroundFetchResult.h @@ -0,0 +1,5 @@ +#import + +@interface RCTConvert (UIBackgroundFetchResult) + +@end diff --git a/ios/RNFirebase/converters/RCTConvert+UIBackgroundFetchResult.m b/ios/RNFirebase/converters/RCTConvert+UIBackgroundFetchResult.m new file mode 100644 index 00000000..28d78ba2 --- /dev/null +++ b/ios/RNFirebase/converters/RCTConvert+UIBackgroundFetchResult.m @@ -0,0 +1,9 @@ +#import "RCTConvert+UIBackgroundFetchResult.h" + +@implementation RCTConvert (UIBackgroundFetchResult) +RCT_ENUM_CONVERTER(UIBackgroundFetchResult, (@{ @"backgroundFetchResultNoData" : @(UIBackgroundFetchResultNoData), + @"backgroundFetchResultNewData" : @(UIBackgroundFetchResultNewData), + @"backgroundFetchResultFailed" : @(UIBackgroundFetchResultFailed)}), + UIBackgroundFetchResultNoData, integerValue) + +@end diff --git a/ios/RNFirebase/notifications/RNFirebaseNotifications.m b/ios/RNFirebase/notifications/RNFirebaseNotifications.m index 2b8a0553..f6a19e84 100644 --- a/ios/RNFirebase/notifications/RNFirebaseNotifications.m +++ b/ios/RNFirebase/notifications/RNFirebaseNotifications.m @@ -16,7 +16,9 @@ #endif @end -@implementation RNFirebaseNotifications +@implementation RNFirebaseNotifications { + NSMutableDictionary *completionHandlers; +} static RNFirebaseNotifications *theRNFirebaseNotifications = nil; // PRE-BRIDGE-EVENTS: Consider enabling this to allow events built up before the bridge is built to be sent to the JS side @@ -54,6 +56,7 @@ RCT_EXPORT_MODULE(); // Set static instance for use from AppDelegate theRNFirebaseNotifications = self; + completionHandlers = [[NSMutableDictionary alloc] init]; } // PRE-BRIDGE-EVENTS: Consider enabling this to allow events built up before the bridge is built to be sent to the JS side @@ -95,6 +98,15 @@ RCT_EXPORT_MODULE(); } } +RCT_EXPORT_METHOD(complete:(NSString*)handlerKey fetchResult:(UIBackgroundFetchResult)fetchResult) { + void (^completionHandler)(UIBackgroundFetchResult) = completionHandlers[handlerKey]; + completionHandlers[handlerKey] = nil; + + if(completionHandler != nil) { + completionHandler(fetchResult); + } +} + // Listen for background messages - (void)didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { @@ -106,6 +118,9 @@ RCT_EXPORT_MODULE(); return; } + NSDictionary *notification = [self parseUserInfo:userInfo]; + NSString *handlerKey = notification[@"notificationId"]; + NSString *event; if (RCTSharedApplication().applicationState == UIApplicationStateBackground) { event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; @@ -124,7 +139,6 @@ RCT_EXPORT_MODULE(); return; } - NSDictionary *notification = [self parseUserInfo:userInfo]; // For onOpened events, we set the default action name as iOS 8/9 has no concept of actions if (event == NOTIFICATIONS_NOTIFICATION_OPENED) { notification = @{ @@ -133,8 +147,9 @@ RCT_EXPORT_MODULE(); }; } + completionHandlers[handlerKey] = completionHandler; + [self sendJSEvent:self name:event body:notification]; - completionHandler(UIBackgroundFetchResultNoData); } // ******************************************************* @@ -226,7 +241,7 @@ RCT_EXPORT_METHOD(cancelNotification:(NSString*) notificationId if ([self isIOS89]) { for (UILocalNotification *notification in RCTSharedApplication().scheduledLocalNotifications) { NSDictionary *notificationInfo = notification.userInfo; - if ([notificationId isEqualToString:[notificationInfo valueForKey:@"notificationId"]]) { + if ([notificationId isEqualToString:notificationInfo[@"notificationId"]]) { [RCTSharedApplication() cancelLocalNotification:notification]; } } @@ -740,6 +755,12 @@ RCT_EXPORT_METHOD(jsInitialised:(RCTPromiseResolveBlock)resolve rejecter:(RCTPro return @[NOTIFICATIONS_NOTIFICATION_DISPLAYED, NOTIFICATIONS_NOTIFICATION_OPENED, NOTIFICATIONS_NOTIFICATION_RECEIVED]; } +- (NSDictionary *) constantsToExport { + return @{ @"backgroundFetchResultNoData" : @(UIBackgroundFetchResultNoData), + @"backgroundFetchResultNewData" : @(UIBackgroundFetchResultNewData), + @"backgroundFetchResultFailed" : @(UIBackgroundFetchResultFailed)}; +} + + (BOOL)requiresMainQueueSetup { return YES; diff --git a/src/index.d.ts b/src/index.d.ts index 43fd133f..9df8876b 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1141,6 +1141,9 @@ declare module 'react-native-firebase' { deleteChannel(channelId: string): Promise; } + type BackgroundFetchResultValue = string; + type CompletionHandler = (backgroundFetchResult: BackgroundFetchResultValue) => void; + interface Notifications { android: AndroidNotifications; @@ -1475,6 +1478,7 @@ declare module 'react-native-firebase' { hasAction?: boolean; launchImage?: string; threadIdentifier?: string; + complete?: CompletionHandler; addAttachment( identifier: string, diff --git a/src/modules/notifications/IOSNotification.js b/src/modules/notifications/IOSNotification.js index 2a0af539..d465ba77 100644 --- a/src/modules/notifications/IOSNotification.js +++ b/src/modules/notifications/IOSNotification.js @@ -3,11 +3,17 @@ * IOSNotification representation wrapper */ import type Notification from './Notification'; +import type Notifications from '.'; +import { type BackgroundFetchResultValue } from './IOSNotifications'; import type { IOSAttachment, IOSAttachmentOptions, NativeIOSNotification, } from './types'; +import { getLogger } from '../../utils/log'; +import { getNativeModule } from '../../utils/native'; + +type CompletionHandler = BackgroundFetchResultValue => void; export default class IOSNotification { _alertAction: string | void; @@ -31,7 +37,13 @@ export default class IOSNotification { _threadIdentifier: string | void; // N/A | threadIdentifier - constructor(notification: Notification, data?: NativeIOSNotification) { + _complete: CompletionHandler; + + constructor( + notification: Notification, + notifications: Notifications, + data?: NativeIOSNotification + ) { this._notification = notification; if (data) { @@ -44,6 +56,20 @@ export default class IOSNotification { this._threadIdentifier = data.threadIdentifier; } + const complete = (fetchResult: BackgroundFetchResultValue) => { + const { notificationId } = notification; + getLogger(notifications).debug( + `Completion handler called for notificationId=${notificationId}` + ); + getNativeModule(notifications).complete(notificationId, fetchResult); + }; + + if (notifications.ios.shouldAutoComplete) { + complete(notifications.ios.backgroundFetchResult.noData); + } else { + this._complete = complete; + } + // Defaults this._attachments = this._attachments || []; } @@ -76,6 +102,10 @@ export default class IOSNotification { return this._threadIdentifier; } + get complete(): CompletionHandler { + return this._complete; + } + /** * * @param identifier diff --git a/src/modules/notifications/IOSNotifications.js b/src/modules/notifications/IOSNotifications.js new file mode 100644 index 00000000..59c86605 --- /dev/null +++ b/src/modules/notifications/IOSNotifications.js @@ -0,0 +1,31 @@ +import { getNativeModule } from '../../utils/native'; + +import type Notifications from '.'; + +export type BackgroundFetchResultValue = string; +type BackgroundFetchResult = { + noData: BackgroundFetchResultValue, + newData: BackgroundFetchResultValue, + failure: BackgroundFetchResultValue, +}; + +export default class IOSNotifications { + _backgroundFetchResult: BackgroundFetchResult; + + shouldAutoComplete: boolean; + + constructor(notifications: Notifications) { + this.shouldAutoComplete = true; + + const nativeModule = getNativeModule(notifications); + this._backgroundFetchResult = { + noData: nativeModule.backgroundFetchResultNoData, + newData: nativeModule.backgroundFetchResultNewData, + failure: nativeModule.backgroundFetchResultFailure, + }; + } + + get backgroundFetchResult(): BackgroundFetchResult { + return { ...this._backgroundFetchResult }; + } +} diff --git a/src/modules/notifications/Notification.js b/src/modules/notifications/Notification.js index 566befd8..5d7bae0e 100644 --- a/src/modules/notifications/Notification.js +++ b/src/modules/notifications/Notification.js @@ -8,6 +8,7 @@ import IOSNotification from './IOSNotification'; import { generatePushID, isObject } from '../../utils'; import type { NativeNotification } from './types'; +import type Notifications from '.'; export type NotificationOpen = {| action: string, @@ -37,19 +38,29 @@ export default class Notification { // N/A | subtitle | subText _title: string; // alertTitle | title | contentTitle - constructor(data?: NativeNotification) { - this._android = new AndroidNotification(this, data && data.android); - this._ios = new IOSNotification(this, data && data.ios); - - if (data) { - this._body = data.body; - this._data = data.data; - this._notificationId = data.notificationId; - this._sound = data.sound; - this._subtitle = data.subtitle; - this._title = data.title; + constructor( + nativeNotification?: NativeNotification, + notifications: Notifications + ) { + if (nativeNotification) { + this._body = nativeNotification.body; + this._data = nativeNotification.data; + this._notificationId = nativeNotification.notificationId; + this._sound = nativeNotification.sound; + this._subtitle = nativeNotification.subtitle; + this._title = nativeNotification.title; } + this._android = new AndroidNotification( + this, + nativeNotification && nativeNotification.android + ); + this._ios = new IOSNotification( + this, + notifications, + nativeNotification && nativeNotification.ios + ); + // Defaults this._data = this._data || {}; // TODO: Is this the best way to generate an ID? diff --git a/src/modules/notifications/index.js b/src/modules/notifications/index.js index ec0ae608..34e4ee30 100644 --- a/src/modules/notifications/index.js +++ b/src/modules/notifications/index.js @@ -12,6 +12,7 @@ import AndroidAction from './AndroidAction'; import AndroidChannel from './AndroidChannel'; import AndroidChannelGroup from './AndroidChannelGroup'; import AndroidNotifications from './AndroidNotifications'; +import IOSNotifications from './IOSNotifications'; import AndroidRemoteInput from './AndroidRemoteInput'; import Notification from './Notification'; import { @@ -74,6 +75,8 @@ export const NAMESPACE = 'notifications'; export default class Notifications extends ModuleBase { _android: AndroidNotifications; + _ios: IOSNotifications; + constructor(app: App) { super(app, { events: NATIVE_EVENTS, @@ -83,6 +86,7 @@ export default class Notifications extends ModuleBase { namespace: NAMESPACE, }); this._android = new AndroidNotifications(this); + this._ios = new IOSNotifications(this); SharedEventEmitter.addListener( // sub to internal native event - this fans out to @@ -91,7 +95,7 @@ export default class Notifications extends ModuleBase { (notification: NativeNotification) => { SharedEventEmitter.emit( 'onNotificationDisplayed', - new Notification(notification) + new Notification(notification, this) ); } ); @@ -103,7 +107,7 @@ export default class Notifications extends ModuleBase { (notificationOpen: NativeNotificationOpen) => { SharedEventEmitter.emit('onNotificationOpened', { action: notificationOpen.action, - notification: new Notification(notificationOpen.notification), + notification: new Notification(notificationOpen.notification, this), results: notificationOpen.results, }); } @@ -116,7 +120,7 @@ export default class Notifications extends ModuleBase { (notification: NativeNotification) => { SharedEventEmitter.emit( 'onNotification', - new Notification(notification) + new Notification(notification, this) ); } ); @@ -131,6 +135,10 @@ export default class Notifications extends ModuleBase { return this._android; } + get ios(): IOSNotifications { + return this._ios; + } + /** * Cancel all notifications */ @@ -184,7 +192,7 @@ export default class Notifications extends ModuleBase { if (notificationOpen) { return { action: notificationOpen.action, - notification: new Notification(notificationOpen.notification), + notification: new Notification(notificationOpen.notification, this), results: notificationOpen.results, }; }