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

@ -30,6 +30,7 @@
@import "./views/auth/_AuthHeader.scss";
@import "./views/auth/_AuthHeaderLogo.scss";
@import "./views/auth/_AuthPage.scss";
@import "./views/auth/_CountryDropdown.scss";
@import "./views/auth/_InteractiveAuthEntryComponents.scss";
@import "./views/auth/_LanguageSelector.scss";
@import "./views/auth/_ServerConfig.scss";

View file

@ -15,13 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_Login_support {
text-align: center;
font-size: 13px;
margin-top: 0px;
opacity: 0.7;
}
.mx_Login_field {
width: 100%;
box-sizing: border-box;
@ -33,14 +26,6 @@ limitations under the License.
margin-bottom: 14px;
}
.mx_Login_fieldLabel {
margin-top: -10px;
margin-left: 8px;
margin-bottom: 14px;
font-size: 13px;
opacity: 0.8;
}
.mx_Login_submit {
@mixin mx_DialogButton;
width: 100%;
@ -58,16 +43,6 @@ limitations under the License.
opacity: 0.3;
}
.mx_Login_label {
font-size: 13px;
opacity: 0.8;
}
.mx_Login_checkbox,
.mx_Login_radio {
margin-right: 10px;
}
.mx_AuthBody a.mx_Login_sso_link:link,
.mx_AuthBody a.mx_Login_sso_link:hover,
.mx_AuthBody a.mx_Login_sso_link:visited {
@ -119,10 +94,6 @@ limitations under the License.
flex: 1 1 auto;
}
.mx_Login_field_group {
display: flex;
}
.mx_Login_field_prefix {
height: 38px;
padding: 0px 5px;
@ -147,7 +118,6 @@ limitations under the License.
.mx_Login_phoneCountry {
margin-bottom: 14px;
width: 150px;
/* To override mx_Login_field_prefix */
text-align: left;

View file

@ -47,6 +47,24 @@ limitations under the License.
box-sizing: border-box;
}
.mx_Auth_fieldRow {
display: flex;
margin-bottom: 10px;
}
.mx_Auth_fieldRow > * {
margin: 0 5px;
flex: 1;
}
.mx_Auth_fieldRow > *:first-child {
margin-left: 0;
}
.mx_Auth_fieldRow > *:last-child {
margin-right: 0;
}
.mx_AuthBody a:link,
.mx_AuthBody a:hover,
.mx_AuthBody a:visited {

View file

@ -0,0 +1,34 @@
/*
Copyright 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.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_CountryDropdown .mx_Dropdown_input .mx_Dropdown_option {
padding: 0 3px;
}
.mx_CountryDropdown .mx_Dropdown_arrow {
padding-right: 3px;
}
.mx_CountryDropdown_shortOption {
display: inline-flex;
align-items: center;
height: 100%;
}
.mx_CountryDropdown_option {
display: flex;
align-items: center;
}

View file

@ -25,22 +25,6 @@ limitations under the License.
color: $authpage-lang-color;
}
/* TODO: Consider using this new arrow for all dropdowns */
.mx_Auth_language .mx_Dropdown_arrow {
width: 10px;
height: 6px;
border: none;
right: 6px;
}
.mx_Auth_language .mx_Dropdown_arrow::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
mask: url('$(res)/img/feather-icons/dropdown-arrow.svg');
mask-repeat: no-repeat;
background: $authpage-lang-color;
}

View file

@ -23,11 +23,12 @@ limitations under the License.
}
.mx_Dropdown_input {
display: flex;
align-items: center;
position: relative;
border-radius: 3px;
border: 1px solid $strong-input-border-color;
font-weight: 300;
font-size: 13px;
font-size: 12px;
user-select: none;
}
@ -41,19 +42,23 @@ limitations under the License.
}
.mx_Dropdown_arrow {
border-color: $primary-fg-color transparent transparent;
border-style: solid;
border-width: 5px 5px 0;
display: block;
height: 0;
position: absolute;
right: 10px;
top: 14px;
width: 0
position: relative;
width: 10px;
height: 6px;
padding-right: 8px;
}
.mx_Dropdown.left_aligned .mx_Dropdown_arrow {
left: 10px;
.mx_Dropdown_arrow::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
mask: url('$(res)/img/feather-icons/dropdown-arrow.svg');
mask-repeat: no-repeat;
background: $primary-fg-color;
}
.mx_Dropdown_input > .mx_Dropdown_option {
@ -62,10 +67,6 @@ limitations under the License.
white-space: nowrap;
}
.mx_Dropdown.left_aligned .mx_Dropdown_input > .mx_Dropdown_option {
padding-left: 25px;
}
.mx_Dropdown_option {
height: 35px;
line-height: 35px;
@ -81,7 +82,7 @@ limitations under the License.
.mx_Dropdown_option img {
margin: 5px;
width: 27px;
width: 16px;
vertical-align: middle;
}

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>

View file

@ -1210,8 +1210,13 @@
"Sign in with": "Sign in with",
"Sign in": "Sign in",
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
"Email address (optional)": "Email address (optional)",
"Mobile phone number (optional)": "Mobile phone number (optional)",
"Create your account": "Create your account",
"Create your %(serverName)s account": "Create your %(serverName)s account",
"Email": "Email",
"Email (optional)": "Email (optional)",
"Phone (optional)": "Phone (optional)",
"Confirm": "Confirm",
"Use an email address to receover your account. Other users can invite you to rooms using your contact details.": "Use an email address to receover your account. Other users can invite you to rooms using your contact details.",
"Other servers": "Other servers",
"Enter custom server URLs <a>What does this mean?</a>": "Enter custom server URLs <a>What does this mean?</a>",
"Homeserver URL": "Homeserver URL",
@ -1372,7 +1377,6 @@
"Desktop specific": "Desktop specific",
"Missing Media Permissions, click here to request.": "Missing Media Permissions, click here to request.",
"VoIP": "VoIP",
"Email": "Email",
"Add email address": "Add email address",
"Display name": "Display name",
"To return to your account in future you need to set a password": "To return to your account in future you need to set a password",
@ -1426,7 +1430,6 @@
"A phone number is required to register on this homeserver.": "A phone number is required to register on this homeserver.",
"You need to enter a username.": "You need to enter a username.",
"An unknown error occurred.": "An unknown error occurred.",
"Create your account": "Create your account",
"Commands": "Commands",
"Results from DuckDuckGo": "Results from DuckDuckGo",
"Emoji": "Emoji",