Convert PasswordLogin to Typescript

This commit is contained in:
Michael Telatynski 2020-11-18 13:44:32 +00:00
parent 85fbc6d89f
commit 7397cebbea
2 changed files with 106 additions and 124 deletions

View file

@ -1,7 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016, 2017, 2019 New Vector Ltd.
Copyright 2017 Vector Creations Ltd
Copyright 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -17,9 +15,8 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
@ -27,78 +24,78 @@ import AccessibleButton from "../elements/AccessibleButton";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import withValidation from "../elements/Validation"; import withValidation from "../elements/Validation";
import * as Email from "../../../email"; import * as Email from "../../../email";
import Field from "../elements/Field";
import CountryDropdown from "./CountryDropdown";
import SignInToText from "./SignInToText";
// 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]*$/;
interface IProps {
username: string; // also used for email address
phoneCountry: string;
phoneNumber: string;
serverConfig: ValidatedServerConfig;
loginIncorrect?: boolean;
disableSubmit?: boolean;
busy?: boolean;
onSubmit(username: string, phoneCountry: void, phoneNumber: void, password: string): void;
onSubmit(username: void, phoneCountry: string, phoneNumber: string, password: string): void;
onUsernameChanged?(username: string): void;
onUsernameBlur?(username: string): void;
onPhoneCountryChanged?(phoneCountry: string): void;
onPhoneNumberChanged?(phoneNumber: string): void;
onEditServerDetailsClick?(): void;
onForgotPasswordClick?(): void;
}
interface IState {
fieldValid: Partial<Record<LoginField, boolean>>;
loginType: LoginField.Email | LoginField.MatrixId | LoginField.Phone,
password: "",
}
enum LoginField {
Email = "login_field_email",
MatrixId = "login_field_mxid",
Phone = "login_field_phone",
Password = "login_field_phone",
}
/* /*
* A pure UI component which displays a username/password form. * A pure UI component which displays a username/password form.
* The email/username/phone fields are fully-controlled, the password field is not. * The email/username/phone fields are fully-controlled, the password field is not.
*/ */
export default class PasswordLogin extends React.Component { export default class PasswordLogin extends React.PureComponent<IProps, IState> {
static propTypes = {
onSubmit: PropTypes.func.isRequired, // fn(username, password)
onEditServerDetailsClick: PropTypes.func,
onForgotPasswordClick: PropTypes.func, // fn()
username: PropTypes.string,
phoneCountry: PropTypes.string,
phoneNumber: PropTypes.string,
onUsernameChanged: PropTypes.func,
onPhoneCountryChanged: PropTypes.func,
onPhoneNumberChanged: PropTypes.func,
loginIncorrect: PropTypes.bool,
disableSubmit: PropTypes.bool,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
busy: PropTypes.bool,
};
static defaultProps = { static defaultProps = {
onEditServerDetailsClick: null, onEditServerDetailsClick: null,
onUsernameChanged: function() {}, onUsernameChanged: function() {},
onUsernameBlur: function() {}, onUsernameBlur: function() {},
onPhoneCountryChanged: function() {}, onPhoneCountryChanged: function() {},
onPhoneNumberChanged: function() {}, onPhoneNumberChanged: function() {},
username: "",
phoneCountry: "",
phoneNumber: "",
loginIncorrect: false, loginIncorrect: false,
disableSubmit: false, disableSubmit: false,
}; };
static LOGIN_FIELD_EMAIL = "login_field_email";
static LOGIN_FIELD_MXID = "login_field_mxid";
static LOGIN_FIELD_PHONE = "login_field_phone";
static LOGIN_FIELD_PASSWORD = "login_field_password";
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
// Field error codes by field ID // Field error codes by field ID
fieldValid: {}, fieldValid: {},
loginType: PasswordLogin.LOGIN_FIELD_MXID, loginType: LoginField.MatrixId,
password: "", password: "",
}; };
this.onForgotPasswordClick = this.onForgotPasswordClick.bind(this);
this.onSubmitForm = this.onSubmitForm.bind(this);
this.onUsernameFocus = this.onUsernameFocus.bind(this);
this.onUsernameChanged = this.onUsernameChanged.bind(this);
this.onUsernameBlur = this.onUsernameBlur.bind(this);
this.onLoginTypeChange = this.onLoginTypeChange.bind(this);
this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this);
this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this);
this.onPhoneNumberBlur = this.onPhoneNumberBlur.bind(this);
this.onPasswordChanged = this.onPasswordChanged.bind(this);
this.isLoginEmpty = this.isLoginEmpty.bind(this);
} }
onForgotPasswordClick(ev) { private onForgotPasswordClick = ev => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.props.onForgotPasswordClick(); this.props.onForgotPasswordClick();
} };
async onSubmitForm(ev) { private onSubmitForm = async ev => {
ev.preventDefault(); ev.preventDefault();
const allFieldsValid = await this.verifyFieldsBeforeSubmit(); const allFieldsValid = await this.verifyFieldsBeforeSubmit();
@ -112,83 +109,78 @@ export default class PasswordLogin extends React.Component {
let phoneNumber = null; let phoneNumber = null;
switch (this.state.loginType) { switch (this.state.loginType) {
case PasswordLogin.LOGIN_FIELD_EMAIL: case LoginField.Email:
case PasswordLogin.LOGIN_FIELD_MXID: case LoginField.MatrixId:
username = this.props.username; username = this.props.username;
break; break;
case PasswordLogin.LOGIN_FIELD_PHONE: case LoginField.Phone:
phoneCountry = this.props.phoneCountry; phoneCountry = this.props.phoneCountry;
phoneNumber = this.props.phoneNumber; phoneNumber = this.props.phoneNumber;
break; break;
} }
this.props.onSubmit( this.props.onSubmit(username, phoneCountry, phoneNumber, this.state.password);
username, };
phoneCountry,
phoneNumber,
this.state.password,
);
}
onUsernameChanged(ev) { private onUsernameChanged = ev => {
this.props.onUsernameChanged(ev.target.value); this.props.onUsernameChanged(ev.target.value);
} };
onUsernameFocus() { private onUsernameFocus = () => {
if (this.state.loginType === PasswordLogin.LOGIN_FIELD_MXID) { if (this.state.loginType === LoginField.MatrixId) {
CountlyAnalytics.instance.track("onboarding_login_mxid_focus"); CountlyAnalytics.instance.track("onboarding_login_mxid_focus");
} else { } else {
CountlyAnalytics.instance.track("onboarding_login_email_focus"); CountlyAnalytics.instance.track("onboarding_login_email_focus");
} }
} };
onUsernameBlur(ev) { private onUsernameBlur = ev => {
if (this.state.loginType === PasswordLogin.LOGIN_FIELD_MXID) { if (this.state.loginType === LoginField.MatrixId) {
CountlyAnalytics.instance.track("onboarding_login_mxid_blur"); CountlyAnalytics.instance.track("onboarding_login_mxid_blur");
} else { } else {
CountlyAnalytics.instance.track("onboarding_login_email_blur"); CountlyAnalytics.instance.track("onboarding_login_email_blur");
} }
this.props.onUsernameBlur(ev.target.value); this.props.onUsernameBlur(ev.target.value);
} };
onLoginTypeChange(ev) { private onLoginTypeChange = ev => {
const loginType = ev.target.value; const loginType = ev.target.value;
this.setState({ loginType }); this.setState({ loginType });
this.props.onUsernameChanged(""); // Reset because email and username use the same state this.props.onUsernameChanged(""); // Reset because email and username use the same state
CountlyAnalytics.instance.track("onboarding_login_type_changed", { loginType }); CountlyAnalytics.instance.track("onboarding_login_type_changed", { loginType });
} };
onPhoneCountryChanged(country) { private onPhoneCountryChanged = country => {
this.props.onPhoneCountryChanged(country.iso2); this.props.onPhoneCountryChanged(country.iso2);
} };
onPhoneNumberChanged(ev) { private onPhoneNumberChanged = ev => {
this.props.onPhoneNumberChanged(ev.target.value); this.props.onPhoneNumberChanged(ev.target.value);
} };
onPhoneNumberFocus() { private onPhoneNumberFocus = () => {
CountlyAnalytics.instance.track("onboarding_login_phone_number_focus"); CountlyAnalytics.instance.track("onboarding_login_phone_number_focus");
} };
onPhoneNumberBlur(ev) { private onPhoneNumberBlur = ev => {
CountlyAnalytics.instance.track("onboarding_login_phone_number_blur"); CountlyAnalytics.instance.track("onboarding_login_phone_number_blur");
} };
onPasswordChanged(ev) { private onPasswordChanged = ev => {
this.setState({password: ev.target.value}); this.setState({password: ev.target.value});
} };
async verifyFieldsBeforeSubmit() { private async verifyFieldsBeforeSubmit() {
// Blur the active element if any, so we first run its blur validation, // Blur the active element if any, so we first run its blur validation,
// which is less strict than the pass we're about to do below for all fields. // which is less strict than the pass we're about to do below for all fields.
const activeElement = document.activeElement; const activeElement = document.activeElement as HTMLElement;
if (activeElement) { if (activeElement) {
activeElement.blur(); activeElement.blur();
} }
const fieldIDsInDisplayOrder = [ const fieldIDsInDisplayOrder = [
this.state.loginType, this.state.loginType,
PasswordLogin.LOGIN_FIELD_PASSWORD, LoginField.Password,
]; ];
// Run all fields with stricter validation that no longer allows empty // Run all fields with stricter validation that no longer allows empty
@ -226,7 +218,7 @@ export default class PasswordLogin extends React.Component {
return false; return false;
} }
allFieldsValid() { private allFieldsValid() {
const keys = Object.keys(this.state.fieldValid); const keys = Object.keys(this.state.fieldValid);
for (let i = 0; i < keys.length; ++i) { for (let i = 0; i < keys.length; ++i) {
if (!this.state.fieldValid[keys[i]]) { if (!this.state.fieldValid[keys[i]]) {
@ -236,7 +228,7 @@ export default class PasswordLogin extends React.Component {
return true; return true;
} }
findFirstInvalidField(fieldIDs) { private findFirstInvalidField(fieldIDs: LoginField[]) {
for (const fieldID of fieldIDs) { for (const fieldID of fieldIDs) {
if (!this.state.fieldValid[fieldID] && this[fieldID]) { if (!this.state.fieldValid[fieldID] && this[fieldID]) {
return this[fieldID]; return this[fieldID];
@ -245,7 +237,7 @@ export default class PasswordLogin extends React.Component {
return null; return null;
} }
markFieldValid(fieldID, valid) { private markFieldValid(fieldID: LoginField, valid: boolean) {
const { fieldValid } = this.state; const { fieldValid } = this.state;
fieldValid[fieldID] = valid; fieldValid[fieldID] = valid;
this.setState({ this.setState({
@ -253,7 +245,7 @@ export default class PasswordLogin extends React.Component {
}); });
} }
validateUsernameRules = withValidation({ private validateUsernameRules = withValidation({
rules: [ rules: [
{ {
key: "required", key: "required",
@ -265,13 +257,13 @@ export default class PasswordLogin extends React.Component {
], ],
}); });
onUsernameValidate = async (fieldState) => { private onUsernameValidate = async (fieldState) => {
const result = await this.validateUsernameRules(fieldState); const result = await this.validateUsernameRules(fieldState);
this.markFieldValid(PasswordLogin.LOGIN_FIELD_MXID, result.valid); this.markFieldValid(LoginField.MatrixId, result.valid);
return result; return result;
}; };
validateEmailRules = withValidation({ private validateEmailRules = withValidation({
rules: [ rules: [
{ {
key: "required", key: "required",
@ -287,13 +279,13 @@ export default class PasswordLogin extends React.Component {
], ],
}); });
onEmailValidate = async (fieldState) => { private onEmailValidate = async (fieldState) => {
const result = await this.validateEmailRules(fieldState); const result = await this.validateEmailRules(fieldState);
this.markFieldValid(PasswordLogin.LOGIN_FIELD_EMAIL, result.valid); this.markFieldValid(LoginField.Email, result.valid);
return result; return result;
}; };
validatePhoneNumberRules = withValidation({ private validatePhoneNumberRules = withValidation({
rules: [ rules: [
{ {
key: "required", key: "required",
@ -309,13 +301,13 @@ export default class PasswordLogin extends React.Component {
], ],
}); });
onPhoneNumberValidate = async (fieldState) => { private onPhoneNumberValidate = async (fieldState) => {
const result = await this.validatePhoneNumberRules(fieldState); const result = await this.validatePhoneNumberRules(fieldState);
this.markFieldValid(PasswordLogin.LOGIN_FIELD_PHONE, result.valid); this.markFieldValid(LoginField.Password, result.valid);
return result; return result;
}; };
validatePasswordRules = withValidation({ private validatePasswordRules = withValidation({
rules: [ rules: [
{ {
key: "required", key: "required",
@ -327,19 +319,19 @@ export default class PasswordLogin extends React.Component {
], ],
}); });
onPasswordValidate = async (fieldState) => { private onPasswordValidate = async (fieldState) => {
const result = await this.validatePasswordRules(fieldState); const result = await this.validatePasswordRules(fieldState);
this.markFieldValid(PasswordLogin.LOGIN_FIELD_PASSWORD, result.valid); this.markFieldValid(LoginField.Password, result.valid);
return result; return result;
} }
renderLoginField(loginType, autoFocus) { private renderLoginField(loginType: IState["loginType"], autoFocus: boolean) {
const Field = sdk.getComponent('elements.Field'); const classes = {
error: false,
const classes = {}; };
switch (loginType) { switch (loginType) {
case PasswordLogin.LOGIN_FIELD_EMAIL: case LoginField.Email:
classes.error = this.props.loginIncorrect && !this.props.username; classes.error = this.props.loginIncorrect && !this.props.username;
return <Field return <Field
className={classNames(classes)} className={classNames(classes)}
@ -355,9 +347,9 @@ export default class PasswordLogin extends React.Component {
disabled={this.props.disableSubmit} disabled={this.props.disableSubmit}
autoFocus={autoFocus} autoFocus={autoFocus}
onValidate={this.onEmailValidate} onValidate={this.onEmailValidate}
ref={field => this[PasswordLogin.LOGIN_FIELD_EMAIL] = field} ref={field => this[LoginField.Email] = field}
/>; />;
case PasswordLogin.LOGIN_FIELD_MXID: case LoginField.MatrixId:
classes.error = this.props.loginIncorrect && !this.props.username; classes.error = this.props.loginIncorrect && !this.props.username;
return <Field return <Field
className={classNames(classes)} className={classNames(classes)}
@ -372,10 +364,9 @@ export default class PasswordLogin extends React.Component {
disabled={this.props.disableSubmit} disabled={this.props.disableSubmit}
autoFocus={autoFocus} autoFocus={autoFocus}
onValidate={this.onUsernameValidate} onValidate={this.onUsernameValidate}
ref={field => this[PasswordLogin.LOGIN_FIELD_MXID] = field} ref={field => this[LoginField.MatrixId] = field}
/>; />;
case PasswordLogin.LOGIN_FIELD_PHONE: { case LoginField.Phone: {
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
classes.error = this.props.loginIncorrect && !this.props.phoneNumber; classes.error = this.props.loginIncorrect && !this.props.phoneNumber;
const phoneCountry = <CountryDropdown const phoneCountry = <CountryDropdown
@ -399,26 +390,23 @@ export default class PasswordLogin extends React.Component {
disabled={this.props.disableSubmit} disabled={this.props.disableSubmit}
autoFocus={autoFocus} autoFocus={autoFocus}
onValidate={this.onPhoneNumberValidate} onValidate={this.onPhoneNumberValidate}
ref={field => this[PasswordLogin.LOGIN_FIELD_PHONE] = field} ref={field => this[LoginField.Password] = field}
/>; />;
} }
} }
} }
isLoginEmpty() { private isLoginEmpty() {
switch (this.state.loginType) { switch (this.state.loginType) {
case PasswordLogin.LOGIN_FIELD_EMAIL: case LoginField.Email:
case PasswordLogin.LOGIN_FIELD_MXID: case LoginField.MatrixId:
return !this.props.username; return !this.props.username;
case PasswordLogin.LOGIN_FIELD_PHONE: case LoginField.Phone:
return !this.props.phoneCountry || !this.props.phoneNumber; return !this.props.phoneCountry || !this.props.phoneNumber;
} }
} }
render() { render() {
const Field = sdk.getComponent('elements.Field');
const SignInToText = sdk.getComponent('views.auth.SignInToText');
let forgotPasswordJsx; let forgotPasswordJsx;
if (this.props.onForgotPasswordClick) { if (this.props.onForgotPasswordClick) {
@ -458,22 +446,16 @@ export default class PasswordLogin extends React.Component {
onChange={this.onLoginTypeChange} onChange={this.onLoginTypeChange}
disabled={this.props.disableSubmit} disabled={this.props.disableSubmit}
> >
<option <option key={LoginField.MatrixId} value={LoginField.MatrixId}>
key={PasswordLogin.LOGIN_FIELD_MXID}
value={PasswordLogin.LOGIN_FIELD_MXID}
>
{_t('Username')} {_t('Username')}
</option> </option>
<option <option
key={PasswordLogin.LOGIN_FIELD_EMAIL} key={LoginField.Email}
value={PasswordLogin.LOGIN_FIELD_EMAIL} value={LoginField.Email}
> >
{_t('Email address')} {_t('Email address')}
</option> </option>
<option <option key={LoginField.Password} value={LoginField.Password}>
key={PasswordLogin.LOGIN_FIELD_PHONE}
value={PasswordLogin.LOGIN_FIELD_PHONE}
>
{_t('Phone')} {_t('Phone')}
</option> </option>
</Field> </Field>
@ -498,7 +480,7 @@ export default class PasswordLogin extends React.Component {
disabled={this.props.disableSubmit} disabled={this.props.disableSubmit}
autoFocus={autoFocusPassword} autoFocus={autoFocusPassword}
onValidate={this.onPasswordValidate} onValidate={this.onPasswordValidate}
ref={field => this[PasswordLogin.LOGIN_FIELD_PASSWORD] = field} ref={field => this[LoginField.Password] = field}
/> />
{forgotPasswordJsx} {forgotPasswordJsx}
{ !this.props.busy && <input className="mx_Login_submit" { !this.props.busy && <input className="mx_Login_submit"

View file

@ -32,7 +32,7 @@ interface IRule<T, D = void> {
interface IArgs<T, D = void> { interface IArgs<T, D = void> {
rules: IRule<T, D>[]; rules: IRule<T, D>[];
description(this: T, derivedData: D): React.ReactChild; description?(this: T, derivedData: D): React.ReactChild;
hideDescriptionIfValid?: boolean; hideDescriptionIfValid?: boolean;
deriveData?(data: Data): Promise<D>; deriveData?(data: Data): Promise<D>;
} }