UNPKG

@elsikora/x-captcha-react

Version:

React components for X-Captcha service

218 lines (214 loc) 15.2 kB
'use strict'; var jsxRuntime = require('react/jsx-runtime'); var xCaptchaClient = require('@elsikora/x-captcha-client'); var react = require('react'); var challengeType_enum = require('../../infrastructure/enum/challenge-type.enum.js'); var powSolver_utility = require('../../infrastructure/utility/pow-solver.utility.js'); var captchaWidget_constant = require('../constant/captcha-widget.constant.js'); var i18n = require('../i18n/i18n.js'); require('../i18n/translations/index.js'); var generateThemeVariables_utility = require('../utility/generate-theme-variables.utility.js'); var captchaWidget_module = require('../styles/captcha-widget.module.css.js'); /** * Captcha widget component with modern styling and animations * @param {ICaptchaWidgetProperties} props - The properties * @returns {React.ReactElement} The captcha widget */ const CaptchaWidget = ({ apiUrl, challengeType, height = captchaWidget_constant.default.BOX_HEIGHT, language, onError, onLoad, onVerify, powSolver, publicKey, shouldShowBrandName = true, theme, width = captchaWidget_constant.default.BOX_WIDTH }) => { // Check if publicKey is provided const isMissingPublicKey = !publicKey; // eslint-disable-next-line @elsikora/react/1/naming-convention/use-state const [client] = react.useState(() => { return new xCaptchaClient.XCaptchaApiClient({ apiKey: publicKey, baseUrl: apiUrl, secretKey: "" }); }); const [challenge, setChallenge] = react.useState(null); const [isLoading, setIsLoading] = react.useState(true); const [isVerifying, setIsVerifying] = react.useState(false); const [isVerified, setIsVerified] = react.useState(false); const [error, setError] = react.useState(null); const [animation, setAnimation] = react.useState("none"); const [hasFakeDelay, setHasFakeDelay] = react.useState(false); // Keep latest onLoad callback without causing loadChallenge identity to change const onLoadReference = react.useRef(undefined); react.useEffect(() => { onLoadReference.current = onLoad; }, [onLoad]); // Determine which language to use - either from props or auto-detect // eslint-disable-next-line @elsikora/react/1/naming-convention/use-state const [translate] = react.useState(() => { const detectedLanguage = language ?? i18n.detectLanguage(); return i18n.createTranslator(detectedLanguage); }); const themeVariables = react.useMemo(() => generateThemeVariables_utility.GenerateThemeVariables(theme), [theme]); const loadChallenge = react.useCallback(async () => { try { setIsLoading(true); setError(null); const startTime = Date.now(); const newChallenge = await client.challenge.create({ type: challengeType }); const elapsedTime = Date.now() - startTime; const remainingTime = Math.max(0, captchaWidget_constant.default.LOADING_FAKE_DELAY - elapsedTime); if (remainingTime > 0) { await new Promise((resolve) => setTimeout(resolve, remainingTime)); } setChallenge(newChallenge); const callback = onLoadReference.current; if (newChallenge && callback) callback(newChallenge); } catch { setError(translate("failedToLoadChallenge")); if (onError) onError(translate("failedToLoadChallenge")); } finally { setIsLoading(false); } }, [client, onError, translate, challengeType]); const validateCaptcha = async (challenge) => { try { if (challenge.data.type === challengeType_enum.EChallengeType.POW) { const solution = await powSolver_utility.PowSolver.solve({ difficulty: challenge.data.difficulty, prefix: challenge.data.challenge, }, powSolver); const result = await client.challenge.solve(challenge.id, { solution: { hash: solution.hash, nonce: solution.nonce, type: challengeType_enum.EChallengeType.POW, }, }); setAnimation("success"); setHasFakeDelay(false); setIsVerified(true); setTimeout(() => { if (onVerify) onVerify(result.token); }, captchaWidget_constant.default.ON_VERIFY_DELAY); // eslint-disable-next-line @elsikora/typescript/no-unsafe-enum-comparison } else if (challenge.data.type === challengeType_enum.EChallengeType.CLICK) { const result = await client.challenge.solve(challenge.id, { solution: { data: true, type: challengeType_enum.EChallengeType.CLICK, }, }); setAnimation("success"); setHasFakeDelay(false); setIsVerified(true); setTimeout(() => { if (onVerify) onVerify(result.token); }, captchaWidget_constant.default.ON_VERIFY_DELAY); } else { throw new Error("Invalid challenge type or missing data"); } } catch (error) { console.error("Verification error:", error); setAnimation("error"); setTimeout(() => { setAnimation("none"); setHasFakeDelay(false); setError(translate("errorDuringVerification")); if (onError) onError(translate("errorDuringVerification")); void loadChallenge(); }, captchaWidget_constant.default.RETRY_DELAY); } finally { setTimeout(() => { setIsVerifying(false); }, captchaWidget_constant.default.RETRY_DELAY); } }; // Handle click for the click captcha const handleClick = react.useCallback(() => { if (!challenge || isVerified || isVerifying) return; try { setIsVerifying(true); setAnimation("verifying"); setHasFakeDelay(true); setTimeout(() => { void validateCaptcha(challenge).catch((error) => { console.error("Unexpected error:", error); }); }, captchaWidget_constant.default.VERIFY_FAKE_DELAY); } catch { setAnimation("error"); setHasFakeDelay(false); setTimeout(() => { setAnimation("none"); setError(translate("errorDuringVerification")); if (onError) onError(translate("errorDuringVerification")); void loadChallenge(); }, captchaWidget_constant.default.RETRY_DELAY); setTimeout(() => { setIsVerifying(false); }, captchaWidget_constant.default.RETRY_DELAY); } }, [challenge, client, loadChallenge, onError, onVerify, isVerified, isVerifying, translate]); react.useEffect(() => { if (client) { void loadChallenge(); } }, [loadChallenge, client]); const renderCaptcha = () => { if (isMissingPublicKey) { return (jsxRuntime.jsx("div", { className: captchaWidget_module.default["x-captcha-error"], children: jsxRuntime.jsx("div", { children: translate("missingPublicKey") }) })); } if (isLoading) { return (jsxRuntime.jsxs("div", { className: captchaWidget_module.default["x-captcha-loading"], children: [jsxRuntime.jsx("div", { className: captchaWidget_module.default["x-captcha-loading-spinner"] }), jsxRuntime.jsx("span", { children: translate("loading") })] })); } if (error) { return (jsxRuntime.jsxs("div", { className: captchaWidget_module.default["x-captcha-error"], children: [jsxRuntime.jsx("div", { children: error }), jsxRuntime.jsxs("button", { className: captchaWidget_module.default["x-captcha-error-button"], onClick: () => { setIsLoading(true); setError(null); setTimeout(() => { void loadChallenge(); }, captchaWidget_constant.default.RETRY_DELAY); }, type: "button", children: [jsxRuntime.jsx("span", { className: captchaWidget_module.default["x-captcha-error-button-icon"], children: jsxRuntime.jsxs("svg", { fill: "none", height: "14", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "2", viewBox: "0 0 24 24", width: "14", xmlns: "http://www.w3.org/2000/svg", children: [jsxRuntime.jsx("path", { d: "M21 12a9 9 0 0 1-9 9c-4.95 0-9-4.05-9-9s4.05-9 9-9c2.4 0 4.65.9 6.3 2.55" }), jsxRuntime.jsx("polyline", { points: "21 3 21 9 15 9" })] }) }), translate("tryAgain")] })] })); } if (isVerified) { return (jsxRuntime.jsxs("div", { className: captchaWidget_module.default["x-captcha-verified"], children: [jsxRuntime.jsx("div", { className: `${captchaWidget_module.default["x-captcha-checkbox"]} ${captchaWidget_module.default["x-captcha-checkbox-verified"]}`, children: jsxRuntime.jsx("svg", { className: `${captchaWidget_module.default["x-captcha-checkmark"]} ${captchaWidget_module.default["x-captcha-checkmark-visible"]}`, fill: "none", height: "16", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "3", viewBox: "0 0 24 24", width: "16", xmlns: "http://www.w3.org/2000/svg", children: jsxRuntime.jsx("polyline", { points: "20 6 9 17 4 12" }) }) }), jsxRuntime.jsx("span", { children: translate("verified") })] })); } if (!challenge) { return jsxRuntime.jsx("div", { className: captchaWidget_module.default["x-captcha-error"], children: translate("noChallenge") }); } switch (challenge.type) { case challengeType_enum.EChallengeType.CLICK: { return ( // eslint-disable-next-line @elsikora/jsx/click-events-have-key-events,@elsikora/jsx/no-static-element-interactions jsxRuntime.jsxs("div", { className: captchaWidget_module.default["x-captcha-container"], onClick: handleClick, children: [jsxRuntime.jsxs("div", { className: `${captchaWidget_module.default["x-captcha-checkbox"]} ${isVerifying ? captchaWidget_module.default["x-captcha-checkbox-verifying"] : ""} ${animation === "error" ? captchaWidget_module.default["x-captcha-checkbox-error"] : ""}`, children: [isVerified && (jsxRuntime.jsx("svg", { className: `${captchaWidget_module.default["x-captcha-checkmark"]} ${isVerified ? captchaWidget_module.default["x-captcha-checkmark-visible"] : ""}`, fill: "none", height: "16", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "3", viewBox: "0 0 24 24", width: "16", xmlns: "http://www.w3.org/2000/svg", children: jsxRuntime.jsx("polyline", { points: "20 6 9 17 4 12" }) })), jsxRuntime.jsx("div", { className: `${captchaWidget_module.default["x-captcha-pulse"]} ${isVerifying ? captchaWidget_module.default["x-captcha-pulse-active"] : ""}`, style: { animation: animation === "verifying" ? "x-captcha-pulse 0.8s ease-out" : "none", } })] }), jsxRuntime.jsx("div", { className: captchaWidget_module.default["x-captcha-text"], children: translate("notRobot") }), shouldShowBrandName && jsxRuntime.jsx("div", { className: captchaWidget_module.default["x-captcha-brand"], children: translate("brandName") }), jsxRuntime.jsx("div", { className: `${captchaWidget_module.default["x-captcha-verifying-overlay"]} ${hasFakeDelay ? captchaWidget_module.default["x-captcha-verifying-overlay-visible"] : ""}`, children: jsxRuntime.jsxs("div", { className: captchaWidget_module.default["x-captcha-loading"], children: [jsxRuntime.jsx("div", { className: captchaWidget_module.default["x-captcha-loading-spinner"] }), jsxRuntime.jsx("span", { children: translate("verifying") })] }) })] })); } // eslint-disable-next-line @elsikora/sonar/no-duplicated-branches case challengeType_enum.EChallengeType.POW: { return ( // eslint-disable-next-line @elsikora/jsx/click-events-have-key-events,@elsikora/jsx/no-static-element-interactions jsxRuntime.jsxs("div", { className: captchaWidget_module.default["x-captcha-container"], onClick: handleClick, children: [jsxRuntime.jsxs("div", { className: `${captchaWidget_module.default["x-captcha-checkbox"]} ${isVerifying ? captchaWidget_module.default["x-captcha-checkbox-verifying"] : ""} ${animation === "error" ? captchaWidget_module.default["x-captcha-checkbox-error"] : ""}`, children: [isVerified && (jsxRuntime.jsx("svg", { className: `${captchaWidget_module.default["x-captcha-checkmark"]} ${isVerified ? captchaWidget_module.default["x-captcha-checkmark-visible"] : ""}`, fill: "none", height: "16", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "3", viewBox: "0 0 24 24", width: "16", xmlns: "http://www.w3.org/2000/svg", children: jsxRuntime.jsx("polyline", { points: "20 6 9 17 4 12" }) })), jsxRuntime.jsx("div", { className: `${captchaWidget_module.default["x-captcha-pulse"]} ${isVerifying ? captchaWidget_module.default["x-captcha-pulse-active"] : ""}`, style: { animation: animation === "verifying" ? "x-captcha-pulse 0.8s ease-out" : "none", } })] }), jsxRuntime.jsx("div", { className: captchaWidget_module.default["x-captcha-text"], children: translate("notRobot") }), shouldShowBrandName && jsxRuntime.jsx("div", { className: captchaWidget_module.default["x-captcha-brand"], children: translate("brandName") }), jsxRuntime.jsx("div", { className: `${captchaWidget_module.default["x-captcha-verifying-overlay"]} ${hasFakeDelay ? captchaWidget_module.default["x-captcha-verifying-overlay-visible"] : ""}`, children: jsxRuntime.jsxs("div", { className: captchaWidget_module.default["x-captcha-loading"], children: [jsxRuntime.jsx("div", { className: captchaWidget_module.default["x-captcha-loading-spinner"] }), jsxRuntime.jsx("span", { children: translate("verifying") })] }) })] })); } default: { return jsxRuntime.jsx("div", { className: captchaWidget_module.default["x-captcha-error"], children: translate("unsupportedCaptchaType") }); } } }; // Set dimensions as inline styles as they're specific to the instance, not part of theming const widgetStyle = { ...themeVariables, height, width, }; return (jsxRuntime.jsx("div", { className: captchaWidget_module.default["x-captcha-widget"], style: widgetStyle, children: renderCaptcha() })); }; exports.CaptchaWidget = CaptchaWidget; //# sourceMappingURL=CaptchaWidget.js.map