Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions blocks/identity-block/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,23 @@
.b-login-form {
@include scss.block-components("login-form");
@include scss.block-properties("login-form");
&__bot-protection-section {
@include scss.block-components("login-form-bot-protection-section");
@include scss.block-properties("login-form-bot-protection-section");
}
&__privacy-statement {
@include scss.block-components("login-form-privacy-statement");
@include scss.block-properties("login-form-privacy-statement");
}
}

.b-login-links {
@include scss.block-components("login-links");
@include scss.block-properties("login-links");
&__inner-link {
@include scss.block-components("login-links-inner-link");
@include scss.block-properties("login-links-inner-link");
}
}

.b-reset-password {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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 ReCAPTCHA from "react-google-recaptcha";

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

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

useEffect(() => {
const checkCaptcha = async () => {
const config = await Identity.getConfig();
Copy link
Copy Markdown
Contributor

@LauraPinilla LauraPinilla Mar 7, 2024

Choose a reason for hiding this comment

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

@edwardcho1231 I think you can move L23-L25 into if on L27, thus you call config from identity or sales, depending on challengeIn

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@LauraPinilla Don't we have to call Identity.config even when the challengeIn === 'checkout' since we need the recaptchaSiteKey?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

good point @edwardcho1231 I missed that. Yep for checkout we need to call both

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}
Copy link
Copy Markdown
Contributor

@LauraPinilla LauraPinilla Mar 12, 2024

Choose a reason for hiding this comment

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

Checking the reCaptcha documentation if you include onExpired, you will be able to see the error "Verification expired, Check the checkbox again" after two minutes the challenge was completed, once the token is expired

<ReCAPTCHA
  sitekey={siteKey}
  onChange={onChange}
  onExpired 
  />

onExpired={() => {}}
/>}
{captchaError && <Paragraph data-testid="bot-challege-captcha-error">{phrases.t("identity-block.bot-protection-error")}</Paragraph>}
</section>
);
};

export default BotChallengeProtection;
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { useIdentity } from "@wpmedia/arc-themes-components";
import BotChallengeProtection from ".";

const mockLogin = jest.fn(() => Promise.resolve());

const mockIdentity = {
isLoggedIn: jest.fn(() => false),
getConfig: jest.fn(() => ({})),
login: mockLogin,
};

const mockSales = {
getConfig: jest.fn(() => {})
}

jest.mock("@wpmedia/arc-themes-components", () => ({
...jest.requireActual("@wpmedia/arc-themes-components"),
useIdentity: jest.fn(() => ({
isInitialized: true,
Identity: {
...mockIdentity,
},
})),
useSales: jest.fn(() => ({
isInitialized: true,
Sales: {
...mockSales,
},
})),
}));

describe("Bot challenge protection", () => {
it("renders with required items", () => {
render(<BotChallengeProtection challengeIn="signin" />);

expect(screen.getByTestId("bot-challege-protection-container")).not.toBeNull();
});
it("it does not render if identity is not initialized", () => {
useIdentity.mockImplementation(() => ({
isInitialized: false,
Identity: {
...mockIdentity,
},
}))
render(<BotChallengeProtection challengeIn="test" />);
expect(screen.queryByTestId("bot-challege-protection-container")).toBeNull();
});
});
4 changes: 2 additions & 2 deletions blocks/identity-block/features/login-links/default.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react";
import PropTypes from "@arc-fusion/prop-types";
import { useFusionContext } from "fusion:context";
import getTranslatedPhrases from "fusion:intl";
import { Link, Stack } from "@wpmedia/arc-themes-components";
import { Link, Paragraph, Stack } from "@wpmedia/arc-themes-components";

