Convert Login to Typescript

This commit is contained in:
Michael Telatynski 2020-11-18 14:01:27 +00:00
parent 7397cebbea
commit 7243ba0fe2

View file

@ -1,7 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016, 2017, 2018, 2019 New Vector Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,8 +14,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, {ComponentProps, ReactNode} from 'react';
import PropTypes from 'prop-types';
import {_t, _td} from '../../../languageHandler'; import {_t, _td} from '../../../languageHandler';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import Login from '../../../Login'; import Login from '../../../Login';
@ -31,12 +29,12 @@ import PlatformPeg from '../../../PlatformPeg';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature"; import {UIFeature} from "../../../settings/UIFeature";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import {IMatrixClientCreds} from "../../../MatrixClientPeg";
// Phases import ServerConfig from "../../views/auth/ServerConfig";
// Show controls to configure server details import PasswordLogin from "../../views/auth/PasswordLogin";
const PHASE_SERVER_DETAILS = 0; import SignInToText from "../../views/auth/SignInToText";
// Show the appropriate login flow(s) for the server import InlineSpinner from "../../views/elements/InlineSpinner";
const PHASE_LOGIN = 1; import Spinner from "../../views/elements/Spinner";
// Enable phases for login // Enable phases for login
const PHASES_ENABLED = true; 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("Identity server URL does not appear to be a valid identity server");
_td("General failure"); _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 * A wire component which glues together login UI components and Login logic
*/ */
export default class LoginComponent extends React.Component { export default class LoginComponent extends React.Component<IProps, IState> {
static propTypes = { private unmounted = false;
// Called when the user has logged in. Params: private loginLogic: Login;
// - The object returned by the login API private readonly stepRendererMap: Record<string, () => ReactNode>;
// - 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,
};
constructor(props) { constructor(props) {
super(props); super(props);
this._unmounted = false;
this.state = { this.state = {
busy: false, busy: false,
busyLoggingIn: null, busyLoggingIn: null,
errorText: null, errorText: null,
loginIncorrect: false, loginIncorrect: false,
canTryLogin: true, // can we attempt to log in or are there validation errors? canTryLogin: true,
// used for preserving form values when changing homeserver
username: "", username: "",
phoneCountry: null, phoneCountry: null,
phoneNumber: "", phoneNumber: "",
phase: Phase.Login,
// Phase of the overall login dialog. currentFlow: null,
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.
serverIsAlive: true, serverIsAlive: true,
serverErrorIsFatal: false, serverErrorIsFatal: false,
serverDeadError: "", 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 // map from login step type to a function which will render a control
// letting you do that login type // letting you do that login type
this._stepRendererMap = { this.stepRendererMap = {
'm.login.password': this._renderPasswordStep, 'm.login.password': this.renderPasswordStep,
// CAS and SSO are the same thing, modulo the url we link to // CAS and SSO are the same thing, modulo the url we link to
'm.login.cas': () => this._renderSsoStep("cas"), 'm.login.cas': () => this.renderSsoStep("cas"),
'm.login.sso': () => this._renderSsoStep("sso"), 'm.login.sso': () => this.renderSsoStep("sso"),
}; };
CountlyAnalytics.instance.track("onboarding_login_begin"); 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 // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
this._initLoginLogic(); this.initLoginLogic(this.props.serverConfig);
} }
componentWillUnmount() { componentWillUnmount() {
this._unmounted = true; this.unmounted = true;
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // 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; newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
// Ensure that we end up actually logging in to the right place // 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; isBusy = () => this.state.busy || this.props.busy;
@ -184,13 +206,13 @@ export default class LoginComponent extends React.Component {
loginIncorrect: false, loginIncorrect: false,
}); });
this._loginLogic.loginViaPassword( this.loginLogic.loginViaPassword(
username, phoneCountry, phoneNumber, password, username, phoneCountry, phoneNumber, password,
).then((data) => { ).then((data) => {
this.setState({serverIsAlive: true}); // it must be, we logged in. this.setState({serverIsAlive: true}); // it must be, we logged in.
this.props.onLoggedIn(data, password); this.props.onLoggedIn(data, password);
}, (error) => { }, (error) => {
if (this._unmounted) { if (this.unmounted) {
return; return;
} }
let errorText; let errorText;
@ -202,21 +224,23 @@ export default class LoginComponent extends React.Component {
} else if (error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { } else if (error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
const errorTop = messageForResourceLimitError( const errorTop = messageForResourceLimitError(
error.data.limit_type, error.data.limit_type,
error.data.admin_contact, { error.data.admin_contact,
'monthly_active_user': _td( {
"This homeserver has hit its Monthly Active User limit.", 'monthly_active_user': _td(
), "This homeserver has hit its Monthly Active User limit.",
'': _td( ),
"This homeserver has exceeded one of its resource limits.", '': _td(
), "This homeserver has exceeded one of its resource limits.",
}); ),
},
);
const errorDetail = messageForResourceLimitError( const errorDetail = messageForResourceLimitError(
error.data.limit_type, error.data.limit_type,
error.data.admin_contact, { error.data.admin_contact,
'': _td( {
"Please <a>contact your service administrator</a> to continue using this service.", '': _td("Please <a>contact your service administrator</a> to continue using this service."),
), },
}); );
errorText = ( errorText = (
<div> <div>
<div>{errorTop}</div> <div>{errorTop}</div>
@ -243,7 +267,7 @@ export default class LoginComponent extends React.Component {
} }
} else { } else {
// other errors, not specific to doing a password login // other errors, not specific to doing a password login
errorText = this._errorTextFromError(error); errorText = this.errorTextFromError(error);
} }
this.setState({ 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 // 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. // 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 // 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. // busy state for its own liveness check.
this.setState({ this.setState({
busy: false, busy: false,
@ -294,7 +318,7 @@ export default class LoginComponent extends React.Component {
message = e.translatedMessage; message = e.translatedMessage;
} }
let errorText = message; let errorText: ReactNode = message;
let discoveryState = {}; let discoveryState = {};
if (AutoDiscoveryUtils.isLivelinessError(e)) { if (AutoDiscoveryUtils.isLivelinessError(e)) {
errorText = this.state.errorText; errorText = this.state.errorText;
@ -327,14 +351,14 @@ export default class LoginComponent extends React.Component {
}; };
onTryRegisterClick = ev => { onTryRegisterClick = ev => {
const step = this._getCurrentFlowStep(); const step = this.getCurrentFlowStep();
if (step === 'm.login.sso' || step === 'm.login.cas') { if (step === 'm.login.sso' || step === 'm.login.cas') {
// If we're showing SSO it means that registration is also probably disabled, // 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'. // so intercept the click and instead pretend the user clicked 'Sign in with SSO'.
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
const ssoKind = step === 'm.login.sso' ? 'sso' : 'cas'; 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); this.props.fragmentAfterLogin);
} else { } else {
// Don't intercept - just go through to the register page // 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({ this.setState({
phase: PHASE_LOGIN, phase: Phase.Login,
}); });
}; };
onEditServerDetailsClick = ev => { private onEditServerDetailsClick = ev => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.setState({ this.setState({
phase: PHASE_SERVER_DETAILS, phase: Phase.ServerDetails,
}); });
}; };
async _initLoginLogic(hsUrl, isUrl) { private async initLoginLogic({hsUrl, isUrl}: ValidatedServerConfig) {
hsUrl = hsUrl || this.props.serverConfig.hsUrl;
isUrl = isUrl || this.props.serverConfig.isUrl;
let isDefaultServer = false; let isDefaultServer = false;
if (this.props.serverConfig.isDefault if (this.props.serverConfig.isDefault
&& hsUrl === this.props.serverConfig.hsUrl && hsUrl === this.props.serverConfig.hsUrl
@ -372,7 +393,7 @@ export default class LoginComponent extends React.Component {
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, { const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
}); });
this._loginLogic = loginLogic; this.loginLogic = loginLogic;
this.setState({ this.setState({
busy: true, busy: true,
@ -403,7 +424,7 @@ export default class LoginComponent extends React.Component {
if (this.state.serverErrorIsFatal) { if (this.state.serverErrorIsFatal) {
// Server is dead: show server details prompt instead // Server is dead: show server details prompt instead
this.setState({ this.setState({
phase: PHASE_SERVER_DETAILS, phase: Phase.ServerDetails,
}); });
return; return;
} }
@ -412,7 +433,7 @@ export default class LoginComponent extends React.Component {
loginLogic.getFlows().then((flows) => { loginLogic.getFlows().then((flows) => {
// look for a flow where we understand all of the steps. // look for a flow where we understand all of the steps.
for (let i = 0; i < flows.length; i++ ) { for (let i = 0; i < flows.length; i++ ) {
if (!this._isSupportedFlow(flows[i])) { if (!this.isSupportedFlow(flows[i])) {
continue; continue;
} }
@ -421,7 +442,7 @@ export default class LoginComponent extends React.Component {
// that for now). // that for now).
loginLogic.chooseFlow(i); loginLogic.chooseFlow(i);
this.setState({ this.setState({
currentFlow: this._getCurrentFlowStep(), currentFlow: this.getCurrentFlowStep(),
}); });
return; return;
} }
@ -435,7 +456,7 @@ export default class LoginComponent extends React.Component {
}); });
}, (err) => { }, (err) => {
this.setState({ this.setState({
errorText: this._errorTextFromError(err), errorText: this.errorTextFromError(err),
loginIncorrect: false, loginIncorrect: false,
canTryLogin: 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 // 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. // 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); console.log("Skipping flow", flow, "due to unsupported login type", flow.type);
return false; return false;
} }
return true; return true;
} }
_getCurrentFlowStep() { private getCurrentFlowStep() {
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null; return this.loginLogic ? this.loginLogic.getCurrentFlowStep() : null;
} }
_errorTextFromError(err) { private errorTextFromError(err) {
let errCode = err.errcode; let errCode = err.errcode;
if (!errCode && err.httpStatus) { if (!errCode && err.httpStatus) {
errCode = "HTTP " + err.httpStatus; errCode = "HTTP " + err.httpStatus;
} }
let errorText = _t("Error: Problem communicating with the given homeserver.") + let errorText: ReactNode = _t("Error: Problem communicating with the given homeserver.") +
(errCode ? " (" + errCode + ")" : ""); (errCode ? " (" + errCode + ")" : "");
if (err.cors === 'rejected') { if (err.cors === 'rejected') {
if (window.location.protocol === 'https:' && if (window.location.protocol === 'https:' &&
@ -477,29 +498,27 @@ export default class LoginComponent extends React.Component {
errorText = <span> errorText = <span>
{ _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " + { _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
"Either use HTTPS or <a>enable unsafe scripts</a>.", {}, "Either use HTTPS or <a>enable unsafe scripts</a>.", {},
{ {
'a': (sub) => { 'a': (sub) => {
return <a target="_blank" rel="noreferrer noopener" return <a target="_blank" rel="noreferrer noopener"
href="https://www.google.com/search?&q=enable%20unsafe%20scripts" href="https://www.google.com/search?&q=enable%20unsafe%20scripts"
> >
{ sub } { sub }
</a>; </a>;
},
}, },
) } }) }
</span>; </span>;
} else { } else {
errorText = <span> errorText = <span>
{ _t("Can't connect to homeserver - please check your connectivity, ensure your " + { _t("Can't connect to homeserver - please check your connectivity, ensure your " +
"<a>homeserver's SSL certificate</a> is trusted, and that a browser extension " + "<a>homeserver's SSL certificate</a> is trusted, and that a browser extension " +
"is not blocking requests.", {}, "is not blocking requests.", {},
{ {
'a': (sub) => 'a': (sub) =>
<a target="_blank" rel="noreferrer noopener" href={this.props.serverConfig.hsUrl}> <a target="_blank" rel="noreferrer noopener" href={this.props.serverConfig.hsUrl}>
{ sub } { sub }
</a>, </a>,
}, }) }
) }
</span>; </span>;
} }
} }
@ -507,18 +526,16 @@ export default class LoginComponent extends React.Component {
return errorText; return errorText;
} }
renderServerComponent() { private renderServerComponent() {
const ServerConfig = sdk.getComponent("auth.ServerConfig");
if (SdkConfig.get()['disable_custom_urls']) { if (SdkConfig.get()['disable_custom_urls']) {
return null; return null;
} }
if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS) { if (PHASES_ENABLED && this.state.phase !== Phase.ServerDetails) {
return null; return null;
} }
const serverDetailsProps = {}; const serverDetailsProps: ComponentProps<typeof ServerConfig> = {};
if (PHASES_ENABLED) { if (PHASES_ENABLED) {
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick; serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
serverDetailsProps.submitText = _t("Next"); serverDetailsProps.submitText = _t("Next");
@ -533,8 +550,8 @@ export default class LoginComponent extends React.Component {
/>; />;
} }
renderLoginComponentForStep() { private renderLoginComponentForStep() {
if (PHASES_ENABLED && this.state.phase !== PHASE_LOGIN) { if (PHASES_ENABLED && this.state.phase !== Phase.Login) {
return null; return null;
} }
@ -544,7 +561,7 @@ export default class LoginComponent extends React.Component {
return null; return null;
} }
const stepRenderer = this._stepRendererMap[step]; const stepRenderer = this.stepRendererMap[step];
if (stepRenderer) { if (stepRenderer) {
return stepRenderer(); return stepRenderer();
@ -553,9 +570,7 @@ export default class LoginComponent extends React.Component {
return null; return null;
} }
_renderPasswordStep = () => { private renderPasswordStep = () => {
const PasswordLogin = sdk.getComponent('auth.PasswordLogin');
let onEditServerDetailsClick = null; let onEditServerDetailsClick = null;
// If custom URLs are allowed, wire up the server details edit link. // If custom URLs are allowed, wire up the server details edit link.
if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) { if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) {
@ -564,27 +579,25 @@ export default class LoginComponent extends React.Component {
return ( return (
<PasswordLogin <PasswordLogin
onSubmit={this.onPasswordLogin} onSubmit={this.onPasswordLogin}
onEditServerDetailsClick={onEditServerDetailsClick} onEditServerDetailsClick={onEditServerDetailsClick}
username={this.state.username} username={this.state.username}
phoneCountry={this.state.phoneCountry} phoneCountry={this.state.phoneCountry}
phoneNumber={this.state.phoneNumber} phoneNumber={this.state.phoneNumber}
onUsernameChanged={this.onUsernameChanged} onUsernameChanged={this.onUsernameChanged}
onUsernameBlur={this.onUsernameBlur} onUsernameBlur={this.onUsernameBlur}
onPhoneCountryChanged={this.onPhoneCountryChanged} onPhoneCountryChanged={this.onPhoneCountryChanged}
onPhoneNumberChanged={this.onPhoneNumberChanged} onPhoneNumberChanged={this.onPhoneNumberChanged}
onForgotPasswordClick={this.props.onForgotPasswordClick} onForgotPasswordClick={this.props.onForgotPasswordClick}
loginIncorrect={this.state.loginIncorrect} loginIncorrect={this.state.loginIncorrect}
serverConfig={this.props.serverConfig} serverConfig={this.props.serverConfig}
disableSubmit={this.isBusy()} disableSubmit={this.isBusy()}
busy={this.props.isSyncing || this.state.busyLoggingIn} busy={this.props.isSyncing || this.state.busyLoggingIn}
/> />
); );
}; };
_renderSsoStep = loginType => { private renderSsoStep = loginType => {
const SignInToText = sdk.getComponent('views.auth.SignInToText');
let onEditServerDetailsClick = null; let onEditServerDetailsClick = null;
// If custom URLs are allowed, wire up the server details edit link. // If custom URLs are allowed, wire up the server details edit link.
if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) { if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) {
@ -605,7 +618,7 @@ export default class LoginComponent extends React.Component {
<SSOButton <SSOButton
className="mx_Login_sso_link mx_Login_submit" className="mx_Login_sso_link mx_Login_submit"
matrixClient={this._loginLogic.createTemporaryClient()} matrixClient={this.loginLogic.createTemporaryClient()}
loginType={loginType} loginType={loginType}
fragmentAfterLogin={this.props.fragmentAfterLogin} fragmentAfterLogin={this.props.fragmentAfterLogin}
/> />
@ -614,12 +627,10 @@ export default class LoginComponent extends React.Component {
}; };
render() { render() {
const Loader = sdk.getComponent("elements.Spinner");
const InlineSpinner = sdk.getComponent("elements.InlineSpinner");
const AuthHeader = sdk.getComponent("auth.AuthHeader"); const AuthHeader = sdk.getComponent("auth.AuthHeader");
const AuthBody = sdk.getComponent("auth.AuthBody"); const AuthBody = sdk.getComponent("auth.AuthBody");
const loader = this.isBusy() && !this.state.busyLoggingIn ? const loader = this.isBusy() && !this.state.busyLoggingIn ?
<div className="mx_Login_loader"><Loader /></div> : null; <div className="mx_Login_loader"><Spinner /></div> : null;
const errorText = this.state.errorText; const errorText = this.state.errorText;