Convert RegistrationForm to Typescript

This commit is contained in:
Michael Telatynski 2020-11-19 15:10:40 +00:00
parent d45d0c6633
commit 3dcb58f108
3 changed files with 113 additions and 94 deletions

View file

@ -21,9 +21,9 @@ import zxcvbn from "zxcvbn";
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import withValidation, {IFieldState, IValidationResult} from "../elements/Validation"; import withValidation, {IFieldState, IValidationResult} from "../elements/Validation";
import {_t, _td} from "../../../languageHandler"; import {_t, _td} from "../../../languageHandler";
import Field from "../elements/Field"; import Field, {IInputProps} from "../elements/Field";
interface IProps { interface IProps extends Omit<IInputProps, "onValidate"> {
autoFocus?: boolean; autoFocus?: boolean;
id?: string; id?: string;
className?: string; className?: string;

View file

@ -1,8 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2015, 2016, 2017, 2018, 2019, 2020 The Matrix.org Foundation C.I.C.
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,8 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, {ReactNode} from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import * as Email from '../../../email'; import * as Email from '../../../email';
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber'; import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
@ -31,32 +29,57 @@ import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import PassphraseField from "./PassphraseField"; import PassphraseField from "./PassphraseField";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
const FIELD_EMAIL = 'field_email'; enum RegistrationField {
const FIELD_PHONE_NUMBER = 'field_phone_number'; Email = "field_email",
const FIELD_USERNAME = 'field_username'; PhoneNumber = "field_phone_number",
const FIELD_PASSWORD = 'field_password'; Username = "field_username",
const FIELD_PASSWORD_CONFIRM = 'field_password_confirm'; Password = "field_password",
PasswordConfirm = "field_password_confirm",
}
const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario. const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
interface IProps {
// Values pre-filled in the input boxes when the component loads
defaultEmail?: string;
defaultPhoneCountry?: string;
defaultPhoneNumber?: string;
defaultUsername?: string;
defaultPassword?: string;
flows: {
stages: string[];
}[];
serverConfig: ValidatedServerConfig;
canSubmit?: boolean;
serverRequiresIdServer?: boolean;
onRegisterClick(params: {
username: string;
password: string;
email?: string;
phoneCountry?: string;
phoneNumber?: string;
}): Promise<void>;
onEditServerDetailsClick?(): void;
}
interface IState {
// Field error codes by field ID
fieldValid: Partial<Record<RegistrationField, boolean>>;
// The ISO2 country code selected in the phone number entry
phoneCountry: string;
username: string;
email: string;
phoneNumber: string;
password: string;
passwordConfirm: string;
passwordComplexity?: number;
}
/* /*
* A pure UI component which displays a registration form. * A pure UI component which displays a registration form.
*/ */
export default class RegistrationForm extends React.Component { export default class RegistrationForm extends React.PureComponent<IProps, IState> {
static propTypes = {
// Values pre-filled in the input boxes when the component loads
defaultEmail: PropTypes.string,
defaultPhoneCountry: PropTypes.string,
defaultPhoneNumber: PropTypes.string,
defaultUsername: PropTypes.string,
defaultPassword: PropTypes.string,
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
flows: PropTypes.arrayOf(PropTypes.object).isRequired,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
canSubmit: PropTypes.bool,
serverRequiresIdServer: PropTypes.bool,
};
static defaultProps = { static defaultProps = {
onValidationChange: console.error, onValidationChange: console.error,
canSubmit: true, canSubmit: true,
@ -66,9 +89,7 @@ export default class RegistrationForm extends React.Component {
super(props); super(props);
this.state = { this.state = {
// Field error codes by field ID
fieldValid: {}, fieldValid: {},
// The ISO2 country code selected in the phone number entry
phoneCountry: this.props.defaultPhoneCountry, phoneCountry: this.props.defaultPhoneCountry,
username: this.props.defaultUsername || "", username: this.props.defaultUsername || "",
email: this.props.defaultEmail || "", email: this.props.defaultEmail || "",
@ -81,7 +102,7 @@ export default class RegistrationForm extends React.Component {
CountlyAnalytics.instance.track("onboarding_registration_begin"); CountlyAnalytics.instance.track("onboarding_registration_begin");
} }
onSubmit = async ev => { private onSubmit = async ev => {
ev.preventDefault(); ev.preventDefault();
if (!this.props.canSubmit) return; if (!this.props.canSubmit) return;
@ -92,7 +113,6 @@ export default class RegistrationForm extends React.Component {
return; return;
} }
const self = this;
if (this.state.email === '') { if (this.state.email === '') {
const haveIs = Boolean(this.props.serverConfig.isUrl); const haveIs = Boolean(this.props.serverConfig.isUrl);
@ -102,14 +122,14 @@ export default class RegistrationForm extends React.Component {
"No identity server is configured so you cannot add an email address in order to " + "No identity server is configured so you cannot add an email address in order to " +
"reset your password in the future.", "reset your password in the future.",
); );
} else if (this._showEmail()) { } else if (this.showEmail()) {
desc = _t( desc = _t(
"If you don't specify an email address, you won't be able to reset your password. " + "If you don't specify an email address, you won't be able to reset your password. " +
"Are you sure?", "Are you sure?",
); );
} else { } else {
// user can't set an e-mail so don't prompt them to // user can't set an e-mail so don't prompt them to
self._doSubmit(ev); this.doSubmit(ev);
return; return;
} }
@ -120,18 +140,18 @@ export default class RegistrationForm extends React.Component {
title: _t("Warning!"), title: _t("Warning!"),
description: desc, description: desc,
button: _t("Continue"), button: _t("Continue"),
onFinished(confirmed) { onFinished: (confirmed) => {
if (confirmed) { if (confirmed) {
self._doSubmit(ev); this.doSubmit(ev);
} }
}, },
}); });
} else { } else {
self._doSubmit(ev); this.doSubmit(ev);
} }
}; };
_doSubmit(ev) { private doSubmit(ev) {
const email = this.state.email.trim(); const email = this.state.email.trim();
CountlyAnalytics.instance.track("onboarding_registration_submit_ok", { CountlyAnalytics.instance.track("onboarding_registration_submit_ok", {
@ -154,20 +174,20 @@ export default class RegistrationForm extends React.Component {
} }
} }
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 = [
FIELD_USERNAME, RegistrationField.Username,
FIELD_PASSWORD, RegistrationField.Password,
FIELD_PASSWORD_CONFIRM, RegistrationField.PasswordConfirm,
FIELD_EMAIL, RegistrationField.Email,
FIELD_PHONE_NUMBER, RegistrationField.PhoneNumber,
]; ];
// Run all fields with stricter validation that no longer allows empty // Run all fields with stricter validation that no longer allows empty
@ -208,7 +228,7 @@ export default class RegistrationForm extends React.Component {
/** /**
* @returns {boolean} true if all fields were valid last time they were validated. * @returns {boolean} true if all fields were valid last time they were validated.
*/ */
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]]) {
@ -218,7 +238,7 @@ export default class RegistrationForm extends React.Component {
return true; return true;
} }
findFirstInvalidField(fieldIDs) { private findFirstInvalidField(fieldIDs: RegistrationField[]) {
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];
@ -227,7 +247,7 @@ export default class RegistrationForm extends React.Component {
return null; return null;
} }
markFieldValid(fieldID, valid) { private markFieldValid(fieldID: RegistrationField, valid: boolean) {
const { fieldValid } = this.state; const { fieldValid } = this.state;
fieldValid[fieldID] = valid; fieldValid[fieldID] = valid;
this.setState({ this.setState({
@ -235,26 +255,26 @@ export default class RegistrationForm extends React.Component {
}); });
} }
onEmailChange = ev => { private onEmailChange = ev => {
this.setState({ this.setState({
email: ev.target.value, email: ev.target.value,
}); });
}; };
onEmailValidate = async fieldState => { private onEmailValidate = async fieldState => {
const result = await this.validateEmailRules(fieldState); const result = await this.validateEmailRules(fieldState);
this.markFieldValid(FIELD_EMAIL, result.valid); this.markFieldValid(RegistrationField.Email, result.valid);
return result; return result;
}; };
validateEmailRules = withValidation({ private validateEmailRules = withValidation({
description: () => _t("Use an email address to recover your account"), description: () => _t("Use an email address to recover your account"),
hideDescriptionIfValid: true, hideDescriptionIfValid: true,
rules: [ rules: [
{ {
key: "required", key: "required",
test({ value, allowEmpty }) { test(this: RegistrationForm, { value, allowEmpty }) {
return allowEmpty || !this._authStepIsRequired('m.login.email.identity') || !!value; return allowEmpty || !this.authStepIsRequired('m.login.email.identity') || !!value;
}, },
invalid: () => _t("Enter email address (required on this homeserver)"), invalid: () => _t("Enter email address (required on this homeserver)"),
}, },
@ -266,29 +286,29 @@ export default class RegistrationForm extends React.Component {
], ],
}); });
onPasswordChange = ev => { private onPasswordChange = ev => {
this.setState({ this.setState({
password: ev.target.value, password: ev.target.value,
}); });
}; };
onPasswordValidate = result => { private onPasswordValidate = result => {
this.markFieldValid(FIELD_PASSWORD, result.valid); this.markFieldValid(RegistrationField.Password, result.valid);
}; };
onPasswordConfirmChange = ev => { private onPasswordConfirmChange = ev => {
this.setState({ this.setState({
passwordConfirm: ev.target.value, passwordConfirm: ev.target.value,
}); });
}; };
onPasswordConfirmValidate = async fieldState => { private onPasswordConfirmValidate = async fieldState => {
const result = await this.validatePasswordConfirmRules(fieldState); const result = await this.validatePasswordConfirmRules(fieldState);
this.markFieldValid(FIELD_PASSWORD_CONFIRM, result.valid); this.markFieldValid(RegistrationField.PasswordConfirm, result.valid);
return result; return result;
}; };
validatePasswordConfirmRules = withValidation({ private validatePasswordConfirmRules = withValidation({
rules: [ rules: [
{ {
key: "required", key: "required",
@ -297,41 +317,40 @@ export default class RegistrationForm extends React.Component {
}, },
{ {
key: "match", key: "match",
test({ value }) { test(this: RegistrationForm, { value }) {
return !value || value === this.state.password; return !value || value === this.state.password;
}, },
invalid: () => _t("Passwords don't match"), invalid: () => _t("Passwords don't match"),
}, },
], ],
}); });
onPhoneCountryChange = newVal => { private onPhoneCountryChange = newVal => {
this.setState({ this.setState({
phoneCountry: newVal.iso2, phoneCountry: newVal.iso2,
phonePrefix: newVal.prefix,
}); });
}; };
onPhoneNumberChange = ev => { private onPhoneNumberChange = ev => {
this.setState({ this.setState({
phoneNumber: ev.target.value, phoneNumber: ev.target.value,
}); });
}; };
onPhoneNumberValidate = async fieldState => { private onPhoneNumberValidate = async fieldState => {
const result = await this.validatePhoneNumberRules(fieldState); const result = await this.validatePhoneNumberRules(fieldState);
this.markFieldValid(FIELD_PHONE_NUMBER, result.valid); this.markFieldValid(RegistrationField.PhoneNumber, result.valid);
return result; return result;
}; };
validatePhoneNumberRules = withValidation({ private validatePhoneNumberRules = withValidation({
description: () => _t("Other users can invite you to rooms using your contact details"), description: () => _t("Other users can invite you to rooms using your contact details"),
hideDescriptionIfValid: true, hideDescriptionIfValid: true,
rules: [ rules: [
{ {
key: "required", key: "required",
test({ value, allowEmpty }) { test(this: RegistrationForm, { value, allowEmpty }) {
return allowEmpty || !this._authStepIsRequired('m.login.msisdn') || !!value; return allowEmpty || !this.authStepIsRequired('m.login.msisdn') || !!value;
}, },
invalid: () => _t("Enter phone number (required on this homeserver)"), invalid: () => _t("Enter phone number (required on this homeserver)"),
}, },
@ -343,19 +362,19 @@ export default class RegistrationForm extends React.Component {
], ],
}); });
onUsernameChange = ev => { private onUsernameChange = ev => {
this.setState({ this.setState({
username: ev.target.value, username: ev.target.value,
}); });
}; };
onUsernameValidate = async fieldState => { private onUsernameValidate = async fieldState => {
const result = await this.validateUsernameRules(fieldState); const result = await this.validateUsernameRules(fieldState);
this.markFieldValid(FIELD_USERNAME, result.valid); this.markFieldValid(RegistrationField.Username, result.valid);
return result; return result;
}; };
validateUsernameRules = withValidation({ private validateUsernameRules = withValidation({
description: () => _t("Use lowercase letters, numbers, dashes and underscores only"), description: () => _t("Use lowercase letters, numbers, dashes and underscores only"),
hideDescriptionIfValid: true, hideDescriptionIfValid: true,
rules: [ rules: [
@ -378,7 +397,7 @@ export default class RegistrationForm extends React.Component {
* @param {string} step A stage name to check * @param {string} step A stage name to check
* @returns {boolean} Whether it is required * @returns {boolean} Whether it is required
*/ */
_authStepIsRequired(step) { private authStepIsRequired(step: string) {
return this.props.flows.every((flow) => { return this.props.flows.every((flow) => {
return flow.stages.includes(step); return flow.stages.includes(step);
}); });
@ -390,46 +409,46 @@ export default class RegistrationForm extends React.Component {
* @param {string} step A stage name to check * @param {string} step A stage name to check
* @returns {boolean} Whether it is used * @returns {boolean} Whether it is used
*/ */
_authStepIsUsed(step) { private authStepIsUsed(step: string) {
return this.props.flows.some((flow) => { return this.props.flows.some((flow) => {
return flow.stages.includes(step); return flow.stages.includes(step);
}); });
} }
_showEmail() { private showEmail() {
const haveIs = Boolean(this.props.serverConfig.isUrl); const haveIs = Boolean(this.props.serverConfig.isUrl);
if ( if (
(this.props.serverRequiresIdServer && !haveIs) || (this.props.serverRequiresIdServer && !haveIs) ||
!this._authStepIsUsed('m.login.email.identity') !this.authStepIsUsed('m.login.email.identity')
) { ) {
return false; return false;
} }
return true; return true;
} }
_showPhoneNumber() { private showPhoneNumber() {
const threePidLogin = !SdkConfig.get().disable_3pid_login; const threePidLogin = !SdkConfig.get().disable_3pid_login;
const haveIs = Boolean(this.props.serverConfig.isUrl); const haveIs = Boolean(this.props.serverConfig.isUrl);
if ( if (
!threePidLogin || !threePidLogin ||
(this.props.serverRequiresIdServer && !haveIs) || (this.props.serverRequiresIdServer && !haveIs) ||
!this._authStepIsUsed('m.login.msisdn') !this.authStepIsUsed('m.login.msisdn')
) { ) {
return false; return false;
} }
return true; return true;
} }
renderEmail() { private renderEmail() {
if (!this._showEmail()) { if (!this.showEmail()) {
return null; return null;
} }
const Field = sdk.getComponent('elements.Field'); const Field = sdk.getComponent('elements.Field');
const emailPlaceholder = this._authStepIsRequired('m.login.email.identity') ? const emailPlaceholder = this.authStepIsRequired('m.login.email.identity') ?
_t("Email") : _t("Email") :
_t("Email (optional)"); _t("Email (optional)");
return <Field return <Field
ref={field => this[FIELD_EMAIL] = field} ref={field => this[RegistrationField.Email] = field}
type="text" type="text"
label={emailPlaceholder} label={emailPlaceholder}
value={this.state.email} value={this.state.email}
@ -440,10 +459,10 @@ export default class RegistrationForm extends React.Component {
/>; />;
} }
renderPassword() { private renderPassword() {
return <PassphraseField return <PassphraseField
id="mx_RegistrationForm_password" id="mx_RegistrationForm_password"
fieldRef={field => this[FIELD_PASSWORD] = field} fieldRef={field => this[RegistrationField.Password] = field}
minScore={PASSWORD_MIN_SCORE} minScore={PASSWORD_MIN_SCORE}
value={this.state.password} value={this.state.password}
onChange={this.onPasswordChange} onChange={this.onPasswordChange}
@ -457,7 +476,7 @@ export default class RegistrationForm extends React.Component {
const Field = sdk.getComponent('elements.Field'); const Field = sdk.getComponent('elements.Field');
return <Field return <Field
id="mx_RegistrationForm_passwordConfirm" id="mx_RegistrationForm_passwordConfirm"
ref={field => this[FIELD_PASSWORD_CONFIRM] = field} ref={field => this[RegistrationField.PasswordConfirm] = field}
type="password" type="password"
autoComplete="new-password" autoComplete="new-password"
label={_t("Confirm password")} label={_t("Confirm password")}
@ -470,12 +489,12 @@ export default class RegistrationForm extends React.Component {
} }
renderPhoneNumber() { renderPhoneNumber() {
if (!this._showPhoneNumber()) { if (!this.showPhoneNumber()) {
return null; return null;
} }
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown'); const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
const Field = sdk.getComponent('elements.Field'); const Field = sdk.getComponent('elements.Field');
const phoneLabel = this._authStepIsRequired('m.login.msisdn') ? const phoneLabel = this.authStepIsRequired('m.login.msisdn') ?
_t("Phone") : _t("Phone") :
_t("Phone (optional)"); _t("Phone (optional)");
const phoneCountry = <CountryDropdown const phoneCountry = <CountryDropdown
@ -485,7 +504,7 @@ export default class RegistrationForm extends React.Component {
onOptionChange={this.onPhoneCountryChange} onOptionChange={this.onPhoneCountryChange}
/>; />;
return <Field return <Field
ref={field => this[FIELD_PHONE_NUMBER] = field} ref={field => this[RegistrationField.PhoneNumber] = field}
type="text" type="text"
label={phoneLabel} label={phoneLabel}
value={this.state.phoneNumber} value={this.state.phoneNumber}
@ -499,7 +518,7 @@ export default class RegistrationForm extends React.Component {
const Field = sdk.getComponent('elements.Field'); const Field = sdk.getComponent('elements.Field');
return <Field return <Field
id="mx_RegistrationForm_username" id="mx_RegistrationForm_username"
ref={field => this[FIELD_USERNAME] = field} ref={field => this[RegistrationField.Username] = field}
type="text" type="text"
autoFocus={true} autoFocus={true}
label={_t("Username")} label={_t("Username")}
@ -517,8 +536,8 @@ export default class RegistrationForm extends React.Component {
); );
let emailHelperText = null; let emailHelperText = null;
if (this._showEmail()) { if (this.showEmail()) {
if (this._showPhoneNumber()) { if (this.showPhoneNumber()) {
emailHelperText = <div> emailHelperText = <div>
{_t( {_t(
"Set an email for account recovery. " + "Set an email for account recovery. " +

View file

@ -64,7 +64,7 @@ interface IProps {
// All other props pass through to the <input>. // All other props pass through to the <input>.
} }
interface IInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> { export interface IInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> {
// The element to create. Defaults to "input". // The element to create. Defaults to "input".
element?: "input"; element?: "input";
// The input's value. This is a controlled component, so the value is required. // The input's value. This is a controlled component, so the value is required.