From 44b8c463478be1e41555999da82dbfbdb7df6e7e Mon Sep 17 00:00:00 2001 From: Christina Holland Date: Wed, 16 Jun 2021 12:34:11 -0700 Subject: [PATCH 1/8] Add public token API --- packages/app-check-interop-types/index.d.ts | 9 +- packages/app-check-types/index.d.ts | 46 ++++++ packages/app-check/src/api.test.ts | 151 +++++++++++++++++++- packages/app-check/src/api.ts | 81 ++++++++++- packages/app-check/src/factory.ts | 47 +++++- packages/app-check/src/index.ts | 3 +- packages/app-check/src/internal-api.test.ts | 35 +++-- packages/app-check/src/internal-api.ts | 43 ++++-- packages/firebase/index.d.ts | 58 ++++++++ 9 files changed, 433 insertions(+), 40 deletions(-) diff --git a/packages/app-check-interop-types/index.d.ts b/packages/app-check-interop-types/index.d.ts index d2309c1b4dd..a6dc9a57a02 100644 --- a/packages/app-check-interop-types/index.d.ts +++ b/packages/app-check-interop-types/index.d.ts @@ -24,13 +24,16 @@ export interface FirebaseAppCheckInternal { // registered at the same time for one or more FirebaseAppAttestation instances. The // listeners call back on the UI thread whenever the current token associated with this // FirebaseAppAttestation changes. - addTokenListener(listener: AppCheckTokenListener): void; + addTokenListener(listener: (token: AppCheckTokenResult) => void): void; // Unregisters a listener to changes in the token state. - removeTokenListener(listener: AppCheckTokenListener): void; + removeTokenListener(listener: (token: AppCheckTokenResult) => void): void; } -type AppCheckTokenListener = (token: AppCheckTokenResult) => void; +interface AppCheckTokenListener { + listener: (token: AppCheckTokenResult) => void; + onError?: (error: Error) => void; +} // If the error field is defined, the token field will be populated with a dummy token interface AppCheckTokenResult { diff --git a/packages/app-check-types/index.d.ts b/packages/app-check-types/index.d.ts index 16fc46f373f..1bcc5418074 100644 --- a/packages/app-check-types/index.d.ts +++ b/packages/app-check-types/index.d.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +import { PartialObserver, Unsubscribe } from '@firebase/util'; + export interface FirebaseAppCheck { /** * Activate AppCheck @@ -36,6 +38,40 @@ export interface FirebaseAppCheck { * during `activate()`. */ setTokenAutoRefreshEnabled(isTokenAutoRefreshEnabled: boolean): void; + + /** + * Get the current App Check token. Attaches to the most recent + * in-flight request if one is present. Returns null if no token + * is present and no token requests are in-flight. + * + * @param forceRefresh - If true, will always try to fetch a fresh token. + * If false, will use a cached token if found in storage. + */ + getToken(forceRefresh?: boolean): Promise; + + /** + * Registers a listener to changes in the token state. There can be more + * than one listener registered at the same time for one or more + * App Check instances. The listeners call back on the UI thread whenever + * the current token associated with this App Check instance changes. + * + * @returns A function that unsubscribes this listener. + */ + onTokenChanged(observer: PartialObserver): Unsubscribe; + + /** + * Registers a listener to changes in the token state. There can be more + * than one listener registered at the same time for one or more + * App Check instances. The listeners call back on the UI thread whenever + * the current token associated with this App Check instance changes. + * + * @returns A function that unsubscribes this listener. + */ + onTokenChanged( + onNext: (tokenResult: AppCheckTokenResult) => void, + onError?: (error: Error) => void, + onCompletion?: () => void + ): Unsubscribe; } /** @@ -64,6 +100,16 @@ interface AppCheckToken { readonly expireTimeMillis: number; } +/** + * Result returned by `getToken()`. + */ +interface AppCheckTokenResult { + /** + * The token string in JWT format. + */ + readonly token: string; +} + export type AppCheckComponentName = 'appCheck'; declare module '@firebase/component' { interface NameServiceMapping { diff --git a/packages/app-check/src/api.test.ts b/packages/app-check/src/api.test.ts index 89cc8fe06c4..404b45e166a 100644 --- a/packages/app-check/src/api.test.ts +++ b/packages/app-check/src/api.test.ts @@ -16,16 +16,26 @@ */ import '../test/setup'; import { expect } from 'chai'; -import { stub } from 'sinon'; -import { activate, setTokenAutoRefreshEnabled } from './api'; +import { stub, spy, restore } from 'sinon'; +import { + activate, + setTokenAutoRefreshEnabled, + getToken, + onTokenChanged +} from './api'; import { FAKE_SITE_KEY, getFakeApp, - getFakeCustomTokenProvider + getFakeCustomTokenProvider, + getFakePlatformLoggingProvider, + removegreCAPTCHAScriptsOnPage } from '../test/util'; -import { getState } from './state'; +import { clearState, getState } from './state'; import * as reCAPTCHA from './recaptcha'; import { FirebaseApp } from '@firebase/app-types'; +import * as internalApi from './internal-api'; +import * as client from './client'; +import * as storage from './storage'; describe('api', () => { describe('activate()', () => { @@ -86,4 +96,137 @@ describe('api', () => { expect(getState(app).isTokenAutoRefreshEnabled).to.equal(true); }); }); + describe('getToken()', () => { + it('getToken() calls the internal getToken() function', async () => { + const app = getFakeApp({ automaticDataCollectionEnabled: true }); + const fakePlatformLoggingProvider = getFakePlatformLoggingProvider(); + const internalGetToken = stub(internalApi, 'getToken').resolves({ + token: 'a-token-string' + }); + await getToken(app, fakePlatformLoggingProvider, true); + expect(internalGetToken).to.be.calledWith( + app, + fakePlatformLoggingProvider, + true + ); + }); + it('getToken() throws errors returned with token', async () => { + const app = getFakeApp({ automaticDataCollectionEnabled: true }); + const fakePlatformLoggingProvider = getFakePlatformLoggingProvider(); + stub(internalApi, 'getToken').resolves({ + token: 'a-token-string', + error: Error('there was an error') + }); + await expect( + getToken(app, fakePlatformLoggingProvider, true) + ).to.be.rejectedWith('there was an error'); + }); + }); + describe('onTokenChanged()', () => { + afterEach(() => { + clearState(); + removegreCAPTCHAScriptsOnPage(); + }); + it('Listeners work when using top-level parameters pattern', async () => { + const app = getFakeApp({ automaticDataCollectionEnabled: true }); + activate(app, FAKE_SITE_KEY, true); + const fakePlatformLoggingProvider = getFakePlatformLoggingProvider(); + const fakeRecaptchaToken = 'fake-recaptcha-token'; + const fakeRecaptchaAppCheckToken = { + token: 'fake-recaptcha-app-check-token', + expireTimeMillis: 123, + issuedAtTimeMillis: 0 + }; + stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken)); + stub(client, 'exchangeToken').returns( + Promise.resolve(fakeRecaptchaAppCheckToken) + ); + stub(storage, 'writeTokenToStorage').returns(Promise.resolve(undefined)); + + const listener1 = (): void => { + throw new Error(); + }; + const listener2 = spy(); + + const errorFn1 = spy(); + const errorFn2 = spy(); + + const unSubscribe1 = onTokenChanged( + app, + fakePlatformLoggingProvider, + listener1, + errorFn1 + ); + const unSubscribe2 = onTokenChanged( + app, + fakePlatformLoggingProvider, + listener2, + errorFn2 + ); + + expect(getState(app).tokenListeners.length).to.equal(2); + + await getToken(app, fakePlatformLoggingProvider); + + expect(listener2).to.be.calledWith({ + token: fakeRecaptchaAppCheckToken.token + }); + expect(errorFn1).to.be.calledOnce; + expect(errorFn2).to.not.be.called; + unSubscribe1(); + unSubscribe2(); + expect(getState(app).tokenListeners.length).to.equal(0); + }); + + it('Listeners work when using Observer pattern', async () => { + const app = getFakeApp({ automaticDataCollectionEnabled: true }); + activate(app, FAKE_SITE_KEY, true); + const fakePlatformLoggingProvider = getFakePlatformLoggingProvider(); + const fakeRecaptchaToken = 'fake-recaptcha-token'; + const fakeRecaptchaAppCheckToken = { + token: 'fake-recaptcha-app-check-token', + expireTimeMillis: 123, + issuedAtTimeMillis: 0 + }; + stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken)); + stub(client, 'exchangeToken').returns( + Promise.resolve(fakeRecaptchaAppCheckToken) + ); + stub(storage, 'writeTokenToStorage').returns(Promise.resolve(undefined)); + + const listener1 = (): void => { + throw new Error(); + }; + const listener2 = spy(); + + const errorFn1 = spy(); + const errorFn2 = spy(); + + /** + * Reverse the order of adding the failed and successful handler, for extra + * testing. + */ + const unSubscribe2 = onTokenChanged(app, fakePlatformLoggingProvider, { + next: listener2, + error: errorFn2 + }); + const unSubscribe1 = onTokenChanged(app, fakePlatformLoggingProvider, { + next: listener1, + error: errorFn1 + }); + + expect(getState(app).tokenListeners.length).to.equal(2); + + await getToken(app, fakePlatformLoggingProvider); + + expect(listener2).to.be.calledWith({ + token: fakeRecaptchaAppCheckToken.token + }); + expect(errorFn1).to.be.calledOnce; + expect(errorFn2).to.not.be.called; + unSubscribe1(); + unSubscribe2(); + expect(getState(app).tokenListeners.length).to.equal(0); + }); + }); }); diff --git a/packages/app-check/src/api.ts b/packages/app-check/src/api.ts index 7913a114127..0cb3e3ea604 100644 --- a/packages/app-check/src/api.ts +++ b/packages/app-check/src/api.ts @@ -15,11 +15,21 @@ * limitations under the License. */ -import { AppCheckProvider } from '@firebase/app-check-types'; +import { + AppCheckProvider, + AppCheckTokenResult +} from '@firebase/app-check-types'; import { FirebaseApp } from '@firebase/app-types'; import { ERROR_FACTORY, AppCheckError } from './errors'; import { initialize as initializeRecaptcha } from './recaptcha'; import { getState, setState, AppCheckState } from './state'; +import { + getToken as getTokenInternal, + addTokenListener, + removeTokenListener +} from './internal-api'; +import { Provider } from '@firebase/component'; +import { ErrorFn, NextFn, PartialObserver, Unsubscribe } from '@firebase/util'; /** * @@ -82,3 +92,72 @@ export function setTokenAutoRefreshEnabled( } setState(app, { ...state, isTokenAutoRefreshEnabled }); } + +/** + * Differs from internal getToken in that it throws the error. + */ +export async function getToken( + app: FirebaseApp, + platformLoggerProvider: Provider<'platform-logger'>, + forceRefresh?: boolean +): Promise { + const result = await getTokenInternal( + app, + platformLoggerProvider, + forceRefresh + ); + if (result.error) { + throw result.error; + } + return { token: result.token }; +} + +/** + * Wraps addTokenListener/removeTokenListener methods in an Observer + * pattern for public use. + */ +export function onTokenChanged( + app: FirebaseApp, + platformLoggerProvider: Provider<'platform-logger'>, + observer: PartialObserver +): Unsubscribe; +export function onTokenChanged( + app: FirebaseApp, + platformLoggerProvider: Provider<'platform-logger'>, + onNext: (tokenResult: AppCheckTokenResult) => void, + onError?: (error: Error) => void, + onCompletion?: () => void +): Unsubscribe; +export function onTokenChanged( + app: FirebaseApp, + platformLoggerProvider: Provider<'platform-logger'>, + onNextOrObserver: + | ((tokenResult: AppCheckTokenResult) => void) + | PartialObserver, + onError?: (error: Error) => void, + /** + * NOTE: Although an `onCompletion` callback can be provided, it will + * never be called because the token stream is never-ending. + * It is added only for API consistency with the observer pattern, which + * we follow in JS APIs. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onCompletion?: () => void +): Unsubscribe { + let nextFn: NextFn = () => {}; + let errorFn: ErrorFn = () => {}; + if ((onNextOrObserver as PartialObserver).next != null) { + nextFn = (onNextOrObserver as PartialObserver).next!; + } else { + nextFn = onNextOrObserver as NextFn; + } + if ( + (onNextOrObserver as PartialObserver).error != null + ) { + errorFn = (onNextOrObserver as PartialObserver).error!; + } else if (onError) { + errorFn = onError; + } + addTokenListener(app, platformLoggerProvider, nextFn, errorFn); + return () => removeTokenListener(app, nextFn); +} diff --git a/packages/app-check/src/factory.ts b/packages/app-check/src/factory.ts index 4789036b881..ee27b9a04cf 100644 --- a/packages/app-check/src/factory.ts +++ b/packages/app-check/src/factory.ts @@ -15,25 +15,60 @@ * limitations under the License. */ -import { FirebaseAppCheck, AppCheckProvider } from '@firebase/app-check-types'; -import { activate, setTokenAutoRefreshEnabled } from './api'; +import { + FirebaseAppCheck, + AppCheckProvider, + AppCheckTokenResult +} from '@firebase/app-check-types'; +import { + activate, + setTokenAutoRefreshEnabled, + getToken, + onTokenChanged +} from './api'; import { FirebaseApp } from '@firebase/app-types'; import { FirebaseAppCheckInternal } from '@firebase/app-check-interop-types'; import { - getToken, + getToken as getTokenInternal, addTokenListener, removeTokenListener } from './internal-api'; import { Provider } from '@firebase/component'; +import { PartialObserver } from '@firebase/util'; -export function factory(app: FirebaseApp): FirebaseAppCheck { +export function factory( + app: FirebaseApp, + platformLoggerProvider: Provider<'platform-logger'> +): FirebaseAppCheck { return { activate: ( siteKeyOrProvider: string | AppCheckProvider, isTokenAutoRefreshEnabled?: boolean ) => activate(app, siteKeyOrProvider, isTokenAutoRefreshEnabled), setTokenAutoRefreshEnabled: (isTokenAutoRefreshEnabled: boolean) => - setTokenAutoRefreshEnabled(app, isTokenAutoRefreshEnabled) + setTokenAutoRefreshEnabled(app, isTokenAutoRefreshEnabled), + + getToken: forceRefresh => + getToken(app, platformLoggerProvider, forceRefresh), + onTokenChanged: ( + onNextOrObserver: + | ((tokenResult: AppCheckTokenResult) => void) + | PartialObserver, + onError?: (error: Error) => void, + onCompletion?: () => void + ) => + onTokenChanged( + app, + platformLoggerProvider, + /** + * This can still be an observer. Need to do this casting because + * according to Typescript: "Implementation signatures of overloads + * are not externally visible" + */ + onNextOrObserver as (tokenResult: AppCheckTokenResult) => void, + onError, + onCompletion + ) }; } @@ -43,7 +78,7 @@ export function internalFactory( ): FirebaseAppCheckInternal { return { getToken: forceRefresh => - getToken(app, platformLoggerProvider, forceRefresh), + getTokenInternal(app, platformLoggerProvider, forceRefresh), addTokenListener: listener => addTokenListener(app, platformLoggerProvider, listener), removeTokenListener: listener => removeTokenListener(app, listener) diff --git a/packages/app-check/src/index.ts b/packages/app-check/src/index.ts index 9ab87738571..047c89d066e 100644 --- a/packages/app-check/src/index.ts +++ b/packages/app-check/src/index.ts @@ -41,7 +41,8 @@ function registerAppCheck(firebase: _FirebaseNamespace): void { container => { // getImmediate for FirebaseApp will always succeed const app = container.getProvider('app').getImmediate(); - return factory(app); + const platformLoggerProvider = container.getProvider('platform-logger'); + return factory(app, platformLoggerProvider); }, ComponentType.PUBLIC ) diff --git a/packages/app-check/src/internal-api.test.ts b/packages/app-check/src/internal-api.test.ts index 3d4abce1019..b86d5692f77 100644 --- a/packages/app-check/src/internal-api.test.ts +++ b/packages/app-check/src/internal-api.test.ts @@ -38,7 +38,7 @@ import * as reCAPTCHA from './recaptcha'; import * as client from './client'; import * as storage from './storage'; import { getState, clearState, setState, getDebugState } from './state'; -import { AppCheckTokenListener } from '@firebase/app-check-interop-types'; +import { AppCheckTokenResult } from '@firebase/app-check-interop-types'; import { Deferred } from '@firebase/util'; const fakePlatformLoggingProvider = getFakePlatformLoggingProvider(); @@ -108,7 +108,7 @@ describe('internal api', () => { it('resolves with a dummy token and an error if failed to get a token', async () => { const errorStub = stub(console, 'error'); - activate(app, FAKE_SITE_KEY); + activate(app, FAKE_SITE_KEY, true); const reCAPTCHASpy = stub(reCAPTCHA, 'getToken').returns( Promise.resolve(fakeRecaptchaToken) @@ -131,7 +131,7 @@ describe('internal api', () => { }); it('notifies listeners using cached token', async () => { - activate(app, FAKE_SITE_KEY); + activate(app, FAKE_SITE_KEY, true); const clock = useFakeTimers(); stub(storage, 'readTokenFromStorage').returns( @@ -156,7 +156,7 @@ describe('internal api', () => { }); it('notifies listeners using new token', async () => { - activate(app, FAKE_SITE_KEY); + activate(app, FAKE_SITE_KEY, true); stub(storage, 'readTokenFromStorage').returns(Promise.resolve(undefined)); stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken)); @@ -179,8 +179,8 @@ describe('internal api', () => { }); }); - it('ignores listeners that throw', async () => { - activate(app, FAKE_SITE_KEY); + it('calls optional handler if a listener throws', async () => { + activate(app, FAKE_SITE_KEY, true); stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken)); stub(client, 'exchangeToken').returns( Promise.resolve(fakeRecaptchaAppCheckToken) @@ -190,14 +190,19 @@ describe('internal api', () => { }; const listener2 = spy(); - addTokenListener(app, fakePlatformLoggingProvider, listener1); - addTokenListener(app, fakePlatformLoggingProvider, listener2); + const errorFn1 = spy(); + const errorFn2 = spy(); + + addTokenListener(app, fakePlatformLoggingProvider, listener1, errorFn1); + addTokenListener(app, fakePlatformLoggingProvider, listener2, errorFn2); await getToken(app, fakePlatformLoggingProvider); expect(listener2).to.be.calledWith({ token: fakeRecaptchaAppCheckToken.token }); + expect(errorFn1).to.be.calledOnce; + expect(errorFn2).to.not.be.called; }); it('loads persisted token to memory and returns it', async () => { @@ -292,7 +297,7 @@ describe('internal api', () => { addTokenListener(app, fakePlatformLoggingProvider, listener); - expect(getState(app).tokenListeners[0]).to.equal(listener); + expect(getState(app).tokenListeners[0].listener).to.equal(listener); }); it('starts proactively refreshing token after adding the first listener', () => { @@ -308,7 +313,7 @@ describe('internal api', () => { it('notifies the listener with the valid token in memory immediately', done => { const clock = useFakeTimers(); - const fakeListener: AppCheckTokenListener = token => { + const fakeListener = (token: AppCheckTokenResult): void => { expect(token).to.deep.equal({ token: `fake-memory-app-check-token` }); @@ -330,7 +335,7 @@ describe('internal api', () => { it('notifies the listener with the valid token in storage', done => { const clock = useFakeTimers(); - activate(app, FAKE_SITE_KEY); + activate(app, FAKE_SITE_KEY, true); stub(storage, 'readTokenFromStorage').returns( Promise.resolve({ token: `fake-cached-app-check-token`, @@ -339,7 +344,7 @@ describe('internal api', () => { }) ); - const fakeListener: AppCheckTokenListener = token => { + const fakeListener = (token: AppCheckTokenResult): void => { expect(token).to.deep.equal({ token: `fake-cached-app-check-token` }); @@ -352,7 +357,7 @@ describe('internal api', () => { }); it('notifies the listener with the debug token immediately', done => { - const fakeListener: AppCheckTokenListener = token => { + const fakeListener = (token: AppCheckTokenResult): void => { expect(token).to.deep.equal({ token: `my-debug-token` }); @@ -364,7 +369,7 @@ describe('internal api', () => { debugState.token = new Deferred(); debugState.token.resolve('my-debug-token'); - activate(app, FAKE_SITE_KEY); + activate(app, FAKE_SITE_KEY, true); addTokenListener(app, fakePlatformLoggingProvider, fakeListener); }); @@ -374,7 +379,7 @@ describe('internal api', () => { debugState.token = new Deferred(); debugState.token.resolve('my-debug-token'); - activate(app, FAKE_SITE_KEY); + activate(app, FAKE_SITE_KEY, true); addTokenListener(app, fakePlatformLoggingProvider, () => {}); const state = getState(app); diff --git a/packages/app-check/src/internal-api.ts b/packages/app-check/src/internal-api.ts index 4e4b687edcf..5782ee49567 100644 --- a/packages/app-check/src/internal-api.ts +++ b/packages/app-check/src/internal-api.ts @@ -167,12 +167,17 @@ export async function getToken( export function addTokenListener( app: FirebaseApp, platformLoggerProvider: Provider<'platform-logger'>, - listener: AppCheckTokenListener + listener: (token: AppCheckTokenResult) => void, + onError?: (error: Error) => void ): void { const state = getState(app); + const tokenListener: AppCheckTokenListener = { + listener, + onError + }; const newState = { ...state, - tokenListeners: [...state.tokenListeners, listener] + tokenListeners: [...state.tokenListeners, tokenListener] }; /** @@ -185,8 +190,14 @@ export function addTokenListener( if (debugState.enabled && debugState.token) { debugState.token.promise .then(token => listener({ token })) - .catch(() => { - /* we don't care about exceptions thrown in listeners */ + .catch(e => { + /** + * An error handler will be provided if this is called by the public + * API. Internal callers don't care about errors in listeners. + */ + if (onError) { + onError(e); + } }); } } else { @@ -214,8 +225,14 @@ export function addTokenListener( const validToken = state.token; Promise.resolve() .then(() => listener({ token: validToken.token })) - .catch(() => { - /* we don't care about exceptions thrown in listeners */ + .catch(e => { + /** + * An error handler will be provided if this is called by the public + * API. Internal callers don't care about errors in listeners. + */ + if (onError) { + onError(e); + } }); } } @@ -225,11 +242,13 @@ export function addTokenListener( export function removeTokenListener( app: FirebaseApp, - listener: AppCheckTokenListener + listener: (token: AppCheckTokenResult) => void ): void { const state = getState(app); - const newListeners = state.tokenListeners.filter(l => l !== listener); + const newListeners = state.tokenListeners.filter( + tokenListener => tokenListener.listener !== listener + ); if ( newListeners.length === 0 && state.tokenRefresher && @@ -306,9 +325,13 @@ function notifyTokenListeners( for (const listener of listeners) { try { - listener(token); + listener.listener(token); } catch (e) { - // If any handler fails, ignore and run next handler. + // If any listener fails, run any provided error handler, + // then run next listener. + if (listener.onError) { + listener.onError(e); + } } } } diff --git a/packages/firebase/index.d.ts b/packages/firebase/index.d.ts index 85f612bc039..18c4e7e1b47 100644 --- a/packages/firebase/index.d.ts +++ b/packages/firebase/index.d.ts @@ -1535,6 +1535,13 @@ declare namespace firebase.app { * @webonly */ declare namespace firebase.appCheck { + /** + * Result returned by + * {@link firebase.appCheck.AppCheck.getToken `firebase.appCheck().getToken()`}. + */ + interface AppCheckTokenResult { + token: string; + } /** * The Firebase AppCheck service interface. * @@ -1563,6 +1570,57 @@ declare namespace firebase.appCheck { * during `activate()`. */ setTokenAutoRefreshEnabled(isTokenAutoRefreshEnabled: boolean): void; + /** + * Get the current App Check token. Attaches to the most recent + * in-flight request if one is present. Returns null if no token + * is present and no token requests are in-flight. + * + * @param forceRefresh - If true, will always try to fetch a fresh token. + * If false, will use a cached token if found in storage. + */ + getToken( + forceRefresh?: boolean + ): Promise; + + /** + * Registers a listener to changes in the token state. There can be more + * than one listener registered at the same time for one or more + * App Check instances. The listeners call back on the UI thread whenever + * the current token associated with this App Check instance changes. + * + * @param observer An object with `next`, `error`, and `complete` + * properties. `next` is called with an + * {@link firebase.appCheck.AppCheckTokenResult `AppCheckTokenResult`} + * whenever the token changes. `error` is optional and is called if an + * error is thrown by the listener (the `next` function). `complete` + * is unused, as the token stream is unending. + * + * @returns A function that unsubscribes this listener. + */ + onTokenChanged(observer: { + next: (tokenResult: firebase.appCheck.AppCheckTokenResult) => void; + error?: (error: Error) => void; + complete?: () => void; + }): Unsubscribe; + + /** + * Registers a listener to changes in the token state. There can be more + * than one listener registered at the same time for one or more + * App Check instances. The listeners call back on the UI thread whenever + * the current token associated with this App Check instance changes. + * + * @param onNext When the token changes, this function is called with aa + * {@link firebase.appCheck.AppCheckTokenResult `AppCheckTokenResult`}. + * @param onError Optional. Called if there is an error thrown by the + * listener (the `onNext` function). + * @param onCompletion Currently unused, as the token stream is unending. + * @returns A function that unsubscribes this listener. + */ + onTokenChanged( + onNext: (tokenResult: firebase.appCheck.AppCheckTokenResult) => void, + onError?: (error: Error) => void, + onCompletion?: () => void + ): Unsubscribe; } /** From b95bc35763024868b0f899d9d9991a4f8d244a2c Mon Sep 17 00:00:00 2001 From: Christina Holland Date: Wed, 16 Jun 2021 13:07:52 -0700 Subject: [PATCH 2/8] Fix lint --- packages/app-check/package.json | 2 +- packages/app-check/src/api.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app-check/package.json b/packages/app-check/package.json index d6b5791a409..f79ac28a7f1 100644 --- a/packages/app-check/package.json +++ b/packages/app-check/package.json @@ -16,7 +16,7 @@ "build": "rollup -c", "build:deps": "lerna run --scope @firebase/app-check --include-dependencies build", "dev": "rollup -c -w", - "test": "yarn type-check && yarn test:browser", + "test": "yarn lint && yarn type-check && yarn test:browser", "test:ci": "node ../../scripts/run_tests_in_ci.js", "test:browser": "karma start --single-run", "test:browser:debug": "karma start --browsers Chrome --auto-watch", diff --git a/packages/app-check/src/api.test.ts b/packages/app-check/src/api.test.ts index 404b45e166a..92df744d287 100644 --- a/packages/app-check/src/api.test.ts +++ b/packages/app-check/src/api.test.ts @@ -16,7 +16,7 @@ */ import '../test/setup'; import { expect } from 'chai'; -import { stub, spy, restore } from 'sinon'; +import { stub, spy } from 'sinon'; import { activate, setTokenAutoRefreshEnabled, From 3b0ce02437112960ecd9c1cbca4285382a682f97 Mon Sep 17 00:00:00 2001 From: Christina Holland Date: Wed, 16 Jun 2021 13:43:21 -0700 Subject: [PATCH 3/8] Restore interop type --- packages/app-check-interop-types/index.d.ts | 5 +---- packages/app-check/src/internal-api.ts | 9 +++++---- packages/app-check/src/state.ts | 6 +++++- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/app-check-interop-types/index.d.ts b/packages/app-check-interop-types/index.d.ts index a6dc9a57a02..060d6998a90 100644 --- a/packages/app-check-interop-types/index.d.ts +++ b/packages/app-check-interop-types/index.d.ts @@ -30,10 +30,7 @@ export interface FirebaseAppCheckInternal { removeTokenListener(listener: (token: AppCheckTokenResult) => void): void; } -interface AppCheckTokenListener { - listener: (token: AppCheckTokenResult) => void; - onError?: (error: Error) => void; -} +type AppCheckTokenListener = (token: AppCheckTokenResult) => void; // If the error field is defined, the token field will be populated with a dummy token interface AppCheckTokenResult { diff --git a/packages/app-check/src/internal-api.ts b/packages/app-check/src/internal-api.ts index 5782ee49567..7c74420954f 100644 --- a/packages/app-check/src/internal-api.ts +++ b/packages/app-check/src/internal-api.ts @@ -18,11 +18,12 @@ import { getToken as getReCAPTCHAToken } from './recaptcha'; import { FirebaseApp } from '@firebase/app-types'; import { - AppCheckTokenResult, - AppCheckTokenListener + AppCheckTokenListener, + AppCheckTokenResult } from '@firebase/app-check-interop-types'; import { AppCheckTokenInternal, + AppCheckTokenListenerInternal, getDebugState, getState, setState @@ -167,11 +168,11 @@ export async function getToken( export function addTokenListener( app: FirebaseApp, platformLoggerProvider: Provider<'platform-logger'>, - listener: (token: AppCheckTokenResult) => void, + listener: AppCheckTokenListener, onError?: (error: Error) => void ): void { const state = getState(app); - const tokenListener: AppCheckTokenListener = { + const tokenListener: AppCheckTokenListenerInternal = { listener, onError }; diff --git a/packages/app-check/src/state.ts b/packages/app-check/src/state.ts index 5d041ec82a4..777ade9e287 100644 --- a/packages/app-check/src/state.ts +++ b/packages/app-check/src/state.ts @@ -25,9 +25,13 @@ import { GreCAPTCHA } from './recaptcha'; export interface AppCheckTokenInternal extends AppCheckToken { issuedAtTimeMillis: number; } +export interface AppCheckTokenListenerInternal { + listener: AppCheckTokenListener; + onError?: (error: Error) => void; +} export interface AppCheckState { activated: boolean; - tokenListeners: AppCheckTokenListener[]; + tokenListeners: AppCheckTokenListenerInternal[]; customProvider?: AppCheckProvider; siteKey?: string; token?: AppCheckTokenInternal; From b8caa9d4b80d4814aac6e18edfcb4757f3166ea0 Mon Sep 17 00:00:00 2001 From: Christina Holland Date: Thu, 17 Jun 2021 10:46:57 -0700 Subject: [PATCH 4/8] Update packages/app-check-types/index.d.ts Co-authored-by: Kevin Cheung --- packages/app-check-types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-check-types/index.d.ts b/packages/app-check-types/index.d.ts index 1bcc5418074..aa5e49d039b 100644 --- a/packages/app-check-types/index.d.ts +++ b/packages/app-check-types/index.d.ts @@ -42,7 +42,7 @@ export interface FirebaseAppCheck { /** * Get the current App Check token. Attaches to the most recent * in-flight request if one is present. Returns null if no token - * is present and no token requests are in-flight. + * is present and no token requests are in flight. * * @param forceRefresh - If true, will always try to fetch a fresh token. * If false, will use a cached token if found in storage. From d7f0635b8b059ecce12b5e3133293f7876bcc953 Mon Sep 17 00:00:00 2001 From: Christina Holland Date: Thu, 17 Jun 2021 13:01:35 -0700 Subject: [PATCH 5/8] Fix onErrors --- packages/app-check-interop-types/index.d.ts | 4 +- packages/app-check/src/api.test.ts | 67 +++++++++++++----- packages/app-check/src/internal-api.test.ts | 32 ++++----- packages/app-check/src/internal-api.ts | 76 +++++++++++---------- packages/app-check/src/state.ts | 21 ++++-- 5 files changed, 119 insertions(+), 81 deletions(-) diff --git a/packages/app-check-interop-types/index.d.ts b/packages/app-check-interop-types/index.d.ts index 060d6998a90..d2309c1b4dd 100644 --- a/packages/app-check-interop-types/index.d.ts +++ b/packages/app-check-interop-types/index.d.ts @@ -24,10 +24,10 @@ export interface FirebaseAppCheckInternal { // registered at the same time for one or more FirebaseAppAttestation instances. The // listeners call back on the UI thread whenever the current token associated with this // FirebaseAppAttestation changes. - addTokenListener(listener: (token: AppCheckTokenResult) => void): void; + addTokenListener(listener: AppCheckTokenListener): void; // Unregisters a listener to changes in the token state. - removeTokenListener(listener: (token: AppCheckTokenResult) => void): void; + removeTokenListener(listener: AppCheckTokenListener): void; } type AppCheckTokenListener = (token: AppCheckTokenResult) => void; diff --git a/packages/app-check/src/api.test.ts b/packages/app-check/src/api.test.ts index 92df744d287..e3025aa5b78 100644 --- a/packages/app-check/src/api.test.ts +++ b/packages/app-check/src/api.test.ts @@ -36,6 +36,7 @@ import { FirebaseApp } from '@firebase/app-types'; import * as internalApi from './internal-api'; import * as client from './client'; import * as storage from './storage'; +import * as logger from './logger'; describe('api', () => { describe('activate()', () => { @@ -151,31 +152,32 @@ describe('api', () => { const errorFn1 = spy(); const errorFn2 = spy(); - const unSubscribe1 = onTokenChanged( + const unsubscribe1 = onTokenChanged( app, fakePlatformLoggingProvider, listener1, errorFn1 ); - const unSubscribe2 = onTokenChanged( + const unsubscribe2 = onTokenChanged( app, fakePlatformLoggingProvider, listener2, errorFn2 ); - expect(getState(app).tokenListeners.length).to.equal(2); + expect(getState(app).tokenObservers.length).to.equal(2); - await getToken(app, fakePlatformLoggingProvider); + await internalApi.getToken(app, fakePlatformLoggingProvider); expect(listener2).to.be.calledWith({ token: fakeRecaptchaAppCheckToken.token }); - expect(errorFn1).to.be.calledOnce; + // onError should not be called on listener errors. + expect(errorFn1).to.not.be.called; expect(errorFn2).to.not.be.called; - unSubscribe1(); - unSubscribe2(); - expect(getState(app).tokenListeners.length).to.equal(0); + unsubscribe1(); + unsubscribe2(); + expect(getState(app).tokenObservers.length).to.equal(0); }); it('Listeners work when using Observer pattern', async () => { @@ -206,27 +208,60 @@ describe('api', () => { * Reverse the order of adding the failed and successful handler, for extra * testing. */ - const unSubscribe2 = onTokenChanged(app, fakePlatformLoggingProvider, { + const unsubscribe2 = onTokenChanged(app, fakePlatformLoggingProvider, { next: listener2, error: errorFn2 }); - const unSubscribe1 = onTokenChanged(app, fakePlatformLoggingProvider, { + const unsubscribe1 = onTokenChanged(app, fakePlatformLoggingProvider, { next: listener1, error: errorFn1 }); - expect(getState(app).tokenListeners.length).to.equal(2); + expect(getState(app).tokenObservers.length).to.equal(2); - await getToken(app, fakePlatformLoggingProvider); + await internalApi.getToken(app, fakePlatformLoggingProvider); expect(listener2).to.be.calledWith({ token: fakeRecaptchaAppCheckToken.token }); - expect(errorFn1).to.be.calledOnce; + // onError should not be called on listener errors. + expect(errorFn1).to.not.be.called; expect(errorFn2).to.not.be.called; - unSubscribe1(); - unSubscribe2(); - expect(getState(app).tokenListeners.length).to.equal(0); + unsubscribe1(); + unsubscribe2(); + expect(getState(app).tokenObservers.length).to.equal(0); + }); + + it('onError() catches token errors', async () => { + stub(logger.logger, 'error'); + const app = getFakeApp(); + activate(app, FAKE_SITE_KEY, false); + const fakePlatformLoggingProvider = getFakePlatformLoggingProvider(); + const fakeRecaptchaToken = 'fake-recaptcha-token'; + stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken)); + stub(client, 'exchangeToken').rejects('exchange error'); + stub(storage, 'writeTokenToStorage').returns(Promise.resolve(undefined)); + + const listener1 = spy(); + + const errorFn1 = spy(); + + const unsubscribe1 = onTokenChanged( + app, + fakePlatformLoggingProvider, + listener1, + errorFn1 + ); + + await internalApi.getToken(app, fakePlatformLoggingProvider); + + expect(getState(app).tokenObservers.length).to.equal(1); + + expect(errorFn1).to.be.calledOnce; + expect(errorFn1.args[0][0].name).to.include('exchange error'); + + unsubscribe1(); + expect(getState(app).tokenObservers.length).to.equal(0); }); }); }); diff --git a/packages/app-check/src/internal-api.test.ts b/packages/app-check/src/internal-api.test.ts index b86d5692f77..02c9148901a 100644 --- a/packages/app-check/src/internal-api.test.ts +++ b/packages/app-check/src/internal-api.test.ts @@ -35,6 +35,7 @@ import { defaultTokenErrorData } from './internal-api'; import * as reCAPTCHA from './recaptcha'; +import * as logger from './logger'; import * as client from './client'; import * as storage from './storage'; import { getState, clearState, setState, getDebugState } from './state'; @@ -179,30 +180,21 @@ describe('internal api', () => { }); }); - it('calls optional handler if a listener throws', async () => { + it('calls optional error handler if there is an error getting a token', async () => { + stub(logger.logger, 'error'); activate(app, FAKE_SITE_KEY, true); stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken)); - stub(client, 'exchangeToken').returns( - Promise.resolve(fakeRecaptchaAppCheckToken) - ); - const listener1 = (): void => { - throw new Error(); - }; - const listener2 = spy(); + stub(client, 'exchangeToken').rejects('exchange error'); + const listener1 = spy(); const errorFn1 = spy(); - const errorFn2 = spy(); addTokenListener(app, fakePlatformLoggingProvider, listener1, errorFn1); - addTokenListener(app, fakePlatformLoggingProvider, listener2, errorFn2); await getToken(app, fakePlatformLoggingProvider); - expect(listener2).to.be.calledWith({ - token: fakeRecaptchaAppCheckToken.token - }); expect(errorFn1).to.be.calledOnce; - expect(errorFn2).to.not.be.called; + expect(errorFn1.args[0][0].name).to.include('exchange error'); }); it('loads persisted token to memory and returns it', async () => { @@ -297,13 +289,13 @@ describe('internal api', () => { addTokenListener(app, fakePlatformLoggingProvider, listener); - expect(getState(app).tokenListeners[0].listener).to.equal(listener); + expect(getState(app).tokenObservers[0].next).to.equal(listener); }); it('starts proactively refreshing token after adding the first listener', () => { const listener = (): void => {}; setState(app, { ...getState(app), isTokenAutoRefreshEnabled: true }); - expect(getState(app).tokenListeners.length).to.equal(0); + expect(getState(app).tokenObservers.length).to.equal(0); expect(getState(app).tokenRefresher).to.equal(undefined); addTokenListener(app, fakePlatformLoggingProvider, listener); @@ -391,10 +383,10 @@ describe('internal api', () => { it('should remove token listeners', () => { const listener = (): void => {}; addTokenListener(app, fakePlatformLoggingProvider, listener); - expect(getState(app).tokenListeners.length).to.equal(1); + expect(getState(app).tokenObservers.length).to.equal(1); removeTokenListener(app, listener); - expect(getState(app).tokenListeners.length).to.equal(0); + expect(getState(app).tokenObservers.length).to.equal(0); }); it('should stop proactively refreshing token after deleting the last listener', () => { @@ -402,11 +394,11 @@ describe('internal api', () => { setState(app, { ...getState(app), isTokenAutoRefreshEnabled: true }); addTokenListener(app, fakePlatformLoggingProvider, listener); - expect(getState(app).tokenListeners.length).to.equal(1); + expect(getState(app).tokenObservers.length).to.equal(1); expect(getState(app).tokenRefresher?.isRunning()).to.be.true; removeTokenListener(app, listener); - expect(getState(app).tokenListeners.length).to.equal(0); + expect(getState(app).tokenObservers.length).to.equal(0); expect(getState(app).tokenRefresher?.isRunning()).to.be.false; }); }); diff --git a/packages/app-check/src/internal-api.ts b/packages/app-check/src/internal-api.ts index 7c74420954f..cfa9f4de86d 100644 --- a/packages/app-check/src/internal-api.ts +++ b/packages/app-check/src/internal-api.ts @@ -23,7 +23,7 @@ import { } from '@firebase/app-check-interop-types'; import { AppCheckTokenInternal, - AppCheckTokenListenerInternal, + AppCheckTokenObserver, getDebugState, getState, setState @@ -172,13 +172,13 @@ export function addTokenListener( onError?: (error: Error) => void ): void { const state = getState(app); - const tokenListener: AppCheckTokenListenerInternal = { - listener, - onError + const tokenListener: AppCheckTokenObserver = { + next: listener, + error: onError }; const newState = { ...state, - tokenListeners: [...state.tokenListeners, tokenListener] + tokenObservers: [...state.tokenObservers, tokenListener] }; /** @@ -191,14 +191,8 @@ export function addTokenListener( if (debugState.enabled && debugState.token) { debugState.token.promise .then(token => listener({ token })) - .catch(e => { - /** - * An error handler will be provided if this is called by the public - * API. Internal callers don't care about errors in listeners. - */ - if (onError) { - onError(e); - } + .catch(() => { + /** Ignore errors in listeners. */ }); } } else { @@ -208,7 +202,11 @@ export function addTokenListener( * invoke the listener with the valid token, then start the token refresher */ if (!newState.tokenRefresher) { - const tokenRefresher = createTokenRefresher(app, platformLoggerProvider); + const tokenRefresher = createTokenRefresher( + app, + platformLoggerProvider, + onError + ); newState.tokenRefresher = tokenRefresher; } @@ -226,14 +224,8 @@ export function addTokenListener( const validToken = state.token; Promise.resolve() .then(() => listener({ token: validToken.token })) - .catch(e => { - /** - * An error handler will be provided if this is called by the public - * API. Internal callers don't care about errors in listeners. - */ - if (onError) { - onError(e); - } + .catch(() => { + /** Ignore errors in listeners. */ }); } } @@ -247,11 +239,11 @@ export function removeTokenListener( ): void { const state = getState(app); - const newListeners = state.tokenListeners.filter( - tokenListener => tokenListener.listener !== listener + const newObservers = state.tokenObservers.filter( + tokenObserver => tokenObserver.next !== listener ); if ( - newListeners.length === 0 && + newObservers.length === 0 && state.tokenRefresher && state.tokenRefresher.isRunning() ) { @@ -260,13 +252,14 @@ export function removeTokenListener( setState(app, { ...state, - tokenListeners: newListeners + tokenObservers: newObservers }); } function createTokenRefresher( app: FirebaseApp, - platformLoggerProvider: Provider<'platform-logger'> + platformLoggerProvider: Provider<'platform-logger'>, + onError?: (error: Error) => void ): Refresher { return new Refresher( // Keep in mind when this fails for any reason other than the ones @@ -284,7 +277,11 @@ function createTokenRefresher( // getToken() always resolves. In case the result has an error field defined, it means the operation failed, and we should retry. if (result.error) { - throw result.error; + if (onError) { + onError(result.error); + } else { + throw result.error; + } } }, () => { @@ -322,17 +319,24 @@ function notifyTokenListeners( app: FirebaseApp, token: AppCheckTokenResult ): void { - const listeners = getState(app).tokenListeners; + const observers = getState(app).tokenObservers; - for (const listener of listeners) { + for (const observer of observers) { try { - listener.listener(token); - } catch (e) { - // If any listener fails, run any provided error handler, - // then run next listener. - if (listener.onError) { - listener.onError(e); + if (observer.error) { + // If this listener has an error handler, handle errors differently + // from successes. + if (token.error) { + observer.error(token.error); + } else { + observer.next(token); + } + } else { + // Otherwise return the token, whether or not it has an error field. + observer.next(token); } + } catch (ignored) { + // If any handler fails, ignore and run next handler. } } } diff --git a/packages/app-check/src/state.ts b/packages/app-check/src/state.ts index 777ade9e287..4c983c835ba 100644 --- a/packages/app-check/src/state.ts +++ b/packages/app-check/src/state.ts @@ -16,22 +16,29 @@ */ import { FirebaseApp } from '@firebase/app-types'; -import { AppCheckProvider, AppCheckToken } from '@firebase/app-check-types'; +import { + AppCheckProvider, + AppCheckToken, + AppCheckTokenResult +} from '@firebase/app-check-types'; import { AppCheckTokenListener } from '@firebase/app-check-interop-types'; import { Refresher } from './proactive-refresh'; -import { Deferred } from '@firebase/util'; +import { Deferred, PartialObserver } from '@firebase/util'; import { GreCAPTCHA } from './recaptcha'; export interface AppCheckTokenInternal extends AppCheckToken { issuedAtTimeMillis: number; } -export interface AppCheckTokenListenerInternal { - listener: AppCheckTokenListener; - onError?: (error: Error) => void; + +export interface AppCheckTokenObserver + extends PartialObserver { + // required + next: AppCheckTokenListener; } + export interface AppCheckState { activated: boolean; - tokenListeners: AppCheckTokenListenerInternal[]; + tokenObservers: AppCheckTokenObserver[]; customProvider?: AppCheckProvider; siteKey?: string; token?: AppCheckTokenInternal; @@ -53,7 +60,7 @@ export interface DebugState { const APP_CHECK_STATES = new Map(); export const DEFAULT_STATE: AppCheckState = { activated: false, - tokenListeners: [] + tokenObservers: [] }; const DEBUG_STATE: DebugState = { From d687bdacb6cd132abdbb3b2e3365648b1ecc39df Mon Sep 17 00:00:00 2001 From: Christina Holland Date: Thu, 17 Jun 2021 13:08:16 -0700 Subject: [PATCH 6/8] Bind next/error functions --- packages/app-check/src/api.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/app-check/src/api.ts b/packages/app-check/src/api.ts index 0cb3e3ea604..d0499bc5711 100644 --- a/packages/app-check/src/api.ts +++ b/packages/app-check/src/api.ts @@ -147,14 +147,18 @@ export function onTokenChanged( let nextFn: NextFn = () => {}; let errorFn: ErrorFn = () => {}; if ((onNextOrObserver as PartialObserver).next != null) { - nextFn = (onNextOrObserver as PartialObserver).next!; + nextFn = (onNextOrObserver as PartialObserver).next!.bind( + onNextOrObserver + ); } else { nextFn = onNextOrObserver as NextFn; } if ( (onNextOrObserver as PartialObserver).error != null ) { - errorFn = (onNextOrObserver as PartialObserver).error!; + errorFn = (onNextOrObserver as PartialObserver).error!.bind( + onNextOrObserver + ); } else if (onError) { errorFn = onError; } From df666f051fd3265f27d1070bf2fcc99cbbfb44d6 Mon Sep 17 00:00:00 2001 From: Christina Holland Date: Wed, 23 Jun 2021 09:28:01 -0700 Subject: [PATCH 7/8] Address PR comments --- packages/app-check/src/api.test.ts | 4 +++- packages/app-check/src/internal-api.ts | 15 +++------------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/app-check/src/api.test.ts b/packages/app-check/src/api.test.ts index e3025aa5b78..14fb1d86305 100644 --- a/packages/app-check/src/api.test.ts +++ b/packages/app-check/src/api.test.ts @@ -114,8 +114,10 @@ describe('api', () => { it('getToken() throws errors returned with token', async () => { const app = getFakeApp({ automaticDataCollectionEnabled: true }); const fakePlatformLoggingProvider = getFakePlatformLoggingProvider(); + // If getToken() errors, it returns a dummy token with an error field + // instead of throwing. stub(internalApi, 'getToken').resolves({ - token: 'a-token-string', + token: 'a-dummy-token', error: Error('there was an error') }); await expect( diff --git a/packages/app-check/src/internal-api.ts b/packages/app-check/src/internal-api.ts index cfa9f4de86d..f7d0767ee9b 100644 --- a/packages/app-check/src/internal-api.ts +++ b/packages/app-check/src/internal-api.ts @@ -202,11 +202,7 @@ export function addTokenListener( * invoke the listener with the valid token, then start the token refresher */ if (!newState.tokenRefresher) { - const tokenRefresher = createTokenRefresher( - app, - platformLoggerProvider, - onError - ); + const tokenRefresher = createTokenRefresher(app, platformLoggerProvider); newState.tokenRefresher = tokenRefresher; } @@ -258,8 +254,7 @@ export function removeTokenListener( function createTokenRefresher( app: FirebaseApp, - platformLoggerProvider: Provider<'platform-logger'>, - onError?: (error: Error) => void + platformLoggerProvider: Provider<'platform-logger'> ): Refresher { return new Refresher( // Keep in mind when this fails for any reason other than the ones @@ -277,11 +272,7 @@ function createTokenRefresher( // getToken() always resolves. In case the result has an error field defined, it means the operation failed, and we should retry. if (result.error) { - if (onError) { - onError(result.error); - } else { - throw result.error; - } + throw result.error; } }, () => { From 99f0293da500b04e7401650564e06b8161b74985 Mon Sep 17 00:00:00 2001 From: Christina Holland Date: Wed, 23 Jun 2021 11:10:23 -0700 Subject: [PATCH 8/8] Add changeset --- .changeset/empty-countries-run.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/empty-countries-run.md diff --git a/.changeset/empty-countries-run.md b/.changeset/empty-countries-run.md new file mode 100644 index 00000000000..ec4b804b612 --- /dev/null +++ b/.changeset/empty-countries-run.md @@ -0,0 +1,7 @@ +--- +'@firebase/app-check': minor +'@firebase/app-check-types': minor +'firebase': minor +--- + +Added `getToken()` and `onTokenChanged` methods to App Check.