Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding recaptcha_v3 #2031

Merged
merged 16 commits into from
Mar 27, 2024
Merged
12 changes: 12 additions & 0 deletions blocks/identity-block/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@
@include scss.block-components("login-form-privacy-statement");
@include scss.block-properties("login-form-privacy-statement");
}
&__sign-up-div {
@include scss.block-components("login-form-sign-up-div");
@include scss.block-properties("login-form-sign-up-div");
}
&__sign-up-button {
@include scss.block-components("login-form-sign-up-button");
@include scss.block-properties("login-form-sign-up-button");
}
&__login-form-error {
@include scss.block-components("login-form-error");
@include scss.block-properties("login-form-error");
}
}

.b-login-links {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,64 +1,49 @@
import React, { useState, useEffect } from "react";
import { useFusionContext } from "fusion:context";
import getProperties from "fusion:properties";
import getTranslatedPhrases from "fusion:intl";
import { Paragraph, useIdentity, useSales } from "@wpmedia/arc-themes-components";
import React from "react";
import { useIdentity, Paragraph } from "@wpmedia/arc-themes-components";
import useRecaptcha, { RECAPTCHA_V2, RECAPTCHA_V3 } from "../../utils/useRecaptcha";
import ReCAPTCHA from "react-google-recaptcha";
import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3";
import RecaptchaV3 from "./reCaptchaV3";

const BotChallengeProtection = ({ challengeIn, setCaptchaToken, className, captchaError, setCaptchaError }) => {
const { Identity, isInitialized } = useIdentity();
const { Sales } = useSales();
const [siteKey, setSiteKey] = useState();
export const ARCXP_CAPTCHA= "ArcXP_captchaToken"

const BotChallengeProtection = ({ challengeIn, setCaptchaToken, className, captchaError, setCaptchaError, resetRecaptcha }) => {

const { isInitialized } = useIdentity();
const { recaptchaVersion, siteKey, isRecaptchaEnabled } = useRecaptcha(challengeIn);

const onChange = (value) => {
setCaptchaToken(value);
setCaptchaError(null);
localStorage.setItem('ArcXP_captchaToken', value);
localStorage.setItem(ARCXP_CAPTCHA, value);
};

useEffect(() => {
const checkCaptcha = async () => {
const config = await Identity.getConfig();
const {recaptchaSiteKey, recaptchaScore } = config;
const isIdentityCaptchaEnabled = config?.[`${challengeIn}Recaptcha`];

if(['signup', 'signin', 'magicLink'].includes(challengeIn)) {
if (isIdentityCaptchaEnabled && recaptchaScore === '-1' && recaptchaSiteKey) {
setSiteKey(recaptchaSiteKey);
}
}

if (challengeIn === 'checkout') {
const salesConfig = await Sales.getConfig();
const isSalesCaptchaEnabled = salesConfig?.checkoutRecaptchaEnabled;
if (isSalesCaptchaEnabled && recaptchaScore === '-1' && recaptchaSiteKey) {
setSiteKey(recaptchaSiteKey);
}
}

};
checkCaptcha();

}, [Identity, Sales, challengeIn]);

const { arcSite } = useFusionContext();
const { locale } = getProperties(arcSite);
const phrases = getTranslatedPhrases(locale);

if (!isInitialized) {
return null;
}

return (
<section className={`${className}__bot-protection-section`} data-testid="bot-challege-protection-container">
{!!siteKey && <ReCAPTCHA
sitekey={siteKey}
onChange={onChange}
onExpired={() => {}}
/>}
{captchaError && <Paragraph data-testid="bot-challege-captcha-error">{phrases.t("identity-block.bot-protection-error")}</Paragraph>}
</section>
);
if (isRecaptchaEnabled && !!siteKey && !!recaptchaVersion) {
if (recaptchaVersion === RECAPTCHA_V2) {
return (
<section
className={`${className}__bot-protection-section`}
data-testid="bot-challege-protection-container"
>
<ReCAPTCHA sitekey={siteKey} onChange={onChange} onExpired={() => {}}/>
{captchaError && <Paragraph>{phrases.t("identity-block.bot-protection-error")}</Paragraph>}
</section>
);
}
if (recaptchaVersion === RECAPTCHA_V3) {
return (
<GoogleReCaptchaProvider reCaptchaKey={siteKey} scriptProps={{ async: true }}>
<RecaptchaV3 setCaptchaToken={setCaptchaToken} resetRecaptcha={resetRecaptcha} />
</GoogleReCaptchaProvider>
);
}
} else {
return null;
}
};

export default BotChallengeProtection;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useEffect, useCallback } from "react";
import { useGoogleReCaptcha } from "react-google-recaptcha-v3";
import { ARCXP_CAPTCHA } from "./index";

const RecaptchaV3 = ({ setCaptchaToken, resetRecaptcha }) => {
const { executeRecaptcha } = useGoogleReCaptcha();

const handleReCaptcha3Verify = useCallback(async () => {
if (!executeRecaptcha) {
console.log("ArcXP - Execute recaptcha not yet available");
return;
}
const token = await executeRecaptcha();
setCaptchaToken(token);
localStorage.setItem(ARCXP_CAPTCHA, token);
}, [executeRecaptcha]);

useEffect(() => {
handleReCaptcha3Verify();
}, [executeRecaptcha, resetRecaptcha]);

return null;
};

export default RecaptchaV3;
55 changes: 42 additions & 13 deletions blocks/identity-block/features/login/default.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* global grecaptcha */
import React, { useState } from "react";
import PropTypes from "@arc-fusion/prop-types";
import { useFusionContext } from "fusion:context";
Expand All @@ -10,9 +9,22 @@ import useLogin from "../../components/login";
import BotChallengeProtection from "../../components/bot-challenge-protection";
import useOIDCLogin from "../../utils/useOIDCLogin";
import validateURL from "../../utils/validate-redirect-url";
import { RECAPTCHA_LOGIN } from "../../utils/useRecaptcha";

const BLOCK_CLASS_NAME = "b-login-form";

export function definedMessageByCode(code) {
return errorCodes[code] || errorCodes["0"];
}

const errorCodes = {
100015: "identity-block.login-form-error.account-is-disabled",
130001: "identity-block.login-form-error.captcha-token-invalid",
130051: "identity-block.login-form-error.unverified-email-address",
100013: "identity-block.login-form-error.max-devices",
0: "identity-block.login-form-error.invalid-email-password",
};

const Login = ({ customFields }) => {
const { redirectURL, redirectToPreviousPage, loggedInPageLocation, OIDC } = customFields;

Expand All @@ -23,9 +35,11 @@ const Login = ({ customFields }) => {
const { locale } = getProperties(arcSite);
const phrases = getTranslatedPhrases(locale);

const isOIDC = OIDC && url.searchParams.get("client_id") && url.searchParams.get("response_type") === "code";
const isOIDC =
OIDC && url.searchParams.get("client_id") && url.searchParams.get("response_type") === "code";
const { Identity, isInitialized } = useIdentity();
const [captchaToken, setCaptchaToken] = useState();
const [resetRecaptcha, setResetRecaptcha] = useState(true);
const [error, setError] = useState();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the purpose of this variable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @edwardcho1231 similar to reCaptchav2, reCaptchav3 also expires, but in order to grab a new token we need to call executeRecaptcha(). Thus, resetRecaptcha is changing from true to false and viceversa everytime the reCaptcha3 is expired, thus the useEffect() detects this change and we need to grab a new one.

const [captchaError, setCaptchaError] = useState();
const { loginRedirect } = useLogin({
Expand All @@ -45,7 +59,6 @@ const Login = ({ customFields }) => {
<HeadlinedSubmitForm
buttonLabel={phrases.t("identity-block.log-in")}
className={BLOCK_CLASS_NAME}
formErrorText={error}
headline={phrases.t("identity-block.log-in-headline")}
onSubmit={({ email, password }) => {
setError(null);
Expand All @@ -63,19 +76,25 @@ const Login = ({ customFields }) => {
}
})
.catch((e) => {
setResetRecaptcha(!resetRecaptcha);
if (e?.code === "130001") {
setCaptchaError(true);
}
else {
setError(phrases.t("identity-block.login-form-error"));
setError(phrases.t(definedMessageByCode(e.code)));
}
if (grecaptcha) {
grecaptcha.reset();
}
})
}
}
});
}}
>
{error ? (
<div className={`${BLOCK_CLASS_NAME}__login-form-error`}>
<Paragraph>{error}</Paragraph>
</div>
) : null}
<Input
autoComplete="email"
label={phrases.t("identity-block.email-label")}
Expand All @@ -93,8 +112,17 @@ const Login = ({ customFields }) => {
showDefaultError={false}
type="password"
/>
<BotChallengeProtection className={BLOCK_CLASS_NAME} challengeIn="signin" setCaptchaToken={setCaptchaToken} captchaError={captchaError} setCaptchaError={setCaptchaError} />
<Paragraph className={`${BLOCK_CLASS_NAME}__privacy-statement`}>{phrases.t("identity-block.privacy-statement")}</Paragraph>
<BotChallengeProtection
className={BLOCK_CLASS_NAME}
challengeIn={RECAPTCHA_LOGIN}
setCaptchaToken={setCaptchaToken}
captchaError={captchaError}
setCaptchaError={setCaptchaError}
resetRecaptcha={resetRecaptcha}
/>
<Paragraph className={`${BLOCK_CLASS_NAME}__privacy-statement`}>
{phrases.t("identity-block.privacy-statement")}
</Paragraph>
</HeadlinedSubmitForm>
);
};
Expand All @@ -120,10 +148,11 @@ Login.propTypes = {
"The URL to which a user would be redirected to if visiting a login page when already logged in.",
}),
OIDC: PropTypes.bool.tag({
name: 'Login with OIDC',
defaultValue: false,
description: 'Used when authenticating a third party site with OIDC PKCE flow. This will use an ArcXp Org as an auth provider',
}),
name: "Login with OIDC",
defaultValue: false,
description:
"Used when authenticating a third party site with OIDC PKCE flow. This will use an ArcXp Org as an auth provider",
}),
}),
};

Expand Down
15 changes: 15 additions & 0 deletions blocks/identity-block/intl.json
Original file line number Diff line number Diff line change
Expand Up @@ -1283,6 +1283,21 @@
"identity-block.log-in-headline": {
"en": "Log in to your account"
},
"identity-block.login-form-error.account-is-disabled": {
"en": "Account is disabled."
},
"identity-block.login-form-error.captcha-token-invalid": {
"en": "Captcha token invalid."
},
"identity-block.login-form-error.unverified-email-address": {
"en": "Email Address is not verified."
},
"identity-block.login-form-error.max-devices": {
"en": "User account has reached the max number of devices."
},
"identity-block.login-form-error.invalid-email-password": {
"en": "Email or password is invalid. Try again."
},
"identity-block.privacy-statement": {
"en": "By creating an account, you agree to the Terms of Service and acknowledge our Privacy Policy."
},
Expand Down
Loading
Loading