Skip to content

feat(recaptcha): support reCAPTCHA v3 in adaptive forms (+ XFA no-iframe embed fixes) - #1986

Merged
kartikey19427 merged 16 commits into
devfrom
feature/recaptcha-v3-support-dev
Sep 18, 2026
Merged

kartikey19427 merged 16 commits into
devfrom
feature/recaptcha-v3-support-dev

Conversation

@kartikey19427

Copy link
Copy Markdown
Contributor

Summary

Adds reCAPTCHA v3 support to the adaptive forms recaptcha component, targeting the dev branch. Same change set as #1977 (which targets master) — cherry-picked onto dev, net diff is identical.

  • RecaptchaImpl.getCaptchaDisplayMode() returns invisible for v3, so af-core's built-in submitForm auto-fetches the token natively (both af-core and af-core-xfa runtimes) — no submitForm override.
  • fetchCaptchaToken is a plain re-export of cf.fetchCaptchaToken; v3 support lands upstream in @aemforms/af-custom-functions (covers headless/EDS). Requires that release + a dependency bump before it is CI-green.
  • Two XFA no-iframe embed fixes: register custom functions into the XFA runtime at bundle load; guard container submit/save handlers against a missing state.events.

Testing

  • Java unit tests for getCaptchaDisplayMode + updated exporter fixture.
  • Frontend unit tests pass.
  • Cypress: authoring (v3 config selection disables size) + runtime (v3 invisible-badge render and a submission should pass for reCAPTCHA v3 test).
  • Verified end-to-end locally: v3 form submit → POST /adobe/forms/af/submit/... → 200.

🤖 Generated with Claude Code

Kartikey Gupta and others added 6 commits September 15, 2026 11:47
Adds handling for the v3 site-key type across the recaptcha component:
- Client-side widget rendering and edit-dialog logic treat v3 the same
  as an invisible/score-based key, since v3 has no visible challenge.
- customFunctions.js fetches the v3 token locally (classic, non-enterprise
  grecaptcha namespace) since af-core's upstream fetchCaptchaToken only
  handles turnstile and Enterprise score-based keys.
- submitForm() is overridden to auto-fetch the captcha token before
  submit for v3 as well, matching the existing turnstile/Enterprise
  behavior.
- For XFA-rendered forms, the token is applied via
  globals.functions.dispatchEvent(field, 'custom:setProperty', {value})
  instead of a direct field.value assignment: @aemforms/af-core-xfa's
  rule-node proxy only implements a get trap (unlike af-core's, which
  also implements set), so a direct assignment invokes the real value
  setter with `this` bound to the proxy and crashes on internal
  `this.parent` access. Dispatching the update looks the field up by id
  on the raw form and applies it directly, avoiding the proxy entirely.
…load

Auto-fetch captcha (invisible reCAPTCHA v2, enterprise-score, and v3) failed on
XFA-backed adaptive forms embedded without an iframe: clicking Submit logged
"fetchCaptchaToken is not defined" and no token was fetched.

The frontend ships two runtime bundles - non-XFA (@aemforms/af-core) and XFA
(@aemforms/af-core-xfa) - each with its own FunctionRuntime. customFunctions were
only registered via setupFormContainer against the non-XFA runtime (window.FormView),
while the XFA form's rule engine runs in af-core-xfa and never received
fetchCaptchaToken (af-core-xfa ships no default for it), so its built-in submitForm
errored.

Register customFunctions at bundle load in the shared entry so each bundle populates
its own FunctionRuntime - in main-xfa.js this happens before the XFA form instance
(and its function-table snapshot) is created. registerFunctions is idempotent, so the
later setupFormContainer registration is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ts under XFA

The container view's submitSuccess/submitError/saveSuccess/saveError handlers read
action.target.getState().events, but the XFA runtime's getState() omits `events`, so
the handlers threw "Cannot read properties of undefined" on submit/save. This was
previously masked by an earlier captcha error and surfaced once captcha submit
succeeds on no-iframe XFA embeds.

Guard the access with optional chaining; a missing events object means no rule is
configured, so the default handler runs (thank-you / error UX). No-op for the non-XFA
runtime where events is always present.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ode + tests

