Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@ import { graphql, VariablesOf } from '~/client/graphql';
import { FieldNameToFieldId } from '~/data-transformers/form-field-transformer/utils';
import { redirect } from '~/i18n/routing';
import { getCartId } from '~/lib/cart';
import { getRecaptchaSiteKey, RECAPTCHA_TOKEN_FORM_KEY } from '~/lib/recaptcha';

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

const RegisterCustomerMutation = graphql(`
mutation RegisterCustomerMutation($input: RegisterCustomerInput!) {
mutation RegisterCustomerMutation(
$input: RegisterCustomerInput!
$reCaptchaV2: ReCaptchaV2Input
) {
customer {
registerCustomer(input: $input) {
registerCustomer(input: $input, reCaptchaV2: $reCaptchaV2) {
customer {
firstName
lastName
Expand Down Expand Up @@ -356,12 +360,29 @@ export async function registerCustomer<F extends Field>(
};
}

const recaptchaSiteKey = await getRecaptchaSiteKey();

if (recaptchaSiteKey) {
const recaptchaToken = formData.get(RECAPTCHA_TOKEN_FORM_KEY);

if (typeof recaptchaToken !== 'string' || !recaptchaToken.trim()) {
return {
lastResult: submission.reply({ formErrors: [t('recaptchaRequired')] }),
};
}
}

try {
const input = parseRegisterCustomerInput(submission.value, fields);
const recaptchaToken = formData.get(RECAPTCHA_TOKEN_FORM_KEY);
const response = await client.fetch({
document: RegisterCustomerMutation,
variables: {
input,
reCaptchaV2:
typeof recaptchaToken === 'string' && recaptchaToken
? { token: recaptchaToken }
: undefined,
},
fetchOptions: { cache: 'no-store' },
});
Expand Down
4 changes: 4 additions & 0 deletions core/app/[locale]/(default)/(auth)/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
REGISTER_CUSTOMER_FORM_LAYOUT,
transformFieldsToLayout,
} from '~/data-transformers/form-field-transformer/utils';
import { getRecaptchaSiteKey } from '~/lib/recaptcha';
import { exists } from '~/lib/utils';

import { ADDRESS_FIELDS_NAME_PREFIX, CUSTOMER_FIELDS_NAME_PREFIX } from './_actions/prefixes';
Expand Down Expand Up @@ -63,6 +64,8 @@ export default async function Register({ params }: Props) {
const { addressFields, customerFields, countries, passwordComplexitySettings } =
registerCustomerData;

const recaptchaSiteKey = await getRecaptchaSiteKey();

const fields = transformFieldsToLayout(
[
...addressFields.map((field) => {
Expand Down Expand Up @@ -154,6 +157,7 @@ export default async function Register({ params }: Props) {
}}
fields={fields}
passwordComplexity={passwordComplexitySettings}
recaptchaSiteKey={recaptchaSiteKey}
submitLabel={t('cta')}
title={t('heading')}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ import { schema } from '@/vibes/soul/sections/reviews/schema';
import { getSessionCustomerAccessToken } from '~/auth';
import { client } from '~/client';
import { graphql } from '~/client/graphql';
import { getRecaptchaSiteKey, RECAPTCHA_TOKEN_FORM_KEY } from '~/lib/recaptcha';

const AddProductReviewMutation = graphql(`
mutation AddProductReviewMutation($input: AddProductReviewInput!) {
mutation AddProductReviewMutation(
$input: AddProductReviewInput!
$reCaptchaV2: ReCaptchaV2Input
) {
catalog {
addProductReview(input: $input) {
addProductReview(input: $input, reCaptchaV2: $reCaptchaV2) {
__typename
errors {
__typename
Expand All @@ -38,7 +42,22 @@ export async function submitReview(
return { ...prevState, lastResult: submission.reply() };
}

const recaptchaSiteKey = await getRecaptchaSiteKey();

if (recaptchaSiteKey) {
const token = payload.get(RECAPTCHA_TOKEN_FORM_KEY);
const tokenValue = typeof token === 'string' ? token.trim() : '';

if (!tokenValue) {
return {
...prevState,
lastResult: submission.reply({ formErrors: [t('recaptchaRequired')] }),
};
}
}

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

try {
const response = await client.fetch({
Expand All @@ -52,6 +71,10 @@ export async function submitReview(
},
productEntityId,
},
reCaptchaV2:
typeof recaptchaToken === 'string' && recaptchaToken
? { token: recaptchaToken }
: undefined,
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,15 @@ interface Props {
pageInfo?: { hasNextPage: boolean; endCursor: string | null };
}>;
streamableProduct: Streamable<Awaited<ReturnType<typeof getStreamableProduct>>>;
recaptchaSiteKey?: string;
}

export const Reviews = async ({
productId,
searchParams,
streamableProduct,
streamableImages,
recaptchaSiteKey,
}: Props) => {
const t = await getTranslations('Product.Reviews');

Expand Down Expand Up @@ -189,6 +191,7 @@ export const Reviews = async ({
paginationInfo={streamablePaginationInfo}
previousLabel={t('previous')}
productId={productId}
recaptchaSiteKey={recaptchaSiteKey}
reviews={streamableReviews}
reviewsLabel={t('title')}
streamableImages={streamableImages}
Expand Down
8 changes: 7 additions & 1 deletion core/app/[locale]/(default)/product/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { pricesTransformer } from '~/data-transformers/prices-transformer';
import { productCardTransformer } from '~/data-transformers/product-card-transformer';
import { productOptionsTransformer } from '~/data-transformers/product-options-transformer';
import { getPreferredCurrencyCode } from '~/lib/currency';
import { getRecaptchaSiteKey } from '~/lib/recaptcha';
import { getMetadataAlternates } from '~/lib/seo/canonical';

import { addToCart } from './_actions/add-to-cart';
Expand Down Expand Up @@ -76,7 +77,10 @@ export default async function Product({ params, searchParams }: Props) {

const productId = Number(slug);

const { product: baseProduct, settings } = await getProduct(productId, customerAccessToken);
const [{ product: baseProduct, settings }, recaptchaSiteKey] = await Promise.all([
getProduct(productId, customerAccessToken),
getRecaptchaSiteKey(),
]);

const reviewsEnabled = Boolean(settings?.reviews.enabled && !settings.display.showProductRating);
const showRating = Boolean(settings?.reviews.enabled && settings.display.showProductRating);
Expand Down Expand Up @@ -581,6 +585,7 @@ export default async function Product({ params, searchParams }: Props) {
backorderDisplayData: streamableBackorderDisplayData,
}}
quantityLabel={t('ProductDetails.quantity')}
recaptchaSiteKey={recaptchaSiteKey}
reviewFormAction={submitReview}
thumbnailLabel={t('ProductDetails.thumbnail')}
user={streamableUser}
Expand All @@ -602,6 +607,7 @@ export default async function Product({ params, searchParams }: Props) {
<div id="reviews">
<Reviews
productId={productId}
recaptchaSiteKey={recaptchaSiteKey}
searchParams={searchParams}
streamableImages={streamableImages}
streamableProduct={streamableProduct}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Field, schema } from '@/vibes/soul/form/dynamic-form/schema';
import { client } from '~/client';
import { graphql, VariablesOf } from '~/client/graphql';
import { redirect } from '~/i18n/routing';
import { getRecaptchaSiteKey, RECAPTCHA_TOKEN_FORM_KEY } from '~/lib/recaptcha';

const inputSchema = z.object({
data: z.object({
Expand All @@ -26,8 +27,8 @@ const inputSchema = z.object({
});

const SubmitContactUsMutation = graphql(`
mutation SubmitContactUsMutation($input: SubmitContactUsInput!) {
submitContactUs(input: $input) {
mutation SubmitContactUsMutation($input: SubmitContactUsInput!, $reCaptchaV2: ReCaptchaV2Input) {
submitContactUs(input: $input, reCaptchaV2: $reCaptchaV2) {
__typename
errors {
__typename
Expand Down Expand Up @@ -74,12 +75,29 @@ export async function submitContactForm<F extends Field>(
};
}

const recaptchaSiteKey = await getRecaptchaSiteKey();

if (recaptchaSiteKey) {
const recaptchaToken = formData.get(RECAPTCHA_TOKEN_FORM_KEY);

if (typeof recaptchaToken !== 'string' || !recaptchaToken.trim()) {
return {
lastResult: submission.reply({ formErrors: [t('recaptchaRequired')] }),
};
}
}

try {
const input = parseContactFormInput(submission.value);
const recaptchaToken = formData.get(RECAPTCHA_TOKEN_FORM_KEY);
const response = await client.fetch({
document: SubmitContactUsMutation,
variables: {
input,
reCaptchaV2:
typeof recaptchaToken === 'string' && recaptchaToken
? { token: recaptchaToken }
: undefined,
},
fetchOptions: { cache: 'no-store' },
});
Expand Down
4 changes: 4 additions & 0 deletions core/app/[locale]/(default)/webpages/[id]/contact/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
breadcrumbsTransformer,
truncateBreadcrumbs,
} from '~/data-transformers/breadcrumbs-transformer';
import { getRecaptchaSiteKey } from '~/lib/recaptcha';
import { getMetadataAlternates } from '~/lib/seo/canonical';

import { WebPage, WebPageContent } from '../_components/web-page';
Expand Down Expand Up @@ -195,6 +196,8 @@ export default async function ContactPage({ params, searchParams }: Props) {
);
}

const recaptchaSiteKey = await getRecaptchaSiteKey();

return (
<WebPageContent
breadcrumbs={Streamable.from(() => getWebPageBreadcrumbs(id))}
Expand All @@ -204,6 +207,7 @@ export default async function ContactPage({ params, searchParams }: Props) {
<DynamicForm
action={submitContactForm}
fields={await getContactFields(id)}
recaptchaSiteKey={recaptchaSiteKey}
submitLabel={t('cta')}
/>
</div>
Expand Down
55 changes: 55 additions & 0 deletions core/lib/recaptcha.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'server-only';

import { cache } from 'react';

import { client } from '~/client';
import { graphql } from '~/client/graphql';
import { revalidate } from '~/client/revalidate-target';

import type { ReCaptchaSettings } from './recaptcha/constants';

export { RECAPTCHA_TOKEN_FORM_KEY } from './recaptcha/constants';
export type { ReCaptchaSettings } from './recaptcha/constants';

export const ReCaptchaSettingsQuery = graphql(`
query ReCaptchaSettingsQuery {
site {
settings {
reCaptcha {
failedLoginLockoutDurationSeconds
isEnabledOnCheckout
isEnabledOnStorefront
siteKey
}
}
}
}
`);

export const getReCaptchaSettings = cache(async (): Promise<ReCaptchaSettings | null> => {
const { data } = await client.fetch({
document: ReCaptchaSettingsQuery,
fetchOptions: { next: { revalidate } },
});

const reCaptcha = data.site.settings?.reCaptcha;

if (!reCaptcha?.siteKey) {
return null;
}

return {
failedLoginLockoutDurationSeconds: reCaptcha.failedLoginLockoutDurationSeconds ?? null,
isEnabledOnCheckout: reCaptcha.isEnabledOnCheckout,
isEnabledOnStorefront: reCaptcha.isEnabledOnStorefront,
siteKey: reCaptcha.siteKey,
};
});

export const getRecaptchaSiteKey = cache(async (): Promise<string | undefined> => {
const settings = await getReCaptchaSettings();

return settings?.isEnabledOnStorefront === true && settings.siteKey
? settings.siteKey
: undefined;
});
8 changes: 8 additions & 0 deletions core/lib/recaptcha/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface ReCaptchaSettings {
failedLoginLockoutDurationSeconds: number | null;
isEnabledOnCheckout: boolean;
isEnabledOnStorefront: boolean;
siteKey: string;
}

export const RECAPTCHA_TOKEN_FORM_KEY = 'recaptchaToken';
6 changes: 5 additions & 1 deletion core/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"heading": "New account",
"cta": "Create account",
"somethingWentWrong": "Something went wrong. Please try again later.",
"recaptchaRequired": "Please complete the reCAPTCHA verification.",
"FieldErrors": {
"firstNameRequired": "First name is required",
"lastNameRequired": "Last name is required",
Expand Down Expand Up @@ -505,6 +506,7 @@
"emailLabel": "Email",
"successMessage": "Your review has been submitted successfully!",
"somethingWentWrong": "Something went wrong. Please try again later.",
"recaptchaRequired": "Please complete the reCAPTCHA verification.",
"FieldErrors": {
"titleRequired": "Title is required",
"authorRequired": "Name is required",
Expand Down Expand Up @@ -535,7 +537,8 @@
"email": "Email",
"comments": "Comments/questions",
"cta": "Submit form",
"somethingWentWrong": "Something went wrong. Please try again later."
"somethingWentWrong": "Something went wrong. Please try again later.",
"recaptchaRequired": "Please complete the reCAPTCHA verification."
}
}
},
Expand Down Expand Up @@ -705,6 +708,7 @@
},
"Form": {
"optional": "optional",
"recaptchaRequired": "Please complete the reCAPTCHA verification.",
"Errors": {
"invalidInput": "Please check your input and try again",
"invalidFormat": "The value entered does not match the required format"
Expand Down
2 changes: 2 additions & 0 deletions core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"react": "19.1.5",
"react-day-picker": "^9.7.0",
"react-dom": "19.1.5",
"react-google-recaptcha": "^3.1.0",
"react-headroom": "^3.2.1",
"schema-dts": "^1.1.5",
"server-only": "^0.0.1",
Expand All @@ -88,6 +89,7 @@
"@types/node": "^22.15.30",
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.6",
"@types/react-google-recaptcha": "^2.1.9",
"@types/react-headroom": "^3.2.3",
"@types/uuid": "^10.0.0",
"autoprefixer": "^10.4.21",
Expand Down
Loading
Loading