diff --git a/eslint.config.mjs b/eslint.config.mjs index 165d111b22..fb95dab779 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -41,9 +41,8 @@ export default [ '**/type-test.ts', 'packages/**/modular/dist/**/*', 'packages/ai/__tests__/test-utils', + 'packages/**/dist/**/*', 'packages/vertexai/__tests__/test-utils', - 'packages/vertexai/dist', - 'packages/ai/dist', ], }, ...compat diff --git a/jest.setup.ts b/jest.setup.ts index cc15b33e9b..e04af31fc6 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -39,6 +39,20 @@ jest.doMock('react-native', () => { ...ReactNative.NativeModules, RNFBAnalyticsModule: { logEvent: jest.fn(), + setAnalyticsCollectionEnabled: jest.fn(), + setSessionTimeoutDuration: jest.fn(), + getAppInstanceId: jest.fn(), + getSessionId: jest.fn(), + setUserId: jest.fn(), + setUserProperty: jest.fn(), + setUserProperties: jest.fn(), + resetAnalyticsData: jest.fn(), + setConsent: jest.fn(), + setDefaultEventParameters: jest.fn(), + initiateOnDeviceConversionMeasurementWithEmailAddress: jest.fn(), + initiateOnDeviceConversionMeasurementWithHashedEmailAddress: jest.fn(), + initiateOnDeviceConversionMeasurementWithPhoneNumber: jest.fn(), + initiateOnDeviceConversionMeasurementWithHashedPhoneNumber: jest.fn(), }, RNFBAppModule: { NATIVE_FIREBASE_APPS: [ @@ -67,32 +81,346 @@ jest.doMock('react-native', () => { '[DEFAULT]': 'en-US', }, APP_USER: { - '[DEFAULT]': 'jestUser', + '[DEFAULT]': null, }, addAuthStateListener: jest.fn(), addIdTokenListener: jest.fn(), - setTenantId: jest.fn(), + setLanguageCode: jest.fn(() => Promise.resolve()), + setTenantId: jest.fn(() => Promise.resolve()), + signOut: jest.fn(() => Promise.resolve()), + signInAnonymously: jest.fn(() => Promise.resolve({ user: null })), + createUserWithEmailAndPassword: jest.fn(() => Promise.resolve({ user: null })), + signInWithEmailAndPassword: jest.fn(() => Promise.resolve({ user: null })), + signInWithCustomToken: jest.fn(() => Promise.resolve({ user: null })), + signInWithCredential: jest.fn(() => Promise.resolve({ user: null })), + signInWithEmailLink: jest.fn(() => Promise.resolve({ user: null })), + signInWithProvider: jest.fn(() => Promise.resolve({ user: null })), + signInWithPhoneNumber: jest.fn(() => Promise.resolve({ verificationId: 'test-id' })), + verifyPhoneNumberWithMultiFactorInfo: jest.fn(() => Promise.resolve()), + verifyPhoneNumberForMultiFactor: jest.fn(() => Promise.resolve()), + resolveTotpSignIn: jest.fn(() => Promise.resolve({})), + revokeToken: jest.fn(() => Promise.resolve()), + sendPasswordResetEmail: jest.fn(() => Promise.resolve()), + sendSignInLinkToEmail: jest.fn(() => Promise.resolve()), + isSignInWithEmailLink: jest.fn(() => false), + applyActionCode: jest.fn(() => Promise.resolve(null)), + checkActionCode: jest.fn(() => Promise.resolve({})), + confirmPasswordReset: jest.fn(() => Promise.resolve()), + fetchSignInMethodsForEmail: jest.fn(() => Promise.resolve([])), + verifyPasswordResetCode: jest.fn(() => Promise.resolve('')), + useUserAccessGroup: jest.fn(() => Promise.resolve()), useEmulator: jest.fn(), - configureAuthDomain: jest.fn(), + getCustomAuthDomain: jest.fn(() => Promise.resolve(null)), + configureAuthDomain: jest.fn(() => Promise.resolve()), + // User methods + delete: jest.fn(() => Promise.resolve()), + getIdToken: jest.fn(() => Promise.resolve('mock-token')), + getIdTokenResult: jest.fn(() => Promise.resolve({ token: 'mock-token' })), + linkWithCredential: jest.fn(() => Promise.resolve({ user: null })), + linkWithProvider: jest.fn(() => Promise.resolve({ user: null })), + reauthenticateWithCredential: jest.fn(() => Promise.resolve({ user: null })), + reauthenticateWithProvider: jest.fn(() => Promise.resolve({ user: null })), + reload: jest.fn(() => Promise.resolve(null)), + sendEmailVerification: jest.fn(() => Promise.resolve(null)), + unlink: jest.fn(() => Promise.resolve(null)), + updateEmail: jest.fn(() => Promise.resolve(null)), + updatePassword: jest.fn(() => Promise.resolve(null)), + updatePhoneNumber: jest.fn(() => Promise.resolve(null)), + updateProfile: jest.fn(() => Promise.resolve(null)), + verifyBeforeUpdateEmail: jest.fn(() => Promise.resolve(null)), + }, + RNFBAppCheckModule: { + initializeAppCheck: jest.fn(), + setTokenAutoRefreshEnabled: jest.fn(), + configureProvider: jest.fn(), + getToken: jest.fn(), + getLimitedUseToken: jest.fn(), + addAppCheckListener: jest.fn(), + removeAppCheckListener: jest.fn(), + }, + RNFBAppDistributionModule: { + isTesterSignedIn: jest.fn(), + signInTester: jest.fn(), + checkForUpdate: jest.fn(), + signOutTester: jest.fn(), + }, + RNFBCrashlyticsModule: { + isCrashlyticsCollectionEnabled: false, + checkForUnsentReports: jest.fn(), + crash: jest.fn(), + deleteUnsentReports: jest.fn(), + didCrashOnPreviousExecution: jest.fn(), + log: jest.fn(), + setAttribute: jest.fn(), + setAttributes: jest.fn(), + setUserId: jest.fn(), + recordError: jest.fn(), + sendUnsentReports: jest.fn(), + setCrashlyticsCollectionEnabled: jest.fn(), }, - RNFBCrashlyticsModule: {}, RNFBDatabaseModule: { + constants: { + isDatabaseCollectionEnabled: true, + url: 'https://test.firebaseio.com', + ref: 'ref()', + }, on: jest.fn(), - useEmulator: jest.fn(), + off: jest.fn(), + once: jest.fn( + (_appName: any, _customUrl: any, path: any, _modifiers: any, eventType: any) => { + // Database native methods receive (appName, customUrlOrRegion, ...actualArgs) + let key = 'test'; + if (path && typeof path === 'string') { + const parts = path.split('/').filter(p => p); + key = parts[parts.length - 1] || 'test'; + } + + const snapshotData = { + key, + value: null, + exists: false, + childKeys: [], + priority: null, + }; + + if (eventType === 'value') { + return Promise.resolve(snapshotData); + } + + return Promise.resolve({ + snapshot: snapshotData, + previousChildName: null, + }); + }, + ), + useEmulator: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()), + set: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()), + update: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()), + setWithPriority: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()), + remove: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()), + setPriority: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()), + keepSynced: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()), + transactionStart: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()), + transactionTryCommit: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()), + goOnline: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()), + goOffline: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()), + setPersistenceEnabled: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()), + setLoggingEnabled: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()), + setPersistenceCacheSizeBytes: jest.fn((_appName: any, _customUrl: any) => + Promise.resolve(), + ), + getServerTime: jest.fn((_appName: any, _customUrl: any) => Promise.resolve(Date.now())), }, RNFBFirestoreModule: { + loadBundle: jest.fn(), + clearPersistence: jest.fn(), + waitForPendingWrites: jest.fn(), + terminate: jest.fn(), + useEmulator: jest.fn(), + disableNetwork: jest.fn(), + enableNetwork: jest.fn(), settings: jest.fn(), - documentSet: jest.fn(), + addSnapshotsInSync: jest.fn(), + removeSnapshotsInSync: jest.fn(), + collectionOffSnapshot: jest.fn(), + namedQueryOnSnapshot: jest.fn(), + collectionOnSnapshot: jest.fn(), + collectionGet: jest.fn(() => + Promise.resolve({ + source: 'cache', + changes: [], + documents: [], + metadata: {}, + }), + ), + collectionCount: jest.fn(() => Promise.resolve({ count: 0 })), + documentDelete: jest.fn(() => Promise.resolve()), + documentOffSnapshot: jest.fn(), + documentOnSnapshot: jest.fn(), + documentGet: jest.fn(() => + Promise.resolve({ + data: {}, + metadata: {}, + path: 'firestore/document', + exists: true, + }), + ), + documentSet: jest.fn(() => Promise.resolve()), + documentUpdate: jest.fn(() => Promise.resolve()), + persistenceCacheIndexManager: jest.fn(), + documentBatch: jest.fn(), + transactionApplyBuffer: jest.fn(), + transactionBegin: jest.fn(), + transactionDispose: jest.fn(), + }, + RNFBInAppMessagingModule: { + isMessagesDisplaySuppressed: false, + isAutomaticDataCollectionEnabled: true, + setMessagesDisplaySuppressed: jest.fn(), + setAutomaticDataCollectionEnabled: jest.fn(), + triggerEvent: jest.fn(), + }, + RNFBInstallationsModule: { + getId: jest.fn(), + getToken: jest.fn(), + delete: jest.fn(), }, RNFBMessagingModule: { + isAutoInitEnabled: true, + isDeliveryMetricsExportToBigQueryEnabled: false, + isRegisteredForRemoteNotifications: false, + isNotificationDelegationEnabled: false, onMessage: jest.fn(), + completeNotificationProcessing: jest.fn(), + setAutoInitEnabled: jest.fn(), + getInitialNotification: jest.fn(() => Promise.resolve(null)), + getDidOpenSettingsForNotification: jest.fn(() => Promise.resolve(false)), + getIsHeadless: jest.fn(() => Promise.resolve(false)), + getToken: jest.fn(), + deleteToken: jest.fn(), + requestPermission: jest.fn(() => Promise.resolve(1)), + registerForRemoteNotifications: jest.fn(), + unregisterForRemoteNotifications: jest.fn(), + getAPNSToken: jest.fn(), + setAPNSToken: jest.fn(), + hasPermission: jest.fn(() => Promise.resolve(1)), + signalBackgroundMessageHandlerSet: jest.fn(), + sendMessage: jest.fn(), + subscribeToTopic: jest.fn(), + unsubscribeFromTopic: jest.fn(), + setDeliveryMetricsExportToBigQuery: jest.fn(), + setNotificationDelegationEnabled: jest.fn(), + }, + RNFBPerfModule: { + isPerformanceCollectionEnabled: true, + isInstrumentationEnabled: true, + instrumentationEnabled: jest.fn(() => Promise.resolve()), + setPerformanceCollectionEnabled: jest.fn(() => Promise.resolve()), + startScreenTrace: jest.fn(() => Promise.resolve()), + stopScreenTrace: jest.fn(() => Promise.resolve()), + startTrace: jest.fn(() => Promise.resolve()), + stopTrace: jest.fn(() => Promise.resolve()), + startHttpMetric: jest.fn(() => Promise.resolve()), + stopHttpMetric: jest.fn(() => Promise.resolve()), }, - RNFBPerfModule: {}, RNFBConfigModule: { onConfigUpdated: jest.fn(), + reset: jest.fn(() => + Promise.resolve({ + result: true, + constants: { + lastFetchTime: Date.now(), + lastFetchStatus: 'success', + fetchTimeout: 60, + minimumFetchInterval: 43200, + values: {}, + }, + }), + ), + setConfigSettings: jest.fn(() => + Promise.resolve({ + result: true, + constants: { + lastFetchTime: Date.now(), + lastFetchStatus: 'success', + fetchTimeout: 60, + minimumFetchInterval: 43200, + values: {}, + }, + }), + ), + activate: jest.fn(() => + Promise.resolve({ + result: true, + constants: { + lastFetchTime: Date.now(), + lastFetchStatus: 'success', + fetchTimeout: 60, + minimumFetchInterval: 43200, + values: {}, + }, + }), + ), + fetch: jest.fn(() => + Promise.resolve({ + result: true, + constants: { + lastFetchTime: Date.now(), + lastFetchStatus: 'success', + fetchTimeout: 60, + minimumFetchInterval: 43200, + values: {}, + }, + }), + ), + fetchAndActivate: jest.fn(() => + Promise.resolve({ + result: true, + constants: { + lastFetchTime: Date.now(), + lastFetchStatus: 'success', + fetchTimeout: 60, + minimumFetchInterval: 43200, + values: {}, + }, + }), + ), + ensureInitialized: jest.fn(() => + Promise.resolve({ + result: true, + constants: { + lastFetchTime: Date.now(), + lastFetchStatus: 'success', + fetchTimeout: 60, + minimumFetchInterval: 43200, + values: {}, + }, + }), + ), + setDefaults: jest.fn(() => + Promise.resolve({ + result: true, + constants: { + lastFetchTime: Date.now(), + lastFetchStatus: 'success', + fetchTimeout: 60, + minimumFetchInterval: 43200, + values: {}, + }, + }), + ), + setDefaultsFromResource: jest.fn(() => + Promise.resolve({ + result: true, + constants: { + lastFetchTime: Date.now(), + lastFetchStatus: 'success', + fetchTimeout: 60, + minimumFetchInterval: 43200, + values: {}, + }, + }), + ), + removeConfigUpdateRegistration: jest.fn(), }, RNFBStorageModule: { + maxUploadRetryTime: 0, + maxDownloadRetryTime: 0, + maxOperationRetryTime: 0, + setMaxOperationRetryTime: jest.fn(), + setMaxUploadRetryTime: jest.fn(), + setMaxDownloadRetryTime: jest.fn(), useEmulator: jest.fn(), + delete: jest.fn(() => Promise.resolve()), + getDownloadURL: jest.fn(() => Promise.resolve('https://example.com/file')), + getMetadata: jest.fn(() => Promise.resolve({})), + putString: jest.fn(() => Promise.resolve()), + updateMetadata: jest.fn(() => Promise.resolve({})), + writeToFile: jest.fn(() => Promise.resolve()), + putFile: jest.fn(() => Promise.resolve()), + setTaskStatus: jest.fn(() => Promise.resolve()), + list: jest.fn(() => Promise.resolve({ items: [], prefixes: [], pageToken: null })), + listAll: jest.fn(() => Promise.resolve({ items: [], prefixes: [], pageToken: null })), }, }, }, diff --git a/packages/ai/tsconfig.json b/packages/ai/tsconfig.json index f1d9865812..b40f80b289 100644 --- a/packages/ai/tsconfig.json +++ b/packages/ai/tsconfig.json @@ -6,9 +6,7 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "jsx": "react-jsx", - "lib": [ - "ESNext" - ], + "lib": ["ESNext"], "module": "ESNext", "target": "ESNext", "moduleResolution": "Bundler", @@ -24,9 +22,20 @@ "strict": true, "baseUrl": ".", "paths": { - "@react-native-firebase/app": ["../app/lib"], + "@react-native-firebase/app/lib/common/*": ["../app/dist/typescript/commonjs/lib/common/*"], + "@react-native-firebase/app/lib/common": ["../app/dist/typescript/commonjs/lib/common"], + "@react-native-firebase/app/lib/internal/web/*": [ + "../app/dist/typescript/commonjs/lib/internal/web/*" + ], + "@react-native-firebase/app/lib/internal/*": [ + "../app/dist/typescript/commonjs/lib/internal/*" + ], + "@react-native-firebase/app/lib/internal": ["../app/dist/typescript/commonjs/lib/internal"], + "@react-native-firebase/app": ["../app/dist/typescript/commonjs/lib"], "@react-native-firebase/auth": ["../auth/lib"], - "@react-native-firebase/app-check": ["../app-check/lib"], + "@react-native-firebase/app-check": ["../app-check/lib"] } - } + }, + "include": ["lib/**/*"], + "exclude": ["node_modules", "dist", "__tests__", "**/*.test.ts"] } diff --git a/packages/app/lib/FirebaseApp.js b/packages/app/lib/FirebaseApp.ts similarity index 59% rename from packages/app/lib/FirebaseApp.js rename to packages/app/lib/FirebaseApp.ts index 841138702c..247c538081 100644 --- a/packages/app/lib/FirebaseApp.js +++ b/packages/app/lib/FirebaseApp.ts @@ -14,12 +14,26 @@ * limitations under the License. * */ -import { warnIfNotModularCall } from '@react-native-firebase/app/lib/common'; +import { warnIfNotModularCall } from './common'; import { getAppModule } from './internal/registry/nativeModule'; +import type { ReactNativeFirebase, Utils } from './types/app'; -export default class FirebaseApp { - constructor(options, appConfig, fromNative, deleteApp) { - const { name, automaticDataCollectionEnabled } = appConfig; +export default class FirebaseApp implements ReactNativeFirebase.FirebaseAppBase { + private _name: string; + private _deleted: boolean; + private _deleteApp: () => Promise; + private _options: ReactNativeFirebase.FirebaseAppOptions; + private _automaticDataCollectionEnabled: boolean; + _initialized: boolean; + _nativeInitialized: boolean; + + constructor( + options: ReactNativeFirebase.FirebaseAppOptions, + appConfig: ReactNativeFirebase.FirebaseAppConfig, + fromNative: boolean, + deleteApp: () => Promise, + ) { + const { name = '[DEFAULT]', automaticDataCollectionEnabled } = appConfig; this._name = name; this._deleted = false; @@ -36,44 +50,49 @@ export default class FirebaseApp { } } - get name() { + get name(): string { return this._name; } - get options() { + get options(): ReactNativeFirebase.FirebaseAppOptions { return Object.assign({}, this._options); } - get automaticDataCollectionEnabled() { + get automaticDataCollectionEnabled(): boolean { return this._automaticDataCollectionEnabled; } - set automaticDataCollectionEnabled(enabled) { + set automaticDataCollectionEnabled(enabled: boolean) { this._checkDestroyed(); getAppModule().setAutomaticDataCollectionEnabled(this.name, enabled); this._automaticDataCollectionEnabled = enabled; } - _checkDestroyed() { + private _checkDestroyed(): void { if (this._deleted) { throw new Error(`Firebase App named '${this._name}' already deleted`); } } - extendApp(extendedProps) { + extendApp(extendedProps: Record): void { warnIfNotModularCall(arguments); this._checkDestroyed(); Object.assign(this, extendedProps); } - delete() { + delete(): Promise { warnIfNotModularCall(arguments, 'deleteApp()'); this._checkDestroyed(); return this._deleteApp(); } - toString() { + toString(): string { warnIfNotModularCall(arguments, '.name property'); return this.name; } + + // For backward compatibility - utils method added by registry + utils(): Utils.Module { + throw new Error('utils() should be added by registry'); + } } diff --git a/packages/app/lib/common/Base64.js b/packages/app/lib/common/Base64.ts similarity index 76% rename from packages/app/lib/common/Base64.js rename to packages/app/lib/common/Base64.ts index 6c12c6efb5..707d7f1796 100644 --- a/packages/app/lib/common/Base64.js +++ b/packages/app/lib/common/Base64.ts @@ -15,6 +15,7 @@ * */ +// @ts-expect-error - No type declarations available import binaryToBase64 from 'react-native/Libraries/Utilities/binaryToBase64'; import { promiseDefer } from './promise'; @@ -23,7 +24,7 @@ const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/= /** * window.btoa */ -function btoa(input) { +function btoa(input: string): string { let map; let i = 0; let block = 0; @@ -51,11 +52,11 @@ function btoa(input) { /** * window.atob */ -function atob(input) { +function atob(input: string): string { let i = 0; let bc = 0; let bs = 0; - let buffer; + let buffer: number | string; let output = ''; const str = input.replace(/[=]+$/, ''); @@ -69,7 +70,7 @@ function atob(input) { for ( bc = 0, bs = 0, i = 0; (buffer = str.charAt(i++)); - ~buffer && ((bs = bc % 4 ? bs * 64 + buffer : buffer), bc++ % 4) + ~buffer && ((bs = bc % 4 ? bs * 64 + (buffer as number) : buffer), bc++ % 4) ? (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6)))) : 0 ) { @@ -79,23 +80,30 @@ function atob(input) { return output; } +export interface Base64Result { + string: string | ArrayBuffer | null; + format: 'data_url' | 'base64'; +} + /** * Converts a Blob, ArrayBuffer or Uint8Array to a base64 string. */ -function fromData(data) { +function fromData(data: Blob | ArrayBuffer | Uint8Array): Promise { if (data instanceof Blob) { const fileReader = new FileReader(); - const { resolve, reject, promise } = promiseDefer(); + const { resolve, reject, promise } = promiseDefer(); fileReader.readAsDataURL(data); - fileReader.onloadend = function onloadend() { - resolve({ string: fileReader.result, format: 'data_url' }); + fileReader.onloadend = () => { + if (fileReader?.result) { + resolve?.({ string: fileReader.result, format: 'data_url' }); + } }; - fileReader.onerror = function onerror(event) { - fileReader.abort(); - reject(event); + fileReader.onerror = event => { + fileReader?.abort(); + reject?.(event); }; return promise; diff --git a/packages/app/lib/common/MutatableParams.js b/packages/app/lib/common/MutatableParams.ts similarity index 75% rename from packages/app/lib/common/MutatableParams.js rename to packages/app/lib/common/MutatableParams.ts index 8b998dade5..8078e4ea52 100644 --- a/packages/app/lib/common/MutatableParams.js +++ b/packages/app/lib/common/MutatableParams.ts @@ -18,7 +18,10 @@ import { deepGet, deepSet } from './deeps'; export default class MutatableParams { - constructor(parentInstance) { + _mutatableParams: Record; + _parentInstance: MutatableParams; + + constructor(parentInstance?: MutatableParams) { if (parentInstance) { this._mutatableParams = parentInstance._mutatableParams; this._parentInstance = parentInstance; @@ -28,20 +31,20 @@ export default class MutatableParams { } } - set(param, value) { + set(param: string, value: unknown): MutatableParams { deepSet(this._mutatableParams, param, value); return this._parentInstance; } - get(param) { - return deepGet(this._mutatableParams, param, '.'); + get(param: string): T | undefined { + return deepGet(this._mutatableParams, param, '.'); } - toJSON() { + toJSON(): Record { return Object.assign({}, this._mutatableParams); } - validate() { + validate(): void { // do nothing } } diff --git a/packages/app/lib/common/ReferenceBase.js b/packages/app/lib/common/ReferenceBase.ts similarity index 94% rename from packages/app/lib/common/ReferenceBase.js rename to packages/app/lib/common/ReferenceBase.ts index 0fe24f608a..5e73ba5e86 100644 --- a/packages/app/lib/common/ReferenceBase.js +++ b/packages/app/lib/common/ReferenceBase.ts @@ -16,7 +16,9 @@ */ export default class ReferenceBase { - constructor(_path) { + path: string; + + constructor(_path: string) { let path = _path; if (path) { @@ -37,7 +39,7 @@ export default class ReferenceBase { * @type {String} * {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#key} */ - get key() { + get key(): string | null { return this.path === '/' ? null : this.path.substring(this.path.lastIndexOf('/') + 1); } } diff --git a/packages/app/lib/common/deeps.js b/packages/app/lib/common/deeps.ts similarity index 70% rename from packages/app/lib/common/deeps.js rename to packages/app/lib/common/deeps.ts index cf235c376b..8ca859ac99 100644 --- a/packages/app/lib/common/deeps.js +++ b/packages/app/lib/common/deeps.ts @@ -25,25 +25,29 @@ import { isArray, isObject } from './validate'; * @param joiner * @returns {*} */ -export function deepGet(object, path, joiner = '/') { +export function deepGet( + object: Record | unknown[], + path: string, + joiner: string = '/', +): T | undefined { if (!isObject(object) && !Array.isArray(object)) { return undefined; } const keys = path.split(joiner); let i = 0; - let tmp = object; + let tmp: unknown = object; const len = keys.length; while (i < len) { - const key = keys[i++]; + const key = keys[i++]!; if (!tmp || !Object.hasOwnProperty.call(tmp, key)) { return undefined; } - tmp = tmp[key]; + tmp = (tmp as Record)[key]; } - return tmp; + return tmp as T; } /** @@ -54,26 +58,32 @@ export function deepGet(object, path, joiner = '/') { * @param initPaths * @param joiner */ -export function deepSet(object, path, value, initPaths = true, joiner = '.') { +export function deepSet( + object: Record, + path: string, + value: unknown, + initPaths: boolean = true, + joiner: string = '.', +): boolean { if (!isObject(object)) { return false; } const keys = path.split(joiner); let i = 0; - let _object = object; + let _object: unknown = object; const len = keys.length - 1; while (i < len) { - const key = keys[i++]; + const key = keys[i++]!; if (initPaths && !Object.hasOwnProperty.call(object, key)) { - _object[key] = {}; + (_object as Record)[key] = {}; } - _object = _object[key]; + _object = (_object as Record)[key]; } if (isObject(_object) || (isArray(_object) && !Number.isNaN(keys[i]))) { - _object[keys[i]] = value; + (_object as Record)[keys[i]!] = value; } else { return false; } diff --git a/packages/app/lib/common/id.js b/packages/app/lib/common/id.ts similarity index 69% rename from packages/app/lib/common/id.js rename to packages/app/lib/common/id.ts index 1266cb815b..568f33ef52 100644 --- a/packages/app/lib/common/id.js +++ b/packages/app/lib/common/id.ts @@ -1,3 +1,20 @@ +/* + * 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. + * + */ + const PUSH_CHARS = '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'; const AUTO_ID_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; @@ -9,14 +26,14 @@ let lastPushTime = 0; // timestamp to prevent collisions with other clients. We store the last characters we // generated because in the event of a collision, we'll use those same characters except // "incremented" by one. -const lastRandChars = []; +const lastRandChars: number[] = []; /** * Generate a firebase id - for use with ref().push(val, cb) - e.g. -KXMr7k2tXUFQqiaZRY4' * @param serverTimeOffset - pass in server time offset from native side * @returns {string} */ -export function generateDatabaseId(serverTimeOffset = 0) { +export function generateDatabaseId(serverTimeOffset: number = 0): string { const timeStampChars = new Array(8); let now = new Date().getTime() + serverTimeOffset; const duplicateTime = now === lastPushTime; @@ -46,11 +63,11 @@ export function generateDatabaseId(serverTimeOffset = 0) { lastRandChars[i] = 0; } - lastRandChars[i] += 1; + lastRandChars[i]! += 1; } for (let i = 0; i < 12; i++) { - id += PUSH_CHARS.charAt(lastRandChars[i]); + id += PUSH_CHARS.charAt(lastRandChars[i]!); } if (id.length !== 20) { @@ -64,7 +81,7 @@ export function generateDatabaseId(serverTimeOffset = 0) { * Generate a firestore auto id for use with collection/document .add() * @return {string} */ -export function generateFirestoreId() { +export function generateFirestoreId(): string { let autoId = ''; for (let i = 0; i < 20; i++) { diff --git a/packages/app/lib/common/index.js b/packages/app/lib/common/index.ts similarity index 89% rename from packages/app/lib/common/index.js rename to packages/app/lib/common/index.ts index 08afb367c2..585f075ccc 100644 --- a/packages/app/lib/common/index.js +++ b/packages/app/lib/common/index.ts @@ -17,6 +17,7 @@ import { Platform } from 'react-native'; import Base64 from './Base64'; import { isFunction, isObject, isString } from './validate'; +import type { DataUrlParts, Observer } from '../types/internal'; export * from './id'; export * from './path'; @@ -26,7 +27,9 @@ export * from './validate'; export { default as Base64 } from './Base64'; export { default as ReferenceBase } from './ReferenceBase'; -export function getDataUrlParts(dataUrlString) { +export type { DataUrlParts, Observer }; + +export function getDataUrlParts(dataUrlString: string): DataUrlParts { const isBase64 = dataUrlString.includes(';base64'); let [mediaType, base64String] = dataUrlString.split(','); if (!mediaType || !base64String) { @@ -42,11 +45,14 @@ export function getDataUrlParts(dataUrlString) { return { base64String, mediaType }; } -export function once(fn, context) { - let onceResult; +export function once any>( + fn: T, + context?: any, +): (...args: Parameters) => ReturnType { + let onceResult: ReturnType; let ranOnce = false; - return function onceInner(...args) { + return function onceInner(this: any, ...args: Parameters): ReturnType { if (!ranOnce) { ranOnce = true; onceResult = fn.apply(context || this, args); @@ -56,7 +62,7 @@ export function once(fn, context) { }; } -export function isError(value) { +export function isError(value: any): value is Error { if (Object.prototype.toString.call(value) === '[object Error]') { return true; } @@ -64,7 +70,7 @@ export function isError(value) { return value instanceof Error; } -export function hasOwnProperty(target, property) { +export function hasOwnProperty(target: object, property: string | symbol): boolean { return Object.hasOwnProperty.call(target, property); } @@ -74,7 +80,7 @@ export function hasOwnProperty(target, property) { * @param string * @returns {*} */ -export function stripTrailingSlash(string) { +export function stripTrailingSlash(string: any): any { if (!isString(string)) { return string; } @@ -87,7 +93,7 @@ export const isAndroid = Platform.OS === 'android'; export const isOther = Platform.OS !== 'ios' && Platform.OS !== 'android'; -export function tryJSONParse(string) { +export function tryJSONParse(string: string | null | undefined): any { try { return string && JSON.parse(string); } catch (_) { @@ -95,7 +101,7 @@ export function tryJSONParse(string) { } } -export function tryJSONStringify(data) { +export function tryJSONStringify(data: any): string | null { try { return JSON.stringify(data); } catch (_) { @@ -103,14 +109,17 @@ export function tryJSONStringify(data) { } } -export function parseListenerOrObserver(listenerOrObserver) { +export function parseListenerOrObserver( + listenerOrObserver: ((value: T) => void) | Observer, +): (value: T) => void { if (!isFunction(listenerOrObserver) && !isObject(listenerOrObserver)) { + throw new Error("'listenerOrObserver' expected a function or an object with 'next' function."); } if (isFunction(listenerOrObserver)) { return listenerOrObserver; } - if (isObject(listenerOrObserver) && isFunction(listenerOrObserver.next)) { - return listenerOrObserver.next.bind(listenerOrObserver); + if (isObject(listenerOrObserver) && isFunction((listenerOrObserver as Observer).next)) { + return (listenerOrObserver as Observer).next!.bind(listenerOrObserver); } throw new Error("'listenerOrObserver' expected a function or an object with 'next' function."); @@ -119,7 +128,11 @@ export function parseListenerOrObserver(listenerOrObserver) { // Used to indicate if there is no corresponding modular function const NO_REPLACEMENT = true; -const mapOfDeprecationReplacements = { +type MethodMap = Record; +type InstanceMap = Record; +type DeprecationMap = Record; + +const mapOfDeprecationReplacements: DeprecationMap = { analytics: { default: { logEvent: 'logEvent()', @@ -523,12 +536,17 @@ const modularDeprecationMessage = 'This method is deprecated (as well as all React Native Firebase namespaced API) and will be removed in the next major release ' + 'as part of move to match Firebase Web modular SDK API. Please see migration guide for more details: https://rnfirebase.io/migrating-to-v22'; -export function deprecationConsoleWarning(nameSpace, methodName, instanceName, isModularMethod) { +export function deprecationConsoleWarning( + nameSpace: string, + methodName: string, + instanceName: string, + isModularMethod: boolean, +): void { if (!isModularMethod) { const moduleMap = mapOfDeprecationReplacements[nameSpace]; if (moduleMap) { const instanceMap = moduleMap[instanceName]; - const deprecatedMethod = instanceMap[methodName]; + const deprecatedMethod = instanceMap?.[methodName]; if (instanceMap && deprecatedMethod) { if (!globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS) { // eslint-disable-next-line no-console @@ -544,11 +562,11 @@ export function deprecationConsoleWarning(nameSpace, methodName, instanceName, i } export function createMessage( - nameSpace, - methodName, - instanceName = 'default', - uniqueMessage = null, -) { + nameSpace: string, + methodName: string, + instanceName: string = 'default', + uniqueMessage: string | null = null, +): string | undefined { if (uniqueMessage) { // Unique deprecation message used for testing return uniqueMessage; @@ -570,9 +588,10 @@ export function createMessage( } } } + return undefined; } -function getNamespace(target) { +function getNamespace(target: any): string | undefined { if (target.constructor.name === 'DatabaseReference') { return 'database'; } @@ -588,13 +607,14 @@ function getNamespace(target) { } const className = target.name ? target.name : target.constructor.name; return Object.keys(mapOfDeprecationReplacements).find(key => { - if (mapOfDeprecationReplacements[key][className]) { + if (mapOfDeprecationReplacements[key]?.[className]) { return key; } + return false; }); } -function getInstanceName(target) { +function getInstanceName(target: any): string { if (target.GeoPoint || target.CustomProvider) { // target is statics object. GeoPoint - Firestore, CustomProvider - AppCheck return 'statics'; @@ -619,13 +639,13 @@ function getInstanceName(target) { return target.constructor.name; } -export function createDeprecationProxy(instance) { +export function createDeprecationProxy(instance: T): T { return new Proxy(instance, { - construct(target, args) { + construct(target: any, args: any[]) { // needed for Timestamp which we pass as static, when we construct new instance, we need to wrap it in proxy again. return createDeprecationProxy(new target(...args)); }, - get(target, prop, receiver) { + get(target: any, prop: string | symbol, receiver: any) { const originalMethod = target[prop]; if (prop === 'constructor') { @@ -633,7 +653,7 @@ export function createDeprecationProxy(instance) { } if (target && target.constructor && target.constructor.name === 'FirestoreTimestamp') { - deprecationConsoleWarning('firestore', prop, 'FirestoreTimestamp', false); + deprecationConsoleWarning('firestore', prop as string, 'FirestoreTimestamp', false); return Reflect.get(target, prop, receiver); } @@ -703,28 +723,30 @@ export function createDeprecationProxy(instance) { const instanceName = getInstanceName(target); const nameSpace = getNamespace(target); - if (descriptor.get) { + if (descriptor.get && nameSpace) { // Handle getter - call it and show deprecation warning - deprecationConsoleWarning(nameSpace, prop, instanceName, _isModularCall); + deprecationConsoleWarning(nameSpace, prop as string, instanceName, _isModularCall); return descriptor.get.call(target); } - if (descriptor.set) { + if (descriptor.set && nameSpace) { // Handle setter - return a function that calls the setter with deprecation warning - return function (value) { - deprecationConsoleWarning(nameSpace, prop, instanceName, _isModularCall); - descriptor.set.call(target, value); + return function (value: any) { + deprecationConsoleWarning(nameSpace, prop as string, instanceName, _isModularCall); + descriptor.set!.call(target, value); }; } } if (typeof originalMethod === 'function') { - return function (...args) { + return function (...args: any[]) { const isModularMethod = args.includes(MODULAR_DEPRECATION_ARG); const instanceName = getInstanceName(target); const nameSpace = getNamespace(target); - deprecationConsoleWarning(nameSpace, prop, instanceName, isModularMethod); + if (nameSpace) { + deprecationConsoleWarning(nameSpace, prop as string, instanceName, isModularMethod); + } return originalMethod.apply(target, filterModularArgument(args)); }; @@ -739,7 +761,7 @@ export const MODULAR_DEPRECATION_ARG = 'react-native-firebase-modular-method-cal // Flag to track if we're currently in a modular call let _isModularCall = false; -export function withModularFlag(fn) { +export function withModularFlag(fn: () => T): T { const previousFlag = _isModularCall; _isModularCall = true; try { @@ -749,11 +771,11 @@ export function withModularFlag(fn) { } } -export function filterModularArgument(list) { +export function filterModularArgument(list: any[]): any[] { return list.filter(arg => arg !== MODULAR_DEPRECATION_ARG); } -export function warnIfNotModularCall(args, replacementMethodName = '') { +export function warnIfNotModularCall(args: IArguments, replacementMethodName: string = ''): void { for (let i = 0; i < args.length; i++) { if (args[i] === MODULAR_DEPRECATION_ARG) { return; diff --git a/packages/app/lib/common/path.js b/packages/app/lib/common/path.ts similarity index 82% rename from packages/app/lib/common/path.js rename to packages/app/lib/common/path.ts index 3e276f8529..89c0f70399 100644 --- a/packages/app/lib/common/path.js +++ b/packages/app/lib/common/path.ts @@ -18,7 +18,7 @@ /** * Returns the next parent of the path e.g. /foo/bar/car -> /foo/bar */ -export function pathParent(path) { +export function pathParent(path: string): string | null { if (path.length === 0) { return null; } @@ -34,7 +34,7 @@ export function pathParent(path) { /** * Joins a parent and a child path */ -export function pathChild(path, childPath) { +export function pathChild(path: string, childPath: string): string { const canonicalChildPath = pathPieces(childPath).join('/'); if (path.length === 0) { @@ -47,7 +47,7 @@ export function pathChild(path, childPath) { /** * Returns the last component of a path, e.g /foo/bar.jpeg -> bar.jpeg */ -export function pathLastComponent(path) { +export function pathLastComponent(path: string): string { const index = path.lastIndexOf('/', path.length - 2); if (index === -1) { return path; @@ -61,7 +61,7 @@ export function pathLastComponent(path) { * @param path * @returns {*} */ -export function pathPieces(path) { +export function pathPieces(path: string): string[] { return path.split('/').filter($ => $.length > 0); } @@ -70,7 +70,7 @@ export function pathPieces(path) { * @param path * @returns {boolean} */ -export function pathIsEmpty(path) { +export function pathIsEmpty(path: string): boolean { return !pathPieces(path).length; } @@ -79,7 +79,7 @@ export function pathIsEmpty(path) { * @param path * @returns {string|string} */ -export function pathToUrlEncodedString(path) { +export function pathToUrlEncodedString(path: string): string { const pieces = pathPieces(path); let pathString = ''; for (let i = 0; i < pieces.length; i++) { @@ -95,7 +95,7 @@ export const INVALID_PATH_REGEX = /[[\].#$\u0000-\u001F\u007F]/; * @param path * @returns {boolean} */ -export function isValidPath(path) { +export function isValidPath(path: unknown): boolean { return typeof path === 'string' && path.length !== 0 && !INVALID_PATH_REGEX.test(path); } @@ -106,8 +106,8 @@ export const INVALID_KEY_REGEX = /[\[\].#$\/\u0000-\u001F\u007F]/; * @param key * @returns {boolean} */ -export function isValidKey(key) { - return typeof key === 'string' && key.length !== 0 && !INVALID_KEY_REGEX.test(path); +export function isValidKey(key: unknown): boolean { + return typeof key === 'string' && key.length !== 0 && !INVALID_KEY_REGEX.test(key); } /** @@ -115,7 +115,7 @@ export function isValidKey(key) { * @param path * @returns {*} */ -export function toFilePath(path) { +export function toFilePath(path: string): string { let _filePath = path.replace('file://', ''); if (_filePath.includes('%')) { _filePath = decodeURIComponent(_filePath); diff --git a/packages/app/lib/common/promise.js b/packages/app/lib/common/promise.ts similarity index 67% rename from packages/app/lib/common/promise.js rename to packages/app/lib/common/promise.ts index b1f9b86e4d..40e1b53e78 100644 --- a/packages/app/lib/common/promise.js +++ b/packages/app/lib/common/promise.ts @@ -16,17 +16,19 @@ */ import { isFunction } from './validate'; +import type { Deferred, Callback } from '../types/internal'; /** - * + * Creates a deferred promise */ -export function promiseDefer() { - const deferred = { +export function promiseDefer(): Deferred { + const deferred: Deferred = { + promise: null as unknown as Promise, resolve: null, reject: null, }; - deferred.promise = new Promise((resolve, reject) => { + deferred.promise = new Promise((resolve, reject) => { deferred.resolve = resolve; deferred.reject = reject; }); @@ -35,10 +37,14 @@ export function promiseDefer() { } /** + * Attaches an optional callback to a promise * @param promise * @param callback */ -export function promiseWithOptionalCallback(promise, callback) { +export function promiseWithOptionalCallback( + promise: Promise, + callback?: Callback, +): Promise { if (!isFunction(callback)) { return promise; } @@ -46,9 +52,9 @@ export function promiseWithOptionalCallback(promise, callback) { return promise .then(result => { if (callback && callback.length === 1) { - callback(null); + (callback as (error: Error | null) => void)(null); } else if (callback) { - callback(null, result); + (callback as (error: Error | null, result?: T) => void)(null, result); } return result; diff --git a/packages/app/lib/common/serialize.js b/packages/app/lib/common/serialize.ts similarity index 86% rename from packages/app/lib/common/serialize.js rename to packages/app/lib/common/serialize.ts index 2d92aecc73..f142f2552c 100644 --- a/packages/app/lib/common/serialize.js +++ b/packages/app/lib/common/serialize.ts @@ -17,8 +17,9 @@ import { tryJSONParse, tryJSONStringify } from './index'; import { isObject } from './validate'; +import type { SerializedValue } from '../types/internal'; -export function serializeType(value) { +export function serializeType(value: any): SerializedValue { if (isObject(value)) { return { type: 'object', @@ -32,7 +33,7 @@ export function serializeType(value) { }; } -export function serializeObject(object) { +export function serializeObject(object: any): any { if (!isObject(object)) { return object; } diff --git a/packages/app/lib/common/unitTestUtils.ts b/packages/app/lib/common/unitTestUtils.ts index 5b764f5005..f2315f06f8 100644 --- a/packages/app/lib/common/unitTestUtils.ts +++ b/packages/app/lib/common/unitTestUtils.ts @@ -1,4 +1,3 @@ -// @ts-nocheck import { expect, jest } from '@jest/globals'; import { createMessage } from './index'; @@ -28,10 +27,10 @@ export const createCheckV9Deprecation = (moduleNames: string[]): CheckV9Deprecat modularFunction: () => void, nonModularFunction: () => void, methodNameKey: string, - uniqueMessage: string?, + uniqueMessage?: string | null, checkFirebaseAppDeprecationWarning: boolean = false, ) => { - const moduleName = moduleNames[0]; // firestore, database, etc + const moduleName = moduleNames[0] as string; // firestore, database, etc const instanceName = moduleNames[1] || 'default'; // default, FirestoreCollectionReference, etc const consoleWarnSpy = jest.spyOn(console, 'warn'); consoleWarnSpy.mockReset(); @@ -53,7 +52,9 @@ export const createCheckV9Deprecation = (moduleNames: string[]): CheckV9Deprecat consoleWarnSpy.mockRestore(); const consoleWarnSpy2 = jest.spyOn(console, 'warn').mockImplementation(warnMessage => { const message = createMessage(moduleName, methodNameKey, instanceName, uniqueMessage); - expect(warnMessage).toMatch(message); + if (message) { + expect(warnMessage).toMatch(message); + } }); nonModularFunction(); diff --git a/packages/app/lib/common/validate.js b/packages/app/lib/common/validate.ts similarity index 68% rename from packages/app/lib/common/validate.js rename to packages/app/lib/common/validate.ts index de974d40bc..00f8a268a5 100644 --- a/packages/app/lib/common/validate.js +++ b/packages/app/lib/common/validate.ts @@ -19,7 +19,10 @@ import { Platform } from 'react-native'; const AlphaNumericUnderscore = /^[a-zA-Z0-9_]+$/; -export function objectKeyValuesAreStrings(object) { +/** + * Validates that all key-value pairs in an object are strings. + */ +export function objectKeyValuesAreStrings(object: unknown): boolean { if (!isObject(object)) { return false; } @@ -27,7 +30,7 @@ export function objectKeyValuesAreStrings(object) { const entries = Object.entries(object); for (let i = 0; i < entries.length; i++) { - const [key, value] = entries[i]; + const [key, value] = entries[i]!; if (!isString(key) || !isString(value)) { return false; } @@ -42,7 +45,7 @@ export function objectKeyValuesAreStrings(object) { * @param value * @returns {boolean} */ -export function isNull(value) { +export function isNull(value: unknown): value is null { return value === null; } @@ -52,7 +55,7 @@ export function isNull(value) { * @param value * @returns {boolean} */ -export function isObject(value) { +export function isObject(value: unknown): value is Record { return value ? typeof value === 'object' && !Array.isArray(value) && !isNull(value) : false; } @@ -62,9 +65,13 @@ export function isObject(value) { * @param value * @returns {boolean} */ -export function isDate(value) { +export function isDate(value: unknown): value is Date { // use the global isNaN() and not Number.isNaN() since it will validate an Invalid Date - return value && Object.prototype.toString.call(value) === '[object Date]' && !isNaN(value); + return value && + Object.prototype.toString.call(value) === '[object Date]' && + !isNaN(value as number) + ? true + : false; } /** @@ -73,7 +80,8 @@ export function isDate(value) { * @param value * @returns {*|boolean} */ -export function isFunction(value) { +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +export function isFunction(value: unknown): value is Function { return value ? typeof value === 'function' : false; } @@ -82,7 +90,7 @@ export function isFunction(value) { * @param value * @return {boolean} */ -export function isString(value) { +export function isString(value: unknown): value is string { return typeof value === 'string'; } @@ -91,7 +99,7 @@ export function isString(value) { * @param value * @return {boolean} */ -export function isNumber(value) { +export function isNumber(value: unknown): value is number { return typeof value === 'number'; } @@ -100,7 +108,10 @@ export function isNumber(value) { * @param value * @return {boolean} */ -export function isE164PhoneNumber(value) { +export function isE164PhoneNumber(value: unknown): boolean { + if (!isString(value)) { + return false; + } const PHONE_NUMBER = /^\+[1-9]\d{1,14}$/; // E.164 return PHONE_NUMBER.test(value); } @@ -110,7 +121,7 @@ export function isE164PhoneNumber(value) { * @param value * @returns {boolean} */ -export function isFinite(value) { +export function isFinite(value: unknown): boolean { return Number.isFinite(value); } @@ -119,7 +130,7 @@ export function isFinite(value) { * @param value * @returns {boolean} */ -export function isInteger(value) { +export function isInteger(value: unknown): boolean { return Number.isInteger(value); } @@ -129,16 +140,16 @@ export function isInteger(value) { * @param value * @return {boolean} */ -export function isBoolean(value) { +export function isBoolean(value: unknown): value is boolean { return typeof value === 'boolean'; } /** * * @param value - * @returns {arg is Array} + * @returns {arg is Array} */ -export function isArray(value) { +export function isArray(value: unknown): value is Array { return Array.isArray(value); } @@ -147,7 +158,7 @@ export function isArray(value) { * @param value * @returns {boolean} */ -export function isUndefined(value) { +export function isUndefined(value: unknown): value is undefined { return typeof value === 'undefined'; } @@ -157,7 +168,10 @@ export function isUndefined(value) { * @param value * @returns {boolean} */ -export function isAlphaNumericUnderscore(value) { +export function isAlphaNumericUnderscore(value: unknown): boolean { + if (!isString(value)) { + return false; + } return AlphaNumericUnderscore.test(value); } @@ -167,7 +181,10 @@ export function isAlphaNumericUnderscore(value) { * @returns {boolean} */ const IS_VALID_URL_REGEX = /^(http|https):\/\/[^ "]+$/; -export function isValidUrl(url) { +export function isValidUrl(url: unknown): boolean { + if (!isString(url)) { + return false; + } return IS_VALID_URL_REGEX.test(url); } @@ -178,18 +195,22 @@ export function isValidUrl(url) { * @param oneOf * @returns {boolean} */ -export function isOneOf(value, oneOf = []) { +export function isOneOf(value: unknown, oneOf: unknown[] = []): boolean { if (!isArray(oneOf)) { return false; } return oneOf.includes(value); } -export function noop() { +export function noop(): void { // noop-🐈 } -export function validateOptionalNativeDependencyExists(firebaseJsonKey, apiName, nativeFnExists) { +export function validateOptionalNativeDependencyExists( + firebaseJsonKey: string, + apiName: string, + nativeFnExists: boolean, +): void { if (nativeFnExists) { return; } diff --git a/packages/app/lib/index.ts b/packages/app/lib/index.ts new file mode 100644 index 0000000000..131ad4a115 --- /dev/null +++ b/packages/app/lib/index.ts @@ -0,0 +1,27 @@ +/* + * 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. + * + */ + +export { firebase, utils, default } from './namespaced'; +export * from './modular'; +export type { + ReactNativeFirebase, + Utils, + FirebaseApp, + LogCallbackParams, + LogCallback, + LogOptions, +} from './types/app'; diff --git a/packages/app/lib/internal/FirebaseModule.js b/packages/app/lib/internal/FirebaseModule.js deleted file mode 100644 index b970643b5c..0000000000 --- a/packages/app/lib/internal/FirebaseModule.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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. - * - */ - -import { getAppModule, getNativeModule } from './registry/nativeModule'; -import SharedEventEmitter from './SharedEventEmitter'; - -let firebaseJson = null; - -export default class FirebaseModule { - constructor(app, config, customUrlOrRegion) { - this._app = app; - this._nativeModule = null; - this._customUrlOrRegion = customUrlOrRegion; - this._config = Object.assign({}, config); - } - - get app() { - return this._app; - } - - get firebaseJson() { - if (firebaseJson) { - return firebaseJson; - } - firebaseJson = JSON.parse(getAppModule().FIREBASE_RAW_JSON); - return firebaseJson; - } - - get emitter() { - return SharedEventEmitter; - } - - // TODO Handle custom url or region? - eventNameForApp(...args) { - return `${this.app.name}-${args.join('-')}`; - } - - get native() { - if (this._nativeModule) { - return this._nativeModule; - } - this._nativeModule = getNativeModule(this); - return this._nativeModule; - } -} - -// Instance of checks don't work once compiled -FirebaseModule.__extended__ = {}; diff --git a/packages/app/lib/internal/FirebaseModule.ts b/packages/app/lib/internal/FirebaseModule.ts new file mode 100644 index 0000000000..c799aecb81 --- /dev/null +++ b/packages/app/lib/internal/FirebaseModule.ts @@ -0,0 +1,78 @@ +/* + * 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. + * + */ + +import { getAppModule, getNativeModule } from './registry/nativeModule'; +import SharedEventEmitter from './SharedEventEmitter'; +import type { ReactNativeFirebase } from '../types/app'; +import type { FirebaseJsonConfig, ModuleConfig } from '../types/internal'; +import type { ReactNativeFirebaseNativeModules } from './NativeModules'; +import type EventEmitter from 'react-native/Libraries/vendor/emitter/EventEmitter'; + +let firebaseJson: FirebaseJsonConfig | null = null; + +export default class FirebaseModule< + NativeModuleName extends keyof ReactNativeFirebaseNativeModules = any, +> { + _app: ReactNativeFirebase.FirebaseAppBase; + _nativeModule: ReactNativeFirebaseNativeModules[NativeModuleName] | null; + _customUrlOrRegion: string | null; + _config: ModuleConfig; + + constructor( + app: ReactNativeFirebase.FirebaseAppBase, + config: ModuleConfig, + customUrlOrRegion?: string | null, + ) { + this._app = app; + this._nativeModule = null; + this._customUrlOrRegion = customUrlOrRegion || null; + this._config = Object.assign({}, config); + } + + get app(): ReactNativeFirebase.FirebaseApp { + return this._app as unknown as ReactNativeFirebase.FirebaseApp; + } + + get firebaseJson(): FirebaseJsonConfig { + if (firebaseJson) { + return firebaseJson; + } + firebaseJson = JSON.parse(getAppModule().FIREBASE_RAW_JSON); + return firebaseJson as FirebaseJsonConfig; + } + + get emitter(): EventEmitter { + return SharedEventEmitter; + } + + eventNameForApp(...args: Array): string { + return `${this.app.name}-${args.join('-')}`; + } + + get native(): ReactNativeFirebaseNativeModules[NativeModuleName] { + if (this._nativeModule) { + return this._nativeModule; + } + this._nativeModule = getNativeModule( + this, + ) as unknown as ReactNativeFirebaseNativeModules[NativeModuleName]; + return this._nativeModule; + } +} + +// Instance of checks don't work once compiled +(FirebaseModule as any).__extended__ = {}; diff --git a/packages/app/lib/internal/NativeFirebaseError.js b/packages/app/lib/internal/NativeFirebaseError.ts similarity index 75% rename from packages/app/lib/internal/NativeFirebaseError.js rename to packages/app/lib/internal/NativeFirebaseError.ts index 5fed6889ba..66a9b1c4ee 100644 --- a/packages/app/lib/internal/NativeFirebaseError.js +++ b/packages/app/lib/internal/NativeFirebaseError.ts @@ -15,12 +15,31 @@ * */ +import type { NativeErrorUserInfo, NativeError } from '../types/internal'; + export default class NativeFirebaseError extends Error { - static fromEvent(errorEvent, namespace, stack) { - return new NativeFirebaseError({ userInfo: errorEvent }, stack || new Error().stack, namespace); + readonly namespace!: string; + readonly code!: string; + readonly jsStack!: string; + readonly userInfo!: NativeErrorUserInfo; + readonly customData: any; + readonly operationType!: string | null; + readonly nativeErrorCode!: string | number | null; + readonly nativeErrorMessage!: string | null; + + static fromEvent( + errorEvent: NativeErrorUserInfo, + namespace: string, + stack?: string, + ): NativeFirebaseError { + return new NativeFirebaseError( + { userInfo: errorEvent }, + stack || new Error().stack!, + namespace, + ); } - constructor(nativeError, jsStack, namespace) { + constructor(nativeError: NativeError, jsStack: string, namespace: string) { super(); const { userInfo } = nativeError; @@ -86,7 +105,7 @@ export default class NativeFirebaseError extends Error { * * @returns {string} */ - static getStackWithMessage(message, jsStack) { + static getStackWithMessage(message: string, jsStack: string): string { return [message, ...jsStack.split('\n').slice(2, 13)].join('\n'); } } diff --git a/packages/app/lib/internal/NativeModules.ts b/packages/app/lib/internal/NativeModules.ts new file mode 100644 index 0000000000..3700721c2d --- /dev/null +++ b/packages/app/lib/internal/NativeModules.ts @@ -0,0 +1,116 @@ +/* + * 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. + * + */ + +import type { ReactNativeFirebase } from '../types/app'; + +/** + * Base type for all React Native Firebase native modules. + * Each package can extend this interface via module augmentation to add their own native methods. + */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface ReactNativeFirebaseNativeModules { + // Base interface - packages will augment this +} + +/** + * Interface for wrapped native modules returned by getAppModule() and getNativeModule() + * This represents the native module after wrapping with error handling + */ +export interface WrappedNativeModule { + [key: string]: unknown; +} + +/** + * App Module native methods that are always available + */ +export interface RNFBAppModuleInterface { + // Constants + NATIVE_FIREBASE_APPS: Array<{ + appConfig: ReactNativeFirebase.FirebaseAppConfig; + options: ReactNativeFirebase.FirebaseAppOptions; + }>; + FIREBASE_RAW_JSON: string; + + // Methods + initializeApp( + options: ReactNativeFirebase.FirebaseAppOptions, + appConfig: ReactNativeFirebase.FirebaseAppConfig, + ): Promise; + deleteApp(name: string): Promise; + setLogLevel(logLevel: string): void; + metaGetAll(): Promise<{ [key: string]: string | boolean }>; + jsonGetAll(): Promise<{ [key: string]: string | boolean }>; + preferencesClearAll(): Promise; + preferencesGetAll(): Promise<{ [key: string]: string | boolean }>; + preferencesSetBool(key: string, value: boolean): Promise; + preferencesSetString(key: string, value: string): Promise; + setAutomaticDataCollectionEnabled(name: string, enabled: boolean): void; + + // Event emitter methods + eventsNotifyReady(ready: boolean): void; + eventsAddListener(eventType: string): void; + eventsRemoveListener(eventType: string, removeAll: boolean): void; + + // React Native EventEmitter compatibility + addListener?: (eventName: string) => void; + removeListeners?: (count: number) => void; +} + +/** + * Utils Module native methods (from the app package) + */ +export interface RNFBUtilsModuleInterface { + // Android-only properties and methods + isRunningInTestLab: boolean; + androidPlayServices: { + isAvailable: boolean; + status: number; + hasResolution: boolean; + isUserResolvableError: boolean; + error: string | undefined; + }; + androidGetPlayServicesStatus(): Promise<{ + isAvailable: boolean; + status: number; + hasResolution: boolean; + isUserResolvableError: boolean; + error: string | undefined; + }>; + androidPromptForPlayServices(): Promise; + androidMakePlayServicesAvailable(): Promise; + androidResolutionForPlayServices(): Promise; +} + +// Augment the base interface with app package's native modules +declare module './NativeModules' { + interface ReactNativeFirebaseNativeModules { + RNFBUtilsModule: RNFBUtilsModuleInterface; + RNFBAppModule: RNFBAppModuleInterface; + } +} + +/** + * Helper type to get a specific native module type by name + */ +export type GetNativeModule = + ReactNativeFirebaseNativeModules[T]; + +/** + * Union type of all available native module types + */ +export type AnyNativeModule = + ReactNativeFirebaseNativeModules[keyof ReactNativeFirebaseNativeModules]; diff --git a/packages/app/lib/internal/RNFBNativeEventEmitter.js b/packages/app/lib/internal/RNFBNativeEventEmitter.ts similarity index 57% rename from packages/app/lib/internal/RNFBNativeEventEmitter.js rename to packages/app/lib/internal/RNFBNativeEventEmitter.ts index 2c13e90ab0..2abb338595 100644 --- a/packages/app/lib/internal/RNFBNativeEventEmitter.js +++ b/packages/app/lib/internal/RNFBNativeEventEmitter.ts @@ -15,10 +15,28 @@ * */ -import { NativeEventEmitter } from 'react-native'; +import { type EmitterSubscription, NativeEventEmitter } from 'react-native'; import { getReactNativeModule } from './nativeModule'; +import type { RNFBAppModuleInterface } from './NativeModules'; + +/** + * Type for the eventsNotifyReady native method + */ +type EventsNotifyReadyMethod = (ready: boolean) => void; + +/** + * Type for the eventsAddListener native method + */ +type EventsAddListenerMethod = (eventType: string) => void; + +/** + * Type for the eventsRemoveListener native method + */ +type EventsRemoveListenerMethod = (eventType: string, removeAll: boolean) => void; class RNFBNativeEventEmitter extends NativeEventEmitter { + ready: boolean; + constructor() { const RNFBAppModule = getReactNativeModule('RNFBAppModule'); if (!RNFBAppModule) { @@ -26,22 +44,29 @@ class RNFBNativeEventEmitter extends NativeEventEmitter { 'Native module RNFBAppModule not found. Re-check module install, linking, configuration, build and install steps.', ); } - super(RNFBAppModule); + // Cast to any for NativeEventEmitter constructor which expects React Native's NativeModule type + super(RNFBAppModule as any); this.ready = false; } - addListener(eventType, listener, context) { - const RNFBAppModule = getReactNativeModule('RNFBAppModule'); + addListener( + eventType: string, + listener: (...args: unknown[]) => unknown, + context?: object, + ): EmitterSubscription { + const RNFBAppModule = getReactNativeModule( + 'RNFBAppModule', + ) as unknown as RNFBAppModuleInterface; if (!this.ready) { - RNFBAppModule.eventsNotifyReady(true); + (RNFBAppModule.eventsNotifyReady as EventsNotifyReadyMethod)(true); this.ready = true; } - RNFBAppModule.eventsAddListener(eventType); + (RNFBAppModule.eventsAddListener as EventsAddListenerMethod)(eventType); if (globalThis.RNFBDebug) { // eslint-disable-next-line no-console console.debug(`[RNFB-->Event][👂] ${eventType} -> listening`); } - const listenerDebugger = (...args) => { + const listenerDebugger = (...args: unknown[]) => { if (globalThis.RNFBDebug) { // eslint-disable-next-line no-console console.debug(`[RNFB<--Event][📣] ${eventType} <-`, JSON.stringify(args[0])); @@ -68,16 +93,18 @@ class RNFBNativeEventEmitter extends NativeEventEmitter { // - addListener returns an unsubscriber instead of a more complex object with eventType etc // make sure eventType for backwards compatibility just in case - subscription.eventType = `rnfb_${eventType}`; + (subscription as any).eventType = `rnfb_${eventType}`; // 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 = () => { - RNFBAppModule.eventsRemoveListener(eventType, false); - if (super.removeSubscription != null) { + const originalRemove = subscription.remove; + const newRemove = () => { + const module = getReactNativeModule('RNFBAppModule') as unknown as RNFBAppModuleInterface; + (module.eventsRemoveListener as EventsRemoveListenerMethod)(eventType, false); + const superClass = Object.getPrototypeOf(Object.getPrototypeOf(this)); + if (superClass.removeSubscription != null) { // This is for RN <= 0.64 - 65 and greater no longer have removeSubscription - super.removeSubscription(subscription); + superClass.removeSubscription(subscription); } else if (originalRemove != null) { // This is for RN >= 0.65 originalRemove(); @@ -87,18 +114,24 @@ class RNFBNativeEventEmitter extends NativeEventEmitter { return subscription; } - removeAllListeners(eventType) { - const RNFBAppModule = getReactNativeModule('RNFBAppModule'); - RNFBAppModule.eventsRemoveListener(eventType, true); + removeAllListeners(eventType: string): void { + const RNFBAppModule = getReactNativeModule( + 'RNFBAppModule', + ) as unknown as RNFBAppModuleInterface; + (RNFBAppModule.eventsRemoveListener as EventsRemoveListenerMethod)(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); + removeSubscription(subscription: EmitterSubscription & { eventType?: string }): void { + const RNFBAppModule = getReactNativeModule( + 'RNFBAppModule', + ) as unknown as RNFBAppModuleInterface; + const eventType = subscription.eventType?.replace('rnfb_', '') || ''; + (RNFBAppModule.eventsRemoveListener as EventsRemoveListenerMethod)(eventType, false); + const superClass = Object.getPrototypeOf(Object.getPrototypeOf(this)); + if (superClass.removeSubscription) { + superClass.removeSubscription(subscription); } } } diff --git a/packages/app/lib/internal/SharedEventEmitter.js b/packages/app/lib/internal/SharedEventEmitter.ts similarity index 100% rename from packages/app/lib/internal/SharedEventEmitter.js rename to packages/app/lib/internal/SharedEventEmitter.ts diff --git a/packages/app/lib/internal/asyncStorage.js b/packages/app/lib/internal/asyncStorage.js deleted file mode 100644 index 3cd4448531..0000000000 --- a/packages/app/lib/internal/asyncStorage.js +++ /dev/null @@ -1,47 +0,0 @@ -export const memoryStorage = new Map(); - -export const prefix = '@react-native-firebase:'; - -const asyncStorageMemory = { - setItem(key, value) { - memoryStorage.set(key, value); - return Promise.resolve(); - }, - getItem(key) { - const hasValue = memoryStorage.has(key); - if (hasValue) { - return Promise.resolve(memoryStorage.get(key)); - } - return Promise.resolve(null); - }, - removeItem: function (key) { - memoryStorage.delete(key); - return Promise.resolve(); - }, -}; - -let asyncStorage = asyncStorageMemory; - -export async function getReactNativeAsyncStorageInternal() { - return asyncStorage; -} - -export function setReactNativeAsyncStorageInternal(asyncStorageInstance) { - asyncStorage = asyncStorageInstance || asyncStorageMemory; -} - -export function isMemoryStorage() { - return asyncStorage === asyncStorageMemory; -} - -export async function setItem(key, value) { - return await asyncStorage.setItem(prefix + key, value); -} - -export async function getItem(key) { - return await asyncStorage.getItem(prefix + key); -} - -export async function removeItem(key) { - return await asyncStorage.removeItem(prefix + key); -} diff --git a/packages/app/lib/internal/asyncStorage.ts b/packages/app/lib/internal/asyncStorage.ts new file mode 100644 index 0000000000..6b87420060 --- /dev/null +++ b/packages/app/lib/internal/asyncStorage.ts @@ -0,0 +1,76 @@ +/* + * 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. + * + */ + +import type { AsyncStorageStatic } from '../types/internal'; + +// Memory storage Map instance +export const memoryStorage = new Map(); + +// Storage key prefix +export const prefix = '@react-native-firebase:'; + +const asyncStorageMemory: AsyncStorageStatic = { + setItem(key: string, value: string): Promise { + memoryStorage.set(key, value); + return Promise.resolve(); + }, + getItem(key: string): Promise { + const hasValue = memoryStorage.has(key); + if (hasValue) { + return Promise.resolve(memoryStorage.get(key) || null); + } + return Promise.resolve(null); + }, + removeItem(key: string): Promise { + memoryStorage.delete(key); + return Promise.resolve(); + }, +}; + +let asyncStorage: AsyncStorageStatic = asyncStorageMemory; + +// Get the current AsyncStorage instance (either React Native AsyncStorage or memory storage) +export async function getReactNativeAsyncStorageInternal(): Promise { + return asyncStorage; +} + +// Set the AsyncStorage instance to use (React Native AsyncStorage or fallback to memory storage) +export function setReactNativeAsyncStorageInternal( + asyncStorageInstance?: AsyncStorageStatic, +): void { + asyncStorage = asyncStorageInstance || asyncStorageMemory; +} + +// Check if currently using memory storage (fallback) +export function isMemoryStorage(): boolean { + return asyncStorage === asyncStorageMemory; +} + +// Set an item in storage with the React Native Firebase prefix +export async function setItem(key: string, value: string): Promise { + return await asyncStorage.setItem(prefix + key, value); +} + +// Get an item from storage with the React Native Firebase prefix +export async function getItem(key: string): Promise { + return await asyncStorage.getItem(prefix + key); +} + +// Remove an item from storage with the React Native Firebase prefix +export async function removeItem(key: string): Promise { + return await asyncStorage.removeItem(prefix + key); +} diff --git a/packages/app/lib/internal/constants.js b/packages/app/lib/internal/constants.ts similarity index 93% rename from packages/app/lib/internal/constants.js rename to packages/app/lib/internal/constants.ts index 642ef31c92..4741b11345 100644 --- a/packages/app/lib/internal/constants.js +++ b/packages/app/lib/internal/constants.ts @@ -39,4 +39,6 @@ export const KNOWN_NAMESPACES = [ 'notifications', 'perf', 'utils', -]; +] as const; + +export type KnownNamespace = (typeof KNOWN_NAMESPACES)[number]; diff --git a/packages/app/lib/internal/global.d.ts b/packages/app/lib/internal/global.d.ts new file mode 100644 index 0000000000..7112dd20c0 --- /dev/null +++ b/packages/app/lib/internal/global.d.ts @@ -0,0 +1,33 @@ +/* + * 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. + * + */ + +/** + * Global RNFB properties for debugging and testing + */ +declare global { + // Debug and testing flags + var RNFBDebug: boolean | undefined; + var RNFBTest: boolean | undefined; + var RNFBDebugInTestLeakDetection: boolean | undefined; + var RNFBDebugLastTest: string | undefined; + + // Modular API deprecation flags + var RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS: boolean | undefined; + var RNFB_MODULAR_DEPRECATION_STRICT_MODE: boolean | undefined; +} + +export {}; diff --git a/packages/app/lib/internal/index.js b/packages/app/lib/internal/index.ts similarity index 97% rename from packages/app/lib/internal/index.js rename to packages/app/lib/internal/index.ts index 120bea13b6..4774df70c0 100644 --- a/packages/app/lib/internal/index.js +++ b/packages/app/lib/internal/index.ts @@ -19,6 +19,7 @@ export { default as FirebaseApp } from '../FirebaseApp'; export * from './constants'; export { default as FirebaseModule } from './FirebaseModule'; export { default as NativeFirebaseError } from './NativeFirebaseError'; +export * from './NativeModules'; export * from './registry/app'; export * from './registry/namespace'; export * from './registry/nativeModule'; diff --git a/packages/app/lib/internal/logger.d.ts b/packages/app/lib/internal/logger.d.ts deleted file mode 100644 index e48f506274..0000000000 --- a/packages/app/lib/internal/logger.d.ts +++ /dev/null @@ -1,85 +0,0 @@ -export type LogLevelString = 'debug' | 'verbose' | 'info' | 'warn' | 'error' | 'silent'; - -export interface LogOptions { - level: LogLevelString; -} - -export type LogCallback = (callbackParams: LogCallbackParams) => void; - -export interface LogCallbackParams { - level: LogLevelString; - message: string; - args: unknown[]; - type: string; -} - -/** - * A container for all of the Logger instances - */ -export const instances: Logger[] = []; - -/** - * The JS SDK supports 5 log levels and also allows a user the ability to - * silence the logs altogether. - * - * The order is a follows: - * DEBUG < VERBOSE < INFO < WARN < ERROR - * - * All of the log types above the current log level will be captured (i.e. if - * you set the log level to `INFO`, errors will still be logged, but `DEBUG` and - * `VERBOSE` logs will not) - */ -export enum LogLevel { - DEBUG, - VERBOSE, - INFO, - WARN, - ERROR, - SILENT, -} - -type LevelStringToEnum = { - debug: LogLevel.DEBUG; - verbose: LogLevel.VERBOSE; - info: LogLevel.INFO; - warn: LogLevel.WARN; - error: LogLevel.ERROR; - silent: LogLevel.SILENT; -}; - -/** - * The default log level - */ -type DefaultLogLevel = LogLevel.INFO; - -/** - * We allow users the ability to pass their own log handler. We will pass the - * type of log, the current log level, and any other arguments passed (i.e. the - * messages that the user wants to log) to this function. - */ -export type LogHandler = (loggerInstance: Logger, logType: LogLevel, ...args: unknown[]) => void; - -export class Logger { - constructor(name: string); - - get logLevel(): LogLevel; - set logLevel(val: LogLevel); - - setLogLevel(val: LogLevel | LogLevelString): void; - - get logHandler(): LogHandler; - set logHandler(val: LogHandler); - - get userLogHandler(): LogHandler | null; - set userLogHandler(val: LogHandler | null); - - debug(...args: unknown[]): void; - log(...args: unknown[]): void; - info(...args: unknown[]): void; - warn(...args: unknown[]): void; - error(...args: unknown[]): void; -} - -export const setLogLevel: (level: LogLevelString | LogLevel) => void; - -export const setUserLogHandler: (logCallback: LogCallback | null, options?: LogOptions) => void; diff --git a/packages/app/lib/internal/logger.js b/packages/app/lib/internal/logger.ts similarity index 62% rename from packages/app/lib/internal/logger.js rename to packages/app/lib/internal/logger.ts index b1b84ace55..fac92d7a9c 100644 --- a/packages/app/lib/internal/logger.js +++ b/packages/app/lib/internal/logger.ts @@ -15,37 +15,48 @@ * */ +import type { LogCallback, LogOptions } from '../types/app'; + +type LogLevelString = 'debug' | 'verbose' | 'info' | 'warn' | 'error' | 'silent'; + /** - * @typedef {import('./logger').Logger} Logger - * @typedef {import('./logger').setLogLevel} setLogLevel - * @typedef {import('./logger').setUserLogHandler} setUserLogHandler - * @typedef {import('./logger').LogHandler} LogHandler - * @typedef {import('./logger').LogLevel} LogLevel - * @typedef {import('./logger').LevelStringToEnum} LevelStringToEnum - * @typedef {import('./logger').DefaultLogLevel} DefaultLogLevel + * The JS SDK supports 5 log levels and also allows a user the ability to + * silence the logs altogether. + * + * The order is a follows: + * DEBUG < VERBOSE < INFO < WARN < ERROR + * + * All of the log types above the current log level will be captured (i.e. if + * you set the log level to `INFO`, errors will still be logged, but `DEBUG` and + * `VERBOSE` logs will not) */ - -const LogLevel = { - DEBUG: 0, - VERBOSE: 1, - INFO: 2, - WARN: 3, - ERROR: 4, - SILENT: 5, -}; +export enum LogLevel { + DEBUG = 0, + VERBOSE = 1, + INFO = 2, + WARN = 3, + ERROR = 4, + SILENT = 5, +} // mimic the LogLevel in firebase-js-sdk TS -const reverseLogLevel = obj => { - const reversed = {}; +const reverseLogLevel = (obj: typeof LogLevel): Record => { + const reversed: Record = {}; for (const [key, value] of Object.entries(obj)) { - reversed[value] = key; + if (typeof value === 'number') { + reversed[value] = key; + } } return reversed; }; const LogLevelReversed = reverseLogLevel(LogLevel); -const levelStringToEnum = { +type LevelStringToEnum = { + [K in LogLevelString]: LogLevel; +}; + +const levelStringToEnum: LevelStringToEnum = { debug: LogLevel.DEBUG, verbose: LogLevel.VERBOSE, info: LogLevel.INFO, @@ -60,21 +71,28 @@ const levelStringToEnum = { * (i.e. once for firebase, and once in the console), we are sending `DEBUG` * logs to the `console.log` function. */ -const ConsoleMethod = { +const ConsoleMethod: Record = { [LogLevel.DEBUG]: 'log', [LogLevel.VERBOSE]: 'log', [LogLevel.INFO]: 'info', [LogLevel.WARN]: 'warn', [LogLevel.ERROR]: 'error', + [LogLevel.SILENT]: 'error', // fallback, should never be used }; +/** + * We allow users the ability to pass their own log handler. We will pass the + * type of log, the current log level, and any other arguments passed (i.e. the + * messages that the user wants to log) to this function. + */ +export type LogHandler = (loggerInstance: Logger, logType: LogLevel, ...args: unknown[]) => void; + /** * The default log handler will forward DEBUG, VERBOSE, INFO, WARN, and ERROR * messages on to their corresponding console counterparts (if the log method * is supported by the current log level) - * @type {LogHandler} */ -const defaultLogHandler = (instance, logType, ...args) => { +const defaultLogHandler: LogHandler = (instance, logType, ...args) => { if (logType < instance.logLevel) { return; } @@ -82,8 +100,7 @@ const defaultLogHandler = (instance, logType, ...args) => { const method = ConsoleMethod[logType]; if (method) { // 'log' | 'info' | 'warn' | 'error' - // eslint-disable-next-line no-console - console[method](`[${now}] ${instance.name}:`, ...args); + (console as any)[method](`[${now}] ${instance.name}:`, ...args); } else { throw new Error(`Attempted to log a message with an invalid logType (value: ${logType})`); } @@ -91,20 +108,27 @@ const defaultLogHandler = (instance, logType, ...args) => { const defaultLogLevel = LogLevel.INFO; -export const instances = []; - /** - * @type {Logger} + * A container for all of the Logger instances */ +export const instances: Logger[] = []; +/** + * Logger class for Firebase + */ export class Logger { + name: string; + private _logLevel: LogLevel = defaultLogLevel; + private _logHandler: LogHandler = defaultLogHandler; + private _userLogHandler: LogHandler | null = null; + /** * Gives you an instance of a Logger to capture messages according to * Firebase's logging scheme. * * @param name The name that the logs will be associated with */ - constructor(name) { + constructor(name: string) { /** * Capture the current instance for later use */ @@ -115,13 +139,11 @@ export class Logger { /** * The log level of the given Logger instance. */ - _logLevel = defaultLogLevel; - - get logLevel() { + get logLevel(): LogLevel { return this._logLevel; } - set logLevel(val) { + set logLevel(val: LogLevel) { if (!(val in LogLevel)) { throw new TypeError(`Invalid value "${val}" assigned to \`logLevel\``); } @@ -129,7 +151,7 @@ export class Logger { } // Workaround for setter/getter having to be the same type. - setLogLevel(val) { + setLogLevel(val: LogLevel | LogLevelString): void { this._logLevel = typeof val === 'string' ? levelStringToEnum[val] : val; } @@ -137,11 +159,11 @@ export class Logger { * The main (internal) log handler for the Logger instance. * Can be set to a new function in internal package code but not by user. */ - _logHandler = defaultLogHandler; - get logHandler() { + get logHandler(): LogHandler { return this._logHandler; } - set logHandler(val) { + + set logHandler(val: LogHandler) { if (typeof val !== 'function') { throw new TypeError('Value assigned to `logHandler` must be a function'); } @@ -151,11 +173,11 @@ export class Logger { /** * The optional, additional, user-defined log handler for the Logger instance. */ - _userLogHandler = null; - get userLogHandler() { + get userLogHandler(): LogHandler | null { return this._userLogHandler; } - set userLogHandler(val) { + + set userLogHandler(val: LogHandler | null) { this._userLogHandler = val; } @@ -163,50 +185,57 @@ export class Logger { * The functions below are all based on the `console` interface */ - debug(...args) { + debug(...args: unknown[]): void { this._userLogHandler && this._userLogHandler(this, LogLevel.DEBUG, ...args); this._logHandler(this, LogLevel.DEBUG, ...args); } - log(...args) { + + log(...args: unknown[]): void { this._userLogHandler && this._userLogHandler(this, LogLevel.VERBOSE, ...args); this._logHandler(this, LogLevel.VERBOSE, ...args); } - info(...args) { + + info(...args: unknown[]): void { this._userLogHandler && this._userLogHandler(this, LogLevel.INFO, ...args); this._logHandler(this, LogLevel.INFO, ...args); } - warn(...args) { + + warn(...args: unknown[]): void { this._userLogHandler && this._userLogHandler(this, LogLevel.WARN, ...args); this._logHandler(this, LogLevel.WARN, ...args); } - error(...args) { + + error(...args: unknown[]): void { this._userLogHandler && this._userLogHandler(this, LogLevel.ERROR, ...args); this._logHandler(this, LogLevel.ERROR, ...args); } } /** - * @type {setLogLevel} + * Sets the log level for all Logger instances */ -export function setLogLevelInternal(level) { +export function setLogLevel(level: LogLevelString | LogLevel): void { instances.forEach(inst => { inst.setLogLevel(level); }); } +// Alias for compatibility +export const setLogLevelInternal = setLogLevel; + /** - * @type {setUserLogHandler} + * Sets a custom user log handler for all Logger instances */ -export function setUserLogHandler(logCallback, options) { +export function setUserLogHandler(logCallback: LogCallback | null, options?: LogOptions): void { for (const instance of instances) { - let customLogLevel = null; - if (options && options.level) { + let customLogLevel: LogLevel | null = null; + if (options?.level) { customLogLevel = levelStringToEnum[options.level]; } if (logCallback === null) { instance.userLogHandler = null; } else { - instance.userLogHandler = (instance, level, ...args) => { + instance.userLogHandler = (_instance, level, ...args) => { const message = args .map(arg => { if (arg == null) { @@ -227,12 +256,12 @@ export function setUserLogHandler(logCallback, options) { }) .filter(arg => arg) .join(' '); - if (level >= (customLogLevel ?? instance.logLevel)) { + if (level >= (customLogLevel ?? _instance.logLevel)) { logCallback({ - level: LogLevelReversed[level].toLowerCase(), + level: LogLevelReversed[level]!.toLowerCase() as LogLevelString, message, args, - type: instance.name, + type: _instance.name, }); } }; diff --git a/packages/app/lib/internal/nativeModule.android.js b/packages/app/lib/internal/nativeModule.android.ts similarity index 100% rename from packages/app/lib/internal/nativeModule.android.js rename to packages/app/lib/internal/nativeModule.android.ts diff --git a/packages/app/lib/internal/nativeModule.ios.js b/packages/app/lib/internal/nativeModule.ios.ts similarity index 100% rename from packages/app/lib/internal/nativeModule.ios.js rename to packages/app/lib/internal/nativeModule.ios.ts diff --git a/packages/app/lib/internal/nativeModule.js b/packages/app/lib/internal/nativeModule.ts similarity index 100% rename from packages/app/lib/internal/nativeModule.js rename to packages/app/lib/internal/nativeModule.ts diff --git a/packages/app/lib/internal/nativeModuleAndroidIos.js b/packages/app/lib/internal/nativeModuleAndroidIos.js deleted file mode 100644 index 8254e4a91a..0000000000 --- a/packages/app/lib/internal/nativeModuleAndroidIos.js +++ /dev/null @@ -1,46 +0,0 @@ -/* eslint-disable no-console */ -import { NativeModules } from 'react-native'; -import { TurboModuleRegistry } 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] || TurboModuleRegistry.get(moduleName); - if (!globalThis.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/nativeModuleAndroidIos.ts b/packages/app/lib/internal/nativeModuleAndroidIos.ts new file mode 100644 index 0000000000..ad6c02fb82 --- /dev/null +++ b/packages/app/lib/internal/nativeModuleAndroidIos.ts @@ -0,0 +1,55 @@ +/* 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 + * @returns Raw native module from React Native (object with methods/properties or undefined) + */ +export function getReactNativeModule(moduleName: string): Record | undefined { + const nativeModule = NativeModules[moduleName]; + if (!globalThis.RNFBDebug) { + return nativeModule; + } + return new Proxy(nativeModule, { + ownKeys(target) { + return Object.keys(target); + }, + get: (_, name) => { + const prop = nativeModule[name as string]; + if (typeof prop !== 'function') return prop; + return (...args: unknown[]) => { + console.debug( + `[RNFB->Native][🔵] ${moduleName}.${String(name)} -> ${JSON.stringify(args)}`, + ); + const result: unknown = (prop as (...args: unknown[]) => unknown)(...args); + if (result && typeof result === 'object' && 'then' in result) { + return (result as Promise).then( + (res: unknown) => { + console.debug( + `[RNFB<-Native][🟢] ${moduleName}.${String(name)} <- ${JSON.stringify(res)}`, + ); + return res; + }, + (err: unknown) => { + console.debug( + `[RNFB<-Native][🔴] ${moduleName}.${String(name)} <- ${JSON.stringify(err)}`, + ); + throw err; + }, + ); + } + console.debug( + `[RNFB<-Native][🟢] ${moduleName}.${String(name)} <- ${JSON.stringify(result)}`, + ); + return result; + }; + }, + }); +} + +export function setReactNativeModule(): void { + // No-op +} diff --git a/packages/app/lib/internal/nativeModuleWeb.js b/packages/app/lib/internal/nativeModuleWeb.js deleted file mode 100644 index e39f939575..0000000000 --- a/packages/app/lib/internal/nativeModuleWeb.js +++ /dev/null @@ -1,49 +0,0 @@ -/* 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 (!globalThis.RNFBDebug) { - return nativeModule; - } - return new Proxy(nativeModule, { - ownKeys(target) { - // FIXME - test in new arch context - I don't think Object.keys works - 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/nativeModuleWeb.ts b/packages/app/lib/internal/nativeModuleWeb.ts new file mode 100644 index 0000000000..cc4e7d02d7 --- /dev/null +++ b/packages/app/lib/internal/nativeModuleWeb.ts @@ -0,0 +1,60 @@ +/* eslint-disable no-console */ +import RNFBAppModule from './web/RNFBAppModule'; + +const nativeModuleRegistry: Record> = {}; + +export function getReactNativeModule(moduleName: string): Record | undefined { + 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 (!globalThis.RNFBDebug) { + return nativeModule; + } + return new Proxy(nativeModule, { + ownKeys(target) { + // FIXME - test in new arch context - I don't think Object.keys works + return Object.keys(target); + }, + get: (_, name) => { + const prop = nativeModule[name as string]; + if (typeof prop !== 'function') return prop; + return (...args: unknown[]) => { + console.debug( + `[RNFB->Native][🔵] ${moduleName}.${String(name)} -> ${JSON.stringify(args)}`, + ); + const result: unknown = (prop as (...args: unknown[]) => unknown)(...args); + if (result && typeof result === 'object' && 'then' in result) { + return (result as Promise).then( + (res: unknown) => { + console.debug( + `[RNFB<-Native][🟢] ${moduleName}.${String(name)} <- ${JSON.stringify(res)}`, + ); + return res; + }, + (err: unknown) => { + console.debug( + `[RNFB<-Native][🔴] ${moduleName}.${String(name)} <- ${JSON.stringify(err)}`, + ); + throw err; + }, + ); + } + console.debug( + `[RNFB<-Native][🟢] ${moduleName}.${String(name)} <- ${JSON.stringify(result)}`, + ); + return result; + }; + }, + }); +} + +export function setReactNativeModule( + moduleName: string, + nativeModule: Record, +): void { + nativeModuleRegistry[moduleName] = nativeModule; +} + +setReactNativeModule('RNFBAppModule', RNFBAppModule); diff --git a/packages/app/lib/internal/nullSerialization.js b/packages/app/lib/internal/nullSerialization.ts similarity index 54% rename from packages/app/lib/internal/nullSerialization.js rename to packages/app/lib/internal/nullSerialization.ts index 6edc36d6b9..f918498952 100644 --- a/packages/app/lib/internal/nullSerialization.js +++ b/packages/app/lib/internal/nullSerialization.ts @@ -17,6 +17,27 @@ const NULL_SENTINEL = { __rnfbNull: true }; +type Encodable = string | number | boolean | null | EncodableObject | EncodableArray; +type EncodableObject = { [key: string]: Encodable }; +type EncodableArray = Encodable[]; + +type ArrayFrame = { + type: 'array'; + original: unknown[]; + encoded: unknown[]; + index: number; +}; + +type ObjectFrame = { + type: 'object'; + original: Record; + encoded: Record; + keys: string[]; + index: number; +}; + +type StackFrame = ArrayFrame | ObjectFrame; + /** * Replaces null values in object properties with sentinel objects for iOS TurboModule compatibility. * Uses iterative stack-based traversal to avoid stack overflow on deeply nested structures. @@ -28,10 +49,10 @@ const NULL_SENTINEL = { __rnfbNull: true }; * Note: Null values in arrays are preserved by iOS TurboModules, so we don't * encode them (but we still process nested objects within arrays). * - * @param {any} data - The data to encode - * @returns {any} - The encoded data with null object properties replaced by sentinels + * @param data - The data to encode + * @returns The encoded data with null object properties replaced by sentinels */ -export function encodeNullValues(data) { +export function encodeNullValues(data: unknown): unknown { if (data === null) { // only null values within objects are encoded return null; @@ -41,15 +62,20 @@ export function encodeNullValues(data) { } // Helper to process a child element and add it to the encoded container - function processChild(child, encoded, keyOrIndex, isArray, stack) { + function processArrayChild( + child: unknown, + encoded: unknown[], + index: number, + stack: StackFrame[], + ): void { if (child === null) { - // Arrays preserve nulls as null, objects convert to sentinel - encoded[keyOrIndex] = isArray ? null : NULL_SENTINEL; + // Arrays preserve nulls as null + encoded[index] = null; } else if (typeof child !== 'object') { - encoded[keyOrIndex] = child; + encoded[index] = child; } else if (Array.isArray(child)) { - const childEncoded = new Array(child.length); - encoded[keyOrIndex] = childEncoded; + const childEncoded: unknown[] = new Array(child.length); + encoded[index] = childEncoded; stack.push({ type: 'array', original: child, @@ -57,12 +83,45 @@ export function encodeNullValues(data) { index: 0, }); } else { - const childEncoded = {}; - encoded[keyOrIndex] = childEncoded; + const childEncoded: Record = {}; + encoded[index] = childEncoded; stack.push({ type: 'object', + original: child as Record, + encoded: childEncoded, + keys: Object.keys(child), + index: 0, + }); + } + } + + function processObjectChild( + child: unknown, + encoded: Record, + key: string, + stack: StackFrame[], + ): void { + if (child === null) { + // Objects convert null to sentinel + encoded[key] = NULL_SENTINEL; + } else if (typeof child !== 'object') { + encoded[key] = child; + } else if (Array.isArray(child)) { + const childEncoded: unknown[] = new Array(child.length); + encoded[key] = childEncoded; + stack.push({ + type: 'array', original: child, encoded: childEncoded, + index: 0, + }); + } else { + const childEncoded: Record = {}; + encoded[key] = childEncoded; + stack.push({ + type: 'object', + original: child as Record, + encoded: childEncoded, keys: Object.keys(child), index: 0, }); @@ -70,8 +129,8 @@ export function encodeNullValues(data) { } // Prepare root encoded container - let rootEncoded; - const stack = []; + let rootEncoded: unknown[] | Record; + const stack: StackFrame[] = []; if (Array.isArray(data)) { rootEncoded = new Array(data.length); @@ -85,7 +144,7 @@ export function encodeNullValues(data) { rootEncoded = {}; stack.push({ type: 'object', - original: data, + original: data as Record, encoded: rootEncoded, keys: Object.keys(data), index: 0, @@ -93,7 +152,7 @@ export function encodeNullValues(data) { } while (stack.length > 0) { - const frame = stack[stack.length - 1]; + const frame = stack[stack.length - 1]!; // Non-null assertion safe due to while condition if (frame.type === 'array') { const { original, encoded } = frame; @@ -106,7 +165,7 @@ export function encodeNullValues(data) { const i = frame.index++; const item = original[i]; - processChild(item, encoded, i, true, stack); + processArrayChild(item, encoded, i, stack); } else { // frame.type === 'object' const { original, encoded, keys } = frame; @@ -117,9 +176,9 @@ export function encodeNullValues(data) { continue; } - const key = keys[frame.index++]; + const key = keys[frame.index++]!; // Non-null assertion safe due to length check above const value = original[key]; - processChild(value, encoded, key, false, stack); + processObjectChild(value, encoded, key, stack); } } diff --git a/packages/app/lib/internal/registry/app.js b/packages/app/lib/internal/registry/app.ts similarity index 74% rename from packages/app/lib/internal/registry/app.js rename to packages/app/lib/internal/registry/app.ts index fda5741665..98c1916347 100644 --- a/packages/app/lib/internal/registry/app.js +++ b/packages/app/lib/internal/registry/app.ts @@ -24,23 +24,28 @@ import { isFunction, isString, isUndefined, -} from '@react-native-firebase/app/lib/common'; +} from '../../common'; import FirebaseApp from '../../FirebaseApp'; import { DEFAULT_APP_NAME } from '../constants'; import { setReactNativeAsyncStorageInternal } from '../asyncStorage'; import { getAppModule } from './nativeModule'; import { setLogLevelInternal } from '../logger'; +import type { ReactNativeFirebase } from '../../types/app'; -const APP_REGISTRY = {}; -let onAppCreateFn = null; -let onAppDestroyFn = null; +type FirebaseAppConfig = ReactNativeFirebase.FirebaseAppConfig; +type FirebaseAppOptions = ReactNativeFirebase.FirebaseAppOptions; +type ReactNativeAsyncStorage = ReactNativeFirebase.ReactNativeAsyncStorage; + +const APP_REGISTRY: Record = {}; +let onAppCreateFn: ((app: FirebaseApp) => void) | null = null; +let onAppDestroyFn: ((app: FirebaseApp) => void) | null = null; let initializedNativeApps = false; /** * This was needed to avoid metro require cycles... * @param fn */ -export function setOnAppCreate(fn) { +export function setOnAppCreate(fn: (app: FirebaseApp) => void): void { onAppCreateFn = fn; } @@ -48,7 +53,7 @@ export function setOnAppCreate(fn) { * This was needed to avoid metro require cycles... * @param fn */ -export function setOnAppDestroy(fn) { +export function setOnAppDestroy(fn: (app: FirebaseApp) => void): void { onAppDestroyFn = fn; } @@ -56,21 +61,23 @@ export function setOnAppDestroy(fn) { * Initializes all apps that were created natively (android/ios), * e.g. the Default firebase app from plist/json google services file. */ -export function initializeNativeApps() { +export function initializeNativeApps(): void { const nativeModule = getAppModule(); const { NATIVE_FIREBASE_APPS } = nativeModule; if (NATIVE_FIREBASE_APPS && NATIVE_FIREBASE_APPS.length) { for (let i = 0; i < NATIVE_FIREBASE_APPS.length; i++) { - const { appConfig, options } = NATIVE_FIREBASE_APPS[i]; - const { name } = appConfig; + const nativeApp = NATIVE_FIREBASE_APPS[i]; + if (!nativeApp) continue; + const { appConfig, options } = nativeApp; + const name = appConfig.name as string; APP_REGISTRY[name] = new FirebaseApp( - options, - appConfig, + options as FirebaseAppOptions, + appConfig as FirebaseAppConfig, true, deleteApp.bind(null, name, true), ); - onAppCreateFn(APP_REGISTRY[name]); + onAppCreateFn?.(APP_REGISTRY[name]); } } @@ -85,7 +92,7 @@ export function initializeNativeApps() { * * @param name */ -export function getApp(name = DEFAULT_APP_NAME) { +export function getApp(name: string = DEFAULT_APP_NAME): ReactNativeFirebase.FirebaseApp { warnIfNotModularCall(arguments, 'getApp()'); if (!initializedNativeApps) { initializeNativeApps(); @@ -96,18 +103,18 @@ export function getApp(name = DEFAULT_APP_NAME) { throw new Error(`No Firebase App '${name}' has been created - call firebase.initializeApp()`); } - return app; + return app as unknown as ReactNativeFirebase.FirebaseApp; } /** * Gets all app instances, used for `firebase.apps` */ -export function getApps() { +export function getApps(): ReactNativeFirebase.FirebaseApp[] { warnIfNotModularCall(arguments, 'getApps()'); if (!initializedNativeApps) { initializeNativeApps(); } - return Object.values(APP_REGISTRY); + return Object.values(APP_REGISTRY) as unknown as ReactNativeFirebase.FirebaseApp[]; } /** @@ -115,13 +122,16 @@ export function getApps() { * @param options * @param configOrName */ -export function initializeApp(options = {}, configOrName) { +export function initializeApp( + options: Partial = {}, + configOrName?: string | FirebaseAppConfig, +): Promise { warnIfNotModularCall(arguments, 'initializeApp()'); - let appConfig = configOrName; + let appConfig: FirebaseAppConfig = configOrName as FirebaseAppConfig; if (!isObject(configOrName) || isNull(configOrName)) { appConfig = { - name: configOrName, + name: configOrName as string, automaticResourceManagement: false, automaticDataCollectionEnabled: true, }; @@ -179,20 +189,25 @@ export function initializeApp(options = {}, configOrName) { ); } - const app = new FirebaseApp(options, appConfig, false, deleteApp.bind(null, name, true)); + const app = new FirebaseApp( + options as FirebaseAppOptions, + appConfig, + false, + deleteApp.bind(null, name, true), + ); // Note these initialization actions with side effects are performed prior to knowledge of // successful initialization in the native code. Native code *may* throw an error. APP_REGISTRY[name] = app; - onAppCreateFn(APP_REGISTRY[name]); + onAppCreateFn?.(APP_REGISTRY[name]); return getAppModule() - .initializeApp(options, appConfig) + .initializeApp(options as FirebaseAppOptions, appConfig) .then(() => { app._initialized = true; - return app; + return app as unknown as ReactNativeFirebase.FirebaseApp; }) - .catch(e => { + .catch((e: any) => { // we need to clean the app entry from registry as the app does not actually exist // There are still possible side effects from `onAppCreateFn` to consider but as existing // code may rely on that function running prior to native create, re-ordering it is a semantic change @@ -204,7 +219,7 @@ export function initializeApp(options = {}, configOrName) { }); } -export function setLogLevel(logLevel) { +export function setLogLevel(logLevel: ReactNativeFirebase.LogLevelString): void { warnIfNotModularCall(arguments, 'setLogLevel()'); if (!['error', 'warn', 'info', 'debug', 'verbose'].includes(logLevel)) { throw new Error('LogLevel must be one of "error", "warn", "info", "debug", "verbose"'); @@ -217,7 +232,7 @@ export function setLogLevel(logLevel) { } } -export function setReactNativeAsyncStorage(asyncStorage) { +export function setReactNativeAsyncStorage(asyncStorage: ReactNativeAsyncStorage): void { warnIfNotModularCall(arguments, 'setReactNativeAsyncStorage()'); if (!isObject(asyncStorage)) { @@ -236,13 +251,13 @@ export function setReactNativeAsyncStorage(asyncStorage) { throw new Error("setReactNativeAsyncStorage(*) 'asyncStorage.removeItem' must be a function."); } - setReactNativeAsyncStorageInternal(asyncStorage); + setReactNativeAsyncStorageInternal(asyncStorage as any); } /** * */ -export function deleteApp(name, nativeInitialized) { +export function deleteApp(name: string, nativeInitialized: boolean): Promise { if (name === DEFAULT_APP_NAME && nativeInitialized) { return Promise.reject(new Error('Unable to delete the default native firebase app instance.')); } @@ -256,8 +271,8 @@ export function deleteApp(name, nativeInitialized) { const nativeModule = getAppModule(); return nativeModule.deleteApp(name).then(() => { - app._deleted = true; - onAppDestroyFn(app); + (app as any)._deleted = true; + onAppDestroyFn?.(app); delete APP_REGISTRY[name]; }); } diff --git a/packages/app/lib/internal/registry/namespace.js b/packages/app/lib/internal/registry/namespace.ts similarity index 57% rename from packages/app/lib/internal/registry/namespace.js rename to packages/app/lib/internal/registry/namespace.ts index 76e3078666..1c391ee2fb 100644 --- a/packages/app/lib/internal/registry/namespace.js +++ b/packages/app/lib/internal/registry/namespace.ts @@ -17,9 +17,10 @@ import { isString, createDeprecationProxy } from '../../common'; import FirebaseApp from '../../FirebaseApp'; -import SDK_VERSION from '../../version'; -import { DEFAULT_APP_NAME, KNOWN_NAMESPACES } from '../constants'; +import { version as SDK_VERSION } from '../../version'; +import { DEFAULT_APP_NAME, KNOWN_NAMESPACES, type KnownNamespace } from '../constants'; import FirebaseModule from '../FirebaseModule'; +import type { ModuleGetter, FirebaseRoot, NamespaceConfig } from '../../types/internal'; import { getApp, getApps, @@ -31,12 +32,12 @@ import { } from './app'; // firebase.X -let FIREBASE_ROOT = null; +let FIREBASE_ROOT: FirebaseRoot | null = null; -const NAMESPACE_REGISTRY = {}; -const APP_MODULE_INSTANCE = {}; -const MODULE_GETTER_FOR_APP = {}; -const MODULE_GETTER_FOR_ROOT = {}; +const NAMESPACE_REGISTRY: Record = {}; +const APP_MODULE_INSTANCE: Record>> = {}; +const MODULE_GETTER_FOR_APP: Record> = {}; +const MODULE_GETTER_FOR_ROOT: Record = {}; /** * Attaches module namespace getters on every newly created app. @@ -46,10 +47,12 @@ const MODULE_GETTER_FOR_ROOT = {}; setOnAppCreate(app => { for (let i = 0; i < KNOWN_NAMESPACES.length; i++) { const moduleNamespace = KNOWN_NAMESPACES[i]; - Object.defineProperty(app, moduleNamespace, { - enumerable: false, - get: firebaseAppModuleProxy.bind(null, app, moduleNamespace), - }); + if (moduleNamespace) { + Object.defineProperty(app, moduleNamespace, { + enumerable: false, + get: firebaseAppModuleProxy.bind(null, app, moduleNamespace), + }); + } } }); @@ -70,17 +73,20 @@ setOnAppDestroy(app => { * @param moduleNamespace * @returns {*} */ -function getOrCreateModuleForApp(app, moduleNamespace) { - if (MODULE_GETTER_FOR_APP[app.name] && MODULE_GETTER_FOR_APP[app.name][moduleNamespace]) { - return MODULE_GETTER_FOR_APP[app.name][moduleNamespace]; +function getOrCreateModuleForApp(app: FirebaseApp, moduleNamespace: KnownNamespace): ModuleGetter { + if (MODULE_GETTER_FOR_APP[app.name] && MODULE_GETTER_FOR_APP[app.name]?.[moduleNamespace]) { + return MODULE_GETTER_FOR_APP[app.name]![moduleNamespace]!; } if (!MODULE_GETTER_FOR_APP[app.name]) { MODULE_GETTER_FOR_APP[app.name] = {}; } - const { hasCustomUrlOrRegionSupport, hasMultiAppSupport, ModuleClass } = - NAMESPACE_REGISTRY[moduleNamespace]; + const config = NAMESPACE_REGISTRY[moduleNamespace]; + if (!config) { + throw new Error(`Module namespace '${moduleNamespace}' is not registered.`); + } + const { hasCustomUrlOrRegionSupport, hasMultiAppSupport, ModuleClass } = config; // modules such as analytics only run on the default app if (!hasMultiAppSupport && app.name !== DEFAULT_APP_NAME) { @@ -94,7 +100,7 @@ function getOrCreateModuleForApp(app, moduleNamespace) { } // e.g. firebase.storage(customUrlOrRegion), firebase.functions(customUrlOrRegion), firebase.firestore(databaseId), firebase.database(url) - function firebaseModuleWithArgs(customUrlOrRegionOrDatabaseId) { + function firebaseModuleWithArgs(customUrlOrRegionOrDatabaseId?: string): FirebaseModule { if (customUrlOrRegionOrDatabaseId !== undefined) { if (!hasCustomUrlOrRegionSupport) { // TODO throw Module does not support arguments error @@ -113,19 +119,24 @@ function getOrCreateModuleForApp(app, moduleNamespace) { APP_MODULE_INSTANCE[app.name] = {}; } - if (!APP_MODULE_INSTANCE[app.name][key]) { + if (!APP_MODULE_INSTANCE[app.name]?.[key]) { + const moduleConfig = NAMESPACE_REGISTRY[moduleNamespace]; + if (!moduleConfig) { + throw new Error(`Module namespace '${moduleNamespace}' is not registered.`); + } const module = createDeprecationProxy( - new ModuleClass(app, NAMESPACE_REGISTRY[moduleNamespace], customUrlOrRegionOrDatabaseId), - ); + new ModuleClass(app, moduleConfig, customUrlOrRegionOrDatabaseId), + ) as unknown as FirebaseModule; - APP_MODULE_INSTANCE[app.name][key] = module; + APP_MODULE_INSTANCE[app.name]![key] = module; } - return APP_MODULE_INSTANCE[app.name][key]; + return APP_MODULE_INSTANCE[app.name]![key]!; } - MODULE_GETTER_FOR_APP[app.name][moduleNamespace] = firebaseModuleWithArgs; - return MODULE_GETTER_FOR_APP[app.name][moduleNamespace]; + MODULE_GETTER_FOR_APP[app.name]![moduleNamespace] = + firebaseModuleWithArgs as unknown as ModuleGetter; + return MODULE_GETTER_FOR_APP[app.name]![moduleNamespace]!; } /** @@ -133,18 +144,29 @@ function getOrCreateModuleForApp(app, moduleNamespace) { * @param moduleNamespace * @returns {*} */ -function getOrCreateModuleForRoot(moduleNamespace) { +function getOrCreateModuleForRoot(moduleNamespace: KnownNamespace): ModuleGetter { if (MODULE_GETTER_FOR_ROOT[moduleNamespace]) { return MODULE_GETTER_FOR_ROOT[moduleNamespace]; } - const { statics, hasMultiAppSupport, ModuleClass } = NAMESPACE_REGISTRY[moduleNamespace]; + const config = NAMESPACE_REGISTRY[moduleNamespace]; + if (!config) { + throw new Error(`Module namespace '${moduleNamespace}' is not registered.`); + } + const { statics, hasMultiAppSupport, ModuleClass } = config; // e.g. firebase.storage(app) - function firebaseModuleWithApp(app) { + function firebaseModuleWithApp(app?: FirebaseApp): FirebaseModule { const _app = app || getApp(); - if (!(_app instanceof FirebaseApp)) { + // Duck-type check for FirebaseApp (checking for required properties) + if ( + !_app || + typeof _app !== 'object' || + !('name' in _app) || + !('options' in _app) || + typeof _app.name !== 'string' + ) { throw new Error( [ `"firebase.${moduleNamespace}(app)" arg expects a FirebaseApp instance or undefined.`, @@ -169,22 +191,28 @@ function getOrCreateModuleForRoot(moduleNamespace) { APP_MODULE_INSTANCE[_app.name] = {}; } - if (!APP_MODULE_INSTANCE[_app.name][moduleNamespace]) { + if (!APP_MODULE_INSTANCE[_app.name]?.[moduleNamespace]) { + const moduleConfig = NAMESPACE_REGISTRY[moduleNamespace]; + if (!moduleConfig) { + throw new Error(`Module namespace '${moduleNamespace}' is not registered.`); + } const module = createDeprecationProxy( - new ModuleClass(_app, NAMESPACE_REGISTRY[moduleNamespace]), - ); - APP_MODULE_INSTANCE[_app.name][moduleNamespace] = module; + new ModuleClass(_app, moduleConfig), + ) as unknown as FirebaseModule; + APP_MODULE_INSTANCE[_app.name]![moduleNamespace] = module; } - return APP_MODULE_INSTANCE[_app.name][moduleNamespace]; + return APP_MODULE_INSTANCE[_app.name]![moduleNamespace]!; } Object.assign(firebaseModuleWithApp, statics || {}); // Object.freeze(firebaseModuleWithApp); // Wrap around statics, e.g. firebase.firestore.FieldValue, removed freeze as it stops proxy working. it is deprecated anyway - MODULE_GETTER_FOR_ROOT[moduleNamespace] = createDeprecationProxy(firebaseModuleWithApp); + MODULE_GETTER_FOR_ROOT[moduleNamespace] = createDeprecationProxy( + firebaseModuleWithApp, + ) as unknown as ModuleGetter; - return MODULE_GETTER_FOR_ROOT[moduleNamespace]; + return MODULE_GETTER_FOR_ROOT[moduleNamespace]!; } /** @@ -193,12 +221,15 @@ function getOrCreateModuleForRoot(moduleNamespace) { * @param moduleNamespace * @returns {*} */ -function firebaseRootModuleProxy(_firebaseNamespace, moduleNamespace) { +function firebaseRootModuleProxy( + _firebaseNamespace: FirebaseRoot, + moduleNamespace: string, +): ModuleGetter { if (NAMESPACE_REGISTRY[moduleNamespace]) { - return getOrCreateModuleForRoot(moduleNamespace); + return getOrCreateModuleForRoot(moduleNamespace as KnownNamespace); } - moduleWithDashes = moduleNamespace + const moduleWithDashes = moduleNamespace .split(/(?=[A-Z])/) .join('-') .toLowerCase(); @@ -218,13 +249,14 @@ function firebaseRootModuleProxy(_firebaseNamespace, moduleNamespace) { * @param moduleNamespace * @returns {*} */ -export function firebaseAppModuleProxy(app, moduleNamespace) { +export function firebaseAppModuleProxy(app: FirebaseApp, moduleNamespace: string): ModuleGetter { if (NAMESPACE_REGISTRY[moduleNamespace]) { - app._checkDestroyed(); - return getOrCreateModuleForApp(app, moduleNamespace); + // Call private _checkDestroyed method + (app as unknown as { _checkDestroyed: () => void })._checkDestroyed(); + return getOrCreateModuleForApp(app, moduleNamespace as KnownNamespace); } - moduleWithDashes = moduleNamespace + const moduleWithDashes = moduleNamespace .split(/(?=[A-Z])/) .join('-') .toLowerCase(); @@ -242,8 +274,9 @@ export function firebaseAppModuleProxy(app, moduleNamespace) { * * @returns {*} */ -export function createFirebaseRoot() { - FIREBASE_ROOT = { +export function createFirebaseRoot(): FirebaseRoot { + // Create partial root object - module namespaces like 'utils' are added dynamically below + const root = { initializeApp, setReactNativeAsyncStorage, get app() { @@ -254,24 +287,27 @@ export function createFirebaseRoot() { }, SDK_VERSION, setLogLevel, - }; + } as FirebaseRoot; for (let i = 0; i < KNOWN_NAMESPACES.length; i++) { const namespace = KNOWN_NAMESPACES[i]; - Object.defineProperty(FIREBASE_ROOT, namespace, { - enumerable: false, - get: firebaseRootModuleProxy.bind(null, FIREBASE_ROOT, namespace), - }); + if (namespace) { + Object.defineProperty(root, namespace, { + enumerable: false, + get: firebaseRootModuleProxy.bind(null, root, namespace), + }); + } } - return FIREBASE_ROOT; + FIREBASE_ROOT = root; + return root; } /** * * @returns {*} */ -export function getFirebaseRoot() { +export function getFirebaseRoot(): FirebaseRoot { if (FIREBASE_ROOT) { return FIREBASE_ROOT; } @@ -283,17 +319,20 @@ export function getFirebaseRoot() { * @param options * @returns {*} */ -export function createModuleNamespace(options = {}) { +export function createModuleNamespace(options: NamespaceConfig): ModuleGetter { const { namespace, ModuleClass } = options; if (!NAMESPACE_REGISTRY[namespace]) { // validation only for internal / module dev usage - if (FirebaseModule.__extended__ !== ModuleClass.__extended__) { + const firebaseModuleExtended = (FirebaseModule as unknown as { __extended__: object }) + .__extended__; + const moduleClassExtended = (ModuleClass as unknown as { __extended__: object }).__extended__; + if (firebaseModuleExtended !== moduleClassExtended) { throw new Error('INTERNAL ERROR: ModuleClass must be an instance of FirebaseModule.'); } NAMESPACE_REGISTRY[namespace] = Object.assign({}, options); } - return getFirebaseRoot()[namespace]; + return getFirebaseRoot()[namespace] as ModuleGetter; } diff --git a/packages/app/lib/internal/registry/nativeModule.js b/packages/app/lib/internal/registry/nativeModule.ts similarity index 68% rename from packages/app/lib/internal/registry/nativeModule.js rename to packages/app/lib/internal/registry/nativeModule.ts index 43611e46d0..559997b248 100644 --- a/packages/app/lib/internal/registry/nativeModule.js +++ b/packages/app/lib/internal/registry/nativeModule.ts @@ -16,16 +16,25 @@ */ import { APP_NATIVE_MODULE } from '../constants'; import NativeFirebaseError from '../NativeFirebaseError'; +import type { NativeError } from '../../types/internal'; import RNFBNativeEventEmitter from '../RNFBNativeEventEmitter'; import SharedEventEmitter from '../SharedEventEmitter'; import { getReactNativeModule } from '../nativeModule'; import { isAndroid, isIOS } from '../../common'; +import FirebaseModule from '../FirebaseModule'; +import type { WrappedNativeModule, RNFBAppModuleInterface } from '../NativeModules'; import { encodeNullValues } from '../nullSerialization'; -const NATIVE_MODULE_REGISTRY = {}; -const NATIVE_MODULE_EVENT_SUBSCRIPTIONS = {}; +interface NativeEvent { + appName?: string; + databaseId?: string; + [key: string]: unknown; +} + +const NATIVE_MODULE_REGISTRY: Record = {}; +const NATIVE_MODULE_EVENT_SUBSCRIPTIONS: Record = {}; -function nativeModuleKey(module) { +function nativeModuleKey(module: FirebaseModule): string { return `${module._customUrlOrRegion || ''}:${module.app.name}:${module._config.namespace}`; } @@ -38,8 +47,13 @@ function nativeModuleKey(module) { * @param argToPrepend * @returns {Function} */ -function nativeModuleMethodWrapped(namespace, method, argToPrepend, isTurboModule) { - return (...args) => { +function nativeModuleMethodWrapped( + namespace: string, + method: (...args: unknown[]) => unknown, + argToPrepend: unknown[], + isTurboModule: boolean, +): (...args: unknown[]) => unknown { + return (...args: unknown[]) => { // For iOS TurboModules, encode null values in arguments to work around // the limitation where null values in object properties get stripped during serialization // See: https://github.com/facebook/react-native/issues/52802 @@ -47,10 +61,10 @@ function nativeModuleMethodWrapped(namespace, method, argToPrepend, isTurboModul const allArgs = [...argToPrepend, ...processedArgs]; const possiblePromise = method(...allArgs); - if (possiblePromise && possiblePromise.then) { + if (possiblePromise && typeof possiblePromise === 'object' && 'then' in possiblePromise) { const jsStack = new Error().stack; - return possiblePromise.catch(nativeError => - Promise.reject(new NativeFirebaseError(nativeError, jsStack, namespace)), + return (possiblePromise as Promise).catch((nativeError: NativeError) => + Promise.reject(new NativeFirebaseError(nativeError, jsStack as string, namespace)), ); } @@ -62,29 +76,36 @@ function nativeModuleMethodWrapped(namespace, method, argToPrepend, isTurboModul * Prepends all arguments in prependArgs to all native method calls * * @param namespace - * @param NativeModule + * @param NativeModule - Raw native module from React Native * @param argToPrepend */ -function nativeModuleWrapped(namespace, NativeModule, argToPrepend, isTurboModule) { - const native = {}; +function nativeModuleWrapped( + namespace: string, + NativeModule: Record | undefined, + argToPrepend: unknown[], + isTurboModule: boolean, +): WrappedNativeModule { + const native: Record = {}; if (!NativeModule) { - return NativeModule; + return NativeModule as unknown as WrappedNativeModule; } - let properties = Object.keys(Object.getPrototypeOf(NativeModule)); - if (!properties.length) properties = Object.keys(NativeModule); + const nativeModuleObj = NativeModule; + let properties = Object.keys(Object.getPrototypeOf(nativeModuleObj)); + if (!properties.length) properties = Object.keys(nativeModuleObj); for (let i = 0, len = properties.length; i < len; i++) { const property = properties[i]; - if (typeof NativeModule[property] === 'function') { + if (!property) continue; + if (typeof nativeModuleObj[property] === 'function') { native[property] = nativeModuleMethodWrapped( namespace, - NativeModule[property], + nativeModuleObj[property] as (...args: unknown[]) => unknown, argToPrepend, isTurboModule, ); } else { - native[property] = NativeModule[property]; + native[property] = nativeModuleObj[property]; } } @@ -97,7 +118,7 @@ function nativeModuleWrapped(namespace, NativeModule, argToPrepend, isTurboModul * @param module * @returns {*} */ -function initialiseNativeModule(module) { +function initialiseNativeModule(module: FirebaseModule): WrappedNativeModule { const config = module._config; const key = nativeModuleKey(module); const { @@ -109,13 +130,16 @@ function initialiseNativeModule(module) { disablePrependCustomUrlOrRegion, turboModule, } = config; + const multiModuleRoot: WrappedNativeModule = {}; const isTurboModule = !!turboModule; - const multiModuleRoot = {}; const multiModule = Array.isArray(nativeModuleName); const nativeModuleNames = multiModule ? nativeModuleName : [nativeModuleName]; for (let i = 0; i < nativeModuleNames.length; i++) { - const nativeModule = getReactNativeModule(nativeModuleNames[i]); + const moduleName = nativeModuleNames[i]; + if (!moduleName) continue; + + const nativeModule = getReactNativeModule(moduleName); // only error if there's a single native module // as multi modules can mean some are optional @@ -124,17 +148,17 @@ function initialiseNativeModule(module) { } if (multiModule) { - multiModuleRoot[nativeModuleNames[i]] = !!nativeModule; + multiModuleRoot[moduleName] = !!nativeModule; } - const argToPrepend = []; + const argToPrepend: Array = []; if (hasMultiAppSupport) { argToPrepend.push(module.app.name); } if (hasCustomUrlOrRegionSupport && !disablePrependCustomUrlOrRegion) { - argToPrepend.push(module._customUrlOrRegion); + argToPrepend.push(module._customUrlOrRegion as string); } Object.assign( @@ -143,9 +167,12 @@ function initialiseNativeModule(module) { ); } - if (nativeEvents && nativeEvents.length) { + if (nativeEvents && Array.isArray(nativeEvents) && nativeEvents.length) { for (let i = 0, len = nativeEvents.length; i < len; i++) { - subscribeToNativeModuleEvent(nativeEvents[i]); + const eventName = nativeEvents[i]; + if (eventName) { + subscribeToNativeModuleEvent(eventName); + } } } @@ -165,9 +192,10 @@ function initialiseNativeModule(module) { * @param eventName * @private */ -function subscribeToNativeModuleEvent(eventName) { +function subscribeToNativeModuleEvent(eventName: string): void { if (!NATIVE_MODULE_EVENT_SUBSCRIPTIONS[eventName]) { - RNFBNativeEventEmitter.addListener(eventName, event => { + RNFBNativeEventEmitter.addListener(eventName, (...args: unknown[]) => { + const event = args[0] as NativeEvent; if (event.appName && event.databaseId) { // Firestore requires both appName and databaseId to prefix SharedEventEmitter.emit(`${event.appName}-${event.databaseId}-${eventName}`, event); @@ -190,7 +218,7 @@ function subscribeToNativeModuleEvent(eventName) { * @param namespace * @returns {string} */ -function getMissingModuleHelpText(namespace) { +function getMissingModuleHelpText(namespace: string): string { const snippet = `firebase.${namespace}()`; if (isIOS || isAndroid) { @@ -215,7 +243,7 @@ function getMissingModuleHelpText(namespace) { * @param module * @returns {*} */ -export function getNativeModule(module) { +export function getNativeModule(module: FirebaseModule): WrappedNativeModule { const key = nativeModuleKey(module); if (NATIVE_MODULE_REGISTRY[key]) { @@ -230,9 +258,9 @@ export function getNativeModule(module) { * * @returns {*} */ -export function getAppModule() { +export function getAppModule(): RNFBAppModuleInterface { if (NATIVE_MODULE_REGISTRY[APP_NATIVE_MODULE]) { - return NATIVE_MODULE_REGISTRY[APP_NATIVE_MODULE]; + return NATIVE_MODULE_REGISTRY[APP_NATIVE_MODULE] as unknown as RNFBAppModuleInterface; } const namespace = 'app'; @@ -250,5 +278,5 @@ export function getAppModule() { false, ); - return NATIVE_MODULE_REGISTRY[APP_NATIVE_MODULE]; + return NATIVE_MODULE_REGISTRY[APP_NATIVE_MODULE] as unknown as RNFBAppModuleInterface; } diff --git a/packages/app/lib/internal/web/RNFBAppModule.js b/packages/app/lib/internal/web/RNFBAppModule.js deleted file mode 100644 index 35bb238e05..0000000000 --- a/packages/app/lib/internal/web/RNFBAppModule.js +++ /dev/null @@ -1,261 +0,0 @@ -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/RNFBAppModule.ts b/packages/app/lib/internal/web/RNFBAppModule.ts new file mode 100644 index 0000000000..257d28135a --- /dev/null +++ b/packages/app/lib/internal/web/RNFBAppModule.ts @@ -0,0 +1,318 @@ +/* + * 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. + * + */ + +import { initializeApp, setLogLevel, getApp, getApps, deleteApp } from './firebaseApp'; +import { DeviceEventEmitter } from 'react-native'; +import { type ReactNativeFirebase } from '../../index'; + +// Types for internal state +interface QueuedEvent { + eventName: string; + eventBody: any; +} + +interface EventListeners { + [eventName: string]: number; +} + +interface ListenersMap { + listeners: number; + queued: number; + events: EventListeners; +} + +interface PreferencesStorage { + [key: string]: boolean | string; +} + +interface InitializeAppResult { + options: ReactNativeFirebase.FirebaseAppOptions; + appConfig: ReactNativeFirebase.FirebaseAppConfig; +} + +// Variables for events tracking. +let jsReady = false; +let jsListenerCount = 0; +let queuedEvents: QueuedEvent[] = []; +let jsListeners: EventListeners = {}; + +// For compatibility we have a fake preferences storage, +// it does not persist across app restarts. +let fakePreferencesStorage: PreferencesStorage = {}; + +function eventsGetListenersMap(): ListenersMap { + return { + listeners: jsListenerCount, + queued: queuedEvents.length, + events: jsListeners, + }; +} + +function eventsSendEvent(eventName: string, eventBody: any): void { + if (!jsReady || !jsListeners.hasOwnProperty(eventName)) { + const event: QueuedEvent = { + eventName, + eventBody, + }; + queuedEvents.push(event); + return; + } + setImmediate(() => DeviceEventEmitter.emit('rnfb_' + eventName, eventBody)); +} + +function eventsSendQueuedEvents(): void { + if (queuedEvents.length === 0) { + return; + } + const events = Array.from(queuedEvents); + queuedEvents = []; + for (let i = 0; i < events.length; i++) { + const event = events[i]; + if (event) { + eventsSendEvent(event.eventName, event.eventBody); + } + } +} + +export default { + // Natively initialized apps, but in the case of web, we don't have any. + NATIVE_FIREBASE_APPS: [] as any[], + // 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 options - The Firebase app options. + * @param appConfig - The Firebase app config. + * @returns The Firebase app instance. + */ + async initializeApp( + options: ReactNativeFirebase.FirebaseAppOptions, + appConfig: ReactNativeFirebase.FirebaseAppConfig, + ): Promise { + const name = appConfig.name; + const existingApp = getApps().find(app => app.name === name); + if (existingApp) { + return { + options, + appConfig, + }; + } + const newAppConfig: any = { + name, + }; + if ( + appConfig.automaticDataCollectionEnabled === true || + appConfig.automaticDataCollectionEnabled === false + ) { + newAppConfig.automaticDataCollectionEnabled = appConfig.automaticDataCollectionEnabled; + } + const optionsCopy = Object.assign({}, options); + + delete (optionsCopy as any).clientId; + initializeApp(optionsCopy, newAppConfig); + return { + options, + appConfig, + }; + }, + + /** + * Sets the log level for the Firebase app. + * + * @param logLevel - The log level to set. + */ + setLogLevel(logLevel: ReactNativeFirebase.LogLevelString): void { + setLogLevel(logLevel as any); + }, + + /** + * Sets the automatic data collection for the Firebase app. + * + * @param appName - The name of the Firebase app. + * @param enabled - Whether to enable automatic data collection. + */ + setAutomaticDataCollectionEnabled(appName: string, enabled: boolean): void { + (getApp(appName) as any).automaticDataCollectionEnabled = enabled; + }, + + /** + * Deletes a Firebase app. + * + * @param appName - The name of the Firebase app to delete. + */ + async deleteApp(appName: string): Promise { + if (getApp(appName)) { + await deleteApp(getApp(appName) as any); + } + }, + + /** + * Gets the meta data. + * Unsupported on web. + * + * @returns The meta data + */ + metaGetAll(): Record { + return {}; + }, + + /** + * Gets the firebase.json data. + * Unsupported on web. + * + * @returns The JSON data for the firebase.json file. + */ + jsonGetAll(): Record { + return {}; + }, + + /** + * Sets a boolean value for a preference. + * Unsupported on web. + * + * @param key - The key of the preference. + * @param value - The value to set. + */ + async preferencesSetBool(key: string, value: boolean): Promise { + fakePreferencesStorage[key] = value; + }, + + /** + * Sets a string value for a preference. + * Unsupported on web. + * + * @param key - The key of the preference. + * @param value - The value to set. + */ + preferencesSetString(key: string, value: string): void { + fakePreferencesStorage[key] = value; + }, + + /** + * Gets all preferences. + * Unsupported on web. + * + * @returns The preferences. + */ + preferencesGetAll(): PreferencesStorage { + return Object.assign({}, fakePreferencesStorage); + }, + + /** + * Clears all preferences. + * Unsupported on web. + */ + preferencesClearAll(): void { + fakePreferencesStorage = {}; + }, + + /** + * Adds a listener for an event. + * Unsupported on web. + * + * @param eventName - The name of the event to listen for. + */ + addListener(): void { + // Keep: Required for RN built in Event Emitter Calls. + }, + /** + * Removes a listener for an event. + * Unsupported on web. + * + * @param eventName - The name of the event to remove the listener for. + */ + removeListeners(): void { + // Keep: Required for RN built in Event Emitter Calls. + }, + + /** + * Notifies the app that it is ready to receive events. + * + * @param ready - Whether the app is ready to receive events. + * @returns void + */ + eventsNotifyReady(ready: boolean): void { + jsReady = ready; + if (jsReady) { + setImmediate(() => eventsSendQueuedEvents()); + } + }, + + /** + * Gets all the listeners registered. + * + * @returns The listeners for the event. + */ + eventsGetListeners(): ListenersMap { + return eventsGetListenersMap(); + }, + + /** + * Sends an event to the app for testing purposes. + * + * @param eventName - The name of the event to send. + * @param eventBody - The body of the event to send. + * @returns void + */ + eventsPing(eventName: string, eventBody: any): void { + eventsSendEvent(eventName, eventBody); + }, + + /** + * Adds a listener for an event. + * + * @param eventName - The name of the event to listen for. + */ + eventsAddListener(eventName: string): void { + jsListenerCount++; + if (!jsListeners.hasOwnProperty(eventName)) { + jsListeners[eventName] = 1; + } else { + if (jsListeners[eventName] !== undefined) { + jsListeners[eventName]++; + } + } + setImmediate(() => eventsSendQueuedEvents()); + }, + + /** + * Removes a single listener for an event or all listeners for an event. + * + * @param eventName - The name of the event to remove the listener for. + * @param all - Optional. Whether to remove all listeners for the event. + */ + eventsRemoveListener(eventName: string, all?: boolean): void { + if (jsListeners.hasOwnProperty(eventName)) { + const count = jsListeners[eventName]; + if (count !== undefined) { + if (count <= 1 || all) { + jsListenerCount -= count; + delete jsListeners[eventName]; + } else { + jsListenerCount--; + const currentCount = jsListeners[eventName]; + if (currentCount !== undefined) { + jsListeners[eventName] = currentCount - 1; + } + } + } + } + }, +}; diff --git a/packages/app/lib/internal/web/firebaseApp.js b/packages/app/lib/internal/web/firebaseApp.js deleted file mode 100644 index c50cf77658..0000000000 --- a/packages/app/lib/internal/web/firebaseApp.js +++ /dev/null @@ -1,3 +0,0 @@ -// 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/firebaseApp.ts b/packages/app/lib/internal/web/firebaseApp.ts new file mode 100644 index 0000000000..dfbe956cad --- /dev/null +++ b/packages/app/lib/internal/web/firebaseApp.ts @@ -0,0 +1,20 @@ +/* + * 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. + * + */ + +// 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/firebaseAppCheck.js b/packages/app/lib/internal/web/firebaseAppCheck.js deleted file mode 100644 index 455eeaf078..0000000000 --- a/packages/app/lib/internal/web/firebaseAppCheck.js +++ /dev/null @@ -1,6 +0,0 @@ -// We need to share firebase imports between modules, otherwise -// apps and instances of the firebase modules are not shared. -import 'firebase/app'; -export { getApp } from 'firebase/app'; -export * from 'firebase/app-check'; -export { makeIDBAvailable } from './memidb'; diff --git a/packages/app/lib/internal/web/firebaseAppCheck.ts b/packages/app/lib/internal/web/firebaseAppCheck.ts new file mode 100644 index 0000000000..d4a5072f58 --- /dev/null +++ b/packages/app/lib/internal/web/firebaseAppCheck.ts @@ -0,0 +1,23 @@ +/* + * 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. + * + */ + +// We need to share firebase imports between modules, otherwise +// apps and instances of the firebase modules are not shared. +import 'firebase/app'; +export { getApp } from 'firebase/app'; +export * from 'firebase/app-check'; +export { makeIDBAvailable } from './memidb'; diff --git a/packages/app/lib/internal/web/firebaseAuth.js b/packages/app/lib/internal/web/firebaseAuth.js deleted file mode 100644 index 809e1208a2..0000000000 --- a/packages/app/lib/internal/web/firebaseAuth.js +++ /dev/null @@ -1,4 +0,0 @@ -// 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/auth'; diff --git a/packages/app/lib/internal/web/firebaseAuth.ts b/packages/app/lib/internal/web/firebaseAuth.ts new file mode 100644 index 0000000000..5091f05121 --- /dev/null +++ b/packages/app/lib/internal/web/firebaseAuth.ts @@ -0,0 +1,21 @@ +/* + * 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. + * + */ + +// 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/auth'; diff --git a/packages/app/lib/internal/web/firebaseDatabase.js b/packages/app/lib/internal/web/firebaseDatabase.js deleted file mode 100644 index bf5ae640b4..0000000000 --- a/packages/app/lib/internal/web/firebaseDatabase.js +++ /dev/null @@ -1,4 +0,0 @@ -// 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/database'; diff --git a/packages/app/lib/internal/web/firebaseDatabase.ts b/packages/app/lib/internal/web/firebaseDatabase.ts new file mode 100644 index 0000000000..4c42dc04ac --- /dev/null +++ b/packages/app/lib/internal/web/firebaseDatabase.ts @@ -0,0 +1,21 @@ +/* + * 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. + * + */ + +// 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/database'; diff --git a/packages/app/lib/internal/web/firebaseFirestore.js b/packages/app/lib/internal/web/firebaseFirestore.js deleted file mode 100644 index e3a7517966..0000000000 --- a/packages/app/lib/internal/web/firebaseFirestore.js +++ /dev/null @@ -1,4 +0,0 @@ -// We need to share firebase imports between modules, otherwise -// apps and instances of the firebase modules are not shared. -export { getApp } from 'firebase/app'; -export * from 'firebase/firestore/lite'; diff --git a/packages/app/lib/internal/web/firebaseFirestore.ts b/packages/app/lib/internal/web/firebaseFirestore.ts new file mode 100644 index 0000000000..a07d4d488d --- /dev/null +++ b/packages/app/lib/internal/web/firebaseFirestore.ts @@ -0,0 +1,21 @@ +/* + * 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. + * + */ + +// We need to share firebase imports between modules, otherwise +// apps and instances of the firebase modules are not shared. +export { getApp } from 'firebase/app'; +export * from 'firebase/firestore/lite'; diff --git a/packages/app/lib/internal/web/firebaseFunctions.js b/packages/app/lib/internal/web/firebaseFunctions.js deleted file mode 100644 index f8e4401a14..0000000000 --- a/packages/app/lib/internal/web/firebaseFunctions.js +++ /dev/null @@ -1,4 +0,0 @@ -// 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/internal/web/firebaseFunctions.ts b/packages/app/lib/internal/web/firebaseFunctions.ts new file mode 100644 index 0000000000..c8caaf81ac --- /dev/null +++ b/packages/app/lib/internal/web/firebaseFunctions.ts @@ -0,0 +1,21 @@ +/* + * 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. + * + */ + +// 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/internal/web/firebaseInstallations.js b/packages/app/lib/internal/web/firebaseInstallations.js deleted file mode 100644 index 9bd0a88b2e..0000000000 --- a/packages/app/lib/internal/web/firebaseInstallations.js +++ /dev/null @@ -1,6 +0,0 @@ -// We need to share firebase imports between modules, otherwise -// apps and instances of the firebase modules are not shared. -import 'firebase/app'; -export { getApp } from 'firebase/app'; -export * from 'firebase/installations'; -export { makeIDBAvailable } from './memidb'; diff --git a/packages/app/lib/internal/web/firebaseInstallations.ts b/packages/app/lib/internal/web/firebaseInstallations.ts new file mode 100644 index 0000000000..6d9dff9dd4 --- /dev/null +++ b/packages/app/lib/internal/web/firebaseInstallations.ts @@ -0,0 +1,23 @@ +/* + * 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. + * + */ + +// We need to share firebase imports between modules, otherwise +// apps and instances of the firebase modules are not shared. +import 'firebase/app'; +export { getApp } from 'firebase/app'; +export * from 'firebase/installations'; +export { makeIDBAvailable } from './memidb'; diff --git a/packages/app/lib/internal/web/firebaseRemoteConfig.js b/packages/app/lib/internal/web/firebaseRemoteConfig.js deleted file mode 100644 index 2079f29130..0000000000 --- a/packages/app/lib/internal/web/firebaseRemoteConfig.js +++ /dev/null @@ -1,6 +0,0 @@ -// We need to share firebase imports between modules, otherwise -// apps and instances of the firebase modules are not shared. -import 'firebase/app'; -export { getApp } from 'firebase/app'; -export * from 'firebase/remote-config'; -export { makeIDBAvailable } from './memidb'; diff --git a/packages/app/lib/internal/web/firebaseRemoteConfig.ts b/packages/app/lib/internal/web/firebaseRemoteConfig.ts new file mode 100644 index 0000000000..ef71a2575d --- /dev/null +++ b/packages/app/lib/internal/web/firebaseRemoteConfig.ts @@ -0,0 +1,23 @@ +/* + * 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. + * + */ + +// We need to share firebase imports between modules, otherwise +// apps and instances of the firebase modules are not shared. +import 'firebase/app'; +export { getApp } from 'firebase/app'; +export * from 'firebase/remote-config'; +export { makeIDBAvailable } from './memidb'; diff --git a/packages/app/lib/internal/web/firebaseStorage.js b/packages/app/lib/internal/web/firebaseStorage.js deleted file mode 100644 index 143a8250cf..0000000000 --- a/packages/app/lib/internal/web/firebaseStorage.js +++ /dev/null @@ -1,4 +0,0 @@ -// 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/storage'; diff --git a/packages/app/lib/internal/web/firebaseStorage.ts b/packages/app/lib/internal/web/firebaseStorage.ts new file mode 100644 index 0000000000..f4026e2045 --- /dev/null +++ b/packages/app/lib/internal/web/firebaseStorage.ts @@ -0,0 +1,21 @@ +/* + * 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. + * + */ + +// 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/storage'; diff --git a/packages/app/lib/internal/web/memidb/index.d.ts b/packages/app/lib/internal/web/memidb/index.d.ts new file mode 100644 index 0000000000..a2b7142062 --- /dev/null +++ b/packages/app/lib/internal/web/memidb/index.d.ts @@ -0,0 +1,21 @@ +/* + * 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. + * + */ + +/** + * Makes IndexedDB available in environments that don't have it + */ +export function makeIDBAvailable(): void; diff --git a/packages/app/lib/internal/web/utils.js b/packages/app/lib/internal/web/utils.js deleted file mode 100644 index 95686ec43a..0000000000 --- a/packages/app/lib/internal/web/utils.js +++ /dev/null @@ -1,35 +0,0 @@ -import { DeviceEventEmitter } from 'react-native'; - -// A general purpose guard function to catch errors and return a structured error object. -export function guard(fn) { - return fn().catch(e => Promise.reject(getWebError(e))); -} - -// Converts a thrown error to a structured error object -// required by RNFirebase native module internals. -export function getWebError(error) { - const obj = { - code: error.code || 'unknown', - message: error.message, - }; - - // Some modules send codes as PERMISSION_DENIED, which is not - // the same as the Firebase error code format. - obj.code = obj.code.toLowerCase(); - // Replace _ with - in code - obj.code = obj.code.replace(/_/g, '-'); - - // Split out prefix, since we internally prefix all error codes already. - if (obj.code.includes('/')) { - obj.code = obj.code.split('/')[1]; - } - - return { - ...obj, - userInfo: obj, - }; -} - -export function emitEvent(eventName, event) { - setImmediate(() => DeviceEventEmitter.emit('rnfb_' + eventName, event)); -} diff --git a/packages/app/lib/internal/web/utils.ts b/packages/app/lib/internal/web/utils.ts new file mode 100644 index 0000000000..feff92f503 --- /dev/null +++ b/packages/app/lib/internal/web/utils.ts @@ -0,0 +1,57 @@ +/* + * 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. + * + */ + +import { DeviceEventEmitter } from 'react-native'; +import { type ReactNativeFirebase } from '../../index'; + +// A general purpose guard function to catch errors and return a structured error object. +export function guard(fn: () => Promise): Promise { + return fn().catch(e => Promise.reject(getWebError(e))); +} + +// Converts a thrown error to a structured error object +// required by RNFirebase native module internals. +export function getWebError( + error: Error & { code?: string }, +): ReactNativeFirebase.NativeFirebaseError { + let code = error.code || 'unknown'; + + // Some modules send codes as PERMISSION_DENIED, which is not + // the same as the Firebase error code format. + code = code.toLowerCase(); + // Replace _ with - in code + code = code.replace(/_/g, '-'); + + // Split out prefix, since we internally prefix all error codes already. + if (code.includes('/')) { + const parts = code.split('/'); + code = parts[1] || code; + } + + return { + code, + message: error.message, + namespace: 'app', + nativeErrorCode: code, + nativeErrorMessage: error.message, + name: error.name, + } as ReactNativeFirebase.NativeFirebaseError; +} + +export function emitEvent(eventName: string, event: unknown): void { + setImmediate(() => DeviceEventEmitter.emit('rnfb_' + eventName, event)); +} diff --git a/packages/app/lib/modular.ts b/packages/app/lib/modular.ts new file mode 100644 index 0000000000..f1d101f4f5 --- /dev/null +++ b/packages/app/lib/modular.ts @@ -0,0 +1,200 @@ +/* + * 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. + * + */ + +import { MODULAR_DEPRECATION_ARG } from './common'; +import type { ReactNativeFirebase, LogCallback, LogOptions } from './types/app'; +import { + deleteApp as deleteAppCompat, + getApp as getAppCompat, + getApps as getAppsCompat, + initializeApp as initializeAppCompat, + setLogLevel as setLogLevelCompat, + setReactNativeAsyncStorage as setReactNativeAsyncStorageCompat, +} from './internal/registry/app'; +import { setUserLogHandler } from './internal/logger'; +import { version as sdkVersion } from './version'; +import { NativeModules } from 'react-native'; + +/** + * Renders this app unusable and frees the resources of all associated services. + * @param app - The app to delete. + * @returns Promise + */ +export function deleteApp(app: ReactNativeFirebase.FirebaseApp): Promise { + return deleteAppCompat.call( + null, + app.name, + (app as any)._nativeInitialized, + // @ts-expect-error - Extra arg used by deprecation proxy to detect modular calls + MODULAR_DEPRECATION_ARG, + ); +} + +/** + * Registers a library's name and version for platform logging purposes. + * @param _libraryKeyOrName - library name or key. + * @param _version - library version. + * @param _variant - library variant. Optional. + * @returns Promise + */ +export function registerVersion( + _libraryKeyOrName: string, + _version: string, + _variant?: string, +): Promise { + throw new Error('registerVersion is only supported on Web'); +} + +/** + * Sets log handler for VertexAI only currently. + * @param logCallback - The callback function to handle logs. + * @param options - Optional settings for log handling. + * @returns void + */ +export function onLog(logCallback: LogCallback | null, options?: LogOptions): void { + setUserLogHandler(logCallback, options); +} + +/** + * Gets the list of all initialized apps. + * @returns An array of all initialized Firebase apps. + */ +export function getApps(): ReactNativeFirebase.FirebaseApp[] { + return getAppsCompat.call( + null, + // @ts-expect-error - Extra arg used by deprecation proxy to detect modular calls + MODULAR_DEPRECATION_ARG, + ); +} + +/** + * Initializes a Firebase app with the provided options and name. + * @param options - Options to configure the services used in the app. + * @param configOrName - The optional name of the app, or config for the app to initialize (a name of '[DEFAULT]' will be used if omitted). + * @returns The initialized Firebase app. + */ +export function initializeApp( + options: ReactNativeFirebase.FirebaseAppOptions, + configOrName?: string | ReactNativeFirebase.FirebaseAppConfig, +): Promise { + return initializeAppCompat.call( + null, + options, + configOrName, + // @ts-expect-error - Extra arg used by deprecation proxy to detect modular calls + MODULAR_DEPRECATION_ARG, + ); +} + +/** + * Retrieves an instance of a Firebase app. + * @param name - The optional name of the app to return ('[DEFAULT]' if omitted). + * @returns The requested Firebase app instance. + */ +export function getApp(name?: string): ReactNativeFirebase.FirebaseApp { + return getAppCompat.call( + null, + name, + // @ts-expect-error - Extra arg used by deprecation proxy to detect modular calls + MODULAR_DEPRECATION_ARG, + ); +} + +/** + * Sets the log level across all Firebase SDKs. + * @param logLevel - The log level to set ('debug', 'verbose', 'info', 'warn', 'error', 'silent'). + * @returns void + */ +export function setLogLevel(logLevel: ReactNativeFirebase.LogLevelString): void { + return setLogLevelCompat.call( + null, + logLevel, + // @ts-expect-error - Extra arg used by deprecation proxy to detect modular calls + MODULAR_DEPRECATION_ARG, + ); +} + +/** + * The `AsyncStorage` implementation to use for persisting data on 'Other' platforms. + * If not specified, in memory persistence is used. + * + * This is required if you want to persist things like Auth sessions, Analytics device IDs, etc. + */ +export function setReactNativeAsyncStorage( + asyncStorage: ReactNativeFirebase.ReactNativeAsyncStorage, +): void { + return setReactNativeAsyncStorageCompat.call( + null, + asyncStorage, + // @ts-expect-error - Extra arg used by deprecation proxy to detect modular calls + MODULAR_DEPRECATION_ARG, + ); +} + +/** + * Gets react-native-firebase specific "meta" data from native Info.plist / AndroidManifest.xml + * @returns map of key / value pairs containing native meta data + */ +export function metaGetAll(): Promise<{ [key: string]: string | boolean }> { + return NativeModules.RNFBAppModule.metaGetAll(); +} + +/** + * Gets react-native-firebase specific "firebase.json" data + * @returns map of key / value pairs containing native firebase.json constants + */ +export function jsonGetAll(): Promise<{ [key: string]: string | boolean }> { + return NativeModules.RNFBAppModule.jsonGetAll(); +} + +/** + * Clears react-native-firebase specific native preferences + * @returns Promise + */ +export function preferencesClearAll(): Promise { + return NativeModules.RNFBAppModule.preferencesClearAll(); +} + +/** + * Gets react-native-firebase specific native preferences + * @returns map of key / value pairs containing native preferences data + */ +export function preferencesGetAll(): Promise<{ [key: string]: string | boolean }> { + return NativeModules.RNFBAppModule.preferencesGetAll(); +} + +/** + * Sets react-native-firebase specific native boolean preference + * @param key the name of the native preference to set + * @param value the value of the native preference to set + * @returns Promise + */ +export function preferencesSetBool(key: string, value: boolean): Promise { + return NativeModules.RNFBAppModule.preferencesSetBool(key, value); +} + +/** + * Sets react-native-firebase specific native string preference + * @param key the name of the native preference to set + * @param value the value of the native preference to set + * @returns Promise + */ +export function preferencesSetString(key: string, value: string): Promise { + return NativeModules.RNFBAppModule.preferencesSetString(key, value); +} + +export const SDK_VERSION = sdkVersion; diff --git a/packages/app/lib/modular/index.d.ts b/packages/app/lib/modular/index.d.ts deleted file mode 100644 index 51f6de4c54..0000000000 --- a/packages/app/lib/modular/index.d.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { ReactNativeFirebase } from '..'; - -type FirebaseApp = ReactNativeFirebase.FirebaseApp & { - functions(regionOrCustomDomain?: string): Functions; -}; -import FirebaseAppOptions = ReactNativeFirebase.FirebaseAppOptions; -import LogLevelString = ReactNativeFirebase.LogLevelString; -import FirebaseAppConfig = ReactNativeFirebase.FirebaseAppConfig; - -/** - * Renders this app unusable and frees the resources of all associated services. - * @param app - FirebaseApp - The app to delete. - * @returns Promise - */ -export function deleteApp(app: FirebaseApp): Promise; - -/** - * Registers a library's name and version for platform logging purposes. - * @param libraryKeyOrName - Library name or key. - * @param version - Library version. - * @param variant - Library variant. Optional. - * @returns Promise - */ -export function registerVersion( - libraryKeyOrName: string, - version: string, - variant?: string, -): Promise; - -/** - * Sets log handler for all Firebase SDKs. Currently only supported on VertexAI. - * @param logCallback - The callback function to handle logs. - * @param options - Optional settings for log handling. - * @returns - */ - -interface LogCallbackParams { - level: LogLevelString; - message: string; - args: unknown[]; - type: string; -} - -export function onLog( - logCallback: (callbackParams: LogCallbackParams) => void, - options?: any, -): void; - -/** - * Gets the list of all initialized apps. - * @returns FirebaseApp[] - An array of all initialized Firebase apps. - */ -export function getApps(): FirebaseApp[]; - -/** - * Initializes a Firebase app with the provided options and name. - * @param options - Options to configure the services used in the app. - * @param name - The optional name of the app to initialize ('[DEFAULT]' if omitted). - * @returns Promise - The initialized Firebase app. - */ -export function initializeApp(options: FirebaseAppOptions, name?: string): Promise; - -/** - * Initializes a Firebase app with the provided options and config. - * @param options - Options to configure the services used in the app. - * @param config - The optional config for your firebase app. - * @returns Promise - The initialized Firebase app. - */ -export function initializeApp( - options: FirebaseAppOptions, - config?: FirebaseAppConfig, -): Promise; -/** - * Retrieves an instance of a Firebase app. - * @param name - The optional name of the app to return ('[DEFAULT]' if omitted). - * @returns FirebaseApp - The requested Firebase app instance. - */ -export function getApp(name?: string): FirebaseApp; - -/** - * Sets the log level across all Firebase SDKs. - * @param logLevel - The log level to set ('debug', 'verbose', 'info', 'warn', 'error', 'silent'). - * @returns void - */ -export function setLogLevel(logLevel: LogLevelString): void; - -/** - * Gets react-native-firebase specific "meta" data from native Info.plist / AndroidManifest.xml - * @returns map of key / value pairs containing native meta data - */ -export function metaGetAll(): Promise<{ [keyof: string]: string | boolean }>; - -/** - * Gets react-native-firebase specific "firebase.json" data - * @returns map of key / value pairs containing native firebase.json constants - */ -export function jsonGetAll(): Promise<{ [keyof: string]: string | boolean }>; - -/** - * Clears react-native-firebase specific native preferences - * @returns Promise - */ -export function preferencesClearAll(): Promise; - -/** - * Gets react-native-firebase specific native preferences - * @returns map of key / value pairs containing native preferences data - */ -export function preferencesGetAll(): Promise<{ [keyof: string]: string | boolean }>; - -/** - * Sets react-native-firebase specific native boolean preference - * @param key the name of the native preference to set - * @param value the value of the native preference to set - * @returns Promise - */ -export function preferencesSetBool(key: string, value: boolean): Promise; - -/** - * Sets react-native-firebase specific native string preference - * @param key the name of the native preference to set - * @param value the value of the native preference to set - * @returns Promise - */ -export function preferencesSetString(key: string, value: string): Promise; - -/** - * The `AsyncStorage` implementation to use for persisting data on 'Other' platforms. - * If not specified, in memory persistence is used. - * - * This is required if you want to persist things like Auth sessions, Analytics device IDs, etc. - */ -export function setReactNativeAsyncStorage(asyncStorage: ReactNativeAsyncStorage): void; diff --git a/packages/app/lib/modular/index.js b/packages/app/lib/modular/index.js deleted file mode 100644 index bc485ca7f3..0000000000 --- a/packages/app/lib/modular/index.js +++ /dev/null @@ -1,150 +0,0 @@ -import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/lib/common'; -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { - deleteApp as deleteAppCompat, - getApp as getAppCompat, - getApps as getAppsCompat, - initializeApp as initializeAppCompat, - setLogLevel as setLogLevelCompat, - setReactNativeAsyncStorage as setReactNativeAsyncStorageCompat, -} from '../internal'; -import { setUserLogHandler } from '../internal/logger'; -import sdkVersion from '../version'; - -/** - * @typedef {import('..').ReactNativeFirebase.FirebaseApp} FirebaseApp - * @typedef {import('..').ReactNativeFirebase.FirebaseAppOptions} FirebaseAppOptions - * @typedef {import('..').ReactNativeFirebase.LogLevelString} LogLevelString - * @typedef {import('../internal/logger').LogCallback} LogCallback - * @typedef {import('../internal/logger').LogOptions} LogOptions - */ - -/** - * Renders this app unusable and frees the resources of all associated services. - * @param {FirebaseApp} app - The app to delete. - * @returns {Promise} - */ -export function deleteApp(app) { - return deleteAppCompat.call(null, app.name, app._nativeInitialized, MODULAR_DEPRECATION_ARG); -} - -/** - * Registers a library's name and version for platform logging purposes. - @param {string} libraryKeyOrName - library name or key. - @param {string} version - library version. - @param {string | undefined} variant - library variant. Optional. - * @returns {Promise} - */ -export function registerVersion(libraryKeyOrName, version, variant) { - throw new Error('registerVersion is only supported on Web'); -} - -/** - * Sets log handler for VertexAI only currently. - * @param {LogCallback | null} logCallback - The callback function to handle logs. - * @param {LogOptions} [options] - Optional settings for log handling. - * @returns {void} - */ -export function onLog(logCallback, options) { - setUserLogHandler(logCallback, options); -} - -/** - * Gets the list of all initialized apps. - * @returns {FirebaseApp[]} - An array of all initialized Firebase apps. - */ -export function getApps() { - return getAppsCompat.call(null, MODULAR_DEPRECATION_ARG); -} - -/** - * Initializes a Firebase app with the provided options and name. - * @param {FirebaseAppOptions} options - Options to configure the services used in the app. - * @param {string | FirebaseAppConfig} [configOrName] - The optional name of the app, or config for the app to initialize (a name of '[DEFAULT]' will be used if omitted). - * @returns {FirebaseApp} - The initialized Firebase app. - */ -export function initializeApp(options, configOrName) { - return initializeAppCompat.call(null, options, configOrName, MODULAR_DEPRECATION_ARG); -} - -/** - * Retrieves an instance of a Firebase app. - * @param {string} [name] - The optional name of the app to return ('[DEFAULT]' if omitted). - * @returns {FirebaseApp} - The requested Firebase app instance. - */ -export function getApp(name) { - return getAppCompat.call(null, name, MODULAR_DEPRECATION_ARG); -} - -/** - * Sets the log level across all Firebase SDKs. - * @param {LogLevelString} logLevel - The log level to set ('debug', 'verbose', 'info', 'warn', 'error', 'silent'). - * @returns {void} - */ -export function setLogLevel(logLevel) { - return setLogLevelCompat.call(null, logLevel, MODULAR_DEPRECATION_ARG); -} - -/** - * The `AsyncStorage` implementation to use for persisting data on 'Other' platforms. - * If not specified, in memory persistence is used. - * - * This is required if you want to persist things like Auth sessions, Analytics device IDs, etc. - */ -export function setReactNativeAsyncStorage(asyncStorage) { - return setReactNativeAsyncStorageCompat.call(null, asyncStorage, MODULAR_DEPRECATION_ARG); -} - -/** - * Gets react-native-firebase specific "meta" data from native Info.plist / AndroidManifest.xml - * @returns map of key / value pairs containing native meta data - */ -export function metaGetAll() { - return NativeModules.RNFBAppModule.metaGetAll(); -} - -/** - * Gets react-native-firebase specific "firebase.json" data - * @returns map of key / value pairs containing native firebase.json constants - */ -export function jsonGetAll() { - return NativeModules.RNFBAppModule.jsonGetAll(); -} - -/** - * Clears react-native-firebase specific native preferences - * @returns Promise - */ -export function preferencesClearAll() { - return NativeModules.RNFBAppModule.preferencesClearAll(); -} - -/** - * Gets react-native-firebase specific native preferences - * @returns map of key / value pairs containing native preferences data - */ -export function preferencesGetAll() { - return NativeModules.RNFBAppModule.preferencesGetAll(); -} - -/** - * Sets react-native-firebase specific native boolean preference - * @param key the name of the native preference to set - * @param value the value of the native preference to set - * @returns Promise - */ -export function preferencesSetBool(key, value) { - return NativeModules.RNFBAppModule.preferencesSetBool(key, value); -} - -/** - * Sets react-native-firebase specific native string preference - * @param key the name of the native preference to set - * @param value the value of the native preference to set - * @returns Promise - */ -export function preferencesSetString(key, value) { - return NativeModules.RNFBAppModule.preferencesSetString(key, value); -} - -export const SDK_VERSION = sdkVersion; diff --git a/packages/app/lib/index.js b/packages/app/lib/namespaced.ts similarity index 91% rename from packages/app/lib/index.js rename to packages/app/lib/namespaced.ts index 17d3e2e495..6840013a3c 100644 --- a/packages/app/lib/index.js +++ b/packages/app/lib/namespaced.ts @@ -16,9 +16,9 @@ */ import { getFirebaseRoot } from './internal/registry/namespace'; +import utils from './utils'; export const firebase = getFirebaseRoot(); -export * from './modular'; -export { default as utils } from './utils'; +export { utils }; export default firebase; diff --git a/packages/app/lib/index.d.ts b/packages/app/lib/types/app.ts similarity index 83% rename from packages/app/lib/index.d.ts rename to packages/app/lib/types/app.ts index a51dce1ad2..1c6d5ad145 100644 --- a/packages/app/lib/index.d.ts +++ b/packages/app/lib/types/app.ts @@ -16,20 +16,11 @@ */ /** - * Core React Native Firebase package. - * - * #### Example 1 - * - * Access the default firebase app from the `app` package: - * - * ```js - * import firebase from '@react-native-firebase/app'; - * - * console.log(firebase.app().name); - * ``` + * Core React Native Firebase package types. * * @firebase app */ +// eslint-disable-next-line @typescript-eslint/no-namespace export namespace ReactNativeFirebase { export interface NativeFirebaseError extends Error { /** @@ -138,7 +129,12 @@ export namespace ReactNativeFirebase { automaticResourceManagement?: boolean; } - export interface FirebaseApp { + /** + * Base interface for FirebaseApp containing core properties and methods. + * The concrete FirebaseApp class implements this interface. + * Module-specific methods (auth(), analytics(), etc.) are added to FirebaseApp via declaration merging. + */ + export interface FirebaseAppBase { /** * The name (identifier) for this App. '[DEFAULT]' is the default App. */ @@ -162,6 +158,19 @@ export namespace ReactNativeFirebase { utils(): Utils.Module; } + /** + * Full FirebaseApp interface that extends the base interface. + * Module-specific methods (auth(), analytics(), etc.) are added here via declaration merging + * from individual package .d.ts files. + */ + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + export interface FirebaseApp extends FirebaseAppBase { + // Module methods are added here via declaration merging, e.g.: + // auth(): FirebaseAuthTypes.Module; + // analytics(): FirebaseAnalyticsTypes.Module; + // etc. + } + /** * Interface for a supplied `AsyncStorage`. */ @@ -258,27 +267,17 @@ export namespace ReactNativeFirebase { * and related services inside React Native, e.g. Test Lab helpers * and Google Play Services version helpers. */ - utils: typeof utils; + utils: Utils.Module; } /** * A class that all React Native Firebase modules extend from to provide default behaviour. */ - export class FirebaseModule { + export abstract class FirebaseModule { /** * The current `FirebaseApp` instance for this Firebase service. */ - app: FirebaseApp; - - /** - * The native module instance for this Firebase service. - */ - private native: any; - - /** - * Returns the shared event emitter instance used for all JS event routing. - */ - private emitter: any; + abstract app: FirebaseApp; } // eslint-disable-next-line @typescript-eslint/no-empty-object-type @@ -300,11 +299,46 @@ export namespace ReactNativeFirebase { */ readonly SDK_VERSION: string; } & S; + + /** + * Type for the `firebase` named export from module packages. + * Provides complete typing for: + * - Root level access: `firebase.functions(app?)` + * - App level access: `firebase.app().functions(region?)` + * - Statics: `firebase.functions.HttpsErrorCode` + * - Root properties: `firebase.SDK_VERSION`, `firebase.app()`, etc. + * + * @typeParam Namespace - The module namespace (e.g., 'functions', 'auth', 'firestore') + * @typeParam M - The module instance type (must extend FirebaseModule with `app` property) + * @typeParam S - The module statics type + * @typeParam HasCustomArg - true if app-level accessor takes optional string (region/url/databaseId) + * + * @example + * // In functions package: + * export const firebase = getFirebaseRoot() as ReactNativeFirebase.FirebaseNamespacedExport< + * 'functions', + * FunctionsModule, + * FunctionsStatics, + * true // functions() takes regionOrCustomDomain + * >; + */ + export type FirebaseNamespacedExport< + Namespace extends string, + M extends FirebaseModule, + S extends object = object, + HasCustomArg extends boolean = false, + > = Module & + Record> & { + app( + name?: string, + ): FirebaseApp & Record M : () => M>; + }; } -/* +/** * @firebase utils */ +// eslint-disable-next-line @typescript-eslint/no-namespace export namespace Utils { import FirebaseModule = ReactNativeFirebase.FirebaseModule; @@ -431,6 +465,7 @@ export namespace Utils { } export interface Statics { + SDK_VERSION: string; FilePath: FilePath; } @@ -533,7 +568,7 @@ export namespace Utils { * const defaultAppUtils = firebase.utils(); * ``` */ - export class Module extends FirebaseModule { + export abstract class Module extends FirebaseModule { /** * Returns true if this app is running inside a Firebase Test Lab environment. * @@ -544,7 +579,7 @@ export namespace Utils { * ``` * @android Android only - iOS returns false */ - isRunningInTestLab: boolean; + abstract isRunningInTestLab: boolean; /** * Returns PlayServicesAvailability properties * @@ -556,7 +591,7 @@ export namespace Utils { * * @android Android only - iOS always returns { isAvailable: true, status: 0 } */ - playServicesAvailability: PlayServicesAvailability; + abstract playServicesAvailability: PlayServicesAvailability; /** * Returns PlayServicesAvailability properties @@ -569,7 +604,7 @@ export namespace Utils { * * @android Android only - iOS always returns { isAvailable: true, status: 0 } */ - getPlayServicesStatus(): Promise; + abstract getPlayServicesStatus(): Promise; /** * A prompt appears on the device to ask the user to update play services @@ -582,7 +617,7 @@ export namespace Utils { * * @android Android only - iOS returns undefined */ - promptForPlayServices(): Promise; + abstract promptForPlayServices(): Promise; /** * Attempts to make Google Play services available on this device * @@ -594,7 +629,7 @@ export namespace Utils { * * @android Android only - iOS returns undefined */ - makePlayServicesAvailable(): Promise; + abstract makePlayServicesAvailable(): Promise; /** * Resolves an error by starting any intents requiring user interaction. * @@ -606,16 +641,22 @@ export namespace Utils { * * @android Android only - iOS returns undefined */ - resolutionForPlayServices(): Promise; + abstract resolutionForPlayServices(): Promise; } } -/** - * Add Utils module as a named export for `app`. - */ -export const utils: ReactNativeFirebase.FirebaseModuleWithStatics; +export interface LogCallbackParams { + level: ReactNativeFirebase.LogLevelString; + message: string; + args: unknown[]; + type: string; +} + +export type LogCallback = (callbackParams: LogCallbackParams) => void; -export * from './modular'; +export interface LogOptions { + level?: ReactNativeFirebase.LogLevelString; +} -declare const module: ReactNativeFirebase.Module; -export default module; +// Re-export FirebaseApp as a named type for easier importing +export type FirebaseApp = ReactNativeFirebase.FirebaseApp; diff --git a/packages/app/lib/types/internal.ts b/packages/app/lib/types/internal.ts new file mode 100644 index 0000000000..c1999aadcc --- /dev/null +++ b/packages/app/lib/types/internal.ts @@ -0,0 +1,142 @@ +/* + * 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. + * + */ + +import type { ReactNativeFirebase, Utils } from './app'; + +/** + * Internal types for React Native Firebase + * These types are used internally across multiple files and should not be exported to consumers + */ + +/** + * Firebase JSON configuration from firebase.json file + * Structure: { "react-native": { [key: string]: boolean | string }, ... } + */ +export type FirebaseJsonConfig = Record; + +/** + * Configuration for module namespace registration + */ +export interface ModuleConfig { + namespace: string; + nativeModuleName?: string | string[]; + hasMultiAppSupport?: boolean; + hasCustomUrlOrRegionSupport?: boolean; + nativeEvents?: boolean | string[]; + disablePrependCustomUrlOrRegion?: boolean; + turboModule?: boolean; +} + +/** + * Extended configuration for namespace registration including native module details + */ +export interface NamespaceConfig extends ModuleConfig { + nativeModuleName: string | string[]; + nativeEvents: boolean | string[]; + // ModuleClass can be FirebaseModule or any subclass of it + // Uses FirebaseAppBase (the concrete class type) rather than FirebaseApp (the augmented interface) + ModuleClass: new ( + app: ReactNativeFirebase.FirebaseAppBase, + config: ModuleConfig, + customUrlOrRegion?: string | null, + ) => ReactNativeFirebase.FirebaseModule; + statics?: object; + version?: string; +} + +/** + * Type for a Firebase module getter function that can optionally accept + * a custom URL/region/databaseId parameter + */ +export type ModuleGetter = { + (customUrlOrRegionOrDatabaseId?: string): ReactNativeFirebase.FirebaseModule; + [key: string]: unknown; +}; + +/** + * Type for Firebase root object with module getters + */ +export interface FirebaseRoot { + initializeApp: ( + options: ReactNativeFirebase.FirebaseAppOptions, + configOrName?: string | ReactNativeFirebase.FirebaseAppConfig, + ) => Promise; + setReactNativeAsyncStorage: (asyncStorage: ReactNativeFirebase.ReactNativeAsyncStorage) => void; + app: (name?: string) => ReactNativeFirebase.FirebaseApp; + apps: ReactNativeFirebase.FirebaseApp[]; + SDK_VERSION: string; + setLogLevel: (logLevel: ReactNativeFirebase.LogLevelString) => void; + utils: Utils.Statics & (() => Utils.Module); + [key: string]: unknown; +} + +/** + * Native error types + */ +export interface NativeErrorUserInfo { + code?: string; + message?: string; + nativeErrorCode?: string | number; + nativeErrorMessage?: string; + [key: string]: any; +} + +export interface NativeError { + userInfo: NativeErrorUserInfo; + message?: string; + customData?: any; + operationType?: string; +} + +/** + * AsyncStorage interface compatible with React Native AsyncStorage + * Internal version used by the library + */ +export interface AsyncStorageStatic { + setItem: (key: string, value: string) => Promise; + getItem: (key: string) => Promise; + removeItem: (key: string) => Promise; +} + +/** + * Common utility types + */ +export interface DataUrlParts { + base64String: string | undefined; + mediaType: string | undefined; +} + +export interface Observer { + next: (value: T) => void; + error?: (error: Error) => void; + complete?: () => void; +} + +export interface SerializedValue { + type: string; + value: any; +} + +export interface Deferred { + promise: Promise; + resolve: ((value: T) => void) | null; + reject: ((reason?: any) => void) | null; +} + +export type Callback = + | ((error: Error | null, result?: T) => void) + | ((error: Error | null) => void); diff --git a/packages/app/lib/utils/UtilsStatics.js b/packages/app/lib/utils/UtilsStatics.ts similarity index 67% rename from packages/app/lib/utils/UtilsStatics.js rename to packages/app/lib/utils/UtilsStatics.ts index addbd7f394..69c9860aa6 100644 --- a/packages/app/lib/utils/UtilsStatics.js +++ b/packages/app/lib/utils/UtilsStatics.ts @@ -16,7 +16,9 @@ */ import { NativeModules } from 'react-native'; -import { stripTrailingSlash, isOther } from '../../lib/common'; +import { stripTrailingSlash, isOther } from '../common'; +import { Utils } from '../types/app'; +import { version } from '../version'; const PATH_NAMES = [ 'MAIN_BUNDLE', @@ -28,38 +30,44 @@ const PATH_NAMES = [ 'LIBRARY_DIRECTORY', 'PICTURES_DIRECTORY', 'MOVIES_DIRECTORY', -]; +] as const; -const PATH_FILE_TYPES = ['FILE_TYPE_REGULAR', 'FILE_TYPE_DIRECTORY']; +const PATH_FILE_TYPES = ['FILE_TYPE_REGULAR', 'FILE_TYPE_DIRECTORY'] as const; -const paths = {}; +const paths: Partial = {}; let processedPathConstants = false; -function processPathConstants(nativeModule) { +function processPathConstants(nativeModule: any): Utils.FilePath { if (processedPathConstants || !nativeModule) { - return paths; + return paths as Utils.FilePath; } processedPathConstants = true; for (let i = 0; i < PATH_NAMES.length; i++) { const path = PATH_NAMES[i]; - paths[path] = nativeModule[path] ? stripTrailingSlash(nativeModule[path]) : null; + if (path) { + (paths as any)[path] = nativeModule[path] ? stripTrailingSlash(nativeModule[path]) : null; + } } for (let i = 0; i < PATH_FILE_TYPES.length; i++) { const pathFileType = PATH_FILE_TYPES[i]; - paths[pathFileType] = stripTrailingSlash(nativeModule[pathFileType]); + if (pathFileType) { + (paths as any)[pathFileType] = stripTrailingSlash(nativeModule[pathFileType]); + } } Object.freeze(paths); - return paths; + return paths as Utils.FilePath; } -export default { - SDK_VERSION: require('./../version'), - get FilePath() { +const statics: Utils.Statics = { + SDK_VERSION: version, + get FilePath(): Utils.FilePath { // We don't support path constants on non-native platforms. return processPathConstants(isOther ? {} : NativeModules.RNFBUtilsModule); }, }; + +export default statics; diff --git a/packages/app/lib/utils/index.js b/packages/app/lib/utils/index.ts similarity index 70% rename from packages/app/lib/utils/index.js rename to packages/app/lib/utils/index.ts index 9f7befa923..757f0e290f 100644 --- a/packages/app/lib/utils/index.js +++ b/packages/app/lib/utils/index.ts @@ -15,66 +15,69 @@ * */ -import { isIOS } from '../../lib/common'; -import { createModuleNamespace, FirebaseModule } from '../../lib/internal'; +import { isIOS } from '../common'; +import { createModuleNamespace, FirebaseModule } from '../internal'; import UtilsStatics from './UtilsStatics'; +import { Utils } from '../types/app'; const namespace = 'utils'; const statics = UtilsStatics; const nativeModuleName = 'RNFBUtilsModule'; -class FirebaseUtilsModule extends FirebaseModule { - get isRunningInTestLab() { +class FirebaseUtilsModule extends FirebaseModule<'RNFBUtilsModule'> { + get isRunningInTestLab(): boolean { if (isIOS) { return false; } return this.native.isRunningInTestLab; } - get playServicesAvailability() { + get playServicesAvailability(): Utils.PlayServicesAvailability { if (isIOS) { return { isAvailable: true, status: 0, + hasResolution: false, + isUserResolvableError: false, + error: undefined, }; } return this.native.androidPlayServices; } - getPlayServicesStatus() { + getPlayServicesStatus(): Promise { if (isIOS) { return Promise.resolve({ isAvailable: true, status: 0, + hasResolution: false, + isUserResolvableError: false, + error: undefined, }); } return this.native.androidGetPlayServicesStatus(); } - promptForPlayServices() { + promptForPlayServices(): Promise { if (isIOS) { return Promise.resolve(); } return this.native.androidPromptForPlayServices(); } - makePlayServicesAvailable() { + makePlayServicesAvailable(): Promise { if (isIOS) { return Promise.resolve(); } return this.native.androidMakePlayServicesAvailable(); } - resolutionForPlayServices() { + resolutionForPlayServices(): Promise { if (isIOS) { return Promise.resolve(); } return this.native.androidResolutionForPlayServices(); } - - logInfo(...args) { - return logger.logInfo(...args); - } } // import { utils } from '@react-native-firebase/app'; @@ -88,4 +91,4 @@ export default createModuleNamespace({ hasMultiAppSupport: false, hasCustomUrlOrRegionSupport: false, ModuleClass: FirebaseUtilsModule, -}); +}) as unknown as Utils.Statics & (() => Utils.Module); diff --git a/packages/app/package.json b/packages/app/package.json index c78429be03..83141fd50a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -3,15 +3,17 @@ "version": "23.7.0", "author": "Invertase (http://invertase.io)", "description": "A well tested, feature rich Firebase implementation for React Native, supporting iOS & Android. Individual module support for Admob, Analytics, Auth, Crash Reporting, Cloud Firestore, Database, Dynamic Links, Functions, Messaging (FCM), Remote Config, Storage and more.", - "main": "lib/index.js", - "types": "lib/index.d.ts", + "main": "./dist/commonjs/index.js", + "module": "./dist/module/index.js", + "types": "./dist/typescript/commonjs/lib/index.d.ts", "scripts": { - "build": "genversion --semi lib/version.js && npm run build:version", + "build": "genversion --esm --semi lib/version.ts && npm run build:version", "build:version": "node ./scripts/genversion-ios && node ./scripts/genversion-android", "build:clean": "rimraf android/build && rimraf ios/build", "build:plugin": "rimraf plugin/build && tsc --build plugin", "lint:plugin": "eslint plugin/src/*", - "prepare": "npm run build && npm run build:plugin" + "compile": "bob build", + "prepare": "npm run build && npm run build:plugin && npm run compile" }, "repository": { "type": "git", @@ -51,6 +53,71 @@ "dynamic-links", "crashlytics" ], + "exports": { + ".": { + "source": "./lib/index.ts", + "import": { + "types": "./dist/typescript/module/lib/index.d.ts", + "default": "./dist/module/index.js" + }, + "require": { + "types": "./dist/typescript/commonjs/lib/index.d.ts", + "default": "./dist/commonjs/index.js" + } + }, + "./lib/internal": { + "source": "./lib/internal/index.ts", + "import": { + "types": "./dist/typescript/module/lib/internal/index.d.ts", + "default": "./dist/module/internal/index.js" + }, + "require": { + "types": "./dist/typescript/commonjs/lib/internal/index.d.ts", + "default": "./dist/commonjs/internal/index.js" + } + }, + "./lib/internal/*": { + "source": "./lib/internal/*.ts", + "import": { + "types": "./dist/typescript/module/lib/internal/*.d.ts", + "default": "./dist/module/internal/*" + }, + "require": { + "types": "./dist/typescript/commonjs/lib/internal/*.d.ts", + "default": "./dist/commonjs/internal/*" + } + }, + "./lib/common": { + "source": "./lib/common/index.ts", + "import": { + "types": "./dist/typescript/module/lib/common/index.d.ts", + "default": "./dist/module/common/index.js" + }, + "require": { + "types": "./dist/typescript/commonjs/lib/common/index.d.ts", + "default": "./dist/commonjs/common/index.js" + } + }, + "./lib/common/*": { + "source": "./lib/common/*.ts", + "import": { + "types": "./dist/typescript/module/lib/common/*.d.ts", + "default": "./dist/module/common/*" + }, + "require": { + "types": "./dist/typescript/commonjs/lib/common/*.d.ts", + "default": "./dist/commonjs/common/*" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "lib", + "dist", + "!**/__tests__", + "!**/__fixtures__", + "!**/__mocks__" + ], "peerDependencies": { "expo": ">=47.0.0", "react": "*", @@ -61,7 +128,8 @@ }, "devDependencies": { "@react-native-async-storage/async-storage": "^2.2.0", - "expo": "^54.0.27" + "expo": "^54.0.27", + "react-native-builder-bob": "^0.40.13" }, "peerDependenciesMeta": { "expo": { @@ -90,5 +158,34 @@ "playServicesAuth": "21.4.0", "firebaseAppDistributionGradle": "5.2.0" } - } + }, + "react-native-builder-bob": { + "source": "lib", + "output": "dist", + "exclude": "**/*.d.ts", + "targets": [ + [ + "module", + { + "esm": true + } + ], + [ + "commonjs", + { + "esm": true + } + ], + [ + "typescript", + { + "tsc": "../../node_modules/.bin/tsc" + } + ] + ] + }, + "eslintIgnore": [ + "node_modules/", + "dist/" + ] } diff --git a/packages/app/scripts/genversion-android.js b/packages/app/scripts/genversion-android.js index ccda19ed0a..12af85b744 100644 --- a/packages/app/scripts/genversion-android.js +++ b/packages/app/scripts/genversion-android.js @@ -1,7 +1,15 @@ const fs = require('fs'); const path = require('path'); -const version = require('../lib/version'); +// Read version from TypeScript file +const versionFilePath = path.resolve(__dirname, '..', 'lib', 'version.ts'); +const versionFileContent = fs.readFileSync(versionFilePath, 'utf8'); +const versionMatch = versionFileContent.match(/version = ['"](.+?)['"]/); +if (!versionMatch) { + throw new Error('Could not extract version from version.ts'); +} +const version = versionMatch[1]; + const outputPath = path.resolve( __dirname, '..', diff --git a/packages/app/scripts/genversion-ios.js b/packages/app/scripts/genversion-ios.js index 6292b7f264..049c6cb73e 100644 --- a/packages/app/scripts/genversion-ios.js +++ b/packages/app/scripts/genversion-ios.js @@ -1,7 +1,15 @@ const fs = require('fs'); const path = require('path'); -const version = require('../lib/version'); +// Read version from TypeScript file +const versionFilePath = path.resolve(__dirname, '..', 'lib', 'version.ts'); +const versionFileContent = fs.readFileSync(versionFilePath, 'utf8'); +const versionMatch = versionFileContent.match(/version = ['"](.+?)['"]/); +if (!versionMatch) { + throw new Error('Could not extract version from version.ts'); +} +const version = versionMatch[1]; + const outputPath = path.resolve(__dirname, '..', 'ios', 'RNFBApp', 'RNFBVersion.m'); const template = `/** * Copyright (c) 2016-present Invertase Limited & Contributors diff --git a/packages/app/tsconfig.json b/packages/app/tsconfig.json new file mode 100644 index 0000000000..33babab5a8 --- /dev/null +++ b/packages/app/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react-jsx", + "lib": ["ESNext", "DOM"], + "module": "ESNext", + "moduleResolution": "bundler", + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitUseStrict": false, + "noStrictGenericChecks": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ESNext", + "verbatimModuleSyntax": true, + "baseUrl": ".", + "rootDir": "." + }, + "include": ["lib/**/*"], + "exclude": ["node_modules", "dist", "__tests__", "**/*.test.ts"] +} diff --git a/packages/app/type-test.ts b/packages/app/type-test.ts index 618a15db0d..37b009fa85 100644 --- a/packages/app/type-test.ts +++ b/packages/app/type-test.ts @@ -19,6 +19,8 @@ import firebase, { utils } from '.'; // checks module exists at root console.log(firebase.utils().app.name); console.log(utils().app.name); +console.log(firebase.app().name); +console.log(firebase.app('foo').name); // checks module exists at app level console.log(firebase.app().utils().app.name); @@ -32,5 +34,18 @@ console.log(utils.FilePath.CACHES_DIRECTORY); console.log(firebase.utils.FilePath.CACHES_DIRECTORY); console.log(utils.FilePath.CACHES_DIRECTORY); +// initialize app variants +firebase.initializeApp({ apiKey: 'a', appId: 'b', projectId: 'c' }); +firebase.initializeApp({ apiKey: 'a', appId: 'b', projectId: 'c' }, 'foo'); + +// utils instance API +const u = firebase.utils(); +console.log(u.isRunningInTestLab); +console.log(u.playServicesAvailability); +u.getPlayServicesStatus(); +u.promptForPlayServices(); +u.makePlayServicesAvailable(); +u.resolutionForPlayServices(); + // checks root exists console.log(firebase.SDK_VERSION); diff --git a/packages/database/__tests__/database.test.ts b/packages/database/__tests__/database.test.ts index 96005442e8..4d61a55903 100644 --- a/packages/database/__tests__/database.test.ts +++ b/packages/database/__tests__/database.test.ts @@ -53,8 +53,6 @@ import { createCheckV9Deprecation, CheckV9DeprecationFunction, } from '../../app/lib/common/unitTestUtils'; - -// @ts-ignore test import FirebaseModule from '../../app/lib/internal/FirebaseModule'; describe('Database', function () { @@ -303,38 +301,56 @@ describe('Database', function () { staticsV9Deprecation = createCheckV9Deprecation(['database', 'statics']); referenceV9Deprecation = createCheckV9Deprecation(['database', 'DatabaseReference']); // @ts-ignore test - jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockImplementation(() => { - return new Proxy( - {}, - { - get: (_target, prop) => { - if (prop === 'constants') { - return { - isDatabaseCollectionEnabled: true, - url: 'https://test.firebaseio.com', - ref: 'ref()', - }; - } - // Mock the once method to return proper snapshot data - if (prop === 'once') { - return jest.fn().mockResolvedValue({ - key: 'test', - value: 'mock_value', - exists: true, - childKeys: [], - priority: null, - } as never); - } - return jest.fn().mockResolvedValue({ - constants: { - isDatabaseCollectionEnabled: true, - url: 'https://test.firebaseio.com', - }, - } as never); - }, + jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockReturnValue({ + constants: { + isDatabaseCollectionEnabled: true, + url: 'https://test.firebaseio.com', + ref: 'ref()', + }, + on: jest.fn(), + off: jest.fn(), + once: jest.fn( + (_appName: any, _customUrl: any, path: any, _modifiers: any, eventType: any) => { + // Database native methods receive (appName, customUrlOrRegion, ...actualArgs) + let key = 'test'; + if (path && typeof path === 'string') { + const parts = path.split('/').filter(p => p); + key = parts[parts.length - 1] || 'test'; + } + + const snapshotData = { + key, + value: 'mock_value', + exists: true, + childKeys: [], + priority: null, + }; + + if (eventType === 'value') { + return Promise.resolve(snapshotData); + } + + return Promise.resolve({ + snapshot: snapshotData, + previousChildName: null, + }); }, - ); - }); + ), + set: jest.fn(() => Promise.resolve()), + update: jest.fn(() => Promise.resolve()), + setWithPriority: jest.fn(() => Promise.resolve()), + remove: jest.fn(() => Promise.resolve()), + setPriority: jest.fn(() => Promise.resolve()), + keepSynced: jest.fn(() => Promise.resolve()), + transactionStart: jest.fn(() => Promise.resolve()), + transactionTryCommit: jest.fn(() => Promise.resolve()), + goOnline: jest.fn(() => Promise.resolve()), + goOffline: jest.fn(() => Promise.resolve()), + setPersistenceEnabled: jest.fn(() => Promise.resolve()), + setLoggingEnabled: jest.fn(() => Promise.resolve()), + setPersistenceCacheSizeBytes: jest.fn(() => Promise.resolve()), + getServerTime: jest.fn(() => Promise.resolve(Date.now())), + } as any); }); it('useEmulator', function () { diff --git a/packages/firestore/__tests__/firestore.test.ts b/packages/firestore/__tests__/firestore.test.ts index b408478d35..29e6bf2172 100644 --- a/packages/firestore/__tests__/firestore.test.ts +++ b/packages/firestore/__tests__/firestore.test.ts @@ -778,21 +778,27 @@ describe('Firestore', function () { timestampV9Deprecation = createCheckV9Deprecation(['firestore', 'FirestoreTimestamp']); - // @ts-ignore test - jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockImplementation(() => { - return new Proxy( - {}, - { - get: () => - jest.fn().mockResolvedValue({ - source: 'cache', - changes: [], - documents: [], - metadata: {}, - path: 'foo', - } as never), - }, - ); + // Mock the native module directly to avoid getter caching issues + const mockNative = new Proxy( + {}, + { + get: () => + (jest.fn() as any).mockResolvedValue({ + source: 'cache', + changes: [], + documents: [], + metadata: {}, + path: 'foo', + }), + }, + ); + + // Override the native getter on FirebaseModule prototype + Object.defineProperty(FirebaseModule.prototype, 'native', { + get: function () { + return mockNative; + }, + configurable: true, }); jest diff --git a/packages/functions/lib/HttpsError.ts b/packages/functions/lib/HttpsError.ts index 5f5cd392dd..1a4650021f 100644 --- a/packages/functions/lib/HttpsError.ts +++ b/packages/functions/lib/HttpsError.ts @@ -56,7 +56,7 @@ export class HttpsError extends Error { this.stack = NativeFirebaseError.getStackWithMessage( `Error: ${message}`, - nativeErrorInstance?.jsStack, + nativeErrorInstance?.jsStack as string, ); } } diff --git a/packages/functions/lib/index.ts b/packages/functions/lib/index.ts index b19d1bb60b..1195a5f45b 100644 --- a/packages/functions/lib/index.ts +++ b/packages/functions/lib/index.ts @@ -22,6 +22,7 @@ export type { FunctionsModule, Functions, FirebaseApp, + FirebaseFunctionsTypes, } from './types/functions'; // Export modular API functions diff --git a/packages/functions/lib/namespaced.ts b/packages/functions/lib/namespaced.ts index 857597108e..f4b7a26623 100644 --- a/packages/functions/lib/namespaced.ts +++ b/packages/functions/lib/namespaced.ts @@ -25,8 +25,8 @@ import { HttpsError, type NativeError } from './HttpsError'; import { version } from './version'; import { setReactNativeModule } from '@react-native-firebase/app/lib/internal/nativeModule'; import fallBackModule from './web/RNFBFunctionsModule'; -import type { HttpsCallableOptions } from './types/functions'; -import type { FirebaseApp } from '@react-native-firebase/app'; +import type { HttpsCallableOptions, FunctionsModule, FunctionsStatics } from './types/functions'; +import type { ReactNativeFirebase } from '@react-native-firebase/app'; const namespace = 'functions'; const nativeModuleName = 'NativeRNFBTurboFunctions'; @@ -75,88 +75,16 @@ const statics = { HttpsErrorCode, }; -// Export the complete FirebaseFunctionsTypes namespace -// eslint-disable-next-line @typescript-eslint/no-namespace -export namespace FirebaseFunctionsTypes { - export type FunctionsErrorCode = - | 'ok' - | 'cancelled' - | 'unknown' - | 'invalid-argument' - | 'deadline-exceeded' - | 'not-found' - | 'already-exists' - | 'permission-denied' - | 'resource-exhausted' - | 'failed-precondition' - | 'aborted' - | 'out-of-range' - | 'unimplemented' - | 'internal' - | 'unavailable' - | 'data-loss' - | 'unauthenticated'; - - export interface HttpsCallableResult { - readonly data: ResponseData; - } - - export interface HttpsCallable { - (data?: RequestData | null): Promise>; - } - - // Re-export HttpsCallableOptions from types/functions - export type HttpsCallableOptions = import('./types/functions').HttpsCallableOptions; - - export interface HttpsError extends Error { - readonly code: FunctionsErrorCode; - readonly details?: any; - } - - export interface HttpsErrorCode { - OK: 'ok'; - CANCELLED: 'cancelled'; - UNKNOWN: 'unknown'; - INVALID_ARGUMENT: 'invalid-argument'; - DEADLINE_EXCEEDED: 'deadline-exceeded'; - NOT_FOUND: 'not-found'; - ALREADY_EXISTS: 'already-exists'; - PERMISSION_DENIED: 'permission-denied'; - UNAUTHENTICATED: 'unauthenticated'; - RESOURCE_EXHAUSTED: 'resource-exhausted'; - FAILED_PRECONDITION: 'failed-precondition'; - ABORTED: 'aborted'; - OUT_OF_RANGE: 'out-of-range'; - UNIMPLEMENTED: 'unimplemented'; - INTERNAL: 'internal'; - UNAVAILABLE: 'unavailable'; - DATA_LOSS: 'data-loss'; - } - - export interface Statics { - HttpsErrorCode: HttpsErrorCode; - } - - export interface Module { - httpsCallable( - name: string, - options?: HttpsCallableOptions, - ): HttpsCallable; - httpsCallableFromUrl( - url: string, - options?: HttpsCallableOptions, - ): HttpsCallable; - useFunctionsEmulator(origin: string): void; - useEmulator(host: string, port: number): void; - } -} - class FirebaseFunctionsModule extends FirebaseModule { _customUrlOrRegion: string; private _useFunctionsEmulatorHost: string | null; private _useFunctionsEmulatorPort: number; - // TODO: config is app package (FirebaseModule) object to be typed in the future - constructor(app: FirebaseApp, config: any, customUrlOrRegion: string | null) { + + constructor( + app: ReactNativeFirebase.FirebaseAppBase, + config: any, + customUrlOrRegion?: string | null, + ) { super(app, config, customUrlOrRegion); this._customUrlOrRegion = customUrlOrRegion || 'us-central1'; this._useFunctionsEmulatorHost = null; @@ -273,9 +201,7 @@ class FirebaseFunctionsModule extends FirebaseModule { // import { SDK_VERSION } from '@react-native-firebase/functions'; export const SDK_VERSION = version; -// import functions from '@react-native-firebase/functions'; -// functions().logEvent(...); -export default createModuleNamespace({ +const functionsNamespace = createModuleNamespace({ statics, version, namespace, @@ -287,10 +213,29 @@ export default createModuleNamespace({ turboModule: true, }); +type FunctionsNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< + FunctionsModule, + FunctionsStatics +> & { + functions: ReactNativeFirebase.FirebaseModuleWithStaticsAndApp; + firebase: ReactNativeFirebase.Module; + app(name?: string): ReactNativeFirebase.FirebaseApp; +}; + +// import functions from '@react-native-firebase/functions'; +// functions().httpsCallable(...); +export default functionsNamespace as unknown as FunctionsNamespace; + // import functions, { firebase } from '@react-native-firebase/functions'; -// functions().logEvent(...); -// firebase.functions().logEvent(...); -export const firebase = getFirebaseRoot(); +// functions().httpsCallable(...); +// firebase.functions().httpsCallable(...); +export const firebase = + getFirebaseRoot() as unknown as ReactNativeFirebase.FirebaseNamespacedExport< + 'functions', + FunctionsModule, + FunctionsStatics, + true + >; // Register the interop module for non-native platforms. setReactNativeModule(nativeModuleName, fallBackModule); diff --git a/packages/functions/lib/types.d.ts b/packages/functions/lib/types.d.ts deleted file mode 100644 index 7e6520ecfc..0000000000 --- a/packages/functions/lib/types.d.ts +++ /dev/null @@ -1,58 +0,0 @@ -declare module '@react-native-firebase/app/lib/common' { - export const MODULAR_DEPRECATION_ARG: string; - export const isAndroid: boolean; - export const isNumber: (value: any) => value is number; -} - -declare module '@react-native-firebase/app/lib/internal' { - export function createModuleNamespace(config: any): any; - export class FirebaseModule { - constructor(...args: any[]); - native: any; - firebaseJson: any; - _customUrlOrRegion: string | null; - } - export function getFirebaseRoot(): any; - export class NativeFirebaseError { - static getStackWithMessage(message: string, jsStack?: string): string; - } -} - -declare module '@react-native-firebase/app/lib/internal/nativeModule' { - export function setReactNativeModule(moduleName: string, module: any): void; -} - -declare module '@react-native-firebase/app/lib' { - namespace ReactNativeFirebase { - import FirebaseModuleWithStaticsAndApp = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp; - interface Module { - functions: FirebaseModuleWithStaticsAndApp; - } - interface FirebaseApp { - functions(customUrlOrRegion?: string): any; - readonly name: string; - } - } -} - -declare module '@react-native-firebase/app/lib/internal/web/firebaseFunctions' { - export function getApp(appName: string): any; - export function getFunctions(app: any, regionOrCustomDomain?: string): any; - export function httpsCallable(functionsInstance: any, name: string, options?: any): any; - export function httpsCallableFromURL(functionsInstance: any, url: string, options?: any): any; - export function connectFunctionsEmulator( - functionsInstance: any, - host: string, - port: number, - ): void; -} - -declare module './version' { - const version: string; - export default version; -} - -declare module './web/RNFBFunctionsModule' { - const fallBackModule: any; - export default fallBackModule; -} diff --git a/packages/functions/lib/types/functions.ts b/packages/functions/lib/types/functions.ts index 16463d1b62..ca437d4ad4 100644 --- a/packages/functions/lib/types/functions.ts +++ b/packages/functions/lib/types/functions.ts @@ -17,28 +17,213 @@ import type { ReactNativeFirebase } from '@react-native-firebase/app'; +// ============ Options & Result Types ============ + export interface HttpsCallableOptions { timeout?: number; } +export interface HttpsCallableResult { + readonly data: ResponseData; +} + export interface HttpsCallable { - (data?: RequestData | null): Promise<{ data: ResponseData }>; + (data?: RequestData | null): Promise>; +} + +// ============ Error Code Types ============ + +export type FunctionsErrorCode = + | 'ok' + | 'cancelled' + | 'unknown' + | 'invalid-argument' + | 'deadline-exceeded' + | 'not-found' + | 'already-exists' + | 'permission-denied' + | 'resource-exhausted' + | 'failed-precondition' + | 'aborted' + | 'out-of-range' + | 'unimplemented' + | 'internal' + | 'unavailable' + | 'data-loss' + | 'unauthenticated' + | 'unsupported-type' + | 'failed-to-parse-wrapped-number'; + +export interface HttpsErrorCode { + OK: 'ok'; + CANCELLED: 'cancelled'; + UNKNOWN: 'unknown'; + INVALID_ARGUMENT: 'invalid-argument'; + DEADLINE_EXCEEDED: 'deadline-exceeded'; + NOT_FOUND: 'not-found'; + ALREADY_EXISTS: 'already-exists'; + PERMISSION_DENIED: 'permission-denied'; + UNAUTHENTICATED: 'unauthenticated'; + RESOURCE_EXHAUSTED: 'resource-exhausted'; + FAILED_PRECONDITION: 'failed-precondition'; + ABORTED: 'aborted'; + OUT_OF_RANGE: 'out-of-range'; + UNIMPLEMENTED: 'unimplemented'; + INTERNAL: 'internal'; + UNAVAILABLE: 'unavailable'; + DATA_LOSS: 'data-loss'; + UNSUPPORTED_TYPE: 'unsupported-type'; + FAILED_TO_PARSE_WRAPPED_NUMBER: 'failed-to-parse-wrapped-number'; +} + +export interface HttpsError extends Error { + readonly code: FunctionsErrorCode; + readonly details?: unknown; } -export interface FunctionsModule { +// ============ Module Interface ============ + +/** + * Functions module instance - returned from firebase.functions() or firebase.app().functions() + */ +export interface FunctionsModule extends ReactNativeFirebase.FirebaseModule { + /** The FirebaseApp this module is associated with */ + app: ReactNativeFirebase.FirebaseApp; + + /** + * Returns a reference to the callable HTTPS trigger with the given name. + * + * @param name The name of the trigger. + * @param options Optional settings for the callable function. + */ httpsCallable( name: string, options?: HttpsCallableOptions, ): HttpsCallable; + + /** + * Returns a reference to the callable HTTPS trigger with the given URL. + * + * @param url The URL of the trigger. + * @param options Optional settings for the callable function. + */ httpsCallableFromUrl( url: string, options?: HttpsCallableOptions, ): HttpsCallable; + + /** + * Changes this instance to point to a Cloud Functions emulator running locally. + * + * @deprecated Use useEmulator(host, port) instead. + * @param origin The origin of the local emulator, e.g. "http://localhost:5001". + */ useFunctionsEmulator(origin: string): void; + + /** + * Changes this instance to point to a Cloud Functions emulator running locally. + * + * @param host The host of the emulator, e.g. "localhost" or "10.0.2.2" for Android. + * @param port The port of the emulator, e.g. 5001. + */ useEmulator(host: string, port: number): void; } +// ============ Statics Interface ============ + +/** + * Static properties available on firebase.functions + */ +export interface FunctionsStatics { + HttpsErrorCode: HttpsErrorCode; +} + +// ============ Type Aliases for Convenience ============ + export type Functions = FunctionsModule; -export type FirebaseApp = ReactNativeFirebase.FirebaseApp & { - functions(regionOrCustomDomain?: string): Functions; -}; + +/** + * FirebaseApp type with functions() method. + * @deprecated Import FirebaseApp from '@react-native-firebase/app' instead. + * The functions() method is added via module augmentation. + */ +export type FirebaseApp = ReactNativeFirebase.FirebaseApp; + +// ============ Module Augmentation ============ + +/* eslint-disable @typescript-eslint/no-namespace */ +declare module '@react-native-firebase/app' { + namespace ReactNativeFirebase { + interface Module { + functions: FirebaseModuleWithStaticsAndApp; + } + interface FirebaseApp { + functions(regionOrCustomDomain?: string): FunctionsModule; + } + } +} +/* eslint-enable @typescript-eslint/no-namespace */ + +// ============ Backwards Compatibility Namespace ============ + +/** + * @deprecated Use the exported types directly instead. + * FirebaseFunctionsTypes namespace is kept for backwards compatibility. + */ +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace FirebaseFunctionsTypes { + // Short name aliases referencing top-level types + export type ErrorCode = FunctionsErrorCode; + export type CallableResult = HttpsCallableResult; + export type Callable = HttpsCallable< + RequestData, + ResponseData + >; + export type CallableOptions = HttpsCallableOptions; + export type Error = HttpsError; + export type ErrorCodeMap = HttpsErrorCode; + export type Statics = FunctionsStatics; + export type Module = FunctionsModule; +} + +// Separate namespace block for Https* aliases to avoid naming conflicts +// These provide backwards compatibility for code using FirebaseFunctionsTypes.HttpsCallableResult +export namespace FirebaseFunctionsTypes { + // These must be inline definitions since TypeScript doesn't allow re-exporting + // with the same name as a type that already exists in scope + export interface HttpsCallableResult { + readonly data: T; + } + export interface HttpsCallable { + (data?: RequestData | null): Promise>; + } + export interface HttpsCallableOptions { + timeout?: number; + } + export interface HttpsError extends globalThis.Error { + readonly code: FunctionsErrorCode; + readonly details?: unknown; + } + export interface HttpsErrorCode { + OK: 'ok'; + CANCELLED: 'cancelled'; + UNKNOWN: 'unknown'; + INVALID_ARGUMENT: 'invalid-argument'; + DEADLINE_EXCEEDED: 'deadline-exceeded'; + NOT_FOUND: 'not-found'; + ALREADY_EXISTS: 'already-exists'; + PERMISSION_DENIED: 'permission-denied'; + UNAUTHENTICATED: 'unauthenticated'; + RESOURCE_EXHAUSTED: 'resource-exhausted'; + FAILED_PRECONDITION: 'failed-precondition'; + ABORTED: 'aborted'; + OUT_OF_RANGE: 'out-of-range'; + UNIMPLEMENTED: 'unimplemented'; + INTERNAL: 'internal'; + UNAVAILABLE: 'unavailable'; + DATA_LOSS: 'data-loss'; + UNSUPPORTED_TYPE: 'unsupported-type'; + FAILED_TO_PARSE_WRAPPED_NUMBER: 'failed-to-parse-wrapped-number'; + } +} +/* eslint-enable @typescript-eslint/no-namespace */ diff --git a/packages/functions/lib/web/RNFBFunctionsModule.ts b/packages/functions/lib/web/RNFBFunctionsModule.ts index 7d4b7b397d..989ca52120 100644 --- a/packages/functions/lib/web/RNFBFunctionsModule.ts +++ b/packages/functions/lib/web/RNFBFunctionsModule.ts @@ -60,7 +60,7 @@ export default { if (host) { connectFunctionsEmulator(functionsInstance, host, port); // Hack to work around emulator origin not being set on the instance. - functionsInstance.emulatorOrigin = `http://${host}:${port}`; + (functionsInstance as any).emulatorOrigin = `http://${host}:${port}`; } let callable; if (Object.keys(options).length) { @@ -125,7 +125,7 @@ export default { if (host) { connectFunctionsEmulator(functionsInstance, host, port); // Hack to work around emulator origin not being set on the instance. - functionsInstance.emulatorOrigin = `http://${host}:${port}`; + (functionsInstance as any).emulatorOrigin = `http://${host}:${port}`; } const callable = httpsCallableFromURL(functionsInstance, url, options); const result = await callable(wrapper['data']); diff --git a/packages/functions/tsconfig.json b/packages/functions/tsconfig.json index 8bf433ac43..ea2ec9d3f5 100644 --- a/packages/functions/tsconfig.json +++ b/packages/functions/tsconfig.json @@ -5,9 +5,7 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "jsx": "react-jsx", - "lib": [ - "ESNext" - ], + "lib": ["ESNext"], "module": "ESNext", "moduleResolution": "bundler", "noFallthroughCasesInSwitch": true, @@ -25,7 +23,16 @@ "baseUrl": ".", "rootDir": ".", "paths": { - "@react-native-firebase/app": ["../app/lib"] + "@react-native-firebase/app/lib/common/*": ["../app/dist/typescript/commonjs/lib/common/*"], + "@react-native-firebase/app/lib/common": ["../app/dist/typescript/commonjs/lib/common"], + "@react-native-firebase/app/lib/internal/web/*": [ + "../app/dist/typescript/commonjs/lib/internal/web/*" + ], + "@react-native-firebase/app/lib/internal/*": [ + "../app/dist/typescript/commonjs/lib/internal/*" + ], + "@react-native-firebase/app/lib/internal": ["../app/dist/typescript/commonjs/lib/internal"], + "@react-native-firebase/app": ["../app/dist/typescript/commonjs/lib"] } }, "include": ["lib/**/*"], diff --git a/packages/functions/type-test.ts b/packages/functions/type-test.ts new file mode 100644 index 0000000000..0bc392bb21 --- /dev/null +++ b/packages/functions/type-test.ts @@ -0,0 +1,43 @@ +import functions, { firebase, FirebaseFunctionsTypes } from '.'; + +console.log(functions().app); + +// checks module exists at root +console.log(firebase.functions().app.name); + +// checks module exists at app level +console.log(firebase.app().functions().app.name); + +// app level module accepts string arg +console.log(firebase.app().functions('some-string').app.name); +console.log(firebase.app().functions('some-string').httpsCallable('foo')); + +// checks statics exist +console.log(firebase.functions.SDK_VERSION); + +// checks statics exist on defaultExport +console.log(functions.firebase.SDK_VERSION); + +// checks root exists +console.log(firebase.SDK_VERSION); + +// checks multi-app support exists +console.log(firebase.functions(firebase.app()).app.name); + +// checks default export supports app arg +console.log(firebase.functions(firebase.app('foo')).app.name); + +console.log(firebase.functions.HttpsErrorCode.ABORTED); + +firebase + .functions() + .httpsCallable('foo')(123) + .then((result: FirebaseFunctionsTypes.HttpsCallableResult) => { + console.log(result.data); + }) + .catch((error: { code: any; details: any }) => { + console.log(error.code); + console.log(error.details); + }); + +firebase.functions().useFunctionsEmulator('123'); \ No newline at end of file diff --git a/packages/vertexai/tsconfig.json b/packages/vertexai/tsconfig.json index f371c6edc9..3ae0a77732 100644 --- a/packages/vertexai/tsconfig.json +++ b/packages/vertexai/tsconfig.json @@ -6,9 +6,7 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "jsx": "react-jsx", - "lib": [ - "ESNext" - ], + "lib": ["ESNext"], "module": "ESNext", "target": "ESNext", "moduleResolution": "Bundler", @@ -24,7 +22,16 @@ "strict": true, "baseUrl": ".", "paths": { - "@react-native-firebase/app": ["../app/lib"], + "@react-native-firebase/app/lib/common/*": ["../app/dist/typescript/commonjs/lib/common/*"], + "@react-native-firebase/app/lib/common": ["../app/dist/typescript/commonjs/lib/common"], + "@react-native-firebase/app/lib/internal/web/*": [ + "../app/dist/typescript/commonjs/lib/internal/web/*" + ], + "@react-native-firebase/app/lib/internal/*": [ + "../app/dist/typescript/commonjs/lib/internal/*" + ], + "@react-native-firebase/app/lib/internal": ["../app/dist/typescript/commonjs/lib/internal"], + "@react-native-firebase/app": ["../app/dist/typescript/commonjs/lib"], "@react-native-firebase/auth": ["../auth/lib"], "@react-native-firebase/app-check": ["../app-check/lib"] } diff --git a/tests/ios/Podfile.lock b/tests/ios/Podfile.lock index 49986238bd..77b685665b 100644 --- a/tests/ios/Podfile.lock +++ b/tests/ios/Podfile.lock @@ -1802,7 +1802,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNDeviceInfo (14.1.1): + - RNDeviceInfo (15.0.1): - React-Core - RNFBAnalytics (23.5.0): - FirebaseAnalytics/Core (= 12.6.0) @@ -2298,24 +2298,24 @@ SPEC CHECKSUMS: ReactCodegen: 69c7aec61821e1860aaaf959189218ecca40e811 ReactCommon: ef3e394efce4b78e9214587689c459cf82927368 RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba - RNCAsyncStorage: 6a8127b6987dc9fbce778669b252b14c8355c7ce - RNDeviceInfo: bcce8752b5043a623fe3c26789679b473f705d3c - RNFBAnalytics: 97c7df49a5316376c95fed222e219b811e7ee3d5 - RNFBApp: 177e3a5210c6fb436f511cc4413dad0e7c428bfd - RNFBAppCheck: d26dabdff94eb45f90fb7c8aa767306c85cdd26d - RNFBAppDistribution: 9c78e3cd8087e9c2fb0852e03fb08ace1480e1d7 - RNFBAuth: 4bcbf1b4b9e1024755dcab122d36a5dafba0e3f3 - RNFBCrashlytics: 61932725c42d3fed9eb255253b6a833162d6ee08 - RNFBDatabase: 4954117115f02550bb40ff0736017a60306e7775 - RNFBFirestore: 2602dab917e06e091b275984722a46c84b88ec48 - RNFBFunctions: 0bc8036021a91a9fbb4d28cae08c5b1089a307f3 - RNFBInAppMessaging: 54c3765e20ac9ceaa87afb727e1f18fe4a6a92ac - RNFBInstallations: 11a5da460997ba084484222580fe61785b38083a - RNFBMessaging: 0835c2f99911197fe95689fea3b4c8725af723f1 - RNFBML: 2cbc15122658e03c20f088a5c0b99dc116a417a4 - RNFBPerf: a976bdc076ef268d1d4ccdae69829d4e11bbc172 - RNFBRemoteConfig: ebca776a11b18ea0070cb3c6c9f29608e397caa3 - RNFBStorage: fe423da7035c0848bd2a051ff03fb97d5bb1107a + RNCAsyncStorage: 481acf401089f312189e100815088ea5dafc583c + RNDeviceInfo: 53f9c84e28e854a308d3e548e25ef120b4615531 + RNFBAnalytics: bd6653e12b1770b4f1b613adfb09f8966bc5156f + RNFBApp: d9e2a89c210b1f08fcf3096b15871d8dd896cdae + RNFBAppCheck: ceab2b24388cd1fd3a4e23de82e0c7a95bd4e210 + RNFBAppDistribution: 753dfc0956a35d319eba78828a32ea32ceb53e1d + RNFBAuth: eb8385644c405668ef77dc8dfd260283b1bc8b8c + RNFBCrashlytics: 2242d1973d2b7d487a0f7cb7a95227f6f90df940 + RNFBDatabase: 700462a4e6515b8ee80f3a20fbe872c57a9a42ad + RNFBFirestore: 4a305c200a7dc57aced0e0ef4ddbede79d758db3 + RNFBFunctions: afc8ca2a11ff7a6380322ec5e63c58333ead5d79 + RNFBInAppMessaging: c6a184af904d87614cec5952ff60fdc049200b0b + RNFBInstallations: a5ace09d4c76c0e1b929cc4650630bdefd7b3c96 + RNFBMessaging: 53d0daa2ead6a94d0ae4bdb37bdbebd4ff26fcdd + RNFBML: 37c38ac8bff7a8d954b1ac9cc1a3685b9bfa227c + RNFBPerf: 859dbab3d443d38a08e28150a0c2ee612332a685 + RNFBRemoteConfig: abb2609ebc25f2a2bdb3659ce88e4697985492be + RNFBStorage: fb85a107606e14f9d9fc919f6f52b9f3e717c79a SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: 66a9fd80007d5d5fce19d1676ce17b4d5e16e9b1 diff --git a/tests/metro.config.js b/tests/metro.config.js index 23a36c692d..a3de79d720 100644 --- a/tests/metro.config.js +++ b/tests/metro.config.js @@ -61,6 +61,24 @@ const config = { }, }, ), + resolveRequest: (context, moduleName, platform) => { + // For @react-native-firebase/app subpath imports, redirect lib/* to dist/commonjs/* + if (moduleName.startsWith('@react-native-firebase/app/lib/')) { + const subpath = moduleName.replace('@react-native-firebase/app/lib/', ''); + const newModuleName = `@react-native-firebase/app/dist/commonjs/${subpath}`; + return context.resolveRequest(context, newModuleName, platform); + } + + // For @react-native-firebase/app/common/*, redirect to dist/commonjs/common/* + if (moduleName.startsWith('@react-native-firebase/app/common/')) { + const subpath = moduleName.replace('@react-native-firebase/app/common/', ''); + const newModuleName = `@react-native-firebase/app/dist/commonjs/common/${subpath}`; + return context.resolveRequest(context, newModuleName, platform); + } + + // Let Metro resolve normally + return context.resolveRequest(context, moduleName, platform); + }, }, transformer: { unstable_allowRequireContext: true, diff --git a/tsconfig-jest.json b/tsconfig-jest.json index 8a42f66917..16b3db65a2 100644 --- a/tsconfig-jest.json +++ b/tsconfig-jest.json @@ -4,9 +4,13 @@ "baseUrl": ".", "paths": { "@react-native-firebase/app": ["packages/app/lib"], + "@react-native-firebase/app/lib/common": ["packages/app/lib/common"], + "@react-native-firebase/app/lib/common/*": ["packages/app/lib/common/*"], + "@react-native-firebase/app/lib/internal": ["packages/app/lib/internal"], + "@react-native-firebase/app/lib/internal/*": ["packages/app/lib/internal/*"], "@react-native-firebase/auth": ["packages/auth/lib"], "@react-native-firebase/app-check": ["packages/app-check/lib"], - "@react-native-firebase/ai": ["packages/ai/lib"], + "@react-native-firebase/ai": ["packages/ai/lib"] } } } diff --git a/tsconfig.json b/tsconfig.json index 4486da5720..b8201cae57 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,12 @@ { - "include": ["packages/**/lib/*.d.ts", "packages/**/type-test.ts"], + "include": ["packages/**/lib/**/*.d.ts", "packages/functions/type-test.ts", "packages/app/type-test.ts"], "compilerOptions": { "noEmit": true, "target": "es5", "module": "commonjs", "declaration": true, "importHelpers": true, - "jsx": "react", + "jsx": "react-native", "sourceMap": true, "strict": true, "noUnusedLocals": true, @@ -15,10 +15,16 @@ "noFallthroughCasesInSwitch": true, "moduleResolution": "node", "skipLibCheck": true, + "skipDefaultLibCheck": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "esModuleInterop": true, - "lib": ["es2015", "es2016", "esnext", "dom"] + "baseUrl": ".", + "lib": ["es2015", "es2016", "esnext", "dom"], + "types": ["react-native", "node"], + "paths": { + "@react-native-firebase/*": ["./packages/*/lib/index.d.ts"] + } }, - "exclude": ["node_modules", "**/*.spec.ts"] + "exclude": ["node_modules", "**/*.spec.ts", "packages/**/dist"] } diff --git a/yarn.lock b/yarn.lock index 2becdc9b4f..d30a9bbb95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5094,6 +5094,7 @@ __metadata: "@react-native-async-storage/async-storage": "npm:^2.2.0" expo: "npm:^54.0.27" firebase: "npm:12.6.0" + react-native-builder-bob: "npm:^0.40.13" peerDependencies: expo: ">=47.0.0" react: "*" @@ -11119,7 +11120,20 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2, fast-glob@npm:^3.3.3": +"fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2": + version: 3.3.2 + resolution: "fast-glob@npm:3.3.2" + dependencies: + "@nodelib/fs.stat": "npm:^2.0.2" + "@nodelib/fs.walk": "npm:^1.2.3" + glob-parent: "npm:^5.1.2" + merge2: "npm:^1.3.0" + micromatch: "npm:^4.0.4" + checksum: 10/222512e9315a0efca1276af9adb2127f02105d7288fa746145bf45e2716383fb79eb983c89601a72a399a56b7c18d38ce70457c5466218c5f13fad957cee16df + languageName: node + linkType: hard + +"fast-glob@npm:^3.3.3": version: 3.3.3 resolution: "fast-glob@npm:3.3.3" dependencies: @@ -20265,7 +20279,7 @@ __metadata: languageName: node linkType: hard -"react-native-builder-bob@npm:^0.40.12, react-native-builder-bob@npm:^0.40.16": +"react-native-builder-bob@npm:^0.40.12, react-native-builder-bob@npm:^0.40.13, react-native-builder-bob@npm:^0.40.16": version: 0.40.16 resolution: "react-native-builder-bob@npm:0.40.16" dependencies: