diff --git a/services/web/frontend/js/features/settings/components/emails/add-email.tsx b/services/web/frontend/js/features/settings/components/emails/add-email.tsx index c7d64e7398..a5b98ba196 100644 --- a/services/web/frontend/js/features/settings/components/emails/add-email.tsx +++ b/services/web/frontend/js/features/settings/components/emails/add-email.tsx @@ -151,7 +151,7 @@ function AddEmail() { return (
- + {InputComponent} @@ -176,7 +176,7 @@ function AddEmail() { return ( - + {InputComponent} diff --git a/services/web/frontend/js/features/settings/components/emails/add-secondary-email-prompt.tsx b/services/web/frontend/js/features/settings/components/emails/add-secondary-email-prompt.tsx index f76f3333e6..79d97265e6 100644 --- a/services/web/frontend/js/features/settings/components/emails/add-secondary-email-prompt.tsx +++ b/services/web/frontend/js/features/settings/components/emails/add-secondary-email-prompt.tsx @@ -80,7 +80,7 @@ export function AddSecondaryEmailPrompt() { <> - +

{t('keep_your_account_safe_add_another_email')}

, 'onChange'> { + page: keyof ExposedSettings['recaptchaDisabled'] + recaptchaRef: React.LegacyRef +} -export const ReCaptcha2 = forwardRef< - ReCAPTCHA, - { page: Page; onChange?: (token: string | null) => void } ->(function ReCaptcha2({ page: site, onChange }, ref) { +export function ReCaptcha2({ + page: site, + onChange, + recaptchaRef, +}: ReCaptcha2Props) { const { recaptchaSiteKey, recaptchaDisabled } = getMeta('ol-ExposedSettings') if (!recaptchaSiteKey) { @@ -19,11 +23,11 @@ export const ReCaptcha2 = forwardRef< } return ( ) -}) +} diff --git a/services/web/frontend/js/shared/hooks/use-recaptcha.ts b/services/web/frontend/js/shared/hooks/use-recaptcha.ts index 93a13cbed6..3b3fe2dce7 100644 --- a/services/web/frontend/js/shared/hooks/use-recaptcha.ts +++ b/services/web/frontend/js/shared/hooks/use-recaptcha.ts @@ -1,13 +1,20 @@ -import { LegacyRef, createRef } from 'react' +import { useRef } from 'react' import ReCAPTCHA from 'react-google-recaptcha' export const useRecaptcha = () => { - const ref: LegacyRef = createRef() - const getReCaptchaToken = async (): Promise => { + const ref = useRef(null) + + const getReCaptchaToken = async (): Promise< + ReturnType + > => { if (!ref.current) { return null } + // Reset the reCAPTCHA before each submission. + // The reCAPTCHA token is meant to be used once per validation + ref.current.reset() return await ref.current.executeAsync() } + return { ref, getReCaptchaToken } } diff --git a/services/web/test/frontend/shared/hooks/use-recaptcha.spec.tsx b/services/web/test/frontend/shared/hooks/use-recaptcha.spec.tsx new file mode 100644 index 0000000000..acc3f09465 --- /dev/null +++ b/services/web/test/frontend/shared/hooks/use-recaptcha.spec.tsx @@ -0,0 +1,37 @@ +import { useRecaptcha } from '@/shared/hooks/use-recaptcha' +import * as ReactGoogleRecaptcha from 'react-google-recaptcha' + +const ReCaptcha = () => { + const { ref: recaptchaRef, getReCaptchaToken } = useRecaptcha() + + const handleClick = async () => { + await getReCaptchaToken() + } + + return ( + <> + + + + ) +} + +describe('useRecaptcha', function () { + it('should reset the captcha', function () { + cy.spy(ReactGoogleRecaptcha.ReCAPTCHA.prototype, 'reset').as('resetSpy') + cy.spy(ReactGoogleRecaptcha.ReCAPTCHA.prototype, 'executeAsync').as( + 'executeAsyncSpy' + ) + + cy.mount() + + cy.findByRole('button', { name: /click/i }).click() + cy.get('@resetSpy').should('have.been.calledOnce') + cy.get('@executeAsyncSpy').should('have.been.calledOnce') + }) +})