Files
SkinbaseNova/resources/js/components/security/TurnstileField.jsx

166 lines
4.4 KiB
JavaScript

import React, { useEffect, useRef } from 'react'
const providerAdapters = {
turnstile: {
globalName: 'turnstile',
render(api, container, { siteKey, theme, onToken }) {
return api.render(container, {
sitekey: siteKey,
theme,
callback: (token) => onToken?.(token || ''),
'expired-callback': () => onToken?.(''),
'error-callback': () => onToken?.(''),
})
},
cleanup(api, widgetId, container, onToken) {
if (widgetId !== null && api?.remove) {
api.remove(widgetId)
}
if (container) {
container.innerHTML = ''
}
onToken?.('')
},
},
recaptcha: {
globalName: 'grecaptcha',
render(api, container, { siteKey, theme, onToken }) {
return api.render(container, {
sitekey: siteKey,
theme,
callback: (token) => onToken?.(token || ''),
'expired-callback': () => onToken?.(''),
'error-callback': () => onToken?.(''),
})
},
cleanup(api, widgetId, container, onToken) {
if (widgetId !== null && api?.reset) {
api.reset(widgetId)
}
if (container) {
container.innerHTML = ''
}
onToken?.('')
},
},
hcaptcha: {
globalName: 'hcaptcha',
render(api, container, { siteKey, theme, onToken }) {
return api.render(container, {
sitekey: siteKey,
theme,
callback: (token) => onToken?.(token || ''),
'expired-callback': () => onToken?.(''),
'error-callback': () => onToken?.(''),
})
},
cleanup(api, widgetId, container, onToken) {
if (widgetId !== null && api?.remove) {
api.remove(widgetId)
}
if (container) {
container.innerHTML = ''
}
onToken?.('')
},
},
}
function loadCaptchaScript(src) {
if (!src) {
return Promise.resolve()
}
if (!window.__skinbaseCaptchaScripts) {
window.__skinbaseCaptchaScripts = {}
}
if (!window.__skinbaseCaptchaScripts[src]) {
window.__skinbaseCaptchaScripts[src] = new Promise((resolve, reject) => {
const existing = document.querySelector(`script[src="${src}"]`)
if (existing) {
if (existing.dataset.loaded === 'true') {
resolve()
return
}
existing.addEventListener('load', () => resolve(), { once: true })
existing.addEventListener('error', () => reject(new Error(`Failed to load captcha script: ${src}`)), { once: true })
return
}
const script = document.createElement('script')
script.src = src
script.async = true
script.defer = true
script.addEventListener('load', () => {
script.dataset.loaded = 'true'
resolve()
}, { once: true })
script.addEventListener('error', () => reject(new Error(`Failed to load captcha script: ${src}`)), { once: true })
document.head.appendChild(script)
})
}
return window.__skinbaseCaptchaScripts[src]
}
export default function TurnstileField({ provider = 'turnstile', siteKey, scriptUrl = '', onToken, theme = 'dark', className = '' }) {
const containerRef = useRef(null)
const widgetIdRef = useRef(null)
useEffect(() => {
const adapter = providerAdapters[provider] || providerAdapters.turnstile
if (!siteKey || !containerRef.current) {
return undefined
}
let cancelled = false
let intervalId = null
const mountWidget = () => {
const api = window[adapter.globalName]
if (cancelled || !api?.render || widgetIdRef.current !== null) {
return
}
widgetIdRef.current = adapter.render(api, containerRef.current, {
siteKey,
theme,
onToken,
})
}
loadCaptchaScript(scriptUrl).catch(() => onToken?.('')).finally(() => {
const api = window[adapter.globalName]
if (typeof api?.ready === 'function') {
api.ready(mountWidget)
} else {
mountWidget()
}
if (widgetIdRef.current === null) {
intervalId = window.setInterval(mountWidget, 250)
}
})
return () => {
cancelled = true
if (intervalId) {
window.clearInterval(intervalId)
}
adapter.cleanup(window[adapter.globalName], widgetIdRef.current, containerRef.current, onToken)
widgetIdRef.current = null
}
}, [className, onToken, provider, scriptUrl, siteKey, theme])
if (!siteKey) {
return null
}
return <div ref={containerRef} className={className} />
}