fix: upgrade ALTCHA widget to v2.3.0 and rewrite component

- Upgrade from v0.5.0 to v2.3.0 (latest)
- Use dangerouslySetInnerHTML for proper web component rendering
- Add MutationObserver for reliable event binding
- Enable debug mode on widget to diagnose issues
- Simplify component API with onVerified/onError callbacks
This commit is contained in:
Patrick Britton 2026-02-16 23:47:02 -06:00
parent 96c0348d3a
commit 9f1dd857c4
2 changed files with 54 additions and 40 deletions

View file

@ -17,14 +17,14 @@ export default function LoginPage() {
const { login } = useAuth(); const { login } = useAuth();
const router = useRouter(); const router = useRouter();
const handleAltchaStateChange = useCallback((state: any) => { const handleAltchaVerified = useCallback((payload: string) => {
if (state.state === 'verified' && state.payload) { setAltchaToken(payload);
setAltchaToken(state.payload);
setAltchaVerified(true); setAltchaVerified(true);
} else { }, []);
const handleAltchaError = useCallback(() => {
setAltchaToken(''); setAltchaToken('');
setAltchaVerified(false); setAltchaVerified(false);
}
}, []); }, []);
const performLogin = useCallback(async () => { const performLogin = useCallback(async () => {
@ -107,7 +107,8 @@ export default function LoginPage() {
<div> <div>
<Altcha <Altcha
challengeurl="https://api.sojorn.net/api/v1/admin/altcha-challenge" challengeurl="https://api.sojorn.net/api/v1/admin/altcha-challenge"
onStateChange={handleAltchaStateChange} onVerified={handleAltchaVerified}
onError={handleAltchaError}
/> />
</div> </div>
<button <button

View file

@ -1,58 +1,71 @@
'use client'; 'use client';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useCallback } from 'react';
interface AltchaProps { interface AltchaProps {
challengeurl: string; challengeurl: string;
onStateChange?: (state: any) => void; onVerified?: (payload: string) => void;
onError?: () => void;
} }
export default function Altcha({ challengeurl, onStateChange }: AltchaProps) { export default function Altcha({ challengeurl, onVerified, onError }: AltchaProps) {
const widgetRef = useRef<HTMLDivElement>(null); const widgetRef = useRef<HTMLDivElement>(null);
const [loaded, setLoaded] = useState(false); 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]);
useEffect(() => { useEffect(() => {
// Load ALTCHA widget script if (scriptLoaded.current) return;
scriptLoaded.current = true;
const script = document.createElement('script'); const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/altcha@0.5.0/dist/altcha.min.js'; script.src = 'https://cdn.jsdelivr.net/npm/altcha@2.3.0/dist/altcha.min.js';
script.type = 'module'; script.type = 'module';
script.async = true; script.async = true;
script.onload = () => setLoaded(true);
document.head.appendChild(script); document.head.appendChild(script);
return () => {
if (script.parentNode) {
script.parentNode.removeChild(script);
}
};
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!loaded || !widgetRef.current) return; const container = widgetRef.current;
if (!container) return;
const widget = widgetRef.current.querySelector('altcha-widget');
if (!widget) return;
const handleStateChange = (event: any) => {
if (onStateChange) {
onStateChange(event.detail);
}
};
const observer = new MutationObserver(() => {
const widget = container.querySelector('altcha-widget');
if (widget) {
widget.addEventListener('statechange', handleStateChange); 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');
if (widget) {
widget.addEventListener('statechange', handleStateChange);
observer.disconnect();
}
return () => { return () => {
widget.removeEventListener('statechange', handleStateChange); observer.disconnect();
const w = container.querySelector('altcha-widget');
if (w) {
w.removeEventListener('statechange', handleStateChange);
}
}; };
}, [loaded, onStateChange]); }, [handleStateChange]);
return ( return (
<div ref={widgetRef}> <div ref={widgetRef} dangerouslySetInnerHTML={{
<altcha-widget __html: `<altcha-widget challengeurl="${challengeurl}" debug></altcha-widget>`
challengeurl={challengeurl} }} />
hidefooter="true"
hidelogo="true"
/>
</div>
); );
} }