Further password reset flow enhancements (#9662)
This commit is contained in:
parent
82ad8d5aa2
commit
89439d4f10
10 changed files with 241 additions and 67 deletions
|
@ -137,15 +137,50 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
/* specialisation for password reset views */
|
/* specialisation for password reset views */
|
||||||
.mx_AuthBody_forgot-password {
|
.mx_AuthBody.mx_AuthBody_forgot-password {
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
color: $primary-content;
|
color: $primary-content;
|
||||||
padding: 50px 32px;
|
padding: 50px 32px;
|
||||||
min-height: 600px;
|
min-height: 600px;
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin-bottom: $spacing-20;
|
margin: $spacing-24 0;
|
||||||
margin-top: $spacing-24;
|
}
|
||||||
|
|
||||||
|
.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 $spacing-8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
color: $secondary-content;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: $spacing-8;
|
gap: $spacing-8;
|
||||||
margin-bottom: 10px;
|
|
||||||
margin-top: $spacing-24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AuthBody_did-not-receive--centered {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AuthBody_resend-button {
|
.mx_AuthBody_resend-button {
|
||||||
|
@ -168,7 +197,7 @@ limitations under the License.
|
||||||
color: $accent;
|
color: $accent;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: $spacing-4;
|
gap: $spacing-4;
|
||||||
padding: 4px;
|
padding: $spacing-4;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $system;
|
background-color: $system;
|
||||||
|
@ -209,7 +238,7 @@ limitations under the License.
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
.mx_AuthBody_paddedFooter_title {
|
.mx_AuthBody_paddedFooter_title {
|
||||||
margin-top: 16px;
|
margin-top: $spacing-16;
|
||||||
font-size: $font-15px;
|
font-size: $font-15px;
|
||||||
line-height: $font-24px;
|
line-height: $font-24px;
|
||||||
|
|
||||||
|
@ -220,7 +249,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AuthBody_paddedFooter_subtitle {
|
.mx_AuthBody_paddedFooter_subtitle {
|
||||||
margin-top: 8px;
|
margin-top: $spacing-8;
|
||||||
font-size: $font-10px;
|
font-size: $font-10px;
|
||||||
line-height: $font-14px;
|
line-height: $font-14px;
|
||||||
}
|
}
|
||||||
|
@ -236,7 +265,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SSOButtons + .mx_AuthBody_changeFlow {
|
.mx_SSOButtons + .mx_AuthBody_changeFlow {
|
||||||
margin-top: 24px;
|
margin-top: $spacing-24;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AuthBody_spinner {
|
.mx_AuthBody_spinner {
|
||||||
|
|
|
@ -20,8 +20,8 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_Dialog {
|
.mx_Dialog {
|
||||||
color: $primary-content;
|
color: $primary-content;
|
||||||
font-size: 14px;
|
font-size: $font-14px;
|
||||||
padding: 16px;
|
padding: $spacing-24 $spacing-24 $spacing-16;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 485px;
|
width: 485px;
|
||||||
|
|
||||||
|
@ -34,5 +34,14 @@ limitations under the License.
|
||||||
color: $secondary-content;
|
color: $secondary-content;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_AuthBody_did-not-receive {
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: $spacing-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Dialog_cancelButton {
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -347,7 +347,11 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
||||||
<div className="mx_Dialog">
|
<div className="mx_Dialog">
|
||||||
{ this.staticModal.elem }
|
{ this.staticModal.elem }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_background mx_Dialog_staticBackground" onClick={this.onBackgroundClick} />
|
<div
|
||||||
|
data-testid="dialog-background"
|
||||||
|
className="mx_Dialog_background mx_Dialog_staticBackground"
|
||||||
|
onClick={this.onBackgroundClick}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -368,7 +372,11 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
||||||
<div className="mx_Dialog">
|
<div className="mx_Dialog">
|
||||||
{ modal.elem }
|
{ modal.elem }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_background" onClick={this.onBackgroundClick} />
|
<div
|
||||||
|
data-testid="dialog-background"
|
||||||
|
className="mx_Dialog_background"
|
||||||
|
onClick={this.onBackgroundClick}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,6 @@ import { createClient, IRequestTokenResponse, MatrixClient } from 'matrix-js-sdk
|
||||||
|
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
const CHECK_EMAIL_VERIFIED_POLL_INTERVAL = 2000;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows a user to reset their password on a homeserver.
|
* Allows a user to reset their password on a homeserver.
|
||||||
*
|
*
|
||||||
|
@ -108,24 +106,6 @@ export default class PasswordReset {
|
||||||
await this.checkEmailLinkClicked();
|
await this.checkEmailLinkClicked();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async retrySetNewPassword(password: string): Promise<void> {
|
|
||||||
this.password = password;
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.tryCheckEmailLinkClicked(resolve);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private tryCheckEmailLinkClicked(resolve: Function): void {
|
|
||||||
this.checkEmailLinkClicked()
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(() => {
|
|
||||||
window.setTimeout(
|
|
||||||
() => this.tryCheckEmailLinkClicked(resolve),
|
|
||||||
CHECK_EMAIL_VERIFIED_POLL_INTERVAL,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the email link has been clicked by attempting to change the password
|
* Checks if the email link has been clicked by attempting to change the password
|
||||||
* for the mxid linked to the email.
|
* for the mxid linked to the email.
|
||||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { logger } from 'matrix-js-sdk/src/logger';
|
import { logger } from 'matrix-js-sdk/src/logger';
|
||||||
import { createClient } from "matrix-js-sdk/src/matrix";
|
import { createClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { sleep } from 'matrix-js-sdk/src/utils';
|
||||||
|
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
|
@ -43,6 +44,8 @@ import Spinner from '../../views/elements/Spinner';
|
||||||
import { formatSeconds } from '../../../DateUtils';
|
import { formatSeconds } from '../../../DateUtils';
|
||||||
import AutoDiscoveryUtils from '../../../utils/AutoDiscoveryUtils';
|
import AutoDiscoveryUtils from '../../../utils/AutoDiscoveryUtils';
|
||||||
|
|
||||||
|
const emailCheckInterval = 2000;
|
||||||
|
|
||||||
enum Phase {
|
enum Phase {
|
||||||
// Show email input
|
// Show email input
|
||||||
EnterEmail = 1,
|
EnterEmail = 1,
|
||||||
|
@ -60,7 +63,7 @@ enum Phase {
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serverConfig: ValidatedServerConfig;
|
serverConfig: ValidatedServerConfig;
|
||||||
onLoginClick?: () => void;
|
onLoginClick: () => void;
|
||||||
onComplete: () => void;
|
onComplete: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,22 +280,43 @@ export default class ForgotPassword extends React.Component<Props, State> {
|
||||||
{
|
{
|
||||||
email: this.state.email,
|
email: this.state.email,
|
||||||
errorText: this.state.errorText,
|
errorText: this.state.errorText,
|
||||||
|
onCloseClick: () => {
|
||||||
|
modal.close();
|
||||||
|
this.setState({ phase: Phase.PasswordInput });
|
||||||
|
},
|
||||||
|
onReEnterEmailClick: () => {
|
||||||
|
modal.close();
|
||||||
|
this.setState({ phase: Phase.EnterEmail });
|
||||||
|
},
|
||||||
onResendClick: this.sendVerificationMail,
|
onResendClick: this.sendVerificationMail,
|
||||||
},
|
},
|
||||||
"mx_VerifyEMailDialog",
|
"mx_VerifyEMailDialog",
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
{
|
{
|
||||||
// this modal cannot be dismissed except reset is done or forced
|
|
||||||
onBeforeClose: async (reason?: string) => {
|
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;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.reset.retrySetNewPassword(this.state.password);
|
// Don't retry if the phase changed. For example when going back to email input.
|
||||||
this.phase = Phase.Done;
|
while (this.state.phase === Phase.ResettingPassword) {
|
||||||
|
try {
|
||||||
|
await this.reset.setNewPassword(this.state.password);
|
||||||
|
this.setState({ phase: Phase.Done });
|
||||||
modal.close();
|
modal.close();
|
||||||
|
} catch (e) {
|
||||||
|
// Email not confirmed, yet. Retry after a while.
|
||||||
|
await sleep(emailCheckInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSubmitForm = async (ev: React.FormEvent): Promise<void> => {
|
private onSubmitForm = async (ev: React.FormEvent): Promise<void> => {
|
||||||
|
@ -339,6 +363,7 @@ export default class ForgotPassword extends React.Component<Props, State> {
|
||||||
homeserver={this.props.serverConfig.hsName}
|
homeserver={this.props.serverConfig.hsName}
|
||||||
loading={this.state.phase === Phase.SendingEmail}
|
loading={this.state.phase === Phase.SendingEmail}
|
||||||
onInputChanged={this.onInputChanged}
|
onInputChanged={this.onInputChanged}
|
||||||
|
onLoginClick={this.props.onLoginClick!} // set by default props
|
||||||
onSubmitForm={this.onSubmitForm}
|
onSubmitForm={this.onSubmitForm}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
@ -374,6 +399,7 @@ export default class ForgotPassword extends React.Component<Props, State> {
|
||||||
return <CheckEmail
|
return <CheckEmail
|
||||||
email={this.state.email}
|
email={this.state.email}
|
||||||
errorText={this.state.errorText}
|
errorText={this.state.errorText}
|
||||||
|
onReEnterEmailClick={() => this.setState({ phase: Phase.EnterEmail })}
|
||||||
onResendClick={this.sendVerificationMail}
|
onResendClick={this.sendVerificationMail}
|
||||||
onSubmitForm={this.onSubmitForm}
|
onSubmitForm={this.onSubmitForm}
|
||||||
/>;
|
/>;
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { ErrorMessage } from "../../ErrorMessage";
|
||||||
interface CheckEmailProps {
|
interface CheckEmailProps {
|
||||||
email: string;
|
email: string;
|
||||||
errorText: string | ReactNode | null;
|
errorText: string | ReactNode | null;
|
||||||
|
onReEnterEmailClick: () => void;
|
||||||
onResendClick: () => Promise<boolean>;
|
onResendClick: () => Promise<boolean>;
|
||||||
onSubmitForm: (ev: React.FormEvent) => void;
|
onSubmitForm: (ev: React.FormEvent) => void;
|
||||||
}
|
}
|
||||||
|
@ -37,6 +38,7 @@ interface CheckEmailProps {
|
||||||
export const CheckEmail: React.FC<CheckEmailProps> = ({
|
export const CheckEmail: React.FC<CheckEmailProps> = ({
|
||||||
email,
|
email,
|
||||||
errorText,
|
errorText,
|
||||||
|
onReEnterEmailClick,
|
||||||
onSubmitForm,
|
onSubmitForm,
|
||||||
onResendClick,
|
onResendClick,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -50,6 +52,7 @@ export const CheckEmail: React.FC<CheckEmailProps> = ({
|
||||||
return <>
|
return <>
|
||||||
<EMailPromptIcon className="mx_AuthBody_emailPromptIcon--shifted" />
|
<EMailPromptIcon className="mx_AuthBody_emailPromptIcon--shifted" />
|
||||||
<h1>{ _t("Check your email to continue") }</h1>
|
<h1>{ _t("Check your email to continue") }</h1>
|
||||||
|
<div className="mx_AuthBody_text">
|
||||||
<p>
|
<p>
|
||||||
{ _t(
|
{ _t(
|
||||||
"Follow the instructions sent to <b>%(email)s</b>",
|
"Follow the instructions sent to <b>%(email)s</b>",
|
||||||
|
@ -57,6 +60,24 @@ export const CheckEmail: React.FC<CheckEmailProps> = ({
|
||||||
{ b: t => <b>{ t }</b> },
|
{ b: t => <b>{ t }</b> },
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
|
<div className="mx_AuthBody_did-not-receive">
|
||||||
|
<span className="mx_VerifyEMailDialog_text-light">{ _t("Wrong email address?") }</span>
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_AuthBody_resend-button"
|
||||||
|
kind="link"
|
||||||
|
onClick={onReEnterEmailClick}
|
||||||
|
>
|
||||||
|
{ _t("Re-enter email address") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{ errorText && <ErrorMessage message={errorText} /> }
|
||||||
|
<input
|
||||||
|
onClick={onSubmitForm}
|
||||||
|
type="button"
|
||||||
|
className="mx_Login_submit"
|
||||||
|
value={_t("Next")}
|
||||||
|
/>
|
||||||
<div className="mx_AuthBody_did-not-receive">
|
<div className="mx_AuthBody_did-not-receive">
|
||||||
<span className="mx_VerifyEMailDialog_text-light">{ _t("Did not receive it?") }</span>
|
<span className="mx_VerifyEMailDialog_text-light">{ _t("Did not receive it?") }</span>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
|
@ -73,12 +94,5 @@ export const CheckEmail: React.FC<CheckEmailProps> = ({
|
||||||
/>
|
/>
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
{ errorText && <ErrorMessage message={errorText} /> }
|
|
||||||
<input
|
|
||||||
onClick={onSubmitForm}
|
|
||||||
type="button"
|
|
||||||
className="mx_Login_submit"
|
|
||||||
value={_t("Next")}
|
|
||||||
/>
|
|
||||||
</>;
|
</>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,6 +22,7 @@ import EmailField from "../../../views/auth/EmailField";
|
||||||
import { ErrorMessage } from "../../ErrorMessage";
|
import { ErrorMessage } from "../../ErrorMessage";
|
||||||
import Spinner from "../../../views/elements/Spinner";
|
import Spinner from "../../../views/elements/Spinner";
|
||||||
import Field from "../../../views/elements/Field";
|
import Field from "../../../views/elements/Field";
|
||||||
|
import AccessibleButton from "../../../views/elements/AccessibleButton";
|
||||||
|
|
||||||
interface EnterEmailProps {
|
interface EnterEmailProps {
|
||||||
email: string;
|
email: string;
|
||||||
|
@ -29,6 +30,7 @@ interface EnterEmailProps {
|
||||||
homeserver: string;
|
homeserver: string;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
onInputChanged: (stateKey: string, ev: React.FormEvent<HTMLInputElement>) => void;
|
onInputChanged: (stateKey: string, ev: React.FormEvent<HTMLInputElement>) => void;
|
||||||
|
onLoginClick: () => void;
|
||||||
onSubmitForm: (ev: React.FormEvent) => void;
|
onSubmitForm: (ev: React.FormEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +43,7 @@ export const EnterEmail: React.FC<EnterEmailProps> = ({
|
||||||
homeserver,
|
homeserver,
|
||||||
loading,
|
loading,
|
||||||
onInputChanged,
|
onInputChanged,
|
||||||
|
onLoginClick,
|
||||||
onSubmitForm,
|
onSubmitForm,
|
||||||
}) => {
|
}) => {
|
||||||
const submitButtonChild = loading
|
const submitButtonChild = loading
|
||||||
|
@ -92,6 +95,15 @@ export const EnterEmail: React.FC<EnterEmailProps> = ({
|
||||||
>
|
>
|
||||||
{ submitButtonChild }
|
{ submitButtonChild }
|
||||||
</button>
|
</button>
|
||||||
|
<div className="mx_AuthBody_button-container">
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_AuthBody_sign-in-instead-button"
|
||||||
|
element="button"
|
||||||
|
kind="link"
|
||||||
|
onClick={onLoginClick}>
|
||||||
|
{ _t("Sign in instead") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
</>;
|
</>;
|
||||||
|
|
|
@ -27,12 +27,16 @@ import { ErrorMessage } from "../../ErrorMessage";
|
||||||
interface Props {
|
interface Props {
|
||||||
email: string;
|
email: string;
|
||||||
errorText: string | null;
|
errorText: string | null;
|
||||||
|
onCloseClick: () => void;
|
||||||
|
onReEnterEmailClick: () => void;
|
||||||
onResendClick: () => Promise<boolean>;
|
onResendClick: () => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VerifyEmailModal: React.FC<Props> = ({
|
export const VerifyEmailModal: React.FC<Props> = ({
|
||||||
email,
|
email,
|
||||||
errorText,
|
errorText,
|
||||||
|
onCloseClick,
|
||||||
|
onReEnterEmailClick,
|
||||||
onResendClick,
|
onResendClick,
|
||||||
}) => {
|
}) => {
|
||||||
const { toggle: toggleTooltipVisible, value: tooltipVisible } = useTimeoutToggle(false, 2500);
|
const { toggle: toggleTooltipVisible, value: tooltipVisible } = useTimeoutToggle(false, 2500);
|
||||||
|
@ -57,7 +61,8 @@ export const VerifyEmailModal: React.FC<Props> = ({
|
||||||
},
|
},
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
<div className="mx_AuthBody_did-not-receive mx_AuthBody_did-not-receive--centered">
|
|
||||||
|
<div className="mx_AuthBody_did-not-receive">
|
||||||
<span className="mx_VerifyEMailDialog_text-light">{ _t("Did not receive it?") }</span>
|
<span className="mx_VerifyEMailDialog_text-light">{ _t("Did not receive it?") }</span>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className="mx_AuthBody_resend-button"
|
className="mx_AuthBody_resend-button"
|
||||||
|
@ -74,5 +79,22 @@ export const VerifyEmailModal: React.FC<Props> = ({
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
{ errorText && <ErrorMessage message={errorText} /> }
|
{ errorText && <ErrorMessage message={errorText} /> }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mx_AuthBody_did-not-receive">
|
||||||
|
<span className="mx_VerifyEMailDialog_text-light">{ _t("Wrong email address?") }</span>
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_AuthBody_resend-button"
|
||||||
|
kind="link"
|
||||||
|
onClick={onReEnterEmailClick}
|
||||||
|
>
|
||||||
|
{ _t("Re-enter email address") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AccessibleButton
|
||||||
|
onClick={onCloseClick}
|
||||||
|
className="mx_Dialog_cancelButton"
|
||||||
|
aria-label={_t("Close dialog")}
|
||||||
|
/>
|
||||||
</>;
|
</>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -3496,6 +3496,8 @@
|
||||||
"Clear personal data": "Clear personal data",
|
"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.",
|
"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 <b>%(email)s</b>": "Follow the instructions sent to <b>%(email)s</b>",
|
"Follow the instructions sent to <b>%(email)s</b>": "Follow the instructions sent to <b>%(email)s</b>",
|
||||||
|
"Wrong email address?": "Wrong email address?",
|
||||||
|
"Re-enter email address": "Re-enter email address",
|
||||||
"Did not receive it?": "Did not receive it?",
|
"Did not receive it?": "Did not receive it?",
|
||||||
"Verification link email resent!": "Verification link email resent!",
|
"Verification link email resent!": "Verification link email resent!",
|
||||||
"Send email": "Send email",
|
"Send email": "Send email",
|
||||||
|
@ -3503,6 +3505,7 @@
|
||||||
"<b>%(homeserver)s</b> will send you a verification link to let you reset your password.": "<b>%(homeserver)s</b> will send you a verification link to let you reset your password.",
|
"<b>%(homeserver)s</b> will send you a verification link to let you reset your password.": "<b>%(homeserver)s</b> 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 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.",
|
"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",
|
"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 <b>%(email)s</b>": "We need to know it’s you before resetting your password.\n Click the link in the email we just sent to <b>%(email)s</b>",
|
"We need to know it’s you before resetting your password.\n Click the link in the email we just sent to <b>%(email)s</b>": "We need to know it’s you before resetting your password.\n Click the link in the email we just sent to <b>%(email)s</b>",
|
||||||
"Commands": "Commands",
|
"Commands": "Commands",
|
||||||
|
|
|
@ -38,6 +38,7 @@ describe("<ForgotPassword>", () => {
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
let serverConfig: ValidatedServerConfig;
|
let serverConfig: ValidatedServerConfig;
|
||||||
let onComplete: () => void;
|
let onComplete: () => void;
|
||||||
|
let onLoginClick: () => void;
|
||||||
let renderResult: RenderResult;
|
let renderResult: RenderResult;
|
||||||
let restoreConsole: () => void;
|
let restoreConsole: () => void;
|
||||||
|
|
||||||
|
@ -49,9 +50,16 @@ describe("<ForgotPassword>", () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitForm = async (submitLabel: string): Promise<void> => {
|
const clickButton = async (label: string): Promise<void> => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await userEvent.click(screen.getByText(submitLabel), { delay: null });
|
await userEvent.click(screen.getByText(label), { delay: null });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const itShouldCloseTheDialogAndShowThePasswordInput = (): void => {
|
||||||
|
it("should close the dialog and show the password input", () => {
|
||||||
|
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Reset your password")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -70,6 +78,7 @@ describe("<ForgotPassword>", () => {
|
||||||
serverConfig.hsName = "example.com";
|
serverConfig.hsName = "example.com";
|
||||||
|
|
||||||
onComplete = jest.fn();
|
onComplete = jest.fn();
|
||||||
|
onLoginClick = jest.fn();
|
||||||
|
|
||||||
jest.spyOn(AutoDiscoveryUtils, "validateServerConfigWithStaticUrls").mockResolvedValue(serverConfig);
|
jest.spyOn(AutoDiscoveryUtils, "validateServerConfigWithStaticUrls").mockResolvedValue(serverConfig);
|
||||||
jest.spyOn(AutoDiscoveryUtils, "authComponentStateForError");
|
jest.spyOn(AutoDiscoveryUtils, "authComponentStateForError");
|
||||||
|
@ -94,6 +103,7 @@ describe("<ForgotPassword>", () => {
|
||||||
renderResult = render(<ForgotPassword
|
renderResult = render(<ForgotPassword
|
||||||
serverConfig={serverConfig}
|
serverConfig={serverConfig}
|
||||||
onComplete={onComplete}
|
onComplete={onComplete}
|
||||||
|
onLoginClick={onLoginClick}
|
||||||
/>);
|
/>);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -108,6 +118,7 @@ describe("<ForgotPassword>", () => {
|
||||||
renderResult.rerender(<ForgotPassword
|
renderResult.rerender(<ForgotPassword
|
||||||
serverConfig={serverConfig}
|
serverConfig={serverConfig}
|
||||||
onComplete={onComplete}
|
onComplete={onComplete}
|
||||||
|
onLoginClick={onLoginClick}
|
||||||
/>);
|
/>);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -116,6 +127,16 @@ describe("<ForgotPassword>", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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", () => {
|
describe("when entering a non-email value", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await typeIntoField("Email address", "not en email");
|
await typeIntoField("Email address", "not en email");
|
||||||
|
@ -132,7 +153,7 @@ describe("<ForgotPassword>", () => {
|
||||||
mocked(client).requestPasswordEmailToken.mockRejectedValue({
|
mocked(client).requestPasswordEmailToken.mockRejectedValue({
|
||||||
errcode: "M_THREEPID_NOT_FOUND",
|
errcode: "M_THREEPID_NOT_FOUND",
|
||||||
});
|
});
|
||||||
await submitForm("Send email");
|
await clickButton("Send email");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show an email not found message", () => {
|
it("should show an email not found message", () => {
|
||||||
|
@ -146,7 +167,7 @@ describe("<ForgotPassword>", () => {
|
||||||
mocked(client).requestPasswordEmailToken.mockRejectedValue({
|
mocked(client).requestPasswordEmailToken.mockRejectedValue({
|
||||||
name: "ConnectionError",
|
name: "ConnectionError",
|
||||||
});
|
});
|
||||||
await submitForm("Send email");
|
await clickButton("Send email");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show an info about that", () => {
|
it("should show an info about that", () => {
|
||||||
|
@ -166,7 +187,7 @@ describe("<ForgotPassword>", () => {
|
||||||
serverIsAlive: false,
|
serverIsAlive: false,
|
||||||
serverDeadError: "server down",
|
serverDeadError: "server down",
|
||||||
});
|
});
|
||||||
await submitForm("Send email");
|
await clickButton("Send email");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the server error", () => {
|
it("should show the server error", () => {
|
||||||
|
@ -180,7 +201,7 @@ describe("<ForgotPassword>", () => {
|
||||||
mocked(client).requestPasswordEmailToken.mockResolvedValue({
|
mocked(client).requestPasswordEmailToken.mockResolvedValue({
|
||||||
sid: testSid,
|
sid: testSid,
|
||||||
});
|
});
|
||||||
await submitForm("Send email");
|
await clickButton("Send email");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should send the mail and show the check email view", () => {
|
it("should send the mail and show the check email view", () => {
|
||||||
|
@ -193,6 +214,16 @@ describe("<ForgotPassword>", () => {
|
||||||
expect(screen.getByText(testEmail)).toBeInTheDocument();
|
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", () => {
|
describe("when clicking resend email", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await userEvent.click(screen.getByText("Resend"), { delay: null });
|
await userEvent.click(screen.getByText("Resend"), { delay: null });
|
||||||
|
@ -212,7 +243,7 @@ describe("<ForgotPassword>", () => {
|
||||||
|
|
||||||
describe("when clicking next", () => {
|
describe("when clicking next", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await submitForm("Next");
|
await clickButton("Next");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the password input view", () => {
|
it("should show the password input view", () => {
|
||||||
|
@ -246,7 +277,7 @@ describe("<ForgotPassword>", () => {
|
||||||
retry_after_ms: (13 * 60 + 37) * 1000,
|
retry_after_ms: (13 * 60 + 37) * 1000,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await submitForm("Reset password");
|
await clickButton("Reset password");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the rate limit error message", () => {
|
it("should show the rate limit error message", () => {
|
||||||
|
@ -258,7 +289,7 @@ describe("<ForgotPassword>", () => {
|
||||||
|
|
||||||
describe("and submitting it", () => {
|
describe("and submitting it", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await submitForm("Reset password");
|
await clickButton("Reset password");
|
||||||
// double flush promises for the modal to appear
|
// double flush promises for the modal to appear
|
||||||
await flushPromisesWithFakeTimers();
|
await flushPromisesWithFakeTimers();
|
||||||
await flushPromisesWithFakeTimers();
|
await flushPromisesWithFakeTimers();
|
||||||
|
@ -284,6 +315,46 @@ describe("<ForgotPassword>", () => {
|
||||||
expect(screen.getByText(testEmail)).toBeInTheDocument();
|
expect(screen.getByText(testEmail)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("and dismissing the dialog by clicking the background", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(screen.getByTestId("dialog-background"), { delay: null });
|
||||||
|
});
|
||||||
|
// double flush promises for the modal to disappear
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
itShouldCloseTheDialogAndShowThePasswordInput();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("and dismissing the dialog", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(screen.getByLabelText("Close dialog"), { delay: null });
|
||||||
|
});
|
||||||
|
// double flush promises for the modal to disappear
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
itShouldCloseTheDialogAndShowThePasswordInput();
|
||||||
|
});
|
||||||
|
|
||||||
|
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", () => {
|
describe("when validating the link from the mail", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mocked(client.setPassword).mockResolvedValue({});
|
mocked(client.setPassword).mockResolvedValue({});
|
||||||
|
|
Loading…
Reference in a new issue