Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 646f87b

Browse files
authored
In forgot password screen, show validation errors inline in the form, instead of in modals (#7113)
1 parent a16e6da commit 646f87b

File tree

3 files changed

+71
-61
lines changed

3 files changed

+71
-61
lines changed

Diff for: src/components/structures/auth/ForgotPassword.tsx

+65-56
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,14 @@ import EmailField from "../../views/auth/EmailField";
2929
import PassphraseField from '../../views/auth/PassphraseField';
3030
import { replaceableComponent } from "../../../utils/replaceableComponent";
3131
import { PASSWORD_MIN_SCORE } from '../../views/auth/RegistrationForm';
32-
import { IValidationResult } from "../../views/elements/Validation";
3332
import InlineSpinner from '../../views/elements/InlineSpinner';
3433
import { logger } from "matrix-js-sdk/src/logger";
3534
import Spinner from "../../views/elements/Spinner";
3635
import QuestionDialog from "../../views/dialogs/QuestionDialog";
3736
import ErrorDialog from "../../views/dialogs/ErrorDialog";
38-
import Field from "../../views/elements/Field";
3937
import AuthHeader from "../../views/auth/AuthHeader";
4038
import AuthBody from "../../views/auth/AuthBody";
39+
import PassphraseConfirmField from "../../views/auth/PassphraseConfirmField";
4140

4241
enum Phase {
4342
// Show the forgot password inputs
@@ -72,11 +71,15 @@ interface IState {
7271
serverErrorIsFatal: boolean;
7372
serverDeadError: string;
7473

75-
emailFieldValid: boolean;
76-
passwordFieldValid: boolean;
7774
currentHttpRequest?: Promise<any>;
7875
}
7976

77+
enum ForgotPasswordField {
78+
Email = 'field_email',
79+
Password = 'field_password',
80+
PasswordConfirm = 'field_password_confirm',
81+
}
82+
8083
@replaceableComponent("structures.auth.ForgotPassword")
8184
export default class ForgotPassword extends React.Component<IProps, IState> {
8285
private reset: PasswordReset;
@@ -95,8 +98,6 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
9598
serverIsAlive: true,
9699
serverErrorIsFatal: false,
97100
serverDeadError: "",
98-
emailFieldValid: false,
99-
passwordFieldValid: false,
100101
};
101102

102103
constructor(props: IProps) {
@@ -175,41 +176,58 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
175176
// refresh the server errors, just in case the server came back online
176177
await this.handleHttpRequest(this.checkServerLiveliness(this.props.serverConfig));
177178

178-
await this['email_field'].validate({ allowEmpty: false });
179-
await this['password_field'].validate({ allowEmpty: false });
180-
181-
if (!this.state.email) {
182-
this.showErrorDialog(_t('The email address linked to your account must be entered.'));
183-
} else if (!this.state.emailFieldValid) {
184-
this.showErrorDialog(_t("The email address doesn't appear to be valid."));
185-
} else if (!this.state.password || !this.state.password2) {
186-
this.showErrorDialog(_t('A new password must be entered.'));
187-
} else if (!this.state.passwordFieldValid) {
188-
this.showErrorDialog(_t('Please choose a strong password'));
189-
} else if (this.state.password !== this.state.password2) {
190-
this.showErrorDialog(_t('New passwords must match each other.'));
191-
} else {
192-
Modal.createTrackedDialog('Forgot Password Warning', '', QuestionDialog, {
193-
title: _t('Warning!'),
194-
description:
195-
<div>
196-
{ _t(
197-
"Changing your password will reset any end-to-end encryption keys " +
198-
"on all of your sessions, making encrypted chat history unreadable. Set up " +
199-
"Key Backup or export your room keys from another session before resetting your " +
200-
"password.",
201-
) }
202-
</div>,
203-
button: _t('Continue'),
204-
onFinished: (confirmed) => {
205-
if (confirmed) {
206-
this.submitPasswordReset(this.state.email, this.state.password);
207-
}
208-
},
209-
});
179+
const allFieldsValid = await this.verifyFieldsBeforeSubmit();
180+
if (!allFieldsValid) {
181+
return;
210182
}
183+
184+
Modal.createTrackedDialog('Forgot Password Warning', '', QuestionDialog, {
185+
title: _t('Warning!'),
186+
description:
187+
<div>
188+
{ _t(
189+
"Changing your password will reset any end-to-end encryption keys " +
190+
"on all of your sessions, making encrypted chat history unreadable. Set up " +
191+
"Key Backup or export your room keys from another session before resetting your " +
192+
"password.",
193+
) }
194+
</div>,
195+
button: _t('Continue'),
196+
onFinished: (confirmed) => {
197+
if (confirmed) {
198+
this.submitPasswordReset(this.state.email, this.state.password);
199+
}
200+
},
201+
});
211202
};
212203

204+
private async verifyFieldsBeforeSubmit() {
205+
const fieldIdsInDisplayOrder = [
206+
ForgotPasswordField.Email,
207+
ForgotPasswordField.Password,
208+
ForgotPasswordField.PasswordConfirm,
209+
];
210+
211+
const invalidFields = [];
212+
for (const fieldId of fieldIdsInDisplayOrder) {
213+
const valid = await this[fieldId].validate({ allowEmpty: false });
214+
if (!valid) {
215+
invalidFields.push(this[fieldId]);
216+
}
217+
}
218+
219+
if (invalidFields.length === 0) {
220+
return true;
221+
}
222+
223+
// Focus on the first invalid field, then re-validate,
224+
// which will result in the error tooltip being displayed for that field.
225+
invalidFields[0].focus();
226+
invalidFields[0].validate({ allowEmpty: false, focused: true });
227+
228+
return false;
229+
}
230+
213231
private onInputChanged = (stateKey: string, ev: React.FormEvent<HTMLInputElement>) => {
214232
this.setState({
215233
[stateKey]: ev.currentTarget.value,
@@ -229,18 +247,6 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
229247
});
230248
}
231249

232-
private onEmailValidate = (result: IValidationResult) => {
233-
this.setState({
234-
emailFieldValid: result.valid,
235-
});
236-
};
237-
238-
private onPasswordValidate(result: IValidationResult) {
239-
this.setState({
240-
passwordFieldValid: result.valid,
241-
});
242-
}
243-
244250
private handleHttpRequest<T = unknown>(request: Promise<T>): Promise<T> {
245251
this.setState({
246252
currentHttpRequest: request,
@@ -284,11 +290,12 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
284290
<div className="mx_AuthBody_fieldRow">
285291
<EmailField
286292
name="reset_email" // define a name so browser's password autofill gets less confused
293+
labelRequired={_t('The email address linked to your account must be entered.')}
294+
labelInvalid={_t("The email address doesn't appear to be valid.")}
287295
value={this.state.email}
288-
fieldRef={field => this['email_field'] = field}
296+
fieldRef={field => this[ForgotPasswordField.Email] = field}
289297
autoFocus={true}
290298
onChange={this.onInputChanged.bind(this, "email")}
291-
onValidate={this.onEmailValidate}
292299
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_focus")}
293300
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_blur")}
294301
/>
@@ -300,18 +307,20 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
300307
label={_td('New Password')}
301308
value={this.state.password}
302309
minScore={PASSWORD_MIN_SCORE}
310+
fieldRef={field => this[ForgotPasswordField.Password] = field}
303311
onChange={this.onInputChanged.bind(this, "password")}
304-
fieldRef={field => this['password_field'] = field}
305-
onValidate={(result) => this.onPasswordValidate(result)}
306312
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_focus")}
307313
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_blur")}
308314
autoComplete="new-password"
309315
/>
310-
<Field
316+
<PassphraseConfirmField
311317
name="reset_password_confirm"
312-
type="password"
313318
label={_t('Confirm')}
319+
labelRequired={_t("A new password must be entered.")}
320+
labelInvalid={_t("New passwords must match each other.")}
314321
value={this.state.password2}
322+
password={this.state.password}
323+
fieldRef={field => this[ForgotPasswordField.PasswordConfirm] = field}
315324
onChange={this.onInputChanged.bind(this, "password2")}
316325
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_focus")}
317326
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_blur")}

Diff for: src/components/views/auth/PassphraseField.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ interface IProps extends Omit<IInputProps, "onValidate"> {
3838
labelAllowedButUnsafe?: string;
3939

4040
onChange(ev: React.FormEvent<HTMLElement>);
41-
onValidate(result: IValidationResult);
41+
onValidate?(result: IValidationResult);
4242
}
4343

4444
@replaceableComponent("views.auth.PassphraseField")
@@ -98,7 +98,9 @@ class PassphraseField extends PureComponent<IProps> {
9898

9999
onValidate = async (fieldState: IFieldState) => {
100100
const result = await this.validate(fieldState);
101-
this.props.onValidate(result);
101+
if (this.props.onValidate) {
102+
this.props.onValidate(result);
103+
}
102104
return result;
103105
};
104106

Diff for: src/i18n/strings/en_EN.json

+2-3
Original file line numberDiff line numberDiff line change
@@ -3058,13 +3058,12 @@
30583058
"Really reset verification keys?": "Really reset verification keys?",
30593059
"Skip verification for now": "Skip verification for now",
30603060
"Failed to send email": "Failed to send email",
3061+
"Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.",
30613062
"The email address linked to your account must be entered.": "The email address linked to your account must be entered.",
30623063
"The email address doesn't appear to be valid.": "The email address doesn't appear to be valid.",
3064+
"New Password": "New Password",
30633065
"A new password must be entered.": "A new password must be entered.",
3064-
"Please choose a strong password": "Please choose a strong password",
30653066
"New passwords must match each other.": "New passwords must match each other.",
3066-
"Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.",
3067-
"New Password": "New Password",
30683067
"A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.",
30693068
"Send Reset Email": "Send Reset Email",
30703069
"Sign in instead": "Sign in instead",

0 commit comments

Comments
 (0)