Skip to content

feat(core): enable recaptcha in forms#2896

Merged
jamesqquick merged 27 commits intocanaryfrom
recaptch
Mar 13, 2026
Merged

feat(core): enable recaptcha in forms#2896
jamesqquick merged 27 commits intocanaryfrom
recaptch

Conversation

@jamesqquick
Copy link
Contributor

@jamesqquick jamesqquick commented Feb 20, 2026

Jira CATALYST-1806

What/Why?

Match functionality of Stencil with regards to reCAPTCHA. Catalyst should respect the reCAPTCHA settings from the Control Panel and apply reCAPTCHA to forms appropriately based on those settings.

CleanShot 2026-03-02 at 12 20 01@2x

Forms that should support reCAPTCHA if enabled include:

  • product reviews
  • user registration
  • contact

Testing

with reCAPTCHA enabled in the Control Panel

Register & contact

  • Submit without completing the reCAPTCHA checkbox → submission is blocked and “Please complete the reCAPTCHA verification.” is shown.
  • Complete reCAPTCHA and submit with valid data → form submits and succeeds as expected.

Product review

  • Open “Write a review” and submit without completing reCAPTCHA → same error, no submit.
  • Complete reCAPTCHA and submit → review submits and success toast appears.

without reCAPTCHA enabled in the Control Panel

When reCAPTCHA is disabled in the control panel, all three forms submit without a reCAPTCHA widget or “complete reCAPTCHA” errors.

Migration

@changeset-bot
Copy link

changeset-bot bot commented Feb 20, 2026

🦋 Changeset detected

Latest commit: 901b3c8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@bigcommerce/catalyst-core Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link

vercel bot commented Feb 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
catalyst Ready Ready Preview, Comment Mar 13, 2026 8:20pm

Request Review

Copy link
Contributor

@matthewvolk matthewvolk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High level, I think this is pretty good - the main "architectural" feedback I have is that it looks like you introduce two ways to get the recaptcha key but are only using one of them... wondering if that's redundant. I could be misunderstanding intent here, so I definitely encourage a second reviewer to weigh in!

Outside of this, my only other concern is the library is a bit old... not sure if it's being maintained, especially looking at some of the recent issues: dozoisch/react-google-recaptcha#301

@github-actions
Copy link
Contributor

github-actions bot commented Mar 3, 2026

Bundle Size Report

Comparing against baseline from 00496f5 (2026-03-13).

Metric Baseline Current Delta
Total JS 422.9 kB 429.7 kB +6.8 kB (+1.6%)

Per-Route First Load JS

Route Baseline Current Delta
/(default)/(auth)/change-password/page 300.8 kB 301.2 kB +0.4 kB (+0.1%)
/(default)/(auth)/login/forgot-password/page 299.9 kB 300.4 kB +0.5 kB (+0.2%)
/(default)/(auth)/login/page 300.4 kB 300.8 kB +0.4 kB (+0.1%)
/(default)/(auth)/register/page 333.3 kB 336.9 kB +3.6 kB (+1.1%)
/(default)/(faceted)/brand/[slug]/page 312.4 kB 312.9 kB +0.5 kB (+0.2%)
/(default)/(faceted)/category/[slug]/page 320.8 kB 321.2 kB +0.4 kB (+0.1%)
/(default)/(faceted)/search/page 312.4 kB 312.9 kB +0.5 kB (+0.2%)
/(default)/[...rest]/page 295.4 kB 295.9 kB +0.5 kB (+0.2%)
/(default)/account/addresses/page 336.8 kB 340.4 kB +3.6 kB (+1.1%)
/(default)/account/orders/[id]/page 303.6 kB 304 kB +0.4 kB (+0.1%)
/(default)/account/orders/page 304.5 kB 305 kB +0.5 kB (+0.2%)
/(default)/account/settings/page 311.2 kB 311.6 kB +0.4 kB (+0.1%)
/(default)/account/wishlists/[id]/page 318.6 kB 319 kB +0.4 kB (+0.1%)
/(default)/account/wishlists/page 313.6 kB 314 kB +0.4 kB (+0.1%)
/(default)/blog/[blogId]/page 295.4 kB 295.9 kB +0.5 kB (+0.2%)
/(default)/blog/page 296.4 kB 296.9 kB +0.5 kB (+0.2%)
/(default)/cart/page 316.3 kB 316.8 kB +0.5 kB (+0.2%)
/(default)/compare/page 307.6 kB 308.1 kB +0.5 kB (+0.2%)
/(default)/gift-certificates/balance/page 299.4 kB 299.8 kB +0.4 kB (+0.1%)
/(default)/gift-certificates/page 295.4 kB 295.9 kB +0.5 kB (+0.2%)
/(default)/gift-certificates/purchase/page 335.8 kB 339.4 kB +3.6 kB (+1.1%)
/(default)/page 312.6 kB 313.1 kB +0.5 kB (+0.2%)
/(default)/product/[slug]/page 364.3 kB 367.9 kB +3.6 kB (+1%)
/(default)/webpages/[id]/contact/page 334.2 kB 337.8 kB +3.6 kB (+1.1%)
/(default)/webpages/[id]/normal/page 303.6 kB 304 kB +0.4 kB (+0.1%)
/(default)/wishlist/[token]/page 308.5 kB 308.9 kB +0.4 kB (+0.1%)

