Skip to content

Commit c0db305

Browse files
author
Ravi Singh
committed
fix: replace visible Turnstile widget with invisible execution
Removes the Turnstile iframe from the DOM entirely. The iframe was causing iOS Safari/PWA keyboard focus fights — the widget stole focus from inputs, triggering keyboard show/hide loops. Now uses invisible mode: Turnstile executes on form submit via a detached div element, gets the token, and discards. No visible UI, no iframe, no focus conflicts.
1 parent f573fcc commit c0db305

1 file changed

Lines changed: 39 additions & 49 deletions

File tree

pwa/client/src/pages/Login.jsx

Lines changed: 39 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
1-
import { useState, useEffect, useRef } from 'react';
1+
import { useState, useEffect, useRef, useCallback } from 'react';
22
import { useNavigate, useSearchParams } from 'react-router-dom';
33
import { useAuth } from '../hooks/useAuth.jsx';
44

55
const TURNSTILE_SITE_KEY = import.meta.env.VITE_TURNSTILE_SITE_KEY || '';
66

7+
// Load Turnstile script once globally
8+
let turnstileLoaded = false;
9+
function loadTurnstileScript() {
10+
if (turnstileLoaded || !TURNSTILE_SITE_KEY) return;
11+
if (document.getElementById('cf-turnstile-script')) { turnstileLoaded = true; return; }
12+
const script = document.createElement('script');
13+
script.id = 'cf-turnstile-script';
14+
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
15+
script.async = true;
16+
document.head.appendChild(script);
17+
turnstileLoaded = true;
18+
}
19+
720
export default function Login() {
8-
const [mode, setMode] = useState('login'); // 'login' | 'register' | 'verify'
21+
const [mode, setMode] = useState('login');
922
const [email, setEmail] = useState('');
1023
const [password, setPassword] = useState('');
1124
const [name, setName] = useState('');
@@ -14,58 +27,21 @@ export default function Login() {
1427
const [info, setInfo] = useState('');
1528
const [loading, setLoading] = useState(false);
1629
const [resendCooldown, setResendCooldown] = useState(0);
17-
const turnstileRef = useRef(null);
18-
const turnstileWidgetId = useRef(null);
1930
const { login, register, verifyEmail, resendCode, needsVerification, user } = useAuth();
2031
const navigate = useNavigate();
2132
const [searchParams] = useSearchParams();
2233
const redirectTo = searchParams.get('redirect') || '/';
2334

24-
// If logged in and verified, redirect (run once, not on every render)
2535
useEffect(() => {
26-
if (user && !needsVerification) {
27-
navigate(redirectTo, { replace: true });
28-
} else if (user && needsVerification && mode !== 'verify') {
36+
if (user && !needsVerification) navigate(redirectTo, { replace: true });
37+
else if (user && needsVerification && mode !== 'verify') {
2938
setEmail(user.email);
3039
setMode('verify');
3140
}
32-
// eslint-disable-next-line react-hooks/exhaustive-deps
3341
}, [user?.id, needsVerification]);
3442

35-
// Load Turnstile script for registration
36-
useEffect(() => {
37-
if (!TURNSTILE_SITE_KEY || mode !== 'register') return;
38-
if (document.getElementById('cf-turnstile-script')) return;
39-
40-
const script = document.createElement('script');
41-
script.id = 'cf-turnstile-script';
42-
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
43-
script.async = true;
44-
script.onload = () => renderTurnstile();
45-
document.head.appendChild(script);
46-
return () => {};
47-
}, [mode]);
48-
49-
useEffect(() => {
50-
if (mode === 'register' && TURNSTILE_SITE_KEY && window.turnstile && turnstileRef.current) {
51-
renderTurnstile();
52-
}
53-
}, [mode]);
54-
55-
function renderTurnstile() {
56-
if (!window.turnstile || !turnstileRef.current) return;
57-
if (turnstileWidgetId.current) window.turnstile.remove(turnstileWidgetId.current);
58-
turnstileWidgetId.current = window.turnstile.render(turnstileRef.current, {
59-
sitekey: TURNSTILE_SITE_KEY,
60-
theme: 'dark',
61-
callback: () => {},
62-
});
63-
}
64-
65-
function getTurnstileToken() {
66-
if (!TURNSTILE_SITE_KEY || !window.turnstile || turnstileWidgetId.current == null) return '';
67-
return window.turnstile.getResponse(turnstileWidgetId.current) || '';
68-
}
43+
// Preload Turnstile script (no widget rendered — invisible mode)
44+
useEffect(() => { loadTurnstileScript(); }, []);
6945

7046
// Resend cooldown timer
7147
useEffect(() => {
@@ -74,16 +50,33 @@ export default function Login() {
7450
return () => clearTimeout(t);
7551
}, [resendCooldown]);
7652

53+
// Get Turnstile token invisibly (execute on demand)
54+
const getTurnstileToken = useCallback(() => {
55+
return new Promise((resolve) => {
56+
if (!TURNSTILE_SITE_KEY || !window.turnstile) { resolve(''); return; }
57+
try {
58+
const widgetId = window.turnstile.render(document.createElement('div'), {
59+
sitekey: TURNSTILE_SITE_KEY,
60+
size: 'invisible',
61+
callback: (token) => { resolve(token); },
62+
'error-callback': () => { resolve(''); },
63+
'expired-callback': () => { resolve(''); },
64+
});
65+
window.turnstile.execute(widgetId);
66+
} catch { resolve(''); }
67+
});
68+
}, []);
69+
7770
const handleSubmit = async (e) => {
7871
e.preventDefault();
7972
setError('');
8073
setInfo('');
8174
setLoading(true);
8275
try {
8376
if (mode === 'register') {
84-
const turnstileToken = getTurnstileToken();
77+
const turnstileToken = await getTurnstileToken();
8578
if (TURNSTILE_SITE_KEY && !turnstileToken) {
86-
setError('Please complete the captcha');
79+
setError('Verification failed. Please try again.');
8780
setLoading(false);
8881
return;
8982
}
@@ -138,7 +131,7 @@ export default function Login() {
138131
</p>
139132
</div>
140133

141-
{/* Form card */}
134+
{/* Form */}
142135
<div className="w-full max-w-sm">
143136
<form onSubmit={handleSubmit} className="space-y-4" autoComplete={mode === 'verify' ? 'off' : 'on'}>
144137
{mode === 'verify' ? (
@@ -187,9 +180,6 @@ export default function Login() {
187180
focus:border-water focus:ring-1 focus:ring-water/50 outline-none transition-all"
188181
placeholder="At least 6 characters" />
189182
</div>
190-
{mode === 'register' && TURNSTILE_SITE_KEY && (
191-
<div ref={turnstileRef} className="flex justify-center" />
192-
)}
193183
</>
194184
)}
195185

0 commit comments

Comments
 (0)