From d429db7e862aa841398fddc93baa6c3af6258ccc Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Tue, 30 Jan 2018 11:15:59 +0000 Subject: [PATCH 01/77] [fcm] First steps towards identifying the new API for messaging / notifications --- ios/RNFirebase.xcodeproj/project.pbxproj | 6 + ios/RNFirebase/RNFirebaseEvents.h | 3 + .../messaging/NewRNFirebaseMessaging.h | 23 +++ .../messaging/NewRNFirebaseMessaging.m | 132 +++++++++++++ lib/modules/core/firebase-app.js | 9 + lib/modules/core/firebase.js | 11 ++ lib/modules/messaging/messagingIndex.js | 186 ++++++++++++++++++ .../notifications/notificationsIndex.js | 138 +++++++++++++ lib/types/index.js | 11 ++ tests/ios/Podfile.lock | 4 +- tests/src/firebase.js | 14 ++ 11 files changed, 535 insertions(+), 2 deletions(-) create mode 100644 ios/RNFirebase/messaging/NewRNFirebaseMessaging.h create mode 100644 ios/RNFirebase/messaging/NewRNFirebaseMessaging.m create mode 100644 lib/modules/messaging/messagingIndex.js create mode 100644 lib/modules/notifications/notificationsIndex.js diff --git a/ios/RNFirebase.xcodeproj/project.pbxproj b/ios/RNFirebase.xcodeproj/project.pbxproj index 92df426e..bc43b797 100644 --- a/ios/RNFirebase.xcodeproj/project.pbxproj +++ b/ios/RNFirebase.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 8376F7141F7C149100D45A85 /* RNFirebaseFirestoreDocumentReference.m in Sources */ = {isa = PBXBuildFile; fileRef = 8376F70E1F7C149000D45A85 /* RNFirebaseFirestoreDocumentReference.m */; }; 8376F7151F7C149100D45A85 /* RNFirebaseFirestore.m in Sources */ = {isa = PBXBuildFile; fileRef = 8376F7101F7C149000D45A85 /* RNFirebaseFirestore.m */; }; 8376F7161F7C149100D45A85 /* RNFirebaseFirestoreCollectionReference.m in Sources */ = {isa = PBXBuildFile; fileRef = 8376F7111F7C149000D45A85 /* RNFirebaseFirestoreCollectionReference.m */; }; + 838E36FE201B9169004DCD3A /* NewRNFirebaseMessaging.m in Sources */ = {isa = PBXBuildFile; fileRef = 838E36FD201B9169004DCD3A /* NewRNFirebaseMessaging.m */; }; 839D916C1EF3E20B0077C7C8 /* RNFirebaseAdMob.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D914F1EF3E20A0077C7C8 /* RNFirebaseAdMob.m */; }; 839D916D1EF3E20B0077C7C8 /* RNFirebaseAdMobInterstitial.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D91511EF3E20A0077C7C8 /* RNFirebaseAdMobInterstitial.m */; }; 839D916E1EF3E20B0077C7C8 /* RNFirebaseAdMobRewardedVideo.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D91531EF3E20A0077C7C8 /* RNFirebaseAdMobRewardedVideo.m */; }; @@ -66,6 +67,8 @@ 8376F7111F7C149000D45A85 /* RNFirebaseFirestoreCollectionReference.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseFirestoreCollectionReference.m; sourceTree = ""; }; 8376F7121F7C149000D45A85 /* RNFirebaseFirestoreDocumentReference.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseFirestoreDocumentReference.h; sourceTree = ""; }; 8376F7131F7C149000D45A85 /* RNFirebaseFirestoreCollectionReference.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseFirestoreCollectionReference.h; sourceTree = ""; }; + 838E36FC201B9169004DCD3A /* NewRNFirebaseMessaging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NewRNFirebaseMessaging.h; sourceTree = ""; }; + 838E36FD201B9169004DCD3A /* NewRNFirebaseMessaging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NewRNFirebaseMessaging.m; sourceTree = ""; }; 839D914E1EF3E20A0077C7C8 /* RNFirebaseAdMob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseAdMob.h; sourceTree = ""; }; 839D914F1EF3E20A0077C7C8 /* RNFirebaseAdMob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseAdMob.m; sourceTree = ""; }; 839D91501EF3E20A0077C7C8 /* RNFirebaseAdMobInterstitial.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseAdMobInterstitial.h; sourceTree = ""; }; @@ -256,6 +259,8 @@ 839D91631EF3E20A0077C7C8 /* messaging */ = { isa = PBXGroup; children = ( + 838E36FC201B9169004DCD3A /* NewRNFirebaseMessaging.h */, + 838E36FD201B9169004DCD3A /* NewRNFirebaseMessaging.m */, 839D91641EF3E20A0077C7C8 /* RNFirebaseMessaging.h */, 839D91651EF3E20A0077C7C8 /* RNFirebaseMessaging.m */, ); @@ -343,6 +348,7 @@ 839D916C1EF3E20B0077C7C8 /* RNFirebaseAdMob.m in Sources */, 17AF4F6B1F59CDBF00C02336 /* RNFirebaseLinks.m in Sources */, 8376F7161F7C149100D45A85 /* RNFirebaseFirestoreCollectionReference.m in Sources */, + 838E36FE201B9169004DCD3A /* NewRNFirebaseMessaging.m in Sources */, 8376F7151F7C149100D45A85 /* RNFirebaseFirestore.m in Sources */, 839D91701EF3E20B0077C7C8 /* RNFirebaseAuth.m in Sources */, 8323CF091F6FBD870071420B /* RNFirebaseAdMobNativeExpressManager.m in Sources */, diff --git a/ios/RNFirebase/RNFirebaseEvents.h b/ios/RNFirebase/RNFirebaseEvents.h index d75c875b..26ce5f3e 100644 --- a/ios/RNFirebase/RNFirebaseEvents.h +++ b/ios/RNFirebase/RNFirebaseEvents.h @@ -32,7 +32,10 @@ static NSString *const STORAGE_DOWNLOAD_SUCCESS = @"download_success"; static NSString *const STORAGE_DOWNLOAD_FAILURE = @"download_failure"; // Messaging +static NSString *const MESSAGING_MESSAGE_RECEIVED = @"messaging_message_received"; static NSString *const MESSAGING_TOKEN_REFRESHED = @"messaging_token_refreshed"; + +// TODO: Remove static NSString *const MESSAGING_NOTIFICATION_RECEIVED = @"messaging_notification_received"; // AdMob diff --git a/ios/RNFirebase/messaging/NewRNFirebaseMessaging.h b/ios/RNFirebase/messaging/NewRNFirebaseMessaging.h new file mode 100644 index 00000000..53511103 --- /dev/null +++ b/ios/RNFirebase/messaging/NewRNFirebaseMessaging.h @@ -0,0 +1,23 @@ +#ifndef NewRNFirebaseMessaging_h +#define NewRNFirebaseMessaging_h +#import + +#if __has_include() +#import +#import +#import + +@interface NewRNFirebaseMessaging : RCTEventEmitter + +#if !TARGET_OS_TV + +#endif + +@end + +#else +@interface NewRNFirebaseMessaging : NSObject +@end +#endif + +#endif diff --git a/ios/RNFirebase/messaging/NewRNFirebaseMessaging.m b/ios/RNFirebase/messaging/NewRNFirebaseMessaging.m new file mode 100644 index 00000000..0880b834 --- /dev/null +++ b/ios/RNFirebase/messaging/NewRNFirebaseMessaging.m @@ -0,0 +1,132 @@ +#import "NewRNFirebaseMessaging.h" + +#if __has_include() +@import UserNotifications; +#import "RNFirebaseEvents.h" +#import "RNFirebaseUtil.h" +#import +#import + +#import +#import +#import + +@interface NewRNFirebaseMessaging () + +@end + +@implementation NewRNFirebaseMessaging + +RCT_EXPORT_MODULE() + +- (id)init { + self = [super init]; + if (self != nil) { + NSLog(@"Setting up RNFirebaseMessaging instance"); + [self initialiseMessaging]; + } + return self; +} + +- (void)initialiseMessaging { + // Establish Firebase managed data channel + [FIRMessaging messaging].shouldEstablishDirectChannel = YES; +} + +- (void)dealloc { + +} + +// ** Start React Module methods ** +RCT_EXPORT_METHOD(getToken:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve([[FIRInstanceID instanceID] token]); +} + +RCT_EXPORT_METHOD(requestPermission:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + if (RCTRunningInAppExtension()) { + reject(@"request_permission_unavailable", @"requestPermission is not supported in App Extensions", nil); + return; + } + + if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { + UIUserNotificationType types = (UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge); + [RCTSharedApplication() registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:types categories:nil]]; + // Unfortunately on iOS 9 or below, there's no way to tell whether the user accepted or + // rejected the permissions popup + // TODO: Is there something we can listen for? + resolve(@{@"status":@"unknown"}); + } else { + // iOS 10 or later + #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + // For iOS 10 display notification (sent via APNS) + UNAuthorizationOptions authOptions = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge; + [[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:authOptions completionHandler:^(BOOL granted, NSError * _Nullable error) { + if (granted) { + resolve(@{@"status": @"granted"}); + } else { + reject(@"permission_error", @"Failed to grant permission", error); + } + }]; + #endif + } + + dispatch_async(dispatch_get_main_queue(), ^{ + [RCTSharedApplication() registerForRemoteNotifications]; + }); +} + +// Non Web SDK methods + +RCT_EXPORT_METHOD(deleteInstanceId:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [[FIRInstanceID instanceID] deleteIDWithHandler:^(NSError * _Nullable error) { + if (!error) { + resolve(nil); + } else { + reject(@"instance_id_error", @"Failed to delete instance id", error); + } + }]; +} + +RCT_EXPORT_METHOD(getBadgeNumber: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve(@([RCTSharedApplication() applicationIconBadgeNumber])); +} + +RCT_EXPORT_METHOD(getInitialNotification:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){ + UILocalNotification *localUserInfo = [self bridge].launchOptions[UIApplicationLaunchOptionsLocalNotificationKey]; + if (localUserInfo) { + resolve([[localUserInfo userInfo] copy]); + } else { + resolve([[self bridge].launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey] copy]); + } +} + +RCT_EXPORT_METHOD(subscribeToTopic: (NSString*) topic) { + [[FIRMessaging messaging] subscribeToTopic:topic]; +} + +RCT_EXPORT_METHOD(unsubscribeFromTopic: (NSString*) topic) { + [[FIRMessaging messaging] unsubscribeFromTopic:topic]; +} + + +RCT_EXPORT_METHOD(setBadgeNumber: (NSInteger*) number) { + dispatch_async(dispatch_get_main_queue(), ^{ + [RCTSharedApplication() setApplicationIconBadgeNumber:*number]; + }); +} + +- (NSArray *)supportedEvents { + return @[MESSAGING_MESSAGE_RECEIVED, MESSAGING_TOKEN_REFRESHED]; +} + ++ (BOOL)requiresMainQueueSetup +{ + return YES; +} + +@end + +#else +@implementation NewRNFirebaseMessaging +@end +#endif diff --git a/lib/modules/core/firebase-app.js b/lib/modules/core/firebase-app.js index 1f81e6aa..0c79517f 100644 --- a/lib/modules/core/firebase-app.js +++ b/lib/modules/core/firebase-app.js @@ -20,6 +20,9 @@ import Database, { NAMESPACE as DatabaseNamespace } from '../database'; import Firestore, { NAMESPACE as FirestoreNamespace } from '../firestore'; import Links, { NAMESPACE as LinksNamespace } from '../links'; import Messaging, { NAMESPACE as MessagingNamespace } from '../messaging'; +import NewMessaging, { + NAMESPACE as NewMessagingNamespace, +} from '../messaging/messagingIndex'; import Performance, { NAMESPACE as PerfNamespace } from '../perf'; import Storage, { NAMESPACE as StorageNamespace } from '../storage'; import Utils, { NAMESPACE as UtilsNamespace } from '../utils'; @@ -46,6 +49,7 @@ export default class App { firestore: () => Firestore; links: () => Links; messaging: () => Messaging; + newmessaging: () => NewMessaging; perf: () => Performance; storage: () => Storage; utils: () => Utils; @@ -85,6 +89,11 @@ export default class App { this.firestore = APPS.appModule(this, FirestoreNamespace, Firestore); this.links = APPS.appModule(this, LinksNamespace, Links); this.messaging = APPS.appModule(this, MessagingNamespace, Messaging); + this.newmessaging = APPS.appModule( + this, + NewMessagingNamespace, + NewMessaging + ); this.perf = APPS.appModule(this, PerfNamespace, Performance); this.storage = APPS.appModule(this, StorageNamespace, Storage); this.utils = APPS.appModule(this, UtilsNamespace, Utils); diff --git a/lib/modules/core/firebase.js b/lib/modules/core/firebase.js index 13aad29d..119d9fc6 100644 --- a/lib/modules/core/firebase.js +++ b/lib/modules/core/firebase.js @@ -47,6 +47,10 @@ import { statics as MessagingStatics, MODULE_NAME as MessagingModuleName, } from '../messaging'; +import { + statics as NewMessagingStatics, + MODULE_NAME as NewMessagingModuleName, +} from '../messaging/messagingIndex'; import { statics as PerformanceStatics, MODULE_NAME as PerfModuleName, @@ -72,6 +76,7 @@ import type { FirestoreModule, LinksModule, MessagingModule, + NewMessagingModule, PerformanceModule, StorageModule, UtilsModule, @@ -90,6 +95,7 @@ class Firebase { firestore: FirestoreModule; links: LinksModule; messaging: MessagingModule; + newmessaging: NewMessagingModule; perf: PerformanceModule; storage: StorageModule; utils: UtilsModule; @@ -137,6 +143,11 @@ class Firebase { MessagingStatics, MessagingModuleName ); + this.newmessaging = APPS.moduleAndStatics( + 'newmessaging', + NewMessagingStatics, + NewMessagingModuleName + ); this.perf = APPS.moduleAndStatics( 'perf', PerformanceStatics, diff --git a/lib/modules/messaging/messagingIndex.js b/lib/modules/messaging/messagingIndex.js new file mode 100644 index 00000000..9d7f4247 --- /dev/null +++ b/lib/modules/messaging/messagingIndex.js @@ -0,0 +1,186 @@ +/** + * @flow + * Messaging (FCM) representation wrapper + */ +import { SharedEventEmitter } from '../../utils/events'; +import INTERNALS from '../../utils/internals'; +import { getLogger } from '../../utils/log'; +import ModuleBase from '../../utils/ModuleBase'; +import { getNativeModule } from '../../utils/native'; +import { isFunction, isObject } from '../../utils'; + +import type App from '../core/firebase-app'; + +type Message = { + // TODO +}; + +type OnMessage = Message => any; + +type OnMessageObserver = { + next: OnMessage, +}; + +type OnTokenRefresh = String => any; + +type OnTokenRefreshObserver = { + next: OnTokenRefresh, +}; + +type RemoteMessage = { + // TODO +}; + +const NATIVE_EVENTS = [ + 'messaging_message_received', + 'messaging_token_refreshed', +]; + +export const MODULE_NAME = 'NewRNFirebaseMessaging'; +export const NAMESPACE = 'newmessaging'; + +/** + * @class Messaging + */ +export default class Messaging extends ModuleBase { + constructor(app: App) { + super(app, { + events: NATIVE_EVENTS, + moduleName: MODULE_NAME, + multiApp: false, + namespace: NAMESPACE, + }); + + SharedEventEmitter.addListener( + // sub to internal native event - this fans out to + // public event name: onMessage + 'messaging_message_received', + (message: Message) => { + SharedEventEmitter.emit('onMessage', message); + } + ); + + SharedEventEmitter.addListener( + // sub to internal native event - this fans out to + // public event name: onMessage + 'messaging_token_refreshed', + (token: string) => { + SharedEventEmitter.emit('onTokenRefresh', token); + } + ); + } + + deleteToken(token: string): Promise { + return getNativeModule(this).deleteToken(token); + } + + getToken(): Promise { + return getNativeModule(this).getToken(); + } + + onMessage(nextOrObserver: OnMessage | OnMessageObserver): () => any { + let listener; + if (isFunction(nextOrObserver)) { + listener = nextOrObserver; + } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { + listener = nextOrObserver.next; + } else { + throw new Error( + 'Messaging.onMessage failed: First argument must be a function or observer object with a `next` function.' + ); + } + + // TODO: iOS finish + getLogger(this).info('Creating onMessage listener'); + SharedEventEmitter.addListener('onMessage', listener); + + return () => { + getLogger(this).info('Removing onMessage listener'); + SharedEventEmitter.removeListener('onMessage', listener); + }; + } + + onTokenRefresh( + nextOrObserver: OnTokenRefresh | OnTokenRefreshObserver + ): () => any { + let listener; + if (isFunction(nextOrObserver)) { + listener = nextOrObserver; + } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { + listener = nextOrObserver.next; + } else { + throw new Error( + 'Messaging.OnTokenRefresh failed: First argument must be a function or observer object with a `next` function.' + ); + } + + getLogger(this).info('Creating onTokenRefresh listener'); + SharedEventEmitter.addListener('onTokenRefresh', listener); + + return () => { + getLogger(this).info('Removing onTokenRefresh listener'); + SharedEventEmitter.removeListener('onTokenRefresh', listener); + }; + } + + requestPermission(): Promise { + return getNativeModule(this).requestPermission(); + } + + /** + * NON WEB-SDK METHODS + */ + deleteInstanceId(): Promise { + return getNativeModule(this).deleteInstanceId(); + } + + getBadgeNumber(): Promise { + return getNativeModule(this).getBadgeNumber(); + } + + getInitialMessage(): Promise { + return getNativeModule(this).getInitialMessage(); + } + + sendMessage(remoteMessage: RemoteMessage): Promise { + return getNativeModule(this).send(remoteMessage); + } + + setBadgeNumber(badge: number): void { + getNativeModule(this).setBadgeNumber(badge); + } + + subscribeToTopic(topic: string): void { + getNativeModule(this).subscribeToTopic(topic); + } + + unsubscribeFromTopic(topic: string): void { + getNativeModule(this).unsubscribeFromTopic(topic); + } + + /** + * KNOWN UNSUPPORTED METHODS + */ + + setBackgroundMessageHandler() { + throw new Error( + INTERNALS.STRINGS.ERROR_UNSUPPORTED_MODULE_METHOD( + 'messaging', + 'setBackgroundMessageHandler' + ) + ); + } + + useServiceWorker() { + throw new Error( + INTERNALS.STRINGS.ERROR_UNSUPPORTED_MODULE_METHOD( + 'messaging', + 'useServiceWorker' + ) + ); + } +} + +export const statics = { + // RemoteMessage, +}; diff --git a/lib/modules/notifications/notificationsIndex.js b/lib/modules/notifications/notificationsIndex.js new file mode 100644 index 00000000..9620292f --- /dev/null +++ b/lib/modules/notifications/notificationsIndex.js @@ -0,0 +1,138 @@ +/** + * @flow + * Messaging (FCM) representation wrapper + */ +import { SharedEventEmitter } from '../../utils/events'; +import { getLogger } from '../../utils/log'; +import ModuleBase from '../../utils/ModuleBase'; +import { getNativeModule } from '../../utils/native'; +import { isFunction, isObject } from '../../utils'; + +import type App from '../core/firebase-app'; + +type CreateNotification = { + // TODO +}; + +type Notification = { + // TODO +}; + +type OnNotification = Notification => any; + +type OnNotificationObserver = { + next: OnNotification, +}; + +const NATIVE_EVENTS = ['notifications_notification_received']; + +export const MODULE_NAME = 'RNFirebaseNotifications'; +export const NAMESPACE = 'notifications'; + +/** + * @class Notifications + */ +export default class Notifications extends ModuleBase { + constructor(app: App) { + super(app, { + events: NATIVE_EVENTS, + moduleName: MODULE_NAME, + multiApp: false, + namespace: NAMESPACE, + }); + + SharedEventEmitter.addListener( + // sub to internal native event - this fans out to + // public event name: onMessage + 'notifications_notification_received', + (notification: Notification) => { + SharedEventEmitter.emit('onNotification', notification); + } + ); + } + + /** + * Cancel a local notification by id - using '*' will cancel + * all local notifications. + * @param id + * @returns {*} + */ + cancelNotification(id: string): Promise { + if (!id) return Promise.reject(new Error('Missing notification id')); + if (id === '*') return getNativeModule(this).cancelAllLocalNotifications(); + return getNativeModule(this).cancelLocalNotification(id); + } + + /** + * Create and display a local notification + * @param notification + * @returns {*} + */ + createNotification(notification: CreateNotification): Promise { + const _notification = Object.assign({}, notification); + _notification.id = _notification.id || new Date().getTime().toString(); + _notification.local_notification = true; + return getNativeModule(this).createLocalNotification(_notification); + } + + /** + * Returns an array of all scheduled notifications + * @returns {Promise.} + */ + getScheduledNotifications(): Promise { + return getNativeModule(this).getScheduledLocalNotifications(); + } + + onNotification( + nextOrObserver: OnNotification | OnNotificationObserver + ): () => any { + let listener; + if (isFunction(nextOrObserver)) { + listener = nextOrObserver; + } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { + listener = nextOrObserver.next; + } else { + throw new Error( + 'Notifications.onNotification failed: First argument must be a function or observer object with a `next` function.' + ); + } + + // TODO: iOS finish + getLogger(this).info('Creating onNotification listener'); + SharedEventEmitter.addListener('onNotification', listener); + + return () => { + getLogger(this).info('Removing onNotification listener'); + SharedEventEmitter.removeListener('onNotification', listener); + }; + } + + /** + * Remove a delivered notification - using '*' will remove + * all delivered notifications. + * @param id + * @returns {*} + */ + removeDeliveredNotification(id: string): Promise { + if (!id) return Promise.reject(new Error('Missing notification id')); + if (id === '*') { + return getNativeModule(this).removeAllDeliveredNotifications(); + } + return getNativeModule(this).removeDeliveredNotification(id); + } + + /** + * + * @param notification + * @returns {*} + */ + scheduleNotification(notification: CreateNotification): Promise { + const _notification = Object.assign({}, notification); + if (!notification.id) + return Promise.reject( + new Error('An id is required to schedule a local notification.') + ); + _notification.local_notification = true; + return getNativeModule(this).scheduleLocalNotification(_notification); + } +} diff --git a/lib/types/index.js b/lib/types/index.js index 0308f711..ce894079 100644 --- a/lib/types/index.js +++ b/lib/types/index.js @@ -19,6 +19,8 @@ import type Links from '../modules/links'; import { typeof statics as LinksStatics } from '../modules/links'; import type Messaging from '../modules/messaging'; import { typeof statics as MessagingStatics } from '../modules/messaging'; +import type NewMessaging from '../modules/messaging/messagingIndex'; +import { typeof statics as NewMessagingStatics } from '../modules/messaging/messagingIndex'; import type ModuleBase from '../utils/ModuleBase'; import type Performance from '../modules/perf'; import { typeof statics as PerformanceStatics } from '../modules/perf'; @@ -58,6 +60,8 @@ export type FirebaseModuleName = | 'RNFirebaseFirestore' | 'RNFirebaseLinks' | 'RNFirebaseMessaging' + | 'NewRNFirebaseMessaging' + | 'RNFirebaseNotifications' | 'RNFirebasePerformance' | 'RNFirebaseStorage' | 'RNFirebaseUtils'; @@ -73,6 +77,8 @@ export type FirebaseNamespace = | 'firestore' | 'links' | 'messaging' + | 'newmessaging' + | 'notifications' | 'perf' | 'storage' | 'utils'; @@ -217,6 +223,11 @@ export type MessagingModule = { nativeModuleExists: boolean, } & MessagingStatics; +export type NewMessagingModule = { + (): NewMessaging, + nativeModuleExists: boolean, +} & NewMessagingStatics; + /* Performance types */ export type PerformanceModule = { diff --git a/tests/ios/Podfile.lock b/tests/ios/Podfile.lock index 480652fa..c964fec5 100644 --- a/tests/ios/Podfile.lock +++ b/tests/ios/Podfile.lock @@ -164,7 +164,7 @@ PODS: - React/Core - React/fishhook - React/RCTBlob - - RNFirebase (3.2.0): + - RNFirebase (3.2.2): - React - yoga (0.52.0.React) @@ -228,7 +228,7 @@ SPEC CHECKSUMS: nanopb: 5601e6bca2dbf1ed831b519092ec110f66982ca3 Protobuf: 8a9838fba8dae3389230e1b7f8c104aa32389c03 React: 61a6bdf17a9ff16875c230e6ff278d9de274e16c - RNFirebase: 22b1917fec663706907bc901ed665ac4f8b9bfd6 + RNFirebase: 5cf5405d1b67c9720ce63a2da3d6d5346415d0f7 yoga: 646606bf554d54a16711f35596178522fbc00480 PODFILE CHECKSUM: 67c98bcb203cb992da590bcab6f690f727653ca5 diff --git a/tests/src/firebase.js b/tests/src/firebase.js index c98816fd..f78cfe1d 100644 --- a/tests/src/firebase.js +++ b/tests/src/firebase.js @@ -7,6 +7,20 @@ import DatabaseContents from './tests/support/DatabaseContents'; RNfirebase.database.enableLogging(true); RNfirebase.firestore.enableLogging(true); +RNfirebase.newmessaging() + .requestPermission() + .then(response => { + console.log('requestPermission:', response); + RNfirebase.newmessaging() + .getToken() + .then(token => { + console.log('token: ', token); + }); + }) + .catch(error => { + console.error('requestPermission:', error); + }); + const config = { apiKey: 'AIzaSyDnVqNhxU0Biit9nCo4RorAh5ulQQwko3E', authDomain: 'rnfirebase-b9ad4.firebaseapp.com', From 850f04914f53f689e1e10ccc7f9fb5568a291373 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 2 Feb 2018 08:40:48 +0000 Subject: [PATCH 02/77] [fcm] Android instanceid and core fcm support; iOS instance and basic fcm support --- android/build.gradle | 2 +- .../instanceid/RNFirebaseInstanceId.java | 45 ++ .../RNFirebaseInstanceIdPackage.java | 38 ++ .../firebase/messaging/BadgeHelper.java | 43 -- .../firebase/messaging/InstanceIdService.java | 32 -- .../firebase/messaging/MessagingService.java | 64 --- .../RNFirebaseInstanceIdService.java | 24 + .../messaging/RNFirebaseMessaging.java | 468 +++++++++--------- .../messaging/RNFirebaseMessagingService.java | 25 + ios/RNFirebase.xcodeproj/project.pbxproj | 48 +- .../instanceid/RNFirebaseInstanceId.h | 19 + .../instanceid/RNFirebaseInstanceId.m | 35 ++ .../messaging/NewRNFirebaseMessaging.h | 23 - .../messaging/NewRNFirebaseMessaging.m | 132 ----- .../messaging/RNFirebaseMessaging.h | 14 +- .../messaging/RNFirebaseMessaging.m | 447 ++--------------- .../notifications/RNFirebaseNotifications.h | 22 + .../notifications/RNFirebaseNotifications.m | 14 + lib/modules/core/firebase-app.js | 17 +- lib/modules/core/firebase.js | 29 +- lib/modules/instanceid/index.js | 31 ++ lib/modules/messaging/index.js | 388 ++++++--------- lib/modules/messaging/messagingIndex.js | 186 ------- .../{notificationsIndex.js => index.js} | 17 +- lib/types/index.js | 25 +- .../android/app/src/main/AndroidManifest.xml | 8 +- .../MainApplication.java | 2 + tests/src/firebase.js | 37 +- 28 files changed, 824 insertions(+), 1411 deletions(-) create mode 100644 android/src/main/java/io/invertase/firebase/instanceid/RNFirebaseInstanceId.java create mode 100644 android/src/main/java/io/invertase/firebase/instanceid/RNFirebaseInstanceIdPackage.java delete mode 100644 android/src/main/java/io/invertase/firebase/messaging/BadgeHelper.java delete mode 100644 android/src/main/java/io/invertase/firebase/messaging/InstanceIdService.java delete mode 100644 android/src/main/java/io/invertase/firebase/messaging/MessagingService.java create mode 100644 android/src/main/java/io/invertase/firebase/messaging/RNFirebaseInstanceIdService.java create mode 100644 android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessagingService.java create mode 100644 ios/RNFirebase/instanceid/RNFirebaseInstanceId.h create mode 100644 ios/RNFirebase/instanceid/RNFirebaseInstanceId.m delete mode 100644 ios/RNFirebase/messaging/NewRNFirebaseMessaging.h delete mode 100644 ios/RNFirebase/messaging/NewRNFirebaseMessaging.m create mode 100644 ios/RNFirebase/notifications/RNFirebaseNotifications.h create mode 100644 ios/RNFirebase/notifications/RNFirebaseNotifications.m create mode 100644 lib/modules/instanceid/index.js delete mode 100644 lib/modules/messaging/messagingIndex.js rename lib/modules/notifications/{notificationsIndex.js => index.js} (93%) diff --git a/android/build.gradle b/android/build.gradle index 5de53c85..ba96df94 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -82,7 +82,7 @@ rootProject.gradle.buildFinished { buildResult -> dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') compile "com.facebook.react:react-native:+" // From node_modules - compile 'me.leolin:ShortcutBadger:1.1.18@aar' + compile 'me.leolin:ShortcutBadger:1.1.21@aar' compile "com.google.android.gms:play-services-base:$firebaseVersion" compile "com.google.firebase:firebase-core:$firebaseVersion" compile "com.google.firebase:firebase-config:$firebaseVersion" diff --git a/android/src/main/java/io/invertase/firebase/instanceid/RNFirebaseInstanceId.java b/android/src/main/java/io/invertase/firebase/instanceid/RNFirebaseInstanceId.java new file mode 100644 index 00000000..b95b5126 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/instanceid/RNFirebaseInstanceId.java @@ -0,0 +1,45 @@ +package io.invertase.firebase.instanceid; + + +import android.util.Log; + +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.google.firebase.iid.FirebaseInstanceId; + +import java.io.IOException; + +public class RNFirebaseInstanceId extends ReactContextBaseJavaModule { + + private static final String TAG = "RNFirebaseInstanceId"; + + public RNFirebaseInstanceId(ReactApplicationContext reactContext) { + super(reactContext); + Log.d(TAG, "New instance"); + } + + @Override + public String getName() { + return TAG; + } + + @ReactMethod + public void delete(Promise promise){ + try { + Log.d(TAG, "Deleting instance id"); + FirebaseInstanceId.getInstance().deleteInstanceId(); + promise.resolve(null); + } catch (IOException e) { + Log.e(TAG, e.getMessage()); + promise.reject("instance_id_error", e.getMessage()); + } + } + + @ReactMethod + public void get(Promise promise){ + String id = FirebaseInstanceId.getInstance().getId(); + promise.resolve(id); + } +} diff --git a/android/src/main/java/io/invertase/firebase/instanceid/RNFirebaseInstanceIdPackage.java b/android/src/main/java/io/invertase/firebase/instanceid/RNFirebaseInstanceIdPackage.java new file mode 100644 index 00000000..2d635dc6 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/instanceid/RNFirebaseInstanceIdPackage.java @@ -0,0 +1,38 @@ +package io.invertase.firebase.instanceid; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@SuppressWarnings("unused") +public class RNFirebaseInstanceIdPackage implements ReactPackage { + public RNFirebaseInstanceIdPackage() { + } + + /** + * @param reactContext react application context that can be used to create modules + * @return list of native modules to register with the newly created catalyst instance + */ + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + modules.add(new RNFirebaseInstanceId(reactContext)); + + return modules; + } + + /** + * @param reactContext + * @return a list of view managers that should be registered with {@link UIManagerModule} + */ + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} diff --git a/android/src/main/java/io/invertase/firebase/messaging/BadgeHelper.java b/android/src/main/java/io/invertase/firebase/messaging/BadgeHelper.java deleted file mode 100644 index ba88bc7e..00000000 --- a/android/src/main/java/io/invertase/firebase/messaging/BadgeHelper.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.invertase.firebase.messaging; - -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; - -import me.leolin.shortcutbadger.ShortcutBadger; - -public class BadgeHelper { - - private static final String TAG = "BadgeHelper"; - private static final String PREFERENCES_FILE = "BadgeCountFile"; - private static final String BADGE_COUNT_KEY = "BadgeCount"; - - private Context mContext; - private SharedPreferences sharedPreferences = null; - - public BadgeHelper(Context context) { - mContext = context; - sharedPreferences = (SharedPreferences) mContext.getSharedPreferences(PREFERENCES_FILE, Context.MODE_PRIVATE); - } - - public int getBadgeCount() { - return sharedPreferences.getInt(BADGE_COUNT_KEY, 0); - } - - public void setBadgeCount(int badgeCount) { - storeBadgeCount(badgeCount); - if (badgeCount == 0) { - ShortcutBadger.removeCount(mContext); - Log.d(TAG, "Remove count"); - } else { - ShortcutBadger.applyCount(mContext, badgeCount); - Log.d(TAG, "Apply count: " + badgeCount); - } - } - - private void storeBadgeCount(int badgeCount) { - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putInt(BADGE_COUNT_KEY, badgeCount); - editor.apply(); - } -} diff --git a/android/src/main/java/io/invertase/firebase/messaging/InstanceIdService.java b/android/src/main/java/io/invertase/firebase/messaging/InstanceIdService.java deleted file mode 100644 index 9e487be3..00000000 --- a/android/src/main/java/io/invertase/firebase/messaging/InstanceIdService.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.invertase.firebase.messaging; - -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; - -import com.google.firebase.iid.FirebaseInstanceId; -import com.google.firebase.iid.FirebaseInstanceIdService; - -public class InstanceIdService extends FirebaseInstanceIdService { - - private static final String TAG = "InstanceIdService"; - - /** - * Called if InstanceID token is updated. This may occur if the security of - * the previous token had been compromised. This call is initiated by the - * InstanceID provider. - */ - @Override - public void onTokenRefresh() { - // Get updated InstanceID token. - String refreshedToken = FirebaseInstanceId.getInstance().getToken(); - Log.d(TAG, "Refreshed token: " + refreshedToken); - - // Broadcast refreshed token - Intent i = new Intent("io.invertase.firebase.messaging.FCMRefreshToken"); - Bundle bundle = new Bundle(); - bundle.putString("token", refreshedToken); - i.putExtras(bundle); - sendBroadcast(i); - } -} diff --git a/android/src/main/java/io/invertase/firebase/messaging/MessagingService.java b/android/src/main/java/io/invertase/firebase/messaging/MessagingService.java deleted file mode 100644 index 9b361e25..00000000 --- a/android/src/main/java/io/invertase/firebase/messaging/MessagingService.java +++ /dev/null @@ -1,64 +0,0 @@ -package io.invertase.firebase.messaging; - -import java.util.Map; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; - -import com.google.firebase.messaging.FirebaseMessagingService; -import com.google.firebase.messaging.RemoteMessage; - -import org.json.JSONException; -import org.json.JSONObject; - -public class MessagingService extends FirebaseMessagingService { - - private static final String TAG = "MessagingService"; - - @Override - public void onMessageReceived(RemoteMessage remoteMessage) { - Log.d(TAG, "Remote message received"); - Intent i = new Intent("io.invertase.firebase.messaging.ReceiveNotification"); - i.putExtra("data", remoteMessage); - handleBadge(remoteMessage); - buildLocalNotification(remoteMessage); - sendOrderedBroadcast(i, null); - } - - private void handleBadge(RemoteMessage remoteMessage) { - BadgeHelper badgeHelper = new BadgeHelper(this); - if (remoteMessage.getData() == null) { - return; - } - - Map data = remoteMessage.getData(); - if (data.get("badge") == null) { - return; - } - - try { - int badgeCount = Integer.parseInt((String)data.get("badge")); - badgeHelper.setBadgeCount(badgeCount); - } catch (Exception e) { - Log.e(TAG, "Badge count needs to be an integer", e); - } - } - - private void buildLocalNotification(RemoteMessage remoteMessage) { - if(remoteMessage.getData() == null){ - return; - } - Map data = remoteMessage.getData(); - String customNotification = data.get("custom_notification"); - if(customNotification != null){ - try { - Bundle bundle = BundleJSONConverter.convertToBundle(new JSONObject(customNotification)); - RNFirebaseLocalMessagingHelper helper = new RNFirebaseLocalMessagingHelper(this.getApplication()); - helper.sendNotification(bundle); - } catch (JSONException e) { - e.printStackTrace(); - } - - } - } -} diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseInstanceIdService.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseInstanceIdService.java new file mode 100644 index 00000000..349760f4 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseInstanceIdService.java @@ -0,0 +1,24 @@ +package io.invertase.firebase.messaging; + + +import android.content.Intent; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; + +import com.google.firebase.iid.FirebaseInstanceIdService; + +public class RNFirebaseInstanceIdService extends FirebaseInstanceIdService { + private static final String TAG = "RNFInstanceIdService"; + public static final String TOKEN_REFRESH_EVENT = "messaging-token-refresh"; + + @Override + public void onTokenRefresh() { + Log.d(TAG, "onTokenRefresh event received"); + + // Build an Intent to pass the token to the RN Application + Intent tokenRefreshEvent = new Intent(TOKEN_REFRESH_EVENT); + + // Broadcast it so it is only available to the RN Application + LocalBroadcastManager.getInstance(this).sendBroadcast(tokenRefreshEvent); + } +} diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java index bc6d1d7b..9292eb2d 100644 --- a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java +++ b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java @@ -1,17 +1,17 @@ package io.invertase.firebase.messaging; import android.app.Activity; -import android.app.Application; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.SharedPreferences; import android.os.Bundle; +import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import com.facebook.react.bridge.ActivityEventListener; import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; @@ -26,26 +26,33 @@ import com.google.firebase.messaging.RemoteMessage; import com.google.firebase.messaging.RemoteMessage.Notification; import io.invertase.firebase.Utils; +import me.leolin.shortcutbadger.ShortcutBadger; -import java.io.IOException; -import java.util.ArrayList; import java.util.Map; -import java.util.Set; -public class RNFirebaseMessaging extends ReactContextBaseJavaModule implements LifecycleEventListener, ActivityEventListener { - private final static String TAG = RNFirebaseMessaging.class.getCanonicalName(); - private RNFirebaseLocalMessagingHelper mRNFirebaseLocalMessagingHelper; - private BadgeHelper mBadgeHelper; +public class RNFirebaseMessaging extends ReactContextBaseJavaModule implements ActivityEventListener { + private static final String BADGE_FILE = "BadgeCountFile"; + private static final String BADGE_KEY = "BadgeCount"; - public RNFirebaseMessaging(ReactApplicationContext reactContext) { - super(reactContext); - mRNFirebaseLocalMessagingHelper = new RNFirebaseLocalMessagingHelper((Application) reactContext.getApplicationContext()); - mBadgeHelper = new BadgeHelper(reactContext.getApplicationContext()); - getReactApplicationContext().addLifecycleEventListener(this); - getReactApplicationContext().addActivityEventListener(this); - registerTokenRefreshHandler(); - registerMessageHandler(); - registerLocalMessageHandler(); + private static final String TAG = RNFirebaseMessaging.class.getCanonicalName(); + + private SharedPreferences sharedPreferences = null; + + public RNFirebaseMessaging(ReactApplicationContext context) { + super(context); + context.addActivityEventListener(this); + + sharedPreferences = context.getSharedPreferences(BADGE_FILE, Context.MODE_PRIVATE); + + LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context); + + // Subscribe to message events + localBroadcastManager.registerReceiver(new MessageReceiver(), + new IntentFilter(RNFirebaseMessagingService.MESSAGE_EVENT)); + + // Subscribe to token refresh events + localBroadcastManager.registerReceiver(new RefreshTokenReceiver(), + new IntentFilter(RNFirebaseInstanceIdService.TOKEN_REFRESH_EVENT)); } @Override @@ -54,73 +61,84 @@ public class RNFirebaseMessaging extends ReactContextBaseJavaModule implements L } @ReactMethod - public void getInitialNotification(Promise promise) { - Activity activity = getCurrentActivity(); - if (activity == null) { + public void getToken(Promise promise) { + String token = FirebaseInstanceId.getInstance().getToken(); + Log.d(TAG, "Firebase token: " + token); + promise.resolve(token); + } + + @ReactMethod + public void requestPermission(Promise promise) { + // TODO: Object structure? + promise.resolve(null); + } + + // Non Web SDK methods + + @ReactMethod + public void getBadge(Promise promise) { + int badge = sharedPreferences.getInt(BADGE_KEY, 0); + Log.d(TAG, "Got badge count: " + badge); + promise.resolve(badge); + } + + @ReactMethod + public void getInitialMessage(Promise promise) { + if (getCurrentActivity() == null) { promise.resolve(null); + } else { + WritableMap messageMap = parseIntentForMessage(getCurrentActivity().getIntent()); + promise.resolve(messageMap); + } + } + + @ReactMethod + public void sendMessage(ReadableMap messageMap, Promise promise) { + if (!messageMap.hasKey("to")) { + promise.reject("messaging/invalid-message", "The supplied message is missing a 'to' field"); return; } - promise.resolve(parseIntent(getCurrentActivity().getIntent())); - } - @ReactMethod - public void requestPermissions() { - } + RemoteMessage.Builder mb = new RemoteMessage.Builder(messageMap.getString("to")); - @ReactMethod - public void getToken(Promise promise) { - Log.d(TAG, "Firebase token: " + FirebaseInstanceId.getInstance().getToken()); - promise.resolve(FirebaseInstanceId.getInstance().getToken()); - } - - @ReactMethod - public void deleteInstanceId(Promise promise){ - try { - Log.d(TAG, "Deleting instance id"); - FirebaseInstanceId.getInstance().deleteInstanceId(); - promise.resolve(null); - } catch (IOException e) { - Log.e(TAG, e.getMessage()); - promise.reject(null, e.getMessage()); + if (messageMap.hasKey("collapseKey")) { + mb = mb.setCollapseKey(messageMap.getString("collapseKey")); } - } - - @ReactMethod - public void createLocalNotification(ReadableMap details) { - Bundle bundle = Arguments.toBundle(details); - mRNFirebaseLocalMessagingHelper.sendNotification(bundle); - } - - @ReactMethod - public void scheduleLocalNotification(ReadableMap details) { - Bundle bundle = Arguments.toBundle(details); - mRNFirebaseLocalMessagingHelper.sendNotificationScheduled(bundle); - } - - @ReactMethod - public void cancelLocalNotification(String notificationID) { - mRNFirebaseLocalMessagingHelper.cancelLocalNotification(notificationID); - } - - @ReactMethod - public void cancelAllLocalNotifications() { - try { - mRNFirebaseLocalMessagingHelper.cancelAllLocalNotifications(); - } catch (SecurityException e) { - // In some devices/situations cancelAllLocalNotifications will throw a SecurityException - // We can safely ignore this error for now as the UX impact of this not working is minor. - Log.w(TAG, e.getMessage()); + if (messageMap.hasKey("messageId")) { + mb = mb.setMessageId(messageMap.getString("messageId")); } + if (messageMap.hasKey("messageType")) { + mb = mb.setMessageType(messageMap.getString("messageType")); + } + if (messageMap.hasKey("ttl")) { + mb = mb.setTtl(messageMap.getInt("ttl")); + } + if (messageMap.hasKey("data")) { + ReadableMap dataMap = messageMap.getMap("data"); + ReadableMapKeySetIterator iterator = dataMap.keySetIterator(); + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + mb = mb.addData(key, dataMap.getString(key)); + } + } + + FirebaseMessaging.getInstance().send(mb.build()); + + // TODO: Listen to onMessageSent and onSendError for better feedback? + promise.resolve(null); } @ReactMethod - public void removeDeliveredNotification(String notificationID) { - mRNFirebaseLocalMessagingHelper.removeDeliveredNotification(notificationID); - } - - @ReactMethod - public void removeAllDeliveredNotifications() { - mRNFirebaseLocalMessagingHelper.removeAllDeliveredNotifications(); + public void setBadge(int badge) { + // Store the badge count for later retrieval + sharedPreferences.edit().putInt(BADGE_KEY, badge).apply(); + if (badge == 0) { + Log.d(TAG, "Remove badge count"); + ShortcutBadger.removeCount(this.getReactApplicationContext()); + } else { + Log.d(TAG, "Apply badge count: " + badge); + ShortcutBadger.applyCount(this.getReactApplicationContext(), badge); + } } @ReactMethod @@ -133,159 +151,163 @@ public class RNFirebaseMessaging extends ReactContextBaseJavaModule implements L FirebaseMessaging.getInstance().unsubscribeFromTopic(topic); } - @ReactMethod - public void getScheduledLocalNotifications(Promise promise) { - ArrayList bundles = mRNFirebaseLocalMessagingHelper.getScheduledLocalNotifications(); - WritableArray array = Arguments.createArray(); - for (Bundle bundle : bundles) { - array.pushMap(Arguments.fromBundle(bundle)); - } - promise.resolve(array); - } - - @ReactMethod - public void setBadgeNumber(int badgeNumber) { - mBadgeHelper.setBadgeCount(badgeNumber); - } - - @ReactMethod - public void getBadgeNumber(Promise promise) { - promise.resolve(mBadgeHelper.getBadgeCount()); - } - - private void registerTokenRefreshHandler() { - IntentFilter intentFilter = new IntentFilter("io.invertase.firebase.messaging.FCMRefreshToken"); - getReactApplicationContext().registerReceiver(new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (getReactApplicationContext().hasActiveCatalystInstance()) { - String token = intent.getStringExtra("token"); - Utils.sendEvent(getReactApplicationContext(), "messaging_token_refreshed", token); - } - } - }, intentFilter); - } - - @ReactMethod - public void send(ReadableMap remoteMessage) { - FirebaseMessaging fm = FirebaseMessaging.getInstance(); - RemoteMessage.Builder message = new RemoteMessage.Builder(remoteMessage.getString("sender")); - - message.setTtl(remoteMessage.getInt("ttl")); - message.setMessageId(remoteMessage.getString("id")); - message.setMessageType(remoteMessage.getString("type")); - - if (remoteMessage.hasKey("collapseKey")) { - message.setCollapseKey(remoteMessage.getString("collapseKey")); - } - - // get data keys and values and add to builder - // js side ensures all data values are strings - // so no need to check types - ReadableMap data = remoteMessage.getMap("data"); - ReadableMapKeySetIterator iterator = data.keySetIterator(); - while (iterator.hasNextKey()) { - String key = iterator.nextKey(); - String value = data.getString(key); - message.addData(key, value); - } - - fm.send(message.build()); - } - - private void registerMessageHandler() { - IntentFilter intentFilter = new IntentFilter("io.invertase.firebase.messaging.ReceiveNotification"); - - getReactApplicationContext().registerReceiver(new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (getReactApplicationContext().hasActiveCatalystInstance()) { - RemoteMessage message = intent.getParcelableExtra("data"); - WritableMap params = Arguments.createMap(); - WritableMap fcmData = Arguments.createMap(); - - if (message.getNotification() != null) { - Notification notification = message.getNotification(); - fcmData.putString("title", notification.getTitle()); - fcmData.putString("body", notification.getBody()); - fcmData.putString("color", notification.getColor()); - fcmData.putString("icon", notification.getIcon()); - fcmData.putString("tag", notification.getTag()); - fcmData.putString("action", notification.getClickAction()); - } - params.putMap("fcm", fcmData); - - if (message.getData() != null) { - Map data = message.getData(); - Set keysIterator = data.keySet(); - for (String key : keysIterator) { - params.putString(key, data.get(key)); - } - } - Utils.sendEvent(getReactApplicationContext(), "messaging_notification_received", params); - - } - } - }, intentFilter); - } - - private void registerLocalMessageHandler() { - IntentFilter intentFilter = new IntentFilter("io.invertase.firebase.messaging.ReceiveLocalNotification"); - - getReactApplicationContext().registerReceiver(new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (getReactApplicationContext().hasActiveCatalystInstance()) { - Utils.sendEvent(getReactApplicationContext(), "messaging_notification_received", Arguments.fromBundle(intent.getExtras())); - } - } - }, intentFilter); - } - - private WritableMap parseIntent(Intent intent) { - WritableMap params; - Bundle extras = intent.getExtras(); - if (extras != null) { - try { - params = Arguments.fromBundle(extras); - } catch (Exception e) { - Log.e(TAG, e.getMessage()); - params = Arguments.createMap(); - } - } else { - params = Arguments.createMap(); - } - WritableMap fcm = Arguments.createMap(); - fcm.putString("action", intent.getAction()); - params.putMap("fcm", fcm); - - params.putBoolean("opened_from_tray", true); - return params; - } - - @Override - public void onHostResume() { - mRNFirebaseLocalMessagingHelper.setApplicationForeground(true); - } - - @Override - public void onHostPause() { - mRNFirebaseLocalMessagingHelper.setApplicationForeground(false); - } - - @Override - public void onHostDestroy() { - - } - @Override public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { - // todo hmm? + // FCM functionality does not need this function } @Override public void onNewIntent(Intent intent) { - // todo hmm? - Utils.sendEvent(getReactApplicationContext(), "messaging_notification_received", parseIntent(intent)); + WritableMap messageMap = parseIntentForMessage(intent); + if (messageMap != null) { + Log.d(TAG, "onNewIntent called with new FCM message"); + Utils.sendEvent(getReactApplicationContext(), "messaging_message_received", messageMap); + } + } + + private WritableMap parseIntentForMessage(Intent intent) { + // Check if FCM data exists + if (intent.getExtras() == null || !intent.hasExtra("google.message_id")) { + return null; + } + + Bundle extras = intent.getExtras(); + + WritableMap messageMap = Arguments.createMap(); + WritableMap dataMap = Arguments.createMap(); + + for (String key : extras.keySet()) { + if (key.equals("collapse_key")) { + messageMap.putString("collapseKey", extras.getString("collapse_key")); + } else if (key.equals("from")) { + messageMap.putString("from", extras.getString("from")); + } else if (key.equals("google.message_id")) { + messageMap.putString("messageId", extras.getString("google.message_id")); + } else if (key.equals("google.sent_time")) { + messageMap.putDouble("sentTime", extras.getLong("google.sent_time")); + } else { + dataMap.putString(key, extras.getString(key)); + } + } + messageMap.putMap("data", dataMap); + messageMap.putBoolean("openedFromTray", true); + + return messageMap; + } + + private class MessageReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (getReactApplicationContext().hasActiveCatalystInstance()) { + Log.d(TAG, "Received new message"); + + RemoteMessage message = intent.getParcelableExtra("message"); + WritableMap messageMap = buildMessageMap(message); + + Utils.sendEvent(getReactApplicationContext(), "messaging_message_received", messageMap); + } + } + + private WritableMap buildMessageMap(RemoteMessage message) { + WritableMap messageMap = Arguments.createMap(); + WritableMap dataMap = Arguments.createMap(); + + if (message.getCollapseKey() != null) { + messageMap.putString("collapseKey", message.getCollapseKey()); + } + + if (message.getData() != null) { + for (Map.Entry e : message.getData().entrySet()) { + dataMap.putString(e.getKey(), e.getValue()); + } + } + messageMap.putMap("data", dataMap); + + if (message.getFrom() != null) { + messageMap.putString("from", message.getFrom()); + } + if (message.getMessageId() != null) { + messageMap.putString("messageId", message.getMessageId()); + } + if (message.getMessageType() != null) { + messageMap.putString("messageType", message.getMessageType()); + } + + if (message.getNotification() != null) { + Notification notification = message.getNotification(); + + WritableMap notificationMap = Arguments.createMap(); + + if (notification.getBody() != null) { + notificationMap.putString("body", notification.getBody()); + } + if (notification.getBodyLocalizationArgs() != null) { + WritableArray bodyArgs = Arguments.createArray(); + for (String arg : notification.getBodyLocalizationArgs()) { + bodyArgs.pushString(arg); + } + notificationMap.putArray("bodyLocalizationArgs", bodyArgs); + } + if (notification.getBodyLocalizationKey() != null) { + notificationMap.putString("bodyLocalizationKey", notification.getBodyLocalizationKey()); + } + if (notification.getClickAction() != null) { + notificationMap.putString("clickAction", notification.getClickAction()); + } + if (notification.getColor() != null) { + notificationMap.putString("color", notification.getColor()); + } + if (notification.getIcon() != null) { + notificationMap.putString("icon", notification.getIcon()); + } + if (notification.getLink() != null) { + notificationMap.putString("link", notification.getLink().toString()); + } + if (notification.getSound() != null) { + notificationMap.putString("sound", notification.getSound()); + } + if (notification.getTag() != null) { + notificationMap.putString("tag", notification.getTag()); + } + if (notification.getTitle() != null) { + notificationMap.putString("title", notification.getTitle()); + } + if (notification.getTitleLocalizationArgs() != null) { + WritableArray titleArgs = Arguments.createArray(); + for (String arg : notification.getTitleLocalizationArgs()) { + titleArgs.pushString(arg); + } + notificationMap.putArray("titleLocalizationArgs", titleArgs); + } + if (notification.getTitleLocalizationKey() != null) { + notificationMap.putString("titleLocalizationKey", notification.getTitleLocalizationKey()); + } + + messageMap.putMap("notification", notificationMap); + } + + messageMap.putBoolean("openedFromTray", false); + messageMap.putDouble("sentTime", message.getSentTime()); + + if (message.getTo() != null) { + messageMap.putString("to", message.getTo()); + } + + messageMap.putDouble("ttl", message.getTtl()); + + return messageMap; + } + } + + private class RefreshTokenReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (getReactApplicationContext().hasActiveCatalystInstance()) { + String token = FirebaseInstanceId.getInstance().getToken(); + Log.d(TAG, "Received new token: " + token); + + Utils.sendEvent(getReactApplicationContext(), "messaging_token_refreshed", token); + } + } } } diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessagingService.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessagingService.java new file mode 100644 index 00000000..fd350e76 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessagingService.java @@ -0,0 +1,25 @@ +package io.invertase.firebase.messaging; + +import android.content.Intent; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; + +import com.google.firebase.messaging.FirebaseMessagingService; +import com.google.firebase.messaging.RemoteMessage; + +public class RNFirebaseMessagingService extends FirebaseMessagingService { + private static final String TAG = "RNFMessagingService"; + public static final String MESSAGE_EVENT = "messaging-message"; + + @Override + public void onMessageReceived(RemoteMessage message) { + Log.d(TAG, "onMessageReceived event received"); + + // Build an Intent to pass the token to the RN Application + Intent messageEvent = new Intent(MESSAGE_EVENT); + messageEvent.putExtra("message", message); + + // Broadcast it so it is only available to the RN Application + LocalBroadcastManager.getInstance(this).sendBroadcast(messageEvent); + } +} diff --git a/ios/RNFirebase.xcodeproj/project.pbxproj b/ios/RNFirebase.xcodeproj/project.pbxproj index bc43b797..46815506 100644 --- a/ios/RNFirebase.xcodeproj/project.pbxproj +++ b/ios/RNFirebase.xcodeproj/project.pbxproj @@ -17,7 +17,9 @@ 8376F7141F7C149100D45A85 /* RNFirebaseFirestoreDocumentReference.m in Sources */ = {isa = PBXBuildFile; fileRef = 8376F70E1F7C149000D45A85 /* RNFirebaseFirestoreDocumentReference.m */; }; 8376F7151F7C149100D45A85 /* RNFirebaseFirestore.m in Sources */ = {isa = PBXBuildFile; fileRef = 8376F7101F7C149000D45A85 /* RNFirebaseFirestore.m */; }; 8376F7161F7C149100D45A85 /* RNFirebaseFirestoreCollectionReference.m in Sources */ = {isa = PBXBuildFile; fileRef = 8376F7111F7C149000D45A85 /* RNFirebaseFirestoreCollectionReference.m */; }; - 838E36FE201B9169004DCD3A /* NewRNFirebaseMessaging.m in Sources */ = {isa = PBXBuildFile; fileRef = 838E36FD201B9169004DCD3A /* NewRNFirebaseMessaging.m */; }; + 838E36FE201B9169004DCD3A /* RNFirebaseMessaging.m in Sources */ = {isa = PBXBuildFile; fileRef = 838E36FD201B9169004DCD3A /* RNFirebaseMessaging.m */; }; + 838E372320231DF0004DCD3A /* RNFirebaseInstanceId.m in Sources */ = {isa = PBXBuildFile; fileRef = 838E372220231DF0004DCD3A /* RNFirebaseInstanceId.m */; }; + 838E372720231E15004DCD3A /* RNFirebaseNotifications.m in Sources */ = {isa = PBXBuildFile; fileRef = 838E372520231E15004DCD3A /* RNFirebaseNotifications.m */; }; 839D916C1EF3E20B0077C7C8 /* RNFirebaseAdMob.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D914F1EF3E20A0077C7C8 /* RNFirebaseAdMob.m */; }; 839D916D1EF3E20B0077C7C8 /* RNFirebaseAdMobInterstitial.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D91511EF3E20A0077C7C8 /* RNFirebaseAdMobInterstitial.m */; }; 839D916E1EF3E20B0077C7C8 /* RNFirebaseAdMobRewardedVideo.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D91531EF3E20A0077C7C8 /* RNFirebaseAdMobRewardedVideo.m */; }; @@ -26,7 +28,6 @@ 839D91711EF3E20B0077C7C8 /* RNFirebaseRemoteConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D915C1EF3E20A0077C7C8 /* RNFirebaseRemoteConfig.m */; }; 839D91721EF3E20B0077C7C8 /* RNFirebaseCrash.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D915F1EF3E20A0077C7C8 /* RNFirebaseCrash.m */; }; 839D91731EF3E20B0077C7C8 /* RNFirebaseDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D91621EF3E20A0077C7C8 /* RNFirebaseDatabase.m */; }; - 839D91741EF3E20B0077C7C8 /* RNFirebaseMessaging.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D91651EF3E20A0077C7C8 /* RNFirebaseMessaging.m */; }; 839D91751EF3E20B0077C7C8 /* RNFirebasePerformance.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D91681EF3E20A0077C7C8 /* RNFirebasePerformance.m */; }; 83C3EEEE1FA1EACC00B64D3C /* RNFirebaseUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 83C3EEEC1FA1EACC00B64D3C /* RNFirebaseUtil.m */; }; BA84AE571FA9E59800E79390 /* RNFirebaseStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = BA84AE561FA9E59800E79390 /* RNFirebaseStorage.m */; }; @@ -67,8 +68,12 @@ 8376F7111F7C149000D45A85 /* RNFirebaseFirestoreCollectionReference.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseFirestoreCollectionReference.m; sourceTree = ""; }; 8376F7121F7C149000D45A85 /* RNFirebaseFirestoreDocumentReference.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseFirestoreDocumentReference.h; sourceTree = ""; }; 8376F7131F7C149000D45A85 /* RNFirebaseFirestoreCollectionReference.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseFirestoreCollectionReference.h; sourceTree = ""; }; - 838E36FC201B9169004DCD3A /* NewRNFirebaseMessaging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NewRNFirebaseMessaging.h; sourceTree = ""; }; - 838E36FD201B9169004DCD3A /* NewRNFirebaseMessaging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NewRNFirebaseMessaging.m; sourceTree = ""; }; + 838E36FC201B9169004DCD3A /* RNFirebaseMessaging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseMessaging.h; sourceTree = ""; }; + 838E36FD201B9169004DCD3A /* RNFirebaseMessaging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseMessaging.m; sourceTree = ""; }; + 838E372120231DF0004DCD3A /* RNFirebaseInstanceId.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseInstanceId.h; sourceTree = ""; }; + 838E372220231DF0004DCD3A /* RNFirebaseInstanceId.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseInstanceId.m; sourceTree = ""; }; + 838E372520231E15004DCD3A /* RNFirebaseNotifications.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseNotifications.m; sourceTree = ""; }; + 838E372620231E15004DCD3A /* RNFirebaseNotifications.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseNotifications.h; sourceTree = ""; }; 839D914E1EF3E20A0077C7C8 /* RNFirebaseAdMob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseAdMob.h; sourceTree = ""; }; 839D914F1EF3E20A0077C7C8 /* RNFirebaseAdMob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseAdMob.m; sourceTree = ""; }; 839D91501EF3E20A0077C7C8 /* RNFirebaseAdMobInterstitial.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseAdMobInterstitial.h; sourceTree = ""; }; @@ -85,8 +90,6 @@ 839D915F1EF3E20A0077C7C8 /* RNFirebaseCrash.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseCrash.m; sourceTree = ""; }; 839D91611EF3E20A0077C7C8 /* RNFirebaseDatabase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseDatabase.h; sourceTree = ""; }; 839D91621EF3E20A0077C7C8 /* RNFirebaseDatabase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseDatabase.m; sourceTree = ""; }; - 839D91641EF3E20A0077C7C8 /* RNFirebaseMessaging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseMessaging.h; sourceTree = ""; }; - 839D91651EF3E20A0077C7C8 /* RNFirebaseMessaging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseMessaging.m; sourceTree = ""; }; 839D91671EF3E20A0077C7C8 /* RNFirebasePerformance.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebasePerformance.h; sourceTree = ""; }; 839D91681EF3E20A0077C7C8 /* RNFirebasePerformance.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebasePerformance.m; sourceTree = ""; }; 839D91771EF3E22F0077C7C8 /* RNFirebaseEvents.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNFirebaseEvents.h; path = RNFirebase/RNFirebaseEvents.h; sourceTree = ""; }; @@ -130,6 +133,8 @@ 58B511D21A9E6C8500147676 = { isa = PBXGroup; children = ( + 838E372420231E15004DCD3A /* notifications */, + 838E372020231DF0004DCD3A /* instanceid */, 8336930F1FD80DE800AA806B /* fabric */, BA84AE541FA9E59800E79390 /* storage */, 17AF4F681F59CDBF00C02336 /* links */, @@ -182,6 +187,26 @@ path = RNFirebase/firestore; sourceTree = ""; }; + 838E372020231DF0004DCD3A /* instanceid */ = { + isa = PBXGroup; + children = ( + 838E372120231DF0004DCD3A /* RNFirebaseInstanceId.h */, + 838E372220231DF0004DCD3A /* RNFirebaseInstanceId.m */, + ); + name = instanceid; + path = RNFirebase/instanceid; + sourceTree = ""; + }; + 838E372420231E15004DCD3A /* notifications */ = { + isa = PBXGroup; + children = ( + 838E372520231E15004DCD3A /* RNFirebaseNotifications.m */, + 838E372620231E15004DCD3A /* RNFirebaseNotifications.h */, + ); + name = notifications; + path = RNFirebase/notifications; + sourceTree = ""; + }; 839D914D1EF3E20A0077C7C8 /* admob */ = { isa = PBXGroup; children = ( @@ -259,10 +284,8 @@ 839D91631EF3E20A0077C7C8 /* messaging */ = { isa = PBXGroup; children = ( - 838E36FC201B9169004DCD3A /* NewRNFirebaseMessaging.h */, - 838E36FD201B9169004DCD3A /* NewRNFirebaseMessaging.m */, - 839D91641EF3E20A0077C7C8 /* RNFirebaseMessaging.h */, - 839D91651EF3E20A0077C7C8 /* RNFirebaseMessaging.m */, + 838E36FC201B9169004DCD3A /* RNFirebaseMessaging.h */, + 838E36FD201B9169004DCD3A /* RNFirebaseMessaging.m */, ); name = messaging; path = RNFirebase/messaging; @@ -344,11 +367,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 838E372320231DF0004DCD3A /* RNFirebaseInstanceId.m in Sources */, 839D916E1EF3E20B0077C7C8 /* RNFirebaseAdMobRewardedVideo.m in Sources */, 839D916C1EF3E20B0077C7C8 /* RNFirebaseAdMob.m in Sources */, 17AF4F6B1F59CDBF00C02336 /* RNFirebaseLinks.m in Sources */, 8376F7161F7C149100D45A85 /* RNFirebaseFirestoreCollectionReference.m in Sources */, - 838E36FE201B9169004DCD3A /* NewRNFirebaseMessaging.m in Sources */, + 838E36FE201B9169004DCD3A /* RNFirebaseMessaging.m in Sources */, 8376F7151F7C149100D45A85 /* RNFirebaseFirestore.m in Sources */, 839D91701EF3E20B0077C7C8 /* RNFirebaseAuth.m in Sources */, 8323CF091F6FBD870071420B /* RNFirebaseAdMobNativeExpressManager.m in Sources */, @@ -358,11 +382,11 @@ 833693131FD824EF00AA806B /* RNFirebaseCrashlytics.m in Sources */, D950369E1D19C77400F7094D /* RNFirebase.m in Sources */, 839D91731EF3E20B0077C7C8 /* RNFirebaseDatabase.m in Sources */, + 838E372720231E15004DCD3A /* RNFirebaseNotifications.m in Sources */, BA84AE571FA9E59800E79390 /* RNFirebaseStorage.m in Sources */, 8323CF071F6FBD870071420B /* NativeExpressComponent.m in Sources */, 83C3EEEE1FA1EACC00B64D3C /* RNFirebaseUtil.m in Sources */, 839D91721EF3E20B0077C7C8 /* RNFirebaseCrash.m in Sources */, - 839D91741EF3E20B0077C7C8 /* RNFirebaseMessaging.m in Sources */, 839D91751EF3E20B0077C7C8 /* RNFirebasePerformance.m in Sources */, 8323CF061F6FBD870071420B /* BannerComponent.m in Sources */, 839D916D1EF3E20B0077C7C8 /* RNFirebaseAdMobInterstitial.m in Sources */, diff --git a/ios/RNFirebase/instanceid/RNFirebaseInstanceId.h b/ios/RNFirebase/instanceid/RNFirebaseInstanceId.h new file mode 100644 index 00000000..e90f72b9 --- /dev/null +++ b/ios/RNFirebase/instanceid/RNFirebaseInstanceId.h @@ -0,0 +1,19 @@ +#ifndef RNFirebaseInstanceId_h +#define RNFirebaseInstanceId_h +#import + +#if __has_include() +#import + +@interface RNFirebaseInstanceId : NSObject { + +} + +@end + +#else +@interface RNFirebaseInstanceId : NSObject +@end +#endif + +#endif diff --git a/ios/RNFirebase/instanceid/RNFirebaseInstanceId.m b/ios/RNFirebase/instanceid/RNFirebaseInstanceId.m new file mode 100644 index 00000000..d03ec1a3 --- /dev/null +++ b/ios/RNFirebase/instanceid/RNFirebaseInstanceId.m @@ -0,0 +1,35 @@ +#import "RNFirebaseInstanceId.h" + +#if __has_include() +#import + +@implementation RNFirebaseInstanceId +RCT_EXPORT_MODULE(); + +RCT_EXPORT_METHOD(delete:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [[FIRInstanceID instanceID] deleteIDWithHandler:^(NSError * _Nullable error) { + if (error) { + reject(@"instance_id_error", @"Failed to delete instance id", error); + } else { + resolve(nil); + } + }]; +} + +RCT_EXPORT_METHOD(get:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [[FIRInstanceID instanceID] getIDWithHandler:^(NSString * _Nullable identity, NSError * _Nullable error) { + if (error) { + reject(@"instance_id_error", @"Failed to get instance id", error); + } else { + resolve(identity); + } + }]; +} + +@end + +#else +@implementation RNFirebaseInstanceId +@end +#endif + diff --git a/ios/RNFirebase/messaging/NewRNFirebaseMessaging.h b/ios/RNFirebase/messaging/NewRNFirebaseMessaging.h deleted file mode 100644 index 53511103..00000000 --- a/ios/RNFirebase/messaging/NewRNFirebaseMessaging.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef NewRNFirebaseMessaging_h -#define NewRNFirebaseMessaging_h -#import - -#if __has_include() -#import -#import -#import - -@interface NewRNFirebaseMessaging : RCTEventEmitter - -#if !TARGET_OS_TV - -#endif - -@end - -#else -@interface NewRNFirebaseMessaging : NSObject -@end -#endif - -#endif diff --git a/ios/RNFirebase/messaging/NewRNFirebaseMessaging.m b/ios/RNFirebase/messaging/NewRNFirebaseMessaging.m deleted file mode 100644 index 0880b834..00000000 --- a/ios/RNFirebase/messaging/NewRNFirebaseMessaging.m +++ /dev/null @@ -1,132 +0,0 @@ -#import "NewRNFirebaseMessaging.h" - -#if __has_include() -@import UserNotifications; -#import "RNFirebaseEvents.h" -#import "RNFirebaseUtil.h" -#import -#import - -#import -#import -#import - -@interface NewRNFirebaseMessaging () - -@end - -@implementation NewRNFirebaseMessaging - -RCT_EXPORT_MODULE() - -- (id)init { - self = [super init]; - if (self != nil) { - NSLog(@"Setting up RNFirebaseMessaging instance"); - [self initialiseMessaging]; - } - return self; -} - -- (void)initialiseMessaging { - // Establish Firebase managed data channel - [FIRMessaging messaging].shouldEstablishDirectChannel = YES; -} - -- (void)dealloc { - -} - -// ** Start React Module methods ** -RCT_EXPORT_METHOD(getToken:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - resolve([[FIRInstanceID instanceID] token]); -} - -RCT_EXPORT_METHOD(requestPermission:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - if (RCTRunningInAppExtension()) { - reject(@"request_permission_unavailable", @"requestPermission is not supported in App Extensions", nil); - return; - } - - if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { - UIUserNotificationType types = (UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge); - [RCTSharedApplication() registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:types categories:nil]]; - // Unfortunately on iOS 9 or below, there's no way to tell whether the user accepted or - // rejected the permissions popup - // TODO: Is there something we can listen for? - resolve(@{@"status":@"unknown"}); - } else { - // iOS 10 or later - #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 - // For iOS 10 display notification (sent via APNS) - UNAuthorizationOptions authOptions = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge; - [[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:authOptions completionHandler:^(BOOL granted, NSError * _Nullable error) { - if (granted) { - resolve(@{@"status": @"granted"}); - } else { - reject(@"permission_error", @"Failed to grant permission", error); - } - }]; - #endif - } - - dispatch_async(dispatch_get_main_queue(), ^{ - [RCTSharedApplication() registerForRemoteNotifications]; - }); -} - -// Non Web SDK methods - -RCT_EXPORT_METHOD(deleteInstanceId:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - [[FIRInstanceID instanceID] deleteIDWithHandler:^(NSError * _Nullable error) { - if (!error) { - resolve(nil); - } else { - reject(@"instance_id_error", @"Failed to delete instance id", error); - } - }]; -} - -RCT_EXPORT_METHOD(getBadgeNumber: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - resolve(@([RCTSharedApplication() applicationIconBadgeNumber])); -} - -RCT_EXPORT_METHOD(getInitialNotification:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){ - UILocalNotification *localUserInfo = [self bridge].launchOptions[UIApplicationLaunchOptionsLocalNotificationKey]; - if (localUserInfo) { - resolve([[localUserInfo userInfo] copy]); - } else { - resolve([[self bridge].launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey] copy]); - } -} - -RCT_EXPORT_METHOD(subscribeToTopic: (NSString*) topic) { - [[FIRMessaging messaging] subscribeToTopic:topic]; -} - -RCT_EXPORT_METHOD(unsubscribeFromTopic: (NSString*) topic) { - [[FIRMessaging messaging] unsubscribeFromTopic:topic]; -} - - -RCT_EXPORT_METHOD(setBadgeNumber: (NSInteger*) number) { - dispatch_async(dispatch_get_main_queue(), ^{ - [RCTSharedApplication() setApplicationIconBadgeNumber:*number]; - }); -} - -- (NSArray *)supportedEvents { - return @[MESSAGING_MESSAGE_RECEIVED, MESSAGING_TOKEN_REFRESHED]; -} - -+ (BOOL)requiresMainQueueSetup -{ - return YES; -} - -@end - -#else -@implementation NewRNFirebaseMessaging -@end -#endif diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.h b/ios/RNFirebase/messaging/RNFirebaseMessaging.h index ccdc700d..c493eabf 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.h +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.h @@ -7,22 +7,10 @@ #import #import -@import UserNotifications; - @interface RNFirebaseMessaging : RCTEventEmitter -typedef void (^RCTRemoteNotificationCallback)(UIBackgroundFetchResult result); -typedef void (^RCTWillPresentNotificationCallback)(UNNotificationPresentationOptions result); -typedef void (^RCTNotificationResponseCallback)(); - -@property (nonatomic, assign) bool connectedToFCM; - #if !TARGET_OS_TV -+ (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo; -+ (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo fetchCompletionHandler:(nonnull RCTRemoteNotificationCallback)completionHandler; -+ (void)didReceiveLocalNotification:(nonnull UILocalNotification *)notification; -+ (void)didReceiveNotificationResponse:(nonnull UNNotificationResponse *)response withCompletionHandler:(nonnull RCTNotificationResponseCallback)completionHandler; -+ (void)willPresentNotification:(nonnull UNNotification *)notification withCompletionHandler:(nonnull RCTWillPresentNotificationCallback)completionHandler; + #endif @end diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index 226b30a9..0bffc7d6 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -1,7 +1,7 @@ #import "RNFirebaseMessaging.h" -@import UserNotifications; #if __has_include() +@import UserNotifications; #import "RNFirebaseEvents.h" #import "RNFirebaseUtil.h" #import @@ -11,167 +11,14 @@ #import #import -@implementation RCTConvert (NSCalendarUnit) - -RCT_ENUM_CONVERTER(NSCalendarUnit, - (@{ - @"year": @(NSCalendarUnitYear), - @"month": @(NSCalendarUnitMonth), - @"week": @(NSCalendarUnitWeekOfYear), - @"day": @(NSCalendarUnitDay), - @"hour": @(NSCalendarUnitHour), - @"minute": @(NSCalendarUnitMinute) - }), - 0, - integerValue) -@end - - -@implementation RCTConvert (UNNotificationRequest) - -+ (UNNotificationRequest *)UNNotificationRequest:(id)json -{ - NSDictionary *details = [self NSDictionary:json]; - UNMutableNotificationContent *content = [UNMutableNotificationContent new]; - content.title =[RCTConvert NSString:details[@"title"]]; - content.body =[RCTConvert NSString:details[@"body"]]; - NSString* sound = [RCTConvert NSString:details[@"sound"]]; - if(sound != nil){ - content.sound = [UNNotificationSound soundNamed:sound]; - }else{ - content.sound = [UNNotificationSound defaultSound]; - } - content.categoryIdentifier = [RCTConvert NSString:details[@"click_action"]]; - content.userInfo = details; - content.badge = [RCTConvert NSNumber:details[@"badge"]]; - - if([details objectForKey:@"show_in_foreground"] != nil) { - if([(NSNumber *)details[@"show_in_foreground"] boolValue] == YES) { - [content setValue:@YES forKeyPath:@"shouldAlwaysAlertWhileAppIsForeground"]; - } - } - - NSDate *fireDate = [RCTConvert NSDate:details[@"fire_date"]]; - - if(fireDate == nil){ - return [UNNotificationRequest requestWithIdentifier:[RCTConvert NSString:details[@"id"]] content:content trigger:nil]; - } - - NSCalendarUnit interval = [RCTConvert NSCalendarUnit:details[@"repeat_interval"]]; - NSCalendarUnit unitFlags; - switch (interval) { - case NSCalendarUnitMinute: { - unitFlags = NSCalendarUnitSecond; - break; - } - case NSCalendarUnitHour: { - unitFlags = NSCalendarUnitMinute | NSCalendarUnitSecond; - break; - } - case NSCalendarUnitDay: { - unitFlags = NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond; - break; - } - case NSCalendarUnitWeekOfYear: { - unitFlags = NSCalendarUnitWeekday | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond; - break; - } - case NSCalendarUnitMonth:{ - unitFlags = NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond; - } - case NSCalendarUnitYear:{ - unitFlags = NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond; - } - default: - unitFlags = NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond; - break; - } - NSDateComponents *components = [[NSCalendar currentCalendar] components:unitFlags fromDate:fireDate]; - UNCalendarNotificationTrigger *trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:interval != 0]; - return [UNNotificationRequest requestWithIdentifier:[RCTConvert NSString:details[@"id"]] content:content trigger:trigger]; -} - -@end - -@implementation RCTConvert (UILocalNotification) - -+ (UILocalNotification *)UILocalNotification:(id)json -{ - NSDictionary *details = [self NSDictionary:json]; - UILocalNotification *notification = [UILocalNotification new]; - notification.fireDate = [RCTConvert NSDate:details[@"fire_date"]] ?: [NSDate date]; - if([notification respondsToSelector:@selector(setAlertTitle:)]){ - [notification setAlertTitle:[RCTConvert NSString:details[@"title"]]]; - } - notification.alertBody = [RCTConvert NSString:details[@"body"]]; - notification.alertAction = [RCTConvert NSString:details[@"alert_action"]]; - notification.soundName = [RCTConvert NSString:details[@"sound"]] ?: UILocalNotificationDefaultSoundName; - notification.userInfo = details; - notification.category = [RCTConvert NSString:details[@"click_action"]]; - notification.repeatInterval = [RCTConvert NSCalendarUnit:details[@"repeat_interval"]]; - notification.applicationIconBadgeNumber = [RCTConvert NSInteger:details[@"badge"]]; - return notification; -} - -RCT_ENUM_CONVERTER(UIBackgroundFetchResult, (@{ - @"UIBackgroundFetchResultNewData": @(UIBackgroundFetchResultNewData), - @"UIBackgroundFetchResultNoData": @(UIBackgroundFetchResultNoData), - @"UIBackgroundFetchResultFailed": @(UIBackgroundFetchResultFailed), - }), UIBackgroundFetchResultNoData, integerValue) - -RCT_ENUM_CONVERTER(UNNotificationPresentationOptions, (@{ - @"UNNotificationPresentationOptionAll": @(UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound), - @"UNNotificationPresentationOptionNone": @(UNNotificationPresentationOptionNone)}), UIBackgroundFetchResultNoData, integerValue) - -@end - @interface RNFirebaseMessaging () -@property (nonatomic, strong) NSMutableDictionary *notificationCallbacks; + @end @implementation RNFirebaseMessaging RCT_EXPORT_MODULE() -+ (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo { - NSMutableDictionary* data = [[NSMutableDictionary alloc] initWithDictionary: userInfo]; - [data setValue:@"remote_notification" forKey:@"_notificationType"]; - [data setValue:@(RCTSharedApplication().applicationState == UIApplicationStateInactive) forKey:@"opened_from_tray"]; - [[NSNotificationCenter defaultCenter] postNotificationName:MESSAGING_NOTIFICATION_RECEIVED object:self userInfo:@{@"data": data}]; -} - -+ (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo fetchCompletionHandler:(nonnull RCTRemoteNotificationCallback)completionHandler { - NSMutableDictionary* data = [[NSMutableDictionary alloc] initWithDictionary: userInfo]; - [data setValue:@"remote_notification" forKey:@"_notificationType"]; - [data setValue:@(RCTSharedApplication().applicationState == UIApplicationStateInactive) forKey:@"opened_from_tray"]; - [[NSNotificationCenter defaultCenter] postNotificationName:MESSAGING_NOTIFICATION_RECEIVED object:self userInfo:@{@"data": data, @"completionHandler": completionHandler}]; -} - -+ (void)didReceiveLocalNotification:(UILocalNotification *)notification { - NSMutableDictionary* data = [[NSMutableDictionary alloc] initWithDictionary: notification.userInfo]; - [data setValue:@"local_notification" forKey:@"_notificationType"]; - [data setValue:@(RCTSharedApplication().applicationState == UIApplicationStateInactive) forKey:@"opened_from_tray"]; - [[NSNotificationCenter defaultCenter] postNotificationName:MESSAGING_NOTIFICATION_RECEIVED object:self userInfo:@{@"data": data}]; -} - -+ (void)willPresentNotification:(UNNotification *)notification withCompletionHandler:(nonnull RCTWillPresentNotificationCallback)completionHandler -{ - NSMutableDictionary* data = [[NSMutableDictionary alloc] initWithDictionary: notification.request.content.userInfo]; - [data setValue:@"will_present_notification" forKey:@"_notificationType"]; - [[NSNotificationCenter defaultCenter] postNotificationName:MESSAGING_NOTIFICATION_RECEIVED object:self userInfo:@{@"data": data, @"completionHandler": completionHandler}]; -} - -+ (void)didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(nonnull RCTNotificationResponseCallback)completionHandler -{ - NSMutableDictionary* data = [[NSMutableDictionary alloc] initWithDictionary: response.notification.request.content.userInfo]; - [data setValue:@"notification_response" forKey:@"_notificationType"]; - [data setValue:@YES forKey:@"opened_from_tray"]; - if (response.actionIdentifier) { - [data setValue:response.actionIdentifier forKey:@"_actionIdentifier"]; - } - [[NSNotificationCenter defaultCenter] postNotificationName:MESSAGING_NOTIFICATION_RECEIVED object:self userInfo:@{@"data": data, @"completionHandler": completionHandler}]; -} - - (id)init { self = [super init]; if (self != nil) { @@ -184,74 +31,56 @@ RCT_EXPORT_MODULE() - (void)initialiseMessaging { // Establish Firebase managed data channel [FIRMessaging messaging].shouldEstablishDirectChannel = YES; - - // Set up listeners for data messages - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(sendDataMessageFailure:) - name:FIRMessagingSendErrorNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(sendDataMessageSuccess:) - name:FIRMessagingSendSuccessNotification - object:nil]; - - // Set up internal listener to send notification over bridge - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(handleNotificationReceived:) - name:MESSAGING_NOTIFICATION_RECEIVED - object:nil]; - - // Set this as a delegate for FIRMessaging - dispatch_async(dispatch_get_main_queue(), ^{ - [FIRMessaging messaging].delegate = self; - }); } - (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; + } -- (void)handleNotificationReceived:(NSNotification *)notification { - id completionHandler = notification.userInfo[@"completionHandler"]; - NSMutableDictionary* data = notification.userInfo[@"data"]; - if(completionHandler != nil){ - NSString *completionHandlerId = [[NSUUID UUID] UUIDString]; - if (!self.notificationCallbacks) { - // Lazy initialization - self.notificationCallbacks = [NSMutableDictionary dictionary]; - } - self.notificationCallbacks[completionHandlerId] = completionHandler; - data[@"_completionHandlerId"] = completionHandlerId; - } - - [RNFirebaseUtil sendJSEvent:self name:MESSAGING_NOTIFICATION_RECEIVED body:data]; -} - - -- (void)sendDataMessageFailure:(NSNotification *)notification { - NSString *messageID = (NSString *)notification.userInfo[@"messageID"]; - NSLog(@"sendDataMessageFailure: %@", messageID); -} - -- (void)sendDataMessageSuccess:(NSNotification *)notification { - NSString *messageID = (NSString *)notification.userInfo[@"messageID"]; - NSLog(@"sendDataMessageSuccess: %@", messageID); -} - -// ** Start FIRMessagingDelegate methods ** -// Handle data messages in the background -- (void)applicationReceivedRemoteMessage:(FIRMessagingRemoteMessage *)remoteMessage { - [RNFirebaseUtil sendJSEvent:self name:MESSAGING_NOTIFICATION_RECEIVED body:[remoteMessage appData]]; -} - -// Listen for token refreshes -- (void)messaging:(nonnull FIRMessaging *)messaging didRefreshRegistrationToken:(nonnull NSString *)fcmToken { - NSLog(@"FCM registration token: %@", fcmToken); - [RNFirebaseUtil sendJSEvent:self name:MESSAGING_TOKEN_REFRESHED body:fcmToken]; -} -// ** End FIRMessagingDelegate methods ** - // ** Start React Module methods ** +RCT_EXPORT_METHOD(getToken:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve([[FIRInstanceID instanceID] token]); +} + +RCT_EXPORT_METHOD(requestPermission:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + if (RCTRunningInAppExtension()) { + reject(@"request_permission_unavailable", @"requestPermission is not supported in App Extensions", nil); + return; + } + + if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { + UIUserNotificationType types = (UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge); + [RCTSharedApplication() registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:types categories:nil]]; + // Unfortunately on iOS 9 or below, there's no way to tell whether the user accepted or + // rejected the permissions popup + // TODO: Is there something we can listen for? + resolve(@{@"status":@"unknown"}); + } else { + // iOS 10 or later + #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + // For iOS 10 display notification (sent via APNS) + UNAuthorizationOptions authOptions = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge; + [[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:authOptions completionHandler:^(BOOL granted, NSError * _Nullable error) { + if (granted) { + resolve(@{@"status": @"granted"}); + } else { + reject(@"permission_error", @"Failed to grant permission", error); + } + }]; + #endif + } + + dispatch_async(dispatch_get_main_queue(), ^{ + [RCTSharedApplication() registerForRemoteNotifications]; + }); +} + +// Non Web SDK methods + +RCT_EXPORT_METHOD(getBadge: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + resolve(@([RCTSharedApplication() applicationIconBadgeNumber])); +} + RCT_EXPORT_METHOD(getInitialNotification:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){ UILocalNotification *localUserInfo = [self bridge].launchOptions[UIApplicationLaunchOptionsLocalNotificationKey]; if (localUserInfo) { @@ -261,51 +90,9 @@ RCT_EXPORT_METHOD(getInitialNotification:(RCTPromiseResolveBlock)resolve rejecte } } -RCT_EXPORT_METHOD(getToken:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - resolve([FIRMessaging messaging].FCMToken); -} - -RCT_EXPORT_METHOD(deleteInstanceId:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - [[FIRInstanceID instanceID] deleteIDWithHandler:^(NSError * _Nullable error) { - if (!error) { - resolve(nil); - } else { - reject(@"instance_id_error", @"Failed to delete instance id", error); - } - }]; -} - -RCT_EXPORT_METHOD(requestPermissions:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - if (RCTRunningInAppExtension()) { - return; - } - - if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { - UIUserNotificationType allNotificationTypes = - (UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge); - UIUserNotificationSettings *settings = - [UIUserNotificationSettings settingsForTypes:allNotificationTypes categories:nil]; - [RCTSharedApplication() registerUserNotificationSettings:settings]; - // Unfortunately on iOS 9 or below, there's no way to tell whether the user accepted or - // rejected the permissions popup - resolve(@{@"status":@"unknown"}); - } else { - // iOS 10 or later -#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 - // For iOS 10 display notification (sent via APNS) - [UNUserNotificationCenter currentNotificationCenter].delegate = self; - UNAuthorizationOptions authOptions = - UNAuthorizationOptionAlert - | UNAuthorizationOptionSound - | UNAuthorizationOptionBadge; - [[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:authOptions completionHandler:^(BOOL granted, NSError * _Nullable error) { - resolve(@{@"granted":@(granted)}); - }]; -#endif - } - +RCT_EXPORT_METHOD(setBadge: (NSInteger*) number) { dispatch_async(dispatch_get_main_queue(), ^{ - [RCTSharedApplication() registerForRemoteNotifications]; + [RCTSharedApplication() setApplicationIconBadgeNumber:*number]; }); } @@ -317,144 +104,8 @@ RCT_EXPORT_METHOD(unsubscribeFromTopic: (NSString*) topic) { [[FIRMessaging messaging] unsubscribeFromTopic:topic]; } -RCT_EXPORT_METHOD(createLocalNotification:(id)data resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - if([UNUserNotificationCenter currentNotificationCenter] != nil){ - UNNotificationRequest* request = [RCTConvert UNNotificationRequest:data]; - [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { - if (!error) { - resolve(nil); - }else{ - reject(@"notification_error", @"Failed to present local notificaton", error); - } - }]; - }else{ - UILocalNotification* notif = [RCTConvert UILocalNotification:data]; - [RCTSharedApplication() presentLocalNotificationNow:notif]; - resolve(nil); - } -} - -RCT_EXPORT_METHOD(scheduleLocalNotification:(id)data resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - if([UNUserNotificationCenter currentNotificationCenter] != nil){ - UNNotificationRequest* request = [RCTConvert UNNotificationRequest:data]; - [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { - if (!error) { - resolve(nil); - }else{ - reject(@"notification_error", @"Failed to present local notificaton", error); - } - }]; - }else{ - UILocalNotification* notif = [RCTConvert UILocalNotification:data]; - [RCTSharedApplication() scheduleLocalNotification:notif]; - resolve(nil); - } -} - -RCT_EXPORT_METHOD(removeDeliveredNotification:(NSString*) notificationId) { - if([UNUserNotificationCenter currentNotificationCenter] != nil){ - [[UNUserNotificationCenter currentNotificationCenter] removeDeliveredNotificationsWithIdentifiers:@[notificationId]]; - } -} - -RCT_EXPORT_METHOD(removeAllDeliveredNotifications) { - if([UNUserNotificationCenter currentNotificationCenter] != nil){ - [[UNUserNotificationCenter currentNotificationCenter] removeAllDeliveredNotifications]; - } else { - [RCTSharedApplication() setApplicationIconBadgeNumber: 0]; - } -} - -RCT_EXPORT_METHOD(cancelAllLocalNotifications) { - if([UNUserNotificationCenter currentNotificationCenter] != nil){ - [[UNUserNotificationCenter currentNotificationCenter] removeAllPendingNotificationRequests]; - } else { - [RCTSharedApplication() cancelAllLocalNotifications]; - } -} - -RCT_EXPORT_METHOD(cancelLocalNotification:(NSString*) notificationId) { - if([UNUserNotificationCenter currentNotificationCenter] != nil){ - [[UNUserNotificationCenter currentNotificationCenter] removePendingNotificationRequestsWithIdentifiers:@[notificationId]]; - } else { - for (UILocalNotification *notification in RCTSharedApplication().scheduledLocalNotifications) { - NSDictionary *notificationInfo = notification.userInfo; - if([notificationId isEqualToString:[notificationInfo valueForKey:@"id"]]){ - [RCTSharedApplication() cancelLocalNotification:notification]; - } - } - } -} - -RCT_EXPORT_METHOD(getScheduledLocalNotifications:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - if([UNUserNotificationCenter currentNotificationCenter] != nil){ - [[UNUserNotificationCenter currentNotificationCenter] getPendingNotificationRequestsWithCompletionHandler:^(NSArray * _Nonnull requests) { - NSMutableArray* list = [[NSMutableArray alloc] init]; - for(UNNotificationRequest * notif in requests){ - UNMutableNotificationContent *content = notif.content; - [list addObject:content.userInfo]; - } - resolve(list); - }]; - } else { - NSMutableArray* list = [[NSMutableArray alloc] init]; - for(UILocalNotification * notif in [RCTSharedApplication() scheduledLocalNotifications]){ - [list addObject:notif.userInfo]; - } - resolve(list); - } -} - -RCT_EXPORT_METHOD(setBadgeNumber: (NSInteger*) number) { - dispatch_async(dispatch_get_main_queue(), ^{ - [RCTSharedApplication() setApplicationIconBadgeNumber:number]; - }); -} - -RCT_EXPORT_METHOD(getBadgeNumber: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - resolve(@([RCTSharedApplication() applicationIconBadgeNumber])); -} - -RCT_EXPORT_METHOD(send:(NSDictionary *)remoteMessage) { - int64_t ttl = @([[remoteMessage valueForKey:@"ttl"] intValue]).doubleValue; - NSString * mId = [remoteMessage valueForKey:@"id"]; - NSString * receiver = [remoteMessage valueForKey:@"sender"]; - NSDictionary * data = [remoteMessage valueForKey:@"data"]; - [[FIRMessaging messaging] sendMessage:data to:receiver withMessageID:mId timeToLive:ttl]; -} - -RCT_EXPORT_METHOD(finishRemoteNotification: (NSString *)completionHandlerId fetchResult:(UIBackgroundFetchResult)result) { - RCTRemoteNotificationCallback completionHandler = self.notificationCallbacks[completionHandlerId]; - if (!completionHandler) { - RCTLogError(@"There is no completion handler with completionHandlerId: %@", completionHandlerId); - return; - } - completionHandler(result); - [self.notificationCallbacks removeObjectForKey:completionHandlerId]; -} - -RCT_EXPORT_METHOD(finishWillPresentNotification: (NSString *)completionHandlerId fetchResult:(UNNotificationPresentationOptions)result) { - RCTWillPresentNotificationCallback completionHandler = self.notificationCallbacks[completionHandlerId]; - if (!completionHandler) { - RCTLogError(@"There is no completion handler with completionHandlerId: %@", completionHandlerId); - return; - } - completionHandler(result); - [self.notificationCallbacks removeObjectForKey:completionHandlerId]; -} - -RCT_EXPORT_METHOD(finishNotificationResponse: (NSString *)completionHandlerId) { - RCTNotificationResponseCallback completionHandler = self.notificationCallbacks[completionHandlerId]; - if (!completionHandler) { - RCTLogError(@"There is no completion handler with completionHandlerId: %@", completionHandlerId); - return; - } - completionHandler(); - [self.notificationCallbacks removeObjectForKey:completionHandlerId]; -} - - (NSArray *)supportedEvents { - return @[MESSAGING_TOKEN_REFRESHED, MESSAGING_NOTIFICATION_RECEIVED]; + return @[MESSAGING_MESSAGE_RECEIVED, MESSAGING_TOKEN_REFRESHED]; } + (BOOL)requiresMainQueueSetup diff --git a/ios/RNFirebase/notifications/RNFirebaseNotifications.h b/ios/RNFirebase/notifications/RNFirebaseNotifications.h new file mode 100644 index 00000000..16298e70 --- /dev/null +++ b/ios/RNFirebase/notifications/RNFirebaseNotifications.h @@ -0,0 +1,22 @@ +#ifndef RNFirebaseNotifications_h +#define RNFirebaseNotifications_h +#import + +#if __has_include() +#import +#import + +@interface RNFirebaseNotifications : RCTEventEmitter + +#if !TARGET_OS_TV + +#endif + +@end + +#else +@interface RNFirebaseNotifications : NSObject +@end +#endif + +#endif diff --git a/ios/RNFirebase/notifications/RNFirebaseNotifications.m b/ios/RNFirebase/notifications/RNFirebaseNotifications.m new file mode 100644 index 00000000..5f149eea --- /dev/null +++ b/ios/RNFirebase/notifications/RNFirebaseNotifications.m @@ -0,0 +1,14 @@ +#import "RNFirebaseNotifications.h" + +#if __has_include() + +@implementation RNFirebaseNotifications +RCT_EXPORT_MODULE(); + +@end + +#else +@implementation RNFirebaseNotifications +@end +#endif + diff --git a/lib/modules/core/firebase-app.js b/lib/modules/core/firebase-app.js index 0c79517f..aa90eb57 100644 --- a/lib/modules/core/firebase-app.js +++ b/lib/modules/core/firebase-app.js @@ -18,11 +18,12 @@ import Crashlytics, { } from '../fabric/crashlytics'; import Database, { NAMESPACE as DatabaseNamespace } from '../database'; import Firestore, { NAMESPACE as FirestoreNamespace } from '../firestore'; +import InstanceId, { NAMESPACE as InstanceIdNamespace } from '../instanceid'; import Links, { NAMESPACE as LinksNamespace } from '../links'; import Messaging, { NAMESPACE as MessagingNamespace } from '../messaging'; -import NewMessaging, { - NAMESPACE as NewMessagingNamespace, -} from '../messaging/messagingIndex'; +import Notifications, { + NAMESPACE as NotificationsNamespace, +} from '../notifications'; import Performance, { NAMESPACE as PerfNamespace } from '../perf'; import Storage, { NAMESPACE as StorageNamespace } from '../storage'; import Utils, { NAMESPACE as UtilsNamespace } from '../utils'; @@ -47,9 +48,10 @@ export default class App { crashlytics: () => Crashlytics, }; firestore: () => Firestore; + instanceid: () => InstanceId; links: () => Links; messaging: () => Messaging; - newmessaging: () => NewMessaging; + notifications: () => Notifications; perf: () => Performance; storage: () => Storage; utils: () => Utils; @@ -87,12 +89,13 @@ export default class App { crashlytics: APPS.appModule(this, CrashlyticsNamespace, Crashlytics), }; this.firestore = APPS.appModule(this, FirestoreNamespace, Firestore); + this.instanceid = APPS.appModule(this, InstanceIdNamespace, InstanceId); this.links = APPS.appModule(this, LinksNamespace, Links); this.messaging = APPS.appModule(this, MessagingNamespace, Messaging); - this.newmessaging = APPS.appModule( + this.notifications = APPS.appModule( this, - NewMessagingNamespace, - NewMessaging + NotificationsNamespace, + Notifications ); this.perf = APPS.appModule(this, PerfNamespace, Performance); this.storage = APPS.appModule(this, StorageNamespace, Storage); diff --git a/lib/modules/core/firebase.js b/lib/modules/core/firebase.js index 119d9fc6..a8647ba8 100644 --- a/lib/modules/core/firebase.js +++ b/lib/modules/core/firebase.js @@ -39,6 +39,10 @@ import { statics as FirestoreStatics, MODULE_NAME as FirestoreModuleName, } from '../firestore'; +import { + statics as InstanceIdStatics, + MODULE_NAME as InstanceIdModuleName, +} from '../instanceid'; import { statics as LinksStatics, MODULE_NAME as LinksModuleName, @@ -48,9 +52,9 @@ import { MODULE_NAME as MessagingModuleName, } from '../messaging'; import { - statics as NewMessagingStatics, - MODULE_NAME as NewMessagingModuleName, -} from '../messaging/messagingIndex'; + statics as NotificationsStatics, + MODULE_NAME as NotificationsModuleName, +} from '../notifications'; import { statics as PerformanceStatics, MODULE_NAME as PerfModuleName, @@ -74,9 +78,10 @@ import type { FabricModule, FirebaseOptions, FirestoreModule, + InstanceIdModule, LinksModule, MessagingModule, - NewMessagingModule, + NotificationsModule, PerformanceModule, StorageModule, UtilsModule, @@ -93,9 +98,10 @@ class Firebase { database: DatabaseModule; fabric: FabricModule; firestore: FirestoreModule; + instanceid: InstanceIdModule; links: LinksModule; messaging: MessagingModule; - newmessaging: NewMessagingModule; + notifications: NotificationsModule; perf: PerformanceModule; storage: StorageModule; utils: UtilsModule; @@ -137,16 +143,21 @@ class Firebase { FirestoreStatics, FirestoreModuleName ); + this.instanceid = APPS.moduleAndStatics( + 'instanceid', + InstanceIdStatics, + InstanceIdModuleName + ); this.links = APPS.moduleAndStatics('links', LinksStatics, LinksModuleName); this.messaging = APPS.moduleAndStatics( 'messaging', MessagingStatics, MessagingModuleName ); - this.newmessaging = APPS.moduleAndStatics( - 'newmessaging', - NewMessagingStatics, - NewMessagingModuleName + this.notifications = APPS.moduleAndStatics( + 'notifications', + NotificationsStatics, + NotificationsModuleName ); this.perf = APPS.moduleAndStatics( 'perf', diff --git a/lib/modules/instanceid/index.js b/lib/modules/instanceid/index.js new file mode 100644 index 00000000..d4f6ad68 --- /dev/null +++ b/lib/modules/instanceid/index.js @@ -0,0 +1,31 @@ +/** + * @flow + * Instance ID representation wrapper + */ +import ModuleBase from '../../utils/ModuleBase'; +import { getNativeModule } from '../../utils/native'; + +import type App from '../core/firebase-app'; + +export const MODULE_NAME = 'RNFirebaseInstanceId'; +export const NAMESPACE = 'instanceid'; + +export default class InstanceId extends ModuleBase { + constructor(app: App) { + super(app, { + moduleName: MODULE_NAME, + multiApp: false, + namespace: NAMESPACE, + }); + } + + delete(): Promise { + return getNativeModule(this).delete(); + } + + get(): Promise { + return getNativeModule(this).get(); + } +} + +export const statics = {}; diff --git a/lib/modules/messaging/index.js b/lib/modules/messaging/index.js index 4030c5ba..b1486210 100644 --- a/lib/modules/messaging/index.js +++ b/lib/modules/messaging/index.js @@ -1,94 +1,69 @@ /** * @flow - * Messaging representation wrapper + * Messaging (FCM) representation wrapper */ -import { Platform, NativeModules } from 'react-native'; import { SharedEventEmitter } from '../../utils/events'; +import INTERNALS from '../../utils/internals'; +import { getLogger } from '../../utils/log'; import ModuleBase from '../../utils/ModuleBase'; -import RemoteMessage from './RemoteMessage'; import { getNativeModule } from '../../utils/native'; +import { isFunction, isObject } from '../../utils'; import type App from '../core/firebase-app'; -const EVENT_TYPE = { - RefreshToken: 'messaging_token_refreshed', - Notification: 'messaging_notification_received', +type Notification = { + body: string, + bodyLocalizationArgs: string[], + bodyLocalizationKey: string, + clickAction: string, + color: string, + icon: string, + link: string, + sound: string, + tag: string, + title: string, + titleLocalizationArgs: string[], + titleLocalizationKey: string, }; -const NOTIFICATION_TYPE = { - Remote: 'remote_notification', - NotificationResponse: 'notification_response', - WillPresent: 'will_present_notification', - Local: 'local_notification', +type Message = { + collapseKey: string, + data: { [string]: string }, + from: string, + messageId: string, + messageType?: string, + openedFromTray: boolean, + notification?: Notification, + sentTime: number, + to?: string, + ttl?: number, }; -const REMOTE_NOTIFICATION_RESULT = { - NewData: 'UIBackgroundFetchResultNewData', - NoData: 'UIBackgroundFetchResultNoData', - ResultFailed: 'UIBackgroundFetchResultFailed', +type OnMessage = Message => any; + +type OnMessageObserver = { + next: OnMessage, }; -const WILL_PRESENT_RESULT = { - All: 'UNNotificationPresentationOptionAll', - None: 'UNNotificationPresentationOptionNone', +type OnTokenRefresh = String => any; + +type OnTokenRefreshObserver = { + next: OnTokenRefresh, }; -const NATIVE_EVENTS = [EVENT_TYPE.RefreshToken, EVENT_TYPE.Notification]; +type RemoteMessage = { + collapseKey?: string, + data: { [string]: string }, + messageId?: string, + messageType?: string, + to: string, + ttl: number, +}; -const FirebaseMessaging = NativeModules.RNFirebaseMessaging; - -/** - * IOS only finish function - * @param data - */ -function finish(data) { - if (Platform.OS !== 'ios') { - return; - } - - if (!this._finishCalled && this._completionHandlerId) { - let result = data; - - this._finishCalled = true; - - switch (this._notificationType) { - case NOTIFICATION_TYPE.Remote: - result = result || REMOTE_NOTIFICATION_RESULT.NoData; - if (!Object.values(REMOTE_NOTIFICATION_RESULT).includes(result)) { - throw new Error( - 'Invalid REMOTE_NOTIFICATION_RESULT value, use messaging().REMOTE_NOTIFICATION_RESULT' - ); - } - - FirebaseMessaging.finishRemoteNotification( - this._completionHandlerId, - result - ); - return; - case NOTIFICATION_TYPE.NotificationResponse: - FirebaseMessaging.finishNotificationResponse(this._completionHandlerId); - return; - case NOTIFICATION_TYPE.WillPresent: - result = - result || - (this.show_in_foreground - ? WILL_PRESENT_RESULT.All - : WILL_PRESENT_RESULT.None); - if (!Object.values(WILL_PRESENT_RESULT).includes(result)) { - throw new Error( - 'Invalid WILL_PRESENT_RESULT value, use messaging().WILL_PRESENT_RESULT' - ); - } - - FirebaseMessaging.finishWillPresentNotification( - this._completionHandlerId, - result - ); - break; - default: - } - } -} +const NATIVE_EVENTS = [ + 'messaging_message_received', + 'messaging_token_refreshed', +]; export const MODULE_NAME = 'RNFirebaseMessaging'; export const NAMESPACE = 'messaging'; @@ -104,204 +79,139 @@ export default class Messaging extends ModuleBase { multiApp: false, namespace: NAMESPACE, }); + + SharedEventEmitter.addListener( + // sub to internal native event - this fans out to + // public event name: onMessage + 'messaging_message_received', + (message: Message) => { + SharedEventEmitter.emit('onMessage', message); + } + ); + + SharedEventEmitter.addListener( + // sub to internal native event - this fans out to + // public event name: onMessage + 'messaging_token_refreshed', + (token: string) => { + SharedEventEmitter.emit('onTokenRefresh', token); + } + ); } - get EVENT_TYPE(): Object { - return EVENT_TYPE; - } - - get NOTIFICATION_TYPE(): Object { - return NOTIFICATION_TYPE; - } - - get REMOTE_NOTIFICATION_RESULT(): Object { - return REMOTE_NOTIFICATION_RESULT; - } - - get WILL_PRESENT_RESULT(): Object { - return WILL_PRESENT_RESULT; - } - - /** - * Returns the notification that triggered application open - * @returns {*} - */ - getInitialNotification(): Promise { - return getNativeModule(this).getInitialNotification(); - } - - /** - * Returns the fcm token for the current device - * @returns {*|Promise.} - */ getToken(): Promise { return getNativeModule(this).getToken(); } - /** - * Reset Instance ID and revokes all tokens. - * @returns {*|Promise.<*>} - */ - deleteInstanceId(): Promise { - return getNativeModule(this).deleteInstanceId(); - } - - /** - * Create and display a local notification - * @param notification - * @returns {*} - */ - createLocalNotification(notification: Object): Promise { - const _notification = Object.assign({}, notification); - _notification.id = _notification.id || new Date().getTime().toString(); - _notification.local_notification = true; - return getNativeModule(this).createLocalNotification(_notification); - } - - /** - * - * @param notification - * @returns {*} - */ - scheduleLocalNotification(notification: Object): Promise { - const _notification = Object.assign({}, notification); - if (!notification.id) - return Promise.reject( - new Error('An id is required to schedule a local notification.') + onMessage(nextOrObserver: OnMessage | OnMessageObserver): () => any { + let listener; + if (isFunction(nextOrObserver)) { + listener = nextOrObserver; + } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { + listener = nextOrObserver.next; + } else { + throw new Error( + 'Messaging.onMessage failed: First argument must be a function or observer object with a `next` function.' ); - _notification.local_notification = true; - return getNativeModule(this).scheduleLocalNotification(_notification); + } + + // TODO: iOS finish + getLogger(this).info('Creating onMessage listener'); + SharedEventEmitter.addListener('onMessage', listener); + + return () => { + getLogger(this).info('Removing onMessage listener'); + SharedEventEmitter.removeListener('onMessage', listener); + }; + } + + onTokenRefresh( + nextOrObserver: OnTokenRefresh | OnTokenRefreshObserver + ): () => any { + let listener; + if (isFunction(nextOrObserver)) { + listener = nextOrObserver; + } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { + listener = nextOrObserver.next; + } else { + throw new Error( + 'Messaging.OnTokenRefresh failed: First argument must be a function or observer object with a `next` function.' + ); + } + + getLogger(this).info('Creating onTokenRefresh listener'); + SharedEventEmitter.addListener('onTokenRefresh', listener); + + return () => { + getLogger(this).info('Removing onTokenRefresh listener'); + SharedEventEmitter.removeListener('onTokenRefresh', listener); + }; + } + + // TODO: Permission structure? + requestPermission(): Promise { + return getNativeModule(this).requestPermission(); } /** - * Returns an array of all scheduled notifications - * @returns {Promise.} + * NON WEB-SDK METHODS */ - getScheduledLocalNotifications(): Promise { - return getNativeModule(this).getScheduledLocalNotifications(); + getBadge(): Promise { + return getNativeModule(this).getBadge(); } - /** - * Cancel a local notification by id - using '*' will cancel - * all local notifications. - * @param id - * @returns {*} - */ - cancelLocalNotification(id: string): Promise { - if (!id) return Promise.reject(new Error('Missing notification id')); - if (id === '*') return getNativeModule(this).cancelAllLocalNotifications(); - return getNativeModule(this).cancelLocalNotification(id); + getInitialMessage(): Promise { + return getNativeModule(this).getInitialMessage(); } - /** - * Remove a delivered notification - using '*' will remove - * all delivered notifications. - * @param id - * @returns {*} - */ - removeDeliveredNotification(id: string): Promise { - if (!id) return Promise.reject(new Error('Missing notification id')); - if (id === '*') - return getNativeModule(this).removeAllDeliveredNotifications(); - return getNativeModule(this).removeDeliveredNotification(id); + sendMessage(remoteMessage: RemoteMessage): Promise { + return getNativeModule(this).send(remoteMessage); } - /** - * Request notification permission - * @platforms ios - * @returns {*|Promise.<*>} - */ - requestPermissions(): Promise { - return getNativeModule(this).requestPermissions(); + setBadge(badge: number): void { + getNativeModule(this).setBadge(badge); } - /** - * Set notification count badge number - * @param n - */ - setBadgeNumber(n: number): void { - getNativeModule(this).setBadgeNumber(n); - } - - /** - * set notification count badge number - * @returns {Promise.} - */ - getBadgeNumber(): Promise { - return getNativeModule(this).getBadgeNumber(); - } - - /** - * Subscribe to messages / notifications - * @param listener - * @returns {*} - */ - onMessage(listener: Object => any): () => any { - const rnListener = SharedEventEmitter.addListener( - EVENT_TYPE.Notification, - async event => { - const data = { - ...event, - finish, - }; - await listener(data); - - if (!data._finishCalled) { - data.finish(); - } - } - ); - return () => rnListener.remove(); - } - - /** - * Subscribe to token refresh events - * @param listener - * @returns {*} - */ - onTokenRefresh(listener: string => any): () => any { - const rnListener = SharedEventEmitter.addListener( - EVENT_TYPE.RefreshToken, - listener - ); - return () => rnListener.remove(); - } - - /** - * Subscribe to a topic - * @param topic - */ subscribeToTopic(topic: string): void { getNativeModule(this).subscribeToTopic(topic); } - /** - * Unsubscribe from a topic - * @param topic - */ unsubscribeFromTopic(topic: string): void { getNativeModule(this).unsubscribeFromTopic(topic); } /** - * Send an upstream message - * @param remoteMessage + * KNOWN UNSUPPORTED METHODS */ - send(remoteMessage: RemoteMessage): Promise { - if (!(remoteMessage instanceof RemoteMessage)) { - throw new Error( - 'messaging().send requires an instance of RemoteMessage as the first argument.' - ); - } - return getNativeModule(this).send(remoteMessage.toJSON()); + deleteToken() { + throw new Error( + INTERNALS.STRINGS.ERROR_UNSUPPORTED_MODULE_METHOD( + 'messaging', + 'deleteToken' + ) + ); + } + + setBackgroundMessageHandler() { + throw new Error( + INTERNALS.STRINGS.ERROR_UNSUPPORTED_MODULE_METHOD( + 'messaging', + 'setBackgroundMessageHandler' + ) + ); + } + + useServiceWorker() { + throw new Error( + INTERNALS.STRINGS.ERROR_UNSUPPORTED_MODULE_METHOD( + 'messaging', + 'useServiceWorker' + ) + ); } } export const statics = { - EVENT_TYPE, - NOTIFICATION_TYPE, - REMOTE_NOTIFICATION_RESULT, - WILL_PRESENT_RESULT, - RemoteMessage, + // RemoteMessage, }; diff --git a/lib/modules/messaging/messagingIndex.js b/lib/modules/messaging/messagingIndex.js deleted file mode 100644 index 9d7f4247..00000000 --- a/lib/modules/messaging/messagingIndex.js +++ /dev/null @@ -1,186 +0,0 @@ -/** - * @flow - * Messaging (FCM) representation wrapper - */ -import { SharedEventEmitter } from '../../utils/events'; -import INTERNALS from '../../utils/internals'; -import { getLogger } from '../../utils/log'; -import ModuleBase from '../../utils/ModuleBase'; -import { getNativeModule } from '../../utils/native'; -import { isFunction, isObject } from '../../utils'; - -import type App from '../core/firebase-app'; - -type Message = { - // TODO -}; - -type OnMessage = Message => any; - -type OnMessageObserver = { - next: OnMessage, -}; - -type OnTokenRefresh = String => any; - -type OnTokenRefreshObserver = { - next: OnTokenRefresh, -}; - -type RemoteMessage = { - // TODO -}; - -const NATIVE_EVENTS = [ - 'messaging_message_received', - 'messaging_token_refreshed', -]; - -export const MODULE_NAME = 'NewRNFirebaseMessaging'; -export const NAMESPACE = 'newmessaging'; - -/** - * @class Messaging - */ -export default class Messaging extends ModuleBase { - constructor(app: App) { - super(app, { - events: NATIVE_EVENTS, - moduleName: MODULE_NAME, - multiApp: false, - namespace: NAMESPACE, - }); - - SharedEventEmitter.addListener( - // sub to internal native event - this fans out to - // public event name: onMessage - 'messaging_message_received', - (message: Message) => { - SharedEventEmitter.emit('onMessage', message); - } - ); - - SharedEventEmitter.addListener( - // sub to internal native event - this fans out to - // public event name: onMessage - 'messaging_token_refreshed', - (token: string) => { - SharedEventEmitter.emit('onTokenRefresh', token); - } - ); - } - - deleteToken(token: string): Promise { - return getNativeModule(this).deleteToken(token); - } - - getToken(): Promise { - return getNativeModule(this).getToken(); - } - - onMessage(nextOrObserver: OnMessage | OnMessageObserver): () => any { - let listener; - if (isFunction(nextOrObserver)) { - listener = nextOrObserver; - } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { - listener = nextOrObserver.next; - } else { - throw new Error( - 'Messaging.onMessage failed: First argument must be a function or observer object with a `next` function.' - ); - } - - // TODO: iOS finish - getLogger(this).info('Creating onMessage listener'); - SharedEventEmitter.addListener('onMessage', listener); - - return () => { - getLogger(this).info('Removing onMessage listener'); - SharedEventEmitter.removeListener('onMessage', listener); - }; - } - - onTokenRefresh( - nextOrObserver: OnTokenRefresh | OnTokenRefreshObserver - ): () => any { - let listener; - if (isFunction(nextOrObserver)) { - listener = nextOrObserver; - } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { - listener = nextOrObserver.next; - } else { - throw new Error( - 'Messaging.OnTokenRefresh failed: First argument must be a function or observer object with a `next` function.' - ); - } - - getLogger(this).info('Creating onTokenRefresh listener'); - SharedEventEmitter.addListener('onTokenRefresh', listener); - - return () => { - getLogger(this).info('Removing onTokenRefresh listener'); - SharedEventEmitter.removeListener('onTokenRefresh', listener); - }; - } - - requestPermission(): Promise { - return getNativeModule(this).requestPermission(); - } - - /** - * NON WEB-SDK METHODS - */ - deleteInstanceId(): Promise { - return getNativeModule(this).deleteInstanceId(); - } - - getBadgeNumber(): Promise { - return getNativeModule(this).getBadgeNumber(); - } - - getInitialMessage(): Promise { - return getNativeModule(this).getInitialMessage(); - } - - sendMessage(remoteMessage: RemoteMessage): Promise { - return getNativeModule(this).send(remoteMessage); - } - - setBadgeNumber(badge: number): void { - getNativeModule(this).setBadgeNumber(badge); - } - - subscribeToTopic(topic: string): void { - getNativeModule(this).subscribeToTopic(topic); - } - - unsubscribeFromTopic(topic: string): void { - getNativeModule(this).unsubscribeFromTopic(topic); - } - - /** - * KNOWN UNSUPPORTED METHODS - */ - - setBackgroundMessageHandler() { - throw new Error( - INTERNALS.STRINGS.ERROR_UNSUPPORTED_MODULE_METHOD( - 'messaging', - 'setBackgroundMessageHandler' - ) - ); - } - - useServiceWorker() { - throw new Error( - INTERNALS.STRINGS.ERROR_UNSUPPORTED_MODULE_METHOD( - 'messaging', - 'useServiceWorker' - ) - ); - } -} - -export const statics = { - // RemoteMessage, -}; diff --git a/lib/modules/notifications/notificationsIndex.js b/lib/modules/notifications/index.js similarity index 93% rename from lib/modules/notifications/notificationsIndex.js rename to lib/modules/notifications/index.js index 9620292f..94b1753c 100644 --- a/lib/modules/notifications/notificationsIndex.js +++ b/lib/modules/notifications/index.js @@ -108,19 +108,24 @@ export default class Notifications extends ModuleBase { } /** - * Remove a delivered notification - using '*' will remove - * all delivered notifications. + * Remove a delivered notification. * @param id * @returns {*} */ removeDeliveredNotification(id: string): Promise { if (!id) return Promise.reject(new Error('Missing notification id')); - if (id === '*') { - return getNativeModule(this).removeAllDeliveredNotifications(); - } return getNativeModule(this).removeDeliveredNotification(id); } + /** + * Remove all delivered notifications. + * @param id + * @returns {*} + */ + removeDeliveredNotifications(): Promise { + return getNativeModule(this).removeDeliveredNotifications(); + } + /** * * @param notification @@ -136,3 +141,5 @@ export default class Notifications extends ModuleBase { return getNativeModule(this).scheduleLocalNotification(_notification); } } + +export const statics = {}; diff --git a/lib/types/index.js b/lib/types/index.js index ce894079..3b94966d 100644 --- a/lib/types/index.js +++ b/lib/types/index.js @@ -15,12 +15,14 @@ import type Database from '../modules/database'; import { typeof statics as DatabaseStatics } from '../modules/database'; import type Firestore from '../modules/firestore'; import { typeof statics as FirestoreStatics } from '../modules/firestore'; +import type InstanceId from '../modules/instanceid'; +import { typeof statics as InstanceIdStatics } from '../modules/instanceid'; import type Links from '../modules/links'; import { typeof statics as LinksStatics } from '../modules/links'; import type Messaging from '../modules/messaging'; import { typeof statics as MessagingStatics } from '../modules/messaging'; -import type NewMessaging from '../modules/messaging/messagingIndex'; -import { typeof statics as NewMessagingStatics } from '../modules/messaging/messagingIndex'; +import type Notifications from '../modules/notifications'; +import { typeof statics as NotificationsStatics } from '../modules/notifications'; import type ModuleBase from '../utils/ModuleBase'; import type Performance from '../modules/perf'; import { typeof statics as PerformanceStatics } from '../modules/perf'; @@ -58,9 +60,9 @@ export type FirebaseModuleName = | 'RNFirebaseCrashlytics' | 'RNFirebaseDatabase' | 'RNFirebaseFirestore' + | 'RNFirebaseInstanceId' | 'RNFirebaseLinks' | 'RNFirebaseMessaging' - | 'NewRNFirebaseMessaging' | 'RNFirebaseNotifications' | 'RNFirebasePerformance' | 'RNFirebaseStorage' @@ -75,9 +77,9 @@ export type FirebaseNamespace = | 'crashlytics' | 'database' | 'firestore' + | 'instanceid' | 'links' | 'messaging' - | 'newmessaging' | 'notifications' | 'perf' | 'storage' @@ -209,6 +211,13 @@ export type FirestoreWriteOptions = { merge?: boolean, }; +/* InstanceId types */ + +export type InstanceIdModule = { + (): InstanceId, + nativeModuleExists: boolean, +} & InstanceIdStatics; + /* Links types */ export type LinksModule = { @@ -223,10 +232,12 @@ export type MessagingModule = { nativeModuleExists: boolean, } & MessagingStatics; -export type NewMessagingModule = { - (): NewMessaging, +/* Notifications types */ + +export type NotificationsModule = { + (): Notifications, nativeModuleExists: boolean, -} & NewMessagingStatics; +} & NotificationsStatics; /* Performance types */ diff --git a/tests/android/app/src/main/AndroidManifest.xml b/tests/android/app/src/main/AndroidManifest.xml index d055d381..0d75e277 100644 --- a/tests/android/app/src/main/AndroidManifest.xml +++ b/tests/android/app/src/main/AndroidManifest.xml @@ -13,10 +13,10 @@ android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" - android:launchMode="singleTop" + android:launchMode="singleTask" android:theme="@style/AppTheme"> @@ -24,7 +24,7 @@ - + @@ -34,6 +34,7 @@ android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:label="@string/app_name" + android:launchMode="singleTop" android:windowSoftInputMode="adjustResize"> @@ -42,5 +43,4 @@ - diff --git a/tests/android/app/src/main/java/com/reactnativefirebasedemo/MainApplication.java b/tests/android/app/src/main/java/com/reactnativefirebasedemo/MainApplication.java index e1b6fe51..ae9de9cd 100644 --- a/tests/android/app/src/main/java/com/reactnativefirebasedemo/MainApplication.java +++ b/tests/android/app/src/main/java/com/reactnativefirebasedemo/MainApplication.java @@ -12,6 +12,7 @@ import io.invertase.firebase.crash.RNFirebaseCrashPackage; import io.invertase.firebase.database.RNFirebaseDatabasePackage; import io.invertase.firebase.fabric.crashlytics.RNFirebaseCrashlyticsPackage; import io.invertase.firebase.firestore.RNFirebaseFirestorePackage; +import io.invertase.firebase.instanceid.RNFirebaseInstanceIdPackage; import io.invertase.firebase.links.RNFirebaseLinksPackage; import io.invertase.firebase.messaging.RNFirebaseMessagingPackage; import io.invertase.firebase.perf.RNFirebasePerformancePackage; @@ -47,6 +48,7 @@ public class MainApplication extends Application implements ReactApplication { new RNFirebaseCrashlyticsPackage(), new RNFirebaseDatabasePackage(), new RNFirebaseFirestorePackage(), + new RNFirebaseInstanceIdPackage(), new RNFirebaseLinksPackage(), new RNFirebaseMessagingPackage(), new RNFirebasePerformancePackage(), diff --git a/tests/src/firebase.js b/tests/src/firebase.js index f78cfe1d..fcdad95f 100644 --- a/tests/src/firebase.js +++ b/tests/src/firebase.js @@ -7,19 +7,30 @@ import DatabaseContents from './tests/support/DatabaseContents'; RNfirebase.database.enableLogging(true); RNfirebase.firestore.enableLogging(true); -RNfirebase.newmessaging() - .requestPermission() - .then(response => { - console.log('requestPermission:', response); - RNfirebase.newmessaging() - .getToken() - .then(token => { - console.log('token: ', token); - }); - }) - .catch(error => { - console.error('requestPermission:', error); - }); +RNfirebase.messaging().onMessage(message => { + console.log('got new message: ', message); +}); + +RNfirebase.messaging().onTokenRefresh(token => { + console.log('got new token: ', token); +}); + +const init = async () => { + try { + await RNfirebase.messaging().requestPermission(); + const instanceid = await RNfirebase.instanceid().get(); + console.log('instanceid: ', instanceid); + const token = await RNfirebase.messaging().getToken(); + console.log('token: ', token); + const initialMessage = await RNfirebase.messaging().getInitialMessage(); + console.log('initial message: ', initialMessage); + // RNfirebase.instanceid().delete(); + } catch (error) { + console.error('messaging init error:', error); + } +}; + +init(); const config = { apiKey: 'AIzaSyDnVqNhxU0Biit9nCo4RorAh5ulQQwko3E', From a460d8f8097f328b47a6dd83d915440a856851b1 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 2 Feb 2018 12:05:51 +0000 Subject: [PATCH 03/77] [fcm] Work in progress iOS message support --- .../messaging/RNFirebaseMessaging.java | 2 +- ios/RNFirebase/RNFirebaseUtil.h | 4 +- ios/RNFirebase/RNFirebaseUtil.m | 4 +- .../messaging/RNFirebaseMessaging.h | 5 +- .../messaging/RNFirebaseMessaging.m | 140 ++++++++++++++++-- .../notifications/RNFirebaseNotifications.m | 19 +++ .../ios/ReactNativeFirebaseDemo/AppDelegate.m | 11 ++ 7 files changed, 170 insertions(+), 15 deletions(-) diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java index 9292eb2d..5788201f 100644 --- a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java +++ b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java @@ -304,7 +304,7 @@ public class RNFirebaseMessaging extends ReactContextBaseJavaModule implements A public void onReceive(Context context, Intent intent) { if (getReactApplicationContext().hasActiveCatalystInstance()) { String token = FirebaseInstanceId.getInstance().getToken(); - Log.d(TAG, "Received new token: " + token); + Log.d(TAG, "Received new FCM token: " + token); Utils.sendEvent(getReactApplicationContext(), "messaging_token_refreshed", token); } diff --git a/ios/RNFirebase/RNFirebaseUtil.h b/ios/RNFirebase/RNFirebaseUtil.h index 8191c387..9a4a49f2 100644 --- a/ios/RNFirebase/RNFirebaseUtil.h +++ b/ios/RNFirebase/RNFirebaseUtil.h @@ -10,8 +10,8 @@ + (FIRApp *)getApp:(NSString *)appDisplayName; + (NSString *)getAppName:(NSString *)appDisplayName; + (NSString *)getAppDisplayName:(NSString *)appName; -+ (void)sendJSEvent:(RCTEventEmitter *)emitter name:(NSString *)name body:(NSDictionary *)body; -+ (void)sendJSEventWithAppName:(RCTEventEmitter *)emitter app:(FIRApp *)app name:(NSString *)name body:(NSDictionary *)body; ++ (void)sendJSEvent:(RCTEventEmitter *)emitter name:(NSString *)name body:(id)body; ++ (void)sendJSEventWithAppName:(RCTEventEmitter *)emitter app:(FIRApp *)app name:(NSString *)name body:(id)body; @end diff --git a/ios/RNFirebase/RNFirebaseUtil.m b/ios/RNFirebase/RNFirebaseUtil.m index c97bb636..2c53fbca 100644 --- a/ios/RNFirebase/RNFirebaseUtil.m +++ b/ios/RNFirebase/RNFirebaseUtil.m @@ -24,7 +24,7 @@ static NSString *const DEFAULT_APP_NAME = @"__FIRAPP_DEFAULT"; return appName; } -+ (void)sendJSEvent:(RCTEventEmitter *)emitter name:(NSString *)name body:(NSDictionary *)body { ++ (void)sendJSEvent:(RCTEventEmitter *)emitter name:(NSString *)name body:(id)body { @try { // TODO: Temporary fix for https://github.com/invertase/react-native-firebase/issues/233 // until a better solution comes around @@ -36,7 +36,7 @@ static NSString *const DEFAULT_APP_NAME = @"__FIRAPP_DEFAULT"; } } -+ (void)sendJSEventWithAppName:(RCTEventEmitter *)emitter app:(FIRApp *)app name:(NSString *)name body:(NSDictionary *)body { ++ (void)sendJSEventWithAppName:(RCTEventEmitter *)emitter app:(FIRApp *)app name:(NSString *)name body:(id)body { // Add the appName to the body NSMutableDictionary *newBody = [body mutableCopy]; newBody[@"appName"] = [RNFirebaseUtil getAppDisplayName:app.name]; diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.h b/ios/RNFirebase/messaging/RNFirebaseMessaging.h index c493eabf..2bf10470 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.h +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.h @@ -9,8 +9,11 @@ @interface RNFirebaseMessaging : RCTEventEmitter -#if !TARGET_OS_TV ++ (_Nonnull instancetype)instance; +#if !TARGET_OS_TV +- (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo; +- (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo fetchCompletionHandler:(void (^_Nonnull)(UIBackgroundFetchResult))completionHandler; #endif @end diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index 0bffc7d6..f2a7e2c5 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -11,32 +11,155 @@ #import #import -@interface RNFirebaseMessaging () +// For iOS 10 we need to implement UNUserNotificationCenterDelegate to receive display +// notifications via APNS +#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 +@import UserNotifications; +@interface RNFirebaseMessaging () @end +#endif @implementation RNFirebaseMessaging +static RNFirebaseMessaging *theRNFirebaseMessaging = nil; + ++ (nonnull instancetype)instance { + return theRNFirebaseMessaging; +} + RCT_EXPORT_MODULE() - (id)init { self = [super init]; if (self != nil) { NSLog(@"Setting up RNFirebaseMessaging instance"); - [self initialiseMessaging]; + [self configure]; } return self; } -- (void)initialiseMessaging { +- (void)configure { + // Set as delegate for FIRMessaging + [FIRMessaging messaging].delegate = self; + // Establish Firebase managed data channel [FIRMessaging messaging].shouldEstablishDirectChannel = YES; + + // If we're on iOS 10 then we need to set this as a delegate for the UNUserNotificationCenter +#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + [UNUserNotificationCenter currentNotificationCenter].delegate = self; +#endif + + // Set static instance for use from AppDelegate + static dispatch_once_t once; + dispatch_once(&once, ^{ + theRNFirebaseMessaging = self; + }); } - (void)dealloc { } +// ** AppDelegate methods ** + +// Listen for background messages +- (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo { + BOOL isFromBackground = (RCTSharedApplication().applicationState == UIApplicationStateInactive); + + // TODO: Format data before send + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:userInfo]; +} + +// Listen for background messages +- (void)didReceiveRemoteNotification:(NSDictionary *)userInfo + fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { + BOOL isFromBackground = (RCTSharedApplication().applicationState == UIApplicationStateInactive); + + // TODO: Format data before send + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:userInfo]; +} + +// ** UNUserNotificationCenterDelegate methods ** +#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 +// Handle incoming notification messages while app is in the foreground. +- (void)userNotificationCenter:(UNUserNotificationCenter *)center + willPresentNotification:(UNNotification *)notification + withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler { + NSDictionary *userInfo = notification.request.content.userInfo; + + // TODO: Format data before send + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:userInfo]; + + // TODO: Change this to your preferred presentation option + completionHandler(UNNotificationPresentationOptionNone); +} + +// Handle notification messages after display notification is tapped by the user. +- (void)userNotificationCenter:(UNUserNotificationCenter *)center +didReceiveNotificationResponse:(UNNotificationResponse *)response +#if defined(__IPHONE_11_0) + withCompletionHandler:(void(^)(void))completionHandler { +#else + withCompletionHandler:(void(^)())completionHandler { +#endif + NSDictionary *userInfo = response.notification.request.content.userInfo; + + // TODO: Format data before send + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:userInfo]; + + completionHandler(); +} + +#endif + +// ** FIRMessagingDelegate methods ** + +// Listen for FCM tokens +- (void)messaging:(FIRMessaging *)messaging didReceiveRegistrationToken:(NSString *)fcmToken { + NSLog(@"Received new FCM token: %@", fcmToken); + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_TOKEN_REFRESHED body:fcmToken]; +} + +// Listen for data messages in the foreground +- (void)applicationReceivedRemoteMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage { + NSDictionary *appData = remoteMessage.appData; + + NSMutableDictionary *message = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; + for (id k1 in appData) { + if ([k1 isEqualToString:@"collapse_key"]) { + message[@"collapseKey"] = appData[@"collapse_key"]; + } else if ([k1 isEqualToString:@"from"]) { + message[@"from"] = appData[@"from"]; + } else if ([k1 isEqualToString:@"notification"]) { + NSDictionary *n = appData[@"notification"]; + NSMutableDictionary *notification = [[NSMutableDictionary alloc] init]; + for (id k2 in appData[@"notification"]) { + if ([k2 isEqualToString:@"badge"]) { + notification[@"badge"] = n[@"badge"]; + } else if ([k2 isEqualToString:@"body"]) { + notification[@"body"] = n[@"body"]; + } else if ([k2 isEqualToString:@"sound"]) { + notification[@"sound"] = n[@"sound"]; + } else if ([k2 isEqualToString:@"title"]) { + notification[@"title"] = n[@"title"]; + } else { + NSLog(@"Unknown notification key: %@", k2); + } + } + message[@"notification"] = notification; + } else { + data[k1] = appData[k1]; + } + } + message[@"data"] = data; + message[@"openedFromTray"] = @(false); + + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; +} + // ** Start React Module methods ** RCT_EXPORT_METHOD(getToken:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { resolve([[FIRInstanceID instanceID] token]); @@ -56,7 +179,6 @@ RCT_EXPORT_METHOD(requestPermission:(RCTPromiseResolveBlock)resolve rejecter:(RC // TODO: Is there something we can listen for? resolve(@{@"status":@"unknown"}); } else { - // iOS 10 or later #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 // For iOS 10 display notification (sent via APNS) UNAuthorizationOptions authOptions = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge; @@ -81,12 +203,12 @@ RCT_EXPORT_METHOD(getBadge: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromise resolve(@([RCTSharedApplication() applicationIconBadgeNumber])); } -RCT_EXPORT_METHOD(getInitialNotification:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){ - UILocalNotification *localUserInfo = [self bridge].launchOptions[UIApplicationLaunchOptionsLocalNotificationKey]; - if (localUserInfo) { - resolve([[localUserInfo userInfo] copy]); +RCT_EXPORT_METHOD(getInitialMessage:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){ + NSDictionary *notification = [self bridge].launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; + if (notification) { + resolve([notification copy]); } else { - resolve([[self bridge].launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey] copy]); + resolve(nil); } } diff --git a/ios/RNFirebase/notifications/RNFirebaseNotifications.m b/ios/RNFirebase/notifications/RNFirebaseNotifications.m index 5f149eea..daf2cf06 100644 --- a/ios/RNFirebase/notifications/RNFirebaseNotifications.m +++ b/ios/RNFirebase/notifications/RNFirebaseNotifications.m @@ -5,6 +5,25 @@ @implementation RNFirebaseNotifications RCT_EXPORT_MODULE(); +RCT_EXPORT_METHOD(getInitialNotification:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){ + UILocalNotification *localUserInfo = [self bridge].launchOptions[UIApplicationLaunchOptionsLocalNotificationKey]; + if (localUserInfo) { + // TODO: Proper format + resolve([[localUserInfo userInfo] copy]); + } else { + resolve(nil); + } +} + +- (NSArray *)supportedEvents { + return @[]; +} + ++ (BOOL)requiresMainQueueSetup +{ + return YES; +} + @end #else diff --git a/tests/ios/ReactNativeFirebaseDemo/AppDelegate.m b/tests/ios/ReactNativeFirebaseDemo/AppDelegate.m index 49716fe4..d266a798 100644 --- a/tests/ios/ReactNativeFirebaseDemo/AppDelegate.m +++ b/tests/ios/ReactNativeFirebaseDemo/AppDelegate.m @@ -12,12 +12,14 @@ #import #import #import +#import @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [FIRApp configure]; + NSURL *jsCodeLocation; jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; @@ -36,4 +38,13 @@ return YES; } +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo { + [[RNFirebaseMessaging instance] didReceiveRemoteNotification:userInfo]; +} + +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo +fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { + [[RNFirebaseMessaging instance] didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; +} + @end From 3e7a1efe4ec0441360b5f18ee1de30fac8363db9 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 2 Feb 2018 17:16:55 +0000 Subject: [PATCH 04/77] [fcm] iOS 10 support for messaging --- .../messaging/RNFirebaseMessaging.m | 192 +++++++++++++----- 1 file changed, 140 insertions(+), 52 deletions(-) diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index f2a7e2c5..1543447d 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -47,9 +47,9 @@ RCT_EXPORT_MODULE() [FIRMessaging messaging].shouldEstablishDirectChannel = YES; // If we're on iOS 10 then we need to set this as a delegate for the UNUserNotificationCenter -#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 [UNUserNotificationCenter currentNotificationCenter].delegate = self; -#endif + #endif // Set static instance for use from AppDelegate static dispatch_once_t once; @@ -59,7 +59,7 @@ RCT_EXPORT_MODULE() } - (void)dealloc { - + } // ** AppDelegate methods ** @@ -87,10 +87,9 @@ RCT_EXPORT_MODULE() - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler { - NSDictionary *userInfo = notification.request.content.userInfo; + NSDictionary *message = [self parseUNNotification:notification openedFromTray:false]; - // TODO: Format data before send - [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:userInfo]; + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; // TODO: Change this to your preferred presentation option completionHandler(UNNotificationPresentationOptionNone); @@ -104,16 +103,16 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response #else withCompletionHandler:(void(^)())completionHandler { #endif - NSDictionary *userInfo = response.notification.request.content.userInfo; - + NSDictionary *userInfo = [self parseUNNotification:response.notification openedFromTray:true]; + // TODO: Format data before send [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:userInfo]; - + completionHandler(); } - + #endif - + // ** FIRMessagingDelegate methods ** // Listen for FCM tokens @@ -124,42 +123,11 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response // Listen for data messages in the foreground - (void)applicationReceivedRemoteMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage { - NSDictionary *appData = remoteMessage.appData; - - NSMutableDictionary *message = [[NSMutableDictionary alloc] init]; - NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; - for (id k1 in appData) { - if ([k1 isEqualToString:@"collapse_key"]) { - message[@"collapseKey"] = appData[@"collapse_key"]; - } else if ([k1 isEqualToString:@"from"]) { - message[@"from"] = appData[@"from"]; - } else if ([k1 isEqualToString:@"notification"]) { - NSDictionary *n = appData[@"notification"]; - NSMutableDictionary *notification = [[NSMutableDictionary alloc] init]; - for (id k2 in appData[@"notification"]) { - if ([k2 isEqualToString:@"badge"]) { - notification[@"badge"] = n[@"badge"]; - } else if ([k2 isEqualToString:@"body"]) { - notification[@"body"] = n[@"body"]; - } else if ([k2 isEqualToString:@"sound"]) { - notification[@"sound"] = n[@"sound"]; - } else if ([k2 isEqualToString:@"title"]) { - notification[@"title"] = n[@"title"]; - } else { - NSLog(@"Unknown notification key: %@", k2); - } - } - message[@"notification"] = notification; - } else { - data[k1] = appData[k1]; - } - } - message[@"data"] = data; - message[@"openedFromTray"] = @(false); + NSDictionary *message = [self parseFIRMessagingRemoteMessage:remoteMessage openedFromTray:false]; [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; } - + // ** Start React Module methods ** RCT_EXPORT_METHOD(getToken:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { resolve([[FIRInstanceID instanceID] token]); @@ -196,7 +164,7 @@ RCT_EXPORT_METHOD(requestPermission:(RCTPromiseResolveBlock)resolve rejecter:(RC [RCTSharedApplication() registerForRemoteNotifications]; }); } - + // Non Web SDK methods RCT_EXPORT_METHOD(getBadge: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { @@ -206,15 +174,16 @@ 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) { - resolve([notification copy]); + NSDictionary *message = [self parseUserInfo:notification clickAction:nil openedFromTray:true]; + resolve(message); } else { resolve(nil); } } -RCT_EXPORT_METHOD(setBadge: (NSInteger*) number) { +RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { dispatch_async(dispatch_get_main_queue(), ^{ - [RCTSharedApplication() setApplicationIconBadgeNumber:*number]; + [RCTSharedApplication() setApplicationIconBadgeNumber:number]; }); } @@ -225,7 +194,126 @@ RCT_EXPORT_METHOD(subscribeToTopic: (NSString*) topic) { RCT_EXPORT_METHOD(unsubscribeFromTopic: (NSString*) topic) { [[FIRMessaging messaging] unsubscribeFromTopic:topic]; } - + +// ** Start internals ** + +- (NSDictionary*)parseFIRMessagingRemoteMessage:(FIRMessagingRemoteMessage *)remoteMessage + openedFromTray:(bool)openedFromTray { + NSDictionary *appData = remoteMessage.appData; + + NSMutableDictionary *message = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; + for (id k1 in appData) { + if ([k1 isEqualToString:@"collapse_key"]) { + message[@"collapseKey"] = appData[@"collapse_key"]; + } else if ([k1 isEqualToString:@"from"]) { + message[@"from"] = appData[k1]; + } else if ([k1 isEqualToString:@"notification"]) { + NSDictionary *n = appData[k1]; + NSMutableDictionary *notification = [[NSMutableDictionary alloc] init]; + for (id k2 in appData[@"notification"]) { + if ([k2 isEqualToString:@"badge"]) { + notification[@"badge"] = n[k2]; + } else if ([k2 isEqualToString:@"body"]) { + notification[@"body"] = n[k2]; + } else if ([k2 isEqualToString:@"sound"]) { + notification[@"sound"] = n[k2]; + } else if ([k2 isEqualToString:@"title"]) { + notification[@"title"] = n[k2]; + } else { + NSLog(@"Unknown notification key: %@", k2); + } + } + message[@"notification"] = notification; + } else { + // Assume custom data key + data[k1] = appData[k1]; + } + } + message[@"data"] = data; + message[@"openedFromTray"] = @(false); + + return message; +} + +- (NSDictionary*)parseUNNotification:(UNNotification *)notification + openedFromTray:(bool)openedFromTray { + NSDictionary *userInfo = notification.request.content.userInfo; + NSString *clickAction = notification.request.content.categoryIdentifier; + + return [self parseUserInfo:userInfo clickAction:clickAction openedFromTray:openedFromTray]; +} + +- (NSDictionary*)parseUserInfo:(NSDictionary *)userInfo + clickAction:(NSString *) clickAction + openedFromTray:(bool)openedFromTray { + NSMutableDictionary *message = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *notif = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; + + for (id k1 in userInfo) { + if ([k1 isEqualToString:@"aps"]) { + NSDictionary *aps = userInfo[k1]; + for (id k2 in aps) { + if ([k2 isEqualToString:@"alert"]) { + NSDictionary *alert = aps[k2]; + for (id k3 in alert) { + if ([k3 isEqualToString:@"body"]) { + notif[@"body"] = alert[k3]; + } else if ([k3 isEqualToString:@"loc-args"]) { + notif[@"bodyLocalizationArgs"] = alert[k3]; + } else if ([k3 isEqualToString:@"loc-key"]) { + notif[@"bodyLocalizationKey"] = alert[k3]; + } else if ([k3 isEqualToString:@"subtitle"]) { + notif[@"subtitle"] = alert[k3]; + } else if ([k3 isEqualToString:@"title"]) { + notif[@"title"] = alert[k3]; + } else if ([k3 isEqualToString:@"title-loc-args"]) { + notif[@"titleLocalizationArgs"] = alert[k3]; + } else if ([k3 isEqualToString:@"title-loc-key"]) { + notif[@"titleLocalizationKey"] = alert[k3]; + } else { + NSLog(@"Unknown alert key: %@", k2); + } + } + } else if ([k2 isEqualToString:@"badge"]) { + notif[@"badge"] = aps[k2]; + } else if ([k2 isEqualToString:@"category"]) { + notif[@"clickAction"] = aps[k2]; + } else if ([k2 isEqualToString:@"sound"]) { + notif[@"sound"] = aps[k2]; + } else { + NSLog(@"Unknown aps key: %@", k2); + } + } + } else if ([k1 isEqualToString:@"gcm.message_id"]) { + message[@"messageId"] = userInfo[k1]; + } else if ([k1 isEqualToString:@"google.c.a.ts"]) { + message[@"sentTime"] = userInfo[k1]; + } else if ([k1 isEqualToString:@"gcm.n.e"] + || [k1 isEqualToString:@"gcm.notification.sound2"] + || [k1 isEqualToString:@"google.c.a.c_id"] + || [k1 isEqualToString:@"google.c.a.c_l"] + || [k1 isEqualToString:@"google.c.a.e"] + || [k1 isEqualToString:@"google.c.a.udt"]) { + // Ignore known keys + } else { + // Assume custom data + data[k1] = userInfo[k1]; + } + } + + if (!notif[@"clickAction"] && clickAction) { + notif[@"clickAction"] = clickAction; + } + + message[@"data"] = data; + message[@"notification"] = notif; + message[@"openedFromTray"] = @(openedFromTray); + + return message; +} + - (NSArray *)supportedEvents { return @[MESSAGING_MESSAGE_RECEIVED, MESSAGING_TOKEN_REFRESHED]; } @@ -236,8 +324,8 @@ RCT_EXPORT_METHOD(unsubscribeFromTopic: (NSString*) topic) { } @end - + #else -@implementation RNFirebaseMessaging -@end + @implementation RNFirebaseMessaging + @end #endif From b71a2c7aec8286e81af8c5e602c8febdc7186836 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Mon, 5 Feb 2018 09:18:53 +0000 Subject: [PATCH 05/77] [fcm] iOS 8/9 support --- .../messaging/RNFirebaseMessaging.m | 80 +++++++++++-------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index 1543447d..3b47d6e5 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -47,19 +47,16 @@ RCT_EXPORT_MODULE() [FIRMessaging messaging].shouldEstablishDirectChannel = YES; // If we're on iOS 10 then we need to set this as a delegate for the UNUserNotificationCenter - #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 +#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 [UNUserNotificationCenter currentNotificationCenter].delegate = self; - #endif +#endif // Set static instance for use from AppDelegate - static dispatch_once_t once; - dispatch_once(&once, ^{ - theRNFirebaseMessaging = self; - }); + theRNFirebaseMessaging = self; } - (void)dealloc { - + } // ** AppDelegate methods ** @@ -67,18 +64,20 @@ 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]; - // TODO: Format data before send - [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:userInfo]; + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; } // Listen for background messages - (void)didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { BOOL isFromBackground = (RCTSharedApplication().applicationState == UIApplicationStateInactive); + NSDictionary *message = [self parseUserInfo:userInfo clickAction:nil openedFromTray:isFromBackground]; - // TODO: Format data before send - [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:userInfo]; + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; + + // TODO: FetchCompletionHandler? } // ** UNUserNotificationCenterDelegate methods ** @@ -104,15 +103,15 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler { #endif NSDictionary *userInfo = [self parseUNNotification:response.notification openedFromTray:true]; - - // TODO: Format data before send + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:userInfo]; - + + // TODO: Validate this completionHandler(); } - + #endif - + // ** FIRMessagingDelegate methods ** // Listen for FCM tokens @@ -127,7 +126,7 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; } - + // ** Start React Module methods ** RCT_EXPORT_METHOD(getToken:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { resolve([[FIRInstanceID instanceID] token]); @@ -164,7 +163,7 @@ RCT_EXPORT_METHOD(requestPermission:(RCTPromiseResolveBlock)resolve rejecter:(RC [RCTSharedApplication() registerForRemoteNotifications]; }); } - + // Non Web SDK methods RCT_EXPORT_METHOD(getBadge: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { @@ -194,13 +193,13 @@ RCT_EXPORT_METHOD(subscribeToTopic: (NSString*) topic) { RCT_EXPORT_METHOD(unsubscribeFromTopic: (NSString*) topic) { [[FIRMessaging messaging] unsubscribeFromTopic:topic]; } - + // ** Start internals ** - + - (NSDictionary*)parseFIRMessagingRemoteMessage:(FIRMessagingRemoteMessage *)remoteMessage openedFromTray:(bool)openedFromTray { NSDictionary *appData = remoteMessage.appData; - + NSMutableDictionary *message = [[NSMutableDictionary alloc] init]; NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; for (id k1 in appData) { @@ -209,22 +208,34 @@ RCT_EXPORT_METHOD(unsubscribeFromTopic: (NSString*) topic) { } else if ([k1 isEqualToString:@"from"]) { message[@"from"] = appData[k1]; } else if ([k1 isEqualToString:@"notification"]) { - NSDictionary *n = appData[k1]; - NSMutableDictionary *notification = [[NSMutableDictionary alloc] init]; - for (id k2 in appData[@"notification"]) { + NSDictionary *notification = appData[k1]; + NSMutableDictionary *notif = [[NSMutableDictionary alloc] init]; + for (id k2 in notification) { if ([k2 isEqualToString:@"badge"]) { - notification[@"badge"] = n[k2]; + notif[@"badge"] = notification[k2]; } else if ([k2 isEqualToString:@"body"]) { - notification[@"body"] = n[k2]; + notif[@"body"] = notification[k2]; + } else if ([k2 isEqualToString:@"body_loc_args"]) { + notif[@"bodyLocalizationArgs"] = notification[k2]; + } else if ([k2 isEqualToString:@"body_loc_key"]) { + notif[@"bodyLocalizationKey"] = notification[k2]; + } else if ([k2 isEqualToString:@"click_action"]) { + notif[@"clickAction"] = notification[k2]; } else if ([k2 isEqualToString:@"sound"]) { - notification[@"sound"] = n[k2]; + notif[@"sound"] = notification[k2]; + } else if ([k2 isEqualToString:@"subtitle"]) { + notif[@"subtitle"] = notification[k2]; } else if ([k2 isEqualToString:@"title"]) { - notification[@"title"] = n[k2]; + notif[@"title"] = notification[k2]; + } else if ([k2 isEqualToString:@"title_loc_args"]) { + notif[@"titleLocalizationArgs"] = notification[k2]; + } else if ([k2 isEqualToString:@"title_loc_key"]) { + notif[@"titleLocalizationKey"] = notification[k2]; } else { NSLog(@"Unknown notification key: %@", k2); } } - message[@"notification"] = notification; + message[@"notification"] = notif; } else { // Assume custom data key data[k1] = appData[k1]; @@ -232,7 +243,7 @@ RCT_EXPORT_METHOD(unsubscribeFromTopic: (NSString*) topic) { } message[@"data"] = data; message[@"openedFromTray"] = @(false); - + return message; } @@ -313,7 +324,7 @@ RCT_EXPORT_METHOD(unsubscribeFromTopic: (NSString*) topic) { return message; } - + - (NSArray *)supportedEvents { return @[MESSAGING_MESSAGE_RECEIVED, MESSAGING_TOKEN_REFRESHED]; } @@ -324,8 +335,9 @@ RCT_EXPORT_METHOD(unsubscribeFromTopic: (NSString*) topic) { } @end - + #else - @implementation RNFirebaseMessaging - @end +@implementation RNFirebaseMessaging +@end #endif + From 121a4d4b910ac57483f0e14a75ad54cb6cee7a0c Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Mon, 5 Feb 2018 15:16:07 +0000 Subject: [PATCH 06/77] [fcm] iOS sendMessage support --- ios/RNFirebase/messaging/RNFirebaseMessaging.m | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index 3b47d6e5..6b9ebde3 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -55,10 +55,6 @@ RCT_EXPORT_MODULE() theRNFirebaseMessaging = self; } -- (void)dealloc { - -} - // ** AppDelegate methods ** // Listen for background messages @@ -180,6 +176,20 @@ RCT_EXPORT_METHOD(getInitialMessage:(RCTPromiseResolveBlock)resolve rejecter:(RC } } +RCT_EXPORT_METHOD(sendMessage: (NSDictionary *) message + resolve:(RCTPromiseResolveBlock) resolve + reject:(RCTPromiseRejectBlock) reject) { + if (!message[@"to"]) { + reject(@"messaging/invalid-message", @"The supplied message is missing a 'to' field", nil); + } + NSString *to = message[@"to"]; + NSString *messageId = message[@"messageId"]; + NSNumber *ttl = message[@"ttl"]; + NSDictionary *data = message[@"data"]; + + [[FIRMessaging messaging] sendMessage:data to:to withMessageID:messageId timeToLive:[ttl intValue]]; +} + RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { dispatch_async(dispatch_get_main_queue(), ^{ [RCTSharedApplication() setApplicationIconBadgeNumber:number]; From 93805062f727f1ab6211cef7e34e74b74362ed46 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Mon, 5 Feb 2018 17:45:08 +0000 Subject: [PATCH 07/77] [fcm] iOS 8/9 grant permission feedback --- .../messaging/RNFirebaseMessaging.h | 5 ++ .../messaging/RNFirebaseMessaging.m | 60 +++++++++++++++---- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.h b/ios/RNFirebase/messaging/RNFirebaseMessaging.h index 2bf10470..ea6ed75f 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.h +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.h @@ -11,9 +11,13 @@ + (_Nonnull instancetype)instance; +@property _Nullable RCTPromiseRejectBlock permissionRejecter; +@property _Nullable RCTPromiseResolveBlock permissionResolver; + #if !TARGET_OS_TV - (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo; - (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo fetchCompletionHandler:(void (^_Nonnull)(UIBackgroundFetchResult))completionHandler; +- (void)didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings; #endif @end @@ -24,3 +28,4 @@ #endif #endif + diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index 6b9ebde3..43280ec4 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -55,7 +55,10 @@ RCT_EXPORT_MODULE() theRNFirebaseMessaging = self; } -// ** AppDelegate methods ** +// ******************************************************* +// ** Start AppDelegate methods +// ** iOS 8/9 Only +// ******************************************************* // Listen for background messages - (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo { @@ -76,7 +79,29 @@ RCT_EXPORT_MODULE() // TODO: FetchCompletionHandler? } -// ** UNUserNotificationCenterDelegate methods ** +// Listen for permission response +- (void) didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { + if (notificationSettings.types == UIUserNotificationTypeNone) { + if (_permissionRejecter) { + _permissionRejecter(@"messaging/permission_error", @"Failed to grant permission", nil); + } + } else if (_permissionResolver) { + _permissionResolver(nil); + } + _permissionRejecter = nil; + _permissionResolver = nil; +} + +// ******************************************************* +// ** Finish AppDelegate methods +// ******************************************************* + + +// ******************************************************* +// ** Start UNUserNotificationCenterDelegate methods +// ** iOS 10+ +// ******************************************************* + #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 // Handle incoming notification messages while app is in the foreground. - (void)userNotificationCenter:(UNUserNotificationCenter *)center @@ -107,8 +132,16 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response } #endif + +// ******************************************************* +// ** Finish UNUserNotificationCenterDelegate methods +// ******************************************************* -// ** FIRMessagingDelegate methods ** + +// ******************************************************* +// ** Start FIRMessagingDelegate methods +// ** iOS 8+ +// ******************************************************* // Listen for FCM tokens - (void)messaging:(FIRMessaging *)messaging didReceiveRegistrationToken:(NSString *)fcmToken { @@ -122,6 +155,10 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; } + +// ******************************************************* +// ** Finish FIRMessagingDelegate methods +// ******************************************************* // ** Start React Module methods ** RCT_EXPORT_METHOD(getToken:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { @@ -130,26 +167,26 @@ RCT_EXPORT_METHOD(getToken:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseR RCT_EXPORT_METHOD(requestPermission:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { if (RCTRunningInAppExtension()) { - reject(@"request_permission_unavailable", @"requestPermission is not supported in App Extensions", nil); + reject(@"messaging/request-permission-unavailable", @"requestPermission is not supported in App Extensions", nil); return; } if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { UIUserNotificationType types = (UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge); [RCTSharedApplication() registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:types categories:nil]]; - // Unfortunately on iOS 9 or below, there's no way to tell whether the user accepted or - // rejected the permissions popup - // TODO: Is there something we can listen for? - resolve(@{@"status":@"unknown"}); + // We set the promise for usage by the AppDelegate callback which listens + // for the result of the permission request + _permissionRejecter = reject; + _permissionResolver = resolve; } else { #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 // For iOS 10 display notification (sent via APNS) UNAuthorizationOptions authOptions = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge; [[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:authOptions completionHandler:^(BOOL granted, NSError * _Nullable error) { if (granted) { - resolve(@{@"status": @"granted"}); + resolve(nil); } else { - reject(@"permission_error", @"Failed to grant permission", error); + reject(@"messaging/permission_error", @"Failed to grant permission", error); } }]; #endif @@ -175,7 +212,7 @@ RCT_EXPORT_METHOD(getInitialMessage:(RCTPromiseResolveBlock)resolve rejecter:(RC resolve(nil); } } - + RCT_EXPORT_METHOD(sendMessage: (NSDictionary *) message resolve:(RCTPromiseResolveBlock) resolve reject:(RCTPromiseRejectBlock) reject) { @@ -350,4 +387,3 @@ RCT_EXPORT_METHOD(unsubscribeFromTopic: (NSString*) topic) { @implementation RNFirebaseMessaging @end #endif - From b5b02ed84d7790f068ee1edc8f26bbda57d4e951 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Mon, 5 Feb 2018 18:04:10 +0000 Subject: [PATCH 08/77] [ios] Add hasPermission method --- .../messaging/RNFirebaseMessaging.m | 13 ++++++++ lib/modules/messaging/index.js | 31 +++++++++++-------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index 43280ec4..5626b407 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -212,6 +212,19 @@ RCT_EXPORT_METHOD(getInitialMessage:(RCTPromiseResolveBlock)resolve rejecter:(RC resolve(nil); } } + +RCT_EXPORT_METHOD(hasPermission: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { + resolve(@([RCTSharedApplication() currentUserNotificationSettings].types != UIUserNotificationTypeNone)); + } else { + #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + [[UNUserNotificationCenter currentNotificationCenter] getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) { + resolve(@(settings.alertSetting == UNNotificationSettingEnabled)); + }]; + #endif + } +} + RCT_EXPORT_METHOD(sendMessage: (NSDictionary *) message resolve:(RCTPromiseResolveBlock) resolve diff --git a/lib/modules/messaging/index.js b/lib/modules/messaging/index.js index b1486210..83a48f68 100644 --- a/lib/modules/messaging/index.js +++ b/lib/modules/messaging/index.js @@ -13,28 +13,29 @@ import type App from '../core/firebase-app'; type Notification = { body: string, - bodyLocalizationArgs: string[], - bodyLocalizationKey: string, - clickAction: string, - color: string, - icon: string, - link: string, + bodyLocalizationArgs?: string[], + bodyLocalizationKey?: string, + clickAction?: string, + color?: string, + icon?: string, + link?: string, sound: string, - tag: string, + subtitle?: string, + tag?: string, title: string, - titleLocalizationArgs: string[], - titleLocalizationKey: string, + titleLocalizationArgs?: string[], + titleLocalizationKey?: string, }; type Message = { - collapseKey: string, + collapseKey?: string, data: { [string]: string }, - from: string, + from?: string, messageId: string, messageType?: string, openedFromTray: boolean, notification?: Notification, - sentTime: number, + sentTime?: number, to?: string, ttl?: number, }; @@ -54,7 +55,7 @@ type OnTokenRefreshObserver = { type RemoteMessage = { collapseKey?: string, data: { [string]: string }, - messageId?: string, + messageId: string, messageType?: string, to: string, ttl: number, @@ -164,6 +165,10 @@ export default class Messaging extends ModuleBase { return getNativeModule(this).getInitialMessage(); } + hasPermission(): Promise { + return getNativeModule(this).hasPermission(); + } + sendMessage(remoteMessage: RemoteMessage): Promise { return getNativeModule(this).send(remoteMessage); } From e329851c449f7ee29ab0c440959de8fdbbd4de65 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Mon, 5 Feb 2018 18:18:54 +0000 Subject: [PATCH 09/77] [ios] Android hasPermission method --- .../io/invertase/firebase/messaging/RNFirebaseMessaging.java | 5 +++++ ios/RNFirebase/messaging/RNFirebaseMessaging.m | 1 + 2 files changed, 6 insertions(+) diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java index 5788201f..f81b79f9 100644 --- a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java +++ b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java @@ -92,6 +92,11 @@ public class RNFirebaseMessaging extends ReactContextBaseJavaModule implements A } } + @ReactMethod + public void hasPermission(Promise promise) { + promise.resolve(true); + } + @ReactMethod public void sendMessage(ReadableMap messageMap, Promise promise) { if (!messageMap.hasKey("to")) { diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index 5626b407..5d1a234d 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -400,3 +400,4 @@ RCT_EXPORT_METHOD(unsubscribeFromTopic: (NSString*) topic) { @implementation RNFirebaseMessaging @end #endif + From cdb613bdee640b340ed0895db9cdc61c07fd51b9 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Mon, 5 Feb 2018 19:07:00 +0000 Subject: [PATCH 10/77] [ios] Make sure all UIApplication methods are called on main thread --- .../messaging/RNFirebaseMessaging.m | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index 5d1a234d..180e7d54 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -173,11 +173,13 @@ RCT_EXPORT_METHOD(requestPermission:(RCTPromiseResolveBlock)resolve rejecter:(RC if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { UIUserNotificationType types = (UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge); - [RCTSharedApplication() registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:types categories:nil]]; - // We set the promise for usage by the AppDelegate callback which listens - // for the result of the permission request - _permissionRejecter = reject; - _permissionResolver = resolve; + dispatch_async(dispatch_get_main_queue(), ^{ + [RCTSharedApplication() registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:types categories:nil]]; + // We set the promise for usage by the AppDelegate callback which listens + // for the result of the permission request + _permissionRejecter = reject; + _permissionResolver = resolve; + }); } else { #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 // For iOS 10 display notification (sent via APNS) @@ -200,7 +202,9 @@ RCT_EXPORT_METHOD(requestPermission:(RCTPromiseResolveBlock)resolve rejecter:(RC // Non Web SDK methods RCT_EXPORT_METHOD(getBadge: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - resolve(@([RCTSharedApplication() applicationIconBadgeNumber])); + dispatch_async(dispatch_get_main_queue(), ^{ + resolve(@([RCTSharedApplication() applicationIconBadgeNumber])); + }); } RCT_EXPORT_METHOD(getInitialMessage:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){ @@ -215,7 +219,9 @@ RCT_EXPORT_METHOD(getInitialMessage:(RCTPromiseResolveBlock)resolve rejecter:(RC RCT_EXPORT_METHOD(hasPermission: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { - resolve(@([RCTSharedApplication() currentUserNotificationSettings].types != UIUserNotificationTypeNone)); + dispatch_async(dispatch_get_main_queue(), ^{ + resolve(@([RCTSharedApplication() currentUserNotificationSettings].types != UIUserNotificationTypeNone)); + }); } else { #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 [[UNUserNotificationCenter currentNotificationCenter] getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) { @@ -400,4 +406,3 @@ RCT_EXPORT_METHOD(unsubscribeFromTopic: (NSString*) topic) { @implementation RNFirebaseMessaging @end #endif - From 4ff20007f5401d4403684b2538e33a2b75e213fc Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 8 Feb 2018 17:07:20 +0000 Subject: [PATCH 11/77] [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, +}; From e2f56ac93c4c65bf4d6ce3609cce7d3fbc3f3ee6 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 9 Feb 2018 07:28:59 +0000 Subject: [PATCH 12/77] [fcm] Fix iOS completion handlers --- ios/RNFirebase/messaging/RNFirebaseMessaging.m | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index 1faed23c..5eca0f21 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -116,9 +116,6 @@ RCT_EXPORT_MODULE() [_callbackHandlers setObject:[completionHandler copy] forKey:message[@"messageId"]]; [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; - - // TODO: Change this to your preferred presentation option - completionHandler(UNNotificationPresentationOptionNone); } // Handle notification messages after display notification is tapped by the user. @@ -451,6 +448,7 @@ RCT_EXPORT_METHOD(finishRemoteNotification: (NSString*) messageId if (!message[@"messageId"]) { message[@"messageId"] = [[NSUUID UUID] UUIDString]; } + message[@"messageType"] = messageType; message[@"data"] = data; message[@"notification"] = notif; @@ -474,4 +472,3 @@ RCT_EXPORT_METHOD(finishRemoteNotification: (NSString*) messageId @implementation RNFirebaseMessaging @end #endif - From 326d1778c67823f12f2ae852889c7bb0937678cc Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 9 Feb 2018 07:50:51 +0000 Subject: [PATCH 13/77] [fcm] Improved RemoteMessage; Exported constants as part of messaging statics; --- lib/modules/messaging/RemoteMessage.js | 134 ++++++++++++++----------- lib/modules/messaging/index.js | 28 +++--- 2 files changed, 91 insertions(+), 71 deletions(-) diff --git a/lib/modules/messaging/RemoteMessage.js b/lib/modules/messaging/RemoteMessage.js index 41370ef8..de41d8b7 100644 --- a/lib/modules/messaging/RemoteMessage.js +++ b/lib/modules/messaging/RemoteMessage.js @@ -1,61 +1,39 @@ /** * @flow - * Remote message representation wrapper + * RemoteMessage representation wrapper */ import { isObject, generatePushID } from './../../utils'; +type NativeRemoteMessage = { + collapseKey?: string, + data: { [string]: string }, + messageId: string, + messageType?: string, + to: string, + ttl: number, +}; + export default class RemoteMessage { - properties: Object; + collapseKey: string | void; + data: { [string]: string }; + messageId: string; + messageType: string | void; + to: string; + ttl: number; - constructor(sender: string) { - this.properties = { - id: generatePushID(), - ttl: 3600, - // add the googleapis sender id part if not already added. - sender: `${sender}`.includes('@') - ? sender - : `${sender}@gcm.googleapis.com`, - type: 'remote', - data: {}, - }; + constructor() { + this.data = {}; + this.messageId = generatePushID(); + this.ttl = 3600; } /** * - * @param ttl + * @param collapseKey * @returns {RemoteMessage} */ - setTtl(ttl: number): RemoteMessage { - this.properties.ttl = ttl; - return this; - } - - /** - * - * @param id - */ - setId(id: string): RemoteMessage { - this.properties.id = `${id}`; - return this; - } - - /** - * - * @param type - * @returns {RemoteMessage} - */ - setType(type: string): RemoteMessage { - this.properties.type = `${type}`; - return this; - } - - /** - * - * @param key - * @returns {RemoteMessage} - */ - setCollapseKey(key: string): RemoteMessage { - this.properties.collapseKey = `${key}`; + withCollapseKey(collapseKey: string): RemoteMessage { + this.collapseKey = collapseKey; return this; } @@ -64,26 +42,64 @@ export default class RemoteMessage { * @param data * @returns {RemoteMessage} */ - setData(data: Object = {}) { + withData(data: Object = {}) { if (!isObject(data)) { throw new Error( - `RemoteMessage:setData expects an object as the first parameter but got type '${typeof data}'.` + `RemoteMessage:withData expects an object but got type '${typeof data}'.` ); } - - const props = Object.keys(data); - - // coerce all property values to strings as - // remote message data only supports strings - for (let i = 0, len = props.length; i < len; i++) { - const prop = props[i]; - this.properties.data[prop] = `${data[prop]}`; - } - + this.data = data; return this; } - toJSON(): Object { - return Object.assign({}, this.properties); + /** + * + * @param messageId + * @returns {RemoteMessage} + */ + withMessageId(messageId: string): RemoteMessage { + this.messageId = messageId; + return this; + } + + /** + * + * @param messageType + * @returns {RemoteMessage} + */ + withMessageType(messageType: string): RemoteMessage { + this.messageType = messageType; + return this; + } + + /** + * + * @param ttl + * @returns {RemoteMessage} + */ + withTtl(ttl: number): RemoteMessage { + this.ttl = ttl; + return this; + } + + build(): NativeRemoteMessage { + if (!this.data) { + throw new Error('RemoteMessage: Missing required `data` property'); + } else if (!this.messageId) { + throw new Error('RemoteMessage: Missing required `messageId` property'); + } else if (!this.to) { + throw new Error('RemoteMessage: Missing required `to` property'); + } else if (!this.ttl) { + throw new Error('RemoteMessage: Missing required `ttl` property'); + } + + return { + collapseKey: this.collapseKey, + data: this.data, + messageId: this.messageId, + messageType: this.messageType, + to: this.to, + ttl: this.ttl, + }; } } diff --git a/lib/modules/messaging/index.js b/lib/modules/messaging/index.js index d411afc4..ed843f99 100644 --- a/lib/modules/messaging/index.js +++ b/lib/modules/messaging/index.js @@ -9,6 +9,12 @@ import ModuleBase from '../../utils/ModuleBase'; import { getNativeModule } from '../../utils/native'; import { isFunction, isObject } from '../../utils'; import Message from './Message'; +import RemoteMessage from './RemoteMessage'; +import { + MessageType, + RemoteNotificationResult, + WillPresentNotificationResult, +} from './types'; import type App from '../core/firebase-app'; import type { NativeMessage } from './types'; @@ -25,15 +31,6 @@ type OnTokenRefreshObserver = { next: OnTokenRefresh, }; -type RemoteMessage = { - collapseKey?: string, - data: { [string]: string }, - messageId: string, - messageType?: string, - to: string, - ttl: number, -}; - const NATIVE_EVENTS = [ 'messaging_message_received', 'messaging_token_refreshed', @@ -90,7 +87,6 @@ export default class Messaging extends ModuleBase { ); } - // TODO: iOS finish getLogger(this).info('Creating onMessage listener'); const wrappedListener = async (nativeMessage: NativeMessage) => { @@ -151,7 +147,12 @@ export default class Messaging extends ModuleBase { } sendMessage(remoteMessage: RemoteMessage): Promise { - return getNativeModule(this).send(remoteMessage); + if (!(remoteMessage instanceof RemoteMessage)) { + throw new Error( + `Messaging:sendMessage expects a 'RemoteMessage' but got type ${typeof remoteMessage}` + ); + } + return getNativeModule(this).send(remoteMessage.build()); } setBadge(badge: number): void { @@ -199,5 +200,8 @@ export default class Messaging extends ModuleBase { } export const statics = { - // RemoteMessage, + MessageType, + RemoteMessage, + RemoteNotificationResult, + WillPresentNotificationResult, }; From 7cac9814685abf456f8f50c4abbfcb609afc3d8f Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 9 Feb 2018 09:23:03 +0000 Subject: [PATCH 14/77] [fcm] Tweak names to make things clearer --- .../messaging/RNFirebaseMessaging.m | 72 +++++++++---------- lib/index.js | 1 + lib/modules/messaging/Message.js | 26 +++---- lib/modules/messaging/index.js | 6 +- lib/modules/messaging/types.js | 16 ++--- 5 files changed, 61 insertions(+), 60 deletions(-) diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index 5eca0f21..7a2ebe39 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -43,18 +43,18 @@ RCT_EXPORT_MODULE() - (void)configure { // Set as delegate for FIRMessaging [FIRMessaging messaging].delegate = self; - + // Establish Firebase managed data channel [FIRMessaging messaging].shouldEstablishDirectChannel = YES; - + // If we're on iOS 10 then we need to set this as a delegate for the UNUserNotificationCenter #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 [UNUserNotificationCenter currentNotificationCenter].delegate = self; #endif - + // Set static instance for use from AppDelegate theRNFirebaseMessaging = self; - + // Initialise callback handlers dictionary _callbackHandlers = [NSMutableDictionary dictionary]; } @@ -68,7 +68,7 @@ RCT_EXPORT_MODULE() - (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo { BOOL isFromBackground = (RCTSharedApplication().applicationState == UIApplicationStateInactive); NSDictionary *message = [self parseUserInfo:userInfo messageType:@"RemoteNotification" clickAction:nil openedFromTray:isFromBackground]; - + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; } @@ -77,9 +77,9 @@ RCT_EXPORT_MODULE() fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { BOOL isFromBackground = (RCTSharedApplication().applicationState == UIApplicationStateInactive); 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]; } @@ -112,9 +112,9 @@ RCT_EXPORT_MODULE() willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler { 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]; } @@ -127,14 +127,14 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler { #endif NSDictionary *message = [self parseUNNotification:response.notification messageType:@"NotificationResponse" openedFromTray:true]; - + [_callbackHandlers setObject:[completionHandler copy] forKey:message[@"messageId"]]; - + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; } #endif - + // ******************************************************* // ** Finish UNUserNotificationCenterDelegate methods // ******************************************************* @@ -154,10 +154,10 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response // Listen for data messages in the foreground - (void)applicationReceivedRemoteMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage { NSDictionary *message = [self parseFIRMessagingRemoteMessage:remoteMessage openedFromTray:false]; - + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; } - + // ******************************************************* // ** Finish FIRMessagingDelegate methods // ******************************************************* @@ -172,7 +172,7 @@ RCT_EXPORT_METHOD(requestPermission:(RCTPromiseResolveBlock)resolve rejecter:(RC reject(@"messaging/request-permission-unavailable", @"requestPermission is not supported in App Extensions", nil); return; } - + if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { UIUserNotificationType types = (UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge); dispatch_async(dispatch_get_main_queue(), ^{ @@ -195,7 +195,7 @@ RCT_EXPORT_METHOD(requestPermission:(RCTPromiseResolveBlock)resolve rejecter:(RC }]; #endif } - + dispatch_async(dispatch_get_main_queue(), ^{ [RCTSharedApplication() registerForRemoteNotifications]; }); @@ -232,8 +232,8 @@ RCT_EXPORT_METHOD(hasPermission: (RCTPromiseResolveBlock)resolve rejecter:(RCTPr #endif } } - - + + RCT_EXPORT_METHOD(sendMessage: (NSDictionary *) message resolve:(RCTPromiseResolveBlock) resolve reject:(RCTPromiseRejectBlock) reject) { @@ -244,7 +244,7 @@ RCT_EXPORT_METHOD(sendMessage: (NSDictionary *) message NSString *messageId = message[@"messageId"]; NSNumber *ttl = message[@"ttl"]; NSDictionary *data = message[@"data"]; - + [[FIRMessaging messaging] sendMessage:data to:to withMessageID:messageId timeToLive:[ttl intValue]]; } @@ -264,7 +264,7 @@ RCT_EXPORT_METHOD(unsubscribeFromTopic: (NSString*) topic) { // Response handler methods -RCT_EXPORT_METHOD(finishNotificationResponse: (NSString*) messageId) { +RCT_EXPORT_METHOD(completeNotificationResponse: (NSString*) messageId) { void(^callbackHandler)() = [_callbackHandlers objectForKey:messageId]; if (!callbackHandler) { NSLog(@"There is no callback handler for messageId: %@", messageId); @@ -273,9 +273,9 @@ RCT_EXPORT_METHOD(finishNotificationResponse: (NSString*) messageId) { callbackHandler(); [_callbackHandlers removeObjectForKey:messageId]; } - -RCT_EXPORT_METHOD(finishPresentNotification: (NSString*) messageId - result: (NSString*) result) { + +RCT_EXPORT_METHOD(completePresentNotification: (NSString*) messageId + result: (NSString*) result) { void(^callbackHandler)(UNNotificationPresentationOptions) = [_callbackHandlers objectForKey:messageId]; if (!callbackHandler) { NSLog(@"There is no callback handler for messageId: %@", messageId); @@ -293,9 +293,9 @@ RCT_EXPORT_METHOD(finishPresentNotification: (NSString*) messageId callbackHandler(options); [_callbackHandlers removeObjectForKey:messageId]; } - -RCT_EXPORT_METHOD(finishRemoteNotification: (NSString*) messageId - result: (NSString*) result) { + +RCT_EXPORT_METHOD(completeRemoteNotification: (NSString*) messageId + result: (NSString*) result) { void(^callbackHandler)(UIBackgroundFetchResult) = [_callbackHandlers objectForKey:messageId]; if (!callbackHandler) { NSLog(@"There is no callback handler for messageId: %@", messageId); @@ -321,7 +321,7 @@ RCT_EXPORT_METHOD(finishRemoteNotification: (NSString*) messageId - (NSDictionary*)parseFIRMessagingRemoteMessage:(FIRMessagingRemoteMessage *)remoteMessage openedFromTray:(bool)openedFromTray { NSDictionary *appData = remoteMessage.appData; - + NSMutableDictionary *message = [[NSMutableDictionary alloc] init]; NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; for (id k1 in appData) { @@ -366,19 +366,19 @@ RCT_EXPORT_METHOD(finishRemoteNotification: (NSString*) messageId message[@"messageType"] = @"RemoteMessage"; message[@"data"] = data; message[@"openedFromTray"] = @(false); - + return message; } - + - (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 messageType:messageType clickAction:clickAction openedFromTray:openedFromTray]; } - + - (NSDictionary*)parseUserInfo:(NSDictionary *)userInfo messageType:(NSString *) messageType clickAction:(NSString *) clickAction @@ -386,7 +386,7 @@ RCT_EXPORT_METHOD(finishRemoteNotification: (NSString*) messageId NSMutableDictionary *message = [[NSMutableDictionary alloc] init]; NSMutableDictionary *notif = [[NSMutableDictionary alloc] init]; NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; - + for (id k1 in userInfo) { if ([k1 isEqualToString:@"aps"]) { NSDictionary *aps = userInfo[k1]; @@ -438,22 +438,22 @@ RCT_EXPORT_METHOD(finishRemoteNotification: (NSString*) messageId data[k1] = userInfo[k1]; } } - + 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[@"messageType"] = messageType; - + message[@"data"] = data; message[@"notification"] = notif; message[@"openedFromTray"] = @(openedFromTray); - + return message; } diff --git a/lib/index.js b/lib/index.js index b31d093b..dda8d832 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,5 +4,6 @@ import firebase from './modules/core/firebase'; export type { default as User } from './modules/auth/User'; +export type { default as Message } from './modules/messaging/Message'; export default firebase; diff --git a/lib/modules/messaging/Message.js b/lib/modules/messaging/Message.js index 19fc8f7a..8b42a438 100644 --- a/lib/modules/messaging/Message.js +++ b/lib/modules/messaging/Message.js @@ -6,23 +6,23 @@ import { Platform } from 'react-native'; import { getNativeModule } from '../../utils/native'; import { MessageType, + PresentNotificationResult, RemoteNotificationResult, - WillPresentNotificationResult, } from './types'; import type Messaging from './'; import type { MessageTypeType, NativeMessage, Notification, + PresentNotificationResultType, RemoteNotificationResultType, - WillPresentNotificationResultType, } from './types'; /** * @class Message */ export default class Message { - _finished: boolean; + _completed: boolean; _messaging: Messaging; _message: NativeMessage; @@ -71,19 +71,19 @@ export default class Message { return this._message.ttl; } - finish( - result?: RemoteNotificationResultType | WillPresentNotificationResultType + complete( + result?: PresentNotificationResultType | RemoteNotificationResultType ): void { if (Platform.OS !== 'ios') { return; } - if (!this._finished) { - this._finished = true; + if (!this._completed) { + this._completed = true; switch (this.messageType) { case MessageType.NotificationResponse: - getNativeModule(this._messaging).finishNotificationResponse( + getNativeModule(this._messaging).completeNotificationResponse( this.messageId ); break; @@ -91,13 +91,13 @@ export default class Message { case MessageType.PresentNotification: if ( result && - !Object.values(WillPresentNotificationResult).includes(result) + !Object.values(PresentNotificationResult).includes(result) ) { - throw new Error(`Invalid WillPresentNotificationResult: ${result}`); + throw new Error(`Invalid PresentNotificationResult: ${result}`); } - getNativeModule(this._messaging).finishPresentNotification( + getNativeModule(this._messaging).completePresentNotification( this.messageId, - result || WillPresentNotificationResult.None + result || PresentNotificationResult.None ); break; @@ -108,7 +108,7 @@ export default class Message { ) { throw new Error(`Invalid RemoteNotificationResult: ${result}`); } - getNativeModule(this._messaging).finishRemoteNotification( + getNativeModule(this._messaging).completeRemoteNotification( this.messageId, result || RemoteNotificationResult.NoData ); diff --git a/lib/modules/messaging/index.js b/lib/modules/messaging/index.js index ed843f99..88b6ac14 100644 --- a/lib/modules/messaging/index.js +++ b/lib/modules/messaging/index.js @@ -12,8 +12,8 @@ import Message from './Message'; import RemoteMessage from './RemoteMessage'; import { MessageType, + PresentNotificationResult, RemoteNotificationResult, - WillPresentNotificationResult, } from './types'; import type App from '../core/firebase-app'; @@ -92,7 +92,7 @@ export default class Messaging extends ModuleBase { const wrappedListener = async (nativeMessage: NativeMessage) => { const message = new Message(this, nativeMessage); await listener(message); - message.finish(); + message.complete(); }; SharedEventEmitter.addListener('onMessage', wrappedListener); @@ -201,7 +201,7 @@ export default class Messaging extends ModuleBase { export const statics = { MessageType, + PresentNotificationResult, RemoteMessage, RemoteNotificationResult, - WillPresentNotificationResult, }; diff --git a/lib/modules/messaging/types.js b/lib/modules/messaging/types.js index 5a34c8af..1909660b 100644 --- a/lib/modules/messaging/types.js +++ b/lib/modules/messaging/types.js @@ -11,24 +11,24 @@ export const MessageType = { RemoteNotificationHandler: 'RemoteNotificationHandler', }; +export const PresentNotificationResult = { + All: 'UNNotificationPresentationOptionAll', + None: 'UNNotificationPresentationOptionNone', +}; + export const RemoteNotificationResult = { NewData: 'UIBackgroundFetchResultNewData', NoData: 'UIBackgroundFetchResultNoData', ResultFailed: 'UIBackgroundFetchResultFailed', }; -export const WillPresentNotificationResult = { - All: 'UNNotificationPresentationOptionAll', - None: 'UNNotificationPresentationOptionNone', -}; - export type MessageTypeType = $Values; +export type PresentNotificationResultType = $Values< + typeof PresentNotificationResult +>; export type RemoteNotificationResultType = $Values< typeof RemoteNotificationResult >; -export type WillPresentNotificationResultType = $Values< - typeof WillPresentNotificationResult ->; export type Notification = { body: string, From 1ddc76616dc4978d06fef14c4d6e7ef4de1994d6 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 9 Feb 2018 10:17:16 +0000 Subject: [PATCH 15/77] [fcm] Fix getInitialMessage; Handle a few more android fields --- .../io/invertase/firebase/messaging/RNFirebaseMessaging.java | 5 ++++- lib/modules/messaging/index.js | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java index f81b79f9..819e48ae 100644 --- a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java +++ b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java @@ -69,7 +69,6 @@ public class RNFirebaseMessaging extends ReactContextBaseJavaModule implements A @ReactMethod public void requestPermission(Promise promise) { - // TODO: Object structure? promise.resolve(null); } @@ -190,6 +189,10 @@ public class RNFirebaseMessaging extends ReactContextBaseJavaModule implements A messageMap.putString("messageId", extras.getString("google.message_id")); } else if (key.equals("google.sent_time")) { messageMap.putDouble("sentTime", extras.getLong("google.sent_time")); + } else if (key.equals("google.ttl")) { + messageMap.putDouble("ttl", extras.getDouble("google.ttl")); + } else if (key.equals("_fbSourceApplicationHasBeenSet")) { + // ignore known unneeded fields } else { dataMap.putString(key, extras.getString(key)); } diff --git a/lib/modules/messaging/index.js b/lib/modules/messaging/index.js index 88b6ac14..c2e1718f 100644 --- a/lib/modules/messaging/index.js +++ b/lib/modules/messaging/index.js @@ -139,7 +139,9 @@ export default class Messaging extends ModuleBase { } getInitialMessage(): Promise { - return getNativeModule(this).getInitialMessage(); + return getNativeModule(this) + .getInitialMessage() + .then(message => new Message(this, message)); } hasPermission(): Promise { From fe095db90d05eb8484245ace2b5463cb63f5640e Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 9 Feb 2018 10:23:35 +0000 Subject: [PATCH 16/77] [fcm] Another little fix --- lib/modules/messaging/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/messaging/index.js b/lib/modules/messaging/index.js index c2e1718f..7b27249d 100644 --- a/lib/modules/messaging/index.js +++ b/lib/modules/messaging/index.js @@ -141,7 +141,7 @@ export default class Messaging extends ModuleBase { getInitialMessage(): Promise { return getNativeModule(this) .getInitialMessage() - .then(message => new Message(this, message)); + .then(message => (message ? new Message(this, message) : null)); } hasPermission(): Promise { From 7b95613ec6749d659b433a0c70056ed6a6e53843 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 9 Feb 2018 16:59:29 +0000 Subject: [PATCH 17/77] [fcm] Some internal JS tweaks --- lib/modules/messaging/Message.js | 2 +- lib/modules/messaging/RemoteMessage.js | 53 +++++++++++++------------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/lib/modules/messaging/Message.js b/lib/modules/messaging/Message.js index 8b42a438..d789377e 100644 --- a/lib/modules/messaging/Message.js +++ b/lib/modules/messaging/Message.js @@ -74,7 +74,7 @@ export default class Message { complete( result?: PresentNotificationResultType | RemoteNotificationResultType ): void { - if (Platform.OS !== 'ios') { + if (Platform.OS === 'android') { return; } diff --git a/lib/modules/messaging/RemoteMessage.js b/lib/modules/messaging/RemoteMessage.js index de41d8b7..15a8493f 100644 --- a/lib/modules/messaging/RemoteMessage.js +++ b/lib/modules/messaging/RemoteMessage.js @@ -14,17 +14,18 @@ type NativeRemoteMessage = { }; export default class RemoteMessage { - collapseKey: string | void; - data: { [string]: string }; - messageId: string; - messageType: string | void; - to: string; - ttl: number; + _collapseKey: string | void; + _data: { [string]: string }; + _messageId: string; + _messageType: string | void; + _to: string; + _ttl: number; constructor() { - this.data = {}; - this.messageId = generatePushID(); - this.ttl = 3600; + this._data = {}; + // TODO: Is this the best way to generate an ID? + this._messageId = generatePushID(); + this._ttl = 3600; } /** @@ -32,8 +33,8 @@ export default class RemoteMessage { * @param collapseKey * @returns {RemoteMessage} */ - withCollapseKey(collapseKey: string): RemoteMessage { - this.collapseKey = collapseKey; + setCollapseKey(collapseKey: string): RemoteMessage { + this._collapseKey = collapseKey; return this; } @@ -42,13 +43,13 @@ export default class RemoteMessage { * @param data * @returns {RemoteMessage} */ - withData(data: Object = {}) { + setData(data: { [string]: string } = {}) { if (!isObject(data)) { throw new Error( - `RemoteMessage:withData expects an object but got type '${typeof data}'.` + `RemoteMessage:setData expects an object but got type '${typeof data}'.` ); } - this.data = data; + this._data = data; return this; } @@ -57,8 +58,8 @@ export default class RemoteMessage { * @param messageId * @returns {RemoteMessage} */ - withMessageId(messageId: string): RemoteMessage { - this.messageId = messageId; + setMessageId(messageId: string): RemoteMessage { + this._messageId = messageId; return this; } @@ -67,8 +68,8 @@ export default class RemoteMessage { * @param messageType * @returns {RemoteMessage} */ - withMessageType(messageType: string): RemoteMessage { - this.messageType = messageType; + setMessageType(messageType: string): RemoteMessage { + this._messageType = messageType; return this; } @@ -77,8 +78,8 @@ export default class RemoteMessage { * @param ttl * @returns {RemoteMessage} */ - withTtl(ttl: number): RemoteMessage { - this.ttl = ttl; + setTtl(ttl: number): RemoteMessage { + this._ttl = ttl; return this; } @@ -94,12 +95,12 @@ export default class RemoteMessage { } return { - collapseKey: this.collapseKey, - data: this.data, - messageId: this.messageId, - messageType: this.messageType, - to: this.to, - ttl: this.ttl, + collapseKey: this._collapseKey, + data: this._data, + messageId: this._messageId, + messageType: this._messageType, + to: this._to, + ttl: this._ttl, }; } } From fb57dc54822455a71b4e060284b5bb404c400c25 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 9 Feb 2018 17:00:03 +0000 Subject: [PATCH 18/77] [notifications] First pass at notifications JS API --- .../RNFirebaseNotifications.java | 25 + .../RNFirebaseNotificationsPackage.java | 37 ++ .../notifications/AndroidNotification.js | 546 ++++++++++++++++++ lib/modules/notifications/IOSNotification.js | 159 +++++ lib/modules/notifications/Notification.js | 120 ++++ lib/modules/notifications/index.js | 125 ++-- 6 files changed, 971 insertions(+), 41 deletions(-) create mode 100644 android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java create mode 100644 android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationsPackage.java create mode 100644 lib/modules/notifications/AndroidNotification.js create mode 100644 lib/modules/notifications/IOSNotification.js create mode 100644 lib/modules/notifications/Notification.js diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java new file mode 100644 index 00000000..5aee7f17 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java @@ -0,0 +1,25 @@ +package io.invertase.firebase.notifications; + +import android.support.v4.app.NotificationCompat; + +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; + +public class RNFirebaseNotifications extends ReactContextBaseJavaModule { + public RNFirebaseNotifications(ReactApplicationContext context) { + super(context); + } + + @Override + public String getName() { + return "RNFirebaseNotifications"; + } + + @ReactMethod + public void sendNotification(Promise promise) { + // + NotificationCompat.Builder builder = new NotificationCompat.Builder() + } +} diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationsPackage.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationsPackage.java new file mode 100644 index 00000000..74d4de23 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationsPackage.java @@ -0,0 +1,37 @@ +package io.invertase.firebase.notifications; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class RNFirebaseNotificationsPackage implements ReactPackage { + public RNFirebaseNotificationsPackage() { + } + + /** + * @param reactContext react application context that can be used to create modules + * @return list of native modules to register with the newly created catalyst instance + */ + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + modules.add(new RNFirebaseNotifications(reactContext)); + + return modules; + } + + /** + * @param reactContext + * @return a list of view managers that should be registered with {@link UIManagerModule} + */ + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} diff --git a/lib/modules/notifications/AndroidNotification.js b/lib/modules/notifications/AndroidNotification.js new file mode 100644 index 00000000..12ed4331 --- /dev/null +++ b/lib/modules/notifications/AndroidNotification.js @@ -0,0 +1,546 @@ +/** + * @flow + * AndroidNotification representation wrapper + */ +import type Notification from './Notification'; + +type Lights = { + argb: number, + onMs: number, + offMs: number, +}; + +type Progress = { + max: number, + progress: number, + indeterminate: boolean, +}; + +type SmallIcon = { + icon: number, + level?: number, +}; + +export type NativeAndroidNotification = { + // TODO actions: Action[], + autoCancel: boolean, + badgeIconType: BadgeIconTypeType, + category: CategoryType, + channelId: string, + color: number, + colorized: boolean, + contentInfo: string, + defaults: DefaultsType[], + group: string, + groupAlertBehaviour: GroupAlertType, + groupSummary: boolean, + largeIcon: string, + lights: Lights, + localOnly: boolean, + number: number, + ongoing: boolean, + onlyAlertOnce: boolean, + people: string[], + priority: PriorityType, + progress: Progress, + publicVersion: Notification, + remoteInputHistory: string[], + shortcutId: string, + showWhen: boolean, + smallIcon: SmallIcon, + sortKey: string, + // TODO: style: Style, + ticker: string, + timeoutAfter: number, + usesChronometer: boolean, + vibrate: number[], + visibility: VisibilityType, + when: number, +}; + +export const BadgeIconType = { + Large: 2, + None: 0, + Small: 1, +}; + +export const Category = { + Alarm: 'alarm', + Call: 'call', + Email: 'email', + Error: 'err', + Event: 'event', + Message: 'msg', + Progress: 'progress', + Promo: 'promo', + Recommendation: 'recommendation', + Reminder: 'reminder', + Service: 'service', + Social: 'social', + Status: 'status', + System: 'system', + Transport: 'transport', +}; + +export const Defaults = { + All: -1, + Lights: 4, + Sound: 1, + Vibrate: 2, +}; + +export const GroupAlert = { + All: 0, + Children: 2, + Summary: 1, +}; + +export const Priority = { + Default: 0, + High: 1, + Low: -1, + Max: 2, + Min: -2, +}; + +export const Visibility = { + Private: 0, + Public: 1, + Secret: -1, +}; + +type BadgeIconTypeType = $Values; +type CategoryType = $Values; +type DefaultsType = $Values; +type GroupAlertType = $Values; +type PriorityType = $Values; +type VisibilityType = $Values; + +export default class AndroidNotification { + // TODO actions: Action[]; // icon, title, ??pendingIntent??, allowGeneratedReplies, extender, extras, remoteinput (ugh) + _autoCancel: boolean; + _badgeIconType: BadgeIconTypeType; + _category: CategoryType; + _channelId: string; + _color: number; + _colorized: boolean; + _contentInfo: string; + _defaults: DefaultsType[]; + _group: string; + _groupAlertBehaviour: GroupAlertType; + _groupSummary: boolean; + _largeIcon: string; + _lights: Lights; + _localOnly: boolean; + _notification: Notification; + _number: number; + _ongoing: boolean; + _onlyAlertOnce: boolean; + _people: string[]; + _priority: PriorityType; + _progress: Progress; + _publicVersion: Notification; + _remoteInputHistory: string[]; + _shortcutId: string; + _showWhen: boolean; + _smallIcon: SmallIcon; + _sortKey: string; + // TODO: style: Style; // Need to figure out if this can work + _ticker: string; + _timeoutAfter: number; + _usesChronometer: boolean; + _vibrate: number[]; + _visibility: VisibilityType; + _when: number; + + // android unsupported + // content: RemoteViews + // contentIntent: PendingIntent - need to look at what this is + // customBigContentView: RemoteViews + // customContentView: RemoteViews + // customHeadsUpContentView: RemoteViews + // deleteIntent: PendingIntent + // fullScreenIntent: PendingIntent + // sound.streamType + + constructor(notification: Notification) { + this._notification = notification; + this._people = []; + } + + /** + * + * @param identifier + * @param identifier + * @param identifier + * @returns {Notification} + */ + addPerson(person: string): Notification { + this._people.push(person); + return this._notification; + } + + /** + * + * @param autoCancel + * @returns {Notification} + */ + setAutoCancel(autoCancel: boolean): Notification { + this._autoCancel = autoCancel; + return this._notification; + } + + /** + * + * @param badgeIconType + * @returns {Notification} + */ + setBadgeIconType(badgeIconType: BadgeIconTypeType): Notification { + this._badgeIconType = badgeIconType; + return this._notification; + } + + /** + * + * @param category + * @returns {Notification} + */ + setCategory(category: CategoryType): Notification { + if (!Object.values(Category).includes(category)) { + throw new Error(`AndroidNotification: Invalid Category: ${category}`); + } + this._category = category; + return this._notification; + } + + /** + * + * @param channelId + * @returns {Notification} + */ + setChannelId(channelId: string): Notification { + this._channelId = channelId; + return this._notification; + } + + /** + * + * @param color + * @returns {Notification} + */ + setColor(color: number): Notification { + this._color = color; + return this._notification; + } + + /** + * + * @param colorized + * @returns {Notification} + */ + setColorized(colorized: boolean): Notification { + this._colorized = colorized; + return this._notification; + } + + /** + * + * @param contentInfo + * @returns {Notification} + */ + setContentInfo(contentInfo: string): Notification { + this._contentInfo = contentInfo; + return this._notification; + } + + /** + * + * @param defaults + * @returns {Notification} + */ + setDefaults(defaults: DefaultsType[]): Notification { + this._defaults = defaults; + return this._notification; + } + + /** + * + * @param group + * @returns {Notification} + */ + setGroup(group: string): Notification { + this._group = group; + return this._notification; + } + + /** + * + * @param groupAlertBehaviour + * @returns {Notification} + */ + setGroupAlertBehaviour(groupAlertBehaviour: GroupAlertType): Notification { + this._groupAlertBehaviour = groupAlertBehaviour; + return this._notification; + } + + /** + * + * @param groupSummary + * @returns {Notification} + */ + setGroupSummary(groupSummary: boolean): Notification { + this._groupSummary = groupSummary; + return this._notification; + } + + /** + * + * @param largeIcon + * @returns {Notification} + */ + setLargeIcon(largeIcon: string): Notification { + this._largeIcon = largeIcon; + return this._notification; + } + + /** + * + * @param argb + * @param onMs + * @param offMs + * @returns {Notification} + */ + setLights(argb: number, onMs: number, offMs: number): Notification { + this._lights = { + argb, + onMs, + offMs, + }; + return this._notification; + } + + /** + * + * @param localOnly + * @returns {Notification} + */ + setLocalOnly(localOnly: boolean): Notification { + this._localOnly = localOnly; + return this._notification; + } + + /** + * + * @param number + * @returns {Notification} + */ + setNumber(number: number): Notification { + this._number = number; + return this._notification; + } + + /** + * + * @param ongoing + * @returns {Notification} + */ + setOngoing(ongoing: boolean): Notification { + this._ongoing = ongoing; + return this._notification; + } + + /** + * + * @param onlyAlertOnce + * @returns {Notification} + */ + setOnlyAlertOnce(onlyAlertOnce: boolean): Notification { + this._onlyAlertOnce = onlyAlertOnce; + return this._notification; + } + + /** + * + * @param priority + * @returns {Notification} + */ + setPriority(priority: PriorityType): Notification { + this._priority = priority; + return this._notification; + } + + /** + * + * @param max + * @param progress + * @param indeterminate + * @returns {Notification} + */ + setProgress( + max: number, + progress: number, + indeterminate: boolean + ): Notification { + this._progress = { + max, + progress, + indeterminate, + }; + return this._notification; + } + + /** + * + * @param publicVersion + * @returns {Notification} + */ + setPublicVersion(publicVersion: Notification): Notification { + this._publicVersion = publicVersion; + return this._notification; + } + + /** + * + * @param remoteInputHistory + * @returns {Notification} + */ + setRemoteInputHistory(remoteInputHistory: string[]): Notification { + this._remoteInputHistory = remoteInputHistory; + return this._notification; + } + + /** + * + * @param shortcutId + * @returns {Notification} + */ + setShortcutId(shortcutId: string): Notification { + this._shortcutId = shortcutId; + return this._notification; + } + + /** + * + * @param showWhen + * @returns {Notification} + */ + setShowWhen(showWhen: boolean): Notification { + this._showWhen = showWhen; + return this._notification; + } + + /** + * + * @param icon + * @param level + * @returns {Notification} + */ + setSmallIcon(icon: number, level?: number): Notification { + this._smallIcon = { + icon, + level, + }; + return this._notification; + } + + /** + * + * @param sortKey + * @returns {Notification} + */ + setSortKey(sortKey: string): Notification { + this._sortKey = sortKey; + return this._notification; + } + + /** + * + * @param ticker + * @returns {Notification} + */ + setTicker(ticker: string): Notification { + this._ticker = ticker; + return this._notification; + } + + /** + * + * @param timeoutAfter + * @returns {Notification} + */ + setTimeoutAfter(timeoutAfter: number): Notification { + this._timeoutAfter = timeoutAfter; + return this._notification; + } + + /** + * + * @param usesChronometer + * @returns {Notification} + */ + setUsesChronometer(usesChronometer: boolean): Notification { + this._usesChronometer = usesChronometer; + return this._notification; + } + + /** + * + * @param vibrate + * @returns {Notification} + */ + setVibrate(vibrate: number[]): Notification { + this._vibrate = vibrate; + return this._notification; + } + + /** + * + * @param when + * @returns {Notification} + */ + setWhen(when: number): Notification { + this._when = when; + return this._notification; + } + + build(): NativeAndroidNotification { + // TODO: Validation + + return { + // TODO actions: Action[], + autoCancel: this._autoCancel, + badgeIconType: this._badgeIconType, + category: this._category, + channelId: this._channelId, + color: this._color, + colorized: this._colorized, + contentInfo: this._contentInfo, + defaults: this._defaults, + group: this._group, + groupAlertBehaviour: this._groupAlertBehaviour, + groupSummary: this._groupSummary, + largeIcon: this._largeIcon, + lights: this._lights, + localOnly: this._localOnly, + number: this._number, + ongoing: this._ongoing, + onlyAlertOnce: this._onlyAlertOnce, + people: this._people, + priority: this._priority, + progress: this._progress, + publicVersion: this._publicVersion, + remoteInputHistory: this._remoteInputHistory, + shortcutId: this._shortcutId, + showWhen: this._showWhen, + smallIcon: this._smallIcon, + sortKey: this._sortKey, + // TODO: style: Style, + ticker: this._ticker, + timeoutAfter: this._timeoutAfter, + usesChronometer: this._usesChronometer, + vibrate: this._vibrate, + visibility: this._visibility, + when: this._when, + }; + } +} diff --git a/lib/modules/notifications/IOSNotification.js b/lib/modules/notifications/IOSNotification.js new file mode 100644 index 00000000..607518e7 --- /dev/null +++ b/lib/modules/notifications/IOSNotification.js @@ -0,0 +1,159 @@ +/** + * @flow + * IOSNotification representation wrapper + */ +import { generatePushID } from '../../utils'; +import type Notification from './Notification'; + +type AttachmentOptions = {| + TypeHint: string, + ThumbnailHidden: boolean, + ThumbnailClippingRect: { + height: number, + width: number, + x: number, + y: number, + }, + ThumbnailTime: number, +|}; + +type Attachment = {| + identifier: string, + options?: AttachmentOptions, + url: string, +|}; + +export type NativeIOSNotification = { + alertAction?: string, + attachments: Attachment[], + badge?: number, + category?: string, + hasAction?: boolean, + identifier?: string, + launchImage?: string, + threadIdentifier?: string, +}; + +export default class IOSNotification { + _alertAction: string; // alertAction | N/A + _attachments: Attachment[]; // N/A | attachments + _badge: number; // applicationIconBadgeNumber | badge + _category: string; + _hasAction: boolean; // hasAction | N/A + _identifier: string; // N/A | identifier + _launchImage: string; // alertLaunchImage | launchImageName + _notification: Notification; + _threadIdentifier: string; // N/A | threadIdentifier + + constructor(notification: Notification) { + this._attachments = []; + // TODO: Is this the best way to generate an ID? + this._identifier = generatePushID(); + this._notification = notification; + } + + /** + * + * @param identifier + * @param identifier + * @param identifier + * @returns {Notification} + */ + addAttachment( + identifier: string, + url: string, + options?: AttachmentOptions + ): Notification { + this._attachments.push({ + identifier, + options, + url, + }); + return this._notification; + } + + /** + * + * @param alertAction + * @returns {Notification} + */ + setAlertAction(alertAction: string): Notification { + this._alertAction = alertAction; + return this._notification; + } + + /** + * + * @param badge + * @returns {Notification} + */ + setBadge(badge: number): Notification { + this._badge = badge; + return this._notification; + } + + /** + * + * @param category + * @returns {Notification} + */ + setCategory(category: string): Notification { + this._category = category; + return this._notification; + } + + /** + * + * @param hasAction + * @returns {Notification} + */ + setHasAction(hasAction: boolean): Notification { + this._hasAction = hasAction; + return this._notification; + } + + /** + * + * @param identifier + * @returns {Notification} + */ + setIdentifier(identifier: string): Notification { + this._identifier = identifier; + return this._notification; + } + + /** + * + * @param launchImage + * @returns {Notification} + */ + setLaunchImage(launchImage: string): Notification { + this._launchImage = launchImage; + return this._notification; + } + + /** + * + * @param threadIdentifier + * @returns {Notification} + */ + setThreadIdentifier(threadIdentifier: string): Notification { + this._threadIdentifier = threadIdentifier; + return this._notification; + } + + build(): NativeIOSNotification { + // TODO: Validation + + return { + alertAction: this._alertAction, + attachments: this._attachments, + badge: this._badge, + category: this._category, + hasAction: this._hasAction, + identifier: this._identifier, + launchImage: this._launchImage, + threadIdentifier: this._threadIdentifier, + }; + } +} diff --git a/lib/modules/notifications/Notification.js b/lib/modules/notifications/Notification.js new file mode 100644 index 00000000..ab7dbc6c --- /dev/null +++ b/lib/modules/notifications/Notification.js @@ -0,0 +1,120 @@ +/** + * @flow + * Notification representation wrapper + */ +import AndroidNotification from './AndroidNotification'; +import IOSNotification from './IOSNotification'; +import { isObject } from '../../utils'; + +import type { NativeAndroidNotification } from './AndroidNotification'; +import type { NativeIOSNotification } from './IOSNotification'; + +type NativeNotification = {| + android: NativeAndroidNotification, + body: string, + data: { [string]: string }, + ios: NativeIOSNotification, + sound?: string, + subtitle?: string, + title: string, +|}; + +export default class Notification { + // iOS 8/9 | 10+ | Android + _android: AndroidNotification; + _body: string; // alertBody | body | contentText + _data: { [string]: string }; // userInfo | userInfo | extras + _ios: IOSNotification; + _sound: string | void; // soundName | sound | sound + _subtitle: string | void; // N/A | subtitle | subText + _title: string; // alertTitle | title | contentTitle + + constructor() { + this._android = new AndroidNotification(this); + this._data = {}; + this._ios = new IOSNotification(this); + } + + get android(): AndroidNotification { + return this._android; + } + + get ios(): IOSNotification { + return this._ios; + } + + /** + * + * @param body + * @returns {Notification} + */ + setBody(body: string): Notification { + this._body = body; + return this; + } + + /** + * + * @param data + * @returns {Notification} + */ + setData(data: Object = {}): Notification { + if (!isObject(data)) { + throw new Error( + `Notification:withData expects an object but got type '${typeof data}'.` + ); + } + this._data = data; + return this; + } + + /** + * + * @param sound + * @returns {Notification} + */ + setSound(sound: string): Notification { + this._sound = sound; + return this; + } + + /** + * + * @param subtitle + * @returns {Notification} + */ + setSubtitle(subtitle: string): Notification { + this._subtitle = subtitle; + return this; + } + + /** + * + * @param title + * @returns {Notification} + */ + setTitle(title: string): Notification { + this._title = title; + return this; + } + + build(): NativeNotification { + // Android required fields: body, title, smallicon + // iOS required fields: TODO + if (!this.body) { + throw new Error('Notification: Missing required `body` property'); + } else if (!this.title) { + throw new Error('Notification: Missing required `title` property'); + } + + return { + android: this._android.build(), + body: this._body, + data: this._data, + ios: this._ios.build(), + sound: this._sound, + subtitle: this._subtitle, + title: this._title, + }; + } +} diff --git a/lib/modules/notifications/index.js b/lib/modules/notifications/index.js index 94b1753c..84fb39ab 100644 --- a/lib/modules/notifications/index.js +++ b/lib/modules/notifications/index.js @@ -7,28 +7,49 @@ import { getLogger } from '../../utils/log'; import ModuleBase from '../../utils/ModuleBase'; import { getNativeModule } from '../../utils/native'; import { isFunction, isObject } from '../../utils'; +import Notification from './Notification'; +import { + BadgeIconType, + Category, + Defaults, + GroupAlert, + Priority, + Visibility, +} from './AndroidNotification'; import type App from '../core/firebase-app'; -type CreateNotification = { - // TODO -}; - -type Notification = { - // TODO -}; - +// TODO: Received notification type will be different from sent notification type OnNotification = Notification => any; type OnNotificationObserver = { next: OnNotification, }; +// TODO: Schedule type +type Schedule = { + build: () => Object, +}; + const NATIVE_EVENTS = ['notifications_notification_received']; export const MODULE_NAME = 'RNFirebaseNotifications'; export const NAMESPACE = 'notifications'; +// iOS 8/9 scheduling +// fireDate: Date; +// timeZone: TimeZone; +// repeatInterval: NSCalendar.Unit; +// repeatCalendar: Calendar; +// region: CLRegion; +// regionTriggersOnce: boolean; + +// iOS 10 scheduling +// TODO + +// Android scheduling +// TODO + /** * @class Notifications */ @@ -51,28 +72,34 @@ export default class Notifications extends ModuleBase { ); } - /** - * Cancel a local notification by id - using '*' will cancel - * all local notifications. - * @param id - * @returns {*} - */ - cancelNotification(id: string): Promise { - if (!id) return Promise.reject(new Error('Missing notification id')); - if (id === '*') return getNativeModule(this).cancelAllLocalNotifications(); - return getNativeModule(this).cancelLocalNotification(id); + cancelAllNotifications(): Promise { + return getNativeModule(this).cancelAllLocalNotifications(); } /** - * Create and display a local notification + * Cancel a local notification by id. + * @param id + * @returns {*} + */ + cancelNotification(notificationId: string): Promise { + if (!notificationId) { + return Promise.reject(new Error('Missing notificationId')); + } + return getNativeModule(this).cancelLocalNotification(notificationId); + } + + /** + * Display a local notification * @param notification * @returns {*} */ - createNotification(notification: CreateNotification): Promise { - const _notification = Object.assign({}, notification); - _notification.id = _notification.id || new Date().getTime().toString(); - _notification.local_notification = true; - return getNativeModule(this).createLocalNotification(_notification); + displayNotification(notification: Notification): Promise { + if (!(notification instanceof Notification)) { + throw new Error( + `Notifications:displayNotification expects a 'Notification' but got type ${typeof notification}` + ); + } + return getNativeModule(this).displayNotification(notification.build()); } /** @@ -108,22 +135,23 @@ export default class Notifications extends ModuleBase { } /** - * Remove a delivered notification. - * @param id + * Remove all delivered notifications. * @returns {*} */ - removeDeliveredNotification(id: string): Promise { - if (!id) return Promise.reject(new Error('Missing notification id')); - return getNativeModule(this).removeDeliveredNotification(id); + removeAllDeliveredNotifications(): Promise { + return getNativeModule(this).removeAllDeliveredNotifications(); } /** - * Remove all delivered notifications. - * @param id + * Remove a delivered notification. + * @param notificationId * @returns {*} */ - removeDeliveredNotifications(): Promise { - return getNativeModule(this).removeDeliveredNotifications(); + removeDeliveredNotification(notificationId: string): Promise { + if (!notificationId) { + return Promise.reject(new Error('Missing notificationId')); + } + return getNativeModule(this).removeDeliveredNotification(notificationId); } /** @@ -131,15 +159,30 @@ export default class Notifications extends ModuleBase { * @param notification * @returns {*} */ - scheduleNotification(notification: CreateNotification): Promise { - const _notification = Object.assign({}, notification); - if (!notification.id) - return Promise.reject( - new Error('An id is required to schedule a local notification.') + scheduleNotification( + notification: Notification, + schedule: Schedule + ): Promise { + if (!(notification instanceof Notification)) { + throw new Error( + `Notifications:scheduleNotification expects a 'Notification' but got type ${typeof notification}` ); - _notification.local_notification = true; - return getNativeModule(this).scheduleLocalNotification(_notification); + } + return getNativeModule(this).scheduleNotification( + notification.build(), + schedule.build() + ); } } -export const statics = {}; +export const statics = { + Android: { + BadgeIconType, + Category, + Defaults, + GroupAlert, + Priority, + Visibility, + }, + Notification, +}; From 6b911b207b4b2ccb4b0081f4924215f9c914211c Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Wed, 14 Feb 2018 09:07:29 +0000 Subject: [PATCH 19/77] [notifications] Start iOS implementation of notifications --- .../messaging/RNFirebaseMessaging.m | 6 +- .../notifications/RNFirebaseNotifications.m | 243 +++++++++++++++++- lib/modules/notifications/index.js | 12 +- 3 files changed, 252 insertions(+), 9 deletions(-) diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index 7a2ebe39..14454ef0 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -15,11 +15,13 @@ // notifications via APNS #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 @import UserNotifications; - @interface RNFirebaseMessaging () +#else +@interface RNFirebaseMessaging () +#endif @property (nonatomic, strong) NSMutableDictionary *callbackHandlers; @end -#endif + @implementation RNFirebaseMessaging diff --git a/ios/RNFirebase/notifications/RNFirebaseNotifications.m b/ios/RNFirebase/notifications/RNFirebaseNotifications.m index daf2cf06..dec1b55d 100644 --- a/ios/RNFirebase/notifications/RNFirebaseNotifications.m +++ b/ios/RNFirebase/notifications/RNFirebaseNotifications.m @@ -1,20 +1,255 @@ #import "RNFirebaseNotifications.h" #if __has_include() +#import + +#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 +@import UserNotifications; +#endif @implementation RNFirebaseNotifications RCT_EXPORT_MODULE(); +RCT_EXPORT_METHOD(cancelAllNotifications) { + if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { + [RCTSharedApplication() cancelAllLocalNotifications]; + } else { + #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + UNUserNotificationCenter *notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; + if (notificationCenter != nil) { + [[UNUserNotificationCenter currentNotificationCenter] removeAllPendingNotificationRequests]; + } + #endif + } +} + +RCT_EXPORT_METHOD(cancelNotification:(NSString*) notificationId) { + if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { + for (UILocalNotification *notification in RCTSharedApplication().scheduledLocalNotifications) { + NSDictionary *notificationInfo = notification.userInfo; + if ([notificationId isEqualToString:[notificationInfo valueForKey:@"notificationId"]]) { + [RCTSharedApplication() cancelLocalNotification:notification]; + } + } + } else { + #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + UNUserNotificationCenter *notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; + if (notificationCenter != nil) { + [[UNUserNotificationCenter currentNotificationCenter] removePendingNotificationRequestsWithIdentifiers:@[notificationId]]; + } + #endif + } +} + +RCT_EXPORT_METHOD(displayNotification:(NSDictionary*) notification + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { + UILocalNotification* notif = [self buildUILocalNotification:notification]; + [RCTSharedApplication() presentLocalNotificationNow:notif]; + resolve(nil); + } else { + #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + UNNotificationRequest* request = [self buildUNNotificationRequest:notification]; + [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { + if (!error) { + resolve(nil); + }else{ + reject(@"notifications/display_notification_error", @"Failed to display notificaton", error); + } + }]; + #endif + } +} + RCT_EXPORT_METHOD(getInitialNotification:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){ - UILocalNotification *localUserInfo = [self bridge].launchOptions[UIApplicationLaunchOptionsLocalNotificationKey]; - if (localUserInfo) { - // TODO: Proper format - resolve([[localUserInfo userInfo] copy]); + UILocalNotification *localNotification = [self bridge].launchOptions[UIApplicationLaunchOptionsLocalNotificationKey]; + if (localNotification) { + NSDictionary *notification = [self parseUILocalNotification:localNotification]; + resolve(notification); } else { resolve(nil); } } +RCT_EXPORT_METHOD(getScheduledNotifications:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { + NSMutableArray* notifications = [[NSMutableArray alloc] init]; + for (UILocalNotification *notif in [RCTSharedApplication() scheduledLocalNotifications]){ + NSDictionary *notification = [self parseUILocalNotification:notif]; + [notifications addObject:notification]; + } + resolve(notifications); + } else { + #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + [[UNUserNotificationCenter currentNotificationCenter] getPendingNotificationRequestsWithCompletionHandler:^(NSArray * _Nonnull requests) { + NSMutableArray* notifications = [[NSMutableArray alloc] init]; + for (UNNotificationRequest *notif in requests){ + NSDictionary *notification = [self parseUNNotificationRequest:notif]; + [notifications addObject:notification]; + } + resolve(notifications); + }]; + #endif + } +} + +RCT_EXPORT_METHOD(removeAllDeliveredNotifications) { + if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { + // No such functionality on iOS 8/9 + } else { + #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + UNUserNotificationCenter *notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; + if (notificationCenter != nil) { + [[UNUserNotificationCenter currentNotificationCenter] removeAllDeliveredNotifications]; + } + #endif + } +} + +RCT_EXPORT_METHOD(removeDeliveredNotification:(NSString*) notificationId) { + if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { + // No such functionality on iOS 8/9 + } else { + #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + UNUserNotificationCenter *notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; + if (notificationCenter != nil) { + [[UNUserNotificationCenter currentNotificationCenter] removeDeliveredNotificationsWithIdentifiers:@[notificationId]]; + } + #endif + } +} + +RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification + schedule:(NSDictionary*) schedule + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { + UILocalNotification* notif = [self buildUILocalNotification:notification]; + // TODO: Schedule + [RCTSharedApplication() scheduleLocalNotification:notif]; + resolve(nil); + } else { + #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + UNNotificationRequest* request = [self buildUNNotificationRequest:notification]; + // TODO: Schedule + [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { + if (!error) { + resolve(nil); + }else{ + reject(@"notification/schedule_notification_error", @"Failed to schedule notificaton", error); + } + }]; + #endif + } +} + +- (UILocalNotification*) buildUILocalNotification:(NSDictionary *) notification { + UILocalNotification *localNotification = [[UILocalNotification alloc] init]; + if (notification[@"body"]) { + localNotification.alertBody = notification[@"body"]; + } + if (notification[@"data"]) { + localNotification.userInfo = notification[@"data"]; + } + if (notification[@"sound"]) { + localNotification.soundName = notification[@"sound"]; + } + if (notification[@"title"]) { + localNotification.alertTitle = notification[@"title"]; + } + if (notification[@"ios"]) { + NSDictionary *ios = notification[@"ios"]; + if (ios[@"alertAction"]) { + localNotification.alertAction = ios[@"alertAction"]; + } + if (ios[@"badge"]) { + NSNumber *badge = ios[@"badge"]; + localNotification.applicationIconBadgeNumber = badge.integerValue; + } + if (ios[@"category"]) { + localNotification.category = ios[@"category"]; + } + if (ios[@"hasAction"]) { + localNotification.hasAction = ios[@"hasAction"]; + } + if (ios[@"launchImage"]) { + localNotification.alertLaunchImage = ios[@"launchImage"]; + } + } + + return localNotification; +} + +- (UNNotificationRequest*) buildUNNotificationRequest:(NSDictionary *) notification { + UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; + if (notification[@"body"]) { + content.body = notification[@"body"]; + } + if (notification[@"data"]) { + content.userInfo = notification[@"data"]; + } + if (notification[@"sound"]) { + content.sound = notification[@"sound"]; + } + if (notification[@"subtitle"]) { + content.title = notification[@"subtitle"]; + } + if (notification[@"title"]) { + content.title = notification[@"title"]; + } + if (notification[@"ios"]) { + NSDictionary *ios = notification[@"ios"]; + if (ios[@"attachments"]) { + NSMutableArray *attachments = [[NSMutableArray alloc] init]; + for (NSDictionary *a in ios[@"attachments"]) { + NSString *identifier = a[@"identifier"]; + NSURL *url = [NSURL URLWithString:a[@"url"]]; + NSDictionary *options = a[@"options"]; + + NSError *error; + UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:identifier URL:url options:options error:&error]; + if (attachment) { + [attachments addObject:attachment]; + } else { + NSLog(@"Failed to create attachment: %@", error); + } + } + content.attachments = attachments; + } + + if (ios[@"badge"]) { + content.badge = ios[@"badge"]; + } + if (ios[@"category"]) { + content.categoryIdentifier = ios[@"category"]; + } + if (ios[@"launchImage"]) { + content.launchImageName = ios[@"launchImage"]; + } + if (ios[@"threadIdentifier"]) { + content.threadIdentifier = ios[@"threadIdentifier"]; + } + } + + // TODO: Scheduling + return [UNNotificationRequest requestWithIdentifier:notification[@"ios"][@"identifier"] content:content trigger:nil]; +} + +- (NSDictionary*) parseUILocalNotification:(UILocalNotification *) localNotification { + NSMutableDictionary *notification = [[NSMutableDictionary alloc] init]; + + + + return notification; + // TODO +} + +- (NSDictionary*) parseUNNotificationRequest:(UNNotificationRequest *) localNotification { + // TODO +} + - (NSArray *)supportedEvents { return @[]; } diff --git a/lib/modules/notifications/index.js b/lib/modules/notifications/index.js index 84fb39ab..c0cda1b5 100644 --- a/lib/modules/notifications/index.js +++ b/lib/modules/notifications/index.js @@ -73,7 +73,7 @@ export default class Notifications extends ModuleBase { } cancelAllNotifications(): Promise { - return getNativeModule(this).cancelAllLocalNotifications(); + return getNativeModule(this).cancelAllNotifications(); } /** @@ -85,7 +85,7 @@ export default class Notifications extends ModuleBase { if (!notificationId) { return Promise.reject(new Error('Missing notificationId')); } - return getNativeModule(this).cancelLocalNotification(notificationId); + return getNativeModule(this).cancelNotification(notificationId); } /** @@ -102,12 +102,18 @@ export default class Notifications extends ModuleBase { return getNativeModule(this).displayNotification(notification.build()); } + getInitialNotification(): Promise { + return getNativeModule(this).getInitialNotification(); + // TODO + // .then(notification => (notification ? new Notification(this, notification) : null)); + } + /** * Returns an array of all scheduled notifications * @returns {Promise.} */ getScheduledNotifications(): Promise { - return getNativeModule(this).getScheduledLocalNotifications(); + return getNativeModule(this).getScheduledNotifications(); } onNotification( From 665cd1a2774cc8eb9650449d7a6c1f096d9c8fbd Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Wed, 14 Feb 2018 15:10:20 +0000 Subject: [PATCH 20/77] [notifications] Continued work on iOS notification functionality --- .../notifications/RNFirebaseNotifications.m | 83 ++++++++++++++++++- tests/ios/Podfile.lock | 78 ++++++++--------- 2 files changed, 119 insertions(+), 42 deletions(-) diff --git a/ios/RNFirebase/notifications/RNFirebaseNotifications.m b/ios/RNFirebase/notifications/RNFirebaseNotifications.m index dec1b55d..895c559e 100644 --- a/ios/RNFirebase/notifications/RNFirebaseNotifications.m +++ b/ios/RNFirebase/notifications/RNFirebaseNotifications.m @@ -27,6 +27,7 @@ RCT_EXPORT_METHOD(cancelNotification:(NSString*) notificationId) { if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { for (UILocalNotification *notification in RCTSharedApplication().scheduledLocalNotifications) { NSDictionary *notificationInfo = notification.userInfo; + // TODO: NotificationId? if ([notificationId isEqualToString:[notificationInfo valueForKey:@"notificationId"]]) { [RCTSharedApplication() cancelLocalNotification:notification]; } @@ -194,7 +195,7 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification content.sound = notification[@"sound"]; } if (notification[@"subtitle"]) { - content.title = notification[@"subtitle"]; + content.subtitle = notification[@"subtitle"]; } if (notification[@"title"]) { content.title = notification[@"title"]; @@ -240,14 +241,90 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification - (NSDictionary*) parseUILocalNotification:(UILocalNotification *) localNotification { NSMutableDictionary *notification = [[NSMutableDictionary alloc] init]; + if (localNotification.alertBody) { + notification[@"body"] = localNotification.alertBody; + } + if (localNotification.userInfo) { + notification[@"data"] = localNotification.userInfo; + } + if (localNotification.soundName) { + notification[@"sound"] = localNotification.soundName; + } + if (localNotification.alertTitle) { + notification[@"title"] = localNotification.alertTitle; + } + NSMutableDictionary *ios = [[NSMutableDictionary alloc] init]; + if (localNotification.alertAction) { + ios[@"alertAction"] = localNotification.alertAction; + } + if (localNotification.applicationIconBadgeNumber) { + ios[@"badge"] = @(localNotification.applicationIconBadgeNumber); + } + if (localNotification.category) { + ios[@"category"] = localNotification.category; + } + if (localNotification.hasAction) { + ios[@"hasAction"] = @(localNotification.hasAction); + } + if (localNotification.alertLaunchImage) { + ios[@"launchImage"] = localNotification.alertLaunchImage; + } + notification[@"ios"] = ios; return notification; - // TODO } - (NSDictionary*) parseUNNotificationRequest:(UNNotificationRequest *) localNotification { - // TODO + NSMutableDictionary *notification = [[NSMutableDictionary alloc] init]; + + notification[@"identifier"] = localNotification.identifier; + + if (localNotification.content.body) { + notification[@"body"] = localNotification.content.body; + } + if (localNotification.content.userInfo) { + notification[@"data"] = localNotification.content.userInfo; + } + if (localNotification.content.sound) { + notification[@"sound"] = localNotification.content.sound; + } + if (localNotification.content.subtitle) { + notification[@"subtitle"] = localNotification.content.subtitle; + } + if (localNotification.content.title) { + notification[@"title"] = localNotification.content.title; + } + + NSMutableDictionary *ios = [[NSMutableDictionary alloc] init]; + + if (localNotification.content.attachments) { + NSMutableArray *attachments = [[NSMutableArray alloc] init]; + for (UNNotificationAttachment *a in localNotification.content.attachments) { + NSMutableDictionary *attachment = [[NSMutableDictionary alloc] init]; + attachment[@"identifier"] = a.identifier; + attachment[@"type"] = a.type; + attachment[@"url"] = [a.URL absoluteString]; + [attachments addObject:attachment]; + } + ios[@"attachments"] = attachments; + } + + if (localNotification.content.badge) { + ios[@"badge"] = localNotification.content.badge; + } + if (localNotification.content.categoryIdentifier) { + ios[@"category"] = localNotification.content.categoryIdentifier; + } + if (localNotification.content.launchImageName) { + ios[@"launchImage"] = localNotification.content.launchImageName; + } + if (localNotification.content.threadIdentifier) { + ios[@"threadIdentifier"] = localNotification.content.threadIdentifier; + } + notification[@"ios"] = ios; + + return notification; } - (NSArray *)supportedEvents { diff --git a/tests/ios/Podfile.lock b/tests/ios/Podfile.lock index c964fec5..0addb5eb 100644 --- a/tests/ios/Podfile.lock +++ b/tests/ios/Podfile.lock @@ -7,44 +7,44 @@ PODS: - BoringSSL/Interface (9.2) - Crashlytics (3.9.3): - Fabric (~> 1.7.2) - - Fabric (1.7.2) - - Firebase/AdMob (4.8.1): + - Fabric (1.7.3) + - Firebase/AdMob (4.8.2): - Firebase/Core - Google-Mobile-Ads-SDK (= 7.27.0) - - Firebase/Auth (4.8.1): + - Firebase/Auth (4.8.2): - Firebase/Core - FirebaseAuth (= 4.4.2) - - Firebase/Core (4.8.1): - - FirebaseAnalytics (= 4.0.7) + - Firebase/Core (4.8.2): + - FirebaseAnalytics (= 4.0.9) - FirebaseCore (= 4.0.14) - - Firebase/Crash (4.8.1): + - Firebase/Crash (4.8.2): - Firebase/Core - FirebaseCrash (= 2.0.2) - - Firebase/Database (4.8.1): + - Firebase/Database (4.8.2): - Firebase/Core - FirebaseDatabase (= 4.1.4) - - Firebase/DynamicLinks (4.8.1): + - Firebase/DynamicLinks (4.8.2): - Firebase/Core - FirebaseDynamicLinks (= 2.3.2) - - Firebase/Firestore (4.8.1): + - Firebase/Firestore (4.8.2): - Firebase/Core - FirebaseFirestore (= 0.10.0) - - Firebase/Messaging (4.8.1): + - Firebase/Messaging (4.8.2): - Firebase/Core - FirebaseMessaging (= 2.0.8) - - Firebase/Performance (4.8.1): + - Firebase/Performance (4.8.2): - Firebase/Core - FirebasePerformance (= 1.1.1) - - Firebase/RemoteConfig (4.8.1): + - Firebase/RemoteConfig (4.8.2): - Firebase/Core - FirebaseRemoteConfig (= 2.1.1) - - Firebase/Storage (4.8.1): + - Firebase/Storage (4.8.2): - Firebase/Core - FirebaseStorage (= 2.1.2) - FirebaseABTesting (1.0.0): - FirebaseCore (~> 4.0) - Protobuf (~> 3.1) - - FirebaseAnalytics (4.0.7): + - FirebaseAnalytics (4.0.9): - FirebaseCore (~> 4.0) - FirebaseInstanceID (~> 2.0) - GoogleToolboxForMac/NSData+zlib (~> 2.1) @@ -114,26 +114,26 @@ PODS: - GoogleToolboxForMac/Defines (= 2.1.3) - GoogleToolboxForMac/NSString+URLArguments (= 2.1.3) - GoogleToolboxForMac/NSString+URLArguments (2.1.3) - - gRPC (1.8.4): - - gRPC-RxLibrary (= 1.8.4) - - gRPC/Main (= 1.8.4) - - gRPC-Core (1.8.4): - - gRPC-Core/Implementation (= 1.8.4) - - gRPC-Core/Interface (= 1.8.4) - - gRPC-Core/Implementation (1.8.4): + - gRPC (1.9.1): + - gRPC-RxLibrary (= 1.9.1) + - gRPC/Main (= 1.9.1) + - gRPC-Core (1.9.1): + - gRPC-Core/Implementation (= 1.9.1) + - gRPC-Core/Interface (= 1.9.1) + - gRPC-Core/Implementation (1.9.1): - BoringSSL (~> 9.0) - - gRPC-Core/Interface (= 1.8.4) + - gRPC-Core/Interface (= 1.9.1) - nanopb (~> 0.3) - - gRPC-Core/Interface (1.8.4) - - gRPC-ProtoRPC (1.8.4): - - gRPC (= 1.8.4) - - gRPC-RxLibrary (= 1.8.4) + - gRPC-Core/Interface (1.9.1) + - gRPC-ProtoRPC (1.9.1): + - gRPC (= 1.9.1) + - gRPC-RxLibrary (= 1.9.1) - Protobuf (~> 3.0) - - gRPC-RxLibrary (1.8.4) - - gRPC/Main (1.8.4): - - gRPC-Core (= 1.8.4) - - gRPC-RxLibrary (= 1.8.4) - - GTMSessionFetcher/Core (1.1.12) + - gRPC-RxLibrary (1.9.1) + - gRPC/Main (1.9.1): + - gRPC-Core (= 1.9.1) + - gRPC-RxLibrary (= 1.9.1) + - GTMSessionFetcher/Core (1.1.13) - leveldb-library (1.20) - nanopb (0.3.8): - nanopb/decode (= 0.3.8) @@ -201,10 +201,10 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: BoringSSL: f3d6b8ce199b9c450a8cfc14895d07a2627fc232 Crashlytics: dbb07d01876c171c5ccbdf7826410380189e452c - Fabric: 9cd6a848efcf1b8b07497e0b6a2e7d336353ba15 - Firebase: 2721056b8885eef90233b03f37be64358d35d262 + Fabric: bb495bb9a7a7677c6d03a1f8b83d95bc49b47e41 + Firebase: 7d3b8cd837ad9fcd391657734c0d56dab8e9a5a3 FirebaseABTesting: d07d0ee833b842d5153549e4c7e2e2cb1c23a3f9 - FirebaseAnalytics: 617afa8c26b57a0c3f11361b248bc9e17bfd8dfd + FirebaseAnalytics: 388b630c15713f5dbf364071f5f3d6077fb52f4e FirebaseAuth: bd2738c5c1e92b108ba5f7f7335908097a4e50bb FirebaseCore: 2e0b98fb2d64ca8140136beff15772bdd14d2dd7 FirebaseCrash: cded0fc566c03651aea606a101bc156085f333ca @@ -219,11 +219,11 @@ SPEC CHECKSUMS: FirebaseSwizzlingUtilities: f1c49a5a372ac852c853722a5891a0a5e2344a6c Google-Mobile-Ads-SDK: 83f7f890e638ce8f1debd440ea363338c9f6be3b GoogleToolboxForMac: 2501e2ad72a52eb3dfe7bd9aee7dad11b858bd20 - gRPC: 572520c17b794362388d5c95396329592a3c199b - gRPC-Core: af0d4f0a53735e335fccc815c50c0a03da695287 - gRPC-ProtoRPC: 6596fde8d27e0718d7de1de1dc99a951d832a809 - gRPC-RxLibrary: f6b1432a667c3354c7b345affed9886c0d4ff549 - GTMSessionFetcher: ebaa1f79a5366922c1735f1566901f50beba23b7 + gRPC: 58828d611419d49da19ad02a60679ffa10a10a87 + gRPC-Core: 66413bf1f2d038a6221bc7bfcbeeaa5a117cee29 + gRPC-ProtoRPC: f29e8b7445e0d3c0311678ab121e6c164da4ca5e + gRPC-RxLibrary: 8e0067bfe8a054022c7a81470baace4f2f633b48 + GTMSessionFetcher: 5bb1eae636127de695590f50e7d248483eb891e6 leveldb-library: '08cba283675b7ed2d99629a4bc5fd052cd2bb6a5' nanopb: 5601e6bca2dbf1ed831b519092ec110f66982ca3 Protobuf: 8a9838fba8dae3389230e1b7f8c104aa32389c03 From 99b4b6550b3d654a276e811d6d686a9d5a9c2771 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 15 Feb 2018 08:11:17 +0000 Subject: [PATCH 21/77] [notifications] WIP Android implementation --- .../RNFirebaseLocalMessagingHelper.java | 272 ----------- .../RNFirebaseLocalMessagingPublisher.java | 14 - .../messaging/RNFirebaseMessaging.java | 2 +- .../RNFirebaseSystemBootEventReceiver.java | 27 -- .../RNFirebaseNotificationManager.java | 434 ++++++++++++++++++ .../RNFirebaseNotificationReceiver.java | 15 + .../RNFirebaseNotifications.java | 54 ++- ...RNFirebaseNotificationsRebootReceiver.java | 18 + .../notifications/AndroidNotification.js | 5 + 9 files changed, 524 insertions(+), 317 deletions(-) delete mode 100644 android/src/main/java/io/invertase/firebase/messaging/RNFirebaseLocalMessagingPublisher.java delete mode 100644 android/src/main/java/io/invertase/firebase/messaging/RNFirebaseSystemBootEventReceiver.java create mode 100644 android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java create mode 100644 android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationReceiver.java create mode 100644 android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationsRebootReceiver.java diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseLocalMessagingHelper.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseLocalMessagingHelper.java index cb32f218..2bfbd12b 100644 --- a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseLocalMessagingHelper.java +++ b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseLocalMessagingHelper.java @@ -43,255 +43,7 @@ public class RNFirebaseLocalMessagingHelper { } public void sendNotification(Bundle bundle) { - try { - Class intentClass = getMainActivityClass(); - if (intentClass == null) { - return; - } - if (bundle.getString("body") == null) { - return; - } - - Resources res = mContext.getResources(); - String packageName = mContext.getPackageName(); - - String title = bundle.getString("title"); - if (title == null) { - ApplicationInfo appInfo = mContext.getApplicationInfo(); - title = mContext.getPackageManager().getApplicationLabel(appInfo).toString(); - } - - NotificationCompat.Builder notification = new NotificationCompat.Builder(mContext) - .setContentTitle(title) - .setContentText(bundle.getString("body")) - .setTicker(bundle.getString("ticker")) - .setVisibility(NotificationCompat.VISIBILITY_PRIVATE) - .setAutoCancel(bundle.getBoolean("auto_cancel", true)) - .setNumber(bundle.getInt("number")) - .setSubText(bundle.getString("sub_text")) - .setGroup(bundle.getString("group")) - .setVibrate(new long[]{0, DEFAULT_VIBRATION}) - .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)) - .setExtras(bundle.getBundle("data")); - - //priority - String priority = bundle.getString("priority", ""); - switch(priority) { - case "min": - notification.setPriority(NotificationCompat.PRIORITY_MIN); - break; - case "high": - notification.setPriority(NotificationCompat.PRIORITY_HIGH); - break; - case "max": - notification.setPriority(NotificationCompat.PRIORITY_MAX); - break; - default: - notification.setPriority(NotificationCompat.PRIORITY_DEFAULT); - } - - //icon - String smallIcon = bundle.getString("icon", "ic_launcher"); - int smallIconResId = res.getIdentifier(smallIcon, "mipmap", packageName); - notification.setSmallIcon(smallIconResId); - - //large icon - String largeIcon = bundle.getString("large_icon"); - if(largeIcon != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP){ - if (largeIcon.startsWith("http://") || largeIcon.startsWith("https://")) { - Bitmap bitmap = getBitmapFromURL(largeIcon); - notification.setLargeIcon(bitmap); - } else { - int largeIconResId = res.getIdentifier(largeIcon, "mipmap", packageName); - Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId); - - if (largeIconResId != 0) { - notification.setLargeIcon(largeIconBitmap); - } - } - } - - //big text - String bigText = bundle.getString("big_text"); - if(bigText != null){ - notification.setStyle(new NotificationCompat.BigTextStyle().bigText(bigText)); - } - - //sound - String soundName = bundle.getString("sound", "default"); - if (!soundName.equalsIgnoreCase("default")) { - int soundResourceId = res.getIdentifier(soundName, "raw", packageName); - if(soundResourceId == 0){ - soundName = soundName.substring(0, soundName.lastIndexOf('.')); - soundResourceId = res.getIdentifier(soundName, "raw", packageName); - } - notification.setSound(Uri.parse("android.resource://" + packageName + "/" + soundResourceId)); - } - - //color - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - notification.setCategory(NotificationCompat.CATEGORY_CALL); - - String color = bundle.getString("color"); - if (color != null) { - notification.setColor(Color.parseColor(color)); - } - } - - //vibrate - if(bundle.containsKey("vibrate")){ - long vibrate = bundle.getLong("vibrate", Math.round(bundle.getDouble("vibrate", bundle.getInt("vibrate")))); - if(vibrate > 0){ - notification.setVibrate(new long[]{0, vibrate}); - }else{ - notification.setVibrate(null); - } - } - - //lights - if (bundle.getBoolean("lights")) { - notification.setDefaults(NotificationCompat.DEFAULT_LIGHTS); - } - - Log.d(TAG, "broadcast intent before showing notification"); - Intent i = new Intent("io.invertase.firebase.messaging.ReceiveLocalNotification"); - i.putExtras(bundle); - mContext.sendOrderedBroadcast(i, null); - - if(!mIsForeground || bundle.getBoolean("show_in_foreground")){ - Intent intent = new Intent(mContext, intentClass); - intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - intent.putExtras(bundle); - intent.setAction(bundle.getString("click_action")); - - int notificationID = bundle.containsKey("id") ? bundle.getString("id", "").hashCode() : (int) System.currentTimeMillis(); - PendingIntent pendingIntent = PendingIntent.getActivity(mContext, notificationID, intent, - PendingIntent.FLAG_UPDATE_CURRENT); - - NotificationManager notificationManager = - (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - - notification.setContentIntent(pendingIntent); - - Notification info = notification.build(); - - if (bundle.containsKey("tag")) { - String tag = bundle.getString("tag"); - notificationManager.notify(tag, notificationID, info); - } else { - notificationManager.notify(notificationID, info); - } - } - //clear out one time scheduled notification once fired - if(!bundle.containsKey("repeat_interval") && bundle.containsKey("fire_date")) { - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.remove(bundle.getString("id")); - editor.apply(); - } - } catch (Exception e) { - Log.e(TAG, "failed to send local notification", e); - } - } - - public void sendNotificationScheduled(Bundle bundle) { - Class intentClass = getMainActivityClass(); - if (intentClass == null) { - return; - } - - String notificationId = bundle.getString("id"); - if(notificationId == null){ - Log.e(TAG, "failed to schedule notification because id is missing"); - return; - } - - Long fireDate = Math.round(bundle.getDouble("fire_date")); - - if (fireDate == 0) { - Log.e(TAG, "failed to schedule notification because fire date is missing"); - return; - } - - Intent notificationIntent = new Intent(mContext, RNFirebaseLocalMessagingPublisher.class); - notificationIntent.putExtras(bundle); - PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, notificationId.hashCode(), notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); - - Long interval = null; - switch (bundle.getString("repeat_interval", "")) { - case "minute": - interval = (long) 60000; - break; - case "hour": - interval = AlarmManager.INTERVAL_HOUR; - break; - case "day": - interval = AlarmManager.INTERVAL_DAY; - break; - case "week": - interval = AlarmManager.INTERVAL_DAY * 7; - break; - } - - if(interval != null){ - getAlarmManager().setRepeating(AlarmManager.RTC_WAKEUP, fireDate, interval, pendingIntent); - } else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){ - getAlarmManager().setExact(AlarmManager.RTC_WAKEUP, fireDate, pendingIntent); - }else { - getAlarmManager().set(AlarmManager.RTC_WAKEUP, fireDate, pendingIntent); - } - - //store intent - SharedPreferences.Editor editor = sharedPreferences.edit(); - try { - JSONObject json = BundleJSONConverter.convertToJSON(bundle); - editor.putString(notificationId, json.toString()); - editor.apply(); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - public void cancelLocalNotification(String notificationId) { - cancelAlarm(notificationId); - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.remove(notificationId); - editor.apply(); - } - - public void cancelAllLocalNotifications() { - java.util.Map keyMap = sharedPreferences.getAll(); - SharedPreferences.Editor editor = sharedPreferences.edit(); - for(java.util.Map.Entry entry:keyMap.entrySet()){ - cancelAlarm(entry.getKey()); - } - editor.clear(); - editor.apply(); - } - - public void removeDeliveredNotification(String notificationId){ - NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(notificationId.hashCode()); - } - - public void removeAllDeliveredNotifications(){ - NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancelAll(); - } - - public ArrayList getScheduledLocalNotifications(){ - ArrayList array = new ArrayList(); - java.util.Map keyMap = sharedPreferences.getAll(); - for(java.util.Map.Entry entry:keyMap.entrySet()){ - try { - JSONObject json = new JSONObject((String)entry.getValue()); - Bundle bundle = BundleJSONConverter.convertToBundle(json); - array.add(bundle); - } catch (JSONException e) { - e.printStackTrace(); - } - } - return array; } public void setApplicationForeground(boolean foreground){ @@ -309,28 +61,4 @@ public class RNFirebaseLocalMessagingHelper { return null; } } - - private AlarmManager getAlarmManager() { - return (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); - } - - private void cancelAlarm(String notificationId) { - Intent notificationIntent = new Intent(mContext, RNFirebaseLocalMessagingPublisher.class); - PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, notificationId.hashCode(), notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); - getAlarmManager().cancel(pendingIntent); - } - - private Bitmap getBitmapFromURL(String strURL) { - try { - URL url = new URL(strURL); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setDoInput(true); - connection.connect(); - InputStream input = connection.getInputStream(); - return BitmapFactory.decodeStream(input); - } catch (IOException e) { - e.printStackTrace(); - return null; - } - } } diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseLocalMessagingPublisher.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseLocalMessagingPublisher.java deleted file mode 100644 index 103b3168..00000000 --- a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseLocalMessagingPublisher.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.invertase.firebase.messaging; - -import android.app.Application; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -public class RNFirebaseLocalMessagingPublisher extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - new RNFirebaseLocalMessagingHelper((Application) context.getApplicationContext()).sendNotification(intent.getExtras()); - } -} diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java index 819e48ae..d87207da 100644 --- a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java +++ b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java @@ -34,7 +34,7 @@ public class RNFirebaseMessaging extends ReactContextBaseJavaModule implements A private static final String BADGE_FILE = "BadgeCountFile"; private static final String BADGE_KEY = "BadgeCount"; - private static final String TAG = RNFirebaseMessaging.class.getCanonicalName(); + private static final String TAG = "RNFirebaseMessaging"; private SharedPreferences sharedPreferences = null; diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseSystemBootEventReceiver.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseSystemBootEventReceiver.java deleted file mode 100644 index b124cde5..00000000 --- a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseSystemBootEventReceiver.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.invertase.firebase.messaging; - -import android.app.Application; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -import java.util.ArrayList; - -import android.os.Bundle; -import android.util.Log; - -/** - * Set alarms for scheduled notification after system reboot. - */ -public class RNFirebaseSystemBootEventReceiver extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - Log.i("FCMSystemBootReceiver", "Received reboot event"); - RNFirebaseLocalMessagingHelper helper = new RNFirebaseLocalMessagingHelper((Application) context.getApplicationContext()); - ArrayList bundles = helper.getScheduledLocalNotifications(); - for(Bundle bundle: bundles){ - helper.sendNotificationScheduled(bundle); - } - } -} diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java new file mode 100644 index 00000000..606334b7 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java @@ -0,0 +1,434 @@ +package io.invertase.firebase.notifications; + + +import android.app.AlarmManager; +import android.app.Application; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.app.NotificationCompat; +import android.util.Log; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReadableMap; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.Map; + +import io.invertase.firebase.messaging.BundleJSONConverter; + +public class RNFirebaseNotificationManager { + private static final String PREFERENCES_KEY = "RNFNotifications"; + private static final String TAG = "RNFNotificationManager"; + private AlarmManager alarmManager; + private Context context; + private NotificationManager notificationManager; + private SharedPreferences preferences; + + public RNFirebaseNotificationManager(Context context) { + this.alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + this.context = context; + this.notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + this.preferences = context.getSharedPreferences(PREFERENCES_KEY, Context.MODE_PRIVATE); + } + + public void cancelAllNotifications() { + try { + Map notifications = preferences.getAll(); + + for(String notificationId : notifications.keySet()){ + cancelAlarm(notificationId); + } + preferences.edit().clear().apply(); + } catch (SecurityException e) { + // TODO: Identify what these situations are + // In some devices/situations cancelAllLocalNotifications can throw a SecurityException. + Log.e(TAG, e.getMessage()); + } + } + + public void cancelNotification(String notificationId) { + cancelAlarm(notificationId); + preferences.edit().remove(notificationId).apply(); + } + + public void displayNotification(Bundle notification) { + displayNotification(notification, null); + } + + public void displayNotification(ReadableMap notification, Promise promise) { + Bundle notificationBundle = Arguments.toBundle(notification); + displayNotification(notificationBundle, promise); + } + + private void displayNotification(Bundle notification, Promise promise) { + try { + Class intentClass = getMainActivityClass(); + if (intentClass == null) { + return; + } + + if (bundle.getString("body") == null) { + return; + } + + Resources res = mContext.getResources(); + String packageName = mContext.getPackageName(); + + String channelId = notification.getString("channelId"); + + NotificationCompat.Builder nb = new NotificationCompat.Builder(context, channelId); + + if (notification.containsKey("body")) { + nb = nb.setContentText(notification.getString("body")); + } + if (notification.containsKey("data")) { + nb = nb.setExtras(notification.getBundle("data")); + } + if (notification.containsKey("sound")) { + // TODO: Sound URI; + nb = nb.setSound(); + } + if (notification.containsKey("subtitle")) { + nb = nb.setSubText(notification.getString("subtitle")); + } + if (notification.containsKey("title")) { + nb = nb.setContentTitle(notification.getString("title")); + } + + if (notification.containsKey("autoCancel")) { + nb = nb.setAutoCancel(notification.getBoolean("autoCancel")); + } + if (notification.containsKey("badgeIconType")) { + nb = nb.setBadgeIconType(notification.getInt("badgeIconType")); + } + if (notification.containsKey("category")) { + nb = nb.setCategory(notification.getString("category")); + } + if (notification.containsKey("color")) { + nb = nb.setColor(notification.getInt("color")); + } + if (notification.containsKey("colorized")) { + nb = nb.setColorized(notification.getBoolean("colorized")); + } + if (notification.containsKey("contentInfo")) { + nb = nb.setContentInfo(notification.getString("contentInfo")); + } + if (notification.containsKey("defaults")) { + // TODO: Bitwise ? + nb = nb.setDefaults() + } + if (notification.containsKey("group")) { + nb = nb.setGroup(notification.getString("group")); + } + if (notification.containsKey("groupAlertBehaviour")) { + nb = nb.setGroupAlertBehavior(notification.getInt("groupAlertBehaviour")); + } + if (notification.containsKey("groupSummary")) { + nb = nb.setGroupSummary(notification.getBoolean("groupSummary")); + } + if (notification.containsKey("largeIcon")) { + Bitmap largeIcon = getBitmap(notification.getString("largeIcon")); + if (largeIcon != null) { + nb = nb.setLargeIcon(largeIcon); + } + } + if (notification.containsKey("lights")) { + Bundle lights = notification.getBundle("lights"); + nb = nb.setLights(lights.getInt("argb"), lights.getInt("onMs"), lights.getInt("offMs")); + } + if (notification.containsKey("localOnly")) { + nb = nb.setLocalOnly(notification.getBoolean("localOnly")); + } + + if (notification.containsKey("number")) { + nb = nb.setNumber(notification.getInt("number")); + } + if (notification.containsKey("ongoing")) { + nb = nb.setOngoing(notification.getBoolean("ongoing")); + } + if (notification.containsKey("onlyAlertOnce")) { + nb = nb.setOngoing(notification.getBoolean("onlyAlertOnce")); + } + if (notification.containsKey("people")) { + String[] people = notification.getStringArray("people"); + for (String person : people) { + nb = nb.addPerson(person); + } + } + if (notification.containsKey("priority")) { + nb = nb.setPriority(notification.getInt("priority")); + } + if (notification.containsKey("progress")) { + Bundle progress = notification.getBundle("lights"); + nb = nb.setProgress(progress.getInt("max"), progress.getInt("progress"), progress.getBoolean("indeterminate")); + } + if (notification.containsKey("publicVersion")) { + // TODO: Build notification + nb = nb.setPublicVersion(); + } + if (notification.containsKey("remoteInputHistory")) { + // TODO: Build notification + nb = nb.setRemoteInputHistory(notification.getStringArray("remoteInputHistory")); + } + if (notification.containsKey("shortcutId")) { + nb = nb.setShortcutId(notification.getString("shortcutId")); + } + if (notification.containsKey("showWhen")) { + nb = nb.setShowWhen(notification.getBoolean("showWhen")); + } + if (notification.containsKey("smallIcon")) { + nb = nb.setSmallIcon(notification.getInt("smallIcon")); + } + if (notification.containsKey("sortKey")) { + nb = nb.setSortKey(notification.getString("sortKey")); + } + if (notification.containsKey("ticker")) { + nb = nb.setTicker(notification.getString("ticker")); + } + if (notification.containsKey("timeoutAfter")) { + nb = nb.setTimeoutAfter(notification.getLong("timeoutAfter")); + } + if (notification.containsKey("usesChronometer")) { + nb = nb.setUsesChronometer(notification.getBoolean("usesChronometer")); + } + if (notification.containsKey("vibrate")) { + nb = nb.setVibrate(notification.getLongArray("vibrate")); + } + if (notification.containsKey("visibility")) { + nb = nb.setVisibility(notification.getInt("visibility")); + } + if (notification.containsKey("when")) { + nb = nb.setWhen(notification.getLong("when")); + } + // TODO actions: Action[]; // icon, title, ??pendingIntent??, allowGeneratedReplies, extender, extras, remoteinput (ugh) + // TODO: style: Style; // Need to figure out if this can work + + //icon + String smallIcon = bundle.getString("icon", "ic_launcher"); + int smallIconResId = res.getIdentifier(smallIcon, "mipmap", packageName); + notification.setSmallIcon(smallIconResId); + + //big text + String bigText = bundle.getString("big_text"); + if(bigText != null){ + notification.setStyle(new NotificationCompat.BigTextStyle().bigText(bigText)); + } + + //sound + String soundName = bundle.getString("sound", "default"); + if (!soundName.equalsIgnoreCase("default")) { + int soundResourceId = res.getIdentifier(soundName, "raw", packageName); + if(soundResourceId == 0){ + soundName = soundName.substring(0, soundName.lastIndexOf('.')); + soundResourceId = res.getIdentifier(soundName, "raw", packageName); + } + notification.setSound(Uri.parse("android.resource://" + packageName + "/" + soundResourceId)); + } + + //color + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + String color = bundle.getString("color"); + if (color != null) { + notification.setColor(Color.parseColor(color)); + } + } + + //lights + if (bundle.getBoolean("lights")) { + notification.setDefaults(NotificationCompat.DEFAULT_LIGHTS); + } + + Log.d(TAG, "broadcast intent before showing notification"); + Intent i = new Intent("io.invertase.firebase.messaging.ReceiveLocalNotification"); + i.putExtras(bundle); + mContext.sendOrderedBroadcast(i, null); + + if(!mIsForeground || bundle.getBoolean("show_in_foreground")){ + Intent intent = new Intent(mContext, intentClass); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.putExtras(bundle); + intent.setAction(bundle.getString("click_action")); + + int notificationID = bundle.containsKey("id") ? bundle.getString("id", "").hashCode() : (int) System.currentTimeMillis(); + PendingIntent pendingIntent = PendingIntent.getActivity(mContext, notificationID, intent, + PendingIntent.FLAG_UPDATE_CURRENT); + + NotificationManager notificationManager = + (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + + notification.setContentIntent(pendingIntent); + + Notification info = notification.build(); + + if (bundle.containsKey("tag")) { + String tag = bundle.getString("tag"); + notificationManager.notify(tag, notificationID, info); + } else { + notificationManager.notify(notificationID, info); + } + } + //clear out one time scheduled notification once fired + if(!bundle.containsKey("repeat_interval") && bundle.containsKey("fire_date")) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.remove(bundle.getString("id")); + editor.apply(); + } + } catch (Exception e) { + Log.e(TAG, "failed to send local notification", e); + } + } + + public ArrayList getScheduledNotifications(){ + ArrayList array = new ArrayList<>(); + + Map notifications = preferences.getAll(); + + for(String notificationId : notifications.keySet()){ + try { + JSONObject json = new JSONObject((String)notifications.get(notificationId)); + Bundle bundle = BundleJSONConverter.convertToBundle(json); + array.add(bundle); + } catch (JSONException e) { + Log.e(TAG, e.getMessage()); + } + } + return array; + } + + public void removeAllDeliveredNotifications() { + notificationManager.cancelAll(); + } + + public void removeDeliveredNotification(String notificationId) { + notificationManager.cancel(notificationId.hashCode()); + } + + + public void rescheduleNotifications() { + ArrayList bundles = getScheduledNotifications(); + for(Bundle bundle: bundles){ + scheduleNotification(bundle, null); + } + } + + public void scheduleNotification(ReadableMap notification, ReadableMap schedule, Promise promise) { + Bundle notificationBundle = Arguments.toBundle(notification); + + scheduleNotification(notificationBundle, promise); + } + + private void scheduleNotification(Bundle notification, Promise promise) { + // TODO + String notificationId = notification.getString("notificationId"); + if (!notification.containsKey("notificationId")) { + if (promise != null) { + promise.reject("notification/schedule_notification_error", "Missing notificationId"); + } else { + Log.e(TAG, "Missing notificationId"); + } + return; + } + + // TODO: Schedule check + if (!notification.hasKey("schedule")) { + + return; + } + /* + Long fireDate = Math.round(bundle.getDouble("fire_date")); + + if (fireDate == 0) { + Log.e(TAG, "failed to schedule notification because fire date is missing"); + return; + }*/ + + + // Scheduled alarms are cleared on restart + // We store them so that they can be re-scheduled when the phone restarts in RNFirebaseNotificationsRebootReceiver + try { + JSONObject json = BundleJSONConverter.convertToJSON(notification); + preferences.edit().putString(notificationId, json.toString()).apply(); + } catch (JSONException e) { + promise.reject("notification/schedule_notification_error", "Failed to store notification", e); + return; + } + + + Intent notificationIntent = new Intent(context, RNFirebaseNotificationReceiver.class); + notificationIntent.putExtras(notification); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, notificationId.hashCode(), notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + // TODO: Scheduling + Long interval = null; + switch (notification.getString("repeat_interval", "")) { + case "minute": + interval = (long) 60000; + break; + case "hour": + interval = AlarmManager.INTERVAL_HOUR; + break; + case "day": + interval = AlarmManager.INTERVAL_DAY; + break; + case "week": + interval = AlarmManager.INTERVAL_DAY * 7; + break; + } + + if(interval != null){ + alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, fireDate, interval, pendingIntent); + } else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){ + alarmManager.setExact(AlarmManager.RTC_WAKEUP, fireDate, pendingIntent); + }else { + alarmManager.set(AlarmManager.RTC_WAKEUP, fireDate, pendingIntent); + } + } + + private void cancelAlarm(String notificationId) { + Intent notificationIntent = new Intent(context, RNFirebaseNotificationManager.class); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, notificationId.hashCode(), notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + alarmManager.cancel(pendingIntent); + } + + private Bitmap getBitmap(String image) { + if (image.startsWith("http://") || image.startsWith("https://")) { + return getBitmapFromUrl(image); + } else { + int largeIconResId = res.getIdentifier(image, "mipmap", packageName); + return BitmapFactory.decodeResource(res, largeIconResId); + } + } + + private Bitmap getBitmapFromUrl(String imageUrl) { + try { + HttpURLConnection connection = (HttpURLConnection) new URL(imageUrl).openConnection(); + connection.setDoInput(true); + connection.connect(); + return BitmapFactory.decodeStream(connection.getInputStream()); + } catch (IOException e) { + Log.e(TAG, e.getMessage()); + return null; + } + } +} diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationReceiver.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationReceiver.java new file mode 100644 index 00000000..50ff4133 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationReceiver.java @@ -0,0 +1,15 @@ +package io.invertase.firebase.notifications; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/* + * This is invoked by the Alarm Manager when it is time to display a scheduled notification. + */ +public class RNFirebaseNotificationReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + new RNFirebaseNotificationManager(context).displayNotification(intent.getExtras()); + } +} diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java index 5aee7f17..230b486a 100644 --- a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java @@ -1,15 +1,24 @@ package io.invertase.firebase.notifications; +import android.os.Bundle; import android.support.v4.app.NotificationCompat; +import android.util.Log; +import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableArray; + +import java.util.ArrayList; public class RNFirebaseNotifications extends ReactContextBaseJavaModule { + private RNFirebaseNotificationManager notificationManager; public RNFirebaseNotifications(ReactApplicationContext context) { super(context); + notificationManager = new RNFirebaseNotificationManager(context.getApplicationContext()); } @Override @@ -18,8 +27,47 @@ public class RNFirebaseNotifications extends ReactContextBaseJavaModule { } @ReactMethod - public void sendNotification(Promise promise) { - // - NotificationCompat.Builder builder = new NotificationCompat.Builder() + public void cancelAllNotifications() { + notificationManager.cancelAllNotifications(); + } + + @ReactMethod + public void cancelNotification(String notificationId) { + notificationManager.cancelNotification(notificationId); + } + + @ReactMethod + public void displayNotification(ReadableMap notification, Promise promise) { + notificationManager.displayNotification(notification, promise); + } + + @ReactMethod + public void getInitialNotification(Promise promise) { + // TODO + } + + @ReactMethod + public void getScheduledNotifications(Promise promise) { + ArrayList bundles = notificationManager.getScheduledNotifications(); + WritableArray array = Arguments.createArray(); + for (Bundle bundle : bundles) { + array.pushMap(parseNotificationBundle(bundle)); + } + promise.resolve(array); + } + + @ReactMethod + public void removeAllDeliveredNotifications() { + notificationManager.removeAllDeliveredNotifications(); + } + + @ReactMethod + public void removeDeliveredNotification(String notificationId) { + notificationManager.removeDeliveredNotification(notificationId); + } + + @ReactMethod + public void scheduleNotification(ReadableMap notification, ReadableMap schedule, Promise promise) { + notificationManager.scheduleNotification(notification, schedule, promise); } } diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationsRebootReceiver.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationsRebootReceiver.java new file mode 100644 index 00000000..80e6750f --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationsRebootReceiver.java @@ -0,0 +1,18 @@ +package io.invertase.firebase.notifications; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +/* + * This is invoked when the phone restarts to ensure that all notifications are rescheduled + * correctly, as Android removes all scheduled alarms when the phone shuts down. + */ +public class RNFirebaseNotificationsRebootReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + Log.i("RNFNotifRebootReceiver", "Received reboot event"); + new RNFirebaseNotificationManager(context).rescheduleNotifications(); + } +} diff --git a/lib/modules/notifications/AndroidNotification.js b/lib/modules/notifications/AndroidNotification.js index 12ed4331..104e13da 100644 --- a/lib/modules/notifications/AndroidNotification.js +++ b/lib/modules/notifications/AndroidNotification.js @@ -505,6 +505,11 @@ export default class AndroidNotification { build(): NativeAndroidNotification { // TODO: Validation + if (!this._channelId) { + throw new Error( + 'AndroidNotification: Missing required `channelId` property' + ); + } return { // TODO actions: Action[], From cd0ef4e3b708bc85c7f436b8a822260f2eb903dc Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 15 Feb 2018 14:59:21 +0000 Subject: [PATCH 22/77] [notifications] Continue android implementation --- android/build.gradle | 5 +- .../RNFirebaseLocalMessagingHelper.java | 15 - .../messaging/RNFirebaseMessaging.java | 6 + .../RNFirebaseNotificationManager.java | 407 ++++++++++-------- .../RNFirebaseNotificationReceiver.java | 2 +- .../RNFirebaseNotifications.java | 64 ++- .../notifications/RNFirebaseNotifications.m | 72 +++- lib/modules/instanceid/index.js | 2 +- .../notifications/AndroidNotification.js | 35 +- lib/modules/notifications/IOSNotification.js | 24 +- lib/modules/notifications/Notification.js | 27 +- lib/modules/notifications/index.js | 16 +- tests/android/app/build.gradle | 4 +- tests/android/build.gradle | 6 +- tests/ios/Podfile.lock | 4 +- 15 files changed, 413 insertions(+), 276 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index ba96df94..fdb05589 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -15,8 +15,8 @@ buildscript { apply plugin: 'com.android.library' android { - compileSdkVersion 26 - buildToolsVersion "25.0.3" + compileSdkVersion 27 + buildToolsVersion "27.0.2" defaultConfig { minSdkVersion 16 targetSdkVersion 26 @@ -82,6 +82,7 @@ rootProject.gradle.buildFinished { buildResult -> dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') compile "com.facebook.react:react-native:+" // From node_modules + compile "com.android.support:support-v4:27.0.2" compile 'me.leolin:ShortcutBadger:1.1.21@aar' compile "com.google.android.gms:play-services-base:$firebaseVersion" compile "com.google.firebase:firebase-core:$firebaseVersion" diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseLocalMessagingHelper.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseLocalMessagingHelper.java index 2bfbd12b..366e1923 100644 --- a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseLocalMessagingHelper.java +++ b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseLocalMessagingHelper.java @@ -42,23 +42,8 @@ public class RNFirebaseLocalMessagingHelper { sharedPreferences = mContext.getSharedPreferences(PREFERENCES_KEY, Context.MODE_PRIVATE); } - public void sendNotification(Bundle bundle) { - - } - public void setApplicationForeground(boolean foreground){ mIsForeground = foreground; } - private Class getMainActivityClass() { - String packageName = mContext.getPackageName(); - Intent launchIntent = mContext.getPackageManager().getLaunchIntentForPackage(packageName); - String className = launchIntent.getComponent().getClassName(); - try { - return Class.forName(className); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - return null; - } - } } diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java index d87207da..7376e452 100644 --- a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java +++ b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java @@ -155,6 +155,9 @@ public class RNFirebaseMessaging extends ReactContextBaseJavaModule implements A FirebaseMessaging.getInstance().unsubscribeFromTopic(topic); } + ////////////////////////////////////////////////////////////////////// + // Start ActivityEventListener methods + ////////////////////////////////////////////////////////////////////// @Override public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { // FCM functionality does not need this function @@ -168,6 +171,9 @@ public class RNFirebaseMessaging extends ReactContextBaseJavaModule implements A Utils.sendEvent(getReactApplicationContext(), "messaging_message_received", messageMap); } } + ////////////////////////////////////////////////////////////////////// + // End ActivityEventListener methods + ////////////////////////////////////////////////////////////////////// private WritableMap parseIntentForMessage(Intent intent) { // Check if FCM data exists diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java index 606334b7..387727ba 100644 --- a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java @@ -2,15 +2,12 @@ package io.invertase.firebase.notifications; import android.app.AlarmManager; -import android.app.Application; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.ApplicationInfo; -import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; @@ -19,6 +16,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.v4.app.NotificationCompat; +import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import com.facebook.react.bridge.Arguments; @@ -29,7 +27,6 @@ import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; -import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; @@ -39,9 +36,11 @@ import io.invertase.firebase.messaging.BundleJSONConverter; public class RNFirebaseNotificationManager { private static final String PREFERENCES_KEY = "RNFNotifications"; + public static final String SCHEDULED_NOTIFICATION_EVENT = "notifications-scheduled-notification"; private static final String TAG = "RNFNotificationManager"; private AlarmManager alarmManager; private Context context; + private boolean isForeground = false; private NotificationManager notificationManager; private SharedPreferences preferences; @@ -72,32 +71,100 @@ public class RNFirebaseNotificationManager { preferences.edit().remove(notificationId).apply(); } - public void displayNotification(Bundle notification) { - displayNotification(notification, null); - } - public void displayNotification(ReadableMap notification, Promise promise) { Bundle notificationBundle = Arguments.toBundle(notification); displayNotification(notificationBundle, promise); } + public void displayScheduledNotification(Bundle notification) { + // Broadcast the notification to the RN Application + Intent scheduledNotificationEvent = new Intent(SCHEDULED_NOTIFICATION_EVENT); + scheduledNotificationEvent.putExtra("notification", notification); + LocalBroadcastManager.getInstance(context).sendBroadcast(scheduledNotificationEvent); + + // If this isn't a repeated notification, clear it from the scheduled notifications list + if (!notification.getBundle("schedule").containsKey("repeated") + || !notification.getBundle("schedule").getBoolean("repeated")) { + String notificationId = notification.getString("notificationId"); + preferences.edit().remove(notificationId).apply();; + } + + // If the app isn't in the foreground, then we display it + // Otherwise, it is up to the JS to decide whether to send the notification + if (!isForeground) { + displayNotification(notification, null); + } + } + + public ArrayList getScheduledNotifications(){ + ArrayList array = new ArrayList<>(); + + Map notifications = preferences.getAll(); + + for(String notificationId : notifications.keySet()){ + try { + JSONObject json = new JSONObject((String)notifications.get(notificationId)); + Bundle bundle = BundleJSONConverter.convertToBundle(json); + array.add(bundle); + } catch (JSONException e) { + Log.e(TAG, e.getMessage()); + } + } + return array; + } + + public void removeAllDeliveredNotifications() { + notificationManager.cancelAll(); + } + + public void removeDeliveredNotification(String notificationId) { + notificationManager.cancel(notificationId.hashCode()); + } + + + public void rescheduleNotifications() { + ArrayList bundles = getScheduledNotifications(); + for(Bundle bundle: bundles){ + scheduleNotification(bundle, null); + } + } + + public void scheduleNotification(ReadableMap notification, Promise promise) { + Bundle notificationBundle = Arguments.toBundle(notification); + + scheduleNotification(notificationBundle, promise); + } + + public void setIsForeground(boolean isForeground) { + this.isForeground = isForeground; + } + + private void cancelAlarm(String notificationId) { + Intent notificationIntent = new Intent(context, RNFirebaseNotificationManager.class); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, notificationId.hashCode(), notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + alarmManager.cancel(pendingIntent); + } + private void displayNotification(Bundle notification, Promise promise) { try { Class intentClass = getMainActivityClass(); if (intentClass == null) { + if (promise != null) { + promise.reject("notification/display_notification_error", "Could not find main activity class"); + } return; } - if (bundle.getString("body") == null) { - return; - } - - Resources res = mContext.getResources(); - String packageName = mContext.getPackageName(); - String channelId = notification.getString("channelId"); + String notificationId = notification.getString("notificationId"); - NotificationCompat.Builder nb = new NotificationCompat.Builder(context, channelId); + NotificationCompat.Builder nb; + // TODO: Change 27 to 'Build.VERSION_CODES.O_MR1' when using appsupport v27 + if (Build.VERSION.SDK_INT >= 27) { + nb = new NotificationCompat.Builder(context, channelId); + } else { + nb = new NotificationCompat.Builder(context); + } if (notification.containsKey("body")) { nb = nb.setContentText(notification.getString("body")); @@ -106,8 +173,16 @@ public class RNFirebaseNotificationManager { nb = nb.setExtras(notification.getBundle("data")); } if (notification.containsKey("sound")) { - // TODO: Sound URI; - nb = nb.setSound(); + String sound = notification.getString("sound"); + if (sound.equalsIgnoreCase("default")) { + nb = nb.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)); + } else { + int soundResourceId = getResourceId("raw", sound); + if (soundResourceId == 0) { + soundResourceId = getResourceId("raw", sound.substring(0, sound.lastIndexOf('.'))); + } + nb = nb.setSound(Uri.parse("android.resource://" + context.getPackageName() + "/" + soundResourceId)); + } } if (notification.containsKey("subtitle")) { nb = nb.setSubText(notification.getString("subtitle")); @@ -126,7 +201,8 @@ public class RNFirebaseNotificationManager { nb = nb.setCategory(notification.getString("category")); } if (notification.containsKey("color")) { - nb = nb.setColor(notification.getInt("color")); + String color = notification.getString("color"); + nb = nb.setColor(Color.parseColor(color)); } if (notification.containsKey("colorized")) { nb = nb.setColorized(notification.getBoolean("colorized")); @@ -135,8 +211,12 @@ public class RNFirebaseNotificationManager { nb = nb.setContentInfo(notification.getString("contentInfo")); } if (notification.containsKey("defaults")) { - // TODO: Bitwise ? - nb = nb.setDefaults() + int[] defaultsArray = notification.getIntArray("defaults"); + int defaults = 0; + for (int d : defaultsArray) { + defaults |= d; + } + nb = nb.setDefaults(defaults); } if (notification.containsKey("group")) { nb = nb.setGroup(notification.getString("group")); @@ -183,12 +263,11 @@ public class RNFirebaseNotificationManager { Bundle progress = notification.getBundle("lights"); nb = nb.setProgress(progress.getInt("max"), progress.getInt("progress"), progress.getBoolean("indeterminate")); } - if (notification.containsKey("publicVersion")) { - // TODO: Build notification + // TODO: Public version of notification + /* if (notification.containsKey("publicVersion")) { nb = nb.setPublicVersion(); - } + } */ if (notification.containsKey("remoteInputHistory")) { - // TODO: Build notification nb = nb.setRemoteInputHistory(notification.getStringArray("remoteInputHistory")); } if (notification.containsKey("shortcutId")) { @@ -198,7 +277,18 @@ public class RNFirebaseNotificationManager { nb = nb.setShowWhen(notification.getBoolean("showWhen")); } if (notification.containsKey("smallIcon")) { - nb = nb.setSmallIcon(notification.getInt("smallIcon")); + Bundle smallIcon = notification.getBundle("smallIcon"); + int smallIconResourceId = getResourceId("mipmap", smallIcon.getString("icon")); + if (smallIconResourceId == 0) { + smallIconResourceId = getResourceId("drawable", smallIcon.getString("icon")); + } + if (smallIconResourceId != 0) { + if (smallIcon.containsKey("level")) { + nb = nb.setSmallIcon(smallIconResourceId, smallIcon.getInt("level")); + } else { + nb = nb.setSmallIcon(smallIconResourceId); + } + } } if (notification.containsKey("sortKey")) { nb = nb.setSortKey(notification.getString("sortKey")); @@ -221,148 +311,109 @@ public class RNFirebaseNotificationManager { if (notification.containsKey("when")) { nb = nb.setWhen(notification.getLong("when")); } - // TODO actions: Action[]; // icon, title, ??pendingIntent??, allowGeneratedReplies, extender, extras, remoteinput (ugh) - // TODO: style: Style; // Need to figure out if this can work - //icon - String smallIcon = bundle.getString("icon", "ic_launcher"); - int smallIconResId = res.getIdentifier(smallIcon, "mipmap", packageName); - notification.setSmallIcon(smallIconResId); - - //big text - String bigText = bundle.getString("big_text"); + // TODO: Big text / Big picture + /* String bigText = bundle.getString("big_text"); if(bigText != null){ notification.setStyle(new NotificationCompat.BigTextStyle().bigText(bigText)); } + String picture = bundle.getString("picture"); + if(picture!=null){ + NotificationCompat.BigPictureStyle bigPicture = new NotificationCompat.BigPictureStyle(); - //sound - String soundName = bundle.getString("sound", "default"); - if (!soundName.equalsIgnoreCase("default")) { - int soundResourceId = res.getIdentifier(soundName, "raw", packageName); - if(soundResourceId == 0){ - soundName = soundName.substring(0, soundName.lastIndexOf('.')); - soundResourceId = res.getIdentifier(soundName, "raw", packageName); + Bitmap pictureBitmap = getBitmap(picture); + if (pictureBitmap != null) { + bigPicture.bigPicture(pictureBitmap); } - notification.setSound(Uri.parse("android.resource://" + packageName + "/" + soundResourceId)); + bigPicture.setBigContentTitle(title); + bigPicture.setSummaryText(bundle.getString("body")); + + notification.setStyle(bigPicture); + } */ + + // Create the notification intent + Intent intent = new Intent(context, intentClass); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.putExtras(notification); + if (notification.containsKey("clickAction")) { + intent.setAction(notification.getString("clickAction")); } - //color - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - String color = bundle.getString("color"); - if (color != null) { - notification.setColor(Color.parseColor(color)); - } - } + PendingIntent contentIntent = PendingIntent.getActivity(context, notificationId.hashCode(), intent, + PendingIntent.FLAG_UPDATE_CURRENT); + nb = nb.setContentIntent(contentIntent); - //lights - if (bundle.getBoolean("lights")) { - notification.setDefaults(NotificationCompat.DEFAULT_LIGHTS); - } - - Log.d(TAG, "broadcast intent before showing notification"); - Intent i = new Intent("io.invertase.firebase.messaging.ReceiveLocalNotification"); - i.putExtras(bundle); - mContext.sendOrderedBroadcast(i, null); - - if(!mIsForeground || bundle.getBoolean("show_in_foreground")){ - Intent intent = new Intent(mContext, intentClass); - intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - intent.putExtras(bundle); - intent.setAction(bundle.getString("click_action")); - - int notificationID = bundle.containsKey("id") ? bundle.getString("id", "").hashCode() : (int) System.currentTimeMillis(); - PendingIntent pendingIntent = PendingIntent.getActivity(mContext, notificationID, intent, - PendingIntent.FLAG_UPDATE_CURRENT); - - NotificationManager notificationManager = - (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - - notification.setContentIntent(pendingIntent); - - Notification info = notification.build(); - - if (bundle.containsKey("tag")) { - String tag = bundle.getString("tag"); - notificationManager.notify(tag, notificationID, info); - } else { - notificationManager.notify(notificationID, info); - } - } - //clear out one time scheduled notification once fired - if(!bundle.containsKey("repeat_interval") && bundle.containsKey("fire_date")) { - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.remove(bundle.getString("id")); - editor.apply(); - } + // Build the notification and send it + Notification builtNotification = nb.build(); + notificationManager.notify(notificationId.hashCode(), builtNotification); } catch (Exception e) { - Log.e(TAG, "failed to send local notification", e); - } - } - - public ArrayList getScheduledNotifications(){ - ArrayList array = new ArrayList<>(); - - Map notifications = preferences.getAll(); - - for(String notificationId : notifications.keySet()){ - try { - JSONObject json = new JSONObject((String)notifications.get(notificationId)); - Bundle bundle = BundleJSONConverter.convertToBundle(json); - array.add(bundle); - } catch (JSONException e) { - Log.e(TAG, e.getMessage()); + if (promise == null) { + Log.e(TAG, "Failed to send notification", e); + } else { + promise.reject("notification/display_notification_error", "Could not send notification", e); } } - return array; } - public void removeAllDeliveredNotifications() { - notificationManager.cancelAll(); - } - - public void removeDeliveredNotification(String notificationId) { - notificationManager.cancel(notificationId.hashCode()); - } - - - public void rescheduleNotifications() { - ArrayList bundles = getScheduledNotifications(); - for(Bundle bundle: bundles){ - scheduleNotification(bundle, null); + private Bitmap getBitmap(String image) { + if (image.startsWith("http://") || image.startsWith("https://")) { + return getBitmapFromUrl(image); + } else { + int largeIconResId = getResourceId("mipmap", image); + return BitmapFactory.decodeResource(context.getResources(), largeIconResId); } } - public void scheduleNotification(ReadableMap notification, ReadableMap schedule, Promise promise) { - Bundle notificationBundle = Arguments.toBundle(notification); + private Bitmap getBitmapFromUrl(String imageUrl) { + try { + HttpURLConnection connection = (HttpURLConnection) new URL(imageUrl).openConnection(); + connection.setDoInput(true); + connection.connect(); + return BitmapFactory.decodeStream(connection.getInputStream()); + } catch (IOException e) { + Log.e(TAG, "Failed to get bitmap for url: " + imageUrl, e); + return null; + } + } - scheduleNotification(notificationBundle, promise); + private Class getMainActivityClass() { + String packageName = context.getPackageName(); + Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName); + try { + return Class.forName(launchIntent.getComponent().getClassName()); + } catch (ClassNotFoundException e) { + Log.e(TAG, "Failed to get main activity class", e); + return null; + } + } + + private int getResourceId(String type, String image) { + return context.getResources().getIdentifier(image, type, context.getPackageName()); } private void scheduleNotification(Bundle notification, Promise promise) { - // TODO - String notificationId = notification.getString("notificationId"); if (!notification.containsKey("notificationId")) { - if (promise != null) { - promise.reject("notification/schedule_notification_error", "Missing notificationId"); - } else { + if (promise == null) { Log.e(TAG, "Missing notificationId"); + } else { + promise.reject("notification/schedule_notification_error", "Missing notificationId"); } return; } - // TODO: Schedule check - if (!notification.hasKey("schedule")) { - + if (!notification.containsKey("schedule")) { + if (promise == null) { + Log.e(TAG, "Missing schedule information"); + } else { + promise.reject("notification/schedule_notification_error", "Missing schedule information"); + } return; } - /* - Long fireDate = Math.round(bundle.getDouble("fire_date")); - if (fireDate == 0) { - Log.e(TAG, "failed to schedule notification because fire date is missing"); - return; - }*/ + String notificationId = notification.getString("notificationId"); + Bundle schedule = notification.getBundle("schedule"); + long fireDate = schedule.getLong("fireDate"); // Scheduled alarms are cleared on restart // We store them so that they can be re-scheduled when the phone restarts in RNFirebaseNotificationsRebootReceiver @@ -374,61 +425,47 @@ public class RNFirebaseNotificationManager { return; } - Intent notificationIntent = new Intent(context, RNFirebaseNotificationReceiver.class); notificationIntent.putExtras(notification); - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, notificationId.hashCode(), notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, notificationId.hashCode(), + notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); - // TODO: Scheduling - Long interval = null; - switch (notification.getString("repeat_interval", "")) { - case "minute": - interval = (long) 60000; - break; - case "hour": - interval = AlarmManager.INTERVAL_HOUR; - break; - case "day": - interval = AlarmManager.INTERVAL_DAY; - break; - case "week": - interval = AlarmManager.INTERVAL_DAY * 7; - break; - } + if (schedule.containsKey("interval")) { + Long interval = null; + switch (schedule.getString("interval")) { + case "minute": + interval = 60000L; + break; + case "hour": + interval = AlarmManager.INTERVAL_HOUR; + break; + case "day": + interval = AlarmManager.INTERVAL_DAY; + break; + case "week": + interval = AlarmManager.INTERVAL_DAY * 7; + break; + default: + Log.e(TAG, "Invalid interval: " + schedule.getString("interval")); + break; + } + + if (interval == null) { + promise.reject("notification/schedule_notification_error", "Invalid interval"); + return; + } - if(interval != null){ alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, fireDate, interval, pendingIntent); - } else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){ - alarmManager.setExact(AlarmManager.RTC_WAKEUP, fireDate, pendingIntent); - }else { - alarmManager.set(AlarmManager.RTC_WAKEUP, fireDate, pendingIntent); - } - } - - private void cancelAlarm(String notificationId) { - Intent notificationIntent = new Intent(context, RNFirebaseNotificationManager.class); - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, notificationId.hashCode(), notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); - alarmManager.cancel(pendingIntent); - } - - private Bitmap getBitmap(String image) { - if (image.startsWith("http://") || image.startsWith("https://")) { - return getBitmapFromUrl(image); } else { - int largeIconResId = res.getIdentifier(image, "mipmap", packageName); - return BitmapFactory.decodeResource(res, largeIconResId); + if (schedule.containsKey("exact") + && schedule.getBoolean("exact") + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + alarmManager.setExact(AlarmManager.RTC_WAKEUP, fireDate, pendingIntent); + } else { + alarmManager.set(AlarmManager.RTC_WAKEUP, fireDate, pendingIntent); + } } - } - private Bitmap getBitmapFromUrl(String imageUrl) { - try { - HttpURLConnection connection = (HttpURLConnection) new URL(imageUrl).openConnection(); - connection.setDoInput(true); - connection.connect(); - return BitmapFactory.decodeStream(connection.getInputStream()); - } catch (IOException e) { - Log.e(TAG, e.getMessage()); - return null; - } + promise.resolve(null); } } diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationReceiver.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationReceiver.java index 50ff4133..903daf65 100644 --- a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationReceiver.java +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationReceiver.java @@ -10,6 +10,6 @@ import android.content.Intent; public class RNFirebaseNotificationReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - new RNFirebaseNotificationManager(context).displayNotification(intent.getExtras()); + new RNFirebaseNotificationManager(context).displayScheduledNotification(intent.getExtras()); } } diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java index 230b486a..7e20f365 100644 --- a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java @@ -1,24 +1,41 @@ package io.invertase.firebase.notifications; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.os.Bundle; -import android.support.v4.app.NotificationCompat; +import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; import java.util.ArrayList; -public class RNFirebaseNotifications extends ReactContextBaseJavaModule { +import io.invertase.firebase.Utils; + +public class RNFirebaseNotifications extends ReactContextBaseJavaModule implements LifecycleEventListener { + private static final String TAG = "RNFirebaseNotifications"; + private RNFirebaseNotificationManager notificationManager; public RNFirebaseNotifications(ReactApplicationContext context) { super(context); notificationManager = new RNFirebaseNotificationManager(context.getApplicationContext()); + context.addLifecycleEventListener(this); + + LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context); + + // Subscribe to scheduled notification events + localBroadcastManager.registerReceiver(new ScheduledNotificationReceiver(), + new IntentFilter(RNFirebaseNotificationManager.SCHEDULED_NOTIFICATION_EVENT)); } @Override @@ -67,7 +84,46 @@ public class RNFirebaseNotifications extends ReactContextBaseJavaModule { } @ReactMethod - public void scheduleNotification(ReadableMap notification, ReadableMap schedule, Promise promise) { - notificationManager.scheduleNotification(notification, schedule, promise); + public void scheduleNotification(ReadableMap notification, Promise promise) { + notificationManager.scheduleNotification(notification, promise); + } + + ////////////////////////////////////////////////////////////////////// + // Start LifecycleEventListener methods + ////////////////////////////////////////////////////////////////////// + @Override + public void onHostResume() { + notificationManager.setIsForeground(true); + } + + @Override + public void onHostPause() { + notificationManager.setIsForeground(false); + } + + @Override + public void onHostDestroy() { + // Do nothing + } + ////////////////////////////////////////////////////////////////////// + // End LifecycleEventListener methods + ////////////////////////////////////////////////////////////////////// + + private WritableMap parseNotificationBundle(Bundle notification) { + return Arguments.makeNativeMap(notification); + } + + private class ScheduledNotificationReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (getReactApplicationContext().hasActiveCatalystInstance()) { + Log.d(TAG, "Received new scheduled notification"); + + Bundle notification = intent.getBundleExtra("notification"); + WritableMap messageMap = parseNotificationBundle(notification); + + Utils.sendEvent(getReactApplicationContext(), "notifications_notification_received", messageMap); + } + } } } diff --git a/ios/RNFirebase/notifications/RNFirebaseNotifications.m b/ios/RNFirebase/notifications/RNFirebaseNotifications.m index 895c559e..12456a63 100644 --- a/ios/RNFirebase/notifications/RNFirebaseNotifications.m +++ b/ios/RNFirebase/notifications/RNFirebaseNotifications.m @@ -27,7 +27,6 @@ RCT_EXPORT_METHOD(cancelNotification:(NSString*) notificationId) { if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { for (UILocalNotification *notification in RCTSharedApplication().scheduledLocalNotifications) { NSDictionary *notificationInfo = notification.userInfo; - // TODO: NotificationId? if ([notificationId isEqualToString:[notificationInfo valueForKey:@"notificationId"]]) { [RCTSharedApplication() cancelLocalNotification:notification]; } @@ -46,12 +45,12 @@ RCT_EXPORT_METHOD(displayNotification:(NSDictionary*) notification resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { - UILocalNotification* notif = [self buildUILocalNotification:notification]; + UILocalNotification* notif = [self buildUILocalNotification:notification withSchedule:false]; [RCTSharedApplication() presentLocalNotificationNow:notif]; resolve(nil); } else { #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 - UNNotificationRequest* request = [self buildUNNotificationRequest:notification]; + UNNotificationRequest* request = [self buildUNNotificationRequest:notification withSchedule:false]; [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { if (!error) { resolve(nil); @@ -123,18 +122,15 @@ RCT_EXPORT_METHOD(removeDeliveredNotification:(NSString*) notificationId) { } RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification - schedule:(NSDictionary*) schedule resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { - UILocalNotification* notif = [self buildUILocalNotification:notification]; - // TODO: Schedule + UILocalNotification* notif = [self buildUILocalNotification:notification withSchedule:true]; [RCTSharedApplication() scheduleLocalNotification:notif]; resolve(nil); } else { #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 - UNNotificationRequest* request = [self buildUNNotificationRequest:notification]; - // TODO: Schedule + UNNotificationRequest* request = [self buildUNNotificationRequest:notification withSchedule:true]; [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { if (!error) { resolve(nil); @@ -146,7 +142,8 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification } } -- (UILocalNotification*) buildUILocalNotification:(NSDictionary *) notification { +- (UILocalNotification*) buildUILocalNotification:(NSDictionary *) notification + withSchedule:(BOOL) withSchedule { UILocalNotification *localNotification = [[UILocalNotification alloc] init]; if (notification[@"body"]) { localNotification.alertBody = notification[@"body"]; @@ -179,11 +176,32 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification localNotification.alertLaunchImage = ios[@"launchImage"]; } } - + if (withSchedule) { + NSDictionary *schedule = notification[@"schedule"]; + NSNumber *fireDateNumber = schedule[@"fireDate"]; + NSDate *fireDate = [NSDate dateWithTimeIntervalSince1970:([fireDateNumber doubleValue] / 1000.0)]; + localNotification.fireDate = fireDate; + + NSString *interval = schedule[@"repeatInterval"]; + if (interval) { + if ([interval isEqualToString:@"minute"]) { + localNotification.repeatInterval = NSCalendarUnitMinute; + } else if ([interval isEqualToString:@"hour"]) { + localNotification.repeatInterval = NSCalendarUnitHour; + } else if ([interval isEqualToString:@"day"]) { + localNotification.repeatInterval = NSCalendarUnitDay; + } else if ([interval isEqualToString:@"week"]) { + localNotification.repeatInterval = NSCalendarUnitWeekday; + } + } + + } + return localNotification; } -- (UNNotificationRequest*) buildUNNotificationRequest:(NSDictionary *) notification { +- (UNNotificationRequest*) buildUNNotificationRequest:(NSDictionary *) notification + withSchedule:(BOOL) withSchedule { UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; if (notification[@"body"]) { content.body = notification[@"body"]; @@ -234,8 +252,34 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification } } - // TODO: Scheduling - return [UNNotificationRequest requestWithIdentifier:notification[@"ios"][@"identifier"] content:content trigger:nil]; + if (withSchedule) { + NSDictionary *schedule = notification[@"schedule"]; + NSNumber *fireDateNumber = schedule[@"fireDate"]; + NSString *interval = schedule[@"repeatInterval"]; + NSDate *fireDate = [NSDate dateWithTimeIntervalSince1970:([fireDateNumber doubleValue] / 1000.0)]; + + NSCalendarUnit calendarUnit; + if (interval) { + if ([interval isEqualToString:@"minute"]) { + calendarUnit = NSCalendarUnitSecond; + } else if ([interval isEqualToString:@"hour"]) { + calendarUnit = NSCalendarUnitMinute | NSCalendarUnitSecond; + } else if ([interval isEqualToString:@"day"]) { + calendarUnit = NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond; + } else if ([interval isEqualToString:@"week"]) { + calendarUnit = NSCalendarUnitWeekday | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond; + } + } else { + // Needs to match exactly to the secpmd + calendarUnit = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond; + } + + NSDateComponents *components = [[NSCalendar currentCalendar] components:calendarUnit fromDate:fireDate]; + UNCalendarNotificationTrigger *trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:interval]; + return [UNNotificationRequest requestWithIdentifier:notification[@"notificationId"] content:content trigger:trigger]; + } else { + return [UNNotificationRequest requestWithIdentifier:notification[@"notificationId"] content:content trigger:nil]; + } } - (NSDictionary*) parseUILocalNotification:(UILocalNotification *) localNotification { @@ -278,7 +322,7 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification - (NSDictionary*) parseUNNotificationRequest:(UNNotificationRequest *) localNotification { NSMutableDictionary *notification = [[NSMutableDictionary alloc] init]; - notification[@"identifier"] = localNotification.identifier; + notification[@"notificationId"] = localNotification.identifier; if (localNotification.content.body) { notification[@"body"] = localNotification.content.body; diff --git a/lib/modules/instanceid/index.js b/lib/modules/instanceid/index.js index d4f6ad68..3924a0a9 100644 --- a/lib/modules/instanceid/index.js +++ b/lib/modules/instanceid/index.js @@ -5,7 +5,7 @@ import ModuleBase from '../../utils/ModuleBase'; import { getNativeModule } from '../../utils/native'; -import type App from '../core/firebase-app'; +import type App from '../core/app'; export const MODULE_NAME = 'RNFirebaseInstanceId'; export const NAMESPACE = 'instanceid'; diff --git a/lib/modules/notifications/AndroidNotification.js b/lib/modules/notifications/AndroidNotification.js index 104e13da..f2591707 100644 --- a/lib/modules/notifications/AndroidNotification.js +++ b/lib/modules/notifications/AndroidNotification.js @@ -17,17 +17,18 @@ type Progress = { }; type SmallIcon = { - icon: number, + icon: string, level?: number, }; -export type NativeAndroidNotification = { +export type NativeAndroidNotification = {| // TODO actions: Action[], autoCancel: boolean, badgeIconType: BadgeIconTypeType, category: CategoryType, channelId: string, - color: number, + clickAction?: string, + color: string, colorized: boolean, contentInfo: string, defaults: DefaultsType[], @@ -43,7 +44,7 @@ export type NativeAndroidNotification = { people: string[], priority: PriorityType, progress: Progress, - publicVersion: Notification, + // publicVersion: Notification, remoteInputHistory: string[], shortcutId: string, showWhen: boolean, @@ -56,7 +57,7 @@ export type NativeAndroidNotification = { vibrate: number[], visibility: VisibilityType, when: number, -}; +|}; export const BadgeIconType = { Large: 2, @@ -122,7 +123,8 @@ export default class AndroidNotification { _badgeIconType: BadgeIconTypeType; _category: CategoryType; _channelId: string; - _color: number; + _clickAction: string; + _color: string; _colorized: boolean; _contentInfo: string; _defaults: DefaultsType[]; @@ -139,11 +141,13 @@ export default class AndroidNotification { _people: string[]; _priority: PriorityType; _progress: Progress; - _publicVersion: Notification; + // _publicVersion: Notification; _remoteInputHistory: string[]; _shortcutId: string; _showWhen: boolean; - _smallIcon: SmallIcon; + _smallIcon: SmallIcon = { + icon: 'ic_launcher', + }; _sortKey: string; // TODO: style: Style; // Need to figure out if this can work _ticker: string; @@ -170,9 +174,7 @@ export default class AndroidNotification { /** * - * @param identifier - * @param identifier - * @param identifier + * @param person * @returns {Notification} */ addPerson(person: string): Notification { @@ -228,7 +230,7 @@ export default class AndroidNotification { * @param color * @returns {Notification} */ - setColor(color: number): Notification { + setColor(color: string): Notification { this._color = color; return this._notification; } @@ -394,10 +396,10 @@ export default class AndroidNotification { * @param publicVersion * @returns {Notification} */ - setPublicVersion(publicVersion: Notification): Notification { + /* setPublicVersion(publicVersion: Notification): Notification { this._publicVersion = publicVersion; return this._notification; - } + } */ /** * @@ -435,7 +437,7 @@ export default class AndroidNotification { * @param level * @returns {Notification} */ - setSmallIcon(icon: number, level?: number): Notification { + setSmallIcon(icon: string, level?: number): Notification { this._smallIcon = { icon, level, @@ -517,6 +519,7 @@ export default class AndroidNotification { badgeIconType: this._badgeIconType, category: this._category, channelId: this._channelId, + clickAction: this._clickAction, color: this._color, colorized: this._colorized, contentInfo: this._contentInfo, @@ -533,7 +536,7 @@ export default class AndroidNotification { people: this._people, priority: this._priority, progress: this._progress, - publicVersion: this._publicVersion, + // publicVersion: this._publicVersion, remoteInputHistory: this._remoteInputHistory, shortcutId: this._shortcutId, showWhen: this._showWhen, diff --git a/lib/modules/notifications/IOSNotification.js b/lib/modules/notifications/IOSNotification.js index 607518e7..d4ad7e11 100644 --- a/lib/modules/notifications/IOSNotification.js +++ b/lib/modules/notifications/IOSNotification.js @@ -2,7 +2,6 @@ * @flow * IOSNotification representation wrapper */ -import { generatePushID } from '../../utils'; import type Notification from './Notification'; type AttachmentOptions = {| @@ -23,16 +22,15 @@ type Attachment = {| url: string, |}; -export type NativeIOSNotification = { +export type NativeIOSNotification = {| alertAction?: string, attachments: Attachment[], badge?: number, category?: string, hasAction?: boolean, - identifier?: string, launchImage?: string, threadIdentifier?: string, -}; +|}; export default class IOSNotification { _alertAction: string; // alertAction | N/A @@ -40,23 +38,20 @@ export default class IOSNotification { _badge: number; // applicationIconBadgeNumber | badge _category: string; _hasAction: boolean; // hasAction | N/A - _identifier: string; // N/A | identifier _launchImage: string; // alertLaunchImage | launchImageName _notification: Notification; _threadIdentifier: string; // N/A | threadIdentifier constructor(notification: Notification) { this._attachments = []; - // TODO: Is this the best way to generate an ID? - this._identifier = generatePushID(); this._notification = notification; } /** * * @param identifier - * @param identifier - * @param identifier + * @param url + * @param options * @returns {Notification} */ addAttachment( @@ -112,16 +107,6 @@ export default class IOSNotification { return this._notification; } - /** - * - * @param identifier - * @returns {Notification} - */ - setIdentifier(identifier: string): Notification { - this._identifier = identifier; - return this._notification; - } - /** * * @param launchImage @@ -151,7 +136,6 @@ export default class IOSNotification { badge: this._badge, category: this._category, hasAction: this._hasAction, - identifier: this._identifier, launchImage: this._launchImage, threadIdentifier: this._threadIdentifier, }; diff --git a/lib/modules/notifications/Notification.js b/lib/modules/notifications/Notification.js index ab7dbc6c..88389505 100644 --- a/lib/modules/notifications/Notification.js +++ b/lib/modules/notifications/Notification.js @@ -4,16 +4,19 @@ */ import AndroidNotification from './AndroidNotification'; import IOSNotification from './IOSNotification'; -import { isObject } from '../../utils'; +import { generatePushID, isObject } from '../../utils'; import type { NativeAndroidNotification } from './AndroidNotification'; import type { NativeIOSNotification } from './IOSNotification'; +import type { Schedule } from './'; type NativeNotification = {| android: NativeAndroidNotification, body: string, data: { [string]: string }, ios: NativeIOSNotification, + notificationId: string, + schedule?: Schedule, sound?: string, subtitle?: string, title: string, @@ -25,6 +28,7 @@ export default class Notification { _body: string; // alertBody | body | contentText _data: { [string]: string }; // userInfo | userInfo | extras _ios: IOSNotification; + _notificationId: string; _sound: string | void; // soundName | sound | sound _subtitle: string | void; // N/A | subtitle | subText _title: string; // alertTitle | title | contentTitle @@ -33,6 +37,8 @@ export default class Notification { this._android = new AndroidNotification(this); this._data = {}; this._ios = new IOSNotification(this); + // TODO: Is this the best way to generate an ID? + this._notificationId = generatePushID(); } get android(): AndroidNotification { @@ -68,6 +74,16 @@ export default class Notification { return this; } + /** + * + * @param notificationId + * @returns {Notification} + */ + setNotificationId(notificationId: string): Notification { + this._notificationId = notificationId; + return this; + } + /** * * @param sound @@ -101,9 +117,13 @@ export default class Notification { build(): NativeNotification { // Android required fields: body, title, smallicon // iOS required fields: TODO - if (!this.body) { + if (!this._body) { throw new Error('Notification: Missing required `body` property'); - } else if (!this.title) { + } else if (!this._notificationId) { + throw new Error( + 'Notification: Missing required `notificationId` property' + ); + } else if (!this._title) { throw new Error('Notification: Missing required `title` property'); } @@ -112,6 +132,7 @@ export default class Notification { body: this._body, data: this._data, ios: this._ios.build(), + notificationId: this._notificationId, sound: this._sound, subtitle: this._subtitle, title: this._title, diff --git a/lib/modules/notifications/index.js b/lib/modules/notifications/index.js index c0cda1b5..17857fbd 100644 --- a/lib/modules/notifications/index.js +++ b/lib/modules/notifications/index.js @@ -17,7 +17,7 @@ import { Visibility, } from './AndroidNotification'; -import type App from '../core/firebase-app'; +import type App from '../core/app'; // TODO: Received notification type will be different from sent notification type OnNotification = Notification => any; @@ -26,9 +26,10 @@ type OnNotificationObserver = { next: OnNotification, }; -// TODO: Schedule type -type Schedule = { - build: () => Object, +export type Schedule = { + exact?: boolean, + fireDate: number, + repeatInterval?: 'minute' | 'hour' | 'day' | 'week', }; const NATIVE_EVENTS = ['notifications_notification_received']; @@ -174,10 +175,9 @@ export default class Notifications extends ModuleBase { `Notifications:scheduleNotification expects a 'Notification' but got type ${typeof notification}` ); } - return getNativeModule(this).scheduleNotification( - notification.build(), - schedule.build() - ); + const nativeNotification = notification.build(); + nativeNotification.schedule = schedule; + return getNativeModule(this).scheduleNotification(nativeNotification); } } diff --git a/tests/android/app/build.gradle b/tests/android/app/build.gradle index 39b6b646..c7b38a71 100644 --- a/tests/android/app/build.gradle +++ b/tests/android/app/build.gradle @@ -98,7 +98,7 @@ def enableProguardInReleaseBuilds = false android { compileSdkVersion 26 - buildToolsVersion '25.0.3' + buildToolsVersion '26.0.2' defaultConfig { applicationId "com.reactnativefirebasedemo" @@ -163,7 +163,7 @@ dependencies { compile('com.crashlytics.sdk.android:crashlytics:2.7.1@aar') { transitive = true } - compile "com.android.support:appcompat-v7:26.0.1" + compile "com.android.support:appcompat-v7:26.0.2" compile "com.facebook.react:react-native:+" // From node_modules } diff --git a/tests/android/build.gradle b/tests/android/build.gradle index 2043c3f7..7c902675 100644 --- a/tests/android/build.gradle +++ b/tests/android/build.gradle @@ -29,10 +29,10 @@ allprojects { subprojects { ext { - compileSdk = 25 - buildTools = "25.0.2" + compileSdk = 26 + buildTools = "26.0.2" minSdk = 16 - targetSdk = 25 + targetSdk = 26 } afterEvaluate { project -> diff --git a/tests/ios/Podfile.lock b/tests/ios/Podfile.lock index 22babbd1..884f82d5 100644 --- a/tests/ios/Podfile.lock +++ b/tests/ios/Podfile.lock @@ -164,7 +164,7 @@ PODS: - React/Core - React/fishhook - React/RCTBlob - - RNFirebase (3.2.4): + - RNFirebase (3.2.5): - React - yoga (0.52.0.React) @@ -228,7 +228,7 @@ SPEC CHECKSUMS: nanopb: 5601e6bca2dbf1ed831b519092ec110f66982ca3 Protobuf: 8a9838fba8dae3389230e1b7f8c104aa32389c03 React: 61a6bdf17a9ff16875c230e6ff278d9de274e16c - RNFirebase: 011e47909cf54070f72d50b8d61eb7b347774d29 + RNFirebase: e3448c730955d51d06dee59a265011536abdd7c4 yoga: 646606bf554d54a16711f35596178522fbc00480 PODFILE CHECKSUM: 67c98bcb203cb992da590bcab6f690f727653ca5 From e6a7004c5da5f1b0864385ec7a998971f063c20a Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 15 Feb 2018 15:34:31 +0000 Subject: [PATCH 23/77] [types] Export messaging and notifications types --- lib/index.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/index.js b/lib/index.js index 82323cc9..8d7a7cc2 100644 --- a/lib/index.js +++ b/lib/index.js @@ -26,7 +26,6 @@ export type { default as ConfirmationResult, } from './modules/auth/ConfirmationResult'; export type { default as User } from './modules/auth/User'; -export type { default as Message } from './modules/messaging/Message'; /* * Export Database types @@ -65,3 +64,18 @@ export type { default as QuerySnapshot, } from './modules/firestore/QuerySnapshot'; export type { default as WriteBatch } from './modules/firestore/WriteBatch'; + +/* + * Export Messaging types + */ +export type { default as Message } from './modules/messaging/Message'; +export type { + default as RemoteMessage, +} from './modules/messaging/RemoteMessage'; + +/* + * Export Notifications types + */ +export type { + default as Notification, +} from './modules/notifications/Notification'; From 9255b4be109cbca7c9070fa0adceb2f3606e61d7 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 16 Feb 2018 09:46:53 +0000 Subject: [PATCH 24/77] [notifications] Minor fixes --- ios/RNFirebase/notifications/RNFirebaseNotifications.m | 3 ++- lib/modules/notifications/Notification.js | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ios/RNFirebase/notifications/RNFirebaseNotifications.m b/ios/RNFirebase/notifications/RNFirebaseNotifications.m index 12456a63..58f0fee4 100644 --- a/ios/RNFirebase/notifications/RNFirebaseNotifications.m +++ b/ios/RNFirebase/notifications/RNFirebaseNotifications.m @@ -1,6 +1,7 @@ #import "RNFirebaseNotifications.h" #if __has_include() +#import "RNFirebaseEvents.h" #import #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 @@ -372,7 +373,7 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification } - (NSArray *)supportedEvents { - return @[]; + return @[NOTIFICATIONS_NOTIFICATION_RECEIVED]; } + (BOOL)requiresMainQueueSetup diff --git a/lib/modules/notifications/Notification.js b/lib/modules/notifications/Notification.js index 88389505..bcaae87b 100644 --- a/lib/modules/notifications/Notification.js +++ b/lib/modules/notifications/Notification.js @@ -2,6 +2,7 @@ * @flow * Notification representation wrapper */ +import { Platform } from 'react-native'; import AndroidNotification from './AndroidNotification'; import IOSNotification from './IOSNotification'; import { generatePushID, isObject } from '../../utils'; @@ -11,10 +12,10 @@ import type { NativeIOSNotification } from './IOSNotification'; import type { Schedule } from './'; type NativeNotification = {| - android: NativeAndroidNotification, + android?: NativeAndroidNotification, body: string, data: { [string]: string }, - ios: NativeIOSNotification, + ios?: NativeIOSNotification, notificationId: string, schedule?: Schedule, sound?: string, @@ -128,10 +129,10 @@ export default class Notification { } return { - android: this._android.build(), + android: Platform.OS === 'android' ? this._android.build() : undefined, body: this._body, data: this._data, - ios: this._ios.build(), + ios: Platform.OS === 'ios' ? this._ios.build() : undefined, notificationId: this._notificationId, sound: this._sound, subtitle: this._subtitle, From b86d51f36b59fa0bb1b5e76b8a85dfc6d554ca7a Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 16 Feb 2018 09:47:46 +0000 Subject: [PATCH 25/77] [fcm] Start considering multiple events --- ios/RNFirebase/RNFirebaseEvents.h | 5 +++ .../messaging/RNFirebaseMessaging.m | 38 +++++++++++++++---- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/ios/RNFirebase/RNFirebaseEvents.h b/ios/RNFirebase/RNFirebaseEvents.h index 26ce5f3e..ac40e86d 100644 --- a/ios/RNFirebase/RNFirebaseEvents.h +++ b/ios/RNFirebase/RNFirebaseEvents.h @@ -38,6 +38,11 @@ static NSString *const MESSAGING_TOKEN_REFRESHED = @"messaging_token_refreshed"; // TODO: Remove static NSString *const MESSAGING_NOTIFICATION_RECEIVED = @"messaging_notification_received"; +// Notifications +static NSString *const NOTIFICATIONS_NOTIFICATION_CLICKED = @"notifications_notification_clicked"; +static NSString *const NOTIFICATIONS_NOTIFICATION_DISPLAYED = @"notifications_notification_displayed"; +static NSString *const NOTIFICATIONS_NOTIFICATION_RECEIVED = @"notifications_notification_received"; + // AdMob static NSString *const ADMOB_INTERSTITIAL_EVENT = @"interstitial_event"; static NSString *const ADMOB_REWARDED_VIDEO_EVENT = @"rewarded_video_event"; diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index 14454ef0..8198a8b7 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -113,11 +113,34 @@ RCT_EXPORT_MODULE() - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler { + + NSString *event; + UNNotificationPresentationOptions options; 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]; + + if (isFCM || isScheduled) { + // If background + if (RCTSharedApplication().applicationState == UIApplicationStateInactive) { + // display notification + options = UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound; + // notification_displayed + event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; + } else { + // don't show notification + options = UNNotificationPresentationOptionNone; + // notification_received + event = NOTIFICATIONS_NOTIFICATION_RECEIVED; + } + } else { + // display notification + options = UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound; + // no event + } + + if (event) { + [RNFirebaseUtil sendJSEvent:self name:event body:message]; + } + completionHandler(options); } // Handle notification messages after display notification is tapped by the user. @@ -129,10 +152,9 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler { #endif NSDictionary *message = [self parseUNNotification:response.notification messageType:@"NotificationResponse" openedFromTray:true]; - - [_callbackHandlers setObject:[completionHandler copy] forKey:message[@"messageId"]]; - - [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; + + [RNFirebaseUtil sendJSEvent:self name:NOTIFICATIONS_NOTIFICATION_CLICKED body:message]; + completionHandler(); } #endif From ce91d2ab093d7cce2090394595f6f9db612f45eb Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Sat, 17 Feb 2018 12:57:25 +0000 Subject: [PATCH 26/77] [build] Test out excluding lib from npm --- .npmignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.npmignore b/.npmignore index 17082b9a..934359e2 100644 --- a/.npmignore +++ b/.npmignore @@ -62,6 +62,8 @@ ios/RnFirebase.xcodeproj/xcuserdata .Trashes ehthumbs.db Thumbs.dbandroid/gradle + +# Project folders android/gradlew android/build android/gradlew.bat @@ -71,7 +73,7 @@ docs coverage yarn.lock tests -lib/.watchmanconfig +lib buddybuild_postclone.sh bin/test.js .github From fac4eba86070df22e80b8f49e3f9c30314415083 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Sat, 17 Feb 2018 13:09:59 +0000 Subject: [PATCH 27/77] [build] Revert .npmignore changes --- .npmignore | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.npmignore b/.npmignore index 934359e2..17082b9a 100644 --- a/.npmignore +++ b/.npmignore @@ -62,8 +62,6 @@ ios/RnFirebase.xcodeproj/xcuserdata .Trashes ehthumbs.db Thumbs.dbandroid/gradle - -# Project folders android/gradlew android/build android/gradlew.bat @@ -73,7 +71,7 @@ docs coverage yarn.lock tests -lib +lib/.watchmanconfig buddybuild_postclone.sh bin/test.js .github From 0002bd075374646508fa09c408d0ab0dd5cbfc56 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Sat, 17 Feb 2018 13:10:13 +0000 Subject: [PATCH 28/77] [build] Remove providesModule --- lib/modules/core/firebase.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/core/firebase.js b/lib/modules/core/firebase.js index a5d4ca6b..acec6ad5 100644 --- a/lib/modules/core/firebase.js +++ b/lib/modules/core/firebase.js @@ -1,5 +1,5 @@ /** - * @providesModule Firebase + * @flow */ import { NativeModules } from 'react-native'; From a244f17853fdabc62e7e723aff266cbf59d0b17f Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Sun, 18 Feb 2018 16:02:31 +0000 Subject: [PATCH 29/77] [fcm] More iOS event separation --- .../messaging/RNFirebaseMessaging.m | 9 +- lib/modules/notifications/index.js | 83 +++++++++++++++++-- 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index 8198a8b7..7e916620 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -119,9 +119,9 @@ RCT_EXPORT_MODULE() NSDictionary *message = [self parseUNNotification:notification messageType:@"PresentNotification" openedFromTray:false]; if (isFCM || isScheduled) { - // If background + // If app is in the background if (RCTSharedApplication().applicationState == UIApplicationStateInactive) { - // display notification + // display the notification options = UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound; // notification_displayed event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; @@ -132,7 +132,8 @@ RCT_EXPORT_MODULE() event = NOTIFICATIONS_NOTIFICATION_RECEIVED; } } else { - // display notification + // Triggered by `notifications().displayNotification(notification)` + // Display the notification options = UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound; // no event } @@ -482,7 +483,7 @@ RCT_EXPORT_METHOD(completeRemoteNotification: (NSString*) messageId } - (NSArray *)supportedEvents { - return @[MESSAGING_MESSAGE_RECEIVED, MESSAGING_TOKEN_REFRESHED]; + return @[MESSAGING_MESSAGE_RECEIVED, MESSAGING_TOKEN_REFRESHED, NOTIFICATIONS_NOTIFICATION_CLICKED, NOTIFICATIONS_NOTIFICATION_DISPLAYED, NOTIFICATIONS_NOTIFICATION_RECEIVED]; } + (BOOL)requiresMainQueueSetup diff --git a/lib/modules/notifications/index.js b/lib/modules/notifications/index.js index 17857fbd..57aed391 100644 --- a/lib/modules/notifications/index.js +++ b/lib/modules/notifications/index.js @@ -32,7 +32,11 @@ export type Schedule = { repeatInterval?: 'minute' | 'hour' | 'day' | 'week', }; -const NATIVE_EVENTS = ['notifications_notification_received']; +const NATIVE_EVENTS = [ + 'notifications_notification_clicked', + 'notifications_notification_displayed', + 'notifications_notification_received', +]; export const MODULE_NAME = 'RNFirebaseNotifications'; export const NAMESPACE = 'notifications'; @@ -65,7 +69,25 @@ export default class Notifications extends ModuleBase { SharedEventEmitter.addListener( // sub to internal native event - this fans out to - // public event name: onMessage + // public event name: onNotificationClicked + 'notifications_notification_clicked', + (notification: Notification) => { + SharedEventEmitter.emit('onNotificationClicked', notification); + } + ); + + SharedEventEmitter.addListener( + // sub to internal native event - this fans out to + // public event name: onNotificationDisplayed + 'notifications_notification_displayed', + (notification: Notification) => { + SharedEventEmitter.emit('onNotificationDisplayed', notification); + } + ); + + SharedEventEmitter.addListener( + // sub to internal native event - this fans out to + // public event name: onNotification 'notifications_notification_received', (notification: Notification) => { SharedEventEmitter.emit('onNotification', notification); @@ -73,12 +95,16 @@ export default class Notifications extends ModuleBase { ); } + /** + * Cancel all notifications + * @returns {*} + */ cancelAllNotifications(): Promise { return getNativeModule(this).cancelAllNotifications(); } /** - * Cancel a local notification by id. + * Cancel a notification by id. * @param id * @returns {*} */ @@ -90,7 +116,7 @@ export default class Notifications extends ModuleBase { } /** - * Display a local notification + * Display a notification * @param notification * @returns {*} */ @@ -131,7 +157,6 @@ export default class Notifications extends ModuleBase { ); } - // TODO: iOS finish getLogger(this).info('Creating onNotification listener'); SharedEventEmitter.addListener('onNotification', listener); @@ -141,6 +166,52 @@ export default class Notifications extends ModuleBase { }; } + onNotificationClicked( + nextOrObserver: OnNotification | OnNotificationObserver + ): () => any { + let listener; + if (isFunction(nextOrObserver)) { + listener = nextOrObserver; + } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { + listener = nextOrObserver.next; + } else { + throw new Error( + 'Notifications.onNotificationClicked failed: First argument must be a function or observer object with a `next` function.' + ); + } + + getLogger(this).info('Creating onNotificationClicked listener'); + SharedEventEmitter.addListener('onNotificationClicked', listener); + + return () => { + getLogger(this).info('Removing onNotificationClicked listener'); + SharedEventEmitter.removeListener('onNotificationClicked', listener); + }; + } + + onNotificationDisplayed( + nextOrObserver: OnNotification | OnNotificationObserver + ): () => any { + let listener; + if (isFunction(nextOrObserver)) { + listener = nextOrObserver; + } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { + listener = nextOrObserver.next; + } else { + throw new Error( + 'Notifications.onNotificationDisplayed failed: First argument must be a function or observer object with a `next` function.' + ); + } + + getLogger(this).info('Creating onNotificationDisplayed listener'); + SharedEventEmitter.addListener('onNotificationDisplayed', listener); + + return () => { + getLogger(this).info('Removing onNotificationDisplayed listener'); + SharedEventEmitter.removeListener('onNotificationDisplayed', listener); + }; + } + /** * Remove all delivered notifications. * @returns {*} @@ -162,7 +233,7 @@ export default class Notifications extends ModuleBase { } /** - * + * Schedule a notification * @param notification * @returns {*} */ From 831eec82f740868f82cdff974496df20128ebf81 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Sun, 18 Feb 2018 17:09:13 +0000 Subject: [PATCH 30/77] [messaging][notifications] Move all notifications from messaging to notifications --- .../messaging/RNFirebaseMessaging.h | 2 - .../messaging/RNFirebaseMessaging.m | 195 +--------------- .../notifications/RNFirebaseNotifications.h | 6 +- .../notifications/RNFirebaseNotifications.m | 220 +++++++++++++++++- .../ios/ReactNativeFirebaseDemo/AppDelegate.m | 6 +- 5 files changed, 235 insertions(+), 194 deletions(-) diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.h b/ios/RNFirebase/messaging/RNFirebaseMessaging.h index ea6ed75f..2aa44131 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.h +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.h @@ -15,8 +15,6 @@ @property _Nullable RCTPromiseResolveBlock permissionResolver; #if !TARGET_OS_TV -- (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo; -- (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo fetchCompletionHandler:(void (^_Nonnull)(UIBackgroundFetchResult))completionHandler; - (void)didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings; #endif diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index 7e916620..19782e02 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -11,14 +11,10 @@ #import #import -// For iOS 10 we need to implement UNUserNotificationCenterDelegate to receive display -// notifications via APNS #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 @import UserNotifications; -@interface RNFirebaseMessaging () -#else -@interface RNFirebaseMessaging () #endif +@interface RNFirebaseMessaging () @property (nonatomic, strong) NSMutableDictionary *callbackHandlers; @end @@ -48,12 +44,7 @@ RCT_EXPORT_MODULE() // Establish Firebase managed data channel [FIRMessaging messaging].shouldEstablishDirectChannel = YES; - - // If we're on iOS 10 then we need to set this as a delegate for the UNUserNotificationCenter -#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 - [UNUserNotificationCenter currentNotificationCenter].delegate = self; -#endif - + // Set static instance for use from AppDelegate theRNFirebaseMessaging = self; @@ -66,25 +57,6 @@ RCT_EXPORT_MODULE() // ** iOS 8/9 Only // ******************************************************* -// Listen for background messages -- (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo { - BOOL isFromBackground = (RCTSharedApplication().applicationState == UIApplicationStateInactive); - NSDictionary *message = [self parseUserInfo:userInfo messageType:@"RemoteNotification" clickAction:nil openedFromTray:isFromBackground]; - - [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; -} - -// Listen for background messages -- (void)didReceiveRemoteNotification:(NSDictionary *)userInfo - fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { - BOOL isFromBackground = (RCTSharedApplication().applicationState == UIApplicationStateInactive); - 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]; -} - // Listen for permission response - (void) didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { if (notificationSettings.types == UIUserNotificationTypeNone) { @@ -103,68 +75,6 @@ RCT_EXPORT_MODULE() // ******************************************************* -// ******************************************************* -// ** Start UNUserNotificationCenterDelegate methods -// ** iOS 10+ -// ******************************************************* - -#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 -// Handle incoming notification messages while app is in the foreground. -- (void)userNotificationCenter:(UNUserNotificationCenter *)center - willPresentNotification:(UNNotification *)notification - withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler { - - NSString *event; - UNNotificationPresentationOptions options; - NSDictionary *message = [self parseUNNotification:notification messageType:@"PresentNotification" openedFromTray:false]; - - if (isFCM || isScheduled) { - // If app is in the background - if (RCTSharedApplication().applicationState == UIApplicationStateInactive) { - // display the notification - options = UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound; - // notification_displayed - event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; - } else { - // don't show notification - options = UNNotificationPresentationOptionNone; - // notification_received - event = NOTIFICATIONS_NOTIFICATION_RECEIVED; - } - } else { - // Triggered by `notifications().displayNotification(notification)` - // Display the notification - options = UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound; - // no event - } - - if (event) { - [RNFirebaseUtil sendJSEvent:self name:event body:message]; - } - completionHandler(options); -} - -// Handle notification messages after display notification is tapped by the user. -- (void)userNotificationCenter:(UNUserNotificationCenter *)center -didReceiveNotificationResponse:(UNNotificationResponse *)response -#if defined(__IPHONE_11_0) - withCompletionHandler:(void(^)(void))completionHandler { -#else - withCompletionHandler:(void(^)())completionHandler { -#endif - NSDictionary *message = [self parseUNNotification:response.notification messageType:@"NotificationResponse" openedFromTray:true]; - - [RNFirebaseUtil sendJSEvent:self name:NOTIFICATIONS_NOTIFICATION_CLICKED body:message]; - completionHandler(); -} - -#endif - -// ******************************************************* -// ** Finish UNUserNotificationCenterDelegate methods -// ******************************************************* - - // ******************************************************* // ** Start FIRMessagingDelegate methods // ** iOS 8+ @@ -228,20 +138,16 @@ RCT_EXPORT_METHOD(requestPermission:(RCTPromiseResolveBlock)resolve rejecter:(RC // Non Web SDK methods +// TODO: Move to notifications RCT_EXPORT_METHOD(getBadge: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { dispatch_async(dispatch_get_main_queue(), ^{ resolve(@([RCTSharedApplication() applicationIconBadgeNumber])); }); } +// TODO: Remove RCT_EXPORT_METHOD(getInitialMessage:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){ - NSDictionary *notification = [self bridge].launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; - if (notification) { - NSDictionary *message = [self parseUserInfo:notification messageType:@"InitialMessage" clickAction:nil openedFromTray:true]; - resolve(message); - } else { - resolve(nil); - } + resolve(nil); } RCT_EXPORT_METHOD(hasPermission: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { @@ -273,6 +179,7 @@ RCT_EXPORT_METHOD(sendMessage: (NSDictionary *) message [[FIRMessaging messaging] sendMessage:data to:to withMessageID:messageId timeToLive:[ttl intValue]]; } +// TODO: Move to notifications RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { dispatch_async(dispatch_get_main_queue(), ^{ [RCTSharedApplication() setApplicationIconBadgeNumber:number]; @@ -395,95 +302,8 @@ RCT_EXPORT_METHOD(completeRemoteNotification: (NSString*) messageId return message; } -- (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 messageType:messageType clickAction:clickAction openedFromTray:openedFromTray]; -} - -- (NSDictionary*)parseUserInfo:(NSDictionary *)userInfo - messageType:(NSString *) messageType - clickAction:(NSString *) clickAction - openedFromTray:(bool)openedFromTray { - NSMutableDictionary *message = [[NSMutableDictionary alloc] init]; - NSMutableDictionary *notif = [[NSMutableDictionary alloc] init]; - NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; - - for (id k1 in userInfo) { - if ([k1 isEqualToString:@"aps"]) { - NSDictionary *aps = userInfo[k1]; - for (id k2 in aps) { - if ([k2 isEqualToString:@"alert"]) { - NSDictionary *alert = aps[k2]; - for (id k3 in alert) { - if ([k3 isEqualToString:@"body"]) { - notif[@"body"] = alert[k3]; - } else if ([k3 isEqualToString:@"loc-args"]) { - notif[@"bodyLocalizationArgs"] = alert[k3]; - } else if ([k3 isEqualToString:@"loc-key"]) { - notif[@"bodyLocalizationKey"] = alert[k3]; - } else if ([k3 isEqualToString:@"subtitle"]) { - notif[@"subtitle"] = alert[k3]; - } else if ([k3 isEqualToString:@"title"]) { - notif[@"title"] = alert[k3]; - } else if ([k3 isEqualToString:@"title-loc-args"]) { - notif[@"titleLocalizationArgs"] = alert[k3]; - } else if ([k3 isEqualToString:@"title-loc-key"]) { - notif[@"titleLocalizationKey"] = alert[k3]; - } else { - NSLog(@"Unknown alert key: %@", k2); - } - } - } else if ([k2 isEqualToString:@"badge"]) { - notif[@"badge"] = aps[k2]; - } else if ([k2 isEqualToString:@"category"]) { - notif[@"clickAction"] = aps[k2]; - } else if ([k2 isEqualToString:@"sound"]) { - notif[@"sound"] = aps[k2]; - } else { - NSLog(@"Unknown aps key: %@", k2); - } - } - } else if ([k1 isEqualToString:@"gcm.message_id"]) { - message[@"messageId"] = userInfo[k1]; - } else if ([k1 isEqualToString:@"google.c.a.ts"]) { - message[@"sentTime"] = userInfo[k1]; - } else if ([k1 isEqualToString:@"gcm.n.e"] - || [k1 isEqualToString:@"gcm.notification.sound2"] - || [k1 isEqualToString:@"google.c.a.c_id"] - || [k1 isEqualToString:@"google.c.a.c_l"] - || [k1 isEqualToString:@"google.c.a.e"] - || [k1 isEqualToString:@"google.c.a.udt"]) { - // Ignore known keys - } else { - // Assume custom data - data[k1] = userInfo[k1]; - } - } - - 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[@"messageType"] = messageType; - - message[@"data"] = data; - message[@"notification"] = notif; - message[@"openedFromTray"] = @(openedFromTray); - - return message; -} - - (NSArray *)supportedEvents { - return @[MESSAGING_MESSAGE_RECEIVED, MESSAGING_TOKEN_REFRESHED, NOTIFICATIONS_NOTIFICATION_CLICKED, NOTIFICATIONS_NOTIFICATION_DISPLAYED, NOTIFICATIONS_NOTIFICATION_RECEIVED]; + return @[MESSAGING_MESSAGE_RECEIVED, MESSAGING_TOKEN_REFRESHED]; } + (BOOL)requiresMainQueueSetup @@ -497,3 +317,4 @@ RCT_EXPORT_METHOD(completeRemoteNotification: (NSString*) messageId @implementation RNFirebaseMessaging @end #endif + diff --git a/ios/RNFirebase/notifications/RNFirebaseNotifications.h b/ios/RNFirebase/notifications/RNFirebaseNotifications.h index 16298e70..56ca289b 100644 --- a/ios/RNFirebase/notifications/RNFirebaseNotifications.h +++ b/ios/RNFirebase/notifications/RNFirebaseNotifications.h @@ -8,8 +8,12 @@ @interface RNFirebaseNotifications : RCTEventEmitter -#if !TARGET_OS_TV ++ (_Nonnull instancetype)instance; +#if !TARGET_OS_TV +- (void)didReceiveLocalNotification:(nonnull UILocalNotification *)notification; +- (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo; +- (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo fetchCompletionHandler:(void (^_Nonnull)(UIBackgroundFetchResult))completionHandler; #endif @end diff --git a/ios/RNFirebase/notifications/RNFirebaseNotifications.m b/ios/RNFirebase/notifications/RNFirebaseNotifications.m index 58f0fee4..abdcd338 100644 --- a/ios/RNFirebase/notifications/RNFirebaseNotifications.m +++ b/ios/RNFirebase/notifications/RNFirebaseNotifications.m @@ -2,15 +2,146 @@ #if __has_include() #import "RNFirebaseEvents.h" +#import "RNFirebaseUtil.h" #import +// For iOS 10 we need to implement UNUserNotificationCenterDelegate to receive display +// notifications via APNS #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 @import UserNotifications; +@interface RNFirebaseNotifications () +#else +@interface RNFirebaseNotifications () #endif +@end @implementation RNFirebaseNotifications + +static RNFirebaseNotifications *theRNFirebaseNotifications = nil; + ++ (nonnull instancetype)instance { + return theRNFirebaseNotifications; +} + RCT_EXPORT_MODULE(); +- (id)init { + self = [super init]; + if (self != nil) { + NSLog(@"Setting up RNFirebaseNotifications instance"); + [self configure]; + } + return self; +} + +- (void)configure { + // If we're on iOS 10 then we need to set this as a delegate for the UNUserNotificationCenter +#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + [UNUserNotificationCenter currentNotificationCenter].delegate = self; +#endif + + // Set static instance for use from AppDelegate + theRNFirebaseNotifications = self; +} + +// ******************************************************* +// ** Start AppDelegate methods +// ** iOS 8/9 Only +// ******************************************************* + +- (void)didReceiveLocalNotification:(nonnull UILocalNotification *)notification { + +} + +// Listen for background messages +- (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo { + BOOL isFromBackground = (RCTSharedApplication().applicationState == UIApplicationStateInactive); + NSDictionary *message = [self parseUserInfo:userInfo messageType:@"RemoteNotification" clickAction:nil openedFromTray:isFromBackground]; + + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; +} + +// Listen for background messages +- (void)didReceiveRemoteNotification:(NSDictionary *)userInfo + fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { + BOOL isFromBackground = (RCTSharedApplication().applicationState == UIApplicationStateInactive); + 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]; +} + +// ******************************************************* +// ** Finish AppDelegate methods +// ******************************************************* + +// ******************************************************* +// ** Start UNUserNotificationCenterDelegate methods +// ** iOS 10+ +// ******************************************************* + +#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 +// Handle incoming notification messages while app is in the foreground. +- (void)userNotificationCenter:(UNUserNotificationCenter *)center + willPresentNotification:(UNNotification *)notification + withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler { + + UNNotificationTrigger *trigger = notification.request.trigger; + BOOL isFcm = trigger && [notification.request.trigger class] == [UNPushNotificationTrigger class]; + BOOL isScheduled = trigger && [notification.request.trigger class] == [UNCalendarNotificationTrigger class]; + + NSString *event; + UNNotificationPresentationOptions options; + NSDictionary *message = [self parseUNNotification:notification messageType:@"PresentNotification" openedFromTray:false]; + + if (isFcm || isScheduled) { + // If app is in the background + if (RCTSharedApplication().applicationState == UIApplicationStateInactive) { + // display the notification + options = UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound; + // notification_displayed + event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; + } else { + // don't show notification + options = UNNotificationPresentationOptionNone; + // notification_received + event = NOTIFICATIONS_NOTIFICATION_RECEIVED; + } + } else { + // Triggered by `notifications().displayNotification(notification)` + // Display the notification + options = UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound; + // notification_displayed + event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; + } + + if (event) { + [RNFirebaseUtil sendJSEvent:self name:event body:message]; + } + completionHandler(options); +} + +// Handle notification messages after display notification is tapped by the user. +- (void)userNotificationCenter:(UNUserNotificationCenter *)center +didReceiveNotificationResponse:(UNNotificationResponse *)response +#if defined(__IPHONE_11_0) + withCompletionHandler:(void(^)(void))completionHandler { +#else + withCompletionHandler:(void(^)())completionHandler { +#endif + NSDictionary *message = [self parseUNNotification:response.notification messageType:@"NotificationResponse" openedFromTray:true]; + + [RNFirebaseUtil sendJSEvent:self name:NOTIFICATIONS_NOTIFICATION_CLICKED body:message]; + completionHandler(); +} + +#endif + +// ******************************************************* +// ** Finish UNUserNotificationCenterDelegate methods +// ******************************************************* + RCT_EXPORT_METHOD(cancelAllNotifications) { if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { [RCTSharedApplication() cancelAllLocalNotifications]; @@ -371,9 +502,96 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification return notification; } + +- (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 messageType:messageType clickAction:clickAction openedFromTray:openedFromTray]; +} + +- (NSDictionary*)parseUserInfo:(NSDictionary *)userInfo + messageType:(NSString *) messageType + clickAction:(NSString *) clickAction + openedFromTray:(bool)openedFromTray { + NSMutableDictionary *message = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *notif = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; + + for (id k1 in userInfo) { + if ([k1 isEqualToString:@"aps"]) { + NSDictionary *aps = userInfo[k1]; + for (id k2 in aps) { + if ([k2 isEqualToString:@"alert"]) { + NSDictionary *alert = aps[k2]; + for (id k3 in alert) { + if ([k3 isEqualToString:@"body"]) { + notif[@"body"] = alert[k3]; + } else if ([k3 isEqualToString:@"loc-args"]) { + notif[@"bodyLocalizationArgs"] = alert[k3]; + } else if ([k3 isEqualToString:@"loc-key"]) { + notif[@"bodyLocalizationKey"] = alert[k3]; + } else if ([k3 isEqualToString:@"subtitle"]) { + notif[@"subtitle"] = alert[k3]; + } else if ([k3 isEqualToString:@"title"]) { + notif[@"title"] = alert[k3]; + } else if ([k3 isEqualToString:@"title-loc-args"]) { + notif[@"titleLocalizationArgs"] = alert[k3]; + } else if ([k3 isEqualToString:@"title-loc-key"]) { + notif[@"titleLocalizationKey"] = alert[k3]; + } else { + NSLog(@"Unknown alert key: %@", k2); + } + } + } else if ([k2 isEqualToString:@"badge"]) { + notif[@"badge"] = aps[k2]; + } else if ([k2 isEqualToString:@"category"]) { + notif[@"clickAction"] = aps[k2]; + } else if ([k2 isEqualToString:@"sound"]) { + notif[@"sound"] = aps[k2]; + } else { + NSLog(@"Unknown aps key: %@", k2); + } + } + } else if ([k1 isEqualToString:@"gcm.message_id"]) { + message[@"messageId"] = userInfo[k1]; + } else if ([k1 isEqualToString:@"google.c.a.ts"]) { + message[@"sentTime"] = userInfo[k1]; + } else if ([k1 isEqualToString:@"gcm.n.e"] + || [k1 isEqualToString:@"gcm.notification.sound2"] + || [k1 isEqualToString:@"google.c.a.c_id"] + || [k1 isEqualToString:@"google.c.a.c_l"] + || [k1 isEqualToString:@"google.c.a.e"] + || [k1 isEqualToString:@"google.c.a.udt"]) { + // Ignore known keys + } else { + // Assume custom data + data[k1] = userInfo[k1]; + } + } + + 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[@"messageType"] = messageType; + + message[@"data"] = data; + message[@"notification"] = notif; + message[@"openedFromTray"] = @(openedFromTray); + + return message; +} - (NSArray *)supportedEvents { - return @[NOTIFICATIONS_NOTIFICATION_RECEIVED]; + return @[NOTIFICATIONS_NOTIFICATION_CLICKED, NOTIFICATIONS_NOTIFICATION_DISPLAYED, NOTIFICATIONS_NOTIFICATION_RECEIVED]; } + (BOOL)requiresMainQueueSetup diff --git a/tests/ios/ReactNativeFirebaseDemo/AppDelegate.m b/tests/ios/ReactNativeFirebaseDemo/AppDelegate.m index d266a798..88cfe550 100644 --- a/tests/ios/ReactNativeFirebaseDemo/AppDelegate.m +++ b/tests/ios/ReactNativeFirebaseDemo/AppDelegate.m @@ -12,7 +12,7 @@ #import #import #import -#import +#import @implementation AppDelegate @@ -39,12 +39,12 @@ } - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo { - [[RNFirebaseMessaging instance] didReceiveRemoteNotification:userInfo]; + [[RNFirebaseNotifications instance] didReceiveRemoteNotification:userInfo]; } - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { - [[RNFirebaseMessaging instance] didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; + [[RNFirebaseNotifications instance] didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; } @end From 303cb4c4288d7b8a3b3f0313a888f2d472ee55fa Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 22 Feb 2018 15:52:24 +0000 Subject: [PATCH 31/77] [notifications] Refactor for better support of separate messages --- ios/RNFirebase/RNFirebaseEvents.h | 2 +- .../messaging/RNFirebaseMessaging.h | 3 +- .../messaging/RNFirebaseMessaging.m | 79 ++++---- .../notifications/RNFirebaseNotifications.m | 153 ++++++++++----- lib/modules/messaging/Message.js | 14 -- lib/modules/messaging/index.js | 7 +- lib/modules/messaging/types.js | 3 - .../notifications/AndroidNotification.js | 180 ++++++------------ lib/modules/notifications/IOSNotification.js | 63 +++--- lib/modules/notifications/Notification.js | 39 ++-- lib/modules/notifications/index.js | 84 ++++---- lib/modules/notifications/types.js | 162 ++++++++++++++++ 12 files changed, 463 insertions(+), 326 deletions(-) create mode 100644 lib/modules/notifications/types.js diff --git a/ios/RNFirebase/RNFirebaseEvents.h b/ios/RNFirebase/RNFirebaseEvents.h index ac40e86d..938a8445 100644 --- a/ios/RNFirebase/RNFirebaseEvents.h +++ b/ios/RNFirebase/RNFirebaseEvents.h @@ -39,8 +39,8 @@ static NSString *const MESSAGING_TOKEN_REFRESHED = @"messaging_token_refreshed"; static NSString *const MESSAGING_NOTIFICATION_RECEIVED = @"messaging_notification_received"; // Notifications -static NSString *const NOTIFICATIONS_NOTIFICATION_CLICKED = @"notifications_notification_clicked"; static NSString *const NOTIFICATIONS_NOTIFICATION_DISPLAYED = @"notifications_notification_displayed"; +static NSString *const NOTIFICATIONS_NOTIFICATION_PRESSED = @"notifications_notification_pressed"; static NSString *const NOTIFICATIONS_NOTIFICATION_RECEIVED = @"notifications_notification_received"; // AdMob diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.h b/ios/RNFirebase/messaging/RNFirebaseMessaging.h index 2aa44131..4db95fa9 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.h +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.h @@ -15,7 +15,8 @@ @property _Nullable RCTPromiseResolveBlock permissionResolver; #if !TARGET_OS_TV -- (void)didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings; +- (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo; +- (void)didRegisterUserNotificationSettings:(nonnull UIUserNotificationSettings *)notificationSettings; #endif @end diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index 19782e02..8ce69e62 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -70,6 +70,12 @@ RCT_EXPORT_MODULE() _permissionResolver = nil; } +// Listen for FCM data messages that arrive as a remote notification +- (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo { + NSDictionary *message = [self parseUserInfo:userInfo]; + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; +} + // ******************************************************* // ** Finish AppDelegate methods // ******************************************************* @@ -88,8 +94,15 @@ RCT_EXPORT_MODULE() // Listen for data messages in the foreground - (void)applicationReceivedRemoteMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage { - NSDictionary *message = [self parseFIRMessagingRemoteMessage:remoteMessage openedFromTray:false]; + NSDictionary *message = [self parseFIRMessagingRemoteMessage:remoteMessage]; + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; +} +// Receive data messages on iOS 10+ directly from FCM (bypassing APNs) when the app is in the foreground. +// To enable direct data messages, you can set [Messaging messaging].shouldEstablishDirectChannel to YES. +- (void)messaging:(nonnull FIRMessaging *)messaging +didReceiveMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage { + NSDictionary *message = [self parseFIRMessagingRemoteMessage:remoteMessage]; [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; } @@ -250,8 +263,7 @@ RCT_EXPORT_METHOD(completeRemoteNotification: (NSString*) messageId // ** Start internals ** -- (NSDictionary*)parseFIRMessagingRemoteMessage:(FIRMessagingRemoteMessage *)remoteMessage - openedFromTray:(bool)openedFromTray { +- (NSDictionary*)parseFIRMessagingRemoteMessage:(FIRMessagingRemoteMessage *)remoteMessage { NSDictionary *appData = remoteMessage.appData; NSMutableDictionary *message = [[NSMutableDictionary alloc] init]; @@ -262,46 +274,46 @@ RCT_EXPORT_METHOD(completeRemoteNotification: (NSString*) messageId } else if ([k1 isEqualToString:@"from"]) { message[@"from"] = appData[k1]; } else if ([k1 isEqualToString:@"notification"]) { - NSDictionary *notification = appData[k1]; - NSMutableDictionary *notif = [[NSMutableDictionary alloc] init]; - for (id k2 in notification) { - if ([k2 isEqualToString:@"badge"]) { - notif[@"badge"] = notification[k2]; - } else if ([k2 isEqualToString:@"body"]) { - notif[@"body"] = notification[k2]; - } else if ([k2 isEqualToString:@"body_loc_args"]) { - notif[@"bodyLocalizationArgs"] = notification[k2]; - } else if ([k2 isEqualToString:@"body_loc_key"]) { - notif[@"bodyLocalizationKey"] = notification[k2]; - } else if ([k2 isEqualToString:@"click_action"]) { - notif[@"clickAction"] = notification[k2]; - } else if ([k2 isEqualToString:@"sound"]) { - notif[@"sound"] = notification[k2]; - } else if ([k2 isEqualToString:@"subtitle"]) { - notif[@"subtitle"] = notification[k2]; - } else if ([k2 isEqualToString:@"title"]) { - notif[@"title"] = notification[k2]; - } else if ([k2 isEqualToString:@"title_loc_args"]) { - notif[@"titleLocalizationArgs"] = notification[k2]; - } else if ([k2 isEqualToString:@"title_loc_key"]) { - notif[@"titleLocalizationKey"] = notification[k2]; - } else { - NSLog(@"Unknown notification key: %@", k2); - } - } - message[@"notification"] = notif; + // Ignore for messages } else { // Assume custom data key data[k1] = appData[k1]; } } - message[@"messageType"] = @"RemoteMessage"; message[@"data"] = data; - message[@"openedFromTray"] = @(false); return message; } +- (NSDictionary*)parseUserInfo:(NSDictionary *)userInfo { + NSMutableDictionary *message = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; + + for (id k1 in userInfo) { + if ([k1 isEqualToString:@"aps"]) { + // Ignore notification section + } else if ([k1 isEqualToString:@"gcm.message_id"]) { + message[@"messageId"] = userInfo[k1]; + } else if ([k1 isEqualToString:@"google.c.a.ts"]) { + message[@"sentTime"] = userInfo[k1]; + } else if ([k1 isEqualToString:@"gcm.n.e"] + || [k1 isEqualToString:@"gcm.notification.sound2"] + || [k1 isEqualToString:@"google.c.a.c_id"] + || [k1 isEqualToString:@"google.c.a.c_l"] + || [k1 isEqualToString:@"google.c.a.e"] + || [k1 isEqualToString:@"google.c.a.udt"]) { + // Ignore known keys + } else { + // Assume custom data + data[k1] = userInfo[k1]; + } + } + + message[@"data"] = data; + + return message; +} + - (NSArray *)supportedEvents { return @[MESSAGING_MESSAGE_RECEIVED, MESSAGING_TOKEN_REFRESHED]; } @@ -317,4 +329,3 @@ RCT_EXPORT_METHOD(completeRemoteNotification: (NSString*) messageId @implementation RNFirebaseMessaging @end #endif - diff --git a/ios/RNFirebase/notifications/RNFirebaseNotifications.m b/ios/RNFirebase/notifications/RNFirebaseNotifications.m index abdcd338..fb602149 100644 --- a/ios/RNFirebase/notifications/RNFirebaseNotifications.m +++ b/ios/RNFirebase/notifications/RNFirebaseNotifications.m @@ -2,6 +2,7 @@ #if __has_include() #import "RNFirebaseEvents.h" +#import "RNFirebaseMessaging.h" #import "RNFirebaseUtil.h" #import @@ -37,7 +38,7 @@ RCT_EXPORT_MODULE(); - (void)configure { // If we're on iOS 10 then we need to set this as a delegate for the UNUserNotificationCenter #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 - [UNUserNotificationCenter currentNotificationCenter].delegate = self; + // [UNUserNotificationCenter currentNotificationCenter].delegate = self; #endif // Set static instance for use from AppDelegate @@ -50,26 +51,78 @@ RCT_EXPORT_MODULE(); // ******************************************************* - (void)didReceiveLocalNotification:(nonnull UILocalNotification *)notification { - + NSString *event; + if (RCTSharedApplication().applicationState == UIApplicationStateBackground) { + // notification_displayed + event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; + } else if (RCTSharedApplication().applicationState == UIApplicationStateInactive) { + // notification_displayed + event = NOTIFICATIONS_NOTIFICATION_PRESSED; + } else { + // notification_received + event = NOTIFICATIONS_NOTIFICATION_RECEIVED; + } + + NSDictionary *message = [self parseUILocalNotification:notification]; + [RNFirebaseUtil sendJSEvent:self name:event body:message]; } // Listen for background messages - (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo { - BOOL isFromBackground = (RCTSharedApplication().applicationState == UIApplicationStateInactive); - NSDictionary *message = [self parseUserInfo:userInfo messageType:@"RemoteNotification" clickAction:nil openedFromTray:isFromBackground]; + // FCM Data messages come through here if they specify content-available=true + // Pass them over to the RNFirebaseMessaging handler instead + if (userInfo[@"aps"] && ((NSDictionary*)userInfo[@"aps"]).count == 1 && userInfo[@"aps"][@"content-available"]) { + [[RNFirebaseMessaging instance] didReceiveRemoteNotification:userInfo]; + return; + } - [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; + NSString *event; + if (RCTSharedApplication().applicationState == UIApplicationStateBackground) { + // notification_displayed + event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; + } else if (RCTSharedApplication().applicationState == UIApplicationStateInactive) { + // notification_displayed + event = NOTIFICATIONS_NOTIFICATION_PRESSED; + } else { + // notification_received + event = NOTIFICATIONS_NOTIFICATION_RECEIVED; + } + + // TODO: Proper notification structure + NSDictionary *message = [self parseUserInfo:userInfo messageType:@"RemoteNotification" clickAction:nil]; + + [RNFirebaseUtil sendJSEvent:self name:event body:message]; } // Listen for background messages - (void)didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { - BOOL isFromBackground = (RCTSharedApplication().applicationState == UIApplicationStateInactive); - NSDictionary *message = [self parseUserInfo:userInfo messageType:@"RemoteNotificationHandler" clickAction:nil openedFromTray:isFromBackground]; + // FCM Data messages come through here if they specify content-available=true + // Pass them over to the RNFirebaseMessaging handler instead + if (userInfo[@"aps"] && ((NSDictionary*)userInfo[@"aps"]).count == 1 && userInfo[@"aps"][@"content-available"]) { + [[RNFirebaseMessaging instance] didReceiveRemoteNotification:userInfo]; + return; + } + NSString *event; + if (RCTSharedApplication().applicationState == UIApplicationStateBackground) { + // notification_displayed + event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; + } else if (RCTSharedApplication().applicationState == UIApplicationStateInactive) { + // notification_displayed + event = NOTIFICATIONS_NOTIFICATION_PRESSED; + } else { + // notification_received + event = NOTIFICATIONS_NOTIFICATION_RECEIVED; + } + + // TODO: Proper notification structure + NSDictionary *message = [self parseUserInfo:userInfo messageType:@"RemoteNotificationHandler" clickAction:nil]; + + // TODO: Callback handler // [_callbackHandlers setObject:[completionHandler copy] forKey:message[@"messageId"]]; - [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; + [RNFirebaseUtil sendJSEvent:self name:event body:message]; } // ******************************************************* @@ -93,7 +146,7 @@ RCT_EXPORT_MODULE(); NSString *event; UNNotificationPresentationOptions options; - NSDictionary *message = [self parseUNNotification:notification messageType:@"PresentNotification" openedFromTray:false]; + NSDictionary *message = [self parseUNNotification:notification messageType:@"PresentNotification"]; if (isFcm || isScheduled) { // If app is in the background @@ -116,9 +169,7 @@ RCT_EXPORT_MODULE(); event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; } - if (event) { - [RNFirebaseUtil sendJSEvent:self name:event body:message]; - } + [RNFirebaseUtil sendJSEvent:self name:event body:message]; completionHandler(options); } @@ -130,9 +181,9 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response #else withCompletionHandler:(void(^)())completionHandler { #endif - NSDictionary *message = [self parseUNNotification:response.notification messageType:@"NotificationResponse" openedFromTray:true]; + NSDictionary *message = [self parseUNNotification:response.notification messageType:@"NotificationResponse"]; - [RNFirebaseUtil sendJSEvent:self name:NOTIFICATIONS_NOTIFICATION_CLICKED body:message]; + [RNFirebaseUtil sendJSEvent:self name:NOTIFICATIONS_NOTIFICATION_PRESSED body:message]; completionHandler(); } @@ -200,7 +251,13 @@ RCT_EXPORT_METHOD(getInitialNotification:(RCTPromiseResolveBlock)resolve rejecte NSDictionary *notification = [self parseUILocalNotification:localNotification]; resolve(notification); } else { - resolve(nil); + NSDictionary *remoteNotification = [self bridge].launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; + if (remoteNotification) { + NSDictionary *message = [self parseUserInfo:remoteNotification messageType:@"InitialMessage" clickAction:nil]; + resolve(message); + } else { + resolve(nil); + } } } @@ -485,7 +542,7 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification } ios[@"attachments"] = attachments; } - + if (localNotification.content.badge) { ios[@"badge"] = localNotification.content.badge; } @@ -504,21 +561,20 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification } - (NSDictionary*)parseUNNotification:(UNNotification *)notification - messageType:(NSString *)messageType - openedFromTray:(bool)openedFromTray { + messageType:(NSString *)messageType { NSDictionary *userInfo = notification.request.content.userInfo; NSString *clickAction = notification.request.content.categoryIdentifier; - return [self parseUserInfo:userInfo messageType:messageType clickAction:clickAction openedFromTray:openedFromTray]; + return [self parseUserInfo:userInfo messageType:messageType clickAction:clickAction]; } - (NSDictionary*)parseUserInfo:(NSDictionary *)userInfo messageType:(NSString *) messageType - clickAction:(NSString *) clickAction - openedFromTray:(bool)openedFromTray { - NSMutableDictionary *message = [[NSMutableDictionary alloc] init]; - NSMutableDictionary *notif = [[NSMutableDictionary alloc] init]; + clickAction:(NSString *) clickAction { + + NSMutableDictionary *notification = [[NSMutableDictionary alloc] init]; NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *ios = [[NSMutableDictionary alloc] init]; for (id k1 in userInfo) { if ([k1 isEqualToString:@"aps"]) { @@ -528,37 +584,42 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification NSDictionary *alert = aps[k2]; for (id k3 in alert) { if ([k3 isEqualToString:@"body"]) { - notif[@"body"] = alert[k3]; + notification[@"body"] = alert[k3]; } else if ([k3 isEqualToString:@"loc-args"]) { - notif[@"bodyLocalizationArgs"] = alert[k3]; + // TODO: What to do with this? + // notif[@"bodyLocalizationArgs"] = alert[k3]; } else if ([k3 isEqualToString:@"loc-key"]) { - notif[@"bodyLocalizationKey"] = alert[k3]; + // TODO: What to do with this? + // notif[@"bodyLocalizationKey"] = alert[k3]; } else if ([k3 isEqualToString:@"subtitle"]) { - notif[@"subtitle"] = alert[k3]; + notification[@"subtitle"] = alert[k3]; } else if ([k3 isEqualToString:@"title"]) { - notif[@"title"] = alert[k3]; + notification[@"title"] = alert[k3]; } else if ([k3 isEqualToString:@"title-loc-args"]) { - notif[@"titleLocalizationArgs"] = alert[k3]; + // TODO: What to do with this? + // notif[@"titleLocalizationArgs"] = alert[k3]; } else if ([k3 isEqualToString:@"title-loc-key"]) { - notif[@"titleLocalizationKey"] = alert[k3]; + // TODO: What to do with this? + // notif[@"titleLocalizationKey"] = alert[k3]; } else { NSLog(@"Unknown alert key: %@", k2); } } } else if ([k2 isEqualToString:@"badge"]) { - notif[@"badge"] = aps[k2]; + ios[@"badge"] = aps[k2]; } else if ([k2 isEqualToString:@"category"]) { - notif[@"clickAction"] = aps[k2]; + ios[@"category"] = aps[k2]; } else if ([k2 isEqualToString:@"sound"]) { - notif[@"sound"] = aps[k2]; + notification[@"sound"] = aps[k2]; } else { NSLog(@"Unknown aps key: %@", k2); } } } else if ([k1 isEqualToString:@"gcm.message_id"]) { - message[@"messageId"] = userInfo[k1]; + notification[@"notificationId"] = userInfo[k1]; } else if ([k1 isEqualToString:@"google.c.a.ts"]) { - message[@"sentTime"] = userInfo[k1]; + // TODO: What to do with this? + // message[@"sentTime"] = userInfo[k1]; } else if ([k1 isEqualToString:@"gcm.n.e"] || [k1 isEqualToString:@"gcm.notification.sound2"] || [k1 isEqualToString:@"google.c.a.c_id"] @@ -572,26 +633,17 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification } } - if (!notif[@"clickAction"] && clickAction) { - notif[@"clickAction"] = clickAction; - } + // TODO: What to do with this? + // message[@"messageType"] = messageType; - // 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[@"messageType"] = messageType; + notification[@"data"] = data; + notification[@"ios"] = ios; - message[@"data"] = data; - message[@"notification"] = notif; - message[@"openedFromTray"] = @(openedFromTray); - - return message; + return notification; } - (NSArray *)supportedEvents { - return @[NOTIFICATIONS_NOTIFICATION_CLICKED, NOTIFICATIONS_NOTIFICATION_DISPLAYED, NOTIFICATIONS_NOTIFICATION_RECEIVED]; + return @[NOTIFICATIONS_NOTIFICATION_DISPLAYED, NOTIFICATIONS_NOTIFICATION_PRESSED, NOTIFICATIONS_NOTIFICATION_RECEIVED]; } + (BOOL)requiresMainQueueSetup @@ -605,4 +657,3 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification @implementation RNFirebaseNotifications @end #endif - diff --git a/lib/modules/messaging/Message.js b/lib/modules/messaging/Message.js index d789377e..38f3433f 100644 --- a/lib/modules/messaging/Message.js +++ b/lib/modules/messaging/Message.js @@ -11,9 +11,7 @@ import { } from './types'; import type Messaging from './'; import type { - MessageTypeType, NativeMessage, - Notification, PresentNotificationResultType, RemoteNotificationResultType, } from './types'; @@ -47,18 +45,6 @@ export default class Message { 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; } diff --git a/lib/modules/messaging/index.js b/lib/modules/messaging/index.js index b4b2abc8..f17d389a 100644 --- a/lib/modules/messaging/index.js +++ b/lib/modules/messaging/index.js @@ -55,8 +55,8 @@ export default class Messaging extends ModuleBase { // sub to internal native event - this fans out to // public event name: onMessage 'messaging_message_received', - (message: Message) => { - SharedEventEmitter.emit('onMessage', message); + (message: NativeMessage) => { + SharedEventEmitter.emit('onMessage', new Message(this, message)); } ); @@ -89,8 +89,7 @@ export default class Messaging extends ModuleBase { getLogger(this).info('Creating onMessage listener'); - const wrappedListener = async (nativeMessage: NativeMessage) => { - const message = new Message(this, nativeMessage); + const wrappedListener = async (message: Message) => { await listener(message); message.complete(); }; diff --git a/lib/modules/messaging/types.js b/lib/modules/messaging/types.js index 1909660b..3b58201a 100644 --- a/lib/modules/messaging/types.js +++ b/lib/modules/messaging/types.js @@ -51,9 +51,6 @@ export type NativeMessage = { data: { [string]: string }, from?: string, messageId: string, - messageType?: MessageTypeType, - openedFromTray: boolean, - notification?: Notification, sentTime?: number, to?: string, ttl?: number, diff --git a/lib/modules/notifications/AndroidNotification.js b/lib/modules/notifications/AndroidNotification.js index f2591707..c3305e0b 100644 --- a/lib/modules/notifications/AndroidNotification.js +++ b/lib/modules/notifications/AndroidNotification.js @@ -2,128 +2,29 @@ * @flow * AndroidNotification representation wrapper */ +import { Category } from './types'; import type Notification from './Notification'; - -type Lights = { - argb: number, - onMs: number, - offMs: number, -}; - -type Progress = { - max: number, - progress: number, - indeterminate: boolean, -}; - -type SmallIcon = { - icon: string, - level?: number, -}; - -export type NativeAndroidNotification = {| - // TODO actions: Action[], - autoCancel: boolean, - badgeIconType: BadgeIconTypeType, - category: CategoryType, - channelId: string, - clickAction?: string, - color: string, - colorized: boolean, - contentInfo: string, - defaults: DefaultsType[], - group: string, - groupAlertBehaviour: GroupAlertType, - groupSummary: boolean, - largeIcon: string, - lights: Lights, - localOnly: boolean, - number: number, - ongoing: boolean, - onlyAlertOnce: boolean, - people: string[], - priority: PriorityType, - progress: Progress, - // publicVersion: Notification, - remoteInputHistory: string[], - shortcutId: string, - showWhen: boolean, - smallIcon: SmallIcon, - sortKey: string, - // TODO: style: Style, - ticker: string, - timeoutAfter: number, - usesChronometer: boolean, - vibrate: number[], - visibility: VisibilityType, - when: number, -|}; - -export const BadgeIconType = { - Large: 2, - None: 0, - Small: 1, -}; - -export const Category = { - Alarm: 'alarm', - Call: 'call', - Email: 'email', - Error: 'err', - Event: 'event', - Message: 'msg', - Progress: 'progress', - Promo: 'promo', - Recommendation: 'recommendation', - Reminder: 'reminder', - Service: 'service', - Social: 'social', - Status: 'status', - System: 'system', - Transport: 'transport', -}; - -export const Defaults = { - All: -1, - Lights: 4, - Sound: 1, - Vibrate: 2, -}; - -export const GroupAlert = { - All: 0, - Children: 2, - Summary: 1, -}; - -export const Priority = { - Default: 0, - High: 1, - Low: -1, - Max: 2, - Min: -2, -}; - -export const Visibility = { - Private: 0, - Public: 1, - Secret: -1, -}; - -type BadgeIconTypeType = $Values; -type CategoryType = $Values; -type DefaultsType = $Values; -type GroupAlertType = $Values; -type PriorityType = $Values; -type VisibilityType = $Values; +import type { + BadgeIconTypeType, + CategoryType, + DefaultsType, + GroupAlertType, + Lights, + NativeAndroidNotification, + PriorityType, + Progress, + SmallIcon, + VisibilityType, +} from './types'; export default class AndroidNotification { + // TODO optional fields // TODO actions: Action[]; // icon, title, ??pendingIntent??, allowGeneratedReplies, extender, extras, remoteinput (ugh) _autoCancel: boolean; _badgeIconType: BadgeIconTypeType; _category: CategoryType; _channelId: string; - _clickAction: string; + _clickAction: string | void; _color: string; _colorized: boolean; _contentInfo: string; @@ -145,9 +46,7 @@ export default class AndroidNotification { _remoteInputHistory: string[]; _shortcutId: string; _showWhen: boolean; - _smallIcon: SmallIcon = { - icon: 'ic_launcher', - }; + _smallIcon: SmallIcon; _sortKey: string; // TODO: style: Style; // Need to figure out if this can work _ticker: string; @@ -167,9 +66,50 @@ export default class AndroidNotification { // fullScreenIntent: PendingIntent // sound.streamType - constructor(notification: Notification) { + constructor(notification: Notification, data?: NativeAndroidNotification) { this._notification = notification; - this._people = []; + + if (data) { + this._autoCancel = data.autoCancel; + this._badgeIconType = data.badgeIconType; + this._category = data.category; + this._channelId = data.channelId; + this._clickAction = data.clickAction; + this._color = data.color; + this._colorized = data.colorized; + this._contentInfo = data.contentInfo; + this._defaults = data.defaults; + this._group = data.group; + this._groupAlertBehaviour = data.groupAlertBehaviour; + this._groupSummary = data.groupSummary; + this._largeIcon = data.largeIcon; + this._lights = data.lights; + this._localOnly = data.localOnly; + this._number = data.number; + this._ongoing = data.ongoing; + this._onlyAlertOnce = data.onlyAlertOnce; + this._people = data.people; + this._priority = data.priority; + this._progress = data.progress; + // _publicVersion: Notification; + this._remoteInputHistory = data.remoteInputHistory; + this._shortcutId = data.shortcutId; + this._showWhen = data.showWhen; + this._smallIcon = data.smallIcon; + this._sortKey = data.sortKey; + this._ticker = data.ticker; + this._timeoutAfter = data.timeoutAfter; + this._usesChronometer = data.usesChronometer; + this._vibrate = data.vibrate; + this._visibility = data.visibility; + this._when = data.when; + } + + // Defaults + this._people = this._people || []; + this._smallIcon = this._smallIcon || { + icon: 'ic_launcher', + }; } /** @@ -506,7 +446,7 @@ export default class AndroidNotification { } build(): NativeAndroidNotification { - // TODO: Validation + // TODO: Validation of required fields if (!this._channelId) { throw new Error( 'AndroidNotification: Missing required `channelId` property' diff --git a/lib/modules/notifications/IOSNotification.js b/lib/modules/notifications/IOSNotification.js index d4ad7e11..5b1e563b 100644 --- a/lib/modules/notifications/IOSNotification.js +++ b/lib/modules/notifications/IOSNotification.js @@ -3,48 +3,37 @@ * IOSNotification representation wrapper */ import type Notification from './Notification'; - -type AttachmentOptions = {| - TypeHint: string, - ThumbnailHidden: boolean, - ThumbnailClippingRect: { - height: number, - width: number, - x: number, - y: number, - }, - ThumbnailTime: number, -|}; - -type Attachment = {| - identifier: string, - options?: AttachmentOptions, - url: string, -|}; - -export type NativeIOSNotification = {| - alertAction?: string, - attachments: Attachment[], - badge?: number, - category?: string, - hasAction?: boolean, - launchImage?: string, - threadIdentifier?: string, -|}; +import type { + Attachment, + AttachmentOptions, + NativeIOSNotification, +} from './types'; export default class IOSNotification { - _alertAction: string; // alertAction | N/A + _alertAction: string | void; // alertAction | N/A _attachments: Attachment[]; // N/A | attachments - _badge: number; // applicationIconBadgeNumber | badge - _category: string; - _hasAction: boolean; // hasAction | N/A - _launchImage: string; // alertLaunchImage | launchImageName + _badge: number | void; // applicationIconBadgeNumber | badge + _category: string | void; + _hasAction: boolean | void; // hasAction | N/A + _launchImage: string | void; // alertLaunchImage | launchImageName _notification: Notification; - _threadIdentifier: string; // N/A | threadIdentifier + _threadIdentifier: string | void; // N/A | threadIdentifier - constructor(notification: Notification) { - this._attachments = []; + constructor(notification: Notification, data?: NativeIOSNotification) { this._notification = notification; + + if (data) { + this._alertAction = data.alertAction; + this._attachments = data.attachments; + this._badge = data.badge; + this._category = data.category; + this._hasAction = data.hasAction; + this._launchImage = data.launchImage; + this._threadIdentifier = data.threadIdentifier; + } + + // Defaults + this._attachments = this._attachments || []; } /** @@ -128,7 +117,7 @@ export default class IOSNotification { } build(): NativeIOSNotification { - // TODO: Validation + // TODO: Validation of required fields return { alertAction: this._alertAction, diff --git a/lib/modules/notifications/Notification.js b/lib/modules/notifications/Notification.js index bcaae87b..50ec6b5d 100644 --- a/lib/modules/notifications/Notification.js +++ b/lib/modules/notifications/Notification.js @@ -7,21 +7,7 @@ import AndroidNotification from './AndroidNotification'; import IOSNotification from './IOSNotification'; import { generatePushID, isObject } from '../../utils'; -import type { NativeAndroidNotification } from './AndroidNotification'; -import type { NativeIOSNotification } from './IOSNotification'; -import type { Schedule } from './'; - -type NativeNotification = {| - android?: NativeAndroidNotification, - body: string, - data: { [string]: string }, - ios?: NativeIOSNotification, - notificationId: string, - schedule?: Schedule, - sound?: string, - subtitle?: string, - title: string, -|}; +import type { NativeNotification } from './types'; export default class Notification { // iOS 8/9 | 10+ | Android @@ -34,12 +20,23 @@ export default class Notification { _subtitle: string | void; // N/A | subtitle | subText _title: string; // alertTitle | title | contentTitle - constructor() { - this._android = new AndroidNotification(this); - this._data = {}; - this._ios = new IOSNotification(this); - // TODO: Is this the best way to generate an ID? - this._notificationId = generatePushID(); + 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; + // TODO: Is this the best way to generate an ID? + this._notificationId = data.notificationId; + this._sound = data.sound; + this._subtitle = data.subtitle; + this._title = data.title; + } + + // Defaults + this._data = this._data || {}; + this._notificationId = this._notificationId || generatePushID(); } get android(): AndroidNotification { diff --git a/lib/modules/notifications/index.js b/lib/modules/notifications/index.js index 57aed391..6b9f3e24 100644 --- a/lib/modules/notifications/index.js +++ b/lib/modules/notifications/index.js @@ -15,26 +15,20 @@ import { GroupAlert, Priority, Visibility, -} from './AndroidNotification'; +} from './types'; import type App from '../core/app'; +import type { NativeNotification, Schedule } from './types'; -// TODO: Received notification type will be different from sent notification type OnNotification = Notification => any; type OnNotificationObserver = { next: OnNotification, }; -export type Schedule = { - exact?: boolean, - fireDate: number, - repeatInterval?: 'minute' | 'hour' | 'day' | 'week', -}; - const NATIVE_EVENTS = [ - 'notifications_notification_clicked', 'notifications_notification_displayed', + 'notifications_notification_pressed', 'notifications_notification_received', ]; @@ -69,10 +63,13 @@ export default class Notifications extends ModuleBase { SharedEventEmitter.addListener( // sub to internal native event - this fans out to - // public event name: onNotificationClicked - 'notifications_notification_clicked', - (notification: Notification) => { - SharedEventEmitter.emit('onNotificationClicked', notification); + // public event name: onNotificationPressed + 'notifications_notification_pressed', + (notification: NativeNotification) => { + SharedEventEmitter.emit( + 'onNotificationPressed', + new Notification(notification) + ); } ); @@ -80,8 +77,11 @@ export default class Notifications extends ModuleBase { // sub to internal native event - this fans out to // public event name: onNotificationDisplayed 'notifications_notification_displayed', - (notification: Notification) => { - SharedEventEmitter.emit('onNotificationDisplayed', notification); + (notification: NativeNotification) => { + SharedEventEmitter.emit( + 'onNotificationDisplayed', + new Notification(notification) + ); } ); @@ -89,8 +89,11 @@ export default class Notifications extends ModuleBase { // sub to internal native event - this fans out to // public event name: onNotification 'notifications_notification_received', - (notification: Notification) => { - SharedEventEmitter.emit('onNotification', notification); + (notification: NativeNotification) => { + SharedEventEmitter.emit( + 'onNotification', + new Notification(notification) + ); } ); } @@ -166,29 +169,6 @@ export default class Notifications extends ModuleBase { }; } - onNotificationClicked( - nextOrObserver: OnNotification | OnNotificationObserver - ): () => any { - let listener; - if (isFunction(nextOrObserver)) { - listener = nextOrObserver; - } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { - listener = nextOrObserver.next; - } else { - throw new Error( - 'Notifications.onNotificationClicked failed: First argument must be a function or observer object with a `next` function.' - ); - } - - getLogger(this).info('Creating onNotificationClicked listener'); - SharedEventEmitter.addListener('onNotificationClicked', listener); - - return () => { - getLogger(this).info('Removing onNotificationClicked listener'); - SharedEventEmitter.removeListener('onNotificationClicked', listener); - }; - } - onNotificationDisplayed( nextOrObserver: OnNotification | OnNotificationObserver ): () => any { @@ -212,6 +192,30 @@ export default class Notifications extends ModuleBase { }; } + onNotificationPressed( + nextOrObserver: OnNotification | OnNotificationObserver + ): () => any { + let listener: Notification => any; + if (isFunction(nextOrObserver)) { + // $FlowBug: Not coping with the overloaded method signature + listener = nextOrObserver; + } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { + listener = nextOrObserver.next; + } else { + throw new Error( + 'Notifications.onNotificationPressed failed: First argument must be a function or observer object with a `next` function.' + ); + } + + getLogger(this).info('Creating onNotificationPressed listener'); + SharedEventEmitter.addListener('onNotificationPressed', listener); + + return () => { + getLogger(this).info('Removing onNotificationPressed listener'); + SharedEventEmitter.removeListener('onNotificationPressed', listener); + }; + } + /** * Remove all delivered notifications. * @returns {*} diff --git a/lib/modules/notifications/types.js b/lib/modules/notifications/types.js new file mode 100644 index 00000000..eec87988 --- /dev/null +++ b/lib/modules/notifications/types.js @@ -0,0 +1,162 @@ +/** + * @flow + */ + +export const BadgeIconType = { + Large: 2, + None: 0, + Small: 1, +}; + +export const Category = { + Alarm: 'alarm', + Call: 'call', + Email: 'email', + Error: 'err', + Event: 'event', + Message: 'msg', + Progress: 'progress', + Promo: 'promo', + Recommendation: 'recommendation', + Reminder: 'reminder', + Service: 'service', + Social: 'social', + Status: 'status', + System: 'system', + Transport: 'transport', +}; + +export const Defaults = { + All: -1, + Lights: 4, + Sound: 1, + Vibrate: 2, +}; + +export const GroupAlert = { + All: 0, + Children: 2, + Summary: 1, +}; + +export const Priority = { + Default: 0, + High: 1, + Low: -1, + Max: 2, + Min: -2, +}; + +export const Visibility = { + Private: 0, + Public: 1, + Secret: -1, +}; + +export type BadgeIconTypeType = $Values; +export type CategoryType = $Values; +export type DefaultsType = $Values; +export type GroupAlertType = $Values; +export type PriorityType = $Values; +export type VisibilityType = $Values; + +export type Lights = { + argb: number, + onMs: number, + offMs: number, +}; + +export type Progress = { + max: number, + progress: number, + indeterminate: boolean, +}; + +export type SmallIcon = { + icon: string, + level?: number, +}; + +export type NativeAndroidNotification = {| + // TODO actions: Action[], + autoCancel: boolean, + badgeIconType: BadgeIconTypeType, + category: CategoryType, + channelId: string, + clickAction?: string, + color: string, + colorized: boolean, + contentInfo: string, + defaults: DefaultsType[], + group: string, + groupAlertBehaviour: GroupAlertType, + groupSummary: boolean, + largeIcon: string, + lights: Lights, + localOnly: boolean, + number: number, + ongoing: boolean, + onlyAlertOnce: boolean, + people: string[], + priority: PriorityType, + progress: Progress, + // publicVersion: Notification, + remoteInputHistory: string[], + shortcutId: string, + showWhen: boolean, + smallIcon: SmallIcon, + sortKey: string, + // TODO: style: Style, + ticker: string, + timeoutAfter: number, + usesChronometer: boolean, + vibrate: number[], + visibility: VisibilityType, + when: number, +|}; + +export type AttachmentOptions = {| + TypeHint: string, + ThumbnailHidden: boolean, + ThumbnailClippingRect: { + height: number, + width: number, + x: number, + y: number, + }, + ThumbnailTime: number, +|}; + +export type Attachment = {| + identifier: string, + options?: AttachmentOptions, + url: string, +|}; + +export type NativeIOSNotification = {| + alertAction?: string, + attachments: Attachment[], + badge?: number, + category?: string, + hasAction?: boolean, + launchImage?: string, + threadIdentifier?: string, +|}; + +export type Schedule = { + exact?: boolean, + fireDate: number, + repeatInterval?: 'minute' | 'hour' | 'day' | 'week', +}; + +export type NativeNotification = {| + android?: NativeAndroidNotification, + body: string, + data: { [string]: string }, + ios?: NativeIOSNotification, + notificationId: string, + schedule?: Schedule, + sound?: string, + subtitle?: string, + title: string, +|}; From 8e84dd576b1c08568ccc0fa148f9f1e0528509df Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 22 Feb 2018 17:25:15 +0000 Subject: [PATCH 32/77] [notifications] Specific notificationPressed data type including action --- .../notifications/RNFirebaseNotifications.m | 114 ++++++++++-------- lib/modules/notifications/Notification.js | 5 + lib/modules/notifications/index.js | 42 ++++--- lib/modules/notifications/types.js | 5 + 4 files changed, 101 insertions(+), 65 deletions(-) diff --git a/ios/RNFirebase/notifications/RNFirebaseNotifications.m b/ios/RNFirebase/notifications/RNFirebaseNotifications.m index fb602149..ed8b7fa5 100644 --- a/ios/RNFirebase/notifications/RNFirebaseNotifications.m +++ b/ios/RNFirebase/notifications/RNFirebaseNotifications.m @@ -38,7 +38,7 @@ RCT_EXPORT_MODULE(); - (void)configure { // If we're on iOS 10 then we need to set this as a delegate for the UNUserNotificationCenter #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 - // [UNUserNotificationCenter currentNotificationCenter].delegate = self; + [UNUserNotificationCenter currentNotificationCenter].delegate = self; #endif // Set static instance for use from AppDelegate @@ -51,20 +51,22 @@ RCT_EXPORT_MODULE(); // ******************************************************* - (void)didReceiveLocalNotification:(nonnull UILocalNotification *)notification { - NSString *event; - if (RCTSharedApplication().applicationState == UIApplicationStateBackground) { - // notification_displayed - event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; - } else if (RCTSharedApplication().applicationState == UIApplicationStateInactive) { - // notification_displayed - event = NOTIFICATIONS_NOTIFICATION_PRESSED; - } else { - // notification_received - event = NOTIFICATIONS_NOTIFICATION_RECEIVED; - } + #if !defined(__IPHONE_10_0) || __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_10_0 + NSString *event; + if (RCTSharedApplication().applicationState == UIApplicationStateBackground) { + // notification_displayed + event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; + } else if (RCTSharedApplication().applicationState == UIApplicationStateInactive) { + // notification_displayed + event = NOTIFICATIONS_NOTIFICATION_PRESSED; + } else { + // notification_received + event = NOTIFICATIONS_NOTIFICATION_RECEIVED; + } - NSDictionary *message = [self parseUILocalNotification:notification]; - [RNFirebaseUtil sendJSEvent:self name:event body:message]; + NSDictionary *message = [self parseUILocalNotification:notification]; + [RNFirebaseUtil sendJSEvent:self name:event body:message]; + #endif } // Listen for background messages @@ -75,7 +77,7 @@ RCT_EXPORT_MODULE(); [[RNFirebaseMessaging instance] didReceiveRemoteNotification:userInfo]; return; } - + NSString *event; if (RCTSharedApplication().applicationState == UIApplicationStateBackground) { // notification_displayed @@ -85,12 +87,17 @@ RCT_EXPORT_MODULE(); event = NOTIFICATIONS_NOTIFICATION_PRESSED; } else { // notification_received - event = NOTIFICATIONS_NOTIFICATION_RECEIVED; + // On IOS 10, foreground notifications also go through willPresentNotification + // This prevents duplicate messages from hitting the JS app + #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + return; + #else + event = NOTIFICATIONS_NOTIFICATION_RECEIVED; + #endif } - - // TODO: Proper notification structure - NSDictionary *message = [self parseUserInfo:userInfo messageType:@"RemoteNotification" clickAction:nil]; - + + NSDictionary *message = [self parseUserInfo:userInfo messageType:@"RemoteNotification" category:nil]; + [RNFirebaseUtil sendJSEvent:self name:event body:message]; } @@ -104,6 +111,7 @@ RCT_EXPORT_MODULE(); return; } + NSString *event; if (RCTSharedApplication().applicationState == UIApplicationStateBackground) { // notification_displayed @@ -113,16 +121,19 @@ RCT_EXPORT_MODULE(); event = NOTIFICATIONS_NOTIFICATION_PRESSED; } else { // notification_received - event = NOTIFICATIONS_NOTIFICATION_RECEIVED; + // On IOS 10, foreground notifications also go through willPresentNotification + // This prevents duplicate messages from hitting the JS app + #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + return; + #else + event = NOTIFICATIONS_NOTIFICATION_RECEIVED; + #endif } - - // TODO: Proper notification structure - NSDictionary *message = [self parseUserInfo:userInfo messageType:@"RemoteNotificationHandler" clickAction:nil]; - - // TODO: Callback handler - // [_callbackHandlers setObject:[completionHandler copy] forKey:message[@"messageId"]]; - + + NSDictionary *message = [self parseUserInfo:userInfo messageType:@"RemoteNotificationHandler" category:nil]; + [RNFirebaseUtil sendJSEvent:self name:event body:message]; + completionHandler(UIBackgroundFetchResultNoData); } // ******************************************************* @@ -181,7 +192,7 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response #else withCompletionHandler:(void(^)())completionHandler { #endif - NSDictionary *message = [self parseUNNotification:response.notification messageType:@"NotificationResponse"]; + NSDictionary *message = [self parseUNNotificationResponse:response messageType:@"NotificationResponse"]; [RNFirebaseUtil sendJSEvent:self name:NOTIFICATIONS_NOTIFICATION_PRESSED body:message]; completionHandler(); @@ -253,7 +264,7 @@ RCT_EXPORT_METHOD(getInitialNotification:(RCTPromiseResolveBlock)resolve rejecte } else { NSDictionary *remoteNotification = [self bridge].launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; if (remoteNotification) { - NSDictionary *message = [self parseUserInfo:remoteNotification messageType:@"InitialMessage" clickAction:nil]; + NSDictionary *message = [self parseUserInfo:remoteNotification messageType:@"InitialMessage" category:nil]; resolve(message); } else { resolve(nil); @@ -560,17 +571,27 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification return notification; } +- (NSDictionary*)parseUNNotificationResponse:(UNNotificationResponse *)response + messageType:(NSString *)messageType { + NSMutableDictionary *notificationResponse = [[NSMutableDictionary alloc] init]; + NSDictionary *notification = [self parseUNNotification:response.notification messageType:messageType]; + notificationResponse[@"notification"] = notification; + notificationResponse[@"action"] = response.actionIdentifier; + + return notificationResponse; +} + - (NSDictionary*)parseUNNotification:(UNNotification *)notification messageType:(NSString *)messageType { NSDictionary *userInfo = notification.request.content.userInfo; - NSString *clickAction = notification.request.content.categoryIdentifier; + NSString *category = notification.request.content.categoryIdentifier; - return [self parseUserInfo:userInfo messageType:messageType clickAction:clickAction]; + return [self parseUserInfo:userInfo messageType:messageType category:category]; } - (NSDictionary*)parseUserInfo:(NSDictionary *)userInfo messageType:(NSString *) messageType - clickAction:(NSString *) clickAction { + category:(NSString *) category { NSMutableDictionary *notification = [[NSMutableDictionary alloc] init]; NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; @@ -585,22 +606,15 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification for (id k3 in alert) { if ([k3 isEqualToString:@"body"]) { notification[@"body"] = alert[k3]; - } else if ([k3 isEqualToString:@"loc-args"]) { - // TODO: What to do with this? - // notif[@"bodyLocalizationArgs"] = alert[k3]; - } else if ([k3 isEqualToString:@"loc-key"]) { - // TODO: What to do with this? - // notif[@"bodyLocalizationKey"] = alert[k3]; } else if ([k3 isEqualToString:@"subtitle"]) { notification[@"subtitle"] = alert[k3]; } else if ([k3 isEqualToString:@"title"]) { notification[@"title"] = alert[k3]; - } else if ([k3 isEqualToString:@"title-loc-args"]) { - // TODO: What to do with this? - // notif[@"titleLocalizationArgs"] = alert[k3]; - } else if ([k3 isEqualToString:@"title-loc-key"]) { - // TODO: What to do with this? - // notif[@"titleLocalizationKey"] = alert[k3]; + } else if ([k3 isEqualToString:@"loc-args"] + || [k3 isEqualToString:@"loc-key"] + || [k3 isEqualToString:@"title-loc-args"] + || [k3 isEqualToString:@"title-loc-key"]) { + // Ignore known keys } else { NSLog(@"Unknown alert key: %@", k2); } @@ -617,21 +631,23 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification } } else if ([k1 isEqualToString:@"gcm.message_id"]) { notification[@"notificationId"] = userInfo[k1]; - } else if ([k1 isEqualToString:@"google.c.a.ts"]) { - // TODO: What to do with this? - // message[@"sentTime"] = userInfo[k1]; } else if ([k1 isEqualToString:@"gcm.n.e"] || [k1 isEqualToString:@"gcm.notification.sound2"] || [k1 isEqualToString:@"google.c.a.c_id"] || [k1 isEqualToString:@"google.c.a.c_l"] || [k1 isEqualToString:@"google.c.a.e"] - || [k1 isEqualToString:@"google.c.a.udt"]) { + || [k1 isEqualToString:@"google.c.a.udt"] + || [k1 isEqualToString:@"google.c.a.ts"]) { // Ignore known keys } else { // Assume custom data data[k1] = userInfo[k1]; } } + + if (!ios[@"category"]) { + ios[@"category"] = category; + } // TODO: What to do with this? // message[@"messageType"] = messageType; diff --git a/lib/modules/notifications/Notification.js b/lib/modules/notifications/Notification.js index 50ec6b5d..1ad7d2cd 100644 --- a/lib/modules/notifications/Notification.js +++ b/lib/modules/notifications/Notification.js @@ -9,6 +9,11 @@ import { generatePushID, isObject } from '../../utils'; import type { NativeNotification } from './types'; +export type NotificationPressed = { + action: string, + notification: Notification, +}; + export default class Notification { // iOS 8/9 | 10+ | Android _android: AndroidNotification; diff --git a/lib/modules/notifications/index.js b/lib/modules/notifications/index.js index 6b9f3e24..60e1dc61 100644 --- a/lib/modules/notifications/index.js +++ b/lib/modules/notifications/index.js @@ -18,7 +18,12 @@ import { } from './types'; import type App from '../core/app'; -import type { NativeNotification, Schedule } from './types'; +import type { NotificationPressed } from './Notification'; +import type { + NativeNotification, + NativeNotificationPressed, + Schedule, +} from './types'; type OnNotification = Notification => any; @@ -26,6 +31,12 @@ type OnNotificationObserver = { next: OnNotification, }; +type OnNotificationPressed = NotificationPressed => any; + +type OnNotificationPressedObserver = { + next: OnNotificationPressed, +}; + const NATIVE_EVENTS = [ 'notifications_notification_displayed', 'notifications_notification_pressed', @@ -61,18 +72,6 @@ export default class Notifications extends ModuleBase { namespace: NAMESPACE, }); - SharedEventEmitter.addListener( - // sub to internal native event - this fans out to - // public event name: onNotificationPressed - 'notifications_notification_pressed', - (notification: NativeNotification) => { - SharedEventEmitter.emit( - 'onNotificationPressed', - new Notification(notification) - ); - } - ); - SharedEventEmitter.addListener( // sub to internal native event - this fans out to // public event name: onNotificationDisplayed @@ -85,6 +84,18 @@ export default class Notifications extends ModuleBase { } ); + SharedEventEmitter.addListener( + // sub to internal native event - this fans out to + // public event name: onNotificationPressed + 'notifications_notification_pressed', + (notificationPressed: NativeNotificationPressed) => { + SharedEventEmitter.emit('onNotificationPressed', { + action: notificationPressed.action, + notification: new Notification(notificationPressed.notification), + }); + } + ); + SharedEventEmitter.addListener( // sub to internal native event - this fans out to // public event name: onNotification @@ -193,11 +204,10 @@ export default class Notifications extends ModuleBase { } onNotificationPressed( - nextOrObserver: OnNotification | OnNotificationObserver + nextOrObserver: OnNotificationPressed | OnNotificationPressedObserver ): () => any { - let listener: Notification => any; + let listener; if (isFunction(nextOrObserver)) { - // $FlowBug: Not coping with the overloaded method signature listener = nextOrObserver; } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { listener = nextOrObserver.next; diff --git a/lib/modules/notifications/types.js b/lib/modules/notifications/types.js index eec87988..9c2abe10 100644 --- a/lib/modules/notifications/types.js +++ b/lib/modules/notifications/types.js @@ -160,3 +160,8 @@ export type NativeNotification = {| subtitle?: string, title: string, |}; + +export type NativeNotificationPressed = {| + action: string, + notification: NativeNotification, +|}; From 2838bbc0cde3fef451139ebaef972aba2ccb8494 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 23 Feb 2018 15:34:00 +0000 Subject: [PATCH 33/77] [notifications] Last part of iOS implementation --- .../messaging/RNFirebaseMessaging.m | 21 +- .../notifications/RNFirebaseNotifications.h | 3 +- .../notifications/RNFirebaseNotifications.m | 308 ++++++++++-------- lib/modules/messaging/index.js | 15 - lib/modules/notifications/index.js | 8 + 5 files changed, 185 insertions(+), 170 deletions(-) diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index 8ce69e62..31365c80 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -150,19 +150,6 @@ RCT_EXPORT_METHOD(requestPermission:(RCTPromiseResolveBlock)resolve rejecter:(RC } // Non Web SDK methods - -// TODO: Move to notifications -RCT_EXPORT_METHOD(getBadge: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - dispatch_async(dispatch_get_main_queue(), ^{ - resolve(@([RCTSharedApplication() applicationIconBadgeNumber])); - }); -} - -// TODO: Remove -RCT_EXPORT_METHOD(getInitialMessage:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){ - resolve(nil); -} - RCT_EXPORT_METHOD(hasPermission: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { dispatch_async(dispatch_get_main_queue(), ^{ @@ -192,13 +179,6 @@ RCT_EXPORT_METHOD(sendMessage: (NSDictionary *) message [[FIRMessaging messaging] sendMessage:data to:to withMessageID:messageId timeToLive:[ttl intValue]]; } -// TODO: Move to notifications -RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { - dispatch_async(dispatch_get_main_queue(), ^{ - [RCTSharedApplication() setApplicationIconBadgeNumber:number]; - }); -} - RCT_EXPORT_METHOD(subscribeToTopic: (NSString*) topic) { [[FIRMessaging messaging] subscribeToTopic:topic]; } @@ -329,3 +309,4 @@ RCT_EXPORT_METHOD(completeRemoteNotification: (NSString*) messageId @implementation RNFirebaseMessaging @end #endif + diff --git a/ios/RNFirebase/notifications/RNFirebaseNotifications.h b/ios/RNFirebase/notifications/RNFirebaseNotifications.h index 56ca289b..5871ba7b 100644 --- a/ios/RNFirebase/notifications/RNFirebaseNotifications.h +++ b/ios/RNFirebase/notifications/RNFirebaseNotifications.h @@ -8,11 +8,11 @@ @interface RNFirebaseNotifications : RCTEventEmitter ++ (void)configure; + (_Nonnull instancetype)instance; #if !TARGET_OS_TV - (void)didReceiveLocalNotification:(nonnull UILocalNotification *)notification; -- (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo; - (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo fetchCompletionHandler:(void (^_Nonnull)(UIBackgroundFetchResult))completionHandler; #endif @@ -24,3 +24,4 @@ #endif #endif + diff --git a/ios/RNFirebase/notifications/RNFirebaseNotifications.m b/ios/RNFirebase/notifications/RNFirebaseNotifications.m index ed8b7fa5..beb733a0 100644 --- a/ios/RNFirebase/notifications/RNFirebaseNotifications.m +++ b/ios/RNFirebase/notifications/RNFirebaseNotifications.m @@ -19,39 +19,60 @@ @implementation RNFirebaseNotifications 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 +// static NSMutableArray *pendingEvents = nil; +static NSDictionary *initialNotification = nil; + (nonnull instancetype)instance { return theRNFirebaseNotifications; } ++ (void)configure { + // PRE-BRIDGE-EVENTS: Consider enabling this to allow events built up before the bridge is built to be sent to the JS side + // pendingEvents = [[NSMutableArray alloc] init]; + theRNFirebaseNotifications = [[RNFirebaseNotifications alloc] init]; +} + RCT_EXPORT_MODULE(); - (id)init { self = [super init]; if (self != nil) { NSLog(@"Setting up RNFirebaseNotifications instance"); - [self configure]; + [self initialise]; } return self; } -- (void)configure { +- (void)initialise { // If we're on iOS 10 then we need to set this as a delegate for the UNUserNotificationCenter -#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 - [UNUserNotificationCenter currentNotificationCenter].delegate = self; -#endif + #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + [UNUserNotificationCenter currentNotificationCenter].delegate = self; + #endif // Set static instance for use from AppDelegate theRNFirebaseNotifications = self; } +// PRE-BRIDGE-EVENTS: Consider enabling this to allow events built up before the bridge is built to be sent to the JS side +// The bridge is initialised after the module is created +// When the bridge is set, check if we have any pending events to send, and send them +/* - (void)setValue:(nullable id)value forKey:(NSString *)key { + [super setValue:value forKey:key]; + if ([key isEqualToString:@"bridge"] && value) { + for (NSDictionary* event in pendingEvents) { + [RNFirebaseUtil sendJSEvent:self name:event[@"name"] body:event[@"body"]]; + } + [pendingEvents removeAllObjects]; + } +} */ + // ******************************************************* // ** Start AppDelegate methods // ** iOS 8/9 Only // ******************************************************* - -- (void)didReceiveLocalNotification:(nonnull UILocalNotification *)notification { - #if !defined(__IPHONE_10_0) || __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_10_0 +- (void)didReceiveLocalNotification:(nonnull UILocalNotification *)localNotification { + if ([self isIOS89]) { NSString *event; if (RCTSharedApplication().applicationState == UIApplicationStateBackground) { // notification_displayed @@ -64,41 +85,15 @@ RCT_EXPORT_MODULE(); event = NOTIFICATIONS_NOTIFICATION_RECEIVED; } - NSDictionary *message = [self parseUILocalNotification:notification]; - [RNFirebaseUtil sendJSEvent:self name:event body:message]; - #endif -} - -// Listen for background messages -- (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo { - // FCM Data messages come through here if they specify content-available=true - // Pass them over to the RNFirebaseMessaging handler instead - if (userInfo[@"aps"] && ((NSDictionary*)userInfo[@"aps"]).count == 1 && userInfo[@"aps"][@"content-available"]) { - [[RNFirebaseMessaging instance] didReceiveRemoteNotification:userInfo]; - return; + NSDictionary *notification = [self parseUILocalNotification:localNotification]; + if (event == NOTIFICATIONS_NOTIFICATION_PRESSED) { + notification = @{ + @"action": UNNotificationDefaultActionIdentifier, + @"notification": notification + }; + } + [self sendJSEvent:self name:event body:notification]; } - - NSString *event; - if (RCTSharedApplication().applicationState == UIApplicationStateBackground) { - // notification_displayed - event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; - } else if (RCTSharedApplication().applicationState == UIApplicationStateInactive) { - // notification_displayed - event = NOTIFICATIONS_NOTIFICATION_PRESSED; - } else { - // notification_received - // On IOS 10, foreground notifications also go through willPresentNotification - // This prevents duplicate messages from hitting the JS app - #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 - return; - #else - event = NOTIFICATIONS_NOTIFICATION_RECEIVED; - #endif - } - - NSDictionary *message = [self parseUserInfo:userInfo messageType:@"RemoteNotification" category:nil]; - - [RNFirebaseUtil sendJSEvent:self name:event body:message]; } // Listen for background messages @@ -111,28 +106,36 @@ RCT_EXPORT_MODULE(); return; } - NSString *event; if (RCTSharedApplication().applicationState == UIApplicationStateBackground) { // notification_displayed event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; - } else if (RCTSharedApplication().applicationState == UIApplicationStateInactive) { - // notification_displayed - event = NOTIFICATIONS_NOTIFICATION_PRESSED; - } else { - // notification_received - // On IOS 10, foreground notifications also go through willPresentNotification - // This prevents duplicate messages from hitting the JS app - #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 - return; - #else + } else if ([self isIOS89]) { + if (RCTSharedApplication().applicationState == UIApplicationStateInactive) { + // notification_displayed + event = NOTIFICATIONS_NOTIFICATION_PRESSED; + } else { + // notification_received event = NOTIFICATIONS_NOTIFICATION_RECEIVED; - #endif + } + } else { + // On IOS 10: + // - foreground notifications also go through willPresentNotification + // - background notification presses also go through didReceiveNotificationResponse + // This prevents duplicate messages from hitting the JS app + return; } - NSDictionary *message = [self parseUserInfo:userInfo messageType:@"RemoteNotificationHandler" category:nil]; + NSDictionary *notification = [self parseUserInfo:userInfo]; + // For onPressed events, we set the default action name as iOS 8/9 has no concept of actions + if (event == NOTIFICATIONS_NOTIFICATION_PRESSED) { + notification = @{ + @"action": UNNotificationDefaultActionIdentifier, + @"notification": notification + }; + } - [RNFirebaseUtil sendJSEvent:self name:event body:message]; + [self sendJSEvent:self name:event body:notification]; completionHandler(UIBackgroundFetchResultNoData); } @@ -157,11 +160,12 @@ RCT_EXPORT_MODULE(); NSString *event; UNNotificationPresentationOptions options; - NSDictionary *message = [self parseUNNotification:notification messageType:@"PresentNotification"]; + NSDictionary *message = [self parseUNNotification:notification]; if (isFcm || isScheduled) { // If app is in the background - if (RCTSharedApplication().applicationState == UIApplicationStateInactive) { + if (RCTSharedApplication().applicationState == UIApplicationStateBackground + || RCTSharedApplication().applicationState == UIApplicationStateInactive) { // display the notification options = UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound; // notification_displayed @@ -180,7 +184,7 @@ RCT_EXPORT_MODULE(); event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; } - [RNFirebaseUtil sendJSEvent:self name:event body:message]; + [self sendJSEvent:self name:event body:message]; completionHandler(options); } @@ -192,9 +196,9 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response #else withCompletionHandler:(void(^)())completionHandler { #endif - NSDictionary *message = [self parseUNNotificationResponse:response messageType:@"NotificationResponse"]; + NSDictionary *message = [self parseUNNotificationResponse:response]; - [RNFirebaseUtil sendJSEvent:self name:NOTIFICATIONS_NOTIFICATION_PRESSED body:message]; + [self sendJSEvent:self name:NOTIFICATIONS_NOTIFICATION_PRESSED body:message]; completionHandler(); } @@ -205,7 +209,7 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response // ******************************************************* RCT_EXPORT_METHOD(cancelAllNotifications) { - if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { + if ([self isIOS89]) { [RCTSharedApplication() cancelAllLocalNotifications]; } else { #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 @@ -218,7 +222,7 @@ RCT_EXPORT_METHOD(cancelAllNotifications) { } RCT_EXPORT_METHOD(cancelNotification:(NSString*) notificationId) { - if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { + if ([self isIOS89]) { for (UILocalNotification *notification in RCTSharedApplication().scheduledLocalNotifications) { NSDictionary *notificationInfo = notification.userInfo; if ([notificationId isEqualToString:[notificationInfo valueForKey:@"notificationId"]]) { @@ -238,7 +242,7 @@ RCT_EXPORT_METHOD(cancelNotification:(NSString*) notificationId) { RCT_EXPORT_METHOD(displayNotification:(NSDictionary*) notification resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { + if ([self isIOS89]) { UILocalNotification* notif = [self buildUILocalNotification:notification withSchedule:false]; [RCTSharedApplication() presentLocalNotificationNow:notif]; resolve(nil); @@ -248,33 +252,48 @@ RCT_EXPORT_METHOD(displayNotification:(NSDictionary*) notification [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { if (!error) { resolve(nil); - }else{ + } else{ reject(@"notifications/display_notification_error", @"Failed to display notificaton", error); } }]; #endif } } + +RCT_EXPORT_METHOD(getBadge: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + dispatch_async(dispatch_get_main_queue(), ^{ + resolve(@([RCTSharedApplication() applicationIconBadgeNumber])); + }); +} -RCT_EXPORT_METHOD(getInitialNotification:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){ - UILocalNotification *localNotification = [self bridge].launchOptions[UIApplicationLaunchOptionsLocalNotificationKey]; - if (localNotification) { - NSDictionary *notification = [self parseUILocalNotification:localNotification]; - resolve(notification); - } else { - NSDictionary *remoteNotification = [self bridge].launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; - if (remoteNotification) { - NSDictionary *message = [self parseUserInfo:remoteNotification messageType:@"InitialMessage" category:nil]; - resolve(message); +RCT_EXPORT_METHOD(getInitialNotification:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + if ([self isIOS89]) { + // With iOS 8/9 we rely on the launch options + UILocalNotification *localNotification = [self bridge].launchOptions[UIApplicationLaunchOptionsLocalNotificationKey]; + NSDictionary *notification; + if (localNotification) { + notification = [self parseUILocalNotification:localNotification]; + } else { + NSDictionary *remoteNotification = [self bridge].launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; + if (remoteNotification) { + notification = [self parseUserInfo:remoteNotification]; + } + } + if (notification) { + resolve(@{@"action": UNNotificationDefaultActionIdentifier, @"notification": notification}); } else { resolve(nil); } + } else { + // With iOS 10+ we are able to intercept the didReceiveNotificationResponse message + // to get both the notification and the action + resolve(initialNotification); } } RCT_EXPORT_METHOD(getScheduledNotifications:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { + if ([self isIOS89]) { NSMutableArray* notifications = [[NSMutableArray alloc] init]; for (UILocalNotification *notif in [RCTSharedApplication() scheduledLocalNotifications]){ NSDictionary *notification = [self parseUILocalNotification:notif]; @@ -296,7 +315,7 @@ RCT_EXPORT_METHOD(getScheduledNotifications:(RCTPromiseResolveBlock)resolve } RCT_EXPORT_METHOD(removeAllDeliveredNotifications) { - if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { + if ([self isIOS89]) { // No such functionality on iOS 8/9 } else { #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 @@ -309,7 +328,7 @@ RCT_EXPORT_METHOD(removeAllDeliveredNotifications) { } RCT_EXPORT_METHOD(removeDeliveredNotification:(NSString*) notificationId) { - if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { + if ([self isIOS89]) { // No such functionality on iOS 8/9 } else { #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 @@ -324,7 +343,7 @@ RCT_EXPORT_METHOD(removeDeliveredNotification:(NSString*) notificationId) { RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { + if ([self isIOS89]) { UILocalNotification* notif = [self buildUILocalNotification:notification withSchedule:true]; [RCTSharedApplication() scheduleLocalNotification:notif]; resolve(nil); @@ -334,13 +353,38 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { if (!error) { resolve(nil); - }else{ + } else{ reject(@"notification/schedule_notification_error", @"Failed to schedule notificaton", error); } }]; #endif } } + +RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { + dispatch_async(dispatch_get_main_queue(), ^{ + [RCTSharedApplication() setApplicationIconBadgeNumber:number]; + }); +} + +// Because of the time delay between the app starting and the bridge being initialised +// we create a temporary instance of RNFirebaseNotifications. +// With this temporary instance, we cache any events to be sent as soon as the bridge is set on the module +- (void)sendJSEvent:(RCTEventEmitter *)emitter name:(NSString *)name body:(id)body { + if (emitter.bridge) { + [RNFirebaseUtil sendJSEvent:emitter name:name body:body]; + } else { + if ([name isEqualToString:NOTIFICATIONS_NOTIFICATION_PRESSED] && !initialNotification) { + initialNotification = body; + } + // PRE-BRIDGE-EVENTS: Consider enabling this to allow events built up before the bridge is built to be sent to the JS side + [pendingEvents addObject:@{@"name":name, @"body":body}]; + } +} + +- (BOOL)isIOS89 { + return floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max; +} - (UILocalNotification*) buildUILocalNotification:(NSDictionary *) notification withSchedule:(BOOL) withSchedule { @@ -518,33 +562,55 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification return notification; } - -- (NSDictionary*) parseUNNotificationRequest:(UNNotificationRequest *) localNotification { - NSMutableDictionary *notification = [[NSMutableDictionary alloc] init]; - - notification[@"notificationId"] = localNotification.identifier; - if (localNotification.content.body) { - notification[@"body"] = localNotification.content.body; +- (NSDictionary*)parseUNNotificationResponse:(UNNotificationResponse *)response { + NSMutableDictionary *notificationResponse = [[NSMutableDictionary alloc] init]; + NSDictionary *notification = [self parseUNNotification:response.notification]; + notificationResponse[@"notification"] = notification; + notificationResponse[@"action"] = response.actionIdentifier; + + return notificationResponse; +} + +- (NSDictionary*)parseUNNotification:(UNNotification *)notification { + return [self parseUNNotificationRequest:notification.request]; +} + +- (NSDictionary*) parseUNNotificationRequest:(UNNotificationRequest *) notificationRequest { + NSMutableDictionary *notification = [[NSMutableDictionary alloc] init]; + + notification[@"notificationId"] = notificationRequest.identifier; + + if (notificationRequest.content.body) { + notification[@"body"] = notificationRequest.content.body; } - if (localNotification.content.userInfo) { - notification[@"data"] = localNotification.content.userInfo; + if (notificationRequest.content.userInfo) { + NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; + for (id k in notificationRequest.content.userInfo) { + if ([k isEqualToString:@"aps"] + || [k isEqualToString:@"gcm.message_id"]) { + // ignore as these are handled by the OS + } else { + data[k] = notificationRequest.content.userInfo[k]; + } + } + notification[@"data"] = data; } - if (localNotification.content.sound) { - notification[@"sound"] = localNotification.content.sound; + if (notificationRequest.content.sound) { + notification[@"sound"] = notificationRequest.content.sound; } - if (localNotification.content.subtitle) { - notification[@"subtitle"] = localNotification.content.subtitle; + if (notificationRequest.content.subtitle) { + notification[@"subtitle"] = notificationRequest.content.subtitle; } - if (localNotification.content.title) { - notification[@"title"] = localNotification.content.title; + if (notificationRequest.content.title) { + notification[@"title"] = notificationRequest.content.title; } NSMutableDictionary *ios = [[NSMutableDictionary alloc] init]; - if (localNotification.content.attachments) { + if (notificationRequest.content.attachments) { NSMutableArray *attachments = [[NSMutableArray alloc] init]; - for (UNNotificationAttachment *a in localNotification.content.attachments) { + for (UNNotificationAttachment *a in notificationRequest.content.attachments) { NSMutableDictionary *attachment = [[NSMutableDictionary alloc] init]; attachment[@"identifier"] = a.identifier; attachment[@"type"] = a.type; @@ -554,45 +620,25 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification ios[@"attachments"] = attachments; } - if (localNotification.content.badge) { - ios[@"badge"] = localNotification.content.badge; + if (notificationRequest.content.badge) { + ios[@"badge"] = notificationRequest.content.badge; } - if (localNotification.content.categoryIdentifier) { - ios[@"category"] = localNotification.content.categoryIdentifier; + if (notificationRequest.content.categoryIdentifier) { + ios[@"category"] = notificationRequest.content.categoryIdentifier; } - if (localNotification.content.launchImageName) { - ios[@"launchImage"] = localNotification.content.launchImageName; + if (notificationRequest.content.launchImageName) { + ios[@"launchImage"] = notificationRequest.content.launchImageName; } - if (localNotification.content.threadIdentifier) { - ios[@"threadIdentifier"] = localNotification.content.threadIdentifier; + if (notificationRequest.content.threadIdentifier) { + ios[@"threadIdentifier"] = notificationRequest.content.threadIdentifier; } notification[@"ios"] = ios; return notification; } - -- (NSDictionary*)parseUNNotificationResponse:(UNNotificationResponse *)response - messageType:(NSString *)messageType { - NSMutableDictionary *notificationResponse = [[NSMutableDictionary alloc] init]; - NSDictionary *notification = [self parseUNNotification:response.notification messageType:messageType]; - notificationResponse[@"notification"] = notification; - notificationResponse[@"action"] = response.actionIdentifier; - - return notificationResponse; -} - -- (NSDictionary*)parseUNNotification:(UNNotification *)notification - messageType:(NSString *)messageType { - NSDictionary *userInfo = notification.request.content.userInfo; - NSString *category = notification.request.content.categoryIdentifier; - return [self parseUserInfo:userInfo messageType:messageType category:category]; -} - -- (NSDictionary*)parseUserInfo:(NSDictionary *)userInfo - messageType:(NSString *) messageType - category:(NSString *) category { - +- (NSDictionary*)parseUserInfo:(NSDictionary *)userInfo { + NSMutableDictionary *notification = [[NSMutableDictionary alloc] init]; NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; NSMutableDictionary *ios = [[NSMutableDictionary alloc] init]; @@ -644,13 +690,6 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification data[k1] = userInfo[k1]; } } - - if (!ios[@"category"]) { - ios[@"category"] = category; - } - - // TODO: What to do with this? - // message[@"messageType"] = messageType; notification[@"data"] = data; notification[@"ios"] = ios; @@ -673,3 +712,4 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification @implementation RNFirebaseNotifications @end #endif + diff --git a/lib/modules/messaging/index.js b/lib/modules/messaging/index.js index f17d389a..5b658afd 100644 --- a/lib/modules/messaging/index.js +++ b/lib/modules/messaging/index.js @@ -125,7 +125,6 @@ export default class Messaging extends ModuleBase { }; } - // TODO: Permission structure? requestPermission(): Promise { return getNativeModule(this).requestPermission(); } @@ -133,16 +132,6 @@ export default class Messaging extends ModuleBase { /** * NON WEB-SDK METHODS */ - getBadge(): Promise { - return getNativeModule(this).getBadge(); - } - - getInitialMessage(): Promise { - return getNativeModule(this) - .getInitialMessage() - .then(message => (message ? new Message(this, message) : null)); - } - hasPermission(): Promise { return getNativeModule(this).hasPermission(); } @@ -156,10 +145,6 @@ export default class Messaging extends ModuleBase { return getNativeModule(this).send(remoteMessage.build()); } - setBadge(badge: number): void { - getNativeModule(this).setBadge(badge); - } - subscribeToTopic(topic: string): void { getNativeModule(this).subscribeToTopic(topic); } diff --git a/lib/modules/notifications/index.js b/lib/modules/notifications/index.js index 60e1dc61..688be880 100644 --- a/lib/modules/notifications/index.js +++ b/lib/modules/notifications/index.js @@ -143,6 +143,10 @@ export default class Notifications extends ModuleBase { return getNativeModule(this).displayNotification(notification.build()); } + getBadge(): Promise { + return getNativeModule(this).getBadge(); + } + getInitialNotification(): Promise { return getNativeModule(this).getInitialNotification(); // TODO @@ -264,6 +268,10 @@ export default class Notifications extends ModuleBase { nativeNotification.schedule = schedule; return getNativeModule(this).scheduleNotification(nativeNotification); } + + setBadge(badge: number): void { + getNativeModule(this).setBadge(badge); + } } export const statics = { From c5778c3d0dfeb68666abe8ff38802787760cdc52 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 23 Feb 2018 16:11:59 +0000 Subject: [PATCH 34/77] [notifications] JS tidy up --- .../messaging/RNFirebaseMessaging.m | 61 ------ .../notifications/RNFirebaseNotifications.m | 2 +- lib/index.js | 1 - lib/modules/messaging/Message.js | 108 ---------- lib/modules/messaging/RemoteMessage.js | 58 ++++-- lib/modules/messaging/index.js | 31 +-- lib/modules/messaging/types.js | 40 +--- .../notifications/AndroidNotification.js | 188 +++++++++++++++--- lib/modules/notifications/IOSNotification.js | 28 +++ lib/modules/notifications/Notification.js | 26 ++- lib/modules/notifications/types.js | 56 +++--- .../ios/ReactNativeFirebaseDemo/AppDelegate.m | 4 - 12 files changed, 307 insertions(+), 296 deletions(-) delete mode 100644 lib/modules/messaging/Message.js diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index 31365c80..4af1e1b3 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -14,10 +14,6 @@ #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 @import UserNotifications; #endif -@interface RNFirebaseMessaging () -@property (nonatomic, strong) NSMutableDictionary *callbackHandlers; -@end - @implementation RNFirebaseMessaging @@ -47,9 +43,6 @@ RCT_EXPORT_MODULE() // Set static instance for use from AppDelegate theRNFirebaseMessaging = self; - - // Initialise callback handlers dictionary - _callbackHandlers = [NSMutableDictionary dictionary]; } // ******************************************************* @@ -187,60 +180,6 @@ RCT_EXPORT_METHOD(unsubscribeFromTopic: (NSString*) topic) { [[FIRMessaging messaging] unsubscribeFromTopic:topic]; } -// Response handler methods - -RCT_EXPORT_METHOD(completeNotificationResponse: (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(completePresentNotification: (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(completeRemoteNotification: (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 { diff --git a/ios/RNFirebase/notifications/RNFirebaseNotifications.m b/ios/RNFirebase/notifications/RNFirebaseNotifications.m index beb733a0..fda16c66 100644 --- a/ios/RNFirebase/notifications/RNFirebaseNotifications.m +++ b/ios/RNFirebase/notifications/RNFirebaseNotifications.m @@ -378,7 +378,7 @@ RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { initialNotification = body; } // PRE-BRIDGE-EVENTS: Consider enabling this to allow events built up before the bridge is built to be sent to the JS side - [pendingEvents addObject:@{@"name":name, @"body":body}]; + // [pendingEvents addObject:@{@"name":name, @"body":body}]; } } diff --git a/lib/index.js b/lib/index.js index 8d7a7cc2..6cd42dce 100644 --- a/lib/index.js +++ b/lib/index.js @@ -68,7 +68,6 @@ export type { default as WriteBatch } from './modules/firestore/WriteBatch'; /* * Export Messaging types */ -export type { default as Message } from './modules/messaging/Message'; export type { default as RemoteMessage, } from './modules/messaging/RemoteMessage'; diff --git a/lib/modules/messaging/Message.js b/lib/modules/messaging/Message.js deleted file mode 100644 index 38f3433f..00000000 --- a/lib/modules/messaging/Message.js +++ /dev/null @@ -1,108 +0,0 @@ -/** - * @flow - * Message representation wrapper - */ -import { Platform } from 'react-native'; -import { getNativeModule } from '../../utils/native'; -import { - MessageType, - PresentNotificationResult, - RemoteNotificationResult, -} from './types'; -import type Messaging from './'; -import type { - NativeMessage, - PresentNotificationResultType, - RemoteNotificationResultType, -} from './types'; - -/** - * @class Message - */ -export default class Message { - _completed: 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 sentTime(): ?number { - return this._message.sentTime; - } - - get to(): ?string { - return this._message.to; - } - - get ttl(): ?number { - return this._message.ttl; - } - - complete( - result?: PresentNotificationResultType | RemoteNotificationResultType - ): void { - if (Platform.OS === 'android') { - return; - } - - if (!this._completed) { - this._completed = true; - - switch (this.messageType) { - case MessageType.NotificationResponse: - getNativeModule(this._messaging).completeNotificationResponse( - this.messageId - ); - break; - - case MessageType.PresentNotification: - if ( - result && - !Object.values(PresentNotificationResult).includes(result) - ) { - throw new Error(`Invalid PresentNotificationResult: ${result}`); - } - getNativeModule(this._messaging).completePresentNotification( - this.messageId, - result || PresentNotificationResult.None - ); - break; - - case MessageType.RemoteNotificationHandler: - if ( - result && - !Object.values(RemoteNotificationResult).includes(result) - ) { - throw new Error(`Invalid RemoteNotificationResult: ${result}`); - } - getNativeModule(this._messaging).completeRemoteNotification( - this.messageId, - result || RemoteNotificationResult.NoData - ); - break; - - default: - break; - } - } - } -} diff --git a/lib/modules/messaging/RemoteMessage.js b/lib/modules/messaging/RemoteMessage.js index 15a8493f..e99de6ca 100644 --- a/lib/modules/messaging/RemoteMessage.js +++ b/lib/modules/messaging/RemoteMessage.js @@ -4,30 +4,64 @@ */ import { isObject, generatePushID } from './../../utils'; -type NativeRemoteMessage = { - collapseKey?: string, - data: { [string]: string }, - messageId: string, - messageType?: string, - to: string, - ttl: number, -}; +import type { + NativeInboundRemoteMessage, + NativeOutboundRemoteMessage, +} from './types'; export default class RemoteMessage { _collapseKey: string | void; _data: { [string]: string }; + _from: string | void; _messageId: string; _messageType: string | void; + _sentTime: number | void; _to: string; _ttl: number; - constructor() { - this._data = {}; + constructor(inboundMessage?: NativeInboundRemoteMessage) { + if (inboundMessage) { + this._collapseKey = inboundMessage.collapseKey; + this._data = inboundMessage.data; + this._from = inboundMessage.from; + this._messageId = inboundMessage.messageId; + this._sentTime = inboundMessage.sentTime; + } + // defaults + this._data = this._data || {}; // TODO: Is this the best way to generate an ID? - this._messageId = generatePushID(); + this._messageId = this._messageId || generatePushID(); this._ttl = 3600; } + get collapseKey(): ?string { + return this._collapseKey; + } + + get data(): { [string]: string } { + return this._data; + } + + get from(): ?string { + return this._from; + } + + get messageId(): ?string { + return this._messageId; + } + + get sentTime(): ?number { + return this._sentTime; + } + + get to(): ?string { + return this._to; + } + + get ttl(): ?number { + return this._ttl; + } + /** * * @param collapseKey @@ -83,7 +117,7 @@ export default class RemoteMessage { return this; } - build(): NativeRemoteMessage { + build(): NativeOutboundRemoteMessage { if (!this.data) { throw new Error('RemoteMessage: Missing required `data` property'); } else if (!this.messageId) { diff --git a/lib/modules/messaging/index.js b/lib/modules/messaging/index.js index 5b658afd..ad46838b 100644 --- a/lib/modules/messaging/index.js +++ b/lib/modules/messaging/index.js @@ -8,18 +8,12 @@ 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 RemoteMessage from './RemoteMessage'; -import { - MessageType, - PresentNotificationResult, - RemoteNotificationResult, -} from './types'; import type App from '../core/app'; -import type { NativeMessage } from './types'; +import type { NativeInboundRemoteMessage } from './types'; -type OnMessage = Message => any; +type OnMessage = RemoteMessage => any; type OnMessageObserver = { next: OnMessage, @@ -55,8 +49,8 @@ export default class Messaging extends ModuleBase { // sub to internal native event - this fans out to // public event name: onMessage 'messaging_message_received', - (message: NativeMessage) => { - SharedEventEmitter.emit('onMessage', new Message(this, message)); + (message: NativeInboundRemoteMessage) => { + SharedEventEmitter.emit('onMessage', new RemoteMessage(message)); } ); @@ -75,7 +69,7 @@ export default class Messaging extends ModuleBase { } onMessage(nextOrObserver: OnMessage | OnMessageObserver): () => any { - let listener: Message => any; + let listener: RemoteMessage => any; if (isFunction(nextOrObserver)) { // $FlowBug: Not coping with the overloaded method signature listener = nextOrObserver; @@ -89,24 +83,20 @@ export default class Messaging extends ModuleBase { getLogger(this).info('Creating onMessage listener'); - const wrappedListener = async (message: Message) => { - await listener(message); - message.complete(); - }; - - SharedEventEmitter.addListener('onMessage', wrappedListener); + SharedEventEmitter.addListener('onMessage', listener); return () => { getLogger(this).info('Removing onMessage listener'); - SharedEventEmitter.removeListener('onMessage', wrappedListener); + SharedEventEmitter.removeListener('onMessage', listener); }; } onTokenRefresh( nextOrObserver: OnTokenRefresh | OnTokenRefreshObserver ): () => any { - let listener; + let listener: String => any; if (isFunction(nextOrObserver)) { + // $FlowBug: Not coping with the overloaded method signature listener = nextOrObserver; } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { listener = nextOrObserver.next; @@ -186,8 +176,5 @@ export default class Messaging extends ModuleBase { } export const statics = { - MessageType, - PresentNotificationResult, RemoteMessage, - RemoteNotificationResult, }; diff --git a/lib/modules/messaging/types.js b/lib/modules/messaging/types.js index 3b58201a..1d187e30 100644 --- a/lib/modules/messaging/types.js +++ b/lib/modules/messaging/types.js @@ -1,35 +1,6 @@ /** * @flow */ - -export const MessageType = { - InitialMessage: 'InitialMessage', - NotificationResponse: 'NotificationResponse', - PresentNotification: 'PresentNotification', - RemoteMessage: 'RemoteMessage', - RemoteNotification: 'RemoteNotification', - RemoteNotificationHandler: 'RemoteNotificationHandler', -}; - -export const PresentNotificationResult = { - All: 'UNNotificationPresentationOptionAll', - None: 'UNNotificationPresentationOptionNone', -}; - -export const RemoteNotificationResult = { - NewData: 'UIBackgroundFetchResultNewData', - NoData: 'UIBackgroundFetchResultNoData', - ResultFailed: 'UIBackgroundFetchResultFailed', -}; - -export type MessageTypeType = $Values; -export type PresentNotificationResultType = $Values< - typeof PresentNotificationResult ->; -export type RemoteNotificationResultType = $Values< - typeof RemoteNotificationResult ->; - export type Notification = { body: string, bodyLocalizationArgs?: string[], @@ -46,7 +17,7 @@ export type Notification = { titleLocalizationKey?: string, }; -export type NativeMessage = { +export type NativeInboundRemoteMessage = { collapseKey?: string, data: { [string]: string }, from?: string, @@ -55,3 +26,12 @@ export type NativeMessage = { to?: string, ttl?: number, }; + +export type NativeOutboundRemoteMessage = { + collapseKey?: string, + data: { [string]: string }, + messageId: string, + messageType?: string, + to: string, + ttl: number, +}; diff --git a/lib/modules/notifications/AndroidNotification.js b/lib/modules/notifications/AndroidNotification.js index c3305e0b..2b33d4d2 100644 --- a/lib/modules/notifications/AndroidNotification.js +++ b/lib/modules/notifications/AndroidNotification.js @@ -20,41 +20,41 @@ import type { export default class AndroidNotification { // TODO optional fields // TODO actions: Action[]; // icon, title, ??pendingIntent??, allowGeneratedReplies, extender, extras, remoteinput (ugh) - _autoCancel: boolean; - _badgeIconType: BadgeIconTypeType; - _category: CategoryType; + _autoCancel: boolean | void; + _badgeIconType: BadgeIconTypeType | void; + _category: CategoryType | void; _channelId: string; _clickAction: string | void; - _color: string; - _colorized: boolean; - _contentInfo: string; - _defaults: DefaultsType[]; - _group: string; - _groupAlertBehaviour: GroupAlertType; - _groupSummary: boolean; - _largeIcon: string; - _lights: Lights; - _localOnly: boolean; + _color: string | void; + _colorized: boolean | void; + _contentInfo: string | void; + _defaults: DefaultsType[] | void; + _group: string | void; + _groupAlertBehaviour: GroupAlertType | void; + _groupSummary: boolean | void; + _largeIcon: string | void; + _lights: Lights | void; + _localOnly: boolean | void; _notification: Notification; - _number: number; - _ongoing: boolean; - _onlyAlertOnce: boolean; + _number: number | void; + _ongoing: boolean | void; + _onlyAlertOnce: boolean | void; _people: string[]; - _priority: PriorityType; - _progress: Progress; + _priority: PriorityType | void; + _progress: Progress | void; // _publicVersion: Notification; - _remoteInputHistory: string[]; - _shortcutId: string; - _showWhen: boolean; + _remoteInputHistory: string[] | void; + _shortcutId: string | void; + _showWhen: boolean | void; _smallIcon: SmallIcon; - _sortKey: string; + _sortKey: string | void; // TODO: style: Style; // Need to figure out if this can work - _ticker: string; - _timeoutAfter: number; - _usesChronometer: boolean; - _vibrate: number[]; - _visibility: VisibilityType; - _when: number; + _ticker: string | void; + _timeoutAfter: number | void; + _usesChronometer: boolean | void; + _vibrate: number[] | void; + _visibility: VisibilityType | void; + _when: number | void; // android unsupported // content: RemoteViews @@ -112,6 +112,134 @@ export default class AndroidNotification { }; } + get autoCancel(): ?boolean { + return this._autoCancel; + } + + get badgeIconType(): ?BadgeIconTypeType { + return this._badgeIconType; + } + + get category(): ?CategoryType { + return this._category; + } + + get channelId(): string { + return this._channelId; + } + + get clickAction(): ?string { + return this._clickAction; + } + + get color(): ?string { + return this._color; + } + + get colorized(): ?boolean { + return this._colorized; + } + + get contentInfo(): ?string { + return this._contentInfo; + } + + get defaults(): ?(DefaultsType[]) { + return this._defaults; + } + + get group(): ?string { + return this._group; + } + + get groupAlertBehaviour(): ?GroupAlertType { + return this._groupAlertBehaviour; + } + + get groupSummary(): ?boolean { + return this._groupSummary; + } + + get largeIcon(): ?string { + return this._largeIcon; + } + + get lights(): ?Lights { + return this._lights; + } + + get localOnly(): ?boolean { + return this._localOnly; + } + + get number(): ?number { + return this._number; + } + + get ongoing(): ?boolean { + return this._ongoing; + } + + get onlyAlertOnce(): ?boolean { + return this._onlyAlertOnce; + } + + get people(): string[] { + return this._people; + } + + get priority(): ?PriorityType { + return this._priority; + } + + get progress(): ?Progress { + return this._progress; + } + + get remoteInputHistory(): ?(string[]) { + return this._remoteInputHistory; + } + + get shortcutId(): ?string { + return this._shortcutId; + } + + get showWhen(): ?boolean { + return this._showWhen; + } + + get smallIcon(): SmallIcon { + return this._smallIcon; + } + + get sortKey(): ?string { + return this._sortKey; + } + + get ticker(): ?string { + return this._ticker; + } + + get timeoutAfter(): ?number { + return this._timeoutAfter; + } + + get usesChronometer(): ?boolean { + return this._usesChronometer; + } + + get vibrate(): ?(number[]) { + return this._vibrate; + } + + get visibility(): ?VisibilityType { + return this._visibility; + } + + get when(): ?number { + return this._when; + } + /** * * @param person @@ -451,6 +579,10 @@ export default class AndroidNotification { throw new Error( 'AndroidNotification: Missing required `channelId` property' ); + } else if (!this._smallIcon) { + throw new Error( + 'AndroidNotification: Missing required `smallIcon` property' + ); } return { diff --git a/lib/modules/notifications/IOSNotification.js b/lib/modules/notifications/IOSNotification.js index 5b1e563b..b78e97bd 100644 --- a/lib/modules/notifications/IOSNotification.js +++ b/lib/modules/notifications/IOSNotification.js @@ -36,6 +36,34 @@ export default class IOSNotification { this._attachments = this._attachments || []; } + get alertAction(): ?string { + return this._alertAction; + } + + get attachments(): Attachment[] { + return this._attachments; + } + + get badge(): ?number { + return this._badge; + } + + get category(): ?string { + return this._category; + } + + get hasAction(): ?boolean { + return this._hasAction; + } + + get launchImage(): ?string { + return this._launchImage; + } + + get threadIdentifier(): ?string { + return this._threadIdentifier; + } + /** * * @param identifier diff --git a/lib/modules/notifications/Notification.js b/lib/modules/notifications/Notification.js index 1ad7d2cd..734c4441 100644 --- a/lib/modules/notifications/Notification.js +++ b/lib/modules/notifications/Notification.js @@ -32,7 +32,6 @@ export default class Notification { if (data) { this._body = data.body; this._data = data.data; - // TODO: Is this the best way to generate an ID? this._notificationId = data.notificationId; this._sound = data.sound; this._subtitle = data.subtitle; @@ -41,6 +40,7 @@ export default class Notification { // Defaults this._data = this._data || {}; + // TODO: Is this the best way to generate an ID? this._notificationId = this._notificationId || generatePushID(); } @@ -48,10 +48,34 @@ export default class Notification { return this._android; } + get body(): string { + return this._body; + } + + get data(): { [string]: string } { + return this._data; + } + get ios(): IOSNotification { return this._ios; } + get notificationId(): string { + return this._notificationId; + } + + get sound(): ?string { + return this._sound; + } + + get subtitle(): ?string { + return this._subtitle; + } + + get title(): string { + return this._title; + } + /** * * @param body diff --git a/lib/modules/notifications/types.js b/lib/modules/notifications/types.js index 9c2abe10..22760c16 100644 --- a/lib/modules/notifications/types.js +++ b/lib/modules/notifications/types.js @@ -79,40 +79,40 @@ export type SmallIcon = { export type NativeAndroidNotification = {| // TODO actions: Action[], - autoCancel: boolean, - badgeIconType: BadgeIconTypeType, - category: CategoryType, + autoCancel?: boolean, + badgeIconType?: BadgeIconTypeType, + category?: CategoryType, channelId: string, clickAction?: string, - color: string, - colorized: boolean, - contentInfo: string, - defaults: DefaultsType[], - group: string, - groupAlertBehaviour: GroupAlertType, - groupSummary: boolean, - largeIcon: string, - lights: Lights, - localOnly: boolean, - number: number, - ongoing: boolean, - onlyAlertOnce: boolean, + color?: string, + colorized?: boolean, + contentInfo?: string, + defaults?: DefaultsType[], + group?: string, + groupAlertBehaviour?: GroupAlertType, + groupSummary?: boolean, + largeIcon?: string, + lights?: Lights, + localOnly?: boolean, + number?: number, + ongoing?: boolean, + onlyAlertOnce?: boolean, people: string[], - priority: PriorityType, - progress: Progress, + priority?: PriorityType, + progress?: Progress, // publicVersion: Notification, - remoteInputHistory: string[], - shortcutId: string, - showWhen: boolean, + remoteInputHistory?: string[], + shortcutId?: string, + showWhen?: boolean, smallIcon: SmallIcon, - sortKey: string, + sortKey?: string, // TODO: style: Style, - ticker: string, - timeoutAfter: number, - usesChronometer: boolean, - vibrate: number[], - visibility: VisibilityType, - when: number, + ticker?: string, + timeoutAfter?: number, + usesChronometer?: boolean, + vibrate?: number[], + visibility?: VisibilityType, + when?: number, |}; export type AttachmentOptions = {| diff --git a/tests/ios/ReactNativeFirebaseDemo/AppDelegate.m b/tests/ios/ReactNativeFirebaseDemo/AppDelegate.m index 88cfe550..81105040 100644 --- a/tests/ios/ReactNativeFirebaseDemo/AppDelegate.m +++ b/tests/ios/ReactNativeFirebaseDemo/AppDelegate.m @@ -38,10 +38,6 @@ return YES; } -- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo { - [[RNFirebaseNotifications instance] didReceiveRemoteNotification:userInfo]; -} - - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { [[RNFirebaseNotifications instance] didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; From 28b6d2b69cc3e25cbe1c5bb0e685ad41cc5b9259 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 23 Feb 2018 16:14:19 +0000 Subject: [PATCH 35/77] [messaging] Correctly resolve the promise after sending a message --- ios/RNFirebase/messaging/RNFirebaseMessaging.m | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index 4af1e1b3..5b92cdb1 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -170,6 +170,9 @@ RCT_EXPORT_METHOD(sendMessage: (NSDictionary *) message NSDictionary *data = message[@"data"]; [[FIRMessaging messaging] sendMessage:data to:to withMessageID:messageId timeToLive:[ttl intValue]]; + + // TODO: Listen for send success / errors + resolve(nil); } RCT_EXPORT_METHOD(subscribeToTopic: (NSString*) topic) { From 07bc258c0857a4965c23fbb29620ea44d42b3eaf Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 23 Feb 2018 16:36:59 +0000 Subject: [PATCH 36/77] [messaging] Add missing setTo method --- lib/modules/messaging/RemoteMessage.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/modules/messaging/RemoteMessage.js b/lib/modules/messaging/RemoteMessage.js index e99de6ca..2ed54228 100644 --- a/lib/modules/messaging/RemoteMessage.js +++ b/lib/modules/messaging/RemoteMessage.js @@ -107,6 +107,16 @@ export default class RemoteMessage { return this; } + /** + * + * @param to + * @returns {RemoteMessage} + */ + setTo(to: string): RemoteMessage { + this._to = to; + return this; + } + /** * * @param ttl From ee3b2932efe1f64606933c06f1c4e67026ebb892 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Sat, 24 Feb 2018 11:32:14 +0000 Subject: [PATCH 37/77] [notifications] Rename `onPressed` to `onOpened` --- ios/RNFirebase/RNFirebaseEvents.h | 2 +- .../notifications/RNFirebaseNotifications.m | 87 +++++++++---------- lib/modules/notifications/Notification.js | 2 +- lib/modules/notifications/index.js | 68 +++++++-------- lib/modules/notifications/types.js | 2 +- 5 files changed, 77 insertions(+), 84 deletions(-) diff --git a/ios/RNFirebase/RNFirebaseEvents.h b/ios/RNFirebase/RNFirebaseEvents.h index 938a8445..bf7dd021 100644 --- a/ios/RNFirebase/RNFirebaseEvents.h +++ b/ios/RNFirebase/RNFirebaseEvents.h @@ -40,7 +40,7 @@ static NSString *const MESSAGING_NOTIFICATION_RECEIVED = @"messaging_notificatio // Notifications static NSString *const NOTIFICATIONS_NOTIFICATION_DISPLAYED = @"notifications_notification_displayed"; -static NSString *const NOTIFICATIONS_NOTIFICATION_PRESSED = @"notifications_notification_pressed"; +static NSString *const NOTIFICATIONS_NOTIFICATION_OPENED = @"notifications_notification_opened"; static NSString *const NOTIFICATIONS_NOTIFICATION_RECEIVED = @"notifications_notification_received"; // AdMob diff --git a/ios/RNFirebase/notifications/RNFirebaseNotifications.m b/ios/RNFirebase/notifications/RNFirebaseNotifications.m index fda16c66..1fe8a342 100644 --- a/ios/RNFirebase/notifications/RNFirebaseNotifications.m +++ b/ios/RNFirebase/notifications/RNFirebaseNotifications.m @@ -49,7 +49,7 @@ RCT_EXPORT_MODULE(); #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 [UNUserNotificationCenter currentNotificationCenter].delegate = self; #endif - + // Set static instance for use from AppDelegate theRNFirebaseNotifications = self; } @@ -75,18 +75,15 @@ RCT_EXPORT_MODULE(); if ([self isIOS89]) { NSString *event; if (RCTSharedApplication().applicationState == UIApplicationStateBackground) { - // notification_displayed event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; } else if (RCTSharedApplication().applicationState == UIApplicationStateInactive) { - // notification_displayed - event = NOTIFICATIONS_NOTIFICATION_PRESSED; + event = NOTIFICATIONS_NOTIFICATION_OPENED; } else { - // notification_received event = NOTIFICATIONS_NOTIFICATION_RECEIVED; } NSDictionary *notification = [self parseUILocalNotification:localNotification]; - if (event == NOTIFICATIONS_NOTIFICATION_PRESSED) { + if (event == NOTIFICATIONS_NOTIFICATION_OPENED) { notification = @{ @"action": UNNotificationDefaultActionIdentifier, @"notification": notification @@ -105,17 +102,14 @@ RCT_EXPORT_MODULE(); [[RNFirebaseMessaging instance] didReceiveRemoteNotification:userInfo]; return; } - + NSString *event; if (RCTSharedApplication().applicationState == UIApplicationStateBackground) { - // notification_displayed event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; } else if ([self isIOS89]) { if (RCTSharedApplication().applicationState == UIApplicationStateInactive) { - // notification_displayed - event = NOTIFICATIONS_NOTIFICATION_PRESSED; + event = NOTIFICATIONS_NOTIFICATION_OPENED; } else { - // notification_received event = NOTIFICATIONS_NOTIFICATION_RECEIVED; } } else { @@ -127,8 +121,8 @@ RCT_EXPORT_MODULE(); } NSDictionary *notification = [self parseUserInfo:userInfo]; - // For onPressed events, we set the default action name as iOS 8/9 has no concept of actions - if (event == NOTIFICATIONS_NOTIFICATION_PRESSED) { + // For onOpened events, we set the default action name as iOS 8/9 has no concept of actions + if (event == NOTIFICATIONS_NOTIFICATION_OPENED) { notification = @{ @"action": UNNotificationDefaultActionIdentifier, @"notification": notification @@ -153,15 +147,15 @@ RCT_EXPORT_MODULE(); - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler { - + UNNotificationTrigger *trigger = notification.request.trigger; BOOL isFcm = trigger && [notification.request.trigger class] == [UNPushNotificationTrigger class]; BOOL isScheduled = trigger && [notification.request.trigger class] == [UNCalendarNotificationTrigger class]; - + NSString *event; UNNotificationPresentationOptions options; NSDictionary *message = [self parseUNNotification:notification]; - + if (isFcm || isScheduled) { // If app is in the background if (RCTSharedApplication().applicationState == UIApplicationStateBackground @@ -183,7 +177,7 @@ RCT_EXPORT_MODULE(); // notification_displayed event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; } - + [self sendJSEvent:self name:event body:message]; completionHandler(options); } @@ -197,8 +191,8 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler { #endif NSDictionary *message = [self parseUNNotificationResponse:response]; - - [self sendJSEvent:self name:NOTIFICATIONS_NOTIFICATION_PRESSED body:message]; + + [self sendJSEvent:self name:NOTIFICATIONS_NOTIFICATION_OPENED body:message]; completionHandler(); } @@ -259,7 +253,7 @@ RCT_EXPORT_METHOD(displayNotification:(NSDictionary*) notification #endif } } - + RCT_EXPORT_METHOD(getBadge: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { dispatch_async(dispatch_get_main_queue(), ^{ resolve(@([RCTSharedApplication() applicationIconBadgeNumber])); @@ -360,13 +354,13 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification #endif } } - + RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { dispatch_async(dispatch_get_main_queue(), ^{ [RCTSharedApplication() setApplicationIconBadgeNumber:number]; }); } - + // Because of the time delay between the app starting and the bridge being initialised // we create a temporary instance of RNFirebaseNotifications. // With this temporary instance, we cache any events to be sent as soon as the bridge is set on the module @@ -374,14 +368,14 @@ RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { if (emitter.bridge) { [RNFirebaseUtil sendJSEvent:emitter name:name body:body]; } else { - if ([name isEqualToString:NOTIFICATIONS_NOTIFICATION_PRESSED] && !initialNotification) { + if ([name isEqualToString:NOTIFICATIONS_NOTIFICATION_OPENED] && !initialNotification) { initialNotification = body; } // PRE-BRIDGE-EVENTS: Consider enabling this to allow events built up before the bridge is built to be sent to the JS side // [pendingEvents addObject:@{@"name":name, @"body":body}]; } } - + - (BOOL)isIOS89 { return floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max; } @@ -425,7 +419,7 @@ RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { NSNumber *fireDateNumber = schedule[@"fireDate"]; NSDate *fireDate = [NSDate dateWithTimeIntervalSince1970:([fireDateNumber doubleValue] / 1000.0)]; localNotification.fireDate = fireDate; - + NSString *interval = schedule[@"repeatInterval"]; if (interval) { if ([interval isEqualToString:@"minute"]) { @@ -438,9 +432,9 @@ RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { localNotification.repeatInterval = NSCalendarUnitWeekday; } } - + } - + return localNotification; } @@ -470,7 +464,7 @@ RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { NSString *identifier = a[@"identifier"]; NSURL *url = [NSURL URLWithString:a[@"url"]]; NSDictionary *options = a[@"options"]; - + NSError *error; UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:identifier URL:url options:options error:&error]; if (attachment) { @@ -495,13 +489,13 @@ RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { content.threadIdentifier = ios[@"threadIdentifier"]; } } - + if (withSchedule) { NSDictionary *schedule = notification[@"schedule"]; NSNumber *fireDateNumber = schedule[@"fireDate"]; NSString *interval = schedule[@"repeatInterval"]; NSDate *fireDate = [NSDate dateWithTimeIntervalSince1970:([fireDateNumber doubleValue] / 1000.0)]; - + NSCalendarUnit calendarUnit; if (interval) { if ([interval isEqualToString:@"minute"]) { @@ -517,7 +511,7 @@ RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { // Needs to match exactly to the secpmd calendarUnit = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond; } - + NSDateComponents *components = [[NSCalendar currentCalendar] components:calendarUnit fromDate:fireDate]; UNCalendarNotificationTrigger *trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:interval]; return [UNNotificationRequest requestWithIdentifier:notification[@"notificationId"] content:content trigger:trigger]; @@ -528,7 +522,7 @@ RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { - (NSDictionary*) parseUILocalNotification:(UILocalNotification *) localNotification { NSMutableDictionary *notification = [[NSMutableDictionary alloc] init]; - + if (localNotification.alertBody) { notification[@"body"] = localNotification.alertBody; } @@ -541,7 +535,7 @@ RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { if (localNotification.alertTitle) { notification[@"title"] = localNotification.alertTitle; } - + NSMutableDictionary *ios = [[NSMutableDictionary alloc] init]; if (localNotification.alertAction) { ios[@"alertAction"] = localNotification.alertAction; @@ -559,28 +553,28 @@ RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { ios[@"launchImage"] = localNotification.alertLaunchImage; } notification[@"ios"] = ios; - + return notification; } - + - (NSDictionary*)parseUNNotificationResponse:(UNNotificationResponse *)response { NSMutableDictionary *notificationResponse = [[NSMutableDictionary alloc] init]; NSDictionary *notification = [self parseUNNotification:response.notification]; notificationResponse[@"notification"] = notification; notificationResponse[@"action"] = response.actionIdentifier; - + return notificationResponse; } - + - (NSDictionary*)parseUNNotification:(UNNotification *)notification { return [self parseUNNotificationRequest:notification.request]; } - + - (NSDictionary*) parseUNNotificationRequest:(UNNotificationRequest *) notificationRequest { NSMutableDictionary *notification = [[NSMutableDictionary alloc] init]; - + notification[@"notificationId"] = notificationRequest.identifier; - + if (notificationRequest.content.body) { notification[@"body"] = notificationRequest.content.body; } @@ -605,9 +599,9 @@ RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { if (notificationRequest.content.title) { notification[@"title"] = notificationRequest.content.title; } - + NSMutableDictionary *ios = [[NSMutableDictionary alloc] init]; - + if (notificationRequest.content.attachments) { NSMutableArray *attachments = [[NSMutableArray alloc] init]; for (UNNotificationAttachment *a in notificationRequest.content.attachments) { @@ -619,7 +613,7 @@ RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { } ios[@"attachments"] = attachments; } - + if (notificationRequest.content.badge) { ios[@"badge"] = notificationRequest.content.badge; } @@ -633,12 +627,12 @@ RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { ios[@"threadIdentifier"] = notificationRequest.content.threadIdentifier; } notification[@"ios"] = ios; - + return notification; } - (NSDictionary*)parseUserInfo:(NSDictionary *)userInfo { - + NSMutableDictionary *notification = [[NSMutableDictionary alloc] init]; NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; NSMutableDictionary *ios = [[NSMutableDictionary alloc] init]; @@ -698,7 +692,7 @@ RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { } - (NSArray *)supportedEvents { - return @[NOTIFICATIONS_NOTIFICATION_DISPLAYED, NOTIFICATIONS_NOTIFICATION_PRESSED, NOTIFICATIONS_NOTIFICATION_RECEIVED]; + return @[NOTIFICATIONS_NOTIFICATION_DISPLAYED, NOTIFICATIONS_NOTIFICATION_OPENED, NOTIFICATIONS_NOTIFICATION_RECEIVED]; } + (BOOL)requiresMainQueueSetup @@ -712,4 +706,3 @@ RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { @implementation RNFirebaseNotifications @end #endif - diff --git a/lib/modules/notifications/Notification.js b/lib/modules/notifications/Notification.js index 734c4441..ad38e9e1 100644 --- a/lib/modules/notifications/Notification.js +++ b/lib/modules/notifications/Notification.js @@ -9,7 +9,7 @@ import { generatePushID, isObject } from '../../utils'; import type { NativeNotification } from './types'; -export type NotificationPressed = { +export type NotificationOpened = { action: string, notification: Notification, }; diff --git a/lib/modules/notifications/index.js b/lib/modules/notifications/index.js index 688be880..f116d4f6 100644 --- a/lib/modules/notifications/index.js +++ b/lib/modules/notifications/index.js @@ -18,10 +18,10 @@ import { } from './types'; import type App from '../core/app'; -import type { NotificationPressed } from './Notification'; +import type { NotificationOpened } from './Notification'; import type { NativeNotification, - NativeNotificationPressed, + NativeNotificationOpened, Schedule, } from './types'; @@ -31,15 +31,15 @@ type OnNotificationObserver = { next: OnNotification, }; -type OnNotificationPressed = NotificationPressed => any; +type OnNotificationOpened = NotificationOpened => any; -type OnNotificationPressedObserver = { - next: OnNotificationPressed, +type OnNotificationOpenedObserver = { + next: OnNotificationOpened, }; const NATIVE_EVENTS = [ 'notifications_notification_displayed', - 'notifications_notification_pressed', + 'notifications_notification_opened', 'notifications_notification_received', ]; @@ -86,12 +86,12 @@ export default class Notifications extends ModuleBase { SharedEventEmitter.addListener( // sub to internal native event - this fans out to - // public event name: onNotificationPressed - 'notifications_notification_pressed', - (notificationPressed: NativeNotificationPressed) => { - SharedEventEmitter.emit('onNotificationPressed', { - action: notificationPressed.action, - notification: new Notification(notificationPressed.notification), + // public event name: onNotificationOpened + 'notifications_notification_opened', + (notificationOpened: NativeNotificationOpened) => { + SharedEventEmitter.emit('OnNotificationOpened', { + action: notificationOpened.action, + notification: new Notification(notificationOpened.notification), }); } ); @@ -111,22 +111,22 @@ export default class Notifications extends ModuleBase { /** * Cancel all notifications - * @returns {*} */ - cancelAllNotifications(): Promise { - return getNativeModule(this).cancelAllNotifications(); + cancelAllNotifications(): void { + getNativeModule(this).cancelAllNotifications(); } /** * Cancel a notification by id. - * @param id - * @returns {*} + * @param notificationId */ - cancelNotification(notificationId: string): Promise { + cancelNotification(notificationId: string): void { if (!notificationId) { - return Promise.reject(new Error('Missing notificationId')); + throw new Error( + 'Notifications: cancelNotification expects a `notificationId`' + ); } - return getNativeModule(this).cancelNotification(notificationId); + getNativeModule(this).cancelNotification(notificationId); } /** @@ -207,8 +207,8 @@ export default class Notifications extends ModuleBase { }; } - onNotificationPressed( - nextOrObserver: OnNotificationPressed | OnNotificationPressedObserver + onNotificationOpened( + nextOrObserver: OnNotificationOpened | OnNotificationOpenedObserver ): () => any { let listener; if (isFunction(nextOrObserver)) { @@ -217,37 +217,37 @@ export default class Notifications extends ModuleBase { listener = nextOrObserver.next; } else { throw new Error( - 'Notifications.onNotificationPressed failed: First argument must be a function or observer object with a `next` function.' + 'Notifications.onNotificationOpened failed: First argument must be a function or observer object with a `next` function.' ); } - getLogger(this).info('Creating onNotificationPressed listener'); - SharedEventEmitter.addListener('onNotificationPressed', listener); + getLogger(this).info('Creating onNotificationOpened listener'); + SharedEventEmitter.addListener('onNotificationOpened', listener); return () => { - getLogger(this).info('Removing onNotificationPressed listener'); - SharedEventEmitter.removeListener('onNotificationPressed', listener); + getLogger(this).info('Removing onNotificationOpened listener'); + SharedEventEmitter.removeListener('onNotificationOpened', listener); }; } /** * Remove all delivered notifications. - * @returns {*} */ - removeAllDeliveredNotifications(): Promise { - return getNativeModule(this).removeAllDeliveredNotifications(); + removeAllDeliveredNotifications(): void { + getNativeModule(this).removeAllDeliveredNotifications(); } /** * Remove a delivered notification. * @param notificationId - * @returns {*} */ - removeDeliveredNotification(notificationId: string): Promise { + removeDeliveredNotification(notificationId: string): void { if (!notificationId) { - return Promise.reject(new Error('Missing notificationId')); + throw new Error( + 'Notifications: removeDeliveredNotification expects a `notificationId`' + ); } - return getNativeModule(this).removeDeliveredNotification(notificationId); + getNativeModule(this).removeDeliveredNotification(notificationId); } /** diff --git a/lib/modules/notifications/types.js b/lib/modules/notifications/types.js index 22760c16..f939d869 100644 --- a/lib/modules/notifications/types.js +++ b/lib/modules/notifications/types.js @@ -161,7 +161,7 @@ export type NativeNotification = {| title: string, |}; -export type NativeNotificationPressed = {| +export type NativeNotificationOpened = {| action: string, notification: NativeNotification, |}; From 7acace4ce68558411b0149ea51d2b29ad5ed74af Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 1 Mar 2018 08:39:24 +0000 Subject: [PATCH 38/77] [notifications][ios] Properly define attachment options --- .../notifications/RNFirebaseNotifications.m | 21 +++++++++++++++++-- lib/modules/notifications/IOSNotification.js | 10 ++++----- lib/modules/notifications/types.js | 16 +++++++------- lib/utils/apps.js | 12 +++++------ 4 files changed, 38 insertions(+), 21 deletions(-) diff --git a/ios/RNFirebase/notifications/RNFirebaseNotifications.m b/ios/RNFirebase/notifications/RNFirebaseNotifications.m index 1fe8a342..985bf812 100644 --- a/ios/RNFirebase/notifications/RNFirebaseNotifications.m +++ b/ios/RNFirebase/notifications/RNFirebaseNotifications.m @@ -463,10 +463,27 @@ RCT_EXPORT_METHOD(setBadge: (NSInteger) number) { for (NSDictionary *a in ios[@"attachments"]) { NSString *identifier = a[@"identifier"]; NSURL *url = [NSURL URLWithString:a[@"url"]]; - NSDictionary *options = a[@"options"]; + NSMutableDictionary *attachmentOptions = nil; + + if (a[@"options"]) { + NSDictionary *options = a[@"options"]; + attachmentOptions = [[NSMutableDictionary alloc] init]; + + for (id key in options) { + if ([key isEqualToString:@"typeHint"]) { + attachmentOptions[UNNotificationAttachmentOptionsTypeHintKey] = options[key]; + } else if ([key isEqualToString:@"thumbnailHidden"]) { + attachmentOptions[UNNotificationAttachmentOptionsThumbnailHiddenKey] = options[key]; + } else if ([key isEqualToString:@"thumbnailClippingRect"]) { + attachmentOptions[UNNotificationAttachmentOptionsThumbnailClippingRectKey] = options[key]; + } else if ([key isEqualToString:@"thumbnailTime"]) { + attachmentOptions[UNNotificationAttachmentOptionsThumbnailTimeKey] = options[key]; + } + } + } NSError *error; - UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:identifier URL:url options:options error:&error]; + UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:identifier URL:url options:attachmentOptions error:&error]; if (attachment) { [attachments addObject:attachment]; } else { diff --git a/lib/modules/notifications/IOSNotification.js b/lib/modules/notifications/IOSNotification.js index b78e97bd..cd1acaae 100644 --- a/lib/modules/notifications/IOSNotification.js +++ b/lib/modules/notifications/IOSNotification.js @@ -4,14 +4,14 @@ */ import type Notification from './Notification'; import type { - Attachment, - AttachmentOptions, + IOSAttachment, + IOSAttachmentOptions, NativeIOSNotification, } from './types'; export default class IOSNotification { _alertAction: string | void; // alertAction | N/A - _attachments: Attachment[]; // N/A | attachments + _attachments: IOSAttachment[]; // N/A | attachments _badge: number | void; // applicationIconBadgeNumber | badge _category: string | void; _hasAction: boolean | void; // hasAction | N/A @@ -40,7 +40,7 @@ export default class IOSNotification { return this._alertAction; } - get attachments(): Attachment[] { + get attachments(): IOSAttachment[] { return this._attachments; } @@ -74,7 +74,7 @@ export default class IOSNotification { addAttachment( identifier: string, url: string, - options?: AttachmentOptions + options?: IOSAttachmentOptions ): Notification { this._attachments.push({ identifier, diff --git a/lib/modules/notifications/types.js b/lib/modules/notifications/types.js index f939d869..87d4f806 100644 --- a/lib/modules/notifications/types.js +++ b/lib/modules/notifications/types.js @@ -115,27 +115,27 @@ export type NativeAndroidNotification = {| when?: number, |}; -export type AttachmentOptions = {| - TypeHint: string, - ThumbnailHidden: boolean, - ThumbnailClippingRect: { +export type IOSAttachmentOptions = {| + typeHint: string, + thumbnailHidden: boolean, + thumbnailClippingRect: { height: number, width: number, x: number, y: number, }, - ThumbnailTime: number, + thumbnailTime: number, |}; -export type Attachment = {| +export type IOSAttachment = {| identifier: string, - options?: AttachmentOptions, + options?: IOSAttachmentOptions, url: string, |}; export type NativeIOSNotification = {| alertAction?: string, - attachments: Attachment[], + attachments: IOSAttachment[], badge?: number, category?: string, hasAction?: boolean, diff --git a/lib/utils/apps.js b/lib/utils/apps.js index 253265e2..c61d7415 100644 --- a/lib/utils/apps.js +++ b/lib/utils/apps.js @@ -18,7 +18,7 @@ import type { const FirebaseCoreModule = NativeModules.RNFirebase; const APPS: { [string]: App } = {}; -const APP_MODULES: { [App]: { [string]: FirebaseModule } } = {}; +const APP_MODULES: { [string]: { [string]: FirebaseModule } } = {}; const DEFAULT_APP_NAME = '[DEFAULT]'; export default { @@ -49,8 +49,8 @@ export default { InstanceClass: Class ): () => FirebaseModule { return (): M => { - if (!APP_MODULES[app]) { - APP_MODULES[app] = {}; + if (!APP_MODULES[app._name]) { + APP_MODULES[app._name] = {}; } if ( @@ -62,11 +62,11 @@ export default { app.utils().checkPlayServicesAvailability(); } - if (!APP_MODULES[app][namespace]) { - APP_MODULES[app][namespace] = new InstanceClass(app, app.options); + if (!APP_MODULES[app._name][namespace]) { + APP_MODULES[app._name][namespace] = new InstanceClass(app, app.options); } - return APP_MODULES[app][namespace]; + return APP_MODULES[app._name][namespace]; }; }, From b9df2584029fcfd4a54bf2aba27ac84f92e49a7e Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Mon, 5 Mar 2018 08:28:13 +0000 Subject: [PATCH 39/77] [notifications] Android channel and channel group support --- .../RNFirebaseLocalMessagingHelper.java | 49 ---- .../messaging/RNFirebaseMessaging.java | 160 +--------- .../messaging/RNFirebaseMessagingPackage.java | 1 - .../messaging/RNFirebaseMessagingService.java | 17 +- .../RNFirebaseNotificationManager.java | 274 ++++++++++++------ .../RNFirebaseNotifications.java | 191 +++++++++++- lib/modules/messaging/RemoteMessage.js | 5 + lib/modules/messaging/types.js | 1 + lib/modules/notifications/AndroidChannel.js | 213 ++++++++++++++ .../notifications/AndroidChannelGroup.js | 57 ++++ .../notifications/AndroidNotifications.js | 94 ++++++ lib/modules/notifications/index.js | 26 +- lib/modules/notifications/types.js | 11 + .../MainApplication.java | 2 + tests/src/firebase.js | 50 +++- 15 files changed, 840 insertions(+), 311 deletions(-) delete mode 100644 android/src/main/java/io/invertase/firebase/messaging/RNFirebaseLocalMessagingHelper.java create mode 100644 lib/modules/notifications/AndroidChannel.js create mode 100644 lib/modules/notifications/AndroidChannelGroup.js create mode 100644 lib/modules/notifications/AndroidNotifications.js diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseLocalMessagingHelper.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseLocalMessagingHelper.java deleted file mode 100644 index 366e1923..00000000 --- a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseLocalMessagingHelper.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.invertase.firebase.messaging; - -import android.app.AlarmManager; -import android.app.Application; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Color; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.support.v4.app.NotificationCompat; -import android.util.Log; -import android.content.SharedPreferences; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.net.HttpURLConnection; - -public class RNFirebaseLocalMessagingHelper { - private static final long DEFAULT_VIBRATION = 300L; - private static final String TAG = RNFirebaseLocalMessagingHelper.class.getSimpleName(); - private final static String PREFERENCES_KEY = "ReactNativeSystemNotification"; - private static boolean mIsForeground = false; //this is a hack - - private Context mContext; - private SharedPreferences sharedPreferences = null; - - public RNFirebaseLocalMessagingHelper(Application context) { - mContext = context; - sharedPreferences = mContext.getSharedPreferences(PREFERENCES_KEY, Context.MODE_PRIVATE); - } - - public void setApplicationForeground(boolean foreground){ - mIsForeground = foreground; - } - -} diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java index 7376e452..c133d0a5 100644 --- a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java +++ b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java @@ -1,16 +1,12 @@ package io.invertase.firebase.messaging; -import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.os.Bundle; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; -import com.facebook.react.bridge.ActivityEventListener; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; @@ -18,32 +14,20 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; -import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import com.google.firebase.iid.FirebaseInstanceId; import com.google.firebase.messaging.FirebaseMessaging; import com.google.firebase.messaging.RemoteMessage; -import com.google.firebase.messaging.RemoteMessage.Notification; import io.invertase.firebase.Utils; -import me.leolin.shortcutbadger.ShortcutBadger; import java.util.Map; -public class RNFirebaseMessaging extends ReactContextBaseJavaModule implements ActivityEventListener { - private static final String BADGE_FILE = "BadgeCountFile"; - private static final String BADGE_KEY = "BadgeCount"; - +public class RNFirebaseMessaging extends ReactContextBaseJavaModule { private static final String TAG = "RNFirebaseMessaging"; - private SharedPreferences sharedPreferences = null; - public RNFirebaseMessaging(ReactApplicationContext context) { super(context); - context.addActivityEventListener(this); - - sharedPreferences = context.getSharedPreferences(BADGE_FILE, Context.MODE_PRIVATE); - LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context); // Subscribe to message events @@ -73,24 +57,6 @@ public class RNFirebaseMessaging extends ReactContextBaseJavaModule implements A } // Non Web SDK methods - - @ReactMethod - public void getBadge(Promise promise) { - int badge = sharedPreferences.getInt(BADGE_KEY, 0); - Log.d(TAG, "Got badge count: " + badge); - promise.resolve(badge); - } - - @ReactMethod - public void getInitialMessage(Promise promise) { - if (getCurrentActivity() == null) { - promise.resolve(null); - } else { - WritableMap messageMap = parseIntentForMessage(getCurrentActivity().getIntent()); - promise.resolve(messageMap); - } - } - @ReactMethod public void hasPermission(Promise promise) { promise.resolve(true); @@ -132,19 +98,6 @@ public class RNFirebaseMessaging extends ReactContextBaseJavaModule implements A promise.resolve(null); } - @ReactMethod - public void setBadge(int badge) { - // Store the badge count for later retrieval - sharedPreferences.edit().putInt(BADGE_KEY, badge).apply(); - if (badge == 0) { - Log.d(TAG, "Remove badge count"); - ShortcutBadger.removeCount(this.getReactApplicationContext()); - } else { - Log.d(TAG, "Apply badge count: " + badge); - ShortcutBadger.applyCount(this.getReactApplicationContext(), badge); - } - } - @ReactMethod public void subscribeToTopic(String topic) { FirebaseMessaging.getInstance().subscribeToTopic(topic); @@ -155,60 +108,6 @@ public class RNFirebaseMessaging extends ReactContextBaseJavaModule implements A FirebaseMessaging.getInstance().unsubscribeFromTopic(topic); } - ////////////////////////////////////////////////////////////////////// - // Start ActivityEventListener methods - ////////////////////////////////////////////////////////////////////// - @Override - public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { - // FCM functionality does not need this function - } - - @Override - public void onNewIntent(Intent intent) { - WritableMap messageMap = parseIntentForMessage(intent); - if (messageMap != null) { - Log.d(TAG, "onNewIntent called with new FCM message"); - Utils.sendEvent(getReactApplicationContext(), "messaging_message_received", messageMap); - } - } - ////////////////////////////////////////////////////////////////////// - // End ActivityEventListener methods - ////////////////////////////////////////////////////////////////////// - - private WritableMap parseIntentForMessage(Intent intent) { - // Check if FCM data exists - if (intent.getExtras() == null || !intent.hasExtra("google.message_id")) { - return null; - } - - Bundle extras = intent.getExtras(); - - WritableMap messageMap = Arguments.createMap(); - WritableMap dataMap = Arguments.createMap(); - - for (String key : extras.keySet()) { - if (key.equals("collapse_key")) { - messageMap.putString("collapseKey", extras.getString("collapse_key")); - } else if (key.equals("from")) { - messageMap.putString("from", extras.getString("from")); - } else if (key.equals("google.message_id")) { - messageMap.putString("messageId", extras.getString("google.message_id")); - } else if (key.equals("google.sent_time")) { - messageMap.putDouble("sentTime", extras.getLong("google.sent_time")); - } else if (key.equals("google.ttl")) { - messageMap.putDouble("ttl", extras.getDouble("google.ttl")); - } else if (key.equals("_fbSourceApplicationHasBeenSet")) { - // ignore known unneeded fields - } else { - dataMap.putString(key, extras.getString(key)); - } - } - messageMap.putMap("data", dataMap); - messageMap.putBoolean("openedFromTray", true); - - return messageMap; - } - private class MessageReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { @@ -246,67 +145,10 @@ public class RNFirebaseMessaging extends ReactContextBaseJavaModule implements A if (message.getMessageType() != null) { messageMap.putString("messageType", message.getMessageType()); } - - if (message.getNotification() != null) { - Notification notification = message.getNotification(); - - WritableMap notificationMap = Arguments.createMap(); - - if (notification.getBody() != null) { - notificationMap.putString("body", notification.getBody()); - } - if (notification.getBodyLocalizationArgs() != null) { - WritableArray bodyArgs = Arguments.createArray(); - for (String arg : notification.getBodyLocalizationArgs()) { - bodyArgs.pushString(arg); - } - notificationMap.putArray("bodyLocalizationArgs", bodyArgs); - } - if (notification.getBodyLocalizationKey() != null) { - notificationMap.putString("bodyLocalizationKey", notification.getBodyLocalizationKey()); - } - if (notification.getClickAction() != null) { - notificationMap.putString("clickAction", notification.getClickAction()); - } - if (notification.getColor() != null) { - notificationMap.putString("color", notification.getColor()); - } - if (notification.getIcon() != null) { - notificationMap.putString("icon", notification.getIcon()); - } - if (notification.getLink() != null) { - notificationMap.putString("link", notification.getLink().toString()); - } - if (notification.getSound() != null) { - notificationMap.putString("sound", notification.getSound()); - } - if (notification.getTag() != null) { - notificationMap.putString("tag", notification.getTag()); - } - if (notification.getTitle() != null) { - notificationMap.putString("title", notification.getTitle()); - } - if (notification.getTitleLocalizationArgs() != null) { - WritableArray titleArgs = Arguments.createArray(); - for (String arg : notification.getTitleLocalizationArgs()) { - titleArgs.pushString(arg); - } - notificationMap.putArray("titleLocalizationArgs", titleArgs); - } - if (notification.getTitleLocalizationKey() != null) { - notificationMap.putString("titleLocalizationKey", notification.getTitleLocalizationKey()); - } - - messageMap.putMap("notification", notificationMap); - } - - messageMap.putBoolean("openedFromTray", false); messageMap.putDouble("sentTime", message.getSentTime()); - if (message.getTo() != null) { messageMap.putString("to", message.getTo()); } - messageMap.putDouble("ttl", message.getTtl()); return messageMap; diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessagingPackage.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessagingPackage.java index 1a88b657..6457c897 100644 --- a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessagingPackage.java +++ b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessagingPackage.java @@ -1,7 +1,6 @@ package io.invertase.firebase.messaging; import com.facebook.react.ReactPackage; -import com.facebook.react.bridge.JavaScriptModule; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.uimanager.UIManagerModule; diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessagingService.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessagingService.java index fd350e76..662ed5c5 100644 --- a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessagingService.java +++ b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessagingService.java @@ -10,16 +10,25 @@ import com.google.firebase.messaging.RemoteMessage; public class RNFirebaseMessagingService extends FirebaseMessagingService { private static final String TAG = "RNFMessagingService"; public static final String MESSAGE_EVENT = "messaging-message"; + public static final String REMOTE_NOTIFICATION_EVENT = "notifications-remote-notification"; @Override public void onMessageReceived(RemoteMessage message) { Log.d(TAG, "onMessageReceived event received"); - // Build an Intent to pass the token to the RN Application - Intent messageEvent = new Intent(MESSAGE_EVENT); - messageEvent.putExtra("message", message); + Intent event; + + if (message.getNotification() != null) { + // It's a notification, pass to the notification module + event = new Intent(REMOTE_NOTIFICATION_EVENT); + event.putExtra("notification", message); + } else { + // It's a data message, pass to the messaging module + event = new Intent(MESSAGE_EVENT); + event.putExtra("message", message); + } // Broadcast it so it is only available to the RN Application - LocalBroadcastManager.getInstance(this).sendBroadcast(messageEvent); + LocalBroadcastManager.getInstance(this).sendBroadcast(event); } } diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java index 387727ba..b99be159 100644 --- a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java @@ -3,6 +3,8 @@ package io.invertase.firebase.notifications; import android.app.AlarmManager; import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationChannelGroup; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; @@ -21,6 +23,7 @@ import android.util.Log; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import org.json.JSONException; @@ -30,6 +33,7 @@ import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; +import java.util.List; import java.util.Map; import io.invertase.firebase.messaging.BundleJSONConverter; @@ -71,6 +75,42 @@ public class RNFirebaseNotificationManager { preferences.edit().remove(notificationId).apply(); } + public void createChannel(ReadableMap channelMap) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = parseChannelMap(channelMap); + notificationManager.createNotificationChannel(channel); + } + } + + public void createChannelGroup(ReadableMap channelGroupMap) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannelGroup channelGroup = parseChannelGroupMap(channelGroupMap); + notificationManager.createNotificationChannelGroup(channelGroup); + } + } + + public void createChannelGroups(ReadableArray channelGroupsArray) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + List channelGroups = new ArrayList<>(); + for (int i = 0; i < channelGroupsArray.size(); i++) { + NotificationChannelGroup channelGroup = parseChannelGroupMap(channelGroupsArray.getMap(i)); + channelGroups.add(channelGroup); + } + notificationManager.createNotificationChannelGroups(channelGroups); + } + } + + public void createChannels(ReadableArray channelsArray) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + List channels = new ArrayList<>(); + for (int i = 0; i < channelsArray.size(); i++) { + NotificationChannel channel = parseChannelMap(channelsArray.getMap(i)); + channels.add(channel); + } + notificationManager.createNotificationChannels(channels); + } + } + public void displayNotification(ReadableMap notification, Promise promise) { Bundle notificationBundle = Arguments.toBundle(notification); displayNotification(notificationBundle, promise); @@ -155,12 +195,14 @@ public class RNFirebaseNotificationManager { return; } - String channelId = notification.getString("channelId"); + Bundle android = notification.getBundle("android"); + + String channelId = android.getString("channelId"); String notificationId = notification.getString("notificationId"); NotificationCompat.Builder nb; // TODO: Change 27 to 'Build.VERSION_CODES.O_MR1' when using appsupport v27 - if (Build.VERSION.SDK_INT >= 27) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { nb = new NotificationCompat.Builder(context, channelId); } else { nb = new NotificationCompat.Builder(context); @@ -173,16 +215,8 @@ public class RNFirebaseNotificationManager { nb = nb.setExtras(notification.getBundle("data")); } if (notification.containsKey("sound")) { - String sound = notification.getString("sound"); - if (sound.equalsIgnoreCase("default")) { - nb = nb.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)); - } else { - int soundResourceId = getResourceId("raw", sound); - if (soundResourceId == 0) { - soundResourceId = getResourceId("raw", sound.substring(0, sound.lastIndexOf('.'))); - } - nb = nb.setSound(Uri.parse("android.resource://" + context.getPackageName() + "/" + soundResourceId)); - } + Uri sound = getSound(notification.getString("sound")); + nb = nb.setSound(sound); } if (notification.containsKey("subtitle")) { nb = nb.setSubText(notification.getString("subtitle")); @@ -191,125 +225,138 @@ public class RNFirebaseNotificationManager { nb = nb.setContentTitle(notification.getString("title")); } - if (notification.containsKey("autoCancel")) { - nb = nb.setAutoCancel(notification.getBoolean("autoCancel")); + if (android.containsKey("autoCancel")) { + nb = nb.setAutoCancel(android.getBoolean("autoCancel")); } - if (notification.containsKey("badgeIconType")) { - nb = nb.setBadgeIconType(notification.getInt("badgeIconType")); + if (android.containsKey("badgeIconType")) { + Double badgeIconType = android.getDouble("badgeIconType"); + nb = nb.setBadgeIconType(badgeIconType.intValue()); } - if (notification.containsKey("category")) { - nb = nb.setCategory(notification.getString("category")); + if (android.containsKey("category")) { + nb = nb.setCategory(android.getString("category")); } - if (notification.containsKey("color")) { - String color = notification.getString("color"); + if (android.containsKey("color")) { + String color = android.getString("color"); nb = nb.setColor(Color.parseColor(color)); } - if (notification.containsKey("colorized")) { - nb = nb.setColorized(notification.getBoolean("colorized")); + if (android.containsKey("colorized")) { + nb = nb.setColorized(android.getBoolean("colorized")); } - if (notification.containsKey("contentInfo")) { - nb = nb.setContentInfo(notification.getString("contentInfo")); + if (android.containsKey("contentInfo")) { + nb = nb.setContentInfo(android.getString("contentInfo")); } if (notification.containsKey("defaults")) { - int[] defaultsArray = notification.getIntArray("defaults"); + double[] defaultsArray = android.getDoubleArray("defaults"); int defaults = 0; - for (int d : defaultsArray) { - defaults |= d; + for (Double d : defaultsArray) { + defaults |= d.intValue(); } nb = nb.setDefaults(defaults); } - if (notification.containsKey("group")) { - nb = nb.setGroup(notification.getString("group")); + if (android.containsKey("group")) { + nb = nb.setGroup(android.getString("group")); } - if (notification.containsKey("groupAlertBehaviour")) { - nb = nb.setGroupAlertBehavior(notification.getInt("groupAlertBehaviour")); + if (android.containsKey("groupAlertBehaviour")) { + Double groupAlertBehaviour = android.getDouble("groupAlertBehaviour"); + nb = nb.setGroupAlertBehavior(groupAlertBehaviour.intValue()); } - if (notification.containsKey("groupSummary")) { - nb = nb.setGroupSummary(notification.getBoolean("groupSummary")); + if (android.containsKey("groupSummary")) { + nb = nb.setGroupSummary(android.getBoolean("groupSummary")); } - if (notification.containsKey("largeIcon")) { - Bitmap largeIcon = getBitmap(notification.getString("largeIcon")); + if (android.containsKey("largeIcon")) { + Bitmap largeIcon = getBitmap(android.getString("largeIcon")); if (largeIcon != null) { nb = nb.setLargeIcon(largeIcon); } } - if (notification.containsKey("lights")) { - Bundle lights = notification.getBundle("lights"); - nb = nb.setLights(lights.getInt("argb"), lights.getInt("onMs"), lights.getInt("offMs")); + if (android.containsKey("lights")) { + Bundle lights = android.getBundle("lights"); + Double argb = lights.getDouble("argb"); + Double onMs = lights.getDouble("onMs"); + Double offMs = lights.getDouble("offMs"); + nb = nb.setLights(argb.intValue(), onMs.intValue(), offMs.intValue()); } - if (notification.containsKey("localOnly")) { - nb = nb.setLocalOnly(notification.getBoolean("localOnly")); + if (android.containsKey("localOnly")) { + nb = nb.setLocalOnly(android.getBoolean("localOnly")); } - if (notification.containsKey("number")) { - nb = nb.setNumber(notification.getInt("number")); + if (android.containsKey("number")) { + Double number = android.getDouble("number"); + nb = nb.setNumber(number.intValue()); } - if (notification.containsKey("ongoing")) { - nb = nb.setOngoing(notification.getBoolean("ongoing")); + if (android.containsKey("ongoing")) { + nb = nb.setOngoing(android.getBoolean("ongoing")); } - if (notification.containsKey("onlyAlertOnce")) { - nb = nb.setOngoing(notification.getBoolean("onlyAlertOnce")); + if (android.containsKey("onlyAlertOnce")) { + nb = nb.setOngoing(android.getBoolean("onlyAlertOnce")); } - if (notification.containsKey("people")) { - String[] people = notification.getStringArray("people"); - for (String person : people) { - nb = nb.addPerson(person); + if (android.containsKey("people")) { + String[] people = android.getStringArray("people"); + if (people != null) { + for (String person : people) { + nb = nb.addPerson(person); + } } } - if (notification.containsKey("priority")) { - nb = nb.setPriority(notification.getInt("priority")); + if (android.containsKey("priority")) { + Double priority = android.getDouble("priority"); + nb = nb.setPriority(priority.intValue()); } - if (notification.containsKey("progress")) { - Bundle progress = notification.getBundle("lights"); - nb = nb.setProgress(progress.getInt("max"), progress.getInt("progress"), progress.getBoolean("indeterminate")); + if (android.containsKey("progress")) { + Bundle progress = android.getBundle("lights"); + Double max = progress.getDouble("max"); + Double progressI = progress.getDouble("progress"); + nb = nb.setProgress(max.intValue(), progressI.intValue(), progress.getBoolean("indeterminate")); } // TODO: Public version of notification - /* if (notification.containsKey("publicVersion")) { + /* if (android.containsKey("publicVersion")) { nb = nb.setPublicVersion(); } */ - if (notification.containsKey("remoteInputHistory")) { - nb = nb.setRemoteInputHistory(notification.getStringArray("remoteInputHistory")); + if (android.containsKey("remoteInputHistory")) { + nb = nb.setRemoteInputHistory(android.getStringArray("remoteInputHistory")); } - if (notification.containsKey("shortcutId")) { - nb = nb.setShortcutId(notification.getString("shortcutId")); + if (android.containsKey("shortcutId")) { + nb = nb.setShortcutId(android.getString("shortcutId")); } - if (notification.containsKey("showWhen")) { - nb = nb.setShowWhen(notification.getBoolean("showWhen")); + if (android.containsKey("showWhen")) { + nb = nb.setShowWhen(android.getBoolean("showWhen")); } - if (notification.containsKey("smallIcon")) { - Bundle smallIcon = notification.getBundle("smallIcon"); + if (android.containsKey("smallIcon")) { + Bundle smallIcon = android.getBundle("smallIcon"); int smallIconResourceId = getResourceId("mipmap", smallIcon.getString("icon")); if (smallIconResourceId == 0) { smallIconResourceId = getResourceId("drawable", smallIcon.getString("icon")); } if (smallIconResourceId != 0) { if (smallIcon.containsKey("level")) { - nb = nb.setSmallIcon(smallIconResourceId, smallIcon.getInt("level")); + Double level = smallIcon.getDouble("level"); + nb = nb.setSmallIcon(smallIconResourceId, level.intValue()); } else { nb = nb.setSmallIcon(smallIconResourceId); } } } - if (notification.containsKey("sortKey")) { - nb = nb.setSortKey(notification.getString("sortKey")); + if (android.containsKey("sortKey")) { + nb = nb.setSortKey(android.getString("sortKey")); } - if (notification.containsKey("ticker")) { - nb = nb.setTicker(notification.getString("ticker")); + if (android.containsKey("ticker")) { + nb = nb.setTicker(android.getString("ticker")); } - if (notification.containsKey("timeoutAfter")) { - nb = nb.setTimeoutAfter(notification.getLong("timeoutAfter")); + if (android.containsKey("timeoutAfter")) { + nb = nb.setTimeoutAfter(android.getLong("timeoutAfter")); } - if (notification.containsKey("usesChronometer")) { - nb = nb.setUsesChronometer(notification.getBoolean("usesChronometer")); + if (android.containsKey("usesChronometer")) { + nb = nb.setUsesChronometer(android.getBoolean("usesChronometer")); } - if (notification.containsKey("vibrate")) { - nb = nb.setVibrate(notification.getLongArray("vibrate")); + if (android.containsKey("vibrate")) { + nb = nb.setVibrate(android.getLongArray("vibrate")); } - if (notification.containsKey("visibility")) { - nb = nb.setVisibility(notification.getInt("visibility")); + if (android.containsKey("visibility")) { + Double visibility = android.getDouble("visibility"); + nb = nb.setVisibility(visibility.intValue()); } - if (notification.containsKey("when")) { - nb = nb.setWhen(notification.getLong("when")); + if (android.containsKey("when")) { + nb = nb.setWhen(android.getLong("when")); } // TODO: Big text / Big picture @@ -335,8 +382,8 @@ public class RNFirebaseNotificationManager { Intent intent = new Intent(context, intentClass); intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); intent.putExtras(notification); - if (notification.containsKey("clickAction")) { - intent.setAction(notification.getString("clickAction")); + if (android.containsKey("clickAction")) { + intent.setAction(android.getString("clickAction")); } PendingIntent contentIntent = PendingIntent.getActivity(context, notificationId.hashCode(), intent, @@ -391,6 +438,71 @@ public class RNFirebaseNotificationManager { return context.getResources().getIdentifier(image, type, context.getPackageName()); } + private Uri getSound(String sound) { + if (sound.equalsIgnoreCase("default")) { + return RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); + } else { + int soundResourceId = getResourceId("raw", sound); + if (soundResourceId == 0) { + soundResourceId = getResourceId("raw", sound.substring(0, sound.lastIndexOf('.'))); + } + return Uri.parse("android.resource://" + context.getPackageName() + "/" + soundResourceId); + } + } + + private NotificationChannelGroup parseChannelGroupMap(ReadableMap channelGroupMap) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + String groupId = channelGroupMap.getString("groupId"); + String name = channelGroupMap.getString("name"); + + return new NotificationChannelGroup(groupId, name); + } + return null; + } + + private NotificationChannel parseChannelMap(ReadableMap channelMap) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + String channelId = channelMap.getString("channelId"); + String name = channelMap.getString("name"); + int importance = channelMap.getInt("importance"); + + NotificationChannel channel = new NotificationChannel(channelId, name, importance); + if (channelMap.hasKey("bypassDnd")) { + channel.setBypassDnd(channelMap.getBoolean("bypassDnd")); + } + if (channelMap.hasKey("description")) { + channel.setDescription(channelMap.getString("description")); + } + if (channelMap.hasKey("group")) { + channel.setGroup(channelMap.getString("group")); + } + if (channelMap.hasKey("lightColor")) { + String lightColor = channelMap.getString("lightColor"); + channel.setLightColor(Color.parseColor(lightColor)); + } + if (channelMap.hasKey("lockScreenVisibility")) { + channel.setLockscreenVisibility(channelMap.getInt("lockScreenVisibility")); + } + if (channelMap.hasKey("showBadge")) { + channel.setShowBadge(channelMap.getBoolean("showBadge")); + } + if (channelMap.hasKey("sound")) { + Uri sound = getSound(channelMap.getString("sound")); + channel.setSound(sound, null); + } + if (channelMap.hasKey("vibrationPattern")) { + ReadableArray vibrationArray = channelMap.getArray("vibrationPattern"); + long[] vibration = new long[]{}; + for (int i = 0; i < vibrationArray.size(); i++) { + vibration[i] = (long) vibrationArray.getDouble(i); + } + channel.setVibrationPattern(vibration); + } + return channel; + } + return null; + } + private void scheduleNotification(Bundle notification, Promise promise) { if (!notification.containsKey("notificationId")) { if (promise == null) { diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java index 7e20f365..dad5af46 100644 --- a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java @@ -1,38 +1,57 @@ package io.invertase.firebase.notifications; +import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.SharedPreferences; import android.os.Bundle; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; +import com.facebook.react.bridge.ActivityEventListener; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; +import com.google.firebase.messaging.RemoteMessage; import java.util.ArrayList; +import java.util.Map; import io.invertase.firebase.Utils; +import io.invertase.firebase.messaging.RNFirebaseMessagingService; +import me.leolin.shortcutbadger.ShortcutBadger; -public class RNFirebaseNotifications extends ReactContextBaseJavaModule implements LifecycleEventListener { +public class RNFirebaseNotifications extends ReactContextBaseJavaModule implements ActivityEventListener, LifecycleEventListener { + private static final String BADGE_FILE = "BadgeCountFile"; + private static final String BADGE_KEY = "BadgeCount"; private static final String TAG = "RNFirebaseNotifications"; + private SharedPreferences sharedPreferences = null; + private RNFirebaseNotificationManager notificationManager; public RNFirebaseNotifications(ReactApplicationContext context) { super(context); - notificationManager = new RNFirebaseNotificationManager(context.getApplicationContext()); + context.addActivityEventListener(this); context.addLifecycleEventListener(this); + notificationManager = new RNFirebaseNotificationManager(context.getApplicationContext()); + sharedPreferences = context.getSharedPreferences(BADGE_FILE, Context.MODE_PRIVATE); + LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context); + // Subscribe to remote notification events + localBroadcastManager.registerReceiver(new RemoteNotificationReceiver(), + new IntentFilter(RNFirebaseMessagingService.REMOTE_NOTIFICATION_EVENT)); + // Subscribe to scheduled notification events localBroadcastManager.registerReceiver(new ScheduledNotificationReceiver(), new IntentFilter(RNFirebaseNotificationManager.SCHEDULED_NOTIFICATION_EVENT)); @@ -58,9 +77,22 @@ public class RNFirebaseNotifications extends ReactContextBaseJavaModule implemen notificationManager.displayNotification(notification, promise); } + @ReactMethod + public void getBadge(Promise promise) { + int badge = sharedPreferences.getInt(BADGE_KEY, 0); + Log.d(TAG, "Got badge count: " + badge); + promise.resolve(badge); + } + @ReactMethod public void getInitialNotification(Promise promise) { // TODO + if (getCurrentActivity() == null) { + promise.resolve(null); + } else { + WritableMap notificationOpenedMap = parseIntentForRemoteNotification(getCurrentActivity().getIntent()); + promise.resolve(notificationOpenedMap); + } } @ReactMethod @@ -83,11 +115,74 @@ public class RNFirebaseNotifications extends ReactContextBaseJavaModule implemen notificationManager.removeDeliveredNotification(notificationId); } + @ReactMethod + public void setBadge(int badge) { + // Store the badge count for later retrieval + sharedPreferences.edit().putInt(BADGE_KEY, badge).apply(); + if (badge == 0) { + Log.d(TAG, "Remove badge count"); + ShortcutBadger.removeCount(this.getReactApplicationContext()); + } else { + Log.d(TAG, "Apply badge count: " + badge); + ShortcutBadger.applyCount(this.getReactApplicationContext(), badge); + } + } + @ReactMethod public void scheduleNotification(ReadableMap notification, Promise promise) { notificationManager.scheduleNotification(notification, promise); } + ////////////////////////////////////////////////////////////////////// + // Start Android specific methods + ////////////////////////////////////////////////////////////////////// + @ReactMethod + public void createChannel(ReadableMap channelMap, Promise promise) { + notificationManager.createChannel(channelMap); + promise.resolve(null); + } + + @ReactMethod + public void createChannelGroup(ReadableMap channelGroupMap, Promise promise) { + notificationManager.createChannelGroup(channelGroupMap); + promise.resolve(null); + } + + @ReactMethod + public void createChannelGroup(ReadableArray channelGroupsArray, Promise promise) { + notificationManager.createChannelGroups(channelGroupsArray); + promise.resolve(null); + } + + @ReactMethod + public void createChannels(ReadableArray channelsArray, Promise promise) { + notificationManager.createChannels(channelsArray); + promise.resolve(null); + } + ////////////////////////////////////////////////////////////////////// + // End Android specific methods + ////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////// + // Start ActivityEventListener methods + ////////////////////////////////////////////////////////////////////// + @Override + public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { + // FCM functionality does not need this function + } + + @Override + public void onNewIntent(Intent intent) { + WritableMap notificationOpenedMap = parseIntentForRemoteNotification(intent); + if (notificationOpenedMap != null) { + Log.d(TAG, "onNewIntent called with new remote notification"); + Utils.sendEvent(getReactApplicationContext(), "notifications_notification_opened", notificationOpenedMap); + } + } + ////////////////////////////////////////////////////////////////////// + // End ActivityEventListener methods + ////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////// // Start LifecycleEventListener methods ////////////////////////////////////////////////////////////////////// @@ -109,10 +204,102 @@ public class RNFirebaseNotifications extends ReactContextBaseJavaModule implemen // End LifecycleEventListener methods ////////////////////////////////////////////////////////////////////// + + private WritableMap parseIntentForRemoteNotification(Intent intent) { + // Check if FCM data exists + if (intent.getExtras() == null || !intent.hasExtra("google.message_id")) { + return null; + } + + Bundle extras = intent.getExtras(); + + WritableMap notificationMap = Arguments.createMap(); + WritableMap dataMap = Arguments.createMap(); + + for (String key : extras.keySet()) { + if (key.equals("google.message_id")) { + notificationMap.putString("notificationId", extras.getString(key)); + } else if (key.equals("collapse_key") + || key.equals("from") + || key.equals("google.sent_time") + || key.equals("google.ttl") + || key.equals("_fbSourceApplicationHasBeenSet")) { + // ignore known unneeded fields + } else { + dataMap.putString(key, extras.getString(key)); + } + } + notificationMap.putMap("data", dataMap); + + WritableMap notificationOpenedMap = Arguments.createMap(); + notificationOpenedMap.putString("action", intent.getAction()); + notificationOpenedMap.putMap("notification", notificationMap); + + return notificationOpenedMap; + } + private WritableMap parseNotificationBundle(Bundle notification) { return Arguments.makeNativeMap(notification); } + private WritableMap parseRemoteMessage(RemoteMessage message) { + RemoteMessage.Notification notification = message.getNotification(); + + WritableMap notificationMap = Arguments.createMap(); + WritableMap dataMap = Arguments.createMap(); + + // Cross platform notification properties + notificationMap.putString("body", notification.getBody()); + if (message.getData() != null) { + for (Map.Entry e : message.getData().entrySet()) { + dataMap.putString(e.getKey(), e.getValue()); + } + } + notificationMap.putMap("data", dataMap); + if (message.getMessageId() != null) { + notificationMap.putString("notificationId", message.getMessageId()); + } + if (notification.getSound() != null) { + notificationMap.putString("sound", notification.getSound()); + } + if (notification.getTitle() != null) { + notificationMap.putString("title", notification.getTitle()); + } + + // Android specific notification properties + WritableMap androidMap = Arguments.createMap(); + if (notification.getClickAction() != null) { + androidMap.putString("clickAction", notification.getClickAction()); + } + if (notification.getColor() != null) { + androidMap.putString("color", notification.getColor()); + } + if (notification.getIcon() != null) { + WritableMap iconMap = Arguments.createMap(); + iconMap.putString("icon", notification.getIcon()); + androidMap.putMap("smallIcon", iconMap); + } + if (notification.getTag() != null) { + androidMap.putString("group", notification.getTag()); + } + + return notificationMap; + } + + private class RemoteNotificationReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (getReactApplicationContext().hasActiveCatalystInstance()) { + Log.d(TAG, "Received new remote notification"); + + RemoteMessage message = intent.getParcelableExtra("notification"); + WritableMap messageMap = parseRemoteMessage(message); + + Utils.sendEvent(getReactApplicationContext(), "notifications_notification_received", messageMap); + } + } + } + private class ScheduledNotificationReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { diff --git a/lib/modules/messaging/RemoteMessage.js b/lib/modules/messaging/RemoteMessage.js index 2ed54228..a518c875 100644 --- a/lib/modules/messaging/RemoteMessage.js +++ b/lib/modules/messaging/RemoteMessage.js @@ -25,6 +25,7 @@ export default class RemoteMessage { this._data = inboundMessage.data; this._from = inboundMessage.from; this._messageId = inboundMessage.messageId; + this._messageType = inboundMessage.messageType; this._sentTime = inboundMessage.sentTime; } // defaults @@ -50,6 +51,10 @@ export default class RemoteMessage { return this._messageId; } + get messageType(): ?string { + return this._messageType; + } + get sentTime(): ?number { return this._sentTime; } diff --git a/lib/modules/messaging/types.js b/lib/modules/messaging/types.js index 1d187e30..e2cbe647 100644 --- a/lib/modules/messaging/types.js +++ b/lib/modules/messaging/types.js @@ -22,6 +22,7 @@ export type NativeInboundRemoteMessage = { data: { [string]: string }, from?: string, messageId: string, + messageType?: string, sentTime?: number, to?: string, ttl?: number, diff --git a/lib/modules/notifications/AndroidChannel.js b/lib/modules/notifications/AndroidChannel.js new file mode 100644 index 00000000..5e3f6cf0 --- /dev/null +++ b/lib/modules/notifications/AndroidChannel.js @@ -0,0 +1,213 @@ +/** + * @flow + * AndroidChannel representation wrapper + */ +import type { ImportanceType, VisibilityType } from './types'; + +type NativeAndroidChannel = {| + bypassDnd?: boolean, + channelId: string, + description?: string, + group?: string, + importance: ImportanceType, + lightColor?: string, + lockScreenVisibility?: VisibilityType, + name: string, + showBadge?: boolean, + sound?: string, + vibrationPattern?: number[], +|}; + +export default class AndroidChannel { + _bypassDnd: boolean | void; + _channelId: string; + _description: string | void; + _group: string | void; + _importance: ImportanceType; + _lightColor: string | void; + _lockScreenVisibility: VisibilityType; + _name: string; + _showBadge: boolean | void; + _sound: string | void; + _vibrationPattern: number[] | void; + + get bypassDnd(): ?boolean { + return this._bypassDnd; + } + + get channelId(): string { + return this._channelId; + } + + get description(): ?string { + return this._description; + } + + get group(): ?string { + return this._group; + } + + get importance(): ImportanceType { + return this._importance; + } + + get lightColor(): ?string { + return this._lightColor; + } + + get lockScreenVisibility(): ?VisibilityType { + return this._lockScreenVisibility; + } + + get name(): string { + return this._name; + } + + get showBadge(): ?boolean { + return this._showBadge; + } + + get sound(): ?string { + return this._sound; + } + + get vibrationPattern(): ?(number[]) { + return this._vibrationPattern; + } + + /** + * + * @param bypassDnd + * @returns {AndroidChannel} + */ + setBypassDnd(bypassDnd: boolean): AndroidChannel { + this._bypassDnd = bypassDnd; + return this; + } + + /** + * + * @param channelId + * @returns {AndroidChannel} + */ + setChannelId(channelId: string): AndroidChannel { + this._channelId = channelId; + return this; + } + + /** + * + * @param description + * @returns {AndroidChannel} + */ + setDescription(description: string): AndroidChannel { + this._description = description; + return this; + } + + /** + * + * @param group + * @returns {AndroidChannel} + */ + setGroup(groupId: string): AndroidChannel { + this._group = groupId; + return this; + } + + /** + * + * @param importance + * @returns {AndroidChannel} + */ + setImportance(importance: ImportanceType): AndroidChannel { + this._importance = importance; + return this; + } + + /** + * + * @param lightColor + * @returns {AndroidChannel} + */ + setLightColor(lightColor: string): AndroidChannel { + this._lightColor = lightColor; + return this; + } + + /** + * + * @param lockScreenVisibility + * @returns {AndroidChannel} + */ + setLockScreenVisibility( + lockScreenVisibility: VisibilityType + ): AndroidChannel { + this._lockScreenVisibility = lockScreenVisibility; + return this; + } + + /** + * + * @param name + * @returns {AndroidChannel} + */ + setName(name: string): AndroidChannel { + this._name = name; + return this; + } + + /** + * + * @param showBadge + * @returns {AndroidChannel} + */ + setShowBadge(showBadge: boolean): AndroidChannel { + this._showBadge = showBadge; + return this; + } + + /** + * + * @param sound + * @returns {AndroidChannel} + */ + setSound(sound: string): AndroidChannel { + this._sound = sound; + return this; + } + + /** + * + * @param vibrationPattern + * @returns {AndroidChannel} + */ + setVibrationPattern(vibrationPattern: number[]): AndroidChannel { + this._vibrationPattern = vibrationPattern; + return this; + } + + build(): NativeAndroidChannel { + if (!this._channelId) { + throw new Error('AndroidChannel: Missing required `channelId` property'); + } else if (!this._importance) { + throw new Error('AndroidChannel: Missing required `importance` property'); + } else if (!this._name) { + throw new Error('AndroidChannel: Missing required `name` property'); + } + + return { + bypassDnd: this._bypassDnd, + channelId: this._channelId, + description: this._description, + group: this._group, + importance: this._importance, + lightColor: this._lightColor, + lockScreenVisibility: this._lockScreenVisibility, + name: this._name, + showBadge: this._showBadge, + sound: this._sound, + vibrationPattern: this._vibrationPattern, + }; + } +} diff --git a/lib/modules/notifications/AndroidChannelGroup.js b/lib/modules/notifications/AndroidChannelGroup.js new file mode 100644 index 00000000..fb37a527 --- /dev/null +++ b/lib/modules/notifications/AndroidChannelGroup.js @@ -0,0 +1,57 @@ +/** + * @flow + * AndroidChannelGroup representation wrapper + */ + +type NativeAndroidChannelGroup = {| + groupId: string, + name: string, +|}; + +export default class AndroidChannel { + _groupId: string; + _name: string; + + get groupId(): string { + return this._groupId; + } + + get name(): string { + return this._name; + } + + /** + * + * @param groupId + * @returns {AndroidChannel} + */ + setGroupId(groupId: string): AndroidChannel { + this._groupId = groupId; + return this; + } + + /** + * + * @param name + * @returns {AndroidChannel} + */ + setName(name: string): AndroidChannel { + this._name = name; + return this; + } + + build(): NativeAndroidChannelGroup { + if (!this._groupId) { + throw new Error( + 'AndroidChannelGroup: Missing required `groupId` property' + ); + } else if (!this._name) { + throw new Error('AndroidChannelGroup: Missing required `name` property'); + } + + return { + groupId: this._groupId, + name: this._name, + }; + } +} diff --git a/lib/modules/notifications/AndroidNotifications.js b/lib/modules/notifications/AndroidNotifications.js new file mode 100644 index 00000000..bd644a60 --- /dev/null +++ b/lib/modules/notifications/AndroidNotifications.js @@ -0,0 +1,94 @@ +/** + * @flow + * AndroidNotifications representation wrapper + */ +import { Platform } from 'react-native'; +import AndroidChannel from './AndroidChannel'; +import AndroidChannelGroup from './AndroidChannelGroup'; +import { getNativeModule } from '../../utils/native'; + +import type Notifications from './'; + +export default class AndroidNotifications { + _notifications: Notifications; + + constructor(notifications: Notifications) { + this._notifications = notifications; + } + + createChannel(channel: AndroidChannel): Promise { + if (Platform.OS === 'android') { + if (!(channel instanceof AndroidChannel)) { + throw new Error( + `AndroidNotifications:createChannel expects an 'AndroidChannel' but got type ${typeof channel}` + ); + } + return getNativeModule(this._notifications).createChannel( + channel.build() + ); + } + return Promise.resolve(); + } + + createChannelGroup(channelGroup: AndroidChannelGroup): Promise { + if (Platform.OS === 'android') { + if (!(channelGroup instanceof AndroidChannelGroup)) { + throw new Error( + `AndroidNotifications:createChannelGroup expects an 'AndroidChannelGroup' but got type ${typeof channelGroup}` + ); + } + return getNativeModule(this._notifications).createChannelGroup( + channelGroup.build() + ); + } + return Promise.resolve(); + } + + createChannelGroups(channelGroups: AndroidChannelGroup[]): Promise { + if (Platform.OS === 'android') { + if (!Array.isArray(channelGroups)) { + throw new Error( + `AndroidNotifications:createChannelGroups expects an 'Array' but got type ${typeof channelGroups}` + ); + } + const nativeChannelGroups = []; + for (let i = 0; i < channelGroups.length; i++) { + const channelGroup = channelGroups[i]; + if (!(channelGroup instanceof AndroidChannelGroup)) { + throw new Error( + `AndroidNotifications:createChannelGroups expects array items of type 'AndroidChannelGroup' but got type ${typeof channelGroup}` + ); + } + nativeChannelGroups.push(channelGroup.build()); + } + return getNativeModule(this._notifications).createChannelGroups( + nativeChannelGroups + ); + } + return Promise.resolve(); + } + + createChannels(channels: AndroidChannel[]): Promise { + if (Platform.OS === 'android') { + if (!Array.isArray(channels)) { + throw new Error( + `AndroidNotifications:createChannels expects an 'Array' but got type ${typeof channels}` + ); + } + const nativeChannels = []; + for (let i = 0; i < channels.length; i++) { + const channel = channels[i]; + if (!(channel instanceof AndroidChannel)) { + throw new Error( + `AndroidNotifications:createChannels expects array items of type 'AndroidChannel' but got type ${typeof channel}` + ); + } + nativeChannels.push(channel.build()); + } + return getNativeModule(this._notifications).createChannels( + nativeChannels + ); + } + return Promise.resolve(); + } +} diff --git a/lib/modules/notifications/index.js b/lib/modules/notifications/index.js index f116d4f6..649831a9 100644 --- a/lib/modules/notifications/index.js +++ b/lib/modules/notifications/index.js @@ -1,18 +1,22 @@ /** * @flow - * Messaging (FCM) representation wrapper + * Notifications representation wrapper */ import { SharedEventEmitter } from '../../utils/events'; import { getLogger } from '../../utils/log'; import ModuleBase from '../../utils/ModuleBase'; import { getNativeModule } from '../../utils/native'; import { isFunction, isObject } from '../../utils'; +import AndroidChannel from './AndroidChannel'; +import AndroidChannelGroup from './AndroidChannelGroup'; +import AndroidNotifications from './AndroidNotifications'; import Notification from './Notification'; import { BadgeIconType, Category, Defaults, GroupAlert, + Importance, Priority, Visibility, } from './types'; @@ -64,6 +68,8 @@ export const NAMESPACE = 'notifications'; * @class Notifications */ export default class Notifications extends ModuleBase { + _android: AndroidNotifications; + constructor(app: App) { super(app, { events: NATIVE_EVENTS, @@ -71,6 +77,7 @@ export default class Notifications extends ModuleBase { multiApp: false, namespace: NAMESPACE, }); + this._android = new AndroidNotifications(this); SharedEventEmitter.addListener( // sub to internal native event - this fans out to @@ -89,7 +96,7 @@ export default class Notifications extends ModuleBase { // public event name: onNotificationOpened 'notifications_notification_opened', (notificationOpened: NativeNotificationOpened) => { - SharedEventEmitter.emit('OnNotificationOpened', { + SharedEventEmitter.emit('onNotificationOpened', { action: notificationOpened.action, notification: new Notification(notificationOpened.notification), }); @@ -109,6 +116,10 @@ export default class Notifications extends ModuleBase { ); } + get android(): AndroidNotifications { + return this._android; + } + /** * Cancel all notifications */ @@ -148,9 +159,11 @@ export default class Notifications extends ModuleBase { } getInitialNotification(): Promise { - return getNativeModule(this).getInitialNotification(); - // TODO - // .then(notification => (notification ? new Notification(this, notification) : null)); + return getNativeModule(this) + .getInitialNotification() + .then( + notification => (notification ? new Notification(notification) : null) + ); } /** @@ -278,8 +291,11 @@ export const statics = { Android: { BadgeIconType, Category, + Channel: AndroidChannel, + ChannelGroup: AndroidChannelGroup, Defaults, GroupAlert, + Importance, Priority, Visibility, }, diff --git a/lib/modules/notifications/types.js b/lib/modules/notifications/types.js index 87d4f806..a9c9f107 100644 --- a/lib/modules/notifications/types.js +++ b/lib/modules/notifications/types.js @@ -39,6 +39,16 @@ export const GroupAlert = { Summary: 1, }; +export const Importance = { + Default: 3, + High: 4, + Low: 2, + Max: 5, + Min: 1, + None: 3, + Unspecified: -1000, +}; + export const Priority = { Default: 0, High: 1, @@ -57,6 +67,7 @@ export type BadgeIconTypeType = $Values; export type CategoryType = $Values; export type DefaultsType = $Values; export type GroupAlertType = $Values; +export type ImportanceType = $Values; export type PriorityType = $Values; export type VisibilityType = $Values; diff --git a/tests/android/app/src/main/java/com/reactnativefirebasedemo/MainApplication.java b/tests/android/app/src/main/java/com/reactnativefirebasedemo/MainApplication.java index ae9de9cd..1c99a0a7 100644 --- a/tests/android/app/src/main/java/com/reactnativefirebasedemo/MainApplication.java +++ b/tests/android/app/src/main/java/com/reactnativefirebasedemo/MainApplication.java @@ -15,6 +15,7 @@ import io.invertase.firebase.firestore.RNFirebaseFirestorePackage; import io.invertase.firebase.instanceid.RNFirebaseInstanceIdPackage; import io.invertase.firebase.links.RNFirebaseLinksPackage; import io.invertase.firebase.messaging.RNFirebaseMessagingPackage; +import io.invertase.firebase.notifications.RNFirebaseNotificationsPackage; import io.invertase.firebase.perf.RNFirebasePerformancePackage; import io.invertase.firebase.storage.RNFirebaseStoragePackage; import com.oblador.vectoricons.VectorIconsPackage; @@ -51,6 +52,7 @@ public class MainApplication extends Application implements ReactApplication { new RNFirebaseInstanceIdPackage(), new RNFirebaseLinksPackage(), new RNFirebaseMessagingPackage(), + new RNFirebaseNotificationsPackage(), new RNFirebasePerformancePackage(), new RNFirebaseStoragePackage() ); diff --git a/tests/src/firebase.js b/tests/src/firebase.js index fcdad95f..5309ab69 100644 --- a/tests/src/firebase.js +++ b/tests/src/firebase.js @@ -7,14 +7,6 @@ import DatabaseContents from './tests/support/DatabaseContents'; RNfirebase.database.enableLogging(true); RNfirebase.firestore.enableLogging(true); -RNfirebase.messaging().onMessage(message => { - console.log('got new message: ', message); -}); - -RNfirebase.messaging().onTokenRefresh(token => { - console.log('got new token: ', token); -}); - const init = async () => { try { await RNfirebase.messaging().requestPermission(); @@ -22,9 +14,47 @@ const init = async () => { console.log('instanceid: ', instanceid); const token = await RNfirebase.messaging().getToken(); console.log('token: ', token); - const initialMessage = await RNfirebase.messaging().getInitialMessage(); - console.log('initial message: ', initialMessage); + const initialNotification = await RNfirebase.notifications().getInitialNotification(); + console.log('initialNotification: ', initialNotification); + + RNfirebase.messaging().onMessage(message => { + console.log('onMessage: ', message); + }); + RNfirebase.messaging().onTokenRefresh(deviceToken => { + dispatch(fcmTokenReceived(deviceToken)); + }); + RNfirebase.notifications().onNotification(notification => { + console.log('onNotification: ', notification); + }); + RNfirebase.notifications().onNotificationOpened(notification => { + console.log('onNotificationOpened: ', notification); + }); + RNfirebase.notifications().onNotificationDisplayed(notification => { + console.log('onNotificationDisplayed: ', notification); + }); // RNfirebase.instanceid().delete(); + const channel = new RNfirebase.notifications.Android.Channel(); + channel + .setChannelId('test') + .setName('test') + .setImportance(RNfirebase.notifications.Android.Importance.Max) + .setDescription('test channel'); + RNfirebase.notifications().android.createChannel(channel); + + const notification = new RNfirebase.notifications.Notification(); + notification + .setTitle('Test title') + .setBody('Test body') + .android.setChannelId('test') + .android.setPriority(RNfirebase.notifications.Android.Priority.Max); + const date = new Date(); + date.setMinutes(date.getMinutes() + 1); + setTimeout(() => { + RNfirebase.notifications().displayNotification(notification); + }, 5); + RNfirebase.notifications().scheduleNotification(notification, { + fireDate: date.getTime(), + }); } catch (error) { console.error('messaging init error:', error); } From 8e7c202846b1fb88594b874b46e7a97fdeb11b3e Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Tue, 6 Mar 2018 17:11:28 +0000 Subject: [PATCH 40/77] 3.3.0 --- package-lock.json | 2 +- package.json | 57 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 40e043ee..10c7bba0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-native-firebase", - "version": "3.2.7", + "version": "3.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 1bddc3d2..ae7009a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-firebase", - "version": "3.2.7", + "version": "3.3.0", "author": "Invertase (http://invertase.io)", "description": "A well tested, feature rich Firebase implementation for React Native, supporting iOS & Android. Individual module support for Admob, Analytics, Auth, Crash Reporting, Cloud Firestore, Database, Dynamic Links, Messaging (FCM), Remote Config, Storage and Performance.", "main": "dist/index.js", @@ -32,10 +32,46 @@ "jest": { "preset": "jest-react-native", "setupFiles": [], - "unmockedModulePathPatterns": ["./node_modules/react", "./node_modules/react-native", "./node_modues/react-native-mock", "./node_modules/react-addons-test-utils"] + "unmockedModulePathPatterns": [ + "./node_modules/react", + "./node_modules/react-native", + "./node_modues/react-native-mock", + "./node_modules/react-addons-test-utils" + ] }, "license": "APACHE-2.0", - "keywords": ["react", "admob", "auth", "config", "digits", "fabric", "phone-auth", "sms", "firestore", "cloud-firestore", "datastore", "remote-config", "transactions", "react-native", "react-native-firebase", "firebase", "fcm", "apn", "gcm", "analytics", "messaging", "database", "android", "ios", "crash", "firestack", "performance", "firestore", "dynamic-links", "crashlytics"], + "keywords": [ + "react", + "admob", + "auth", + "config", + "digits", + "fabric", + "phone-auth", + "sms", + "firestore", + "cloud-firestore", + "datastore", + "remote-config", + "transactions", + "react-native", + "react-native-firebase", + "firebase", + "fcm", + "apn", + "gcm", + "analytics", + "messaging", + "database", + "android", + "ios", + "crash", + "firestack", + "performance", + "firestore", + "dynamic-links", + "crashlytics" + ], "peerDependencies": { "react": "*", "react-native": ">= 0.48.0", @@ -92,8 +128,17 @@ "logo": "https://opencollective.com/opencollective/logo.txt" }, "lint-staged": { - "lib/**/*.js": ["eslint --fix", "git add"], - "tests/{src|lib}/**/*.js": ["eslint --fix", "git add"], - "*.{json,md,scss}": ["prettier --write", "git add"] + "lib/**/*.js": [ + "eslint --fix", + "git add" + ], + "tests/{src|lib}/**/*.js": [ + "eslint --fix", + "git add" + ], + "*.{json,md,scss}": [ + "prettier --write", + "git add" + ] } } From 7b9269fec5c18714a09d3e4430366670d21375df Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Tue, 6 Mar 2018 18:20:49 +0000 Subject: [PATCH 41/77] [notifications] Rename `onNotificationOpened` to `onNotificationOpen` --- .../notifications/RNFirebaseNotifications.java | 18 +++++++++--------- lib/modules/notifications/Notification.js | 2 +- lib/modules/notifications/index.js | 14 +++++++------- lib/modules/notifications/types.js | 2 +- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java index dad5af46..2e9de173 100644 --- a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java @@ -90,8 +90,8 @@ public class RNFirebaseNotifications extends ReactContextBaseJavaModule implemen if (getCurrentActivity() == null) { promise.resolve(null); } else { - WritableMap notificationOpenedMap = parseIntentForRemoteNotification(getCurrentActivity().getIntent()); - promise.resolve(notificationOpenedMap); + WritableMap notificationOpenMap = parseIntentForRemoteNotification(getCurrentActivity().getIntent()); + promise.resolve(notificationOpenMap); } } @@ -173,10 +173,10 @@ public class RNFirebaseNotifications extends ReactContextBaseJavaModule implemen @Override public void onNewIntent(Intent intent) { - WritableMap notificationOpenedMap = parseIntentForRemoteNotification(intent); - if (notificationOpenedMap != null) { + WritableMap notificationOpenMap = parseIntentForRemoteNotification(intent); + if (notificationOpenMap != null) { Log.d(TAG, "onNewIntent called with new remote notification"); - Utils.sendEvent(getReactApplicationContext(), "notifications_notification_opened", notificationOpenedMap); + Utils.sendEvent(getReactApplicationContext(), "notifications_notification_opened", notificationOpenMap); } } ////////////////////////////////////////////////////////////////////// @@ -231,11 +231,11 @@ public class RNFirebaseNotifications extends ReactContextBaseJavaModule implemen } notificationMap.putMap("data", dataMap); - WritableMap notificationOpenedMap = Arguments.createMap(); - notificationOpenedMap.putString("action", intent.getAction()); - notificationOpenedMap.putMap("notification", notificationMap); + WritableMap notificationOpenMap = Arguments.createMap(); + notificationOpenMap.putString("action", intent.getAction()); + notificationOpenMap.putMap("notification", notificationMap); - return notificationOpenedMap; + return notificationOpenMap; } private WritableMap parseNotificationBundle(Bundle notification) { diff --git a/lib/modules/notifications/Notification.js b/lib/modules/notifications/Notification.js index ad38e9e1..37ccb2bc 100644 --- a/lib/modules/notifications/Notification.js +++ b/lib/modules/notifications/Notification.js @@ -9,7 +9,7 @@ import { generatePushID, isObject } from '../../utils'; import type { NativeNotification } from './types'; -export type NotificationOpened = { +export type NotificationOpen = { action: string, notification: Notification, }; diff --git a/lib/modules/notifications/index.js b/lib/modules/notifications/index.js index 649831a9..3f862a31 100644 --- a/lib/modules/notifications/index.js +++ b/lib/modules/notifications/index.js @@ -22,10 +22,10 @@ import { } from './types'; import type App from '../core/app'; -import type { NotificationOpened } from './Notification'; +import type { NotificationOpen } from './Notification'; import type { NativeNotification, - NativeNotificationOpened, + NativeNotificationOpen, Schedule, } from './types'; @@ -35,10 +35,10 @@ type OnNotificationObserver = { next: OnNotification, }; -type OnNotificationOpened = NotificationOpened => any; +type OnNotificationOpened = NotificationOpen => any; type OnNotificationOpenedObserver = { - next: OnNotificationOpened, + next: OnNotificationOpen, }; const NATIVE_EVENTS = [ @@ -95,10 +95,10 @@ export default class Notifications extends ModuleBase { // sub to internal native event - this fans out to // public event name: onNotificationOpened 'notifications_notification_opened', - (notificationOpened: NativeNotificationOpened) => { + (notificationOpen: NativeNotificationOpen) => { SharedEventEmitter.emit('onNotificationOpened', { - action: notificationOpened.action, - notification: new Notification(notificationOpened.notification), + action: notificationOpen.action, + notification: new Notification(notificationOpen.notification), }); } ); diff --git a/lib/modules/notifications/types.js b/lib/modules/notifications/types.js index a9c9f107..5a77c74a 100644 --- a/lib/modules/notifications/types.js +++ b/lib/modules/notifications/types.js @@ -172,7 +172,7 @@ export type NativeNotification = {| title: string, |}; -export type NativeNotificationOpened = {| +export type NativeNotificationOpen = {| action: string, notification: NativeNotification, |}; From a6734fa4397271726219d8ebf22bed90b31d380c Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Wed, 7 Mar 2018 14:33:16 +0000 Subject: [PATCH 42/77] [auth][types] Fix incorrect return type for createUserAndRetrieveDataWithEmailAndPassword #862 --- lib/modules/auth/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/auth/index.js b/lib/modules/auth/index.js index e7cbe2ed..4288a7e0 100644 --- a/lib/modules/auth/index.js +++ b/lib/modules/auth/index.js @@ -251,7 +251,7 @@ export default class Auth extends ModuleBase { createUserAndRetrieveDataWithEmailAndPassword( email: string, password: string - ): Promise { + ): Promise { return getNativeModule(this) .createUserAndRetrieveDataWithEmailAndPassword(email, password) .then(userCredential => this._setUserCredential(userCredential)); From 57ffa9bd3e4f19c57e633297bee03baa8a6999da Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Wed, 7 Mar 2018 18:29:53 +0000 Subject: [PATCH 43/77] [notifications] Fix some android issues with local notifications --- .../java/io/invertase/firebase/Utils.java | 24 ++++++++ .../RNFirebaseNotificationManager.java | 55 ++++++++++++------- .../RNFirebaseNotifications.java | 52 ++++++++---------- .../notifications/AndroidNotification.js | 10 ++++ lib/modules/notifications/index.js | 16 ++++-- .../android/app/src/main/AndroidManifest.xml | 10 ++++ tests/src/firebase.js | 9 ++- 7 files changed, 119 insertions(+), 57 deletions(-) diff --git a/android/src/main/java/io/invertase/firebase/Utils.java b/android/src/main/java/io/invertase/firebase/Utils.java index 69c82a75..cf05b9be 100644 --- a/android/src/main/java/io/invertase/firebase/Utils.java +++ b/android/src/main/java/io/invertase/firebase/Utils.java @@ -1,5 +1,7 @@ package io.invertase.firebase; +import android.app.ActivityManager; +import android.content.Context; import android.support.annotation.Nullable; import android.util.Log; @@ -522,4 +524,26 @@ public class Utils { } return deconstructedList; } + + public static boolean isAppInForeground(Context context) { + /** + We need to check if app is in foreground otherwise the app will crash. + http://stackoverflow.com/questions/8489993/check-android-application-is-in-foreground-or-not + **/ + ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + List appProcesses = + activityManager.getRunningAppProcesses(); + if (appProcesses == null) { + return false; + } + final String packageName = context.getPackageName(); + for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) { + if (appProcess.importance == + ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND && + appProcess.processName.equals(packageName)) { + return true; + } + } + return false; + } } diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java index b99be159..310f63a9 100644 --- a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java @@ -23,6 +23,7 @@ import android.util.Log; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; @@ -36,6 +37,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import io.invertase.firebase.Utils; import io.invertase.firebase.messaging.BundleJSONConverter; public class RNFirebaseNotificationManager { @@ -44,10 +46,15 @@ public class RNFirebaseNotificationManager { private static final String TAG = "RNFNotificationManager"; private AlarmManager alarmManager; private Context context; - private boolean isForeground = false; + private ReactApplicationContext reactContext; private NotificationManager notificationManager; private SharedPreferences preferences; + public RNFirebaseNotificationManager(ReactApplicationContext reactContext) { + this(reactContext.getApplicationContext()); + this.reactContext = reactContext; + } + public RNFirebaseNotificationManager(Context context) { this.alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); this.context = context; @@ -117,11 +124,6 @@ public class RNFirebaseNotificationManager { } public void displayScheduledNotification(Bundle notification) { - // Broadcast the notification to the RN Application - Intent scheduledNotificationEvent = new Intent(SCHEDULED_NOTIFICATION_EVENT); - scheduledNotificationEvent.putExtra("notification", notification); - LocalBroadcastManager.getInstance(context).sendBroadcast(scheduledNotificationEvent); - // If this isn't a repeated notification, clear it from the scheduled notifications list if (!notification.getBundle("schedule").containsKey("repeated") || !notification.getBundle("schedule").getBoolean("repeated")) { @@ -129,9 +131,14 @@ public class RNFirebaseNotificationManager { preferences.edit().remove(notificationId).apply();; } - // If the app isn't in the foreground, then we display it - // Otherwise, it is up to the JS to decide whether to send the notification - if (!isForeground) { + if (Utils.isAppInForeground(context)) { + // If the app is in the foregound, broadcast the notification to the RN Application + // It is up to the JS to decide whether to display the notification + Intent scheduledNotificationEvent = new Intent(SCHEDULED_NOTIFICATION_EVENT); + scheduledNotificationEvent.putExtra("notification", notification); + LocalBroadcastManager.getInstance(context).sendBroadcast(scheduledNotificationEvent); + } else { + // If the app is in the background, then we display it automatically displayNotification(notification, null); } } @@ -175,10 +182,6 @@ public class RNFirebaseNotificationManager { scheduleNotification(notificationBundle, promise); } - public void setIsForeground(boolean isForeground) { - this.isForeground = isForeground; - } - private void cancelAlarm(String notificationId) { Intent notificationIntent = new Intent(context, RNFirebaseNotificationManager.class); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, notificationId.hashCode(), notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); @@ -201,7 +204,6 @@ public class RNFirebaseNotificationManager { String notificationId = notification.getString("notificationId"); NotificationCompat.Builder nb; - // TODO: Change 27 to 'Build.VERSION_CODES.O_MR1' when using appsupport v27 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { nb = new NotificationCompat.Builder(context, channelId); } else { @@ -343,20 +345,27 @@ public class RNFirebaseNotificationManager { nb = nb.setTicker(android.getString("ticker")); } if (android.containsKey("timeoutAfter")) { - nb = nb.setTimeoutAfter(android.getLong("timeoutAfter")); + Double timeoutAfter = android.getDouble("timeoutAfter"); + nb = nb.setTimeoutAfter(timeoutAfter.longValue()); } if (android.containsKey("usesChronometer")) { nb = nb.setUsesChronometer(android.getBoolean("usesChronometer")); } if (android.containsKey("vibrate")) { - nb = nb.setVibrate(android.getLongArray("vibrate")); + double[] vibrate = android.getDoubleArray("vibrate"); + long[] vibrateArray = new long[vibrate.length]; + for (int i = 0; i < vibrate.length; i++) { + vibrateArray[i] = ((Double)vibrate[i]).longValue(); + } + nb = nb.setVibrate(vibrateArray); } if (android.containsKey("visibility")) { Double visibility = android.getDouble("visibility"); nb = nb.setVisibility(visibility.intValue()); } if (android.containsKey("when")) { - nb = nb.setWhen(android.getLong("when")); + Double when = android.getDouble("when"); + nb = nb.setWhen(when.longValue()); } // TODO: Big text / Big picture @@ -393,6 +402,10 @@ public class RNFirebaseNotificationManager { // Build the notification and send it Notification builtNotification = nb.build(); notificationManager.notify(notificationId.hashCode(), builtNotification); + + if (reactContext != null) { + Utils.sendEvent(reactContext, "notifications_notification_displayed", Arguments.fromBundle(notification)); + } } catch (Exception e) { if (promise == null) { Log.e(TAG, "Failed to send notification", e); @@ -525,7 +538,7 @@ public class RNFirebaseNotificationManager { String notificationId = notification.getString("notificationId"); Bundle schedule = notification.getBundle("schedule"); - long fireDate = schedule.getLong("fireDate"); + Double fireDate = schedule.getDouble("fireDate"); // Scheduled alarms are cleared on restart // We store them so that they can be re-scheduled when the phone restarts in RNFirebaseNotificationsRebootReceiver @@ -567,14 +580,14 @@ public class RNFirebaseNotificationManager { return; } - alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, fireDate, interval, pendingIntent); + alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, fireDate.longValue(), interval, pendingIntent); } else { if (schedule.containsKey("exact") && schedule.getBoolean("exact") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - alarmManager.setExact(AlarmManager.RTC_WAKEUP, fireDate, pendingIntent); + alarmManager.setExact(AlarmManager.RTC_WAKEUP, fireDate.longValue(), pendingIntent); } else { - alarmManager.set(AlarmManager.RTC_WAKEUP, fireDate, pendingIntent); + alarmManager.set(AlarmManager.RTC_WAKEUP, fireDate.longValue(), pendingIntent); } } diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java index 2e9de173..879be98a 100644 --- a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java @@ -30,7 +30,7 @@ import io.invertase.firebase.Utils; import io.invertase.firebase.messaging.RNFirebaseMessagingService; import me.leolin.shortcutbadger.ShortcutBadger; -public class RNFirebaseNotifications extends ReactContextBaseJavaModule implements ActivityEventListener, LifecycleEventListener { +public class RNFirebaseNotifications extends ReactContextBaseJavaModule implements ActivityEventListener { private static final String BADGE_FILE = "BadgeCountFile"; private static final String BADGE_KEY = "BadgeCount"; private static final String TAG = "RNFirebaseNotifications"; @@ -41,9 +41,8 @@ public class RNFirebaseNotifications extends ReactContextBaseJavaModule implemen public RNFirebaseNotifications(ReactApplicationContext context) { super(context); context.addActivityEventListener(this); - context.addLifecycleEventListener(this); - notificationManager = new RNFirebaseNotificationManager(context.getApplicationContext()); + notificationManager = new RNFirebaseNotificationManager(context); sharedPreferences = context.getSharedPreferences(BADGE_FILE, Context.MODE_PRIVATE); LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context); @@ -86,13 +85,11 @@ public class RNFirebaseNotifications extends ReactContextBaseJavaModule implemen @ReactMethod public void getInitialNotification(Promise promise) { - // TODO - if (getCurrentActivity() == null) { - promise.resolve(null); - } else { - WritableMap notificationOpenMap = parseIntentForRemoteNotification(getCurrentActivity().getIntent()); - promise.resolve(notificationOpenMap); + WritableMap notificationOpenMap = null; + if (getCurrentActivity() != null) { + notificationOpenMap = parseIntentForNotification(getCurrentActivity().getIntent()); } + promise.resolve(notificationOpenMap); } @ReactMethod @@ -173,37 +170,36 @@ public class RNFirebaseNotifications extends ReactContextBaseJavaModule implemen @Override public void onNewIntent(Intent intent) { - WritableMap notificationOpenMap = parseIntentForRemoteNotification(intent); + WritableMap notificationOpenMap = parseIntentForNotification(intent); if (notificationOpenMap != null) { - Log.d(TAG, "onNewIntent called with new remote notification"); Utils.sendEvent(getReactApplicationContext(), "notifications_notification_opened", notificationOpenMap); } } + ////////////////////////////////////////////////////////////////////// // End ActivityEventListener methods ////////////////////////////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////// - // Start LifecycleEventListener methods - ////////////////////////////////////////////////////////////////////// - @Override - public void onHostResume() { - notificationManager.setIsForeground(true); + private WritableMap parseIntentForNotification(Intent intent) { + WritableMap notificationOpenMap = parseIntentForRemoteNotification(intent); + if (notificationOpenMap == null) { + notificationOpenMap = parseIntentForLocalNotification(intent); + } + return notificationOpenMap; } - @Override - public void onHostPause() { - notificationManager.setIsForeground(false); - } + private WritableMap parseIntentForLocalNotification(Intent intent) { + if (intent.getExtras() == null || !intent.hasExtra("notificationId")) { + return null; + } - @Override - public void onHostDestroy() { - // Do nothing - } - ////////////////////////////////////////////////////////////////////// - // End LifecycleEventListener methods - ////////////////////////////////////////////////////////////////////// + WritableMap notificationMap = Arguments.makeNativeMap(intent.getExtras()); + WritableMap notificationOpenMap = Arguments.createMap(); + notificationOpenMap.putString("action", intent.getAction()); + notificationOpenMap.putMap("notification", notificationMap); + return notificationOpenMap; + } private WritableMap parseIntentForRemoteNotification(Intent intent) { // Check if FCM data exists diff --git a/lib/modules/notifications/AndroidNotification.js b/lib/modules/notifications/AndroidNotification.js index 2b33d4d2..4fadb223 100644 --- a/lib/modules/notifications/AndroidNotification.js +++ b/lib/modules/notifications/AndroidNotification.js @@ -293,6 +293,16 @@ export default class AndroidNotification { return this._notification; } + /** + * + * @param clickAction + * @returns {Notification} + */ + setClickAction(clickAction: string): Notification { + this._clickAction = clickAction; + return this._notification; + } + /** * * @param color diff --git a/lib/modules/notifications/index.js b/lib/modules/notifications/index.js index 3f862a31..662d137a 100644 --- a/lib/modules/notifications/index.js +++ b/lib/modules/notifications/index.js @@ -38,7 +38,7 @@ type OnNotificationObserver = { type OnNotificationOpened = NotificationOpen => any; type OnNotificationOpenedObserver = { - next: OnNotificationOpen, + next: NotificationOpen, }; const NATIVE_EVENTS = [ @@ -158,12 +158,18 @@ export default class Notifications extends ModuleBase { return getNativeModule(this).getBadge(); } - getInitialNotification(): Promise { + getInitialNotification(): Promise { return getNativeModule(this) .getInitialNotification() - .then( - notification => (notification ? new Notification(notification) : null) - ); + .then((notificationOpen: NativeNotificationOpen) => { + if (notificationOpen) { + return { + action: notificationOpen.action, + notification: new Notification(notificationOpen.notification), + }; + } + return null; + }); } /** diff --git a/tests/android/app/src/main/AndroidManifest.xml b/tests/android/app/src/main/AndroidManifest.xml index 0d75e277..aa4f2cd3 100644 --- a/tests/android/app/src/main/AndroidManifest.xml +++ b/tests/android/app/src/main/AndroidManifest.xml @@ -30,6 +30,16 @@ + + + + + + + + + + { notification .setTitle('Test title') .setBody('Test body') + .setNotificationId('displayed') .android.setChannelId('test') + .android.setClickAction('action') .android.setPriority(RNfirebase.notifications.Android.Priority.Max); const date = new Date(); date.setMinutes(date.getMinutes() + 1); setTimeout(() => { RNfirebase.notifications().displayNotification(notification); + notification.setNotificationId('scheduled'); + RNfirebase.notifications().scheduleNotification(notification, { + fireDate: date.getTime(), + }); }, 5); - RNfirebase.notifications().scheduleNotification(notification, { - fireDate: date.getTime(), - }); } catch (error) { console.error('messaging init error:', error); } From 7ce7f5ae589ce5b02e8fae6a71c732c39dc70a27 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 8 Mar 2018 09:28:27 +0000 Subject: [PATCH 44/77] [messaging] Support FCM data-only messages in the background --- .../messaging/MessagingSerializer.java | 43 +++++++++++++++++++ .../RNFirebaseBackgroundMessagingService.java | 30 +++++++++++++ .../messaging/RNFirebaseMessaging.java | 38 +--------------- .../messaging/RNFirebaseMessagingService.java | 36 ++++++++++------ .../android/app/src/main/AndroidManifest.xml | 2 + tests/index.js | 6 +++ tests/src/bgMessaging.js | 17 ++++++++ 7 files changed, 123 insertions(+), 49 deletions(-) create mode 100644 android/src/main/java/io/invertase/firebase/messaging/MessagingSerializer.java create mode 100644 android/src/main/java/io/invertase/firebase/messaging/RNFirebaseBackgroundMessagingService.java create mode 100644 tests/src/bgMessaging.js diff --git a/android/src/main/java/io/invertase/firebase/messaging/MessagingSerializer.java b/android/src/main/java/io/invertase/firebase/messaging/MessagingSerializer.java new file mode 100644 index 00000000..2bba2ec0 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/messaging/MessagingSerializer.java @@ -0,0 +1,43 @@ +package io.invertase.firebase.messaging; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.google.firebase.messaging.RemoteMessage; + +import java.util.Map; + + +public class MessagingSerializer { + public static WritableMap parseRemoteMessage(RemoteMessage message) { + WritableMap messageMap = Arguments.createMap(); + WritableMap dataMap = Arguments.createMap(); + + if (message.getCollapseKey() != null) { + messageMap.putString("collapseKey", message.getCollapseKey()); + } + + if (message.getData() != null) { + for (Map.Entry e : message.getData().entrySet()) { + dataMap.putString(e.getKey(), e.getValue()); + } + } + messageMap.putMap("data", dataMap); + + if (message.getFrom() != null) { + messageMap.putString("from", message.getFrom()); + } + if (message.getMessageId() != null) { + messageMap.putString("messageId", message.getMessageId()); + } + if (message.getMessageType() != null) { + messageMap.putString("messageType", message.getMessageType()); + } + messageMap.putDouble("sentTime", message.getSentTime()); + if (message.getTo() != null) { + messageMap.putString("to", message.getTo()); + } + messageMap.putDouble("ttl", message.getTtl()); + + return messageMap; + } +} diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseBackgroundMessagingService.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseBackgroundMessagingService.java new file mode 100644 index 00000000..de3c1382 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseBackgroundMessagingService.java @@ -0,0 +1,30 @@ +package io.invertase.firebase.messaging; + +import android.content.Intent; +import android.os.Bundle; + +import com.facebook.react.HeadlessJsTaskService; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.jstasks.HeadlessJsTaskConfig; +import com.google.firebase.messaging.RemoteMessage; + +import javax.annotation.Nullable; + +public class RNFirebaseBackgroundMessagingService extends HeadlessJsTaskService { + @Override + protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) { + Bundle extras = intent.getExtras(); + if (extras != null) { + RemoteMessage message = intent.getParcelableExtra("message"); + WritableMap messageMap = MessagingSerializer.parseRemoteMessage(message); + return new HeadlessJsTaskConfig( + "RNFirebaseBackgroundMessage", + messageMap, + 10000, + false + ); + } + return null; + } +} diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java index c133d0a5..5e7c7067 100644 --- a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java +++ b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java @@ -7,7 +7,6 @@ import android.content.IntentFilter; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; -import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; @@ -21,8 +20,6 @@ import com.google.firebase.messaging.RemoteMessage; import io.invertase.firebase.Utils; -import java.util.Map; - public class RNFirebaseMessaging extends ReactContextBaseJavaModule { private static final String TAG = "RNFirebaseMessaging"; @@ -115,44 +112,11 @@ public class RNFirebaseMessaging extends ReactContextBaseJavaModule { Log.d(TAG, "Received new message"); RemoteMessage message = intent.getParcelableExtra("message"); - WritableMap messageMap = buildMessageMap(message); + WritableMap messageMap = MessagingSerializer.parseRemoteMessage(message); Utils.sendEvent(getReactApplicationContext(), "messaging_message_received", messageMap); } } - - private WritableMap buildMessageMap(RemoteMessage message) { - WritableMap messageMap = Arguments.createMap(); - WritableMap dataMap = Arguments.createMap(); - - if (message.getCollapseKey() != null) { - messageMap.putString("collapseKey", message.getCollapseKey()); - } - - if (message.getData() != null) { - for (Map.Entry e : message.getData().entrySet()) { - dataMap.putString(e.getKey(), e.getValue()); - } - } - messageMap.putMap("data", dataMap); - - if (message.getFrom() != null) { - messageMap.putString("from", message.getFrom()); - } - if (message.getMessageId() != null) { - messageMap.putString("messageId", message.getMessageId()); - } - if (message.getMessageType() != null) { - messageMap.putString("messageType", message.getMessageType()); - } - messageMap.putDouble("sentTime", message.getSentTime()); - if (message.getTo() != null) { - messageMap.putString("to", message.getTo()); - } - messageMap.putDouble("ttl", message.getTtl()); - - return messageMap; - } } private class RefreshTokenReceiver extends BroadcastReceiver { diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessagingService.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessagingService.java index 662ed5c5..6ba60c25 100644 --- a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessagingService.java +++ b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessagingService.java @@ -4,9 +4,12 @@ import android.content.Intent; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; +import com.facebook.react.HeadlessJsTaskService; import com.google.firebase.messaging.FirebaseMessagingService; import com.google.firebase.messaging.RemoteMessage; +import io.invertase.firebase.Utils; + public class RNFirebaseMessagingService extends FirebaseMessagingService { private static final String TAG = "RNFMessagingService"; public static final String MESSAGE_EVENT = "messaging-message"; @@ -16,19 +19,28 @@ public class RNFirebaseMessagingService extends FirebaseMessagingService { public void onMessageReceived(RemoteMessage message) { Log.d(TAG, "onMessageReceived event received"); - Intent event; - if (message.getNotification() != null) { - // It's a notification, pass to the notification module - event = new Intent(REMOTE_NOTIFICATION_EVENT); - event.putExtra("notification", message); - } else { - // It's a data message, pass to the messaging module - event = new Intent(MESSAGE_EVENT); - event.putExtra("message", message); - } + // It's a notification, pass to the Notifications module + Intent notificationEvent = new Intent(REMOTE_NOTIFICATION_EVENT); + notificationEvent.putExtra("notification", message); - // Broadcast it so it is only available to the RN Application - LocalBroadcastManager.getInstance(this).sendBroadcast(event); + // Broadcast it to the (foreground) RN Application + LocalBroadcastManager.getInstance(this).sendBroadcast(notificationEvent); + } else { + // It's a data message + // If the app is in the foreground we send it to the Messaging module + if (Utils.isAppInForeground(this.getApplicationContext())) { + Intent messagingEvent = new Intent(MESSAGE_EVENT); + messagingEvent.putExtra("message", message); + // Broadcast it so it is only available to the RN Application + LocalBroadcastManager.getInstance(this).sendBroadcast(messagingEvent); + } else { + // If the app is in the background we send it to the Headless JS Service + Intent headlessIntent = new Intent(this.getApplicationContext(), RNFirebaseBackgroundMessagingService.class); + headlessIntent.putExtra("message", message); + this.getApplicationContext().startService(headlessIntent); + HeadlessJsTaskService.acquireWakeLockNow(this.getApplicationContext()); + } + } } } diff --git a/tests/android/app/src/main/AndroidManifest.xml b/tests/android/app/src/main/AndroidManifest.xml index aa4f2cd3..08d12b4d 100644 --- a/tests/android/app/src/main/AndroidManifest.xml +++ b/tests/android/app/src/main/AndroidManifest.xml @@ -30,6 +30,8 @@ + + diff --git a/tests/index.js b/tests/index.js index 597382bc..744aa791 100644 --- a/tests/index.js +++ b/tests/index.js @@ -1,4 +1,10 @@ import { AppRegistry } from 'react-native'; import bootstrap from './src/main'; +import bgMessaging from './src/bgMessaging'; AppRegistry.registerComponent('ReactNativeFirebaseDemo', () => bootstrap); +// Task registered to handle background data-only FCM messages +AppRegistry.registerHeadlessTask( + 'RNFirebaseBackgroundMessage', + () => bgMessaging +); diff --git a/tests/src/bgMessaging.js b/tests/src/bgMessaging.js new file mode 100644 index 00000000..3a633122 --- /dev/null +++ b/tests/src/bgMessaging.js @@ -0,0 +1,17 @@ +import RNfirebase from './../firebase'; + +export default async message => { + console.log('Message', message); + + const notification = new RNfirebase.notifications.Notification(); + notification + .setTitle('Background notification') + .setBody('Background body') + .setNotificationId('background') + .android.setChannelId('test') + .android.setClickAction('action') + .android.setPriority(RNfirebase.notifications.Android.Priority.Max); + + await RNfirebase.notifications().displayNotification(notification); + return Promise.resolve(); +}; From 7ff5b3c38248f3bf062838b8ee15be695111b5b1 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 8 Mar 2018 09:56:22 +0000 Subject: [PATCH 45/77] [tests] Bump all test pod dependencies --- tests/ios/Podfile.lock | 202 ++++++++++++++++++++--------------------- 1 file changed, 101 insertions(+), 101 deletions(-) diff --git a/tests/ios/Podfile.lock b/tests/ios/Podfile.lock index 44e1d4b7..4083cbeb 100644 --- a/tests/ios/Podfile.lock +++ b/tests/ios/Podfile.lock @@ -1,59 +1,59 @@ PODS: - - BoringSSL (9.2): - - BoringSSL/Implementation (= 9.2) - - BoringSSL/Interface (= 9.2) - - BoringSSL/Implementation (9.2): - - BoringSSL/Interface (= 9.2) - - BoringSSL/Interface (9.2) + - BoringSSL (10.0): + - BoringSSL/Implementation (= 10.0) + - BoringSSL/Interface (= 10.0) + - BoringSSL/Implementation (10.0): + - BoringSSL/Interface (= 10.0) + - BoringSSL/Interface (10.0) - Crashlytics (3.9.3): - Fabric (~> 1.7.2) - - Fabric (1.7.3) - - Firebase/AdMob (4.8.2): + - Fabric (1.7.5) + - Firebase/AdMob (4.10.0): - Firebase/Core - - Google-Mobile-Ads-SDK (= 7.27.0) - - Firebase/Auth (4.8.2): + - Google-Mobile-Ads-SDK (= 7.29.0) + - Firebase/Auth (4.10.0): - Firebase/Core - - FirebaseAuth (= 4.4.2) - - Firebase/Core (4.8.2): - - FirebaseAnalytics (= 4.0.9) - - FirebaseCore (= 4.0.14) - - Firebase/Crash (4.8.2): + - FirebaseAuth (= 4.4.4) + - Firebase/Core (4.10.0): + - FirebaseAnalytics (= 4.1.0) + - FirebaseCore (= 4.0.16) + - Firebase/Crash (4.10.0): - Firebase/Core - FirebaseCrash (= 2.0.2) - - Firebase/Database (4.8.2): + - Firebase/Database (4.10.0): - Firebase/Core - - FirebaseDatabase (= 4.1.4) - - Firebase/DynamicLinks (4.8.2): + - FirebaseDatabase (= 4.1.5) + - Firebase/DynamicLinks (4.10.0): - Firebase/Core - FirebaseDynamicLinks (= 2.3.2) - - Firebase/Firestore (4.8.2): + - Firebase/Firestore (4.10.0): - Firebase/Core - - FirebaseFirestore (= 0.10.0) - - Firebase/Messaging (4.8.2): + - FirebaseFirestore (= 0.10.2) + - Firebase/Messaging (4.10.0): - Firebase/Core - - FirebaseMessaging (= 2.0.8) - - Firebase/Performance (4.8.2): + - FirebaseMessaging (= 2.1.1) + - Firebase/Performance (4.10.0): - Firebase/Core - - FirebasePerformance (= 1.1.1) - - Firebase/RemoteConfig (4.8.2): + - FirebasePerformance (= 1.1.2) + - Firebase/RemoteConfig (4.10.0): - Firebase/Core - - FirebaseRemoteConfig (= 2.1.1) - - Firebase/Storage (4.8.2): + - FirebaseRemoteConfig (= 2.1.2) + - Firebase/Storage (4.10.0): - Firebase/Core - - FirebaseStorage (= 2.1.2) + - FirebaseStorage (= 2.1.3) - FirebaseABTesting (1.0.0): - FirebaseCore (~> 4.0) - Protobuf (~> 3.1) - - FirebaseAnalytics (4.0.9): + - FirebaseAnalytics (4.1.0): - FirebaseCore (~> 4.0) - FirebaseInstanceID (~> 2.0) - GoogleToolboxForMac/NSData+zlib (~> 2.1) - nanopb (~> 0.3) - - FirebaseAuth (4.4.2): - - FirebaseAnalytics (~> 4.0) + - FirebaseAuth (4.4.4): + - FirebaseAnalytics (~> 4.1) - GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1) - GTMSessionFetcher/Core (~> 1.1) - - FirebaseCore (4.0.14): + - FirebaseCore (4.0.16): - GoogleToolboxForMac/NSData+zlib (~> 2.1) - FirebaseCrash (2.0.2): - FirebaseAnalytics (~> 4.0) @@ -61,47 +61,47 @@ PODS: - GoogleToolboxForMac/Logger (~> 2.1) - GoogleToolboxForMac/NSData+zlib (~> 2.1) - Protobuf (~> 3.1) - - FirebaseDatabase (4.1.4): - - FirebaseAnalytics (~> 4.0) + - FirebaseDatabase (4.1.5): + - FirebaseAnalytics (~> 4.1) - FirebaseCore (~> 4.0) - leveldb-library (~> 1.18) - FirebaseDynamicLinks (2.3.2): - FirebaseAnalytics (~> 4.0) - - FirebaseFirestore (0.10.0): - - FirebaseAnalytics (~> 4.0) + - FirebaseFirestore (0.10.2): + - FirebaseAnalytics (~> 4.1) - FirebaseCore (~> 4.0) - gRPC-ProtoRPC (~> 1.0) - leveldb-library (~> 1.18) - - Protobuf (~> 3.1) - - FirebaseInstanceID (2.0.8): + - Protobuf (~> 3.5) + - FirebaseInstanceID (2.0.9): - FirebaseCore (~> 4.0) - - FirebaseMessaging (2.0.8): - - FirebaseAnalytics (~> 4.0) + - FirebaseMessaging (2.1.1): + - FirebaseAnalytics (~> 4.1) - FirebaseCore (~> 4.0) - FirebaseInstanceID (~> 2.0) - GoogleToolboxForMac/Logger (~> 2.1) - - Protobuf (~> 3.1) - - FirebasePerformance (1.1.1): - - FirebaseAnalytics (~> 4.0) + - Protobuf (~> 3.5) + - FirebasePerformance (1.1.2): + - FirebaseAnalytics (~> 4.1) - FirebaseInstanceID (~> 2.0) - FirebaseSwizzlingUtilities (~> 1.0) - GoogleToolboxForMac/Logger (~> 2.1) - GoogleToolboxForMac/NSData+zlib (~> 2.1) - GTMSessionFetcher/Core (~> 1.1) - - Protobuf (~> 3.1) - - FirebaseRemoteConfig (2.1.1): + - Protobuf (~> 3.5) + - FirebaseRemoteConfig (2.1.2): - FirebaseABTesting (~> 1.0) - FirebaseAnalytics (~> 4.0) - FirebaseCore (~> 4.0) - FirebaseInstanceID (~> 2.0) - GoogleToolboxForMac/NSData+zlib (~> 2.1) - Protobuf (~> 3.1) - - FirebaseStorage (2.1.2): - - FirebaseAnalytics (~> 4.0) + - FirebaseStorage (2.1.3): + - FirebaseAnalytics (~> 4.1) - FirebaseCore (~> 4.0) - GTMSessionFetcher/Core (~> 1.1) - FirebaseSwizzlingUtilities (1.0.0) - - Google-Mobile-Ads-SDK (7.27.0) + - Google-Mobile-Ads-SDK (7.29.0) - GoogleToolboxForMac/DebugUtils (2.1.3): - GoogleToolboxForMac/Defines (= 2.1.3) - GoogleToolboxForMac/Defines (2.1.3) @@ -114,26 +114,26 @@ PODS: - GoogleToolboxForMac/Defines (= 2.1.3) - GoogleToolboxForMac/NSString+URLArguments (= 2.1.3) - GoogleToolboxForMac/NSString+URLArguments (2.1.3) - - gRPC (1.9.1): - - gRPC-RxLibrary (= 1.9.1) - - gRPC/Main (= 1.9.1) - - gRPC-Core (1.9.1): - - gRPC-Core/Implementation (= 1.9.1) - - gRPC-Core/Interface (= 1.9.1) - - gRPC-Core/Implementation (1.9.1): - - BoringSSL (~> 9.0) - - gRPC-Core/Interface (= 1.9.1) + - gRPC (1.10.0): + - gRPC-RxLibrary (= 1.10.0) + - gRPC/Main (= 1.10.0) + - gRPC-Core (1.10.0): + - gRPC-Core/Implementation (= 1.10.0) + - gRPC-Core/Interface (= 1.10.0) + - gRPC-Core/Implementation (1.10.0): + - BoringSSL (~> 10.0) + - gRPC-Core/Interface (= 1.10.0) - nanopb (~> 0.3) - - gRPC-Core/Interface (1.9.1) - - gRPC-ProtoRPC (1.9.1): - - gRPC (= 1.9.1) - - gRPC-RxLibrary (= 1.9.1) + - gRPC-Core/Interface (1.10.0) + - gRPC-ProtoRPC (1.10.0): + - gRPC (= 1.10.0) + - gRPC-RxLibrary (= 1.10.0) - Protobuf (~> 3.0) - - gRPC-RxLibrary (1.9.1) - - gRPC/Main (1.9.1): - - gRPC-Core (= 1.9.1) - - gRPC-RxLibrary (= 1.9.1) - - GTMSessionFetcher/Core (1.1.13) + - gRPC-RxLibrary (1.10.0) + - gRPC/Main (1.10.0): + - gRPC-Core (= 1.10.0) + - gRPC-RxLibrary (= 1.10.0) + - GTMSessionFetcher/Core (1.1.14) - leveldb-library (1.20) - nanopb (0.3.8): - nanopb/decode (= 0.3.8) @@ -141,32 +141,32 @@ PODS: - nanopb/decode (0.3.8) - nanopb/encode (0.3.8) - Protobuf (3.5.0) - - React (0.52.0): - - React/Core (= 0.52.0) - - React/BatchedBridge (0.52.0): + - React (0.52.3): + - React/Core (= 0.52.3) + - React/BatchedBridge (0.52.3): - React/Core - React/cxxreact_legacy - - React/Core (0.52.0): - - yoga (= 0.52.0.React) - - React/cxxreact_legacy (0.52.0): + - React/Core (0.52.3): + - yoga (= 0.52.3.React) + - React/cxxreact_legacy (0.52.3): - React/jschelpers_legacy - React/jsinspector_legacy - - React/fishhook (0.52.0) - - React/jschelpers_legacy (0.52.0) - - React/jsinspector_legacy (0.52.0) - - React/RCTBlob (0.52.0): + - React/fishhook (0.52.3) + - React/jschelpers_legacy (0.52.3) + - React/jsinspector_legacy (0.52.3) + - React/RCTBlob (0.52.3): - React/Core - - React/RCTNetwork (0.52.0): + - React/RCTNetwork (0.52.3): - React/Core - - React/RCTText (0.52.0): + - React/RCTText (0.52.3): - React/Core - - React/RCTWebSocket (0.52.0): + - React/RCTWebSocket (0.52.3): - React/Core - React/fishhook - React/RCTBlob - RNFirebase (3.3.0): - React - - yoga (0.52.0.React) + - yoga (0.52.3.React) DEPENDENCIES: - Crashlytics (~> 3.9.3) @@ -199,37 +199,37 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - BoringSSL: f3d6b8ce199b9c450a8cfc14895d07a2627fc232 + BoringSSL: 32764dbaf5f5888cf51fbaa172a010126b41bcd4 Crashlytics: dbb07d01876c171c5ccbdf7826410380189e452c - Fabric: bb495bb9a7a7677c6d03a1f8b83d95bc49b47e41 - Firebase: 7d3b8cd837ad9fcd391657734c0d56dab8e9a5a3 + Fabric: ae7146a5f505ea370a1e44820b4b1dc8890e2890 + Firebase: c98c8b1fbcbdccd82539a36c2b17a9b1bb0ee798 FirebaseABTesting: d07d0ee833b842d5153549e4c7e2e2cb1c23a3f9 - FirebaseAnalytics: 388b630c15713f5dbf364071f5f3d6077fb52f4e - FirebaseAuth: bd2738c5c1e92b108ba5f7f7335908097a4e50bb - FirebaseCore: 2e0b98fb2d64ca8140136beff15772bdd14d2dd7 + FirebaseAnalytics: 3dfae28d4a5e06f86c4fae830efc2ad3fadb19bc + FirebaseAuth: d040bb7a9db6dfc29d0e7ec82d48be51352b2581 + FirebaseCore: eb9e1a56733ff1094ecf3e28af9069c344b25239 FirebaseCrash: cded0fc566c03651aea606a101bc156085f333ca - FirebaseDatabase: de4446507ccd3257fca37d16f40e1540324571fd + FirebaseDatabase: 5f0bc6134c5c237cf55f9e1249d406770a75eafd FirebaseDynamicLinks: 38b68641d24e78d0277a9205d988ce22875d5a25 - FirebaseFirestore: 713f0c555e7af5ac03d0fec0e2477c48857f4977 - FirebaseInstanceID: 81df5805a08001e69138664bdd02c6719a9ac80f - FirebaseMessaging: dfdcd307c2382290a1e297a81d0f18370f5b1bcd - FirebasePerformance: 4e1f8091e400eaf88505234caef5718313653709 - FirebaseRemoteConfig: 3310f264fff78b6c2e78b24dcfc4c1b3d6766209 - FirebaseStorage: 181bb543d39ee3c53e0558de7ba86b1286a0427f + FirebaseFirestore: 9423ca756bbf77bfa3cd02fafc8027ae79da625a + FirebaseInstanceID: d2058a35e9bebda1b6dd42486b84917bde552a9d + FirebaseMessaging: db0e01c52ef7e1f42846431273558107d084ede4 + FirebasePerformance: 96c831a9eaf8d2ddf8bb37a4a6f6dd1b4bfe929f + FirebaseRemoteConfig: df64ce784a45e9b9d4988030a59da6baede8bdb0 + FirebaseStorage: 9a863a2bb96c406958eeff7c2f1dfa9f44c44a13 FirebaseSwizzlingUtilities: f1c49a5a372ac852c853722a5891a0a5e2344a6c - Google-Mobile-Ads-SDK: 83f7f890e638ce8f1debd440ea363338c9f6be3b + Google-Mobile-Ads-SDK: 375bbb821b3df2106c37b74f6b0a97576ddc5f6b GoogleToolboxForMac: 2501e2ad72a52eb3dfe7bd9aee7dad11b858bd20 - gRPC: 58828d611419d49da19ad02a60679ffa10a10a87 - gRPC-Core: 66413bf1f2d038a6221bc7bfcbeeaa5a117cee29 - gRPC-ProtoRPC: f29e8b7445e0d3c0311678ab121e6c164da4ca5e - gRPC-RxLibrary: 8e0067bfe8a054022c7a81470baace4f2f633b48 - GTMSessionFetcher: 5bb1eae636127de695590f50e7d248483eb891e6 + gRPC: f54f0e6d603052b4562447da442ce2ff30bcdacc + gRPC-Core: a030b1678ded49c88ec5ba7c90ee8ee5f47ec6e1 + gRPC-ProtoRPC: 22712b23eb1bda656a59715fa5c1da0ea1493ea4 + gRPC-RxLibrary: a41a4652d220f230ba1c0491a94ce2ee04c6180a + GTMSessionFetcher: 390ea358e5a0d0133153806f744662dad933d06b leveldb-library: '08cba283675b7ed2d99629a4bc5fd052cd2bb6a5' nanopb: 5601e6bca2dbf1ed831b519092ec110f66982ca3 Protobuf: 8a9838fba8dae3389230e1b7f8c104aa32389c03 - React: 61a6bdf17a9ff16875c230e6ff278d9de274e16c + React: c0dfd2dfc970019d1ae7d48bf24cef530992e079 RNFirebase: 056b672391f3e7b572d8ef221c84603970e0c114 - yoga: 646606bf554d54a16711f35596178522fbc00480 + yoga: f45a46b966e1eb0c7a532cfd4beec5b97332ba48 PODFILE CHECKSUM: 67c98bcb203cb992da590bcab6f690f727653ca5 From 168f3b948ef084c83b4526d11a1e8ae5532929b1 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 8 Mar 2018 10:04:53 +0000 Subject: [PATCH 46/77] [tests] Bump crashlytics dependencies --- tests/android/app/build.gradle | 2 +- tests/android/build.gradle | 2 +- tests/ios/Podfile | 4 ++-- tests/ios/Podfile.lock | 12 ++++++------ 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/android/app/build.gradle b/tests/android/app/build.gradle index c7b38a71..38943153 100644 --- a/tests/android/app/build.gradle +++ b/tests/android/app/build.gradle @@ -160,7 +160,7 @@ dependencies { compile "com.google.firebase:firebase-storage:$firebaseVersion" compile "com.google.firebase:firebase-firestore:$firebaseVersion" compile "com.google.firebase:firebase-invites:$firebaseVersion" - compile('com.crashlytics.sdk.android:crashlytics:2.7.1@aar') { + compile('com.crashlytics.sdk.android:crashlytics:2.9.1@aar') { transitive = true } compile "com.android.support:appcompat-v7:26.0.2" diff --git a/tests/android/build.gradle b/tests/android/build.gradle index 7c902675..a720fd72 100644 --- a/tests/android/build.gradle +++ b/tests/android/build.gradle @@ -11,7 +11,7 @@ buildscript { classpath 'com.android.tools.build:gradle:2.3.3' classpath 'com.google.gms:google-services:3.1.2' classpath 'com.google.firebase:firebase-plugins:1.1.1' - classpath 'io.fabric.tools:gradle:1.24.4' + classpath 'io.fabric.tools:gradle:1.25.1' } } diff --git a/tests/ios/Podfile b/tests/ios/Podfile index 9e7e49a2..d305099b 100644 --- a/tests/ios/Podfile +++ b/tests/ios/Podfile @@ -30,8 +30,8 @@ target 'ReactNativeFirebaseDemo' do pod 'Firebase/RemoteConfig' pod 'Firebase/Storage' pod 'Firebase/Performance' - pod 'Fabric', '~> 1.7.2' - pod 'Crashlytics', '~> 3.9.3' + pod 'Fabric', '~> 1.7.5' + pod 'Crashlytics', '~> 3.10.1' pod 'RNFirebase', :path => '../../ios/RNFirebase.podspec' diff --git a/tests/ios/Podfile.lock b/tests/ios/Podfile.lock index 4083cbeb..f0516b2f 100644 --- a/tests/ios/Podfile.lock +++ b/tests/ios/Podfile.lock @@ -5,8 +5,8 @@ PODS: - BoringSSL/Implementation (10.0): - BoringSSL/Interface (= 10.0) - BoringSSL/Interface (10.0) - - Crashlytics (3.9.3): - - Fabric (~> 1.7.2) + - Crashlytics (3.10.1): + - Fabric (~> 1.7.5) - Fabric (1.7.5) - Firebase/AdMob (4.10.0): - Firebase/Core @@ -169,8 +169,8 @@ PODS: - yoga (0.52.3.React) DEPENDENCIES: - - Crashlytics (~> 3.9.3) - - Fabric (~> 1.7.2) + - Crashlytics (~> 3.10.1) + - Fabric (~> 1.7.5) - Firebase/AdMob - Firebase/Auth - Firebase/Core @@ -200,7 +200,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: BoringSSL: 32764dbaf5f5888cf51fbaa172a010126b41bcd4 - Crashlytics: dbb07d01876c171c5ccbdf7826410380189e452c + Crashlytics: aee1a064cbbf99b32efa3f056a5f458d846bc8ff Fabric: ae7146a5f505ea370a1e44820b4b1dc8890e2890 Firebase: c98c8b1fbcbdccd82539a36c2b17a9b1bb0ee798 FirebaseABTesting: d07d0ee833b842d5153549e4c7e2e2cb1c23a3f9 @@ -231,6 +231,6 @@ SPEC CHECKSUMS: RNFirebase: 056b672391f3e7b572d8ef221c84603970e0c114 yoga: f45a46b966e1eb0c7a532cfd4beec5b97332ba48 -PODFILE CHECKSUM: 67c98bcb203cb992da590bcab6f690f727653ca5 +PODFILE CHECKSUM: d20e53c395f89ffcfe49e809a8fd7f54beca0700 COCOAPODS: 1.2.1 From 68cfd24810dae1589e84148524aee78f84101c07 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 8 Mar 2018 10:42:35 +0000 Subject: [PATCH 47/77] [messaging] Fix a couple of flow errors --- lib/modules/messaging/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modules/messaging/index.js b/lib/modules/messaging/index.js index ad46838b..9eaf649f 100644 --- a/lib/modules/messaging/index.js +++ b/lib/modules/messaging/index.js @@ -71,7 +71,7 @@ export default class Messaging extends ModuleBase { onMessage(nextOrObserver: OnMessage | OnMessageObserver): () => any { let listener: RemoteMessage => any; if (isFunction(nextOrObserver)) { - // $FlowBug: Not coping with the overloaded method signature + // $FlowExpectedError: Not coping with the overloaded method signature listener = nextOrObserver; } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { listener = nextOrObserver.next; @@ -96,7 +96,7 @@ export default class Messaging extends ModuleBase { ): () => any { let listener: String => any; if (isFunction(nextOrObserver)) { - // $FlowBug: Not coping with the overloaded method signature + // $FlowExpectedError: Not coping with the overloaded method signature listener = nextOrObserver; } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { listener = nextOrObserver.next; From 7f8a486b40cf53b5a35ddb18c2f873cdf42caf15 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 8 Mar 2018 11:13:21 +0000 Subject: [PATCH 48/77] [types] Fix transaction flow type errors --- lib/modules/firestore/Transaction.js | 4 +- lib/modules/firestore/TransactionHandler.js | 40 ++++++++++++------- tests/ios/Podfile.lock | 14 +++---- .../project.pbxproj | 2 +- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/lib/modules/firestore/Transaction.js b/lib/modules/firestore/Transaction.js index 07db5162..71cd1b5e 100644 --- a/lib/modules/firestore/Transaction.js +++ b/lib/modules/firestore/Transaction.js @@ -16,8 +16,8 @@ import { getNativeModule } from '../../utils/native'; type Command = { type: 'set' | 'update' | 'delete', path: string, - data: ?{ [string]: any }, - options: ?{ merge: boolean }, + data?: { [string]: any }, + options?: SetOptions | {}, }; type SetOptions = { diff --git a/lib/modules/firestore/TransactionHandler.js b/lib/modules/firestore/TransactionHandler.js index d8f0e190..3b4f5761 100644 --- a/lib/modules/firestore/TransactionHandler.js +++ b/lib/modules/firestore/TransactionHandler.js @@ -19,9 +19,9 @@ const generateTransactionId = (): number => transactionId++; export type TransactionMeta = { id: number, - stack: Array, - reject: null | Function, - resolve: null | Function, + stack: string[], + reject?: Function, + resolve?: Function, transaction: Transaction, updateFunction: (transaction: Transaction) => Promise, }; @@ -37,7 +37,12 @@ type TransactionEvent = { */ export default class TransactionHandler { _firestore: Firestore; - _pending: { [number]: TransactionMeta }; + _pending: { + [number]: { + meta: TransactionMeta, + transaction: Transaction, + }, + }; constructor(firestore: Firestore) { this._pending = {}; @@ -62,10 +67,9 @@ export default class TransactionHandler { updateFunction: (transaction: Transaction) => Promise ): Promise { const id = generateTransactionId(); - const meta = { + // $FlowExpectedError: Transaction has to be populated + const meta: TransactionMeta = { id, - reject: null, - resolve: null, updateFunction, stack: new Error().stack .split('\n') @@ -73,8 +77,10 @@ export default class TransactionHandler { .join('\n'), }; - meta.transaction = new Transaction(this._firestore, meta); - this._pending[id] = meta; + this._pending[id] = { + meta, + transaction: new Transaction(this._firestore, meta), + }; // deferred promise return new Promise((resolve, reject) => { @@ -145,7 +151,8 @@ export default class TransactionHandler { // abort if no longer exists js side if (!this._pending[id]) return this._remove(id); - const { updateFunction, transaction, reject } = this._pending[id]; + const { meta, transaction } = this._pending[id]; + const { updateFunction, reject } = meta; // clear any saved state from previous transaction runs transaction._prepare(); @@ -178,6 +185,7 @@ export default class TransactionHandler { // update is failed when either the users updateFunction // throws an error or rejects a promise if (updateFailed) { + // $FlowExpectedError: Reject will always be present return reject(finalError); } @@ -201,17 +209,20 @@ export default class TransactionHandler { */ _handleError(event: TransactionEvent) { const { id, error } = event; - const meta = this._pending[id]; + const { meta } = this._pending[id]; - if (meta) { + if (meta && error) { const { code, message } = error; // build a JS error and replace its stack // with the captured one at start of transaction // so it's actually relevant to the user const errorWithStack = new Error(message); + // $FlowExpectedError: code is needed for Firebase errors errorWithStack.code = code; + // $FlowExpectedError: stack should be a stack trace errorWithStack.stack = meta.stack; + // $FlowExpectedError: Reject will always be present meta.reject(errorWithStack); } } @@ -224,10 +235,11 @@ export default class TransactionHandler { */ _handleComplete(event: TransactionEvent) { const { id } = event; - const meta = this._pending[id]; + const { meta, transaction } = this._pending[id]; if (meta) { - const pendingResult = meta.transaction._pendingResult; + const pendingResult = transaction._pendingResult; + // $FlowExpectedError: Resolve will always be present meta.resolve(pendingResult); } } diff --git a/tests/ios/Podfile.lock b/tests/ios/Podfile.lock index fcb5d645..266af2b2 100644 --- a/tests/ios/Podfile.lock +++ b/tests/ios/Podfile.lock @@ -164,7 +164,7 @@ PODS: - React/Core - React/fishhook - React/RCTBlob - - RNFirebase (3.2.7): + - RNFirebase (3.3.0): - React - yoga (0.52.3.React) @@ -192,11 +192,11 @@ DEPENDENCIES: EXTERNAL SOURCES: React: - :path: ../node_modules/react-native + :path: "../node_modules/react-native" RNFirebase: - :path: ../../ios/RNFirebase.podspec + :path: "../../ios/RNFirebase.podspec" yoga: - :path: ../node_modules/react-native/ReactCommon/yoga + :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: BoringSSL: f3d6b8ce199b9c450a8cfc14895d07a2627fc232 @@ -224,13 +224,13 @@ SPEC CHECKSUMS: gRPC-ProtoRPC: f29e8b7445e0d3c0311678ab121e6c164da4ca5e gRPC-RxLibrary: 8e0067bfe8a054022c7a81470baace4f2f633b48 GTMSessionFetcher: 5bb1eae636127de695590f50e7d248483eb891e6 - leveldb-library: 08cba283675b7ed2d99629a4bc5fd052cd2bb6a5 + leveldb-library: '08cba283675b7ed2d99629a4bc5fd052cd2bb6a5' nanopb: 5601e6bca2dbf1ed831b519092ec110f66982ca3 Protobuf: 8a9838fba8dae3389230e1b7f8c104aa32389c03 React: c0dfd2dfc970019d1ae7d48bf24cef530992e079 - RNFirebase: 3a141a97041ea0757e2036c2bb18acbe9f0e105d + RNFirebase: 056b672391f3e7b572d8ef221c84603970e0c114 yoga: f45a46b966e1eb0c7a532cfd4beec5b97332ba48 PODFILE CHECKSUM: 67c98bcb203cb992da590bcab6f690f727653ca5 -COCOAPODS: 1.3.1 +COCOAPODS: 1.2.1 diff --git a/tests/ios/ReactNativeFirebaseDemo.xcodeproj/project.pbxproj b/tests/ios/ReactNativeFirebaseDemo.xcodeproj/project.pbxproj index b09fdd99..4265ac18 100644 --- a/tests/ios/ReactNativeFirebaseDemo.xcodeproj/project.pbxproj +++ b/tests/ios/ReactNativeFirebaseDemo.xcodeproj/project.pbxproj @@ -1006,7 +1006,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; showEnvVarsInLog = 0; }; 6AE1012F46FF8A4D1D818A12 /* [CP] Copy Pods Resources */ = { From 73d5fe34c4d7a375dfe3087e435ced8bb40468be Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 8 Mar 2018 11:36:26 +0000 Subject: [PATCH 49/77] 3.3.1 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 10c7bba0..e05ea92d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-native-firebase", - "version": "3.3.0", + "version": "3.3.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index ae7009a1..7cb966b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-firebase", - "version": "3.3.0", + "version": "3.3.1", "author": "Invertase (http://invertase.io)", "description": "A well tested, feature rich Firebase implementation for React Native, supporting iOS & Android. Individual module support for Admob, Analytics, Auth, Crash Reporting, Cloud Firestore, Database, Dynamic Links, Messaging (FCM), Remote Config, Storage and Performance.", "main": "dist/index.js", From aa2511cbd391d37b51d00d81e6f9b9f3f08bf017 Mon Sep 17 00:00:00 2001 From: Simon Bengtsson Date: Thu, 8 Mar 2018 14:10:41 +0100 Subject: [PATCH 50/77] [database][js] Reject promise instead of resolving an error object when ref.once('value') fails --- lib/modules/database/Reference.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/database/Reference.js b/lib/modules/database/Reference.js index 9702da0e..67d2c9c5 100644 --- a/lib/modules/database/Reference.js +++ b/lib/modules/database/Reference.js @@ -289,7 +289,7 @@ export default class Reference extends ReferenceBase { }) .catch(error => { if (isFunction(cancelOrContext)) return cancelOrContext(error); - return error; + throw error; }); } From 54567620f7d298595469476238ffb7759fac00d2 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 8 Mar 2018 14:05:34 +0000 Subject: [PATCH 51/77] [messaging] Bump background message timeout to 60s --- .../messaging/RNFirebaseBackgroundMessagingService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseBackgroundMessagingService.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseBackgroundMessagingService.java index de3c1382..b0a0b046 100644 --- a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseBackgroundMessagingService.java +++ b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseBackgroundMessagingService.java @@ -21,7 +21,7 @@ public class RNFirebaseBackgroundMessagingService extends HeadlessJsTaskService return new HeadlessJsTaskConfig( "RNFirebaseBackgroundMessage", messageMap, - 10000, + 60000, false ); } From a6880a00a90064e36f7d3bf93b9a2b79774253a8 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhus Date: Fri, 9 Mar 2018 05:16:40 +0100 Subject: [PATCH 52/77] [types] Use literal values for strings --- lib/modules/config/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modules/config/index.js b/lib/modules/config/index.js index 9f5136f2..777ddecc 100644 --- a/lib/modules/config/index.js +++ b/lib/modules/config/index.js @@ -143,11 +143,11 @@ export default class RemoteConfig extends ModuleBase { * "source" : OneOf(remoteConfigSourceRemote|remoteConfigSourceDefault|remoteConfigSourceStatic) * } */ - getValues(keys: Array) { + getValues(keys: Array) { return getNativeModule(this) .getValues(keys || []) .then(nativeValues => { - const values: { [String]: Object } = {}; + const values: { [string]: Object } = {}; for (let i = 0, len = keys.length; i < len; i++) { values[keys[i]] = this._nativeValueToJS(nativeValues[i]); } From e537955212c17126ac235ee612f2b7436f92e8d8 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 9 Mar 2018 11:09:20 +0000 Subject: [PATCH 53/77] [notifications] Add support for android actions --- .../messaging/BundleJSONConverter.java | 38 +++-- .../RNFirebaseNotificationManager.java | 107 +++++++++++-- .../RNFirebaseNotifications.java | 8 +- lib/modules/notifications/AndroidAction.js | 150 ++++++++++++++++++ lib/modules/notifications/AndroidChannel.js | 45 ++---- .../notifications/AndroidChannelGroup.js | 27 +--- .../notifications/AndroidNotification.js | 50 +++++- .../notifications/AndroidRemoteInput.js | 123 ++++++++++++++ lib/modules/notifications/Notification.js | 5 +- lib/modules/notifications/index.js | 8 + lib/modules/notifications/types.js | 56 +++++-- tests/src/firebase.js | 25 ++- 12 files changed, 539 insertions(+), 103 deletions(-) create mode 100644 lib/modules/notifications/AndroidAction.js create mode 100644 lib/modules/notifications/AndroidRemoteInput.js diff --git a/android/src/main/java/io/invertase/firebase/messaging/BundleJSONConverter.java b/android/src/main/java/io/invertase/firebase/messaging/BundleJSONConverter.java index 4e7446d3..d784f694 100644 --- a/android/src/main/java/io/invertase/firebase/messaging/BundleJSONConverter.java +++ b/android/src/main/java/io/invertase/firebase/messaging/BundleJSONConverter.java @@ -110,23 +110,28 @@ public class BundleJSONConverter { SETTERS.put(JSONArray.class, new Setter() { public void setOnBundle(Bundle bundle, String key, Object value) throws JSONException { JSONArray jsonArray = (JSONArray) value; - ArrayList stringArrayList = new ArrayList(); // Empty list, can't even figure out the type, assume an ArrayList if (jsonArray.length() == 0) { - bundle.putStringArrayList(key, stringArrayList); + bundle.putStringArrayList(key, new ArrayList()); return; } // Only strings are supported for now - for (int i = 0; i < jsonArray.length(); i++) { - Object current = jsonArray.get(i); - if (current instanceof String) { - stringArrayList.add((String) current); - } else { - throw new IllegalArgumentException("Unexpected type in an array: " + current.getClass()); + if (jsonArray.get(0) instanceof String) { + ArrayList stringArrayList = new ArrayList(); + for (int i = 0; i < jsonArray.length(); i++) { + stringArrayList.add((String) jsonArray.get(i)); } + bundle.putStringArrayList(key, stringArrayList); + } else if (jsonArray.get(0) instanceof JSONObject) { + ArrayList bundleArrayList = new ArrayList<>(); + for (int i =0; i < jsonArray.length(); i++) { + bundleArrayList.add(convertToBundle((JSONObject) jsonArray.get(i))); + } + bundle.putSerializable(key, bundleArrayList); + } else { + throw new IllegalArgumentException("Unexpected type in an array: " + jsonArray.get(0).getClass()); } - bundle.putStringArrayList(key, stringArrayList); } @Override @@ -152,13 +157,18 @@ public class BundleJSONConverter { continue; } - // Special case List as getClass would not work, since List is an interface + // Special case List as getClass would not work, since List is an interface if (value instanceof List) { JSONArray jsonArray = new JSONArray(); - @SuppressWarnings("unchecked") - List listValue = (List) value; - for (String stringValue : listValue) { - jsonArray.put(stringValue); + List listValue = (List) value; + for (Object objValue : listValue) { + if (objValue instanceof String) { + jsonArray.put(objValue); + } else if (objValue instanceof Bundle) { + jsonArray.put(convertToJSON((Bundle) objValue)); + } else { + throw new IllegalArgumentException("Unsupported type: " + objValue.getClass()); + } } json.put(key, jsonArray); continue; diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java index 310f63a9..3d8c3923 100644 --- a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java @@ -17,7 +17,9 @@ import android.media.RingtoneManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.Parcelable; import android.support.v4.app.NotificationCompat; +import android.support.v4.app.RemoteInput; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; @@ -325,10 +327,8 @@ public class RNFirebaseNotificationManager { } if (android.containsKey("smallIcon")) { Bundle smallIcon = android.getBundle("smallIcon"); - int smallIconResourceId = getResourceId("mipmap", smallIcon.getString("icon")); - if (smallIconResourceId == 0) { - smallIconResourceId = getResourceId("drawable", smallIcon.getString("icon")); - } + int smallIconResourceId = getIcon(smallIcon.getString("icon")); + if (smallIconResourceId != 0) { if (smallIcon.containsKey("level")) { Double level = smallIcon.getDouble("level"); @@ -386,17 +386,17 @@ public class RNFirebaseNotificationManager { notification.setStyle(bigPicture); } */ - - // Create the notification intent - Intent intent = new Intent(context, intentClass); - intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - intent.putExtras(notification); - if (android.containsKey("clickAction")) { - intent.setAction(android.getString("clickAction")); + // Build any actions + if (android.containsKey("actions")) { + List actions = (List) android.getSerializable("actions"); + for (Bundle a : actions) { + NotificationCompat.Action action = createAction(a, intentClass, notification); + nb = nb.addAction(action); + } } - PendingIntent contentIntent = PendingIntent.getActivity(context, notificationId.hashCode(), intent, - PendingIntent.FLAG_UPDATE_CURRENT); + // Create the notification intent + PendingIntent contentIntent = createIntent(intentClass, notification, android.getString("clickAction")); nb = nb.setContentIntent(contentIntent); // Build the notification and send it @@ -415,6 +415,75 @@ public class RNFirebaseNotificationManager { } } + private NotificationCompat.Action createAction(Bundle action, Class intentClass, Bundle notification) { + String actionKey = action.getString("action"); + PendingIntent actionIntent = createIntent(intentClass, notification, actionKey); + + int icon = getIcon(action.getString("icon")); + String title = action.getString("title"); + + NotificationCompat.Action.Builder ab = new NotificationCompat.Action.Builder(icon, title, actionIntent); + + if (action.containsKey("allowGeneratedReplies")) { + ab = ab.setAllowGeneratedReplies(action.getBoolean("allowGeneratedReplies")); + } + if (action.containsKey("remoteInputs")) { + List remoteInputs = (List) action.getSerializable("remoteInputs"); + for (Bundle ri : remoteInputs) { + RemoteInput remoteInput = createRemoteInput(ri); + ab = ab.addRemoteInput(remoteInput); + } + } + // TODO: SemanticAction and ShowsUserInterface only available on v28? + // if (action.containsKey("semanticAction")) { + // Double semanticAction = action.getDouble("semanticAction"); + // ab = ab.setSemanticAction(semanticAction.intValue()); + // } + // if (action.containsKey("showsUserInterface")) { + // ab = ab.setShowsUserInterface(action.getBoolean("showsUserInterface")); + // } + + return ab.build(); + } + + private PendingIntent createIntent(Class intentClass, Bundle notification, String action) { + Intent intent = new Intent(context, intentClass); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.putExtras(notification); + + if (action != null) { + intent.setAction(action); + } + + String notificationId = notification.getString("notificationId"); + + return PendingIntent.getActivity(context, notificationId.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + private RemoteInput createRemoteInput(Bundle remoteInput) { + String resultKey = remoteInput.getString("resultKey"); + + RemoteInput.Builder rb = new RemoteInput.Builder(resultKey); + + if (remoteInput.containsKey("allowedDataTypes")) { + List allowedDataTypes = (List) remoteInput.getSerializable("allowedDataTypes"); + for (Bundle adt : allowedDataTypes) { + rb.setAllowDataType(adt.getString("mimeType"), adt.getBoolean("allow")); + } + } + if (remoteInput.containsKey("allowFreeFormInput")) { + rb.setAllowFreeFormInput(remoteInput.getBoolean("allowFreeFormInput")); + } + if (remoteInput.containsKey("choices")) { + rb.setChoices(remoteInput.getStringArray("choices")); + } + if (remoteInput.containsKey("label")) { + rb.setLabel(remoteInput.getString("label")); + } + + return rb.build(); + } + private Bitmap getBitmap(String image) { if (image.startsWith("http://") || image.startsWith("https://")) { return getBitmapFromUrl(image); @@ -436,6 +505,14 @@ public class RNFirebaseNotificationManager { } } + private int getIcon(String icon) { + int smallIconResourceId = getResourceId("mipmap", icon); + if (smallIconResourceId == 0) { + smallIconResourceId = getResourceId("drawable", icon); + } + return smallIconResourceId; + } + private Class getMainActivityClass() { String packageName = context.getPackageName(); Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName); @@ -555,9 +632,9 @@ public class RNFirebaseNotificationManager { PendingIntent pendingIntent = PendingIntent.getBroadcast(context, notificationId.hashCode(), notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); - if (schedule.containsKey("interval")) { + if (schedule.containsKey("repeatInterval")) { Long interval = null; - switch (schedule.getString("interval")) { + switch (schedule.getString("repeatInterval")) { case "minute": interval = 60000L; break; diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java index 879be98a..65c487d5 100644 --- a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java @@ -7,12 +7,12 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.os.Bundle; +import android.support.v4.app.RemoteInput; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import com.facebook.react.bridge.ActivityEventListener; import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; @@ -198,6 +198,12 @@ public class RNFirebaseNotifications extends ReactContextBaseJavaModule implemen notificationOpenMap.putString("action", intent.getAction()); notificationOpenMap.putMap("notification", notificationMap); + // Check for remote input results + Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); + if (remoteInput != null) { + notificationOpenMap.putMap("results", Arguments.makeNativeMap(remoteInput)); + } + return notificationOpenMap; } diff --git a/lib/modules/notifications/AndroidAction.js b/lib/modules/notifications/AndroidAction.js new file mode 100644 index 00000000..ed2a940c --- /dev/null +++ b/lib/modules/notifications/AndroidAction.js @@ -0,0 +1,150 @@ +/** + * @flow + * AndroidAction representation wrapper + */ +import RemoteInput, { + fromNativeAndroidRemoteInput, +} from './AndroidRemoteInput'; +import { SemanticAction } from './types'; +import type { NativeAndroidAction, SemanticActionType } from './types'; + +export default class AndroidAction { + _action: string; + _allowGeneratedReplies: boolean | void; + _icon: string; + _remoteInputs: RemoteInput[]; + _semanticAction: SemanticActionType | void; + _showUserInterface: boolean | void; + _title: string; + + constructor(action: string, icon: string, title: string) { + this._action = action; + this._icon = icon; + this._remoteInputs = []; + this._title = title; + } + + get action(): string { + return this._action; + } + + get allowGeneratedReplies(): ?boolean { + return this._allowGeneratedReplies; + } + + get icon(): string { + return this._icon; + } + + get remoteInputs(): RemoteInput[] { + return this._remoteInputs; + } + + get semanticAction(): ?SemanticActionType { + return this._semanticAction; + } + + get showUserInterface(): ?boolean { + return this._showUserInterface; + } + + get title(): string { + return this._title; + } + + /** + * + * @param remoteInput + * @returns {AndroidAction} + */ + addRemoteInput(remoteInput: RemoteInput): AndroidAction { + if (!(remoteInput instanceof RemoteInput)) { + throw new Error( + `AndroidAction:addRemoteInput expects an 'RemoteInput' but got type ${typeof remoteInput}` + ); + } + this._remoteInputs.push(remoteInput); + return this; + } + + /** + * + * @param allowGeneratedReplies + * @returns {AndroidAction} + */ + setAllowGenerateReplies(allowGeneratedReplies: boolean): AndroidAction { + this._allowGeneratedReplies = allowGeneratedReplies; + return this; + } + + /** + * + * @param semanticAction + * @returns {AndroidAction} + */ + setSemanticAction(semanticAction: SemanticActionType): AndroidAction { + if (!Object.values(SemanticAction).includes(semanticAction)) { + throw new Error( + `AndroidAction:setSemanticAction Invalid Semantic Action: ${semanticAction}` + ); + } + this._semanticAction = semanticAction; + return this; + } + + /** + * + * @param showUserInterface + * @returns {AndroidAction} + */ + setShowUserInterface(showUserInterface: boolean): AndroidAction { + this._showUserInterface = showUserInterface; + return this; + } + + build(): NativeAndroidAction { + if (!this._action) { + throw new Error('AndroidAction: Missing required `action` property'); + } else if (!this._icon) { + throw new Error('AndroidAction: Missing required `icon` property'); + } else if (!this._title) { + throw new Error('AndroidAction: Missing required `title` property'); + } + + return { + action: this._action, + allowGeneratedReplies: this._allowGeneratedReplies, + icon: this._icon, + remoteInputs: this._remoteInputs.map(remoteInput => remoteInput.build()), + semanticAction: this._semanticAction, + showUserInterface: this._showUserInterface, + title: this._title, + }; + } +} + +export const fromNativeAndroidAction = ( + nativeAction: NativeAndroidAction +): AndroidAction => { + const action = new AndroidAction( + nativeAction.action, + nativeAction.icon, + nativeAction.title + ); + if (nativeAction.allowGeneratedReplies) { + action.setAllowGenerateReplies(nativeAction.allowGeneratedReplies); + } + if (nativeAction.remoteInputs) { + nativeAction.remoteInputs.forEach(remoteInput => { + action.addRemoteInput(fromNativeAndroidRemoteInput(remoteInput)); + }); + } + if (nativeAction.semanticAction) { + action.setSemanticAction(nativeAction.semanticAction); + } + if (nativeAction.showUserInterface) { + action.setShowUserInterface(nativeAction.showUserInterface); + } + + return action; +}; diff --git a/lib/modules/notifications/AndroidChannel.js b/lib/modules/notifications/AndroidChannel.js index 5e3f6cf0..f64cbf9e 100644 --- a/lib/modules/notifications/AndroidChannel.js +++ b/lib/modules/notifications/AndroidChannel.js @@ -2,6 +2,7 @@ * @flow * AndroidChannel representation wrapper */ +import { Importance, Visibility } from './types'; import type { ImportanceType, VisibilityType } from './types'; type NativeAndroidChannel = {| @@ -31,6 +32,15 @@ export default class AndroidChannel { _sound: string | void; _vibrationPattern: number[] | void; + constructor(channelId: string, name: string, importance: ImportanceType) { + if (!Object.values(Importance).includes(importance)) { + throw new Error(`AndroidChannel() Invalid Importance: ${importance}`); + } + this._channelId = channelId; + this._name = name; + this._importance = importance; + } + get bypassDnd(): ?boolean { return this._bypassDnd; } @@ -85,16 +95,6 @@ export default class AndroidChannel { return this; } - /** - * - * @param channelId - * @returns {AndroidChannel} - */ - setChannelId(channelId: string): AndroidChannel { - this._channelId = channelId; - return this; - } - /** * * @param description @@ -115,16 +115,6 @@ export default class AndroidChannel { return this; } - /** - * - * @param importance - * @returns {AndroidChannel} - */ - setImportance(importance: ImportanceType): AndroidChannel { - this._importance = importance; - return this; - } - /** * * @param lightColor @@ -143,20 +133,15 @@ export default class AndroidChannel { setLockScreenVisibility( lockScreenVisibility: VisibilityType ): AndroidChannel { + if (!Object.values(Visibility).includes(lockScreenVisibility)) { + throw new Error( + `AndroidChannel:setLockScreenVisibility Invalid Visibility: ${lockScreenVisibility}` + ); + } this._lockScreenVisibility = lockScreenVisibility; return this; } - /** - * - * @param name - * @returns {AndroidChannel} - */ - setName(name: string): AndroidChannel { - this._name = name; - return this; - } - /** * * @param showBadge diff --git a/lib/modules/notifications/AndroidChannelGroup.js b/lib/modules/notifications/AndroidChannelGroup.js index fb37a527..3485c276 100644 --- a/lib/modules/notifications/AndroidChannelGroup.js +++ b/lib/modules/notifications/AndroidChannelGroup.js @@ -8,10 +8,15 @@ type NativeAndroidChannelGroup = {| name: string, |}; -export default class AndroidChannel { +export default class AndroidChannelGroup { _groupId: string; _name: string; + constructor(groupId: string, name: string) { + this._groupId = groupId; + this._name = name; + } + get groupId(): string { return this._groupId; } @@ -20,26 +25,6 @@ export default class AndroidChannel { return this._name; } - /** - * - * @param groupId - * @returns {AndroidChannel} - */ - setGroupId(groupId: string): AndroidChannel { - this._groupId = groupId; - return this; - } - - /** - * - * @param name - * @returns {AndroidChannel} - */ - setName(name: string): AndroidChannel { - this._name = name; - return this; - } - build(): NativeAndroidChannelGroup { if (!this._groupId) { throw new Error( diff --git a/lib/modules/notifications/AndroidNotification.js b/lib/modules/notifications/AndroidNotification.js index 4fadb223..f67b3d3f 100644 --- a/lib/modules/notifications/AndroidNotification.js +++ b/lib/modules/notifications/AndroidNotification.js @@ -2,7 +2,8 @@ * @flow * AndroidNotification representation wrapper */ -import { Category } from './types'; +import AndroidAction, { fromNativeAndroidAction } from './AndroidAction'; +import { BadgeIconType, Category, GroupAlert, Priority } from './types'; import type Notification from './Notification'; import type { BadgeIconTypeType, @@ -18,8 +19,7 @@ import type { } from './types'; export default class AndroidNotification { - // TODO optional fields - // TODO actions: Action[]; // icon, title, ??pendingIntent??, allowGeneratedReplies, extender, extras, remoteinput (ugh) + _actions: AndroidAction[]; _autoCancel: boolean | void; _badgeIconType: BadgeIconTypeType | void; _category: CategoryType | void; @@ -70,6 +70,9 @@ export default class AndroidNotification { this._notification = notification; if (data) { + this._actions = data.actions + ? data.actions.map(action => fromNativeAndroidAction(action)) + : []; this._autoCancel = data.autoCancel; this._badgeIconType = data.badgeIconType; this._category = data.category; @@ -106,12 +109,17 @@ export default class AndroidNotification { } // Defaults + this._actions = this._actions || []; this._people = this._people || []; this._smallIcon = this._smallIcon || { icon: 'ic_launcher', }; } + get actions(): AndroidAction[] { + return this._actions; + } + get autoCancel(): ?boolean { return this._autoCancel; } @@ -240,6 +248,21 @@ export default class AndroidNotification { return this._when; } + /** + * + * @param action + * @returns {Notification} + */ + addAction(action: AndroidAction): Notification { + if (!(action instanceof AndroidAction)) { + throw new Error( + `AndroidNotification:addAction expects an 'AndroidAction' but got type ${typeof action}` + ); + } + this._actions.push(action); + return this._notification; + } + /** * * @param person @@ -266,6 +289,11 @@ export default class AndroidNotification { * @returns {Notification} */ setBadgeIconType(badgeIconType: BadgeIconTypeType): Notification { + if (!Object.values(BadgeIconType).includes(badgeIconType)) { + throw new Error( + `AndroidNotification:setBadgeIconType Invalid BadgeIconType: ${badgeIconType}` + ); + } this._badgeIconType = badgeIconType; return this._notification; } @@ -277,7 +305,9 @@ export default class AndroidNotification { */ setCategory(category: CategoryType): Notification { if (!Object.values(Category).includes(category)) { - throw new Error(`AndroidNotification: Invalid Category: ${category}`); + throw new Error( + `AndroidNotification:setCategory Invalid Category: ${category}` + ); } this._category = category; return this._notification; @@ -359,6 +389,11 @@ export default class AndroidNotification { * @returns {Notification} */ setGroupAlertBehaviour(groupAlertBehaviour: GroupAlertType): Notification { + if (!Object.values(GroupAlert).includes(groupAlertBehaviour)) { + throw new Error( + `AndroidNotification:setGroupAlertBehaviour Invalid GroupAlert: ${groupAlertBehaviour}` + ); + } this._groupAlertBehaviour = groupAlertBehaviour; return this._notification; } @@ -445,6 +480,11 @@ export default class AndroidNotification { * @returns {Notification} */ setPriority(priority: PriorityType): Notification { + if (!Object.values(Priority).includes(priority)) { + throw new Error( + `AndroidNotification:setPriority Invalid Priority: ${priority}` + ); + } this._priority = priority; return this._notification; } @@ -596,7 +636,7 @@ export default class AndroidNotification { } return { - // TODO actions: Action[], + actions: this._actions.map(action => action.build()), autoCancel: this._autoCancel, badgeIconType: this._badgeIconType, category: this._category, diff --git a/lib/modules/notifications/AndroidRemoteInput.js b/lib/modules/notifications/AndroidRemoteInput.js new file mode 100644 index 00000000..29b29fbb --- /dev/null +++ b/lib/modules/notifications/AndroidRemoteInput.js @@ -0,0 +1,123 @@ +/** + * @flow + * AndroidRemoteInput representation wrapper + */ + +import type { AndroidAllowDataType, NativeAndroidRemoteInput } from './types'; + +export default class AndroidRemoteInput { + _allowedDataTypes: AndroidAllowDataType[]; + _allowFreeFormInput: boolean | void; + _choices: string[]; + _label: string | void; + _resultKey: string; + + constructor(resultKey: string) { + this._allowedDataTypes = []; + this._choices = []; + this._resultKey = resultKey; + } + + get allowedDataTypes(): AndroidAllowDataType[] { + return this._allowedDataTypes; + } + + get allowFreeFormInput(): ?boolean { + return this._allowFreeFormInput; + } + + get choices(): string[] { + return this._choices; + } + + get label(): ?string { + return this._label; + } + + get resultKey(): string { + return this._resultKey; + } + + /** + * + * @param mimeType + * @param allow + * @returns {AndroidRemoteInput} + */ + setAllowDataType(mimeType: string, allow: boolean): AndroidRemoteInput { + this._allowedDataTypes.push({ + allow, + mimeType, + }); + return this; + } + + /** + * + * @param allowFreeFormInput + * @returns {AndroidRemoteInput} + */ + setAllowFreeFormInput(allowFreeFormInput: boolean): AndroidRemoteInput { + this._allowFreeFormInput = allowFreeFormInput; + return this; + } + + /** + * + * @param choices + * @returns {AndroidRemoteInput} + */ + setChoices(choices: string[]): AndroidRemoteInput { + this._choices = choices; + return this; + } + + /** + * + * @param label + * @returns {AndroidRemoteInput} + */ + setLabel(label: string): AndroidRemoteInput { + this._label = label; + return this; + } + + build(): NativeAndroidRemoteInput { + if (!this._resultKey) { + throw new Error( + 'AndroidRemoteInput: Missing required `resultKey` property' + ); + } + + return { + allowedDataTypes: this._allowedDataTypes, + allowFreeFormInput: this._allowFreeFormInput, + choices: this._choices, + label: this._label, + resultKey: this._resultKey, + }; + } +} + +export const fromNativeAndroidRemoteInput = ( + nativeRemoteInput: NativeAndroidRemoteInput +): AndroidRemoteInput => { + const remoteInput = new AndroidRemoteInput(nativeRemoteInput.resultKey); + if (nativeRemoteInput.allowDataType) { + for (let i = 0; i < nativeRemoteInput.allowDataType.length; i++) { + const allowDataType = nativeRemoteInput.allowDataType[i]; + remoteInput.setAllowDataType(allowDataType.mimeType, allowDataType.allow); + } + } + if (nativeRemoteInput.allowFreeFormInput) { + remoteInput.setAllowFreeFormInput(nativeRemoteInput.allowFreeFormInput); + } + if (nativeRemoteInput.choices) { + remoteInput.setChoices(nativeRemoteInput.choices); + } + if (nativeRemoteInput.label) { + remoteInput.setLabel(nativeRemoteInput.label); + } + + return remoteInput; +}; diff --git a/lib/modules/notifications/Notification.js b/lib/modules/notifications/Notification.js index 37ccb2bc..9385c27e 100644 --- a/lib/modules/notifications/Notification.js +++ b/lib/modules/notifications/Notification.js @@ -9,10 +9,11 @@ import { generatePushID, isObject } from '../../utils'; import type { NativeNotification } from './types'; -export type NotificationOpen = { +export type NotificationOpen = {| action: string, notification: Notification, -}; + results?: { [string]: string }, +|}; export default class Notification { // iOS 8/9 | 10+ | Android diff --git a/lib/modules/notifications/index.js b/lib/modules/notifications/index.js index 662d137a..1a4fc09f 100644 --- a/lib/modules/notifications/index.js +++ b/lib/modules/notifications/index.js @@ -7,9 +7,11 @@ import { getLogger } from '../../utils/log'; import ModuleBase from '../../utils/ModuleBase'; import { getNativeModule } from '../../utils/native'; import { isFunction, isObject } from '../../utils'; +import AndroidAction from './AndroidAction'; import AndroidChannel from './AndroidChannel'; import AndroidChannelGroup from './AndroidChannelGroup'; import AndroidNotifications from './AndroidNotifications'; +import AndroidRemoteInput from './AndroidRemoteInput'; import Notification from './Notification'; import { BadgeIconType, @@ -18,6 +20,7 @@ import { GroupAlert, Importance, Priority, + SemanticAction, Visibility, } from './types'; @@ -99,6 +102,7 @@ export default class Notifications extends ModuleBase { SharedEventEmitter.emit('onNotificationOpened', { action: notificationOpen.action, notification: new Notification(notificationOpen.notification), + results: notificationOpen.results, }); } ); @@ -166,6 +170,7 @@ export default class Notifications extends ModuleBase { return { action: notificationOpen.action, notification: new Notification(notificationOpen.notification), + results: notificationOpen.results, }; } return null; @@ -295,6 +300,7 @@ export default class Notifications extends ModuleBase { export const statics = { Android: { + Action: AndroidAction, BadgeIconType, Category, Channel: AndroidChannel, @@ -303,6 +309,8 @@ export const statics = { GroupAlert, Importance, Priority, + RemoteInput: AndroidRemoteInput, + SemanticAction, Visibility, }, Notification, diff --git a/lib/modules/notifications/types.js b/lib/modules/notifications/types.js index 5a77c74a..1441d8e3 100644 --- a/lib/modules/notifications/types.js +++ b/lib/modules/notifications/types.js @@ -1,7 +1,6 @@ /** * @flow */ - export const BadgeIconType = { Large: 2, None: 0, @@ -57,6 +56,20 @@ export const Priority = { Min: -2, }; +export const SemanticAction = { + Archive: 5, + Call: 10, + Delete: 4, + MarkAsRead: 2, + MarkAsUnread: 3, + Mute: 6, + None: 0, + Reply: 1, + ThumbsDown: 9, + ThumbsUp: 8, + Unmute: 7, +}; + export const Visibility = { Private: 0, Public: 1, @@ -69,27 +82,51 @@ export type DefaultsType = $Values; export type GroupAlertType = $Values; export type ImportanceType = $Values; export type PriorityType = $Values; +export type SemanticActionType = $Values; export type VisibilityType = $Values; -export type Lights = { +export type Lights = {| argb: number, onMs: number, offMs: number, -}; +|}; -export type Progress = { +export type Progress = {| max: number, progress: number, indeterminate: boolean, -}; +|}; -export type SmallIcon = { +export type SmallIcon = {| icon: string, level?: number, +|}; + +export type AndroidAllowDataType = { + allow: boolean, + mimeType: string, }; +export type NativeAndroidRemoteInput = {| + allowedDataTypes: AndroidAllowDataType[], + allowFreeFormInput?: boolean, + choices: string[], + label?: string, + resultKey: string, +|}; + +export type NativeAndroidAction = {| + action: string, + allowGeneratedReplies?: boolean, + icon: string, + remoteInputs: NativeAndroidRemoteInput[], + semanticAction?: SemanticActionType, + showUserInterface?: boolean, + title: string, +|}; + export type NativeAndroidNotification = {| - // TODO actions: Action[], + actions?: NativeAndroidAction[], autoCancel?: boolean, badgeIconType?: BadgeIconTypeType, category?: CategoryType, @@ -154,11 +191,11 @@ export type NativeIOSNotification = {| threadIdentifier?: string, |}; -export type Schedule = { +export type Schedule = {| exact?: boolean, fireDate: number, repeatInterval?: 'minute' | 'hour' | 'day' | 'week', -}; +|}; export type NativeNotification = {| android?: NativeAndroidNotification, @@ -175,4 +212,5 @@ export type NativeNotification = {| export type NativeNotificationOpen = {| action: string, notification: NativeNotification, + results?: { [string]: string }, |}; diff --git a/tests/src/firebase.js b/tests/src/firebase.js index 79320804..dd3c9c72 100644 --- a/tests/src/firebase.js +++ b/tests/src/firebase.js @@ -37,22 +37,35 @@ const init = async () => { console.log('onNotificationDisplayed: ', notification); }); // RNfirebase.instanceid().delete(); - const channel = new RNfirebase.notifications.Android.Channel(); - channel - .setChannelId('test') - .setName('test') - .setImportance(RNfirebase.notifications.Android.Importance.Max) - .setDescription('test channel'); + const channel = new RNfirebase.notifications.Android.Channel( + 'test', + 'test', + RNfirebase.notifications.Android.Importance.Max + ); + channel.setDescription('test channel'); RNfirebase.notifications().android.createChannel(channel); + const remoteInput = new RNfirebase.notifications.Android.RemoteInput( + 'inputText' + ); + remoteInput.setLabel('Message'); + const action = new RNfirebase.notifications.Android.Action( + 'test_action', + 'ic_launcher', + 'My Test Action' + ); + action.addRemoteInput(remoteInput); + const notification = new RNfirebase.notifications.Notification(); notification .setTitle('Test title') .setBody('Test body') .setNotificationId('displayed') + .android.addAction(action) .android.setChannelId('test') .android.setClickAction('action') .android.setPriority(RNfirebase.notifications.Android.Priority.Max); + const date = new Date(); date.setMinutes(date.getMinutes() + 1); setTimeout(() => { From 287ad216cf8895dce892e53c8fff4fd48e81d265 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 9 Mar 2018 11:15:27 +0000 Subject: [PATCH 54/77] [notifications] Fix flow type --- lib/modules/notifications/index.js | 2 +- tests/src/firebase.js | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/modules/notifications/index.js b/lib/modules/notifications/index.js index 1a4fc09f..d29ec73b 100644 --- a/lib/modules/notifications/index.js +++ b/lib/modules/notifications/index.js @@ -181,7 +181,7 @@ export default class Notifications extends ModuleBase { * Returns an array of all scheduled notifications * @returns {Promise.} */ - getScheduledNotifications(): Promise { + getScheduledNotifications(): Promise { return getNativeModule(this).getScheduledNotifications(); } diff --git a/tests/src/firebase.js b/tests/src/firebase.js index dd3c9c72..6b4800de 100644 --- a/tests/src/firebase.js +++ b/tests/src/firebase.js @@ -44,6 +44,7 @@ const init = async () => { ); channel.setDescription('test channel'); RNfirebase.notifications().android.createChannel(channel); + RNfirebase.notifications().cancelAllNotifications(); const remoteInput = new RNfirebase.notifications.Android.RemoteInput( 'inputText' @@ -71,9 +72,15 @@ const init = async () => { setTimeout(() => { RNfirebase.notifications().displayNotification(notification); notification.setNotificationId('scheduled'); - RNfirebase.notifications().scheduleNotification(notification, { - fireDate: date.getTime(), - }); + RNfirebase.notifications() + .scheduleNotification(notification, { + fireDate: date.getTime(), + }) + .then(() => { + RNfirebase.notifications() + .getScheduledNotifications() + .then(notifications => console.log('scheduled: ', notifications)); + }); }, 5); } catch (error) { console.error('messaging init error:', error); From 65cd6c8ae1ac4651f5ceb26bf5066a438a1a0c71 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Tue, 20 Mar 2018 07:19:39 +0000 Subject: [PATCH 55/77] 4.0.0-alpha.1 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index e05ea92d..a12111f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-native-firebase", - "version": "3.3.1", + "version": "4.0.0-alpha.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 7cb966b1..fd294b93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-firebase", - "version": "3.3.1", + "version": "4.0.0-alpha.1", "author": "Invertase (http://invertase.io)", "description": "A well tested, feature rich Firebase implementation for React Native, supporting iOS & Android. Individual module support for Admob, Analytics, Auth, Crash Reporting, Cloud Firestore, Database, Dynamic Links, Messaging (FCM), Remote Config, Storage and Performance.", "main": "dist/index.js", From e791648c243d98a41b60be704ca0b1660e7486df Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Tue, 20 Mar 2018 16:07:37 +0000 Subject: [PATCH 56/77] Tests for firestore and auth --- lib/index.js | 2 +- lib/modules/auth/index.js | 5 +-- .../auth/{ => phone}/ConfirmationResult.js | 6 +-- .../auth/{ => phone}/PhoneAuthListener.js | 10 ++--- lib/modules/firestore/DocumentReference.js | 38 ++----------------- lib/modules/firestore/Path.js | 19 +++------- lib/modules/firestore/Transaction.js | 34 +---------------- lib/modules/firestore/TransactionHandler.js | 19 ++++------ lib/modules/firestore/WriteBatch.js | 37 ++---------------- lib/modules/firestore/utils/index.js | 33 ++++++++++++++++ tests/ios/Podfile.lock | 2 +- tests/src/firebase.js | 7 +++- .../tests/firestore/documentReferenceTests.js | 15 +++++++- tests/src/tests/firestore/firestoreTests.js | 4 +- 14 files changed, 86 insertions(+), 145 deletions(-) rename lib/modules/auth/{ => phone}/ConfirmationResult.js (85%) rename lib/modules/auth/{ => phone}/PhoneAuthListener.js (97%) diff --git a/lib/index.js b/lib/index.js index 6cd42dce..9d8a3e8a 100644 --- a/lib/index.js +++ b/lib/index.js @@ -24,7 +24,7 @@ export type { } from './modules/auth/types'; export type { default as ConfirmationResult, -} from './modules/auth/ConfirmationResult'; +} from './modules/auth/phone/ConfirmationResult'; export type { default as User } from './modules/auth/User'; /* diff --git a/lib/modules/auth/index.js b/lib/modules/auth/index.js index 4288a7e0..4b2863e8 100644 --- a/lib/modules/auth/index.js +++ b/lib/modules/auth/index.js @@ -8,7 +8,8 @@ import { getAppEventName, SharedEventEmitter } from '../../utils/events'; import { getLogger } from '../../utils/log'; import { getNativeModule } from '../../utils/native'; import INTERNALS from '../../utils/internals'; -import ConfirmationResult from './ConfirmationResult'; +import ConfirmationResult from './phone/ConfirmationResult'; +import PhoneAuthListener from './phone/PhoneAuthListener'; // providers import EmailAuthProvider from './providers/EmailAuthProvider'; @@ -19,8 +20,6 @@ import OAuthProvider from './providers/OAuthProvider'; import TwitterAuthProvider from './providers/TwitterAuthProvider'; import FacebookAuthProvider from './providers/FacebookAuthProvider'; -import PhoneAuthListener from './PhoneAuthListener'; - import type { ActionCodeInfo, ActionCodeSettings, diff --git a/lib/modules/auth/ConfirmationResult.js b/lib/modules/auth/phone/ConfirmationResult.js similarity index 85% rename from lib/modules/auth/ConfirmationResult.js rename to lib/modules/auth/phone/ConfirmationResult.js index fd76e69a..25d701d1 100644 --- a/lib/modules/auth/ConfirmationResult.js +++ b/lib/modules/auth/phone/ConfirmationResult.js @@ -2,9 +2,9 @@ * @flow * ConfirmationResult representation wrapper */ -import { getNativeModule } from '../../utils/native'; -import type Auth from './'; -import type User from './User'; +import { getNativeModule } from '../../../utils/native'; +import type Auth from '../'; +import type User from '../User'; export default class ConfirmationResult { _auth: Auth; diff --git a/lib/modules/auth/PhoneAuthListener.js b/lib/modules/auth/phone/PhoneAuthListener.js similarity index 97% rename from lib/modules/auth/PhoneAuthListener.js rename to lib/modules/auth/phone/PhoneAuthListener.js index d85bcace..68bc78fb 100644 --- a/lib/modules/auth/PhoneAuthListener.js +++ b/lib/modules/auth/phone/PhoneAuthListener.js @@ -1,6 +1,6 @@ // @flow -import INTERNALS from '../../utils/internals'; -import { SharedEventEmitter } from '../../utils/events'; +import INTERNALS from '../../../utils/internals'; +import { SharedEventEmitter } from '../../../utils/events'; import { generatePushID, isFunction, @@ -8,10 +8,10 @@ import { isIOS, isString, nativeToJSError, -} from '../../utils'; -import { getNativeModule } from '../../utils/native'; +} from '../../../utils'; +import { getNativeModule } from '../../../utils/native'; -import type Auth from './'; +import type Auth from '../'; type PhoneAuthSnapshot = { state: 'sent' | 'timeout' | 'verified' | 'error', diff --git a/lib/modules/firestore/DocumentReference.js b/lib/modules/firestore/DocumentReference.js index 91772833..ebce412b 100644 --- a/lib/modules/firestore/DocumentReference.js +++ b/lib/modules/firestore/DocumentReference.js @@ -4,12 +4,11 @@ */ import CollectionReference from './CollectionReference'; import DocumentSnapshot from './DocumentSnapshot'; -import FieldPath from './FieldPath'; -import { mergeFieldPathData } from './utils'; +import { parseUpdateArgs } from './utils'; import { buildNativeMap } from './utils/serialize'; import { getAppEventName, SharedEventEmitter } from '../../utils/events'; import { getLogger } from '../../utils/log'; -import { firestoreAutoId, isFunction, isObject, isString } from '../../utils'; +import { firestoreAutoId, isFunction, isObject } from '../../utils'; import { getNativeModule } from '../../utils/native'; import type Firestore from './'; @@ -50,9 +49,7 @@ export default class DocumentReference { get parent(): CollectionReference { const parentPath = this._documentPath.parent(); - if (!parentPath) { - throw new Error('Invalid document path'); - } + // $FlowExpectedError: parentPath can never be null return new CollectionReference(this._firestore, parentPath); } @@ -232,34 +229,7 @@ export default class DocumentReference { } update(...args: any[]): Promise { - let data = {}; - if (args.length === 1) { - if (!isObject(args[0])) { - throw new Error( - 'DocumentReference.update failed: If using a single argument, it must be an object.' - ); - } - // eslint-disable-next-line prefer-destructuring - data = args[0]; - } else if (args.length % 2 === 1) { - throw new Error( - 'DocumentReference.update failed: Must have either a single object argument, or equal numbers of key/value pairs.' - ); - } else { - for (let i = 0; i < args.length; i += 2) { - const key = args[i]; - const value = args[i + 1]; - if (isString(key)) { - data[key] = value; - } else if (key instanceof FieldPath) { - data = mergeFieldPathData(data, key._segments, value); - } else { - throw new Error( - `DocumentReference.update failed: Argument at index ${i} must be a string or FieldPath` - ); - } - } - } + const data = parseUpdateArgs(args, 'DocumentReference.update'); const nativeData = buildNativeMap(data); return getNativeModule(this._firestore).documentUpdate( this.path, diff --git a/lib/modules/firestore/Path.js b/lib/modules/firestore/Path.js index 5818b874..dd215cb1 100644 --- a/lib/modules/firestore/Path.js +++ b/lib/modules/firestore/Path.js @@ -14,10 +14,7 @@ export default class Path { } get id(): string | null { - if (this._parts.length > 0) { - return this._parts[this._parts.length - 1]; - } - return null; + return this._parts.length > 0 ? this._parts[this._parts.length - 1] : null; } get isDocument(): boolean { @@ -37,11 +34,9 @@ export default class Path { } parent(): Path | null { - if (this._parts.length === 0) { - return null; - } - - return new Path(this._parts.slice(0, this._parts.length - 1)); + return this._parts.length > 0 + ? new Path(this._parts.slice(0, this._parts.length - 1)) + : null; } /** @@ -50,10 +45,6 @@ export default class Path { */ static fromName(name: string): Path { const parts = name.split('/'); - - if (parts.length === 0) { - return new Path([]); - } - return new Path(parts); + return parts.length === 0 ? new Path([]) : new Path(parts); } } diff --git a/lib/modules/firestore/Transaction.js b/lib/modules/firestore/Transaction.js index 71cd1b5e..1fa0ce13 100644 --- a/lib/modules/firestore/Transaction.js +++ b/lib/modules/firestore/Transaction.js @@ -2,15 +2,13 @@ * @flow * Firestore Transaction representation wrapper */ -import { mergeFieldPathData } from './utils'; +import { parseUpdateArgs } from './utils'; import { buildNativeMap } from './utils/serialize'; import type Firestore from './'; import type { TransactionMeta } from './TransactionHandler'; import type DocumentReference from './DocumentReference'; import DocumentSnapshot from './DocumentSnapshot'; -import { isObject, isString } from '../../utils'; -import FieldPath from './FieldPath'; import { getNativeModule } from '../../utils/native'; type Command = { @@ -124,35 +122,7 @@ export default class Transaction { */ update(documentRef: DocumentReference, ...args: Array): Transaction { // todo validate doc ref - let data = {}; - if (args.length === 1) { - if (!isObject(args[0])) { - throw new Error( - 'Transaction.update failed: If using a single data argument, it must be an object.' - ); - } - - [data] = args; - } else if (args.length % 2 === 1) { - throw new Error( - 'Transaction.update failed: Must have either a single object data argument, or equal numbers of data key/value pairs.' - ); - } else { - for (let i = 0; i < args.length; i += 2) { - const key = args[i]; - const value = args[i + 1]; - if (isString(key)) { - data[key] = value; - } else if (key instanceof FieldPath) { - data = mergeFieldPathData(data, key._segments, value); - } else { - throw new Error( - `Transaction.update failed: Argument at index ${i} must be a string or FieldPath` - ); - } - } - } - + const data = parseUpdateArgs(args, 'Transaction.update'); this._commandBuffer.push({ type: 'update', path: documentRef.path, diff --git a/lib/modules/firestore/TransactionHandler.js b/lib/modules/firestore/TransactionHandler.js index 3b4f5761..1f424a80 100644 --- a/lib/modules/firestore/TransactionHandler.js +++ b/lib/modules/firestore/TransactionHandler.js @@ -3,7 +3,6 @@ * Firestore Transaction representation wrapper */ import { getAppEventName, SharedEventEmitter } from '../../utils/events'; -import { getLogger } from '../../utils/log'; import { getNativeModule } from '../../utils/native'; import Transaction from './Transaction'; import type Firestore from './'; @@ -103,9 +102,7 @@ export default class TransactionHandler { * @private */ _remove(id) { - // todo confirm pending arg no longer needed getNativeModule(this._firestore).transactionDispose(id); - // TODO may need delaying to next event loop delete this._pending[id]; } @@ -124,19 +121,17 @@ export default class TransactionHandler { * @private */ _handleTransactionEvent(event: TransactionEvent) { + // eslint-disable-next-line default-case switch (event.type) { case 'update': - return this._handleUpdate(event); + this._handleUpdate(event); + break; case 'error': - return this._handleError(event); + this._handleError(event); + break; case 'complete': - return this._handleComplete(event); - default: - getLogger(this._firestore).warn( - `Unknown transaction event type: '${event.type}'`, - event - ); - return undefined; + this._handleComplete(event); + break; } } diff --git a/lib/modules/firestore/WriteBatch.js b/lib/modules/firestore/WriteBatch.js index fe4f15a9..5f4fd163 100644 --- a/lib/modules/firestore/WriteBatch.js +++ b/lib/modules/firestore/WriteBatch.js @@ -2,10 +2,8 @@ * @flow * WriteBatch representation wrapper */ -import FieldPath from './FieldPath'; -import { mergeFieldPathData } from './utils'; +import { parseUpdateArgs } from './utils'; import { buildNativeMap } from './utils/serialize'; -import { isObject, isString } from '../../utils'; import { getNativeModule } from '../../utils/native'; import type DocumentReference from './DocumentReference'; @@ -66,38 +64,9 @@ export default class WriteBatch { update(docRef: DocumentReference, ...args: any[]): WriteBatch { // TODO: Validation // validate.isDocumentReference('docRef', docRef); - let data = {}; - if (args.length === 1) { - if (!isObject(args[0])) { - throw new Error( - 'WriteBatch.update failed: If using two arguments, the second must be an object.' - ); - } - // eslint-disable-next-line prefer-destructuring - data = args[0]; - } else if (args.length % 2 === 1) { - throw new Error( - 'WriteBatch.update failed: Must have a document reference, followed by either a single object argument, or equal numbers of key/value pairs.' - ); - } else { - for (let i = 0; i < args.length; i += 2) { - const key = args[i]; - const value = args[i + 1]; - if (isString(key)) { - data[key] = value; - } else if (key instanceof FieldPath) { - data = mergeFieldPathData(data, key._segments, value); - } else { - throw new Error( - `WriteBatch.update failed: Argument at index ${i} must be a string or FieldPath` - ); - } - } - } - - const nativeData = buildNativeMap(data); + const data = parseUpdateArgs(args, 'WriteBatch.update'); this._writes.push({ - data: nativeData, + data: buildNativeMap(data), path: docRef.path, type: 'UPDATE', }); diff --git a/lib/modules/firestore/utils/index.js b/lib/modules/firestore/utils/index.js index 8405183b..6aabeeaf 100644 --- a/lib/modules/firestore/utils/index.js +++ b/lib/modules/firestore/utils/index.js @@ -1,6 +1,8 @@ /** * @flow */ +import FieldPath from '../FieldPath'; +import { isObject, isString } from '../../../utils'; const buildFieldPathData = (segments: string[], value: any): Object => { if (segments.length === 1) { @@ -39,3 +41,34 @@ export const mergeFieldPathData = ( [segments[0]]: buildFieldPathData(segments.slice(1), value), }; }; + +export const parseUpdateArgs = (args: any[], methodName: string) => { + let data = {}; + if (args.length === 1) { + if (!isObject(args[0])) { + throw new Error( + `${methodName} failed: If using a single update argument, it must be an object.` + ); + } + [data] = args; + } else if (args.length % 2 === 1) { + throw new Error( + `${methodName} failed: The update arguments must be either a single object argument, or equal numbers of key/value pairs.` + ); + } else { + for (let i = 0; i < args.length; i += 2) { + const key = args[i]; + const value = args[i + 1]; + if (isString(key)) { + data[key] = value; + } else if (key instanceof FieldPath) { + data = mergeFieldPathData(data, key._segments, value); + } else { + throw new Error( + `${methodName} failed: Argument at index ${i} must be a string or FieldPath` + ); + } + } + } + return data; +}; diff --git a/tests/ios/Podfile.lock b/tests/ios/Podfile.lock index 0ff42ead..3af0c2e6 100644 --- a/tests/ios/Podfile.lock +++ b/tests/ios/Podfile.lock @@ -164,7 +164,7 @@ PODS: - React/Core - React/fishhook - React/RCTBlob - - RNFirebase (3.3.1): + - RNFirebase (4.0.0-alpha.1): - React - yoga (0.52.3.React) diff --git a/tests/src/firebase.js b/tests/src/firebase.js index 6b4800de..58580998 100644 --- a/tests/src/firebase.js +++ b/tests/src/firebase.js @@ -4,8 +4,11 @@ import firebase from 'firebase'; import RNfirebase from './../firebase'; import DatabaseContents from './tests/support/DatabaseContents'; -// RNfirebase.database.enableLogging(true); -// RNfirebase.firestore.enableLogging(true); +// Verify logging works +RNfirebase.database.enableLogging(true); +RNfirebase.database.enableLogging(false); +RNfirebase.firestore.enableLogging(true); +RNfirebase.firestore.enableLogging(false); // RNfirebase.utils().logLevel = 'debug'; // RNfirebase.utils().logLevel = 'info'; diff --git a/tests/src/tests/firestore/documentReferenceTests.js b/tests/src/tests/firestore/documentReferenceTests.js index bd14e3d8..417c6166 100644 --- a/tests/src/tests/firestore/documentReferenceTests.js +++ b/tests/src/tests/firestore/documentReferenceTests.js @@ -37,6 +37,17 @@ function documentReferenceTests({ describe, it, context, firebase }) { const collection = document.collection('pages'); collection.id.should.equal('pages'); }); + + it('should error if invalid collection path supplied', () => { + (() => { + firebase.native + .firestore() + .doc('documents/doc1') + .collection('pages/page1'); + }).should.throw( + 'Argument "collectionPath" must point to a collection.' + ); + }); }); context('delete()', () => { @@ -586,12 +597,12 @@ function documentReferenceTests({ describe, it, context, firebase }) { (() => { docRef.update('error'); }).should.throw( - 'DocumentReference.update failed: If using a single argument, it must be an object.' + 'DocumentReference.update failed: If using a single update argument, it must be an object.' ); (() => { docRef.update('error1', 'error2', 'error3'); }).should.throw( - 'DocumentReference.update failed: Must have either a single object argument, or equal numbers of key/value pairs.' + 'DocumentReference.update failed: The update arguments must be either a single object argument, or equal numbers of key/value pairs.' ); (() => { docRef.update(0, 'error'); diff --git a/tests/src/tests/firestore/firestoreTests.js b/tests/src/tests/firestore/firestoreTests.js index 94b0c91b..1445069a 100644 --- a/tests/src/tests/firestore/firestoreTests.js +++ b/tests/src/tests/firestore/firestoreTests.js @@ -111,12 +111,12 @@ function firestoreTests({ describe, it, context, fcontext, firebase }) { (() => { batch.update(ref, 'error'); }).should.throw( - 'WriteBatch.update failed: If using two arguments, the second must be an object.' + 'WriteBatch.update failed: If using a single update argument, it must be an object.' ); (() => { batch.update(ref, 'error1', 'error2', 'error3'); }).should.throw( - 'WriteBatch.update failed: Must have a document reference, followed by either a single object argument, or equal numbers of key/value pairs.' + 'WriteBatch.update failed: The update arguments must be either a single object argument, or equal numbers of key/value pairs.' ); (() => { batch.update(ref, 0, 'error'); From 274e93d96c74db8edd975ed7142cd9c2eedf58d1 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Tue, 20 Mar 2018 17:49:08 +0000 Subject: [PATCH 57/77] Optimise firestore tests --- .../firestore/collectionReferenceTests.js | 83 +++++++++++-------- tests/src/tests/firestore/data/index.js | 24 ++++++ .../tests/firestore/documentReferenceTests.js | 21 ++++- tests/src/tests/firestore/fieldPathTests.js | 15 +++- tests/src/tests/firestore/fieldValueTests.js | 15 +++- tests/src/tests/firestore/firestoreTests.js | 34 ++++---- tests/src/tests/firestore/index.js | 35 +------- 7 files changed, 139 insertions(+), 88 deletions(-) create mode 100644 tests/src/tests/firestore/data/index.js diff --git a/tests/src/tests/firestore/collectionReferenceTests.js b/tests/src/tests/firestore/collectionReferenceTests.js index 7234b9d6..fd879acb 100644 --- a/tests/src/tests/firestore/collectionReferenceTests.js +++ b/tests/src/tests/firestore/collectionReferenceTests.js @@ -2,9 +2,10 @@ import sinon from 'sinon'; import 'should-sinon'; import should from 'should'; -import { COL_1, cleanCollection } from './index'; +import { cleanCollection, COL_DOC_1 } from './data'; function collectionReferenceTests({ + beforeEach, describe, it, context, @@ -13,6 +14,18 @@ function collectionReferenceTests({ after, }) { describe('CollectionReference', () => { + let collectionTestsCollection; + beforeEach(async () => { + collectionTestsCollection = firebase.native + .firestore() + .collection('collection-tests'); + + // We clean as part of initialisation in case a test errors + // We don't clean after the test as it slows tests significantly + await cleanCollection(collectionTestsCollection); + await collectionTestsCollection.doc('col1').set(COL_DOC_1); + }); + context('class', () => { it('should return instance methods', () => new Promise(resolve => { @@ -131,7 +144,7 @@ function collectionReferenceTests({ const collectionRef = firebase.native .firestore() .collection('collection-tests'); - const newDocValue = { ...COL_1, foo: 'updated' }; + const newDocValue = { ...COL_DOC_1, foo: 'updated' }; const callback = sinon.spy(); @@ -145,7 +158,7 @@ function collectionReferenceTests({ }); }); - callback.should.be.calledWith(COL_1); + callback.should.be.calledWith(COL_DOC_1); const docRef = firebase.native.firestore().doc('collection-tests/col1'); await docRef.set(newDocValue); @@ -182,7 +195,7 @@ function collectionReferenceTests({ }); }); - callback.should.be.calledWith(COL_1); + callback.should.be.calledWith(COL_DOC_1); const docRef = firebase.native.firestore().doc('collection-tests/col2'); await docRef.set(newDocValue); @@ -193,7 +206,7 @@ function collectionReferenceTests({ // Assertions - callback.should.be.calledWith(COL_1); + callback.should.be.calledWith(COL_DOC_1); callback.should.be.calledWith(newDocValue); callback.should.be.calledThrice(); @@ -219,10 +232,10 @@ function collectionReferenceTests({ }); }); - callback.should.be.calledWith(COL_1); + callback.should.be.calledWith(COL_DOC_1); const docRef = firebase.native.firestore().doc('collection-tests/col1'); - await docRef.set(COL_1); + await docRef.set(COL_DOC_1); await new Promise(resolve2 => { setTimeout(() => resolve2(), 5); @@ -242,7 +255,7 @@ function collectionReferenceTests({ const collectionRef = firebase.native .firestore() .collection('collection-tests'); - const newDocValue = { ...COL_1, foo: 'updated' }; + const newDocValue = { ...COL_DOC_1, foo: 'updated' }; const callbackA = sinon.spy(); const callbackB = sinon.spy(); @@ -263,10 +276,10 @@ function collectionReferenceTests({ }); }); - callbackA.should.be.calledWith(COL_1); + callbackA.should.be.calledWith(COL_DOC_1); callbackA.should.be.calledOnce(); - callbackB.should.be.calledWith(COL_1); + callbackB.should.be.calledWith(COL_DOC_1); callbackB.should.be.calledOnce(); const docRef = firebase.native.firestore().doc('collection-tests/col1'); @@ -293,7 +306,7 @@ function collectionReferenceTests({ const collectionRef = firebase.native .firestore() .collection('collection-tests'); - const newDocValue = { ...COL_1, foo: 'updated' }; + const newDocValue = { ...COL_DOC_1, foo: 'updated' }; const callbackA = sinon.spy(); const callbackB = sinon.spy(); @@ -314,10 +327,10 @@ function collectionReferenceTests({ }); }); - callbackA.should.be.calledWith(COL_1); + callbackA.should.be.calledWith(COL_DOC_1); callbackA.should.be.calledOnce(); - callbackB.should.be.calledWith(COL_1); + callbackB.should.be.calledWith(COL_DOC_1); callbackB.should.be.calledOnce(); const docRef = firebase.native.firestore().doc('collection-tests/col1'); @@ -337,13 +350,13 @@ function collectionReferenceTests({ unsubscribeA(); - await docRef.set(COL_1); + await docRef.set(COL_DOC_1); await new Promise(resolve2 => { setTimeout(() => resolve2(), 5); }); - callbackB.should.be.calledWith(COL_1); + callbackB.should.be.calledWith(COL_DOC_1); callbackA.should.be.calledTwice(); callbackB.should.be.calledThrice(); @@ -366,7 +379,7 @@ function collectionReferenceTests({ const collectionRef = firebase.native .firestore() .collection('collection-tests'); - const newDocValue = { ...COL_1, foo: 'updated' }; + const newDocValue = { ...COL_DOC_1, foo: 'updated' }; const callback = sinon.spy(); @@ -386,7 +399,7 @@ function collectionReferenceTests({ ); }); - callback.should.be.calledWith(COL_1); + callback.should.be.calledWith(COL_DOC_1); const docRef = firebase.native.firestore().doc('collection-tests/col1'); await docRef.set(newDocValue); @@ -408,7 +421,7 @@ function collectionReferenceTests({ const collectionRef = firebase.native .firestore() .collection('collection-tests'); - const newDocValue = { ...COL_1, foo: 'updated' }; + const newDocValue = { ...COL_DOC_1, foo: 'updated' }; const callback = sinon.spy(); @@ -425,7 +438,7 @@ function collectionReferenceTests({ unsubscribe = collectionRef.onSnapshot(observer); }); - callback.should.be.calledWith(COL_1); + callback.should.be.calledWith(COL_DOC_1); const docRef = firebase.native.firestore().doc('collection-tests/col1'); await docRef.set(newDocValue); @@ -448,7 +461,7 @@ function collectionReferenceTests({ const collectionRef = firebase.native .firestore() .collection('collection-tests'); - const newDocValue = { ...COL_1, foo: 'updated' }; + const newDocValue = { ...COL_DOC_1, foo: 'updated' }; const callback = sinon.spy(); @@ -472,7 +485,7 @@ function collectionReferenceTests({ ); }); - callback.should.be.calledWith(COL_1); + callback.should.be.calledWith(COL_DOC_1); const docRef = firebase.native.firestore().doc('collection-tests/col1'); await docRef.set(newDocValue); @@ -621,7 +634,7 @@ function collectionReferenceTests({ firebase.native .firestore() .collection('collection-tests') - .where('timestamp', '==', COL_1.timestamp) + .where('timestamp', '==', COL_DOC_1.timestamp) .get() .then(querySnapshot => { should.equal(querySnapshot.size, 1); @@ -631,7 +644,7 @@ function collectionReferenceTests({ firebase.native .firestore() .collection('collection-tests') - .where('geopoint', '==', COL_1.geopoint) + .where('geopoint', '==', COL_DOC_1.geopoint) .get() .then(querySnapshot => { should.equal(querySnapshot.size, 1); @@ -698,11 +711,11 @@ function collectionReferenceTests({ .firestore() .collection('collection-tests2'); await Promise.all([ - collectionTests.doc('col1').set(COL_1), - collectionTests.doc('col2').set({ ...COL_1, daz: 234 }), - collectionTests.doc('col3').set({ ...COL_1, daz: 234 }), - collectionTests.doc('col4').set({ ...COL_1, daz: 234 }), - collectionTests.doc('col5').set({ ...COL_1, daz: 234 }), + collectionTests.doc('col1').set(COL_DOC_1), + collectionTests.doc('col2').set({ ...COL_DOC_1, daz: 234 }), + collectionTests.doc('col3').set({ ...COL_DOC_1, daz: 234 }), + collectionTests.doc('col4').set({ ...COL_DOC_1, daz: 234 }), + collectionTests.doc('col5').set({ ...COL_DOC_1, daz: 234 }), ]); }); @@ -748,30 +761,30 @@ function collectionReferenceTests({ .firestore() .collection('collection-tests2'); await Promise.all([ - collectionTests.doc('col1').set({ ...COL_1, foo: 'bar0' }), + collectionTests.doc('col1').set({ ...COL_DOC_1, foo: 'bar0' }), collectionTests.doc('col2').set({ - ...COL_1, + ...COL_DOC_1, foo: 'bar1', daz: 234, object: { daz: 234 }, timestamp: new Date(2017, 2, 11, 10, 0, 0), }), collectionTests.doc('col3').set({ - ...COL_1, + ...COL_DOC_1, foo: 'bar2', daz: 345, object: { daz: 345 }, timestamp: new Date(2017, 2, 12, 10, 0, 0), }), collectionTests.doc('col4').set({ - ...COL_1, + ...COL_DOC_1, foo: 'bar3', daz: 456, object: { daz: 456 }, timestamp: new Date(2017, 2, 13, 10, 0, 0), }), collectionTests.doc('col5').set({ - ...COL_1, + ...COL_DOC_1, foo: 'bar4', daz: 567, object: { daz: 567 }, @@ -1191,7 +1204,7 @@ function collectionReferenceTests({ const collectionRef = collectionTests .orderBy('object.daz') .endAt(345); - const newDocValue = { ...COL_1, object: { daz: 346 } }; + const newDocValue = { ...COL_DOC_1, object: { daz: 346 } }; const callback = sinon.spy(); @@ -1230,7 +1243,7 @@ function collectionReferenceTests({ const collectionRef = collectionTests .where('baz', '==', true) .orderBy('daz'); - const newDocValue = { ...COL_1, daz: 678 }; + const newDocValue = { ...COL_DOC_1, daz: 678 }; const callback = sinon.spy(); diff --git a/tests/src/tests/firestore/data/index.js b/tests/src/tests/firestore/data/index.js new file mode 100644 index 00000000..e848fab4 --- /dev/null +++ b/tests/src/tests/firestore/data/index.js @@ -0,0 +1,24 @@ +import firebase from '../../../firebase'; + +export const COL_DOC_1 = { + baz: true, + daz: 123, + foo: 'bar', + gaz: 12.1234567, + geopoint: new firebase.native.firestore.GeoPoint(0, 0), + naz: null, + object: { + daz: 123, + }, + timestamp: new Date(2017, 2, 10, 10, 0, 0), +}; +export const DOC_1 = { name: 'doc1' }; +export const DOC_2 = { name: 'doc2', title: 'Document 2' }; + +/* HELPER FUNCTIONS */ +export async function cleanCollection(collection) { + const collectionTestsDocs = await collection.get(); + const tasks = []; + collectionTestsDocs.forEach(doc => tasks.push(doc.ref.delete())); + await Promise.all(tasks); +} diff --git a/tests/src/tests/firestore/documentReferenceTests.js b/tests/src/tests/firestore/documentReferenceTests.js index 417c6166..2587f14e 100644 --- a/tests/src/tests/firestore/documentReferenceTests.js +++ b/tests/src/tests/firestore/documentReferenceTests.js @@ -1,9 +1,28 @@ import sinon from 'sinon'; import 'should-sinon'; import should from 'should'; +import { cleanCollection, DOC_1 } from './data'; -function documentReferenceTests({ describe, it, context, firebase }) { +function documentReferenceTests({ + beforeEach, + describe, + it, + context, + firebase, +}) { describe('DocumentReference', () => { + let documentTestsCollection; + beforeEach(async () => { + documentTestsCollection = firebase.native + .firestore() + .collection('document-tests'); + + // We clean as part of initialisation in case a test errors + // We don't clean after the test as it slows tests significantly + await cleanCollection(documentTestsCollection); + await documentTestsCollection.doc('doc1').set(DOC_1); + }); + context('class', () => { it('should return instance methods', () => new Promise(resolve => { diff --git a/tests/src/tests/firestore/fieldPathTests.js b/tests/src/tests/firestore/fieldPathTests.js index aba24123..f7d321fd 100644 --- a/tests/src/tests/firestore/fieldPathTests.js +++ b/tests/src/tests/firestore/fieldPathTests.js @@ -1,6 +1,7 @@ import should from 'should'; +import { cleanCollection, COL_DOC_1 } from './data'; -function fieldPathTests({ describe, it, context, firebase }) { +function fieldPathTests({ before, describe, it, context, firebase }) { describe('FieldPath', () => { context('documentId', () => { it('should be a FieldPath', () => { @@ -10,6 +11,18 @@ function fieldPathTests({ describe, it, context, firebase }) { }); context('DocumentSnapshot.get()', () => { + let collectionTestsCollection; + before(async () => { + collectionTestsCollection = firebase.native + .firestore() + .collection('collection-tests'); + + // We clean as part of initialisation in case a test errors + // We don't clean after the test as it slows tests significantly + await cleanCollection(collectionTestsCollection); + await collectionTestsCollection.doc('col1').set(COL_DOC_1); + }); + it('should get the correct values', () => firebase.native .firestore() diff --git a/tests/src/tests/firestore/fieldValueTests.js b/tests/src/tests/firestore/fieldValueTests.js index ff145f0d..b9812f53 100644 --- a/tests/src/tests/firestore/fieldValueTests.js +++ b/tests/src/tests/firestore/fieldValueTests.js @@ -1,7 +1,20 @@ import should from 'should'; +import { cleanCollection, DOC_2 } from './data'; -function fieldValueTests({ describe, it, context, firebase }) { +function fieldValueTests({ beforeEach, describe, it, context, firebase }) { describe('FieldValue', () => { + let documentTestsCollection; + beforeEach(async () => { + documentTestsCollection = firebase.native + .firestore() + .collection('document-tests'); + + // We clean as part of initialisation in case a test errors + // We don't clean after the test as it slows tests significantly + await cleanCollection(documentTestsCollection); + await documentTestsCollection.doc('doc2').set(DOC_2); + }); + context('delete()', () => { it('should delete field', () => firebase.native diff --git a/tests/src/tests/firestore/firestoreTests.js b/tests/src/tests/firestore/firestoreTests.js index 1445069a..a6dc4e7c 100644 --- a/tests/src/tests/firestore/firestoreTests.js +++ b/tests/src/tests/firestore/firestoreTests.js @@ -1,6 +1,7 @@ import should from 'should'; +import { cleanCollection } from './data'; -function firestoreTests({ describe, it, context, fcontext, firebase }) { +function firestoreTests({ before, describe, it, context, firebase }) { describe('firestore()', () => { context('collection()', () => { it('should create CollectionReference with the right id', () => @@ -39,23 +40,22 @@ function firestoreTests({ describe, it, context, fcontext, firebase }) { }); context('batch()', () => { + let firestoreTestsCollection; + before(async () => { + firestoreTestsCollection = firebase.native + .firestore() + .collection('firestore-tests'); + + // We clean as part of initialisation in case a test errors + // We don't clean after the test as it slows tests significantly + await cleanCollection(firestoreTestsCollection); + }); + it('should create / update / delete as expected', () => { - const ayRef = firebase.native - .firestore() - .collection('firestore-tests') - .doc('AY'); - const lRef = firebase.native - .firestore() - .collection('firestore-tests') - .doc('LON'); - const nycRef = firebase.native - .firestore() - .collection('firestore-tests') - .doc('NYC'); - const sfRef = firebase.native - .firestore() - .collection('firestore-tests') - .doc('SF'); + const ayRef = firestoreTestsCollection.doc('AY'); + const lRef = firestoreTestsCollection.doc('LON'); + const nycRef = firestoreTestsCollection.doc('NYC'); + const sfRef = firestoreTestsCollection.doc('SF'); return firebase.native .firestore() diff --git a/tests/src/tests/firestore/index.js b/tests/src/tests/firestore/index.js index 7b75bcb5..554eb5e6 100644 --- a/tests/src/tests/firestore/index.js +++ b/tests/src/tests/firestore/index.js @@ -23,9 +23,6 @@ export const COL_1 = { timestamp: new Date(2017, 2, 10, 10, 0, 0), }; -export const DOC_1 = { name: 'doc1' }; -export const DOC_2 = { name: 'doc2', title: 'Document 2' }; - const suite = new TestSuite('Firestore', 'firebase.firestore()', firebase); const testGroups = [ @@ -38,31 +35,11 @@ const testGroups = [ function firestoreTestSuite(testSuite) { testSuite.beforeEach(async () => { - this.collectionTestsCollection = testSuite.firebase.native - .firestore() - .collection('collection-tests'); - this.documentTestsCollection = testSuite.firebase.native - .firestore() - .collection('document-tests'); - this.firestoreTestsCollection = testSuite.firebase.native - .firestore() - .collection('firestore-tests'); - // Make sure the collections are cleaned and initialised correctly - await cleanCollection(this.collectionTestsCollection); - await cleanCollection(this.documentTestsCollection); - await cleanCollection(this.firestoreTestsCollection); - - const tasks = []; - tasks.push(this.collectionTestsCollection.doc('col1').set(COL_1)); - tasks.push(this.documentTestsCollection.doc('doc1').set(DOC_1)); - tasks.push(this.documentTestsCollection.doc('doc2').set(DOC_2)); - - await Promise.all(tasks); + // Do nothing }); testSuite.afterEach(async () => { - // All data will be cleaned an re-initialised before each test - // Adding a clean here slows down the test suite dramatically + // Do nothing }); testGroups.forEach(testGroup => { @@ -76,11 +53,3 @@ function firestoreTestSuite(testSuite) { suite.addTests(firestoreTestSuite); export default suite; - -/* HELPER FUNCTIONS */ -export async function cleanCollection(collection) { - const collectionTestsDocs = await collection.get(); - const tasks = []; - collectionTestsDocs.forEach(doc => tasks.push(doc.ref.delete())); - await Promise.all(tasks); -} From c178a7cbd18894d33b5491a1093a03a1d633d3f3 Mon Sep 17 00:00:00 2001 From: Michael Diarmid Date: Tue, 20 Mar 2018 17:57:39 +0000 Subject: [PATCH 58/77] update logo --- React-Native-Firebase.svg | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/React-Native-Firebase.svg b/React-Native-Firebase.svg index 685431f9..e5b62202 100644 --- a/React-Native-Firebase.svg +++ b/React-Native-Firebase.svg @@ -1 +1,32 @@ -Slice 1 \ No newline at end of file + + + + +Slice 1 + + + + + + + + + + + + + + + + + From 213f03737bb35808e8493362691b96fb47506ae1 Mon Sep 17 00:00:00 2001 From: Michael Diarmid Date: Tue, 20 Mar 2018 17:58:40 +0000 Subject: [PATCH 59/77] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2aa8743e..98b82909 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

-
+

React Native Firebase

From cc8799b465d1ff24da69ef0fab9e8c0b9b69de15 Mon Sep 17 00:00:00 2001 From: JinHao Chen Date: Tue, 21 Nov 2017 15:37:05 -0800 Subject: [PATCH 60/77] Implement multiple database shard support --- .../firebase/database/RNFirebaseDatabase.java | 94 ++++++++++--------- .../database/RNFirebaseDatabaseReference.java | 13 ++- .../RNFirebaseTransactionHandler.java | 6 +- ios/RNFirebase/database/RNFirebaseDatabase.h | 1 + ios/RNFirebase/database/RNFirebaseDatabase.m | 88 +++++++++++------ .../database/RNFirebaseDatabaseReference.h | 3 +- .../database/RNFirebaseDatabaseReference.m | 4 +- lib/modules/admob/index.js | 1 + lib/modules/analytics/index.js | 1 + lib/modules/auth/index.js | 1 + lib/modules/config/index.js | 1 + lib/modules/crash/index.js | 1 + lib/modules/database/Reference.js | 10 +- lib/modules/database/index.js | 41 ++++++-- lib/modules/fabric/crashlytics/index.js | 1 + lib/modules/firestore/index.js | 1 + lib/modules/links/index.js | 1 + lib/modules/messaging/index.js | 1 + lib/modules/perf/index.js | 1 + lib/modules/storage/index.js | 1 + lib/modules/utils/index.js | 1 + lib/types/index.js | 1 + lib/utils/ModuleBase.js | 6 +- lib/utils/apps.js | 33 +++++-- lib/utils/internals.js | 7 ++ lib/utils/native.js | 39 +++++--- 26 files changed, 242 insertions(+), 116 deletions(-) diff --git a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java index 59eb04d4..e628fc05 100644 --- a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java +++ b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java @@ -53,16 +53,16 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param appName */ @ReactMethod - public void goOnline(String appName) { - getDatabaseForApp(appName).goOnline(); + public void goOnline(String appName, String dbURL) { + getDatabaseForApp(appName, dbURL).goOnline(); } /** * @param appName */ @ReactMethod - public void goOffline(String appName) { - getDatabaseForApp(appName).goOffline(); + public void goOffline(String appName, String dbURL) { + getDatabaseForApp(appName, dbURL).goOffline(); } /** @@ -70,8 +70,8 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param state */ @ReactMethod - public void setPersistence(String appName, Boolean state) { - getDatabaseForApp(appName).setPersistenceEnabled(state); + public void setPersistence(String appName, String dbURL, Boolean state) { + getDatabaseForApp(appName, dbURL).setPersistenceEnabled(state); } /** @@ -79,8 +79,8 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param size */ @ReactMethod - public void setPersistenceCacheSizeBytes(String appName, int size) { - getDatabaseForApp(appName).setPersistenceCacheSizeBytes((long) size); + public void setPersistenceCacheSizeBytes(String appName, String dbURL, int size) { + getDatabaseForApp(appName, dbURL).setPersistenceCacheSizeBytes((long) size); } @@ -116,8 +116,8 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param state */ @ReactMethod - public void keepSynced(String appName, String key, String path, ReadableArray modifiers, Boolean state) { - getInternalReferenceForApp(appName, key, path, modifiers).getQuery().keepSynced(state); + public void keepSynced(String appName, String dbURL, String key, String path, ReadableArray modifiers, Boolean state) { + getInternalReferenceForApp(appName, dbURL, key, path, modifiers).getQuery().keepSynced(state); } @@ -130,7 +130,7 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param updates */ @ReactMethod - public void transactionTryCommit(String appName, int transactionId, ReadableMap updates) { + public void transactionTryCommit(String appName, String dbURL, int transactionId, ReadableMap updates) { RNFirebaseTransactionHandler handler = transactionHandlers.get(transactionId); if (handler != null) { @@ -147,16 +147,16 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param applyLocally */ @ReactMethod - public void transactionStart(final String appName, final String path, final int transactionId, final Boolean applyLocally) { + public void transactionStart(final String appName, final String dbURL, final String path, final int transactionId, final Boolean applyLocally) { AsyncTask.execute(new Runnable() { @Override public void run() { - DatabaseReference reference = getReferenceForAppPath(appName, path); + DatabaseReference reference = getReferenceForAppPath(appName, dbURL, path); reference.runTransaction(new Transaction.Handler() { @Override public Transaction.Result doTransaction(MutableData mutableData) { - final RNFirebaseTransactionHandler transactionHandler = new RNFirebaseTransactionHandler(transactionId, appName); + final RNFirebaseTransactionHandler transactionHandler = new RNFirebaseTransactionHandler(transactionId, appName, dbURL); transactionHandlers.put(transactionId, transactionHandler); final WritableMap updatesMap = transactionHandler.createUpdateMap(mutableData); @@ -212,9 +212,9 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param promise */ @ReactMethod - public void onDisconnectSet(String appName, String path, ReadableMap props, final Promise promise) { + public void onDisconnectSet(String appName, String dbURL, String path, ReadableMap props, final Promise promise) { String type = props.getString("type"); - DatabaseReference ref = getReferenceForAppPath(appName, path); + DatabaseReference ref = getReferenceForAppPath(appName, dbURL, path); OnDisconnect onDisconnect = ref.onDisconnect(); DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { @@ -257,8 +257,8 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param promise */ @ReactMethod - public void onDisconnectUpdate(String appName, String path, ReadableMap props, final Promise promise) { - DatabaseReference ref = getReferenceForAppPath(appName, path); + public void onDisconnectUpdate(String appName, String dbURL, String path, ReadableMap props, final Promise promise) { + DatabaseReference ref = getReferenceForAppPath(appName, dbURL, path); OnDisconnect ondDisconnect = ref.onDisconnect(); Map map = Utils.recursivelyDeconstructReadableMap(props); @@ -279,8 +279,8 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param promise */ @ReactMethod - public void onDisconnectRemove(String appName, String path, final Promise promise) { - DatabaseReference ref = getReferenceForAppPath(appName, path); + public void onDisconnectRemove(String appName, String dbURL, String path, final Promise promise) { + DatabaseReference ref = getReferenceForAppPath(appName, dbURL, path); OnDisconnect onDisconnect = ref.onDisconnect(); onDisconnect.removeValue(new DatabaseReference.CompletionListener() { @@ -299,8 +299,8 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param promise */ @ReactMethod - public void onDisconnectCancel(String appName, String path, final Promise promise) { - DatabaseReference ref = getReferenceForAppPath(appName, path); + public void onDisconnectCancel(String appName, String dbURL, String path, final Promise promise) { + DatabaseReference ref = getReferenceForAppPath(appName, dbURL, path); OnDisconnect onDisconnect = ref.onDisconnect(); onDisconnect.cancel(new DatabaseReference.CompletionListener() { @@ -318,8 +318,8 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param promise */ @ReactMethod - public void set(String appName, String path, ReadableMap props, final Promise promise) { - DatabaseReference ref = getReferenceForAppPath(appName, path); + public void set(String appName, String dbURL, String path, ReadableMap props, final Promise promise) { + DatabaseReference ref = getReferenceForAppPath(appName, dbURL, path); Object value = Utils.recursivelyDeconstructReadableMap(props).get("value"); DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { @@ -339,8 +339,8 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param promise */ @ReactMethod - public void setPriority(String appName, String path, ReadableMap priority, final Promise promise) { - DatabaseReference ref = getReferenceForAppPath(appName, path); + public void setPriority(String appName, String dbURL, String path, ReadableMap priority, final Promise promise) { + DatabaseReference ref = getReferenceForAppPath(appName, dbURL, path); Object priorityValue = Utils.recursivelyDeconstructReadableMap(priority).get("value"); DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { @@ -361,8 +361,8 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param promise */ @ReactMethod - public void setWithPriority(String appName, String path, ReadableMap data, ReadableMap priority, final Promise promise) { - DatabaseReference ref = getReferenceForAppPath(appName, path); + public void setWithPriority(String appName, String dbURL, String path, ReadableMap data, ReadableMap priority, final Promise promise) { + DatabaseReference ref = getReferenceForAppPath(appName, dbURL, path); Object dataValue = Utils.recursivelyDeconstructReadableMap(data).get("value"); Object priorityValue = Utils.recursivelyDeconstructReadableMap(priority).get("value"); @@ -383,8 +383,8 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param promise */ @ReactMethod - public void update(String appName, String path, ReadableMap props, final Promise promise) { - DatabaseReference ref = getReferenceForAppPath(appName, path); + public void update(String appName, String dbURL, String path, ReadableMap props, final Promise promise) { + DatabaseReference ref = getReferenceForAppPath(appName, dbURL, path); Map updates = Utils.recursivelyDeconstructReadableMap(props); DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { @@ -403,8 +403,8 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param promise */ @ReactMethod - public void remove(String appName, String path, final Promise promise) { - DatabaseReference ref = getReferenceForAppPath(appName, path); + public void remove(String appName, String dbURL, String path, final Promise promise) { + DatabaseReference ref = getReferenceForAppPath(appName, dbURL, path); DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { @Override @@ -428,8 +428,8 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param promise */ @ReactMethod - public void once(String appName, String key, String path, ReadableArray modifiers, String eventType, Promise promise) { - getInternalReferenceForApp(appName, key, path, modifiers).once(eventType, promise); + public void once(String appName, String dbURL, String key, String path, ReadableArray modifiers, String eventType, Promise promise) { + getInternalReferenceForApp(appName, dbURL, key, path, modifiers).once(eventType, promise); } /** @@ -439,8 +439,8 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param props ReadableMap */ @ReactMethod - public void on(String appName, ReadableMap props) { - getCachedInternalReferenceForApp(appName, props) + public void on(String appName, String dbURL, ReadableMap props) { + getCachedInternalReferenceForApp(appName, dbURL, props) .on( props.getString("eventType"), props.getMap("registration") @@ -496,9 +496,14 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param appName * @return */ - private FirebaseDatabase getDatabaseForApp(String appName) { - FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); - FirebaseDatabase firebaseDatabase = FirebaseDatabase.getInstance(firebaseApp); + private FirebaseDatabase getDatabaseForApp(String appName, String dbURL) { + FirebaseDatabase firebaseDatabase; + if(dbURL != null && dbURL.length() > 0) { + firebaseDatabase = FirebaseDatabase.getInstance(dbURL); + } else { + FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); + firebaseDatabase = FirebaseDatabase.getInstance(firebaseApp); + } Boolean logLevel = loggingLevelSet.get(firebaseDatabase.getApp().getName()); if (enableLogging && (logLevel == null || !logLevel)) { @@ -535,8 +540,8 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param path * @return */ - private DatabaseReference getReferenceForAppPath(String appName, String path) { - return getDatabaseForApp(appName).getReference(path); + private DatabaseReference getReferenceForAppPath(String appName, String dbURL, String path) { + return getDatabaseForApp(appName, dbURL).getReference(path); } /** @@ -548,10 +553,11 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param modifiers * @return */ - private RNFirebaseDatabaseReference getInternalReferenceForApp(String appName, String key, String path, ReadableArray modifiers) { + private RNFirebaseDatabaseReference getInternalReferenceForApp(String appName, String dbURL, String key, String path, ReadableArray modifiers) { return new RNFirebaseDatabaseReference( getReactApplicationContext(), appName, + dbURL, key, path, modifiers @@ -565,7 +571,7 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @param props * @return */ - private RNFirebaseDatabaseReference getCachedInternalReferenceForApp(String appName, ReadableMap props) { + private RNFirebaseDatabaseReference getCachedInternalReferenceForApp(String appName, String dbURL, ReadableMap props) { String key = props.getString("key"); String path = props.getString("path"); ReadableArray modifiers = props.getArray("modifiers"); @@ -573,7 +579,7 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { RNFirebaseDatabaseReference existingRef = references.get(key); if (existingRef == null) { - existingRef = getInternalReferenceForApp(appName, key, path, modifiers); + existingRef = getInternalReferenceForApp(appName, dbURL, key, path, modifiers); references.put(key, existingRef); } diff --git a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java index 9cf197de..1e8f68cb 100644 --- a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java +++ b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java @@ -28,6 +28,7 @@ class RNFirebaseDatabaseReference { private String key; private Query query; private String appName; + private String dbURL; private ReactContext reactContext; private static final String TAG = "RNFirebaseDBReference"; private HashMap childEventListeners = new HashMap<>(); @@ -43,10 +44,11 @@ class RNFirebaseDatabaseReference { * @param refPath * @param modifiersArray */ - RNFirebaseDatabaseReference(ReactContext context, String app, String refKey, String refPath, ReadableArray modifiersArray) { + RNFirebaseDatabaseReference(ReactContext context, String app, String url, String refKey, String refPath, ReadableArray modifiersArray) { key = refKey; query = null; appName = app; + dbURL = url; reactContext = context; buildDatabaseQueryAtPathAndModifiers(refPath, modifiersArray); } @@ -346,8 +348,13 @@ class RNFirebaseDatabaseReference { * @return */ private void buildDatabaseQueryAtPathAndModifiers(String path, ReadableArray modifiers) { - FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); - FirebaseDatabase firebaseDatabase = FirebaseDatabase.getInstance(firebaseApp); + FirebaseDatabase firebaseDatabase; + if(dbURL != null && dbURL.length() > 0) { + firebaseDatabase = FirebaseDatabase.getInstance(dbURL); + } else { + FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); + firebaseDatabase= FirebaseDatabase.getInstance(firebaseApp); + } query = firebaseDatabase.getReference(path); List modifiersList = Utils.recursivelyDeconstructReadableArray(modifiers); diff --git a/android/src/main/java/io/invertase/firebase/database/RNFirebaseTransactionHandler.java b/android/src/main/java/io/invertase/firebase/database/RNFirebaseTransactionHandler.java index 19022e98..3c3f9c08 100644 --- a/android/src/main/java/io/invertase/firebase/database/RNFirebaseTransactionHandler.java +++ b/android/src/main/java/io/invertase/firebase/database/RNFirebaseTransactionHandler.java @@ -21,6 +21,7 @@ import io.invertase.firebase.Utils; public class RNFirebaseTransactionHandler { private int transactionId; private String appName; + private String dbURL; private final ReentrantLock lock; private final Condition condition; private Map data; @@ -31,8 +32,9 @@ public class RNFirebaseTransactionHandler { boolean abort = false; boolean timeout = false; - RNFirebaseTransactionHandler(int id, String app) { + RNFirebaseTransactionHandler(int id, String app, String url) { appName = app; + dbURL = url; transactionId = id; lock = new ReentrantLock(); condition = lock.newCondition(); @@ -107,6 +109,7 @@ public class RNFirebaseTransactionHandler { // all events get distributed js side based on app name updatesMap.putString("appName", appName); + updatesMap.putString("dbURL", dbURL); if (!updatesData.hasChildren()) { Utils.mapPutValue("value", updatesData.getValue(), updatesMap); @@ -129,6 +132,7 @@ public class RNFirebaseTransactionHandler { resultMap.putInt("id", transactionId); resultMap.putString("appName", appName); + resultMap.putString("dbURL", dbURL); resultMap.putBoolean("timeout", timeout); resultMap.putBoolean("committed", committed); diff --git a/ios/RNFirebase/database/RNFirebaseDatabase.h b/ios/RNFirebase/database/RNFirebaseDatabase.h index e88d5b5a..86840a2d 100644 --- a/ios/RNFirebase/database/RNFirebaseDatabase.h +++ b/ios/RNFirebase/database/RNFirebaseDatabase.h @@ -17,6 +17,7 @@ + (void)handlePromise:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject databaseError:(NSError *)databaseError; + (FIRDatabase *)getDatabaseForApp:(NSString *)appDisplayName; ++ (FIRDatabase *)getDatabaseForApp:(NSString *)appDisplayName URL:(NSString *)url; + (NSDictionary *)getJSError:(NSError *)nativeError; diff --git a/ios/RNFirebase/database/RNFirebaseDatabase.m b/ios/RNFirebase/database/RNFirebaseDatabase.m index 9d6b2730..fac42b79 100644 --- a/ios/RNFirebase/database/RNFirebaseDatabase.m +++ b/ios/RNFirebase/database/RNFirebaseDatabase.m @@ -22,22 +22,26 @@ RCT_EXPORT_MODULE(); return self; } -RCT_EXPORT_METHOD(goOnline:(NSString *)appDisplayName) { - [[RNFirebaseDatabase getDatabaseForApp:appDisplayName] goOnline]; +RCT_EXPORT_METHOD(goOnline:(NSString *)appDisplayName + dbURL:(NSString *)dbURL) { + [[RNFirebaseDatabase getDatabaseForApp:appDisplayName URL:dbURL] goOnline]; } -RCT_EXPORT_METHOD(goOffline:(NSString *)appDisplayName) { - [[RNFirebaseDatabase getDatabaseForApp:appDisplayName] goOffline]; +RCT_EXPORT_METHOD(goOffline:(NSString *)appDisplayName + dbURL:(NSString *)dbURL) { + [[RNFirebaseDatabase getDatabaseForApp:appDisplayName URL:dbURL] goOffline]; } RCT_EXPORT_METHOD(setPersistence:(NSString *)appDisplayName + dbURL:(NSString *)dbURL state:(BOOL)state) { - [RNFirebaseDatabase getDatabaseForApp:appDisplayName].persistenceEnabled = state; + [RNFirebaseDatabase getDatabaseForApp:appDisplayName URL:dbURL].persistenceEnabled = state; } RCT_EXPORT_METHOD(setPersistenceCacheSizeBytes:(NSString *)appDisplayName + dbURL:(NSString *)dbURL size:(NSInteger *)size) { - [RNFirebaseDatabase getDatabaseForApp:appDisplayName].persistenceCacheSizeBytes = (NSUInteger)size; + [RNFirebaseDatabase getDatabaseForApp:appDisplayName URL:dbURL].persistenceCacheSizeBytes = (NSUInteger)size; } RCT_EXPORT_METHOD(enableLogging:(BOOL)enabled) { @@ -45,15 +49,17 @@ RCT_EXPORT_METHOD(enableLogging:(BOOL)enabled) { } RCT_EXPORT_METHOD(keepSynced:(NSString *)appDisplayName + dbURL:(NSString *)dbURL key:(NSString *)key path:(NSString *)path modifiers:(NSArray *)modifiers state:(BOOL)state) { - FIRDatabaseQuery *query = [self getInternalReferenceForApp:appDisplayName key:key path:path modifiers:modifiers].query; + FIRDatabaseQuery *query = [self getInternalReferenceForApp:appDisplayName dbURL:dbURL key:key path:path modifiers:modifiers].query; [query keepSynced:state]; } RCT_EXPORT_METHOD(transactionTryCommit:(NSString *)appDisplayName + dbURL:(NSString *)dbURL transactionId:(nonnull NSNumber *)transactionId updates:(NSDictionary *)updates) { __block NSMutableDictionary *transactionState; @@ -83,6 +89,7 @@ RCT_EXPORT_METHOD(transactionTryCommit:(NSString *)appDisplayName RCT_EXPORT_METHOD(transactionStart:(NSString *)appDisplayName + dbURL:(NSString *)dbURL path:(NSString *)path transactionId:(nonnull NSNumber *)transactionId applyLocally:(BOOL)applyLocally) { @@ -90,12 +97,12 @@ RCT_EXPORT_METHOD(transactionStart:(NSString *)appDisplayName NSMutableDictionary *transactionState = [NSMutableDictionary new]; dispatch_semaphore_t sema = dispatch_semaphore_create(0); transactionState[@"semaphore"] = sema; - FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName path:path]; + FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName dbURL:dbURL path:path]; [ref runTransactionBlock:^FIRTransactionResult *_Nonnull (FIRMutableData *_Nonnull currentData) { dispatch_barrier_async(_transactionQueue, ^{ [_transactions setValue:transactionState forKey:[transactionId stringValue]]; - NSDictionary *updateMap = [self createTransactionUpdateMap:appDisplayName transactionId:transactionId updatesData:currentData]; + NSDictionary *updateMap = [self createTransactionUpdateMap:appDisplayName dbURL:dbURL transactionId:transactionId updatesData:currentData]; [RNFirebaseUtil sendJSEvent:self name:DATABASE_TRANSACTION_EVENT body:updateMap]; }); @@ -120,123 +127,134 @@ RCT_EXPORT_METHOD(transactionStart:(NSString *)appDisplayName return [FIRTransactionResult successWithValue:currentData]; } } andCompletionBlock:^(NSError *_Nullable databaseError, BOOL committed, FIRDataSnapshot *_Nullable snapshot) { - NSDictionary *resultMap = [self createTransactionResultMap:appDisplayName transactionId:transactionId error:databaseError committed:committed snapshot:snapshot]; + NSDictionary *resultMap = [self createTransactionResultMap:appDisplayName dbURL:dbURL transactionId:transactionId error:databaseError committed:committed snapshot:snapshot]; [RNFirebaseUtil sendJSEvent:self name:DATABASE_TRANSACTION_EVENT body:resultMap]; } withLocalEvents:applyLocally]; }); } RCT_EXPORT_METHOD(onDisconnectSet:(NSString *)appDisplayName + dbURL:(NSString *)dbURL path:(NSString *)path props:(NSDictionary *)props resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName path:path]; + FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName dbURL:dbURL path:path]; [ref onDisconnectSetValue:props[@"value"] withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) { [RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error]; }]; } RCT_EXPORT_METHOD(onDisconnectUpdate:(NSString *)appDisplayName + dbURL:(NSString *)dbURL path:(NSString *)path props:(NSDictionary *)props resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName path:path]; + FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName dbURL:dbURL path:path]; [ref onDisconnectUpdateChildValues:props withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) { [RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error]; }]; } RCT_EXPORT_METHOD(onDisconnectRemove:(NSString *)appDisplayName + dbURL:(NSString *)dbURL path:(NSString *)path resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName path:path]; + FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName dbURL:dbURL path:path]; [ref onDisconnectRemoveValueWithCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) { [RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error]; }]; } RCT_EXPORT_METHOD(onDisconnectCancel:(NSString *)appDisplayName + dbURL:(NSString *)dbURL path:(NSString *)path resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName path:path]; + FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName dbURL:dbURL path:path]; [ref cancelDisconnectOperationsWithCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) { [RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error]; }]; } RCT_EXPORT_METHOD(set:(NSString *)appDisplayName + dbURL:(NSString *)dbURL path:(NSString *)path props:(NSDictionary *)props resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName path:path]; + FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName dbURL:dbURL path:path]; [ref setValue:[props valueForKey:@"value"] withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) { [RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error]; }]; } RCT_EXPORT_METHOD(setPriority:(NSString *)appDisplayName + dbURL:(NSString *)dbURL path:(NSString *)path priority:(NSDictionary *)priority resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName path:path]; + FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName dbURL:dbURL path:path]; [ref setPriority:[priority valueForKey:@"value"] withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull ref) { [RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error]; }]; } RCT_EXPORT_METHOD(setWithPriority:(NSString *)appDisplayName + dbURL:(NSString *)dbURL path:(NSString *)path data:(NSDictionary *)data priority:(NSDictionary *)priority resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName path:path]; + FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName dbURL:dbURL path:path]; [ref setValue:[data valueForKey:@"value"] andPriority:[priority valueForKey:@"value"] withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull ref) { [RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error]; }]; } RCT_EXPORT_METHOD(update:(NSString *)appDisplayName + dbURL:(NSString *)dbURL path:(NSString *)path props:(NSDictionary *)props resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName path:path]; + FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName dbURL:dbURL path:path]; [ref updateChildValues:props withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) { [RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error]; }]; } RCT_EXPORT_METHOD(remove:(NSString *)appDisplayName + dbURL:(NSString *)dbURL path:(NSString *)path resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName path:path]; + FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName dbURL:dbURL path:path]; [ref removeValueWithCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) { [RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error]; }]; } RCT_EXPORT_METHOD(once:(NSString *)appDisplayName + dbURL:(NSString *)dbURL key:(NSString *)key path:(NSString *)path modifiers:(NSArray *)modifiers eventName:(NSString *)eventName resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - RNFirebaseDatabaseReference *ref = [self getInternalReferenceForApp:appDisplayName key:key path:path modifiers:modifiers]; + RNFirebaseDatabaseReference *ref = [self getInternalReferenceForApp:appDisplayName dbURL:dbURL key:key path:path modifiers:modifiers]; [ref once:eventName resolver:resolve rejecter:reject]; } RCT_EXPORT_METHOD(on:(NSString *)appDisplayName + dbURL:(NSString *)dbURL props:(NSDictionary *)props) { - RNFirebaseDatabaseReference *ref = [self getCachedInternalReferenceForApp:appDisplayName props:props]; + RNFirebaseDatabaseReference *ref = [self getCachedInternalReferenceForApp:appDisplayName dbURL:dbURL props:props]; [ref on:props[@"eventType"] registration:props[@"registration"]]; } @@ -271,23 +289,31 @@ RCT_EXPORT_METHOD(off:(NSString *)key return [FIRDatabase databaseForApp:app]; } -- (FIRDatabaseReference *)getReferenceForAppPath:(NSString *)appDisplayName path:(NSString *)path { - return [[RNFirebaseDatabase getDatabaseForApp:appDisplayName] referenceWithPath:path]; ++ (FIRDatabase *)getDatabaseForApp:(NSString *)appDisplayName URL:(NSString *)url { + if (url == nil) { + return [self getDatabaseForApp:appDisplayName]; + } + FIRApp *app = [RNFirebaseUtil getApp:appDisplayName]; + return [FIRDatabase databaseForApp:app URL:url]; } -- (RNFirebaseDatabaseReference *)getInternalReferenceForApp:(NSString *)appDisplayName key:(NSString *)key path:(NSString *)path modifiers:(NSArray *)modifiers { - return [[RNFirebaseDatabaseReference alloc] initWithPathAndModifiers:self appDisplayName:appDisplayName key:key refPath:path modifiers:modifiers]; +- (FIRDatabaseReference *)getReferenceForAppPath:(NSString *)appDisplayName dbURL:(NSString *)dbURL path:(NSString *)path { + return [[RNFirebaseDatabase getDatabaseForApp:appDisplayName URL:dbURL] referenceWithPath:path]; } -- (RNFirebaseDatabaseReference *)getCachedInternalReferenceForApp:(NSString *)appDisplayName props:(NSDictionary *)props { +- (RNFirebaseDatabaseReference *)getInternalReferenceForApp:(NSString *)appDisplayName dbURL:(NSString *)dbURL key:(NSString *)key path:(NSString *)path modifiers:(NSArray *)modifiers { + return [[RNFirebaseDatabaseReference alloc] initWithPathAndModifiers:self appDisplayName:appDisplayName dbURL:dbURL key:key refPath:path modifiers:modifiers]; +} + +- (RNFirebaseDatabaseReference *)getCachedInternalReferenceForApp:(NSString *)appDisplayName dbURL:(NSString *)dbURL props:(NSDictionary *)props { NSString *key = props[@"key"]; NSString *path = props[@"path"]; - NSDictionary *modifiers = props[@"modifiers"]; + NSArray *modifiers = props[@"modifiers"]; RNFirebaseDatabaseReference *ref = _dbReferences[key]; if (ref == nil) { - ref = [[RNFirebaseDatabaseReference alloc] initWithPathAndModifiers:self appDisplayName:appDisplayName key:key refPath:path modifiers:modifiers]; + ref = [[RNFirebaseDatabaseReference alloc] initWithPathAndModifiers:self appDisplayName:appDisplayName dbURL:dbURL key:key refPath:path modifiers:modifiers]; _dbReferences[key] = ref; } return ref; @@ -375,20 +401,22 @@ RCT_EXPORT_METHOD(off:(NSString *)key return errorMap; } -- (NSDictionary *)createTransactionUpdateMap:(NSString *)appDisplayName transactionId:(NSNumber *)transactionId updatesData:(FIRMutableData *)updatesData { +- (NSDictionary *)createTransactionUpdateMap:(NSString *)appDisplayName dbURL:(NSString *)dbURL transactionId:(NSNumber *)transactionId updatesData:(FIRMutableData *)updatesData { NSMutableDictionary *updatesMap = [[NSMutableDictionary alloc] init]; [updatesMap setValue:transactionId forKey:@"id"]; [updatesMap setValue:@"update" forKey:@"type"]; [updatesMap setValue:appDisplayName forKey:@"appName"]; + [updatesMap setValue:dbURL forKey:@"dbURL"]; [updatesMap setValue:updatesData.value forKey:@"value"]; return updatesMap; } -- (NSDictionary *)createTransactionResultMap:(NSString *)appDisplayName transactionId:(NSNumber *)transactionId error:(NSError *)error committed:(BOOL)committed snapshot:(FIRDataSnapshot *)snapshot { +- (NSDictionary *)createTransactionResultMap:(NSString *)appDisplayName dbURL:(NSString *)dbURL transactionId:(NSNumber *)transactionId error:(NSError *)error committed:(BOOL)committed snapshot:(FIRDataSnapshot *)snapshot { NSMutableDictionary *resultMap = [[NSMutableDictionary alloc] init]; [resultMap setValue:transactionId forKey:@"id"]; [resultMap setValue:appDisplayName forKey:@"appName"]; + [resultMap setValue:dbURL forKey:@"dbURL"]; // TODO: no timeout on iOS [resultMap setValue:@(committed) forKey:@"committed"]; // TODO: no interrupted on iOS diff --git a/ios/RNFirebase/database/RNFirebaseDatabaseReference.h b/ios/RNFirebase/database/RNFirebaseDatabaseReference.h index fb64295c..9bf1d18e 100644 --- a/ios/RNFirebase/database/RNFirebaseDatabaseReference.h +++ b/ios/RNFirebase/database/RNFirebaseDatabaseReference.h @@ -13,11 +13,12 @@ @property RCTEventEmitter *emitter; @property FIRDatabaseQuery *query; @property NSString *appDisplayName; +@property NSString *dbURL; @property NSString *key; @property NSString *path; @property NSMutableDictionary *listeners; -- (id)initWithPathAndModifiers:(RCTEventEmitter *)emitter appDisplayName:(NSString *)appDisplayName key:(NSString *)key refPath:(NSString *)refPath modifiers:(NSArray *)modifiers; +- (id)initWithPathAndModifiers:(RCTEventEmitter *)emitter appDisplayName:(NSString *)appDisplayName dbURL:(NSString *)dbURL key:(NSString *)key refPath:(NSString *)refPath modifiers:(NSArray *)modifiers; - (void)on:(NSString *) eventName registration:(NSDictionary *) registration; - (void)once:(NSString *) eventType resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; - (void)removeEventListener:(NSString *)eventRegistrationKey; diff --git a/ios/RNFirebase/database/RNFirebaseDatabaseReference.m b/ios/RNFirebase/database/RNFirebaseDatabaseReference.m index 5a4a8a4b..782ec096 100644 --- a/ios/RNFirebase/database/RNFirebaseDatabaseReference.m +++ b/ios/RNFirebase/database/RNFirebaseDatabaseReference.m @@ -6,6 +6,7 @@ - (id)initWithPathAndModifiers:(RCTEventEmitter *)emitter appDisplayName:(NSString *)appDisplayName + dbURL:(NSString *)dbURL key:(NSString *)key refPath:(NSString *)refPath modifiers:(NSArray *)modifiers { @@ -13,6 +14,7 @@ if (self) { _emitter = emitter; _appDisplayName = appDisplayName; + _dbURL = dbURL; _key = key; _path = refPath; _listeners = [[NSMutableDictionary alloc] init]; @@ -123,7 +125,7 @@ - (FIRDatabaseQuery *)buildQueryAtPathWithModifiers:(NSString *)path modifiers:(NSArray *)modifiers { - FIRDatabase *firebaseDatabase = [RNFirebaseDatabase getDatabaseForApp:_appDisplayName]; + FIRDatabase *firebaseDatabase = [RNFirebaseDatabase getDatabaseForApp:_appDisplayName URL:_dbURL]; FIRDatabaseQuery *query = [[firebaseDatabase reference] child:path]; for (NSDictionary *modifier in modifiers) { diff --git a/lib/modules/admob/index.js b/lib/modules/admob/index.js index 795099a3..063e42ea 100644 --- a/lib/modules/admob/index.js +++ b/lib/modules/admob/index.js @@ -41,6 +41,7 @@ export default class AdMob extends ModuleBase { events: NATIVE_EVENTS, moduleName: MODULE_NAME, multiApp: false, + hasShards: false, namespace: NAMESPACE, }); diff --git a/lib/modules/analytics/index.js b/lib/modules/analytics/index.js index 3cba2d47..7a0cc4bb 100644 --- a/lib/modules/analytics/index.js +++ b/lib/modules/analytics/index.js @@ -34,6 +34,7 @@ export default class Analytics extends ModuleBase { super(app, { moduleName: MODULE_NAME, multiApp: false, + hasShards: false, namespace: NAMESPACE, }); } diff --git a/lib/modules/auth/index.js b/lib/modules/auth/index.js index 4b2863e8..b4b697af 100644 --- a/lib/modules/auth/index.js +++ b/lib/modules/auth/index.js @@ -53,6 +53,7 @@ export default class Auth extends ModuleBase { events: NATIVE_EVENTS, moduleName: MODULE_NAME, multiApp: true, + hasShards: false, namespace: NAMESPACE, }); this._user = null; diff --git a/lib/modules/config/index.js b/lib/modules/config/index.js index 777ddecc..05511262 100644 --- a/lib/modules/config/index.js +++ b/lib/modules/config/index.js @@ -32,6 +32,7 @@ export default class RemoteConfig extends ModuleBase { super(app, { moduleName: MODULE_NAME, multiApp: false, + hasShards: false, namespace: NAMESPACE, }); this._developerModeEnabled = false; diff --git a/lib/modules/crash/index.js b/lib/modules/crash/index.js index e18ec36b..2d9a0a8f 100644 --- a/lib/modules/crash/index.js +++ b/lib/modules/crash/index.js @@ -16,6 +16,7 @@ export default class Crash extends ModuleBase { super(app, { moduleName: MODULE_NAME, multiApp: false, + hasShards: false, namespace: NAMESPACE, }); } diff --git a/lib/modules/database/Reference.js b/lib/modules/database/Reference.js index 67d2c9c5..ce54e645 100644 --- a/lib/modules/database/Reference.js +++ b/lib/modules/database/Reference.js @@ -497,7 +497,7 @@ export default class Reference extends ReferenceBase { * @returns {string} */ toString(): string { - return `${this._database.app.options.databaseURL}/${this.path}`; + return `${this._database.databaseUrl}/${this.path}`; } /** @@ -606,7 +606,7 @@ export default class Reference extends ReferenceBase { * @return {string} */ _getRegistrationKey(eventType: string): string { - return `$${this._database.app.name}$/${ + return `$${this._database.databaseUrl}$/${ this.path }$${this._query.queryIdentifier()}$${listeners}$${eventType}`; } @@ -618,8 +618,8 @@ export default class Reference extends ReferenceBase { * @return {string} * @private */ - _getRefKey(): string { - return `$${this._database.app.name}$/${ + _getRefKey() { + return `$${this._database.databaseUrl}$/${ this.path }$${this._query.queryIdentifier()}`; } @@ -758,6 +758,7 @@ export default class Reference extends ReferenceBase { path: this.path, key: this._getRefKey(), appName: this._database.app.name, + dbURL: this._database.databaseUrl, eventRegistrationKey, }; @@ -776,6 +777,7 @@ export default class Reference extends ReferenceBase { path: this.path, key: this._getRefKey(), appName: this._database.app.name, + dbURL: this._database.databaseUrl, eventType: `${eventType}$cancelled`, eventRegistrationKey: registrationCancellationKey, listener: _context diff --git a/lib/modules/database/index.js b/lib/modules/database/index.js index 4967e0e8..66dc05fc 100644 --- a/lib/modules/database/index.js +++ b/lib/modules/database/index.js @@ -10,6 +10,7 @@ import ModuleBase from '../../utils/ModuleBase'; import { getNativeModule } from '../../utils/native'; import type App from '../core/app'; +import firebase from '../core/firebase'; const NATIVE_EVENTS = [ 'database_transaction_event', @@ -26,14 +27,32 @@ export default class Database extends ModuleBase { _offsetRef: Reference; _serverTimeOffset: number; _transactionHandler: TransactionHandler; + _serviceUrl: string; - constructor(app: App, options: Object = {}) { - super(app, { - events: NATIVE_EVENTS, - moduleName: MODULE_NAME, - multiApp: true, - namespace: NAMESPACE, - }); + constructor(appOrUrl: App | string, options: Object = {}) { + let app; + let serviceUrl; + if (typeof appOrUrl === 'string') { + app = firebase.app(); + serviceUrl = appOrUrl; + } else { + app = appOrUrl; + serviceUrl = app.options.databaseURL; + } + + super( + app, + { + events: NATIVE_EVENTS, + moduleName: MODULE_NAME, + multiApp: true, + hasShards: true, + namespace: NAMESPACE, + }, + serviceUrl + ); + + this._serviceUrl = serviceUrl; this._transactionHandler = new TransactionHandler(this); if (options.persistence) { @@ -83,6 +102,14 @@ export default class Database extends ModuleBase { ref(path: string): Reference { return new Reference(this, path); } + + /** + * Returns the database url + * @returns {string} + */ + get databaseUrl(): string { + return this._serviceUrl; + } } export const statics = { diff --git a/lib/modules/fabric/crashlytics/index.js b/lib/modules/fabric/crashlytics/index.js index 6b8a4484..7e797774 100644 --- a/lib/modules/fabric/crashlytics/index.js +++ b/lib/modules/fabric/crashlytics/index.js @@ -15,6 +15,7 @@ export default class Crashlytics extends ModuleBase { super(app, { moduleName: MODULE_NAME, multiApp: false, + hasShards: false, namespace: NAMESPACE, }); } diff --git a/lib/modules/firestore/index.js b/lib/modules/firestore/index.js index 35ee7047..e8047894 100644 --- a/lib/modules/firestore/index.js +++ b/lib/modules/firestore/index.js @@ -58,6 +58,7 @@ export default class Firestore extends ModuleBase { events: NATIVE_EVENTS, moduleName: MODULE_NAME, multiApp: true, + hasShards: false, namespace: NAMESPACE, }); diff --git a/lib/modules/links/index.js b/lib/modules/links/index.js index 28c31879..95908922 100644 --- a/lib/modules/links/index.js +++ b/lib/modules/links/index.js @@ -80,6 +80,7 @@ export default class Links extends ModuleBase { events: NATIVE_EVENTS, moduleName: MODULE_NAME, multiApp: false, + hasShards: false, namespace: NAMESPACE, }); } diff --git a/lib/modules/messaging/index.js b/lib/modules/messaging/index.js index 9eaf649f..4d7fc40d 100644 --- a/lib/modules/messaging/index.js +++ b/lib/modules/messaging/index.js @@ -42,6 +42,7 @@ export default class Messaging extends ModuleBase { events: NATIVE_EVENTS, moduleName: MODULE_NAME, multiApp: false, + hasShards: false, namespace: NAMESPACE, }); diff --git a/lib/modules/perf/index.js b/lib/modules/perf/index.js index e394d370..c22ed35e 100644 --- a/lib/modules/perf/index.js +++ b/lib/modules/perf/index.js @@ -16,6 +16,7 @@ export default class PerformanceMonitoring extends ModuleBase { super(app, { moduleName: MODULE_NAME, multiApp: false, + hasShards: false, namespace: NAMESPACE, }); } diff --git a/lib/modules/storage/index.js b/lib/modules/storage/index.js index 3803feb9..457de361 100644 --- a/lib/modules/storage/index.js +++ b/lib/modules/storage/index.js @@ -30,6 +30,7 @@ export default class Storage extends ModuleBase { events: NATIVE_EVENTS, moduleName: MODULE_NAME, multiApp: true, + hasShards: false, namespace: NAMESPACE, }); diff --git a/lib/modules/utils/index.js b/lib/modules/utils/index.js index 7215f2b1..ce48a2ad 100644 --- a/lib/modules/utils/index.js +++ b/lib/modules/utils/index.js @@ -23,6 +23,7 @@ export default class RNFirebaseUtils extends ModuleBase { super(app, { moduleName: MODULE_NAME, multiApp: false, + hasShards: false, namespace: NAMESPACE, }); } diff --git a/lib/types/index.js b/lib/types/index.js index f48b3a5c..87d95701 100644 --- a/lib/types/index.js +++ b/lib/types/index.js @@ -48,6 +48,7 @@ export type FirebaseModuleConfig = { events?: string[], moduleName: FirebaseModuleName, multiApp: boolean, + hasShards: boolean, namespace: FirebaseNamespace, }; diff --git a/lib/utils/ModuleBase.js b/lib/utils/ModuleBase.js index 83dd5f48..ffa78088 100644 --- a/lib/utils/ModuleBase.js +++ b/lib/utils/ModuleBase.js @@ -9,6 +9,7 @@ import type { FirebaseModuleConfig, FirebaseNamespace } from '../types'; export default class ModuleBase { _app: App; + _serviceUrl: ?string; namespace: FirebaseNamespace; /** @@ -16,7 +17,7 @@ export default class ModuleBase { * @param app * @param config */ - constructor(app: App, config: FirebaseModuleConfig) { + constructor(app: App, config: FirebaseModuleConfig, serviceUrl: ?string) { if (!config.moduleName) { throw new Error('Missing module name'); } @@ -25,10 +26,11 @@ export default class ModuleBase { } const { moduleName } = config; this._app = app; + this._serviceUrl = serviceUrl; this.namespace = config.namespace; // check if native module exists as all native - initialiseNativeModule(this, config); + initialiseNativeModule(this, config, serviceUrl); initialiseLogger( this, `${app.name}:${moduleName.replace('RNFirebase', '')}` diff --git a/lib/utils/apps.js b/lib/utils/apps.js index 75f7778a..57ac16ea 100644 --- a/lib/utils/apps.js +++ b/lib/utils/apps.js @@ -49,9 +49,16 @@ export default { namespace: FirebaseNamespace, InstanceClass: Class ): () => FirebaseModule { - return (): M => { - if (!APP_MODULES[app._name]) { - APP_MODULES[app._name] = {}; + return (serviceUrl: ?string = null): M => { + if (serviceUrl && namespace !== 'database') { + throw new Error( + INTERNALS.STRINGS.ERROR_INIT_SERVICE_URL_UNSUPPORTED(namespace) + ); + } + + const appOrShardName = serviceUrl || app.name; + if (!APP_MODULES[appOrShardName]) { + APP_MODULES[appOrShardName] = {}; } if ( @@ -63,11 +70,14 @@ export default { app.utils().checkPlayServicesAvailability(); } - if (!APP_MODULES[app._name][namespace]) { - APP_MODULES[app._name][namespace] = new InstanceClass(app, app.options); + if (!APP_MODULES[appOrShardName][namespace]) { + APP_MODULES[appOrShardName][namespace] = new InstanceClass( + serviceUrl || app, + app.options + ); } - return APP_MODULES[app._name][namespace]; + return APP_MODULES[appOrShardName][namespace]; }; }, @@ -163,8 +173,13 @@ export default { statics: S, moduleName: FirebaseModuleName ): FirebaseModuleAndStatics { - const getModule = (app?: App): FirebaseModule => { - let _app = app; + const getModule = (appOrUrl?: App | string): FirebaseModule => { + let _app = appOrUrl; + let _serviceUrl: ?string = null; + if (typeof appOrUrl === 'string' && namespace === 'database') { + _app = null; + _serviceUrl = appOrUrl; + } // throw an error if it's not a valid app instance if (_app && !(_app instanceof App)) @@ -178,7 +193,7 @@ export default { } // $FlowExpectedError: Flow doesn't support indexable signatures on classes: https://github.com/facebook/flow/issues/1323 const module = _app[namespace]; - return module(); + return module(_serviceUrl); }; return Object.assign(getModule, statics, { diff --git a/lib/utils/internals.js b/lib/utils/internals.js index de46d40a..fc656c04 100644 --- a/lib/utils/internals.js +++ b/lib/utils/internals.js @@ -98,6 +98,13 @@ export default { ERROR_INIT_STRING_NAME: 'Firebase.initializeApp(options, name <-- requires a valid string value.', + /** + * @return {string} + */ + ERROR_INIT_SERVICE_URL_UNSUPPORTED(namespace: string) { + return `${namespace} does not support URL as a param, please pass in an app.`; + }, + /** * @return {string} */ diff --git a/lib/utils/native.js b/lib/utils/native.js index 248130c7..1b8ddd66 100644 --- a/lib/utils/native.js +++ b/lib/utils/native.js @@ -11,37 +11,40 @@ import type { FirebaseModuleConfig } from '../types'; const NATIVE_MODULES: { [string]: Object } = {}; /** - * Prepends appName arg to all native method calls - * @param appName + * Prepends all arguments in prependArgs to all native method calls * @param NativeModule + * @param argToPrepend */ -const nativeWithApp = (appName: string, NativeModule: Object): Object => { +const nativeWithArgs = ( + NativeModule: Object, + argToPrepend: Array +): Object => { const native = {}; const methods = Object.keys(NativeModule); for (let i = 0, len = methods.length; i < len; i++) { const method = methods[i]; - native[method] = (...args) => NativeModule[method](...[appName, ...args]); + native[method] = (...args) => + NativeModule[method](...[...argToPrepend, ...args]); } return native; }; -const getModuleKey = (module: ModuleBase): string => - `${module.app.name}:${module.namespace}`; +const nativeModuleKey = (module: ModuleBase): string => + `${module._serviceUrl || module.app.name}:${module.namespace}`; -export const getNativeModule = (module: ModuleBase): Object => { - const key = getModuleKey(module); - return NATIVE_MODULES[key]; -}; +export const getNativeModule = (module: ModuleBase): Object => + NATIVE_MODULES[nativeModuleKey(module)]; export const initialiseNativeModule = ( module: ModuleBase, - config: FirebaseModuleConfig + config: FirebaseModuleConfig, + serviceUrl: ?string ): Object => { - const { moduleName, multiApp, namespace } = config; + const { moduleName, multiApp, hasShards, namespace } = config; const nativeModule = NativeModules[moduleName]; - const key = getModuleKey(module); + const key = nativeModuleKey(module); if (!nativeModule && namespace !== 'utils') { throw new Error( @@ -51,8 +54,16 @@ export const initialiseNativeModule = ( // used by the modules that extend ModuleBase // to access their native module counterpart + const argToPrepend = []; if (multiApp) { - NATIVE_MODULES[key] = nativeWithApp(module.app.name, nativeModule); + argToPrepend.push(module.app.name); + } + if (hasShards) { + argToPrepend.push(serviceUrl); + } + + if (argToPrepend.length) { + NATIVE_MODULES[key] = nativeWithArgs(nativeModule, argToPrepend); } else { NATIVE_MODULES[key] = nativeModule; } From a9470e78c5b1a25bb1c51bcd5d26ef029c36a4ff Mon Sep 17 00:00:00 2001 From: Akshet Pandey Date: Wed, 21 Mar 2018 13:08:46 -0700 Subject: [PATCH 61/77] CR Changes --- .../firebase/database/RNFirebaseDatabase.java | 26 ++++++++++++++----- .../database/RNFirebaseDatabaseReference.java | 9 +------ 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java index e628fc05..c9e5a41a 100644 --- a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java +++ b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java @@ -54,7 +54,7 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { */ @ReactMethod public void goOnline(String appName, String dbURL) { - getDatabaseForApp(appName, dbURL).goOnline(); + getDatabaseForAppAndSetLogging(appName, dbURL).goOnline(); } /** @@ -62,7 +62,7 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { */ @ReactMethod public void goOffline(String appName, String dbURL) { - getDatabaseForApp(appName, dbURL).goOffline(); + getDatabaseForAppAndSetLogging(appName, dbURL).goOffline(); } /** @@ -71,7 +71,7 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { */ @ReactMethod public void setPersistence(String appName, String dbURL, Boolean state) { - getDatabaseForApp(appName, dbURL).setPersistenceEnabled(state); + getDatabaseForAppAndSetLogging(appName, dbURL).setPersistenceEnabled(state); } /** @@ -80,7 +80,7 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { */ @ReactMethod public void setPersistenceCacheSizeBytes(String appName, String dbURL, int size) { - getDatabaseForApp(appName, dbURL).setPersistenceCacheSizeBytes((long) size); + getDatabaseForAppAndSetLogging(appName, dbURL).setPersistenceCacheSizeBytes((long) size); } @@ -494,9 +494,10 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * Get a database instance for a specific firebase app instance * * @param appName + * @param dbURL * @return */ - private FirebaseDatabase getDatabaseForApp(String appName, String dbURL) { + public static FirebaseDatabase getDatabaseForApp(String appName, String dbURL) { FirebaseDatabase firebaseDatabase; if(dbURL != null && dbURL.length() > 0) { firebaseDatabase = FirebaseDatabase.getInstance(dbURL); @@ -504,6 +505,19 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); firebaseDatabase = FirebaseDatabase.getInstance(firebaseApp); } + + return firebaseDatabase; + } + + /** + * Get a database instance for a specific firebase app instance and enable/disable logging + * + * @param appName + * @param dbURL + * @return + */ + private FirebaseDatabase getDatabaseForAppAndSetLogging(String appName, String dbURL) { + FirebaseDatabase firebaseDatabase = RNFirebaseDatabase.getDatabaseForApp(appName, dbURL); Boolean logLevel = loggingLevelSet.get(firebaseDatabase.getApp().getName()); if (enableLogging && (logLevel == null || !logLevel)) { @@ -541,7 +555,7 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @return */ private DatabaseReference getReferenceForAppPath(String appName, String dbURL, String path) { - return getDatabaseForApp(appName, dbURL).getReference(path); + return getDatabaseForAppAndSetLogging(appName, dbURL).getReference(path); } /** diff --git a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java index 1e8f68cb..bbf51038 100644 --- a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java +++ b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java @@ -348,14 +348,7 @@ class RNFirebaseDatabaseReference { * @return */ private void buildDatabaseQueryAtPathAndModifiers(String path, ReadableArray modifiers) { - FirebaseDatabase firebaseDatabase; - if(dbURL != null && dbURL.length() > 0) { - firebaseDatabase = FirebaseDatabase.getInstance(dbURL); - } else { - FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); - firebaseDatabase= FirebaseDatabase.getInstance(firebaseApp); - } - + FirebaseDatabase firebaseDatabase = RNFirebaseDatabase.getDatabaseForApp(appName, dbURL); query = firebaseDatabase.getReference(path); List modifiersList = Utils.recursivelyDeconstructReadableArray(modifiers); From e3ea0d3fc7660a5cea7f8d3e048476468573227b Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 22 Mar 2018 08:46:00 +0000 Subject: [PATCH 62/77] [analytics] Add additional validation --- lib/modules/analytics/index.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/modules/analytics/index.js b/lib/modules/analytics/index.js index 3cba2d47..39ad9bc9 100644 --- a/lib/modules/analytics/index.js +++ b/lib/modules/analytics/index.js @@ -122,7 +122,12 @@ export default class Analytics extends ModuleBase { * Sets the user ID property. * @param id */ - setUserId(id: string): void { + setUserId(id: string | null): void { + if (id !== null && !isString(id)) { + throw new Error( + 'analytics.setUserId(): The supplied userId must be a string value or null.' + ); + } getNativeModule(this).setUserId(id); } @@ -131,17 +136,28 @@ export default class Analytics extends ModuleBase { * @param name * @param value */ - setUserProperty(name: string, value: string): void { + setUserProperty(name: string, value: string | null): void { + if (value !== null && !isString(value)) { + throw new Error( + 'analytics.setUserProperty(): The supplied property must be a string value or null.' + ); + } getNativeModule(this).setUserProperty(name, value); } /** - * Sets a user property to a given value. + * Sets multiple user properties to the supplied values. * @RNFirebaseSpecific * @param object */ setUserProperties(object: Object): void { Object.keys(object).forEach(property => { + const value = object[property]; + if (value !== null && !isString(value)) { + throw new Error( + `analytics.setUserProperties(): The property with name '${property}' must be a string value or null.` + ); + } getNativeModule(this).setUserProperty(property, object[property]); }); } From 26c3ed9a60b4feb366afbb7e3ade3768e8eb9d37 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 22 Mar 2018 08:48:00 +0000 Subject: [PATCH 63/77] [notifications] Ensure android specific notification properties are populated --- .../firebase/notifications/RNFirebaseNotifications.java | 1 + 1 file changed, 1 insertion(+) diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java index 65c487d5..e242c5e3 100644 --- a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java @@ -284,6 +284,7 @@ public class RNFirebaseNotifications extends ReactContextBaseJavaModule implemen if (notification.getTag() != null) { androidMap.putString("group", notification.getTag()); } + notificationMap.putMap("android", androidMap); return notificationMap; } From 53babb4cd9060c7437f9f515fe88f755aecf6e6c Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 22 Mar 2018 12:46:37 +0000 Subject: [PATCH 64/77] [invites] Initial JS and Android invites functionality --- .../firebase/invites/RNFirebaseInvites.java | 236 ++++++++++++++++++ .../invites/RNFirebaseInvitesPackage.java | 38 +++ .../firebase/links/RNFirebaseLinks.java | 194 +++++++------- ios/RNFirebase/RNFirebaseEvents.h | 2 +- lib/modules/core/app.js | 3 + lib/modules/core/firebase.js | 11 + lib/modules/invites/AndroidInvitation.js | 69 +++++ lib/modules/invites/Invitation.js | 106 ++++++++ lib/modules/invites/index.js | 77 ++++++ lib/modules/invites/types.js | 21 ++ lib/modules/links/index.js | 57 +++-- lib/modules/messaging/RemoteMessage.js | 8 +- lib/types/index.js | 15 +- .../android/app/src/main/AndroidManifest.xml | 9 + .../MainApplication.java | 2 + tests/src/firebase.js | 34 ++- 16 files changed, 747 insertions(+), 135 deletions(-) create mode 100644 android/src/main/java/io/invertase/firebase/invites/RNFirebaseInvites.java create mode 100644 android/src/main/java/io/invertase/firebase/invites/RNFirebaseInvitesPackage.java create mode 100644 lib/modules/invites/AndroidInvitation.js create mode 100644 lib/modules/invites/Invitation.js create mode 100644 lib/modules/invites/index.js create mode 100644 lib/modules/invites/types.js diff --git a/android/src/main/java/io/invertase/firebase/invites/RNFirebaseInvites.java b/android/src/main/java/io/invertase/firebase/invites/RNFirebaseInvites.java new file mode 100644 index 00000000..7a01983a --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/invites/RNFirebaseInvites.java @@ -0,0 +1,236 @@ +package io.invertase.firebase.invites; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.util.Log; + +import com.facebook.react.bridge.ActivityEventListener; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.facebook.react.bridge.WritableMap; +import com.google.android.gms.appinvite.AppInviteInvitation; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; +import com.google.firebase.appinvite.FirebaseAppInvite; +import com.google.firebase.dynamiclinks.FirebaseDynamicLinks; +import com.google.firebase.dynamiclinks.PendingDynamicLinkData; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import io.invertase.firebase.Utils; +import io.invertase.firebase.links.RNFirebaseLinks; + +public class RNFirebaseInvites extends ReactContextBaseJavaModule implements ActivityEventListener, LifecycleEventListener { + private static final String TAG = "RNFirebaseInvites"; + private static final int REQUEST_INVITE = 81283; + private boolean mInitialInvitationInitialized = false; + private String mInitialDeepLink = null; + private String mInitialInvitationId = null; + private Promise mPromise = null; + + public RNFirebaseInvites(ReactApplicationContext context) { + super(context); + getReactApplicationContext().addActivityEventListener(this); + } + + @Override + public String getName() { + return "RNFirebaseInvites"; + } + + @ReactMethod + public void getInitialInvitation(final Promise promise) { + if (mInitialInvitationInitialized) { + if (mInitialDeepLink != null || mInitialInvitationId != null) { + promise.resolve(buildInvitationMap(mInitialDeepLink, mInitialInvitationId)); + } else { + promise.resolve(null); + } + } else { + if (getCurrentActivity() != null) { + FirebaseDynamicLinks.getInstance() + .getDynamicLink(getCurrentActivity().getIntent()) + .addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(PendingDynamicLinkData pendingDynamicLinkData) { + if (pendingDynamicLinkData != null) { + FirebaseAppInvite invite = FirebaseAppInvite.getInvitation(pendingDynamicLinkData); + if (invite == null) { + promise.resolve(null); + return; + } + + mInitialDeepLink = pendingDynamicLinkData.getLink().toString(); + mInitialInvitationId = invite.getInvitationId(); + promise.resolve(buildInvitationMap(mInitialDeepLink, mInitialInvitationId)); + } else { + promise.resolve(null); + } + mInitialInvitationInitialized = true; + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + Log.e(TAG, "getInitialInvitation: failed to resolve invitation", e); + promise.reject("invites/initial-invitation-error", e.getMessage(), e); + } + }); + } else { + Log.d(TAG, "getInitialInvitation: activity is null"); + promise.resolve(null); + } + } + } + + @ReactMethod + public void sendInvitation(ReadableMap invitationMap, Promise promise) { + if (!invitationMap.hasKey("message")) { + promise.reject("invites/invalid-invitation", "The supplied invitation is missing a 'message' field"); + return; + } + if (!invitationMap.hasKey("title")) { + promise.reject("invites/invalid-invitation", "The supplied invitation is missing a 'title' field"); + return; + } + + AppInviteInvitation.IntentBuilder ib = new AppInviteInvitation.IntentBuilder(invitationMap.getString("title")); + if (invitationMap.hasKey("androidMinimumVersionCode")) { + Double androidMinimumVersionCode = invitationMap.getDouble("androidMinimumVersionCode"); + ib = ib.setAndroidMinimumVersionCode(androidMinimumVersionCode.intValue()); + } + if (invitationMap.hasKey("callToActionText")) { + ib = ib.setCallToActionText(invitationMap.getString("callToActionText")); + } + if (invitationMap.hasKey("customImage")) { + ib = ib.setCustomImage(Uri.parse(invitationMap.getString("customImage"))); + } + if (invitationMap.hasKey("deepLink")) { + ib = ib.setDeepLink(Uri.parse(invitationMap.getString("deepLink"))); + } + if (invitationMap.hasKey("iosClientId")) { + ib = ib.setOtherPlatformsTargetApplication( + AppInviteInvitation.IntentBuilder.PlatformMode.PROJECT_PLATFORM_IOS, + invitationMap.getString("iosClientId")); + } + ib = ib.setMessage(invitationMap.getString("message")); + + // Android specific properties + if (invitationMap.hasKey("android")) { + ReadableMap androidMap = invitationMap.getMap("android"); + + if (androidMap.hasKey("additionalReferralParameters")) { + Map arpMap = new HashMap<>(); + ReadableMap arpReadableMap = androidMap.getMap("additionalReferralParameters"); + ReadableMapKeySetIterator iterator = arpReadableMap.keySetIterator(); + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + arpMap.put(key, arpReadableMap.getString(key)); + } + ib = ib.setAdditionalReferralParameters(arpMap); + } + if (androidMap.hasKey("emailHtmlContent")) { + ib = ib.setEmailHtmlContent(invitationMap.getString("emailHtmlContent")); + } + if (androidMap.hasKey("emailSubject")) { + ib = ib.setEmailSubject(invitationMap.getString("emailSubject")); + } + if (androidMap.hasKey("googleAnalyticsTrackingId")) { + ib = ib.setGoogleAnalyticsTrackingId(invitationMap.getString("googleAnalyticsTrackingId")); + } + } + + Intent invitationIntent = ib.build(); + // Save the promise for later + this.mPromise = promise; + + // Start the intent + this.getCurrentActivity().startActivityForResult(invitationIntent, REQUEST_INVITE); + } + + ////////////////////////////////////////////////////////////////////// + // Start ActivityEventListener methods + ////////////////////////////////////////////////////////////////////// + @Override + public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_INVITE) { + if (resultCode == Activity.RESULT_OK) { + String[] ids = AppInviteInvitation.getInvitationIds(resultCode, data); + mPromise.resolve(Arguments.fromList(Arrays.asList(ids))); + } else if (resultCode == Activity.RESULT_CANCELED) { + mPromise.reject("invites/invitation-cancelled", " The invitation was cancelled"); + } else { + mPromise.reject("invites/invitation-error", " The invitation failed to send"); + } + // Clear the promise + mPromise = null; + } + } + + @Override + public void onNewIntent(Intent intent) { + FirebaseDynamicLinks.getInstance() + .getDynamicLink(intent) + .addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(PendingDynamicLinkData pendingDynamicLinkData) { + if (pendingDynamicLinkData != null) { + FirebaseAppInvite invite = FirebaseAppInvite.getInvitation(pendingDynamicLinkData); + if (invite == null) { + // this is a dynamic link, not an invitation + return; + } + + String deepLink = pendingDynamicLinkData.getLink().toString(); + String invitationId = invite.getInvitationId(); + WritableMap invitationMap = buildInvitationMap(deepLink, invitationId); + Utils.sendEvent(getReactApplicationContext(), "invites_invitation_received", invitationMap); + } + } + }); + } + ////////////////////////////////////////////////////////////////////// + // End ActivityEventListener methods + ////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////// + // Start LifecycleEventListener methods + ////////////////////////////////////////////////////////////////////// + @Override + public void onHostResume() { + // Not required for this module + } + + @Override + public void onHostPause() { + // Not required for this module + } + + @Override + public void onHostDestroy() { + mInitialDeepLink = null; + mInitialInvitationId = null; + mInitialInvitationInitialized = false; + } + ////////////////////////////////////////////////////////////////////// + // End LifecycleEventListener methods + ////////////////////////////////////////////////////////////////////// + + private WritableMap buildInvitationMap(String deepLink, String invitationId) { + WritableMap invitationMap = Arguments.createMap(); + invitationMap.putString("deepLink", deepLink); + invitationMap.putString("invitationId", invitationId); + + return invitationMap; + } +} diff --git a/android/src/main/java/io/invertase/firebase/invites/RNFirebaseInvitesPackage.java b/android/src/main/java/io/invertase/firebase/invites/RNFirebaseInvitesPackage.java new file mode 100644 index 00000000..23bd64c7 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/invites/RNFirebaseInvitesPackage.java @@ -0,0 +1,38 @@ +package io.invertase.firebase.invites; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@SuppressWarnings("unused") +public class RNFirebaseInvitesPackage implements ReactPackage { + public RNFirebaseInvitesPackage() { + } + + /** + * @param reactContext react application context that can be used to create modules + * @return list of native modules to register with the newly created catalyst instance + */ + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + modules.add(new RNFirebaseInvites(reactContext)); + + return modules; + } + + /** + * @param reactContext + * @return a list of view managers that should be registered with {@link UIManagerModule} + */ + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} diff --git a/android/src/main/java/io/invertase/firebase/links/RNFirebaseLinks.java b/android/src/main/java/io/invertase/firebase/links/RNFirebaseLinks.java index 8ea56e0c..7734513f 100644 --- a/android/src/main/java/io/invertase/firebase/links/RNFirebaseLinks.java +++ b/android/src/main/java/io/invertase/firebase/links/RNFirebaseLinks.java @@ -16,6 +16,8 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.google.firebase.appinvite.FirebaseAppInvite; import com.google.firebase.dynamiclinks.DynamicLink; import com.google.firebase.dynamiclinks.FirebaseDynamicLinks; import com.google.firebase.dynamiclinks.ShortDynamicLink; @@ -33,14 +35,6 @@ public class RNFirebaseLinks extends ReactContextBaseJavaModule implements Activ private String mInitialLink = null; private boolean mInitialLinkInitialized = false; - private interface ResolveHandler { - void onResolved(String url); - } - - private interface ErrorHandler { - void onError(Exception e); - } - public RNFirebaseLinks(ReactApplicationContext reactContext) { super(reactContext); getReactApplicationContext().addActivityEventListener(this); @@ -52,101 +46,6 @@ public class RNFirebaseLinks extends ReactContextBaseJavaModule implements Activ return "RNFirebaseLinks"; } - - private void resolveLink(Intent intent, final ResolveHandler resolveHandler, final ErrorHandler errorHandler) { - FirebaseDynamicLinks.getInstance() - .getDynamicLink(intent) - .addOnSuccessListener(new OnSuccessListener() { - @Override - public void onSuccess(PendingDynamicLinkData pendingDynamicLinkData) { - if (pendingDynamicLinkData != null) { - Uri deepLinkUri = pendingDynamicLinkData.getLink(); - String url = deepLinkUri.toString(); - resolveHandler.onResolved(url); - } else { - resolveHandler.onResolved(null); - } - } - }) - .addOnFailureListener(new OnFailureListener() { - @Override - public void onFailure(@NonNull Exception e) { - errorHandler.onError(e); - } - }); - } - - @ReactMethod - public void getInitialLink(final Promise promise) { - if (mInitialLinkInitialized) { - promise.resolve(mInitialLink); - } else { - Activity activity = getCurrentActivity(); - if (activity != null) { - resolveLink(activity.getIntent(), new ResolveHandler() { - @Override - public void onResolved(String url) { - if (url != null) { - mInitialLink = url; - Log.d(TAG, "getInitialLink received a new dynamic link from pendingDynamicLinkData"); - } - Log.d(TAG, "initial link is: " + mInitialLink); - promise.resolve(mInitialLink); - mInitialLinkInitialized = true; - } - }, new ErrorHandler() { - @Override - public void onError(Exception e) { - Log.e(TAG, "getInitialLink: failed to resolve link", e); - promise.reject("links/getDynamicLink", e.getMessage(), e); - } - }); - } else { - Log.d(TAG, "getInitialLink: activity is null"); - promise.resolve(null); - } - } - } - - @Override - public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { - // Not required for this module - } - - @Override - public void onNewIntent(Intent intent) { - resolveLink(intent, new ResolveHandler() { - @Override - public void onResolved(String url) { - if (url != null) { - Log.d(TAG, "handleLink: sending link: " + url); - Utils.sendEvent(getReactApplicationContext(), "dynamic_link_received", url); - } - } - }, new ErrorHandler() { - @Override - public void onError(Exception e) { - Log.e(TAG, "handleLink: failed to resolve link", e); - } - }); - } - - @Override - public void onHostResume() { - // Not required for this module - } - - @Override - public void onHostPause() { - // Not required for this module - } - - @Override - public void onHostDestroy() { - mInitialLink = null; - mInitialLinkInitialized = false; - } - @ReactMethod public void createDynamicLink(final ReadableMap parameters, final Promise promise) { try { @@ -190,6 +89,95 @@ public class RNFirebaseLinks extends ReactContextBaseJavaModule implements Activ } } + @ReactMethod + public void getInitialLink(final Promise promise) { + if (mInitialLinkInitialized) { + promise.resolve(mInitialLink); + } else { + if (getCurrentActivity() != null) { + FirebaseDynamicLinks.getInstance() + .getDynamicLink(getCurrentActivity().getIntent()) + .addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(PendingDynamicLinkData pendingDynamicLinkData) { + if (pendingDynamicLinkData != null + && !isInvitation(pendingDynamicLinkData)) { + + mInitialLink = pendingDynamicLinkData.getLink().toString(); + } + Log.d(TAG, "getInitialLink: link is: " + mInitialLink); + mInitialLinkInitialized = true; + promise.resolve(mInitialLink); + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + Log.e(TAG, "getInitialLink: failed to resolve link", e); + promise.reject("link/initial-link-error", e.getMessage(), e); + } + }); + } else { + Log.d(TAG, "getInitialLink: activity is null"); + promise.resolve(null); + } + } + } + + ////////////////////////////////////////////////////////////////////// + // Start ActivityEventListener methods + ////////////////////////////////////////////////////////////////////// + @Override + public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { + // Not required for this module + } + + @Override + public void onNewIntent(Intent intent) { + FirebaseDynamicLinks.getInstance() + .getDynamicLink(intent) + .addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(PendingDynamicLinkData pendingDynamicLinkData) { + if (pendingDynamicLinkData != null + && !isInvitation(pendingDynamicLinkData)) { + String link = pendingDynamicLinkData.getLink().toString(); + Utils.sendEvent(getReactApplicationContext(), "links_link_received", link); + } + } + }); + } + ////////////////////////////////////////////////////////////////////// + // End ActivityEventListener methods + ////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////// + // Start LifecycleEventListener methods + ////////////////////////////////////////////////////////////////////// + @Override + public void onHostResume() { + // Not required for this module + } + + @Override + public void onHostPause() { + // Not required for this module + } + + @Override + public void onHostDestroy() { + mInitialLink = null; + mInitialLinkInitialized = false; + } + ////////////////////////////////////////////////////////////////////// + // End LifecycleEventListener methods + ////////////////////////////////////////////////////////////////////// + + // Looks at the internals of the link data to detect whether it's an invitation or not + private boolean isInvitation(PendingDynamicLinkData pendingDynamicLinkData) { + return pendingDynamicLinkData.zzcbj().getString("com.google.firebase.appinvite.fdl.extension.InvitationId") != null; + } + private DynamicLink.Builder getDynamicLinkBuilderFromMap(final Map metaData) { DynamicLink.Builder parametersBuilder = FirebaseDynamicLinks.getInstance().createDynamicLink(); try { diff --git a/ios/RNFirebase/RNFirebaseEvents.h b/ios/RNFirebase/RNFirebaseEvents.h index e0fa4eb2..82564b28 100644 --- a/ios/RNFirebase/RNFirebaseEvents.h +++ b/ios/RNFirebase/RNFirebaseEvents.h @@ -49,6 +49,6 @@ static NSString *const ADMOB_INTERSTITIAL_EVENT = @"interstitial_event"; static NSString *const ADMOB_REWARDED_VIDEO_EVENT = @"rewarded_video_event"; // Links -static NSString *const LINKS_DYNAMIC_LINK_RECEIVED = @"dynamic_link_received"; +static NSString *const LINKS_DYNAMIC_LINK_RECEIVED = @"links_link_received"; #endif diff --git a/lib/modules/core/app.js b/lib/modules/core/app.js index 60465f9b..352ec7d4 100644 --- a/lib/modules/core/app.js +++ b/lib/modules/core/app.js @@ -19,6 +19,7 @@ import Crashlytics, { import Database, { NAMESPACE as DatabaseNamespace } from '../database'; import Firestore, { NAMESPACE as FirestoreNamespace } from '../firestore'; import InstanceId, { NAMESPACE as InstanceIdNamespace } from '../instanceid'; +import Invites, { NAMESPACE as InvitesNamespace } from '../invites'; import Links, { NAMESPACE as LinksNamespace } from '../links'; import Messaging, { NAMESPACE as MessagingNamespace } from '../messaging'; import Notifications, { @@ -49,6 +50,7 @@ export default class App { }; firestore: () => Firestore; instanceid: () => InstanceId; + invites: () => Invites; links: () => Links; messaging: () => Messaging; notifications: () => Notifications; @@ -90,6 +92,7 @@ export default class App { }; this.firestore = APPS.appModule(this, FirestoreNamespace, Firestore); this.instanceid = APPS.appModule(this, InstanceIdNamespace, InstanceId); + this.invites = APPS.appModule(this, InvitesNamespace, Invites); this.links = APPS.appModule(this, LinksNamespace, Links); this.messaging = APPS.appModule(this, MessagingNamespace, Messaging); this.notifications = APPS.appModule( diff --git a/lib/modules/core/firebase.js b/lib/modules/core/firebase.js index 06f8a194..91f3ffcc 100644 --- a/lib/modules/core/firebase.js +++ b/lib/modules/core/firebase.js @@ -42,6 +42,10 @@ import { statics as InstanceIdStatics, MODULE_NAME as InstanceIdModuleName, } from '../instanceid'; +import { + statics as InvitesStatics, + MODULE_NAME as InvitesModuleName, +} from '../invites'; import { statics as LinksStatics, MODULE_NAME as LinksModuleName, @@ -78,6 +82,7 @@ import type { FirebaseOptions, FirestoreModule, InstanceIdModule, + InvitesModule, LinksModule, MessagingModule, NotificationsModule, @@ -98,6 +103,7 @@ class Firebase { fabric: FabricModule; firestore: FirestoreModule; instanceid: InstanceIdModule; + invites: InvitesModule; links: LinksModule; messaging: MessagingModule; notifications: NotificationsModule; @@ -147,6 +153,11 @@ class Firebase { InstanceIdStatics, InstanceIdModuleName ); + this.invites = APPS.moduleAndStatics( + 'invites', + InvitesStatics, + InvitesModuleName + ); this.links = APPS.moduleAndStatics('links', LinksStatics, LinksModuleName); this.messaging = APPS.moduleAndStatics( 'messaging', diff --git a/lib/modules/invites/AndroidInvitation.js b/lib/modules/invites/AndroidInvitation.js new file mode 100644 index 00000000..31a87d51 --- /dev/null +++ b/lib/modules/invites/AndroidInvitation.js @@ -0,0 +1,69 @@ +/** + * @flow + * AndroidInvitation representation wrapper + */ +import type Invitation from './Invitation'; +import type { NativeAndroidInvitation } from './types'; + +export default class AndroidInvitation { + _additionalReferralParameters: { [string]: string } | void; + _emailHtmlContent: string | void; + _emailSubject: string | void; + _googleAnalyticsTrackingId: string | void; + _invitation: Invitation; + + constructor(invitation: Invitation) { + this._invitation = invitation; + } + + /** + * + * @param additionalReferralParameters + * @returns {Invitation} + */ + setAdditionalReferralParameters(additionalReferralParameters: { + [string]: string, + }): Invitation { + this._additionalReferralParameters = additionalReferralParameters; + return this._invitation; + } + + /** + * + * @param emailHtmlContent + * @returns {Invitation} + */ + setEmailHtmlContent(emailHtmlContent: string): Invitation { + this._emailHtmlContent = emailHtmlContent; + return this._invitation; + } + + /** + * + * @param emailSubject + * @returns {Invitation} + */ + setEmailSubject(emailSubject: string): Invitation { + this._emailSubject = emailSubject; + return this._invitation; + } + + /** + * + * @param googleAnalyticsTrackingId + * @returns {Invitation} + */ + setGoogleAnalyticsTrackingId(googleAnalyticsTrackingId: string): Invitation { + this._googleAnalyticsTrackingId = googleAnalyticsTrackingId; + return this._invitation; + } + + build(): NativeAndroidInvitation { + return { + additionalReferralParameters: this._additionalReferralParameters, + emailHtmlContent: this._emailHtmlContent, + emailSubject: this._emailSubject, + googleAnalyticsTrackingId: this._googleAnalyticsTrackingId, + }; + } +} diff --git a/lib/modules/invites/Invitation.js b/lib/modules/invites/Invitation.js new file mode 100644 index 00000000..66a82f61 --- /dev/null +++ b/lib/modules/invites/Invitation.js @@ -0,0 +1,106 @@ +/** + * @flow + * Invitation representation wrapper + */ +import { Platform } from 'react-native'; +import AndroidInvitation from './AndroidInvitation'; + +import type { NativeInvitation } from './types'; + +export default class Invitation { + _android: AndroidInvitation; + _androidClientId: string | void; + _androidMinimumVersionCode: number | void; + _callToActionText: string | void; + _customImage: string | void; + _deepLink: string | void; + _iosClientId: string | void; + _message: string; + _title: string; + + constructor(title: string, message: string) { + this._android = new AndroidInvitation(this); + this._message = message; + this._title = title; + } + + /** + * + * @param androidClientId + * @returns {Invitation} + */ + setAndroidClientId(androidClientId: string): Invitation { + this._androidClientId = androidClientId; + return this; + } + + /** + * + * @param androidMinimumVersionCode + * @returns {Invitation} + */ + setAndroidMinimumVersionCode(androidMinimumVersionCode: number): Invitation { + this._androidMinimumVersionCode = androidMinimumVersionCode; + return this; + } + + /** + * + * @param callToActionText + * @returns {Invitation} + */ + setCallToActionText(callToActionText: string): Invitation { + this._callToActionText = callToActionText; + return this; + } + + /** + * + * @param customImage + * @returns {Invitation} + */ + setCustomImage(customImage: string): Invitation { + this._customImage = customImage; + return this; + } + + /** + * + * @param deepLink + * @returns {Invitation} + */ + setDeepLink(deepLink: string): Invitation { + this._deepLink = deepLink; + return this; + } + + /** + * + * @param iosClientId + * @returns {Invitation} + */ + setIOSClientId(iosClientId: string): Invitation { + this._iosClientId = iosClientId; + return this; + } + + build(): NativeInvitation { + if (!this._message) { + throw new Error('Invitation: Missing required `message` property'); + } else if (!this._title) { + throw new Error('Invitation: Missing required `title` property'); + } + + return { + android: Platform.OS === 'android' ? this._android.build() : undefined, + androidClientId: this._androidClientId, + androidMinimumVersionCode: this._androidMinimumVersionCode, + callToActionText: this._callToActionText, + customImage: this._customImage, + deepLink: this._deepLink, + iosClientId: this._iosClientId, + message: this._message, + title: this._title, + }; + } +} diff --git a/lib/modules/invites/index.js b/lib/modules/invites/index.js new file mode 100644 index 00000000..099a6ba9 --- /dev/null +++ b/lib/modules/invites/index.js @@ -0,0 +1,77 @@ +/** + * @flow + * Invites representation wrapper + */ +import { SharedEventEmitter } from '../../utils/events'; +import { getLogger } from '../../utils/log'; +import ModuleBase from '../../utils/ModuleBase'; +import { getNativeModule } from '../../utils/native'; +import Invitation from './Invitation'; + +import type App from '../core/app'; + +export const MODULE_NAME = 'RNFirebaseInvites'; +export const NAMESPACE = 'invites'; +const NATIVE_EVENTS = ['invites_invitation_received']; + +type InvitationOpen = { + deepLink: string, + invitationId: string, +}; + +export default class Invites extends ModuleBase { + constructor(app: App) { + super(app, { + events: NATIVE_EVENTS, + moduleName: MODULE_NAME, + multiApp: false, + namespace: NAMESPACE, + }); + + SharedEventEmitter.addListener( + // sub to internal native event - this fans out to + // public event name: onMessage + 'invites_invitation_received', + (invitation: InvitationOpen) => { + SharedEventEmitter.emit('onInvitation', invitation); + } + ); + } + + /** + * Returns the invitation that triggered application open + * @returns {Promise.} + */ + getInitialInvitation(): Promise { + return getNativeModule(this).getInitialInvitation(); + } + + /** + * Subscribe to invites + * @param listener + * @returns {Function} + */ + onInvitation(listener: InvitationOpen => any) { + getLogger(this).info('Creating onInvitation listener'); + + SharedEventEmitter.addListener('onInvitation', listener); + + return () => { + getLogger(this).info('Removing onInvitation listener'); + SharedEventEmitter.removeListener('onInvitation', listener); + }; + } + + sendInvitation(invitation: Invitation): Promise { + if (!(invitation instanceof Invitation)) { + throw new Error( + `Invites:sendInvitation expects an 'Invitation' but got type ${typeof invitation}` + ); + } + return getNativeModule(this).sendInvitation(invitation.build()); + } +} + +export const statics = { + Invitation, +}; diff --git a/lib/modules/invites/types.js b/lib/modules/invites/types.js new file mode 100644 index 00000000..45e24727 --- /dev/null +++ b/lib/modules/invites/types.js @@ -0,0 +1,21 @@ +/** + * @flow + */ +export type NativeAndroidInvitation = {| + additionalReferralParameters?: { [string]: string }, + emailHtmlContent?: string, + emailSubject?: string, + googleAnalyticsTrackingId?: string, +|}; + +export type NativeInvitation = {| + android?: NativeAndroidInvitation, + androidClientId?: string, + androidMinimumVersionCode?: number, + callToActionText?: string, + customImage?: string, + deepLink?: string, + iosClientId?: string, + message: string, + title: string, +|}; diff --git a/lib/modules/links/index.js b/lib/modules/links/index.js index 28c31879..b653d0ee 100644 --- a/lib/modules/links/index.js +++ b/lib/modules/links/index.js @@ -3,6 +3,7 @@ * Dynamic Links representation wrapper */ import { SharedEventEmitter } from '../../utils/events'; +import { getLogger } from '../../utils/log'; import ModuleBase from '../../utils/ModuleBase'; import { areObjectKeysContainedInOther, isObject, isString } from '../../utils'; import { getNativeModule } from '../../utils/native'; @@ -10,7 +11,7 @@ import { getNativeModule } from '../../utils/native'; import type App from '../core/app'; const EVENT_TYPE = { - Link: 'dynamic_link_received', + Link: 'links_link_received', }; const NATIVE_EVENTS = [EVENT_TYPE.Link]; @@ -82,33 +83,21 @@ export default class Links extends ModuleBase { multiApp: false, namespace: NAMESPACE, }); + + SharedEventEmitter.addListener( + // sub to internal native event - this fans out to + // public event name: onMessage + 'links_link_received', + (link: string) => { + SharedEventEmitter.emit('onLink', link); + } + ); } get EVENT_TYPE(): Object { return EVENT_TYPE; } - /** - * Returns the link that triggered application open - * @returns {Promise.} - */ - getInitialLink(): Promise { - return getNativeModule(this).getInitialLink(); - } - - /** - * Subscribe to dynamic links - * @param listener - * @returns {Function} - */ - onLink(listener: Function): () => any { - const rnListener = SharedEventEmitter.addListener( - EVENT_TYPE.Link, - listener - ); - return () => rnListener.remove(); - } - /** * Create long Dynamic Link from parameters * @param parameters @@ -138,6 +127,30 @@ export default class Links extends ModuleBase { return Promise.reject(error); } } + + /** + * Returns the link that triggered application open + * @returns {Promise.} + */ + getInitialLink(): Promise { + return getNativeModule(this).getInitialLink(); + } + + /** + * Subscribe to dynamic links + * @param listener + * @returns {Function} + */ + onLink(listener: string => any): () => any { + getLogger(this).info('Creating onLink listener'); + + SharedEventEmitter.addListener('onLink', listener); + + return () => { + getLogger(this).info('Removing onLink listener'); + SharedEventEmitter.removeListener('onLink', listener); + }; + } } export const statics = { diff --git a/lib/modules/messaging/RemoteMessage.js b/lib/modules/messaging/RemoteMessage.js index a518c875..c40b06ff 100644 --- a/lib/modules/messaging/RemoteMessage.js +++ b/lib/modules/messaging/RemoteMessage.js @@ -133,13 +133,13 @@ export default class RemoteMessage { } build(): NativeOutboundRemoteMessage { - if (!this.data) { + if (!this._data) { throw new Error('RemoteMessage: Missing required `data` property'); - } else if (!this.messageId) { + } else if (!this._messageId) { throw new Error('RemoteMessage: Missing required `messageId` property'); - } else if (!this.to) { + } else if (!this._to) { throw new Error('RemoteMessage: Missing required `to` property'); - } else if (!this.ttl) { + } else if (!this._ttl) { throw new Error('RemoteMessage: Missing required `ttl` property'); } diff --git a/lib/types/index.js b/lib/types/index.js index f48b3a5c..bfaf5ae1 100644 --- a/lib/types/index.js +++ b/lib/types/index.js @@ -17,6 +17,8 @@ import type Firestore from '../modules/firestore'; import { typeof statics as FirestoreStatics } from '../modules/firestore'; import type InstanceId from '../modules/instanceid'; import { typeof statics as InstanceIdStatics } from '../modules/instanceid'; +import type Invites from '../modules/invites'; +import { typeof statics as InvitesStatics } from '../modules/invites'; import type Links from '../modules/links'; import { typeof statics as LinksStatics } from '../modules/links'; import type Messaging from '../modules/messaging'; @@ -61,6 +63,7 @@ export type FirebaseModuleName = | 'RNFirebaseDatabase' | 'RNFirebaseFirestore' | 'RNFirebaseInstanceId' + | 'RNFirebaseInvites' | 'RNFirebaseLinks' | 'RNFirebaseMessaging' | 'RNFirebaseNotifications' @@ -78,6 +81,7 @@ export type FirebaseNamespace = | 'database' | 'firestore' | 'instanceid' + | 'invites' | 'links' | 'messaging' | 'notifications' @@ -173,10 +177,17 @@ export type FirestoreModule = { /* InstanceId types */ export type InstanceIdModule = { - (): InstanceId, -nativeModuleExists: boolean, + (): InstanceId, + nativeModuleExists: boolean, } & InstanceIdStatics; +/* Invites types */ + +export type InvitesModule = { + (): Invites, + nativeModuleExists: boolean, +} & InvitesStatics; + /* Links types */ export type LinksModule = { diff --git a/tests/android/app/src/main/AndroidManifest.xml b/tests/android/app/src/main/AndroidManifest.xml index 08d12b4d..e739648c 100644 --- a/tests/android/app/src/main/AndroidManifest.xml +++ b/tests/android/app/src/main/AndroidManifest.xml @@ -42,6 +42,15 @@ + + + + + + + + + { +// Messaging and Notifications testing +const notifications = async () => { try { await RNfirebase.messaging().requestPermission(); const instanceid = await RNfirebase.instanceid().get(); @@ -82,7 +83,9 @@ const init = async () => { .then(() => { RNfirebase.notifications() .getScheduledNotifications() - .then(notifications => console.log('scheduled: ', notifications)); + .then(scheduledNotifications => + console.log('scheduled: ', scheduledNotifications) + ); }); }, 5); } catch (error) { @@ -90,7 +93,32 @@ const init = async () => { } }; -init(); +// notifications(); + +// Invitations testing +const invitations = async () => { + try { + const initialLink = await RNfirebase.links().getInitialLink(); + console.log('initialLink: ', initialLink); + const initialInvite = await RNfirebase.invites().getInitialInvitation(); + console.log('initialInvite: ', initialInvite); + + RNfirebase.links().onLink(link => { + console.log('onLink: ', link); + }); + RNfirebase.invites().onInvitation(invite => { + console.log('onInvitation: ', invite); + }); + const invitation = new RNfirebase.invites.Invitation('Title', 'Message'); + invitation.setDeepLink('https://je786.app.goo.gl/testing'); + const invitationIds = await RNfirebase.invites().sendInvitation(invitation); + console.log('InvitationIds: ', invitationIds); + } catch (error) { + console.error('invitations init error:', error); + } +}; + +invitations(); const config = { apiKey: 'AIzaSyDnVqNhxU0Biit9nCo4RorAh5ulQQwko3E', From 2891027f25717a1e14cfde6cf9d76d95567746df Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 22 Mar 2018 14:55:02 +0000 Subject: [PATCH 65/77] [tests] Fix some configuration issues --- .../firebase/invites/RNFirebaseInvites.java | 4 +- lib/index.d.ts | 2 +- lib/modules/messaging/index.js | 2 +- tests/ios/GoogleService-Info.plist | 28 ++++---- tests/ios/Podfile | 1 + tests/ios/Podfile.lock | 49 ++++++++++++- .../project.pbxproj | 68 +++++++++++++++++++ tests/ios/ReactNativeFirebaseDemo/Info.plist | 17 ++++- tests/src/firebase.js | 6 +- tests/src/tests/messaging/messagingTests.js | 4 +- 10 files changed, 156 insertions(+), 25 deletions(-) diff --git a/android/src/main/java/io/invertase/firebase/invites/RNFirebaseInvites.java b/android/src/main/java/io/invertase/firebase/invites/RNFirebaseInvites.java index 7a01983a..1504194c 100644 --- a/android/src/main/java/io/invertase/firebase/invites/RNFirebaseInvites.java +++ b/android/src/main/java/io/invertase/firebase/invites/RNFirebaseInvites.java @@ -168,9 +168,9 @@ public class RNFirebaseInvites extends ReactContextBaseJavaModule implements Act String[] ids = AppInviteInvitation.getInvitationIds(resultCode, data); mPromise.resolve(Arguments.fromList(Arrays.asList(ids))); } else if (resultCode == Activity.RESULT_CANCELED) { - mPromise.reject("invites/invitation-cancelled", " The invitation was cancelled"); + mPromise.reject("invites/invitation-cancelled", "Invitation cancelled"); } else { - mPromise.reject("invites/invitation-error", " The invitation failed to send"); + mPromise.reject("invites/invitation-error", "Invitation failed to send"); } // Clear the promise mPromise = null; diff --git a/lib/index.d.ts b/lib/index.d.ts index 4f239a9e..29b931f1 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -971,7 +971,7 @@ declare module "react-native-firebase" { * @param senderId * @param payload */ - send(senderId: string, payload: RemoteMessage): any + sendMessage(senderId: string, payload: RemoteMessage): any NOTIFICATION_TYPE: Object REMOTE_NOTIFICATION_RESULT: Object diff --git a/lib/modules/messaging/index.js b/lib/modules/messaging/index.js index 9eaf649f..5aacfc46 100644 --- a/lib/modules/messaging/index.js +++ b/lib/modules/messaging/index.js @@ -132,7 +132,7 @@ export default class Messaging extends ModuleBase { `Messaging:sendMessage expects a 'RemoteMessage' but got type ${typeof remoteMessage}` ); } - return getNativeModule(this).send(remoteMessage.build()); + return getNativeModule(this).sendMessage(remoteMessage.build()); } subscribeToTopic(topic: string): void { diff --git a/tests/ios/GoogleService-Info.plist b/tests/ios/GoogleService-Info.plist index 07e6b464..0a85e4c0 100644 --- a/tests/ios/GoogleService-Info.plist +++ b/tests/ios/GoogleService-Info.plist @@ -7,34 +7,34 @@ AD_UNIT_ID_FOR_INTERSTITIAL_TEST ca-app-pub-3940256099942544/4411468910 CLIENT_ID - 305229645282-22imndi01abc2p6esgtu1i1m9mqrd0ib.apps.googleusercontent.com + 17067372085-siujfe334vool17t2mtrmjrsgl81nhd9.apps.googleusercontent.com REVERSED_CLIENT_ID - com.googleusercontent.apps.305229645282-22imndi01abc2p6esgtu1i1m9mqrd0ib + com.googleusercontent.apps.17067372085-siujfe334vool17t2mtrmjrsgl81nhd9 API_KEY - AIzaSyAcdVLG5dRzA1ck_fa_xd4Z0cY7cga7S5A + AIzaSyC8ZEruBCvS_6woF8_l07ILy1eXaD6J4vQ GCM_SENDER_ID - 305229645282 + 17067372085 PLIST_VERSION 1 BUNDLE_ID - com.invertase.ReactNativeFirebaseDemo + com.invertase.RNFirebaseTests PROJECT_ID - rnfirebase-b9ad4 + rnfirebase STORAGE_BUCKET - rnfirebase-b9ad4.appspot.com + rnfirebase.appspot.com IS_ADS_ENABLED - + IS_ANALYTICS_ENABLED - + IS_APPINVITE_ENABLED - + IS_GCM_ENABLED - + IS_SIGNIN_ENABLED - + GOOGLE_APP_ID - 1:305229645282:ios:7b45748cb1117d2d + 1:17067372085:ios:d9ef660bd02cc2f1 DATABASE_URL - https://rnfirebase-b9ad4.firebaseio.com + https://rnfirebase-5579a.firebaseio.com diff --git a/tests/ios/Podfile b/tests/ios/Podfile index d305099b..36cf41c5 100644 --- a/tests/ios/Podfile +++ b/tests/ios/Podfile @@ -26,6 +26,7 @@ target 'ReactNativeFirebaseDemo' do pod 'Firebase/Database' pod 'Firebase/DynamicLinks' pod 'Firebase/Firestore' + pod 'Firebase/Invites' pod 'Firebase/Messaging' pod 'Firebase/RemoteConfig' pod 'Firebase/Storage' diff --git a/tests/ios/Podfile.lock b/tests/ios/Podfile.lock index 3af0c2e6..aeb21870 100644 --- a/tests/ios/Podfile.lock +++ b/tests/ios/Podfile.lock @@ -29,6 +29,9 @@ PODS: - Firebase/Firestore (4.10.0): - Firebase/Core - FirebaseFirestore (= 0.10.2) + - Firebase/Invites (4.10.0): + - Firebase/Core + - FirebaseInvites (= 2.0.2) - Firebase/Messaging (4.10.0): - Firebase/Core - FirebaseMessaging (= 2.1.1) @@ -75,6 +78,20 @@ PODS: - Protobuf (~> 3.5) - FirebaseInstanceID (2.0.9): - FirebaseCore (~> 4.0) + - FirebaseInvites (2.0.2): + - FirebaseAnalytics (~> 4.0) + - FirebaseDynamicLinks (~> 2.2) + - GoogleAPIClientForREST (~> 1.0) + - GoogleSignIn (~> 4.1) + - GoogleToolboxForMac/Logger (~> 2.1) + - GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1) + - GoogleToolboxForMac/NSString+URLArguments (~> 2.1) + - GoogleToolboxForMac/StringEncoding (~> 2.1) + - GoogleToolboxForMac/URLBuilder (~> 2.1) + - GTMOAuth2 (~> 1.0) + - GTMSessionFetcher/Core (~> 1.1) + - GTMSessionFetcher/Full (~> 1.1) + - Protobuf (~> 3.1) - FirebaseMessaging (2.1.1): - FirebaseAnalytics (~> 4.1) - FirebaseCore (~> 4.0) @@ -102,6 +119,18 @@ PODS: - GTMSessionFetcher/Core (~> 1.1) - FirebaseSwizzlingUtilities (1.0.0) - Google-Mobile-Ads-SDK (7.29.0) + - GoogleAPIClientForREST (1.3.2): + - GoogleAPIClientForREST/Core (= 1.3.2) + - GTMSessionFetcher (>= 1.1.7) + - GoogleAPIClientForREST/Core (1.3.2): + - GTMSessionFetcher (>= 1.1.7) + - GoogleSignIn (4.1.2): + - GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1) + - GoogleToolboxForMac/NSString+URLArguments (~> 2.1) + - GTMOAuth2 (~> 1.0) + - GTMSessionFetcher/Core (~> 1.1) + - GoogleToolboxForMac/Core (2.1.3): + - GoogleToolboxForMac/Defines (= 2.1.3) - GoogleToolboxForMac/DebugUtils (2.1.3): - GoogleToolboxForMac/Defines (= 2.1.3) - GoogleToolboxForMac/Defines (2.1.3) @@ -114,6 +143,13 @@ PODS: - GoogleToolboxForMac/Defines (= 2.1.3) - GoogleToolboxForMac/NSString+URLArguments (= 2.1.3) - GoogleToolboxForMac/NSString+URLArguments (2.1.3) + - GoogleToolboxForMac/StringEncoding (2.1.3): + - GoogleToolboxForMac/Defines (= 2.1.3) + - GoogleToolboxForMac/URLBuilder (2.1.3): + - GoogleToolboxForMac/Core (= 2.1.3) + - GoogleToolboxForMac/Defines (= 2.1.3) + - GoogleToolboxForMac/NSDictionary+URLArguments (= 2.1.3) + - GoogleToolboxForMac/NSString+URLArguments (= 2.1.3) - gRPC (1.10.0): - gRPC-RxLibrary (= 1.10.0) - gRPC/Main (= 1.10.0) @@ -133,7 +169,13 @@ PODS: - gRPC/Main (1.10.0): - gRPC-Core (= 1.10.0) - gRPC-RxLibrary (= 1.10.0) + - GTMOAuth2 (1.1.6): + - GTMSessionFetcher (~> 1.1) + - GTMSessionFetcher (1.1.14): + - GTMSessionFetcher/Full (= 1.1.14) - GTMSessionFetcher/Core (1.1.14) + - GTMSessionFetcher/Full (1.1.14): + - GTMSessionFetcher/Core (= 1.1.14) - leveldb-library (1.20) - nanopb (0.3.8): - nanopb/decode (= 0.3.8) @@ -178,6 +220,7 @@ DEPENDENCIES: - Firebase/Database - Firebase/DynamicLinks - Firebase/Firestore + - Firebase/Invites - Firebase/Messaging - Firebase/Performance - Firebase/RemoteConfig @@ -212,17 +255,21 @@ SPEC CHECKSUMS: FirebaseDynamicLinks: 38b68641d24e78d0277a9205d988ce22875d5a25 FirebaseFirestore: 9423ca756bbf77bfa3cd02fafc8027ae79da625a FirebaseInstanceID: d2058a35e9bebda1b6dd42486b84917bde552a9d + FirebaseInvites: ae15e0636f9eb42bdf5c1ef4c8f7bd4a88f9878b FirebaseMessaging: db0e01c52ef7e1f42846431273558107d084ede4 FirebasePerformance: 96c831a9eaf8d2ddf8bb37a4a6f6dd1b4bfe929f FirebaseRemoteConfig: df64ce784a45e9b9d4988030a59da6baede8bdb0 FirebaseStorage: 9a863a2bb96c406958eeff7c2f1dfa9f44c44a13 FirebaseSwizzlingUtilities: f1c49a5a372ac852c853722a5891a0a5e2344a6c Google-Mobile-Ads-SDK: 375bbb821b3df2106c37b74f6b0a97576ddc5f6b + GoogleAPIClientForREST: 4fa84fc61fdeea48dd7088de5c1a864ace18125b + GoogleSignIn: d9ef55b10f0aa401a5de2747f59b725e4b9732ac GoogleToolboxForMac: 2501e2ad72a52eb3dfe7bd9aee7dad11b858bd20 gRPC: f54f0e6d603052b4562447da442ce2ff30bcdacc gRPC-Core: a030b1678ded49c88ec5ba7c90ee8ee5f47ec6e1 gRPC-ProtoRPC: 22712b23eb1bda656a59715fa5c1da0ea1493ea4 gRPC-RxLibrary: a41a4652d220f230ba1c0491a94ce2ee04c6180a + GTMOAuth2: c77fe325e4acd453837e72d91e3b5f13116857b2 GTMSessionFetcher: 390ea358e5a0d0133153806f744662dad933d06b leveldb-library: 08cba283675b7ed2d99629a4bc5fd052cd2bb6a5 nanopb: 5601e6bca2dbf1ed831b519092ec110f66982ca3 @@ -231,6 +278,6 @@ SPEC CHECKSUMS: RNFirebase: dff98ceb517ef0b5e1858deb59eca186acc6edb0 yoga: 55da126afc384965b96bff46652464373b330add -PODFILE CHECKSUM: d20e53c395f89ffcfe49e809a8fd7f54beca0700 +PODFILE CHECKSUM: 98821556e9f752c7e43b1d6c5cf757cdaefcffa2 COCOAPODS: 1.4.0 diff --git a/tests/ios/ReactNativeFirebaseDemo.xcodeproj/project.pbxproj b/tests/ios/ReactNativeFirebaseDemo.xcodeproj/project.pbxproj index 08168fda..c5df5307 100644 --- a/tests/ios/ReactNativeFirebaseDemo.xcodeproj/project.pbxproj +++ b/tests/ios/ReactNativeFirebaseDemo.xcodeproj/project.pbxproj @@ -223,6 +223,34 @@ remoteGlobalIDString = 58B5119B1A9E6C1200147676; remoteInfo = RCTText; }; + 83AAA0B52063E593007EC5F7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = EBF21BDC1FC498900052F4D5; + remoteInfo = jsinspector; + }; + 83AAA0B72063E593007EC5F7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = EBF21BFA1FC4989A0052F4D5; + remoteInfo = "jsinspector-tvOS"; + }; + 83AAA0B92063E593007EC5F7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 9936F3131F5F2E4B0010BF04; + remoteInfo = privatedata; + }; + 83AAA0BB2063E593007EC5F7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 9936F32F1F5F2E5B0010BF04; + remoteInfo = "privatedata-tvOS"; + }; 8E7D4A501F83AF5200699FE4 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; @@ -464,10 +492,14 @@ 3DAD3EAB1DF850E9000B6D8A /* libcxxreact.a */, 3DAD3EAD1DF850E9000B6D8A /* libjschelpers.a */, 3DAD3EAF1DF850E9000B6D8A /* libjschelpers.a */, + 83AAA0B62063E593007EC5F7 /* libjsinspector.a */, + 83AAA0B82063E593007EC5F7 /* libjsinspector-tvOS.a */, 8EC7AC351F8397E50041B0FE /* libthird-party.a */, 8EC7AC371F8397E50041B0FE /* libthird-party.a */, 8EC7AC391F8397E50041B0FE /* libdouble-conversion.a */, 8EC7AC3B1F8397E50041B0FE /* libdouble-conversion.a */, + 83AAA0BA2063E593007EC5F7 /* libprivatedata.a */, + 83AAA0BC2063E593007EC5F7 /* libprivatedata-tvOS.a */, ); name = Products; sourceTree = ""; @@ -894,6 +926,34 @@ remoteRef = 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 83AAA0B62063E593007EC5F7 /* libjsinspector.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libjsinspector.a; + remoteRef = 83AAA0B52063E593007EC5F7 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 83AAA0B82063E593007EC5F7 /* libjsinspector-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libjsinspector-tvOS.a"; + remoteRef = 83AAA0B72063E593007EC5F7 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 83AAA0BA2063E593007EC5F7 /* libprivatedata.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libprivatedata.a; + remoteRef = 83AAA0B92063E593007EC5F7 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 83AAA0BC2063E593007EC5F7 /* libprivatedata-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libprivatedata-tvOS.a"; + remoteRef = 83AAA0BB2063E593007EC5F7 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 8E7D4A511F83AF5200699FE4 /* libfishhook.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; @@ -1016,10 +1076,18 @@ ); inputPaths = ( "${SRCROOT}/Pods/Target Support Files/Pods-ReactNativeFirebaseDemo/Pods-ReactNativeFirebaseDemo-resources.sh", + "${PODS_ROOT}/FirebaseInvites/Resources/GINInviteResources.bundle", + "${PODS_ROOT}/FirebaseInvites/Resources/GPPACLPickerResources.bundle", + "${PODS_ROOT}/GTMOAuth2/Source/Touch/GTMOAuth2ViewTouch.xib", + "${PODS_ROOT}/GoogleSignIn/Resources/GoogleSignIn.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/gRPC/gRPCCertificates.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GINInviteResources.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GPPACLPickerResources.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMOAuth2ViewTouch.nib", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/gRPCCertificates.bundle", ); runOnlyForDeploymentPostprocessing = 0; diff --git a/tests/ios/ReactNativeFirebaseDemo/Info.plist b/tests/ios/ReactNativeFirebaseDemo/Info.plist index 6bb5a3b6..98dbc10f 100644 --- a/tests/ios/ReactNativeFirebaseDemo/Info.plist +++ b/tests/ios/ReactNativeFirebaseDemo/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) + com.invertase.RNFirebaseTests CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -18,8 +18,21 @@ 1.0 CFBundleSignature ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + com.googleusercontent.apps.17067372085-siujfe334vool17t2mtrmjrsgl81nhd9 + + + CFBundleVersion 1 + LSApplicationCategoryType + LSRequiresIPhoneOS NSAppTransportSecurity @@ -33,6 +46,8 @@ + NSContactsUsageDescription + Needs access to your contacts to send invitations NSLocationWhenInUseUsageDescription UIAppFonts diff --git a/tests/src/firebase.js b/tests/src/firebase.js index 8f088001..5859017e 100644 --- a/tests/src/firebase.js +++ b/tests/src/firebase.js @@ -142,10 +142,10 @@ const android = { const ios = { clientId: - '305229645282-22imndi01abc2p6esgtu1i1m9mqrd0ib.apps.googleusercontent.com', + '305229645282-t29pn6o2t7se1f7rvrfsll4r0pvd6fb6.apps.googleusercontent.com', androidClientId: android.clientId, - appId: '1:305229645282:ios:7b45748cb1117d2d', - apiKey: 'AIzaSyDnVqNhxU0Biit9nCo4RorAh5ulQQwko3E', + appId: '1:305229645282:ios:d9ef660bd02cc2f1', + apiKey: 'AIzaSyAcdVLG5dRzA1ck_fa_xd4Z0cY7cga7S5A', databaseURL: 'https://rnfirebase-b9ad4.firebaseio.com', storageBucket: 'rnfirebase-b9ad4.appspot.com', messagingSenderId: '305229645282', diff --git a/tests/src/tests/messaging/messagingTests.js b/tests/src/tests/messaging/messagingTests.js index 2a82cb90..3f0e3d15 100644 --- a/tests/src/tests/messaging/messagingTests.js +++ b/tests/src/tests/messaging/messagingTests.js @@ -50,7 +50,7 @@ function messagingTests({ describe, it, firebase }) { number: 123456, }); - firebase.native.messaging().send(remoteMessage); + firebase.native.messaging().sendMessage(remoteMessage); return Promise.resolve(); }); @@ -116,7 +116,7 @@ function messagingTests({ describe, it, firebase }) { number: 123456, }); - firebase.native.messaging().send(remoteMessage); + firebase.native.messaging().sendMessage(remoteMessage); return Promise.resolve(); }); From 417331837923ba56d77318632462ef670693c6a0 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 22 Mar 2018 14:56:15 +0000 Subject: [PATCH 66/77] [links] Tweak links event name --- ios/RNFirebase/RNFirebaseEvents.h | 8 ++++---- ios/RNFirebase/links/RNFirebaseLinks.m | 20 +++++++++----------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/ios/RNFirebase/RNFirebaseEvents.h b/ios/RNFirebase/RNFirebaseEvents.h index 82564b28..ef8b7c79 100644 --- a/ios/RNFirebase/RNFirebaseEvents.h +++ b/ios/RNFirebase/RNFirebaseEvents.h @@ -36,9 +36,6 @@ static NSString *const STORAGE_DOWNLOAD_FAILURE = @"download_failure"; static NSString *const MESSAGING_MESSAGE_RECEIVED = @"messaging_message_received"; static NSString *const MESSAGING_TOKEN_REFRESHED = @"messaging_token_refreshed"; -// TODO: Remove -static NSString *const MESSAGING_NOTIFICATION_RECEIVED = @"messaging_notification_received"; - // Notifications static NSString *const NOTIFICATIONS_NOTIFICATION_DISPLAYED = @"notifications_notification_displayed"; static NSString *const NOTIFICATIONS_NOTIFICATION_OPENED = @"notifications_notification_opened"; @@ -49,6 +46,9 @@ static NSString *const ADMOB_INTERSTITIAL_EVENT = @"interstitial_event"; static NSString *const ADMOB_REWARDED_VIDEO_EVENT = @"rewarded_video_event"; // Links -static NSString *const LINKS_DYNAMIC_LINK_RECEIVED = @"links_link_received"; +static NSString *const LINKS_LINK_RECEIVED = @"links_link_received"; + +// Invites +static NSString *const INVITES_INVITATION_RECEIVED = @"invites_invitation_received"; #endif diff --git a/ios/RNFirebase/links/RNFirebaseLinks.m b/ios/RNFirebase/links/RNFirebaseLinks.m index 067b6169..0251e8d6 100644 --- a/ios/RNFirebase/links/RNFirebaseLinks.m +++ b/ios/RNFirebase/links/RNFirebaseLinks.m @@ -7,7 +7,7 @@ static void sendDynamicLink(NSURL *url, id sender) { if (url) { - [[NSNotificationCenter defaultCenter] postNotificationName:LINKS_DYNAMIC_LINK_RECEIVED + [[NSNotificationCenter defaultCenter] postNotificationName:LINKS_LINK_RECEIVED object:sender userInfo:@{@"url": url.absoluteString}]; NSLog(@"sendDynamicLink Success: %@", url.absoluteString); @@ -31,7 +31,7 @@ RCT_EXPORT_MODULE(); // Set up internal listener to send notification over bridge [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sendDynamicLinkEvent:) - name:LINKS_DYNAMIC_LINK_RECEIVED + name:LINKS_LINK_RECEIVED object:nil]; } @@ -83,11 +83,11 @@ continueUserActivity:(NSUserActivity *)userActivity } - (NSArray *)supportedEvents { - return @[LINKS_DYNAMIC_LINK_RECEIVED]; + return @[LINKS_LINK_RECEIVED]; } - (void)sendDynamicLinkEvent:(NSNotification *)notification { - [self sendEventWithName:LINKS_DYNAMIC_LINK_RECEIVED body:notification.userInfo[@"url"]]; + [self sendEventWithName:LINKS_LINK_RECEIVED body:notification.userInfo[@"url"]]; } -(void)handleInitialLinkFromCustomSchemeURL:(NSURL*)url resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject { @@ -126,7 +126,7 @@ RCT_EXPORT_METHOD(getInitialLink:(RCTPromiseResolveBlock)resolve rejecter:(RCTPr if (self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey]) { NSURL* url = (NSURL*)self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey]; [self handleInitialLinkFromCustomSchemeURL:url resolver:resolve rejecter:reject]; - + } else { NSDictionary *userActivityDictionary = self.bridge.launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey]; @@ -137,7 +137,7 @@ RCT_EXPORT_METHOD(getInitialLink:(RCTPromiseResolveBlock)resolve rejecter:(RCTPr RCT_EXPORT_METHOD(createDynamicLink: (NSDictionary *) metadata resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { @try { FIRDynamicLinkComponents *components = [self getDynamicLinkComponentsFromMetadata:metadata]; - + if (components == nil) { reject(@"links/failure", @"Failed to create Dynamic Link", nil); } else { @@ -181,11 +181,11 @@ RCT_EXPORT_METHOD(createShortDynamicLink: (NSDictionary *) metadata resolver:(RC NSURL *link = [NSURL URLWithString:metadata[@"link"]]; FIRDynamicLinkComponents *components = [FIRDynamicLinkComponents componentsWithLink:link domain:metadata[@"dynamicLinkDomain"]]; - + [self setAndroidParameters:metadata components:components]; [self setIosParameters:metadata components:components]; [self setSocialMetaTagParameters:metadata components:components]; - + return components; } @catch(NSException * e) { @@ -200,7 +200,7 @@ RCT_EXPORT_METHOD(createShortDynamicLink: (NSDictionary *) metadata resolver:(RC if (androidParametersDict) { FIRDynamicLinkAndroidParameters *androidParams = [FIRDynamicLinkAndroidParameters parametersWithPackageName: androidParametersDict[@"androidPackageName"]]; - + if (androidParametersDict[@"androidFallbackLink"]) { androidParams.fallbackURL = [NSURL URLWithString:androidParametersDict[@"androidFallbackLink"]]; } @@ -283,5 +283,3 @@ RCT_EXPORT_METHOD(createShortDynamicLink: (NSDictionary *) metadata resolver:(RC @implementation RNFirebaseLinks @end #endif - - From 54b419dfcf9b9c69efe6621065a28ecfdad9ea3e Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 22 Mar 2018 14:56:49 +0000 Subject: [PATCH 67/77] [invites] Initial iOS functionality --- ios/RNFirebase.xcodeproj/project.pbxproj | 15 ++ ios/RNFirebase/invites/RNFirebaseInvites.h | 29 ++++ ios/RNFirebase/invites/RNFirebaseInvites.m | 133 ++++++++++++++++++ .../ios/ReactNativeFirebaseDemo/AppDelegate.m | 4 +- tests/src/firebase.js | 2 +- 5 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 ios/RNFirebase/invites/RNFirebaseInvites.h create mode 100644 ios/RNFirebase/invites/RNFirebaseInvites.m diff --git a/ios/RNFirebase.xcodeproj/project.pbxproj b/ios/RNFirebase.xcodeproj/project.pbxproj index 46815506..b548d583 100644 --- a/ios/RNFirebase.xcodeproj/project.pbxproj +++ b/ios/RNFirebase.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ 839D91721EF3E20B0077C7C8 /* RNFirebaseCrash.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D915F1EF3E20A0077C7C8 /* RNFirebaseCrash.m */; }; 839D91731EF3E20B0077C7C8 /* RNFirebaseDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D91621EF3E20A0077C7C8 /* RNFirebaseDatabase.m */; }; 839D91751EF3E20B0077C7C8 /* RNFirebasePerformance.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D91681EF3E20A0077C7C8 /* RNFirebasePerformance.m */; }; + 83AAA0792063DEC2007EC5F7 /* RNFirebaseInvites.m in Sources */ = {isa = PBXBuildFile; fileRef = 83AAA0772063DEC2007EC5F7 /* RNFirebaseInvites.m */; }; 83C3EEEE1FA1EACC00B64D3C /* RNFirebaseUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 83C3EEEC1FA1EACC00B64D3C /* RNFirebaseUtil.m */; }; BA84AE571FA9E59800E79390 /* RNFirebaseStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = BA84AE561FA9E59800E79390 /* RNFirebaseStorage.m */; }; D950369E1D19C77400F7094D /* RNFirebase.m in Sources */ = {isa = PBXBuildFile; fileRef = D950369D1D19C77400F7094D /* RNFirebase.m */; }; @@ -93,6 +94,8 @@ 839D91671EF3E20A0077C7C8 /* RNFirebasePerformance.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebasePerformance.h; sourceTree = ""; }; 839D91681EF3E20A0077C7C8 /* RNFirebasePerformance.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebasePerformance.m; sourceTree = ""; }; 839D91771EF3E22F0077C7C8 /* RNFirebaseEvents.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNFirebaseEvents.h; path = RNFirebase/RNFirebaseEvents.h; sourceTree = ""; }; + 83AAA0772063DEC2007EC5F7 /* RNFirebaseInvites.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseInvites.m; sourceTree = ""; }; + 83AAA0782063DEC2007EC5F7 /* RNFirebaseInvites.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseInvites.h; sourceTree = ""; }; 83C3EEEC1FA1EACC00B64D3C /* RNFirebaseUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RNFirebaseUtil.m; path = RNFirebase/RNFirebaseUtil.m; sourceTree = ""; }; 83C3EEED1FA1EACC00B64D3C /* RNFirebaseUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNFirebaseUtil.h; path = RNFirebase/RNFirebaseUtil.h; sourceTree = ""; }; BA84AE551FA9E59800E79390 /* RNFirebaseStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseStorage.h; sourceTree = ""; }; @@ -133,6 +136,7 @@ 58B511D21A9E6C8500147676 = { isa = PBXGroup; children = ( + 83AAA0762063DEC2007EC5F7 /* invites */, 838E372420231E15004DCD3A /* notifications */, 838E372020231DF0004DCD3A /* instanceid */, 8336930F1FD80DE800AA806B /* fabric */, @@ -301,6 +305,16 @@ path = RNFirebase/perf; sourceTree = ""; }; + 83AAA0762063DEC2007EC5F7 /* invites */ = { + isa = PBXGroup; + children = ( + 83AAA0772063DEC2007EC5F7 /* RNFirebaseInvites.m */, + 83AAA0782063DEC2007EC5F7 /* RNFirebaseInvites.h */, + ); + name = invites; + path = RNFirebase/invites; + sourceTree = ""; + }; BA84AE541FA9E59800E79390 /* storage */ = { isa = PBXGroup; children = ( @@ -384,6 +398,7 @@ 839D91731EF3E20B0077C7C8 /* RNFirebaseDatabase.m in Sources */, 838E372720231E15004DCD3A /* RNFirebaseNotifications.m in Sources */, BA84AE571FA9E59800E79390 /* RNFirebaseStorage.m in Sources */, + 83AAA0792063DEC2007EC5F7 /* RNFirebaseInvites.m in Sources */, 8323CF071F6FBD870071420B /* NativeExpressComponent.m in Sources */, 83C3EEEE1FA1EACC00B64D3C /* RNFirebaseUtil.m in Sources */, 839D91721EF3E20B0077C7C8 /* RNFirebaseCrash.m in Sources */, diff --git a/ios/RNFirebase/invites/RNFirebaseInvites.h b/ios/RNFirebase/invites/RNFirebaseInvites.h new file mode 100644 index 00000000..ddeda5ac --- /dev/null +++ b/ios/RNFirebase/invites/RNFirebaseInvites.h @@ -0,0 +1,29 @@ +#ifndef RNFirebaseInvites_h +#define RNFirebaseInvites_h +#import + +#if __has_include() +#import +#import +#import + +@interface RNFirebaseInvites : RCTEventEmitter + ++ (_Nonnull instancetype)instance; + +@property _Nullable RCTPromiseRejectBlock invitationsRejecter; +@property _Nullable RCTPromiseResolveBlock invitationsResolver; + +#if !TARGET_OS_TV +- (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo; +- (void)didRegisterUserNotificationSettings:(nonnull UIUserNotificationSettings *)notificationSettings; +#endif + +@end + +#else +@interface RNFirebaseInvites : NSObject +@end +#endif + +#endif diff --git a/ios/RNFirebase/invites/RNFirebaseInvites.m b/ios/RNFirebase/invites/RNFirebaseInvites.m new file mode 100644 index 00000000..2d064436 --- /dev/null +++ b/ios/RNFirebase/invites/RNFirebaseInvites.m @@ -0,0 +1,133 @@ +#import "RNFirebaseInvites.h" + +#if __has_include() +#import "RNFirebaseEvents.h" +#import + +#import +#import +#import + +@implementation RNFirebaseInvites + +static RNFirebaseInvites *theRNFirebaseInvites = nil; + ++ (nonnull instancetype)instance { + return theRNFirebaseInvites; +} + +RCT_EXPORT_MODULE() + +- (id)init { + self = [super init]; + if (self != nil) { + NSLog(@"Setting up RNFirebaseInvites instance"); + // Set static instance for use from AppDelegate + theRNFirebaseInvites = self; + } + return self; +} + +// ******************************************************* +// ** Start AppDelegate methods +// ******************************************************* + +// Listen for permission response +- (void) didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { + +} + +// Listen for FCM data messages that arrive as a remote notification +- (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo { + +} + +// ******************************************************* +// ** Finish AppDelegate methods +// ******************************************************* + + +// ******************************************************* +// ** Start FIRInviteDelegate methods +// ******************************************************* + +// Listen for invitation response +- (void)inviteFinishedWithInvitations:(NSArray *)invitationIds error:(NSError *)error { + if (error) { + _invitationsRejecter(@"invites/invitation-error", @"Invitation failed to send", error); + } else if (invitationIds.count == 0) { + _invitationsRejecter(@"invites/invitation-cancelled", @"Invitation cancelled", nil); + } else { + _invitationsResolver(invitationIds); + } + _invitationsRejecter = nil; + _invitationsResolver = nil; +} + +// ******************************************************* +// ** Finish FIRInviteDelegate methods +// ******************************************************* + +// ** Start React Module methods ** +RCT_EXPORT_METHOD(getInitialInvitation:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + // TODO + resolve(nil); +} + +RCT_EXPORT_METHOD(sendInvitation:(NSDictionary *) invitation + resolve:(RCTPromiseResolveBlock) resolve + reject:(RCTPromiseRejectBlock) reject) { + if (!invitation[@"message"]) { + reject(@"invites/invalid-invitation", @"The supplied invitation is missing a 'message' field", nil); + } + if (!invitation[@"title"]) { + reject(@"invites/invalid-invitation", @"The supplied invitation is missing a 'title' field", nil); + } + id inviteDialog = [FIRInvites inviteDialog]; + [inviteDialog setInviteDelegate: self]; + [inviteDialog setMessage:invitation[@"message"]]; + [inviteDialog setTitle:invitation[@"title"]]; + + if (invitation[@"androidClientId"]) { + FIRInvitesTargetApplication *targetApplication = [[FIRInvitesTargetApplication alloc] init]; + targetApplication.androidClientID = invitation[@"androidClientId"]; + [inviteDialog setOtherPlatformsTargetApplication:targetApplication]; + } + if (invitation[@"androidMinimumVersionCode"]) { + [inviteDialog setAndroidMinimumVersionCode:invitation[@"androidMinimumVersionCode"]]; + } + if (invitation[@"callToActionText"]) { + [inviteDialog setCallToActionText:invitation[@"callToActionText"]]; + } + if (invitation[@"customImage"]) { + [inviteDialog setCustomImage:invitation[@"customImage"]]; + } + if (invitation[@"deepLink"]) { + [inviteDialog setDeepLink:invitation[@"deepLink"]]; + } + + // Save the promise details for later + _invitationsRejecter = reject; + _invitationsResolver = resolve; + + // Open the invitation dialog + [inviteDialog open]; +} + +// ** Start internals ** + +- (NSArray *)supportedEvents { + return @[INVITES_INVITATION_RECEIVED]; +} + ++ (BOOL)requiresMainQueueSetup +{ + return YES; +} + +@end + +#else +@implementation RNFirebaseInvites +@end +#endif diff --git a/tests/ios/ReactNativeFirebaseDemo/AppDelegate.m b/tests/ios/ReactNativeFirebaseDemo/AppDelegate.m index 81105040..478c7af9 100644 --- a/tests/ios/ReactNativeFirebaseDemo/AppDelegate.m +++ b/tests/ios/ReactNativeFirebaseDemo/AppDelegate.m @@ -11,14 +11,16 @@ #import #import -#import #import +@import Firebase; +@import GoogleSignIn; @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [FIRApp configure]; + [GIDSignIn sharedInstance].clientID = [FIRApp defaultApp].options.clientID; NSURL *jsCodeLocation; jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; diff --git a/tests/src/firebase.js b/tests/src/firebase.js index 5859017e..201b28c2 100644 --- a/tests/src/firebase.js +++ b/tests/src/firebase.js @@ -156,7 +156,7 @@ const instances = { web: firebase.initializeApp(config), native: RNfirebase, another: RNfirebase.initializeApp( - Platform.OS === 'ios' ? ios : android, + ios, // Platform.OS === 'ios' ? ios : android, 'anotherApp' ), }; From d1d53a442d23c5b83ba6304aff5b5ce904c462b5 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 22 Mar 2018 16:36:42 +0000 Subject: [PATCH 68/77] [invites] iOS getInitialInvitation and onInvitation functionality --- ios/RNFirebase/invites/RNFirebaseInvites.h | 10 ++- ios/RNFirebase/invites/RNFirebaseInvites.m | 90 ++++++++++++++++++---- 2 files changed, 81 insertions(+), 19 deletions(-) diff --git a/ios/RNFirebase/invites/RNFirebaseInvites.h b/ios/RNFirebase/invites/RNFirebaseInvites.h index ddeda5ac..5987dbf3 100644 --- a/ios/RNFirebase/invites/RNFirebaseInvites.h +++ b/ios/RNFirebase/invites/RNFirebaseInvites.h @@ -14,10 +14,11 @@ @property _Nullable RCTPromiseRejectBlock invitationsRejecter; @property _Nullable RCTPromiseResolveBlock invitationsResolver; -#if !TARGET_OS_TV -- (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo; -- (void)didRegisterUserNotificationSettings:(nonnull UIUserNotificationSettings *)notificationSettings; -#endif +- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options; + +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation; + +- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *))restorationHandler; @end @@ -27,3 +28,4 @@ #endif #endif + diff --git a/ios/RNFirebase/invites/RNFirebaseInvites.m b/ios/RNFirebase/invites/RNFirebaseInvites.m index 2d064436..76ad8b1b 100644 --- a/ios/RNFirebase/invites/RNFirebaseInvites.m +++ b/ios/RNFirebase/invites/RNFirebaseInvites.m @@ -2,6 +2,7 @@ #if __has_include() #import "RNFirebaseEvents.h" +#import "RNFirebaseUtil.h" #import #import @@ -32,21 +33,34 @@ RCT_EXPORT_MODULE() // ** Start AppDelegate methods // ******************************************************* -// Listen for permission response -- (void) didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { - +- (BOOL)application:(UIApplication *)app + openURL:(NSURL *)url + options:(NSDictionary *)options { + return [self application:app + openURL:url + sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey] + annotation:options[UIApplicationOpenURLOptionsAnnotationKey]]; } -// Listen for FCM data messages that arrive as a remote notification -- (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo { - +- (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + sourceApplication:(NSString *)sourceApplication + annotation:(id)annotation { + return [self handleUrl:url]; } +- (BOOL)application:(UIApplication *)application +continueUserActivity:(NSUserActivity *)userActivity + restorationHandler:(void (^)(NSArray *))restorationHandler { + if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { + return [self handleUrl:userActivity.webpageURL]; + } + return NO; +} // ******************************************************* // ** Finish AppDelegate methods // ******************************************************* - // ******************************************************* // ** Start FIRInviteDelegate methods // ******************************************************* @@ -54,9 +68,13 @@ RCT_EXPORT_MODULE() // Listen for invitation response - (void)inviteFinishedWithInvitations:(NSArray *)invitationIds error:(NSError *)error { if (error) { - _invitationsRejecter(@"invites/invitation-error", @"Invitation failed to send", error); - } else if (invitationIds.count == 0) { - _invitationsRejecter(@"invites/invitation-cancelled", @"Invitation cancelled", nil); + if (error.code == -402) { + _invitationsRejecter(@"invites/invitation-cancelled", @"Invitation cancelled", nil); + } else if (error.code == -404) { + _invitationsRejecter(@"invites/invitation-error", @"User must be signed in with GoogleSignIn", nil); + } else { + _invitationsRejecter(@"invites/invitation-error", @"Invitation failed to send", error); + } } else { _invitationsResolver(invitationIds); } @@ -70,13 +88,40 @@ RCT_EXPORT_MODULE() // ** Start React Module methods ** RCT_EXPORT_METHOD(getInitialInvitation:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - // TODO - resolve(nil); + NSURL* url = nil; + if (self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey]) { + url = (NSURL*)self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey]; + } else if (self.bridge.launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey]) { + NSDictionary *dictionary = + self.bridge.launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey]; + if ([dictionary[UIApplicationLaunchOptionsUserActivityTypeKey] isEqual:NSUserActivityTypeBrowsingWeb]) { + NSUserActivity* userActivity = (NSUserActivity*) dictionary[@"UIApplicationLaunchOptionsUserActivityKey"]; + url = userActivity.webpageURL; + } + } + + if (url) { + [FIRInvites handleUniversalLink:url completion:^(FIRReceivedInvite * _Nullable receivedInvite, NSError * _Nullable error) { + if (error) { + NSLog(@"Failed to handle universal link: %@", [error localizedDescription]); + reject(@"invites/initial-invitation-error", @"Failed to handle invitation", error); + } else if (receivedInvite) { + resolve(@{ + @"deepLink": receivedInvite.deepLink, + @"invitationId": receivedInvite.inviteId, + }); + } else { + resolve(nil); + } + }]; + } else { + resolve(nil); + } } RCT_EXPORT_METHOD(sendInvitation:(NSDictionary *) invitation - resolve:(RCTPromiseResolveBlock) resolve - reject:(RCTPromiseRejectBlock) reject) { + resolve:(RCTPromiseResolveBlock) resolve + reject:(RCTPromiseRejectBlock) reject) { if (!invitation[@"message"]) { reject(@"invites/invalid-invitation", @"The supplied invitation is missing a 'message' field", nil); } @@ -111,10 +156,25 @@ RCT_EXPORT_METHOD(sendInvitation:(NSDictionary *) invitation _invitationsResolver = resolve; // Open the invitation dialog - [inviteDialog open]; + dispatch_async(dispatch_get_main_queue(), ^{ + [inviteDialog open]; + }); } // ** Start internals ** +- (BOOL)handleUrl:(NSURL *)url { + return [FIRInvites handleUniversalLink:url completion:^(FIRReceivedInvite * _Nullable receivedInvite, NSError * _Nullable error) { + if (error) { + NSLog(@"Failed to handle invitation: %@", [error localizedDescription]); + } else if (receivedInvite) { + [RNFirebaseUtil sendJSEvent:self name:LINKS_LINK_RECEIVED body:@{ + @"deepLink": receivedInvite.deepLink, + @"invitationId": receivedInvite.inviteId, + }]; + } + }]; +} + - (NSArray *)supportedEvents { return @[INVITES_INVITATION_RECEIVED]; From 878f928d8c92a2d3fbb51111f61208dc8cd1a367 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 22 Mar 2018 16:57:33 +0000 Subject: [PATCH 69/77] [links] Tidy up links to match messaging and invitations implementation more closely --- ios/RNFirebase/invites/RNFirebaseInvites.h | 3 - ios/RNFirebase/invites/RNFirebaseInvites.m | 11 +- ios/RNFirebase/links/RNFirebaseLinks.h | 18 +-- ios/RNFirebase/links/RNFirebaseLinks.m | 168 +++++++++------------ 4 files changed, 80 insertions(+), 120 deletions(-) diff --git a/ios/RNFirebase/invites/RNFirebaseInvites.h b/ios/RNFirebase/invites/RNFirebaseInvites.h index 5987dbf3..8438c722 100644 --- a/ios/RNFirebase/invites/RNFirebaseInvites.h +++ b/ios/RNFirebase/invites/RNFirebaseInvites.h @@ -15,9 +15,7 @@ @property _Nullable RCTPromiseResolveBlock invitationsResolver; - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options; - - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation; - - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *))restorationHandler; @end @@ -28,4 +26,3 @@ #endif #endif - diff --git a/ios/RNFirebase/invites/RNFirebaseInvites.m b/ios/RNFirebase/invites/RNFirebaseInvites.m index 76ad8b1b..d63f40e6 100644 --- a/ios/RNFirebase/invites/RNFirebaseInvites.m +++ b/ios/RNFirebase/invites/RNFirebaseInvites.m @@ -92,14 +92,13 @@ RCT_EXPORT_METHOD(getInitialInvitation:(RCTPromiseResolveBlock)resolve rejecter: if (self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey]) { url = (NSURL*)self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey]; } else if (self.bridge.launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey]) { - NSDictionary *dictionary = - self.bridge.launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey]; + NSDictionary *dictionary = self.bridge.launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey]; if ([dictionary[UIApplicationLaunchOptionsUserActivityTypeKey] isEqual:NSUserActivityTypeBrowsingWeb]) { NSUserActivity* userActivity = (NSUserActivity*) dictionary[@"UIApplicationLaunchOptionsUserActivityKey"]; url = userActivity.webpageURL; } } - + if (url) { [FIRInvites handleUniversalLink:url completion:^(FIRReceivedInvite * _Nullable receivedInvite, NSError * _Nullable error) { if (error) { @@ -132,7 +131,7 @@ RCT_EXPORT_METHOD(sendInvitation:(NSDictionary *) invitation [inviteDialog setInviteDelegate: self]; [inviteDialog setMessage:invitation[@"message"]]; [inviteDialog setTitle:invitation[@"title"]]; - + if (invitation[@"androidClientId"]) { FIRInvitesTargetApplication *targetApplication = [[FIRInvitesTargetApplication alloc] init]; targetApplication.androidClientID = invitation[@"androidClientId"]; @@ -150,11 +149,11 @@ RCT_EXPORT_METHOD(sendInvitation:(NSDictionary *) invitation if (invitation[@"deepLink"]) { [inviteDialog setDeepLink:invitation[@"deepLink"]]; } - + // Save the promise details for later _invitationsRejecter = reject; _invitationsResolver = resolve; - + // Open the invitation dialog dispatch_async(dispatch_get_main_queue(), ^{ [inviteDialog open]; diff --git a/ios/RNFirebase/links/RNFirebaseLinks.h b/ios/RNFirebase/links/RNFirebaseLinks.h index ac96b7e6..6846969a 100644 --- a/ios/RNFirebase/links/RNFirebaseLinks.h +++ b/ios/RNFirebase/links/RNFirebaseLinks.h @@ -7,21 +7,13 @@ #import #import -@interface RNFirebaseLinks : RCTEventEmitter { - -} -+ (BOOL)application:(UIApplication *)app - openURL:(NSURL *)url - options:(NSDictionary *)options; +@interface RNFirebaseLinks : RCTEventEmitter -+ (BOOL)application:(UIApplication *)application - openURL:(NSURL *)url - sourceApplication:(NSString *)sourceApplication - annotation:(id)annotation; ++ (_Nonnull instancetype)instance; -+ (BOOL)application:(UIApplication *)application -continueUserActivity:(NSUserActivity *)userActivity - restorationHandler:(void (^)(NSArray *))restorationHandler; +- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options; +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation; +- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *))restorationHandler; @end diff --git a/ios/RNFirebase/links/RNFirebaseLinks.m b/ios/RNFirebase/links/RNFirebaseLinks.m index 0251e8d6..b35fa14f 100644 --- a/ios/RNFirebase/links/RNFirebaseLinks.m +++ b/ios/RNFirebase/links/RNFirebaseLinks.m @@ -3,137 +3,80 @@ #if __has_include() #import #import "RNFirebaseEvents.h" - - -static void sendDynamicLink(NSURL *url, id sender) { - if (url) { - [[NSNotificationCenter defaultCenter] postNotificationName:LINKS_LINK_RECEIVED - object:sender - userInfo:@{@"url": url.absoluteString}]; - NSLog(@"sendDynamicLink Success: %@", url.absoluteString); - } -} +#import "RNFirebaseUtil.h" @implementation RNFirebaseLinks +static RNFirebaseLinks *theRNFirebaseLinks = nil; + ++ (nonnull instancetype)instance { + return theRNFirebaseLinks; +} + RCT_EXPORT_MODULE(); - (id)init { self = [super init]; if (self != nil) { NSLog(@"Setting up RNFirebaseLinks instance"); - [self initialiseLinks]; + // Set static instance for use from AppDelegate + theRNFirebaseLinks = self; } return self; } -- (void)initialiseLinks { - // Set up internal listener to send notification over bridge - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(sendDynamicLinkEvent:) - name:LINKS_LINK_RECEIVED - object:nil]; -} - - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } -+ (BOOL)application:(UIApplication *)app +// ******************************************************* +// ** Start AppDelegate methods +// ******************************************************* + +- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { - return [self handleLinkFromCustomSchemeURL:url]; + return [self application:app + openURL:url + sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey] + annotation:options[UIApplicationOpenURLOptionsAnnotationKey]]; } -+ (BOOL)application:(UIApplication *)application +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { - return [self handleLinkFromCustomSchemeURL:url]; -} - -+ (BOOL)handleLinkFromCustomSchemeURL:(NSURL *)url { - FIRDynamicLink *dynamicLink = - [[FIRDynamicLinks dynamicLinks] dynamicLinkFromCustomSchemeURL:url]; + FIRDynamicLink *dynamicLink = [[FIRDynamicLinks dynamicLinks] dynamicLinkFromCustomSchemeURL:url]; if (dynamicLink && dynamicLink.url) { - NSURL* dynamicLinkUrl = dynamicLink.url; - sendDynamicLink(dynamicLinkUrl, self); + NSURL* url = dynamicLink.url; + [RNFirebaseUtil sendJSEvent:self name:LINKS_LINK_RECEIVED body:url]; return YES; } return NO; } -+ (BOOL)application:(UIApplication *)application +- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *))restorationHandler { - BOOL handled = [[FIRDynamicLinks dynamicLinks] - handleUniversalLink:userActivity.webpageURL - completion:^(FIRDynamicLink * _Nullable dynamicLink, NSError * _Nullable error) { - if (error != nil){ - NSLog(@"Failed to handle universal link: %@", [error localizedDescription]); - } - else { - if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { - NSURL* url = dynamicLink ? dynamicLink.url : userActivity.webpageURL; - sendDynamicLink(url, self); - } - } - }]; - return handled; -} - -- (NSArray *)supportedEvents { - return @[LINKS_LINK_RECEIVED]; -} - -- (void)sendDynamicLinkEvent:(NSNotification *)notification { - [self sendEventWithName:LINKS_LINK_RECEIVED body:notification.userInfo[@"url"]]; -} - --(void)handleInitialLinkFromCustomSchemeURL:(NSURL*)url resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject { - FIRDynamicLink *dynamicLink = - [[FIRDynamicLinks dynamicLinks] dynamicLinkFromCustomSchemeURL:url]; - NSString* urlString = dynamicLink ? dynamicLink.url.absoluteString : (id)kCFNull; - NSLog(@"initial link is: %@", urlString); - resolve(urlString); -} - --(void)handleInitialLinkFromUniversalLinkURL:(NSDictionary *)userActivityDictionary resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject { - NSUserActivity* userActivity = (NSUserActivity*) userActivityDictionary[@"UIApplicationLaunchOptionsUserActivityKey"]; - if ([userActivityDictionary[UIApplicationLaunchOptionsUserActivityTypeKey] isEqual:NSUserActivityTypeBrowsingWeb]) - { - [[FIRDynamicLinks dynamicLinks] - handleUniversalLink:userActivity.webpageURL - completion:^(FIRDynamicLink * _Nullable dynamicLink, NSError * _Nullable error) { - if (error != nil){ - NSLog(@"Failed to handle universal link: %@", [error localizedDescription]); - reject(@"links/failure", @"Failed to handle universal link", error); - } - else { - NSString* urlString = dynamicLink ? dynamicLink.url.absoluteString : userActivity.webpageURL.absoluteString; - NSLog(@"initial link is: %@", urlString); - resolve(urlString); - } - }]; - } - else { - NSLog(@"no initial link"); - resolve((id)kCFNull); - } -} - -RCT_EXPORT_METHOD(getInitialLink:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - if (self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey]) { - NSURL* url = (NSURL*)self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey]; - [self handleInitialLinkFromCustomSchemeURL:url resolver:resolve rejecter:reject]; - - } else { - NSDictionary *userActivityDictionary = - self.bridge.launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey]; - [self handleInitialLinkFromUniversalLinkURL:userActivityDictionary resolver:resolve rejecter:reject]; + if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { + return [[FIRDynamicLinks dynamicLinks] + handleUniversalLink:userActivity.webpageURL + completion:^(FIRDynamicLink * _Nullable dynamicLink, NSError * _Nullable error) { + if (error != nil){ + NSLog(@"Failed to handle universal link: %@", [error localizedDescription]); + } else { + NSURL* url = dynamicLink ? dynamicLink.url : userActivity.webpageURL; + [RNFirebaseUtil sendJSEvent:self name:LINKS_LINK_RECEIVED body:url]; + } + }]; } + return NO; } +// ******************************************************* +// ** Finish AppDelegate methods +// ******************************************************* +// ** Start React Module methods ** RCT_EXPORT_METHOD(createDynamicLink: (NSDictionary *) metadata resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { @try { FIRDynamicLinkComponents *components = [self getDynamicLinkComponentsFromMetadata:metadata]; @@ -176,6 +119,32 @@ RCT_EXPORT_METHOD(createShortDynamicLink: (NSDictionary *) metadata resolver:(RC } } +RCT_EXPORT_METHOD(getInitialLink:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + if (self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey]) { + NSURL* url = (NSURL*)self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey]; + FIRDynamicLink *dynamicLink = [[FIRDynamicLinks dynamicLinks] dynamicLinkFromCustomSchemeURL:url]; + resolve(dynamicLink ? dynamicLink.url.absoluteString : nil); + } else if (self.bridge.launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey] + && [self.bridge.launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey][UIApplicationLaunchOptionsUserActivityTypeKey] isEqualToString:NSUserActivityTypeBrowsingWeb]) { + NSDictionary *dictionary = self.bridge.launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey]; + NSUserActivity* userActivity = (NSUserActivity*) dictionary[@"UIApplicationLaunchOptionsUserActivityKey"]; + [[FIRDynamicLinks dynamicLinks] handleUniversalLink:userActivity.webpageURL + completion:^(FIRDynamicLink * _Nullable dynamicLink, NSError * _Nullable error) { + if (error != nil){ + NSLog(@"Failed to handle universal link: %@", [error localizedDescription]); + reject(@"links/failure", @"Failed to handle universal link", error); + } else { + NSString* urlString = dynamicLink ? dynamicLink.url.absoluteString : userActivity.webpageURL.absoluteString; + NSLog(@"initial link is: %@", urlString); + resolve(urlString); + } + }]; + } else { + resolve(nil); + } +} + +// ** Start internals ** - (FIRDynamicLinkComponents *)getDynamicLinkComponentsFromMetadata:(NSDictionary *)metadata { @try { NSURL *link = [NSURL URLWithString:metadata[@"link"]]; @@ -272,8 +241,11 @@ RCT_EXPORT_METHOD(createShortDynamicLink: (NSDictionary *) metadata resolver:(RC } } -+ (BOOL)requiresMainQueueSetup -{ +- (NSArray *)supportedEvents { + return @[LINKS_LINK_RECEIVED]; +} + ++ (BOOL)requiresMainQueueSetup { return YES; } From 8e2d03cc5a573aa83c9da232d92d177069cf1274 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 22 Mar 2018 17:26:32 +0000 Subject: [PATCH 70/77] [links][invites] Remove iOS 8 specific methods --- ios/RNFirebase/invites/RNFirebaseInvites.h | 1 - ios/RNFirebase/invites/RNFirebaseInvites.m | 10 ---------- ios/RNFirebase/links/RNFirebaseLinks.h | 1 - ios/RNFirebase/links/RNFirebaseLinks.m | 10 ---------- 4 files changed, 22 deletions(-) diff --git a/ios/RNFirebase/invites/RNFirebaseInvites.h b/ios/RNFirebase/invites/RNFirebaseInvites.h index 8438c722..1554834e 100644 --- a/ios/RNFirebase/invites/RNFirebaseInvites.h +++ b/ios/RNFirebase/invites/RNFirebaseInvites.h @@ -15,7 +15,6 @@ @property _Nullable RCTPromiseResolveBlock invitationsResolver; - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options; -- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation; - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *))restorationHandler; @end diff --git a/ios/RNFirebase/invites/RNFirebaseInvites.m b/ios/RNFirebase/invites/RNFirebaseInvites.m index d63f40e6..ab761c44 100644 --- a/ios/RNFirebase/invites/RNFirebaseInvites.m +++ b/ios/RNFirebase/invites/RNFirebaseInvites.m @@ -36,16 +36,6 @@ RCT_EXPORT_MODULE() - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { - return [self application:app - openURL:url - sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey] - annotation:options[UIApplicationOpenURLOptionsAnnotationKey]]; -} - -- (BOOL)application:(UIApplication *)application - openURL:(NSURL *)url - sourceApplication:(NSString *)sourceApplication - annotation:(id)annotation { return [self handleUrl:url]; } diff --git a/ios/RNFirebase/links/RNFirebaseLinks.h b/ios/RNFirebase/links/RNFirebaseLinks.h index 6846969a..85f2209b 100644 --- a/ios/RNFirebase/links/RNFirebaseLinks.h +++ b/ios/RNFirebase/links/RNFirebaseLinks.h @@ -12,7 +12,6 @@ + (_Nonnull instancetype)instance; - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options; -- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation; - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *))restorationHandler; @end diff --git a/ios/RNFirebase/links/RNFirebaseLinks.m b/ios/RNFirebase/links/RNFirebaseLinks.m index b35fa14f..f5952269 100644 --- a/ios/RNFirebase/links/RNFirebaseLinks.m +++ b/ios/RNFirebase/links/RNFirebaseLinks.m @@ -36,16 +36,6 @@ RCT_EXPORT_MODULE(); - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { - return [self application:app - openURL:url - sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey] - annotation:options[UIApplicationOpenURLOptionsAnnotationKey]]; -} - -- (BOOL)application:(UIApplication *)application - openURL:(NSURL *)url - sourceApplication:(NSString *)sourceApplication - annotation:(id)annotation { FIRDynamicLink *dynamicLink = [[FIRDynamicLinks dynamicLinks] dynamicLinkFromCustomSchemeURL:url]; if (dynamicLink && dynamicLink.url) { NSURL* url = dynamicLink.url; From b06d68e39e7e82230f476b176440b6110324e5d0 Mon Sep 17 00:00:00 2001 From: Ifiok Jr Date: Fri, 23 Mar 2018 06:32:32 +0000 Subject: [PATCH 71/77] fix: types for request permission Currently returns void rather than a promise which causes TypeScript compilation to fail. --- lib/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/index.d.ts b/lib/index.d.ts index 29b931f1..205580ac 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -954,7 +954,7 @@ declare module "react-native-firebase" { * IOS * Requests app notification permissions in an Alert dialog. */ - requestPermissions(): void + requestPermissions(): Promise<{ granted: boolean }>; /** * Sets the badge number on the iOS app icon. From 1a5253997da148d5df45c245913ed0112ba116fb Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 23 Mar 2018 08:42:10 +0000 Subject: [PATCH 72/77] [database] Multi database check url format, and make logging static --- .../firebase/database/RNFirebaseDatabase.java | 26 +++++-------------- lib/modules/database/index.js | 2 +- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java index c9e5a41a..3880c66c 100644 --- a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java +++ b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java @@ -35,9 +35,9 @@ import io.invertase.firebase.Utils; public class RNFirebaseDatabase extends ReactContextBaseJavaModule { private static final String TAG = "RNFirebaseDatabase"; - private boolean enableLogging = false; + private static boolean enableLogging = false; private HashMap references = new HashMap<>(); - private HashMap loggingLevelSet = new HashMap<>(); + private static HashMap loggingLevelSet = new HashMap<>(); private SparseArray transactionHandlers = new SparseArray<>(); RNFirebaseDatabase(ReactApplicationContext reactContext) { @@ -54,7 +54,7 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { */ @ReactMethod public void goOnline(String appName, String dbURL) { - getDatabaseForAppAndSetLogging(appName, dbURL).goOnline(); + getDatabaseForApp(appName, dbURL).goOnline(); } /** @@ -62,7 +62,7 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { */ @ReactMethod public void goOffline(String appName, String dbURL) { - getDatabaseForAppAndSetLogging(appName, dbURL).goOffline(); + getDatabaseForApp(appName, dbURL).goOffline(); } /** @@ -71,7 +71,7 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { */ @ReactMethod public void setPersistence(String appName, String dbURL, Boolean state) { - getDatabaseForAppAndSetLogging(appName, dbURL).setPersistenceEnabled(state); + getDatabaseForApp(appName, dbURL).setPersistenceEnabled(state); } /** @@ -80,7 +80,7 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { */ @ReactMethod public void setPersistenceCacheSizeBytes(String appName, String dbURL, int size) { - getDatabaseForAppAndSetLogging(appName, dbURL).setPersistenceCacheSizeBytes((long) size); + getDatabaseForApp(appName, dbURL).setPersistenceCacheSizeBytes((long) size); } @@ -506,18 +506,6 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { firebaseDatabase = FirebaseDatabase.getInstance(firebaseApp); } - return firebaseDatabase; - } - - /** - * Get a database instance for a specific firebase app instance and enable/disable logging - * - * @param appName - * @param dbURL - * @return - */ - private FirebaseDatabase getDatabaseForAppAndSetLogging(String appName, String dbURL) { - FirebaseDatabase firebaseDatabase = RNFirebaseDatabase.getDatabaseForApp(appName, dbURL); Boolean logLevel = loggingLevelSet.get(firebaseDatabase.getApp().getName()); if (enableLogging && (logLevel == null || !logLevel)) { @@ -555,7 +543,7 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { * @return */ private DatabaseReference getReferenceForAppPath(String appName, String dbURL, String path) { - return getDatabaseForAppAndSetLogging(appName, dbURL).getReference(path); + return getDatabaseForApp(appName, dbURL).getReference(path); } /** diff --git a/lib/modules/database/index.js b/lib/modules/database/index.js index 66dc05fc..fb198588 100644 --- a/lib/modules/database/index.js +++ b/lib/modules/database/index.js @@ -34,7 +34,7 @@ export default class Database extends ModuleBase { let serviceUrl; if (typeof appOrUrl === 'string') { app = firebase.app(); - serviceUrl = appOrUrl; + serviceUrl = appOrUrl.endsWith('/') ? appOrUrl : `${appOrUrl}/`; } else { app = appOrUrl; serviceUrl = app.options.databaseURL; From 72a1d1d439ba65e19e3eb03af164ce04cf564c72 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 23 Mar 2018 10:33:17 +0000 Subject: [PATCH 73/77] [invites][links] Fix some iOS specific issues --- ios/RNFirebase/invites/RNFirebaseInvites.m | 21 ++++++++++++--------- ios/RNFirebase/links/RNFirebaseLinks.h | 2 ++ ios/RNFirebase/links/RNFirebaseLinks.m | 16 ++++++++++------ lib/modules/links/index.js | 14 ++------------ 4 files changed, 26 insertions(+), 27 deletions(-) diff --git a/ios/RNFirebase/invites/RNFirebaseInvites.m b/ios/RNFirebase/invites/RNFirebaseInvites.m index ab761c44..fc62ab67 100644 --- a/ios/RNFirebase/invites/RNFirebaseInvites.m +++ b/ios/RNFirebase/invites/RNFirebaseInvites.m @@ -2,6 +2,7 @@ #if __has_include() #import "RNFirebaseEvents.h" +#import "RNFirebaseLinks.h" #import "RNFirebaseUtil.h" #import @@ -88,7 +89,7 @@ RCT_EXPORT_METHOD(getInitialInvitation:(RCTPromiseResolveBlock)resolve rejecter: url = userActivity.webpageURL; } } - + if (url) { [FIRInvites handleUniversalLink:url completion:^(FIRReceivedInvite * _Nullable receivedInvite, NSError * _Nullable error) { if (error) { @@ -121,7 +122,7 @@ RCT_EXPORT_METHOD(sendInvitation:(NSDictionary *) invitation [inviteDialog setInviteDelegate: self]; [inviteDialog setMessage:invitation[@"message"]]; [inviteDialog setTitle:invitation[@"title"]]; - + if (invitation[@"androidClientId"]) { FIRInvitesTargetApplication *targetApplication = [[FIRInvitesTargetApplication alloc] init]; targetApplication.androidClientID = invitation[@"androidClientId"]; @@ -139,11 +140,11 @@ RCT_EXPORT_METHOD(sendInvitation:(NSDictionary *) invitation if (invitation[@"deepLink"]) { [inviteDialog setDeepLink:invitation[@"deepLink"]]; } - + // Save the promise details for later _invitationsRejecter = reject; _invitationsResolver = resolve; - + // Open the invitation dialog dispatch_async(dispatch_get_main_queue(), ^{ [inviteDialog open]; @@ -155,11 +156,13 @@ RCT_EXPORT_METHOD(sendInvitation:(NSDictionary *) invitation return [FIRInvites handleUniversalLink:url completion:^(FIRReceivedInvite * _Nullable receivedInvite, NSError * _Nullable error) { if (error) { NSLog(@"Failed to handle invitation: %@", [error localizedDescription]); - } else if (receivedInvite) { - [RNFirebaseUtil sendJSEvent:self name:LINKS_LINK_RECEIVED body:@{ - @"deepLink": receivedInvite.deepLink, - @"invitationId": receivedInvite.inviteId, - }]; + } else if (receivedInvite && receivedInvite.inviteId) { + [RNFirebaseUtil sendJSEvent:self name:INVITES_INVITATION_RECEIVED body:@{ + @"deepLink": receivedInvite.deepLink, + @"invitationId": receivedInvite.inviteId, + }]; + } else { + [[RNFirebaseLinks instance] sendLink:receivedInvite.deepLink]; } }]; } diff --git a/ios/RNFirebase/links/RNFirebaseLinks.h b/ios/RNFirebase/links/RNFirebaseLinks.h index 85f2209b..f2b68445 100644 --- a/ios/RNFirebase/links/RNFirebaseLinks.h +++ b/ios/RNFirebase/links/RNFirebaseLinks.h @@ -13,6 +13,7 @@ - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options; - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *))restorationHandler; +- (void)sendLink:(NSString *)link; @end @@ -22,3 +23,4 @@ #endif #endif + diff --git a/ios/RNFirebase/links/RNFirebaseLinks.m b/ios/RNFirebase/links/RNFirebaseLinks.m index f5952269..6c4d2f81 100644 --- a/ios/RNFirebase/links/RNFirebaseLinks.m +++ b/ios/RNFirebase/links/RNFirebaseLinks.m @@ -39,7 +39,7 @@ RCT_EXPORT_MODULE(); FIRDynamicLink *dynamicLink = [[FIRDynamicLinks dynamicLinks] dynamicLinkFromCustomSchemeURL:url]; if (dynamicLink && dynamicLink.url) { NSURL* url = dynamicLink.url; - [RNFirebaseUtil sendJSEvent:self name:LINKS_LINK_RECEIVED body:url]; + [RNFirebaseUtil sendJSEvent:self name:LINKS_LINK_RECEIVED body:url.absoluteString]; return YES; } return NO; @@ -56,7 +56,7 @@ continueUserActivity:(NSUserActivity *)userActivity NSLog(@"Failed to handle universal link: %@", [error localizedDescription]); } else { NSURL* url = dynamicLink ? dynamicLink.url : userActivity.webpageURL; - [RNFirebaseUtil sendJSEvent:self name:LINKS_LINK_RECEIVED body:url]; + [RNFirebaseUtil sendJSEvent:self name:LINKS_LINK_RECEIVED body:url.absoluteString]; } }]; } @@ -66,11 +66,15 @@ continueUserActivity:(NSUserActivity *)userActivity // ** Finish AppDelegate methods // ******************************************************* +- (void)sendLink:(NSString *)link { + [RNFirebaseUtil sendJSEvent:self name:LINKS_LINK_RECEIVED body:link]; +} + // ** Start React Module methods ** RCT_EXPORT_METHOD(createDynamicLink: (NSDictionary *) metadata resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { @try { FIRDynamicLinkComponents *components = [self getDynamicLinkComponentsFromMetadata:metadata]; - + if (components == nil) { reject(@"links/failure", @"Failed to create Dynamic Link", nil); } else { @@ -140,11 +144,11 @@ RCT_EXPORT_METHOD(getInitialLink:(RCTPromiseResolveBlock)resolve rejecter:(RCTPr NSURL *link = [NSURL URLWithString:metadata[@"link"]]; FIRDynamicLinkComponents *components = [FIRDynamicLinkComponents componentsWithLink:link domain:metadata[@"dynamicLinkDomain"]]; - + [self setAndroidParameters:metadata components:components]; [self setIosParameters:metadata components:components]; [self setSocialMetaTagParameters:metadata components:components]; - + return components; } @catch(NSException * e) { @@ -159,7 +163,7 @@ RCT_EXPORT_METHOD(getInitialLink:(RCTPromiseResolveBlock)resolve rejecter:(RCTPr if (androidParametersDict) { FIRDynamicLinkAndroidParameters *androidParams = [FIRDynamicLinkAndroidParameters parametersWithPackageName: androidParametersDict[@"androidPackageName"]]; - + if (androidParametersDict[@"androidFallbackLink"]) { androidParams.fallbackURL = [NSURL URLWithString:androidParametersDict[@"androidFallbackLink"]]; } diff --git a/lib/modules/links/index.js b/lib/modules/links/index.js index b653d0ee..872eb0ce 100644 --- a/lib/modules/links/index.js +++ b/lib/modules/links/index.js @@ -10,11 +10,7 @@ import { getNativeModule } from '../../utils/native'; import type App from '../core/app'; -const EVENT_TYPE = { - Link: 'links_link_received', -}; - -const NATIVE_EVENTS = [EVENT_TYPE.Link]; +const NATIVE_EVENTS = ['links_link_received']; export const MODULE_NAME = 'RNFirebaseLinks'; export const NAMESPACE = 'links'; @@ -94,10 +90,6 @@ export default class Links extends ModuleBase { ); } - get EVENT_TYPE(): Object { - return EVENT_TYPE; - } - /** * Create long Dynamic Link from parameters * @param parameters @@ -153,6 +145,4 @@ export default class Links extends ModuleBase { } } -export const statics = { - EVENT_TYPE, -}; +export const statics = {}; From e430a4f1327023a30b4ca0112470032e37718ea4 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 23 Mar 2018 11:38:59 +0000 Subject: [PATCH 74/77] [invites] Tweaks --- .../java/io/invertase/firebase/links/RNFirebaseLinks.java | 2 +- lib/modules/invites/Invitation.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/io/invertase/firebase/links/RNFirebaseLinks.java b/android/src/main/java/io/invertase/firebase/links/RNFirebaseLinks.java index 7734513f..746dcf9b 100644 --- a/android/src/main/java/io/invertase/firebase/links/RNFirebaseLinks.java +++ b/android/src/main/java/io/invertase/firebase/links/RNFirebaseLinks.java @@ -175,7 +175,7 @@ public class RNFirebaseLinks extends ReactContextBaseJavaModule implements Activ // Looks at the internals of the link data to detect whether it's an invitation or not private boolean isInvitation(PendingDynamicLinkData pendingDynamicLinkData) { - return pendingDynamicLinkData.zzcbj().getString("com.google.firebase.appinvite.fdl.extension.InvitationId") != null; + return FirebaseAppInvite.getInvitation(pendingDynamicLinkData) != null; } private DynamicLink.Builder getDynamicLinkBuilderFromMap(final Map metaData) { diff --git a/lib/modules/invites/Invitation.js b/lib/modules/invites/Invitation.js index 66a82f61..04c0b0aa 100644 --- a/lib/modules/invites/Invitation.js +++ b/lib/modules/invites/Invitation.js @@ -24,6 +24,10 @@ export default class Invitation { this._title = title; } + get android(): AndroidInvitation { + return this._android; + } + /** * * @param androidClientId From 8918139fd09e392e325181b784985990441593c9 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 23 Mar 2018 11:46:48 +0000 Subject: [PATCH 75/77] [invites] Exclude pure links from getInitialInvitation --- ios/RNFirebase/invites/RNFirebaseInvites.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ios/RNFirebase/invites/RNFirebaseInvites.m b/ios/RNFirebase/invites/RNFirebaseInvites.m index fc62ab67..98f47561 100644 --- a/ios/RNFirebase/invites/RNFirebaseInvites.m +++ b/ios/RNFirebase/invites/RNFirebaseInvites.m @@ -95,7 +95,7 @@ RCT_EXPORT_METHOD(getInitialInvitation:(RCTPromiseResolveBlock)resolve rejecter: if (error) { NSLog(@"Failed to handle universal link: %@", [error localizedDescription]); reject(@"invites/initial-invitation-error", @"Failed to handle invitation", error); - } else if (receivedInvite) { + } else if (receivedInvite && receivedInvite.inviteId) { resolve(@{ @"deepLink": receivedInvite.deepLink, @"invitationId": receivedInvite.inviteId, @@ -183,3 +183,4 @@ RCT_EXPORT_METHOD(sendInvitation:(NSDictionary *) invitation @implementation RNFirebaseInvites @end #endif + From 7f90e485c87c4dc89ab204cc5d5dfd732ed7a0d2 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 23 Mar 2018 13:24:31 +0000 Subject: [PATCH 76/77] [links] Refactor links to use builder classes --- .../firebase/links/RNFirebaseLinks.java | 204 ++++++++++-------- ios/RNFirebase/links/RNFirebaseLinks.m | 191 +++++++++------- lib/modules/instanceid/index.js | 1 + lib/modules/invites/index.js | 1 + lib/modules/links/AnalyticsParameters.js | 79 +++++++ lib/modules/links/AndroidParameters.js | 60 ++++++ lib/modules/links/DynamicLink.js | 79 +++++++ lib/modules/links/IOSParameters.js | 114 ++++++++++ lib/modules/links/ITunesParameters.js | 55 +++++ lib/modules/links/NavigationParameters.js | 31 +++ lib/modules/links/SocialParameters.js | 55 +++++ lib/modules/links/index.js | 70 +----- lib/modules/links/types.js | 53 +++++ lib/modules/notifications/index.js | 1 + 14 files changed, 766 insertions(+), 228 deletions(-) create mode 100644 lib/modules/links/AnalyticsParameters.js create mode 100644 lib/modules/links/AndroidParameters.js create mode 100644 lib/modules/links/DynamicLink.js create mode 100644 lib/modules/links/IOSParameters.js create mode 100644 lib/modules/links/ITunesParameters.js create mode 100644 lib/modules/links/NavigationParameters.js create mode 100644 lib/modules/links/SocialParameters.js create mode 100644 lib/modules/links/types.js diff --git a/android/src/main/java/io/invertase/firebase/links/RNFirebaseLinks.java b/android/src/main/java/io/invertase/firebase/links/RNFirebaseLinks.java index 746dcf9b..9c308168 100644 --- a/android/src/main/java/io/invertase/firebase/links/RNFirebaseLinks.java +++ b/android/src/main/java/io/invertase/firebase/links/RNFirebaseLinks.java @@ -6,8 +6,6 @@ import android.net.Uri; import android.support.annotation.NonNull; import android.util.Log; -import java.util.Map; - import com.facebook.react.bridge.ActivityEventListener; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.Promise; @@ -16,7 +14,6 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.WritableMap; import com.google.firebase.appinvite.FirebaseAppInvite; import com.google.firebase.dynamiclinks.DynamicLink; import com.google.firebase.dynamiclinks.FirebaseDynamicLinks; @@ -47,15 +44,12 @@ public class RNFirebaseLinks extends ReactContextBaseJavaModule implements Activ } @ReactMethod - public void createDynamicLink(final ReadableMap parameters, final Promise promise) { + public void createDynamicLink(final ReadableMap linkData, final Promise promise) { try { - Map metaData = Utils.recursivelyDeconstructReadableMap(parameters); - - DynamicLink.Builder builder = getDynamicLinkBuilderFromMap(metaData); - Uri link = builder.buildDynamicLink().getUri(); - - Log.d(TAG, "created dynamic link: " + link.toString()); - promise.resolve(link.toString()); + DynamicLink.Builder builder = getDynamicLinkBuilder(linkData); + String link = builder.buildDynamicLink().getUri().toString(); + Log.d(TAG, "created dynamic link: " + link); + promise.resolve(link); } catch (Exception ex) { Log.e(TAG, "create dynamic link failure " + ex.getMessage()); promise.reject("links/failure", ex.getMessage(), ex); @@ -63,26 +57,31 @@ public class RNFirebaseLinks extends ReactContextBaseJavaModule implements Activ } @ReactMethod - public void createShortDynamicLink(final ReadableMap parameters, final Promise promise) { + public void createShortDynamicLink(final ReadableMap linkData, final String type, final Promise promise) { try { - Map metaData = Utils.recursivelyDeconstructReadableMap(parameters); + DynamicLink.Builder builder = getDynamicLinkBuilder(linkData); + Task shortLinkTask; + if ("SHORT".equals(type)) { + shortLinkTask = builder.buildShortDynamicLink(ShortDynamicLink.Suffix.SHORT); + } else if ("UNGUESSABLE".equals(type)) { + shortLinkTask = builder.buildShortDynamicLink(ShortDynamicLink.Suffix.UNGUESSABLE); + } else { + shortLinkTask = builder.buildShortDynamicLink(); + } - DynamicLink.Builder builder = getDynamicLinkBuilderFromMap(metaData); - - Task shortLinkTask = getShortDynamicLinkTask(builder, metaData) - .addOnCompleteListener(new OnCompleteListener() { + shortLinkTask.addOnCompleteListener(new OnCompleteListener() { @Override public void onComplete(@NonNull Task task) { - if (task.isSuccessful()) { - Uri shortLink = task.getResult().getShortLink(); - Log.d(TAG, "created short dynamic link: " + shortLink.toString()); - promise.resolve(shortLink.toString()); - } else { - Log.e(TAG, "create short dynamic link failure " + task.getException().getMessage()); - promise.reject("links/failure", task.getException().getMessage(), task.getException()); - } - } - }); + if (task.isSuccessful()) { + String shortLink = task.getResult().getShortLink().toString(); + Log.d(TAG, "created short dynamic link: " + shortLink); + promise.resolve(shortLink); + } else { + Log.e(TAG, "create short dynamic link failure " + task.getException().getMessage()); + promise.reject("links/failure", task.getException().getMessage(), task.getException()); + } + } + }); } catch (Exception ex) { Log.e(TAG, "create short dynamic link failure " + ex.getMessage()); promise.reject("links/failure", ex.getMessage(), ex); @@ -178,95 +177,122 @@ public class RNFirebaseLinks extends ReactContextBaseJavaModule implements Activ return FirebaseAppInvite.getInvitation(pendingDynamicLinkData) != null; } - private DynamicLink.Builder getDynamicLinkBuilderFromMap(final Map metaData) { - DynamicLink.Builder parametersBuilder = FirebaseDynamicLinks.getInstance().createDynamicLink(); + private DynamicLink.Builder getDynamicLinkBuilder(final ReadableMap linkData) { + DynamicLink.Builder builder = FirebaseDynamicLinks.getInstance().createDynamicLink(); try { - parametersBuilder.setLink(Uri.parse((String) metaData.get("link"))); - parametersBuilder.setDynamicLinkDomain((String) metaData.get("dynamicLinkDomain")); - setAndroidParameters(metaData, parametersBuilder); - setIosParameters(metaData, parametersBuilder); - setSocialMetaTagParameters(metaData, parametersBuilder); + builder.setLink(Uri.parse(linkData.getString("link"))); + builder.setDynamicLinkDomain(linkData.getString("dynamicLinkDomain")); + setAnalyticsParameters(linkData.getMap("analytics"), builder); + setAndroidParameters(linkData.getMap("android"), builder); + setIosParameters(linkData.getMap("ios"), builder); + setITunesParameters(linkData.getMap("itunes"), builder); + setNavigationParameters(linkData.getMap("navigation"), builder); + setSocialParameters(linkData.getMap("social"), builder); } catch (Exception e) { Log.e(TAG, "error while building parameters " + e.getMessage()); throw e; } - return parametersBuilder; + return builder; } - private Task getShortDynamicLinkTask(final DynamicLink.Builder builder, final Map metaData) { - Map suffix = (Map) metaData.get("suffix"); - if (suffix != null) { - String option = (String) suffix.get("option"); - if ("SHORT".equals(option)) { - return builder.buildShortDynamicLink(ShortDynamicLink.Suffix.SHORT); - } else if ("UNGUESSABLE".equals(option)) { - return builder.buildShortDynamicLink(ShortDynamicLink.Suffix.UNGUESSABLE); - } + private void setAnalyticsParameters(final ReadableMap analyticsData, final DynamicLink.Builder builder) { + DynamicLink.GoogleAnalyticsParameters.Builder analyticsParameters = new DynamicLink.GoogleAnalyticsParameters.Builder(); + + if (analyticsData.hasKey("campaign")) { + analyticsParameters.setCampaign(analyticsData.getString("campaign")); } - return builder.buildShortDynamicLink(); + if (analyticsData.hasKey("content")) { + analyticsParameters.setContent(analyticsData.getString("content")); + } + if (analyticsData.hasKey("medium")) { + analyticsParameters.setMedium(analyticsData.getString("medium")); + } + if (analyticsData.hasKey("source")) { + analyticsParameters.setSource(analyticsData.getString("source")); + } + if (analyticsData.hasKey("term")) { + analyticsParameters.setTerm(analyticsData.getString("term")); + } + builder.setGoogleAnalyticsParameters(analyticsParameters.build()); } + private void setAndroidParameters(final ReadableMap androidData, final DynamicLink.Builder builder) { + if (androidData.hasKey("packageName")) { + DynamicLink.AndroidParameters.Builder androidParameters = new DynamicLink.AndroidParameters.Builder(androidData.getString("packageName")); - private void setAndroidParameters(final Map metaData, final DynamicLink.Builder parametersBuilder) { - Map androidParameters = (Map) metaData.get("androidInfo"); - if (androidParameters != null) { - DynamicLink.AndroidParameters.Builder androidParametersBuilder = - new DynamicLink.AndroidParameters.Builder((String) androidParameters.get("androidPackageName")); - - if (androidParameters.containsKey("androidFallbackLink")) { - androidParametersBuilder.setFallbackUrl(Uri.parse((String) androidParameters.get("androidFallbackLink"))); + if (androidData.hasKey("fallbackUrl")) { + androidParameters.setFallbackUrl(Uri.parse(androidData.getString("fallbackUrl"))); } - if (androidParameters.containsKey("androidMinPackageVersionCode")) { - androidParametersBuilder.setMinimumVersion(Integer.parseInt((String) androidParameters.get("androidMinPackageVersionCode"))); + if (androidData.hasKey("minimumVersion")) { + androidParameters.setMinimumVersion(Integer.parseInt(androidData.getString("minimumVersion"))); } - parametersBuilder.setAndroidParameters(androidParametersBuilder.build()); + builder.setAndroidParameters(androidParameters.build()); } } - private void setIosParameters(final Map metaData, final DynamicLink.Builder parametersBuilder) { - Map iosParameters = (Map) metaData.get("iosInfo"); - if (iosParameters != null) { - DynamicLink.IosParameters.Builder iosParametersBuilder = - new DynamicLink.IosParameters.Builder((String) iosParameters.get("iosBundleId")); + private void setIosParameters(final ReadableMap iosData, final DynamicLink.Builder builder) { + if (iosData.hasKey("bundleId")) { + DynamicLink.IosParameters.Builder iosParameters = + new DynamicLink.IosParameters.Builder(iosData.getString("bundleId")); - if (iosParameters.containsKey("iosAppStoreId")) { - iosParametersBuilder.setAppStoreId((String) iosParameters.get("iosAppStoreId")); + if (iosData.hasKey("appStoreId")) { + iosParameters.setAppStoreId(iosData.getString("appStoreId")); } - if (iosParameters.containsKey("iosCustomScheme")) { - iosParametersBuilder.setCustomScheme((String) iosParameters.get("iosCustomScheme")); + if (iosData.hasKey("customScheme")) { + iosParameters.setCustomScheme(iosData.getString("customScheme")); } - if (iosParameters.containsKey("iosFallbackLink")) { - iosParametersBuilder.setFallbackUrl(Uri.parse((String) iosParameters.get("iosFallbackLink"))); + if (iosData.hasKey("fallbackUrl")) { + iosParameters.setFallbackUrl(Uri.parse(iosData.getString("fallbackUrl"))); } - if (iosParameters.containsKey("iosIpadBundleId")) { - iosParametersBuilder.setIpadBundleId((String) iosParameters.get("iosIpadBundleId")); + if (iosData.hasKey("iPadBundleId")) { + iosParameters.setIpadBundleId(iosData.getString("iPadBundleId")); } - if (iosParameters.containsKey("iosIpadFallbackLink")) { - iosParametersBuilder.setIpadFallbackUrl(Uri.parse((String) iosParameters.get("iosIpadFallbackLink"))); + if (iosData.hasKey("iPadFallbackUrl")) { + iosParameters.setIpadFallbackUrl(Uri.parse(iosData.getString("iPadFallbackUrl"))); } - if (iosParameters.containsKey("iosMinPackageVersionCode")) { - iosParametersBuilder.setMinimumVersion((String) iosParameters.get("iosMinPackageVersionCode")); + if (iosData.hasKey("minimumVersion")) { + iosParameters.setMinimumVersion(iosData.getString("minimumVersion")); } - parametersBuilder.setIosParameters(iosParametersBuilder.build()); + builder.setIosParameters(iosParameters.build()); } } - private void setSocialMetaTagParameters(final Map metaData, final DynamicLink.Builder parametersBuilder) { - Map socialMetaTagParameters = (Map) metaData.get("socialMetaTagInfo"); - if (socialMetaTagParameters != null) { - DynamicLink.SocialMetaTagParameters.Builder socialMetaTagParametersBuilder = - new DynamicLink.SocialMetaTagParameters.Builder(); + private void setITunesParameters(final ReadableMap itunesData, final DynamicLink.Builder builder) { + DynamicLink.ItunesConnectAnalyticsParameters.Builder itunesParameters = new DynamicLink.ItunesConnectAnalyticsParameters.Builder(); - if (socialMetaTagParameters.containsKey("socialDescription")) { - socialMetaTagParametersBuilder.setDescription((String) socialMetaTagParameters.get("socialDescription")); - } - if (socialMetaTagParameters.containsKey("socialImageLink")) { - socialMetaTagParametersBuilder.setImageUrl(Uri.parse((String) socialMetaTagParameters.get("socialImageLink"))); - } - if (socialMetaTagParameters.containsKey("socialTitle")) { - socialMetaTagParametersBuilder.setTitle((String) socialMetaTagParameters.get("socialTitle")); - } - parametersBuilder.setSocialMetaTagParameters(socialMetaTagParametersBuilder.build()); + if (itunesData.hasKey("affiliateToken")) { + itunesParameters.setAffiliateToken(itunesData.getString("affiliateToken")); } + if (itunesData.hasKey("campaignToken")) { + itunesParameters.setCampaignToken(itunesData.getString("campaignToken")); + } + if (itunesData.hasKey("providerToken")) { + itunesParameters.setProviderToken(itunesData.getString("providerToken")); + } + builder.setItunesConnectAnalyticsParameters(itunesParameters.build()); + } + + private void setNavigationParameters(final ReadableMap navigationData, final DynamicLink.Builder builder) { + DynamicLink.NavigationInfoParameters.Builder navigationParameters = new DynamicLink.NavigationInfoParameters.Builder(); + + if (navigationData.hasKey("forcedRedirectEnabled")) { + navigationParameters.setForcedRedirectEnabled(navigationData.getBoolean("forcedRedirectEnabled")); + } + builder.setNavigationInfoParameters(navigationParameters.build()); + } + + private void setSocialParameters(final ReadableMap socialData, final DynamicLink.Builder builder) { + DynamicLink.SocialMetaTagParameters.Builder socialParameters = new DynamicLink.SocialMetaTagParameters.Builder(); + + if (socialData.hasKey("descriptionText")) { + socialParameters.setDescription(socialData.getString("descriptionText")); + } + if (socialData.hasKey("imageUrl")) { + socialParameters.setImageUrl(Uri.parse(socialData.getString("imageUrl"))); + } + if (socialData.hasKey("title")) { + socialParameters.setTitle(socialData.getString("title")); + } + builder.setSocialMetaTagParameters(socialParameters.build()); } } diff --git a/ios/RNFirebase/links/RNFirebaseLinks.m b/ios/RNFirebase/links/RNFirebaseLinks.m index 6c4d2f81..63ce16c8 100644 --- a/ios/RNFirebase/links/RNFirebaseLinks.m +++ b/ios/RNFirebase/links/RNFirebaseLinks.m @@ -71,16 +71,18 @@ continueUserActivity:(NSUserActivity *)userActivity } // ** Start React Module methods ** -RCT_EXPORT_METHOD(createDynamicLink: (NSDictionary *) metadata resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { +RCT_EXPORT_METHOD(createDynamicLink:(NSDictionary *)linkData + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { @try { - FIRDynamicLinkComponents *components = [self getDynamicLinkComponentsFromMetadata:metadata]; + FIRDynamicLinkComponents *dynamicLink = [self buildDynamicLink:linkData]; - if (components == nil) { + if (dynamicLink == nil) { reject(@"links/failure", @"Failed to create Dynamic Link", nil); } else { - NSURL *longLink = components.url; - NSLog(@"created long dynamic link: %@", longLink.absoluteString); - resolve(longLink.absoluteString); + NSString *longLink = dynamicLink.url.absoluteString; + NSLog(@"created long dynamic link: %@", longLink); + resolve(longLink); } } @catch(NSException * e) { @@ -89,21 +91,29 @@ RCT_EXPORT_METHOD(createDynamicLink: (NSDictionary *) metadata resolver:(RCTProm } } -RCT_EXPORT_METHOD(createShortDynamicLink: (NSDictionary *) metadata resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { +RCT_EXPORT_METHOD(createShortDynamicLink:(NSDictionary *)linkData + type:(NSString *)type + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { @try { - FIRDynamicLinkComponents *components = [self getDynamicLinkComponentsFromMetadata:metadata]; - [self setSuffixParameters:metadata components:components]; - [components shortenWithCompletion:^(NSURL *_Nullable shortURL, - NSArray *_Nullable warnings, - NSError *_Nullable error) { + FIRDynamicLinkComponents *components = [self buildDynamicLink:linkData]; + if (type) { + FIRDynamicLinkComponentsOptions *options = [FIRDynamicLinkComponentsOptions options]; + if ([type isEqual: @"SHORT"]) { + options.pathLength = FIRShortDynamicLinkPathLengthShort; + } else if ([type isEqual: @"UNGUESSABLE"]) { + options.pathLength = FIRShortDynamicLinkPathLengthUnguessable; + } + components.options = options; + } + [components shortenWithCompletion:^(NSURL *_Nullable shortURL, NSArray *_Nullable warnings, NSError *_Nullable error) { if (error) { NSLog(@"create short dynamic link failure %@", [error localizedDescription]); reject(@"links/failure", @"Failed to create Short Dynamic Link", error); - } - else { - NSURL *shortLink = shortURL; - NSLog(@"created short dynamic link: %@", shortLink.absoluteString); - resolve(shortLink.absoluteString); + } else { + NSString *shortLink = shortURL.absoluteString; + NSLog(@"created short dynamic link: %@", shortLink); + resolve(shortLink); } }]; } @@ -139,15 +149,17 @@ RCT_EXPORT_METHOD(getInitialLink:(RCTPromiseResolveBlock)resolve rejecter:(RCTPr } // ** Start internals ** -- (FIRDynamicLinkComponents *)getDynamicLinkComponentsFromMetadata:(NSDictionary *)metadata { +- (FIRDynamicLinkComponents *)buildDynamicLink:(NSDictionary *)linkData { @try { - NSURL *link = [NSURL URLWithString:metadata[@"link"]]; - FIRDynamicLinkComponents *components = - [FIRDynamicLinkComponents componentsWithLink:link domain:metadata[@"dynamicLinkDomain"]]; + NSURL *link = [NSURL URLWithString:linkData[@"link"]]; + FIRDynamicLinkComponents *components = [FIRDynamicLinkComponents componentsWithLink:link domain:linkData[@"dynamicLinkDomain"]]; - [self setAndroidParameters:metadata components:components]; - [self setIosParameters:metadata components:components]; - [self setSocialMetaTagParameters:metadata components:components]; + [self setAnalyticsParameters:linkData[@"analytics"] components:components]; + [self setAndroidParameters:linkData[@"android"] components:components]; + [self setIosParameters:linkData[@"ios"] components:components]; + [self setITunesParameters:linkData[@"itunes"] components:components]; + [self setNavigationParameters:linkData[@"navigation"] components:components]; + [self setSocialParameters:linkData[@"social"] components:components]; return components; } @@ -157,82 +169,106 @@ RCT_EXPORT_METHOD(getInitialLink:(RCTPromiseResolveBlock)resolve rejecter:(RCTPr } } -- (void)setAndroidParameters:(NSDictionary *)metadata +- (void)setAnalyticsParameters:(NSDictionary *)analyticsData + components:(FIRDynamicLinkComponents *)components { + FIRDynamicLinkGoogleAnalyticsParameters *analyticsParams = [FIRDynamicLinkGoogleAnalyticsParameters parameters]; + + if (analyticsData[@"campaign"]) { + analyticsParams.campaign = analyticsData[@"campaign"]; + } + if (analyticsData[@"content"]) { + analyticsParams.content = analyticsData[@"content"]; + } + if (analyticsData[@"medium"]) { + analyticsParams.medium = analyticsData[@"medium"]; + } + if (analyticsData[@"source"]) { + analyticsParams.source = analyticsData[@"source"]; + } + if (analyticsData[@"term"]) { + analyticsParams.term = analyticsData[@"term"]; + } + components.analyticsParameters = analyticsParams; +} + +- (void)setAndroidParameters:(NSDictionary *)androidData components:(FIRDynamicLinkComponents *)components { - NSDictionary *androidParametersDict = metadata[@"androidInfo"]; - if (androidParametersDict) { - FIRDynamicLinkAndroidParameters *androidParams = [FIRDynamicLinkAndroidParameters - parametersWithPackageName: androidParametersDict[@"androidPackageName"]]; + if (androidData[@"packageName"]) { + FIRDynamicLinkAndroidParameters *androidParams = [FIRDynamicLinkAndroidParameters parametersWithPackageName: androidData[@"packageName"]]; - if (androidParametersDict[@"androidFallbackLink"]) { - androidParams.fallbackURL = [NSURL URLWithString:androidParametersDict[@"androidFallbackLink"]]; + if (androidData[@"fallbackUrl"]) { + androidParams.fallbackURL = [NSURL URLWithString:androidData[@"fallbackUrl"]]; } - if (androidParametersDict[@"androidMinPackageVersionCode"]) { - androidParams.minimumVersion = [androidParametersDict[@"androidMinPackageVersionCode"] integerValue]; + if (androidData[@"minimumVersion"]) { + androidParams.minimumVersion = [androidData[@"minimumVersion"] integerValue]; } components.androidParameters = androidParams; } } -- (void)setIosParameters:(NSDictionary *)metadata +- (void)setIosParameters:(NSDictionary *)iosData components:(FIRDynamicLinkComponents *)components { - NSDictionary *iosParametersDict = metadata[@"iosInfo"]; - if (iosParametersDict) { - FIRDynamicLinkIOSParameters *iOSParams = [FIRDynamicLinkIOSParameters - parametersWithBundleID:iosParametersDict[@"iosBundleId"]]; - if (iosParametersDict[@"iosAppStoreId"]) { - iOSParams.appStoreID = iosParametersDict[@"iosAppStoreId"]; + if (iosData[@"bundleId"]) { + FIRDynamicLinkIOSParameters *iOSParams = [FIRDynamicLinkIOSParameters parametersWithBundleID:iosData[@"bundleId"]]; + if (iosData[@"appStoreId"]) { + iOSParams.appStoreID = iosData[@"appStoreId"]; } - if (iosParametersDict[@"iosCustomScheme"]) { - iOSParams.customScheme = iosParametersDict[@"iosCustomScheme"]; + if (iosData[@"customScheme"]) { + iOSParams.customScheme = iosData[@"customScheme"]; } - if (iosParametersDict[@"iosFallbackLink"]) { - iOSParams.fallbackURL = [NSURL URLWithString:iosParametersDict[@"iosFallbackLink"]]; + if (iosData[@"fallbackUrl"]) { + iOSParams.fallbackURL = [NSURL URLWithString:iosData[@"fallbackUrl"]]; } - if (iosParametersDict[@"iosIpadBundleId"]) { - iOSParams.iPadBundleID = iosParametersDict[@"iosIpadBundleId"]; + if (iosData[@"iPadBundleId"]) { + iOSParams.iPadBundleID = iosData[@"iPadBundleId"]; } - if (iosParametersDict[@"iosIpadFallbackLink"]) { - iOSParams.iPadFallbackURL = [NSURL URLWithString:iosParametersDict[@"iosIpadFallbackLink"]]; + if (iosData[@"iPadFallbackUrl"]) { + iOSParams.iPadFallbackURL = [NSURL URLWithString:iosData[@"iPadFallbackUrl"]]; } - if (iosParametersDict[@"iosMinPackageVersionCode"]) { - iOSParams.minimumAppVersion = iosParametersDict[@"iosMinPackageVersionCode"]; + if (iosData[@"minimumVersion"]) { + iOSParams.minimumAppVersion = iosData[@"minimumVersion"]; } components.iOSParameters = iOSParams; } } -- (void)setSocialMetaTagParameters:(NSDictionary *)metadata - components:(FIRDynamicLinkComponents *)components { - NSDictionary *socialParamsDict = metadata[@"socialMetaTagInfo"]; - if (socialParamsDict) { - FIRDynamicLinkSocialMetaTagParameters *socialParams = [FIRDynamicLinkSocialMetaTagParameters parameters]; - if (socialParamsDict[@"socialTitle"]) { - socialParams.title = socialParamsDict[@"socialTitle"]; - } - if (socialParamsDict[@"socialDescription"]) { - socialParams.descriptionText = socialParamsDict[@"socialDescription"]; - } - if (socialParamsDict[@"socialImageLink"]) { - socialParams.imageURL = [NSURL URLWithString:socialParamsDict[@"socialImageLink"]]; - } - components.socialMetaTagParameters = socialParams; +- (void)setITunesParameters:(NSDictionary *)itunesData + components:(FIRDynamicLinkComponents *)components { + FIRDynamicLinkItunesConnectAnalyticsParameters *itunesParams = [FIRDynamicLinkItunesConnectAnalyticsParameters parameters]; + if (itunesData[@"affiliateToken"]) { + itunesParams.affiliateToken = itunesData[@"affiliateToken"]; } + if (itunesData[@"campaignToken"]) { + itunesParams.campaignToken = itunesData[@"campaignToken"]; + } + if (itunesData[@"providerToken"]) { + itunesParams.providerToken = itunesData[@"providerToken"]; + } + components.iTunesConnectParameters = itunesParams; } -- (void)setSuffixParameters:(NSDictionary *)metadata - components:(FIRDynamicLinkComponents *)components { - NSDictionary *suffixParametersDict = metadata[@"suffix"]; - if (suffixParametersDict) { - FIRDynamicLinkComponentsOptions *options = [FIRDynamicLinkComponentsOptions options]; - if ([suffixParametersDict[@"option"] isEqual: @"SHORT"]) { - options.pathLength = FIRShortDynamicLinkPathLengthShort; - } - else if ([suffixParametersDict[@"option"] isEqual: @"UNGUESSABLE"]) { - options.pathLength = FIRShortDynamicLinkPathLengthUnguessable; - } - components.options = options; +- (void)setNavigationParameters:(NSDictionary *)navigationData + components:(FIRDynamicLinkComponents *)components { + FIRDynamicLinkNavigationInfoParameters *navigationParams = [FIRDynamicLinkNavigationInfoParameters parameters]; + if (navigationData[@"forcedRedirectEnabled"]) { + navigationParams.forcedRedirectEnabled = navigationData[@"forcedRedirectEnabled"]; } + components.navigationInfoParameters = navigationParams; +} + +- (void)setSocialParameters:(NSDictionary *)socialData + components:(FIRDynamicLinkComponents *)components { + FIRDynamicLinkSocialMetaTagParameters *socialParams = [FIRDynamicLinkSocialMetaTagParameters parameters]; + if (socialData[@"descriptionText"]) { + socialParams.descriptionText = socialData[@"descriptionText"]; + } + if (socialData[@"imageUrl"]) { + socialParams.imageURL = [NSURL URLWithString:socialData[@"imageUrl"]]; + } + if (socialData[@"title"]) { + socialParams.title = socialData[@"title"]; + } + components.socialMetaTagParameters = socialParams; } - (NSArray *)supportedEvents { @@ -249,3 +285,4 @@ RCT_EXPORT_METHOD(getInitialLink:(RCTPromiseResolveBlock)resolve rejecter:(RCTPr @implementation RNFirebaseLinks @end #endif + diff --git a/lib/modules/instanceid/index.js b/lib/modules/instanceid/index.js index 3924a0a9..0cd6528d 100644 --- a/lib/modules/instanceid/index.js +++ b/lib/modules/instanceid/index.js @@ -13,6 +13,7 @@ export const NAMESPACE = 'instanceid'; export default class InstanceId extends ModuleBase { constructor(app: App) { super(app, { + hasShards: false, moduleName: MODULE_NAME, multiApp: false, namespace: NAMESPACE, diff --git a/lib/modules/invites/index.js b/lib/modules/invites/index.js index 099a6ba9..3ba53fae 100644 --- a/lib/modules/invites/index.js +++ b/lib/modules/invites/index.js @@ -23,6 +23,7 @@ export default class Invites extends ModuleBase { constructor(app: App) { super(app, { events: NATIVE_EVENTS, + hasShards: false, moduleName: MODULE_NAME, multiApp: false, namespace: NAMESPACE, diff --git a/lib/modules/links/AnalyticsParameters.js b/lib/modules/links/AnalyticsParameters.js new file mode 100644 index 00000000..36e77209 --- /dev/null +++ b/lib/modules/links/AnalyticsParameters.js @@ -0,0 +1,79 @@ +/** + * @flow + * AnalyticsParameters representation wrapper + */ +import type DynamicLink from './DynamicLink'; +import type { NativeAnalyticsParameters } from './types'; + +export default class AnalyticsParameters { + _campaign: string | void; + _content: string | void; + _link: DynamicLink; + _medium: string | void; + _source: string | void; + _term: string | void; + + constructor(link: DynamicLink) { + this._link = link; + } + + /** + * + * @param campaign + * @returns {DynamicLink} + */ + setCampaign(campaign: string): DynamicLink { + this._campaign = campaign; + return this._link; + } + + /** + * + * @param content + * @returns {DynamicLink} + */ + setContent(content: string): DynamicLink { + this._content = content; + return this._link; + } + + /** + * + * @param medium + * @returns {DynamicLink} + */ + setMedium(medium: string): DynamicLink { + this._medium = medium; + return this._link; + } + + /** + * + * @param source + * @returns {DynamicLink} + */ + setSource(source: string): DynamicLink { + this._source = source; + return this._link; + } + + /** + * + * @param term + * @returns {DynamicLink} + */ + setTerm(term: string): DynamicLink { + this._term = term; + return this._link; + } + + build(): NativeAnalyticsParameters { + return { + campaign: this._campaign, + content: this._content, + medium: this._medium, + source: this._source, + term: this._term, + }; + } +} diff --git a/lib/modules/links/AndroidParameters.js b/lib/modules/links/AndroidParameters.js new file mode 100644 index 00000000..985bfd4b --- /dev/null +++ b/lib/modules/links/AndroidParameters.js @@ -0,0 +1,60 @@ +/** + * @flow + * AndroidParameters representation wrapper + */ +import type DynamicLink from './DynamicLink'; +import type { NativeAndroidParameters } from './types'; + +export default class AndroidParameters { + _fallbackUrl: string | void; + _link: DynamicLink; + _minimumVersion: number | void; + _packageName: string | void; + + constructor(link: DynamicLink) { + this._link = link; + } + + /** + * + * @param fallbackUrl + * @returns {DynamicLink} + */ + setFallbackUrl(fallbackUrl: string): DynamicLink { + this._fallbackUrl = fallbackUrl; + return this._link; + } + + /** + * + * @param minimumVersion + * @returns {DynamicLink} + */ + setMinimumVersion(minimumVersion: number): DynamicLink { + this._minimumVersion = minimumVersion; + return this._link; + } + + /** + * + * @param packageName + * @returns {DynamicLink} + */ + setPackageName(packageName: string): DynamicLink { + this._packageName = packageName; + return this._link; + } + + build(): NativeAndroidParameters { + if ((this._fallbackUrl || this._minimumVersion) && !this._packageName) { + throw new Error( + 'AndroidParameters: Missing required `packageName` property' + ); + } + return { + fallbackUrl: this._fallbackUrl, + minimumVersion: this._minimumVersion, + packageName: this._packageName, + }; + } +} diff --git a/lib/modules/links/DynamicLink.js b/lib/modules/links/DynamicLink.js new file mode 100644 index 00000000..2930a3d0 --- /dev/null +++ b/lib/modules/links/DynamicLink.js @@ -0,0 +1,79 @@ +/** + * @flow + * DynamicLink representation wrapper + */ +import AnalyticsParameters from './AnalyticsParameters'; +import AndroidParameters from './AndroidParameters'; +import IOSParameters from './IOSParameters'; +import ITunesParameters from './ITunesParameters'; +import NavigationParameters from './NavigationParameters'; +import SocialParameters from './SocialParameters'; + +import type { NativeDynamicLink } from './types'; + +export default class DynamicLink { + _analytics: AnalyticsParameters; + _android: AndroidParameters; + _dynamicLinkDomain: string; + _ios: IOSParameters; + _itunes: ITunesParameters; + _link: string; + _navigation: NavigationParameters; + _social: SocialParameters; + + constructor(link: string, dynamicLinkDomain: string) { + this._analytics = new AnalyticsParameters(this); + this._android = new AndroidParameters(this); + this._dynamicLinkDomain = dynamicLinkDomain; + this._ios = new IOSParameters(this); + this._itunes = new ITunesParameters(this); + this._link = link; + this._navigation = new NavigationParameters(this); + this._social = new SocialParameters(this); + } + + get analytics(): AnalyticsParameters { + return this._analytics; + } + + get android(): AndroidParameters { + return this._android; + } + + get ios(): IOSParameters { + return this._ios; + } + + get itunes(): ITunesParameters { + return this._itunes; + } + + get navigation(): NavigationParameters { + return this._navigation; + } + + get social(): SocialParameters { + return this._social; + } + + build(): NativeDynamicLink { + if (!this._link) { + throw new Error('DynamicLink: Missing required `link` property'); + } else if (!this._dynamicLinkDomain) { + throw new Error( + 'DynamicLink: Missing required `dynamicLinkDomain` property' + ); + } + + return { + analytics: this._analytics.build(), + android: this._android.build(), + dynamicLinkDomain: this._dynamicLinkDomain, + ios: this._ios.build(), + itunes: this._itunes.build(), + link: this._link, + navigation: this._navigation.build(), + social: this._social.build(), + }; + } +} diff --git a/lib/modules/links/IOSParameters.js b/lib/modules/links/IOSParameters.js new file mode 100644 index 00000000..1ac46631 --- /dev/null +++ b/lib/modules/links/IOSParameters.js @@ -0,0 +1,114 @@ +/** + * @flow + * IOSParameters representation wrapper + */ +import type DynamicLink from './DynamicLink'; +import type { NativeIOSParameters } from './types'; + +export default class IOSParameters { + _appStoreId: string | void; + _bundleId: string | void; + _customScheme: string | void; + _fallbackUrl: string | void; + _iPadBundleId: string | void; + _iPadFallbackUrl: string | void; + _link: DynamicLink; + _minimumVersion: string | void; + + constructor(link: DynamicLink) { + this._link = link; + } + + /** + * + * @param appStoreId + * @returns {DynamicLink} + */ + setAppStoreId(appStoreId: string): DynamicLink { + this._appStoreId = appStoreId; + return this._link; + } + + /** + * + * @param bundleId + * @returns {DynamicLink} + */ + setBundleId(bundleId: string): DynamicLink { + this._bundleId = bundleId; + return this._link; + } + + /** + * + * @param customScheme + * @returns {DynamicLink} + */ + setCustomScheme(customScheme: string): DynamicLink { + this._customScheme = customScheme; + return this._link; + } + + /** + * + * @param fallbackUrl + * @returns {DynamicLink} + */ + setFallbackUrl(fallbackUrl: string): DynamicLink { + this._fallbackUrl = fallbackUrl; + return this._link; + } + + /** + * + * @param iPadBundleId + * @returns {DynamicLink} + */ + setIPadBundleId(iPadBundleId: string): DynamicLink { + this._iPadBundleId = iPadBundleId; + return this._link; + } + + /** + * + * @param iPadFallbackUrl + * @returns {DynamicLink} + */ + setIPadFallbackUrl(iPadFallbackUrl: string): DynamicLink { + this._iPadFallbackUrl = iPadFallbackUrl; + return this._link; + } + + /** + * + * @param minimumVersion + * @returns {DynamicLink} + */ + setMinimumVersion(minimumVersion: string): DynamicLink { + this._minimumVersion = minimumVersion; + return this._link; + } + + build(): NativeIOSParameters { + if ( + (this._appStoreId || + this._customScheme || + this._fallbackUrl || + this._iPadBundleId || + this._iPadFallbackUrl || + this._minimumVersion) && + !this._bundleId + ) { + throw new Error('IOSParameters: Missing required `bundleId` property'); + } + return { + appStoreId: this._appStoreId, + bundleId: this._bundleId, + customScheme: this._customScheme, + fallbackUrl: this._fallbackUrl, + iPadBundleId: this._iPadBundleId, + iPadFallbackUrl: this._iPadFallbackUrl, + minimumVersion: this._minimumVersion, + }; + } +} diff --git a/lib/modules/links/ITunesParameters.js b/lib/modules/links/ITunesParameters.js new file mode 100644 index 00000000..784a99ff --- /dev/null +++ b/lib/modules/links/ITunesParameters.js @@ -0,0 +1,55 @@ +/** + * @flow + * ITunesParameters representation wrapper + */ +import type DynamicLink from './DynamicLink'; +import type { NativeITunesParameters } from './types'; + +export default class ITunesParameters { + _affiliateToken: string | void; + _campaignToken: string | void; + _link: DynamicLink; + _providerToken: string | void; + + constructor(link: DynamicLink) { + this._link = link; + } + + /** + * + * @param affiliateToken + * @returns {DynamicLink} + */ + setAffiliateToken(affiliateToken: string): DynamicLink { + this._affiliateToken = affiliateToken; + return this._link; + } + + /** + * + * @param campaignToken + * @returns {DynamicLink} + */ + setCampaignToken(campaignToken: string): DynamicLink { + this._campaignToken = campaignToken; + return this._link; + } + + /** + * + * @param providerToken + * @returns {DynamicLink} + */ + setProviderToken(providerToken: string): DynamicLink { + this._providerToken = providerToken; + return this._link; + } + + build(): NativeITunesParameters { + return { + affiliateToken: this._affiliateToken, + campaignToken: this._campaignToken, + providerToken: this._providerToken, + }; + } +} diff --git a/lib/modules/links/NavigationParameters.js b/lib/modules/links/NavigationParameters.js new file mode 100644 index 00000000..dbb408fb --- /dev/null +++ b/lib/modules/links/NavigationParameters.js @@ -0,0 +1,31 @@ +/** + * @flow + * NavigationParameters representation wrapper + */ +import type DynamicLink from './DynamicLink'; +import type { NativeNavigationParameters } from './types'; + +export default class NavigationParameters { + _forcedRedirectEnabled: string | void; + _link: DynamicLink; + + constructor(link: DynamicLink) { + this._link = link; + } + + /** + * + * @param forcedRedirectEnabled + * @returns {DynamicLink} + */ + setForcedRedirectEnabled(forcedRedirectEnabled: string): DynamicLink { + this._forcedRedirectEnabled = forcedRedirectEnabled; + return this._link; + } + + build(): NativeNavigationParameters { + return { + forcedRedirectEnabled: this._forcedRedirectEnabled, + }; + } +} diff --git a/lib/modules/links/SocialParameters.js b/lib/modules/links/SocialParameters.js new file mode 100644 index 00000000..be81386b --- /dev/null +++ b/lib/modules/links/SocialParameters.js @@ -0,0 +1,55 @@ +/** + * @flow + * SocialParameters representation wrapper + */ +import type DynamicLink from './DynamicLink'; +import type { NativeSocialParameters } from './types'; + +export default class SocialParameters { + _descriptionText: string | void; + _imageUrl: string | void; + _link: DynamicLink; + _title: string | void; + + constructor(link: DynamicLink) { + this._link = link; + } + + /** + * + * @param descriptionText + * @returns {DynamicLink} + */ + setDescriptionText(descriptionText: string): DynamicLink { + this._descriptionText = descriptionText; + return this._link; + } + + /** + * + * @param imageUrl + * @returns {DynamicLink} + */ + setImageUrl(imageUrl: string): DynamicLink { + this._imageUrl = imageUrl; + return this._link; + } + + /** + * + * @param title + * @returns {DynamicLink} + */ + setTitle(title: string): DynamicLink { + this._title = title; + return this._link; + } + + build(): NativeSocialParameters { + return { + descriptionText: this._descriptionText, + imageUrl: this._imageUrl, + title: this._title, + }; + } +} diff --git a/lib/modules/links/index.js b/lib/modules/links/index.js index 95fb7566..bb128399 100644 --- a/lib/modules/links/index.js +++ b/lib/modules/links/index.js @@ -2,10 +2,10 @@ * @flow * Dynamic Links representation wrapper */ +import DynamicLink from './DynamicLink'; import { SharedEventEmitter } from '../../utils/events'; import { getLogger } from '../../utils/log'; import ModuleBase from '../../utils/ModuleBase'; -import { areObjectKeysContainedInOther, isObject, isString } from '../../utils'; import { getNativeModule } from '../../utils/native'; import type App from '../core/app'; @@ -15,59 +15,6 @@ const NATIVE_EVENTS = ['links_link_received']; export const MODULE_NAME = 'RNFirebaseLinks'; export const NAMESPACE = 'links'; -function validateParameters(parameters: Object): void { - const suportedParametersObject = { - dynamicLinkDomain: 'string', - link: 'string', - androidInfo: { - androidPackageName: 'string', - androidFallbackLink: 'string', - androidMinPackageVersionCode: 'string', - androidLink: 'string', - }, - iosInfo: { - iosBundleId: 'string', - iosFallbackLink: 'string', - iosCustomScheme: 'string', - iosIpadFallbackLink: 'string', - iosIpadBundleId: 'string', - iosAppStoreId: 'string', - }, - socialMetaTagInfo: { - socialTitle: 'string', - socialDescription: 'string', - socialImageLink: 'string', - }, - suffix: { - option: 'string', - }, - }; - if (!areObjectKeysContainedInOther(parameters, suportedParametersObject)) { - throw new Error('Invalid Parameters.'); - } -} - -function checkForMandatoryParameters(parameters: Object): void { - if (!isString(parameters.dynamicLinkDomain)) { - throw new Error('No dynamicLinkDomain was specified.'); - } - if (!isString(parameters.link)) { - throw new Error('No link was specified.'); - } - if ( - isObject(parameters.androidInfo) && - !isString(parameters.androidInfo.androidPackageName) - ) { - throw new Error('No androidPackageName was specified.'); - } - if ( - isObject(parameters.iosInfo) && - !isString(parameters.iosInfo.iosBundleId) - ) { - throw new Error('No iosBundleId was specified.'); - } -} - /** * @class Links */ @@ -96,11 +43,9 @@ export default class Links extends ModuleBase { * @param parameters * @returns {Promise.} */ - createDynamicLink(parameters: Object = {}): Promise { + createDynamicLink(link: DynamicLink): Promise { try { - checkForMandatoryParameters(parameters); - validateParameters(parameters); - return getNativeModule(this).createDynamicLink(parameters); + return getNativeModule(this).createDynamicLink(link.build()); } catch (error) { return Promise.reject(error); } @@ -111,11 +56,12 @@ export default class Links extends ModuleBase { * @param parameters * @returns {Promise.} */ - createShortDynamicLink(parameters: Object = {}): Promise { + createShortDynamicLink( + link: DynamicLink, + type?: 'SHORT' | 'UNGUESSABLE' + ): Promise { try { - checkForMandatoryParameters(parameters); - validateParameters(parameters); - return getNativeModule(this).createShortDynamicLink(parameters); + return getNativeModule(this).createShortDynamicLink(link.build(), type); } catch (error) { return Promise.reject(error); } diff --git a/lib/modules/links/types.js b/lib/modules/links/types.js new file mode 100644 index 00000000..27c947a4 --- /dev/null +++ b/lib/modules/links/types.js @@ -0,0 +1,53 @@ +/** + * @flow + */ +export type NativeAnalyticsParameters = {| + campaign?: string, + content?: string, + medium?: string, + source?: string, + term?: string, +|}; + +export type NativeAndroidParameters = {| + fallbackUrl?: string, + minimumVersion?: number, + packageName?: string, +|}; + +export type NativeIOSParameters = {| + appStoreId?: string, + bundleId?: string, + customScheme?: string, + fallbackUrl?: string, + iPadBundleId?: string, + iPadFallbackUrl?: string, + minimumVersion?: string, +|}; + +export type NativeITunesParameters = {| + affiliateToken?: string, + campaignToken?: string, + providerToken?: string, +|}; + +export type NativeNavigationParameters = {| + forcedRedirectEnabled?: string, +|}; + +export type NativeSocialParameters = {| + descriptionText?: string, + imageUrl?: string, + title?: string, +|}; + +export type NativeDynamicLink = {| + analytics: NativeAnalyticsParameters, + android: NativeAndroidParameters, + dynamicLinkDomain: string, + ios: NativeIOSParameters, + itunes: NativeITunesParameters, + link: string, + navigation: NativeNavigationParameters, + social: NativeSocialParameters, +|}; diff --git a/lib/modules/notifications/index.js b/lib/modules/notifications/index.js index d29ec73b..504bd9e0 100644 --- a/lib/modules/notifications/index.js +++ b/lib/modules/notifications/index.js @@ -76,6 +76,7 @@ export default class Notifications extends ModuleBase { constructor(app: App) { super(app, { events: NATIVE_EVENTS, + hasShards: false, moduleName: MODULE_NAME, multiApp: false, namespace: NAMESPACE, From b9dab3cb175daae41833f33d6315ef8c5b8bb030 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 23 Mar 2018 13:26:20 +0000 Subject: [PATCH 77/77] [links] Some error checking --- lib/modules/links/index.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/modules/links/index.js b/lib/modules/links/index.js index bb128399..a926b182 100644 --- a/lib/modules/links/index.js +++ b/lib/modules/links/index.js @@ -44,11 +44,12 @@ export default class Links extends ModuleBase { * @returns {Promise.} */ createDynamicLink(link: DynamicLink): Promise { - try { - return getNativeModule(this).createDynamicLink(link.build()); - } catch (error) { - return Promise.reject(error); + if (!(link instanceof DynamicLink)) { + throw new Error( + `Links:createDynamicLink expects a 'DynamicLink' but got type ${typeof link}` + ); } + return getNativeModule(this).createDynamicLink(link.build()); } /** @@ -60,11 +61,12 @@ export default class Links extends ModuleBase { link: DynamicLink, type?: 'SHORT' | 'UNGUESSABLE' ): Promise { - try { - return getNativeModule(this).createShortDynamicLink(link.build(), type); - } catch (error) { - return Promise.reject(error); + if (!(link instanceof DynamicLink)) { + throw new Error( + `Links:createShortDynamicLink expects a 'DynamicLink' but got type ${typeof link}` + ); } + return getNativeModule(this).createShortDynamicLink(link.build(), type); } /**