Skip to content

Commit 692f32a

Browse files
authored
fix(platform): Turnstile CAPTCHA reset after failed login attempts (#10056)
Users were unable to retry login attempts after a failed authentication because the Turnstile CAPTCHA widget was not properly resetting. This forced users to refresh the entire page to attempt login again, creating a terrible user experience. Root Cause: The useTurnstile hook had several critical issues: - The reset() function only cleared state when shouldRender was true and widget existed - Widget ID tracking was unreliable due to intercepting window.turnstile.render - Token wasn't being cleared on verification failures - State wasn't being reset consistently across error scenarios Changes 🏗️ <!-- Concisely describe all of the changes made in this pull request: --> - Fixed useTurnstile hook reset logic: Modified the reset() function to always clear all state (token, verified, verifying, error) regardless of shouldRender condition - Improved widget ID synchronization: Added setWidgetId prop to the Turnstile component interface and hook for reliable widget tracking between component and hook - Enhanced error handling: Updated handleVerify, handleExpire, and handleError to properly reset tokens on failures - Updated all auth components: Added setWidgetId prop to all Turnstile component usages in login, signup, and password reset pages - Removed unreliable widget tracking: Eliminated the window.turnstile.render interception approach in favor of explicit prop-based communication Checklist 📋 For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - <!-- Put your test plan here: --> - [x] Test failed login attempt - CAPTCHA resets properly without page refresh - [x] Test failed signup attempt - CAPTCHA resets properly without page refresh - [x] Test successful login flow - CAPTCHA works normally - [x] Test CAPTCHA expiration - State resets correctly - [x] Test CAPTCHA error scenarios - Error handling works properly
1 parent 9f2b9d0 commit 692f32a

File tree

5 files changed

+58
-21
lines changed

5 files changed

+58
-21
lines changed

autogpt_platform/frontend/src/app/(platform)/login/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ export default function LoginPage() {
163163
onVerify={turnstile.handleVerify}
164164
onExpire={turnstile.handleExpire}
165165
onError={turnstile.handleError}
166+
setWidgetId={turnstile.setWidgetId}
166167
action="login"
167168
shouldRender={turnstile.shouldRender}
168169
/>

autogpt_platform/frontend/src/app/(platform)/reset_password/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ export default function ResetPasswordPage() {
188188
onVerify={changePasswordTurnstile.handleVerify}
189189
onExpire={changePasswordTurnstile.handleExpire}
190190
onError={changePasswordTurnstile.handleError}
191+
setWidgetId={changePasswordTurnstile.setWidgetId}
191192
action="change_password"
192193
shouldRender={changePasswordTurnstile.shouldRender}
193194
/>
@@ -230,6 +231,7 @@ export default function ResetPasswordPage() {
230231
onVerify={sendEmailTurnstile.handleVerify}
231232
onExpire={sendEmailTurnstile.handleExpire}
232233
onError={sendEmailTurnstile.handleError}
234+
setWidgetId={sendEmailTurnstile.setWidgetId}
233235
action="reset_password"
234236
shouldRender={sendEmailTurnstile.shouldRender}
235237
/>

autogpt_platform/frontend/src/app/(platform)/signup/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ export default function SignupPage() {
164164
onVerify={turnstile.handleVerify}
165165
onExpire={turnstile.handleExpire}
166166
onError={turnstile.handleError}
167+
setWidgetId={turnstile.setWidgetId}
167168
action="signup"
168169
shouldRender={turnstile.shouldRender}
169170
/>

autogpt_platform/frontend/src/components/auth/Turnstile.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface TurnstileProps {
1111
className?: string;
1212
id?: string;
1313
shouldRender?: boolean;
14+
setWidgetId?: (id: string | null) => void;
1415
}
1516

1617
export function Turnstile({
@@ -22,6 +23,7 @@ export function Turnstile({
2223
className,
2324
id = "cf-turnstile",
2425
shouldRender = true,
26+
setWidgetId,
2527
}: TurnstileProps) {
2628
const containerRef = useRef<HTMLDivElement>(null);
2729
const widgetIdRef = useRef<string | null>(null);
@@ -68,7 +70,11 @@ export function Turnstile({
6870

6971
// Reset any existing widget
7072
if (widgetIdRef.current && window.turnstile) {
71-
window.turnstile.reset(widgetIdRef.current);
73+
try {
74+
window.turnstile.reset(widgetIdRef.current);
75+
} catch (err) {
76+
console.warn("Failed to reset existing Turnstile widget:", err);
77+
}
7278
}
7379

7480
// Render a new widget
@@ -86,15 +92,32 @@ export function Turnstile({
8692
},
8793
action,
8894
});
95+
96+
// Notify the hook about the widget ID
97+
setWidgetId?.(widgetIdRef.current);
8998
}
9099

91100
return () => {
92101
if (widgetIdRef.current && window.turnstile) {
93-
window.turnstile.remove(widgetIdRef.current);
102+
try {
103+
window.turnstile.remove(widgetIdRef.current);
104+
} catch (err) {
105+
console.warn("Failed to remove Turnstile widget:", err);
106+
}
107+
setWidgetId?.(null);
94108
widgetIdRef.current = null;
95109
}
96110
};
97-
}, [loaded, siteKey, onVerify, onExpire, onError, action, shouldRender]);
111+
}, [
112+
loaded,
113+
siteKey,
114+
onVerify,
115+
onExpire,
116+
onError,
117+
action,
118+
shouldRender,
119+
setWidgetId,
120+
]);
98121

99122
// Method to reset the widget manually
100123
const reset = useCallback(() => {

autogpt_platform/frontend/src/hooks/useTurnstile.ts

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ interface UseTurnstileResult {
2121
reset: () => void;
2222
siteKey: string;
2323
shouldRender: boolean;
24+
setWidgetId: (id: string | null) => void;
2425
}
2526

2627
const TURNSTILE_SITE_KEY =
@@ -34,7 +35,7 @@ export function useTurnstile({
3435
autoVerify = true,
3536
onSuccess,
3637
onError,
37-
resetOnError = false,
38+
resetOnError = true,
3839
}: UseTurnstileOptions = {}): UseTurnstileResult {
3940
const [token, setToken] = useState<string | null>(null);
4041
const [verifying, setVerifying] = useState(false);
@@ -60,26 +61,30 @@ export function useTurnstile({
6061
}
6162
}, [token, autoVerify, shouldRender]);
6263

63-
useEffect(() => {
64-
if (typeof window !== "undefined" && window.turnstile) {
65-
const originalRender = window.turnstile.render;
66-
window.turnstile.render = (container, options) => {
67-
const id = originalRender(container, options);
68-
setWidgetId(id);
69-
return id;
70-
};
71-
}
64+
const setWidgetIdCallback = useCallback((id: string | null) => {
65+
setWidgetId(id);
7266
}, []);
7367

7468
const reset = useCallback(() => {
75-
if (shouldRender && window.turnstile && widgetId) {
76-
window.turnstile.reset(widgetId);
77-
78-
// Always reset the state when reset is called
79-
setToken(null);
80-
setVerified(false);
81-
setVerifying(false);
82-
setError(null);
69+
// Always reset the state when reset is called, regardless of shouldRender
70+
// This ensures users can retry CAPTCHA after failed attempts
71+
setToken(null);
72+
setVerified(false);
73+
setVerifying(false);
74+
setError(null);
75+
76+
// Only reset the actual Turnstile widget if it exists and shouldRender is true
77+
if (
78+
shouldRender &&
79+
typeof window !== "undefined" &&
80+
window.turnstile &&
81+
widgetId
82+
) {
83+
try {
84+
window.turnstile.reset(widgetId);
85+
} catch (err) {
86+
console.warn("Failed to reset Turnstile widget:", err);
87+
}
8388
}
8489
}, [shouldRender, widgetId]);
8590

@@ -106,6 +111,7 @@ export function useTurnstile({
106111
setError(newError);
107112
if (onError) onError(newError);
108113
if (resetOnError) {
114+
setToken(null);
109115
setVerified(false);
110116
}
111117
}
@@ -119,6 +125,7 @@ export function useTurnstile({
119125
: new Error("Unknown error during verification");
120126
setError(newError);
121127
if (resetOnError) {
128+
setToken(null);
122129
setVerified(false);
123130
}
124131
setVerifying(false);
@@ -138,6 +145,7 @@ export function useTurnstile({
138145
if (shouldRender) {
139146
setToken(null);
140147
setVerified(false);
148+
setError(null);
141149
}
142150
}, [shouldRender]);
143151

@@ -146,6 +154,7 @@ export function useTurnstile({
146154
if (shouldRender) {
147155
setError(err);
148156
if (resetOnError) {
157+
setToken(null);
149158
setVerified(false);
150159
}
151160
if (onError) onError(err);
@@ -165,5 +174,6 @@ export function useTurnstile({
165174
reset,
166175
siteKey: TURNSTILE_SITE_KEY,
167176
shouldRender,
177+
setWidgetId: setWidgetIdCallback,
168178
};
169179
}

0 commit comments

Comments
 (0)