Repository navigation
fix(captcha): hide solved Turnstile widget so its success box leaves the form - #7770
tenderdeve wants to merge 9 commits into
Conversation
|
@tenderdeve is attempting to deploy a commit to the cow-dev Team on Vercel. A member of the Team first needs to authorize it. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
WalkthroughCaptchaWidget now hides the Turnstile widget after success, exchanges the token with stale-result protection, shows a retry state on exchange failure, and resets the widget on error, expiry, or retry. Tests were expanded to cover the updated lifecycle. ChangesCaptcha widget lifecycle handling
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Cloudflare Pages preview mirror Preview branch URL: https://github.com/cowprotocol/cowswap/tree/cf-preview/pr-7770 Source fork branch:
|
There was a problem hiding this comment.
Hey @tenderdeve , thanks, but the fix is illogical: in case token fails, why would we show 'success' captcha again? In this case, we need to show a 'failed' status on it with a possibility to re-confirm it.
In case of success. we need to show this status, then hide the captcha block from forms.
|
@elena-zh good point — reworked in 694924a. Dropped the display:none/re-show approach that made the solved "Success!" box reappear on failure. Now: the solved widget stays visible while the JWT exchange runs; on success the JWT is stored and the widget unmounts (Success box leaves the form); on exchange failure (and onError) the widget is |
elena-zh
left a comment
There was a problem hiding this comment.
Thank you, looks good now!
elena-zh
left a comment
There was a problem hiding this comment.
Requesting changes because it would be great to rebase the PR against develop instead of Main
…the form The Turnstile widget only unmounted once the exchanged JWT was stored, so its built-in "Success!" box lingered on the trade form after the challenge was solved — and stayed indefinitely whenever the token exchange was slow or failed. Hide the widget as soon as the challenge succeeds while the exchange runs in the background, and reveal it again on expiry/error so a fresh challenge can be solved.
…success The previous approach hid the solved widget via display:none during the JWT exchange and re-showed it on failure, which re-rendered Cloudflare's stale "Success!" box even though the exchange had failed. Instead, leave the solved widget visible while the exchange runs, then: - on success, store the JWT (the widget unmounts, so the "Success!" box leaves the form) — the original issue; - on failure (or onError), reset the widget so it drops the stale "Success!" box and re-runs the challenge for the user to re-confirm.
2e4fb60 to
ca50f13
Compare
|
@elena-zh rebased onto |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
apps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.test.tsx (1)
26-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd direct coverage for the new
onErrorreset path.The production callback now resets the widget on Turnstile errors, but the mock only exposes
onSuccess, so this behavior can regress without a focused test.🧪 Proposed test coverage
-let mockTurnstileProps: { style?: CSSProperties; onSuccess?: (token: string) => void } = {} +let mockTurnstileProps: { + style?: CSSProperties + onSuccess?: (token: string) => void | Promise<void> + onError?: (errorCode: string) => void +} = {} const mockReset = jest.fn() @@ - (props: { style?: CSSProperties; onSuccess?: (token: string) => void }, ref: unknown) => { + (props: typeof mockTurnstileProps, ref: unknown) => { mockTurnstileProps = props react.useImperativeHandle(ref, () => ({ reset: mockReset, execute: jest.fn() })) return <div data-testid="turnstile" style={props.style} /> }, @@ it('resets the widget so the user can re-confirm when the JWT exchange fails', async () => { @@ expect(store.get(captchaJwtAtom)).toBeNull() }) + + it('resets the widget when Turnstile reports an error', async () => { + const store = createStore() + store.set(featureFlagsAtom, { isCaptchaEnabled: true }) + + renderWithStore(store) + + act(() => { + mockTurnstileProps.onError?.('mock-error') + }) + + expect(mockReset).toHaveBeenCalled() + expect(store.get(captchaJwtAtom)).toBeNull() + })Also applies to: 146-162
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.test.tsx` around lines 26 - 42, The CaptchaWidget test mock only captures onSuccess, so the new Turnstile error reset behavior is not directly covered. Update the `@marsidev/react-turnstile` mock in CaptchaWidget.container.test.tsx to expose onError alongside onSuccess via mockTurnstileProps, then add a focused test against CaptchaWidget that invokes the captured onError callback and asserts mockReset is called on the mocked Turnstile ref. Use the existing mockTurnstileProps and mockReset setup to locate the relevant test block.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In
`@apps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.test.tsx`:
- Around line 26-42: The CaptchaWidget test mock only captures onSuccess, so the
new Turnstile error reset behavior is not directly covered. Update the
`@marsidev/react-turnstile` mock in CaptchaWidget.container.test.tsx to expose
onError alongside onSuccess via mockTurnstileProps, then add a focused test
against CaptchaWidget that invokes the captured onError callback and asserts
mockReset is called on the mocked Turnstile ref. Use the existing
mockTurnstileProps and mockReset setup to locate the relevant test block.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 2245651c-aa25-46a2-ac8d-ddde68f8b457
📒 Files selected for processing (2)
apps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.test.tsxapps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.tsx
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
fairlighteth
left a comment
There was a problem hiding this comment.
⚠️ AI Review (Codex GPT-5, worked 11m): pending exchange still leaves the Turnstile success box visible
Finding: [BLOCKING] Pending JWT exchange still shows the solved captcha
- Location:
apps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.test.tsx:134 - This one is important: the linked issue asks for the captcha message to disappear when the box is checked, and the PR body still says the widget hides as soon as the challenge succeeds.
- Current head now asserts the opposite: after
onSuccess, the widget remainsdisplay: blockuntilexchangeTurnstileToken()resolves. If/auth/turnstileis slow or never settles, the “Success!” box can still linger on the form for the whole pending exchange. - The failure reset path is better now, but it does not cover the slow/pending case.
Suggested fix
- Keep the immediate hide-on-success state for the pending exchange, and on exchange failure call
captchaRef.current?.reset()before showing/re-enabling the widget for a fresh challenge. - Update the pending-exchange test to assert the solved widget is hidden immediately, plus keep the failure test asserting
reset()is called.
Review scope and related context
- This is separate from CodeRabbit’s existing nit, which only asks for direct
onErrorreset test coverage. - I also rechecked the earlier human review request about not re-showing a stale success state after exchange failure; current code does reset on exchange failure, so I’m not repeating that as a finding.
- The PR body/checklist should be updated if the intended UX is no longer “hide immediately after solve,” because it currently conflicts with the implementation and test.
🤖 Prompt for AI agents
Verify this finding against current PR head. The linked issue #7768 expects the Turnstile success message to disappear when the checkbox is checked, but current tests assert the widget remains display:block while exchangeTurnstileToken is pending.
Fix only if still valid:
- Hide the solved Turnstile widget immediately after onSuccess while the JWT exchange is pending.
- If the exchange fails, reset the Turnstile ref so the stale Success state is cleared before showing/retrying.
- Preserve stale request guards.
- Add/update tests for pending exchange hidden immediately, success unmount, exchange failure reset/retry, and optionally onError reset.
Generated using the pr-review skill from the CoW Protocol skills repo.
|
Hey @fairlighteth , just FYI: we have another task to fix a failed captcha state if JWT token verification failed #7801 |
The solved Turnstile widget stayed visible until exchangeTurnstileToken resolved, so a slow /auth/turnstile left the "Success!" box lingering on the form. Hide it as soon as the challenge is solved (display:none, kept mounted) and reveal + reset it only if the exchange fails.
|
@fairlighteth good catch — updated in a7f0ed5. The solved widget now hides immediately on |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
fd CaptchaWidget.container.tsx
echo "---"
cat -n apps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.tsx 2>/dev/null | sed -n '1,200p'Length of output: 7554
This looks like a solid resolution for both the original issue ( ヽ(・∀・)ノ [resolve] |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.test.tsx (1)
164-176: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExercise
onErrorfrom the hidden solved state.This currently starts from a visible widget, so it does not prove
onErrorclears the newisSolvedstate. TriggeronSuccessfirst, assertdisplay: none, then fireonError.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.test.tsx` around lines 164 - 176, The test for CaptchaWidget.container’s Turnstile error handling should start from the hidden solved state instead of a visible widget. In the CaptchaWidget.container.test.tsx case that calls mockTurnstileProps.onError, first trigger mockTurnstileProps.onSuccess to put the widget into the solved state and assert the element is hidden with display none, then fire onError and verify mockReset is called and the widget becomes visible again. This should exercise the isSolved state transition in the CaptchaWidget container behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@apps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.tsx`:
- Around line 66-69: The CaptchaWidget.container state can get stuck with
isSolved=true when captcha is disabled during a pending exchange, so the widget
remounts hidden after re-enable. Update the CaptchaWidget.container logic to
clear isSolved whenever captcha is disabled or the pending exchange is
invalidated, using the existing isSolved state and exchangeRequestIdRef flow so
the widget can render normally again when re-enabled.
---
Nitpick comments:
In
`@apps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.test.tsx`:
- Around line 164-176: The test for CaptchaWidget.container’s Turnstile error
handling should start from the hidden solved state instead of a visible widget.
In the CaptchaWidget.container.test.tsx case that calls
mockTurnstileProps.onError, first trigger mockTurnstileProps.onSuccess to put
the widget into the solved state and assert the element is hidden with display
none, then fire onError and verify mockReset is called and the widget becomes
visible again. This should exercise the isSolved state transition in the
CaptchaWidget container behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 571852b2-e411-4de0-81c9-47ce7bf0cf27
📒 Files selected for processing (2)
apps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.test.tsxapps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.tsx
There was a problem hiding this comment.
AI Review (Codex GPT-5, worked 2m): original finding addressed; failure-state UX still open
Rechecked
- Author response:
a7f0ed5fdclaims the solved widget now hides immediately ononSuccess, then reveals + resets on exchange failure. - Code path:
apps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.tsx:onSuccess. - Test coverage:
CaptchaWidget.container.test.tsxnow asserts the widget isdisplay: nonewhile the JWT exchange is still pending, anddisplay: blockagain after exchange failure/reset.
Result: Fixed for the issue I raised. The prior slow /auth/turnstile failure mode no longer applies: onSuccess sets isSolved before starting the JWT exchange, so the Turnstile stays mounted but hidden while the exchange is pending.
Follow-up context: Elena raised a newer review comment, and #7801 adds a separate UX requirement: failed JWT verification should show a failure state, not just reset/reveal the widget. I would keep my original finding marked addressed, but I would not treat the PR as fully clear if #7801 is intended to be solved here.
Verification notes for AI agents
Prior finding:
- After onSuccess, the Turnstile success box stayed visible until exchangeTurnstileToken resolved.
Current verification:
- onSuccess sets isSolved(true) before calling handleChallengeSuccess.
- Turnstile style uses display: isSolved ? "none" : "block".
- Exchange failure sets isSolved(false) and calls captchaRef.current?.reset().
- Tests cover pending exchange hidden immediately and exchange failure reset/reveal.
Remaining separate UX question:
- #7801 expects a visible failed state when JWT verification fails. Current behavior resets/reveals for retry; it does not implement a failed-state UI.
Generated using the pr-review skill from the CoW Protocol skills repo.
elena-zh
left a comment
There was a problem hiding this comment.
Hey @tenderdeve , now I don't like when the captcha just disappears if token verification is failed. As I've described in #7801 , it should show 'failure' instead
On a failed token exchange the solved widget was silently reset, briefly
implying success on a rejected token. Introduce an explicit status
('challenge' | 'solved' | 'failed'): on exchange failure show a 'Verification
failed' notice with a 'Try again' button that resets and re-runs the challenge,
instead of auto-resetting. Resolves cowprotocol#7801.
Addresses @elena-zh's review feedback.
|
@elena-zh @fairlighteth updated in 873104d — the widget no longer silently resets when JWT verification fails. Introduced an explicit |
|
@fairlighteth , could you please review this PR and let us know if this 'failed verification' widget looks good for you? |
fairlighteth
left a comment
There was a problem hiding this comment.
⚠️ AI Review (Codex GPT-5, worked 5m): failed state addressed, pending exchange still needs a fallback
Rechecked
- Author response:
873104d81/ #7770 (comment). - Code path:
apps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.tsx:41. - Test coverage: current tests cover hidden-pending, rejected-exchange failed notice, retry reset/reveal, and Turnstile error reset.
Result: Addressed for rejected JWT verification. The failed notice shown in Elena's screenshot matches #7801's expected state and looks fine to me.
Finding: [BLOCKING] Hidden pending exchange still has no timeout/fallback
- Location:
apps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.tsx:171andapps/cowswap-frontend/src/modules/captcha/api/captchaApi.ts:9 - This one is important:
onSuccessnow hides Turnstile immediately withstatus = 'solved', butexchangeTurnstileToken()is a barefetch()without an abort timeout. - If
/auth/turnstilenever settles, thecatchpath never runs, so the user gets no failed notice, no Try again button, no JWT, and the captcha area stays hidden indefinitely.
Suggested fix
- Add a timeout/abort fallback to the JWT exchange request and route timeout/abort into the same
status = 'failed'UI. - Add a focused test for a non-settling exchange becoming retryable after the timeout.
Review scope and related context
- The prior pending-success-box finding is fixed: the widget is
display: nonewhile the JWT exchange is pending. - Elena's failed-state request is fixed for rejected exchanges: the code shows
Verification failed. Please try again.and only resets Turnstile after the user clicks Try again. - CodeRabbit's resolved feature-flag edge still looks equivalent in current code: disabling captcha while
status === 'solved'invalidates the request but does not resetstatustochallenge, so re-enabling can remount Turnstile hidden. I'm not making that the main blocker because runtime flag toggling is lower-risk than the auth request fallback.
CI context checked:
- Lint, Test, Typecheck, Agent Harness: passing.
- Cypress is red because
INTEGRATION_TEST_PRIVATE_KEY env missing, which looks environment-related rather than captcha-specific.
🤖 Prompt for AI agents
Verify this follow-up against current PR head.
Context:
- CaptchaWidget sets status to "solved" in onSuccess and hides Turnstile while exchangeTurnstileToken is pending.
- exchangeTurnstileToken uses fetch(TURNSTILE_AUTH_URL) without an AbortController or request timeout.
- Rejected exchanges show the failed notice correctly, but a never-settling exchange never reaches that path.
Fix:
- Add a bounded timeout/abort fallback for the JWT exchange.
- Ensure timeout/abort shows the same failed verification notice with Try again.
- Add/update tests for non-settling exchange timeout, rejected exchange failed notice, and retry reset/reveal.
Generated using the pr-review skill from the CoW Protocol skills repo.
🧪 Scoped browser QA passed: disconnected captcha solved/failed states verified; live Turnstile + signing not checkedOutcome
Run details
Artifacts
How to retest
Not checked / follow-up
Generated using the |
|
@tenderdeve Functionally this looks good to me: the failed JWT exchange no longer leaves the solved Turnstile success state hanging around, and the user gets a clear retry path. Small UI polish suggestion: I’d avoid rendering this as a loose red text line plus a full-width Here's a suggested render example: I think this would fit better as a compact inline error state using the existing shared banner pattern:
Rough shape: This is roughly the same composition we already use in places like So my take is: behavior is good, but I’d polish the presentation by grouping the error and retry action into a single danger |
Group the failed-verification message and its "Try again" action into a single horizontal danger InlineBanner instead of a loose red text line plus a full-width button. Keeps the retry CTA visually secondary to the form's primary "Connect Wallet" button so the two no longer compete. Addresses @fairlighteth's UI polish feedback.
|
@fairlighteth updated in b43a274 — reworked the failed-verification UI into a single compact horizontal danger |
|
Closing in favor of #7812, really sorry @tenderdeve, thank you for your effort! |





Summary
Fixes #7768
The Cloudflare Turnstile widget only unmounted once the exchanged JWT was stored. Turnstile keeps its own "Success!" box on screen after a challenge is solved, so that box lingered on the trade form between solving and the JWT exchange resolving — and stayed there indefinitely whenever the exchange was slow or failed.
Now the widget is hidden as soon as the challenge succeeds, while the token→JWT exchange runs in the background. It stays mounted (just hidden) so an expiry or error reveals it again for a fresh challenge, and a successful exchange unmounts it entirely as before.
To Test
/auth/turnstile), the widget reappears instead of leaving a stale "Success!" box stuck on the form.Background
onSuccesspreviously gated visibility solely on the stored JWT. Extracted the exchange handler and added anisChallengeSolvedflag that hides the widget on solve and resets on expiry/error/exchange-failure.Summary by CodeRabbit
Bug Fixes
Tests