const BLOCK_CLASS_NAME = "b-login-links";
const defaultLoginURL = "/account/login/";
Expand Down Expand Up @@ -35,7 +35,7 @@ const LoginLinks = ({ customFields }) => {
<Link href={forgotURL}>{phrases.t("identity-block.login-links-forgot")}</Link>
) : null}
{showSignUp ? (
<Link href={signUpURL}>{phrases.t("identity-block.login-links-signup")}</Link>
<Paragraph>{phrases.t("identity-block.login-links-signup")}<Link href={signUpURL} className={`${BLOCK_CLASS_NAME}__inner-link`}>{phrases.t("identity-block.sign-up-natural")}</Link></Paragraph>
) : null}
</Stack>
);
Expand Down
40 changes: 31 additions & 9 deletions blocks/identity-block/features/login/default.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React, { useState, useEffect } from "react";
/* global grecaptcha */
import React, { useState } from "react";
import PropTypes from "@arc-fusion/prop-types";
import { useFusionContext } from "fusion:context";
import getProperties from "fusion:properties";
import getTranslatedPhrases from "fusion:intl";
import { Input, useIdentity } from "@wpmedia/arc-themes-components";
import { Input, useIdentity, Paragraph } from "@wpmedia/arc-themes-components";
import HeadlinedSubmitForm from "../../components/headlined-submit-form";
import useLogin from "../../components/login";
import BotChallengeProtection from "../../components/bot-challenge-protection";
import useOIDCLogin from "../../utils/useOIDCLogin";
import validateURL from "../../utils/validate-redirect-url";