Address review feedback on reCAPTCHA v3 support:
- RecaptchaImpl.getCaptchaDisplayMode() returns "invisible" for v3 so af-core's
  built-in submitForm auto-fetches the token natively (both af-core and
  af-core-xfa runtimes), removing the need to override submitForm.
- Remove the customFunctions.js submitForm override and its helpers
  (isArrayValue/valueOf/toStringOrEmpty), which only existed to support it.
- Add Cypress authoring + runtime specs for v3 and the v3 IT fixtures
  (cloud config + sample page).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…unctions

Collapse the local fetchCaptchaToken wrapper to a plain re-export of
cf.fetchCaptchaToken now that v3 support lives upstream in
@aemforms/af-custom-functions, removing the duplicated v3 grecaptcha.execute
logic.

Requires an @aemforms/af-custom-functions release that includes v3 support
plus a matching dependency bump; until that lands, v3 relies on a local
symlink to the updated package.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add "submission should pass for reCAPTCHA v3" runtime test plus the
  updateRecaptchaV3Config helper (secret injected via the
  RECAPTCHA_V3_API_KEY env var; low threshold for headless reliability).
- Set the real v3 site key in the v3 cloud config fixture (site keys are
  public; the secret stays out of git).
- Gitignore ui.tests/test-module/cypress.env.json so the local secret is
  never committed.

Verified end-to-end locally: v3 form submit -> POST /adobe/forms/af/submit -> 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
target-size serious

…container

On the addon build CI runs, the Threshold Score field is nested inside the
enterprise-fields container which is display:none for a v3 config, so Cypress
refused to clear/type into it ("element is not visible"), failing the v3
submission test. Use {force: true} — a hidden input is still submitted on save —
so the threshold is applied and the v3 submit flow runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
target-size serious

3 similar comments
@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
target-size serious

@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
target-size serious

@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
target-size serious

@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
page-has-heading-one moderate
target-size serious

@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
target-size serious

kartikey19427 and others added 2 commits September 15, 2026 20:18
…test

The v3 threshold field is a coral-numberinput nested in the hidden
enterprise-fields container, so the wrapper is never upgraded to a
clearable element and cy.clear() fails even with {force:true}. Target
the inner native input, which is always clearable regardless of the
wrapper's visibility/upgrade state, and syncs to the field on save.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…HA v3

1.0.18 (published to npm) contains the v3 token-fetch fix that routes v3
site keys through the classic grecaptcha namespace. customFunctions.js
already delegates to cf.fetchCaptchaToken, so this makes the branch
CI-safe now that the version is available in the registry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
target-size serious

1 similar comment
@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
target-size serious

Switch the v3 site/secret key to Google's published test pair, which always
returns a constant score of 0.9 server-side regardless of domain. This makes
the submission test deterministic and removes the retry-until-success loop
and the RECAPTCHA_V3_API_KEY dependency that were needed to work around real
scoring variability in headless CI browsers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
target-size serious

@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
page-has-heading-one moderate
target-size serious

@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
target-size serious

Comment thread ui.frontend/src/index.js Outdated
// the XFA rule engine would otherwise see only its built-in defaults and have no fetchCaptchaToken
// (breaking auto-fetch captcha - invisible/enterprise-score/v3 - on no-iframe XFA embeds).
// registerFunctions is idempotent, so the later setupFormContainer registration is unaffected.
FunctionRuntime.registerFunctions(customFunctions);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is not required, this already happens today, why did you add this ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

On a no-iframe XFA form the rule engine runs on af-core-xfa's FunctionRuntime, which ships no default fetchCaptchaToken, so auto-fetch captcha (invisible v2 / enterprise-score / v3) failed at submit with fetchCaptchaToken is not defined.
I tested the no iframe case for recaptcha v3 and enterprise without this change. Both were failing with no submit action. This line was actually needed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This already happens here, it is redundant check,

FunctionRuntime.registerFunctions(customFunctions);

This should not happen at multiple places, check the existing occurence and add it correctly

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right that registration shouldn't be scattered — but these two calls target different FunctionRuntime instances, so the index.js one isn't redundant. The -xfa build aliases @aemforms/af-core → @aemforms/af-core-xfa (webpack.common.cjs), so main.js and main-xfa.js each carry their own runtime. utils.js:312 runs inside setupFormContainer, invoked only via the global window.FormView (formcontainerview.js) — the non-XFA bundle — so it only ever populates af-core's runtime. The XFA rule engine runs on af-core-xfa, which never received customFunctions, so no-iframe XFA captcha submit failed with fetchCaptchaToken is not defined (verified: removing the index.js line reproduces it).

