diff --git a/.github/scripts/compare-types/packages/remote-config/config.ts b/.github/scripts/compare-types/packages/remote-config/config.ts new file mode 100644 index 0000000000..88c023307c --- /dev/null +++ b/.github/scripts/compare-types/packages/remote-config/config.ts @@ -0,0 +1,117 @@ +/** + * Known differences between the firebase-js-sdk @firebase/remote-config public + * API and the @react-native-firebase/remote-config modular API. + * + * Each entry must have a `name` (the export name) and a `reason` explaining + * why the difference exists. Any difference NOT listed here will cause CI to + * fail so that new drift is caught and deliberately acknowledged. + * + * Sections: + * nameMapping — exports that exist in both packages but under different names + * missingInRN — firebase-js-sdk exports absent from RN Firebase + * extraInRN — RN Firebase exports not present in the firebase-js-sdk + * differentShape — exports present in both but with differing signatures/members + */ + +import type { PackageConfig } from '../../src/types'; + +const config: PackageConfig = { + // --------------------------------------------------------------------------- + // Name mapping + // --------------------------------------------------------------------------- + // ValueSource exists in both packages. In RN Firebase we export only the + // value (from modular/statics) and use the value as the type; no type alias. + // Both sides see `ValueSource` and they match — no mapping entry needed. + nameMapping: {}, + + // --------------------------------------------------------------------------- + // Missing in RN Firebase + // --------------------------------------------------------------------------- + missingInRN: [ + // Currently empty — all firebase-js-sdk remote-config exports are present + // in the RN Firebase modular API (though some have different shapes; see below). + ], + + // --------------------------------------------------------------------------- + // Extra in RN Firebase + // --------------------------------------------------------------------------- + extraInRN: [ + { + name: 'LastFetchStatus', + reason: + 'Namespaced constant re-exported from statics.d.ts for backwards ' + + 'compatibility with the class-based (namespaced) API. Not part of ' + + 'the firebase-js-sdk modular API.', + }, + { + name: 'onConfigUpdated', + reason: + 'Deprecated RN Firebase listener for real-time config updates. ' + + 'Replaced by `onConfigUpdate()` which matches the firebase-js-sdk API.', + }, + { + name: 'reset', + reason: + 'Android-only API that deletes all activated, fetched and default ' + + 'configs and resets all Remote Config settings. No equivalent exists ' + + 'in the firebase-js-sdk.', + }, + { + name: 'setConfigSettings', + reason: + 'RN Firebase helper to update `minimumFetchIntervalMillis` and ' + + '`fetchTimeoutMillis` asynchronously via the native module. In the ' + + 'firebase-js-sdk these properties are set by direct property assignment ' + + 'on the `RemoteConfig.settings` object.', + }, + { + name: 'setDefaults', + reason: + 'RN Firebase API for setting default config values programmatically. ' + + 'The firebase-js-sdk uses direct assignment to `RemoteConfig.defaultConfig` ' + + 'instead.', + }, + { + name: 'setDefaultsFromResource', + reason: + 'RN Firebase-specific API that loads default config values from a ' + + 'platform resource file (iOS .plist / Android XML). No equivalent ' + + 'exists in the firebase-js-sdk web API.', + }, + ], + + // --------------------------------------------------------------------------- + // Different shape + // --------------------------------------------------------------------------- + differentShape: [ + { + name: 'ConfigUpdateObserver', + reason: + 'The `error` callback parameter uses `ReactNativeFirebase.NativeFirebaseError` ' + + 'instead of `FirebaseError` from `@firebase/app`. Both represent Firebase ' + + 'errors but the RN type extends the native bridge error structure.', + }, + { + name: 'getRemoteConfig', + reason: + 'The optional `app` parameter uses `ReactNativeFirebase.FirebaseApp` from ' + + '`@react-native-firebase/app` instead of `FirebaseApp` from `@firebase/app`. ' + + 'Both types represent a Firebase app instance but come from different packages.', + }, + { + name: 'setLogLevel', + reason: + 'Uses `LogLevel` matching firebase-js-sdk. RN Firebase returns `LogLevel`; ' + + 'firebase-js-sdk returns `void`. The return type is a legacy artefact of the RN implementation.', + }, + { + name: 'setCustomSignals', + reason: + 'Returns `Promise` in RN Firebase vs `Promise` in the ' + + 'firebase-js-sdk. The native module resolves with `null` rather than ' + + '`undefined`; both signal successful completion.', + }, + ], +}; + +export default config; diff --git a/.github/scripts/compare-types/packages/remote-config/firebase-sdk.d.ts b/.github/scripts/compare-types/packages/remote-config/firebase-sdk.d.ts new file mode 100644 index 0000000000..5776b0450d --- /dev/null +++ b/.github/scripts/compare-types/packages/remote-config/firebase-sdk.d.ts @@ -0,0 +1,266 @@ +/** + * Public types snapshot from the Firebase JS SDK (@firebase/remote-config). + * + * Source: firebase-js-sdk package @firebase/remote-config + * Modality: modular (tree-shakeable) API only + * + * This file is the reference snapshot used to detect API drift between the + * firebase-js-sdk and the @react-native-firebase/remote-config modular API. + * + * When a new version of the firebase-js-sdk ships with type changes, update + * this file with the new public types from @firebase/remote-config/dist/index.d.ts. + */ + +import { FirebaseApp } from '@firebase/app'; +import { FirebaseError } from '@firebase/app'; + +/** + * Makes the last fetched config available to the getters. + * @public + */ +export declare function activate(remoteConfig: RemoteConfig): Promise; + +/** + * Contains information about which keys have been updated. + * @public + */ +export declare interface ConfigUpdate { + /** + * Parameter keys whose values have been updated from the currently activated values. + * Includes keys that are added, deleted, or whose value, value source, or metadata has changed. + */ + getUpdatedKeys(): Set; +} + +/** + * Observer interface for receiving real-time Remote Config update notifications. + * @public + */ +export declare interface ConfigUpdateObserver { + next: (configUpdate: ConfigUpdate) => void; + error: (error: FirebaseError) => void; + complete: () => void; +} + +/** + * Defines the type for representing custom signals and their values. + * @public + */ +export declare interface CustomSignals { + [key: string]: string | number | null; +} + +/** + * Ensures the last activated config are available to the getters. + * @public + */ +export declare function ensureInitialized( + remoteConfig: RemoteConfig, +): Promise; + +/** + * Performs fetch and activate operations, as a convenience. + * @public + */ +export declare function fetchAndActivate( + remoteConfig: RemoteConfig, +): Promise; + +/** + * Fetches and caches configuration from the Remote Config service. + * @public + */ +export declare function fetchConfig(remoteConfig: RemoteConfig): Promise; + +/** + * Defines a successful response (200 or 304). + * @public + */ +export declare interface FetchResponse { + status: number; + eTag?: string; + config?: FirebaseRemoteConfigObject; + templateVersion?: number; + experiments?: FirebaseExperimentDescription[]; +} + +/** + * Summarizes the outcome of the last attempt to fetch config. + * @public + */ +export declare type FetchStatus = + | 'no-fetch-yet' + | 'success' + | 'failure' + | 'throttle'; + +/** + * Indicates the type of fetch request. + * @public + */ +export declare type FetchType = 'BASE' | 'REALTIME'; + +/** + * Defines experiment and variant attached to a config parameter. + * @public + */ +export declare interface FirebaseExperimentDescription { + experimentId: string; + variantId: string; + experimentStartTime: string; + triggerTimeoutMillis: string; + timeToLiveMillis: string; + affectedParameterKeys?: string[]; +} + +/** + * Defines a self-descriptive reference for config key-value pairs. + * @public + */ +export declare interface FirebaseRemoteConfigObject { + [key: string]: string; +} + +/** + * Gets all config. + * @public + */ +export declare function getAll( + remoteConfig: RemoteConfig, +): Record; + +/** + * Gets the value for the given key as a boolean. + * @public + */ +export declare function getBoolean( + remoteConfig: RemoteConfig, + key: string, +): boolean; + +/** + * Gets the value for the given key as a number. + * @public + */ +export declare function getNumber( + remoteConfig: RemoteConfig, + key: string, +): number; + +/** + * Returns a RemoteConfig instance for the given app. + * @public + */ +export declare function getRemoteConfig( + app?: FirebaseApp, + options?: RemoteConfigOptions, +): RemoteConfig; + +/** + * Gets the value for the given key as a string. + * @public + */ +export declare function getString( + remoteConfig: RemoteConfig, + key: string, +): string; + +/** + * Gets the Value for the given key. + * @public + */ +export declare function getValue(remoteConfig: RemoteConfig, key: string): Value; + +/** + * Returns true if a RemoteConfig instance can be initialized in this environment. + * @public + */ +export declare function isSupported(): Promise; + +/** + * Defines levels of Remote Config logging. + * @public + */ +export declare type LogLevel = 'debug' | 'error' | 'silent'; + +/** + * Starts listening for real-time config updates from the Remote Config backend. + * @public + */ +export declare function onConfigUpdate( + remoteConfig: RemoteConfig, + observer: ConfigUpdateObserver, +): Unsubscribe; + +/** + * The Firebase Remote Config service interface. + * @public + */ +export declare interface RemoteConfig { + app: FirebaseApp; + settings: RemoteConfigSettings; + defaultConfig: { + [key: string]: string | number | boolean; + }; + fetchTimeMillis: number; + lastFetchStatus: FetchStatus; +} + +/** + * Options for Remote Config initialization. + * @public + */ +export declare interface RemoteConfigOptions { + templateId?: string; + initialFetchResponse?: FetchResponse; +} + +/** + * Defines configuration options for the Remote Config SDK. + * @public + */ +export declare interface RemoteConfigSettings { + minimumFetchIntervalMillis: number; + fetchTimeoutMillis: number; +} + +/** + * Sets the custom signals for the app instance. + * @public + */ +export declare function setCustomSignals( + remoteConfig: RemoteConfig, + customSignals: CustomSignals, +): Promise; + +/** + * Defines the log level to use. + * @public + */ +export declare function setLogLevel( + remoteConfig: RemoteConfig, + logLevel: LogLevel, +): void; + +/** + * A function that unsubscribes from a real-time event stream. + * @public + */ +export declare type Unsubscribe = () => void; + +/** + * Wraps a value with metadata and type-safe getters. + * @public + */ +export declare interface Value { + asBoolean(): boolean; + asNumber(): number; + asString(): string; + getSource(): ValueSource; +} + +/** + * Indicates the source of a value. + * @public + */ +export declare type ValueSource = 'static' | 'default' | 'remote'; diff --git a/.github/scripts/compare-types/src/parse.ts b/.github/scripts/compare-types/src/parse.ts index 1656f6e0a5..6aed1251a2 100644 --- a/.github/scripts/compare-types/src/parse.ts +++ b/.github/scripts/compare-types/src/parse.ts @@ -6,12 +6,7 @@ */ import fs from 'fs'; -import { - Project, - Node, - type ExportedDeclarations, - type SourceFile, -} from 'ts-morph'; +import { Project, Node, type ExportedDeclarations, type SourceFile } from 'ts-morph'; import type { ExportEntry, ExportShape, @@ -44,7 +39,10 @@ function normalizeType(s: string): string { function extractFunctionShape( decl: Parameters[0] & { - getParameters: () => { getTypeNode: () => { getText: () => string } | undefined; isRestParameter: () => boolean }[]; + getParameters: () => { + getTypeNode: () => { getText: () => string } | undefined; + isRestParameter: () => boolean; + }[]; getReturnTypeNode: () => { getText: () => string } | undefined; }, ): FunctionShape { @@ -53,9 +51,7 @@ function extractFunctionShape( const typeText = typeNode ? normalizeType(typeNode.getText()) : 'any'; return p.isRestParameter() ? `...${typeText}` : typeText; }); - const returnType = normalizeType( - (decl as any).getReturnTypeNode()?.getText() ?? 'void', - ); + const returnType = normalizeType((decl as any).getReturnTypeNode()?.getText() ?? 'void'); return { kind: 'function', params, returnType }; } @@ -74,9 +70,7 @@ function extractInterfaceShape(decl: any): InterfaceShape { const methodParams = member .getParameters() .map((p: any) => normalizeType(p.getTypeNode()?.getText() ?? 'any')); - const retType = normalizeType( - member.getReturnTypeNode()?.getText() ?? 'void', - ); + const retType = normalizeType(member.getReturnTypeNode()?.getText() ?? 'void'); members.push({ name: member.getName(), type: `(${methodParams.join(', ')}) => ${retType}`, @@ -99,9 +93,7 @@ function extractVariableShape(decl: any): VariableShape { return { kind: 'variable', type }; } -function extractShape( - declarations: ReadonlyArray, -): ExportShape | null { +function extractShape(declarations: ReadonlyArray): ExportShape | null { for (const decl of declarations) { if (Node.isFunctionDeclaration(decl)) { return extractFunctionShape(decl as any); @@ -135,10 +127,7 @@ function createProject(): Project { }); } -function collectExportsFromSourceFile( - sf: SourceFile, - into: Map, -): void { +function collectExportsFromSourceFile(sf: SourceFile, into: Map): void { for (const [name, decls] of sf.getExportedDeclarations()) { if (into.has(name)) continue; // first file wins (no overwriting) const shape = extractShape(decls); diff --git a/.github/scripts/compare-types/src/registry.ts b/.github/scripts/compare-types/src/registry.ts index 30aac48c24..f90de1de92 100644 --- a/.github/scripts/compare-types/src/registry.ts +++ b/.github/scripts/compare-types/src/registry.ts @@ -39,16 +39,7 @@ export interface PackageEntry { } function rnDist(packageName: string): string { - return path.join( - REPO_ROOT, - 'packages', - packageName, - 'dist', - 'typescript', - 'lib', - ); + return path.join(REPO_ROOT, 'packages', packageName, 'dist', 'typescript', 'lib'); } -export const packages: PackageEntry[] = [ - -]; +export const packages: PackageEntry[] = []; diff --git a/packages/app/lib/common/index.ts b/packages/app/lib/common/index.ts index 585f075ccc..ee4780add3 100644 --- a/packages/app/lib/common/index.ts +++ b/packages/app/lib/common/index.ts @@ -91,6 +91,8 @@ export const isIOS = Platform.OS === 'ios'; export const isAndroid = Platform.OS === 'android'; +export const isWeb = Platform.OS === 'web'; + export const isOther = Platform.OS !== 'ios' && Platform.OS !== 'android'; export function tryJSONParse(string: string | null | undefined): any { @@ -491,7 +493,7 @@ const mapOfDeprecationReplacements: DeprecationMap = { getValue: 'getValue()', reset: 'reset()', setConfigSettings: 'setConfigSettings()', - fetch: 'fetch()', + fetch: 'fetchConfig()', setDefaults: 'setDefaults()', setDefaultsFromResource: 'setDefaultsFromResource()', onConfigUpdated: 'onConfigUpdated()', diff --git a/packages/remote-config/__tests__/remote-config.test.ts b/packages/remote-config/__tests__/remote-config.test.ts index 2ca627fb6b..f7a46637cf 100644 --- a/packages/remote-config/__tests__/remote-config.test.ts +++ b/packages/remote-config/__tests__/remote-config.test.ts @@ -30,21 +30,18 @@ import { getValue, setLogLevel, isSupported, - fetchTimeMillis, - settings, - lastFetchStatus, reset, setConfigSettings, - fetch, setDefaults, setDefaultsFromResource, onConfigUpdate, - onConfigUpdated, setCustomSignals, - LastFetchStatus, - ValueSource, } from '../lib'; +import type { FetchStatus, ValueSource } from '../lib'; + +import type { RemoteConfigWithDeprecationArg } from '../lib/types/internal'; + import { createCheckV9Deprecation, CheckV9DeprecationFunction, @@ -203,16 +200,15 @@ describe('remoteConfig()', function () { expect(isSupported).toBeDefined(); }); - it('`fetchTimeMillis` function is properly exposed to end user', function () { - expect(fetchTimeMillis).toBeDefined(); - }); - - it('`settings` function is properly exposed to end user', function () { - expect(settings).toBeDefined(); - }); - - it('`lastFetchStatus` function is properly exposed to end user', function () { - expect(lastFetchStatus).toBeDefined(); + it('`fetchTimeMillis`, `lastFetchStatus` and `settings` are on RemoteConfig instance (firebase-js-sdk shape)', function () { + const rc = getRemoteConfig(); + expect(rc.fetchTimeMillis).toBeDefined(); + expect(typeof rc.fetchTimeMillis).toBe('number'); + expect(rc.lastFetchStatus).toBeDefined(); + expect(typeof rc.lastFetchStatus).toBe('string'); + expect(rc.settings).toBeDefined(); + expect(rc.settings.minimumFetchIntervalMillis).toBeDefined(); + expect(rc.settings.fetchTimeoutMillis).toBeDefined(); }); it('`reset` function is properly exposed to end user', function () { @@ -223,10 +219,6 @@ describe('remoteConfig()', function () { expect(setConfigSettings).toBeDefined(); }); - it('`fetch` function is properly exposed to end user', function () { - expect(fetch).toBeDefined(); - }); - it('`setDefaults` function is properly exposed to end user', function () { expect(setDefaults).toBeDefined(); }); @@ -239,25 +231,26 @@ describe('remoteConfig()', function () { expect(onConfigUpdate).toBeDefined(); }); - it('`onConfigUpdated` function is properly exposed to end user', function () { - expect(onConfigUpdated).toBeDefined(); - }); - it('`setCustomSignals` function is properly exposed to end user', function () { expect(setCustomSignals).toBeDefined(); }); - it('`LastFetchStatus` is properly exposed to end user', function () { - expect(LastFetchStatus.FAILURE).toBeDefined(); - expect(LastFetchStatus.NO_FETCH_YET).toBeDefined(); - expect(LastFetchStatus.SUCCESS).toBeDefined(); - expect(LastFetchStatus.THROTTLED).toBeDefined(); - }); + it('`FetchStatus` and `ValueSource` types are exposed (modular string literal types)', function () { + const fetchStatusFailure: FetchStatus = 'failure'; + const fetchStatusSuccess: FetchStatus = 'success'; + const fetchStatusNoFetchYet: FetchStatus = 'no-fetch-yet'; + const fetchStatusThrottle: FetchStatus = 'throttle'; + expect(fetchStatusFailure).toBe('failure'); + expect(fetchStatusSuccess).toBe('success'); + expect(fetchStatusNoFetchYet).toBe('no-fetch-yet'); + expect(fetchStatusThrottle).toBe('throttle'); - it('`ValueSource` is properly exposed to end user', function () { - expect(ValueSource.DEFAULT).toBeDefined(); - expect(ValueSource.REMOTE).toBeDefined(); - expect(ValueSource.STATIC).toBeDefined(); + const valueSourceDefault: ValueSource = 'default'; + const valueSourceRemote: ValueSource = 'remote'; + const valueSourceStatic: ValueSource = 'static'; + expect(valueSourceDefault).toBe('default'); + expect(valueSourceRemote).toBe('remote'); + expect(valueSourceStatic).toBe('static'); }); describe('test `console.warn` is called for RNFB v8 API & not called for v9 API', function () { @@ -292,7 +285,7 @@ describe('remoteConfig()', function () { describe('remoteConfig functions', function () { it('activate()', function () { - const remoteConfig = getRemoteConfig(); + const remoteConfig = getRemoteConfig() as RemoteConfigWithDeprecationArg; remoteConfigV9Deprecation( () => activate(remoteConfig), () => remoteConfig.activate(), @@ -301,7 +294,7 @@ describe('remoteConfig()', function () { }); it('ensureInitialized()', function () { - const remoteConfig = getRemoteConfig(); + const remoteConfig = getRemoteConfig() as RemoteConfigWithDeprecationArg; remoteConfigV9Deprecation( () => ensureInitialized(remoteConfig), () => remoteConfig.ensureInitialized(), @@ -310,7 +303,7 @@ describe('remoteConfig()', function () { }); it('fetchAndActivate()', function () { - const remoteConfig = getRemoteConfig(); + const remoteConfig = getRemoteConfig() as RemoteConfigWithDeprecationArg; remoteConfigV9Deprecation( () => fetchAndActivate(remoteConfig), () => remoteConfig.fetchAndActivate(), @@ -319,7 +312,7 @@ describe('remoteConfig()', function () { }); it('getAll()', function () { - const remoteConfig = getRemoteConfig(); + const remoteConfig = getRemoteConfig() as RemoteConfigWithDeprecationArg; remoteConfigV9Deprecation( () => getAll(remoteConfig), () => remoteConfig.getAll(), @@ -328,7 +321,7 @@ describe('remoteConfig()', function () { }); it('getBoolean()', function () { - const remoteConfig = getRemoteConfig(); + const remoteConfig = getRemoteConfig() as RemoteConfigWithDeprecationArg; remoteConfigV9Deprecation( () => getBoolean(remoteConfig, 'foo'), () => remoteConfig.getBoolean('foo'), @@ -337,7 +330,7 @@ describe('remoteConfig()', function () { }); it('getNumber()', function () { - const remoteConfig = getRemoteConfig(); + const remoteConfig = getRemoteConfig() as RemoteConfigWithDeprecationArg; remoteConfigV9Deprecation( () => getNumber(remoteConfig, 'foo'), () => remoteConfig.getNumber('foo'), @@ -346,7 +339,7 @@ describe('remoteConfig()', function () { }); it('getString()', function () { - const remoteConfig = getRemoteConfig(); + const remoteConfig = getRemoteConfig() as RemoteConfigWithDeprecationArg; remoteConfigV9Deprecation( () => getString(remoteConfig, 'foo'), () => remoteConfig.getString('foo'), @@ -355,7 +348,7 @@ describe('remoteConfig()', function () { }); it('getValue()', function () { - const remoteConfig = getRemoteConfig(); + const remoteConfig = getRemoteConfig() as RemoteConfigWithDeprecationArg; remoteConfigV9Deprecation( () => getValue(remoteConfig, 'foo'), () => remoteConfig.getValue('foo'), @@ -364,7 +357,7 @@ describe('remoteConfig()', function () { }); it('reset()', function () { - const remoteConfig = getRemoteConfig(); + const remoteConfig = getRemoteConfig() as RemoteConfigWithDeprecationArg; remoteConfigV9Deprecation( () => reset(remoteConfig), () => remoteConfig.reset(), @@ -373,25 +366,27 @@ describe('remoteConfig()', function () { }); it('setConfigSettings()', function () { - const remoteConfig = getRemoteConfig(); + const remoteConfig = getRemoteConfig() as RemoteConfigWithDeprecationArg; + const settings = { minimumFetchIntervalMillis: 12, fetchTimeoutMillis: 60000 }; + remoteConfigV9Deprecation( - () => setConfigSettings(remoteConfig, { minimumFetchIntervalMillis: 12 }), - () => remoteConfig.setConfigSettings({ minimumFetchIntervalMillis: 12 }), + () => setConfigSettings(remoteConfig, settings), + () => remoteConfig.setConfigSettings(settings), 'setConfigSettings', ); }); - it('fetch()', function () { - const remoteConfig = getRemoteConfig(); + it('fetchConfig()', function () { + const remoteConfig = getRemoteConfig() as RemoteConfigWithDeprecationArg; remoteConfigV9Deprecation( - () => fetch(remoteConfig, 12), - () => remoteConfig.fetch(12), - 'fetch', + () => fetchConfig(remoteConfig), + () => remoteConfig.fetch(), + 'fetch', // namespaced method name (proxy passes .fetch) ); }); it('setDefaults()', function () { - const remoteConfig = getRemoteConfig(); + const remoteConfig = getRemoteConfig() as RemoteConfigWithDeprecationArg; remoteConfigV9Deprecation( () => setDefaults(remoteConfig, { foo: 'bar' }), () => remoteConfig.setDefaults({ foo: 'bar' }), @@ -400,28 +395,19 @@ describe('remoteConfig()', function () { }); it('setDefaultsFromResource()', function () { - const remoteConfig = getRemoteConfig(); + const remoteConfig = getRemoteConfig() as RemoteConfigWithDeprecationArg; remoteConfigV9Deprecation( () => setDefaultsFromResource(remoteConfig, 'foo'), () => remoteConfig.setDefaultsFromResource('foo'), 'setDefaultsFromResource', ); }); - - it('onConfigUpdated()', function () { - const remoteConfig = getRemoteConfig(); - remoteConfigV9Deprecation( - () => onConfigUpdated(remoteConfig, () => {}), - () => remoteConfig.onConfigUpdated(() => {}), - 'onConfigUpdated', - ); - }); }); describe('statics', function () { it('LastFetchStatus', function () { staticsV9Deprecation( - () => LastFetchStatus.FAILURE, + () => 'failure' as FetchStatus, () => firebase.remoteConfig.LastFetchStatus.FAILURE, 'LastFetchStatus', ); @@ -429,7 +415,7 @@ describe('remoteConfig()', function () { it('ValueSource', function () { staticsV9Deprecation( - () => ValueSource.DEFAULT, + () => 'default' as ValueSource, () => firebase.remoteConfig.ValueSource.DEFAULT, 'ValueSource', ); diff --git a/packages/remote-config/e2e/config.e2e.js b/packages/remote-config/e2e/config.e2e.js index 44fa77ce6e..88e7b3bd7f 100644 --- a/packages/remote-config/e2e/config.e2e.js +++ b/packages/remote-config/e2e/config.e2e.js @@ -374,7 +374,7 @@ describe('remoteConfig()', function () { describe('fetch()', function () { it('with expiration provided', async function () { - const { getRemoteConfig, ensureInitialized, fetch, LastFetchStatus } = remoteConfigModular; + const { getRemoteConfig, ensureInitialized, fetchConfig } = remoteConfigModular; const date = Date.now() - 30000; const remoteConfig = getRemoteConfig(); await ensureInitialized(remoteConfig); @@ -384,14 +384,14 @@ describe('remoteConfig()', function () { remoteConfig.fetchTimeMillis.should.be.a.Number(); } - await fetch(remoteConfig, 0); - remoteConfig.lastFetchStatus.should.equal(LastFetchStatus.SUCCESS); + await fetchConfig(remoteConfig, 0); + remoteConfig.lastFetchStatus.should.equal('success'); should.equal(getRemoteConfig().fetchTimeMillis >= date, true); }); it('without expiration provided', function () { - const { getRemoteConfig, fetch } = remoteConfigModular; - return fetch(getRemoteConfig()); + const { getRemoteConfig, fetchConfig } = remoteConfigModular; + return fetchConfig(getRemoteConfig()); }); }); @@ -404,14 +404,14 @@ describe('remoteConfig()', function () { describe('activate()', function () { it('with expiration provided', async function () { - const { getRemoteConfig, fetch, activate } = remoteConfigModular; - await fetch(getRemoteConfig(), 0); + const { getRemoteConfig, fetchConfig, activate } = remoteConfigModular; + await fetchConfig(getRemoteConfig(), 0); (await activate(getRemoteConfig())).should.be.a.Boolean(); }); it('without expiration provided', async function () { - const { getRemoteConfig, fetch, activate } = remoteConfigModular; - await fetch(getRemoteConfig()); + const { getRemoteConfig, fetchConfig, activate } = remoteConfigModular; + await fetchConfig(getRemoteConfig()); (await activate(getRemoteConfig())).should.be.a.Boolean(); }); }); 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..77ec4be38b --- /dev/null +++ b/packages/remote-config/lib/index.ts @@ -0,0 +1,29 @@ +/* + * 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 './polyfills'; + +// Export modular types from types/modular +export type * from './types/modular'; + +// Export modular API functions +export * from './modular'; + +// Export namespaced API +export type { FirebaseRemoteConfigTypes } from './types/namespaced'; +export * from './namespaced'; +export { default } from './namespaced'; diff --git a/packages/remote-config/lib/modular.ts b/packages/remote-config/lib/modular.ts new file mode 100644 index 0000000000..df6d25afc8 --- /dev/null +++ b/packages/remote-config/lib/modular.ts @@ -0,0 +1,256 @@ +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 { + ConfigUpdateObserver, + CustomSignals, + LogLevel, + RemoteConfig, + Unsubscribe, + RemoteConfigOptions, + RemoteConfigSettings, + Value, +} from './types/modular'; +import type { + ConfigDefaults, + RemoteConfigWithDeprecationArg, + RemoteConfigWithSetCustomSignalsNative, +} from './types/internal'; + +export type { CustomSignals }; + +/** + * Returns a RemoteConfig instance for the given app. + * @param app - FirebaseApp. Optional. + * @returns RemoteConfig instance + */ +export function getRemoteConfig( + app?: ReactNativeFirebase.FirebaseApp, + options?: RemoteConfigOptions, +): RemoteConfig { + if (app) { + if (options != null) { + return getApp(app.name).remoteConfig(options) as RemoteConfig; + } + + return getApp(app.name).remoteConfig() as RemoteConfig; + } + + if (options != null) { + return getApp().remoteConfig(options) as RemoteConfig; + } + + return getApp().remoteConfig() as 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 + * @param expirationDurationSeconds - Optional. Time in seconds before the fetch expires. + * @returns Promise + */ +export function fetchConfig( + remoteConfig: RemoteConfig, + expirationDurationSeconds?: number, +): Promise { + return rc(remoteConfig).fetch.call( + remoteConfig, + expirationDurationSeconds, + MODULAR_DEPRECATION_ARG, + ); +} + +/** + * Gets all config. + * @param remoteConfig - RemoteConfig instance + * @returns Record of config key to Value + */ +export function getAll(remoteConfig: RemoteConfig): Record { + 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 Value + */ +export function getValue(remoteConfig: RemoteConfig, key: string): Value { + 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 LogLevel + */ +export function setLogLevel(_remoteConfig: RemoteConfig, _logLevel: LogLevel): 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(): Promise { + // always return "true" for now. Web only. + return Promise.resolve(true); +} + +/** + * 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 - RemoteConfigSettings instance + * @returns Promise + */ +export function setConfigSettings( + remoteConfig: RemoteConfig, + settings: RemoteConfigSettings, +): Promise { + return rc(remoteConfig).setConfigSettings.call(remoteConfig, settings, 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 + */ +export function onConfigUpdate( + remoteConfig: RemoteConfig, + observer: ConfigUpdateObserver, +): Unsubscribe { + return rc(remoteConfig).onConfigUpdate.call(remoteConfig, observer, 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 RemoteConfigWithSetCustomSignalsNative; + + await rcInstance._promiseWithConstants( + rcInstance.native.setCustomSignals(customSignals) as Promise<{ + result: unknown; + constants: unknown; + }>, + ); + + return null; +} 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 61% rename from packages/remote-config/lib/index.js rename to packages/remote-config/lib/namespaced.ts index 9364f77334..6bb27630d1 100644 --- a/packages/remote-config/lib/index.js +++ b/packages/remote-config/lib/namespaced.ts @@ -24,6 +24,7 @@ import { isIOS, isFunction, parseListenerOrObserver, + isWeb, } from '@react-native-firebase/app/dist/module/common'; import Value from './RemoteConfigValue'; import { @@ -33,8 +34,18 @@ import { } 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'; +import { version } from './version'; import { LastFetchStatus, ValueSource } from './statics'; +import type { FirebaseRemoteConfigTypes } from './types/namespaced'; +import type { + NativeConstantsResult, + ConfigUpdatedEvent, + SetConfigSettingsWithInternalArg, + SetDefaultsWithInternalArg, + OnConfigUpdatedListenerCallback, + UpdateFromConstantsPayload, +} from './types/internal'; +import type { ReactNativeFirebase } from '@react-native-firebase/app'; const statics = { LastFetchStatus, @@ -44,31 +55,39 @@ const statics = { const namespace = 'remoteConfig'; const nativeModuleName = 'RNFBConfigModule'; -class FirebaseConfigModule extends FirebaseModule { - constructor(...args) { +class FirebaseConfigModule extends FirebaseModule { + _settings: FirebaseRemoteConfigTypes.ConfigSettings; + _lastFetchTime: number; + _values: Record; + _lastFetchStatus: FirebaseRemoteConfigTypes.FetchStatus; + _configUpdateListenerCount: number; + + constructor(...args: ConstructorParameters) { super(...args); this._settings = { // defaults to 1 minute. fetchTimeMillis: 60000, + fetchTimeoutMillis: 60000, // defaults to 12 hours. minimumFetchIntervalMillis: 43200000, }; this._lastFetchTime = -1; + this._lastFetchStatus = 'no_fetch_yet'; this._values = {}; 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 +95,24 @@ 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 SetDefaultsWithInternalArg).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 SetConfigSettingsWithInternalArg).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 +124,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.FetchStatus { return this._lastFetchStatus; } @@ -138,21 +164,25 @@ class FirebaseConfigModule extends FirebaseModule { * Deletes all activated, fetched and defaults configs and resets all Firebase Remote Config settings. * @returns {Promise} */ - reset() { - if (isIOS) { + reset(): Promise { + if (isIOS || isWeb) { return Promise.resolve(null); } - return this._promiseWithConstants(this.native.reset()); } - setConfigSettings(settings) { - const updatedSettings = {}; + setConfigSettings(settings: FirebaseRemoteConfigTypes.ConfigSettings): Promise { + const updatedSettings: { + fetchTimeout: number; + minimumFetchInterval: number; + } = { + fetchTimeout: (this._settings.fetchTimeMillis ?? 60000) / 1000, + minimumFetchInterval: (this._settings.minimumFetchIntervalMillis ?? 43200000) / 1000, + }; - updatedSettings.fetchTimeout = this._settings.fetchTimeMillis / 1000; - updatedSettings.minimumFetchInterval = this._settings.minimumFetchIntervalMillis / 1000; + const apiCalled = + (arguments as unknown as [unknown, unknown])[1] === true ? 'settings' : 'setConfigSettings'; - const apiCalled = arguments[1] == true ? 'settings' : 'setConfigSettings'; if (!isObject(settings)) { throw new Error(`firebase.remoteConfig().${apiCalled}(*): settings must set an object.`); } @@ -184,17 +214,16 @@ class FirebaseConfigModule extends FirebaseModule { * 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.", @@ -206,21 +235,22 @@ class FirebaseConfigModule extends FirebaseModule { ); } - fetchAndActivate() { + fetchAndActivate(): Promise { return this._promiseWithConstants(this.native.fetchAndActivate()); } - ensureInitialized() { + ensureInitialized(): Promise { return this._promiseWithConstants(this.native.ensureInitialized()); } /** * 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.`); } @@ -232,7 +262,7 @@ class FirebaseConfigModule extends FirebaseModule { * 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.", @@ -244,39 +274,43 @@ class FirebaseConfigModule extends FirebaseModule { /** * 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: ConfigUpdatedEvent) => { 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) { this.native.onConfigUpdated(); } @@ -288,11 +322,13 @@ 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) { this.native.removeConfigUpdateRegistration(); } @@ -301,18 +337,21 @@ 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 OnConfigUpdatedListenerCallback; let unsubscribed = false; + const subscription = this.emitter.addListener( this.eventNameForApp('on_config_updated'), - event => { + (event: ConfigUpdatedEvent) => { const { resultType } = event; + if (resultType === 'success') { listener({ updatedKeys: event.updatedKeys }, undefined); return; @@ -325,6 +364,7 @@ class FirebaseConfigModule extends FirebaseModule { }); }, ); + if (this._configUpdateListenerCount === 0) { this.native.onConfigUpdated(); } @@ -336,40 +376,63 @@ 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) { this.native.removeConfigUpdateRegistration(); } }; } - _updateFromConstants(constants) { + _updateFromConstants(constants: UpdateFromConstantsPayload): 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 (c.lastFetchStatus) { + this._lastFetchStatus = c.lastFetchStatus as FirebaseRemoteConfigTypes.FetchStatus; } - this._settings = { - fetchTimeMillis: constants.fetchTimeout * 1000, - minimumFetchIntervalMillis: constants.minimumFetchInterval * 1000, - }; + if (typeof c.fetchTimeout === 'number' && typeof c.minimumFetchInterval === 'number') { + const timeoutMillis = c.fetchTimeout * 1000; + + this._settings = { + fetchTimeMillis: timeoutMillis, + fetchTimeoutMillis: timeoutMillis, + minimumFetchIntervalMillis: c.minimumFetchInterval * 1000, + }; + } else if ( + 'fetchTimeMillis' in c || + 'fetchTimeoutMillis' in c || + 'minimumFetchIntervalMillis' in c + ) { + const s = c as unknown as FirebaseRemoteConfigTypes.ConfigSettings; + const timeoutMillis = + s.fetchTimeoutMillis ?? s.fetchTimeMillis ?? this._settings.fetchTimeoutMillis; + + this._settings = { + fetchTimeMillis: timeoutMillis, + fetchTimeoutMillis: timeoutMillis, + minimumFetchIntervalMillis: + s.minimumFetchIntervalMillis ?? this._settings.minimumFetchIntervalMillis, + }; + } - this._values = Object.freeze(constants.values); + this._values = Object.freeze(c.values); } - _promiseWithConstants(promise) { + _promiseWithConstants(promise: Promise): Promise { return promise.then(({ result, constants }) => { this._updateFromConstants(constants); - return result; + return result as T; }); } } @@ -377,9 +440,19 @@ class FirebaseConfigModule extends FirebaseModule { // import { SDK_VERSION } from '@react-native-firebase/remote-config'; export const SDK_VERSION = version; -// import config from '@react-native-firebase/remote-config'; +// import config, { firebase } from '@react-native-firebase/remote-config'; // config().X(...); -export default createModuleNamespace({ +// firebase.remoteConfig().X(...); +const firebaseRoot = getFirebaseRoot() as unknown as ReactNativeFirebase.FirebaseNamespacedExport< + 'remoteConfig', + FirebaseRemoteConfigTypes.Module, + FirebaseRemoteConfigTypes.Statics, + false +>; + +export const firebase = firebaseRoot; + +const moduleGetter = createModuleNamespace({ statics, version, namespace, @@ -390,11 +463,14 @@ export default createModuleNamespace({ ModuleClass: FirebaseConfigModule, }); -export * from './modular'; +type RemoteConfigDefaultExport = typeof moduleGetter & { firebase: typeof firebase }; -// import config, { firebase } from '@react-native-firebase/remote-config'; -// config().X(...); -// firebase.remoteConfig().X(...); -export const firebase = getFirebaseRoot(); +const defaultExport: RemoteConfigDefaultExport = Object.assign(moduleGetter, { + firebase: firebaseRoot, +}); + +// import config from '@react-native-firebase/remote-config'; +// config().X(...); config.firebase.SDK_VERSION +export default defaultExport; setReactNativeModule(nativeModuleName, fallBackModule); 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/internal.ts b/packages/remote-config/lib/types/internal.ts new file mode 100644 index 0000000000..eb751d657f --- /dev/null +++ b/packages/remote-config/lib/types/internal.ts @@ -0,0 +1,164 @@ +/* + * 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 types for the Remote Config implementation. + * Not part of the public API; use types from modular.ts for public usage. + */ + +/** Shape of constants passed from native after config operations (fetch, activate, setDefaults, etc.). */ +export interface ConstantsUpdate { + lastFetchTime?: number; + lastFetchStatus?: string; + fetchTimeout: number; + minimumFetchInterval: number; + values: Record; +} + +/** Result wrapper from native module methods that return updated constants. */ +export interface NativeConstantsResult { + result: unknown; + constants: ConstantsUpdate; +} + +/** Native event payload for on_config_updated. */ +export interface ConfigUpdatedEvent { + resultType: string; + updatedKeys: string[]; + code: string; + message: string; + nativeErrorMessage: string; +} + +import type { + ConfigUpdateObserver, + CustomSignals, + RemoteConfig, + RemoteConfigSettings, + Unsubscribe, + Value, +} from './modular'; +import type { FirebaseRemoteConfigTypes } from './namespaced'; + +/** Alias for RemoteConfigSettings. Used by modular functions and namespaced bridge. */ +export type ConfigSettings = RemoteConfigSettings; + +/** Alias for default config object shape. */ +export type ConfigDefaults = { [key: string]: string | number | boolean }; + +/** + * Payload for _updateFromConstants: native constants result, defaults, or settings. + * Used to type the parameter so callers pass a known shape; implementation narrows at runtime. + */ +export type UpdateFromConstantsPayload = ConstantsUpdate | ConfigDefaults | ConfigSettings; + +/** + * setConfigSettings when called from the settings setter (internal second arg). + * Used by namespaced bridge; uses namespaced ConfigSettings to match FirebaseRemoteConfigTypes. + */ +export type SetConfigSettingsWithInternalArg = ( + settings: FirebaseRemoteConfigTypes.ConfigSettings, + internal?: boolean, +) => Promise; + +/** + * setDefaults when called from the defaultConfig setter (internal second arg). + * Used by namespaced bridge; uses namespaced ConfigDefaults to match FirebaseRemoteConfigTypes. + */ +export type SetDefaultsWithInternalArg = ( + defaults: FirebaseRemoteConfigTypes.ConfigDefaults, + internal?: boolean, +) => Promise; + +/** + * Listener can be a callback or an object with a next method (Observer shape). + * Used by onConfigUpdated. + * @deprecated use onConfigUpdate with ConfigUpdateObserver + */ +export type CallbackOrObserver any> = T | { next: T }; + +/** + * Callback signature for onConfigUpdated listener. + * @deprecated use onConfigUpdate with ConfigUpdateObserver + */ +export type OnConfigUpdatedListenerCallback = ( + event?: { updatedKeys: string[] }, + error?: { code: string; message: string; nativeErrorMessage: string }, +) => void; + +/** + * 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; + getAll(_dep?: unknown): Record; + getBoolean(key: string, _dep?: unknown): boolean; + getNumber(key: string, _dep?: unknown): number; + getString(key: string, _dep?: unknown): string; + getValue(key: string, _dep?: unknown): Value; + 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; +}; + +/** + * Internal shape of RemoteConfig used by setCustomSignals to call the native bridge. + * Not part of the public API. + */ +export type RemoteConfigWithSetCustomSignalsNative = RemoteConfig & { + _promiseWithConstants: (p: Promise<{ result: unknown; constants: unknown }>) => Promise; + native: { setCustomSignals: (s: CustomSignals) => Promise }; +}; + +/** + * Wrapped native module interface for Remote Config. + * + * Note: React Native Firebase internally wraps native methods and auto-prepends the app name + * when `hasMultiAppSupport` is enabled. This interface represents the *wrapped* module shape + * that is exposed as `this.native` within FirebaseModule subclasses. + */ +export interface RNFBConfigModule { + reset(): Promise; + setConfigSettings(settings: { + fetchTimeout: number; + minimumFetchInterval: number; + }): Promise; + activate(): Promise; + fetch(expirationDurationSeconds: number): Promise; + fetchAndActivate(): Promise; + ensureInitialized(): Promise; + setDefaults(defaults: ConfigDefaults): Promise; + setDefaultsFromResource(resourceName: string): Promise; + onConfigUpdated(): void; + removeConfigUpdateRegistration(): void; +} + +declare module '@react-native-firebase/app/dist/module/internal/NativeModules' { + interface ReactNativeFirebaseNativeModules { + RNFBConfigModule: RNFBConfigModule; + } +} diff --git a/packages/remote-config/lib/types/modular.ts b/packages/remote-config/lib/types/modular.ts new file mode 100644 index 0000000000..8955be6a60 --- /dev/null +++ b/packages/remote-config/lib/types/modular.ts @@ -0,0 +1,324 @@ +/* + * 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. + * + */ + +/** + * Modular Remote Config types aligned with the Firebase Web SDK (@firebase/remote-config). + * Type definitions match the official Firebase JS SDK for compatibility. + */ + +import type { FirebaseApp, ReactNativeFirebase } from '@react-native-firebase/app'; + +/** + * The Firebase Remote Config service interface. + * + * @public + */ +export interface RemoteConfig { + /** + * The {@link FirebaseApp} this `RemoteConfig` instance is associated with. + */ + app: FirebaseApp; + /** + * Defines configuration for the Remote Config SDK. + */ + settings: RemoteConfigSettings; + + /** + * Object containing default values for configs. + */ + defaultConfig: { [key: string]: string | number | boolean }; + + /** + * The Unix timestamp in milliseconds of the last successful fetch, or negative one if + * the {@link RemoteConfig} instance either hasn't fetched or initialization + * is incomplete. + */ + fetchTimeMillis: number; + + /** + * The status of the last fetch attempt. + */ + lastFetchStatus: FetchStatus; +} + +/** + * Defines a self-descriptive reference for config key-value pairs. + * + * @public + */ +export interface FirebaseRemoteConfigObject { + [key: string]: string; +} + +/** + * Defines experiment and variant attached to a config parameter. + * + * Web only. Supported in the Firebase JS SDK; not implemented in React Native Firebase + * at the moment. These types are reserved for a future web implementation. + * + * @public + */ +export interface FirebaseExperimentDescription { + // A string of max length 22 characters and of format: _exp_ + experimentId: string; + + // The variant of the experiment assigned to the app instance. + variantId: string; + + // When the experiment was started. + experimentStartTime: string; + + // How long the experiment can remain in STANDBY state. Valid range from 1 ms + // to 6 months. + triggerTimeoutMillis: string; + + // How long the experiment can remain in ON state. Valid range from 1 ms to 6 + // months. + timeToLiveMillis: string; + + // Which all parameters are affected by this experiment. + affectedParameterKeys?: string[]; +} + +/** + * Defines a successful response (200 or 304). + * + * Modeled after the native `Response` interface, but simplified for Remote Config's + * use case. + * + * Web only. Supported in the Firebase JS SDK; not implemented in React Native Firebase + * at the moment. Reserved for a future web implementation. + * + * @public + */ +export interface FetchResponse { + /** + * The HTTP status, which is useful for differentiating success responses with data from + * those without. + * + *

The Remote Config client is modeled after the native `Fetch` interface, so + * HTTP status is first-class. + * + *

Disambiguation: the fetch response returns a legacy "state" value that is redundant with the + * HTTP status code. The former is normalized into the latter. + */ + status: number; + + /** + * Defines the ETag response header value. + * + *

Only defined for 200 and 304 responses. + */ + eTag?: string; + + /** + * Defines the map of parameters returned as "entries" in the fetch response body. + * + *

Only defined for 200 responses. + */ + config?: FirebaseRemoteConfigObject; + + /** + * The version number of the config template fetched from the server. + */ + templateVersion?: number; + + /** + * Metadata for A/B testing and Remote Config Rollout experiments. + * + * @remarks Only defined for 200 responses. + */ + experiments?: FirebaseExperimentDescription[]; +} + +/** + * Options for Remote Config initialization. + * + * Web only. Supported in the Firebase JS SDK; not implemented in React Native Firebase + * at the moment. Reserved for a future web implementation. + * + * @public + */ +export interface RemoteConfigOptions { + /** + * The ID of the template to use. If not provided, defaults to "firebase". + */ + templateId?: string; + + /** + * Hydrates the state with an initial fetch response. + */ + initialFetchResponse?: FetchResponse; +} + +/** + * Indicates the source of a value. + * + *

    + *
  • "static" indicates the value was defined by a static constant.
  • + *
  • "default" indicates the value was defined by default config.
  • + *
  • "remote" indicates the value was defined by fetched config.
  • + *
+ * + * @public + */ +export type ValueSource = 'static' | 'default' | 'remote'; + +/** + * Wraps a value with metadata and type-safe getters. + * + * @public + */ +export interface Value { + /** + * Gets the value as a boolean. + * + * The following values (case-insensitive) are interpreted as true: + * "1", "true", "t", "yes", "y", "on". Other values are interpreted as false. + */ + asBoolean(): boolean; + + /** + * Gets the value as a number. Comparable to calling Number(value) || 0. + */ + asNumber(): number; + + /** + * Gets the value as a string. + */ + asString(): string; + + /** + * Gets the {@link ValueSource} for the given key. + */ + getSource(): ValueSource; +} + +/** + * Defines configuration options for the Remote Config SDK. + * + * @public + */ +export interface RemoteConfigSettings { + /** + * Defines the maximum age in milliseconds of an entry in the config cache before + * it is considered stale. Defaults to 43200000 (Twelve hours). + */ + minimumFetchIntervalMillis: number; + + /** + * Defines the maximum amount of milliseconds to wait for a response when fetching + * configuration from the Remote Config server. Defaults to 60000 (One minute). + */ + fetchTimeoutMillis: number; +} + +/** + * Summarizes the outcome of the last attempt to fetch config from the Firebase Remote Config server. + * + *
    + *
  • "no-fetch-yet" indicates the {@link RemoteConfig} instance has not yet attempted + * to fetch config, or that SDK initialization is incomplete.
  • + *
  • "success" indicates the last attempt succeeded.
  • + *
  • "failure" indicates the last attempt failed.
  • + *
  • "throttle" indicates the last attempt was rate-limited.
  • + *
+ * + * @public + */ +export type FetchStatus = 'no-fetch-yet' | 'success' | 'failure' | 'throttle'; + +/** + * Defines levels of Remote Config logging. + * + * @public + */ +export type LogLevel = 'debug' | 'error' | 'silent'; + +/** + * Defines the type for representing custom signals and their values. + * + *

The values in CustomSignals must be one of the following types: + * + *

    + *
  • string + *
  • number + *
  • null + *
+ * + * @public + */ +export interface CustomSignals { + [key: string]: string | number | null; +} + +/** + * Contains information about which keys have been updated. + * + * @public + */ +export interface ConfigUpdate { + /** + * Parameter keys whose values have been updated from the currently activated values. + * Includes keys that are added, deleted, or whose value, value source, or metadata has changed. + */ + getUpdatedKeys(): Set; +} + +/** + * Observer interface for receiving real-time Remote Config update notifications. + * + * NOTE: Although an `complete` callback can be provided, it will + * never be called because the ConfigUpdate stream is never-ending. + * + * @public + */ +export interface ConfigUpdateObserver { + /** + * Called when a new ConfigUpdate is available. + */ + next: (configUpdate: ConfigUpdate) => void; + + /** + * Called if an error occurs during the stream. + */ + error: (error: ReactNativeFirebase.NativeFirebaseError) => void; + + /** + * Called when the stream is gracefully terminated. + */ + complete: () => void; +} + +/** + * A function that unsubscribes from a real-time event stream. + * + * @public + */ +export type Unsubscribe = () => void; + +/** + * Indicates the type of fetch request. + * + *
    + *
  • "BASE" indicates a standard fetch request.
  • + *
  • "REALTIME" indicates a fetch request triggered by a real-time update.
  • + *
+ * + * @public + */ +export type FetchType = 'BASE' | 'REALTIME'; diff --git a/packages/remote-config/lib/index.d.ts b/packages/remote-config/lib/types/namespaced.ts similarity index 76% rename from packages/remote-config/lib/index.d.ts rename to packages/remote-config/lib/types/namespaced.ts index fcbd4cb73c..4947a8b93f 100644 --- a/packages/remote-config/lib/index.d.ts +++ b/packages/remote-config/lib/types/namespaced.ts @@ -15,11 +15,17 @@ * */ -import { ReactNativeFirebase } from '@react-native-firebase/app'; +import type { ReactNativeFirebase } from '@react-native-firebase/app'; +import type { RemoteConfigOptions } from './modular'; + +type FirebaseModule = ReactNativeFirebase.FirebaseModule; /** * Firebase Remote RemoteConfig package for React Native. * + * @deprecated Namespaced API is deprecated. Use the modular API from '@react-native-firebase/remote-config' + * (e.g. getRemoteConfig(), getString(), activate()) instead. + * * #### Example 1 * * Access the firebase export from the `config` package: @@ -53,14 +59,11 @@ 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. - */ - export declare type RemoteConfigLogLevel = 'debug' | 'error' | 'silent'; /** * A pseudo-enum for usage with ConfigSettingsRead.lastFetchStatus to determine the last fetch status. + * @deprecated Use the modular API from '@react-native-firebase/remote-config' instead. * * #### Example * @@ -118,6 +121,8 @@ export namespace FirebaseRemoteConfigTypes { * ```js * firebase.remoteConfig.ValueSource; * ``` + * + * @deprecated Use the modular API from '@react-native-firebase/remote-config' instead. */ export interface ValueSource { /** @@ -152,6 +157,8 @@ export namespace FirebaseRemoteConfigTypes { * ```js * firebase.config; * ``` + * + * @deprecated Use the modular API from '@react-native-firebase/remote-config' instead. */ export interface Statics { /** @@ -180,6 +187,7 @@ export namespace FirebaseRemoteConfigTypes { /** * An Interface representing a RemoteConfig value. + * @deprecated Use the modular API from '@react-native-firebase/remote-config' (e.g. getValue()) instead. */ export interface ConfigValue { /** @@ -242,6 +250,8 @@ export namespace FirebaseRemoteConfigTypes { * ```js * const values = firebase.remoteConfig().getAll(); * ``` + * + * @deprecated Use the modular API from '@react-native-firebase/remote-config' (e.g. getAll()) instead. */ export interface ConfigValues { [key: string]: ConfigValue; @@ -259,17 +269,25 @@ export namespace FirebaseRemoteConfigTypes { * fetchTimeMillis: 6000, * }); * ``` + * + * @deprecated Use the modular API from '@react-native-firebase/remote-config' (e.g. setConfigSettings()) instead. */ export interface ConfigSettings { /** * Indicates the default value in milliseconds to set for the minimum interval that needs to elapse * before a fetch request can again be made to the Remote Config server. Defaults to 43200000 (Twelve hours). */ - minimumFetchIntervalMillis?: number; + minimumFetchIntervalMillis: number; /** * Indicates the default value in milliseconds to abandon a pending fetch request made to the Remote Config server. Defaults to 60000 (One minute). + * @deprecated Use fetchTimeoutMillis to match Firebase Web SDK. This is an alias for the same value. */ fetchTimeMillis?: number; + /** + * Defines the maximum amount of milliseconds to wait for a response when fetching configuration. + * Defaults to 60000 (One minute). Matches Firebase Web SDK RemoteConfigSettings. + */ + fetchTimeoutMillis: number; } /** @@ -282,18 +300,23 @@ export namespace FirebaseRemoteConfigTypes { * experiment_enabled: false, * }); * ``` + * + * @deprecated Use the modular API from '@react-native-firebase/remote-config' (e.g. setDefaults()) instead. */ export interface ConfigDefaults { [key: string]: number | string | boolean; } /** - * The status of the latest Remote RemoteConfig fetch action. + * The status of the latest Remote Config fetch action. + * Use with the namespaced API (e.g. `firebase.remoteConfig().lastFetchStatus`). + * @deprecated Use the modular API from '@react-native-firebase/remote-config' (e.g. lastFetchStatus()) instead. */ - type LastFetchStatusType = 'success' | 'failure' | 'no_fetch_yet' | 'throttled'; + export type FetchStatus = 'success' | 'failure' | 'no_fetch_yet' | 'throttled'; /** * Contains information about which keys have been updated. + * @deprecated Use the modular API from '@react-native-firebase/remote-config' (e.g. onConfigUpdate()) instead. */ export interface ConfigUpdate { /** @@ -308,6 +331,8 @@ export namespace FirebaseRemoteConfigTypes { * * NOTE: Although an `complete` callback can be provided, it will * never be called because the ConfigUpdate stream is never-ending. + * + * @deprecated Use the modular API from '@react-native-firebase/remote-config' (e.g. onConfigUpdate()) instead. */ export interface ConfigUpdateObserver { /** @@ -318,7 +343,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. @@ -328,6 +353,7 @@ export namespace FirebaseRemoteConfigTypes { /** * A function that unsubscribes from a real-time event stream. + * @deprecated Use the modular API from '@react-native-firebase/remote-config' instead. */ export type Unsubscribe = () => void; @@ -343,34 +369,41 @@ export namespace FirebaseRemoteConfigTypes { * ```js * const defaultAppRemoteConfig = firebase.remoteConfig(); * ``` + * + * @deprecated Use the modular API from '@react-native-firebase/remote-config' (e.g. getRemoteConfig()) instead. */ - export class Module extends FirebaseModule { + export interface Module extends FirebaseModule { /** * The current `FirebaseApp` instance for this Firebase service. + * @deprecated Use the modular API from '@react-native-firebase/remote-config' instead. */ app: ReactNativeFirebase.FirebaseApp; /** * The number of milliseconds since the last Remote RemoteConfig fetch was performed. + * @deprecated Use the modular API fetchTimeMillis() from '@react-native-firebase/remote-config' instead. */ fetchTimeMillis: number; /** * The status of the latest Remote RemoteConfig fetch action. * * See the `LastFetchStatus` statics definition. + * @deprecated Use the modular API lastFetchStatus() from '@react-native-firebase/remote-config' instead. */ - lastFetchStatus: LastFetchStatusType; + lastFetchStatus: FetchStatus; /** * Provides an object which provides the properties `minimumFetchIntervalMillis` & `fetchTimeMillis` if they have been set * using setConfigSettings({ fetchTimeMillis: number, minimumFetchIntervalMillis: number }). A description of the properties * can be found above * + * @deprecated Use the modular API settings() from '@react-native-firebase/remote-config' instead. */ settings: ConfigSettings; /** * Provides an object with the type ConfigDefaults for default configuration values + * @deprecated Use the modular API from '@react-native-firebase/remote-config' instead. */ defaultConfig: ConfigDefaults; @@ -386,6 +419,7 @@ export namespace FirebaseRemoteConfigTypes { * ``` * * @param configSettings A ConfigSettingsWrite instance used to set Remote RemoteConfig settings. + * @deprecated Use the modular API setConfigSettings() from '@react-native-firebase/remote-config' instead. */ setConfigSettings(configSettings: ConfigSettings): Promise; @@ -402,6 +436,7 @@ export namespace FirebaseRemoteConfigTypes { * ``` * * @param defaults A ConfigDefaults instance used to set default values. + * @deprecated Use the modular API setDefaults() from '@react-native-firebase/remote-config' instead. */ setDefaults(defaults: ConfigDefaults): Promise; @@ -418,6 +453,7 @@ export namespace FirebaseRemoteConfigTypes { * ``` * * @param resourceName The plist/xml file name with no extension. + * @deprecated Use the modular API setDefaultsFromResource() from '@react-native-firebase/remote-config' instead. */ setDefaultsFromResource(resourceName: string): Promise; @@ -434,11 +470,11 @@ 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. + * @deprecated Use the modular API onConfigUpdate() from '@react-native-firebase/remote-config' instead. */ - onConfigUpdate(remoteConfig: RemoteConfig, observer: ConfigUpdateObserver): Unsubscribe; + onConfigUpdate(observer: ConfigUpdateObserver): Unsubscribe; /** * Start listening for real-time config updates from the Remote Config backend and @@ -468,6 +504,8 @@ export namespace FirebaseRemoteConfigTypes { * console.log('Fetched values were already activated.'); * } * ``` + * + * @deprecated Use the modular API activate() from '@react-native-firebase/remote-config' instead. */ activate(): Promise; /** @@ -479,6 +517,8 @@ export namespace FirebaseRemoteConfigTypes { * await firebase.remoteConfig().ensureInitialized(); * // get remote config values * ``` + * + * @deprecated Use the modular API ensureInitialized() from '@react-native-firebase/remote-config' instead. */ ensureInitialized(): Promise; @@ -493,6 +533,7 @@ export namespace FirebaseRemoteConfigTypes { * ``` * * @param expirationDurationSeconds Duration in seconds to cache the data for. To skip cache, use a duration of 0. + * @deprecated Use the modular API fetchConfig() from '@react-native-firebase/remote-config' instead. */ fetch(expirationDurationSeconds?: number): Promise; @@ -513,6 +554,7 @@ export namespace FirebaseRemoteConfigTypes { * } * ``` * + * @deprecated Use the modular API fetchAndActivate() from '@react-native-firebase/remote-config' instead. */ fetchAndActivate(): Promise; @@ -532,6 +574,7 @@ export namespace FirebaseRemoteConfigTypes { * }); * ``` * + * @deprecated Use the modular API getAll() from '@react-native-firebase/remote-config' instead. */ getAll(): ConfigValues; @@ -547,6 +590,7 @@ export namespace FirebaseRemoteConfigTypes { * ``` * * @param key A key used to retrieve a specific value. + * @deprecated Use the modular API getValue() from '@react-native-firebase/remote-config' instead. */ getValue(key: string): ConfigValue; /** @@ -560,6 +604,7 @@ export namespace FirebaseRemoteConfigTypes { * ``` * * @param key A key used to retrieve a specific value. + * @deprecated Use the modular API getBoolean() from '@react-native-firebase/remote-config' instead. */ getBoolean(key: string): boolean; /** @@ -573,6 +618,7 @@ export namespace FirebaseRemoteConfigTypes { * ``` * * @param key A key used to retrieve a specific value. + * @deprecated Use the modular API getString() from '@react-native-firebase/remote-config' instead. */ getString(key: string): string; /** @@ -587,6 +633,7 @@ export namespace FirebaseRemoteConfigTypes { * ``` * * @param key A key used to retrieve a specific value. + * @deprecated Use the modular API getNumber() from '@react-native-firebase/remote-config' instead. */ getNumber(key: string): number; @@ -601,14 +648,19 @@ export namespace FirebaseRemoteConfigTypes { * // get remote config values * ``` * + * @deprecated Use the modular API reset() from '@react-native-firebase/remote-config' instead. */ reset(): Promise; } - // deprecated: from pre-Web realtime remote-config support - remove with onConfigUpdated + /** + * @deprecated Use the modular API onConfigUpdate() from '@react-native-firebase/remote-config' instead. + */ export type CallbackOrObserver any> = T | { next: T }; - // deprecated: from pre-Web realtime remote-config support - remove with onConfigUpdated + /** + * @deprecated Use the modular API onConfigUpdate() from '@react-native-firebase/remote-config' instead. + */ export type OnConfigUpdatedListenerCallback = ( event?: { updatedKeys: string[] }, error?: { @@ -619,41 +671,24 @@ 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.` + * + * @deprecated Namespaced API is deprecated. Use the modular API from '@react-native-firebase/remote-config' instead. */ 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< + /** @deprecated Use getRemoteConfig() from '@react-native-firebase/remote-config' instead. */ + remoteConfig: ReactNativeFirebase.FirebaseModuleWithStatics< FirebaseRemoteConfigTypes.Module, FirebaseRemoteConfigTypes.Statics >; } interface FirebaseApp { - remoteConfig(): FirebaseRemoteConfigTypes.Module; + /** @deprecated Use getRemoteConfig(app) from '@react-native-firebase/remote-config' instead. */ + remoteConfig(options?: RemoteConfigOptions): FirebaseRemoteConfigTypes.Module; } } } 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.ts similarity index 50% rename from packages/remote-config/lib/web/RNFBConfigModule.js rename to packages/remote-config/lib/web/RNFBConfigModule.ts index 422d45a1fe..0efa3641fb 100644 --- a/packages/remote-config/lib/web/RNFBConfigModule.js +++ b/packages/remote-config/lib/web/RNFBConfigModule.ts @@ -1,4 +1,5 @@ import '../polyfills'; + import { getApp, getRemoteConfig, @@ -11,76 +12,118 @@ import { 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 } +import type { RemoteConfig } from '../types/modular'; +import type { ConfigDefaults } from '../types/internal'; + +type RemoteConfigInstance = RemoteConfig & { + settings: Record; + defaultConfig: ConfigDefaults; + fetchTimeMillis: number; + lastFetchStatus: string; }; -function makeGlobalsAvailable() { - navigator.onLine = true; +const configSettingsForInstance: Record< + string, + { minimumFetchIntervalMillis: number; fetchTimeoutMillis: number } +> = {}; + +const defaultConfigForInstance: Record = {}; + +function makeGlobalsAvailable(): void { + (navigator as { onLine?: boolean }).onLine = true; makeIDBAvailable(); } -const onConfigUpdateListeners = {}; +const onConfigUpdateListeners: Record void> = {}; -function getRemoteConfigInstanceForApp(appName, overrides /*: RemoteConfigSettings */) { +function getRemoteConfigInstanceForApp( + appName: string, + overrides?: Record, +): RemoteConfigInstance { makeGlobalsAvailable(); const configSettings = configSettingsForInstance[appName] ?? { minimumFetchIntervalMillis: 43200000, fetchTimeoutMillis: 60000, }; + const defaultConfig = defaultConfigForInstance[appName] ?? {}; - Object.assign(configSettings, overrides); + + if (overrides) { + Object.assign(configSettings, overrides); + } + const app = getApp(appName); - const instance = getRemoteConfig(app); + const instance = getRemoteConfig(app) as unknown as RemoteConfigInstance; + for (const key in configSettings) { - instance.settings[key] = configSettings[key]; + (instance.settings as Record)[key] = + configSettings[key as keyof typeof configSettings]; } + instance.defaultConfig = defaultConfig; return instance; } -async function resultAndConstants(instance, result) { - const response = { result }; - const valuesRaw = getAll(instance); - const values = {}; +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]; - values[key] = { - source: raw.getSource(), - value: raw.asString(), - }; + + 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.replace(/-/g, '_'), - minimumFetchInterval: instance.settings.minimumFetchIntervalMillis - ? instance.settings.minimumFetchIntervalMillis / 1000 + lastFetchStatus: (instance.lastFetchStatus as string).replace(/-/g, '_'), + minimumFetchInterval: settings.minimumFetchIntervalMillis + ? settings.minimumFetchIntervalMillis / 1000 : 43200, - fetchTimeout: instance.settings.fetchTimeoutMillis - ? instance.settings.fetchTimeoutMillis / 1000 - : 60, + fetchTimeout: settings.fetchTimeoutMillis ? settings.fetchTimeoutMillis / 1000 : 60, }; + return response; } export default { - activate(appName) { + activate(appName: string) { return guard(async () => { const remoteConfig = getRemoteConfigInstanceForApp(appName); - return resultAndConstants(remoteConfig, await activate(remoteConfig)); + return resultAndConstants(remoteConfig, await activate(remoteConfig as any)); }); }, - setConfigSettings(appName, settings) { + + setConfigSettings( + appName: string, + settings: { minimumFetchInterval: number; fetchTimeout: number }, + ) { return guard(async () => { configSettingsForInstance[appName] = { minimumFetchIntervalMillis: settings.minimumFetchInterval * 1000, @@ -90,46 +133,52 @@ export default { return resultAndConstants(remoteConfig, null); }); }, - fetch(appName, expirationDurationSeconds) { + + fetch(appName: string, expirationDurationSeconds: number) { return guard(async () => { - let overrides = {}; - if (expirationDurationSeconds != -1) { - overrides.minimumFetchIntervalMillis = expirationDurationSeconds * 1000; + let overrides: Record = {}; + if (expirationDurationSeconds !== -1) { + overrides = { minimumFetchIntervalMillis: expirationDurationSeconds * 1000 }; } const remoteConfig = getRemoteConfigInstanceForApp(appName, overrides); - await fetchConfig(remoteConfig); + await fetchConfig(remoteConfig as any); return resultAndConstants(remoteConfig, null); }); }, - fetchAndActivate(appName) { + + fetchAndActivate(appName: string) { return guard(async () => { const remoteConfig = getRemoteConfigInstanceForApp(appName); - const activated = await fetchAndActivate(remoteConfig); + const activated = await fetchAndActivate(remoteConfig as any); return resultAndConstants(remoteConfig, activated); }); }, - ensureInitialized(appName) { + + ensureInitialized(appName: string) { return guard(async () => { const remoteConfig = getRemoteConfigInstanceForApp(appName); - await ensureInitialized(remoteConfig); + await ensureInitialized(remoteConfig as any); return resultAndConstants(remoteConfig, null); }); }, - setDefaults(appName, defaults) { + + setDefaults(appName: string, defaults: ConfigDefaults) { return guard(async () => { defaultConfigForInstance[appName] = defaults; const remoteConfig = getRemoteConfigInstanceForApp(appName); return resultAndConstants(remoteConfig, null); }); }, - setCustomSignals(appName, customSignals) { + + setCustomSignals(appName: string, customSignals: Record) { return guard(async () => { const remoteConfig = getRemoteConfigInstanceForApp(appName); - await setCustomSignals(remoteConfig, customSignals); + await setCustomSignals(remoteConfig as any, customSignals); return resultAndConstants(remoteConfig, null); }); }, - onConfigUpdated(appName) { + + onConfigUpdated(appName: string): void { if (onConfigUpdateListeners[appName]) { return; } @@ -137,25 +186,26 @@ export default { const remoteConfig = getRemoteConfigInstanceForApp(appName); const nativeObserver = { - next: configUpdate => { + next: (configUpdate: { getUpdatedKeys: () => Set }) => { emitEvent('on_config_updated', { appName, resultType: 'success', updatedKeys: Array.from(configUpdate.getUpdatedKeys()), }); }, - error: firebaseError => { + error: (firebaseError: unknown) => { emitEvent('on_config_updated', { appName, - event: getWebError(firebaseError), + event: getWebError(firebaseError as Error & { code?: string }), }); }, complete: () => {}, }; - onConfigUpdateListeners[appName] = onConfigUpdate(remoteConfig, nativeObserver); + onConfigUpdateListeners[appName] = onConfigUpdate(remoteConfig as any, nativeObserver); }, - removeConfigUpdateRegistration(appName) { + + removeConfigUpdateRegistration(appName: string): void { if (!onConfigUpdateListeners[appName]) { return; } 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..827c2ce579 100644 --- a/packages/remote-config/type-test.ts +++ b/packages/remote-config/type-test.ts @@ -1,6 +1,11 @@ +import type { + CustomSignals, + FetchStatus, + FirebaseRemoteConfigTypes, + ValueSource, +} from '@react-native-firebase/remote-config'; import remoteConfig, { firebase, - FirebaseRemoteConfigTypes, getRemoteConfig, activate, ensureInitialized, @@ -13,20 +18,13 @@ import remoteConfig, { getValue, setLogLevel, isSupported, - fetchTimeMillis, - settings, - lastFetchStatus, reset, setConfigSettings, - fetch, setDefaults, setDefaultsFromResource, onConfigUpdate, - onConfigUpdated, setCustomSignals, - LastFetchStatus, - ValueSource, -} from '.'; +} from '@react-native-firebase/remote-config'; console.log(remoteConfig().app); @@ -60,7 +58,7 @@ console.log(firebase.SDK_VERSION); 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(); @@ -70,11 +68,19 @@ console.log(remoteConfigInstance.lastFetchStatus); console.log(remoteConfigInstance.settings); console.log(remoteConfigInstance.defaultConfig); -remoteConfigInstance.setConfigSettings({ minimumFetchIntervalMillis: 30000 }).then(() => { - console.log('Config settings set'); -}); +remoteConfigInstance + .setConfigSettings({ + minimumFetchIntervalMillis: 30000, + fetchTimeoutMillis: 60000, + }) + .then(() => { + console.log('Config settings set'); + }); -remoteConfigInstance.settings = { minimumFetchIntervalMillis: 60000 }; +remoteConfigInstance.settings = { + minimumFetchIntervalMillis: 60000, + fetchTimeoutMillis: 60000, +}; remoteConfigInstance.setDefaults({ key: 'value' }).then(() => { console.log('Defaults set'); @@ -86,7 +92,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()); }, @@ -192,24 +198,27 @@ isSupported().then((supported: boolean) => { console.log(supported); }); -console.log(fetchTimeMillis(modularRemoteConfig1)); -console.log(settings(modularRemoteConfig1)); -console.log(lastFetchStatus(modularRemoteConfig1)); +console.log(modularRemoteConfig1.fetchTimeMillis); +console.log(modularRemoteConfig1.settings); +console.log(modularRemoteConfig1.lastFetchStatus); reset(modularRemoteConfig1).then(() => { console.log('Modular reset'); }); -setConfigSettings(modularRemoteConfig1, { minimumFetchIntervalMillis: 30000 }).then(() => { +setConfigSettings(modularRemoteConfig1, { + minimumFetchIntervalMillis: 30000, + fetchTimeoutMillis: 60000, +}).then(() => { console.log('Modular config settings set'); }); -fetch(modularRemoteConfig1).then(() => { - console.log('Modular fetch'); +fetchConfig(modularRemoteConfig1).then(() => { + console.log('Modular fetch config'); }); -fetch(modularRemoteConfig1, 300).then(() => { - console.log('Modular fetch with expiration'); +fetchConfig(modularRemoteConfig1, 300).then(() => { + console.log('Modular fetchConfig with expiration'); }); setDefaults(modularRemoteConfig1, { modularKey: 'modularValue' }).then(() => { @@ -233,26 +242,14 @@ const modularUnsubscribeOnConfigUpdate = onConfigUpdate(modularRemoteConfig1, { }); modularUnsubscribeOnConfigUpdate(); -const modularUnsubscribeOnConfigUpdated = onConfigUpdated( - modularRemoteConfig1, - ( - event?: { updatedKeys: string[] }, - error?: { code: string; message: string; nativeErrorMessage: string }, - ) => { - if (event) { - console.log(event.updatedKeys); - } - if (error) { - console.log(error); - } - }, -); -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'); }); -// checks modular statics exports -console.log(LastFetchStatus.SUCCESS); -console.log(ValueSource.REMOTE); +// checks modular statics (string literal types per Firebase Web SDK; use literals in code) +const fetchStatusSuccess: FetchStatus = 'success'; +const valueSourceRemote: ValueSource = 'remote'; +console.log(fetchStatusSuccess); +console.log(valueSourceRemote); 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. + + +