Skip to content

Commit 2d9270d

Browse files
committed
added recaptcha v3
1 parent a161583 commit 2d9270d

File tree

14 files changed

+235
-16
lines changed

14 files changed

+235
-16
lines changed

core/app/[locale]/(default)/(auth)/register/_actions/register-customer.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ import { graphql, VariablesOf } from '~/client/graphql';
1414
import { FieldNameToFieldId } from '~/data-transformers/form-field-transformer/utils';
1515
import { redirect } from '~/i18n/routing';
1616
import { getCartId } from '~/lib/cart';
17+
import { RECAPTCHA_TOKEN_FORM_KEY } from '~/lib/recaptcha';
1718

1819
import { ADDRESS_FIELDS_NAME_PREFIX, CUSTOMER_FIELDS_NAME_PREFIX } from './prefixes';
1920

2021
const RegisterCustomerMutation = graphql(`
21-
mutation RegisterCustomerMutation($input: RegisterCustomerInput!) {
22+
mutation RegisterCustomerMutation($input: RegisterCustomerInput!, $reCaptchaV2: ReCaptchaV2Input) {
2223
customer {
23-
registerCustomer(input: $input) {
24+
registerCustomer(input: $input, reCaptchaV2: $reCaptchaV2) {
2425
customer {
2526
firstName
2627
lastName
@@ -358,10 +359,15 @@ export async function registerCustomer<F extends Field>(
358359

359360
try {
360361
const input = parseRegisterCustomerInput(submission.value, fields);
362+
const recaptchaToken = formData.get(RECAPTCHA_TOKEN_FORM_KEY);
361363
const response = await client.fetch({
362364
document: RegisterCustomerMutation,
363365
variables: {
364366
input,
367+
reCaptchaV2:
368+
typeof recaptchaToken === 'string' && recaptchaToken
369+
? { token: recaptchaToken }
370+
: undefined,
365371
},
366372
fetchOptions: { cache: 'no-store' },
367373
});

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

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

67+
const reCaptchaSettings = await getReCaptchaSettings();
68+
const recaptchaEnabled =
69+
(reCaptchaSettings?.isEnabledOnStorefront === true && Boolean(reCaptchaSettings?.siteKey)) ??
70+
false;
71+
6672
const fields = transformFieldsToLayout(
6773
[
6874
...addressFields.map((field) => {
@@ -109,6 +115,7 @@ export default async function Register({ params }: Props) {
109115
return (
110116
<DynamicFormSection
111117
action={registerCustomer}
118+
recaptchaEnabled={recaptchaEnabled}
112119
errorTranslations={{
113120
firstName: {
114121
invalid_type: t('FieldErrors.firstNameRequired'),

core/app/[locale]/(default)/layout.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { PropsWithChildren } from 'react';
33

44
import { Footer } from '~/components/footer';
55
import { Header } from '~/components/header';
6+
import { ReCaptchaProvider } from '~/components/recaptcha-provider';
7+
import { getReCaptchaSettings } from '~/lib/recaptcha';
68

79
interface Props extends PropsWithChildren {
810
params: Promise<{ locale: string }>;
@@ -13,13 +15,15 @@ export default async function DefaultLayout({ params, children }: Props) {
1315

1416
setRequestLocale(locale);
1517

18+
const reCaptchaSettings = await getReCaptchaSettings();
19+
1620
return (
17-
<>
21+
<ReCaptchaProvider settings={reCaptchaSettings}>
1822
<Header />
1923

2024
<main>{children}</main>
2125

2226
<Footer />
23-
</>
27+
</ReCaptchaProvider>
2428
);
2529
}

core/app/[locale]/(default)/product/[slug]/_actions/submit-review.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import { schema } from '@/vibes/soul/sections/reviews/schema';
99
import { getSessionCustomerAccessToken } from '~/auth';
1010
import { client } from '~/client';
1111
import { graphql } from '~/client/graphql';
12+
import { RECAPTCHA_TOKEN_FORM_KEY } from '~/lib/recaptcha';
1213

1314
const AddProductReviewMutation = graphql(`
14-
mutation AddProductReviewMutation($input: AddProductReviewInput!) {
15+
mutation AddProductReviewMutation($input: AddProductReviewInput!, $reCaptchaV2: ReCaptchaV2Input) {
1516
catalog {
16-
addProductReview(input: $input) {
17+
addProductReview(input: $input, reCaptchaV2: $reCaptchaV2) {
1718
__typename
1819
errors {
1920
__typename
@@ -39,6 +40,7 @@ export async function submitReview(
3940
}
4041

4142
const { productEntityId, ...input } = submission.value;
43+
const recaptchaToken = payload.get(RECAPTCHA_TOKEN_FORM_KEY);
4244

4345
try {
4446
const response = await client.fetch({
@@ -52,6 +54,10 @@ export async function submitReview(
5254
},
5355
productEntityId,
5456
},
57+
reCaptchaV2:
58+
typeof recaptchaToken === 'string' && recaptchaToken
59+
? { token: recaptchaToken }
60+
: undefined,
5561
},
5662
});
5763

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ 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';
1415

1516
import { submitReview } from '../_actions/submit-review';
1617
import { getStreamableProduct } from '../page-data';
@@ -171,6 +172,11 @@ export const Reviews = async ({
171172
return product?.reviewSummary.numberOfReviews ?? 0;
172173
});
173174

175+
const reCaptchaSettings = await getReCaptchaSettings();
176+
const recaptchaEnabled =
177+
(reCaptchaSettings?.isEnabledOnStorefront === true && Boolean(reCaptchaSettings?.siteKey)) ??
178+
false;
179+
174180
return (
175181
<>
176182
<ReviewsSection
@@ -189,6 +195,7 @@ export const Reviews = async ({
189195
paginationInfo={streamablePaginationInfo}
190196
previousLabel={t('previous')}
191197
productId={productId}
198+
recaptchaEnabled={recaptchaEnabled}
192199
reviews={streamableReviews}
193200
reviewsLabel={t('title')}
194201
streamableImages={streamableImages}

core/app/[locale]/(default)/webpages/[id]/contact/_actions/submit-contact-form.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Field, schema } from '@/vibes/soul/form/dynamic-form/schema';
1111
import { client } from '~/client';
1212
import { graphql, VariablesOf } from '~/client/graphql';
1313
import { redirect } from '~/i18n/routing';
14+
import { RECAPTCHA_TOKEN_FORM_KEY } from '~/lib/recaptcha';
1415

1516
const inputSchema = z.object({
1617
data: z.object({
@@ -26,8 +27,8 @@ const inputSchema = z.object({
2627
});
2728

2829
const SubmitContactUsMutation = graphql(`
29-
mutation SubmitContactUsMutation($input: SubmitContactUsInput!) {
30-
submitContactUs(input: $input) {
30+
mutation SubmitContactUsMutation($input: SubmitContactUsInput!, $reCaptchaV2: ReCaptchaV2Input) {
31+
submitContactUs(input: $input, reCaptchaV2: $reCaptchaV2) {
3132
__typename
3233
errors {
3334
__typename
@@ -76,10 +77,15 @@ export async function submitContactForm<F extends Field>(
7677

7778
try {
7879
const input = parseContactFormInput(submission.value);
80+
const recaptchaToken = formData.get(RECAPTCHA_TOKEN_FORM_KEY);
7981
const response = await client.fetch({
8082
document: SubmitContactUsMutation,
8183
variables: {
8284
input,
85+
reCaptchaV2:
86+
typeof recaptchaToken === 'string' && recaptchaToken
87+
? { token: recaptchaToken }
88+
: undefined,
8389
},
8490
fetchOptions: { cache: 'no-store' },
8591
});

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

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

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

197+
const reCaptchaSettings = await getReCaptchaSettings();
198+
const recaptchaEnabled =
199+
(reCaptchaSettings?.isEnabledOnStorefront === true && Boolean(reCaptchaSettings?.siteKey)) ??
200+
false;
201+
196202
return (
197203
<WebPageContent
198204
breadcrumbs={Streamable.from(() => getWebPageBreadcrumbs(id))}
@@ -202,6 +208,7 @@ export default async function ContactPage({ params, searchParams }: Props) {
202208
<DynamicForm
203209
action={submitContactForm}
204210
fields={await getContactFields(id)}
211+
recaptchaEnabled={recaptchaEnabled}
205212
submitLabel={t('cta')}
206213
/>
207214
</div>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
'use client';
2+
3+
import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
4+
import { createContext, useContext, type PropsWithChildren } from 'react';
5+
6+
import type { ReCaptchaSettings } from '~/lib/recaptcha';
7+
8+
const ReCaptchaContext = createContext<ReCaptchaSettings | null>(null);
9+
10+
export function useReCaptchaSettings(): ReCaptchaSettings | null {
11+
return useContext(ReCaptchaContext);
12+
}
13+
14+
/** Whether reCAPTCHA is enabled on the storefront (signup, contact, reviews). */
15+
export function useReCaptchaEnabledOnStorefront(): boolean {
16+
const settings = useReCaptchaSettings();
17+
return settings?.isEnabledOnStorefront === true && Boolean(settings?.siteKey);
18+
}
19+
20+
interface ReCaptchaProviderProps extends PropsWithChildren {
21+
settings: ReCaptchaSettings | null;
22+
}
23+
24+
/**
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.
27+
*/
28+
export function ReCaptchaProvider({ settings, children }: ReCaptchaProviderProps) {
29+
const hasProvider = Boolean(settings?.siteKey);
30+
31+
return (
32+
<ReCaptchaContext.Provider value={settings}>
33+
{hasProvider ? (
34+
<GoogleReCaptchaProvider reCaptchaKey={settings.siteKey}>
35+
{children}
36+
</GoogleReCaptchaProvider>
37+
) : (
38+
children
39+
)}
40+
</ReCaptchaContext.Provider>
41+
);
42+
}

core/lib/recaptcha.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { cache } from 'react';
2+
3+
import { client } from '~/client';
4+
import { graphql } from '~/client/graphql';
5+
import { revalidate } from '~/client/revalidate-target';
6+
7+
export const ReCaptchaSettingsQuery = graphql(`
8+
query ReCaptchaSettingsQuery {
9+
site {
10+
settings {
11+
reCaptcha {
12+
failedLoginLockoutDurationSeconds
13+
isEnabledOnCheckout
14+
isEnabledOnStorefront
15+
siteKey
16+
}
17+
}
18+
}
19+
}
20+
`);
21+
22+
export type ReCaptchaSettings = {
23+
failedLoginLockoutDurationSeconds: number | null;
24+
isEnabledOnCheckout: boolean;
25+
isEnabledOnStorefront: boolean;
26+
siteKey: string;
27+
};
28+
29+
export const getReCaptchaSettings = cache(async (): Promise<ReCaptchaSettings | null> => {
30+
const { data } = await client.fetch({
31+
document: ReCaptchaSettingsQuery,
32+
fetchOptions: { next: { revalidate } },
33+
});
34+
35+
const reCaptcha = data.site.settings?.reCaptcha;
36+
37+
if (!reCaptcha?.siteKey) {
38+
return null;
39+
}
40+
41+
return {
42+
failedLoginLockoutDurationSeconds: reCaptcha.failedLoginLockoutDurationSeconds ?? null,
43+
isEnabledOnCheckout: reCaptcha.isEnabledOnCheckout,
44+
isEnabledOnStorefront: reCaptcha.isEnabledOnStorefront,
45+
siteKey: reCaptcha.siteKey,
46+
};
47+
});
48+
49+
/** FormData key used to pass the reCAPTCHA token from client to server actions */
50+
export const RECAPTCHA_TOKEN_FORM_KEY = 'recaptchaToken';

core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +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",
6869
"react-headroom": "^3.2.1",
6970
"schema-dts": "^1.1.5",
7071
"server-only": "^0.0.1",

0 commit comments

Comments
 (0)