11import { isString } from '@guardian/libs' ;
22import type { FormEvent , ReactEventHandler } from 'react' ;
3- import { useEffect , useState } from 'react' ;
3+ import type React from 'react' ;
4+ import { useEffect , useRef , useState } from 'react' ;
5+ import type ReactGoogleRecaptcha from 'react-google-recaptcha' ;
46import { submitComponentEvent } from '../client/ophan/ophan' ;
57import type { RenderingTarget } from '../types/renderingTarget' ;
68import { lazyFetchEmailWithTimeout } from './fetchEmail' ;
@@ -15,6 +17,7 @@ import { useBrowserId } from './useBrowserId';
1517const buildFormData = (
1618 emailAddress : string ,
1719 newsletterId : string ,
20+ token : string ,
1821 marketingOptIn ?: boolean ,
1922 browserId ?: string ,
2023) : FormData => {
@@ -28,7 +31,7 @@ const buildFormData = (
2831 formData . append ( 'ref' , pageRef ) ;
2932 formData . append ( 'refViewId' , refViewId ) ;
3033 formData . append ( 'name' , '' ) ;
31- formData . append ( 'g-recaptcha-response' , '' ) ;
34+ formData . append ( 'g-recaptcha-response' , token ) ;
3235
3336 if ( marketingOptIn !== undefined ) {
3437 formData . append ( 'marketing' , marketingOptIn ? 'true' : 'false' ) ;
@@ -79,7 +82,11 @@ type EventDescription =
7982 | 'form-submission'
8083 | 'submission-confirmed'
8184 | 'submission-failed'
82- | 'form-submit-error' ;
85+ | 'open-captcha'
86+ | 'captcha-load-error'
87+ | 'form-submit-error'
88+ | 'captcha-not-passed'
89+ | 'captcha-passed' ;
8390
8491const sendTracking = (
8592 newsletterId : string ,
@@ -90,15 +97,21 @@ const sendTracking = (
9097
9198 switch ( eventDescription ) {
9299 case 'form-submission' :
100+ case 'captcha-not-passed' :
101+ case 'captcha-passed' :
93102 action = 'ANSWER' ;
94103 break ;
95104 case 'submission-confirmed' :
96105 action = 'SUBSCRIBE' ;
97106 break ;
107+ case 'captcha-load-error' :
98108 case 'form-submit-error' :
99109 case 'submission-failed' :
100110 action = 'CLOSE' ;
101111 break ;
112+ case 'open-captcha' :
113+ action = 'EXPAND' as typeof action ;
114+ break ;
102115 case 'click-button' :
103116 default :
104117 action = 'CLICK' ;
@@ -157,6 +170,15 @@ export type NewsletterSignupFormState = {
157170 /** Inline validation / network error copy. */
158171 errorMessage : string | undefined ;
159172
173+ /** Ref to pass to the `<ReactGoogleRecaptcha>` widget. */
174+ recaptchaRef : React . RefObject < ReactGoogleRecaptcha > ;
175+ /** Site key for the reCAPTCHA widget — `undefined` until resolved. */
176+ captchaSiteKey : string | undefined ;
177+ /** Pass to `ReactGoogleRecaptcha`'s `onChange` prop. */
178+ handleCaptchaComplete : ( token : string | null ) => void ;
179+ /** Pass to `ReactGoogleRecaptcha`'s `onError` prop. */
180+ handleCaptchaLoadError : ( ) => void ;
181+
160182 // Event handlers
161183 handleEmailChange : ( value : string ) => void ;
162184 handleEmailFocus : ( ) => void ;
@@ -180,6 +202,8 @@ export const useNewsletterSignupForm = (
180202 newsletterId : string ,
181203 renderingTarget : RenderingTarget ,
182204) : NewsletterSignupFormState => {
205+ const recaptchaRef = useRef < ReactGoogleRecaptcha > ( null ) ;
206+ const [ captchaSiteKey , setCaptchaSiteKey ] = useState < string > ( ) ;
183207 const [ userEmail , setUserEmail ] = useState < string > ( ) ;
184208 const [ hideEmailInput , setHideEmailInput ] = useState ( false ) ;
185209 const [ isWaitingForResponse , setIsWaitingForResponse ] = useState ( false ) ;
@@ -205,6 +229,7 @@ export const useNewsletterSignupForm = (
205229 } , [ isSignedIn ] ) ;
206230
207231 useEffect ( ( ) => {
232+ setCaptchaSiteKey ( window . guardian . config . page . googleRecaptchaSiteKey ) ;
208233 void resolveEmailIfSignedIn ( ) . then ( ( email ) => {
209234 setUserEmail ( email ) ;
210235 setHideEmailInput ( isString ( email ) ) ;
@@ -214,12 +239,16 @@ export const useNewsletterSignupForm = (
214239 } ) ;
215240 } , [ ] ) ;
216241
217- const submitForm = async ( emailAddress : string ) : Promise < void > => {
242+ const submitForm = async (
243+ emailAddress : string ,
244+ token : string ,
245+ ) : Promise < void > => {
218246 sendTracking ( newsletterId , 'form-submission' , renderingTarget ) ;
219247
220248 const formData = buildFormData (
221249 emailAddress ,
222250 newsletterId ,
251+ token ,
223252 marketingOptIn ,
224253 browserId ,
225254 ) ;
@@ -243,6 +272,30 @@ export const useNewsletterSignupForm = (
243272 ) ;
244273 } ;
245274
275+ const handleCaptchaComplete = ( token : string | null ) : void => {
276+ if ( ! token ) {
277+ sendTracking ( newsletterId , 'captcha-not-passed' , renderingTarget ) ;
278+ setIsWaitingForResponse ( false ) ;
279+ return ;
280+ }
281+ sendTracking ( newsletterId , 'captcha-passed' , renderingTarget ) ;
282+ const emailAddress = userEmail ?. trim ( ) ?? '' ;
283+ submitForm ( emailAddress , token ) . catch ( ( error ) => {
284+ // eslint-disable-next-line no-console -- unexpected error
285+ console . error ( error ) ;
286+ sendTracking ( newsletterId , 'form-submit-error' , renderingTarget ) ;
287+ setErrorMessage ( 'Sorry, there was an error signing you up.' ) ;
288+ setIsWaitingForResponse ( false ) ;
289+ recaptchaRef . current ?. reset ( ) ;
290+ } ) ;
291+ } ;
292+
293+ const handleCaptchaLoadError = ( ) : void => {
294+ sendTracking ( newsletterId , 'captcha-load-error' , renderingTarget ) ;
295+ setErrorMessage ( 'Sorry, the reCAPTCHA failed to load.' ) ;
296+ recaptchaRef . current ?. reset ( ) ;
297+ } ;
298+
246299 const handleSubmit = ( event : FormEvent < HTMLFormElement > ) : void => {
247300 event . preventDefault ( ) ;
248301 if ( isWaitingForResponse ) return ;
@@ -255,13 +308,8 @@ export const useNewsletterSignupForm = (
255308
256309 setErrorMessage ( undefined ) ;
257310 setIsWaitingForResponse ( true ) ;
258- submitForm ( emailAddress ) . catch ( ( error ) => {
259- // eslint-disable-next-line no-console -- unexpected error
260- console . error ( error ) ;
261- sendTracking ( newsletterId , 'form-submit-error' , renderingTarget ) ;
262- setErrorMessage ( 'Sorry, there was an error signing you up.' ) ;
263- setIsWaitingForResponse ( false ) ;
264- } ) ;
311+ sendTracking ( newsletterId , 'open-captcha' , renderingTarget ) ;
312+ recaptchaRef . current ?. execute ( ) ;
265313 } ;
266314
267315 return {
@@ -273,6 +321,10 @@ export const useNewsletterSignupForm = (
273321 isWaitingForResponse,
274322 responseOk,
275323 errorMessage,
324+ recaptchaRef,
325+ captchaSiteKey,
326+ handleCaptchaComplete,
327+ handleCaptchaLoadError,
276328 handleEmailChange : ( value ) => {
277329 setUserEmail ( value ) ;
278330 setIsInteracted ( true ) ;
0 commit comments