diff --git a/lib/index.js b/lib/index.js index 0d65d53e..b31d093b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,8 +1,8 @@ /** * @flow */ -import Firebase from './modules/core/firebase'; +import firebase from './modules/core/firebase'; -export const AdMob = require('./modules/admob'); +export type { default as User } from './modules/auth/User'; -export default Firebase; +export default firebase; diff --git a/lib/modules/admob/Interstitial.js b/lib/modules/admob/Interstitial.js index 617945be..06ff6eee 100644 --- a/lib/modules/admob/Interstitial.js +++ b/lib/modules/admob/Interstitial.js @@ -1,6 +1,7 @@ import { NativeModules, Platform } from 'react-native'; import { statics } from './'; import AdRequest from './AdRequest'; +import { SharedEventEmitter } from '../../utils/events'; import { nativeToJSError } from '../../utils'; const FirebaseAdMob = NativeModules.RNFirebaseAdMob; @@ -23,8 +24,8 @@ export default class Interstitial { this.admob = admob; this.adUnit = adUnit; this.loaded = false; - this.admob.removeAllListeners(`interstitial_${adUnit}`); - this.admob.on(`interstitial_${adUnit}`, this._onInterstitialEvent); + SharedEventEmitter.removeAllListeners(`interstitial_${adUnit}`); + SharedEventEmitter.addListener(`interstitial_${adUnit}`, this._onInterstitialEvent); } /** @@ -48,8 +49,8 @@ export default class Interstitial { default: } - this.admob.emit(eventType, emitData); - this.admob.emit(`interstitial:${this.adUnit}:*`, emitData); + SharedEventEmitter.emit(eventType, emitData); + SharedEventEmitter.emit(`interstitial:${this.adUnit}:*`, emitData); }; /** @@ -97,7 +98,7 @@ export default class Interstitial { return null; } - const sub = this.admob.on(`interstitial:${this.adUnit}:${eventType}`, listenerCb); + const sub = SharedEventEmitter.addListener(`interstitial:${this.adUnit}:${eventType}`, listenerCb); subscriptions.push(sub); return sub; } diff --git a/lib/modules/admob/RewardedVideo.js b/lib/modules/admob/RewardedVideo.js index f34f5eec..84b02860 100644 --- a/lib/modules/admob/RewardedVideo.js +++ b/lib/modules/admob/RewardedVideo.js @@ -1,6 +1,7 @@ import { NativeModules } from 'react-native'; import { statics } from './'; import AdRequest from './AdRequest'; +import { SharedEventEmitter } from '../../utils/events'; import { nativeToJSError } from '../../utils'; const FirebaseAdMob = NativeModules.RNFirebaseAdMob; @@ -18,8 +19,8 @@ export default class RewardedVideo { this.admob = admob; this.adUnit = adUnit; this.loaded = false; - this.admob.removeAllListeners(`rewarded_video_${adUnit}`); - this.admob.on(`rewarded_video_${adUnit}`, this._onRewardedVideoEvent); + SharedEventEmitter.removeAllListeners(`rewarded_video_${adUnit}`); + SharedEventEmitter.addListener(`rewarded_video_${adUnit}`, this._onRewardedVideoEvent); } /** @@ -43,8 +44,8 @@ export default class RewardedVideo { default: } - this.admob.emit(eventType, emitData); - this.admob.emit(`rewarded_video:${this.adUnit}:*`, emitData); + SharedEventEmitter.emit(eventType, emitData); + SharedEventEmitter.emit(`rewarded_video:${this.adUnit}:*`, emitData); }; /** @@ -97,7 +98,7 @@ export default class RewardedVideo { return null; } - const sub = this.admob.on(`rewarded_video:${this.adUnit}:${eventType}`, listenerCb); + const sub = SharedEventEmitter.addListener(`rewarded_video:${this.adUnit}:${eventType}`, listenerCb); subscriptions.push(sub); return sub; } diff --git a/lib/modules/admob/index.js b/lib/modules/admob/index.js index c044646b..070e1a3f 100644 --- a/lib/modules/admob/index.js +++ b/lib/modules/admob/index.js @@ -2,7 +2,9 @@ * @flow * AdMob representation wrapper */ -import ModuleBase from './../../utils/ModuleBase'; +import { SharedEventEmitter } from '../../utils/events'; +import { getLogger } from '../../utils/log'; +import ModuleBase from '../../utils/ModuleBase'; import Interstitial from './Interstitial'; import RewardedVideo from './RewardedVideo'; @@ -37,35 +39,35 @@ export default class AdMob extends ModuleBase { this._initialized = false; this._appId = null; - this._eventEmitter.addListener('interstitial_event', this._onInterstitialEvent.bind(this)); - this._eventEmitter.addListener('rewarded_video_event', this._onRewardedVideoEvent.bind(this)); + SharedEventEmitter.addListener('interstitial_event', this._onInterstitialEvent.bind(this)); + SharedEventEmitter.addListener('rewarded_video_event', this._onRewardedVideoEvent.bind(this)); } _onInterstitialEvent(event: NativeEvent): void { const { adUnit } = event; const jsEventType = `interstitial_${adUnit}`; - if (!this.hasListeners(jsEventType)) { + if (!SharedEventEmitter.hasListeners(jsEventType)) { // TODO } - this.emit(jsEventType, event); + SharedEventEmitter.emit(jsEventType, event); } _onRewardedVideoEvent(event: NativeEvent): void { const { adUnit } = event; const jsEventType = `rewarded_video_${adUnit}`; - if (!this.hasListeners(jsEventType)) { + if (!SharedEventEmitter.hasListeners(jsEventType)) { // TODO } - this.emit(jsEventType, event); + SharedEventEmitter.emit(jsEventType, event); } initialize(appId: string): void { if (this._initialized) { - this.log.warn('AdMob has already been initialized!'); + getLogger(this).warn('AdMob has already been initialized!'); } else { this._initialized = true; this._appId = appId; @@ -75,9 +77,9 @@ export default class AdMob extends ModuleBase { openDebugMenu(): void { if (!this._initialized) { - this.log.warn('AdMob needs to be initialized before opening the dev menu!'); + getLogger(this).warn('AdMob needs to be initialized before opening the dev menu!'); } else { - this.log.info('Opening debug menu'); + getLogger(this).info('Opening debug menu'); this._native.openDebugMenu(this._appId); } } diff --git a/lib/modules/auth/PhoneAuthListener.js b/lib/modules/auth/PhoneAuthListener.js index 0848b080..9503d8cf 100644 --- a/lib/modules/auth/PhoneAuthListener.js +++ b/lib/modules/auth/PhoneAuthListener.js @@ -1,5 +1,6 @@ // @flow import INTERNALS from '../../utils/internals'; +import { SharedEventEmitter } from '../../utils/events'; import { generatePushID, isFunction, isAndroid, isIOS, isString, nativeToJSError } from './../../utils'; import type Auth from './'; @@ -92,7 +93,7 @@ export default class PhoneAuthListener { for (let i = 0, len = events.length; i < len; i++) { const type = events[i]; - this._auth.once(this._internalEvents[type], this[`_${type}Handler`].bind(this)); + SharedEventEmitter.once(this._internalEvents[type], this[`_${type}Handler`].bind(this)); } } @@ -102,7 +103,7 @@ export default class PhoneAuthListener { * @private */ _addUserObserver(observer) { - this._auth.on(this._publicEvents.event, observer); + SharedEventEmitter.addListener(this._publicEvents.event, observer); } /** @@ -111,7 +112,7 @@ export default class PhoneAuthListener { * @private */ _emitToObservers(snapshot: PhoneAuthSnapshot) { - this._auth.emit(this._publicEvents.event, snapshot); + SharedEventEmitter.emit(this._publicEvents.event, snapshot); } /** @@ -122,7 +123,7 @@ export default class PhoneAuthListener { _emitToErrorCb(snapshot) { const error = snapshot.error; if (this._reject) this._reject(error); - this._auth.emit(this._publicEvents.error, error); + SharedEventEmitter.emit(this._publicEvents.error, error); } /** @@ -132,7 +133,7 @@ export default class PhoneAuthListener { */ _emitToSuccessCb(snapshot) { if (this._resolve) this._resolve(snapshot); - this._auth.emit(this._publicEvents.success, snapshot); + SharedEventEmitter.emit(this._publicEvents.success, snapshot); } /** @@ -143,12 +144,12 @@ export default class PhoneAuthListener { setTimeout(() => { // move to next event loop - not sure if needed // internal listeners Object.values(this._internalEvents).forEach((event) => { - this._auth.removeAllListeners(event); + SharedEventEmitter.removeAllListeners(event); }); // user observer listeners Object.values(this._publicEvents).forEach((publicEvent) => { - this._auth.removeAllListeners(publicEvent); + SharedEventEmitter.removeAllListeners(publicEvent); }); }, 0); } @@ -279,11 +280,11 @@ export default class PhoneAuthListener { this._addUserObserver(observer); if (isFunction(errorCb)) { - this._auth.once(this._publicEvents.error, errorCb); + SharedEventEmitter.once(this._publicEvents.error, errorCb); } if (isFunction(successCb)) { - this._auth.once(this._publicEvents.success, successCb); + SharedEventEmitter.once(this._publicEvents.success, successCb); } return this; diff --git a/lib/modules/auth/index.js b/lib/modules/auth/index.js index b8d073db..18e27066 100644 --- a/lib/modules/auth/index.js +++ b/lib/modules/auth/index.js @@ -4,6 +4,8 @@ */ import User from './User'; import ModuleBase from '../../utils/ModuleBase'; +import { getAppEventName, SharedEventEmitter } from '../../utils/events'; +import { getLogger } from '../../utils/log'; import INTERNALS from '../../utils/internals'; import ConfirmationResult from './ConfirmationResult'; @@ -37,24 +39,24 @@ export default class Auth extends ModuleBase { this._user = null; this._authResult = null; - this.addListener( + SharedEventEmitter.addListener( // sub to internal native event - this fans out to // public event name: onAuthStateChanged - super._getAppEventName('auth_state_changed'), + getAppEventName(this, 'auth_state_changed'), this._onInternalAuthStateChanged.bind(this), ); - this.addListener( + SharedEventEmitter.addListener( // sub to internal native event - this fans out to // public events based on event.type - super._getAppEventName('phone_auth_state_changed'), + getAppEventName(this, 'phone_auth_state_changed'), this._onInternalPhoneAuthStateChanged.bind(this), ); - this.addListener( + SharedEventEmitter.addListener( // sub to internal native event - this fans out to // public event name: onIdTokenChanged - super._getAppEventName('auth_id_token_changed'), + getAppEventName(this, 'auth_id_token_changed'), this._onInternalIdTokenChanged.bind(this), ); @@ -69,13 +71,13 @@ export default class Auth extends ModuleBase { */ _onInternalPhoneAuthStateChanged(event: Object) { const eventKey = `phone:auth:${event.requestKey}:${event.type}`; - this.emit(eventKey, event.state); + SharedEventEmitter.emit(eventKey, event.state); } _setAuthState(auth: AuthResult) { this._authResult = auth; this._user = auth && auth.user ? new User(this, auth.user) : null; - this.emit(this._getAppEventName('onUserChanged'), this._user); + SharedEventEmitter.emit(getAppEventName(this, 'onUserChanged'), this._user); } /** @@ -85,7 +87,7 @@ export default class Auth extends ModuleBase { */ _onInternalAuthStateChanged(auth: AuthResult) { this._setAuthState(auth); - this.emit(this._getAppEventName('onAuthStateChanged'), this._user); + SharedEventEmitter.emit(getAppEventName(this, 'onAuthStateChanged'), this._user); } /** @@ -96,7 +98,7 @@ export default class Auth extends ModuleBase { */ _onInternalIdTokenChanged(auth: AuthResult) { this._setAuthState(auth); - this.emit(this._getAppEventName('onIdTokenChanged'), this._user); + SharedEventEmitter.emit(getAppEventName(this, 'onIdTokenChanged'), this._user); } /** @@ -129,8 +131,8 @@ export default class Auth extends ModuleBase { * @param listener */ onAuthStateChanged(listener: Function) { - this.log.info('Creating onAuthStateChanged listener'); - this.on(this._getAppEventName('onAuthStateChanged'), listener); + getLogger(this).info('Creating onAuthStateChanged listener'); + SharedEventEmitter.addListener(getAppEventName(this, 'onAuthStateChanged'), listener); if (this._authResult) listener(this._user || null); return this._offAuthStateChanged.bind(this, listener); } @@ -140,8 +142,8 @@ export default class Auth extends ModuleBase { * @param listener */ _offAuthStateChanged(listener: Function) { - this.log.info('Removing onAuthStateChanged listener'); - this.removeListener(this._getAppEventName('onAuthStateChanged'), listener); + getLogger(this).info('Removing onAuthStateChanged listener'); + SharedEventEmitter.removeListener(getAppEventName(this, 'onAuthStateChanged'), listener); } /** @@ -149,8 +151,8 @@ export default class Auth extends ModuleBase { * @param listener */ onIdTokenChanged(listener: Function) { - this.log.info('Creating onIdTokenChanged listener'); - this.on(this._getAppEventName('onIdTokenChanged'), listener); + getLogger(this).info('Creating onIdTokenChanged listener'); + SharedEventEmitter.addListener(getAppEventName(this, 'onIdTokenChanged'), listener); if (this._authResult) listener(this._user || null); return this._offIdTokenChanged.bind(this, listener); } @@ -160,8 +162,8 @@ export default class Auth extends ModuleBase { * @param listener */ _offIdTokenChanged(listener: Function) { - this.log.info('Removing onIdTokenChanged listener'); - this.removeListener(this._getAppEventName('onIdTokenChanged'), listener); + getLogger(this).info('Removing onIdTokenChanged listener'); + SharedEventEmitter.removeListener(getAppEventName(this, 'onIdTokenChanged'), listener); } /** @@ -169,8 +171,8 @@ export default class Auth extends ModuleBase { * @param listener */ onUserChanged(listener: Function) { - this.log.info('Creating onUserChanged listener'); - this.on(this._getAppEventName('onUserChanged'), listener); + getLogger(this).info('Creating onUserChanged listener'); + SharedEventEmitter.addListener(getAppEventName(this, 'onUserChanged'), listener); if (this._authResult) listener(this._user || null); return this._offUserChanged.bind(this, listener); } @@ -180,8 +182,8 @@ export default class Auth extends ModuleBase { * @param listener */ _offUserChanged(listener: Function) { - this.log.info('Removing onUserChanged listener'); - this.removeListener(this._getAppEventName('onUserChanged'), listener); + getLogger(this).info('Removing onUserChanged listener'); + SharedEventEmitter.removeListener(getAppEventName(this, 'onUserChanged'), listener); } /** diff --git a/lib/modules/config/index.js b/lib/modules/config/index.js index 999c838a..990d8e9a 100644 --- a/lib/modules/config/index.js +++ b/lib/modules/config/index.js @@ -2,6 +2,7 @@ * @flow * Remote Config representation wrapper */ +import { getLogger } from '../../utils/log'; import ModuleBase from './../../utils/ModuleBase'; import type FirebaseApp from '../core/firebase-app'; @@ -51,7 +52,7 @@ export default class RemoteConfig extends ModuleBase { */ enableDeveloperMode() { if (!this._developerModeEnabled) { - this.log.debug('Enabled developer mode'); + getLogger(this).debug('Enabled developer mode'); this._native.enableDeveloperMode(); this._developerModeEnabled = true; } @@ -64,10 +65,10 @@ export default class RemoteConfig extends ModuleBase { */ fetch(expiration?: number) { if (expiration !== undefined) { - this.log.debug(`Fetching remote config data with expiration ${expiration.toString()}`); + getLogger(this).debug(`Fetching remote config data with expiration ${expiration.toString()}`); return this._native.fetchWithExpirationDuration(expiration); } - this.log.debug('Fetching remote config data'); + getLogger(this).debug('Fetching remote config data'); return this._native.fetch(); } @@ -78,7 +79,7 @@ export default class RemoteConfig extends ModuleBase { * rejects if no Fetched Config was found, or the Fetched Config was already activated. */ activateFetched() { - this.log.debug('Activating remote config'); + getLogger(this).debug('Activating remote config'); return this._native.activateFetched(); } diff --git a/lib/modules/core/firebase-app.js b/lib/modules/core/firebase-app.js index 686a0bed..2711d4f1 100644 --- a/lib/modules/core/firebase-app.js +++ b/lib/modules/core/firebase-app.js @@ -3,41 +3,27 @@ */ import { NativeModules } from 'react-native'; +import APPS from '../../utils/apps'; +import { SharedEventEmitter } from '../../utils/events'; import INTERNALS from '../../utils/internals'; -import { isObject, isAndroid } from '../../utils'; +import { isObject } from '../../utils'; -import AdMob, { statics as AdMobStatics } from '../admob'; -import Auth, { statics as AuthStatics } from '../auth'; -import Analytics, { statics as AnalyticsStatics } from '../analytics'; -import Config, { statics as ConfigStatics } from '../config'; -import Crash, { statics as CrashStatics } from '../crash'; -import Crashlytics, { statics as CrashlyticsStatics } from '../fabric/crashlytics'; -import Database, { statics as DatabaseStatics } from '../database'; -import Firestore, { statics as FirestoreStatics } from '../firestore'; -import Links, { statics as LinksStatics } from '../links'; -import Messaging, { statics as MessagingStatics } from '../messaging'; -import Performance, { statics as PerformanceStatics } from '../perf'; -import Storage, { statics as StorageStatics } from '../storage'; -import Utils, { statics as UtilsStatics } from '../utils'; +import AdMob from '../admob'; +import Auth from '../auth'; +import Analytics from '../analytics'; +import Config from '../config'; +import Crash from '../crash'; +import Crashlytics from '../fabric/crashlytics'; +import Database from '../database'; +import Firestore from '../firestore'; +import Links from '../links'; +import Messaging from '../messaging'; +import Performance from '../perf'; +import Storage from '../storage'; +import Utils from '../utils'; import type { - AdMobModule, - AnalyticsModule, - AuthModule, - ConfigModule, - CrashModule, - DatabaseModule, - FabricModule, - FirebaseModule, - FirebaseModuleAndStatics, FirebaseOptions, - FirebaseStatics, - FirestoreModule, - LinksModule, - MessagingModule, - PerformanceModule, - StorageModule, - UtilsModule, } from '../../types'; const FirebaseCoreModule = NativeModules.RNFirebase; @@ -45,70 +31,57 @@ const FirebaseCoreModule = NativeModules.RNFirebase; export default class FirebaseApp { _extendedProps: { [string] : boolean }; - _initialized: boolean; + _initialized: boolean = false; _name: string; - _namespaces: { [string]: FirebaseModule }; - _nativeInitialized: boolean; + _nativeInitialized: boolean = false; _options: FirebaseOptions; - admob: AdMobModule; - analytics: AnalyticsModule; - auth: AuthModule; - config: ConfigModule; - crash: CrashModule; - database: DatabaseModule; - fabric: FabricModule; - firestore: FirestoreModule; - links: LinksModule; - messaging: MessagingModule; - perf: PerformanceModule; - storage: StorageModule; - utils: UtilsModule; + admob: () => AdMob; + analytics: () => Analytics; + auth: () => Auth; + config: () => Config; + crash: () => Crash; + database: () => Database; + fabric: { + crashlytics: () => Crashlytics, + }; + firestore: () => Firestore; + links: () => Links; + messaging: () => Messaging; + perf: () => Performance; + storage: () => Storage; + utils: () => Utils; - constructor(name: string, options: FirebaseOptions) { + constructor(name: string, options: FirebaseOptions, fromNative: boolean = false) { this._name = name; - this._namespaces = {}; this._options = Object.assign({}, options); - // native ios/android to confirm initialized - this._initialized = false; - this._nativeInitialized = false; - - // modules - this.admob = this._staticsOrModuleInstance(AdMobStatics, AdMob); - this.analytics = this._staticsOrModuleInstance(AnalyticsStatics, Analytics); - this.auth = this._staticsOrModuleInstance(AuthStatics, Auth); - this.config = this._staticsOrModuleInstance(ConfigStatics, Config); - this.crash = this._staticsOrModuleInstance(CrashStatics, Crash); - this.database = this._staticsOrModuleInstance(DatabaseStatics, Database); - this.fabric = { - crashlytics: this._staticsOrModuleInstance(CrashlyticsStatics, Crashlytics), - }; - this.firestore = this._staticsOrModuleInstance(FirestoreStatics, Firestore); - this.links = this._staticsOrModuleInstance(LinksStatics, Links); - this.messaging = this._staticsOrModuleInstance(MessagingStatics, Messaging); - this.perf = this._staticsOrModuleInstance(PerformanceStatics, Performance); - this.storage = this._staticsOrModuleInstance(StorageStatics, Storage); - this.utils = this._staticsOrModuleInstance(UtilsStatics, Utils); - this._extendedProps = {}; - } - - /** - * - * @param native - * @private - */ - _initializeApp(native: boolean = false) { - if (native) { - // for apps already initialized natively that - // we have info from RN constants + if (fromNative) { this._initialized = true; this._nativeInitialized = true; - } else { + } else if (options.databaseURL && options.apiKey) { FirebaseCoreModule.initializeApp(this._name, this._options, (error, result) => { this._initialized = true; - INTERNALS.SharedEventEmitter.emit(`AppReady:${this._name}`, { error, result }); + SharedEventEmitter.emit(`AppReady:${this._name}`, { error, result }); }); } + + // modules + this.admob = APPS.appModule(this, 'admob', AdMob); + this.analytics = APPS.appModule(this, 'analytics', Analytics); + this.auth = APPS.appModule(this, 'auth', Auth); + this.config = APPS.appModule(this, 'config', Config); + this.crash = APPS.appModule(this, 'crash', Crash); + this.database = APPS.appModule(this, 'database', Database); + this.fabric = { + crashlytics: APPS.appModule(this, 'crashlytics', Crashlytics), + }; + this.firestore = APPS.appModule(this, 'firestore', Firestore); + this.links = APPS.appModule(this, 'links', Links); + this.messaging = APPS.appModule(this, 'messaging', Messaging); + this.perf = APPS.appModule(this, 'perf', Performance); + this.storage = APPS.appModule(this, 'storage', Storage); + this.utils = APPS.appModule(this, 'utils', Utils); + this._extendedProps = {}; } /** @@ -183,38 +156,10 @@ export default class FirebaseApp { if (this._initialized) return Promise.resolve(this); return new Promise((resolve, reject) => { - INTERNALS.SharedEventEmitter.once(`AppReady:${this._name}`, ({ error }) => { + SharedEventEmitter.once(`AppReady:${this._name}`, ({ error }) => { if (error) return reject(new Error(error)); // error is a string as it's from native return resolve(this); // return app }); }); } - - /** - * - * @param statics - * @param InstanceClass - * @return {function()} - * @private - */ - _staticsOrModuleInstance(statics: S, InstanceClass: Class): FirebaseModuleAndStatics { - const getInstance = (): M => { - const _name = `_${InstanceClass._NAMESPACE}`; - - if (isAndroid && InstanceClass._NAMESPACE !== Utils._NAMESPACE && !INTERNALS.FLAGS.checkedPlayServices) { - INTERNALS.FLAGS.checkedPlayServices = true; - this.utils().checkPlayServicesAvailability(); - } - - if (!this._namespaces[_name]) { - this._namespaces[_name] = new InstanceClass(this, this._options); - } - - return this._namespaces[_name]; - }; - - return Object.assign(getInstance, statics, { - nativeModuleExists: !!NativeModules[InstanceClass._NATIVE_MODULE], - }); - } } diff --git a/lib/modules/core/firebase.js b/lib/modules/core/firebase.js index e9fe087f..4dc60e88 100644 --- a/lib/modules/core/firebase.js +++ b/lib/modules/core/firebase.js @@ -2,11 +2,11 @@ * @providesModule Firebase * @flow */ -import { NativeModules, NativeEventEmitter } from 'react-native'; +import { NativeModules } from 'react-native'; +import APPS from '../../utils/apps'; import INTERNALS from '../../utils/internals'; import FirebaseApp from './firebase-app'; -import { isObject, isString } from '../../utils'; // module imports import AdMob, { statics as AdMobStatics } from '../admob'; @@ -31,10 +31,7 @@ import type { CrashModule, DatabaseModule, FabricModule, - FirebaseModule, - FirebaseModuleAndStatics, FirebaseOptions, - FirebaseStatics, FirestoreModule, LinksModule, MessagingModule, @@ -46,8 +43,6 @@ import type { const FirebaseCoreModule = NativeModules.RNFirebase; class FirebaseCore { - _nativeEmitters: { [string]: NativeEventEmitter }; - _nativeSubscriptions: { [string]: boolean }; admob: AdMobModule; analytics: AnalyticsModule; auth: AuthModule; @@ -63,45 +58,27 @@ class FirebaseCore { utils: UtilsModule; constructor() { - this._nativeEmitters = {}; - this._nativeSubscriptions = {}; - if (!FirebaseCoreModule) { throw (new Error(INTERNALS.STRINGS.ERROR_MISSING_CORE)); } - - this._initializeNativeApps(); + APPS.initializeNativeApps(); // modules - this.admob = this._appNamespaceOrStatics(AdMobStatics, AdMob); - this.analytics = this._appNamespaceOrStatics(AnalyticsStatics, Analytics); - this.auth = this._appNamespaceOrStatics(AuthStatics, Auth); - this.config = this._appNamespaceOrStatics(ConfigStatics, Config); - this.crash = this._appNamespaceOrStatics(CrashStatics, Crash); - this.database = this._appNamespaceOrStatics(DatabaseStatics, Database); + this.admob = APPS.moduleAndStatics('admob', AdMobStatics, AdMob); + this.analytics = APPS.moduleAndStatics('analytics', AnalyticsStatics, Analytics); + this.auth = APPS.moduleAndStatics('auth', AuthStatics, Auth); + this.config = APPS.moduleAndStatics('config', ConfigStatics, Config); + this.crash = APPS.moduleAndStatics('crash', CrashStatics, Crash); + this.database = APPS.moduleAndStatics('database', DatabaseStatics, Database); this.fabric = { - crashlytics: this._appNamespaceOrStatics(CrashlyticsStatics, Crashlytics), + crashlytics: APPS.moduleAndStatics('crashlytics', CrashlyticsStatics, Crashlytics), }; - this.firestore = this._appNamespaceOrStatics(FirestoreStatics, Firestore); - this.links = this._appNamespaceOrStatics(LinksStatics, Links); - this.messaging = this._appNamespaceOrStatics(MessagingStatics, Messaging); - this.perf = this._appNamespaceOrStatics(PerformanceStatics, Performance); - this.storage = this._appNamespaceOrStatics(StorageStatics, Storage); - this.utils = this._appNamespaceOrStatics(UtilsStatics, Utils); - } - - /** - * Bootstraps all native app instances that were discovered on boot - * @private - */ - _initializeNativeApps() { - for (let i = 0, len = FirebaseCoreModule.apps.length; i < len; i++) { - const app = FirebaseCoreModule.apps[i]; - const options = Object.assign({}, app); - delete options.name; - INTERNALS.APPS[app.name] = new FirebaseApp(app.name, options); - INTERNALS.APPS[app.name]._initializeApp(true); - } + this.firestore = APPS.moduleAndStatics('firestore', FirestoreStatics, Firestore); + this.links = APPS.moduleAndStatics('links', LinksStatics, Links); + this.messaging = APPS.moduleAndStatics('messaging', MessagingStatics, Messaging); + this.perf = APPS.moduleAndStatics('perf', PerformanceStatics, Performance); + this.storage = APPS.moduleAndStatics('storage', StorageStatics, Storage); + this.utils = APPS.moduleAndStatics('utils', UtilsStatics, Utils); } /** @@ -112,57 +89,7 @@ class FirebaseCore { * @return {*} */ initializeApp(options: FirebaseOptions, name: string): FirebaseApp { - if (name && !isString(name)) { - throw new Error(INTERNALS.STRINGS.ERROR_INIT_STRING_NAME); - } - - const _name = (name || INTERNALS.STRINGS.DEFAULT_APP_NAME).toUpperCase(); - - // return an existing app if found - // todo in v4 remove deprecation and throw an error - if (INTERNALS.APPS[_name]) { - console.warn(INTERNALS.STRINGS.WARN_INITIALIZE_DEPRECATION); - return INTERNALS.APPS[_name]; - } - - // only validate if app doesn't already exist - // to allow apps already initialized natively - // to still go through init without erroring (backwards compatibility) - if (!isObject(options)) { - throw new Error(INTERNALS.STRINGS.ERROR_INIT_OBJECT); - } - - if (!options.apiKey) { - throw new Error(INTERNALS.STRINGS.ERROR_MISSING_OPT('apiKey')); - } - - if (!options.appId) { - throw new Error(INTERNALS.STRINGS.ERROR_MISSING_OPT('appId')); - } - - if (!options.databaseURL) { - throw new Error(INTERNALS.STRINGS.ERROR_MISSING_OPT('databaseURL')); - } - - if (!options.messagingSenderId) { - throw new Error(INTERNALS.STRINGS.ERROR_MISSING_OPT('messagingSenderId')); - } - - if (!options.projectId) { - throw new Error(INTERNALS.STRINGS.ERROR_MISSING_OPT('projectId')); - } - - if (!options.storageBucket) { - throw new Error(INTERNALS.STRINGS.ERROR_MISSING_OPT('storageBucket')); - } - - INTERNALS.APPS[_name] = new FirebaseApp(_name, options); - // only initialize if certain props are available - if (options.databaseURL && options.apiKey) { - INTERNALS.APPS[_name]._initializeApp(); - } - - return INTERNALS.APPS[_name]; + return APPS.initializeApp(options, name); } /** @@ -175,10 +102,7 @@ class FirebaseCore { * @return {*} */ app(name?: string): FirebaseApp { - const _name = name ? name.toUpperCase() : INTERNALS.STRINGS.DEFAULT_APP_NAME; - const app = INTERNALS.APPS[_name]; - if (!app) throw new Error(INTERNALS.STRINGS.ERROR_APP_NOT_INIT(_name)); - return app; + return APPS.app(name); } /** @@ -186,83 +110,7 @@ class FirebaseCore { * @return {Array} */ get apps(): Array { - return Object.values(INTERNALS.APPS); - } - - /* - * INTERNALS - */ - - /** - * Subscribe to a native event for js side distribution by appName - * React Native events are hard set at compile - cant do dynamic event names - * so we use a single event send it to js and js then internally can prefix it - * and distribute dynamically. - * - * @param eventName - * @param nativeEmitter - * @private - */ - _subscribeForDistribution(eventName: string, nativeEmitter: NativeEventEmitter) { - if (!this._nativeSubscriptions[eventName]) { - nativeEmitter.addListener(eventName, (event) => { - if (event.appName) { - // native event has an appName property - auto prefix and internally emit - INTERNALS.SharedEventEmitter.emit(`${event.appName}-${eventName}`, event); - } else { - // standard event - no need to prefix - INTERNALS.SharedEventEmitter.emit(eventName, event); - } - }); - - this._nativeSubscriptions[eventName] = true; - } - } - - /** - * - * @param statics - * @param InstanceClass - * @return {function(FirebaseApp=)} - * @private - */ - _appNamespaceOrStatics(statics: S, InstanceClass: Class): FirebaseModuleAndStatics { - const namespace = InstanceClass._NAMESPACE; - - const getNamespace = (app?: FirebaseApp) => { - let _app = app; - - // throw an error if it's not a valid app instance - if (_app && !(_app instanceof FirebaseApp)) throw new Error(INTERNALS.STRINGS.ERROR_NOT_APP(namespace)); - - // default to the 'DEFAULT' app if no arg provided - will throw an error - // if default app not initialized - else if (!_app) _app = this.app(INTERNALS.STRINGS.DEFAULT_APP_NAME); - const firebaseApp = INTERNALS.APPS[_app._name]; - if (namespace === 'crashlytics') { - return firebaseApp.fabric[namespace](_app); - } - return firebaseApp[namespace](_app); - }; - - return Object.assign(getNamespace, statics, { - nativeModuleExists: !!NativeModules[InstanceClass._NATIVE_MODULE], - }); - } - - /** - * - * @param name - * @param nativeModule - * @return {*} - * @private - */ - _getOrSetNativeEmitter(name: string, nativeModule: Object): NativeEventEmitter { - if (this._nativeEmitters[name]) { - return this._nativeEmitters[name]; - } - - return this._nativeEmitters[name] = new NativeEventEmitter(nativeModule); + return APPS.apps(); } } diff --git a/lib/modules/database/reference.js b/lib/modules/database/reference.js index ee0f6283..43d61b9f 100644 --- a/lib/modules/database/reference.js +++ b/lib/modules/database/reference.js @@ -5,6 +5,7 @@ import Query from './query.js'; import Snapshot from './snapshot'; import Disconnect from './disconnect'; +import { getLogger } from '../../utils/log'; import ReferenceBase from '../../utils/ReferenceBase'; import { @@ -19,6 +20,7 @@ import { import INTERNALS from '../../utils/internals'; +import type Database from './'; import type { DatabaseModifier, FirebaseError } from '../../types'; import type SyncTree from '../../utils/SyncTree'; @@ -73,18 +75,18 @@ type DatabaseListener = { * @extends ReferenceBase */ export default class Reference extends ReferenceBase { - _database: Object; + _database: Database; _promise: ?Promise<*>; _query: Query; _refListeners: { [listenerId: number]: DatabaseListener }; - constructor(database: Object, path: string, existingModifiers?: Array) { + constructor(database: Database, path: string, existingModifiers?: Array) { super(path, database); this._promise = null; this._refListeners = {}; this._database = database; this._query = new Query(this, path, existingModifiers); - this.log.debug('Created new Reference', this._getRefKey()); + getLogger(database).debug('Created new Reference', this._getRefKey()); } /** @@ -549,13 +551,6 @@ export default class Reference extends ReferenceBase { return `$${this._database._appName}$/${this.path}$${this._query.queryIdentifier()}`; } - /** - * Return instance of db logger - */ - get log() { - return this._database.log; - } - /** * Set the promise this 'thenable' reference relates to * @param promise diff --git a/lib/modules/database/transaction.js b/lib/modules/database/transaction.js index d84a1d26..79d7bcf3 100644 --- a/lib/modules/database/transaction.js +++ b/lib/modules/database/transaction.js @@ -2,6 +2,8 @@ * @flow * Database Transaction representation wrapper */ +import { getAppEventName, SharedEventEmitter } from '../../utils/events'; +import { getLogger } from '../../utils/log'; import type Database from './'; let transactionId = 0; @@ -18,8 +20,8 @@ export default class TransactionHandler { this._transactions = {}; this._database = database; - this._transactionListener = this._database.addListener( - this._database._getAppEventName('database_transaction_event'), + this._transactionListener = SharedEventEmitter.addListener( + getAppEventName(this._database, 'database_transaction_event'), this._handleTransactionEvent.bind(this), ); } @@ -75,7 +77,7 @@ export default class TransactionHandler { case 'complete': return this._handleComplete(event); default: - this.log.warn(`Unknown transaction event type: '${event.type}'`, event); + getLogger(this._database).warn(`Unknown transaction event type: '${event.type}'`, event); return undefined; } } diff --git a/lib/modules/firestore/DocumentReference.js b/lib/modules/firestore/DocumentReference.js index 97d7c4ae..ae42c758 100644 --- a/lib/modules/firestore/DocumentReference.js +++ b/lib/modules/firestore/DocumentReference.js @@ -5,6 +5,7 @@ import CollectionReference from './CollectionReference'; import DocumentSnapshot from './DocumentSnapshot'; import { buildNativeMap } from './utils/serialize'; +import { getAppEventName, SharedEventEmitter } from '../../utils/events'; import { firestoreAutoId, isFunction, isObject, isString } from '../../utils'; import type Firestore from './'; @@ -136,15 +137,15 @@ export default class DocumentReference { }; // Listen to snapshot events - this._firestore.on( - this._firestore._getAppEventName(`onDocumentSnapshot:${listenerId}`), + SharedEventEmitter.addListener( + getAppEventName(this._firestore, `onDocumentSnapshot:${listenerId}`), listener, ); // Listen for snapshot error events if (observer.error) { - this._firestore.on( - this._firestore._getAppEventName(`onDocumentSnapshotError:${listenerId}`), + SharedEventEmitter.addListener( + getAppEventName(this._firestore, `onDocumentSnapshotError:${listenerId}`), observer.error, ); } @@ -197,8 +198,8 @@ export default class DocumentReference { */ _offDocumentSnapshot(listenerId: string, listener: Function) { this._firestore.log.info('Removing onDocumentSnapshot listener'); - this._firestore.removeListener(this._firestore._getAppEventName(`onDocumentSnapshot:${listenerId}`), listener); - this._firestore.removeListener(this._firestore._getAppEventName(`onDocumentSnapshotError:${listenerId}`), listener); + SharedEventEmitter.removeListener(getAppEventName(this._firestore, `onDocumentSnapshot:${listenerId}`), listener); + SharedEventEmitter.removeListener(getAppEventName(this._firestore, `onDocumentSnapshotError:${listenerId}`), listener); this._firestore._native .documentOffSnapshot(this.path, listenerId); } diff --git a/lib/modules/firestore/Query.js b/lib/modules/firestore/Query.js index af11cae4..99846818 100644 --- a/lib/modules/firestore/Query.js +++ b/lib/modules/firestore/Query.js @@ -5,6 +5,7 @@ import DocumentSnapshot from './DocumentSnapshot'; import QuerySnapshot from './QuerySnapshot'; import { buildNativeArray, buildTypeMap } from './utils/serialize'; +import { getAppEventName, SharedEventEmitter } from '../../utils/events'; import { firestoreAutoId, isFunction, isObject } from '../../utils'; import type Firestore from './'; @@ -206,15 +207,15 @@ export default class Query { }; // Listen to snapshot events - this._firestore.on( - this._firestore._getAppEventName(`onQuerySnapshot:${listenerId}`), + SharedEventEmitter.addListener( + getAppEventName(this._firestore, `onQuerySnapshot:${listenerId}`), listener, ); // Listen for snapshot error events if (observer.error) { - this._firestore.on( - this._firestore._getAppEventName(`onQuerySnapshotError:${listenerId}`), + SharedEventEmitter.addListener( + getAppEventName(this._firestore, `onQuerySnapshotError:${listenerId}`), observer.error, ); } @@ -335,8 +336,8 @@ export default class Query { */ _offCollectionSnapshot(listenerId: string, listener: Function) { this._firestore.log.info('Removing onQuerySnapshot listener'); - this._firestore.removeListener(this._firestore._getAppEventName(`onQuerySnapshot:${listenerId}`), listener); - this._firestore.removeListener(this._firestore._getAppEventName(`onQuerySnapshotError:${listenerId}`), listener); + SharedEventEmitter.removeListener(getAppEventName(this._firestore, `onQuerySnapshot:${listenerId}`), listener); + SharedEventEmitter.removeListener(getAppEventName(this._firestore, `onQuerySnapshotError:${listenerId}`), listener); this._firestore._native .collectionOffSnapshot( this._referencePath.relativeName, diff --git a/lib/modules/firestore/index.js b/lib/modules/firestore/index.js index ac624ae4..78c94e3d 100644 --- a/lib/modules/firestore/index.js +++ b/lib/modules/firestore/index.js @@ -4,7 +4,8 @@ */ import { NativeModules } from 'react-native'; -import ModuleBase from './../../utils/ModuleBase'; +import { getAppEventName, SharedEventEmitter } from '../../utils/events'; +import ModuleBase from '../../utils/ModuleBase'; import CollectionReference from './CollectionReference'; import DocumentReference from './DocumentReference'; import FieldValue from './FieldValue'; @@ -33,7 +34,10 @@ type DocumentSyncEvent = { path: string, } -class FirestoreInternalModule extends ModuleBase { +/** + * @class Firestore + */ +export default class Firestore extends ModuleBase { static _NAMESPACE = 'firestore'; static _NATIVE_MODULE = 'RNFirebaseFirestore'; @@ -43,56 +47,21 @@ class FirestoreInternalModule extends ModuleBase { super(firebaseApp, options, true); this._referencePath = new Path([]); - super.addListener( + SharedEventEmitter.addListener( // sub to internal native event - this fans out to // public event name: onCollectionSnapshot - super._getAppEventName('firestore_collection_sync_event'), + getAppEventName(this, 'firestore_collection_sync_event'), this._onCollectionSyncEvent.bind(this), ); - super.addListener( + SharedEventEmitter.addListener( // sub to internal native event - this fans out to // public event name: onDocumentSnapshot - super._getAppEventName('firestore_document_sync_event'), + getAppEventName(this, 'firestore_document_sync_event'), this._onDocumentSyncEvent.bind(this), ); } - /** - * Internal collection sync listener - * @param event - * @private - */ - _onCollectionSyncEvent(event: CollectionSyncEvent) { - if (event.error) { - this.emit(super._getAppEventName(`onQuerySnapshotError:${event.listenerId}`), event.error); - } else { - this.emit(super._getAppEventName(`onQuerySnapshot:${event.listenerId}`), event.querySnapshot); - } - } - - /** - * Internal document sync listener - * @param event - * @private - */ - _onDocumentSyncEvent(event: DocumentSyncEvent) { - if (event.error) { - this.emit(super._getAppEventName(`onDocumentSnapshotError:${event.listenerId}`), event.error); - } else { - this.emit(super._getAppEventName(`onDocumentSnapshot:${event.listenerId}`), event.documentSnapshot); - } - } -} - -/** - * @class Firestore - */ -export default class Firestore extends FirestoreInternalModule { - constructor(firebaseApp: FirebaseApp, options: Object = {}) { - super(firebaseApp, options); - } - batch(): WriteBatch { return new WriteBatch(this); } @@ -140,6 +109,32 @@ export default class Firestore extends FirestoreInternalModule { settings(): void { throw new Error('firebase.firestore().settings() coming soon'); } + + /** + * Internal collection sync listener + * @param event + * @private + */ + _onCollectionSyncEvent(event: CollectionSyncEvent) { + if (event.error) { + SharedEventEmitter.emit(getAppEventName(this, `onQuerySnapshotError:${event.listenerId}`), event.error); + } else { + SharedEventEmitter.emit(getAppEventName(this, `onQuerySnapshot:${event.listenerId}`), event.querySnapshot); + } + } + + /** + * Internal document sync listener + * @param event + * @private + */ + _onDocumentSyncEvent(event: DocumentSyncEvent) { + if (event.error) { + SharedEventEmitter.emit(getAppEventName(this, `onDocumentSnapshotError:${event.listenerId}`), event.error); + } else { + SharedEventEmitter.emit(getAppEventName(this, `onDocumentSnapshot:${event.listenerId}`), event.documentSnapshot); + } + } } export const statics = { diff --git a/lib/modules/links/index.js b/lib/modules/links/index.js index 68b332bb..c4c26e6f 100644 --- a/lib/modules/links/index.js +++ b/lib/modules/links/index.js @@ -2,7 +2,8 @@ * @flow * Dynamic Links representation wrapper */ -import ModuleBase from './../../utils/ModuleBase'; +import { SharedEventEmitter } from '../../utils/events'; +import ModuleBase from '../../utils/ModuleBase'; import { areObjectKeysContainedInOther, isObject, isString } from './../../utils'; import type FirebaseApp from '../core/firebase-app'; @@ -87,7 +88,7 @@ export default class Links extends ModuleBase { * @returns {Function} */ onLink(listener: Function): () => any { - const rnListener = this._eventEmitter.addListener(EVENT_TYPE.Link, listener); + const rnListener = SharedEventEmitter.addListener(EVENT_TYPE.Link, listener); return () => rnListener.remove(); } diff --git a/lib/modules/messaging/index.js b/lib/modules/messaging/index.js index 595ab402..4a631137 100644 --- a/lib/modules/messaging/index.js +++ b/lib/modules/messaging/index.js @@ -3,7 +3,8 @@ * Messaging representation wrapper */ import { Platform, NativeModules } from 'react-native'; -import ModuleBase from './../../utils/ModuleBase'; +import { SharedEventEmitter } from '../../utils/events'; +import ModuleBase from '../../utils/ModuleBase'; import RemoteMessage from './RemoteMessage'; import type FirebaseApp from '../core/firebase-app'; @@ -213,7 +214,7 @@ export default class Messaging extends ModuleBase { * @returns {*} */ onMessage(listener: (Object) => any): () => any { - const rnListener = this._eventEmitter.addListener( + const rnListener = SharedEventEmitter.addListener( EVENT_TYPE.Notification, async (event) => { const data = { @@ -236,7 +237,7 @@ export default class Messaging extends ModuleBase { * @returns {*} */ onTokenRefresh(listener: (string) => any): () => any { - const rnListener = this._eventEmitter.addListener(EVENT_TYPE.RefreshToken, listener); + const rnListener = SharedEventEmitter.addListener(EVENT_TYPE.RefreshToken, listener); return () => rnListener.remove(); } diff --git a/lib/modules/storage/index.js b/lib/modules/storage/index.js index fdf5f520..a8e12c87 100644 --- a/lib/modules/storage/index.js +++ b/lib/modules/storage/index.js @@ -5,6 +5,8 @@ import { NativeModules } from 'react-native'; import StorageRef from './reference'; +import { getAppEventName, SharedEventEmitter } from '../../utils/events'; +import { getLogger } from '../../utils/log'; import ModuleBase from './../../utils/ModuleBase'; import type FirebaseApp from '../core/firebase-app'; @@ -23,13 +25,13 @@ export default class Storage extends ModuleBase { constructor(firebaseApp: FirebaseApp, options: Object = {}) { super(firebaseApp, options, true); - this.addListener( - this._getAppEventName('storage_event'), + SharedEventEmitter.addListener( + getAppEventName(this, 'storage_event'), this._handleStorageEvent.bind(this), ); - this.addListener( - this._getAppEventName('storage_error'), + SharedEventEmitter.addListener( + getAppEventName(this, 'storage_error'), this._handleStorageEvent.bind(this), ); } @@ -86,31 +88,31 @@ export default class Storage extends ModuleBase { * INTERNALS */ _getSubEventName(path: string, eventName: string) { - return this._getAppEventName(`${path}-${eventName}`); + return getAppEventName(this, `${path}-${eventName}`); } _handleStorageEvent(event: Object) { const { path, eventName } = event; const body = event.body || {}; - this.log.debug('_handleStorageEvent: ', path, eventName, body); - this.emit(this._getSubEventName(path, eventName), body); + getLogger(this).debug('_handleStorageEvent: ', path, eventName, body); + SharedEventEmitter.emit(this._getSubEventName(path, eventName), body); } _handleStorageError(err: Object) { const { path, eventName } = err; const body = err.body || {}; - this.log.debug('_handleStorageError ->', err); - this.emit(this._getSubEventName(path, eventName), body); + getLogger(this).debug('_handleStorageError ->', err); + SharedEventEmitter.emit(this._getSubEventName(path, eventName), body); } _addListener(path: string, eventName: string, cb: (evt: Object) => Object): void { - this.on(this._getSubEventName(path, eventName), cb); + SharedEventEmitter.addListener(this._getSubEventName(path, eventName), cb); } _removeListener(path: string, eventName: string, origCB: (evt: Object) => Object): void { - this.removeListener(this._getSubEventName(path, eventName), origCB); + SharedEventEmitter.removeListener(this._getSubEventName(path, eventName), origCB); } } diff --git a/lib/modules/utils/index.js b/lib/modules/utils/index.js index c7325462..e72f9beb 100644 --- a/lib/modules/utils/index.js +++ b/lib/modules/utils/index.js @@ -63,10 +63,6 @@ export default class RNFirebaseUtils extends ModuleBase { return FirebaseCoreModule.makePlayServicesAvailable(); } - get sharedEventEmitter(): Object { - return INTERNALS.SharedEventEmitter; - } - /** * Set the global logging level for all logs. * diff --git a/lib/utils/ModuleBase.js b/lib/utils/ModuleBase.js index 0f89825a..1b432583 100644 --- a/lib/utils/ModuleBase.js +++ b/lib/utils/ModuleBase.js @@ -1,45 +1,12 @@ /** * @flow */ -import { NativeModules } from 'react-native'; - -import Log from './log'; -import INTERNALS from './internals'; -import FirebaseCore from '../modules/core/firebase'; -import { nativeWithApp } from '../utils'; +import { initialiseNativeModuleEventEmitter } from './events'; +import { getNativeModule, initialiseNativeModule } from './native'; import type FirebaseApp from '../modules/core/firebase-app'; import type { FirebaseModuleName } from '../types'; -const logs = {}; - -// Firebase Native SDKs that support multiple app instances -const MULTI_APP_MODULES = [ - 'auth', - 'database', - 'firestore', - 'storage', -]; - -const NATIVE_MODULE_EVENTS = { - Storage: [ - 'storage_event', - 'storage_error', - ], - Auth: [ - 'auth_state_changed', - 'phone_auth_state_changed', - ], - Database: [ - 'database_transaction_event', - // 'database_server_offset', // TODO - ], - Firestore: [ - 'firestore_collection_sync_event', - 'firestore_document_sync_event', - ], -}; - const DEFAULTS = { Database: { persistence: false, @@ -53,7 +20,6 @@ export default class ModuleBase { _appName: string; _namespace: string; _firebaseApp: FirebaseApp; - _eventEmitter: Object; static _NAMESPACE: FirebaseModuleName; static _NATIVE_MODULE: string; @@ -71,53 +37,15 @@ export default class ModuleBase { this._options = Object.assign({}, DEFAULTS[this._module] || {}, options); // check if native module exists as all native - // modules are now optionally part of build - const nativeModule = NativeModules[this.constructor._NATIVE_MODULE]; - - if (!nativeModule && !this.constructor._NATIVE_DISABLED) { - throw new Error(INTERNALS.STRINGS.ERROR_MISSING_MODULE(this.constructor._NAMESPACE, this.constructor._NATIVE_MODULE)); - } - - // used by the modules that extend ModuleBase - // to access their native module counterpart - if (!MULTI_APP_MODULES.includes(this._module.toLowerCase())) { - this._native = nativeModule; - } else { - this._native = nativeWithApp(this._appName, nativeModule); - } + initialiseNativeModule(this); + // TODO: Get rid of + this._native = getNativeModule(this); if (withEventEmitter) { - this._setupEventEmitter(nativeModule, this._module); + initialiseNativeModuleEventEmitter(this); } } - /** - * - * @param nativeModule - * @param moduleName - * @private - */ - _setupEventEmitter(nativeModule: Object, moduleName: string) { - this._eventEmitter = FirebaseCore._getOrSetNativeEmitter(`${this._appName}-${this._module}`, nativeModule); - const events = NATIVE_MODULE_EVENTS[moduleName]; - - if (events && events.length) { - for (let i = 0, len = events.length; i < len; i++) { - FirebaseCore._subscribeForDistribution(events[i], this._eventEmitter); - } - } - } - - /** - * - * @param eventName - * @return {string} - * @private - */ - _getAppEventName(eventName: string) { - return `${this._appName}-${eventName}`; - } - /** * Returns the FirebaseApp instance for current module * @return {*} @@ -125,50 +53,4 @@ export default class ModuleBase { get app(): FirebaseApp { return this._firebaseApp; } - - get log(): Log { - if (logs[this._namespace]) return logs[this._namespace]; - return logs[this._namespace] = Log.createLogger(`🔥 ${this._namespace.toUpperCase()}`); - } - - /* - * Proxy functions to shared event emitter instance - * https://github.com/facebook/react-native/blob/master/Libraries/EventEmitter/EventEmitter.js - */ - get sharedEventEmitter(): Object { - return INTERNALS.SharedEventEmitter; - } - - get addListener(): Function { - return INTERNALS.SharedEventEmitter.addListener.bind(INTERNALS.SharedEventEmitter); - } - - get once(): Function { - return INTERNALS.SharedEventEmitter.once.bind(INTERNALS.SharedEventEmitter); - } - - get on(): Function { - return INTERNALS.SharedEventEmitter.addListener.bind(INTERNALS.SharedEventEmitter); - } - - get emit(): Function { - return INTERNALS.SharedEventEmitter.emit.bind(INTERNALS.SharedEventEmitter); - } - - get listeners(): Function { - return INTERNALS.SharedEventEmitter.listeners.bind(INTERNALS.SharedEventEmitter); - } - - hasListeners(eventType: string): Boolean { - const subscriptions = INTERNALS.SharedEventEmitter._subscriber.getSubscriptionsForType(eventType); - return subscriptions && subscriptions.length; - } - - get removeListener(): Function { - return INTERNALS.SharedEventEmitter.removeListener.bind(INTERNALS.SharedEventEmitter); - } - - get removeAllListeners(): Function { - return INTERNALS.SharedEventEmitter.removeAllListeners.bind(INTERNALS.SharedEventEmitter); - } } diff --git a/lib/utils/ReferenceBase.js b/lib/utils/ReferenceBase.js index 4d0daf4e..631d23a1 100644 --- a/lib/utils/ReferenceBase.js +++ b/lib/utils/ReferenceBase.js @@ -1,8 +1,6 @@ /** * @flow */ -import Log from './log'; - import type Database from '../modules/database'; import type Storage from '../modules/storage'; @@ -24,8 +22,4 @@ export default class ReferenceBase { get key(): string | null { return this.path === '/' ? null : this.path.substring(this.path.lastIndexOf('/') + 1); } - - get log(): Log { - return this._module.log; - } } diff --git a/lib/utils/SyncTree.js b/lib/utils/SyncTree.js index 6cd4acb1..a1849bb3 100644 --- a/lib/utils/SyncTree.js +++ b/lib/utils/SyncTree.js @@ -3,7 +3,7 @@ */ import { NativeEventEmitter } from 'react-native'; -import INTERNALS from './internals'; +import { SharedEventEmitter } from './events'; import DatabaseSnapshot from '../modules/database/snapshot'; import DatabaseReference from '../modules/database/reference'; import { isString, nativeToJSError } from '../utils'; @@ -77,7 +77,7 @@ export default class SyncTree { const { snapshot, previousChildName } = event.data; // forward on to users .on(successCallback <-- listener - return INTERNALS.SharedEventEmitter.emit( + return SharedEventEmitter.emit( eventRegistrationKey, new DatabaseSnapshot(registration.ref, snapshot), previousChildName, @@ -104,7 +104,7 @@ export default class SyncTree { const error = nativeToJSError(code, message, { ref: registration.ref }); // forward on to users .on(successCallback, cancellationCallback <-- listener - INTERNALS.SharedEventEmitter.emit(registrationCancellationKey, error); + SharedEventEmitter.emit(registrationCancellationKey, error); // remove the paired event registration - if we received a cancellation // event then it's guaranteed that they'll be no further value events @@ -131,14 +131,14 @@ export default class SyncTree { removeListenersForRegistrations(registrations: string | string[]) { if (isString(registrations)) { this.removeRegistration(registrations); - INTERNALS.SharedEventEmitter.removeAllListeners(registrations); + SharedEventEmitter.removeAllListeners(registrations); return 1; } if (!Array.isArray(registrations)) return 0; for (let i = 0, len = registrations.length; i < len; i++) { this.removeRegistration(registrations[i]); - INTERNALS.SharedEventEmitter.removeAllListeners(registrations[i]); + SharedEventEmitter.removeAllListeners(registrations[i]); } return registrations.length; @@ -157,7 +157,7 @@ export default class SyncTree { for (let i = 0, len = registrations.length; i < len; i++) { const registration = registrations[i]; - const subscriptions = INTERNALS.SharedEventEmitter._subscriber.getSubscriptionsForType(registration); + const subscriptions = SharedEventEmitter._subscriber.getSubscriptionsForType(registration); if (subscriptions) { for (let j = 0, l = subscriptions.length; j < l; j++) { const subscription = subscriptions[j]; @@ -251,12 +251,12 @@ export default class SyncTree { this._reverseLookup[eventRegistrationKey] = Object.assign({ listener }, parameters); if (once) { - INTERNALS.SharedEventEmitter.once( + SharedEventEmitter.once( eventRegistrationKey, this._onOnceRemoveRegistration(eventRegistrationKey, listener), ); } else { - INTERNALS.SharedEventEmitter.addListener(eventRegistrationKey, listener); + SharedEventEmitter.addListener(eventRegistrationKey, listener); } return eventRegistrationKey; diff --git a/lib/utils/apps.js b/lib/utils/apps.js new file mode 100644 index 00000000..2d39e0f6 --- /dev/null +++ b/lib/utils/apps.js @@ -0,0 +1,167 @@ +/** + * @flow + */ +import { NativeModules } from 'react-native'; +import FirebaseApp from '../modules/core/firebase-app'; +import INTERNALS from './internals'; +import { isAndroid, isObject, isString } from './'; + +import type { + FirebaseModule, + FirebaseModuleAndStatics, + FirebaseModuleName, + FirebaseOptions, + FirebaseStatics, +} from '../types'; + +const FirebaseCoreModule = NativeModules.RNFirebase; + +const APPS: { [string]: FirebaseApp } = {}; +const APP_MODULES: { [FirebaseApp]: { [string]: FirebaseModule }} = {}; + +export default { + app(name?: string): FirebaseApp { + const _name = name ? name.toUpperCase() : INTERNALS.STRINGS.DEFAULT_APP_NAME; + const app = APPS[_name]; + if (!app) throw new Error(INTERNALS.STRINGS.ERROR_APP_NOT_INIT(_name)); + return app; + }, + + apps(): Array { + return Object.values(APPS); + }, + + /** + * + * @param statics + * @param InstanceClass + * @return {function()} + * @private + */ + appModule(firebaseApp: FirebaseApp, moduleName: FirebaseModuleName, InstanceClass: Class): () => FirebaseModule { + return (): M => { + if (!APP_MODULES[firebaseApp]) { + APP_MODULES[firebaseApp] = {}; + } + + if (isAndroid && moduleName !== 'utils' && !INTERNALS.FLAGS.checkedPlayServices) { + INTERNALS.FLAGS.checkedPlayServices = true; + this.utils().checkPlayServicesAvailability(); + } + + if (!APP_MODULES[firebaseApp][moduleName]) { + APP_MODULES[firebaseApp][moduleName] = new InstanceClass(firebaseApp, this._options); + } + + return APP_MODULES[firebaseApp][moduleName]; + }; + }, + + deleteApp(name: string): Promise { + const app = APPS[name]; + if (!app) return Promise.resolve(true); + + // https://firebase.google.com/docs/reference/js/firebase.app.App#delete + return app.delete().then(() => { + delete APPS[name]; + return true; + }); + }, + + /** + * Web SDK initializeApp + * + * @param options + * @param name + * @return {*} + */ + initializeApp(options: FirebaseOptions, name: string): FirebaseApp { + if (name && !isString(name)) { + throw new Error(INTERNALS.STRINGS.ERROR_INIT_STRING_NAME); + } + + const _name = (name || INTERNALS.STRINGS.DEFAULT_APP_NAME).toUpperCase(); + + // return an existing app if found + // todo in v4 remove deprecation and throw an error + if (APPS[_name]) { + console.warn(INTERNALS.STRINGS.WARN_INITIALIZE_DEPRECATION); + return APPS[_name]; + } + + // only validate if app doesn't already exist + // to allow apps already initialized natively + // to still go through init without erroring (backwards compatibility) + if (!isObject(options)) { + throw new Error(INTERNALS.STRINGS.ERROR_INIT_OBJECT); + } + + if (!options.apiKey) { + throw new Error(INTERNALS.STRINGS.ERROR_MISSING_OPT('apiKey')); + } + + if (!options.appId) { + throw new Error(INTERNALS.STRINGS.ERROR_MISSING_OPT('appId')); + } + + if (!options.databaseURL) { + throw new Error(INTERNALS.STRINGS.ERROR_MISSING_OPT('databaseURL')); + } + + if (!options.messagingSenderId) { + throw new Error(INTERNALS.STRINGS.ERROR_MISSING_OPT('messagingSenderId')); + } + + if (!options.projectId) { + throw new Error(INTERNALS.STRINGS.ERROR_MISSING_OPT('projectId')); + } + + if (!options.storageBucket) { + throw new Error(INTERNALS.STRINGS.ERROR_MISSING_OPT('storageBucket')); + } + + APPS[_name] = new FirebaseApp(_name, options); + + return APPS[_name]; + }, + + /** + * Bootstraps all native app instances that were discovered on boot + */ + initializeNativeApps() { + for (let i = 0, len = FirebaseCoreModule.apps.length; i < len; i++) { + const app = FirebaseCoreModule.apps[i]; + const options = Object.assign({}, app); + delete options.name; + APPS[app.name] = new FirebaseApp(app.name, options, true); + } + }, + + /** + * + * @param statics + * @param InstanceClass + * @return {function(FirebaseApp=)} + */ + moduleAndStatics(moduleName: FirebaseModuleName, statics: S, InstanceClass: Class): FirebaseModuleAndStatics { + const getModule = (app?: FirebaseApp): FirebaseModule => { + let firebaseApp = app; + + // throw an error if it's not a valid app instance + if (firebaseApp && !(firebaseApp instanceof FirebaseApp)) throw new Error(INTERNALS.STRINGS.ERROR_NOT_APP(moduleName)); + + // default to the 'DEFAULT' app if no arg provided - will throw an error + // if default app not initialized + else if (!firebaseApp) firebaseApp = this.app(INTERNALS.STRINGS.DEFAULT_APP_NAME); + if (moduleName === 'crashlytics') { + return firebaseApp.fabric[moduleName](); + } + const module = firebaseApp[moduleName]; + return module(); + }; + + return Object.assign(getModule, statics, { + nativeModuleExists: !!NativeModules[InstanceClass._NATIVE_MODULE], + }); + }, +}; diff --git a/lib/utils/events.js b/lib/utils/events.js new file mode 100644 index 00000000..07a0e1e3 --- /dev/null +++ b/lib/utils/events.js @@ -0,0 +1,81 @@ +/** + * @flow + */ +import { NativeEventEmitter } from 'react-native'; +import EventEmitter from './emitter/EventEmitter'; +import { getRawNativeModule } from './native'; + +import type ModuleBase from './ModuleBase'; + +const NATIVE_MODULE_EVENTS = { + Storage: [ + 'storage_event', + 'storage_error', + ], + Auth: [ + 'auth_state_changed', + 'phone_auth_state_changed', + ], + Database: [ + 'database_transaction_event', + // 'database_server_offset', // TODO + ], + Firestore: [ + 'firestore_collection_sync_event', + 'firestore_document_sync_event', + ], +}; +const NATIVE_EMITTERS: { [string]: NativeEventEmitter } = {}; +const NATIVE_SUBSCRIPTIONS: { [string]: boolean } = {}; + +export const SharedEventEmitter = new EventEmitter(); + +export const getAppEventName = (module: ModuleBase, eventName: string): string => { + return `${module._firebaseApp._name}-${eventName}`; +}; + +const getNativeEmitter = (module: ModuleBase): NativeEventEmitter => { + const name = `${module._appName}-${module._module}`; + const nativeModule = getRawNativeModule(module); + if (!NATIVE_EMITTERS[name]) { + NATIVE_EMITTERS[name] = new NativeEventEmitter(nativeModule); + } + return NATIVE_EMITTERS[name]; +}; + +/** + * Subscribe to a native event for js side distribution by appName + * React Native events are hard set at compile - cant do dynamic event names + * so we use a single event send it to js and js then internally can prefix it + * and distribute dynamically. + * + * @param module + * @param eventName + * @private + */ +const subscribeToNativeModuleEvents = (module: ModuleBase, eventName: string): void => { + if (!NATIVE_SUBSCRIPTIONS[eventName]) { + const nativeEmitter = getNativeEmitter(module); + nativeEmitter.addListener(eventName, (event) => { + if (event.appName) { + // native event has an appName property - auto prefix and internally emit + SharedEventEmitter.emit(`${event.appName}-${eventName}`, event); + } else { + // standard event - no need to prefix + SharedEventEmitter.emit(eventName, event); + } + }); + + NATIVE_SUBSCRIPTIONS[eventName] = true; + } +}; + +export const initialiseNativeModuleEventEmitter = (module: ModuleBase): void => { + const events = NATIVE_MODULE_EVENTS[module._module]; + + if (events && events.length) { + for (let i = 0, len = events.length; i < len; i++) { + subscribeToNativeModuleEvents(module, events[i]); + } + } +}; diff --git a/lib/utils/index.js b/lib/utils/index.js index 6f619e4e..cea0b5d2 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -340,25 +340,6 @@ export function nativeToJSError(code: string, message: string, additionalProps?: return error; } -/** - * Prepends appName arg to all native method calls - * @param appName - * @param NativeModule - */ -export function nativeWithApp(appName: string, NativeModule: 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) => { - return NativeModule[method](...[appName, ...args]); - }; - } - - return native; -} - /** * * @param object diff --git a/lib/utils/internals.js b/lib/utils/internals.js index efab8da1..53bbc73c 100644 --- a/lib/utils/internals.js +++ b/lib/utils/internals.js @@ -3,12 +3,9 @@ */ import { Platform, NativeModules } from 'react-native'; -import EventEmitter from './emitter/EventEmitter'; import ModuleBase from './ModuleBase'; import SyncTree from './SyncTree'; -import type FirebaseApp from '../modules/core/firebase-app'; - const DEFAULT_APP_NAME = Platform.OS === 'ios' ? '__FIRAPP_DEFAULT' : '[DEFAULT]'; const NAMESPACE_PODS = { @@ -55,8 +52,6 @@ const PLAY_SERVICES_CODES = { }, }; -const APPS: { [string]: FirebaseApp } = {}; - export default { // default options OPTIONS: { @@ -69,9 +64,6 @@ export default { checkedPlayServices: false, }, - // track all initialized firebase apps - APPS, - STRINGS: { WARN_INITIALIZE_DEPRECATION: 'Deprecation: Calling \'initializeApp()\' for apps that are already initialised natively ' + 'is unnecessary, use \'firebase.app()\' instead to access the already initialized default app instance.', @@ -227,19 +219,5 @@ export default { DEFAULT_APP_NAME, }, - - SharedEventEmitter: new EventEmitter(), SyncTree: NativeModules.RNFirebaseDatabase ? new SyncTree(NativeModules.RNFirebaseDatabase) : null, - - // internal utils - deleteApp(name: String): Promise { - const app = this.APPS[name]; - if (!app) return Promise.resolve(true); - - // https://firebase.google.com/docs/reference/js/firebase.app.App#delete - return app.delete().then(() => { - delete this.APPS[name]; - return true; - }); - }, }; diff --git a/lib/utils/log.js b/lib/utils/log.js index b43dbe73..73f5f83d 100644 --- a/lib/utils/log.js +++ b/lib/utils/log.js @@ -1,5 +1,10 @@ +/* + * @flow + */ import { windowOrGlobal } from './'; +import type ModuleBase from './ModuleBase'; + ((base) => { window = base || window; if (!window.localStorage) window.localStorage = {}; @@ -7,6 +12,15 @@ import { windowOrGlobal } from './'; // clean up time +const NATIVE_LOGGERS: { [ModuleBase]: Object } = {}; + +export const getLogger = (module: ModuleBase) => { + if (!NATIVE_LOGGERS[module]) { + NATIVE_LOGGERS[module] = require('bows')(`🔥 ${module._namespace.toUpperCase()}`); + } + return NATIVE_LOGGERS[module]; +}; + export default class Log { static createLogger(namespace) { return require('bows')(namespace); diff --git a/lib/utils/native.js b/lib/utils/native.js new file mode 100644 index 00000000..a6ff5926 --- /dev/null +++ b/lib/utils/native.js @@ -0,0 +1,62 @@ +/* + * @flow + */ +import { NativeModules } from 'react-native'; +import INTERNALS from './internals'; + +import type ModuleBase from './ModuleBase'; + +// Firebase Native SDKs that support multiple app instances +const MULTI_APP_MODULES = [ + 'auth', + 'database', + 'firestore', + 'storage', +]; + +const NATIVE_MODULES: { [ModuleBase]: Object } = {}; +const RAW_NATIVE_MODULES: { [ModuleBase]: Object } = {}; + +/** + * Prepends appName arg to all native method calls + * @param appName + * @param NativeModule + */ +const nativeWithApp = (appName: string, NativeModule: Object): 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) => { + return NativeModule[method](...[appName, ...args]); + }; + } + + return native; +}; + +export const getNativeModule = (module: ModuleBase): Object => { + return NATIVE_MODULES[module]; +}; + +export const getRawNativeModule = (module: ModuleBase): Object => { + return RAW_NATIVE_MODULES[module]; +}; + +export const initialiseNativeModule = (module: ModuleBase): void => { + const nativeModule = NativeModules[module.constructor._NATIVE_MODULE]; + + if (!nativeModule && !module.constructor._NATIVE_DISABLED) { + throw new Error(INTERNALS.STRINGS.ERROR_MISSING_MODULE(module.constructor._NAMESPACE, module.constructor._NATIVE_MODULE)); + } + + // used by the modules that extend ModuleBase + // to access their native module counterpart + RAW_NATIVE_MODULES[module] = nativeModule; + if (!MULTI_APP_MODULES.includes(module._module.toLowerCase())) { + NATIVE_MODULES[module] = nativeModule; + } else { + NATIVE_MODULES[module] = nativeWithApp(module._appName, nativeModule); + } +}; diff --git a/tests/ios/Podfile.lock b/tests/ios/Podfile.lock index 55bf24ba..a8f8ff96 100644 --- a/tests/ios/Podfile.lock +++ b/tests/ios/Podfile.lock @@ -153,7 +153,7 @@ PODS: - React/Core - React/fishhook - React/RCTBlob - - RNFirebase (3.1.1): + - RNFirebase (3.2.0): - React - yoga (0.49.1.React) @@ -215,7 +215,7 @@ SPEC CHECKSUMS: nanopb: 5601e6bca2dbf1ed831b519092ec110f66982ca3 Protobuf: 03eef2ee0b674770735cf79d9c4d3659cf6908e8 React: cf892fb84b7d06bf5fea7f328e554c6dcabe85ee - RNFirebase: 976f3b35d112017c69da5ada20cf1f15fc2c327e + RNFirebase: 22b1917fec663706907bc901ed665ac4f8b9bfd6 yoga: 3abf02d6d9aeeb139b4c930eb1367feae690a35a PODFILE CHECKSUM: f17a538903249834df5049668d10174810db4c4c diff --git a/tests/src/firebase.js b/tests/src/firebase.js index f51fb163..4df18e2f 100644 --- a/tests/src/firebase.js +++ b/tests/src/firebase.js @@ -40,7 +40,7 @@ const ios = { const instances = { web: firebase.initializeApp(config), - native: RNfirebase.app(), + native: RNfirebase, another: RNfirebase.initializeApp(Platform.OS === 'ios' ? ios : android, 'anotherApp'), }; diff --git a/tests/src/tests/core/coreTests.js b/tests/src/tests/core/coreTests.js index d96e5d78..b7e971a4 100644 --- a/tests/src/tests/core/coreTests.js +++ b/tests/src/tests/core/coreTests.js @@ -53,8 +53,7 @@ function coreTests({ describe, it }) { it('it should provide an array of apps', () => { should.equal(!!RNFirebase.apps.length, true); - should.equal(RNFirebase.apps[0]._name, RNFirebase.utils.DEFAULT_APP_NAME); - should.equal(RNFirebase.apps[0].name, '[DEFAULT]'); + should.equal(RNFirebase.apps.includes(RNFirebase.app(RNFirebase.utils.DEFAULT_APP_NAME)), true); return Promise.resolve(); });