From 0b74d3a0efcdbc844ce82442d93c2509b1006388 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 18 Nov 2020 12:28:46 +0000 Subject: [PATCH 1/7] Use field validation for PasswordLogin instead of global errors --- src/components/structures/auth/Login.js | 27 --- src/components/views/auth/PasswordLogin.js | 207 ++++++++++++++++++--- src/i18n/strings/en_EN.json | 13 +- 3 files changed, 184 insertions(+), 63 deletions(-) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index c3cbac0442..3aba4a571e 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -32,9 +32,6 @@ import SettingsStore from "../../../settings/SettingsStore"; import {UIFeature} from "../../../settings/UIFeature"; import CountlyAnalytics from "../../../CountlyAnalytics"; -// For validating phone numbers without country codes -const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; - // Phases // Show controls to configure server details const PHASE_SERVER_DETAILS = 0; @@ -151,13 +148,6 @@ export default class LoginComponent extends React.Component { this._initLoginLogic(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl); } - onPasswordLoginError = errorText => { - this.setState({ - errorText, - loginIncorrect: Boolean(errorText), - }); - }; - isBusy = () => this.state.busy || this.props.busy; onPasswordLogin = async (username, phoneCountry, phoneNumber, password) => { @@ -330,21 +320,6 @@ export default class LoginComponent extends React.Component { }); }; - onPhoneNumberBlur = phoneNumber => { - // Validate the phone number entered - if (!PHONE_NUMBER_REGEX.test(phoneNumber)) { - this.setState({ - errorText: _t('The phone number entered looks invalid'), - canTryLogin: false, - }); - } else { - this.setState({ - errorText: null, - canTryLogin: true, - }); - } - }; - onRegisterClick = ev => { ev.preventDefault(); ev.stopPropagation(); @@ -590,7 +565,6 @@ export default class LoginComponent extends React.Component { return ( this.setState({}, resolve)); + + if (this.allFieldsValid()) { + return true; + } + + const invalidField = this.findFirstInvalidField(fieldIDsInDisplayOrder); + + if (!invalidField) { + return true; + } + + // Focus the first invalid field and show feedback in the stricter mode + // that no longer allows empty values for required fields. + invalidField.focus(); + invalidField.validate({ allowEmpty: false, focused: true }); + return false; + } + + allFieldsValid() { + const keys = Object.keys(this.state.fieldValid); + for (let i = 0; i < keys.length; ++i) { + if (!this.state.fieldValid[keys[i]]) { + return false; + } + } + return true; + } + + findFirstInvalidField(fieldIDs) { + for (const fieldID of fieldIDs) { + if (!this.state.fieldValid[fieldID] && this[fieldID]) { + return this[fieldID]; + } + } + return null; + } + + markFieldValid(fieldID, valid) { + const { fieldValid } = this.state; + fieldValid[fieldID] = valid; + this.setState({ + fieldValid, + }); + } + + validateUsernameRules = withValidation({ + rules: [ + { + key: "required", + test({ value, allowEmpty }) { + return allowEmpty || !!value; + }, + invalid: () => _t("Enter username"), + }, + ], + }); + + onUsernameValidate = async (fieldState) => { + const result = await this.validateUsernameRules(fieldState); + this.markFieldValid(PasswordLogin.LOGIN_FIELD_MXID, result.valid); + return result; + }; + + 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"), + }, + ], + }); + + onEmailValidate = async (fieldState) => { + const result = await this.validateEmailRules(fieldState); + this.markFieldValid(PasswordLogin.LOGIN_FIELD_EMAIL, result.valid); + return result; + }; + + validatePhoneNumberRules = withValidation({ + rules: [ + { + key: "required", + test({ value, allowEmpty }) { + return allowEmpty || !!value; + }, + invalid: () => _t("Enter phone number"), + }, { + key: "number", + test: ({ value }) => !value || PHONE_NUMBER_REGEX.test(value), + invalid: () => _t("Doesn't look like a valid phone number"), + }, + ], + }); + + onPhoneNumberValidate = async (fieldState) => { + const result = await this.validatePhoneNumberRules(fieldState); + this.markFieldValid(PasswordLogin.LOGIN_FIELD_PHONE, result.valid); + return result; + }; + + validatePasswordRules = withValidation({ + rules: [ + { + key: "required", + test({ value, allowEmpty }) { + return allowEmpty || !!value; + }, + invalid: () => _t("Enter password"), + }, + ], + }); + + onPasswordValidate = async (fieldState) => { + const result = await this.validatePasswordRules(fieldState); + this.markFieldValid(PasswordLogin.LOGIN_FIELD_PASSWORD, result.valid); + return result; + } + renderLoginField(loginType, autoFocus) { const Field = sdk.getComponent('elements.Field'); @@ -226,6 +369,8 @@ export default class PasswordLogin extends React.Component { onBlur={this.onUsernameBlur} disabled={this.props.disableSubmit} autoFocus={autoFocus} + onValidate={this.onEmailValidate} + ref={field => this[PasswordLogin.LOGIN_FIELD_EMAIL] = field} />; case PasswordLogin.LOGIN_FIELD_MXID: classes.error = this.props.loginIncorrect && !this.state.username; @@ -241,6 +386,8 @@ export default class PasswordLogin extends React.Component { onBlur={this.onUsernameBlur} disabled={this.props.disableSubmit} autoFocus={autoFocus} + onValidate={this.onUsernameValidate} + ref={field => this[PasswordLogin.LOGIN_FIELD_MXID] = field} />; case PasswordLogin.LOGIN_FIELD_PHONE: { const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown'); @@ -266,6 +413,8 @@ export default class PasswordLogin extends React.Component { onBlur={this.onPhoneNumberBlur} disabled={this.props.disableSubmit} autoFocus={autoFocus} + onValidate={this.onPhoneNumberValidate} + ref={field => this[PasswordLogin.LOGIN_FIELD_PHONE] = field} />; } } @@ -363,6 +512,8 @@ export default class PasswordLogin extends React.Component { onChange={this.onPasswordChanged} disabled={this.props.disableSubmit} autoFocus={autoFocusPassword} + onValidate={this.onPasswordValidate} + ref={field => this[PasswordLogin.LOGIN_FIELD_PASSWORD] = field} /> {forgotPasswordJsx} { !this.props.busy && enable unsafe scripts.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.", From 85fbc6d89ffccac8d7809de07eb1d6338fb03251 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 18 Nov 2020 12:38:56 +0000 Subject: [PATCH 2/7] Consolidate PasswordLogin state and input control/ownership previously data was stored in two places which drifted --- src/components/structures/auth/Login.js | 6 +-- src/components/views/auth/PasswordLogin.js | 61 ++++++++-------------- 2 files changed, 26 insertions(+), 41 deletions(-) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 3aba4a571e..a5ecbe36c7 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -566,9 +566,9 @@ export default class LoginComponent extends React.Component { this[PasswordLogin.LOGIN_FIELD_EMAIL] = field} />; case PasswordLogin.LOGIN_FIELD_MXID: - classes.error = this.props.loginIncorrect && !this.state.username; + classes.error = this.props.loginIncorrect && !this.props.username; return ; case PasswordLogin.LOGIN_FIELD_PHONE: { const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown'); - classes.error = this.props.loginIncorrect && !this.state.phoneNumber; + classes.error = this.props.loginIncorrect && !this.props.phoneNumber; const phoneCountry = Date: Wed, 18 Nov 2020 13:44:32 +0000 Subject: [PATCH 3/7] Convert PasswordLogin to Typescript --- .../{PasswordLogin.js => PasswordLogin.tsx} | 228 ++++++++---------- src/components/views/elements/Validation.tsx | 2 +- 2 files changed, 106 insertions(+), 124 deletions(-) rename src/components/views/auth/{PasswordLogin.js => PasswordLogin.tsx} (72%) diff --git a/src/components/views/auth/PasswordLogin.js b/src/components/views/auth/PasswordLogin.tsx similarity index 72% rename from src/components/views/auth/PasswordLogin.js rename to src/components/views/auth/PasswordLogin.tsx index 3d374d57e7..fced2e08d0 100644 --- a/src/components/views/auth/PasswordLogin.js +++ b/src/components/views/auth/PasswordLogin.tsx @@ -1,7 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2019 New Vector Ltd. +Copyright 2015, 2016, 2017, 2019 New Vector Ltd. Licensed under the Apache License, Version 2.0 (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 PropTypes from 'prop-types'; import classNames from 'classnames'; -import * as sdk from '../../../index'; + import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; @@ -27,78 +24,78 @@ import AccessibleButton from "../elements/AccessibleButton"; import CountlyAnalytics from "../../../CountlyAnalytics"; import withValidation from "../elements/Validation"; 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 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>; + 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. * The email/username/phone fields are fully-controlled, the password field is not. */ -export default class PasswordLogin extends React.Component { - 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, - }; - +export default class PasswordLogin extends React.PureComponent { static defaultProps = { onEditServerDetailsClick: null, onUsernameChanged: function() {}, onUsernameBlur: function() {}, onPhoneCountryChanged: function() {}, onPhoneNumberChanged: function() {}, - username: "", - phoneCountry: "", - phoneNumber: "", loginIncorrect: 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) { super(props); this.state = { // Field error codes by field ID fieldValid: {}, - loginType: PasswordLogin.LOGIN_FIELD_MXID, + loginType: LoginField.MatrixId, 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.stopPropagation(); this.props.onForgotPasswordClick(); - } + }; - async onSubmitForm(ev) { + private onSubmitForm = async ev => { ev.preventDefault(); const allFieldsValid = await this.verifyFieldsBeforeSubmit(); @@ -112,83 +109,78 @@ export default class PasswordLogin extends React.Component { let phoneNumber = null; switch (this.state.loginType) { - case PasswordLogin.LOGIN_FIELD_EMAIL: - case PasswordLogin.LOGIN_FIELD_MXID: + case LoginField.Email: + case LoginField.MatrixId: username = this.props.username; break; - case PasswordLogin.LOGIN_FIELD_PHONE: + case LoginField.Phone: phoneCountry = this.props.phoneCountry; phoneNumber = this.props.phoneNumber; break; } - this.props.onSubmit( - username, - phoneCountry, - phoneNumber, - this.state.password, - ); - } + this.props.onSubmit(username, phoneCountry, phoneNumber, this.state.password); + }; - onUsernameChanged(ev) { + private onUsernameChanged = ev => { this.props.onUsernameChanged(ev.target.value); - } + }; - onUsernameFocus() { - if (this.state.loginType === PasswordLogin.LOGIN_FIELD_MXID) { + private onUsernameFocus = () => { + if (this.state.loginType === LoginField.MatrixId) { CountlyAnalytics.instance.track("onboarding_login_mxid_focus"); } else { CountlyAnalytics.instance.track("onboarding_login_email_focus"); } - } + }; - onUsernameBlur(ev) { - if (this.state.loginType === PasswordLogin.LOGIN_FIELD_MXID) { + private onUsernameBlur = ev => { + if (this.state.loginType === LoginField.MatrixId) { CountlyAnalytics.instance.track("onboarding_login_mxid_blur"); } else { CountlyAnalytics.instance.track("onboarding_login_email_blur"); } this.props.onUsernameBlur(ev.target.value); - } + }; - onLoginTypeChange(ev) { + private onLoginTypeChange = ev => { const loginType = ev.target.value; this.setState({ loginType }); this.props.onUsernameChanged(""); // Reset because email and username use the same state CountlyAnalytics.instance.track("onboarding_login_type_changed", { loginType }); - } + }; - onPhoneCountryChanged(country) { + private onPhoneCountryChanged = country => { this.props.onPhoneCountryChanged(country.iso2); - } + }; - onPhoneNumberChanged(ev) { + private onPhoneNumberChanged = ev => { this.props.onPhoneNumberChanged(ev.target.value); - } + }; - onPhoneNumberFocus() { + private onPhoneNumberFocus = () => { CountlyAnalytics.instance.track("onboarding_login_phone_number_focus"); - } + }; - onPhoneNumberBlur(ev) { + private onPhoneNumberBlur = ev => { CountlyAnalytics.instance.track("onboarding_login_phone_number_blur"); - } + }; - onPasswordChanged(ev) { + private onPasswordChanged = ev => { this.setState({password: ev.target.value}); - } + }; - async verifyFieldsBeforeSubmit() { + private async verifyFieldsBeforeSubmit() { // 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. - const activeElement = document.activeElement; + const activeElement = document.activeElement as HTMLElement; if (activeElement) { activeElement.blur(); } const fieldIDsInDisplayOrder = [ this.state.loginType, - PasswordLogin.LOGIN_FIELD_PASSWORD, + LoginField.Password, ]; // Run all fields with stricter validation that no longer allows empty @@ -226,7 +218,7 @@ export default class PasswordLogin extends React.Component { return false; } - allFieldsValid() { + private allFieldsValid() { const keys = Object.keys(this.state.fieldValid); for (let i = 0; i < keys.length; ++i) { if (!this.state.fieldValid[keys[i]]) { @@ -236,7 +228,7 @@ export default class PasswordLogin extends React.Component { return true; } - findFirstInvalidField(fieldIDs) { + private findFirstInvalidField(fieldIDs: LoginField[]) { for (const fieldID of fieldIDs) { if (!this.state.fieldValid[fieldID] && this[fieldID]) { return this[fieldID]; @@ -245,7 +237,7 @@ export default class PasswordLogin extends React.Component { return null; } - markFieldValid(fieldID, valid) { + private markFieldValid(fieldID: LoginField, valid: boolean) { const { fieldValid } = this.state; fieldValid[fieldID] = valid; this.setState({ @@ -253,7 +245,7 @@ export default class PasswordLogin extends React.Component { }); } - validateUsernameRules = withValidation({ + private validateUsernameRules = withValidation({ rules: [ { 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); - this.markFieldValid(PasswordLogin.LOGIN_FIELD_MXID, result.valid); + this.markFieldValid(LoginField.MatrixId, result.valid); return result; }; - validateEmailRules = withValidation({ + private validateEmailRules = withValidation({ rules: [ { 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); - this.markFieldValid(PasswordLogin.LOGIN_FIELD_EMAIL, result.valid); + this.markFieldValid(LoginField.Email, result.valid); return result; }; - validatePhoneNumberRules = withValidation({ + private validatePhoneNumberRules = withValidation({ rules: [ { 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); - this.markFieldValid(PasswordLogin.LOGIN_FIELD_PHONE, result.valid); + this.markFieldValid(LoginField.Password, result.valid); return result; }; - validatePasswordRules = withValidation({ + private validatePasswordRules = withValidation({ rules: [ { 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); - this.markFieldValid(PasswordLogin.LOGIN_FIELD_PASSWORD, result.valid); + this.markFieldValid(LoginField.Password, result.valid); return result; } - renderLoginField(loginType, autoFocus) { - const Field = sdk.getComponent('elements.Field'); - - const classes = {}; + private renderLoginField(loginType: IState["loginType"], autoFocus: boolean) { + const classes = { + error: false, + }; switch (loginType) { - case PasswordLogin.LOGIN_FIELD_EMAIL: + case LoginField.Email: classes.error = this.props.loginIncorrect && !this.props.username; return 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; return this[PasswordLogin.LOGIN_FIELD_MXID] = field} + ref={field => this[LoginField.MatrixId] = field} />; - case PasswordLogin.LOGIN_FIELD_PHONE: { - const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown'); + case LoginField.Phone: { classes.error = this.props.loginIncorrect && !this.props.phoneNumber; const phoneCountry = this[PasswordLogin.LOGIN_FIELD_PHONE] = field} + ref={field => this[LoginField.Password] = field} />; } } } - isLoginEmpty() { + private isLoginEmpty() { switch (this.state.loginType) { - case PasswordLogin.LOGIN_FIELD_EMAIL: - case PasswordLogin.LOGIN_FIELD_MXID: + case LoginField.Email: + case LoginField.MatrixId: return !this.props.username; - case PasswordLogin.LOGIN_FIELD_PHONE: + case LoginField.Phone: return !this.props.phoneCountry || !this.props.phoneNumber; } } render() { - const Field = sdk.getComponent('elements.Field'); - const SignInToText = sdk.getComponent('views.auth.SignInToText'); - let forgotPasswordJsx; if (this.props.onForgotPasswordClick) { @@ -458,22 +446,16 @@ export default class PasswordLogin extends React.Component { onChange={this.onLoginTypeChange} disabled={this.props.disableSubmit} > - - @@ -498,7 +480,7 @@ export default class PasswordLogin extends React.Component { disabled={this.props.disableSubmit} autoFocus={autoFocusPassword} onValidate={this.onPasswordValidate} - ref={field => this[PasswordLogin.LOGIN_FIELD_PASSWORD] = field} + ref={field => this[LoginField.Password] = field} /> {forgotPasswordJsx} { !this.props.busy && { interface IArgs { rules: IRule[]; - description(this: T, derivedData: D): React.ReactChild; + description?(this: T, derivedData: D): React.ReactChild; hideDescriptionIfValid?: boolean; deriveData?(data: Data): Promise; } From 7243ba0fe29c726bb2c257d7c3438a70a4df1cfd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 18 Nov 2020 14:01:27 +0000 Subject: [PATCH 4/7] Convert Login to Typescript --- .../structures/auth/{Login.js => Login.tsx} | 309 +++++++++--------- 1 file changed, 160 insertions(+), 149 deletions(-) rename src/components/structures/auth/{Login.js => Login.tsx} (72%) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.tsx similarity index 72% rename from src/components/structures/auth/Login.js rename to src/components/structures/auth/Login.tsx index a5ecbe36c7..4cd8981a65 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.tsx @@ -1,7 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2018, 2019 New Vector Ltd +Copyright 2015, 2016, 2017, 2018, 2019 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, {ComponentProps, ReactNode} from 'react'; + import {_t, _td} from '../../../languageHandler'; import * as sdk from '../../../index'; import Login from '../../../Login'; @@ -31,12 +29,12 @@ import PlatformPeg from '../../../PlatformPeg'; import SettingsStore from "../../../settings/SettingsStore"; import {UIFeature} from "../../../settings/UIFeature"; import CountlyAnalytics from "../../../CountlyAnalytics"; - -// Phases -// Show controls to configure server details -const PHASE_SERVER_DETAILS = 0; -// Show the appropriate login flow(s) for the server -const PHASE_LOGIN = 1; +import {IMatrixClientCreds} from "../../../MatrixClientPeg"; +import ServerConfig from "../../views/auth/ServerConfig"; +import PasswordLogin from "../../views/auth/PasswordLogin"; +import SignInToText from "../../views/auth/SignInToText"; +import InlineSpinner from "../../views/elements/InlineSpinner"; +import Spinner from "../../views/elements/Spinner"; // Enable phases for login const PHASES_ENABLED = true; @@ -52,64 +50,88 @@ _td("Invalid base_url for m.identity_server"); _td("Identity server URL does not appear to be a valid identity server"); _td("General failure"); +interface IProps { + serverConfig: ValidatedServerConfig; + // If true, the component will consider itself busy. + busy?: boolean; + isSyncing?: boolean; + // Secondary HS which we try to log into if the user is using + // the default HS but login fails. Useful for migrating to a + // different homeserver without confusing users. + fallbackHsUrl?: string; + defaultDeviceDisplayName?: string; + fragmentAfterLogin?: string; + + // Called when the user has logged in. Params: + // - The object returned by the login API + // - The user's password, if applicable, (may be cached in memory for a + // short time so the user is not required to re-enter their password + // for operations like uploading cross-signing keys). + onLoggedIn(data: IMatrixClientCreds, password: string): void; + + // login shouldn't know or care how registration, password recovery, etc is done. + onRegisterClick(): void; + onForgotPasswordClick?(): void; + onServerConfigChange(config: ValidatedServerConfig): void; +} + +enum Phase { + // Show controls to configure server details + ServerDetails, + // Show the appropriate login flow(s) for the server + Login, +} + +interface IState { + busy: boolean; + busyLoggingIn?: boolean; + errorText?: ReactNode; + loginIncorrect: boolean; + // can we attempt to log in or are there validation errors? + canTryLogin: boolean; + + // used for preserving form values when changing homeserver + username: string; + phoneCountry?: string; + phoneNumber: string; + + // Phase of the overall login dialog. + phase: Phase; + // The current login flow, such as password, SSO, etc. + // we need to load the flows from the server + currentFlow?: string; + + // We perform liveliness checks later, but for now suppress the errors. + // We also track the server dead errors independently of the regular errors so + // that we can render it differently, and override any other error the user may + // be seeing. + serverIsAlive: boolean; + serverErrorIsFatal: boolean; + serverDeadError: string; +} + /* * A wire component which glues together login UI components and Login logic */ -export default class LoginComponent extends React.Component { - static propTypes = { - // Called when the user has logged in. Params: - // - The object returned by the login API - // - The user's password, if applicable, (may be cached in memory for a - // short time so the user is not required to re-enter their password - // for operations like uploading cross-signing keys). - onLoggedIn: PropTypes.func.isRequired, - - // If true, the component will consider itself busy. - busy: PropTypes.bool, - - // Secondary HS which we try to log into if the user is using - // the default HS but login fails. Useful for migrating to a - // different homeserver without confusing users. - fallbackHsUrl: PropTypes.string, - - defaultDeviceDisplayName: PropTypes.string, - - // login shouldn't know or care how registration, password recovery, - // etc is done. - onRegisterClick: PropTypes.func.isRequired, - onForgotPasswordClick: PropTypes.func, - onServerConfigChange: PropTypes.func.isRequired, - - serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, - isSyncing: PropTypes.bool, - }; +export default class LoginComponent extends React.Component { + private unmounted = false; + private loginLogic: Login; + private readonly stepRendererMap: Record ReactNode>; constructor(props) { super(props); - this._unmounted = false; - this.state = { busy: false, busyLoggingIn: null, errorText: null, loginIncorrect: false, - canTryLogin: true, // can we attempt to log in or are there validation errors? - - // used for preserving form values when changing homeserver + canTryLogin: true, username: "", phoneCountry: null, phoneNumber: "", - - // Phase of the overall login dialog. - phase: PHASE_LOGIN, - // The current login flow, such as password, SSO, etc. - currentFlow: null, // we need to load the flows from the server - - // We perform liveliness checks later, but for now suppress the errors. - // We also track the server dead errors independently of the regular errors so - // that we can render it differently, and override any other error the user may - // be seeing. + phase: Phase.Login, + currentFlow: null, serverIsAlive: true, serverErrorIsFatal: false, serverDeadError: "", @@ -117,12 +139,12 @@ export default class LoginComponent extends React.Component { // map from login step type to a function which will render a control // letting you do that login type - this._stepRendererMap = { - 'm.login.password': this._renderPasswordStep, + this.stepRendererMap = { + 'm.login.password': this.renderPasswordStep, // CAS and SSO are the same thing, modulo the url we link to - 'm.login.cas': () => this._renderSsoStep("cas"), - 'm.login.sso': () => this._renderSsoStep("sso"), + 'm.login.cas': () => this.renderSsoStep("cas"), + 'm.login.sso': () => this.renderSsoStep("sso"), }; CountlyAnalytics.instance.track("onboarding_login_begin"); @@ -131,11 +153,11 @@ export default class LoginComponent extends React.Component { // TODO: [REACT-WARNING] Replace with appropriate lifecycle event // eslint-disable-next-line camelcase UNSAFE_componentWillMount() { - this._initLoginLogic(); + this.initLoginLogic(this.props.serverConfig); } componentWillUnmount() { - this._unmounted = true; + this.unmounted = true; } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event @@ -145,7 +167,7 @@ export default class LoginComponent extends React.Component { newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; // Ensure that we end up actually logging in to the right place - this._initLoginLogic(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl); + this.initLoginLogic(newProps.serverConfig); } isBusy = () => this.state.busy || this.props.busy; @@ -184,13 +206,13 @@ export default class LoginComponent extends React.Component { loginIncorrect: false, }); - this._loginLogic.loginViaPassword( + this.loginLogic.loginViaPassword( username, phoneCountry, phoneNumber, password, ).then((data) => { this.setState({serverIsAlive: true}); // it must be, we logged in. this.props.onLoggedIn(data, password); }, (error) => { - if (this._unmounted) { + if (this.unmounted) { return; } let errorText; @@ -202,21 +224,23 @@ export default class LoginComponent extends React.Component { } else if (error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { const errorTop = messageForResourceLimitError( error.data.limit_type, - error.data.admin_contact, { - 'monthly_active_user': _td( - "This homeserver has hit its Monthly Active User limit.", - ), - '': _td( - "This homeserver has exceeded one of its resource limits.", - ), - }); + error.data.admin_contact, + { + 'monthly_active_user': _td( + "This homeserver has hit its Monthly Active User limit.", + ), + '': _td( + "This homeserver has exceeded one of its resource limits.", + ), + }, + ); const errorDetail = messageForResourceLimitError( error.data.limit_type, - error.data.admin_contact, { - '': _td( - "Please contact your service administrator to continue using this service.", - ), - }); + error.data.admin_contact, + { + '': _td("Please contact your service administrator to continue using this service."), + }, + ); errorText = (
{errorTop}
@@ -243,7 +267,7 @@ export default class LoginComponent extends React.Component { } } else { // other errors, not specific to doing a password login - errorText = this._errorTextFromError(error); + errorText = this.errorTextFromError(error); } this.setState({ @@ -281,7 +305,7 @@ export default class LoginComponent extends React.Component { // the busy state. In the case of a full MXID that resolves to the same // HS as Element's default HS though, there may not be any server change. // To avoid this trap, we clear busy here. For cases where the server - // actually has changed, `_initLoginLogic` will be called and manages + // actually has changed, `initLoginLogic` will be called and manages // busy state for its own liveness check. this.setState({ busy: false, @@ -294,7 +318,7 @@ export default class LoginComponent extends React.Component { message = e.translatedMessage; } - let errorText = message; + let errorText: ReactNode = message; let discoveryState = {}; if (AutoDiscoveryUtils.isLivelinessError(e)) { errorText = this.state.errorText; @@ -327,14 +351,14 @@ export default class LoginComponent extends React.Component { }; onTryRegisterClick = ev => { - const step = this._getCurrentFlowStep(); + const step = this.getCurrentFlowStep(); if (step === 'm.login.sso' || step === 'm.login.cas') { // If we're showing SSO it means that registration is also probably disabled, // so intercept the click and instead pretend the user clicked 'Sign in with SSO'. ev.preventDefault(); ev.stopPropagation(); const ssoKind = step === 'm.login.sso' ? 'sso' : 'cas'; - PlatformPeg.get().startSingleSignOn(this._loginLogic.createTemporaryClient(), ssoKind, + PlatformPeg.get().startSingleSignOn(this.loginLogic.createTemporaryClient(), ssoKind, this.props.fragmentAfterLogin); } else { // Don't intercept - just go through to the register page @@ -342,24 +366,21 @@ export default class LoginComponent extends React.Component { } }; - onServerDetailsNextPhaseClick = () => { + private onServerDetailsNextPhaseClick = () => { this.setState({ - phase: PHASE_LOGIN, + phase: Phase.Login, }); }; - onEditServerDetailsClick = ev => { + private onEditServerDetailsClick = ev => { ev.preventDefault(); ev.stopPropagation(); this.setState({ - phase: PHASE_SERVER_DETAILS, + phase: Phase.ServerDetails, }); }; - async _initLoginLogic(hsUrl, isUrl) { - hsUrl = hsUrl || this.props.serverConfig.hsUrl; - isUrl = isUrl || this.props.serverConfig.isUrl; - + private async initLoginLogic({hsUrl, isUrl}: ValidatedServerConfig) { let isDefaultServer = false; if (this.props.serverConfig.isDefault && hsUrl === this.props.serverConfig.hsUrl @@ -372,7 +393,7 @@ export default class LoginComponent extends React.Component { const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, { defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, }); - this._loginLogic = loginLogic; + this.loginLogic = loginLogic; this.setState({ busy: true, @@ -403,7 +424,7 @@ export default class LoginComponent extends React.Component { if (this.state.serverErrorIsFatal) { // Server is dead: show server details prompt instead this.setState({ - phase: PHASE_SERVER_DETAILS, + phase: Phase.ServerDetails, }); return; } @@ -412,7 +433,7 @@ export default class LoginComponent extends React.Component { loginLogic.getFlows().then((flows) => { // look for a flow where we understand all of the steps. for (let i = 0; i < flows.length; i++ ) { - if (!this._isSupportedFlow(flows[i])) { + if (!this.isSupportedFlow(flows[i])) { continue; } @@ -421,7 +442,7 @@ export default class LoginComponent extends React.Component { // that for now). loginLogic.chooseFlow(i); this.setState({ - currentFlow: this._getCurrentFlowStep(), + currentFlow: this.getCurrentFlowStep(), }); return; } @@ -435,7 +456,7 @@ export default class LoginComponent extends React.Component { }); }, (err) => { this.setState({ - errorText: this._errorTextFromError(err), + errorText: this.errorTextFromError(err), loginIncorrect: false, canTryLogin: false, }); @@ -446,28 +467,28 @@ export default class LoginComponent extends React.Component { }); } - _isSupportedFlow(flow) { + private isSupportedFlow(flow) { // technically the flow can have multiple steps, but no one does this // for login and loginLogic doesn't support it so we can ignore it. - if (!this._stepRendererMap[flow.type]) { + if (!this.stepRendererMap[flow.type]) { console.log("Skipping flow", flow, "due to unsupported login type", flow.type); return false; } return true; } - _getCurrentFlowStep() { - return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null; + private getCurrentFlowStep() { + return this.loginLogic ? this.loginLogic.getCurrentFlowStep() : null; } - _errorTextFromError(err) { + private errorTextFromError(err) { let errCode = err.errcode; if (!errCode && err.httpStatus) { errCode = "HTTP " + err.httpStatus; } - let errorText = _t("Error: Problem communicating with the given homeserver.") + - (errCode ? " (" + errCode + ")" : ""); + let errorText: ReactNode = _t("Error: Problem communicating with the given homeserver.") + + (errCode ? " (" + errCode + ")" : ""); if (err.cors === 'rejected') { if (window.location.protocol === 'https:' && @@ -477,29 +498,27 @@ export default class LoginComponent extends React.Component { errorText = { _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " + "Either use HTTPS or enable unsafe scripts.", {}, - { - 'a': (sub) => { - return - { sub } - ; - }, + { + 'a': (sub) => { + return + { sub } + ; }, - ) } + }) } ; } else { errorText = { _t("Can't connect to homeserver - please check your connectivity, ensure your " + "homeserver's SSL certificate is trusted, and that a browser extension " + "is not blocking requests.", {}, - { - 'a': (sub) => - - { sub } - , - }, - ) } + { + 'a': (sub) => + + { sub } + , + }) } ; } } @@ -507,18 +526,16 @@ export default class LoginComponent extends React.Component { return errorText; } - renderServerComponent() { - const ServerConfig = sdk.getComponent("auth.ServerConfig"); - + private renderServerComponent() { if (SdkConfig.get()['disable_custom_urls']) { return null; } - if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS) { + if (PHASES_ENABLED && this.state.phase !== Phase.ServerDetails) { return null; } - const serverDetailsProps = {}; + const serverDetailsProps: ComponentProps = {}; if (PHASES_ENABLED) { serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick; serverDetailsProps.submitText = _t("Next"); @@ -533,8 +550,8 @@ export default class LoginComponent extends React.Component { />; } - renderLoginComponentForStep() { - if (PHASES_ENABLED && this.state.phase !== PHASE_LOGIN) { + private renderLoginComponentForStep() { + if (PHASES_ENABLED && this.state.phase !== Phase.Login) { return null; } @@ -544,7 +561,7 @@ export default class LoginComponent extends React.Component { return null; } - const stepRenderer = this._stepRendererMap[step]; + const stepRenderer = this.stepRendererMap[step]; if (stepRenderer) { return stepRenderer(); @@ -553,9 +570,7 @@ export default class LoginComponent extends React.Component { return null; } - _renderPasswordStep = () => { - const PasswordLogin = sdk.getComponent('auth.PasswordLogin'); - + private renderPasswordStep = () => { let onEditServerDetailsClick = null; // If custom URLs are allowed, wire up the server details edit link. if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) { @@ -564,27 +579,25 @@ export default class LoginComponent extends React.Component { return ( ); }; - _renderSsoStep = loginType => { - const SignInToText = sdk.getComponent('views.auth.SignInToText'); - + private renderSsoStep = loginType => { let onEditServerDetailsClick = null; // If custom URLs are allowed, wire up the server details edit link. if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) { @@ -605,7 +618,7 @@ export default class LoginComponent extends React.Component { @@ -614,12 +627,10 @@ export default class LoginComponent extends React.Component { }; render() { - const Loader = sdk.getComponent("elements.Spinner"); - const InlineSpinner = sdk.getComponent("elements.InlineSpinner"); const AuthHeader = sdk.getComponent("auth.AuthHeader"); const AuthBody = sdk.getComponent("auth.AuthBody"); const loader = this.isBusy() && !this.state.busyLoggingIn ? -
: null; +
: null; const errorText = this.state.errorText; From d45d0c66336ba309bbe023c7f9c6dc25b768d64a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 19 Nov 2020 14:57:57 +0000 Subject: [PATCH 5/7] Convert Registration to Typescript --- .../{Registration.js => Registration.tsx} | 273 ++++++++++-------- 1 file changed, 148 insertions(+), 125 deletions(-) rename src/components/structures/auth/{Registration.js => Registration.tsx} (79%) diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.tsx similarity index 79% rename from src/components/structures/auth/Registration.js rename to src/components/structures/auth/Registration.tsx index 80bf3b72cd..ebf218c1a4 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.tsx @@ -1,8 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2018, 2019 New Vector Ltd -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2015, 2016, 2017, 2018, 2019, 2020 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. @@ -18,8 +15,9 @@ limitations under the License. */ import Matrix from 'matrix-js-sdk'; -import React from 'react'; -import PropTypes from 'prop-types'; +import React, {ComponentProps, ReactNode} from 'react'; +import {MatrixClient} from "matrix-js-sdk/src/client"; + import * as sdk from '../../../index'; import { _t, _td } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; @@ -34,36 +32,96 @@ import Login from "../../../Login"; import dis from "../../../dispatcher/dispatcher"; // Phases -// Show controls to configure server details -const PHASE_SERVER_DETAILS = 0; -// Show the appropriate registration flow(s) for the server -const PHASE_REGISTRATION = 1; +enum Phase { + // Show controls to configure server details + ServerDetails = 0, + // Show the appropriate registration flow(s) for the server + Registration = 1, +} + +interface IProps { + serverConfig: ValidatedServerConfig; + defaultDeviceDisplayName: string; + email?: string; + brand?: string; + clientSecret?: string; + sessionId?: string; + idSid?: string; + + // Called when the user has logged in. Params: + // - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken + // - The user's password, if available and applicable (may be cached in memory + // for a short time so the user is not required to re-enter their password + // for operations like uploading cross-signing keys). + onLoggedIn(params: { + userId: string; + deviceId: string + homeserverUrl: string; + identityServerUrl?: string; + accessToken: string; + }, password: string): void; + makeRegistrationUrl(params: { + /* eslint-disable camelcase */ + client_secret: string; + hs_url: string; + is_url?: string; + session_id: string; + /* eslint-enable camelcase */ + }): void; + // registration shouldn't know or care how login is done. + onLoginClick(): void; + onServerConfigChange(config: ValidatedServerConfig): void; +} + +interface IState { + busy: boolean; + errorText?: ReactNode; + // true if we're waiting for the user to complete + // We remember the values entered by the user because + // the registration form will be unmounted during the + // course of registration, but if there's an error we + // want to bring back the registration form with the + // values the user entered still in it. We can keep + // them in this component's state since this component + // persist for the duration of the registration process. + formVals: Record; + // user-interactive auth + // If we've been given a session ID, we're resuming + // straight back into UI auth + doingUIAuth: boolean; + // If set, we've registered but are not going to log + // the user in to their new account automatically. + completedNoSignin: boolean; + serverType: ServerType.FREE | ServerType.PREMIUM | ServerType.ADVANCED; + // Phase of the overall registration dialog. + phase: Phase; + flows: { + stages: string[]; + }[]; + // We perform liveliness checks later, but for now suppress the errors. + // We also track the server dead errors independently of the regular errors so + // that we can render it differently, and override any other error the user may + // be seeing. + serverIsAlive: boolean; + serverErrorIsFatal: boolean; + serverDeadError: string; + + // Our matrix client - part of state because we can't render the UI auth + // component without it. + matrixClient?: MatrixClient; + // whether the HS requires an ID server to register with a threepid + serverRequiresIdServer?: boolean; + // The user ID we've just registered + registeredUsername?: string; + // if a different user ID to the one we just registered is logged in, + // this is the user ID that's logged in. + differentLoggedInUserId?: string; +} // Enable phases for registration const PHASES_ENABLED = true; -export default class Registration extends React.Component { - static propTypes = { - // Called when the user has logged in. Params: - // - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken - // - The user's password, if available and applicable (may be cached in memory - // for a short time so the user is not required to re-enter their password - // for operations like uploading cross-signing keys). - onLoggedIn: PropTypes.func.isRequired, - - clientSecret: PropTypes.string, - sessionId: PropTypes.string, - makeRegistrationUrl: PropTypes.func.isRequired, - idSid: PropTypes.string, - serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, - brand: PropTypes.string, - email: PropTypes.string, - // registration shouldn't know or care how login is done. - onLoginClick: PropTypes.func.isRequired, - onServerConfigChange: PropTypes.func.isRequired, - defaultDeviceDisplayName: PropTypes.string, - }; - +export default class Registration extends React.Component { constructor(props) { super(props); @@ -71,56 +129,22 @@ export default class Registration extends React.Component { this.state = { busy: false, errorText: null, - // We remember the values entered by the user because - // the registration form will be unmounted during the - // course of registration, but if there's an error we - // want to bring back the registration form with the - // values the user entered still in it. We can keep - // them in this component's state since this component - // persist for the duration of the registration process. formVals: { email: this.props.email, }, - // true if we're waiting for the user to complete - // user-interactive auth - // If we've been given a session ID, we're resuming - // straight back into UI auth doingUIAuth: Boolean(this.props.sessionId), serverType, - // Phase of the overall registration dialog. - phase: PHASE_REGISTRATION, + phase: Phase.Registration, flows: null, - // If set, we've registered but are not going to log - // the user in to their new account automatically. completedNoSignin: false, - - // We perform liveliness checks later, but for now suppress the errors. - // We also track the server dead errors independently of the regular errors so - // that we can render it differently, and override any other error the user may - // be seeing. serverIsAlive: true, serverErrorIsFatal: false, serverDeadError: "", - - // Our matrix client - part of state because we can't render the UI auth - // component without it. - matrixClient: null, - - // whether the HS requires an ID server to register with a threepid - serverRequiresIdServer: null, - - // The user ID we've just registered - registeredUsername: null, - - // if a different user ID to the one we just registered is logged in, - // this is the user ID that's logged in. - differentLoggedInUserId: null, }; } componentDidMount() { - this._unmounted = false; - this._replaceClient(); + this.replaceClient(this.props.serverConfig); } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event @@ -129,7 +153,7 @@ export default class Registration extends React.Component { if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; - this._replaceClient(newProps.serverConfig); + this.replaceClient(newProps.serverConfig); // Handle cases where the user enters "https://matrix.org" for their server // from the advanced option - we should default to FREE at that point. @@ -138,25 +162,25 @@ export default class Registration extends React.Component { // Reset the phase to default phase for the server type. this.setState({ serverType, - phase: this.getDefaultPhaseForServerType(serverType), + phase: Registration.getDefaultPhaseForServerType(serverType), }); } } - getDefaultPhaseForServerType(type) { + private static getDefaultPhaseForServerType(type: IState["serverType"]) { switch (type) { case ServerType.FREE: { // Move directly to the registration phase since the server // details are fixed. - return PHASE_REGISTRATION; + return Phase.Registration; } case ServerType.PREMIUM: case ServerType.ADVANCED: - return PHASE_SERVER_DETAILS; + return Phase.ServerDetails; } } - onServerTypeChange = type => { + private onServerTypeChange = (type: IState["serverType"]) => { this.setState({ serverType: type, }); @@ -181,11 +205,11 @@ export default class Registration extends React.Component { // Reset the phase to default phase for the server type. this.setState({ - phase: this.getDefaultPhaseForServerType(type), + phase: Registration.getDefaultPhaseForServerType(type), }); }; - async _replaceClient(serverConfig) { + private async replaceClient(serverConfig: ValidatedServerConfig) { this.setState({ errorText: null, serverDeadError: null, @@ -194,7 +218,6 @@ export default class Registration extends React.Component { // the UI auth component while we don't have a matrix client) busy: true, }); - if (!serverConfig) serverConfig = this.props.serverConfig; // Do a liveliness check on the URLs try { @@ -246,7 +269,7 @@ export default class Registration extends React.Component { // do SSO instead. If we've already started the UI Auth process though, we don't // need to. if (!this.state.doingUIAuth) { - await this._makeRegisterRequest(null); + await this.makeRegisterRequest(null); // This should never succeed since we specified no auth object. console.log("Expecting 401 from register request but got success!"); } @@ -287,7 +310,7 @@ export default class Registration extends React.Component { } } - onFormSubmit = formVals => { + private onFormSubmit = formVals => { this.setState({ errorText: "", busy: true, @@ -296,7 +319,7 @@ export default class Registration extends React.Component { }); }; - _requestEmailToken = (emailAddress, clientSecret, sendAttempt, sessionId) => { + private requestEmailToken = (emailAddress, clientSecret, sendAttempt, sessionId) => { return this.state.matrixClient.requestRegisterEmailToken( emailAddress, clientSecret, @@ -310,28 +333,26 @@ export default class Registration extends React.Component { ); } - _onUIAuthFinished = async (success, response, extra) => { + private onUIAuthFinished = async (success, response, extra) => { if (!success) { let msg = response.message || response.toString(); // can we give a better error message? if (response.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { const errorTop = messageForResourceLimitError( response.data.limit_type, - response.data.admin_contact, { - 'monthly_active_user': _td( - "This homeserver has hit its Monthly Active User limit.", - ), - '': _td( - "This homeserver has exceeded one of its resource limits.", - ), - }); + response.data.admin_contact, + { + 'monthly_active_user': _td("This homeserver has hit its Monthly Active User limit."), + '': _td("This homeserver has exceeded one of its resource limits."), + }, + ); const errorDetail = messageForResourceLimitError( response.data.limit_type, - response.data.admin_contact, { - '': _td( - "Please contact your service administrator to continue using this service.", - ), - }); + response.data.admin_contact, + { + '': _td("Please contact your service administrator to continue using this service."), + }, + ); msg =

{errorTop}

{errorDetail}

@@ -339,7 +360,7 @@ export default class Registration extends React.Component { } else if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) { let msisdnAvailable = false; for (const flow of response.available_flows) { - msisdnAvailable |= flow.stages.indexOf('m.login.msisdn') > -1; + msisdnAvailable = msisdnAvailable || flow.stages.includes('m.login.msisdn'); } if (!msisdnAvailable) { msg = _t('This server does not support authentication with a phone number.'); @@ -358,6 +379,10 @@ export default class Registration extends React.Component { const newState = { doingUIAuth: false, registeredUsername: response.user_id, + differentLoggedInUserId: null, + completedNoSignin: false, + // we're still busy until we get unmounted: don't show the registration form again + busy: true, }; // The user came in through an email validation link. To avoid overwriting @@ -372,8 +397,6 @@ export default class Registration extends React.Component { `Found a session for ${sessionOwner} but ${response.userId} has just registered.`, ); newState.differentLoggedInUserId = sessionOwner; - } else { - newState.differentLoggedInUserId = null; } if (response.access_token) { @@ -385,9 +408,7 @@ export default class Registration extends React.Component { accessToken: response.access_token, }, this.state.formVals.password); - this._setupPushers(); - // we're still busy until we get unmounted: don't show the registration form again - newState.busy = true; + this.setupPushers(); } else { newState.busy = false; newState.completedNoSignin = true; @@ -396,7 +417,7 @@ export default class Registration extends React.Component { this.setState(newState); }; - _setupPushers() { + private setupPushers() { if (!this.props.brand) { return Promise.resolve(); } @@ -419,38 +440,38 @@ export default class Registration extends React.Component { }); } - onLoginClick = ev => { + private onLoginClick = ev => { ev.preventDefault(); ev.stopPropagation(); this.props.onLoginClick(); }; - onGoToFormClicked = ev => { + private onGoToFormClicked = ev => { ev.preventDefault(); ev.stopPropagation(); - this._replaceClient(); + this.replaceClient(this.props.serverConfig); this.setState({ busy: false, doingUIAuth: false, - phase: PHASE_REGISTRATION, + phase: Phase.Registration, }); }; - onServerDetailsNextPhaseClick = async () => { + private onServerDetailsNextPhaseClick = async () => { this.setState({ - phase: PHASE_REGISTRATION, + phase: Phase.Registration, }); }; - onEditServerDetailsClick = ev => { + private onEditServerDetailsClick = ev => { ev.preventDefault(); ev.stopPropagation(); this.setState({ - phase: PHASE_SERVER_DETAILS, + phase: Phase.ServerDetails, }); }; - _makeRegisterRequest = auth => { + private makeRegisterRequest = auth => { // We inhibit login if we're trying to register with an email address: this // avoids a lot of complex race conditions that can occur if we try to log // the user in one one or both of the tabs they might end up with after @@ -466,13 +487,15 @@ export default class Registration extends React.Component { username: this.state.formVals.username, password: this.state.formVals.password, initial_device_display_name: this.props.defaultDeviceDisplayName, + auth: undefined, + inhibit_login: undefined, }; if (auth) registerParams.auth = auth; if (inhibitLogin !== undefined && inhibitLogin !== null) registerParams.inhibit_login = inhibitLogin; return this.state.matrixClient.registerRequest(registerParams); }; - _getUIAuthInputs() { + private getUIAuthInputs() { return { emailAddress: this.state.formVals.email, phoneCountry: this.state.formVals.phoneCountry, @@ -483,7 +506,7 @@ export default class Registration extends React.Component { // Links to the login page shown after registration is completed are routed through this // which checks the user hasn't already logged in somewhere else (perhaps we should do // this more generally?) - _onLoginClickWithCheck = async ev => { + private onLoginClickWithCheck = async ev => { ev.preventDefault(); const sessionLoaded = await Lifecycle.loadSession({ignoreGuest: true}); @@ -493,7 +516,7 @@ export default class Registration extends React.Component { } }; - renderServerComponent() { + private renderServerComponent() { const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector"); const ServerConfig = sdk.getComponent("auth.ServerConfig"); const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig"); @@ -511,7 +534,7 @@ export default class Registration extends React.Component { // which is always shown if we allow custom URLs at all. // (if there's a fatal server error, we need to show the full server // config as the user may need to change servers to resolve the error). - if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS && !this.state.serverErrorIsFatal) { + if (PHASES_ENABLED && this.state.phase !== Phase.ServerDetails && !this.state.serverErrorIsFatal) { return
; } - const serverDetailsProps = {}; + const serverDetailsProps: ComponentProps = {}; if (PHASES_ENABLED) { serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick; serverDetailsProps.submitText = _t("Next"); @@ -559,8 +582,8 @@ export default class Registration extends React.Component {
; } - renderRegisterComponent() { - if (PHASES_ENABLED && this.state.phase !== PHASE_REGISTRATION) { + private renderRegisterComponent() { + if (PHASES_ENABLED && this.state.phase !== Phase.Registration) { return null; } @@ -571,10 +594,10 @@ export default class Registration extends React.Component { if (this.state.matrixClient && this.state.doingUIAuth) { return { _t('Go back') } ; @@ -651,7 +674,7 @@ export default class Registration extends React.Component { loggedInUserId: this.state.differentLoggedInUserId, }, )}

-

+

{_t("Continue with previous account")}

; @@ -660,7 +683,7 @@ export default class Registration extends React.Component { regDoneText =

{_t( "Log in to your new account.", {}, { - a: (sub) => {sub}, + a: (sub) => {sub}, }, )}

; } else { @@ -670,7 +693,7 @@ export default class Registration extends React.Component { regDoneText =

{_t( "You can now close this window or log in to your new account.", {}, { - a: (sub) => {sub}, + a: (sub) => {sub}, }, )}

; } From 3dcb58f108468036c6d8f34cb267e3bd117f5a22 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 19 Nov 2020 15:10:40 +0000 Subject: [PATCH 6/7] Convert RegistrationForm to Typescript --- src/components/views/auth/PassphraseField.tsx | 4 +- ...gistrationForm.js => RegistrationForm.tsx} | 201 ++++++++++-------- src/components/views/elements/Field.tsx | 2 +- 3 files changed, 113 insertions(+), 94 deletions(-) rename src/components/views/auth/{RegistrationForm.js => RegistrationForm.tsx} (77%) diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx index b420ed0872..e240ad61ca 100644 --- a/src/components/views/auth/PassphraseField.tsx +++ b/src/components/views/auth/PassphraseField.tsx @@ -21,9 +21,9 @@ import zxcvbn from "zxcvbn"; import SdkConfig from "../../../SdkConfig"; import withValidation, {IFieldState, IValidationResult} from "../elements/Validation"; import {_t, _td} from "../../../languageHandler"; -import Field from "../elements/Field"; +import Field, {IInputProps} from "../elements/Field"; -interface IProps { +interface IProps extends Omit { autoFocus?: boolean; id?: string; className?: string; diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.tsx similarity index 77% rename from src/components/views/auth/RegistrationForm.js rename to src/components/views/auth/RegistrationForm.tsx index 70c1017427..fbe5572240 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.tsx @@ -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 2015, 2016, 2017, 2018, 2019, 2020 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. @@ -17,8 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, {ReactNode} from 'react'; + import * as sdk from '../../../index'; import * as Email from '../../../email'; import { looksValid as phoneNumberLooksValid } from '../../../phonenumber'; @@ -31,32 +29,57 @@ import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import PassphraseField from "./PassphraseField"; import CountlyAnalytics from "../../../CountlyAnalytics"; -const FIELD_EMAIL = 'field_email'; -const FIELD_PHONE_NUMBER = 'field_phone_number'; -const FIELD_USERNAME = 'field_username'; -const FIELD_PASSWORD = 'field_password'; -const FIELD_PASSWORD_CONFIRM = 'field_password_confirm'; +enum RegistrationField { + Email = "field_email", + PhoneNumber = "field_phone_number", + Username = "field_username", + Password = "field_password", + PasswordConfirm = "field_password_confirm", +} 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; + onEditServerDetailsClick?(): void; +} + +interface IState { + // Field error codes by field ID + fieldValid: Partial>; + // 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. */ -export default class RegistrationForm extends React.Component { - 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, - }; - +export default class RegistrationForm extends React.PureComponent { static defaultProps = { onValidationChange: console.error, canSubmit: true, @@ -66,9 +89,7 @@ export default class RegistrationForm extends React.Component { super(props); this.state = { - // Field error codes by field ID fieldValid: {}, - // The ISO2 country code selected in the phone number entry phoneCountry: this.props.defaultPhoneCountry, username: this.props.defaultUsername || "", email: this.props.defaultEmail || "", @@ -81,7 +102,7 @@ export default class RegistrationForm extends React.Component { CountlyAnalytics.instance.track("onboarding_registration_begin"); } - onSubmit = async ev => { + private onSubmit = async ev => { ev.preventDefault(); if (!this.props.canSubmit) return; @@ -92,7 +113,6 @@ export default class RegistrationForm extends React.Component { return; } - const self = this; if (this.state.email === '') { 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 " + "reset your password in the future.", ); - } else if (this._showEmail()) { + } else if (this.showEmail()) { desc = _t( "If you don't specify an email address, you won't be able to reset your password. " + "Are you sure?", ); } else { // user can't set an e-mail so don't prompt them to - self._doSubmit(ev); + this.doSubmit(ev); return; } @@ -120,18 +140,18 @@ export default class RegistrationForm extends React.Component { title: _t("Warning!"), description: desc, button: _t("Continue"), - onFinished(confirmed) { + onFinished: (confirmed) => { if (confirmed) { - self._doSubmit(ev); + this.doSubmit(ev); } }, }); } else { - self._doSubmit(ev); + this.doSubmit(ev); } }; - _doSubmit(ev) { + private doSubmit(ev) { const email = this.state.email.trim(); 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, // 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) { activeElement.blur(); } const fieldIDsInDisplayOrder = [ - FIELD_USERNAME, - FIELD_PASSWORD, - FIELD_PASSWORD_CONFIRM, - FIELD_EMAIL, - FIELD_PHONE_NUMBER, + RegistrationField.Username, + RegistrationField.Password, + RegistrationField.PasswordConfirm, + RegistrationField.Email, + RegistrationField.PhoneNumber, ]; // 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. */ - allFieldsValid() { + private allFieldsValid() { const keys = Object.keys(this.state.fieldValid); for (let i = 0; i < keys.length; ++i) { if (!this.state.fieldValid[keys[i]]) { @@ -218,7 +238,7 @@ export default class RegistrationForm extends React.Component { return true; } - findFirstInvalidField(fieldIDs) { + private findFirstInvalidField(fieldIDs: RegistrationField[]) { for (const fieldID of fieldIDs) { if (!this.state.fieldValid[fieldID] && this[fieldID]) { return this[fieldID]; @@ -227,7 +247,7 @@ export default class RegistrationForm extends React.Component { return null; } - markFieldValid(fieldID, valid) { + private markFieldValid(fieldID: RegistrationField, valid: boolean) { const { fieldValid } = this.state; fieldValid[fieldID] = valid; this.setState({ @@ -235,26 +255,26 @@ export default class RegistrationForm extends React.Component { }); } - onEmailChange = ev => { + private onEmailChange = ev => { this.setState({ email: ev.target.value, }); }; - onEmailValidate = async fieldState => { + private onEmailValidate = async fieldState => { const result = await this.validateEmailRules(fieldState); - this.markFieldValid(FIELD_EMAIL, result.valid); + this.markFieldValid(RegistrationField.Email, result.valid); return result; }; - validateEmailRules = withValidation({ + private validateEmailRules = withValidation({ description: () => _t("Use an email address to recover your account"), hideDescriptionIfValid: true, rules: [ { key: "required", - test({ value, allowEmpty }) { - return allowEmpty || !this._authStepIsRequired('m.login.email.identity') || !!value; + test(this: RegistrationForm, { value, allowEmpty }) { + return allowEmpty || !this.authStepIsRequired('m.login.email.identity') || !!value; }, 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({ password: ev.target.value, }); }; - onPasswordValidate = result => { - this.markFieldValid(FIELD_PASSWORD, result.valid); + private onPasswordValidate = result => { + this.markFieldValid(RegistrationField.Password, result.valid); }; - onPasswordConfirmChange = ev => { + private onPasswordConfirmChange = ev => { this.setState({ passwordConfirm: ev.target.value, }); }; - onPasswordConfirmValidate = async fieldState => { + private onPasswordConfirmValidate = async fieldState => { const result = await this.validatePasswordConfirmRules(fieldState); - this.markFieldValid(FIELD_PASSWORD_CONFIRM, result.valid); + this.markFieldValid(RegistrationField.PasswordConfirm, result.valid); return result; }; - validatePasswordConfirmRules = withValidation({ + private validatePasswordConfirmRules = withValidation({ rules: [ { key: "required", @@ -297,41 +317,40 @@ export default class RegistrationForm extends React.Component { }, { key: "match", - test({ value }) { + test(this: RegistrationForm, { value }) { return !value || value === this.state.password; }, invalid: () => _t("Passwords don't match"), }, - ], + ], }); - onPhoneCountryChange = newVal => { + private onPhoneCountryChange = newVal => { this.setState({ phoneCountry: newVal.iso2, - phonePrefix: newVal.prefix, }); }; - onPhoneNumberChange = ev => { + private onPhoneNumberChange = ev => { this.setState({ phoneNumber: ev.target.value, }); }; - onPhoneNumberValidate = async fieldState => { + private onPhoneNumberValidate = async fieldState => { const result = await this.validatePhoneNumberRules(fieldState); - this.markFieldValid(FIELD_PHONE_NUMBER, result.valid); + this.markFieldValid(RegistrationField.PhoneNumber, result.valid); return result; }; - validatePhoneNumberRules = withValidation({ + private validatePhoneNumberRules = withValidation({ description: () => _t("Other users can invite you to rooms using your contact details"), hideDescriptionIfValid: true, rules: [ { key: "required", - test({ value, allowEmpty }) { - return allowEmpty || !this._authStepIsRequired('m.login.msisdn') || !!value; + test(this: RegistrationForm, { value, allowEmpty }) { + return allowEmpty || !this.authStepIsRequired('m.login.msisdn') || !!value; }, 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({ username: ev.target.value, }); }; - onUsernameValidate = async fieldState => { + private onUsernameValidate = async fieldState => { const result = await this.validateUsernameRules(fieldState); - this.markFieldValid(FIELD_USERNAME, result.valid); + this.markFieldValid(RegistrationField.Username, result.valid); return result; }; - validateUsernameRules = withValidation({ + private validateUsernameRules = withValidation({ description: () => _t("Use lowercase letters, numbers, dashes and underscores only"), hideDescriptionIfValid: true, rules: [ @@ -378,7 +397,7 @@ export default class RegistrationForm extends React.Component { * @param {string} step A stage name to check * @returns {boolean} Whether it is required */ - _authStepIsRequired(step) { + private authStepIsRequired(step: string) { return this.props.flows.every((flow) => { return flow.stages.includes(step); }); @@ -390,46 +409,46 @@ export default class RegistrationForm extends React.Component { * @param {string} step A stage name to check * @returns {boolean} Whether it is used */ - _authStepIsUsed(step) { + private authStepIsUsed(step: string) { return this.props.flows.some((flow) => { return flow.stages.includes(step); }); } - _showEmail() { + private showEmail() { const haveIs = Boolean(this.props.serverConfig.isUrl); if ( (this.props.serverRequiresIdServer && !haveIs) || - !this._authStepIsUsed('m.login.email.identity') + !this.authStepIsUsed('m.login.email.identity') ) { return false; } return true; } - _showPhoneNumber() { + private showPhoneNumber() { const threePidLogin = !SdkConfig.get().disable_3pid_login; const haveIs = Boolean(this.props.serverConfig.isUrl); if ( !threePidLogin || (this.props.serverRequiresIdServer && !haveIs) || - !this._authStepIsUsed('m.login.msisdn') + !this.authStepIsUsed('m.login.msisdn') ) { return false; } return true; } - renderEmail() { - if (!this._showEmail()) { + private renderEmail() { + if (!this.showEmail()) { return null; } 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 (optional)"); return this[FIELD_EMAIL] = field} + ref={field => this[RegistrationField.Email] = field} type="text" label={emailPlaceholder} value={this.state.email} @@ -440,10 +459,10 @@ export default class RegistrationForm extends React.Component { />; } - renderPassword() { + private renderPassword() { return this[FIELD_PASSWORD] = field} + fieldRef={field => this[RegistrationField.Password] = field} minScore={PASSWORD_MIN_SCORE} value={this.state.password} onChange={this.onPasswordChange} @@ -457,7 +476,7 @@ export default class RegistrationForm extends React.Component { const Field = sdk.getComponent('elements.Field'); return this[FIELD_PASSWORD_CONFIRM] = field} + ref={field => this[RegistrationField.PasswordConfirm] = field} type="password" autoComplete="new-password" label={_t("Confirm password")} @@ -470,12 +489,12 @@ export default class RegistrationForm extends React.Component { } renderPhoneNumber() { - if (!this._showPhoneNumber()) { + if (!this.showPhoneNumber()) { return null; } const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown'); const Field = sdk.getComponent('elements.Field'); - const phoneLabel = this._authStepIsRequired('m.login.msisdn') ? + const phoneLabel = this.authStepIsRequired('m.login.msisdn') ? _t("Phone") : _t("Phone (optional)"); const phoneCountry = ; return this[FIELD_PHONE_NUMBER] = field} + ref={field => this[RegistrationField.PhoneNumber] = field} type="text" label={phoneLabel} value={this.state.phoneNumber} @@ -499,7 +518,7 @@ export default class RegistrationForm extends React.Component { const Field = sdk.getComponent('elements.Field'); return this[FIELD_USERNAME] = field} + ref={field => this[RegistrationField.Username] = field} type="text" autoFocus={true} label={_t("Username")} @@ -517,8 +536,8 @@ export default class RegistrationForm extends React.Component { ); let emailHelperText = null; - if (this._showEmail()) { - if (this._showPhoneNumber()) { + if (this.showEmail()) { + if (this.showPhoneNumber()) { emailHelperText =
{_t( "Set an email for account recovery. " + diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 7fd154047d..fb34f51b60 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -64,7 +64,7 @@ interface IProps { // All other props pass through to the . } -interface IInputProps extends IProps, InputHTMLAttributes { +export interface IInputProps extends IProps, InputHTMLAttributes { // The element to create. Defaults to "input". element?: "input"; // The input's value. This is a controlled component, so the value is required. From d85b5b6e2bc2ce13d91840350d63377dc9d28f00 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 20 Nov 2020 13:34:44 +0000 Subject: [PATCH 7/7] delint & post-rebase fixes --- src/components/structures/auth/Registration.tsx | 6 +++--- src/components/views/auth/RegistrationForm.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index ebf218c1a4..f97f20cf59 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -526,7 +526,7 @@ export default class Registration extends React.Component { } // Hide the server picker once the user is doing UI Auth unless encountered a fatal server error - if (this.state.phase !== PHASE_SERVER_DETAILS && this.state.doingUIAuth && !this.state.serverErrorIsFatal) { + if (this.state.phase !== Phase.ServerDetails && this.state.doingUIAuth && !this.state.serverErrorIsFatal) { return null; } @@ -702,7 +702,7 @@ export default class Registration extends React.Component { { regDoneText }
; } else { - let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', { + let yourMatrixAccountText: ReactNode = _t('Create your Matrix account on %(serverName)s', { serverName: this.props.serverConfig.hsName, }); if (this.props.serverConfig.hsNameIsDifferent) { @@ -740,7 +740,7 @@ export default class Registration extends React.Component { { errorText } { serverDeadSection } { this.renderServerComponent() } - { this.state.phase !== PHASE_SERVER_DETAILS &&

+ { this.state.phase !== Phase.ServerDetails &&

{yourMatrixAccountText} {editLink}

} diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx index fbe5572240..f6fb3bb3ea 100644 --- a/src/components/views/auth/RegistrationForm.tsx +++ b/src/components/views/auth/RegistrationForm.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {ReactNode} from 'react'; +import React from 'react'; import * as sdk from '../../../index'; import * as Email from '../../../email';