fix: rewrite ALTCHA component for reliable event binding

- Use refs for callbacks to avoid stale closures
- Create widget via innerHTML and bind events immediately
- Remove dev bypass - use real ALTCHA token
- Add console logging for debugging
This commit is contained in:
Patrick Britton 2026-02-16 23:51:46 -06:00
parent 9f1dd857c4
commit 913fbdb8f7
2 changed files with 36 additions and 56 deletions

View file

@ -28,24 +28,21 @@ export default function LoginPage() {
}, []); }, []);
const performLogin = useCallback(async () => { const performLogin = useCallback(async () => {
// Use development bypass for now if (!altchaToken) {
const token = process.env.NODE_ENV === 'development' ? 'BYPASS_DEV_MODE' : altchaToken;
if (!token && !altchaVerified) {
setError('Please complete the security verification'); setError('Please complete the security verification');
return; return;
} }
setLoading(true); setLoading(true);
try { try {
await login(emailRef.current, passwordRef.current, token); await login(emailRef.current, passwordRef.current, altchaToken);
router.push('/'); router.push('/');
} catch (err: any) { } catch (err: any) {
setError(err.message || 'Login failed. Check your credentials.'); setError(err.message || 'Login failed. Check your credentials.');
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [login, router, altchaToken, altchaVerified]); }, [login, router, altchaToken]);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@ -114,7 +111,7 @@ export default function LoginPage() {
<button <button
type="submit" type="submit"
className="btn-primary w-full" className="btn-primary w-full"
disabled={loading || (process.env.NODE_ENV !== 'development' && !altchaVerified)} disabled={loading || !altchaVerified}
> >
{loading ? 'Signing in...' : 'Sign In'} {loading ? 'Signing in...' : 'Sign In'}
</button> </button>

View file

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useEffect, useRef, useCallback } from 'react'; import { useEffect, useRef } from 'react';
interface AltchaProps { interface AltchaProps {
challengeurl: string; challengeurl: string;
@ -9,63 +9,46 @@ interface AltchaProps {
} }
export default function Altcha({ challengeurl, onVerified, onError }: AltchaProps) { export default function Altcha({ challengeurl, onVerified, onError }: AltchaProps) {
const widgetRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const scriptLoaded = useRef(false); const callbacksRef = useRef({ onVerified, onError });
callbacksRef.current = { onVerified, onError };
const handleStateChange = useCallback((e: Event) => { useEffect(() => {
const detail = (e as CustomEvent).detail; // Load script if not already loaded
if (detail?.state === 'verified' && detail?.payload) { if (!document.querySelector('script[data-altcha]')) {
onVerified?.(detail.payload); const script = document.createElement('script');
} else if (detail?.state === 'error') { script.src = 'https://cdn.jsdelivr.net/npm/altcha@2.3.0/dist/altcha.min.js';
onError?.(); script.type = 'module';
script.async = true;
script.setAttribute('data-altcha', 'true');
document.head.appendChild(script);
} }
}, [onVerified, onError]);
useEffect(() => { const container = containerRef.current;
if (scriptLoaded.current) return;
scriptLoaded.current = true;
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/altcha@2.3.0/dist/altcha.min.js';
script.type = 'module';
script.async = true;
document.head.appendChild(script);
}, []);
useEffect(() => {
const container = widgetRef.current;
if (!container) return; if (!container) return;
const observer = new MutationObserver(() => { // Create the widget element
const widget = container.querySelector('altcha-widget'); container.innerHTML = `<altcha-widget challengeurl="${challengeurl}"></altcha-widget>`;
if (widget) {
widget.addEventListener('statechange', handleStateChange);
observer.disconnect();
}
});
observer.observe(container, { childList: true, subtree: true });
// Also try immediately in case widget already exists
const widget = container.querySelector('altcha-widget'); const widget = container.querySelector('altcha-widget');
if (widget) { if (!widget) return;
widget.addEventListener('statechange', handleStateChange);
observer.disconnect();
}
return () => { const handler = (e: Event) => {
observer.disconnect(); const detail = (e as CustomEvent).detail;
const w = container.querySelector('altcha-widget'); console.log('[ALTCHA] statechange:', detail);
if (w) { if (detail?.state === 'verified' && detail?.payload) {
w.removeEventListener('statechange', handleStateChange); callbacksRef.current.onVerified?.(detail.payload);
} else if (detail?.state === 'error') {
callbacksRef.current.onError?.();
} }
}; };
}, [handleStateChange]);
return ( widget.addEventListener('statechange', handler);
<div ref={widgetRef} dangerouslySetInnerHTML={{
__html: `<altcha-widget challengeurl="${challengeurl}" debug></altcha-widget>` return () => {
}} /> widget.removeEventListener('statechange', handler);
); };
}, [challengeurl]);
return <div ref={containerRef} />;
} }