65 lines
2.1 KiB
JavaScript
65 lines
2.1 KiB
JavaScript
// Reusable Cloudflare Turnstile widget. Loads the CF script on demand,
|
|
// renders the widget, and reports the token up via onToken. Exposes a
|
|
// reset() through a ref so forms can re-challenge after a failure.
|
|
import { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
|
|
|
|
const SITE_KEY = process.env.REACT_APP_TURNSTILE_SITE_KEY;
|
|
const SCRIPT_SRC = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
|
|
|
|
let scriptPromise = null;
|
|
function loadTurnstileScript() {
|
|
if (window.turnstile) return Promise.resolve();
|
|
if (scriptPromise) return scriptPromise;
|
|
scriptPromise = new Promise((resolve, reject) => {
|
|
const s = document.createElement('script');
|
|
s.src = SCRIPT_SRC;
|
|
s.async = true;
|
|
s.defer = true;
|
|
s.onload = () => resolve();
|
|
s.onerror = () => reject(new Error('Failed to load CAPTCHA.'));
|
|
document.head.appendChild(s);
|
|
});
|
|
return scriptPromise;
|
|
}
|
|
|
|
const Turnstile = forwardRef(function Turnstile({ onToken }, ref) {
|
|
const containerRef = useRef(null);
|
|
const widgetId = useRef(null);
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
reset() {
|
|
if (widgetId.current !== null && window.turnstile) {
|
|
window.turnstile.reset(widgetId.current);
|
|
onToken?.('');
|
|
}
|
|
},
|
|
}));
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
loadTurnstileScript()
|
|
.then(() => {
|
|
if (cancelled || !containerRef.current || widgetId.current !== null) return;
|
|
widgetId.current = window.turnstile.render(containerRef.current, {
|
|
sitekey: SITE_KEY,
|
|
callback: (token) => onToken?.(token),
|
|
'expired-callback': () => onToken?.(''),
|
|
'error-callback': () => onToken?.(''),
|
|
});
|
|
})
|
|
.catch(() => {/* surfaced via missing token on submit */});
|
|
return () => {
|
|
cancelled = true;
|
|
if (widgetId.current !== null && window.turnstile) {
|
|
window.turnstile.remove(widgetId.current);
|
|
widgetId.current = null;
|
|
}
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
return <div ref={containerRef} className="mt-8" />;
|
|
});
|
|
|
|
export default Turnstile;
|