Skip to content

Add environment check to Remote-Config Module #3623

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Sep 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/blue-rice-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"firebase": minor
"@firebase/remote-config": minor
---

Issue 2393 - Add environment check to Remote-Config Module
3 changes: 3 additions & 0 deletions common/api-review/remote-config.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export function getString(remoteConfig: RemoteConfig, key: string): string;
// @public
export function getValue(remoteConfig: RemoteConfig, key: string): Value;

// @public
export function isSupported(): Promise<boolean>;

// @public
export type LogLevel = 'debug' | 'error' | 'silent';

Expand Down
23 changes: 18 additions & 5 deletions packages/firebase/compat/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2031,6 +2031,16 @@ declare namespace firebase.remoteConfig {
* Defines levels of Remote Config logging.
*/
export type LogLevel = 'debug' | 'error' | 'silent';
/**
* This method provides two different checks:
*
* 1. Check if IndexedDB exists in the browser environment.
* 2. Check if the current browser context allows IndexedDB `open()` calls.
*
* It returns a `Promise` which resolves to true if a {@link RemoteConfig} instance
* can be initialized in this environment, or false if it cannot.
*/
export function isSupported(): Promise<boolean>;
}

declare namespace firebase.functions {
Expand Down Expand Up @@ -4623,13 +4633,14 @@ declare namespace firebase.auth {
* instance must be initialized with an API key, otherwise an error will be
* thrown.
*/
class RecaptchaVerifier extends RecaptchaVerifier_Instance { }
class RecaptchaVerifier extends RecaptchaVerifier_Instance {}
/**
* @webonly
* @hidden
*/
class RecaptchaVerifier_Instance
implements firebase.auth.ApplicationVerifier {
implements firebase.auth.ApplicationVerifier
{
constructor(
container: any | string,
parameters?: Object | null,
Expand Down Expand Up @@ -7310,7 +7321,7 @@ declare namespace firebase.database {

interface ThenableReference
extends firebase.database.Reference,
Pick<Promise<Reference>, 'then' | 'catch'> { }
Pick<Promise<Reference>, 'then' | 'catch'> {}

/**
* Logs debugging information to the console.
Expand Down Expand Up @@ -7690,7 +7701,9 @@ declare namespace firebase.storage {
* resolves with the full updated metadata or rejects if the updated failed,
* including if the object did not exist.
*/
updateMetadata(metadata: firebase.storage.SettableMetadata): Promise<FullMetadata>;
updateMetadata(
metadata: firebase.storage.SettableMetadata
): Promise<FullMetadata>;
/**
* List all items (files) and prefixes (folders) under this storage reference.
*
Expand Down Expand Up @@ -9520,7 +9533,7 @@ declare namespace firebase.firestore {
*/
export class QueryDocumentSnapshot<
T = DocumentData
> extends DocumentSnapshot<T> {
> extends DocumentSnapshot<T> {
private constructor();

/**
Expand Down
6 changes: 4 additions & 2 deletions packages/remote-config-compat/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
ComponentType,
InstanceFactoryOptions
} from '@firebase/component';
import { RemoteConfigCompatImpl } from './remoteConfig';
import { RemoteConfigCompatImpl, isSupported } from './remoteConfig';
import { name as packageName, version } from '../package.json';
import { RemoteConfig as RemoteConfigCompat } from '@firebase/remote-config-types';

Expand All @@ -34,7 +34,9 @@ function registerRemoteConfigCompat(
'remoteConfig-compat',
remoteConfigFactory,
ComponentType.PUBLIC
).setMultipleInstances(true)
)
.setMultipleInstances(true)
.setServiceProps({ isSupported })
);

firebaseInstance.registerVersion(packageName, version);
Expand Down
8 changes: 6 additions & 2 deletions packages/remote-config-compat/src/remoteConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,15 @@ import {
getBoolean,
getNumber,
getString,
getValue
getValue,
isSupported
} from '@firebase/remote-config';

export { isSupported };

export class RemoteConfigCompatImpl
implements RemoteConfigCompat, _FirebaseService {
implements RemoteConfigCompat, _FirebaseService
{
constructor(public app: FirebaseApp, readonly _delegate: RemoteConfig) {}

get defaultConfig(): { [key: string]: string | number | boolean } {
Expand Down
29 changes: 28 additions & 1 deletion packages/remote-config/src/api2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@

import { RemoteConfig } from './public_types';
import { activate, fetchConfig } from './api';
import { getModularInstance } from '@firebase/util';
import {
getModularInstance,
isIndexedDBAvailable,
validateIndexedDBOpenable
} from '@firebase/util';

// This API is put in a separate file, so we can stub fetchConfig and activate in tests.
// It's not possible to stub standalone functions from the same module.
Expand All @@ -39,3 +43,26 @@ export async function fetchAndActivate(
await fetchConfig(remoteConfig);
return activate(remoteConfig);
}

/**
* This method provides two different checks:
*
* 1. Check if IndexedDB exists in the browser environment.
* 2. Check if the current browser context allows IndexedDB `open()` calls.
*
* @returns A `Promise` which resolves to true if a {@link RemoteConfig} instance
* can be initialized in this environment, or false if it cannot.
* @public
*/
export async function isSupported(): Promise<boolean> {
if (!isIndexedDBAvailable()) {
return false;
}

try {
const isDBOpenable: boolean = await validateIndexedDBOpenable();
return isDBOpenable;
} catch (error) {
return false;
}
}
7 changes: 5 additions & 2 deletions packages/remote-config/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export const enum ErrorCode {
FETCH_TIMEOUT = 'fetch-timeout',
FETCH_THROTTLE = 'fetch-throttle',
FETCH_PARSE = 'fetch-client-parse',
FETCH_STATUS = 'fetch-status'
FETCH_STATUS = 'fetch-status',
INDEXED_DB_UNAVAILABLE = 'indexed-db-unavailable'
}

const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = {
Expand Down Expand Up @@ -64,7 +65,9 @@ const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = {
'Fetch client could not parse response.' +
' Original error: {$originalErrorMessage}.',
[ErrorCode.FETCH_STATUS]:
'Fetch server returned an HTTP error status. HTTP status: {$httpStatus}.'
'Fetch server returned an HTTP error status. HTTP status: {$httpStatus}.',
[ErrorCode.INDEXED_DB_UNAVAILABLE]:
'Indexed DB is not supported by current browser'
};

// Note this is effectively a type system binding a code to params. This approach overlaps with the
Expand Down
6 changes: 5 additions & 1 deletion packages/remote-config/src/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
registerVersion,
SDK_VERSION
} from '@firebase/app';
import { isIndexedDBAvailable } from '@firebase/util';
import {
Component,
ComponentType,
Expand Down Expand Up @@ -68,7 +69,10 @@ export function registerRemoteConfig(): void {
if (typeof window === 'undefined') {
throw ERROR_FACTORY.create(ErrorCode.REGISTRATION_WINDOW);
}

// Guards against the SDK being used when indexedDB is not available.
if (!isIndexedDBAvailable()) {
throw ERROR_FACTORY.create(ErrorCode.INDEXED_DB_UNAVAILABLE);
}
// Normalizes optional inputs.
const { projectId, apiKey, appId } = app.options;
if (!projectId) {
Expand Down
50 changes: 29 additions & 21 deletions packages/remote-config/src/storage/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,28 +75,36 @@ type ProjectNamespaceKeyFieldValue =
// Visible for testing.
export function openDatabase(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = event => {
reject(toFirebaseError(event, ErrorCode.STORAGE_OPEN));
};
request.onsuccess = event => {
resolve((event.target as IDBOpenDBRequest).result);
};
request.onupgradeneeded = event => {
const db = (event.target as IDBOpenDBRequest).result;
try {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = event => {
reject(toFirebaseError(event, ErrorCode.STORAGE_OPEN));
};
request.onsuccess = event => {
resolve((event.target as IDBOpenDBRequest).result);
};
request.onupgradeneeded = event => {
const db = (event.target as IDBOpenDBRequest).result;

// We don't use 'break' in this switch statement, the fall-through
// behavior is what we want, because if there are multiple versions between
// the old version and the current version, we want ALL the migrations
// that correspond to those versions to run, not only the last one.
// eslint-disable-next-line default-case
switch (event.oldVersion) {
case 0:
db.createObjectStore(APP_NAMESPACE_STORE, {
keyPath: 'compositeKey'
});
}
};
// We don't use 'break' in this switch statement, the fall-through
// behavior is what we want, because if there are multiple versions between
// the old version and the current version, we want ALL the migrations
// that correspond to those versions to run, not only the last one.
// eslint-disable-next-line default-case
switch (event.oldVersion) {
case 0:
db.createObjectStore(APP_NAMESPACE_STORE, {
keyPath: 'compositeKey'
});
}
};
} catch (error) {
reject(
ERROR_FACTORY.create(ErrorCode.STORAGE_OPEN, {
originalErrorMessage: error
})
);
}
});
}

Expand Down