diff --git a/res/css/views/auth/_AuthBody.pcss b/res/css/views/auth/_AuthBody.pcss index 824f6411dfd4..d59c1fe25b8d 100644 --- a/res/css/views/auth/_AuthBody.pcss +++ b/res/css/views/auth/_AuthBody.pcss @@ -137,15 +137,50 @@ limitations under the License. } /* specialisation for password reset views */ -.mx_AuthBody_forgot-password { +.mx_AuthBody.mx_AuthBody_forgot-password { font-size: $font-14px; color: $primary-content; padding: 50px 32px; min-height: 600px; h1 { - margin-bottom: $spacing-20; - margin-top: $spacing-24; + margin: $spacing-24 0; + } + + .mx_AuthBody_button-container { + display: flex; + justify-content: center; + } + + .mx_Login_submit { + font-weight: $font-semi-bold; + margin: 0 0 $spacing-16; + } + + .mx_AuthBody_text { + margin-bottom: $spacing-32; + + p { + margin: 0 0 8px; + } + } + + .mx_AuthBody_sign-in-instead-button { + font-weight: $font-semi-bold; + padding: $spacing-4; + } + + .mx_AuthBody_fieldRow { + margin-bottom: $spacing-24; + } + + .mx_AccessibleButton.mx_AccessibleButton_hasKind { + background: none; + + &:disabled { + cursor: default; + opacity: .4; + } } } @@ -154,12 +189,6 @@ limitations under the License. color: $secondary-content; display: flex; gap: $spacing-8; - margin-bottom: 10px; - margin-top: $spacing-24; -} - -.mx_AuthBody_did-not-receive--centered { - justify-content: center; } .mx_AuthBody_resend-button { diff --git a/res/css/views/dialogs/_VerifyEMailDialog.pcss b/res/css/views/dialogs/_VerifyEMailDialog.pcss index fa36f0e114f0..3d92f08babe0 100644 --- a/res/css/views/dialogs/_VerifyEMailDialog.pcss +++ b/res/css/views/dialogs/_VerifyEMailDialog.pcss @@ -21,7 +21,7 @@ limitations under the License. .mx_Dialog { color: $primary-content; font-size: 14px; - padding: 16px; + padding: $spacing-24 $spacing-24 $spacing-16; text-align: center; width: 485px; @@ -34,5 +34,14 @@ limitations under the License. color: $secondary-content; line-height: 20px; } + + .mx_AuthBody_did-not-receive { + justify-content: center; + margin-bottom: 8px; + } + + .mx_Dialog_cancelButton { + right: 10px; + } } } diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index fe246aabf7e6..c338b4f029f7 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -277,15 +277,28 @@ export default class ForgotPassword extends React.Component { { email: this.state.email, errorText: this.state.errorText, + onCloseClick: () => () => { + modal.close(); + this.setState({ phase: Phase.PasswordInput }); + }, + onReEnterEmailClick: () => { + modal.close(); + this.setState({ phase: Phase.EnterEmail }); + }, onResendClick: this.sendVerificationMail, }, "mx_VerifyEMailDialog", false, false, { - // this modal cannot be dismissed except reset is done or forced onBeforeClose: async (reason?: string) => { - return this.state.phase === Phase.Done || reason === "force"; + if (reason === "backgroundClick") { + // Modal dismissed by clicking the background. + // Go one phase back. + this.setState({ phase: Phase.PasswordInput }); + } + + return true; }, }, ); @@ -339,6 +352,7 @@ export default class ForgotPassword extends React.Component { homeserver={this.props.serverConfig.hsName} loading={this.state.phase === Phase.SendingEmail} onInputChanged={this.onInputChanged} + onLoginClick={this.props.onLoginClick} onSubmitForm={this.onSubmitForm} />; } @@ -374,6 +388,7 @@ export default class ForgotPassword extends React.Component { return this.setState({ phase: Phase.EnterEmail })} onResendClick={this.sendVerificationMail} onSubmitForm={this.onSubmitForm} />; diff --git a/src/components/structures/auth/forgot-password/CheckEmail.tsx b/src/components/structures/auth/forgot-password/CheckEmail.tsx index 27fa82f25e1d..b1faba936e96 100644 --- a/src/components/structures/auth/forgot-password/CheckEmail.tsx +++ b/src/components/structures/auth/forgot-password/CheckEmail.tsx @@ -27,6 +27,7 @@ import { ErrorMessage } from "../../ErrorMessage"; interface CheckEmailProps { email: string; errorText: string | ReactNode | null; + onReEnterEmailClick: () => void; onResendClick: () => Promise; onSubmitForm: (ev: React.FormEvent) => void; } @@ -37,6 +38,7 @@ interface CheckEmailProps { export const CheckEmail: React.FC = ({ email, errorText, + onReEnterEmailClick, onSubmitForm, onResendClick, }) => { @@ -50,13 +52,32 @@ export const CheckEmail: React.FC = ({ return <>

{ _t("Check your email to continue") }

-

- { _t( - "Follow the instructions sent to %(email)s", - { email: email }, - { b: t => { t } }, - ) } -

+
+

+ { _t( + "Follow the instructions sent to %(email)s", + { email: email }, + { b: t => { t } }, + ) } +

+
+ { _t("Wrong email address?") } + + { _t("Re-enter email address") } + +
+
+ { errorText && } +
{ _t("Did not receive it?") } = ({ />
- { errorText && } - ; }; diff --git a/src/components/structures/auth/forgot-password/EnterEmail.tsx b/src/components/structures/auth/forgot-password/EnterEmail.tsx index a630291ae26b..47db80695bc6 100644 --- a/src/components/structures/auth/forgot-password/EnterEmail.tsx +++ b/src/components/structures/auth/forgot-password/EnterEmail.tsx @@ -22,6 +22,7 @@ import EmailField from "../../../views/auth/EmailField"; import { ErrorMessage } from "../../ErrorMessage"; import Spinner from "../../../views/elements/Spinner"; import Field from "../../../views/elements/Field"; +import AccessibleButton from "../../../views/elements/AccessibleButton"; interface EnterEmailProps { email: string; @@ -29,6 +30,7 @@ interface EnterEmailProps { homeserver: string; loading: boolean; onInputChanged: (stateKey: string, ev: React.FormEvent) => void; + onLoginClick?: () => void; onSubmitForm: (ev: React.FormEvent) => void; } @@ -41,6 +43,7 @@ export const EnterEmail: React.FC = ({ homeserver, loading, onInputChanged, + onLoginClick = () => {}, onSubmitForm, }) => { const submitButtonChild = loading @@ -92,6 +95,15 @@ export const EnterEmail: React.FC = ({ > { submitButtonChild } +
+ + { _t("Sign in instead") } + +
; diff --git a/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx b/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx index d63e4c97d79c..41bdb7a05181 100644 --- a/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx +++ b/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx @@ -27,12 +27,16 @@ import { ErrorMessage } from "../../ErrorMessage"; interface Props { email: string; errorText: string | null; + onCloseClick: () => void; + onReEnterEmailClick: () => void; onResendClick: () => Promise; } export const VerifyEmailModal: React.FC = ({ email, errorText, + onCloseClick, + onReEnterEmailClick, onResendClick, }) => { const { toggle: toggleTooltipVisible, value: tooltipVisible } = useTimeoutToggle(false, 2500); @@ -57,7 +61,8 @@ export const VerifyEmailModal: React.FC = ({ }, ) }

-
+ +
{ _t("Did not receive it?") } = ({ { errorText && }
+ +
+ { _t("Wrong email address?") } + + { _t("Re-enter email address") } + +
+ + ; }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4ee870bd72d6..5b3ad0cb29de 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3484,6 +3484,8 @@ "Clear personal data": "Clear personal data", "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.", "Follow the instructions sent to %(email)s": "Follow the instructions sent to %(email)s", + "Wrong email address?": "Wrong email address?", + "Re-enter email address": "Re-enter email address", "Did not receive it?": "Did not receive it?", "Verification link email resent!": "Verification link email resent!", "Send email": "Send email", @@ -3491,6 +3493,7 @@ "%(homeserver)s will send you a verification link to let you reset your password.": "%(homeserver)s will send you a verification link to let you reset your password.", "The email address linked to your account must be entered.": "The email address linked to your account must be entered.", "The email address doesn't appear to be valid.": "The email address doesn't appear to be valid.", + "Sign in instead": "Sign in instead", "Verify your email to continue": "Verify your email to continue", "We need to know it’s you before resetting your password.\n Click the link in the email we just sent to %(email)s": "We need to know it’s you before resetting your password.\n Click the link in the email we just sent to %(email)s", "Commands": "Commands", diff --git a/test/components/structures/auth/ForgotPassword-test.tsx b/test/components/structures/auth/ForgotPassword-test.tsx index 9f4b192aa9b3..e3c2291847c3 100644 --- a/test/components/structures/auth/ForgotPassword-test.tsx +++ b/test/components/structures/auth/ForgotPassword-test.tsx @@ -38,6 +38,7 @@ describe("", () => { let client: MatrixClient; let serverConfig: ValidatedServerConfig; let onComplete: () => void; + let onLoginClick: () => void; let renderResult: RenderResult; let restoreConsole: () => void; @@ -49,9 +50,9 @@ describe("", () => { }); }; - const submitForm = async (submitLabel: string): Promise => { + const clickButton = async (label: string): Promise => { await act(async () => { - await userEvent.click(screen.getByText(submitLabel), { delay: null }); + await userEvent.click(screen.getByText(label), { delay: null }); }); }; @@ -70,6 +71,7 @@ describe("", () => { serverConfig.hsName = "example.com"; onComplete = jest.fn(); + onLoginClick = jest.fn(); jest.spyOn(AutoDiscoveryUtils, "validateServerConfigWithStaticUrls").mockResolvedValue(serverConfig); jest.spyOn(AutoDiscoveryUtils, "authComponentStateForError"); @@ -94,6 +96,7 @@ describe("", () => { renderResult = render(); }); @@ -108,6 +111,7 @@ describe("", () => { renderResult.rerender(); }); @@ -116,6 +120,16 @@ describe("", () => { }); }); + describe("when clicking »Sign in instead«", () => { + beforeEach(async () => { + await clickButton("Sign in instead"); + }); + + it("should call onLoginClick()", () => { + expect(onLoginClick).toHaveBeenCalled(); + }); + }); + describe("when entering a non-email value", () => { beforeEach(async () => { await typeIntoField("Email address", "not en email"); @@ -132,7 +146,7 @@ describe("", () => { mocked(client).requestPasswordEmailToken.mockRejectedValue({ errcode: "M_THREEPID_NOT_FOUND", }); - await submitForm("Send email"); + await clickButton("Send email"); }); it("should show an email not found message", () => { @@ -146,7 +160,7 @@ describe("", () => { mocked(client).requestPasswordEmailToken.mockRejectedValue({ name: "ConnectionError", }); - await submitForm("Send email"); + await clickButton("Send email"); }); it("should show an info about that", () => { @@ -166,7 +180,7 @@ describe("", () => { serverIsAlive: false, serverDeadError: "server down", }); - await submitForm("Send email"); + await clickButton("Send email"); }); it("should show the server error", () => { @@ -180,7 +194,7 @@ describe("", () => { mocked(client).requestPasswordEmailToken.mockResolvedValue({ sid: testSid, }); - await submitForm("Send email"); + await clickButton("Send email"); }); it("should send the mail and show the check email view", () => { @@ -193,6 +207,16 @@ describe("", () => { expect(screen.getByText(testEmail)).toBeInTheDocument(); }); + describe("when clicking re-enter email", () => { + beforeEach(async () => { + await clickButton("Re-enter email address"); + }); + + it("go back to the email input", () => { + expect(screen.queryByText("Enter your email to reset password")).toBeInTheDocument(); + }); + }); + describe("when clicking resend email", () => { beforeEach(async () => { await userEvent.click(screen.getByText("Resend"), { delay: null }); @@ -212,7 +236,7 @@ describe("", () => { describe("when clicking next", () => { beforeEach(async () => { - await submitForm("Next"); + await clickButton("Next"); }); it("should show the password input view", () => { @@ -246,7 +270,7 @@ describe("", () => { retry_after_ms: (13 * 60 + 37) * 1000, }, }); - await submitForm("Reset password"); + await clickButton("Reset password"); }); it("should show the rate limit error message", () => { @@ -258,7 +282,7 @@ describe("", () => { describe("and submitting it", () => { beforeEach(async () => { - await submitForm("Reset password"); + await clickButton("Reset password"); // double flush promises for the modal to appear await flushPromisesWithFakeTimers(); await flushPromisesWithFakeTimers(); @@ -284,6 +308,20 @@ describe("", () => { expect(screen.getByText(testEmail)).toBeInTheDocument(); }); + describe("when clicking re-enter email", () => { + beforeEach(async () => { + await clickButton("Re-enter email address"); + // double flush promises for the modal to disappear + await flushPromisesWithFakeTimers(); + await flushPromisesWithFakeTimers(); + }); + + it("should close the dialog and go back to the email input", () => { + expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument(); + expect(screen.queryByText("Enter your email to reset password")).toBeInTheDocument(); + }); + }); + describe("when validating the link from the mail", () => { beforeEach(async () => { mocked(client.setPassword).mockResolvedValue({});