diff --git a/.changeset/loud-lamps-camp.md b/.changeset/loud-lamps-camp.md new file mode 100644 index 00000000000..79b2e59c205 --- /dev/null +++ b/.changeset/loud-lamps-camp.md @@ -0,0 +1,5 @@ +--- +'@firebase/app-check': patch +--- + +Fix an error causing App Check to log `HTTP status 429` errors in debug mode. diff --git a/packages/app-check/src/api.test.ts b/packages/app-check/src/api.test.ts index 14fb1d86305..894fbde65dd 100644 --- a/packages/app-check/src/api.test.ts +++ b/packages/app-check/src/api.test.ts @@ -28,6 +28,7 @@ import { getFakeApp, getFakeCustomTokenProvider, getFakePlatformLoggingProvider, + getFakeGreCAPTCHA, removegreCAPTCHAScriptsOnPage } from '../test/util'; import { clearState, getState } from './state'; @@ -37,8 +38,12 @@ import * as internalApi from './internal-api'; import * as client from './client'; import * as storage from './storage'; import * as logger from './logger'; +import * as util from './util'; describe('api', () => { + beforeEach(() => { + stub(util, 'getRecaptcha').returns(getFakeGreCAPTCHA()); + }); describe('activate()', () => { let app: FirebaseApp; @@ -126,25 +131,29 @@ describe('api', () => { }); }); describe('onTokenChanged()', () => { + const fakePlatformLoggingProvider = getFakePlatformLoggingProvider(); + const fakeRecaptchaToken = 'fake-recaptcha-token'; + const fakeRecaptchaAppCheckToken = { + token: 'fake-recaptcha-app-check-token', + expireTimeMillis: Date.now() + 60000, + issuedAtTimeMillis: 0 + }; + + beforeEach(() => { + stub(storage, 'readTokenFromStorage').resolves(undefined); + stub(storage, 'writeTokenToStorage'); + }); 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 - }; + const app = getFakeApp(); + activate(app, FAKE_SITE_KEY, false); 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(); @@ -183,20 +192,12 @@ describe('api', () => { }); 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 - }; + const app = getFakeApp(); + activate(app, FAKE_SITE_KEY, false); 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(); @@ -238,11 +239,8 @@ describe('api', () => { 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(); diff --git a/packages/app-check/src/client.test.ts b/packages/app-check/src/client.test.ts index 78a06f90799..352e511915b 100644 --- a/packages/app-check/src/client.test.ts +++ b/packages/app-check/src/client.test.ts @@ -52,7 +52,8 @@ describe('client', () => { }); it('returns a AppCheck token', async () => { - useFakeTimers(); + // To get a consistent expireTime/issuedAtTime. + const clock = useFakeTimers(); fetchStub.returns( Promise.resolve({ status: 200, @@ -77,6 +78,7 @@ describe('client', () => { expireTimeMillis: 3600, issuedAtTimeMillis: 0 }); + clock.restore(); }); it('throws when there is a network error', async () => { diff --git a/packages/app-check/src/factory.ts b/packages/app-check/src/factory.ts index ee27b9a04cf..4173813b52b 100644 --- a/packages/app-check/src/factory.ts +++ b/packages/app-check/src/factory.ts @@ -36,18 +36,21 @@ import { import { Provider } from '@firebase/component'; import { PartialObserver } from '@firebase/util'; +import { FirebaseService } from '@firebase/app-types/private'; +import { getState } from './state'; + export function factory( app: FirebaseApp, platformLoggerProvider: Provider<'platform-logger'> -): FirebaseAppCheck { +): FirebaseAppCheck & FirebaseService { return { + app, activate: ( siteKeyOrProvider: string | AppCheckProvider, isTokenAutoRefreshEnabled?: boolean ) => activate(app, siteKeyOrProvider, isTokenAutoRefreshEnabled), setTokenAutoRefreshEnabled: (isTokenAutoRefreshEnabled: boolean) => setTokenAutoRefreshEnabled(app, isTokenAutoRefreshEnabled), - getToken: forceRefresh => getToken(app, platformLoggerProvider, forceRefresh), onTokenChanged: ( @@ -68,7 +71,16 @@ export function factory( onNextOrObserver as (tokenResult: AppCheckTokenResult) => void, onError, onCompletion - ) + ), + INTERNAL: { + delete: () => { + const { tokenObservers } = getState(app); + for (const tokenObserver of tokenObservers) { + removeTokenListener(app, tokenObserver.next); + } + return Promise.resolve(); + } + } }; } diff --git a/packages/app-check/src/internal-api.test.ts b/packages/app-check/src/internal-api.test.ts index 02c9148901a..c0bf48fcfa3 100644 --- a/packages/app-check/src/internal-api.test.ts +++ b/packages/app-check/src/internal-api.test.ts @@ -23,6 +23,7 @@ import { FAKE_SITE_KEY, getFakeApp, getFakeCustomTokenProvider, + getFakeGreCAPTCHA, getFakePlatformLoggingProvider, removegreCAPTCHAScriptsOnPage } from '../test/util'; @@ -38,20 +39,26 @@ import * as reCAPTCHA from './recaptcha'; import * as logger from './logger'; import * as client from './client'; import * as storage from './storage'; +import * as util from './util'; import { getState, clearState, setState, getDebugState } from './state'; -import { AppCheckTokenResult } from '@firebase/app-check-interop-types'; import { Deferred } from '@firebase/util'; +import { AppCheckTokenResult } from '../../app-check-interop-types'; const fakePlatformLoggingProvider = getFakePlatformLoggingProvider(); describe('internal api', () => { let app: FirebaseApp; + let storageReadStub: SinonStub; + let storageWriteStub: SinonStub; beforeEach(() => { app = getFakeApp(); + storageReadStub = stub(storage, 'readTokenFromStorage').resolves(undefined); + storageWriteStub = stub(storage, 'writeTokenToStorage'); + stub(util, 'getRecaptcha').returns(getFakeGreCAPTCHA()); }); - afterEach(() => { + afterEach(async () => { clearState(); removegreCAPTCHAScriptsOnPage(); }); @@ -60,18 +67,19 @@ describe('internal api', () => { const fakeRecaptchaToken = 'fake-recaptcha-token'; const fakeRecaptchaAppCheckToken = { token: 'fake-recaptcha-app-check-token', - expireTimeMillis: 123, + // This makes isValid(token) true. + expireTimeMillis: Date.now() + 60000, issuedAtTimeMillis: 0 }; const fakeCachedAppCheckToken = { token: 'fake-cached-app-check-token', - expireTimeMillis: 123, + // This makes isValid(token) true. + expireTimeMillis: Date.now() + 60000, issuedAtTimeMillis: 0 }; it('uses customTokenProvider to get an AppCheck token', async () => { - const clock = useFakeTimers(); const customTokenProvider = getFakeCustomTokenProvider(); const customProviderSpy = spy(customTokenProvider, 'getToken'); @@ -82,20 +90,18 @@ describe('internal api', () => { expect(token).to.deep.equal({ token: 'fake-custom-app-check-token' }); - - clock.restore(); }); it('uses reCAPTCHA token to exchange for AppCheck token if no customTokenProvider is provided', async () => { activate(app, FAKE_SITE_KEY); - const reCAPTCHASpy = stub(reCAPTCHA, 'getToken').returns( - Promise.resolve(fakeRecaptchaToken) + const reCAPTCHASpy = stub(reCAPTCHA, 'getToken').resolves( + fakeRecaptchaToken ); const exchangeTokenStub: SinonStub = stub( client, 'exchangeToken' - ).returns(Promise.resolve(fakeRecaptchaAppCheckToken)); + ).resolves(fakeRecaptchaAppCheckToken); const token = await getToken(app, fakePlatformLoggingProvider); @@ -111,12 +117,12 @@ describe('internal api', () => { const errorStub = stub(console, 'error'); activate(app, FAKE_SITE_KEY, true); - const reCAPTCHASpy = stub(reCAPTCHA, 'getToken').returns( - Promise.resolve(fakeRecaptchaToken) + const reCAPTCHASpy = stub(reCAPTCHA, 'getToken').resolves( + fakeRecaptchaToken ); const error = new Error('oops, something went wrong'); - stub(client, 'exchangeToken').returns(Promise.reject(error)); + stub(client, 'exchangeToken').rejects(error); const token = await getToken(app, fakePlatformLoggingProvider); @@ -132,12 +138,8 @@ describe('internal api', () => { }); it('notifies listeners using cached token', async () => { - activate(app, FAKE_SITE_KEY, true); - - const clock = useFakeTimers(); - stub(storage, 'readTokenFromStorage').returns( - Promise.resolve(fakeCachedAppCheckToken) - ); + activate(app, FAKE_SITE_KEY, false); + storageReadStub.resolves(fakeCachedAppCheckToken); const listener1 = spy(); const listener2 = spy(); @@ -152,18 +154,13 @@ describe('internal api', () => { expect(listener2).to.be.calledWith({ token: fakeCachedAppCheckToken.token }); - - clock.restore(); }); it('notifies listeners using new token', async () => { - activate(app, FAKE_SITE_KEY, true); + activate(app, FAKE_SITE_KEY, false); - stub(storage, 'readTokenFromStorage').returns(Promise.resolve(undefined)); - stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken)); - stub(client, 'exchangeToken').returns( - Promise.resolve(fakeRecaptchaAppCheckToken) - ); + stub(reCAPTCHA, 'getToken').resolves(fakeRecaptchaToken); + stub(client, 'exchangeToken').resolves(fakeRecaptchaAppCheckToken); const listener1 = spy(); const listener2 = spy(); @@ -182,8 +179,8 @@ describe('internal api', () => { 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)); + activate(app, FAKE_SITE_KEY, false); + stub(reCAPTCHA, 'getToken').resolves(fakeRecaptchaToken); stub(client, 'exchangeToken').rejects('exchange error'); const listener1 = spy(); @@ -197,35 +194,46 @@ describe('internal api', () => { expect(errorFn1.args[0][0].name).to.include('exchange error'); }); + it('ignores listeners that throw', async () => { + activate(app, FAKE_SITE_KEY, false); + stub(reCAPTCHA, 'getToken').resolves(fakeRecaptchaToken); + stub(client, 'exchangeToken').resolves(fakeRecaptchaAppCheckToken); + const listener1 = stub().throws(new Error()); + const listener2 = spy(); + + const errorFn1 = spy(); + + addTokenListener(app, fakePlatformLoggingProvider, listener1, errorFn1); + addTokenListener(app, fakePlatformLoggingProvider, listener2); + + await getToken(app, fakePlatformLoggingProvider); + + expect(errorFn1).not.to.be.called; + expect(listener1).to.be.called; + expect(listener2).to.be.called; + }); + it('loads persisted token to memory and returns it', async () => { - const clock = useFakeTimers(); activate(app, FAKE_SITE_KEY); - stub(storage, 'readTokenFromStorage').returns( - Promise.resolve(fakeCachedAppCheckToken) - ); + storageReadStub.resolves(fakeCachedAppCheckToken); const clientStub = stub(client, 'exchangeToken'); expect(getState(app).token).to.equal(undefined); - expect(await getToken(app, fakePlatformLoggingProvider)).to.deep.equal({ + const result = await getToken(app, fakePlatformLoggingProvider); + expect(result).to.deep.equal({ token: fakeCachedAppCheckToken.token }); expect(getState(app).token).to.equal(fakeCachedAppCheckToken); expect(clientStub).has.not.been.called; - - clock.restore(); }); it('persists token to storage', async () => { - activate(app, FAKE_SITE_KEY); + activate(app, FAKE_SITE_KEY, false); - stub(storage, 'readTokenFromStorage').returns(Promise.resolve(undefined)); - stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken)); - stub(client, 'exchangeToken').returns( - Promise.resolve(fakeRecaptchaAppCheckToken) - ); - const storageWriteStub = stub(storage, 'writeTokenToStorage'); + stub(reCAPTCHA, 'getToken').resolves(fakeRecaptchaToken); + stub(client, 'exchangeToken').resolves(fakeRecaptchaAppCheckToken); const result = await getToken(app, fakePlatformLoggingProvider); expect(result).to.deep.equal({ token: fakeRecaptchaAppCheckToken.token }); expect(storageWriteStub).has.been.calledWith( @@ -235,27 +243,23 @@ describe('internal api', () => { }); it('returns the valid token in memory without making network request', async () => { - const clock = useFakeTimers(); activate(app, FAKE_SITE_KEY); setState(app, { ...getState(app), token: fakeRecaptchaAppCheckToken }); const clientStub = stub(client, 'exchangeToken'); - expect(await getToken(app, fakePlatformLoggingProvider)).to.deep.equal({ + const result = await getToken(app, fakePlatformLoggingProvider); + expect(result).to.deep.equal({ token: fakeRecaptchaAppCheckToken.token }); expect(clientStub).to.not.have.been.called; - - clock.restore(); }); it('force to get new token when forceRefresh is true', async () => { activate(app, FAKE_SITE_KEY); setState(app, { ...getState(app), token: fakeRecaptchaAppCheckToken }); - stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken)); - stub(client, 'exchangeToken').returns( - Promise.resolve(fakeRecaptchaAppCheckToken) - ); + stub(reCAPTCHA, 'getToken').resolves(fakeRecaptchaToken); + stub(client, 'exchangeToken').resolves(fakeRecaptchaAppCheckToken); expect( await getToken(app, fakePlatformLoggingProvider, true) @@ -264,11 +268,11 @@ describe('internal api', () => { }); }); - it('exchanges debug token if in debug mode', async () => { + it('exchanges debug token if in debug mode and there is no cached token', async () => { const exchangeTokenStub: SinonStub = stub( client, 'exchangeToken' - ).returns(Promise.resolve(fakeRecaptchaAppCheckToken)); + ).resolves(fakeRecaptchaAppCheckToken); const debugState = getDebugState(); debugState.enabled = true; debugState.token = new Deferred(); @@ -284,8 +288,15 @@ describe('internal api', () => { }); describe('addTokenListener', () => { + const fakeRecaptchaAppCheckToken = { + token: 'fake-recaptcha-app-check-token', + // This makes isValid(token) true. + expireTimeMillis: Date.now() + 60000, + issuedAtTimeMillis: 0 + }; it('adds token listeners', () => { const listener = (): void => {}; + stub(client, 'exchangeToken').resolves(fakeRecaptchaAppCheckToken); addTokenListener(app, fakePlatformLoggingProvider, listener); @@ -294,6 +305,7 @@ describe('internal api', () => { it('starts proactively refreshing token after adding the first listener', () => { const listener = (): void => {}; + stub(client, 'exchangeToken').resolves(fakeRecaptchaAppCheckToken); setState(app, { ...getState(app), isTokenAutoRefreshEnabled: true }); expect(getState(app).tokenObservers.length).to.equal(0); expect(getState(app).tokenRefresher).to.equal(undefined); @@ -301,86 +313,61 @@ describe('internal api', () => { addTokenListener(app, fakePlatformLoggingProvider, listener); expect(getState(app).tokenRefresher?.isRunning()).to.be.true; + + removeTokenListener(app, listener); }); - it('notifies the listener with the valid token in memory immediately', done => { + it('notifies the listener with the valid token in memory immediately', async () => { const clock = useFakeTimers(); - const fakeListener = (token: AppCheckTokenResult): void => { - expect(token).to.deep.equal({ - token: `fake-memory-app-check-token` - }); - clock.restore(); - done(); - }; + const listener = stub(); setState(app, { ...getState(app), token: { token: `fake-memory-app-check-token`, - expireTimeMillis: 123, + expireTimeMillis: Date.now() + 60000, issuedAtTimeMillis: 0 } }); - addTokenListener(app, fakePlatformLoggingProvider, fakeListener); + addTokenListener(app, fakePlatformLoggingProvider, listener); + await clock.runAllAsync(); + expect(listener).to.be.calledWith({ + token: 'fake-memory-app-check-token' + }); + clock.restore(); }); it('notifies the listener with the valid token in storage', done => { - const clock = useFakeTimers(); - activate(app, FAKE_SITE_KEY, true); - stub(storage, 'readTokenFromStorage').returns( - Promise.resolve({ - token: `fake-cached-app-check-token`, - expireTimeMillis: 123, - issuedAtTimeMillis: 0 - }) - ); + activate(app, FAKE_SITE_KEY); + storageReadStub.resolves({ + token: `fake-cached-app-check-token`, + expireTimeMillis: Date.now() + 60000, + issuedAtTimeMillis: 0 + }); + // Need to use done() if the callback will be called by the + // refresher. const fakeListener = (token: AppCheckTokenResult): void => { expect(token).to.deep.equal({ token: `fake-cached-app-check-token` }); - clock.restore(); done(); }; addTokenListener(app, fakePlatformLoggingProvider, fakeListener); - clock.tick(1); - }); - - it('notifies the listener with the debug token immediately', done => { - const fakeListener = (token: AppCheckTokenResult): void => { - expect(token).to.deep.equal({ - token: `my-debug-token` - }); - done(); - }; - - const debugState = getDebugState(); - debugState.enabled = true; - debugState.token = new Deferred(); - debugState.token.resolve('my-debug-token'); - - activate(app, FAKE_SITE_KEY, true); - addTokenListener(app, fakePlatformLoggingProvider, fakeListener); - }); - - it('does NOT start token refresher in debug mode', () => { - const debugState = getDebugState(); - debugState.enabled = true; - debugState.token = new Deferred(); - debugState.token.resolve('my-debug-token'); - - activate(app, FAKE_SITE_KEY, true); - addTokenListener(app, fakePlatformLoggingProvider, () => {}); - - const state = getState(app); - expect(state.tokenRefresher).is.undefined; }); }); describe('removeTokenListener', () => { + const fakeRecaptchaAppCheckToken = { + token: 'fake-recaptcha-app-check-token', + // This makes isValid(token) true. + expireTimeMillis: Date.now() + 60000, + issuedAtTimeMillis: 0 + }; it('should remove token listeners', () => { + stub(client, 'exchangeToken').resolves(fakeRecaptchaAppCheckToken); const listener = (): void => {}; addTokenListener(app, fakePlatformLoggingProvider, listener); expect(getState(app).tokenObservers.length).to.equal(1); @@ -390,6 +377,7 @@ describe('internal api', () => { }); it('should stop proactively refreshing token after deleting the last listener', () => { + stub(client, 'exchangeToken').resolves(fakeRecaptchaAppCheckToken); const listener = (): void => {}; setState(app, { ...getState(app), isTokenAutoRefreshEnabled: true }); diff --git a/packages/app-check/src/internal-api.ts b/packages/app-check/src/internal-api.ts index f7d0767ee9b..ebbe70fe4fd 100644 --- a/packages/app-check/src/internal-api.ts +++ b/packages/app-check/src/internal-api.ts @@ -24,7 +24,6 @@ import { import { AppCheckTokenInternal, AppCheckTokenObserver, - getDebugState, getState, setState } from './state'; @@ -72,25 +71,17 @@ export async function getToken( forceRefresh = false ): Promise { ensureActivated(app); - /** - * DEBUG MODE - * return the debug token directly - */ - if (isDebugMode()) { - const tokenFromDebugExchange: AppCheckTokenInternal = await exchangeToken( - getExchangeDebugTokenRequest(app, await getDebugToken()), - platformLoggerProvider - ); - return { token: tokenFromDebugExchange.token }; - } const state = getState(app); + /** + * First check if there is a token in memory from a previous `getToken()` call. + */ let token: AppCheckTokenInternal | undefined = state.token; let error: Error | undefined = undefined; /** - * try to load token from indexedDB if it's the first time this function is called + * If there is no token in memory, try to load token from indexedDB. */ if (!token) { // readTokenFromStorage() always resolves. In case of an error, it resolves with `undefined`. @@ -104,13 +95,30 @@ export async function getToken( } } - // return the cached token if it's valid + // Return the cached token (from either memory or indexedDB) if it's valid if (!forceRefresh && token && isValid(token)) { return { token: token.token }; } + /** + * DEBUG MODE + * If debug mode is set, and there is no cached token, fetch a new App + * Check token using the debug token, and return it directly. + */ + if (isDebugMode()) { + const tokenFromDebugExchange: AppCheckTokenInternal = await exchangeToken( + getExchangeDebugTokenRequest(app, await getDebugToken()), + platformLoggerProvider + ); + // Write debug token to indexedDB. + await writeTokenToStorage(app, tokenFromDebugExchange); + // Write debug token to state. + setState(app, { ...state, token: tokenFromDebugExchange }); + return { token: tokenFromDebugExchange.token }; + } + /** * request a new token */ @@ -155,7 +163,7 @@ export async function getToken( interopTokenResult = { token: token.token }; - // write the new token to the memory state as well ashe persistent storage. + // write the new token to the memory state as well as the persistent storage. // Only do it if we got a valid new token setState(app, { ...state, token }); await writeTokenToStorage(app, token); @@ -182,48 +190,30 @@ export function addTokenListener( }; /** - * DEBUG MODE - * - * invoke the listener once with the debug token. + * Invoke the listener with the valid token, then start the token refresher */ - if (isDebugMode()) { - const debugState = getDebugState(); - if (debugState.enabled && debugState.token) { - debugState.token.promise - .then(token => listener({ token })) - .catch(() => { - /** Ignore errors in listeners. */ - }); - } - } else { - /** - * PROD MODE - * - * invoke the listener with the valid token, then start the token refresher - */ - if (!newState.tokenRefresher) { - const tokenRefresher = createTokenRefresher(app, platformLoggerProvider); - newState.tokenRefresher = tokenRefresher; - } + if (!newState.tokenRefresher) { + const tokenRefresher = createTokenRefresher(app, platformLoggerProvider); + newState.tokenRefresher = tokenRefresher; + } - // Create the refresher but don't start it if `isTokenAutoRefreshEnabled` - // is not true. - if ( - !newState.tokenRefresher.isRunning() && - state.isTokenAutoRefreshEnabled === true - ) { - newState.tokenRefresher.start(); - } + // Create the refresher but don't start it if `isTokenAutoRefreshEnabled` + // is not true. + if ( + !newState.tokenRefresher.isRunning() && + state.isTokenAutoRefreshEnabled === true + ) { + newState.tokenRefresher.start(); + } - // invoke the listener async immediately if there is a valid token - if (state.token && isValid(state.token)) { - const validToken = state.token; - Promise.resolve() - .then(() => listener({ token: validToken.token })) - .catch(() => { - /** Ignore errors in listeners. */ - }); - } + // invoke the listener async immediately if there is a valid token + if (state.token && isValid(state.token)) { + const validToken = state.token; + Promise.resolve() + .then(() => listener({ token: validToken.token })) + .catch(() => { + /** Ignore errors in listeners. */ + }); } setState(app, newState); diff --git a/packages/app-check/src/state.ts b/packages/app-check/src/state.ts index 4c983c835ba..d8fe52b046e 100644 --- a/packages/app-check/src/state.ts +++ b/packages/app-check/src/state.ts @@ -54,6 +54,7 @@ export interface ReCAPTCHAState { export interface DebugState { enabled: boolean; + // This is the debug token string the user interacts with. token?: Deferred; }