Skip to content

Commit

Permalink
Asub 8194/magic link login without pw (#2110)
Browse files Browse the repository at this point in the history
* add login without password

* add login without password

* Bump @babel/preset-react from 7.23.3 to 7.24.1 (#2050)

Bumps [@babel/preset-react](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react) from 7.23.3 to 7.24.1.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.24.1/packages/babel-preset-react)

---
updated-dependencies:
- dependency-name: "@babel/preset-react"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Added esc event listener to useEffect (#2044)

* Added esc event listener to useEffect

---------

Co-authored-by: Malavika Koppula <[email protected]>

* Added overflow to section-title links (#2027)

* Added flex to section-title links

---------

Co-authored-by: Malavika Koppula <[email protected]>

* Bump algoliasearch from 4.23.1 to 4.23.2 (#2057)

Bumps [algoliasearch](https://github.com/algolia/algoliasearch-client-javascript) from 4.23.1 to 4.23.2.
- [Release notes](https://github.com/algolia/algoliasearch-client-javascript/releases)
- [Changelog](https://github.com/algolia/algoliasearch-client-javascript/blob/master/CHANGELOG.md)
- [Commits](algolia/algoliasearch-client-javascript@4.23.1...4.23.2)

---
updated-dependencies:
- dependency-name: algoliasearch
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump eslint-plugin-react from 7.33.2 to 7.34.1 (#2062)

Bumps [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) from 7.33.2 to 7.34.1.
- [Release notes](https://github.com/jsx-eslint/eslint-plugin-react/releases)
- [Changelog](https://github.com/jsx-eslint/eslint-plugin-react/blob/v7.34.1/CHANGELOG.md)
- [Commits](jsx-eslint/eslint-plugin-react@v7.33.2...v7.34.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-react
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump glob from 10.3.10 to 10.3.12 (#2063)

Bumps [glob](https://github.com/isaacs/node-glob) from 10.3.10 to 10.3.12.
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](isaacs/node-glob@v10.3.10...v10.3.12)

---
updated-dependencies:
- dependency-name: glob
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Manual promo blocks now respect focal point (#1996)

* add focal point code and tests

* fix eslint errors

* fix formatting

* fix formatting p2

* shorten PR template based on eng sync discussions (#2065)

* add onetime password

* ASUB-8201 Sign In with Apple (#2067)

* Sign In with Apple

* fixing linting and tests

* fixing test & linting

* fixing linting errors

* removing update from  package.json

* fixing linting errors

* disable eslint warnings

* fixing warnings

* fixing sintax

* removing keys

* removing only

* fixing linting errors

* add sucess page and recaptcha

* add translations

* update intl.json

* add tests for ota feature

* remove act

* lint fixes

* lint fixes

* lint fixes

* update translation to sort by order

* revision changes

* remove extra error message

* lint fix

* update styling fontsize for heading and remove padding

* fix for button not showing up

* fix for button not showing up

* update default url

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: malavikakoppula <[email protected]>
Co-authored-by: Malavika Koppula <[email protected]>
Co-authored-by: Anna Sherman <[email protected]>
Co-authored-by: LauraPinilla <[email protected]>
  • Loading branch information
6 people authored May 8, 2024
1 parent 27458c9 commit 1940170
Show file tree
Hide file tree
Showing 12 changed files with 703 additions and 190 deletions.
4 changes: 4 additions & 0 deletions .storybook/themes/news.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2854,6 +2854,10 @@
"display": flex,
"flex-wrap": wrap
),
"section-title-links": (
"display": flex,
"flex-wrap": wrap
),
"share-bar": (
"background": var(--background-color),
"box-shadow": var(--global-box-shadow-1),
Expand Down
24 changes: 22 additions & 2 deletions blocks/identity-block/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,27 @@
@include scss.block-properties("login-form");
}

.b-one-time-login-form {
&__ota-sub-headline {
@include scss.block-components("ota-sub-headline");
@include scss.block-properties("ota-sub-headline");
}

&__recaptcha {
@include scss.block-components("ota-recaptcha");
@include scss.block-properties("ota-recaptcha");
}

@include scss.block-components("one-time-login-form");
@include scss.block-properties("one-time-login-form");
}

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

&__inner-link {
@include scss.block-components("login-links-inner-link");
@include scss.block-properties("login-links-inner-link");
Expand Down Expand Up @@ -162,12 +182,12 @@
@include scss.block-components("social-sign-on-dividerWithText-before");
@include scss.block-properties("social-sign-on-dividerWithText-before");
}

&::after{
@include scss.block-components("social-sign-on-dividerWithText-after");
@include scss.block-properties("social-sign-on-dividerWithText-after");
}

@include scss.block-components("social-sign-on-dividerWithText");
@include scss.block-properties("social-sign-on-dividerWithText");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const HeadlinedSubmitForm = ({
</HeadingSection>
<form aria-label={headline} onSubmit={handleSubmit} ref={formRef}>
{children}
<Button size="medium" variant="primary" fullWidth type="submit">
<Button size="large" variant="primary" fullWidth type="submit">
{buttonLabel}
</Button>
{formErrorText ? (
Expand Down
29 changes: 27 additions & 2 deletions blocks/identity-block/features/login-links/default.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@ import React from "react";
import PropTypes from "@arc-fusion/prop-types";
import { useFusionContext } from "fusion:context";
import getTranslatedPhrases from "fusion:intl";
import { Link, Paragraph, Stack } from "@wpmedia/arc-themes-components";
import { Link, Stack, Button, Paragraph } from "@wpmedia/arc-themes-components";

const BLOCK_CLASS_NAME = "b-login-links";
const defaultLoginURL = "/account/login/";
const defaultForgotURL = "/account/forgot-password/";
const defaultSignUpURL = "/account/signup/";
const defaultRequestOneTimePasswordURL = "/account/request-one-time/";

const LoginLinks = ({ customFields }) => {
const {
showLogin = false,
showLoginWithoutPassword = false,
loginURL = defaultLoginURL,
loginWithOutPasswordUrl = defaultRequestOneTimePasswordURL,
showForgot = false,
forgotURL = defaultForgotURL,
showSignUp = false,
Expand All @@ -22,12 +25,24 @@ const LoginLinks = ({ customFields }) => {
const { locale } = siteProperties;
const phrases = getTranslatedPhrases(locale);

if (!showLogin && !showForgot && !showSignUp) {
const logInLinks = [showLogin, showForgot, showSignUp, showLoginWithoutPassword];

if (!logInLinks.includes(true)) {
return null;
}

return (
<Stack as="div" className={BLOCK_CLASS_NAME}>
{showLoginWithoutPassword && (
<Button
href={loginWithOutPasswordUrl}
className={`${BLOCK_CLASS_NAME}__ota-link`}
size="large"
fullWidth
>
{phrases.t("identity-block.ota-button")}
</Button>
)}
{showLogin ? (
<Paragraph>{phrases.t("identity-block.login-links-login")}<Link href={loginURL} className={`${BLOCK_CLASS_NAME}__inner-link`}>{phrases.t("identity-block.log-in")}</Link></Paragraph>
) : null}
Expand Down Expand Up @@ -57,6 +72,16 @@ LoginLinks.propTypes = {
defaultValue: defaultLoginURL,
group: "Login",
}),
showLoginWithoutPassword: PropTypes.bool.tag({
name: "Show Login without password",
defaultValue: false,
group: "Login Without Password",
}),
loginWithOutPasswordUrl: PropTypes.string.tag({
name: "Login without password URL",
defaultValue: defaultRequestOneTimePasswordURL,
group: "Login Without Password",
}),
showForgot: PropTypes.bool.tag({
name: "Show Forgot Password link",
defaultValue: false,
Expand Down
16 changes: 16 additions & 0 deletions blocks/identity-block/features/login-links/default.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,20 @@ describe("LoginLinks", () => {
expect(links[1]).toHaveAttribute("href", customFields.forgotURL);
expect(links[2]).toHaveAttribute("href", customFields.signUpURL);
});

it("renders one-time password", () => {
const customFields = {
showLogin: true,
showLoginWithoutPassword: true,
showSignUp: false,
showForgot: false,
loginURL: "custom-login",
forgotURL: "custom-forgot",
signUpURL: "custom-signup",
};
render(<LoginLinks customFields={customFields} />);
const links = screen.getAllByRole("link");

expect(links[0]).toHaveAttribute("href", customFields.loginWithOutPasswordUrl);
});
});
106 changes: 106 additions & 0 deletions blocks/identity-block/features/one-time-password/default.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, { useState } from "react";
import { useFusionContext } from "fusion:context";
import getProperties from "fusion:properties";
import getTranslatedPhrases from "fusion:intl";
import { Input, useIdentity, BotChallengeProtection, Heading, HeadingSection } from "@wpmedia/arc-themes-components";
import HeadlinedSubmitForm from "../../components/headlined-submit-form";

const BLOCK_CLASS_NAME = "b-one-time-login-form";

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 definedMessageByCode = (code) => errorCodes[code] || errorCodes["0"];

const OneTimePasswordLogin = () => {
const { arcSite } = useFusionContext();
const { locale } = getProperties(arcSite);
const phrases = getTranslatedPhrases(locale);
const { Identity, isInitialized } = useIdentity();
const [captchaToken, setCaptchaToken] = useState();
const [resetRecaptcha, setResetRecaptcha] = useState(true);
const [error, setError] = useState();
const [captchaError, setCaptchaError] = useState();
const [success, setSuccess] = useState(false);
const [userEmail, setUserEmail] = useState('');

if (!isInitialized) {
return null;
}

if (success) {
return (
<div className={BLOCK_CLASS_NAME}>
<HeadingSection>
<Heading>{phrases.t("identity-block.ota-success-heading")}</Heading>
</HeadingSection>
<p
className={`${BLOCK_CLASS_NAME}__ota-sub-headline`}
dangerouslySetInnerHTML={{__html: phrases.t("identity-block.ota-success-body", { userEmail })}}
/>
</div>
)
}

return (
<div>
<HeadlinedSubmitForm
buttonLabel={phrases.t("identity-block.ota-form-button")}
className={BLOCK_CLASS_NAME}
formErrorText={error}
headline={phrases.t("identity-block.ota-headline")}
onSubmit={({ email }) => {
setError(null);
setCaptchaError(null);
return Identity.requestOTALink(
email,
captchaToken,
).then(() => {
setUserEmail(email);
setSuccess(true);
})
.catch((e) => {
setResetRecaptcha(!resetRecaptcha);
if (e?.code === "130001") {
setCaptchaError(phrases.t(definedMessageByCode(e.code)));
} else {
setError(phrases.t(definedMessageByCode(e.code)));
}
});
}}
>
<p className={`${BLOCK_CLASS_NAME}__ota-sub-headline`}>{phrases.t("identity-block.ota-subheadline")}</p>
<Input
autoComplete="email"
label={phrases.t("identity-block.email-label")}
name="email"
placeholder={phrases.t("identity-block.ota-input-placeholder")}
required
showDefaultError={false}
type="email"
validationErrorMessage={phrases.t("identity-block.email-requirements")}
/>

<div className={`${BLOCK_CLASS_NAME}__recaptcha`}>
<BotChallengeProtection
className={BLOCK_CLASS_NAME}
challengeIn="magicLink"
setCaptchaToken={setCaptchaToken}
captchaError={captchaError}
setCaptchaError={setCaptchaError}
resetRecaptcha={resetRecaptcha}
/>
</div>
</HeadlinedSubmitForm>
</div>
);
};

OneTimePasswordLogin.label = "Identity One Time Password Request Form - Arc Block";

export default OneTimePasswordLogin;
126 changes: 126 additions & 0 deletions blocks/identity-block/features/one-time-password/default.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import React from "react";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { useIdentity } from "@wpmedia/arc-themes-components";
import OneTimePassword from "./default";

const mockSubmitForm = jest.fn(() => Promise.resolve());
const mockIdentity = {
requestOTALink: mockSubmitForm,
};

jest.mock("@wpmedia/arc-themes-components", () => ({
...jest.requireActual("@wpmedia/arc-themes-components"),
useIdentity: jest.fn(() => ({
isInitialized: false,
Identity: {
...mockIdentity,
},
})),
BotChallengeProtection: ({ challengeIn= 'magicLink' }) => <div data-testid={`reCapctha-${challengeIn}`} />
}));
jest.mock("fusion:properties", () => jest.fn(() => ({})));

describe("Identity One Time Password Request Form - Arc Block", () => {
beforeEach(() => {
useIdentity.mockImplementation(() => ({
isInitialized: true,
Identity: {
...mockIdentity,
},
}));
});

afterEach(() => {
jest.clearAllMocks();
});

it("Renders", () => {
useIdentity.mockImplementation(() => ({
isInitialized: false,
Identity: {
...mockIdentity,
},
}));

render(<OneTimePassword />);

expect(screen.queryAllByRole("form").length).toEqual(0);
});

it("Does not render if Identity isn't initialized", () => {
render(<OneTimePassword />);

expect(screen.queryAllByRole("form").length).toEqual(1);
expect(screen.getByTestId('reCapctha-magicLink')).toBeTruthy();
});

it("Should be able submit form", async () => {
render(<OneTimePassword />);

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

await waitFor(() => expect(screen.getByRole("button")));
fireEvent.click(screen.getByRole("button"));

await waitFor(() => expect(mockSubmitForm).toHaveBeenCalled());
await waitFor(() => expect(screen.getByText("identity-block.ota-success-heading")));
await waitFor(() => expect(screen.getByText("identity-block.ota-success-body")));
});

it("Form submission handles 130001 error", async () => {
const error = new Error("Captcha token invalid");
error.code = "130001";

const errorMessage = jest.fn(() => Promise.reject(error));

useIdentity.mockImplementation(() => ({
isInitialized: true,
Identity: {
...mockIdentity,
requestOTALink: errorMessage,
},
}));

render(<OneTimePassword />);

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

await waitFor(() => expect(screen.getByRole("button")));
fireEvent.click(screen.getByRole("button"));

await waitFor(() => expect(errorMessage).toHaveBeenCalled());
});

it("Form submission handle other errors", async () => {
const error = new Error("Fake error");
error.code = "30001";

const errorMessage = jest.fn(() => Promise.reject(error));

useIdentity.mockImplementation(() => ({
isInitialized: true,
Identity: {
...mockIdentity,
requestOTALink: errorMessage,
},
}));

render(<OneTimePassword />);

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

await waitFor(() => expect(screen.getByRole("button")));
fireEvent.click(screen.getByRole("button"));

await waitFor(() => expect(errorMessage).toHaveBeenCalled());
});
});
Loading

0 comments on commit 1940170

Please sign in to comment.