From 6a3fb5cbb431a94729eb62733a6f87ae201e189a Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Wed, 27 Oct 2021 09:52:34 +0100 Subject: [PATCH] Add EmailField component for login, registration and password recovery screens (#7006) Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- .../structures/auth/ForgotPassword.tsx | 35 ++----- src/components/views/auth/EmailField.tsx | 92 +++++++++++++++++++ src/components/views/auth/PasswordLogin.tsx | 30 +----- .../views/auth/RegistrationForm.tsx | 17 ++-- .../dialogs/RegistrationEmailPromptDialog.tsx | 26 ++---- src/i18n/strings/en_EN.json | 6 +- 6 files changed, 121 insertions(+), 85 deletions(-) create mode 100644 src/components/views/auth/EmailField.tsx diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index 4c65fac983..66ade9e6ed 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -26,13 +26,12 @@ import classNames from 'classnames'; import AuthPage from "../../views/auth/AuthPage"; import CountlyAnalytics from "../../../CountlyAnalytics"; import ServerPicker from "../../views/elements/ServerPicker"; +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 withValidation, { IValidationResult } from "../../views/elements/Validation"; -import * as Email from "../../../email"; +import { IValidationResult } from "../../views/elements/Validation"; import InlineSpinner from '../../views/elements/InlineSpinner'; - import { logger } from "matrix-js-sdk/src/logger"; enum Phase { @@ -227,30 +226,10 @@ export default class ForgotPassword extends React.Component { }); } - private validateEmailRules = withValidation({ - rules: [ - { - key: "required", - test({ value, allowEmpty }) { - return allowEmpty || !!value; - }, - invalid: () => _t("Enter email address"), - }, { - key: "email", - test: ({ value }) => !value || Email.looksValid(value), - invalid: () => _t("Doesn't look like a valid email address"), - }, - ], - }); - - private onEmailValidate = async (fieldState) => { - const result = await this.validateEmailRules(fieldState); - + private onEmailValidate = (result: IValidationResult) => { this.setState({ emailFieldValid: result.valid, }); - - return result; }; private onPasswordValidate(result: IValidationResult) { @@ -302,14 +281,12 @@ export default class ForgotPassword extends React.Component { />
- this['email_field'] = field} + autoFocus={true} onChange={this.onInputChanged.bind(this, "email")} - ref={field => this['email_field'] = field} - autoFocus onValidate={this.onEmailValidate} onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_focus")} onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_blur")} diff --git a/src/components/views/auth/EmailField.tsx b/src/components/views/auth/EmailField.tsx new file mode 100644 index 0000000000..3ff1700030 --- /dev/null +++ b/src/components/views/auth/EmailField.tsx @@ -0,0 +1,92 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { PureComponent, RefCallback, RefObject } from "react"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Field, { IInputProps } from "../elements/Field"; +import { _t, _td } from "../../../languageHandler"; +import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; +import * as Email from "../../../email"; + +interface IProps extends Omit { + id?: string; + fieldRef?: RefCallback | RefObject; + value: string; + autoFocus?: boolean; + + label?: string; + labelRequired?: string; + labelInvalid?: string; + + // When present, completely overrides the default validation rules. + validationRules?: (fieldState: IFieldState) => Promise; + + onChange(ev: React.FormEvent): void; + onValidate?(result: IValidationResult): void; +} + +@replaceableComponent("views.auth.EmailField") +class EmailField extends PureComponent { + static defaultProps = { + label: _td("Email"), + labelRequired: _td("Enter email address"), + labelInvalid: _td("Doesn't look like a valid email address"), + }; + + public readonly validate = withValidation({ + rules: [ + { + key: "required", + test: ({ value, allowEmpty }) => allowEmpty || !!value, + invalid: () => _t(this.props.labelRequired), + }, + { + key: "email", + test: ({ value }) => !value || Email.looksValid(value), + invalid: () => _t(this.props.labelInvalid), + }, + ], + }); + + onValidate = async (fieldState: IFieldState) => { + let validate = this.validate; + if (this.props.validationRules) { + validate = this.props.validationRules; + } + + const result = await validate(fieldState); + if (this.props.onValidate) { + this.props.onValidate(result); + } + + return result; + }; + + render() { + return ; + } +} + +export default EmailField; diff --git a/src/components/views/auth/PasswordLogin.tsx b/src/components/views/auth/PasswordLogin.tsx index 587d7f2453..920cec4e5f 100644 --- a/src/components/views/auth/PasswordLogin.tsx +++ b/src/components/views/auth/PasswordLogin.tsx @@ -22,11 +22,11 @@ import SdkConfig from '../../../SdkConfig'; import { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils"; import AccessibleButton from "../elements/AccessibleButton"; import CountlyAnalytics from "../../../CountlyAnalytics"; -import withValidation from "../elements/Validation"; -import * as Email from "../../../email"; +import withValidation, { IValidationResult } from "../elements/Validation"; import Field from "../elements/Field"; import CountryDropdown from "./CountryDropdown"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import EmailField from "./EmailField"; // For validating phone numbers without country codes const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; @@ -262,26 +262,8 @@ export default class PasswordLogin extends React.PureComponent { return result; }; - private validateEmailRules = withValidation({ - rules: [ - { - key: "required", - test({ value, allowEmpty }) { - return allowEmpty || !!value; - }, - invalid: () => _t("Enter email address"), - }, { - key: "email", - test: ({ value }) => !value || Email.looksValid(value), - invalid: () => _t("Doesn't look like a valid email address"), - }, - ], - }); - - private onEmailValidate = async (fieldState) => { - const result = await this.validateEmailRules(fieldState); + private onEmailValidate = (result: IValidationResult) => { this.markFieldValid(LoginField.Email, result.valid); - return result; }; private validatePhoneNumberRules = withValidation({ @@ -332,12 +314,10 @@ export default class PasswordLogin extends React.PureComponent { switch (loginType) { case LoginField.Email: classes.error = this.props.loginIncorrect && !this.props.username; - return { disabled={this.props.disableSubmit} autoFocus={autoFocus} onValidate={this.onEmailValidate} - ref={field => this[LoginField.Email] = field} + fieldRef={field => this[LoginField.Email] = field} />; case LoginField.MatrixId: classes.error = this.props.loginIncorrect && !this.props.username; diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx index c66d6b80fd..24e73f2992 100644 --- a/src/components/views/auth/RegistrationForm.tsx +++ b/src/components/views/auth/RegistrationForm.tsx @@ -23,8 +23,9 @@ import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import { SAFE_LOCALPART_REGEX } from '../../../Registration'; -import withValidation from '../elements/Validation'; +import withValidation, { IValidationResult } from '../elements/Validation'; import { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils"; +import EmailField from "./EmailField"; import PassphraseField from "./PassphraseField"; import CountlyAnalytics from "../../../CountlyAnalytics"; import Field from '../elements/Field'; @@ -253,10 +254,8 @@ export default class RegistrationForm extends React.PureComponent { - const result = await this.validateEmailRules(fieldState); + private onEmailValidate = (result: IValidationResult) => { this.markFieldValid(RegistrationField.Email, result.valid); - return result; }; private validateEmailRules = withValidation({ @@ -426,14 +425,14 @@ export default class RegistrationForm extends React.PureComponent this[RegistrationField.Email] = field} - type="text" - label={emailPlaceholder} + return this[RegistrationField.Email] = field} + label={emailLabel} value={this.state.email} + validationRules={this.validateEmailRules.bind(this)} onChange={this.onEmailChange} onValidate={this.onEmailValidate} onFocus={() => CountlyAnalytics.instance.track("onboarding_registration_email_focus")} diff --git a/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx b/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx index 804a1aec35..8e406c9dc8 100644 --- a/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx +++ b/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx @@ -21,25 +21,14 @@ import { IDialogProps } from "./IDialogProps"; import { useRef, useState } from "react"; import Field from "../elements/Field"; import CountlyAnalytics from "../../../CountlyAnalytics"; -import withValidation from "../elements/Validation"; -import * as Email from "../../../email"; import BaseDialog from "./BaseDialog"; import DialogButtons from "../elements/DialogButtons"; +import EmailField from "../auth/EmailField"; interface IProps extends IDialogProps { onFinished(continued: boolean, email?: string): void; } -const validation = withValidation({ - rules: [ - { - key: "email", - test: ({ value }) => !value || Email.looksValid(value), - invalid: () => _t("Doesn't look like a valid email address"), - }, - ], -}); - const RegistrationEmailPromptDialog: React.FC = ({ onFinished }) => { const [email, setEmail] = useState(""); const fieldRef = useRef(); @@ -47,11 +36,11 @@ const RegistrationEmailPromptDialog: React.FC = ({ onFinished }) => { const onSubmit = async (e) => { e.preventDefault(); if (email) { - const valid = await fieldRef.current.validate({ allowEmpty: false }); + const valid = await fieldRef.current.validate({}); if (!valid) { fieldRef.current.focus(); - fieldRef.current.validate({ allowEmpty: false, focused: true }); + fieldRef.current.validate({ focused: true }); return; } } @@ -72,16 +61,15 @@ const RegistrationEmailPromptDialog: React.FC = ({ onFinished }) => { b: sub => { sub }, }) }

- { - setEmail(ev.target.value); + const target = ev.target as HTMLInputElement; + setEmail(target.value); }} - onValidate={async fieldState => await validation(fieldState)} onFocus={() => CountlyAnalytics.instance.track("onboarding_registration_email2_focus")} onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email2_blur")} /> diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4554d848e4..47242cd402 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2523,7 +2523,6 @@ "Message edits": "Message edits", "Modal Widget": "Modal Widget", "Data on this screen is shared with %(widgetDomain)s": "Data on this screen is shared with %(widgetDomain)s", - "Doesn't look like a valid email address": "Doesn't look like a valid email address", "Continuing without email": "Continuing without email", "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.": "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.", "Email (optional)": "Email (optional)", @@ -2738,6 +2737,9 @@ "powered by Matrix": "powered by Matrix", "This homeserver would like to make sure you are not a robot.": "This homeserver would like to make sure you are not a robot.", "Country Dropdown": "Country Dropdown", + "Email": "Email", + "Enter email address": "Enter email address", + "Doesn't look like a valid email address": "Doesn't look like a valid email address", "Confirm your identity by entering your account password below.": "Confirm your identity by entering your account password below.", "Password": "Password", "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.", @@ -2757,10 +2759,8 @@ "Password is allowed, but unsafe": "Password is allowed, but unsafe", "Keep going...": "Keep going...", "Enter username": "Enter username", - "Enter email address": "Enter email address", "Enter phone number": "Enter phone number", "That phone number doesn't look quite right, please check and try again": "That phone number doesn't look quite right, please check and try again", - "Email": "Email", "Username": "Username", "Phone": "Phone", "Forgot password?": "Forgot password?",