The clean consolidation would be to make index.js (the shared per-bundle entry) the single registration point and drop utils.js:312 — but that line predates this PR and sits in the universal form-bootstrap path (FormView.Utils.setupFormContainer is public API), so removing it inside a reCAPTCHA fix is a wider, cross-cutting change than I'd want to bundle here without full regression testing. I'd prefer to keep this PR minimal (index.js only) and do the consolidation as a follow-up — happy to raise a ticket.

I feel claude is right here and removing any of them effects either the captcha or have cross repo implications(public API).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Got the issue, this is specific to XFA. Can you move the registration here and remove it from utils.js

Comment thread ui.tests/test-module/specs/recaptcha/recaptcha.runtime.cy.js

@rismehta rismehta left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

check comments

Google does not publish an official test key pair for v3 score-based
verification (the public 6LeIx... pair is documented for v2 checkbox only
and does not reliably render the v3 invisible badge). Restore the real
site key in the v3 cloud config fixture and the RECAPTCHA_V3_API_KEY-based
secret in the Cypress test, and drop the threshold to 0 so a real headless-
browser score still passes verification.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
target-size serious

1 similar comment
@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
target-size serious

@rismehta rismehta left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

check comments

});
})

it("submission should pass for reCAPTCHA v3", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we add a test case for reacaptcha v3failure too ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done- added a v3 failure test case with threshold score 1. Same as enterprise.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The newly added test case for failure is failing on the PR, please check it

Mirror the enterprise-score failure test for v3: set the threshold to 1.0
(above any real headless-browser score) so server-side verification always
rejects the token, deterministically asserting the 400 + CAPTCHA-validation
error. Gated on isLatestAddon like the other failure tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
target-size serious

1 similar comment
@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
target-size serious

@navneet-ag

Copy link
Copy Markdown
Contributor

Comment thread ui.tests/test-module/specs/recaptcha/recaptcha.runtime.cy.js
Reverts the index.js bundle-load registerFunctions call added for reCAPTCHA
v3. The `fetchCaptchaToken is not defined` failure it worked around was
traced to the XFA runtime clientlib (core.forms.components.runtime.all.xfa)
being loaded on pages that have no XFA form, which creates a second
FunctionRuntime singleton that setupFormContainer never populates. The
proper fix is to stop importing the XFA clientlib when no XFA form is
present, which makes this bundle-load registration unnecessary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@rismehta rismehta left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
target-size serious

1 similar comment
@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
target-size serious

kartikey19427 and others added 2 commits September 18, 2026 16:21
The reCAPTCHA v3 test cases are gated behind FT_FORMS-27648. Add it to the
IT DynamicToggleProviderImpl enabledToggles so the toggle is on when the
Cypress v3 submission tests (success and failure) run on CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
reCAPTCHA v3 is gated behind FT_FORMS-27648, so the v3 test cases (render,
submission pass, submission fail) should only run when that toggle is on and
the latest addon is in use. Fetch the enabled toggles in before() and guard
each v3 test with cy.af.isLatestAddon() && toggle_array.includes(FT_RECAPTCHA_V3),
matching the existing hCaptcha pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
target-size serious

1 similar comment
@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
target-size serious

@kartikey19427

kartikey19427 commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Add reCAPTCHA v3 to the supported-versions list (invisible/badge-only,
token auto-fetched at submit, gated behind FT_FORMS-27648) and note that
recaptchaSize does not apply to v3. Addresses PR review feedback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
target-size serious

1 similar comment
@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
target-size serious

@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
page-has-heading-one moderate
target-size serious

@adobe-bot

Copy link
Copy Markdown

Accessibility Violations Found

Id Impact
aria-required-attr critical
empty-heading minor
label-title-only serious
target-size serious

@codecov

codecov Bot commented Sep 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 71.42857% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ponents/internal/models/v1/form/RecaptchaImpl.java 71.42% 2 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@kartikey19427
kartikey19427 merged commit d142415 into dev Sep 18, 2026
10 of 11 checks passed
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.

4 participants