1- import { useState , useEffect , useRef } from 'react' ;
1+ import { useState , useEffect , useRef , useCallback } from 'react' ;
22import { useNavigate , useSearchParams } from 'react-router-dom' ;
33import { useAuth } from '../hooks/useAuth.jsx' ;
44
55const 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+
720export 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