Threshold: 5% increase. Routes with ⚠️ exceed the threshold.

const token = recaptchaRef.current.getValue();

if (!token || typeof token !== 'string') {
setRecaptchaError(t('recaptchaRequired'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there no way to validate for recaptcha within the form action? And return the errors accordingly? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Refactored to move that logic to the server and create utility functions to parse and validate the token.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even with the refactor this is still present. I think we can remove all client facing logic for showing the errors and validating.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Removed all client logic in lieu of leveraging logic on the server

Copy link
Contributor

@jorgemoya jorgemoya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for looking into validating and erroring from the actions, but I still see some leftover logic from the previous implementation. Should be easy to clean up, I believe.

Another question, should we include reCaptcha in other pages? Login, reset password? I forget if once we enable reCaptcha on a storefront, forms automatically require a token to succeed?

Another detail missing is how to make sure the forms still succeed in our e2e tests when reCaptcha is enabled. Our old approach was to use the X-Vercel-Set-Bypass-Cookie: true, however another approach is to make sure the E2E test store has Google's test key 🤔? This would work now since our E2E test store is separate from our demo sites.

Comment on lines +19 to +20
failedLoginLockoutDurationSeconds
isEnabledOnCheckout
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think these two are being used. Can we use it for something here? I would remove if not needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch. Removed.

return { siteKey, token };
}

export function validateRecaptchaToken(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe change to assertRecaptchaTokenPresent since it's not really validating anything (this happens server side)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated!

Comment on lines +165 to +181
setRecaptchaError(null);

let payload: FormData = formData;

if (recaptchaSiteKey && recaptchaRef.current) {
const token = recaptchaRef.current.getValue();

if (!token || typeof token !== 'string') {
setRecaptchaError(t('recaptchaRequired'));

return;
}

payload = new FormData(event.currentTarget);
payload.set(RECAPTCHA_TOKEN_FORM_KEY, token);
recaptchaRef.current.reset();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cool thing about validating on the server is that we no longer need this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed. Great call.

{submitLabel}
</SubmitButton>
</div>
{recaptchaError ? <FormStatus type="error">{recaptchaError}</FormStatus> : null}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer needed too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

Comment on lines +88 to +90
export function DynamicForm<F extends Field>(props: DynamicFormProps<F>) {
return <DynamicFormInner {...props} />;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we add this Inner component? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

const token = recaptchaRef.current.getValue();

if (!token || typeof token !== 'string') {
setRecaptchaError(t('recaptchaRequired'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even with the refactor this is still present. I think we can remove all client facing logic for showing the errors and validating.

…ut from getReCaptchaSettings

Made-with: Cursor
@jamesqquick
Copy link
Contributor Author

Thanks for looking into validating and erroring from the actions, but I still see some leftover logic from the previous implementation. Should be easy to clean up, I believe.

Another question, should we include reCaptcha in other pages? Login, reset password? I forget if once we enable reCaptcha on a storefront, forms automatically require a token to succeed?

Another detail missing is how to make sure the forms still succeed in our e2e tests when reCaptcha is enabled. Our old approach was to use the X-Vercel-Set-Bypass-Cookie: true, however another approach is to make sure the E2E test store has Google's test key 🤔? This would work now since our E2E test store is separate from our demo sites.

I'm not sure what we want to do for forms on other pages. I started with mirroring the Stencil functionality which is specific to these forms and not all of them.

@jamesqquick jamesqquick changed the title Recaptcha feat(core): enable recaptcha in forms Mar 13, 2026
@jamesqquick jamesqquick added this pull request to the merge queue Mar 13, 2026
Merged via the queue into canary with commit fc84210 Mar 13, 2026
17 of 18 checks passed
@jamesqquick jamesqquick deleted the recaptch branch March 13, 2026 22:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants