From 786fc92a609c8003c0270ba28124035bd654d20d Mon Sep 17 00:00:00 2001 From: Christina Holland Date: Thu, 6 Apr 2023 11:12:21 -0700 Subject: [PATCH 1/7] Catch all recaptcha errors --- packages/app-check/src/api.test.ts | 10 +++ packages/app-check/src/internal-api.test.ts | 74 +++++++++++++++------ packages/app-check/src/providers.test.ts | 21 ++++-- packages/app-check/src/providers.ts | 5 ++ packages/app-check/src/recaptcha.test.ts | 10 ++- packages/app-check/src/recaptcha.ts | 14 +++- packages/app-check/src/state.ts | 2 + 7 files changed, 105 insertions(+), 31 deletions(-) diff --git a/packages/app-check/src/api.test.ts b/packages/app-check/src/api.test.ts index 655e34e9d70..5f6cf1082a3 100644 --- a/packages/app-check/src/api.test.ts +++ b/packages/app-check/src/api.test.ts @@ -60,6 +60,10 @@ describe('api', () => { let storageReadStub: SinonStub; let storageWriteStub: SinonStub; + function setRecaptchaSuccess(isSuccess: boolean = true): void { + getStateReference(app).reCAPTCHAState!.succeeded = isSuccess; + } + beforeEach(() => { app = getFullApp(); storageReadStub = stub(storage, 'readTokenFromStorage').resolves(undefined); @@ -291,6 +295,8 @@ describe('api', () => { isTokenAutoRefreshEnabled: true }); + setRecaptchaSuccess(true); + expect(getStateReference(app).tokenObservers.length).to.equal(1); const fakeRecaptchaToken = 'fake-recaptcha-token'; @@ -335,6 +341,8 @@ describe('api', () => { isTokenAutoRefreshEnabled: true }); + setRecaptchaSuccess(true); + expect(getStateReference(app).tokenObservers.length).to.equal(1); const fakeRecaptchaToken = 'fake-recaptcha-token'; @@ -391,6 +399,8 @@ describe('api', () => { isTokenAutoRefreshEnabled: false }); + setRecaptchaSuccess(true); + expect(getStateReference(app).tokenObservers.length).to.equal(0); const fakeRecaptchaToken = 'fake-recaptcha-token'; diff --git a/packages/app-check/src/internal-api.test.ts b/packages/app-check/src/internal-api.test.ts index 4c29476e897..81a0bdb2f99 100644 --- a/packages/app-check/src/internal-api.test.ts +++ b/packages/app-check/src/internal-api.test.ts @@ -70,6 +70,15 @@ describe('internal api', () => { let storageReadStub: SinonStub; let storageWriteStub: SinonStub; + function stubGetRecaptchaToken( + token: string = fakeRecaptchaToken, + isSuccess: boolean = true + ): SinonStub { + getStateReference(app).reCAPTCHAState!.succeeded = isSuccess; + + return stub(reCAPTCHA, 'getToken').returns(Promise.resolve(token)); + } + beforeEach(() => { app = getFullApp(); storageReadStub = stub(storage, 'readTokenFromStorage').resolves(undefined); @@ -104,9 +113,7 @@ describe('internal api', () => { provider: new ReCaptchaV3Provider(FAKE_SITE_KEY) }); - const reCAPTCHASpy = stub(reCAPTCHA, 'getToken').returns( - Promise.resolve(fakeRecaptchaToken) - ); + const reCAPTCHASpy = stubGetRecaptchaToken(); const exchangeTokenStub: SinonStub = stub( client, 'exchangeToken' @@ -127,9 +134,8 @@ describe('internal api', () => { provider: new ReCaptchaEnterpriseProvider(FAKE_SITE_KEY) }); - const reCAPTCHASpy = stub(reCAPTCHA, 'getToken').returns( - Promise.resolve(fakeRecaptchaToken) - ); + const reCAPTCHASpy = stubGetRecaptchaToken(); + const exchangeTokenStub: SinonStub = stub( client, 'exchangeToken' @@ -151,9 +157,7 @@ describe('internal api', () => { provider: new ReCaptchaV3Provider(FAKE_SITE_KEY) }); - const reCAPTCHASpy = stub(reCAPTCHA, 'getToken').returns( - Promise.resolve(fakeRecaptchaToken) - ); + const reCAPTCHASpy = stubGetRecaptchaToken(); const error = new Error('oops, something went wrong'); stub(client, 'exchangeToken').returns(Promise.reject(error)); @@ -171,6 +175,26 @@ describe('internal api', () => { errorStub.restore(); }); + it('resolves with a dummy token and an error if recaptcha failed', async () => { + const errorStub = stub(console, 'error'); + const appCheck = initializeAppCheck(app, { + provider: new ReCaptchaV3Provider(FAKE_SITE_KEY) + }); + + const reCAPTCHASpy = stubGetRecaptchaToken('', false); + const exchangeTokenStub = stub(client, 'exchangeToken'); + + const token = await getToken(appCheck as AppCheckService); + + expect(reCAPTCHASpy).to.be.called; + expect(exchangeTokenStub).to.not.be.called; + expect(token.token).to.equal(formatDummyToken(defaultTokenErrorData)); + expect(errorStub.args[0][1].message).to.include( + AppCheckError.RECAPTCHA_ERROR + ); + errorStub.restore(); + }); + it('notifies listeners using cached token', async () => { storageReadStub.resolves(fakeCachedAppCheckToken); const appCheck = initializeAppCheck(app, { @@ -213,7 +237,7 @@ describe('internal api', () => { isTokenAutoRefreshEnabled: true }); - stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken)); + stubGetRecaptchaToken(); stub(client, 'exchangeToken').returns( Promise.resolve(fakeRecaptchaAppCheckToken) ); @@ -247,7 +271,7 @@ describe('internal api', () => { provider: new ReCaptchaV3Provider(FAKE_SITE_KEY), isTokenAutoRefreshEnabled: true }); - stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken)); + stubGetRecaptchaToken(); stub(client, 'exchangeToken').rejects('exchange error'); const listener1 = spy(); const errorFn1 = spy(); @@ -271,7 +295,7 @@ describe('internal api', () => { provider: new ReCaptchaV3Provider(FAKE_SITE_KEY), isTokenAutoRefreshEnabled: true }); - stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken)); + stubGetRecaptchaToken(); stub(client, 'exchangeToken').returns( Promise.resolve(fakeRecaptchaAppCheckToken) ); @@ -324,7 +348,7 @@ describe('internal api', () => { provider: new ReCaptchaV3Provider(FAKE_SITE_KEY) }); - stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken)); + stubGetRecaptchaToken(); stub(client, 'exchangeToken').returns( Promise.resolve(fakeRecaptchaAppCheckToken) ); @@ -365,7 +389,7 @@ describe('internal api', () => { token: fakeRecaptchaAppCheckToken }); - stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken)); + stubGetRecaptchaToken(); stub(client, 'exchangeToken').returns( Promise.resolve({ token: 'new-recaptcha-app-check-token', @@ -390,7 +414,7 @@ describe('internal api', () => { cachedTokenPromise: undefined }); - stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken)); + stubGetRecaptchaToken(); stub(client, 'exchangeToken').returns( Promise.resolve({ token: 'new-recaptcha-app-check-token', @@ -431,7 +455,7 @@ describe('internal api', () => { cachedTokenPromise: undefined }); - stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken)); + stubGetRecaptchaToken(); let count = 0; stub(client, 'exchangeToken').callsFake( () => @@ -485,7 +509,7 @@ describe('internal api', () => { } }); - stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken)); + stubGetRecaptchaToken(); stub(client, 'exchangeToken').returns( Promise.resolve({ token: 'new-recaptcha-app-check-token', @@ -532,7 +556,7 @@ describe('internal api', () => { issuedAtTimeMillis: 0 }; - stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken)); + stubGetRecaptchaToken(); stub(client, 'exchangeToken').returns(Promise.resolve(freshToken)); expect(await getToken(appCheck as AppCheckService)).to.deep.equal({ @@ -556,7 +580,7 @@ describe('internal api', () => { token: fakeRecaptchaAppCheckToken }); - stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken)); + stubGetRecaptchaToken(); stub(client, 'exchangeToken').returns(Promise.reject(new Error('blah'))); const tokenResult = await getToken(appCheck as AppCheckService, true); @@ -589,6 +613,7 @@ describe('internal api', () => { const appCheck = initializeAppCheck(app, { provider: new ReCaptchaV3Provider(FAKE_SITE_KEY) }); + stubGetRecaptchaToken(); const warnStub = stub(logger, 'warn'); stub(client, 'exchangeToken').returns( Promise.reject( @@ -615,6 +640,7 @@ describe('internal api', () => { const appCheck = initializeAppCheck(app, { provider: new ReCaptchaV3Provider(FAKE_SITE_KEY) }); + stubGetRecaptchaToken(); const warnStub = stub(logger, 'warn'); stub(client, 'exchangeToken').returns( Promise.reject( @@ -765,6 +791,8 @@ describe('internal api', () => { }) ); + stubGetRecaptchaToken(); + addTokenListener( appCheck as AppCheckService, ListenerType.INTERNAL, @@ -799,6 +827,8 @@ describe('internal api', () => { } }); + stubGetRecaptchaToken(); + const fakeListener: AppCheckTokenListener = stub(); const fakeExchange = stub(client, 'exchangeToken').returns( @@ -838,6 +868,8 @@ describe('internal api', () => { } }); + stubGetRecaptchaToken(); + const fakeListener: AppCheckTokenListener = stub(); const fakeExchange = stub(client, 'exchangeToken').returns( @@ -865,6 +897,8 @@ describe('internal api', () => { provider: new ReCaptchaV3Provider(FAKE_SITE_KEY), isTokenAutoRefreshEnabled: true }); + + stubGetRecaptchaToken(); setInitialState(app, { ...getStateReference(app), token: { @@ -905,6 +939,8 @@ describe('internal api', () => { provider: new ReCaptchaV3Provider(FAKE_SITE_KEY), isTokenAutoRefreshEnabled: true }); + + stubGetRecaptchaToken(); setInitialState(app, { ...getStateReference(app), token: { diff --git a/packages/app-check/src/providers.test.ts b/packages/app-check/src/providers.test.ts index c9734d1ffc3..2342b582ad8 100644 --- a/packages/app-check/src/providers.test.ts +++ b/packages/app-check/src/providers.test.ts @@ -25,7 +25,12 @@ import { stub, useFakeTimers } from 'sinon'; import { expect } from 'chai'; import { FirebaseError } from '@firebase/util'; import { AppCheckError } from './errors'; -import { clearState } from './state'; +import { + clearState, + DEFAULT_STATE, + getStateReference, + setInitialState +} from './state'; import { deleteApp, FirebaseApp } from '@firebase/app'; describe('ReCaptchaV3Provider', () => { @@ -34,6 +39,7 @@ describe('ReCaptchaV3Provider', () => { beforeEach(() => { clock = useFakeTimers(); app = getFullApp(); + setInitialState(app, DEFAULT_STATE); stub(util, 'getRecaptcha').returns(getFakeGreCAPTCHA()); stub(reCAPTCHA, 'getToken').returns( Promise.resolve('fake-recaptcha-token') @@ -46,7 +52,6 @@ describe('ReCaptchaV3Provider', () => { return deleteApp(app); }); it('getToken() gets a token from the exchange endpoint', async () => { - const app = getFullApp(); const provider = new ReCaptchaV3Provider('fake-site-key'); stub(client, 'exchangeToken').resolves({ token: 'fake-exchange-token', @@ -54,11 +59,11 @@ describe('ReCaptchaV3Provider', () => { expireTimeMillis: 10 }); provider.initialize(app); + getStateReference(app).reCAPTCHAState!.succeeded = true; const token = await provider.getToken(); expect(token.token).to.equal('fake-exchange-token'); }); it('getToken() throttles 1d on 403', async () => { - const app = getFullApp(); const provider = new ReCaptchaV3Provider('fake-site-key'); stub(client, 'exchangeToken').rejects( new FirebaseError(AppCheckError.FETCH_STATUS_ERROR, 'some-message', { @@ -66,13 +71,13 @@ describe('ReCaptchaV3Provider', () => { }) ); provider.initialize(app); + getStateReference(app).reCAPTCHAState!.succeeded = true; await expect(provider.getToken()).to.be.rejectedWith('1d'); // Wait 10s and try again to see if wait time string decreases. clock.tick(10000); await expect(provider.getToken()).to.be.rejectedWith('23h'); }); it('getToken() throttles exponentially on 503', async () => { - const app = getFullApp(); const provider = new ReCaptchaV3Provider('fake-site-key'); let exchangeTokenStub = stub(client, 'exchangeToken').rejects( new FirebaseError(AppCheckError.FETCH_STATUS_ERROR, 'some-message', { @@ -80,6 +85,7 @@ describe('ReCaptchaV3Provider', () => { }) ); provider.initialize(app); + getStateReference(app).reCAPTCHAState!.succeeded = true; await expect(provider.getToken()).to.be.rejectedWith('503'); expect(exchangeTokenStub).to.be.called; exchangeTokenStub.resetHistory(); @@ -120,6 +126,7 @@ describe('ReCaptchaEnterpriseProvider', () => { beforeEach(() => { clock = useFakeTimers(); app = getFullApp(); + setInitialState(app, DEFAULT_STATE); stub(util, 'getRecaptcha').returns(getFakeGreCAPTCHA()); stub(reCAPTCHA, 'getToken').returns( Promise.resolve('fake-recaptcha-token') @@ -132,7 +139,6 @@ describe('ReCaptchaEnterpriseProvider', () => { return deleteApp(app); }); it('getToken() gets a token from the exchange endpoint', async () => { - const app = getFullApp(); const provider = new ReCaptchaEnterpriseProvider('fake-site-key'); stub(client, 'exchangeToken').resolves({ token: 'fake-exchange-token', @@ -140,11 +146,11 @@ describe('ReCaptchaEnterpriseProvider', () => { expireTimeMillis: 10 }); provider.initialize(app); + getStateReference(app).reCAPTCHAState!.succeeded = true; const token = await provider.getToken(); expect(token.token).to.equal('fake-exchange-token'); }); it('getToken() throttles 1d on 403', async () => { - const app = getFullApp(); const provider = new ReCaptchaEnterpriseProvider('fake-site-key'); stub(client, 'exchangeToken').rejects( new FirebaseError(AppCheckError.FETCH_STATUS_ERROR, 'some-message', { @@ -152,13 +158,13 @@ describe('ReCaptchaEnterpriseProvider', () => { }) ); provider.initialize(app); + getStateReference(app).reCAPTCHAState!.succeeded = true; await expect(provider.getToken()).to.be.rejectedWith('1d'); // Wait 10s and try again to see if wait time string decreases. clock.tick(10000); await expect(provider.getToken()).to.be.rejectedWith('23h'); }); it('getToken() throttles exponentially on 503', async () => { - const app = getFullApp(); const provider = new ReCaptchaEnterpriseProvider('fake-site-key'); let exchangeTokenStub = stub(client, 'exchangeToken').rejects( new FirebaseError(AppCheckError.FETCH_STATUS_ERROR, 'some-message', { @@ -166,6 +172,7 @@ describe('ReCaptchaEnterpriseProvider', () => { }) ); provider.initialize(app); + getStateReference(app).reCAPTCHAState!.succeeded = true; await expect(provider.getToken()).to.be.rejectedWith('503'); expect(exchangeTokenStub).to.be.called; exchangeTokenStub.resetHistory(); diff --git a/packages/app-check/src/providers.ts b/packages/app-check/src/providers.ts index bf288259a82..4b7f9de6627 100644 --- a/packages/app-check/src/providers.ts +++ b/packages/app-check/src/providers.ts @@ -35,6 +35,7 @@ import { initializeV3 as initializeRecaptchaV3, initializeEnterprise as initializeRecaptchaEnterprise } from './recaptcha'; +import { getStateReference } from './state'; import { AppCheckProvider, AppCheckTokenInternal, ThrottleData } from './types'; import { getDurationString } from './util'; @@ -73,6 +74,10 @@ export class ReCaptchaV3Provider implements AppCheckProvider { throw ERROR_FACTORY.create(AppCheckError.RECAPTCHA_ERROR); } ); + // Check if a failure state was set by the recaptcha "error-callback". + if (!getStateReference(this._app!).reCAPTCHAState?.succeeded) { + throw ERROR_FACTORY.create(AppCheckError.RECAPTCHA_ERROR); + } let result; try { result = await exchangeToken( diff --git a/packages/app-check/src/recaptcha.test.ts b/packages/app-check/src/recaptcha.test.ts index 8d371025195..7f028cca1a5 100644 --- a/packages/app-check/src/recaptcha.test.ts +++ b/packages/app-check/src/recaptcha.test.ts @@ -17,7 +17,7 @@ import '../test/setup'; import { expect } from 'chai'; -import { stub } from 'sinon'; +import { stub, match } from 'sinon'; import { deleteApp, FirebaseApp } from '@firebase/app'; import { getFullApp, @@ -93,7 +93,9 @@ describe('recaptcha', () => { expect(renderStub).to.be.calledWith(`fire_app_check_${app.name}`, { sitekey: FAKE_SITE_KEY, - size: 'invisible' + size: 'invisible', + callback: match.any, + 'error-callback': match.any }); expect(getStateReference(app).reCAPTCHAState?.widgetId).to.equal( @@ -141,7 +143,9 @@ describe('recaptcha', () => { expect(renderStub).to.be.calledWith(`fire_app_check_${app.name}`, { sitekey: FAKE_SITE_KEY, - size: 'invisible' + size: 'invisible', + callback: match.any, + 'error-callback': match.any }); expect(getStateReference(app).reCAPTCHAState?.widgetId).to.equal( diff --git a/packages/app-check/src/recaptcha.ts b/packages/app-check/src/recaptcha.ts index 2956f830a7b..d69dc3091cd 100644 --- a/packages/app-check/src/recaptcha.ts +++ b/packages/app-check/src/recaptcha.ts @@ -143,11 +143,19 @@ function renderInvisibleWidget( grecaptcha: GreCAPTCHA, container: string ): void { + const state = getStateReference(app); const widgetId = grecaptcha.render(container, { sitekey: siteKey, - size: 'invisible' + size: 'invisible', + // Success callback - set state + callback: () => { + getStateReference(app).reCAPTCHAState!.succeeded = true; + }, + // Failure callback - set state + 'error-callback': () => { + getStateReference(app).reCAPTCHAState!.succeeded = false; + } }); - const state = getStateReference(app); state.reCAPTCHAState = { ...state.reCAPTCHAState!, // state.reCAPTCHAState is set in the initialize() @@ -191,4 +199,6 @@ export interface GreCAPTCHA { export interface GreCAPTCHARenderOption { sitekey: string; size: 'invisible'; + callback: () => void; + 'error-callback': () => void; } diff --git a/packages/app-check/src/state.ts b/packages/app-check/src/state.ts index 23c0efc0bd9..e1c6966f75f 100644 --- a/packages/app-check/src/state.ts +++ b/packages/app-check/src/state.ts @@ -39,6 +39,8 @@ export interface AppCheckState { export interface ReCAPTCHAState { initialized: Deferred; widgetId?: string; + // True if the most recent recaptcha check succeeded. + succeeded?: boolean; } export interface DebugState { From f9f73ea0ecd3c5e38c4cbe1d26ef363b41be245c Mon Sep 17 00:00:00 2001 From: Christina Holland Date: Mon, 10 Apr 2023 10:28:10 -0700 Subject: [PATCH 2/7] cover enterprise, add changeset --- .changeset/shaggy-zebras-leave.md | 5 +++++ packages/app-check/src/providers.ts | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 .changeset/shaggy-zebras-leave.md diff --git a/.changeset/shaggy-zebras-leave.md b/.changeset/shaggy-zebras-leave.md new file mode 100644 index 00000000000..d9b5fcdfb7a --- /dev/null +++ b/.changeset/shaggy-zebras-leave.md @@ -0,0 +1,5 @@ +--- +'@firebase/app-check': patch +--- + +Catch all ReCAPTCHA errors and, if caught, prevent App Check from making a request to the exchange endpoint. diff --git a/packages/app-check/src/providers.ts b/packages/app-check/src/providers.ts index 4b7f9de6627..55ab598b5e9 100644 --- a/packages/app-check/src/providers.ts +++ b/packages/app-check/src/providers.ts @@ -164,6 +164,10 @@ export class ReCaptchaEnterpriseProvider implements AppCheckProvider { throw ERROR_FACTORY.create(AppCheckError.RECAPTCHA_ERROR); } ); + // Check if a failure state was set by the recaptcha "error-callback". + if (!getStateReference(this._app!).reCAPTCHAState?.succeeded) { + throw ERROR_FACTORY.create(AppCheckError.RECAPTCHA_ERROR); + } let result; try { result = await exchangeToken( From aa3bb14a5237711c7e8d4b58e77724c49105109e Mon Sep 17 00:00:00 2001 From: Christina Holland Date: Mon, 10 Apr 2023 16:39:40 -0700 Subject: [PATCH 3/7] Fix annoying messages from heartbeat --- packages/app-check/test/util.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/app-check/test/util.ts b/packages/app-check/test/util.ts index 4a4f8671adb..56c0892631d 100644 --- a/packages/app-check/test/util.ts +++ b/packages/app-check/test/util.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { FirebaseApp, initializeApp, _registerComponent } from '@firebase/app'; +import { FirebaseApp, initializeApp, _addOrOverwriteComponent, _registerComponent } from '@firebase/app'; import { GreCAPTCHA, GreCAPTCHATopLevel, @@ -62,11 +62,14 @@ export function getFakeAppCheck(app: FirebaseApp): AppCheck { export function getFullApp(): FirebaseApp { const app = initializeApp(fakeConfig); - _registerComponent( + _addOrOverwriteComponent(app, + //@ts-ignore new Component( 'heartbeat', () => { - return {} as any; + return { + triggerHeartbeat: () => {} + } as any; }, ComponentType.PUBLIC ) From 9ac65a0aef29dbdc54c5c07fdc437bc50c23dc83 Mon Sep 17 00:00:00 2001 From: Christina Holland Date: Mon, 10 Apr 2023 16:42:18 -0700 Subject: [PATCH 4/7] prettier pass --- packages/app-check/test/util.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/app-check/test/util.ts b/packages/app-check/test/util.ts index 56c0892631d..4255c89cc98 100644 --- a/packages/app-check/test/util.ts +++ b/packages/app-check/test/util.ts @@ -15,7 +15,12 @@ * limitations under the License. */ -import { FirebaseApp, initializeApp, _addOrOverwriteComponent, _registerComponent } from '@firebase/app'; +import { + FirebaseApp, + initializeApp, + _addOrOverwriteComponent, + _registerComponent +} from '@firebase/app'; import { GreCAPTCHA, GreCAPTCHATopLevel, @@ -62,7 +67,8 @@ export function getFakeAppCheck(app: FirebaseApp): AppCheck { export function getFullApp(): FirebaseApp { const app = initializeApp(fakeConfig); - _addOrOverwriteComponent(app, + _addOrOverwriteComponent( + app, //@ts-ignore new Component( 'heartbeat', From bde26c78850be613416d2ccd144ba179b9013223 Mon Sep 17 00:00:00 2001 From: Christina Holland Date: Tue, 11 Apr 2023 09:47:11 -0700 Subject: [PATCH 5/7] trigger CI From 9e602d0d8b820653c49c8d53509c9fd7ae1e5661 Mon Sep 17 00:00:00 2001 From: Christina Holland Date: Wed, 12 Apr 2023 11:43:17 -0700 Subject: [PATCH 6/7] Move lines around --- packages/app-check/src/recaptcha.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app-check/src/recaptcha.ts b/packages/app-check/src/recaptcha.ts index d69dc3091cd..287b56494d6 100644 --- a/packages/app-check/src/recaptcha.ts +++ b/packages/app-check/src/recaptcha.ts @@ -143,7 +143,6 @@ function renderInvisibleWidget( grecaptcha: GreCAPTCHA, container: string ): void { - const state = getStateReference(app); const widgetId = grecaptcha.render(container, { sitekey: siteKey, size: 'invisible', @@ -156,6 +155,8 @@ function renderInvisibleWidget( getStateReference(app).reCAPTCHAState!.succeeded = false; } }); + + const state = getStateReference(app); state.reCAPTCHAState = { ...state.reCAPTCHAState!, // state.reCAPTCHAState is set in the initialize() From 7ff8cb9b155d28fc850abd912a4fb3ea985cf8f4 Mon Sep 17 00:00:00 2001 From: Christina Holland Date: Wed, 12 Apr 2023 11:43:34 -0700 Subject: [PATCH 7/7] format --- packages/app-check/src/recaptcha.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-check/src/recaptcha.ts b/packages/app-check/src/recaptcha.ts index 287b56494d6..8eb72e2add7 100644 --- a/packages/app-check/src/recaptcha.ts +++ b/packages/app-check/src/recaptcha.ts @@ -155,7 +155,7 @@ function renderInvisibleWidget( getStateReference(app).reCAPTCHAState!.succeeded = false; } }); - + const state = getStateReference(app); state.reCAPTCHAState = {