diff --git a/packages/app/e2e/app.modular.e2e.js b/packages/app/e2e/app.modular.e2e.js new file mode 100644 index 0000000000..b4b19fda60 --- /dev/null +++ b/packages/app/e2e/app.modular.e2e.js @@ -0,0 +1,321 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +describe('modular', function () { + describe('firebase v8 compatibility', function () { + it('it should allow read the default app from native', function () { + // 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 () { + 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 () { + const platformAppConfig = FirebaseHelpers.app.config(); + should.equal(firebase.app().options.apiKey, platformAppConfig.apiKey); + should.equal(firebase.app().options.appId, platformAppConfig.appId); + should.equal(firebase.app().options.databaseURL, platformAppConfig.databaseURL); + should.equal(firebase.app().options.messagingSenderId, platformAppConfig.messagingSenderId); + should.equal(firebase.app().options.projectId, platformAppConfig.projectId); + should.equal(firebase.app().options.storageBucket, platformAppConfig.storageBucket); + }); + + it('SDK_VERSION should return a string version', function () { + firebase.SDK_VERSION.should.be.a.String(); + }); + + 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 () { + firebase.app().automaticDataCollectionEnabled = false; + should.equal(firebase.app().automaticDataCollectionEnabled, false); + }); + + it('should allow setting of log level', function () { + firebase.setLogLevel('error'); + firebase.setLogLevel('verbose'); + }); + + it('should error if logLevel is invalid', function () { + try { + firebase.setLogLevel('silent'); + throw new Error('did not throw on invalid loglevel'); + } catch (e) { + e.message.should.containEql('LogLevel must be one of'); + } + }); + + it('it should initialize dynamic apps', async function () { + const appCount = firebase.apps.length; + const name = `testscoreapp${FirebaseHelpers.id}`; + const platformAppConfig = FirebaseHelpers.app.config(); + const newApp = await firebase.initializeApp(platformAppConfig, name); + newApp.name.should.equal(name); + newApp.toString().should.equal(name); + newApp.options.apiKey.should.equal(platformAppConfig.apiKey); + should.equal(firebase.apps.includes(firebase.app(name)), true); + should.equal(firebase.apps.length, appCount + 1); + return newApp.delete(); + }); + + it('should error if dynamic app initialization values are incorrect', async function () { + const appCount = firebase.apps.length; + try { + await firebase.initializeApp({ appId: 'myid' }, 'myname'); + throw new Error('Should have rejected incorrect initializeApp input'); + } catch (e) { + e.message.should.equal("Missing or invalid FirebaseOptions property 'apiKey'."); + should.equal(firebase.apps.length, appCount); + should.equal(firebase.apps.includes('myname'), false); + } + }); + + 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') { + return; + } + + const appCount = firebase.apps.length; + try { + const firebaseConfig = { + apiKey: 'XXXXXXXXXXXXXXXXXXXXXXX', + authDomain: 'test-XXXXX.firebaseapp.com', + databaseURL: 'https://test-XXXXXX.firebaseio.com', + projectId: 'test-XXXXX', + storageBucket: 'tes-XXXXX.appspot.com', + messagingSenderId: 'XXXXXXXXXXXXX', + appId: '1:XXXXXXXXX', + app_name: 'TEST', + }; + await firebase.initializeApp(firebaseConfig, 'myname'); + throw new Error('Should have rejected incorrect initializeApp input'); + } catch (e) { + e.code.should.containEql('app/unknown'); + e.message.should.containEql('Configuration fails'); + should.equal(firebase.apps.length, appCount); + should.equal(firebase.apps.includes('myname'), false); + } + }); + + it('apps can be deleted, but only once', async function () { + const name = `testscoreapp${FirebaseHelpers.id}`; + const platformAppConfig = FirebaseHelpers.app.config(); + const newApp = await firebase.initializeApp(platformAppConfig, name); + + newApp.name.should.equal(name); + newApp.toString().should.equal(name); + newApp.options.apiKey.should.equal(platformAppConfig.apiKey); + + await newApp.delete(); + try { + await newApp.delete(); + } catch (e) { + e.message.should.equal(`Firebase App named '${name}' already deleted`); + } + try { + firebase.app(name); + } catch (e) { + e.message.should.equal( + `No Firebase App '${name}' has been created - call firebase.initializeApp()`, + ); + } + }); + + it('prevents the default app from being deleted', async function () { + try { + await firebase.app().delete(); + } catch (e) { + e.message.should.equal('Unable to delete the default native firebase app instance.'); + } + }); + + it('extendApp should provide additional functionality', function () { + const extension = {}; + firebase.app().extendApp({ + extension, + }); + firebase.app().extension.should.equal(extension); + }); + }); + + describe('firebase', function () { + it('it should allow read the default app from native', function () { + const { getApp } = modular; + + // app is created in tests app before all hook + should.equal(getApp()._nativeInitialized, true); + should.equal(getApp().name, '[DEFAULT]'); + }); + + it('it should create js apps for natively initialized apps', function () { + const { getApp } = modular; + + should.equal(getApp('secondaryFromNative')._nativeInitialized, true); + should.equal(getApp('secondaryFromNative').name, 'secondaryFromNative'); + }); + + it('should allow setting of log level', function () { + const { setLogLevel } = modular; + + setLogLevel('error'); + setLogLevel('verbose'); + }); + + it('should error if logLevel is invalid', function () { + const { setLogLevel } = modular; + + try { + setLogLevel('silent'); + throw new Error('did not throw on invalid loglevel'); + } catch (e) { + e.message.should.containEql('LogLevel must be one of'); + } + }); + + it('it should initialize dynamic apps', async function () { + const { initializeApp, getApps, getApp } = modular; + + const appCount = firebase.apps.length; + const name = `testscoreapp${FirebaseHelpers.id}`; + const platformAppConfig = FirebaseHelpers.app.config(); + const newApp = await initializeApp(platformAppConfig, name); + newApp.name.should.equal(name); + newApp.toString().should.equal(name); + newApp.options.apiKey.should.equal(platformAppConfig.apiKey); + + const apps = getApps(); + + should.equal(apps.includes(getApp(name)), true); + should.equal(apps.length, appCount + 1); + return newApp.delete(); + }); + + it('should error if dynamic app initialization values are incorrect', async function () { + const { initializeApp, getApps } = modular; + + const appCount = getApps().length; + try { + await initializeApp({ appId: 'myid' }, 'myname'); + throw new Error('Should have rejected incorrect initializeApp input'); + } catch (e) { + e.message.should.equal("Missing or invalid FirebaseOptions property 'apiKey'."); + should.equal(getApps().length, appCount); + should.equal(getApps().includes('myname'), false); + } + }); + + 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') { + return; + } + + const appCount = getApps().length; + try { + const firebaseConfig = { + apiKey: 'XXXXXXXXXXXXXXXXXXXXXXX', + authDomain: 'test-XXXXX.firebaseapp.com', + databaseURL: 'https://test-XXXXXX.firebaseio.com', + projectId: 'test-XXXXX', + storageBucket: 'tes-XXXXX.appspot.com', + messagingSenderId: 'XXXXXXXXXXXXX', + appId: '1:XXXXXXXXX', + app_name: 'TEST', + }; + await initializeApp(firebaseConfig, 'myname'); + throw new Error('Should have rejected incorrect initializeApp input'); + } catch (e) { + e.code.should.containEql('app/unknown'); + e.message.should.containEql('Configuration fails'); + should.equal(firebase.apps.length, appCount); + should.equal(firebase.apps.includes('myname'), false); + } + }); + + it('apps can be deleted, but only once', async function () { + const { initializeApp, getApp, deleteApp } = modular; + + const name = `testscoreapp${FirebaseHelpers.id}`; + const platformAppConfig = FirebaseHelpers.app.config(); + const newApp = await initializeApp(platformAppConfig, name); + + newApp.name.should.equal(name); + newApp.toString().should.equal(name); + newApp.options.apiKey.should.equal(platformAppConfig.apiKey); + + await deleteApp(newApp); + try { + await deleteApp(newApp); + throw new Error('Should have rejected incorrect deleteApp'); + } catch (e) { + e.message.should.equal(`Firebase App named '${name}' already deleted`); + } + try { + getApp(name); + throw new Error('Should have rejected incorrect getApp'); + } catch (e) { + e.message.should.equal( + `No Firebase App '${name}' has been created - call firebase.initializeApp()`, + ); + } + }); + + it('prevents the default app from being deleted', async function () { + const { getApp, deleteApp } = modular; + + try { + await deleteApp(getApp()); + throw new Error('Should have rejected incorrect deleteApp'); + } catch (e) { + e.message.should.equal('Unable to delete the default native firebase app instance.'); + } + }); + + it('registerVersion is not supported on react-native', async function () { + const { registerVersion } = modular; + + try { + await registerVersion(); + throw new Error('Should have rejected incorrect registerVersion'); + } catch (e) { + e.message.should.equal('registerVersion is only supported on Web'); + } + }); + + it('onLog is not supported on react-native', async function () { + const { onLog } = modular; + + try { + await onLog(() => {}, {}); + throw new Error('Should have rejected incorrect onLog'); + } catch (e) { + e.message.should.equal('onLog is only supported on Web'); + } + }); + }); +}); diff --git a/packages/app/lib/index.js b/packages/app/lib/index.js index 55fc1ca36b..fbb6697030 100644 --- a/packages/app/lib/index.js +++ b/packages/app/lib/index.js @@ -18,6 +18,7 @@ import { getFirebaseRoot } from './internal/registry/namespace'; export const firebase = getFirebaseRoot(); +export * from './modular/app'; export { default as utils } from './utils'; export default firebase; diff --git a/packages/app/lib/internal/registry/app.js b/packages/app/lib/internal/registry/app.js index fbf6b8bf79..9456630dd0 100644 --- a/packages/app/lib/internal/registry/app.js +++ b/packages/app/lib/internal/registry/app.js @@ -216,6 +216,10 @@ export function deleteApp(name, nativeInitialized) { const app = APP_REGISTRY[name]; + if (app === undefined) { + throw new Error(`Firebase App named '${name}' already deleted`); + } + const nativeModule = getAppModule(); return nativeModule.deleteApp(name).then(() => { diff --git a/packages/app/lib/modular/app.js b/packages/app/lib/modular/app.js new file mode 100644 index 0000000000..7a3df0a0d7 --- /dev/null +++ b/packages/app/lib/modular/app.js @@ -0,0 +1,33 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { + deleteApp as deleteAppCompat, + getApp, + getApps, + initializeApp, + setLogLevel, +} from '../internal'; + +/** + * Renders this app unusable and frees the resources of all associated services. + * @param app - FirebaseApp - The app to delete. + * @returns + */ +export function deleteApp(app) { + return deleteAppCompat(app.name, app._nativeInitialized); +} + +/** + * Registers a library's name and version for platform logging purposes. + */ +export function registerVersion() { + throw new Error('registerVersion is only supported on Web'); +} + +/** + * Sets log handler for all Firebase SDKs. + */ +export function onLog(logCallback, options) { + throw new Error('onLog is only supported on Web'); +} + +export { getApps, initializeApp, getApp, setLogLevel }; diff --git a/tests/app.js b/tests/app.js index 46551d8249..7acf0e6281 100644 --- a/tests/app.js +++ b/tests/app.js @@ -16,11 +16,11 @@ */ import '@react-native-firebase/analytics'; -import firebase from '@react-native-firebase/app'; -import NativeEventEmitter from '@react-native-firebase/app/lib/internal/RNFBNativeEventEmitter'; -import '@react-native-firebase/app/lib/utils'; +import firebase, * as modular from '@react-native-firebase/app'; import '@react-native-firebase/app-check'; import '@react-native-firebase/app-distribution'; +import NativeEventEmitter from '@react-native-firebase/app/lib/internal/RNFBNativeEventEmitter'; +import '@react-native-firebase/app/lib/utils'; import '@react-native-firebase/auth'; import '@react-native-firebase/crashlytics'; import '@react-native-firebase/database'; @@ -41,6 +41,7 @@ import { AppRegistry, Button, NativeModules, Text, View } from 'react-native'; jet.exposeContextProperty('NativeModules', NativeModules); jet.exposeContextProperty('NativeEventEmitter', NativeEventEmitter); jet.exposeContextProperty('module', firebase); +jet.exposeContextProperty('modular', modular); firebase.database().useEmulator('localhost', 9000); firebase.auth().useEmulator('http://localhost:9099'); diff --git a/tests/e2e/globals.js b/tests/e2e/globals.js index 3bab79dba5..070adfc271 100644 --- a/tests/e2e/globals.js +++ b/tests/e2e/globals.js @@ -59,4 +59,10 @@ Object.defineProperty(global, 'TestsAPI', { }, }); +Object.defineProperty(global, 'modular', { + get() { + return jet.modular; + }, +}); + global.isCI = !!process.env.CI;