Expand All @@ -14,16 +16,18 @@ const BLOCK_CLASS_NAME = "b-login-form";
const Login = ({ customFields }) => {
const { redirectURL, redirectToPreviousPage, loggedInPageLocation, OIDC } = customFields;

const url_string = window.location.href;
const url = new URL(url_string);
const urlString = window.location.href;
const url = new URL(urlString);

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

const isOIDC = OIDC && url.searchParams.get("client_id") && url.searchParams.get("response_type") === "code";
const { Identity, isInitialized } = useIdentity();
const [captchaToken, setCaptchaToken] = useState();
const [error, setError] = useState();
const [captchaError, setCaptchaError] = useState();
const { loginRedirect } = useLogin({
isAdmin,
redirectURL,
Expand All @@ -42,9 +46,14 @@ const Login = ({ customFields }) => {
buttonLabel={phrases.t("identity-block.log-in")}
className={BLOCK_CLASS_NAME}
formErrorText={error}
headline={phrases.t("identity-block.log-in")}
onSubmit={({ email, password }) =>
Identity.login(email, password, {rememberMe: true})
headline={phrases.t("identity-block.log-in-headline")}
onSubmit={({ email, password }) => {
setError(null);
setCaptchaError(null);
return Identity.login(email, password, {
rememberMe: true,
recaptchaToken: captchaToken
})
.then(() => {
if (isOIDC) {
loginByOIDC();
Expand All @@ -53,12 +62,23 @@ const Login = ({ customFields }) => {
window.location = validatedURL;
}
})
.catch(() => setError(phrases.t("identity-block.login-form-error")))
.catch((e) => {
if (e?.code === "130001") {
setCaptchaError(true);
}
else {
setError(phrases.t("identity-block.login-form-error"));
}
if (grecaptcha) {
grecaptcha.reset();
}
})
}
}
>
<Input
autoComplete="email"
label={phrases.t("identity-block.email")}
label={phrases.t("identity-block.email-label")}
name="email"
required
showDefaultError={false}
Expand All @@ -73,6 +93,8 @@ 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>
</HeadlinedSubmitForm>
);
};
Expand Down
32 changes: 23 additions & 9 deletions blocks/identity-block/features/login/default.test.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from "react";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import Login from "./default";
import { useIdentity } from "@wpmedia/arc-themes-components";
import Login from "./default";

const defaultCustomFields = {
redirectURL: "",
redirectToPreviousPage: true,
signUpURL: ""
};

const mockLogin = jest.fn(() => Promise.resolve());
Expand All @@ -15,6 +16,10 @@ const mockIdentity = {
login: mockLogin,
};

const mockSales = {
getConfig: jest.fn(() => {})
}

jest.mock("@wpmedia/arc-themes-components", () => ({
...jest.requireActual("@wpmedia/arc-themes-components"),
useIdentity: jest.fn(() => ({
Expand All @@ -23,27 +28,33 @@ jest.mock("@wpmedia/arc-themes-components", () => ({
...mockIdentity,
},
})),
useSales: jest.fn(() => ({
isInitialized: true,
Sales: {
...mockSales,
},
})),
}));
jest.mock("fusion:properties", () => jest.fn(() => ({})));

describe("Identity Login Feature", () => {
it("renders", () => {
render(<Login customFields={defaultCustomFields} />);
expect(screen.queryByRole("form")).not.toBeNull();
expect(screen.getByRole("form")).not.toBeNull();
});

it("shows login form", () => {
render(<Login customFields={defaultCustomFields} />);
expect(screen.queryByRole("form")).not.toBeNull();
expect(screen.getByRole("form")).not.toBeNull();
expect(screen.getByLabelText("identity-block.password")).not.toBeNull();
expect(screen.getByLabelText("identity-block.email")).not.toBeNull();
expect(screen.getByLabelText("identity-block.email-label")).not.toBeNull();
});

it("submits the login form", async () => {
render(<Login customFields={defaultCustomFields} />);

await waitFor(() => expect(screen.getByLabelText("identity-block.email")));
fireEvent.change(screen.getByLabelText("identity-block.email"), {
await waitFor(() => expect(screen.getByLabelText("identity-block.email-label")));
fireEvent.change(screen.getByLabelText("identity-block.email-label"), {
target: { value: "email@test.com" },
});
await waitFor(() => expect(screen.getByLabelText("identity-block.password")));
Expand All @@ -60,6 +71,9 @@ describe("Identity Login Feature", () => {
describe("Identity Login Feature - rejected Login", () => {
beforeEach(() => {
mockLogin.mockImplementation(() => Promise.reject());
global.grecaptcha = {
reset: jest.fn()
}
});

afterEach(() => {
Expand All @@ -69,8 +83,8 @@ describe("Identity Login Feature - rejected Login", () => {
it("rejects the login", async () => {
render(<Login customFields={defaultCustomFields} />);

await waitFor(() => expect(screen.getByLabelText("identity-block.email")));
fireEvent.change(screen.getByLabelText("identity-block.email"), {
await waitFor(() => expect(screen.getByLabelText("identity-block.email-label")));
fireEvent.change(screen.getByLabelText("identity-block.email-label"), {
target: { value: "email@test.com" },
});

Expand All @@ -83,7 +97,7 @@ describe("Identity Login Feature - rejected Login", () => {
fireEvent.click(screen.getByRole("button"));

await waitFor(() => expect(mockLogin).toHaveBeenCalled());
await waitFor(() => screen.getByText("identity-block.login-form-error"));
await screen.findByText("identity-block.login-form-error");
});
});

Expand Down
17 changes: 16 additions & 1 deletion blocks/identity-block/intl.json
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,7 @@
"bn": "কোনো অ্যাকাউন্ট নেই?\nসাইন আপ করুন",
"bo": "ཁྱེད་ལ་དྲ་གྲངས་མེད་དམ། ཐོ་འགོད།",
"de": "Sign up for an account.",
"en": "Don't have an account? Sign up",
"en": "Need to create an account? ",
"es": "Sign up for an account.",
"fr": "Sign up for an account.",
"id": "Belum memiliki akun? Daftar",
Expand Down Expand Up @@ -1273,5 +1273,20 @@
"vi": "Tên người dùng",
"zh-CN": "用户名",
"zh-TW": "使用者名稱"
},
"identity-block.bot-protection-error": {
Copy link
Copy Markdown
Contributor

@LauraPinilla LauraPinilla Mar 12, 2024

Choose a reason for hiding this comment

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

Hi @edwardcho1231 , As part of the designs we have an additional reCaptcha state when it expires.

"en": "Please verify that you are not a robot."
},
"identity-block.email-label": {
"en": "Email"
},
"identity-block.log-in-headline": {
"en": "Log in to your account"
},
"identity-block.privacy-statement": {
"en": "By creating an account, you agree to the Terms of Service and acknowledge our Privacy Policy."
},
"identity-block.sign-up-natural": {
"en": "Sign up"
}
}
Loading