From 040f954bc7039bd40b1bce007f5f15d41a1ef23b Mon Sep 17 00:00:00 2001 From: akashdeep931 Date: Thu, 19 Feb 2026 10:43:27 +0000 Subject: [PATCH 01/37] refactor(remote-config): move to typescript --- ...oteConfigValue.js => RemoteConfigValue.ts} | 33 +- packages/remote-config/lib/index.ts | 5 + packages/remote-config/lib/modular.ts | 310 ++++++++++++++++++ packages/remote-config/lib/modular/index.d.ts | 265 --------------- packages/remote-config/lib/modular/index.js | 289 ---------------- .../lib/{index.js => namespaced.ts} | 244 +++++++++----- packages/remote-config/lib/polyfills.js | 32 -- packages/remote-config/lib/polyfills.ts | 19 ++ packages/remote-config/lib/statics.js | 12 - packages/remote-config/lib/statics.ts | 14 + packages/remote-config/lib/types/modular.ts | 73 +++++ .../lib/{index.d.ts => types/namespaced.ts} | 42 +-- ...android.js => RNFBConfigModule.android.ts} | 0 ...gModule.ios.js => RNFBConfigModule.ios.ts} | 0 .../remote-config/lib/web/RNFBConfigModule.js | 165 ---------- .../remote-config/lib/web/RNFBConfigModule.ts | 214 ++++++++++++ packages/remote-config/package.json | 45 ++- packages/remote-config/tsconfig.json | 17 + packages/remote-config/type-test.ts | 62 ++-- tests/local-tests/index.js | 2 + .../RemoteConfigService.ts | 63 ++++ .../firebaseRemoteConfigClient.ts | 30 ++ .../RemoteConfigService/index.ts | 9 + .../RemoteConfigService/types.ts | 28 ++ tests/local-tests/remote-config/index.tsx | 53 +++ tsconfig.json | 17 +- yarn.lock | 2 + 27 files changed, 1130 insertions(+), 915 deletions(-) rename packages/remote-config/lib/{RemoteConfigValue.js => RemoteConfigValue.ts} (58%) create mode 100644 packages/remote-config/lib/index.ts create mode 100644 packages/remote-config/lib/modular.ts delete mode 100644 packages/remote-config/lib/modular/index.d.ts delete mode 100644 packages/remote-config/lib/modular/index.js rename packages/remote-config/lib/{index.js => namespaced.ts} (57%) delete mode 100644 packages/remote-config/lib/polyfills.js create mode 100644 packages/remote-config/lib/polyfills.ts delete mode 100644 packages/remote-config/lib/statics.js create mode 100644 packages/remote-config/lib/statics.ts create mode 100644 packages/remote-config/lib/types/modular.ts rename packages/remote-config/lib/{index.d.ts => types/namespaced.ts} (93%) rename packages/remote-config/lib/web/{RNFBConfigModule.android.js => RNFBConfigModule.android.ts} (100%) rename packages/remote-config/lib/web/{RNFBConfigModule.ios.js => RNFBConfigModule.ios.ts} (100%) delete mode 100644 packages/remote-config/lib/web/RNFBConfigModule.js create mode 100644 packages/remote-config/lib/web/RNFBConfigModule.ts create mode 100644 packages/remote-config/tsconfig.json create mode 100644 tests/local-tests/remote-config/RemoteConfigService/RemoteConfigService.ts create mode 100644 tests/local-tests/remote-config/RemoteConfigService/firebaseRemoteConfigClient.ts create mode 100644 tests/local-tests/remote-config/RemoteConfigService/index.ts create mode 100644 tests/local-tests/remote-config/RemoteConfigService/types.ts create mode 100644 tests/local-tests/remote-config/index.tsx diff --git a/packages/remote-config/lib/RemoteConfigValue.js b/packages/remote-config/lib/RemoteConfigValue.ts similarity index 58% rename from packages/remote-config/lib/RemoteConfigValue.js rename to packages/remote-config/lib/RemoteConfigValue.ts index da0c94cb02..0a1b27e490 100644 --- a/packages/remote-config/lib/RemoteConfigValue.js +++ b/packages/remote-config/lib/RemoteConfigValue.ts @@ -1,51 +1,64 @@ -// as per firebase web sdk specification +import type { FirebaseRemoteConfigTypes } from './types/namespaced'; + const BOOL_VALUES = ['1', 'true', 't', 'yes', 'y', 'on']; -export default class ConfigValue { - constructor({ value, source }) { +type ValueSource = 'remote' | 'default' | 'static'; + +interface ConfigValueParams { + value: string; + source: ValueSource; +} + +export default class ConfigValue implements FirebaseRemoteConfigTypes.ConfigValue { + _value: string; + _source: ValueSource; + + constructor({ value, source }: ConfigValueParams) { this._value = value; this._source = source; } - get value() { + get value(): undefined { // eslint-disable-next-line no-console console.warn( 'firebase.remoteConfig().getValue(*).value has been removed. Please use one of the alternative methods such as firebase.remoteConfig().getValue(*).asString()', ); + return undefined; } - get source() { + get source(): undefined { // eslint-disable-next-line no-console console.warn( 'firebase.remoteConfig().getValue(*).source has been removed. Please use firebase.remoteConfig().getValue(*).getSource()', ); + return undefined; } - asBoolean() { + public asBoolean(): boolean { if (this._source === 'static') { return false; } return BOOL_VALUES.includes(this._value.toLowerCase()); } - asNumber() { + public asNumber(): number { if (this._source === 'static') { return 0; } let num = Number(this._value); - if (isNaN(num)) { + if (Number.isNaN(num)) { num = 0; } return num; } - asString() { + public asString(): string { return this._value; } - getSource() { + public getSource(): ValueSource { return this._source; } } diff --git a/packages/remote-config/lib/index.ts b/packages/remote-config/lib/index.ts new file mode 100644 index 0000000000..f6fa537387 --- /dev/null +++ b/packages/remote-config/lib/index.ts @@ -0,0 +1,5 @@ +import './polyfills'; + +export type { FirebaseRemoteConfigTypes } from './types/namespaced'; +export * from './modular'; +export { default, firebase, SDK_VERSION } from './namespaced'; diff --git a/packages/remote-config/lib/modular.ts b/packages/remote-config/lib/modular.ts new file mode 100644 index 0000000000..480f7f595e --- /dev/null +++ b/packages/remote-config/lib/modular.ts @@ -0,0 +1,310 @@ +import { getApp } from '@react-native-firebase/app'; +import type { ReactNativeFirebase } from '@react-native-firebase/app'; +import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common'; +import type { + CallbackOrObserver, + ConfigDefaults, + ConfigSettings, + ConfigUpdateObserver, + ConfigValue, + ConfigValues, + CustomSignals, + LastFetchStatusType, + OnConfigUpdatedListenerCallback, + RemoteConfig, + RemoteConfigLogLevel, + RemoteConfigWithDeprecationArg, + Unsubscribe, +} from './types/modular'; + +export type { CustomSignals }; + +/** + * Returns a RemoteConfig instance for the given app. + * @param app - FirebaseApp. Optional. + * @returns RemoteConfig instance + */ +export function getRemoteConfig(app?: ReactNativeFirebase.FirebaseApp): RemoteConfig { + if (app) { + return getApp(app.name).remoteConfig(); + } + return getApp().remoteConfig(); +} + +function rc(remoteConfig: RemoteConfig): RemoteConfigWithDeprecationArg { + return remoteConfig as RemoteConfigWithDeprecationArg; +} + +/** + * Returns a Boolean which resolves to true if the current call activated the fetched configs. + * @param remoteConfig - RemoteConfig instance + * @returns Promise + */ +export function activate(remoteConfig: RemoteConfig): Promise { + return rc(remoteConfig).activate.call(remoteConfig, MODULAR_DEPRECATION_ARG); +} + +/** + * Ensures the last activated config are available to the getters. + * @param remoteConfig - RemoteConfig instance + * @returns Promise + */ +export function ensureInitialized(remoteConfig: RemoteConfig): Promise { + return rc(remoteConfig).ensureInitialized.call(remoteConfig, MODULAR_DEPRECATION_ARG); +} + +/** + * Performs a fetch and returns a Boolean which resolves to true if the current call activated the fetched configs. + * @param remoteConfig - RemoteConfig instance + * @returns Promise + */ +export function fetchAndActivate(remoteConfig: RemoteConfig): Promise { + return rc(remoteConfig).fetchAndActivate.call(remoteConfig, MODULAR_DEPRECATION_ARG); +} + +/** + * Fetches and caches configuration from the Remote Config service. + * @param remoteConfig - RemoteConfig instance + * @returns Promise + */ +export function fetchConfig(remoteConfig: RemoteConfig): Promise { + const r = rc(remoteConfig); + if (r.fetchConfig) { + return r.fetchConfig.call(remoteConfig, MODULAR_DEPRECATION_ARG); + } + return Promise.resolve(); +} + +/** + * Gets all config. + * @param remoteConfig - RemoteConfig instance + * @returns ConfigValues + */ +export function getAll(remoteConfig: RemoteConfig): ConfigValues { + return rc(remoteConfig).getAll.call(remoteConfig, MODULAR_DEPRECATION_ARG); +} + +/** + * Gets the value for the given key as a boolean. + * @param remoteConfig - RemoteConfig instance + * @param key - key for boolean value + * @returns boolean + */ +export function getBoolean(remoteConfig: RemoteConfig, key: string): boolean { + return rc(remoteConfig).getBoolean.call(remoteConfig, key, MODULAR_DEPRECATION_ARG); +} + +/** + * Gets the value for the given key as a number. + * @param remoteConfig - RemoteConfig instance + * @param key - key for number value + * @returns number + */ +export function getNumber(remoteConfig: RemoteConfig, key: string): number { + return rc(remoteConfig).getNumber.call(remoteConfig, key, MODULAR_DEPRECATION_ARG); +} + +/** + * Gets the value for the given key as a string. + * @param remoteConfig - RemoteConfig instance + * @param key - key for string value + * @returns string + */ +export function getString(remoteConfig: RemoteConfig, key: string): string { + return rc(remoteConfig).getString.call(remoteConfig, key, MODULAR_DEPRECATION_ARG); +} + +/** + * Gets the value for the given key. + * @param remoteConfig - RemoteConfig instance + * @param key - key for the given value + * @returns ConfigValue + */ +export function getValue(remoteConfig: RemoteConfig, key: string): ConfigValue { + return rc(remoteConfig).getValue.call(remoteConfig, key, MODULAR_DEPRECATION_ARG); +} + +/** + * Defines the log level to use. + * @param _remoteConfig - RemoteConfig instance + * @param _logLevel - The log level to set + * @returns RemoteConfigLogLevel + */ +export function setLogLevel( + _remoteConfig: RemoteConfig, + _logLevel: RemoteConfigLogLevel, +): RemoteConfigLogLevel { + // always return the "error" log level for now as the setter is ignored on native. Web only. + return 'error'; +} + +/** + * Checks two different things. + * 1. Check if IndexedDB exists in the browser environment. + * 2. Check if the current browser context allows IndexedDB open() calls. + * @returns Promise + */ +export function isSupported(): Promise { + // always return "true" for now. Web only. + return Promise.resolve(true); +} + +/** + * Indicates the default value in milliseconds to abandon a pending fetch request made to the Remote Config server. Defaults to 60000 (One minute). + * @param remoteConfig - RemoteConfig instance + * @returns number + */ +export function fetchTimeMillis(remoteConfig: RemoteConfig): number { + return remoteConfig.fetchTimeMillis; +} + +/** + * Returns a ConfigSettings object which provides the properties `minimumFetchIntervalMillis` & `fetchTimeMillis` if they have been set + * using setConfigSettings({ fetchTimeMillis: number, minimumFetchIntervalMillis: number }). + * @param remoteConfig - RemoteConfig instance + * @returns ConfigSettings + */ +export function settings(remoteConfig: RemoteConfig): ConfigSettings { + return remoteConfig.settings; +} + +/** + * The status of the latest Remote Config fetch action. + * @param remoteConfig - RemoteConfig instance + * @returns LastFetchStatusType + */ +export function lastFetchStatus(remoteConfig: RemoteConfig): LastFetchStatusType { + return remoteConfig.lastFetchStatus; +} + +/** + * Deletes all activated, fetched and defaults configs and resets all Firebase Remote Config settings. + * Android only. iOS does not reset anything. + * @param remoteConfig - RemoteConfig instance + * @returns Promise + */ +export function reset(remoteConfig: RemoteConfig): Promise { + return rc(remoteConfig).reset.call(remoteConfig, MODULAR_DEPRECATION_ARG); +} + +/** + * Set the Remote Config settings, currently able to set `fetchTimeMillis` & `minimumFetchIntervalMillis`. + * @param remoteConfig - RemoteConfig instance + * @param settings - ConfigSettings instance + * @returns Promise + */ +export function setConfigSettings( + remoteConfig: RemoteConfig, + settings: ConfigSettings, +): Promise { + return rc(remoteConfig).setConfigSettings.call(remoteConfig, settings, MODULAR_DEPRECATION_ARG); +} + +/** + * Fetches parameter values for your app. + * @param remoteConfig - RemoteConfig instance + * @param expirationDurationSeconds - number + * @returns Promise + */ +export function fetch( + remoteConfig: RemoteConfig, + expirationDurationSeconds?: number, +): Promise { + return rc(remoteConfig).fetch.call( + remoteConfig, + expirationDurationSeconds, + MODULAR_DEPRECATION_ARG, + ); +} + +/** + * Fetches parameter values for your app (setDefaults). + * @param remoteConfig - RemoteConfig instance + * @param defaults - ConfigDefaults + * @returns Promise + */ +export function setDefaults(remoteConfig: RemoteConfig, defaults: ConfigDefaults): Promise { + return rc(remoteConfig).setDefaults.call(remoteConfig, defaults, MODULAR_DEPRECATION_ARG); +} + +/** + * Fetches parameter values for your app (setDefaultsFromResource). + * @param remoteConfig - RemoteConfig instance + * @param resourceName - string + * @returns Promise + */ +export function setDefaultsFromResource( + remoteConfig: RemoteConfig, + resourceName: string, +): Promise { + return rc(remoteConfig).setDefaultsFromResource.call( + remoteConfig, + resourceName, + MODULAR_DEPRECATION_ARG, + ); +} + +/** + * Registers a listener to changes in the configuration. + * @param remoteConfig - RemoteConfig instance + * @param observer - to be notified of config updates + * @returns Unsubscribe function to remove the listener + * @deprecated use official firebase-js-sdk onConfigUpdate now that web supports realtime + */ +export function onConfigUpdate( + remoteConfig: RemoteConfig, + observer: ConfigUpdateObserver, +): Unsubscribe { + return rc(remoteConfig).onConfigUpdate.call(remoteConfig, observer, MODULAR_DEPRECATION_ARG); +} + +/** + * Registers a listener to changes in the configuration. + * @param remoteConfig - RemoteConfig instance + * @param callback - function called on config change + * @returns unsubscribe listener + * @deprecated use official firebase-js-sdk onConfigUpdate now that web supports realtime + */ +export function onConfigUpdated( + remoteConfig: RemoteConfig, + callback: CallbackOrObserver, +): () => void { + return rc(remoteConfig).onConfigUpdated.call(remoteConfig, callback, MODULAR_DEPRECATION_ARG); +} + +/** + * Sets the custom signals for the app instance. + * @param remoteConfig - RemoteConfig instance + * @param customSignals - CustomSignals + * @returns Promise + */ +export async function setCustomSignals( + remoteConfig: RemoteConfig, + customSignals: CustomSignals, +): Promise { + for (const [key, value] of Object.entries(customSignals)) { + if (typeof value !== 'string' && typeof value !== 'number' && value !== null) { + throw new Error( + `firebase.remoteConfig().setCustomSignals(): Invalid type for custom signal '${key}': ${typeof value}. Expected 'string', 'number', or 'null'.`, + ); + } + } + + const rcInstance = remoteConfig as unknown as { + _promiseWithConstants: ( + p: Promise<{ result: unknown; constants: unknown }>, + ) => Promise; + native: { setCustomSignals: (s: CustomSignals) => Promise }; + }; + + await rcInstance._promiseWithConstants( + rcInstance.native.setCustomSignals(customSignals) as Promise<{ + result: unknown; + constants: unknown; + }>, + ); + + return null; +} + +export { LastFetchStatus, ValueSource } from './statics'; diff --git a/packages/remote-config/lib/modular/index.d.ts b/packages/remote-config/lib/modular/index.d.ts deleted file mode 100644 index 3d56046d39..0000000000 --- a/packages/remote-config/lib/modular/index.d.ts +++ /dev/null @@ -1,265 +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 { ReactNativeFirebase } from '@react-native-firebase/app'; -import { FirebaseRemoteConfigTypes } from '..'; - -import RemoteConfig = FirebaseRemoteConfigTypes.Module; -import ConfigValues = FirebaseRemoteConfigTypes.ConfigValues; -import ConfigValue = FirebaseRemoteConfigTypes.ConfigValue; -import ConfigDefaults = FirebaseRemoteConfigTypes.ConfigDefaults; -import ConfigSettings = FirebaseRemoteConfigTypes.ConfigSettings; -import LastFetchStatusType = FirebaseRemoteConfigTypes.LastFetchStatusType; -import RemoteConfigLogLevel = FirebaseRemoteConfigTypes.RemoteConfigLogLevel; -import FirebaseApp = ReactNativeFirebase.FirebaseApp; -import LastFetchStatusInterface = FirebaseRemoteConfigTypes.LastFetchStatus; -import ValueSourceInterface = FirebaseRemoteConfigTypes.ValueSource; -import ConfigUpdateObserver = FirebaseRemoteConfigTypes.ConfigUpdateObserver; -import Unsubscribe = FirebaseRemoteConfigTypes.Unsubscribe; -// deprecated: from pre-Web realtime remote-config support - remove with onConfigUpdated -import CallbackOrObserver = FirebaseRemoteConfigTypes.CallbackOrObserver; -// deprecated: from pre-Web realtime remote-config support - remove with onConfigUpdated -import OnConfigUpdatedListenerCallback = FirebaseRemoteConfigTypes.OnConfigUpdatedListenerCallback; - -export const LastFetchStatus: LastFetchStatusInterface; -export const ValueSource: ValueSourceInterface; - -/** - * Returns a RemoteConfig instance for the given app. - * @param app - FirebaseApp. Optional. - * @returns {RemoteConfig} - */ -export function getRemoteConfig(app?: FirebaseApp): RemoteConfig; - -/** - * Returns a Boolean which resolves to true if the current call - * activated the fetched configs. - * @param remoteConfig - RemoteConfig instance - * @returns {Promise} - */ -export function activate(remoteConfig: RemoteConfig): Promise; - -/** - * Ensures the last activated config are available to the getters. - * @param remoteConfig - RemoteConfig instance - * @returns {Promise} - */ -export function ensureInitialized(remoteConfig: RemoteConfig): Promise; - -/** - * Performs a fetch and returns a Boolean which resolves to true - * if the current call activated the fetched configs. - * @param remoteConfig - RemoteConfig instance - * @returns {Promise} - */ -export function fetchAndActivate(remoteConfig: RemoteConfig): Promise; - -/** - * Fetches and caches configuration from the Remote Config service. - * @param remoteConfig - RemoteConfig instance - * @returns {Promise} - */ -export function fetchConfig(remoteConfig: RemoteConfig): Promise; - -/** - * Gets all config. - * @param remoteConfig - RemoteConfig instance - * @returns {ConfigValues} - */ -export function getAll(remoteConfig: RemoteConfig): ConfigValues; - -/** - * Gets the value for the given key as a boolean. - * @param remoteConfig - RemoteConfig instance - * @param key - key for boolean value - * @returns {boolean} - */ -export function getBoolean(remoteConfig: RemoteConfig, key: string): boolean; - -/** - * Gets the value for the given key as a number. - * @param remoteConfig - RemoteConfig instance - * @param key - key for number value - * @returns {number} - */ -export function getNumber(remoteConfig: RemoteConfig, key: string): number; - -/** - * Gets the value for the given key as a string. - * @param remoteConfig - RemoteConfig instance - * @param key - key for string value - * @returns {string} - */ -export function getString(remoteConfig: RemoteConfig, key: string): string; - -/** - * Gets the value for the given key - * @param remoteConfig - RemoteConfig instance - * @param key - key for the given value - * @returns {ConfigValue} - */ -export function getValue(remoteConfig: RemoteConfig, key: string): ConfigValue; - -/** - * Defines the log level to use. - * @param remoteConfig - RemoteConfig instance - * @param logLevel - The log level to set - * @returns {RemoteConfigLogLevel} - */ -export function setLogLevel( - remoteConfig: RemoteConfig, - logLevel: RemoteConfigLogLevel, -): RemoteConfigLogLevel; - -/** - * Checks two different things. - * 1. Check if IndexedDB exists in the browser environment. - * 2. Check if the current browser context allows IndexedDB open() calls. - * @returns {Promise} - */ -export function isSupported(): Promise; - -/** - * Indicates the default value in milliseconds to abandon a pending fetch - * request made to the Remote Config server. Defaults to 60000 (One minute). - * @param remoteConfig - RemoteConfig instance - * @returns {number} - */ -export function fetchTimeMillis(remoteConfig: RemoteConfig): number; - -/** - * Returns a ConfigSettings object which provides the properties `minimumFetchIntervalMillis` & `fetchTimeMillis` if they have been set - * using setConfigSettings({ fetchTimeMillis: number, minimumFetchIntervalMillis: number }). - * @param remoteConfig - RemoteConfig instance - * @returns {ConfigSettings} - */ -export function settings(remoteConfig: RemoteConfig): ConfigSettings; - -/** - * The status of the latest Remote RemoteConfig fetch action. - * @param remoteConfig - RemoteConfig instance - * @returns {LastFetchStatusType} - */ -export function lastFetchStatus(remoteConfig: RemoteConfig): LastFetchStatusType; - -/** - * Deletes all activated, fetched and defaults configs and - * resets all Firebase Remote Config settings. - * Android only. iOS does not reset anything. - * @param remoteConfig - RemoteConfig instance - * @returns {Promise} - */ -export function reset(remoteConfig: RemoteConfig): Promise; - -/** - * Set the Remote RemoteConfig settings, currently able to set - * `fetchTimeMillis` & `minimumFetchIntervalMillis` - * @param remoteConfig - RemoteConfig instance - * @param settings - ConfigSettings instance - * @returns {Promise} - */ -export function setConfigSettings( - remoteConfig: RemoteConfig, - settings: ConfigSettings, -): Promise; - -/** - * Fetches parameter values for your app. - * @param remoteConfig - RemoteConfig instance - * @param expirationDurationSeconds - number - * @returns {Promise} - */ -export function fetch( - remoteConfig: RemoteConfig, - expirationDurationSeconds?: number, -): Promise; - -/** - * Fetches parameter values for your app. - * @param remoteConfig - RemoteConfig instance - * @param defaults - ConfigDefaults - * @returns {Promise} - */ -export function setDefaults(remoteConfig: RemoteConfig, defaults: ConfigDefaults): Promise; - -/** - * Fetches parameter values for your app. - * @param remoteConfig - RemoteConfig instance - * @param resourceName - string - * @returns {Promise} - */ -export function setDefaultsFromResource( - remoteConfig: RemoteConfig, - resourceName: string, -): Promise; - -/** - * Starts listening for real-time config updates from the Remote Config backend and automatically - * fetches updates from the Remote Config backend when they are available. - * - * @remarks - * If a connection to the Remote Config backend is not already open, calling this method will - * open it. Multiple listeners can be added by calling this method again, but subsequent calls - * re-use the same connection to the backend. - * - * The list of updated keys passed to the callback will include all keys not currently active, - * and the config update process fetches the new config but does not automatically activate - * it for you. Typically you will activate the config in your callback to use the new values. - * - * @param remoteConfig - The {@link RemoteConfig} instance. - * @param observer - The {@link ConfigUpdateObserver} to be notified of config updates. - * @returns An {@link Unsubscribe} function to remove the listener. - */ -export function onConfigUpdate( - remoteConfig: RemoteConfig, - observer: ConfigUpdateObserver, -): Unsubscribe; - -/** - * Registers a listener to changes in the configuration. - * - * @param remoteConfig - RemoteConfig instance - * @param callback - function called on config change - * @returns {function} unsubscribe listener - * @deprecated use official firebase-js-sdk onConfigUpdate now that web supports realtime - */ -export function onConfigUpdated( - remoteConfig: RemoteConfig, - callback: CallbackOrObserver, -): () => void; - -/** - * Defines the type for representing custom signals and their values. - * The values in CustomSignals must be one of the following types: string, number, or null. - * There are additional limitations on key and value length, for a full description see https://firebase.google.com/docs/remote-config/parameters?template_type=client#custom_signal_conditions - * Failing to stay within these limitations will result in a silent API failure with only a warning in device logs - */ - -export interface CustomSignals { - [key: string]: string | number | null; -} - -/** - * Sets the custom signals for the app instance. - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @param {CustomSignals} customSignals - CustomSignals - * @returns {Promise} - */ - -export declare function setCustomSignals( - remoteConfig: RemoteConfig, - customSignals: CustomSignals, -): Promise; diff --git a/packages/remote-config/lib/modular/index.js b/packages/remote-config/lib/modular/index.js deleted file mode 100644 index 2e7840f5ea..0000000000 --- a/packages/remote-config/lib/modular/index.js +++ /dev/null @@ -1,289 +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 { getApp } from '@react-native-firebase/app'; - -import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common'; - -/** - * @typedef {import('@firebase/app').FirebaseApp} FirebaseApp - * @typedef {import('..').FirebaseRemoteConfigTypes.Module} RemoteConfig - * @typedef {import('..').FirebaseRemoteConfigTypes.ConfigDefaults} ConfigDefaults - * @typedef {import('..').FirebaseRemoteConfigTypes.ConfigSettings} ConfigSettings - * @typedef {import('..').FirebaseRemoteConfigTypes.ConfigValue} ConfigValue - * @typedef {import('..').FirebaseRemoteConfigTypes.ConfigValues} ConfigValues - * @typedef {import('..').FirebaseRemoteConfigTypes.LastFetchStatusType} LastFetchStatusType - * @typedef {import('..').FirebaseRemoteConfigTypes.RemoteConfigLogLevel} RemoteConfigLogLevel - * @typedef {import('..').FirebaseRemoteConfigTypes.ConfigUpdateObserver} ConfigUpdateObserver - * @typedef {import('..').FirebaseRemoteConfigTypes.Unsubscribe} Unsubscribe - * @typedef {import('.').CustomSignals} CustomSignals - */ - -/** - * Returns a RemoteConfig instance for the given app. - * @param {FirebaseApp} [app] - FirebaseApp. Optional. - * @returns {RemoteConfig} - */ -export function getRemoteConfig(app) { - if (app) { - return getApp(app.name).remoteConfig(); - } - - return getApp().remoteConfig(); -} - -/** - * Returns a Boolean which resolves to true if the current call - * activated the fetched configs. - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @returns {Promise} - */ -export function activate(remoteConfig) { - return remoteConfig.activate.call(remoteConfig, MODULAR_DEPRECATION_ARG); -} - -/** - * Ensures the last activated config are available to the getters. - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @returns {Promise} - */ -export function ensureInitialized(remoteConfig) { - return remoteConfig.ensureInitialized.call(remoteConfig, MODULAR_DEPRECATION_ARG); -} - -/** - * Performs a fetch and returns a Boolean which resolves to true - * if the current call activated the fetched configs. - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @returns {Promise} - */ -export function fetchAndActivate(remoteConfig) { - return remoteConfig.fetchAndActivate.call(remoteConfig, MODULAR_DEPRECATION_ARG); -} - -/** - * Fetches and caches configuration from the Remote Config service. - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @returns {Promise} - */ -export function fetchConfig(remoteConfig) { - return remoteConfig.fetchConfig.call(remoteConfig, MODULAR_DEPRECATION_ARG); -} - -/** - * Gets all config. - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @returns {ConfigValues} - */ -export function getAll(remoteConfig) { - return remoteConfig.getAll.call(remoteConfig, MODULAR_DEPRECATION_ARG); -} - -/** - * Gets the value for the given key as a boolean. - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @param {string} key - key for boolean value - * @returns {boolean} - */ -export function getBoolean(remoteConfig, key) { - return remoteConfig.getBoolean.call(remoteConfig, key, MODULAR_DEPRECATION_ARG); -} - -/** - * Gets the value for the given key as a number. - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @param {string} key - key for number value - * @returns {number} - */ -export function getNumber(remoteConfig, key) { - return remoteConfig.getNumber.call(remoteConfig, key, MODULAR_DEPRECATION_ARG); -} - -/** - * Gets the value for the given key as a string. - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @param {string} key - key for string value - * @returns {string} - */ -export function getString(remoteConfig, key) { - return remoteConfig.getString.call(remoteConfig, key, MODULAR_DEPRECATION_ARG); -} - -/** - * Gets the value for the given key - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @param {string} key - key for the given value - * @returns {ConfigValue} - */ -export function getValue(remoteConfig, key) { - return remoteConfig.getValue.call(remoteConfig, key, MODULAR_DEPRECATION_ARG); -} - -/** - * Defines the log level to use. - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @param {RemoteConfigLogLevel} logLevel - The log level to set - * @returns {RemoteConfigLogLevel} - */ -// eslint-disable-next-line -export function setLogLevel(remoteConfig, logLevel) { - // always return the "error" log level for now as the setter is ignored on native. Web only. - return 'error'; -} - -/** - * Checks two different things. - * 1. Check if IndexedDB exists in the browser environment. - * 2. Check if the current browser context allows IndexedDB open() calls. - * @returns {Promise} - */ -export function isSupported() { - // always return "true" for now. Web only. - return Promise.resolve(true); -} - -/** - * Indicates the default value in milliseconds to abandon a pending fetch - * request made to the Remote Config server. Defaults to 60000 (One minute). - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @returns {number} - */ -export function fetchTimeMillis(remoteConfig) { - return remoteConfig.fetchTimeMillis.call(remoteConfig, MODULAR_DEPRECATION_ARG); -} - -/** - * Returns a ConfigSettings object which provides the properties `minimumFetchIntervalMillis` & `fetchTimeMillis` if they have been set - * using setConfigSettings({ fetchTimeMillis: number, minimumFetchIntervalMillis: number }). - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @returns {ConfigSettings} - */ -export function settings(remoteConfig) { - return remoteConfig.settings.call(remoteConfig, MODULAR_DEPRECATION_ARG); -} - -/** - * The status of the latest Remote RemoteConfig fetch action. - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @returns {LastFetchStatusType} - */ -export function lastFetchStatus(remoteConfig) { - return remoteConfig.lastFetchStatus.call(remoteConfig, MODULAR_DEPRECATION_ARG); -} - -/** - * Deletes all activated, fetched and defaults configs and - * resets all Firebase Remote Config settings. - * Android only. iOS does not reset anything. - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @returns {Promise} - */ -export function reset(remoteConfig) { - return remoteConfig.reset.call(remoteConfig, MODULAR_DEPRECATION_ARG); -} - -/** - * Set the Remote RemoteConfig settings, currently able to set - * `fetchTimeMillis` & `minimumFetchIntervalMillis` - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @param {ConfigSettings} settings - ConfigSettings instance - * @returns {Promise} - */ -export function setConfigSettings(remoteConfig, settings) { - return remoteConfig.setConfigSettings.call(remoteConfig, settings, MODULAR_DEPRECATION_ARG); -} - -/** - * Fetches parameter values for your app. - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @param {number} expirationDurationSeconds - number - * @returns {Promise} - */ -export function fetch(remoteConfig, expirationDurationSeconds) { - return remoteConfig.fetch.call(remoteConfig, expirationDurationSeconds, MODULAR_DEPRECATION_ARG); -} - -/** - * Fetches parameter values for your app. - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @param {ConfigDefaults} defaults - ConfigDefaults - * @returns {Promise} - */ -export function setDefaults(remoteConfig, defaults) { - return remoteConfig.setDefaults.call(remoteConfig, defaults, MODULAR_DEPRECATION_ARG); -} - -/** - * Fetches parameter values for your app. - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @param {string} resourceName - string - * @returns {Promise} - */ -export function setDefaultsFromResource(remoteConfig, resourceName) { - return remoteConfig.setDefaultsFromResource.call( - remoteConfig, - resourceName, - MODULAR_DEPRECATION_ARG, - ); -} - -/** - * Registers a listener to changes in the configuration. - * - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @param {ConfigUpdateObserver} observer - to be notified of config updates. - * @returns {Unsubscribe} function to remove the listener. - * @deprecated use official firebase-js-sdk onConfigUpdate now that web supports realtime - */ -export function onConfigUpdate(remoteConfig, observer) { - return remoteConfig.onConfigUpdate.call(remoteConfig, observer, MODULAR_DEPRECATION_ARG); -} - -/** - * Registers a listener to changes in the configuration. - * - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @param {CallbackOrObserver} callback - function called on config change - * @returns {function} unsubscribe listener - * @deprecated use official firebase-js-sdk onConfigUpdate now that web supports realtime - */ -export function onConfigUpdated(remoteConfig, callback) { - return remoteConfig.onConfigUpdated.call(remoteConfig, callback, MODULAR_DEPRECATION_ARG); -} - -/** - * Sets the custom signals for the app instance. - * @param {RemoteConfig} remoteConfig - RemoteConfig instance - * @param {CustomSignals} customSignals - CustomSignals - * @returns {Promise} - */ -export async function setCustomSignals(remoteConfig, customSignals) { - for (const [key, value] of Object.entries(customSignals)) { - if (typeof value !== 'string' && typeof value !== 'number' && value !== null) { - throw new Error( - `firebase.remoteConfig().setCustomSignals(): Invalid type for custom signal '${key}': ${typeof value}. Expected 'string', 'number', or 'null'.`, - ); - } - } - return remoteConfig._promiseWithConstants.call( - remoteConfig, - remoteConfig.native.setCustomSignals(customSignals), - MODULAR_DEPRECATION_ARG, - ); -} - -export { LastFetchStatus, ValueSource } from '../statics'; diff --git a/packages/remote-config/lib/index.js b/packages/remote-config/lib/namespaced.ts similarity index 57% rename from packages/remote-config/lib/index.js rename to packages/remote-config/lib/namespaced.ts index 9364f77334..ef13323502 100644 --- a/packages/remote-config/lib/index.js +++ b/packages/remote-config/lib/namespaced.ts @@ -32,9 +32,21 @@ import { getFirebaseRoot, } from '@react-native-firebase/app/dist/module/internal'; import { setReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; -import fallBackModule from './web/RNFBConfigModule'; -import version from './version'; +// Web fallback module; type inferred from implementation (RNFBConfigModule.ts) +const fallBackModule = require('./web/RNFBConfigModule') as { + activate: (appName: string) => Promise; + setConfigSettings: (appName: string, settings: unknown) => Promise; + fetch: (appName: string, expirationDurationSeconds: number) => Promise; + fetchAndActivate: (appName: string) => Promise; + ensureInitialized: (appName: string) => Promise; + setDefaults: (appName: string, defaults: unknown) => Promise; + setCustomSignals: (appName: string, customSignals: unknown) => Promise; + onConfigUpdated: (appName: string) => void; + removeConfigUpdateRegistration: (appName: string) => void; +}; +import { version } from './version'; import { LastFetchStatus, ValueSource } from './statics'; +import type { FirebaseRemoteConfigTypes } from './types/namespaced'; const statics = { LastFetchStatus, @@ -44,8 +56,27 @@ const statics = { const namespace = 'remoteConfig'; const nativeModuleName = 'RNFBConfigModule'; +interface ConstantsUpdate { + lastFetchTime?: number; + lastFetchStatus?: string; + fetchTimeout: number; + minimumFetchInterval: number; + values: Record; +} + +interface NativeConstantsResult { + result: unknown; + constants: ConstantsUpdate; +} + class FirebaseConfigModule extends FirebaseModule { - constructor(...args) { + _settings: FirebaseRemoteConfigTypes.ConfigSettings; + _lastFetchTime: number; + _values: Record; + _lastFetchStatus!: FirebaseRemoteConfigTypes.LastFetchStatusType; + _configUpdateListenerCount: number; + + constructor(...args: ConstructorParameters) { super(...args); this._settings = { // defaults to 1 minute. @@ -58,17 +89,17 @@ class FirebaseConfigModule extends FirebaseModule { this._configUpdateListenerCount = 0; } - get defaultConfig() { - const updatedDefaultConfig = {}; + get defaultConfig(): FirebaseRemoteConfigTypes.ConfigDefaults { + const updatedDefaultConfig: FirebaseRemoteConfigTypes.ConfigDefaults = {}; Object.keys(this._values).forEach(key => { + const entry = this._values[key]; // Need to make it an object with key and literal value. Not `Value` instance. - updatedDefaultConfig[key] = this._values[key].value; + if (entry) updatedDefaultConfig[key] = entry.value; }); - return updatedDefaultConfig; } - set defaultConfig(defaults) { + set defaultConfig(defaults: FirebaseRemoteConfigTypes.ConfigDefaults) { if (!isObject(defaults)) { throw new Error("firebase.remoteConfig().defaultConfig: 'defaults' must be an object."); } @@ -76,23 +107,33 @@ class FirebaseConfigModule extends FirebaseModule { // updates defaults on the instance. We then pass to underlying SDK to update. We do this because // there is no way to "await" a setter. this._updateFromConstants(defaults); - this.setDefaults.call(this, defaults, true); + ( + this.setDefaults as ( + d: FirebaseRemoteConfigTypes.ConfigDefaults, + internal?: boolean, + ) => Promise + ).call(this, defaults, true); } - get settings() { + get settings(): FirebaseRemoteConfigTypes.ConfigSettings { return this._settings; } - set settings(settings) { + set settings(settings: FirebaseRemoteConfigTypes.ConfigSettings) { // To make Firebase web v9 API compatible, we update the settings first so it immediately // updates settings on the instance. We then pass to underlying SDK to update. We do this because // there is no way to "await" a setter. We can't delegate to `setConfigSettings()` as it is setup // for native. this._updateFromConstants(settings); - this.setConfigSettings.call(this, settings, true); + ( + this.setConfigSettings as ( + s: FirebaseRemoteConfigTypes.ConfigSettings, + internal?: boolean, + ) => Promise + ).call(this, settings, true); } - getValue(key) { + getValue(key: string): FirebaseRemoteConfigTypes.ConfigValue { if (!isString(key)) { throw new Error("firebase.remoteConfig().getValue(): 'key' must be a string value."); } @@ -104,33 +145,39 @@ class FirebaseConfigModule extends FirebaseModule { }); } - return new Value({ value: `${this._values[key].value}`, source: this._values[key].source }); + const entry = this._values[key]; + return new Value({ + value: `${entry?.value ?? ''}`, + source: (entry?.source ?? 'static') as 'remote' | 'default' | 'static', + }); } - getBoolean(key) { + getBoolean(key: string): boolean { return this.getValue(key).asBoolean(); } - getNumber(key) { + getNumber(key: string): number { return this.getValue(key).asNumber(); } - getString(key) { + getString(key: string): string { return this.getValue(key).asString(); } - getAll() { - const values = {}; - Object.keys(this._values).forEach(key => (values[key] = this.getValue(key))); + getAll(): FirebaseRemoteConfigTypes.ConfigValues { + const values: FirebaseRemoteConfigTypes.ConfigValues = {}; + Object.keys(this._values).forEach(key => { + values[key] = this.getValue(key); + }); return values; } - get fetchTimeMillis() { + get fetchTimeMillis(): number { // android returns -1 if no fetch yet and iOS returns 0 return this._lastFetchTime; } - get lastFetchStatus() { + get lastFetchStatus(): FirebaseRemoteConfigTypes.LastFetchStatusType { return this._lastFetchStatus; } @@ -138,21 +185,24 @@ class FirebaseConfigModule extends FirebaseModule { * Deletes all activated, fetched and defaults configs and resets all Firebase Remote Config settings. * @returns {Promise} */ - reset() { + reset(): Promise { if (isIOS) { return Promise.resolve(null); } - - return this._promiseWithConstants(this.native.reset()); + return this._promiseWithConstants(this.native.reset()) as Promise; } - setConfigSettings(settings) { - const updatedSettings = {}; - - updatedSettings.fetchTimeout = this._settings.fetchTimeMillis / 1000; - updatedSettings.minimumFetchInterval = this._settings.minimumFetchIntervalMillis / 1000; + setConfigSettings(settings: FirebaseRemoteConfigTypes.ConfigSettings): Promise { + const updatedSettings: { + fetchTimeout: number; + minimumFetchInterval: number; + } = { + fetchTimeout: (this._settings.fetchTimeMillis ?? 60000) / 1000, + minimumFetchInterval: (this._settings.minimumFetchIntervalMillis ?? 43200000) / 1000, + }; - const apiCalled = arguments[1] == true ? 'settings' : 'setConfigSettings'; + const apiCalled = + (arguments as unknown as [unknown, unknown])[1] === true ? 'settings' : 'setConfigSettings'; if (!isObject(settings)) { throw new Error(`firebase.remoteConfig().${apiCalled}(*): settings must set an object.`); } @@ -177,24 +227,25 @@ class FirebaseConfigModule extends FirebaseModule { } } - return this._promiseWithConstants(this.native.setConfigSettings(updatedSettings)); + return this._promiseWithConstants( + this.native.setConfigSettings(updatedSettings), + ) as Promise; } /** * Activates the Fetched RemoteConfig, so that the fetched key-values take effect. * @returns {Promise} */ - activate() { + activate(): Promise { return this._promiseWithConstants(this.native.activate()); } /** * Fetches parameter values for your app. - * @param {number} expirationDurationSeconds * @returns {Promise} */ - fetch(expirationDurationSeconds) { + fetch(expirationDurationSeconds?: number): Promise { if (!isUndefined(expirationDurationSeconds) && !isNumber(expirationDurationSeconds)) { throw new Error( "firebase.remoteConfig().fetch(): 'expirationDurationSeconds' must be a number value.", @@ -203,78 +254,87 @@ class FirebaseConfigModule extends FirebaseModule { return this._promiseWithConstants( this.native.fetch(expirationDurationSeconds !== undefined ? expirationDurationSeconds : -1), - ); + ) as Promise; } - fetchAndActivate() { + fetchAndActivate(): Promise { return this._promiseWithConstants(this.native.fetchAndActivate()); } - ensureInitialized() { - return this._promiseWithConstants(this.native.ensureInitialized()); + ensureInitialized(): Promise { + return this._promiseWithConstants(this.native.ensureInitialized()) as Promise; } /** * Sets defaults. - * * @param {object} defaults */ - setDefaults(defaults) { - const apiCalled = arguments[1] === true ? 'defaultConfig' : 'setDefaults'; + setDefaults(defaults: FirebaseRemoteConfigTypes.ConfigDefaults): Promise { + const apiCalled = + (arguments as unknown as [unknown, unknown])[1] === true ? 'defaultConfig' : 'setDefaults'; if (!isObject(defaults)) { throw new Error(`firebase.remoteConfig().${apiCalled}(): 'defaults' must be an object.`); } - return this._promiseWithConstants(this.native.setDefaults(defaults)); + return this._promiseWithConstants(this.native.setDefaults(defaults)) as Promise; } /** * Sets defaults based on resource. * @param {string} resourceName */ - setDefaultsFromResource(resourceName) { + setDefaultsFromResource(resourceName: string): Promise { if (!isString(resourceName)) { throw new Error( "firebase.remoteConfig().setDefaultsFromResource(): 'resourceName' must be a string value.", ); } - return this._promiseWithConstants(this.native.setDefaultsFromResource(resourceName)); + return this._promiseWithConstants( + this.native.setDefaultsFromResource(resourceName), + ) as Promise; } /** * Registers an observer to changes in the configuration. - * * @param observer - The {@link ConfigUpdateObserver} to be notified of config updates. * @returns An {@link Unsubscribe} function to remove the listener. */ - onConfigUpdate(observer) { + onConfigUpdate( + observer: FirebaseRemoteConfigTypes.ConfigUpdateObserver, + ): FirebaseRemoteConfigTypes.Unsubscribe { if (!isObject(observer) || !isFunction(observer.next) || !isFunction(observer.error)) { throw new Error("'observer' expected an object with 'next' and 'error' functions."); } - // We maintaine our pre-web-support native interface but bend it to match + // We maintain our pre-web-support native interface but bend it to match // the official JS SDK API by assuming the callback is an Observer, and sending it a ConfigUpdate // compatible parameter that implements the `getUpdatedKeys` method let unsubscribed = false; const subscription = this.emitter.addListener( this.eventNameForApp('on_config_updated'), - event => { + (event: { + resultType: string; + updatedKeys: string[]; + code: string; + message: string; + nativeErrorMessage: string; + }) => { const { resultType } = event; if (resultType === 'success') { observer.next({ - getUpdatedKeys: () => { - return new Set(event.updatedKeys); - }, + getUpdatedKeys: () => new Set(event.updatedKeys), }); return; } - observer.error({ - code: event.code, - message: event.message, - nativeErrorMessage: event.nativeErrorMessage, - }); + observer.error( + Object.assign(new Error(event.message), { + code: event.code, + message: event.message, + nativeErrorMessage: event.nativeErrorMessage, + }), + ); }, ); if (this._configUpdateListenerCount === 0) { @@ -288,9 +348,8 @@ class FirebaseConfigModule extends FirebaseModule { // there is no harm in calling this multiple times to unsubscribe, // but anything after the first call is a no-op return; - } else { - unsubscribed = true; } + unsubscribed = true; subscription.remove(); this._configUpdateListenerCount--; if (this._configUpdateListenerCount === 0) { @@ -301,17 +360,27 @@ class FirebaseConfigModule extends FirebaseModule { /** * Registers a listener to changes in the configuration. - * * @param listenerOrObserver - function called on config change * @returns {function} unsubscribe listener * @deprecated use official firebase-js-sdk onConfigUpdate now that web supports realtime */ - onConfigUpdated(listenerOrObserver) { - const listener = parseListenerOrObserver(listenerOrObserver); + onConfigUpdated( + listenerOrObserver: FirebaseRemoteConfigTypes.CallbackOrObserver, + ): () => void { + const listener = parseListenerOrObserver(listenerOrObserver) as ( + event?: { updatedKeys: string[] }, + error?: { code: string; message: string; nativeErrorMessage: string }, + ) => void; let unsubscribed = false; const subscription = this.emitter.addListener( this.eventNameForApp('on_config_updated'), - event => { + (event: { + resultType: string; + updatedKeys: string[]; + code: string; + message: string; + nativeErrorMessage: string; + }) => { const { resultType } = event; if (resultType === 'success') { listener({ updatedKeys: event.updatedKeys }, undefined); @@ -336,9 +405,8 @@ class FirebaseConfigModule extends FirebaseModule { // there is no harm in calling this multiple times to unsubscribe, // but anything after the first call is a no-op return; - } else { - unsubscribed = true; } + unsubscribed = true; subscription.remove(); this._configUpdateListenerCount--; if (this._configUpdateListenerCount === 0) { @@ -347,29 +415,49 @@ class FirebaseConfigModule extends FirebaseModule { }; } - _updateFromConstants(constants) { + _updateFromConstants( + constants: + | ConstantsUpdate + | FirebaseRemoteConfigTypes.ConfigDefaults + | FirebaseRemoteConfigTypes.ConfigSettings, + ): void { + const c = constants as Record; // Wrapped this as we update using sync getters initially for `defaultConfig` & `settings` - if (constants.lastFetchTime) { - this._lastFetchTime = constants.lastFetchTime; + if (typeof c.lastFetchTime === 'number') { + this._lastFetchTime = c.lastFetchTime; } // Wrapped this as we update using sync getters initially for `defaultConfig` & `settings` - if (constants.lastFetchStatus) { - this._lastFetchStatus = constants.lastFetchStatus; + if (typeof c.lastFetchStatus === 'string') { + this._lastFetchStatus = c.lastFetchStatus as FirebaseRemoteConfigTypes.LastFetchStatusType; } - this._settings = { - fetchTimeMillis: constants.fetchTimeout * 1000, - minimumFetchIntervalMillis: constants.minimumFetchInterval * 1000, - }; + if (typeof c.fetchTimeout === 'number' && typeof c.minimumFetchInterval === 'number') { + this._settings = { + fetchTimeMillis: c.fetchTimeout * 1000, + minimumFetchIntervalMillis: c.minimumFetchInterval * 1000, + }; + } else if ( + typeof (c as FirebaseRemoteConfigTypes.ConfigSettings).fetchTimeMillis === 'number' || + typeof (c as FirebaseRemoteConfigTypes.ConfigSettings).minimumFetchIntervalMillis === 'number' + ) { + const s = c as FirebaseRemoteConfigTypes.ConfigSettings; + this._settings = { + fetchTimeMillis: s.fetchTimeMillis ?? this._settings.fetchTimeMillis, + minimumFetchIntervalMillis: + s.minimumFetchIntervalMillis ?? this._settings.minimumFetchIntervalMillis, + }; + } - this._values = Object.freeze(constants.values); + if (c.values && typeof c.values === 'object' && !Array.isArray(c.values)) { + this._values = Object.freeze(c.values) as Record; + } } - _promiseWithConstants(promise) { + _promiseWithConstants(promise: Promise): Promise { return promise.then(({ result, constants }) => { this._updateFromConstants(constants); - return result; + return result as T; }); } } @@ -390,8 +478,6 @@ export default createModuleNamespace({ ModuleClass: FirebaseConfigModule, }); -export * from './modular'; - // import config, { firebase } from '@react-native-firebase/remote-config'; // config().X(...); // firebase.remoteConfig().X(...); diff --git a/packages/remote-config/lib/polyfills.js b/packages/remote-config/lib/polyfills.js deleted file mode 100644 index f2294f55fe..0000000000 --- a/packages/remote-config/lib/polyfills.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @license - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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 { polyfillGlobal } from 'react-native/Libraries/Utilities/PolyfillFunctions'; -import { ReadableStream } from 'web-streams-polyfill/dist/ponyfill'; -import { fetch, Headers, Request, Response } from 'react-native-fetch-api'; - -polyfillGlobal( - 'fetch', - () => - (...args) => - fetch(args[0], { ...args[1], reactNative: { textStreaming: true } }), -); -polyfillGlobal('Headers', () => Headers); -polyfillGlobal('Request', () => Request); -polyfillGlobal('Response', () => Response); -polyfillGlobal('ReadableStream', () => ReadableStream); - -import 'text-encoding'; diff --git a/packages/remote-config/lib/polyfills.ts b/packages/remote-config/lib/polyfills.ts new file mode 100644 index 0000000000..7c40c8eb03 --- /dev/null +++ b/packages/remote-config/lib/polyfills.ts @@ -0,0 +1,19 @@ +// @ts-expect-error - react-native internal module may lack types +import { polyfillGlobal } from 'react-native/Libraries/Utilities/PolyfillFunctions'; +// @ts-expect-error - web-streams-polyfill ponyfill may lack types +import { ReadableStream } from 'web-streams-polyfill/dist/ponyfill'; +// @ts-expect-error - react-native-fetch-api may lack types +import { fetch, Headers, Request, Response } from 'react-native-fetch-api'; + +polyfillGlobal( + 'fetch', + () => + (...args: Parameters) => + fetch(args[0], { ...args[1], reactNative: { textStreaming: true } } as RequestInit), +); +polyfillGlobal('Headers', () => Headers); +polyfillGlobal('Request', () => Request); +polyfillGlobal('Response', () => Response); +polyfillGlobal('ReadableStream', () => ReadableStream); + +import 'text-encoding'; diff --git a/packages/remote-config/lib/statics.js b/packages/remote-config/lib/statics.js deleted file mode 100644 index 98c67751d3..0000000000 --- a/packages/remote-config/lib/statics.js +++ /dev/null @@ -1,12 +0,0 @@ -export const LastFetchStatus = { - SUCCESS: 'success', - FAILURE: 'failure', - THROTTLED: 'throttled', - NO_FETCH_YET: 'no_fetch_yet', -}; - -export const ValueSource = { - REMOTE: 'remote', - DEFAULT: 'default', - STATIC: 'static', -}; diff --git a/packages/remote-config/lib/statics.ts b/packages/remote-config/lib/statics.ts new file mode 100644 index 0000000000..87306ff04c --- /dev/null +++ b/packages/remote-config/lib/statics.ts @@ -0,0 +1,14 @@ +import type { FirebaseRemoteConfigTypes } from './types/namespaced'; + +export const LastFetchStatus: FirebaseRemoteConfigTypes.LastFetchStatus = { + SUCCESS: 'success', + FAILURE: 'failure', + THROTTLED: 'throttled', + NO_FETCH_YET: 'no_fetch_yet', +}; + +export const ValueSource: FirebaseRemoteConfigTypes.ValueSource = { + REMOTE: 'remote', + DEFAULT: 'default', + STATIC: 'static', +}; diff --git a/packages/remote-config/lib/types/modular.ts b/packages/remote-config/lib/types/modular.ts new file mode 100644 index 0000000000..fa8aab4c89 --- /dev/null +++ b/packages/remote-config/lib/types/modular.ts @@ -0,0 +1,73 @@ +/* + * 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 { FirebaseRemoteConfigTypes } from './namespaced'; + +/** + * Type aliases for the modular Remote Config API. + * Re-exported from FirebaseRemoteConfigTypes for use by the modular functions. + */ +export type RemoteConfig = FirebaseRemoteConfigTypes.Module; +export type ConfigSettings = FirebaseRemoteConfigTypes.ConfigSettings; +export type ConfigDefaults = FirebaseRemoteConfigTypes.ConfigDefaults; +export type ConfigValue = FirebaseRemoteConfigTypes.ConfigValue; +export type ConfigValues = FirebaseRemoteConfigTypes.ConfigValues; +export type LastFetchStatusType = FirebaseRemoteConfigTypes.LastFetchStatusType; +export type RemoteConfigLogLevel = FirebaseRemoteConfigTypes.RemoteConfigLogLevel; +export type ConfigUpdateObserver = FirebaseRemoteConfigTypes.ConfigUpdateObserver; +export type Unsubscribe = FirebaseRemoteConfigTypes.Unsubscribe; +export type OnConfigUpdatedListenerCallback = + FirebaseRemoteConfigTypes.OnConfigUpdatedListenerCallback; + +/** + * Listener can be a callback or an object with a next method (Observer shape). + * Used by onConfigUpdated. + */ +export type CallbackOrObserver any> = T | { next: T }; + +/** + * Custom signals for the app instance (modular setCustomSignals). + */ +export interface CustomSignals { + [key: string]: string | number | null; +} + +/** + * Internal: Module instance methods accept optional deprecation arg (string). + * Used to cast RemoteConfig when calling methods with MODULAR_DEPRECATION_ARG. + */ +export type RemoteConfigWithDeprecationArg = RemoteConfig & { + activate(_dep?: unknown): Promise; + ensureInitialized(_dep?: unknown): Promise; + fetchAndActivate(_dep?: unknown): Promise; + fetchConfig?(_dep?: unknown): Promise; + getAll(_dep?: unknown): ConfigValues; + getBoolean(key: string, _dep?: unknown): boolean; + getNumber(key: string, _dep?: unknown): number; + getString(key: string, _dep?: unknown): string; + getValue(key: string, _dep?: unknown): ConfigValue; + reset(_dep?: unknown): Promise; + setConfigSettings(settings: ConfigSettings, _dep?: unknown): Promise; + fetch(expirationDurationSeconds?: number, _dep?: unknown): Promise; + setDefaults(defaults: ConfigDefaults, _dep?: unknown): Promise; + setDefaultsFromResource(resourceName: string, _dep?: unknown): Promise; + onConfigUpdate(observer: ConfigUpdateObserver, _dep?: unknown): Unsubscribe; + onConfigUpdated( + callback: CallbackOrObserver, + _dep?: unknown, + ): () => void; +}; diff --git a/packages/remote-config/lib/index.d.ts b/packages/remote-config/lib/types/namespaced.ts similarity index 93% rename from packages/remote-config/lib/index.d.ts rename to packages/remote-config/lib/types/namespaced.ts index fcbd4cb73c..63cef0c6f2 100644 --- a/packages/remote-config/lib/index.d.ts +++ b/packages/remote-config/lib/types/namespaced.ts @@ -15,7 +15,9 @@ * */ -import { ReactNativeFirebase } from '@react-native-firebase/app'; +import type { ReactNativeFirebase } from '@react-native-firebase/app'; + +type FirebaseModule = ReactNativeFirebase.FirebaseModule; /** * Firebase Remote RemoteConfig package for React Native. @@ -53,8 +55,8 @@ import { ReactNativeFirebase } from '@react-native-firebase/app'; * * @firebase remote-config */ +// eslint-disable-next-line @typescript-eslint/no-namespace -- public API shape export namespace FirebaseRemoteConfigTypes { - import FirebaseModule = ReactNativeFirebase.FirebaseModule; /** * Defines levels of Remote Config logging. Web only. */ @@ -290,7 +292,7 @@ export namespace FirebaseRemoteConfigTypes { /** * The status of the latest Remote RemoteConfig fetch action. */ - type LastFetchStatusType = 'success' | 'failure' | 'no_fetch_yet' | 'throttled'; + export type LastFetchStatusType = 'success' | 'failure' | 'no_fetch_yet' | 'throttled'; /** * Contains information about which keys have been updated. @@ -318,7 +320,7 @@ export namespace FirebaseRemoteConfigTypes { /** * Called if an error occurs during the stream. */ - error: (error: FirebaseError) => void; + error: (error: Error) => void; /** * Called when the stream is gracefully terminated. @@ -344,7 +346,7 @@ export namespace FirebaseRemoteConfigTypes { * const defaultAppRemoteConfig = firebase.remoteConfig(); * ``` */ - export class Module extends FirebaseModule { + export interface Module extends FirebaseModule { /** * The current `FirebaseApp` instance for this Firebase service. */ @@ -434,11 +436,10 @@ export namespace FirebaseRemoteConfigTypes { * and the config update process fetches the new config but does not automatically activate * it for you. Typically you will activate the config in your callback to use the new values. * - * @param remoteConfig - The {@link RemoteConfig} instance. * @param observer - The {@link ConfigUpdateObserver} to be notified of config updates. * @returns An {@link Unsubscribe} function to remove the listener. */ - onConfigUpdate(remoteConfig: RemoteConfig, observer: ConfigUpdateObserver): Unsubscribe; + onConfigUpdate(observer: ConfigUpdateObserver): Unsubscribe; /** * Start listening for real-time config updates from the Remote Config backend and @@ -619,35 +620,14 @@ export namespace FirebaseRemoteConfigTypes { ) => void; } -type RemoteConfigNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseRemoteConfigTypes.Module, - FirebaseRemoteConfigTypes.Statics -> & { - firebase: ReactNativeFirebase.Module; - app(name?: string): ReactNativeFirebase.FirebaseApp; -}; - -declare const defaultExport: RemoteConfigNamespace; - -export const firebase: ReactNativeFirebase.Module & { - remoteConfig: typeof defaultExport; - app( - name?: string, - ): ReactNativeFirebase.FirebaseApp & { remoteConfig(): FirebaseRemoteConfigTypes.Module }; -}; - -export default defaultExport; - -export * from './modular'; - /** - * Attach namespace to `firebase.` and `FirebaseApp.`. + * Attach namespace to `firebase.` and `FirebaseApp.` */ declare module '@react-native-firebase/app' { + // eslint-disable-next-line @typescript-eslint/no-namespace -- module augmentation namespace ReactNativeFirebase { - import FirebaseModuleWithStatics = ReactNativeFirebase.FirebaseModuleWithStatics; interface Module { - remoteConfig: FirebaseModuleWithStatics< + remoteConfig: ReactNativeFirebase.FirebaseModuleWithStatics< FirebaseRemoteConfigTypes.Module, FirebaseRemoteConfigTypes.Statics >; diff --git a/packages/remote-config/lib/web/RNFBConfigModule.android.js b/packages/remote-config/lib/web/RNFBConfigModule.android.ts similarity index 100% rename from packages/remote-config/lib/web/RNFBConfigModule.android.js rename to packages/remote-config/lib/web/RNFBConfigModule.android.ts diff --git a/packages/remote-config/lib/web/RNFBConfigModule.ios.js b/packages/remote-config/lib/web/RNFBConfigModule.ios.ts similarity index 100% rename from packages/remote-config/lib/web/RNFBConfigModule.ios.js rename to packages/remote-config/lib/web/RNFBConfigModule.ios.ts diff --git a/packages/remote-config/lib/web/RNFBConfigModule.js b/packages/remote-config/lib/web/RNFBConfigModule.js deleted file mode 100644 index 422d45a1fe..0000000000 --- a/packages/remote-config/lib/web/RNFBConfigModule.js +++ /dev/null @@ -1,165 +0,0 @@ -import '../polyfills'; -import { - getApp, - getRemoteConfig, - activate, - ensureInitialized, - fetchAndActivate, - fetchConfig, - getAll, - makeIDBAvailable, - onConfigUpdate, - setCustomSignals, -} from '@react-native-firebase/app/dist/module/internal/web/firebaseRemoteConfig'; -import { - guard, - getWebError, - emitEvent, -} from '@react-native-firebase/app/dist/module/internal/web/utils'; - -let configSettingsForInstance = { - // [APP_NAME]: RemoteConfigSettings -}; -let defaultConfigForInstance = { - // [APP_NAME]: { [key: string]: string | number | boolean } -}; - -function makeGlobalsAvailable() { - navigator.onLine = true; - makeIDBAvailable(); -} - -const onConfigUpdateListeners = {}; - -function getRemoteConfigInstanceForApp(appName, overrides /*: RemoteConfigSettings */) { - makeGlobalsAvailable(); - const configSettings = configSettingsForInstance[appName] ?? { - minimumFetchIntervalMillis: 43200000, - fetchTimeoutMillis: 60000, - }; - const defaultConfig = defaultConfigForInstance[appName] ?? {}; - Object.assign(configSettings, overrides); - const app = getApp(appName); - const instance = getRemoteConfig(app); - for (const key in configSettings) { - instance.settings[key] = configSettings[key]; - } - instance.defaultConfig = defaultConfig; - return instance; -} - -async function resultAndConstants(instance, result) { - const response = { result }; - const valuesRaw = getAll(instance); - const values = {}; - for (const key in valuesRaw) { - const raw = valuesRaw[key]; - values[key] = { - source: raw.getSource(), - value: raw.asString(), - }; - } - response.constants = { - values, - lastFetchTime: instance.fetchTimeMillis === -1 ? 0 : instance.fetchTimeMillis, - lastFetchStatus: instance.lastFetchStatus.replace(/-/g, '_'), - minimumFetchInterval: instance.settings.minimumFetchIntervalMillis - ? instance.settings.minimumFetchIntervalMillis / 1000 - : 43200, - fetchTimeout: instance.settings.fetchTimeoutMillis - ? instance.settings.fetchTimeoutMillis / 1000 - : 60, - }; - return response; -} - -export default { - activate(appName) { - return guard(async () => { - const remoteConfig = getRemoteConfigInstanceForApp(appName); - return resultAndConstants(remoteConfig, await activate(remoteConfig)); - }); - }, - setConfigSettings(appName, settings) { - return guard(async () => { - configSettingsForInstance[appName] = { - minimumFetchIntervalMillis: settings.minimumFetchInterval * 1000, - fetchTimeoutMillis: settings.fetchTimeout * 1000, - }; - const remoteConfig = getRemoteConfigInstanceForApp(appName, settings); - return resultAndConstants(remoteConfig, null); - }); - }, - fetch(appName, expirationDurationSeconds) { - return guard(async () => { - let overrides = {}; - if (expirationDurationSeconds != -1) { - overrides.minimumFetchIntervalMillis = expirationDurationSeconds * 1000; - } - const remoteConfig = getRemoteConfigInstanceForApp(appName, overrides); - await fetchConfig(remoteConfig); - return resultAndConstants(remoteConfig, null); - }); - }, - fetchAndActivate(appName) { - return guard(async () => { - const remoteConfig = getRemoteConfigInstanceForApp(appName); - const activated = await fetchAndActivate(remoteConfig); - return resultAndConstants(remoteConfig, activated); - }); - }, - ensureInitialized(appName) { - return guard(async () => { - const remoteConfig = getRemoteConfigInstanceForApp(appName); - await ensureInitialized(remoteConfig); - return resultAndConstants(remoteConfig, null); - }); - }, - setDefaults(appName, defaults) { - return guard(async () => { - defaultConfigForInstance[appName] = defaults; - const remoteConfig = getRemoteConfigInstanceForApp(appName); - return resultAndConstants(remoteConfig, null); - }); - }, - setCustomSignals(appName, customSignals) { - return guard(async () => { - const remoteConfig = getRemoteConfigInstanceForApp(appName); - await setCustomSignals(remoteConfig, customSignals); - return resultAndConstants(remoteConfig, null); - }); - }, - onConfigUpdated(appName) { - if (onConfigUpdateListeners[appName]) { - return; - } - - const remoteConfig = getRemoteConfigInstanceForApp(appName); - - const nativeObserver = { - next: configUpdate => { - emitEvent('on_config_updated', { - appName, - resultType: 'success', - updatedKeys: Array.from(configUpdate.getUpdatedKeys()), - }); - }, - error: firebaseError => { - emitEvent('on_config_updated', { - appName, - event: getWebError(firebaseError), - }); - }, - complete: () => {}, - }; - - onConfigUpdateListeners[appName] = onConfigUpdate(remoteConfig, nativeObserver); - }, - removeConfigUpdateRegistration(appName) { - if (!onConfigUpdateListeners[appName]) { - return; - } - onConfigUpdateListeners[appName](); - delete onConfigUpdateListeners[appName]; - }, -}; diff --git a/packages/remote-config/lib/web/RNFBConfigModule.ts b/packages/remote-config/lib/web/RNFBConfigModule.ts new file mode 100644 index 0000000000..8854ca7725 --- /dev/null +++ b/packages/remote-config/lib/web/RNFBConfigModule.ts @@ -0,0 +1,214 @@ +import '../polyfills'; + +import { + getApp, + getRemoteConfig, + activate, + ensureInitialized, + fetchAndActivate, + fetchConfig, + getAll, + makeIDBAvailable, + onConfigUpdate, + setCustomSignals, +} from '@react-native-firebase/app/dist/module/internal/web/firebaseRemoteConfig'; + +import { + guard, + getWebError, + emitEvent, +} from '@react-native-firebase/app/dist/module/internal/web/utils'; + +import type { FirebaseRemoteConfigTypes } from '../types/namespaced'; + +type RemoteConfigInstance = FirebaseRemoteConfigTypes.Module & { + settings: Record; + defaultConfig: FirebaseRemoteConfigTypes.ConfigDefaults; + fetchTimeMillis: number; + lastFetchStatus: string; +}; + +const configSettingsForInstance: Record< + string, + { minimumFetchIntervalMillis: number; fetchTimeoutMillis: number } +> = {}; + +const defaultConfigForInstance: Record = {}; + +function makeGlobalsAvailable(): void { + (navigator as { onLine?: boolean }).onLine = true; + makeIDBAvailable(); +} + +const onConfigUpdateListeners: Record void> = {}; + +function getRemoteConfigInstanceForApp( + appName: string, + overrides?: Record, +): RemoteConfigInstance { + makeGlobalsAvailable(); + const configSettings = configSettingsForInstance[appName] ?? { + minimumFetchIntervalMillis: 43200000, + fetchTimeoutMillis: 60000, + }; + + const defaultConfig = defaultConfigForInstance[appName] ?? {}; + + if (overrides) { + Object.assign(configSettings, overrides); + } + + const app = getApp(appName); + const instance = getRemoteConfig(app) as unknown as RemoteConfigInstance; + + for (const key in configSettings) { + (instance.settings as Record)[key] = + configSettings[key as keyof typeof configSettings]; + } + + instance.defaultConfig = defaultConfig; + return instance; +} + +async function resultAndConstants( + instance: RemoteConfigInstance, + result: unknown, +): Promise<{ result: unknown; constants: Record }> { + const response: { result: unknown; constants: Record } = { + result, + constants: {}, + }; + + const valuesRaw = getAll(instance as any); + const values: Record = {}; + + for (const key in valuesRaw) { + const raw = valuesRaw[key]; + + if (raw) { + values[key] = { + source: raw.getSource(), + value: raw.asString(), + }; + } + } + + const settings = instance.settings as { + minimumFetchIntervalMillis?: number; + fetchTimeoutMillis?: number; + }; + + response.constants = { + values, + lastFetchTime: instance.fetchTimeMillis === -1 ? 0 : instance.fetchTimeMillis, + lastFetchStatus: (instance.lastFetchStatus as string).replace(/-/g, '_'), + minimumFetchInterval: settings.minimumFetchIntervalMillis + ? settings.minimumFetchIntervalMillis / 1000 + : 43200, + fetchTimeout: settings.fetchTimeoutMillis ? settings.fetchTimeoutMillis / 1000 : 60, + }; + + return response; +} + +export default { + activate(appName: string) { + return guard(async () => { + const remoteConfig = getRemoteConfigInstanceForApp(appName); + return resultAndConstants(remoteConfig, await activate(remoteConfig as any)); + }); + }, + + setConfigSettings( + appName: string, + settings: { minimumFetchInterval: number; fetchTimeout: number }, + ) { + return guard(async () => { + configSettingsForInstance[appName] = { + minimumFetchIntervalMillis: settings.minimumFetchInterval * 1000, + fetchTimeoutMillis: settings.fetchTimeout * 1000, + }; + const remoteConfig = getRemoteConfigInstanceForApp(appName, settings); + return resultAndConstants(remoteConfig, null); + }); + }, + + fetch(appName: string, expirationDurationSeconds: number) { + return guard(async () => { + let overrides: Record = {}; + if (expirationDurationSeconds !== -1) { + overrides = { minimumFetchIntervalMillis: expirationDurationSeconds * 1000 }; + } + const remoteConfig = getRemoteConfigInstanceForApp(appName, overrides); + await fetchConfig(remoteConfig as any); + return resultAndConstants(remoteConfig, null); + }); + }, + + fetchAndActivate(appName: string) { + return guard(async () => { + const remoteConfig = getRemoteConfigInstanceForApp(appName); + const activated = await fetchAndActivate(remoteConfig as any); + return resultAndConstants(remoteConfig, activated); + }); + }, + + ensureInitialized(appName: string) { + return guard(async () => { + const remoteConfig = getRemoteConfigInstanceForApp(appName); + await ensureInitialized(remoteConfig as any); + return resultAndConstants(remoteConfig, null); + }); + }, + + setDefaults(appName: string, defaults: FirebaseRemoteConfigTypes.ConfigDefaults) { + return guard(async () => { + defaultConfigForInstance[appName] = defaults; + const remoteConfig = getRemoteConfigInstanceForApp(appName); + return resultAndConstants(remoteConfig, null); + }); + }, + + setCustomSignals(appName: string, customSignals: Record) { + return guard(async () => { + const remoteConfig = getRemoteConfigInstanceForApp(appName); + await setCustomSignals(remoteConfig as any, customSignals); + return resultAndConstants(remoteConfig, null); + }); + }, + + onConfigUpdated(appName: string): void { + if (onConfigUpdateListeners[appName]) { + return; + } + + const remoteConfig = getRemoteConfigInstanceForApp(appName); + + const nativeObserver = { + next: (configUpdate: { getUpdatedKeys: () => Set }) => { + emitEvent('on_config_updated', { + appName, + resultType: 'success', + updatedKeys: Array.from(configUpdate.getUpdatedKeys()), + }); + }, + error: (firebaseError: unknown) => { + emitEvent('on_config_updated', { + appName, + event: getWebError(firebaseError as Error & { code?: string }), + }); + }, + complete: () => {}, + }; + + onConfigUpdateListeners[appName] = onConfigUpdate(remoteConfig as any, nativeObserver); + }, + + removeConfigUpdateRegistration(appName: string): void { + if (!onConfigUpdateListeners[appName]) { + return; + } + onConfigUpdateListeners[appName](); + delete onConfigUpdateListeners[appName]; + }, +}; diff --git a/packages/remote-config/package.json b/packages/remote-config/package.json index ef4476bfe4..5ede0ff81a 100644 --- a/packages/remote-config/package.json +++ b/packages/remote-config/package.json @@ -3,12 +3,13 @@ "version": "23.8.6", "author": "Invertase (http://invertase.io)", "description": "React Native Firebase - React Native Firebase provides native integration with Remote Config, allowing you to change the appearance and/or functionality of your app without requiring an app update.", - "main": "lib/index.js", - "types": "lib/index.d.ts", + "main": "./dist/module/index.js", + "types": "./dist/typescript/lib/index.d.ts", "scripts": { - "build": "genversion --semi lib/version.js", + "build": "genversion --esm --semi lib/version.ts", "build:clean": "rimraf android/build && rimraf ios/build", - "prepare": "yarn run build" + "compile": "bob build", + "prepare": "yarn run build && yarn compile" }, "repository": { "type": "git", @@ -37,6 +38,38 @@ "web-streams-polyfill": "^4.2.0" }, "devDependencies": { - "@types/text-encoding": "^0.0.40" - } + "@types/text-encoding": "^0.0.40", + "react-native-builder-bob": "^0.40.17", + "typescript": "^5.9.3" + }, + "exports": { + ".": { + "source": "./lib/index.ts", + "types": "./dist/typescript/lib/index.d.ts", + "default": "./dist/module/index.js" + }, + "./package.json": "./package.json" + }, + "react-native-builder-bob": { + "source": "lib", + "output": "dist", + "targets": [ + [ + "module", + { + "esm": true + } + ], + [ + "typescript", + { + "tsc": "../../node_modules/.bin/tsc" + } + ] + ] + }, + "eslintIgnore": [ + "node_modules/", + "dist/" + ] } diff --git a/packages/remote-config/tsconfig.json b/packages/remote-config/tsconfig.json new file mode 100644 index 0000000000..219bbca56a --- /dev/null +++ b/packages/remote-config/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.packages.base.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": ".", + "paths": { + "@react-native-firebase/app/dist/module/common/*": ["../app/dist/typescript/lib/common/*"], + "@react-native-firebase/app/dist/module/common": ["../app/dist/typescript/lib/common"], + "@react-native-firebase/app/dist/module/internal/web/*": ["../app/dist/typescript/lib/internal/web/*"], + "@react-native-firebase/app/dist/module/internal/*": ["../app/dist/typescript/lib/internal/*"], + "@react-native-firebase/app/dist/module/internal": ["../app/dist/typescript/lib/internal"], + "@react-native-firebase/app": ["../app/dist/typescript/lib"] + } + }, + "include": ["lib/**/*", "../app/lib/internal/global.d.ts"], + "exclude": ["node_modules", "dist", "__tests__", "**/*.test.ts"] +} diff --git a/packages/remote-config/type-test.ts b/packages/remote-config/type-test.ts index 5f32b64542..ece18e21d2 100644 --- a/packages/remote-config/type-test.ts +++ b/packages/remote-config/type-test.ts @@ -1,6 +1,6 @@ +import type { CustomSignals, FirebaseRemoteConfigTypes } from '@react-native-firebase/remote-config'; import remoteConfig, { firebase, - FirebaseRemoteConfigTypes, getRemoteConfig, activate, ensureInitialized, @@ -26,44 +26,56 @@ import remoteConfig, { setCustomSignals, LastFetchStatus, ValueSource, -} from '.'; +} from '@react-native-firebase/remote-config'; + +// Root tsconfig resolves app from source; getFirebaseRoot() may not get remote-config augmentation. +type FirebaseRoot = { + app(name?: string): { name: string; remoteConfig(): ReturnType }; + remoteConfig: ((app?: { name: string }) => ReturnType) & { + SDK_VERSION: string; + ValueSource: { REMOTE: string; DEFAULT: string; STATIC: string }; + LastFetchStatus: { SUCCESS: string; FAILURE: string; THROTTLED: string; NO_FETCH_YET: string }; + }; + SDK_VERSION?: string; +}; +const _firebase = firebase as unknown as FirebaseRoot; console.log(remoteConfig().app); // checks module exists at root -console.log(firebase.remoteConfig().app.name); -console.log(firebase.remoteConfig().fetchTimeMillis); -console.log(firebase.remoteConfig().lastFetchStatus); -console.log(firebase.remoteConfig().settings); -console.log(firebase.remoteConfig().defaultConfig); +console.log(_firebase.remoteConfig().app.name); +console.log(_firebase.remoteConfig().fetchTimeMillis); +console.log(_firebase.remoteConfig().lastFetchStatus); +console.log(_firebase.remoteConfig().settings); +console.log(_firebase.remoteConfig().defaultConfig); // checks module exists at app level -console.log(firebase.app().remoteConfig().app.name); +console.log(_firebase.app().remoteConfig().app.name); // checks statics exist -console.log(firebase.remoteConfig.SDK_VERSION); -console.log(firebase.remoteConfig.ValueSource.REMOTE); -console.log(firebase.remoteConfig.ValueSource.DEFAULT); -console.log(firebase.remoteConfig.ValueSource.STATIC); -console.log(firebase.remoteConfig.LastFetchStatus.SUCCESS); -console.log(firebase.remoteConfig.LastFetchStatus.FAILURE); -console.log(firebase.remoteConfig.LastFetchStatus.THROTTLED); -console.log(firebase.remoteConfig.LastFetchStatus.NO_FETCH_YET); +console.log(_firebase.remoteConfig.SDK_VERSION); +console.log(_firebase.remoteConfig.ValueSource.REMOTE); +console.log(_firebase.remoteConfig.ValueSource.DEFAULT); +console.log(_firebase.remoteConfig.ValueSource.STATIC); +console.log(_firebase.remoteConfig.LastFetchStatus.SUCCESS); +console.log(_firebase.remoteConfig.LastFetchStatus.FAILURE); +console.log(_firebase.remoteConfig.LastFetchStatus.THROTTLED); +console.log(_firebase.remoteConfig.LastFetchStatus.NO_FETCH_YET); // checks statics exist on defaultExport -console.log(remoteConfig.firebase.SDK_VERSION); +console.log((remoteConfig as unknown as { firebase: FirebaseRoot }).firebase.SDK_VERSION); // checks root exists -console.log(firebase.SDK_VERSION); +console.log(_firebase.SDK_VERSION); // checks multi-app support exists -console.log(firebase.remoteConfig(firebase.app()).app.name); +console.log(_firebase.remoteConfig(_firebase.app()).app.name); // checks default export supports app arg -console.log(remoteConfig(firebase.app()).app.name); +console.log(remoteConfig(_firebase.app().name).app.name); // checks Module instance APIs -const remoteConfigInstance = firebase.remoteConfig(); +const remoteConfigInstance = _firebase.remoteConfig(); console.log(remoteConfigInstance.app.name); console.log(remoteConfigInstance.fetchTimeMillis); console.log(remoteConfigInstance.lastFetchStatus); @@ -86,7 +98,7 @@ remoteConfigInstance.setDefaultsFromResource('config_resource').then(() => { console.log('Defaults from resource set'); }); -const unsubscribeOnConfigUpdate = remoteConfigInstance.onConfigUpdate(remoteConfigInstance, { +const unsubscribeOnConfigUpdate = remoteConfigInstance.onConfigUpdate({ next: (configUpdate: FirebaseRemoteConfigTypes.ConfigUpdate) => { console.log(configUpdate.getUpdatedKeys()); }, @@ -155,7 +167,7 @@ remoteConfigInstance.reset().then(() => { const modularRemoteConfig1 = getRemoteConfig(); console.log(modularRemoteConfig1.app.name); -const modularRemoteConfig2 = getRemoteConfig(firebase.app()); +const modularRemoteConfig2 = getRemoteConfig(_firebase.app() as Parameters[0]); console.log(modularRemoteConfig2.app.name); activate(modularRemoteConfig1).then((activated: boolean) => { @@ -249,7 +261,9 @@ const modularUnsubscribeOnConfigUpdated = onConfigUpdated( ); modularUnsubscribeOnConfigUpdated(); -setCustomSignals(modularRemoteConfig1, { signal1: 'value1', signal2: 123 }).then(() => { +// Explicit use of modular type CustomSignals (from lib/types/modular) +const customSignals: CustomSignals = { signal1: 'value1', signal2: 123, signal3: null }; +setCustomSignals(modularRemoteConfig1, customSignals).then(() => { console.log('Modular custom signals set'); }); diff --git a/tests/local-tests/index.js b/tests/local-tests/index.js index 019928b382..bb907184ba 100644 --- a/tests/local-tests/index.js +++ b/tests/local-tests/index.js @@ -29,6 +29,7 @@ import { VertexAITestComponent } from './vertexai/vertexai'; import { AuthMFADemonstrator } from './auth/auth-mfa-demonstrator'; import { HttpsCallableTestComponent } from './functions/https-callable'; import { StreamingCallableTestComponent } from './functions/streaming-callable'; +import { RemoteConfigTestComponent } from './remote-config'; const testComponents = { // List your imported components here... @@ -40,6 +41,7 @@ const testComponents = { 'Auth MFA Demonstrator': AuthMFADemonstrator, 'HttpsCallable Test': HttpsCallableTestComponent, 'Streaming Callable Test': StreamingCallableTestComponent, + 'Remote Config Test': RemoteConfigTestComponent, }; export function TestComponents() { diff --git a/tests/local-tests/remote-config/RemoteConfigService/RemoteConfigService.ts b/tests/local-tests/remote-config/RemoteConfigService/RemoteConfigService.ts new file mode 100644 index 0000000000..735b22fb14 --- /dev/null +++ b/tests/local-tests/remote-config/RemoteConfigService/RemoteConfigService.ts @@ -0,0 +1,63 @@ +import type { FetchAndActivateResult, FetchAndActivateSuccess, IRemoteConfigClient } from './types'; + +export class RemoteConfigService { + constructor( + private readonly app: { name: string }, + private readonly client: IRemoteConfigClient, + ) {} + + async fetchAndActivate(keys: string[]): Promise { + const remoteConfig = this.client.getRemoteConfig(this.app); + + try { + const activated = await this.client.fetchAndActivate(remoteConfig); + const lastFetchStatus = this.client.lastFetchStatus(remoteConfig); + const timeMillis = this.client.fetchTimeMillis(remoteConfig); + const fetchTimeStr = timeMillis >= 0 ? new Date(timeMillis).toISOString() : 'never'; + + const keyResults: Record = {}; + + for (const key of keys) { + try { + const configValue = this.client.getValue(remoteConfig, key); + const source = configValue.getSource ? configValue.getSource() : 'unknown'; + keyResults[key] = { + value: configValue.asString(), + source, + }; + } catch (e) { + const err = e instanceof Error ? e.message : String(e); + keyResults[key] = { value: '', source: `error: ${err}` }; + } + } + + const success: FetchAndActivateSuccess = { + success: true, + activated, + lastFetchStatus, + fetchTimeMillis: timeMillis, + fetchTimeStr, + keys: keyResults, + }; + + return success; + } catch (e) { + const error = e instanceof Error ? e.message : String(e); + return { success: false, error }; + } + } + + static formatResult(result: FetchAndActivateResult): string { + if (!result.success) { + return `Error: ${result.error}`; + } + + let text = `Activated: ${result.activated}\nlastFetchStatus: ${result.lastFetchStatus}\nfetchTimeMillis: ${result.fetchTimeStr}\n\n`; + + for (const [key, { value, source }] of Object.entries(result.keys)) { + text += `${key}: "${value}" (source: ${source})\n`; + } + + return text; + } +} diff --git a/tests/local-tests/remote-config/RemoteConfigService/firebaseRemoteConfigClient.ts b/tests/local-tests/remote-config/RemoteConfigService/firebaseRemoteConfigClient.ts new file mode 100644 index 0000000000..9513521cc5 --- /dev/null +++ b/tests/local-tests/remote-config/RemoteConfigService/firebaseRemoteConfigClient.ts @@ -0,0 +1,30 @@ +import { + getRemoteConfig, + fetchAndActivate, + getValue, + fetchTimeMillis, + lastFetchStatus, +} from '@react-native-firebase/remote-config'; +import type { IRemoteConfigClient } from './types'; + +type RemoteConfigInstance = ReturnType; + +export function createFirebaseRemoteConfigClient(): IRemoteConfigClient { + return { + getRemoteConfig(app: { name: string }) { + return getRemoteConfig(app as Parameters[0]); + }, + fetchAndActivate(remoteConfig: unknown) { + return fetchAndActivate(remoteConfig as RemoteConfigInstance); + }, + getValue(remoteConfig: unknown, key: string) { + return getValue(remoteConfig as RemoteConfigInstance, key); + }, + lastFetchStatus(remoteConfig: unknown) { + return lastFetchStatus(remoteConfig as RemoteConfigInstance); + }, + fetchTimeMillis(remoteConfig: unknown) { + return fetchTimeMillis(remoteConfig as RemoteConfigInstance); + }, + }; +} diff --git a/tests/local-tests/remote-config/RemoteConfigService/index.ts b/tests/local-tests/remote-config/RemoteConfigService/index.ts new file mode 100644 index 0000000000..5d25fb2cd9 --- /dev/null +++ b/tests/local-tests/remote-config/RemoteConfigService/index.ts @@ -0,0 +1,9 @@ +export { RemoteConfigService } from './RemoteConfigService'; +export { createFirebaseRemoteConfigClient } from './firebaseRemoteConfigClient'; +export type { + FetchAndActivateResult, + FetchAndActivateSuccess, + FetchAndActivateError, + ConfigKeyResult, + IRemoteConfigClient, +} from './types'; diff --git a/tests/local-tests/remote-config/RemoteConfigService/types.ts b/tests/local-tests/remote-config/RemoteConfigService/types.ts new file mode 100644 index 0000000000..11cee5fc49 --- /dev/null +++ b/tests/local-tests/remote-config/RemoteConfigService/types.ts @@ -0,0 +1,28 @@ +export interface ConfigKeyResult { + value: string; + source: string; +} + +export interface FetchAndActivateSuccess { + success: true; + activated: boolean; + lastFetchStatus: string; + fetchTimeMillis: number; + fetchTimeStr: string; + keys: Record; +} + +export interface FetchAndActivateError { + success: false; + error: string; +} + +export type FetchAndActivateResult = FetchAndActivateSuccess | FetchAndActivateError; + +export interface IRemoteConfigClient { + getRemoteConfig(app: { name: string }): unknown; + fetchAndActivate(remoteConfig: unknown): Promise; + getValue(remoteConfig: unknown, key: string): { asString(): string; getSource?(): string }; + lastFetchStatus(remoteConfig: unknown): string; + fetchTimeMillis(remoteConfig: unknown): number; +} diff --git a/tests/local-tests/remote-config/index.tsx b/tests/local-tests/remote-config/index.tsx new file mode 100644 index 0000000000..50709dfa82 --- /dev/null +++ b/tests/local-tests/remote-config/index.tsx @@ -0,0 +1,53 @@ +import React, { useMemo, useState } from 'react'; +import { Button, Text, View, ScrollView } from 'react-native'; + +import { getApp } from '@react-native-firebase/app'; +import { RemoteConfigService, createFirebaseRemoteConfigClient } from './RemoteConfigService'; + +const REMOTE_CONFIG_KEYS = ['local_test_1', 'local_test_2']; + +export function RemoteConfigTestComponent(): React.JSX.Element { + const [status, setStatus] = useState(''); + const [loading, setLoading] = useState(false); + + const remoteConfigService = useMemo(() => { + const app = getApp(); + const client = createFirebaseRemoteConfigClient(); + return new RemoteConfigService(app, client); + }, []); + + const handleFetchAndActivate = async (): Promise => { + setLoading(true); + setStatus(''); + + try { + const result = await remoteConfigService.fetchAndActivate(REMOTE_CONFIG_KEYS); + setStatus(RemoteConfigService.formatResult(result)); + } finally { + setLoading(false); + } + }; + + return ( + + + Uses real Remote Config backend (no emulator). Ensure project has config in Firebase + Console. + + +