From 646f87b2dbae1d5ebfa8351cd850ebdafd103a9d Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 12 Nov 2021 11:39:39 +0000 Subject: [PATCH] In forgot password screen, show validation errors inline in the form, instead of in modals (#7113) --- .../structures/auth/ForgotPassword.tsx | 121 ++++++++++-------- src/components/views/auth/PassphraseField.tsx | 6 +- src/i18n/strings/en_EN.json | 7 +- 3 files changed, 72 insertions(+), 62 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index d5f5344eae..f4d0f66861 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -29,15 +29,14 @@ import EmailField from "../../views/auth/EmailField"; import PassphraseField from '../../views/auth/PassphraseField'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { PASSWORD_MIN_SCORE } from '../../views/auth/RegistrationForm'; -import { IValidationResult } from "../../views/elements/Validation"; import InlineSpinner from '../../views/elements/InlineSpinner'; import { logger } from "matrix-js-sdk/src/logger"; import Spinner from "../../views/elements/Spinner"; import QuestionDialog from "../../views/dialogs/QuestionDialog"; import ErrorDialog from "../../views/dialogs/ErrorDialog"; -import Field from "../../views/elements/Field"; import AuthHeader from "../../views/auth/AuthHeader"; import AuthBody from "../../views/auth/AuthBody"; +import PassphraseConfirmField from "../../views/auth/PassphraseConfirmField"; enum Phase { // Show the forgot password inputs @@ -72,11 +71,15 @@ interface IState { serverErrorIsFatal: boolean; serverDeadError: string; - emailFieldValid: boolean; - passwordFieldValid: boolean; currentHttpRequest?: Promise; } +enum ForgotPasswordField { + Email = 'field_email', + Password = 'field_password', + PasswordConfirm = 'field_password_confirm', +} + @replaceableComponent("structures.auth.ForgotPassword") export default class ForgotPassword extends React.Component { private reset: PasswordReset; @@ -95,8 +98,6 @@ export default class ForgotPassword extends React.Component { serverIsAlive: true, serverErrorIsFatal: false, serverDeadError: "", - emailFieldValid: false, - passwordFieldValid: false, }; constructor(props: IProps) { @@ -175,41 +176,58 @@ export default class ForgotPassword extends React.Component { // refresh the server errors, just in case the server came back online await this.handleHttpRequest(this.checkServerLiveliness(this.props.serverConfig)); - await this['email_field'].validate({ allowEmpty: false }); - await this['password_field'].validate({ allowEmpty: false }); - - if (!this.state.email) { - this.showErrorDialog(_t('The email address linked to your account must be entered.')); - } else if (!this.state.emailFieldValid) { - this.showErrorDialog(_t("The email address doesn't appear to be valid.")); - } else if (!this.state.password || !this.state.password2) { - this.showErrorDialog(_t('A new password must be entered.')); - } else if (!this.state.passwordFieldValid) { - this.showErrorDialog(_t('Please choose a strong password')); - } else if (this.state.password !== this.state.password2) { - this.showErrorDialog(_t('New passwords must match each other.')); - } else { - Modal.createTrackedDialog('Forgot Password Warning', '', QuestionDialog, { - title: _t('Warning!'), - description: -
- { _t( - "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.", - ) } -
, - button: _t('Continue'), - onFinished: (confirmed) => { - if (confirmed) { - this.submitPasswordReset(this.state.email, this.state.password); - } - }, - }); + const allFieldsValid = await this.verifyFieldsBeforeSubmit(); + if (!allFieldsValid) { + return; } + + Modal.createTrackedDialog('Forgot Password Warning', '', QuestionDialog, { + title: _t('Warning!'), + description: +
+ { _t( + "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.", + ) } +
, + button: _t('Continue'), + onFinished: (confirmed) => { + if (confirmed) { + this.submitPasswordReset(this.state.email, this.state.password); + } + }, + }); }; + private async verifyFieldsBeforeSubmit() { + const fieldIdsInDisplayOrder = [ + ForgotPasswordField.Email, + ForgotPasswordField.Password, + ForgotPasswordField.PasswordConfirm, + ]; + + const invalidFields = []; + for (const fieldId of fieldIdsInDisplayOrder) { + const valid = await this[fieldId].validate({ allowEmpty: false }); + if (!valid) { + invalidFields.push(this[fieldId]); + } + } + + if (invalidFields.length === 0) { + return true; + } + + // Focus on the first invalid field, then re-validate, + // which will result in the error tooltip being displayed for that field. + invalidFields[0].focus(); + invalidFields[0].validate({ allowEmpty: false, focused: true }); + + return false; + } + private onInputChanged = (stateKey: string, ev: React.FormEvent) => { this.setState({ [stateKey]: ev.currentTarget.value, @@ -229,18 +247,6 @@ export default class ForgotPassword extends React.Component { }); } - private onEmailValidate = (result: IValidationResult) => { - this.setState({ - emailFieldValid: result.valid, - }); - }; - - private onPasswordValidate(result: IValidationResult) { - this.setState({ - passwordFieldValid: result.valid, - }); - } - private handleHttpRequest(request: Promise): Promise { this.setState({ currentHttpRequest: request, @@ -284,11 +290,12 @@ export default class ForgotPassword extends React.Component {
this['email_field'] = field} + fieldRef={field => this[ForgotPasswordField.Email] = field} autoFocus={true} onChange={this.onInputChanged.bind(this, "email")} - onValidate={this.onEmailValidate} onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_focus")} onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_blur")} /> @@ -300,18 +307,20 @@ export default class ForgotPassword extends React.Component { label={_td('New Password')} value={this.state.password} minScore={PASSWORD_MIN_SCORE} + fieldRef={field => this[ForgotPasswordField.Password] = field} onChange={this.onInputChanged.bind(this, "password")} - fieldRef={field => this['password_field'] = field} - onValidate={(result) => this.onPasswordValidate(result)} onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_focus")} onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_blur")} autoComplete="new-password" /> - this[ForgotPasswordField.PasswordConfirm] = field} onChange={this.onInputChanged.bind(this, "password2")} onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_focus")} onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_blur")} diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx index bab7e59d2a..157ec521fd 100644 --- a/src/components/views/auth/PassphraseField.tsx +++ b/src/components/views/auth/PassphraseField.tsx @@ -38,7 +38,7 @@ interface IProps extends Omit { labelAllowedButUnsafe?: string; onChange(ev: React.FormEvent); - onValidate(result: IValidationResult); + onValidate?(result: IValidationResult); } @replaceableComponent("views.auth.PassphraseField") @@ -98,7 +98,9 @@ class PassphraseField extends PureComponent { onValidate = async (fieldState: IFieldState) => { const result = await this.validate(fieldState); - this.props.onValidate(result); + if (this.props.onValidate) { + this.props.onValidate(result); + } return result; }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 76c4a48b37..6a22904f31 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3058,13 +3058,12 @@ "Really reset verification keys?": "Really reset verification keys?", "Skip verification for now": "Skip verification for now", "Failed to send email": "Failed to send email", + "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.", "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.", - "A new password must be entered.": "A new password must be entered.", - "Please choose a strong password": "Please choose a strong password", - "New passwords must match each other.": "New passwords must match each other.", - "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.", "New Password": "New Password", + "A new password must be entered.": "A new password must be entered.", + "New passwords must match each other.": "New passwords must match each other.", "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.", "Send Reset Email": "Send Reset Email", "Sign in instead": "Sign in instead",