Skip to content

Commit 8182cc4

Browse files
committed
update recaptcha package and make visible on forms
1 parent 2d9270d commit 8182cc4

File tree

12 files changed

+96
-166
lines changed

12 files changed

+96
-166
lines changed

core/app/[locale]/(default)/(auth)/register/page.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
formFieldTransformer,
99
injectCountryCodeOptions,
1010
} from '~/data-transformers/form-field-transformer';
11-
import { getReCaptchaSettings } from '~/lib/recaptcha';
1211
import {
1312
CUSTOMER_FIELDS_TO_EXCLUDE,
1413
REGISTER_CUSTOMER_FORM_LAYOUT,
@@ -64,11 +63,6 @@ export default async function Register({ params }: Props) {
6463
const { addressFields, customerFields, countries, passwordComplexitySettings } =
6564
registerCustomerData;
6665

67-
const reCaptchaSettings = await getReCaptchaSettings();
68-
const recaptchaEnabled =
69-
(reCaptchaSettings?.isEnabledOnStorefront === true && Boolean(reCaptchaSettings?.siteKey)) ??
70-
false;
71-
7266
const fields = transformFieldsToLayout(
7367
[
7468
...addressFields.map((field) => {
@@ -115,7 +109,6 @@ export default async function Register({ params }: Props) {
115109
return (
116110
<DynamicFormSection
117111
action={registerCustomer}
118-
recaptchaEnabled={recaptchaEnabled}
119112
errorTranslations={{
120113
firstName: {
121114
invalid_type: t('FieldErrors.firstNameRequired'),

core/app/[locale]/(default)/product/[slug]/_components/reviews.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { PaginationFragment } from '~/client/fragments/pagination';
1111
import { graphql } from '~/client/graphql';
1212
import { revalidate } from '~/client/revalidate-target';
1313
import { defaultPageInfo, pageInfoTransformer } from '~/data-transformers/page-info-transformer';
14-
import { getReCaptchaSettings } from '~/lib/recaptcha';
1514

1615
import { submitReview } from '../_actions/submit-review';
1716
import { getStreamableProduct } from '../page-data';
@@ -172,11 +171,6 @@ export const Reviews = async ({
172171
return product?.reviewSummary.numberOfReviews ?? 0;
173172
});
174173

175-
const reCaptchaSettings = await getReCaptchaSettings();
176-
const recaptchaEnabled =
177-
(reCaptchaSettings?.isEnabledOnStorefront === true && Boolean(reCaptchaSettings?.siteKey)) ??
178-
false;
179-
180174
return (
181175
<>
182176
<ReviewsSection
@@ -195,7 +189,6 @@ export const Reviews = async ({
195189
paginationInfo={streamablePaginationInfo}
196190
previousLabel={t('previous')}
197191
productId={productId}
198-
recaptchaEnabled={recaptchaEnabled}
199192
reviews={streamableReviews}
200193
reviewsLabel={t('title')}
201194
streamableImages={streamableImages}

core/app/[locale]/(default)/webpages/[id]/contact/page.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
breadcrumbsTransformer,
1313
truncateBreadcrumbs,
1414
} from '~/data-transformers/breadcrumbs-transformer';
15-
import { getReCaptchaSettings } from '~/lib/recaptcha';
1615
import { getMetadataAlternates } from '~/lib/seo/canonical';
1716

1817
import { WebPage, WebPageContent } from '../_components/web-page';
@@ -194,11 +193,6 @@ export default async function ContactPage({ params, searchParams }: Props) {
194193
);
195194
}
196195

197-
const reCaptchaSettings = await getReCaptchaSettings();
198-
const recaptchaEnabled =
199-
(reCaptchaSettings?.isEnabledOnStorefront === true && Boolean(reCaptchaSettings?.siteKey)) ??
200-
false;
201-
202196
return (
203197
<WebPageContent
204198
breadcrumbs={Streamable.from(() => getWebPageBreadcrumbs(id))}
@@ -208,7 +202,6 @@ export default async function ContactPage({ params, searchParams }: Props) {
208202
<DynamicForm
209203
action={submitContactForm}
210204
fields={await getContactFields(id)}
211-
recaptchaEnabled={recaptchaEnabled}
212205
submitLabel={t('cta')}
213206
/>
214207
</div>
Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,33 @@
11
'use client';
22

3-
import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
43
import { createContext, useContext, type PropsWithChildren } from 'react';
54

6-
import type { ReCaptchaSettings } from '~/lib/recaptcha';
5+
import type { ReCaptchaSettings } from '~/lib/recaptcha/constants';
76

87
const ReCaptchaContext = createContext<ReCaptchaSettings | null>(null);
98

109
export function useReCaptchaSettings(): ReCaptchaSettings | null {
1110
return useContext(ReCaptchaContext);
1211
}
1312

14-
/** Whether reCAPTCHA is enabled on the storefront (signup, contact, reviews). */
15-
export function useReCaptchaEnabledOnStorefront(): boolean {
13+
/** Site key when reCAPTCHA is enabled on the storefront; undefined otherwise. */
14+
export function useReCaptchaSiteKey(): string | undefined {
1615
const settings = useReCaptchaSettings();
17-
return settings?.isEnabledOnStorefront === true && Boolean(settings?.siteKey);
16+
return settings?.isEnabledOnStorefront === true && settings?.siteKey
17+
? settings.siteKey
18+
: undefined;
1819
}
1920

2021
interface ReCaptchaProviderProps extends PropsWithChildren {
2122
settings: ReCaptchaSettings | null;
2223
}
2324

2425
/**
25-
* Wraps children with reCAPTCHA v3 provider when a site key is configured,
26-
* so useGoogleReCaptcha() is available. Token is only used when isEnabledOnStorefront is true.
26+
* Provides reCAPTCHA settings (e.g. siteKey) via context.
27+
* Forms use react-google-recaptcha (v2) and receive siteKey via props from server.
2728
*/
2829
export function ReCaptchaProvider({ settings, children }: ReCaptchaProviderProps) {
29-
const hasProvider = Boolean(settings?.siteKey);
30-
3130
return (
32-
<ReCaptchaContext.Provider value={settings}>
33-
{hasProvider ? (
34-
<GoogleReCaptchaProvider reCaptchaKey={settings.siteKey}>
35-
{children}
36-
</GoogleReCaptchaProvider>
37-
) : (
38-
children
39-
)}
40-
</ReCaptchaContext.Provider>
31+
<ReCaptchaContext.Provider value={settings}>{children}</ReCaptchaContext.Provider>
4132
);
4233
}

core/lib/recaptcha.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
import 'server-only';
2+
13
import { cache } from 'react';
24

35
import { client } from '~/client';
46
import { graphql } from '~/client/graphql';
57
import { revalidate } from '~/client/revalidate-target';
8+
import type { ReCaptchaSettings } from './recaptcha/constants';
9+
10+
export { RECAPTCHA_TOKEN_FORM_KEY } from './recaptcha/constants';
11+
export type { ReCaptchaSettings } from './recaptcha/constants';
612

713
export const ReCaptchaSettingsQuery = graphql(`
814
query ReCaptchaSettingsQuery {
@@ -19,13 +25,6 @@ export const ReCaptchaSettingsQuery = graphql(`
1925
}
2026
`);
2127

22-
export type ReCaptchaSettings = {
23-
failedLoginLockoutDurationSeconds: number | null;
24-
isEnabledOnCheckout: boolean;
25-
isEnabledOnStorefront: boolean;
26-
siteKey: string;
27-
};
28-
2928
export const getReCaptchaSettings = cache(async (): Promise<ReCaptchaSettings | null> => {
3029
const { data } = await client.fetch({
3130
document: ReCaptchaSettingsQuery,
@@ -45,6 +44,3 @@ export const getReCaptchaSettings = cache(async (): Promise<ReCaptchaSettings |
4544
siteKey: reCaptcha.siteKey,
4645
};
4746
});
48-
49-
/** FormData key used to pass the reCAPTCHA token from client to server actions */
50-
export const RECAPTCHA_TOKEN_FORM_KEY = 'recaptchaToken';

core/lib/recaptcha/constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export type ReCaptchaSettings = {
2+
failedLoginLockoutDurationSeconds: number | null;
3+
isEnabledOnCheckout: boolean;
4+
isEnabledOnStorefront: boolean;
5+
siteKey: string;
6+
};
7+
8+
/** FormData key used to pass the reCAPTCHA token from client to server actions */
9+
export const RECAPTCHA_TOKEN_FORM_KEY = 'recaptchaToken';

core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
"react": "19.1.5",
6666
"react-day-picker": "^9.7.0",
6767
"react-dom": "19.1.5",
68-
"react-google-recaptcha-v3": "^1.10.1",
68+
"react-google-recaptcha": "^3.1.0",
6969
"react-headroom": "^3.2.1",
7070
"schema-dts": "^1.1.5",
7171
"server-only": "^0.0.1",

core/vibes/soul/form/dynamic-form/index.tsx

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ import {
2020
useActionState,
2121
useEffect,
2222
} from 'react';
23+
import { useRef } from 'react';
2324
import { useFormStatus } from 'react-dom';
24-
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
25+
import ReCAPTCHA from 'react-google-recaptcha';
2526
import { 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

2931
import { ButtonRadioGroup } from '@/vibes/soul/form/button-radio-group';
3032
import { 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

8787
export 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-
10791
function 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

core/vibes/soul/sections/dynamic-form-section/index.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ interface Props<F extends Field> {
1818
className?: string;
1919
passwordComplexity?: PasswordComplexitySettings | null;
2020
errorTranslations?: FormErrorTranslationMap;
21-
recaptchaEnabled?: boolean;
2221
}
2322

2423
export function DynamicFormSection<F extends Field>({
@@ -30,7 +29,6 @@ export function DynamicFormSection<F extends Field>({
3029
action,
3130
passwordComplexity,
3231
errorTranslations,
33-
recaptchaEnabled = false,
3432
}: Props<F>) {
3533
return (
3634
<SectionLayout className={clsx('mx-auto w-full max-w-4xl', className)} containerSize="lg">
@@ -49,7 +47,6 @@ export function DynamicFormSection<F extends Field>({
4947
errorTranslations={errorTranslations}
5048
fields={fields}
5149
passwordComplexity={passwordComplexity}
52-
recaptchaEnabled={recaptchaEnabled}
5350
submitLabel={submitLabel}
5451
/>
5552
</SectionLayout>

core/vibes/soul/sections/reviews/index.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ interface Props {
4040
}>;
4141
streamableProduct: Streamable<{ name: string }>;
4242
streamableUser: Streamable<{ email: string; name: string }>;
43-
recaptchaEnabled?: boolean;
4443
}
4544

4645
export function Reviews({
@@ -66,7 +65,6 @@ export function Reviews({
6665
streamableProduct,
6766
streamableImages,
6867
streamableUser,
69-
recaptchaEnabled = false,
7068
}: Readonly<Props>) {
7169
return (
7270
<Stream fallback={<ReviewsSkeleton reviewsLabel={reviewsLabel} />} value={streamableReviews}>
@@ -86,7 +84,6 @@ export function Reviews({
8684
formTitleLabel={formTitleLabel}
8785
message={emptyStateMessage}
8886
productId={productId}
89-
recaptchaEnabled={recaptchaEnabled}
9087
reviewsLabel={reviewsLabel}
9188
streamableImages={streamableImages}
9289
streamableProduct={streamableProduct}
@@ -140,7 +137,6 @@ export function Reviews({
140137
formRatingLabel={formRatingLabel}
141138
formReviewLabel={formReviewLabel}
142139
formSubmitLabel={formSubmitLabel}
143-
recaptchaEnabled={recaptchaEnabled}
144140
formTitleLabel={formTitleLabel}
145141
productId={productId}
146142
streamableImages={streamableImages}
@@ -205,7 +201,6 @@ export function ReviewsEmptyState({
205201
streamableProduct,
206202
streamableImages,
207203
streamableUser,
208-
recaptchaEnabled = false,
209204
}: {
210205
message?: string;
211206
reviewsLabel?: string;
@@ -226,7 +221,6 @@ export function ReviewsEmptyState({
226221
}>;
227222
streamableProduct: Streamable<{ name: string }>;
228223
streamableUser: Streamable<{ email: string; name: string }>;
229-
recaptchaEnabled?: boolean;
230224
}) {
231225
return (
232226
<StickySidebarLayout
@@ -256,7 +250,6 @@ export function ReviewsEmptyState({
256250
formSubmitLabel={formSubmitLabel}
257251
formTitleLabel={formTitleLabel}
258252
productId={productId}
259-
recaptchaEnabled={recaptchaEnabled}
260253
streamableImages={streamableImages}
261254
streamableProduct={streamableProduct}
262255
streamableUser={streamableUser}

0 commit comments

Comments
 (0)