From 1b2e2473d526c3356ab8619b226943446fd49452 Mon Sep 17 00:00:00 2001 From: Salakar Date: Thu, 4 Jul 2024 18:04:33 +0100 Subject: [PATCH] feat(other): add App/Core support --- packages/app/e2e/app.constants.e2e.js | 12 +- packages/app/e2e/app.e2e.js | 26 +- packages/app/e2e/config.e2e.js | 3 + packages/app/e2e/events.e2e.js | 68 +++-- packages/app/e2e/utils.e2e.js | 2 + packages/app/e2e/utilsStatics.e2e.js | 2 + packages/app/lib/common/index.js | 2 + .../lib/internal/RNFBNativeEventEmitter.js | 25 +- .../app/lib/internal/nativeModule.android.js | 2 + packages/app/lib/internal/nativeModule.ios.js | 2 + packages/app/lib/internal/nativeModule.js | 4 + .../lib/internal/nativeModuleAndroidIos.js | 45 +++ packages/app/lib/internal/nativeModuleWeb.js | 48 ++++ packages/app/lib/internal/registry/app.js | 3 +- .../app/lib/internal/registry/nativeModule.js | 26 +- .../app/lib/internal/web/RNFBAppModule.js | 261 ++++++++++++++++++ packages/app/lib/internal/web/firebaseApp.js | 3 + .../app/lib/internal/web/firebaseFunctions.js | 4 + packages/app/lib/utils/UtilsStatics.js | 5 +- packages/app/package.json | 1 + yarn.lock | 3 +- 21 files changed, 494 insertions(+), 53 deletions(-) create mode 100644 packages/app/lib/internal/nativeModule.android.js create mode 100644 packages/app/lib/internal/nativeModule.ios.js create mode 100644 packages/app/lib/internal/nativeModule.js create mode 100644 packages/app/lib/internal/nativeModuleAndroidIos.js create mode 100644 packages/app/lib/internal/nativeModuleWeb.js create mode 100644 packages/app/lib/internal/web/RNFBAppModule.js create mode 100644 packages/app/lib/internal/web/firebaseApp.js create mode 100644 packages/app/lib/internal/web/firebaseFunctions.js diff --git a/packages/app/e2e/app.constants.e2e.js b/packages/app/e2e/app.constants.e2e.js index 492dca39ad..c6a8dfed4a 100644 --- a/packages/app/e2e/app.constants.e2e.js +++ b/packages/app/e2e/app.constants.e2e.js @@ -14,21 +14,29 @@ * limitations under the License. * */ +import { getAppModule } from '@react-native-firebase/app/lib/internal/registry/nativeModule'; describe('App -> NativeModules -> Constants', function () { describe('.apps', function () { it('should be an array', function () { - const { NATIVE_FIREBASE_APPS } = NativeModules.RNFBAppModule; + const { NATIVE_FIREBASE_APPS } = getAppModule(); NATIVE_FIREBASE_APPS.should.be.an.Array(); + + // There is no native app initialization on non-native platforms. + if (Platform.other) return; // secondaryFromNative + default NATIVE_FIREBASE_APPS.length.should.equal(2); }); it('array items contain name, options & state properties', function () { - const { NATIVE_FIREBASE_APPS } = NativeModules.RNFBAppModule; + const { NATIVE_FIREBASE_APPS } = getAppModule(); NATIVE_FIREBASE_APPS.should.be.an.Array(); + + // There is no native app initialization on non-native platforms. + if (Platform.other) return; + NATIVE_FIREBASE_APPS.length.should.equal(2); for (let i = 0; i < NATIVE_FIREBASE_APPS.length; i++) { diff --git a/packages/app/e2e/app.e2e.js b/packages/app/e2e/app.e2e.js index b4b19fda60..dc891bbf15 100644 --- a/packages/app/e2e/app.e2e.js +++ b/packages/app/e2e/app.e2e.js @@ -16,19 +16,22 @@ */ describe('modular', function () { - describe('firebase v8 compatibility', function () { + describe('firebase v8 compat', function () { it('it should allow read the default app from native', function () { + if (Platform.other) return; // Not supported on non-native platforms. // app is created in tests app before all hook should.equal(firebase.app()._nativeInitialized, true); should.equal(firebase.app().name, '[DEFAULT]'); }); it('it should create js apps for natively initialized apps', function () { + if (Platform.other) return; // Not supported on non-native platforms. should.equal(firebase.app('secondaryFromNative')._nativeInitialized, true); should.equal(firebase.app('secondaryFromNative').name, 'secondaryFromNative'); }); it('natively initialized apps should have options available in js', function () { + if (Platform.other) return; // Not supported on non-native platforms. const platformAppConfig = FirebaseHelpers.app.config(); should.equal(firebase.app().options.apiKey, platformAppConfig.apiKey); should.equal(firebase.app().options.appId, platformAppConfig.appId); @@ -45,7 +48,6 @@ describe('modular', function () { it('apps should provide an array of apps', function () { should.equal(!!firebase.apps.length, true); should.equal(firebase.apps.includes(firebase.app('[DEFAULT]')), true); - return Promise.resolve(); }); it('apps can get and set data collection', async function () { @@ -93,8 +95,8 @@ describe('modular', function () { }); it('should error if dynamic app initialization values are invalid', async function () { - // firebase-android-sdk will not complain on invalid initialization values, iOS throws - if (device.getPlatform() === 'android') { + // firebase-android-sdk and js-sdk will not complain on invalid initialization values, iOS throws + if (Platform.android || Platform.other) { return; } @@ -120,7 +122,7 @@ describe('modular', function () { } }); - it('apps can be deleted, but only once', async function () { + it('apps can be deleted, but only if it exists', async function () { const name = `testscoreapp${FirebaseHelpers.id}`; const platformAppConfig = FirebaseHelpers.app.config(); const newApp = await firebase.initializeApp(platformAppConfig, name); @@ -145,6 +147,7 @@ describe('modular', function () { }); it('prevents the default app from being deleted', async function () { + if (Platform.other) return; // We can delete the default app on other platforms. try { await firebase.app().delete(); } catch (e) { @@ -161,8 +164,9 @@ describe('modular', function () { }); }); - describe('firebase', function () { + describe('firebase v9 modular', function () { it('it should allow read the default app from native', function () { + if (Platform.other) return; // Not supported on non-native platforms. const { getApp } = modular; // app is created in tests app before all hook @@ -171,6 +175,7 @@ describe('modular', function () { }); it('it should create js apps for natively initialized apps', function () { + if (Platform.other) return; // Not supported on non-native platforms. const { getApp } = modular; should.equal(getApp('secondaryFromNative')._nativeInitialized, true); @@ -189,7 +194,7 @@ describe('modular', function () { try { setLogLevel('silent'); - throw new Error('did not throw on invalid loglevel'); + throw new Error('did not throw on invalid log level'); } catch (e) { e.message.should.containEql('LogLevel must be one of'); } @@ -230,8 +235,8 @@ describe('modular', function () { it('should error if dynamic app initialization values are invalid', async function () { const { initializeApp, getApps } = modular; - // firebase-android-sdk will not complain on invalid initialization values, iOS throws - if (device.getPlatform() === 'android') { + // firebase-android-sdk & js-sdk will not complain on invalid initialization values, iOS throws + if (Platform.android || Platform.other) { return; } @@ -257,7 +262,7 @@ describe('modular', function () { } }); - it('apps can be deleted, but only once', async function () { + it('apps can be deleted, but only if it exists', async function () { const { initializeApp, getApp, deleteApp } = modular; const name = `testscoreapp${FirebaseHelpers.id}`; @@ -286,6 +291,7 @@ describe('modular', function () { }); it('prevents the default app from being deleted', async function () { + if (Platform.other) return; // We can delete the default app on other platforms. const { getApp, deleteApp } = modular; try { diff --git a/packages/app/e2e/config.e2e.js b/packages/app/e2e/config.e2e.js index f8c93ba52b..9e4ea2ec99 100644 --- a/packages/app/e2e/config.e2e.js +++ b/packages/app/e2e/config.e2e.js @@ -19,6 +19,7 @@ describe('config', function () { describe('meta', function () { it('should read Info.plist/AndroidManifest.xml meta data', async function () { const metaData = await NativeModules.RNFBAppModule.metaGetAll(); + if (Platform.other) return; metaData.rnfirebase_meta_testing_string.should.equal('abc'); metaData.rnfirebase_meta_testing_boolean_false.should.equal(false); metaData.rnfirebase_meta_testing_boolean_true.should.equal(true); @@ -28,6 +29,7 @@ describe('config', function () { describe('json', function () { it('should read firebase.json data', async function () { const jsonData = await NativeModules.RNFBAppModule.jsonGetAll(); + if (Platform.other) return; jsonData.rnfirebase_json_testing_string.should.equal('abc'); jsonData.rnfirebase_json_testing_boolean_false.should.equal(false); jsonData.rnfirebase_json_testing_boolean_true.should.equal(true); @@ -41,6 +43,7 @@ describe('config', function () { // NOTE: "preferencesClearAll" clears Firestore settings. Set DB as emulator again. after(async function () { + if (Platform.other) return; await firebase .firestore() .settings({ host: 'localhost:8080', ssl: false, persistence: true }); diff --git a/packages/app/e2e/events.e2e.js b/packages/app/e2e/events.e2e.js index e3bf280b55..22d8dfad81 100644 --- a/packages/app/e2e/events.e2e.js +++ b/packages/app/e2e/events.e2e.js @@ -31,14 +31,23 @@ describe('Core -> EventEmitter', function () { const { resolve, reject, promise } = Promise.defer(); const emitter = NativeEventEmitter; - emitter.addListener(eventName, event => { - event.foo.should.equal(eventBody.foo); - if (!readyToResolve) { - return reject(new Error('Event was received before being ready!')); - } - - return resolve(); - }); + emitter.addListener( + eventName, + event => { + try { + should.notEqual(event.foo, null); + event.foo.should.equal(eventBody.foo); + } catch (e) { + return reject(e); + } + if (!readyToResolve) { + return reject(new Error('Event was received before being ready!')); + } + return resolve(); + }, + undefined, + true, + ); await eventsNotifyReady(false); await eventsPing(eventName, eventBody); @@ -57,24 +66,51 @@ describe('Core -> EventEmitter', function () { should.equal(nativeListenersAfter.events.pong, undefined); }); + it('can send and receive lots of events', async function () { + const { eventsPing, eventsNotifyReady } = NativeModules.RNFBAppModule; + await eventsNotifyReady(true); + const { resolve, reject, promise } = Promise.defer(); + const emitter = NativeEventEmitter; + let eventCount = 0; + emitter.addListener(eventName, event => { + try { + should.notEqual(event.foo, null); + event.foo.should.equal(eventBody.foo); + } catch (e) { + return reject(e); + } + + eventCount++; + if (eventCount === 100) { + return resolve(); + } + }); + await Utils.sleep(100); + for (let i = 0; i < 100; i++) { + eventsPing(eventName, eventBody); + } + + await promise; + emitter.removeAllListeners(eventName); + }); + it('queues events before a js listener is registered', async function () { const { eventsPing, eventsNotifyReady, eventsGetListeners, eventsRemoveListener } = NativeModules.RNFBAppModule; await eventsNotifyReady(true); - const { resolve, promise } = Promise.defer(); + const { resolve, reject, promise } = Promise.defer(); const emitter = NativeEventEmitter; - await eventsPing(eventName2, eventBody); - await Utils.sleep(500); - // const nativeListenersBefore = await eventsGetListeners(); - // console.error('we have listeners? ' + JSON.stringify(nativeListenersBefore)); - // should.equal(nativeListenersBefore.events.ping, undefined); const subscription = emitter.addListener(eventName2, event => { - event.foo.should.equal(eventBody.foo); + try { + should.notEqual(event.foo, null); + event.foo.should.equal(eventBody.foo); + } catch (e) { + return reject(e); + } return resolve(); }); - await promise; subscription.remove(); diff --git a/packages/app/e2e/utils.e2e.js b/packages/app/e2e/utils.e2e.js index 3e08b94b16..39c65ce54c 100644 --- a/packages/app/e2e/utils.e2e.js +++ b/packages/app/e2e/utils.e2e.js @@ -16,6 +16,8 @@ */ describe('utils()', function () { + if (Platform.other) return; // Not supported on non-native platforms. + describe('namespace', function () { it('accessible from firebase.app()', function () { const app = firebase.app(); diff --git a/packages/app/e2e/utilsStatics.e2e.js b/packages/app/e2e/utilsStatics.e2e.js index cc9af98936..5f70a15dfd 100644 --- a/packages/app/e2e/utilsStatics.e2e.js +++ b/packages/app/e2e/utilsStatics.e2e.js @@ -16,6 +16,8 @@ */ describe('utils()', function () { + if (Platform.other) return; // Not supported on non-native platforms. + describe('statics', function () { it('provides native path strings', function () { firebase.utils.FilePath.should.be.an.Object(); diff --git a/packages/app/lib/common/index.js b/packages/app/lib/common/index.js index 809d8b3a6b..8eba4403c2 100644 --- a/packages/app/lib/common/index.js +++ b/packages/app/lib/common/index.js @@ -85,6 +85,8 @@ export const isIOS = Platform.OS === 'ios'; export const isAndroid = Platform.OS === 'android'; +export const isOther = Platform.OS !== 'ios' && Platform.OS !== 'android'; + export function tryJSONParse(string) { try { return string && JSON.parse(string); diff --git a/packages/app/lib/internal/RNFBNativeEventEmitter.js b/packages/app/lib/internal/RNFBNativeEventEmitter.js index 88cee6fae4..20e8e18cec 100644 --- a/packages/app/lib/internal/RNFBNativeEventEmitter.js +++ b/packages/app/lib/internal/RNFBNativeEventEmitter.js @@ -15,24 +15,35 @@ * */ -import { NativeEventEmitter, NativeModules } from 'react-native'; - -const { RNFBAppModule } = NativeModules; +import { NativeEventEmitter } from 'react-native'; +import { getReactNativeModule } from './nativeModule'; class RNFBNativeEventEmitter extends NativeEventEmitter { constructor() { - super(RNFBAppModule); + super(getReactNativeModule('RNFBAppModule')); this.ready = false; } addListener(eventType, listener, context) { + const RNFBAppModule = getReactNativeModule('RNFBAppModule'); if (!this.ready) { RNFBAppModule.eventsNotifyReady(true); this.ready = true; } RNFBAppModule.eventsAddListener(eventType); + if (global.RNFBDebug) { + // eslint-disable-next-line no-console + console.debug(`[RNFB-->Event][👂] ${eventType} -> listening`); + } + const listenerDebugger = (...args) => { + if (global.RNFBDebug) { + // eslint-disable-next-line no-console + console.debug(`[RNFB<--Event][📣] ${eventType} <-`, JSON.stringify(args[0])); + } + return listener(...args); + }; - let subscription = super.addListener(`rnfb_${eventType}`, listener, context); + let subscription = super.addListener(`rnfb_${eventType}`, listenerDebugger, context); // React Native 0.65+ altered EventEmitter: // - removeSubscription is gone @@ -41,7 +52,7 @@ class RNFBNativeEventEmitter extends NativeEventEmitter { // make sure eventType for backwards compatibility just in case subscription.eventType = `rnfb_${eventType}`; - // New style is to return a remove function on the object, just in csae people call that, + // New style is to return a remove function on the object, just in case people call that, // we will modify it to do our native unsubscription then call the original let originalRemove = subscription.remove; let newRemove = () => { @@ -59,12 +70,14 @@ class RNFBNativeEventEmitter extends NativeEventEmitter { } removeAllListeners(eventType) { + const RNFBAppModule = getReactNativeModule('RNFBAppModule'); RNFBAppModule.eventsRemoveListener(eventType, true); super.removeAllListeners(`rnfb_${eventType}`); } // This is likely no longer ever called, but it is here for backwards compatibility with RN <= 0.64 removeSubscription(subscription) { + const RNFBAppModule = getReactNativeModule('RNFBAppModule'); RNFBAppModule.eventsRemoveListener(subscription.eventType.replace('rnfb_'), false); if (super.removeSubscription) { super.removeSubscription(subscription); diff --git a/packages/app/lib/internal/nativeModule.android.js b/packages/app/lib/internal/nativeModule.android.js new file mode 100644 index 0000000000..95f31361ba --- /dev/null +++ b/packages/app/lib/internal/nativeModule.android.js @@ -0,0 +1,2 @@ +export { getReactNativeModule } from './nativeModuleAndroidIos'; +export { setReactNativeModule } from './nativeModuleAndroidIos'; diff --git a/packages/app/lib/internal/nativeModule.ios.js b/packages/app/lib/internal/nativeModule.ios.js new file mode 100644 index 0000000000..95f31361ba --- /dev/null +++ b/packages/app/lib/internal/nativeModule.ios.js @@ -0,0 +1,2 @@ +export { getReactNativeModule } from './nativeModuleAndroidIos'; +export { setReactNativeModule } from './nativeModuleAndroidIos'; diff --git a/packages/app/lib/internal/nativeModule.js b/packages/app/lib/internal/nativeModule.js new file mode 100644 index 0000000000..e1a9e708b6 --- /dev/null +++ b/packages/app/lib/internal/nativeModule.js @@ -0,0 +1,4 @@ +// This is fallback for when the platform is not with native SDKs. +// In this case we use the web Firebase JS SDK. +export { getReactNativeModule } from './nativeModuleWeb'; +export { setReactNativeModule } from './nativeModuleWeb'; diff --git a/packages/app/lib/internal/nativeModuleAndroidIos.js b/packages/app/lib/internal/nativeModuleAndroidIos.js new file mode 100644 index 0000000000..4083a15435 --- /dev/null +++ b/packages/app/lib/internal/nativeModuleAndroidIos.js @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ +import { NativeModules } from 'react-native'; + +/** + * This is used by Android and iOS to get a native module. + * We additionally add a Proxy to the module to intercept calls + * and log them to the console for debugging purposes, if enabled. + * @param moduleName + */ +export function getReactNativeModule(moduleName) { + const nativeModule = NativeModules[moduleName]; + if (!global.RNFBDebug) { + return nativeModule; + } + return new Proxy(nativeModule, { + ownKeys(target) { + return Object.keys(target); + }, + get: (_, name) => { + if (typeof nativeModule[name] !== 'function') return nativeModule[name]; + return (...args) => { + console.debug(`[RNFB->Native][🔵] ${moduleName}.${name} -> ${JSON.stringify(args)}`); + const result = nativeModule[name](...args); + if (result && result.then) { + return result.then( + res => { + console.debug(`[RNFB<-Native][🟢] ${moduleName}.${name} <- ${JSON.stringify(res)}`); + return res; + }, + err => { + console.debug(`[RNFB<-Native][🔴] ${moduleName}.${name} <- ${JSON.stringify(err)}`); + throw err; + }, + ); + } + console.debug(`[RNFB<-Native][🟢] ${moduleName}.${name} <- ${JSON.stringify(result)}`); + return result; + }; + }, + }); +} + +export function setReactNativeModule() { + // No-op +} diff --git a/packages/app/lib/internal/nativeModuleWeb.js b/packages/app/lib/internal/nativeModuleWeb.js new file mode 100644 index 0000000000..02f395449e --- /dev/null +++ b/packages/app/lib/internal/nativeModuleWeb.js @@ -0,0 +1,48 @@ +/* eslint-disable no-console */ + +import RNFBAppModule from './web/RNFBAppModule'; + +const nativeModuleRegistry = {}; + +export function getReactNativeModule(moduleName) { + const nativeModule = nativeModuleRegistry[moduleName]; + // Throw an error if the module is not registered. + if (!nativeModule) { + throw new Error(`Native module ${moduleName} is not registered.`); + } + if (!global.RNFBDebug) { + return nativeModule; + } + return new Proxy(nativeModule, { + ownKeys(target) { + return Object.keys(target); + }, + get: (_, name) => { + if (typeof nativeModule[name] !== 'function') return nativeModule[name]; + return (...args) => { + console.debug(`[RNFB->Native][🔵] ${moduleName}.${name} -> ${JSON.stringify(args)}`); + const result = nativeModule[name](...args); + if (result && result.then) { + return result.then( + res => { + console.debug(`[RNFB<-Native][🟢] ${moduleName}.${name} <- ${JSON.stringify(res)}`); + return res; + }, + err => { + console.debug(`[RNFB<-Native][🔴] ${moduleName}.${name} <- ${JSON.stringify(err)}`); + throw err; + }, + ); + } + console.debug(`[RNFB<-Native][🟢] ${moduleName}.${name} <- ${JSON.stringify(result)}`); + return result; + }; + }, + }); +} + +export function setReactNativeModule(moduleName, nativeModule) { + nativeModuleRegistry[moduleName] = nativeModule; +} + +setReactNativeModule('RNFBAppModule', RNFBAppModule); diff --git a/packages/app/lib/internal/registry/app.js b/packages/app/lib/internal/registry/app.js index 9456630dd0..22a4315624 100644 --- a/packages/app/lib/internal/registry/app.js +++ b/packages/app/lib/internal/registry/app.js @@ -17,6 +17,7 @@ import { isIOS, + isOther, isNull, isObject, isString, @@ -201,7 +202,7 @@ export function setLogLevel(logLevel) { throw new Error('LogLevel must be one of "error", "warn", "info", "debug", "verbose"'); } - if (isIOS) { + if (isIOS || isOther) { getAppModule().setLogLevel(logLevel); } } diff --git a/packages/app/lib/internal/registry/nativeModule.js b/packages/app/lib/internal/registry/nativeModule.js index e93cb6f12f..5657ef8ce3 100644 --- a/packages/app/lib/internal/registry/nativeModule.js +++ b/packages/app/lib/internal/registry/nativeModule.js @@ -15,11 +15,12 @@ * */ -import { NativeModules, Platform } from 'react-native'; import { APP_NATIVE_MODULE } from '../constants'; import NativeFirebaseError from '../NativeFirebaseError'; import RNFBNativeEventEmitter from '../RNFBNativeEventEmitter'; import SharedEventEmitter from '../SharedEventEmitter'; +import { getReactNativeModule } from '../nativeModule'; +import { isAndroid, isIOS } from '../../common'; const NATIVE_MODULE_REGISTRY = {}; const NATIVE_MODULE_EVENT_SUBSCRIPTIONS = {}; @@ -102,7 +103,7 @@ function initialiseNativeModule(module) { const nativeModuleNames = multiModule ? nativeModuleName : [nativeModuleName]; for (let i = 0; i < nativeModuleNames.length; i++) { - const nativeModule = NativeModules[nativeModuleNames[i]]; + const nativeModule = getReactNativeModule(nativeModuleNames[i]); // only error if there's a single native module // as multi modules can mean some are optional @@ -173,24 +174,19 @@ function subscribeToNativeModuleEvent(eventName) { */ function getMissingModuleHelpText(namespace) { const snippet = `firebase.${namespace}()`; - const nativeModule = namespace.charAt(0).toUpperCase() + namespace.slice(1); - if (Platform.OS === 'ios') { + if (isIOS || isAndroid) { return ( - `You attempted to use a firebase module that's not installed natively on your iOS project by calling ${snippet}.` + - '\r\n\r\nEnsure you have either linked the module or added it to your projects Podfile.' + - '\r\n\r\nSee http://invertase.link/ios for full setup instructions.' + `You attempted to use a Firebase module that's not installed natively on your project by calling ${snippet}.` + + `\r\n\r\nEnsure you have installed the npm package '@react-native-firebase/${namespace}',` + + ' have imported it in your project, and have rebuilt your native application.' ); } - const rnFirebasePackage = `'io.invertase.firebase.${namespace}.ReactNativeFirebase${nativeModule}Package'`; - const newInstance = `'new ReactNativeFirebase${nativeModule}Package()'`; - return ( - `You attempted to use a firebase module that's not installed on your Android project by calling ${snippet}.` + - `\r\n\r\nEnsure you have:\r\n\r\n1) imported the ${rnFirebasePackage} module in your 'MainApplication.java' file.\r\n\r\n2) Added the ` + - `${newInstance} line inside of the RN 'getPackages()' method list.` + - '\r\n\r\nSee http://invertase.link/android for full setup instructions.' + `You attempted to use a Firebase module that's not installed on your project by calling ${snippet}.` + + `\r\n\r\nEnsure you have installed the npm package '@react-native-firebase/${namespace}' and` + + ' have imported it in your project.' ); } @@ -222,7 +218,7 @@ export function getAppModule() { } const namespace = 'app'; - const nativeModule = NativeModules[APP_NATIVE_MODULE]; + const nativeModule = getReactNativeModule(APP_NATIVE_MODULE); if (!nativeModule) { throw new Error(getMissingModuleHelpText(namespace)); diff --git a/packages/app/lib/internal/web/RNFBAppModule.js b/packages/app/lib/internal/web/RNFBAppModule.js new file mode 100644 index 0000000000..0564840982 --- /dev/null +++ b/packages/app/lib/internal/web/RNFBAppModule.js @@ -0,0 +1,261 @@ +/* eslint-disable no-console */ +import { initializeApp, setLogLevel, getApp, getApps, deleteApp } from './firebaseApp'; + +import { DeviceEventEmitter } from 'react-native'; + +// Variables for events tracking. +let jsReady = false; +let jsListenerCount = 0; +let queuedEvents = []; +let jsListeners = {}; + +// For compatibility we have a fake preferences storage, +// it does not persist across app restarts. +let fakePreferencesStorage = {}; + +function eventsGetListenersMap() { + return { + listeners: jsListenerCount, + queued: queuedEvents.length, + events: jsListeners, + }; +} + +function eventsSendEvent(eventName, eventBody) { + if (!jsReady || !jsListeners.hasOwnProperty(eventName)) { + const event = { + eventName, + eventBody, + }; + queuedEvents.push(event); + return; + } + setImmediate(() => DeviceEventEmitter.emit('rnfb_' + eventName, eventBody)); +} + +function eventsSendQueuedEvents() { + if (queuedEvents.length === 0) { + return; + } + const events = Array.from(queuedEvents); + queuedEvents = []; + for (let i = 0; i < events.length; i++) { + const event = events[i]; + eventsSendEvent(event.eventName, event.eventBody); + } +} + +export default { + // Natively initialized apps, but in the case of web, we don't have any. + NATIVE_FIREBASE_APPS: [], + // The raw JSON string of the Firebase config, from the users firebase.json file. + // In the case of web, we can't support this. + FIREBASE_RAW_JSON: '{}', + + /** + * Initializes a Firebase app. + * + * @param {object} options - The Firebase app options. + * @param {object} appConfig - The Firebase app config. + * @returns {object} - The Firebase app instance. + */ + async initializeApp(options, appConfig) { + const name = appConfig.name; + const existingApp = getApps().find(app => app.name === name); + if (existingApp) { + return existingApp; + } + const newAppConfig = { + name, + }; + if ( + appConfig.automaticDataCollectionEnabled === true || + appConfig.automaticDataCollectionEnabled === false + ) { + newAppConfig.automaticDataCollectionEnabled = appConfig.automaticDataCollectionEnabled; + } + const optionsCopy = Object.assign({}, options); + delete optionsCopy.clientId; + initializeApp(optionsCopy, newAppConfig); + return { + options, + appConfig, + }; + }, + + /** + * Sets the log level for the Firebase app. + * + * @param {string} logLevel - The log level to set. + */ + setLogLevel(logLevel) { + setLogLevel(logLevel); + }, + + /** + * Sets the automatic data collection for the Firebase app. + * + * @param {string} appName - The name of the Firebase app. + * @param {boolean} enabled - Whether to enable automatic data collection. + */ + setAutomaticDataCollectionEnabled(appName, enabled) { + getApp(appName).automaticDataCollectionEnabled = enabled; + }, + + /** + * Deletes a Firebase app. + * + * @param {string} appName - The name of the Firebase app to delete. + */ + async deleteApp(appName) { + if (getApp(appName)) { + deleteApp(appName); + } + }, + + /** + * Gets the meta data. + * Unsupported on web. + * + * @returns {object} - The meta data + */ + metaGetAll() { + return {}; + }, + + /** + * Gets the firebase.json data. + * Unsupported on web. + * + * @returns {object} - The JSON data for the firebase.json file. + */ + jsonGetAll() { + return {}; + }, + + /** + * Sets a boolean value for a preference. + * Unsupported on web. + * + * @param {string} key - The key of the preference. + * @param {boolean} value - The value to set. + */ + async preferencesSetBool(key, value) { + fakePreferencesStorage[key] = value; + }, + + /** + * Sets a string value for a preference. + * Unsupported on web. + * + * @param {string} key - The key of the preference. + * @param {string} value - The value to set. + */ + preferencesSetString(key, value) { + fakePreferencesStorage[key] = value; + }, + + /** + * Gets all preferences. + * Unsupported on web. + * + * @returns {object} - The preferences. + */ + preferencesGetAll() { + return Object.assign({}, fakePreferencesStorage); + }, + + /** + * Clears all preferences. + * Unsupported on web. + */ + preferencesClearAll() { + fakePreferencesStorage = {}; + }, + + /** + * Adds a listener for an event. + * Unsupported on web. + * + * @param {string} eventName - The name of the event to listen for. + */ + addListener() { + // Keep: Required for RN built in Event Emitter Calls. + }, + /** + * Removes a listener for an event. + * Unsupported on web. + * + * @param {string} eventName - The name of the event to remove the listener for. + */ + removeListeners() { + // Keep: Required for RN built in Event Emitter Calls. + }, + + /** + * Notifies the app that it is ready to receive events. + * + * @param {boolean} ready - Whether the app is ready to receive events. + * @returns {void} + */ + eventsNotifyReady(ready) { + jsReady = ready; + if (jsReady) { + setImmediate(() => eventsSendQueuedEvents()); + } + }, + + /** + * Gets all the listeners registered. + * + * @returns {object} - The listeners for the event. + */ + eventsGetListeners() { + return eventsGetListenersMap(); + }, + + /** + * Sends an event to the app for testing purposes. + * + * @param {string} eventName - The name of the event to send. + * @param {object} eventBody - The body of the event to send. + * @returns {void} + */ + eventsPing(eventName, eventBody) { + eventsSendEvent(eventName, eventBody); + }, + + /** + * Adds a listener for an event. + * + * @param {string} eventName - The name of the event to listen for. + */ + eventsAddListener(eventName) { + jsListenerCount++; + if (!jsListeners.hasOwnProperty(eventName)) { + jsListeners[eventName] = 1; + } else { + jsListeners[eventName]++; + } + setImmediate(() => eventsSendQueuedEvents()); + }, + + /** + * Removes a single listener for an event or all listeners for an event. + * + * @param {string} eventName - The name of the event to remove the listener for. + * @param {boolean} all - Optional. Whether to remove all listeners for the event. + */ + eventsRemoveListener(eventName, all) { + if (jsListeners.hasOwnProperty(eventName)) { + if (jsListeners[eventName] <= 1 || all) { + const count = jsListeners[eventName]; + jsListenerCount -= count; + delete jsListeners[eventName]; + } else { + jsListenerCount--; + jsListeners[eventName]--; + } + } + }, +}; diff --git a/packages/app/lib/internal/web/firebaseApp.js b/packages/app/lib/internal/web/firebaseApp.js new file mode 100644 index 0000000000..c50cf77658 --- /dev/null +++ b/packages/app/lib/internal/web/firebaseApp.js @@ -0,0 +1,3 @@ +// We need to share firebase imports between modules, otherwise +// apps and instances of the firebase modules are not shared. +export * from 'firebase/app'; diff --git a/packages/app/lib/internal/web/firebaseFunctions.js b/packages/app/lib/internal/web/firebaseFunctions.js new file mode 100644 index 0000000000..f8e4401a14 --- /dev/null +++ b/packages/app/lib/internal/web/firebaseFunctions.js @@ -0,0 +1,4 @@ +// We need to share firebase imports between modules, otherwise +// apps and instances of the firebase modules are not shared. +export * from 'firebase/app'; +export * from 'firebase/functions'; diff --git a/packages/app/lib/utils/UtilsStatics.js b/packages/app/lib/utils/UtilsStatics.js index fde297be90..e3b7c0e7e9 100644 --- a/packages/app/lib/utils/UtilsStatics.js +++ b/packages/app/lib/utils/UtilsStatics.js @@ -17,7 +17,7 @@ */ import { NativeModules } from 'react-native'; -import { stripTrailingSlash } from '../../lib/common'; +import { stripTrailingSlash, isOther } from '../../lib/common'; const PATH_NAMES = [ 'MAIN_BUNDLE', @@ -60,6 +60,7 @@ function processPathConstants(nativeModule) { export default { SDK_VERSION: require('./../version'), get FilePath() { - return processPathConstants(NativeModules.RNFBUtilsModule); + // We don't support path constants on non-native platforms. + return processPathConstants(isOther ? {} : NativeModules.RNFBUtilsModule); }, }; diff --git a/packages/app/package.json b/packages/app/package.json index aa59aa368a..6bb365a2c7 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -57,6 +57,7 @@ "react-native": "*" }, "dependencies": { + "firebase": "10.12.2", "opencollective-postinstall": "^2.0.3", "superstruct": "^0.6.2" }, diff --git a/yarn.lock b/yarn.lock index 15946fd10f..8c5fa58d9b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5849,6 +5849,7 @@ __metadata: resolution: "@react-native-firebase/app@workspace:packages/app" dependencies: expo: "npm:^50.0.19" + firebase: "npm:10.12.2" opencollective-postinstall: "npm:^2.0.3" superstruct: "npm:^0.6.2" peerDependencies: @@ -11793,7 +11794,7 @@ __metadata: languageName: node linkType: hard -"firebase@npm:^10.12.2": +"firebase@npm:10.12.2, firebase@npm:^10.12.2": version: 10.12.2 resolution: "firebase@npm:10.12.2" dependencies: