Add EmailField component for login, registration and password recovery screens (#7006)

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Paulo Pinto 2021-10-27 09:52:34 +01:00 committed by GitHub
parent 82c2102ccb
commit 6a3fb5cbb4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 121 additions and 85 deletions

View file

@ -26,13 +26,12 @@ import classNames from 'classnames';
import AuthPage from "../../views/auth/AuthPage"; import AuthPage from "../../views/auth/AuthPage";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import ServerPicker from "../../views/elements/ServerPicker"; import ServerPicker from "../../views/elements/ServerPicker";
import EmailField from "../../views/auth/EmailField";
import PassphraseField from '../../views/auth/PassphraseField'; import PassphraseField from '../../views/auth/PassphraseField';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { PASSWORD_MIN_SCORE } from '../../views/auth/RegistrationForm'; import { PASSWORD_MIN_SCORE } from '../../views/auth/RegistrationForm';
import withValidation, { IValidationResult } from "../../views/elements/Validation"; import { IValidationResult } from "../../views/elements/Validation";
import * as Email from "../../../email";
import InlineSpinner from '../../views/elements/InlineSpinner'; import InlineSpinner from '../../views/elements/InlineSpinner';
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
enum Phase { enum Phase {
@ -227,30 +226,10 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
}); });
} }
private validateEmailRules = withValidation({ private onEmailValidate = (result: IValidationResult) => {
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);
this.setState({ this.setState({
emailFieldValid: result.valid, emailFieldValid: result.valid,
}); });
return result;
}; };
private onPasswordValidate(result: IValidationResult) { private onPasswordValidate(result: IValidationResult) {
@ -302,14 +281,12 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
/> />
<form onSubmit={this.onSubmitForm}> <form onSubmit={this.onSubmitForm}>
<div className="mx_AuthBody_fieldRow"> <div className="mx_AuthBody_fieldRow">
<Field <EmailField
name="reset_email" // define a name so browser's password autofill gets less confused name="reset_email" // define a name so browser's password autofill gets less confused
type="text"
label={_t('Email')}
value={this.state.email} value={this.state.email}
fieldRef={field => this['email_field'] = field}
autoFocus={true}
onChange={this.onInputChanged.bind(this, "email")} onChange={this.onInputChanged.bind(this, "email")}
ref={field => this['email_field'] = field}
autoFocus
onValidate={this.onEmailValidate} onValidate={this.onEmailValidate}
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_focus")} onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_focus")}
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_blur")} onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_blur")}

View file

@ -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<IInputProps, "onValidate"> {
id?: string;
fieldRef?: RefCallback<Field> | RefObject<Field>;
value: string;
autoFocus?: boolean;
label?: string;
labelRequired?: string;
labelInvalid?: string;
// When present, completely overrides the default validation rules.
validationRules?: (fieldState: IFieldState) => Promise<IValidationResult>;
onChange(ev: React.FormEvent<HTMLElement>): void;
onValidate?(result: IValidationResult): void;
}
@replaceableComponent("views.auth.EmailField")
class EmailField extends PureComponent<IProps> {
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 <Field
id={this.props.id}
ref={this.props.fieldRef}
type="text"
label={_t(this.props.label)}
value={this.props.value}
autoFocus={this.props.autoFocus}
onChange={this.props.onChange}
onValidate={this.onValidate}
/>;
}
}
export default EmailField;

View file

@ -22,11 +22,11 @@ import SdkConfig from '../../../SdkConfig';
import { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils"; import { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import withValidation from "../elements/Validation"; import withValidation, { IValidationResult } from "../elements/Validation";
import * as Email from "../../../email";
import Field from "../elements/Field"; import Field from "../elements/Field";
import CountryDropdown from "./CountryDropdown"; import CountryDropdown from "./CountryDropdown";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import EmailField from "./EmailField";
// For validating phone numbers without country codes // For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
@ -262,26 +262,8 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
return result; return result;
}; };
private validateEmailRules = withValidation({ private onEmailValidate = (result: IValidationResult) => {
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);
this.markFieldValid(LoginField.Email, result.valid); this.markFieldValid(LoginField.Email, result.valid);
return result;
}; };
private validatePhoneNumberRules = withValidation({ private validatePhoneNumberRules = withValidation({
@ -332,12 +314,10 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
switch (loginType) { switch (loginType) {
case LoginField.Email: case LoginField.Email:
classes.error = this.props.loginIncorrect && !this.props.username; classes.error = this.props.loginIncorrect && !this.props.username;
return <Field return <EmailField
className={classNames(classes)} className={classNames(classes)}
name="username" // make it a little easier for browser's remember-password name="username" // make it a little easier for browser's remember-password
key="email_input" key="email_input"
type="text"
label={_t("Email")}
placeholder="joe@example.com" placeholder="joe@example.com"
value={this.props.username} value={this.props.username}
onChange={this.onUsernameChanged} onChange={this.onUsernameChanged}
@ -346,7 +326,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
disabled={this.props.disableSubmit} disabled={this.props.disableSubmit}
autoFocus={autoFocus} autoFocus={autoFocus}
onValidate={this.onEmailValidate} onValidate={this.onEmailValidate}
ref={field => this[LoginField.Email] = field} fieldRef={field => this[LoginField.Email] = field}
/>; />;
case LoginField.MatrixId: case LoginField.MatrixId:
classes.error = this.props.loginIncorrect && !this.props.username; classes.error = this.props.loginIncorrect && !this.props.username;

View file

@ -23,8 +23,9 @@ import Modal from '../../../Modal';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import { SAFE_LOCALPART_REGEX } from '../../../Registration'; import { SAFE_LOCALPART_REGEX } from '../../../Registration';
import withValidation from '../elements/Validation'; import withValidation, { IValidationResult } from '../elements/Validation';
import { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils"; import { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
import EmailField from "./EmailField";
import PassphraseField from "./PassphraseField"; import PassphraseField from "./PassphraseField";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import Field from '../elements/Field'; import Field from '../elements/Field';
@ -253,10 +254,8 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
}); });
}; };
private onEmailValidate = async fieldState => { private onEmailValidate = (result: IValidationResult) => {
const result = await this.validateEmailRules(fieldState);
this.markFieldValid(RegistrationField.Email, result.valid); this.markFieldValid(RegistrationField.Email, result.valid);
return result;
}; };
private validateEmailRules = withValidation({ private validateEmailRules = withValidation({
@ -426,14 +425,14 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
if (!this.showEmail()) { if (!this.showEmail()) {
return null; return null;
} }
const emailPlaceholder = this.authStepIsRequired('m.login.email.identity') ? const emailLabel = this.authStepIsRequired('m.login.email.identity') ?
_t("Email") : _t("Email") :
_t("Email (optional)"); _t("Email (optional)");
return <Field return <EmailField
ref={field => this[RegistrationField.Email] = field} fieldRef={field => this[RegistrationField.Email] = field}
type="text" label={emailLabel}
label={emailPlaceholder}
value={this.state.email} value={this.state.email}
validationRules={this.validateEmailRules.bind(this)}
onChange={this.onEmailChange} onChange={this.onEmailChange}
onValidate={this.onEmailValidate} onValidate={this.onEmailValidate}
onFocus={() => CountlyAnalytics.instance.track("onboarding_registration_email_focus")} onFocus={() => CountlyAnalytics.instance.track("onboarding_registration_email_focus")}

View file

@ -21,25 +21,14 @@ import { IDialogProps } from "./IDialogProps";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import Field from "../elements/Field"; import Field from "../elements/Field";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import withValidation from "../elements/Validation";
import * as Email from "../../../email";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons"; import DialogButtons from "../elements/DialogButtons";
import EmailField from "../auth/EmailField";
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
onFinished(continued: boolean, email?: string): void; 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<IProps> = ({ onFinished }) => { const RegistrationEmailPromptDialog: React.FC<IProps> = ({ onFinished }) => {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const fieldRef = useRef<Field>(); const fieldRef = useRef<Field>();
@ -47,11 +36,11 @@ const RegistrationEmailPromptDialog: React.FC<IProps> = ({ onFinished }) => {
const onSubmit = async (e) => { const onSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
if (email) { if (email) {
const valid = await fieldRef.current.validate({ allowEmpty: false }); const valid = await fieldRef.current.validate({});
if (!valid) { if (!valid) {
fieldRef.current.focus(); fieldRef.current.focus();
fieldRef.current.validate({ allowEmpty: false, focused: true }); fieldRef.current.validate({ focused: true });
return; return;
} }
} }
@ -72,16 +61,15 @@ const RegistrationEmailPromptDialog: React.FC<IProps> = ({ onFinished }) => {
b: sub => <b>{ sub }</b>, b: sub => <b>{ sub }</b>,
}) }</p> }) }</p>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<Field <EmailField
ref={fieldRef} fieldRef={fieldRef}
autoFocus={true} autoFocus={true}
type="text"
label={_t("Email (optional)")} label={_t("Email (optional)")}
value={email} value={email}
onChange={ev => { onChange={ev => {
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")} onFocus={() => CountlyAnalytics.instance.track("onboarding_registration_email2_focus")}
onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email2_blur")} onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email2_blur")}
/> />

View file

@ -2523,7 +2523,6 @@
"Message edits": "Message edits", "Message edits": "Message edits",
"Modal Widget": "Modal Widget", "Modal Widget": "Modal Widget",
"Data on this screen is shared with %(widgetDomain)s": "Data on this screen is shared with %(widgetDomain)s", "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", "Continuing without email": "Continuing without email",
"Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.": "Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.", "Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.": "Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.",
"Email (optional)": "Email (optional)", "Email (optional)": "Email (optional)",
@ -2738,6 +2737,9 @@
"powered by Matrix": "powered by Matrix", "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.", "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", "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.", "Confirm your identity by entering your account password below.": "Confirm your identity by entering your account password below.",
"Password": "Password", "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.", "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", "Password is allowed, but unsafe": "Password is allowed, but unsafe",
"Keep going...": "Keep going...", "Keep going...": "Keep going...",
"Enter username": "Enter username", "Enter username": "Enter username",
"Enter email address": "Enter email address",
"Enter phone number": "Enter phone number", "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", "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", "Username": "Username",
"Phone": "Phone", "Phone": "Phone",
"Forgot password?": "Forgot password?", "Forgot password?": "Forgot password?",