From e647728d5e3f5ce5d613b0c137c8851749d9cda8 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Tue, 26 Oct 2021 12:18:11 +0100 Subject: [PATCH 1/9] Customise error messages Signed-off-by: Paulo Pinto --- src/components/structures/auth/ForgotPassword.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index d5f5344eae1..89b62093661 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -284,6 +284,8 @@ export default class ForgotPassword extends React.Component {
this['email_field'] = field} autoFocus={true} From 61c18fdeaf0f9ee0e28b89c1786fd101230be16f Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Tue, 26 Oct 2021 12:48:58 +0100 Subject: [PATCH 2/9] Add enum for form fields Signed-off-by: Paulo Pinto --- src/components/structures/auth/ForgotPassword.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index 89b62093661..ec7a6aa1286 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -77,6 +77,12 @@ interface IState { 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; @@ -175,8 +181,8 @@ 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 }); + await this[ForgotPasswordField.Email].validate({ allowEmpty: false }); + await this[ForgotPasswordField.Password].validate({ allowEmpty: false }); if (!this.state.email) { this.showErrorDialog(_t('The email address linked to your account must be entered.')); @@ -287,7 +293,7 @@ export default class ForgotPassword extends React.Component { labelRequired={_t('The email address linked to your account must be entered.')} labelInvalid={_t("The email address doesn't appear to be valid.")} value={this.state.email} - fieldRef={field => this['email_field'] = field} + fieldRef={field => this[ForgotPasswordField.Email] = field} autoFocus={true} onChange={this.onInputChanged.bind(this, "email")} onValidate={this.onEmailValidate} @@ -302,8 +308,8 @@ 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")} @@ -314,6 +320,7 @@ export default class ForgotPassword extends React.Component { type="password" label={_t('Confirm')} value={this.state.password2} + ref={field => 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")} From 75282fee696b6602025debc25b90ac0cc5e4e886 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Tue, 26 Oct 2021 12:56:14 +0100 Subject: [PATCH 3/9] Extract validation logic to a function Signed-off-by: Paulo Pinto --- .../structures/auth/ForgotPassword.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index ec7a6aa1286..4e0e54b5ad9 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -181,8 +181,10 @@ 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[ForgotPasswordField.Email].validate({ allowEmpty: false }); - await this[ForgotPasswordField.Password].validate({ allowEmpty: false }); + const allFieldsValid = await this.verifyFieldsBeforeSubmit(); + if (!allFieldsValid) { + return; + } if (!this.state.email) { this.showErrorDialog(_t('The email address linked to your account must be entered.')); @@ -216,6 +218,18 @@ export default class ForgotPassword extends React.Component { } }; + private async verifyFieldsBeforeSubmit() { + if (!await this[ForgotPasswordField.Email].validate({ allowEmpty: false })) { + return false; + } + + if (!await this[ForgotPasswordField.Password].validate({ allowEmpty: false })) { + return false; + } + + return true; + } + private onInputChanged = (stateKey: string, ev: React.FormEvent) => { this.setState({ [stateKey]: ev.currentTarget.value, From d271e2743970da1a38a6f2b3866ed692f328f7cc Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Tue, 26 Oct 2021 13:17:45 +0100 Subject: [PATCH 4/9] Focus on the first invalid field Signed-off-by: Paulo Pinto --- .../structures/auth/ForgotPassword.tsx | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index 4e0e54b5ad9..613ae1edfb9 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -219,15 +219,30 @@ export default class ForgotPassword extends React.Component { }; private async verifyFieldsBeforeSubmit() { - if (!await this[ForgotPasswordField.Email].validate({ allowEmpty: false })) { - return false; + 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 (!await this[ForgotPasswordField.Password].validate({ allowEmpty: false })) { - return false; + if (invalidFields.length === 0) { + return true; } - 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) => { From 2b43ec092a0de0cf07cfabad1f3ae10b04941239 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Tue, 26 Oct 2021 15:26:43 +0100 Subject: [PATCH 5/9] Make onValidate prop optional Signed-off-by: Paulo Pinto --- src/components/views/auth/PassphraseField.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx index bab7e59d2a5..157ec521fde 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; }; From 090ac28d34bbab51c9e1dd9f387c6e394a0f8387 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Tue, 26 Oct 2021 15:27:50 +0100 Subject: [PATCH 6/9] Remove no longer used code for email and password validation Signed-off-by: Paulo Pinto --- .../structures/auth/ForgotPassword.tsx | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index 613ae1edfb9..13d9269cd0c 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -29,7 +29,6 @@ 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"; @@ -72,8 +71,6 @@ interface IState { serverErrorIsFatal: boolean; serverDeadError: string; - emailFieldValid: boolean; - passwordFieldValid: boolean; currentHttpRequest?: Promise; } @@ -101,8 +98,6 @@ export default class ForgotPassword extends React.Component { serverIsAlive: true, serverErrorIsFatal: false, serverDeadError: "", - emailFieldValid: false, - passwordFieldValid: false, }; constructor(props: IProps) { @@ -186,14 +181,8 @@ export default class ForgotPassword extends React.Component { return; } - 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) { + if (!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 { @@ -264,18 +253,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, @@ -325,7 +302,6 @@ export default class ForgotPassword extends React.Component { 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")} /> @@ -339,7 +315,6 @@ export default class ForgotPassword extends React.Component { minScore={PASSWORD_MIN_SCORE} fieldRef={field => this[ForgotPasswordField.Password] = field} onChange={this.onInputChanged.bind(this, "password")} - 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" From 360a2b4608188af3abed25e30a7a6bd2f90a9e4d Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Tue, 26 Oct 2021 16:19:51 +0100 Subject: [PATCH 7/9] Use PassphraseConfirmField For the validation Signed-off-by: Paulo Pinto --- src/components/structures/auth/ForgotPassword.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index 13d9269cd0c..d009ffc2c51 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -34,9 +34,9 @@ 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 @@ -319,12 +319,14 @@ export default class ForgotPassword extends React.Component { onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_blur")} autoComplete="new-password" /> - this[ForgotPasswordField.PasswordConfirm] = field} + password={this.state.password} + fieldRef={field => 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")} From 4da90b81d440b34101616957af26151e74040df1 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Tue, 26 Oct 2021 16:28:10 +0100 Subject: [PATCH 8/9] Remove no longer used code for password confirmation validation Signed-off-by: Paulo Pinto --- .../structures/auth/ForgotPassword.tsx | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index d009ffc2c51..f4d0f668614 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -181,30 +181,24 @@ export default class ForgotPassword extends React.Component { return; } - if (!this.state.password2) { - this.showErrorDialog(_t('A new password must be entered.')); - } 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); - } - }, - }); - } + 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() { From a6c89b3ebf2a10869fe9c39f4b716add1f7d1260 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Tue, 26 Oct 2021 16:30:12 +0100 Subject: [PATCH 9/9] Update translations Signed-off-by: Paulo Pinto --- src/i18n/strings/en_EN.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 86d77489cba..935d036aec1 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3042,13 +3042,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.", + "New Password": "New Password", "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 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",