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

View file

@ -1,58 +1,71 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { useEffect, useRef, useCallback } from 'react';
interface AltchaProps {
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 [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(() => {
// Load ALTCHA widget script
if (scriptLoaded.current) return;
scriptLoaded.current = true;
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.async = true;
script.onload = () => setLoaded(true);
document.head.appendChild(script);
return () => {
if (script.parentNode) {
script.parentNode.removeChild(script);
}
};
}, []);
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);
observer.disconnect();
}
};
});
widget.addEventListener('statechange', handleStateChange);
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 () => {
widget.removeEventListener('statechange', handleStateChange);
observer.disconnect();
const w = container.querySelector('altcha-widget');
if (w) {
w.removeEventListener('statechange', handleStateChange);
}
};
}, [loaded, onStateChange]);
}, [handleStateChange]);
return (
<div ref={widgetRef}>
<altcha-widget
challengeurl={challengeurl}
hidefooter="true"
hidelogo="true"
/>
</div>
<div ref={widgetRef} dangerouslySetInnerHTML={{
__html: `<altcha-widget challengeurl="${challengeurl}" debug></altcha-widget>`
}} />
);
}