Merge pull request #2527 from jryans/auth-registration

Style registration flow
This commit is contained in:
Bruno Windels 2019-01-30 11:27:45 +00:00 committed by GitHub
commit 4d2a93eaaf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 324 additions and 152 deletions

View file

@ -539,7 +539,7 @@ module.exports = React.createClass({
return errorText;
},
serverComponentForStep() {
renderServerComponentForStep() {
const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector");
const ServerConfig = sdk.getComponent("auth.ServerConfig");
const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig");
@ -605,7 +605,7 @@ module.exports = React.createClass({
</div>;
},
loginComponentForStep() {
renderLoginComponentForStep() {
if (PHASES_ENABLED && this.state.phase !== PHASE_LOGIN) {
return null;
}
@ -707,8 +707,8 @@ module.exports = React.createClass({
{loader}
</h2>
{ errorTextSection }
{ this.serverComponentForStep() }
{ this.loginComponentForStep() }
{ this.renderServerComponentForStep() }
{ this.renderLoginComponentForStep() }
<a className="mx_Auth_changeFlow" onClick={this.onRegisterClick} href="#">
{ _t('Create account') }
</a>

View file

@ -23,13 +23,22 @@ import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import RegistrationForm from '../../views/auth/RegistrationForm';
import { _t, _td } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
import * as ServerType from '../../views/auth/ServerTypeSelector';
const MIN_PASSWORD_LENGTH = 6;
// 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;
// Enable phases for registration
const PHASES_ENABLED = true;
module.exports = React.createClass({
displayName: 'Registration',
@ -82,6 +91,7 @@ module.exports = React.createClass({
// If we've been given a session ID, we're resuming
// straight back into UI auth
doingUIAuth: Boolean(this.props.sessionId),
serverType: null,
hsUrl: this.props.customHsUrl,
isUrl: this.props.customIsUrl,
flows: null,
@ -107,6 +117,39 @@ module.exports = React.createClass({
});
},
onServerTypeChange(type) {
this.setState({
serverType: type,
});
// When changing server types, set the HS / IS URLs to reasonable defaults for the
// the new type.
switch (type) {
case ServerType.FREE: {
const { hsUrl, isUrl } = ServerType.TYPES.FREE;
this.onServerConfigChange({
hsUrl,
isUrl,
});
// Move directly to the registration phase since the server details are fixed.
this.setState({
phase: PHASE_REGISTRATION,
});
break;
}
case ServerType.PREMIUM:
case ServerType.ADVANCED:
this.onServerConfigChange({
hsUrl: this.props.defaultHsUrl,
isUrl: this.props.defaultIsUrl,
});
this.setState({
phase: PHASE_SERVER_DETAILS,
});
break;
}
},
_replaceClient: async function() {
this._matrixClient = Matrix.createClient({
baseUrl: this.state.hsUrl,
@ -273,6 +316,21 @@ module.exports = React.createClass({
this.props.onLoginClick();
},
onServerDetailsNextPhaseClick(ev) {
ev.stopPropagation();
this.setState({
phase: PHASE_REGISTRATION,
});
},
onEditServerDetailsClick(ev) {
ev.preventDefault();
ev.stopPropagation();
this.setState({
phase: PHASE_SERVER_DETAILS,
});
},
_makeRegisterRequest: function(auth) {
// Only send the bind params if we're sending username / pw params
// (Since we need to send no params at all to use the ones saved in the
@ -300,62 +358,127 @@ module.exports = React.createClass({
};
},
renderServerComponent() {
const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector");
const ServerConfig = sdk.getComponent("auth.ServerConfig");
const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
// TODO: May need to adjust the behavior of this config option
if (SdkConfig.get()['disable_custom_urls']) {
return null;
}
// If we're on a different phase, we only show the server type selector,
// which is always shown if we allow custom URLs at all.
if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS) {
return <div>
<ServerTypeSelector
defaultHsUrl={this.props.defaultHsUrl}
onChange={this.onServerTypeChange}
/>
</div>;
}
let serverDetails = null;
switch (this.state.serverType) {
case ServerType.FREE:
break;
case ServerType.PREMIUM:
serverDetails = <ModularServerConfig
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={1000}
/>;
break;
case ServerType.ADVANCED:
serverDetails = <ServerConfig
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
customIsUrl={this.state.discoveredIsUrl || this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={1000}
/>;
break;
}
let nextButton = null;
if (PHASES_ENABLED) {
nextButton = <AccessibleButton className="mx_Login_submit"
onClick={this.onServerDetailsNextPhaseClick}
>
{_t("Next")}
</AccessibleButton>;
}
return <div>
<ServerTypeSelector
defaultHsUrl={this.props.defaultHsUrl}
onChange={this.onServerTypeChange}
/>
{serverDetails}
{nextButton}
</div>;
},
renderRegisterComponent() {
if (PHASES_ENABLED && this.state.phase !== PHASE_REGISTRATION) {
return null;
}
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
const Spinner = sdk.getComponent('elements.Spinner');
const RegistrationForm = sdk.getComponent('auth.RegistrationForm');
if (this.state.doingUIAuth) {
return <InteractiveAuth
matrixClient={this._matrixClient}
makeRequest={this._makeRegisterRequest}
onAuthFinished={this._onUIAuthFinished}
inputs={this._getUIAuthInputs()}
makeRegistrationUrl={this.props.makeRegistrationUrl}
sessionId={this.props.sessionId}
clientSecret={this.props.clientSecret}
emailSid={this.props.idSid}
poll={true}
/>;
} else if (this.state.busy || !this.state.flows) {
return <Spinner />;
} else {
let onEditServerDetailsClick = null;
// If custom URLs are allowed and we haven't selected the Free server type, wire
// up the server details edit link.
if (
PHASES_ENABLED &&
!SdkConfig.get()['disable_custom_urls'] &&
this.state.serverType !== ServerType.FREE
) {
onEditServerDetailsClick = this.onEditServerDetailsClick;
}
return <RegistrationForm
defaultUsername={this.state.formVals.username}
defaultEmail={this.state.formVals.email}
defaultPhoneCountry={this.state.formVals.phoneCountry}
defaultPhoneNumber={this.state.formVals.phoneNumber}
defaultPassword={this.state.formVals.password}
minPasswordLength={MIN_PASSWORD_LENGTH}
onError={this.onFormValidationFailed}
onRegisterClick={this.onFormSubmit}
onEditServerDetailsClick={onEditServerDetailsClick}
flows={this.state.flows}
hsUrl={this.state.hsUrl}
hsName={this.props.defaultServerName}
/>;
}
},
render: function() {
const AuthHeader = sdk.getComponent('auth.AuthHeader');
const AuthBody = sdk.getComponent("auth.AuthBody");
const AuthPage = sdk.getComponent('auth.AuthPage');
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
const Spinner = sdk.getComponent("elements.Spinner");
const ServerConfig = sdk.getComponent('views.auth.ServerConfig');
let registerBody;
if (this.state.doingUIAuth) {
registerBody = (
<InteractiveAuth
matrixClient={this._matrixClient}
makeRequest={this._makeRegisterRequest}
onAuthFinished={this._onUIAuthFinished}
inputs={this._getUIAuthInputs()}
makeRegistrationUrl={this.props.makeRegistrationUrl}
sessionId={this.props.sessionId}
clientSecret={this.props.clientSecret}
emailSid={this.props.idSid}
poll={true}
/>
);
} else if (this.state.busy || !this.state.flows) {
registerBody = <Spinner />;
} else {
let serverConfigSection;
if (!SdkConfig.get()['disable_custom_urls']) {
serverConfigSection = (
<ServerConfig ref="serverConfig"
customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={1000}
/>
);
}
registerBody = (
<div>
<RegistrationForm
defaultUsername={this.state.formVals.username}
defaultEmail={this.state.formVals.email}
defaultPhoneCountry={this.state.formVals.phoneCountry}
defaultPhoneNumber={this.state.formVals.phoneNumber}
defaultPassword={this.state.formVals.password}
minPasswordLength={MIN_PASSWORD_LENGTH}
onError={this.onFormValidationFailed}
onRegisterClick={this.onFormSubmit}
flows={this.state.flows}
/>
{ serverConfigSection }
</div>
);
}
let errorText;
const err = this.state.errorText || this.props.defaultServerDiscoveryError;
@ -377,9 +500,10 @@ module.exports = React.createClass({
<AuthHeader />
<AuthBody>
<h2>{ _t('Create your account') }</h2>
{ registerBody }
{ signIn }
{ errorText }
{ this.renderServerComponent() }
{ this.renderRegisterComponent() }
{ signIn }
</AuthBody>
</AuthPage>
);

View file

@ -81,7 +81,7 @@ export default class CountryDropdown extends React.Component {
if (this.props.showPrefix) {
countryPrefix = '+' + COUNTRIES_BY_ISO2[iso2].prefix;
}
return <span>
return <span className="mx_CountryDropdown_shortOption">
{ this._flagImgForIso2(iso2) }
{ countryPrefix }
</span>;
@ -111,7 +111,7 @@ export default class CountryDropdown extends React.Component {
}
const options = displayedCountries.map((country) => {
return <div key={country.iso2}>
return <div className="mx_CountryDropdown_option" key={country.iso2}>
{ this._flagImgForIso2(country.iso2) }
{ country.name } <span>(+{ country.prefix })</span>
</div>;
@ -121,7 +121,7 @@ export default class CountryDropdown extends React.Component {
// values between mounting and the initial value propgating
const value = this.props.value || COUNTRIES[0].iso2;
return <Dropdown className={this.props.className + " left_aligned"}
return <Dropdown className={this.props.className + " mx_CountryDropdown"}
onOptionChange={this._onOptionChange} onSearchChange={this._onSearchChange}
menuWidth={298} getShortOption={this._getShortOption}
value={value} searchEnabled={true} disabled={this.props.disabled}

View file

@ -40,7 +40,7 @@ class PasswordLogin extends React.Component {
initialPhoneNumber: "",
initialPassword: "",
loginIncorrect: false,
hsDomain: "",
hsUrl: "",
hsName: null,
disableSubmit: false,
}

View file

@ -49,6 +49,7 @@ module.exports = React.createClass({
minPasswordLength: PropTypes.number,
onError: PropTypes.func,
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
onEditServerDetailsClick: PropTypes.func,
flows: PropTypes.arrayOf(PropTypes.object).isRequired,
},
@ -256,9 +257,34 @@ module.exports = React.createClass({
render: function() {
const self = this;
let yourMatrixAccountText = _t('Create your account');
if (this.props.hsName) {
yourMatrixAccountText = _t('Create your %(serverName)s account', {
serverName: this.props.hsName,
});
} else {
try {
const parsedHsUrl = new URL(this.props.hsUrl);
yourMatrixAccountText = _t('Create your %(serverName)s account', {
serverName: parsedHsUrl.hostname,
});
} catch (e) {
// ignore
}
}
let editLink = null;
if (this.props.onEditServerDetailsClick) {
editLink = <a className="mx_Auth_editServerDetails"
href="#" onClick={this.props.onEditServerDetailsClick}
>
{_t('Edit')}
</a>;
}
const emailPlaceholder = this._authStepIsRequired('m.login.email.identity') ?
_t("Email address") :
_t("Email address (optional)");
_t("Email") :
_t("Email (optional)");
const emailSection = (
<div>
@ -275,8 +301,8 @@ module.exports = React.createClass({
let phoneSection;
if (!SdkConfig.get().disable_3pid_login) {
const phonePlaceholder = this._authStepIsRequired('m.login.msisdn') ?
_t("Mobile phone number") :
_t("Mobile phone number (optional)");
_t("Phone") :
_t("Phone (optional)");
phoneSection = (
<div className="mx_Login_phoneSection">
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
@ -309,25 +335,36 @@ module.exports = React.createClass({
return (
<div>
<h3>
{yourMatrixAccountText}
{editLink}
</h3>
<form onSubmit={this.onSubmit}>
{ emailSection }
{ phoneSection }
<input type="text" ref="username"
placeholder={placeholderUsername} defaultValue={this.props.defaultUsername}
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_USERNAME);}} />
<br />
<input type="password" ref="password"
className={this._classForField(FIELD_PASSWORD, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_PASSWORD);}}
placeholder={_t("Password")} defaultValue={this.props.defaultPassword} />
<br />
<input type="password" ref="passwordConfirm"
placeholder={_t("Confirm password")}
className={this._classForField(FIELD_PASSWORD_CONFIRM, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM);}}
defaultValue={this.props.defaultPassword} />
<br />
<div className="mx_Auth_fieldRow">
<input type="text" ref="username"
placeholder={placeholderUsername} defaultValue={this.props.defaultUsername}
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_USERNAME);}} />
</div>
<div className="mx_Auth_fieldRow">
<input type="password" ref="password"
className={this._classForField(FIELD_PASSWORD, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_PASSWORD);}}
placeholder={_t("Password")} defaultValue={this.props.defaultPassword} />
<input type="password" ref="passwordConfirm"
placeholder={_t("Confirm")}
className={this._classForField(FIELD_PASSWORD_CONFIRM, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM);}}
defaultValue={this.props.defaultPassword} />
</div>
<div className="mx_Auth_fieldRow">
{ emailSection }
{ phoneSection }
</div>
{_t(
"Use an email address to receover your account. Other users " +
"can invite you to rooms using your contact details.",
)}
{ registerButton }
</form>
</div>