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

View file

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