@elsikora/x-captcha-react
Version:
React components for X-Captcha service
218 lines (214 loc) • 15.2 kB
JavaScript
'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