@@ -20,11 +20,13 @@ import {
2020 useActionState ,
2121 useEffect ,
2222} from 'react' ;
23+ import { useRef } from 'react' ;
2324import { useFormStatus } from 'react-dom' ;
24- import { useGoogleReCaptcha } from 'react-google-recaptcha-v3 ' ;
25+ import ReCAPTCHA from 'react-google-recaptcha' ;
2526import { z } from 'zod' ;
2627
27- import { RECAPTCHA_TOKEN_FORM_KEY } from '~/lib/recaptcha' ;
28+ import { useReCaptchaSiteKey } from '~/components/recaptcha-provider' ;
29+ import { RECAPTCHA_TOKEN_FORM_KEY } from '~/lib/recaptcha/constants' ;
2830
2931import { ButtonRadioGroup } from '@/vibes/soul/form/button-radio-group' ;
3032import { CardRadioGroup } from '@/vibes/soul/form/card-radio-group' ;
@@ -80,30 +82,12 @@ export interface DynamicFormProps<F extends Field> {
8082 onSuccess ?: ( lastResult : SubmissionResult , successMessage : ReactNode ) => void ;
8183 passwordComplexity ?: PasswordComplexitySettings | null ;
8284 errorTranslations ?: FormErrorTranslationMap ;
83- /** When true, a reCAPTCHA v3 token is obtained and sent with the form (requires ReCaptchaProvider with storefront enabled). */
84- recaptchaEnabled ?: boolean ;
8585}
8686
8787export function DynamicForm < F extends Field > ( props : DynamicFormProps < F > ) {
88- const { recaptchaEnabled = false } = props ;
89-
90- if ( recaptchaEnabled ) {
91- return < DynamicFormWithRecaptcha { ...props } /> ;
92- }
93-
9488 return < DynamicFormInner { ...props } /> ;
9589}
9690
97- function DynamicFormWithRecaptcha < F extends Field > ( props : DynamicFormProps < F > ) {
98- const { executeRecaptcha } = useGoogleReCaptcha ( ) ;
99- return (
100- < DynamicFormInner
101- { ...props }
102- recaptchaExecute = { executeRecaptcha ?? undefined }
103- />
104- ) ;
105- }
106-
10791function DynamicFormInner < F extends Field > ( {
10892 action,
10993 fields,
@@ -117,9 +101,9 @@ function DynamicFormInner<F extends Field>({
117101 onSuccess,
118102 passwordComplexity,
119103 errorTranslations,
120- recaptchaExecute,
121- } : DynamicFormProps < F > & { recaptchaExecute ?: ( action : string ) => Promise < string > } ) {
104+ } : DynamicFormProps < F > ) {
122105 const t = useTranslations ( 'Form' ) ;
106+ const recaptchaSiteKey = useReCaptchaSiteKey ( ) ;
123107 // Remove options from fields before passing to action to reduce payload size
124108 // Options are only needed for rendering, not for processing form submissions
125109 // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -130,6 +114,8 @@ function DynamicFormInner<F extends Field>({
130114 lastResult : null ,
131115 } ) ;
132116
117+ const recaptchaRef = useRef < ReCAPTCHA | null > ( null ) ;
118+
133119 const dynamicSchema = schema ( fields , passwordComplexity , errorTranslations ) ;
134120 const defaultValue = fields
135121 . flatMap ( ( f ) => ( Array . isArray ( f ) ? f : [ f ] ) )
@@ -175,11 +161,14 @@ function DynamicFormInner<F extends Field>({
175161 event . preventDefault ( ) ;
176162
177163 let payload : FormData = formData ;
178- if ( recaptchaExecute ) {
164+ if ( recaptchaSiteKey && recaptchaRef . current ) {
179165 try {
180- const token = await recaptchaExecute ( 'submit' ) ;
181- payload = new FormData ( event . currentTarget ) ;
182- payload . set ( RECAPTCHA_TOKEN_FORM_KEY , token ) ;
166+ const token = await recaptchaRef . current . executeAsync ( ) ;
167+ if ( token ) {
168+ payload = new FormData ( event . currentTarget ) ;
169+ payload . set ( RECAPTCHA_TOKEN_FORM_KEY , token ) ;
170+ }
171+ recaptchaRef . current . reset ( ) ;
183172 } catch {
184173 // Proceed without token
185174 }
@@ -228,6 +217,7 @@ function DynamicFormInner<F extends Field>({
228217
229218 return < DynamicFormField field = { field } formField = { formField } key = { formField . id } /> ;
230219 } ) }
220+ { recaptchaSiteKey && < ReCAPTCHA ref = { recaptchaRef } sitekey = { recaptchaSiteKey } /> }
231221 < div className = "flex gap-1 pt-3" >
232222 { onCancel && (
233223 < Button
0 commit comments