diff --git a/packages/ai/tsconfig.json b/packages/ai/tsconfig.json index f1d9865812..b8f113d3bf 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,11 @@ "strict": true, "baseUrl": ".", "paths": { - "@react-native-firebase/app": ["../app/lib"], + "@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/analytics/lib/internal-types/native-modules.d.ts b/packages/analytics/lib/internal-types/native-modules.d.ts new file mode 100644 index 0000000000..86ee43c01a --- /dev/null +++ b/packages/analytics/lib/internal-types/native-modules.d.ts @@ -0,0 +1,61 @@ +/* + * 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. + * + */ + +/** + * Internal type definitions for the Analytics native module. + * This file augments the base ReactNativeFirebaseNativeModules interface + * to add analytics-specific native methods. + * + * @internal - This file is not exposed to package consumers + */ + +import type { ConsentSettings } from './types/analytics'; + +/** + * Interface for the Analytics native module methods + */ +export interface RNFBAnalyticsModuleInterface { + logEvent(name: string, params: { [key: string]: any }): Promise; + setAnalyticsCollectionEnabled(enabled: boolean): Promise; + setSessionTimeoutDuration(milliseconds: number): Promise; + getAppInstanceId(): Promise; + getSessionId(): Promise; + setUserId(id: string | null): Promise; + setUserProperty(name: string, value: string | null): Promise; + setUserProperties(properties: { [key: string]: string | null }): Promise; + resetAnalyticsData(): Promise; + setConsent(consentSettings: ConsentSettings): Promise; + setDefaultEventParameters(params: { [key: string]: any } | null | undefined): Promise; + initiateOnDeviceConversionMeasurementWithEmailAddress(emailAddress: string): Promise; + initiateOnDeviceConversionMeasurementWithHashedEmailAddress( + hashedEmailAddress: string, + ): Promise; + initiateOnDeviceConversionMeasurementWithPhoneNumber(phoneNumber: string): Promise; + initiateOnDeviceConversionMeasurementWithHashedPhoneNumber( + hashedPhoneNumber: string, + ): Promise; +} + +/** + * Augment the base ReactNativeFirebaseNativeModules interface + * from the app package to include our analytics module + */ +declare module '@react-native-firebase/app/lib/internal/NativeModules' { + interface ReactNativeFirebaseNativeModules { + RNFBAnalyticsModule: RNFBAnalyticsModuleInterface; + } +} diff --git a/packages/analytics/lib/internal-types/web-modules.d.ts b/packages/analytics/lib/internal-types/web-modules.d.ts new file mode 100644 index 0000000000..d6b4c93b9a --- /dev/null +++ b/packages/analytics/lib/internal-types/web-modules.d.ts @@ -0,0 +1,52 @@ +/* + * 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. + * + */ + +/** + * Type declarations for web-only modules from the app package. + * These modules are JavaScript files with separate .d.ts declarations. + * + * @internal - This file is not exposed to package consumers + */ + +declare module '@react-native-firebase/app/lib/internal/web/firebaseApp' { + import type { ReactNativeFirebase } from '@react-native-firebase/app'; + + export function getApp(appName?: string): ReactNativeFirebase.FirebaseApp; +} + +declare module '@react-native-firebase/app/lib/internal/web/utils' { + export function guard(fn: () => T | Promise): Promise; +} + +declare module '@react-native-firebase/app/lib/internal/web/firebaseInstallations' { + import type { ReactNativeFirebase } from '@react-native-firebase/app'; + + export interface Installations { + app: ReactNativeFirebase.FirebaseApp; + } + + export type Unsubscribe = () => void; + + export function getApp(appName?: string): ReactNativeFirebase.FirebaseApp; + export function getInstallations(app?: ReactNativeFirebase.FirebaseApp): Installations; + export function getId(installations: Installations): Promise; + export function onIdChange( + installations: Installations, + callback: (installationId: string) => void, + ): Unsubscribe; + export function makeIDBAvailable(): void; +} diff --git a/packages/analytics/lib/namespaced.ts b/packages/analytics/lib/namespaced.ts index b95bcc1fbc..b9208bcb12 100644 --- a/packages/analytics/lib/namespaced.ts +++ b/packages/analytics/lib/namespaced.ts @@ -14,7 +14,7 @@ * limitations under the License. * */ -import { ReactNativeFirebase } from '@react-native-firebase/app'; +import type { ReactNativeFirebase } from '@react-native-firebase/app'; import { isAlphaNumericUnderscore, isE164PhoneNumber, @@ -37,6 +37,10 @@ import { import { setReactNativeModule } from '@react-native-firebase/app/lib/internal/nativeModule'; import { isBoolean } from '@react-native-firebase/app/lib/common'; +// Import internal types to register native module augmentation +import './internal-types/native-modules'; +import './internal-types/web-modules'; + import { validateStruct, validateCompound } from './struct'; import { RNFBAnalyticsModule } from './web/RNFBAnalyticsModule'; import { version } from './version'; @@ -120,7 +124,7 @@ const namespace = 'analytics'; const nativeModuleName = 'RNFBAnalyticsModule'; -class FirebaseAnalyticsModule extends FirebaseModule { +class FirebaseAnalyticsModule extends FirebaseModule<'RNFBAnalyticsModule'> { logEvent( name: string, params: { [key: string]: any } = {}, @@ -879,13 +883,13 @@ export default createModuleNamespace({ hasMultiAppSupport: false, hasCustomUrlOrRegionSupport: false, ModuleClass: FirebaseAnalyticsModule, -}) as typeof defaultExport; +}) as unknown as typeof defaultExport; // Register the interop module for non-native platforms. -setReactNativeModule(nativeModuleName, RNFBAnalyticsModule); +setReactNativeModule(nativeModuleName, RNFBAnalyticsModule as unknown as Record); -export const firebase: FirebaseAnalyticsModule & { +export const firebase = getFirebaseRoot() as unknown as typeof defaultExport & { analytics: typeof defaultExport; SDK_VERSION: string; app(name?: string): ReactNativeFirebase.FirebaseApp & { analytics(): FirebaseAnalyticsModule }; -} = getFirebaseRoot(); +}; diff --git a/packages/analytics/lib/types.d.ts b/packages/analytics/lib/types.d.ts deleted file mode 100644 index 5180af5334..0000000000 --- a/packages/analytics/lib/types.d.ts +++ /dev/null @@ -1,97 +0,0 @@ -declare module '@react-native-firebase/app/lib/common' { - export const MODULAR_DEPRECATION_ARG: string; - export const isAlphaNumericUnderscore: (value: any) => boolean; - export const isE164PhoneNumber: (value: any) => boolean; - export const isIOS: boolean; - export const isNull: (value: any) => value is null; - export const isNumber: (value: any) => value is number; - export const isObject: (value: any) => value is object; - export const isOneOf: (value: any, validValues: any[]) => boolean; - export const isString: (value: any) => value is string; - export const isUndefined: (value: any) => value is undefined; - export const isBoolean: (value: any) => value is boolean; -} - -declare module '@react-native-firebase/app/lib/common/unitTestUtils' { - export const getFirebaseApp: () => any; - export const getMockNativeModule: (name: string) => any; - export function createCheckV9Deprecation(moduleNames: string[]): CheckV9DeprecationFunction; - export type CheckV9DeprecationFunction = ( - modularFunction: () => void, - nonModularFunction: () => void, - methodNameKey: string, - uniqueMessage?: string | null, - ignoreFirebaseAppDeprecationWarning?: boolean, - ) => void; -} - -declare module '@react-native-firebase/app/lib/common/validate' { - export const isUndefined: (value: any) => value is undefined; -} - -declare module '@react-native-firebase/app/lib/internal' { - export function createModuleNamespace(config: any): any; - export class FirebaseModule { - constructor(...args: any[]); - native: any; - app: 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/internal/web/firebaseInstallations' { - export function getApp(appName?: string): any; - export function getId(installations: any): Promise; - export function onIdChange( - installations: any, - callback: (installationId: string) => void, - ): () => void; - export function getInstallations(app: any): any; - export function makeIDBAvailable(): void; -} - -declare module '@react-native-firebase/app/lib/internal/asyncStorage' { - export function setItem(key: string, value: string): Promise; - export function getItem(key: string): Promise; - export function isMemoryStorage(): boolean; -} - -declare module '@react-native-firebase/app/lib/internal/web/firebaseApp' { - export function getApp(appName?: string): any; -} - -declare module '@react-native-firebase/app/lib/internal/web/utils' { - export function guard(fn: () => any): any; -} - -declare module '@react-native-firebase/app/lib' { - namespace ReactNativeFirebase { - import FirebaseModuleWithStaticsAndApp = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp; - interface Module { - analytics: FirebaseModuleWithStaticsAndApp; - } - interface FirebaseApp { - analytics(): any; - readonly name: string; - } - } -} - -declare module './version' { - const version: string; - export default version; -} - -declare module './web/RNFBAnalyticsModule' { - const fallBackModule: any; - export default fallBackModule; -} diff --git a/packages/analytics/tsconfig.json b/packages/analytics/tsconfig.json index 8e4d4e23c0..01e51f7603 100644 --- a/packages/analytics/tsconfig.json +++ b/packages/analytics/tsconfig.json @@ -23,8 +23,16 @@ "baseUrl": ".", "rootDir": ".", "paths": { - "@react-native-firebase/app/common/*": ["../app/lib/common/*"], - "@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/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..1a208e4afa 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'; -export default class FirebaseApp { - constructor(options, appConfig, fromNative, deleteApp) { - const { name, automaticDataCollectionEnabled } = appConfig; +export default class FirebaseApp implements ReactNativeFirebase.FirebaseApp { + 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 81% rename from packages/app/lib/common/MutatableParams.js rename to packages/app/lib/common/MutatableParams.ts index 8b998dade5..00710b7867 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: any): MutatableParams { deepSet(this._mutatableParams, param, value); return this._parentInstance; } - get(param) { + get(param: string): any { 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 77% rename from packages/app/lib/common/deeps.js rename to packages/app/lib/common/deeps.ts index cf235c376b..8c7660966a 100644 --- a/packages/app/lib/common/deeps.js +++ b/packages/app/lib/common/deeps.ts @@ -25,7 +25,7 @@ import { isArray, isObject } from './validate'; * @param joiner * @returns {*} */ -export function deepGet(object, path, joiner = '/') { +export function deepGet(object: any, path: string, joiner: string = '/'): any { if (!isObject(object) && !Array.isArray(object)) { return undefined; } @@ -36,11 +36,11 @@ export function deepGet(object, path, joiner = '/') { 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; @@ -54,7 +54,13 @@ export function deepGet(object, path, joiner = '/') { * @param initPaths * @param joiner */ -export function deepSet(object, path, value, initPaths = true, joiner = '.') { +export function deepSet( + object: any, + path: string, + value: any, + initPaths: boolean = true, + joiner: string = '.', +): boolean { if (!isObject(object)) { return false; } @@ -65,15 +71,15 @@ export function deepSet(object, path, value, initPaths = true, joiner = '.') { 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.d.ts b/packages/app/lib/common/index.d.ts deleted file mode 100644 index fc3b4a8963..0000000000 --- a/packages/app/lib/common/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const MODULAR_DEPRECATION_ARG: string; -export const isAndroid: boolean; -export const isNumber: (value: any) => value is number; - -export function createMessage( - nameSpace: string, - methodName: string, - instanceName?: string, - uniqueMessage?: string | null, -): string; diff --git a/packages/app/lib/common/index.js b/packages/app/lib/common/index.ts similarity index 88% rename from packages/app/lib/common/index.js rename to packages/app/lib/common/index.ts index 08afb367c2..04b795b125 100644 --- a/packages/app/lib/common/index.js +++ b/packages/app/lib/common/index.ts @@ -26,7 +26,12 @@ export * from './validate'; export { default as Base64 } from './Base64'; export { default as ReferenceBase } from './ReferenceBase'; -export function getDataUrlParts(dataUrlString) { +export interface DataUrlParts { + base64String: string | undefined; + mediaType: string | undefined; +} + +export function getDataUrlParts(dataUrlString: string): DataUrlParts { const isBase64 = dataUrlString.includes(';base64'); let [mediaType, base64String] = dataUrlString.split(','); if (!mediaType || !base64String) { @@ -42,11 +47,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 +64,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 +72,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 +82,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 +95,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 +103,7 @@ export function tryJSONParse(string) { } } -export function tryJSONStringify(data) { +export function tryJSONStringify(data: any): string | null { try { return JSON.stringify(data); } catch (_) { @@ -103,14 +111,23 @@ export function tryJSONStringify(data) { } } -export function parseListenerOrObserver(listenerOrObserver) { +export interface Observer { + next: (value: T) => void; + error?: (error: Error) => void; + complete?: () => void; +} + +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 +136,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,18 +544,23 @@ 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) { + if (!(globalThis as any).RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS) { // eslint-disable-next-line no-console console.warn(createMessage(nameSpace, methodName, instanceName)); - if (globalThis.RNFB_MODULAR_DEPRECATION_STRICT_MODE === true) { + if ((globalThis as any).RNFB_MODULAR_DEPRECATION_STRICT_MODE === true) { throw new Error('Deprecated API usage detected while in strict mode.'); } } @@ -544,11 +570,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 +596,10 @@ export function createMessage( } } } + return undefined; } -function getNamespace(target) { +function getNamespace(target: any): string | undefined { if (target.constructor.name === 'DatabaseReference') { return 'database'; } @@ -588,13 +615,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 +647,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 +661,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 +731,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 +769,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 +779,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 83% rename from packages/app/lib/common/path.js rename to packages/app/lib/common/path.ts index 3e276f8529..81fdbcddd4 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: any): 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: any): 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 61% rename from packages/app/lib/common/promise.js rename to packages/app/lib/common/promise.ts index b1f9b86e4d..bfe160075d 100644 --- a/packages/app/lib/common/promise.js +++ b/packages/app/lib/common/promise.ts @@ -17,16 +17,23 @@ import { isFunction } from './validate'; +export interface Deferred { + promise: Promise; + resolve: ((value: T) => void) | null; + reject: ((reason?: any) => void) | null; +} + /** - * + * Creates a deferred promise */ -export function promiseDefer() { - const deferred = { +export function promiseDefer(): Deferred { + const deferred: Deferred = { + promise: null as any, resolve: null, reject: null, }; - deferred.promise = new Promise((resolve, reject) => { + deferred.promise = new Promise((resolve, reject) => { deferred.resolve = resolve; deferred.reject = reject; }); @@ -34,11 +41,17 @@ export function promiseDefer() { return deferred; } +type Callback = ((error: Error | null, result?: T) => void) | ((error: Error | null) => void); + /** + * 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 +59,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..404e02615b 100644 --- a/packages/app/lib/common/serialize.js +++ b/packages/app/lib/common/serialize.ts @@ -18,7 +18,12 @@ import { tryJSONParse, tryJSONStringify } from './index'; import { isObject } from './validate'; -export function serializeType(value) { +export interface SerializedValue { + type: string; + value: any; +} + +export function serializeType(value: any): SerializedValue { if (isObject(value)) { return { type: 'object', @@ -32,7 +37,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 98e7f6437e..f2315f06f8 100644 --- a/packages/app/lib/common/unitTestUtils.ts +++ b/packages/app/lib/common/unitTestUtils.ts @@ -52,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.d.ts b/packages/app/lib/common/validate.d.ts deleted file mode 100644 index 1d1131c48a..0000000000 --- a/packages/app/lib/common/validate.d.ts +++ /dev/null @@ -1,110 +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. - * - */ - -/** - * Validates that all key-value pairs in an object are strings. - */ -export function objectKeyValuesAreStrings(object: any): boolean; - -/** - * Simple is null check. - */ -export function isNull(value: any): value is null; - -/** - * Simple is object check. - */ -export function isObject(value: any): value is Record; - -/** - * Simple is date check - */ -export function isDate(value: any): value is Date; - -/** - * Simple is function check - */ -export function isFunction(value: any): value is (...args: any[]) => any; - -/** - * Simple is string check - */ -export function isString(value: any): value is string; - -/** - * Simple is number check - */ -export function isNumber(value: any): value is number; - -/** - * Simple is phone number check for E.164 format - */ -export function isE164PhoneNumber(value: any): boolean; - -/** - * Simple finite check - */ -export function isFinite(value: any): boolean; - -/** - * Simple integer check - */ -export function isInteger(value: any): boolean; - -/** - * Simple is boolean check - */ -export function isBoolean(value: any): value is boolean; - -/** - * Array check - */ -export function isArray(value: any): value is Array; - -/** - * Undefined check - */ -export function isUndefined(value: any): value is undefined; - -/** - * Alphanumeric underscore pattern check (/^[a-zA-Z0-9_]+$/) - */ -export function isAlphaNumericUnderscore(value: any): boolean; - -/** - * URL validation test - */ -export function isValidUrl(url: any): boolean; - -/** - * Array includes check - */ -export function isOneOf(value: any, oneOf?: any[]): boolean; - -/** - * No-operation function - */ -export function noop(): void; - -/** - * Validates that an optional native dependency exists - */ -export function validateOptionalNativeDependencyExists( - firebaseJsonKey: string, - apiName: string, - nativeFnExists: boolean, -): void; diff --git a/packages/app/lib/common/validate.js b/packages/app/lib/common/validate.ts similarity index 75% rename from packages/app/lib/common/validate.js rename to packages/app/lib/common/validate.ts index de974d40bc..03b02e9324 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: any): 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: any): 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: any): value is Record { return value ? typeof value === 'object' && !Array.isArray(value) && !isNull(value) : false; } @@ -62,7 +65,7 @@ export function isObject(value) { * @param value * @returns {boolean} */ -export function isDate(value) { +export function isDate(value: any): 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); } @@ -73,7 +76,7 @@ export function isDate(value) { * @param value * @returns {*|boolean} */ -export function isFunction(value) { +export function isFunction(value: any): value is (...args: any[]) => any { return value ? typeof value === 'function' : false; } @@ -82,7 +85,7 @@ export function isFunction(value) { * @param value * @return {boolean} */ -export function isString(value) { +export function isString(value: any): value is string { return typeof value === 'string'; } @@ -91,7 +94,7 @@ export function isString(value) { * @param value * @return {boolean} */ -export function isNumber(value) { +export function isNumber(value: any): value is number { return typeof value === 'number'; } @@ -100,7 +103,7 @@ export function isNumber(value) { * @param value * @return {boolean} */ -export function isE164PhoneNumber(value) { +export function isE164PhoneNumber(value: any): boolean { const PHONE_NUMBER = /^\+[1-9]\d{1,14}$/; // E.164 return PHONE_NUMBER.test(value); } @@ -110,7 +113,7 @@ export function isE164PhoneNumber(value) { * @param value * @returns {boolean} */ -export function isFinite(value) { +export function isFinite(value: any): boolean { return Number.isFinite(value); } @@ -119,7 +122,7 @@ export function isFinite(value) { * @param value * @returns {boolean} */ -export function isInteger(value) { +export function isInteger(value: any): boolean { return Number.isInteger(value); } @@ -129,7 +132,7 @@ export function isInteger(value) { * @param value * @return {boolean} */ -export function isBoolean(value) { +export function isBoolean(value: any): value is boolean { return typeof value === 'boolean'; } @@ -138,7 +141,7 @@ export function isBoolean(value) { * @param value * @returns {arg is Array} */ -export function isArray(value) { +export function isArray(value: any): value is Array { return Array.isArray(value); } @@ -147,7 +150,7 @@ export function isArray(value) { * @param value * @returns {boolean} */ -export function isUndefined(value) { +export function isUndefined(value: any): value is undefined { return typeof value === 'undefined'; } @@ -157,7 +160,7 @@ export function isUndefined(value) { * @param value * @returns {boolean} */ -export function isAlphaNumericUnderscore(value) { +export function isAlphaNumericUnderscore(value: any): boolean { return AlphaNumericUnderscore.test(value); } @@ -167,7 +170,7 @@ export function isAlphaNumericUnderscore(value) { * @returns {boolean} */ const IS_VALID_URL_REGEX = /^(http|https):\/\/[^ "]+$/; -export function isValidUrl(url) { +export function isValidUrl(url: any): boolean { return IS_VALID_URL_REGEX.test(url); } @@ -178,18 +181,22 @@ export function isValidUrl(url) { * @param oneOf * @returns {boolean} */ -export function isOneOf(value, oneOf = []) { +export function isOneOf(value: any, oneOf: any[] = []): 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..557d8f5b3b --- /dev/null +++ b/packages/app/lib/index.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. + * + */ + +export { firebase, utils, default } from './namespaced'; +export * from './modular'; +export type { ReactNativeFirebase, Utils } from './types'; diff --git a/packages/app/lib/internal.d.ts b/packages/app/lib/internal.d.ts deleted file mode 100644 index 0531871988..0000000000 --- a/packages/app/lib/internal.d.ts +++ /dev/null @@ -1,49 +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 type { ReactNativeFirebase } from './index'; - -// Type definitions for internal modules used by other packages during TypeScript migration - -declare module '@react-native-firebase/app/lib/internal' { - import BaseFirebaseModule = ReactNativeFirebase.FirebaseModule; - export class FirebaseModule extends BaseFirebaseModule { - native: any; - emitter: any; - } - export function createModuleNamespace(config: any): any; - 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(name: string, module: any): void; -} - -declare module '@react-native-firebase/app/lib/common' { - export function isAlphaNumericUnderscore(value: any): boolean; - export function isE164PhoneNumber(value: any): boolean; - export function isIOS(): boolean; - export function isNull(value: any): boolean; - export function isNumber(value: any): boolean; - export function isObject(value: any): boolean; - export function isOneOf(value: any, array: any[]): boolean; - export function isString(value: any): boolean; - export function isUndefined(value: any): boolean; - export function isBoolean(value: any): boolean; -} 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..b3d22ba9fe --- /dev/null +++ b/packages/app/lib/internal/FirebaseModule.ts @@ -0,0 +1,93 @@ +/* + * 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'; +import type { ReactNativeFirebaseNativeModules } from './NativeModules'; +import type EventEmitter from 'react-native/Libraries/vendor/emitter/EventEmitter'; + +/** + * Firebase JSON configuration from firebase.json file + * Structure: { "react-native": { [key: string]: boolean | string }, ... } + */ +export type FirebaseJsonConfig = Record; + +let firebaseJson: FirebaseJsonConfig | null = null; + +export interface ModuleConfig { + namespace: string; + nativeModuleName?: string | string[]; + hasMultiAppSupport?: boolean; + hasCustomUrlOrRegionSupport?: boolean; + nativeEvents?: boolean | string[]; + disablePrependCustomUrlOrRegion?: boolean; + turboModule?: boolean; +} + +export default class FirebaseModule< + NativeModuleName extends keyof ReactNativeFirebaseNativeModules = any, +> { + _app: ReactNativeFirebase.FirebaseApp; + _nativeModule: ReactNativeFirebaseNativeModules[NativeModuleName] | null; + _customUrlOrRegion: string | null; + _config: ModuleConfig; + + constructor( + app: ReactNativeFirebase.FirebaseApp, + 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; + } + + 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 70% rename from packages/app/lib/internal/NativeFirebaseError.js rename to packages/app/lib/internal/NativeFirebaseError.ts index 5fed6889ba..aacb761c5e 100644 --- a/packages/app/lib/internal/NativeFirebaseError.js +++ b/packages/app/lib/internal/NativeFirebaseError.ts @@ -15,12 +15,44 @@ * */ +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; +} + 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 +118,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..76f07e4494 --- /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'; + +/** + * 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.d.ts b/packages/app/lib/internal/asyncStorage.d.ts deleted file mode 100644 index 5be94ee0fb..0000000000 --- a/packages/app/lib/internal/asyncStorage.d.ts +++ /dev/null @@ -1,47 +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. - * - */ - -// AsyncStorage interface compatible with React Native AsyncStorage -export interface AsyncStorageStatic { - setItem(key: string, value: string): Promise; - getItem(key: string): Promise; - removeItem(key: string): Promise; -} - -// Memory storage Map instance -export const memoryStorage: Map; - -// Storage key prefix -export const prefix: string; - -// Get the current AsyncStorage instance (either React Native AsyncStorage or memory storage) -export function getReactNativeAsyncStorageInternal(): Promise; - -// Set the AsyncStorage instance to use (React Native AsyncStorage or fallback to memory storage) -export function setReactNativeAsyncStorageInternal(asyncStorageInstance?: AsyncStorageStatic): void; - -// Check if currently using memory storage (fallback) -export function isMemoryStorage(): boolean; - -// Set an item in storage with the React Native Firebase prefix -export function setItem(key: string, value: string): Promise; - -// Get an item from storage with the React Native Firebase prefix -export function getItem(key: string): Promise; - -// Remove an item from storage with the React Native Firebase prefix -export function removeItem(key: string): Promise; 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..ed2cc89591 --- /dev/null +++ b/packages/app/lib/internal/asyncStorage.ts @@ -0,0 +1,81 @@ +/* + * 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. + * + */ + +// AsyncStorage interface compatible with React Native AsyncStorage +export interface AsyncStorageStatic { + setItem: (key: string, value: string) => Promise; + getItem: (key: string) => Promise; + removeItem: (key: string) => Promise; +} + +// 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 60% rename from packages/app/lib/internal/logger.js rename to packages/app/lib/internal/logger.ts index b1b84ace55..73d4ccd84f 100644 --- a/packages/app/lib/internal/logger.js +++ b/packages/app/lib/internal/logger.ts @@ -15,37 +15,59 @@ * */ +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; +} + /** - * @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 +82,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 +111,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 +119,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 +150,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 +162,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 +170,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 +184,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 +196,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 +267,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 99% rename from packages/app/lib/internal/nativeModule.android.js rename to packages/app/lib/internal/nativeModule.android.ts index 95f31361ba..a6f72d6a97 100644 --- a/packages/app/lib/internal/nativeModule.android.js +++ b/packages/app/lib/internal/nativeModule.android.ts @@ -1,2 +1,3 @@ export { getReactNativeModule } from './nativeModuleAndroidIos'; export { setReactNativeModule } from './nativeModuleAndroidIos'; + diff --git a/packages/app/lib/internal/nativeModule.d.ts b/packages/app/lib/internal/nativeModule.d.ts index b901315eb0..87889396f6 100644 --- a/packages/app/lib/internal/nativeModule.d.ts +++ b/packages/app/lib/internal/nativeModule.d.ts @@ -1,2 +1,3 @@ -export declare function getReactNativeModule(moduleName: string): any; -export declare function setReactNativeModule(moduleName: string, module: any): any; +export { getReactNativeModule } from './nativeModuleWeb'; +export { setReactNativeModule } from './nativeModuleWeb'; +//# sourceMappingURL=nativeModule.d.ts.map \ No newline at end of file diff --git a/packages/app/lib/internal/nativeModule.ios.js b/packages/app/lib/internal/nativeModule.ios.ts similarity index 99% rename from packages/app/lib/internal/nativeModule.ios.js rename to packages/app/lib/internal/nativeModule.ios.ts index 95f31361ba..a6f72d6a97 100644 --- a/packages/app/lib/internal/nativeModule.ios.js +++ b/packages/app/lib/internal/nativeModule.ios.ts @@ -1,2 +1,3 @@ export { getReactNativeModule } from './nativeModuleAndroidIos'; export { setReactNativeModule } from './nativeModuleAndroidIos'; + diff --git a/packages/app/lib/internal/nativeModule.js b/packages/app/lib/internal/nativeModule.ts similarity index 99% rename from packages/app/lib/internal/nativeModule.js rename to packages/app/lib/internal/nativeModule.ts index e1a9e708b6..e4e77b5c8a 100644 --- a/packages/app/lib/internal/nativeModule.js +++ b/packages/app/lib/internal/nativeModule.ts @@ -2,3 +2,4 @@ // In this case we use the web Firebase JS SDK. export { getReactNativeModule } from './nativeModuleWeb'; export { setReactNativeModule } from './nativeModuleWeb'; + diff --git a/packages/app/lib/internal/nativeModuleAndroidIos.js b/packages/app/lib/internal/nativeModuleAndroidIos.js 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..24918e0a5a --- /dev/null +++ b/packages/app/lib/internal/nativeModuleWeb.ts @@ -0,0 +1,63 @@ +/* eslint-disable no-console */ + +import './global'; +// @ts-expect-error +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 83% rename from packages/app/lib/internal/nullSerialization.js rename to packages/app/lib/internal/nullSerialization.ts index 6edc36d6b9..e0bf7be7f3 100644 --- a/packages/app/lib/internal/nullSerialization.js +++ b/packages/app/lib/internal/nullSerialization.ts @@ -17,6 +17,25 @@ const NULL_SENTINEL = { __rnfbNull: true }; +type NullSentinel = typeof NULL_SENTINEL; + +type ArrayFrame = { + type: 'array'; + original: any[]; + encoded: any[]; + 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. @@ -31,7 +50,7 @@ const NULL_SENTINEL = { __rnfbNull: true }; * @param {any} data - The data to encode * @returns {any} - The encoded data with null object properties replaced by sentinels */ -export function encodeNullValues(data) { +export function encodeNullValues(data: any): any { if (data === null) { // only null values within objects are encoded return null; @@ -41,7 +60,13 @@ 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 processChild( + child: any, + encoded: any[] | Record, + keyOrIndex: string | number, + isArray: boolean, + stack: StackFrame[], + ): void { if (child === null) { // Arrays preserve nulls as null, objects convert to sentinel encoded[keyOrIndex] = isArray ? null : NULL_SENTINEL; @@ -57,7 +82,7 @@ export function encodeNullValues(data) { index: 0, }); } else { - const childEncoded = {}; + const childEncoded: Record = {}; encoded[keyOrIndex] = childEncoded; stack.push({ type: 'object', @@ -70,8 +95,8 @@ export function encodeNullValues(data) { } // Prepare root encoded container - let rootEncoded; - const stack = []; + let rootEncoded: any[] | Record; + const stack: StackFrame[] = []; if (Array.isArray(data)) { rootEncoded = new Array(data.length); @@ -125,3 +150,4 @@ export function encodeNullValues(data) { return rootEncoded; } + diff --git a/packages/app/lib/internal/registry/app.js b/packages/app/lib/internal/registry/app.ts similarity index 76% rename from packages/app/lib/internal/registry/app.js rename to packages/app/lib/internal/registry/app.ts index fda5741665..97f4b4aa33 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'; -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, _deprecationArg?: any): FirebaseApp { warnIfNotModularCall(arguments, 'getApp()'); if (!initializedNativeApps) { initializeNativeApps(); @@ -102,7 +109,7 @@ export function getApp(name = DEFAULT_APP_NAME) { /** * Gets all app instances, used for `firebase.apps` */ -export function getApps() { +export function getApps(_deprecationArg?: any): FirebaseApp[] { warnIfNotModularCall(arguments, 'getApps()'); if (!initializedNativeApps) { initializeNativeApps(); @@ -115,13 +122,17 @@ export function getApps() { * @param options * @param configOrName */ -export function initializeApp(options = {}, configOrName) { +export function initializeApp( + options: Partial = {}, + configOrName?: string | FirebaseAppConfig, + _deprecationArg?: any, +): 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 +190,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; }) - .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 +220,10 @@ export function initializeApp(options = {}, configOrName) { }); } -export function setLogLevel(logLevel) { +export function setLogLevel( + logLevel: ReactNativeFirebase.LogLevelString, + _deprecationArg?: any, +): 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 +236,10 @@ export function setLogLevel(logLevel) { } } -export function setReactNativeAsyncStorage(asyncStorage) { +export function setReactNativeAsyncStorage( + asyncStorage: ReactNativeAsyncStorage, + _deprecationArg?: any, +): void { warnIfNotModularCall(arguments, 'setReactNativeAsyncStorage()'); if (!isObject(asyncStorage)) { @@ -236,13 +258,17 @@ 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, + _deprecationArg?: any, +): Promise { if (name === DEFAULT_APP_NAME && nativeInitialized) { return Promise.reject(new Error('Unable to delete the default native firebase app instance.')); } @@ -256,8 +282,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 55% rename from packages/app/lib/internal/registry/namespace.js rename to packages/app/lib/internal/registry/namespace.ts index 76e3078666..9a7e19bcd9 100644 --- a/packages/app/lib/internal/registry/namespace.js +++ b/packages/app/lib/internal/registry/namespace.ts @@ -17,9 +17,9 @@ import { isString, createDeprecationProxy } from '../../common'; import FirebaseApp from '../../FirebaseApp'; -import SDK_VERSION from '../../version'; -import { DEFAULT_APP_NAME, KNOWN_NAMESPACES } from '../constants'; -import FirebaseModule from '../FirebaseModule'; +import { version as SDK_VERSION } from '../../version'; +import { DEFAULT_APP_NAME, KNOWN_NAMESPACES, type KnownNamespace } from '../constants'; +import FirebaseModule, { type ModuleConfig } from '../FirebaseModule'; import { getApp, getApps, @@ -30,13 +30,48 @@ import { setOnAppDestroy, } from './app'; +/** + * Type for a Firebase module getter function that can optionally accept + * a custom URL/region/databaseId parameter + */ +export type ModuleGetter = ((customUrlOrRegionOrDatabaseId?: string) => FirebaseModule) & { + [key: string]: unknown; +}; + +/** + * Type for Firebase root object with module getters + */ +export interface FirebaseRoot { + initializeApp: typeof initializeApp; + setReactNativeAsyncStorage: typeof setReactNativeAsyncStorage; + app: typeof getApp; + apps: FirebaseApp[]; + SDK_VERSION: string; + setLogLevel: typeof setLogLevel; + [key: string]: unknown; +} + +interface NamespaceConfig extends ModuleConfig { + nativeModuleName: string | string[]; + nativeEvents: boolean | string[]; + disablePrependCustomUrlOrRegion?: boolean; + // ModuleClass can be FirebaseModule or any subclass of it + ModuleClass: new ( + app: FirebaseApp, + config: ModuleConfig, + customUrlOrRegion?: string | null, + ) => FirebaseModule; + statics?: object; + version?: string; +} + // 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 +81,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 +107,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 +134,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 +153,23 @@ 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), ); - 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 ModuleGetter; + return MODULE_GETTER_FOR_APP[app.name]![moduleNamespace]!; } /** @@ -133,15 +177,19 @@ 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)) { @@ -169,22 +217,26 @@ function getOrCreateModuleForRoot(moduleNamespace) { APP_MODULE_INSTANCE[_app.name] = {}; } - if (!APP_MODULE_INSTANCE[_app.name][moduleNamespace]) { - const module = createDeprecationProxy( - new ModuleClass(_app, NAMESPACE_REGISTRY[moduleNamespace]), - ); - APP_MODULE_INSTANCE[_app.name][moduleNamespace] = module; + 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, moduleConfig)); + 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 ModuleGetter; - return MODULE_GETTER_FOR_ROOT[moduleNamespace]; + return MODULE_GETTER_FOR_ROOT[moduleNamespace]!; } /** @@ -193,12 +245,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 +273,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,7 +298,7 @@ export function firebaseAppModuleProxy(app, moduleNamespace) { * * @returns {*} */ -export function createFirebaseRoot() { +export function createFirebaseRoot(): FirebaseRoot { FIREBASE_ROOT = { initializeApp, setReactNativeAsyncStorage, @@ -258,10 +314,12 @@ export function createFirebaseRoot() { 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(FIREBASE_ROOT, namespace, { + enumerable: false, + get: firebaseRootModuleProxy.bind(null, FIREBASE_ROOT, namespace), + }); + } } return FIREBASE_ROOT; @@ -271,7 +329,7 @@ export function createFirebaseRoot() { * * @returns {*} */ -export function getFirebaseRoot() { +export function getFirebaseRoot(): FirebaseRoot { if (FIREBASE_ROOT) { return FIREBASE_ROOT; } @@ -283,17 +341,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 67% rename from packages/app/lib/internal/registry/nativeModule.js rename to packages/app/lib/internal/registry/nativeModule.ts index 43611e46d0..97c6cf27b8 100644 --- a/packages/app/lib/internal/registry/nativeModule.js +++ b/packages/app/lib/internal/registry/nativeModule.ts @@ -15,17 +15,25 @@ * */ import { APP_NATIVE_MODULE } from '../constants'; -import NativeFirebaseError from '../NativeFirebaseError'; +import NativeFirebaseError, { type NativeError } from '../NativeFirebaseError'; 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 +46,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 +60,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 +75,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 native; } - 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 +117,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 +129,20 @@ function initialiseNativeModule(module) { disablePrependCustomUrlOrRegion, turboModule, } = config; + const multiModuleRoot: WrappedNativeModule = {}; const isTurboModule = !!turboModule; - const multiModuleRoot = {}; const multiModule = Array.isArray(nativeModuleName); - const nativeModuleNames = multiModule ? nativeModuleName : [nativeModuleName]; + const nativeModuleNames = multiModule + ? nativeModuleName + : 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 +151,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 +170,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 +195,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 +221,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 +246,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 +261,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 +281,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/modular.ts b/packages/app/lib/modular.ts new file mode 100644 index 0000000000..0acf3efe9e --- /dev/null +++ b/packages/app/lib/modular.ts @@ -0,0 +1,187 @@ +/* + * 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 } from './types'; +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'; + +export interface LogCallbackParams { + level: ReactNativeFirebase.LogLevelString; + message: string; + args: unknown[]; + type: string; +} + +export type LogCallback = (callbackParams: LogCallbackParams) => void; + +export interface LogOptions { + level?: ReactNativeFirebase.LogLevelString; +} + +/** + * 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, + 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, 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, 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, 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, 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, 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 82% rename from packages/app/lib/index.js rename to packages/app/lib/namespaced.ts index 17d3e2e495..83b0ae0181 100644 --- a/packages/app/lib/index.js +++ b/packages/app/lib/namespaced.ts @@ -16,9 +16,14 @@ */ import { getFirebaseRoot } from './internal/registry/namespace'; +import utils from './utils'; +import type { ReactNativeFirebase, Utils } from './types'; export const firebase = getFirebaseRoot(); export * from './modular'; -export { default as utils } from './utils'; +export { utils }; + +// Export types +export type { ReactNativeFirebase, Utils }; export default firebase; diff --git a/packages/app/lib/index.d.ts b/packages/app/lib/types/index.ts similarity index 92% rename from packages/app/lib/index.d.ts rename to packages/app/lib/types/index.ts index de97d89580..6e9fdbced9 100644 --- a/packages/app/lib/index.d.ts +++ b/packages/app/lib/types/index.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 { /** @@ -258,17 +249,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; + abstract app: FirebaseApp; } // eslint-disable-next-line @typescript-eslint/no-empty-object-type @@ -292,9 +283,10 @@ export namespace ReactNativeFirebase { } & S; } -/* +/** * @firebase utils */ +// eslint-disable-next-line @typescript-eslint/no-namespace export namespace Utils { import FirebaseModule = ReactNativeFirebase.FirebaseModule; @@ -421,6 +413,7 @@ export namespace Utils { } export interface Statics { + SDK_VERSION: string; FilePath: FilePath; } @@ -523,7 +516,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. * @@ -534,7 +527,7 @@ export namespace Utils { * ``` * @android Android only - iOS returns false */ - isRunningInTestLab: boolean; + abstract isRunningInTestLab: boolean; /** * Returns PlayServicesAvailability properties * @@ -546,7 +539,7 @@ export namespace Utils { * * @android Android only - iOS always returns { isAvailable: true, status: 0 } */ - playServicesAvailability: PlayServicesAvailability; + abstract playServicesAvailability: PlayServicesAvailability; /** * Returns PlayServicesAvailability properties @@ -559,7 +552,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 @@ -572,7 +565,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 * @@ -584,7 +577,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. * @@ -596,31 +589,6 @@ 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 * from './modular'; - -// Internal module type definitions for consumption by other packages -export namespace 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 const module: ReactNativeFirebase.Module; -export default module; diff --git a/packages/app/lib/utils/UtilsStatics.js b/packages/app/lib/utils/UtilsStatics.ts similarity index 68% rename from packages/app/lib/utils/UtilsStatics.js rename to packages/app/lib/utils/UtilsStatics.ts index addbd7f394..1b942186cf 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'; +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 72% rename from packages/app/lib/utils/index.js rename to packages/app/lib/utils/index.ts index 9f7befa923..78242987b9 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'; 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'; diff --git a/packages/app/package.json b/packages/app/package.json index c78429be03..49067e7722 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,93 @@ "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" + } + }, + "./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/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/*" + } + }, + "./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 +150,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 +180,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/vertexai/tsconfig.json b/packages/vertexai/tsconfig.json index f371c6edc9..59193c9ff4 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,7 @@ "strict": true, "baseUrl": ".", "paths": { - "@react-native-firebase/app": ["../app/lib"], + "@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/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/yarn.lock b/yarn.lock index ece20f20d0..a46ffdba47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5095,6 +5095,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: "*"