UNPKG

@elsikora/x-captcha-react

Version:

React components for X-Captcha service

218 lines (215 loc) 13.7 kB
import { jsx, jsxs } from 'react/jsx-runtime'; import { XCaptchaApiClient } from '@elsikora/x-captcha-client'; import { useState, useMemo, useCallback, useEffect } from 'react'; import { EChallengeType } from '../../infrastructure/enum/challenge-type.enum.js'; import { PowSolver } from '../../infrastructure/utility/pow-solver.utility.js'; import CAPTCHA_WIDGET_CONSTANT from '../constant/captcha-widget.constant.js'; import { detectLanguage, createTranslator } from '../i18n/i18n.js'; import '../i18n/translations/index.js'; import { GenerateThemeVariables } from '../utility/generate-theme-variables.utility.js'; import styles from '../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, backgroundColor, brandNameColor, challengeType, checkmarkColor, errorTextColor, height = CAPTCHA_WIDGET_CONSTANT.BOX_HEIGHT, language, onError, onVerify, powSolver, publicKey, shouldShowBrandName = true, themeColor = "#4285F4", tryAgainButtonBackgroundColor, tryAgainButtonTextColor, width = CAPTCHA_WIDGET_CONSTANT.BOX_WIDTH }) => { // Check if publicKey is provided const isMissingPublicKey = !publicKey; // eslint-disable-next-line @elsikora/react/1/naming-convention/use-state const [client] = useState(() => { return new XCaptchaApiClient({ apiKey: publicKey, baseUrl: apiUrl, secretKey: "" }); }); const [challenge, setChallenge] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isVerifying, setIsVerifying] = useState(false); const [isVerified, setIsVerified] = useState(false); const [error, setError] = useState(null); const [animation, setAnimation] = useState("none"); const [hasFakeDelay, setHasFakeDelay] = useState(false); // Determine which language to use - either from props or auto-detect // eslint-disable-next-line @elsikora/react/1/naming-convention/use-state const [translate] = useState(() => { const detectedLanguage = language ?? detectLanguage(); return createTranslator(detectedLanguage); }); const themeVariables = useMemo(() => GenerateThemeVariables({ backgroundColor, brandNameColor, checkmarkColor, errorTextColor, themeColor, tryAgainButtonBackgroundColor, tryAgainButtonTextColor, }), [backgroundColor, brandNameColor, checkmarkColor, errorTextColor, themeColor, tryAgainButtonBackgroundColor, tryAgainButtonTextColor]); const loadChallenge = 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, CAPTCHA_WIDGET_CONSTANT.LOADING_FAKE_DELAY - elapsedTime); if (remainingTime > 0) { await new Promise((resolve) => setTimeout(resolve, remainingTime)); } setChallenge(newChallenge); } catch (error_) { console.log("PIDOR", error_); setError(translate("failedToLoadChallenge")); if (onError) onError(translate("failedToLoadChallenge")); } finally { setIsLoading(false); } }, [client, onError, translate]); const validateCaptcha = async (challenge) => { try { if (challenge.data.type === EChallengeType.POW) { const solution = await 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: EChallengeType.POW, }, }); setAnimation("success"); setHasFakeDelay(false); setIsVerified(true); setTimeout(() => { if (onVerify) onVerify(result.token); }, CAPTCHA_WIDGET_CONSTANT.ON_VERIFY_DELAY); // eslint-disable-next-line @elsikora/typescript/no-unsafe-enum-comparison } else if (challenge.data.type === EChallengeType.CLICK) { const result = await client.challenge.solve(challenge.id, { solution: { // eslint-disable-next-line @elsikora/typescript/naming-convention data: true, type: EChallengeType.CLICK, }, }); setAnimation("success"); setHasFakeDelay(false); setIsVerified(true); setTimeout(() => { if (onVerify) onVerify(result.token); }, CAPTCHA_WIDGET_CONSTANT.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(); }, CAPTCHA_WIDGET_CONSTANT.RETRY_DELAY); } finally { setTimeout(() => { setIsVerifying(false); }, CAPTCHA_WIDGET_CONSTANT.RETRY_DELAY); } }; // Handle click for the click captcha const handleClick = useCallback(() => { if (!challenge || isVerified || isVerifying) return; try { setIsVerifying(true); setAnimation("verifying"); setHasFakeDelay(true); setTimeout(() => { void validateCaptcha(challenge).catch((error) => { console.error("Unexpected error:", error); }); }, CAPTCHA_WIDGET_CONSTANT.VERIFY_FAKE_DELAY); } catch { setAnimation("error"); setHasFakeDelay(false); setTimeout(() => { setAnimation("none"); setError(translate("errorDuringVerification")); if (onError) onError(translate("errorDuringVerification")); void loadChallenge(); }, CAPTCHA_WIDGET_CONSTANT.RETRY_DELAY); setTimeout(() => { setIsVerifying(false); }, CAPTCHA_WIDGET_CONSTANT.RETRY_DELAY); } }, [challenge, client, loadChallenge, onError, onVerify, isVerified, isVerifying, translate]); useEffect(() => { if (client) { void loadChallenge(); } }, [loadChallenge, client]); const renderCaptcha = () => { if (isMissingPublicKey) { return (jsx("div", { className: styles["x-captcha-error"], children: jsx("div", { children: translate("missingPublicKey") }) })); } if (isLoading) { return (jsxs("div", { className: styles["x-captcha-loading"], children: [jsx("div", { className: styles["x-captcha-loading-spinner"] }), jsx("span", { children: translate("loading") })] })); } if (error) { return (jsxs("div", { className: styles["x-captcha-error"], children: [jsx("div", { children: error }), jsxs("button", { className: styles["x-captcha-error-button"], onClick: () => { setIsLoading(true); setError(null); setTimeout(() => { void loadChallenge(); }, CAPTCHA_WIDGET_CONSTANT.RETRY_DELAY); }, type: "button", children: [jsx("span", { className: styles["x-captcha-error-button-icon"], children: 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: [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" }), jsx("polyline", { points: "21 3 21 9 15 9" })] }) }), translate("tryAgain")] })] })); } if (isVerified) { return (jsxs("div", { className: styles["x-captcha-verified"], children: [jsx("div", { className: `${styles["x-captcha-checkbox"]} ${styles["x-captcha-checkbox-verified"]}`, children: jsx("svg", { className: `${styles["x-captcha-checkmark"]} ${styles["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: jsx("polyline", { points: "20 6 9 17 4 12" }) }) }), jsx("span", { children: translate("verified") })] })); } if (!challenge) { return jsx("div", { className: styles["x-captcha-error"], children: translate("noChallenge") }); } switch (challenge.type) { case EChallengeType.CLICK: { return ( // eslint-disable-next-line @elsikora/jsx/click-events-have-key-events,@elsikora/jsx/no-static-element-interactions jsxs("div", { className: styles["x-captcha-container"], onClick: handleClick, children: [jsxs("div", { className: `${styles["x-captcha-checkbox"]} ${isVerifying ? styles["x-captcha-checkbox-verifying"] : ""} ${animation === "error" ? styles["x-captcha-checkbox-error"] : ""}`, children: [isVerified && (jsx("svg", { className: `${styles["x-captcha-checkmark"]} ${isVerified ? styles["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: jsx("polyline", { points: "20 6 9 17 4 12" }) })), jsx("div", { className: `${styles["x-captcha-pulse"]} ${isVerifying ? styles["x-captcha-pulse-active"] : ""}`, style: { animation: animation === "verifying" ? "x-captcha-pulse 0.8s ease-out" : "none", } })] }), jsx("div", { className: styles["x-captcha-text"], children: translate("notRobot") }), shouldShowBrandName && jsx("div", { className: styles["x-captcha-brand"], children: translate("brandName") }), jsx("div", { className: `${styles["x-captcha-verifying-overlay"]} ${hasFakeDelay ? styles["x-captcha-verifying-overlay-visible"] : ""}`, children: jsxs("div", { className: styles["x-captcha-loading"], children: [jsx("div", { className: styles["x-captcha-loading-spinner"] }), jsx("span", { children: translate("verifying") })] }) })] })); } // eslint-disable-next-line @elsikora/sonar/no-duplicated-branches case EChallengeType.POW: { return ( // eslint-disable-next-line @elsikora/jsx/click-events-have-key-events,@elsikora/jsx/no-static-element-interactions jsxs("div", { className: styles["x-captcha-container"], onClick: handleClick, children: [jsxs("div", { className: `${styles["x-captcha-checkbox"]} ${isVerifying ? styles["x-captcha-checkbox-verifying"] : ""} ${animation === "error" ? styles["x-captcha-checkbox-error"] : ""}`, children: [isVerified && (jsx("svg", { className: `${styles["x-captcha-checkmark"]} ${isVerified ? styles["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: jsx("polyline", { points: "20 6 9 17 4 12" }) })), jsx("div", { className: `${styles["x-captcha-pulse"]} ${isVerifying ? styles["x-captcha-pulse-active"] : ""}`, style: { animation: animation === "verifying" ? "x-captcha-pulse 0.8s ease-out" : "none", } })] }), jsx("div", { className: styles["x-captcha-text"], children: translate("notRobot") }), shouldShowBrandName && jsx("div", { className: styles["x-captcha-brand"], children: translate("brandName") }), jsx("div", { className: `${styles["x-captcha-verifying-overlay"]} ${hasFakeDelay ? styles["x-captcha-verifying-overlay-visible"] : ""}`, children: jsxs("div", { className: styles["x-captcha-loading"], children: [jsx("div", { className: styles["x-captcha-loading-spinner"] }), jsx("span", { children: translate("verifying") })] }) })] })); } default: { return jsx("div", { className: styles["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 (jsx("div", { className: styles["x-captcha-widget"], style: widgetStyle, children: renderCaptcha() })); }; export { CaptchaWidget }; //# sourceMappingURL=CaptchaWidget.js.map