From 636cb8a5ccb00e91df624386af66c2c35f310c31 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 22:57:49 -0600 Subject: [PATCH 01/22] Have ServerConfig and co. do validation of the config in-house This also causes the components to produce a ValidatedServerConfig for use by other components. --- res/css/views/auth/_ServerConfig.scss | 5 + .../views/auth/ModularServerConfig.js | 92 ++++++++++----- src/components/views/auth/ServerConfig.js | 111 +++++++++++------- src/utils/AutoDiscoveryUtils.js | 104 ++++++++++++++++ 4 files changed, 238 insertions(+), 74 deletions(-) create mode 100644 src/utils/AutoDiscoveryUtils.js diff --git a/res/css/views/auth/_ServerConfig.scss b/res/css/views/auth/_ServerConfig.scss index 79ad9e8238..fe96da2019 100644 --- a/res/css/views/auth/_ServerConfig.scss +++ b/res/css/views/auth/_ServerConfig.scss @@ -35,3 +35,8 @@ limitations under the License. .mx_ServerConfig_help:link { opacity: 0.8; } + +.mx_ServerConfig_error { + display: block; + color: $warning-color; +} diff --git a/src/components/views/auth/ModularServerConfig.js b/src/components/views/auth/ModularServerConfig.js index 9c6c4b01bf..ea22577dbd 100644 --- a/src/components/views/auth/ModularServerConfig.js +++ b/src/components/views/auth/ModularServerConfig.js @@ -18,9 +18,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; +import SdkConfig from "../../../SdkConfig"; +import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils"; +import * as ServerType from '../../views/auth/ServerTypeSelector'; const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication'; +// TODO: TravisR - Can this extend ServerConfig for most things? + /* * Configure the Modular server name. * @@ -31,65 +37,87 @@ export default class ModularServerConfig extends React.PureComponent { static propTypes = { onServerConfigChange: PropTypes.func, - // default URLs are defined in config.json (or the hardcoded defaults) - // they are used if the user has not overridden them with a custom URL. - // In other words, if the custom URL is blank, the default is used. - defaultHsUrl: PropTypes.string, // e.g. https://matrix.org - - // This component always uses the default IS URL and doesn't allow it - // to be changed. We still receive it as a prop here to simplify - // consumers by still passing the IS URL via onServerConfigChange. - defaultIsUrl: PropTypes.string, // e.g. https://vector.im - - // custom URLs are explicitly provided by the user and override the - // default URLs. The user enters them via the component's input fields, - // which is reflected on these properties whenever on..UrlChanged fires. - // They are persisted in localStorage by MatrixClientPeg, and so can - // override the default URLs when the component initially loads. - customHsUrl: PropTypes.string, + // The current configuration that the user is expecting to change. + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, delayTimeMs: PropTypes.number, // time to wait before invoking onChanged - } + }; static defaultProps = { onServerConfigChange: function() {}, customHsUrl: "", delayTimeMs: 0, - } + }; constructor(props) { super(props); this.state = { - hsUrl: props.customHsUrl, + busy: false, + errorText: "", + hsUrl: props.serverConfig.hsUrl, + isUrl: props.serverConfig.isUrl, }; } componentWillReceiveProps(newProps) { - if (newProps.customHsUrl === this.state.hsUrl) return; + if (newProps.serverConfig.hsUrl === this.state.hsUrl && + newProps.serverConfig.isUrl === this.state.isUrl) return; + + this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl); + } + + async validateAndApplyServer(hsUrl, isUrl) { + // Always try and use the defaults first + const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"]; + if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) { + this.setState({busy: false, errorText: ""}); + this.props.onServerConfigChange(defaultConfig); + return defaultConfig; + } this.setState({ - hsUrl: newProps.customHsUrl, - }); - this.props.onServerConfigChange({ - hsUrl: newProps.customHsUrl, - isUrl: this.props.defaultIsUrl, + hsUrl, + isUrl, + busy: true, + errorText: "", }); + + try { + const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl); + this.setState({busy: false, errorText: ""}); + this.props.onServerConfigChange(result); + return result; + } catch (e) { + console.error(e); + let message = _t("Unable to validate homeserver/identity server"); + if (e.translatedMessage) { + message = e.translatedMessage; + } + this.setState({ + busy: false, + errorText: message, + }); + } + } + + async validateServer() { + // TODO: Do we want to support .well-known lookups here? + // If for some reason someone enters "matrix.org" for a URL, we could do a lookup to + // find their homeserver without demanding they use "https://matrix.org" + return this.validateAndApplyServer(this.state.hsUrl, ServerType.TYPES.PREMIUM.identityServerUrl); } onHomeserverBlur = (ev) => { this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => { - this.props.onServerConfigChange({ - hsUrl: this.state.hsUrl, - isUrl: this.props.defaultIsUrl, - }); + this.validateServer(); }); - } + }; onHomeserverChange = (ev) => { const hsUrl = ev.target.value; this.setState({ hsUrl }); - } + }; _waitThenInvoke(existingTimeoutId, fn) { if (existingTimeoutId) { @@ -116,7 +144,7 @@ export default class ModularServerConfig extends React.PureComponent {
{ this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => { - this.props.onServerConfigChange({ - hsUrl: this.state.hsUrl, - isUrl: this.state.isUrl, - }); + this.validateServer(); }); - } + }; onHomeserverChange = (ev) => { const hsUrl = ev.target.value; this.setState({ hsUrl }); - } + }; onIdentityServerBlur = (ev) => { this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => { - this.props.onServerConfigChange({ - hsUrl: this.state.hsUrl, - isUrl: this.state.isUrl, - }); + this.validateServer(); }); - } + }; onIdentityServerChange = (ev) => { const isUrl = ev.target.value; this.setState({ isUrl }); - } + }; _waitThenInvoke(existingTimeoutId, fn) { if (existingTimeoutId) { @@ -114,11 +134,15 @@ export default class ServerConfig extends React.PureComponent { showHelpPopup = () => { const CustomServerDialog = sdk.getComponent('auth.CustomServerDialog'); Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog); - } + }; render() { const Field = sdk.getComponent('elements.Field'); + const errorText = this.state.errorText + ? {this.state.errorText} + : null; + return (

{_t("Other servers")}

@@ -127,20 +151,23 @@ export default class ServerConfig extends React.PureComponent { { sub } , })} + {errorText}
diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.js new file mode 100644 index 0000000000..318c706136 --- /dev/null +++ b/src/utils/AutoDiscoveryUtils.js @@ -0,0 +1,104 @@ +/* +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. +*/ + +import {AutoDiscovery} from "matrix-js-sdk"; +import {_td, newTranslatableError} from "../languageHandler"; +import {makeType} from "./TypeUtils"; +import SdkConfig from "../SdkConfig"; + +export class ValidatedServerConfig { + hsUrl: string; + hsName: string; + hsNameIsDifferent: string; + + isUrl: string; + identityEnabled: boolean; +} + +export default class AutoDiscoveryUtils { + static async validateServerConfigWithStaticUrls(homeserverUrl: string, identityUrl: string): ValidatedServerConfig { + if (!homeserverUrl) { + throw newTranslatableError(_td("No homeserver URL provided")); + } + + const wellknownConfig = { + "m.homeserver": { + base_url: homeserverUrl, + }, + "m.identity_server": { + base_url: identityUrl, + }, + }; + + const result = await AutoDiscovery.fromDiscoveryConfig(wellknownConfig); + + const url = new URL(homeserverUrl); + const serverName = url.hostname; + + return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result); + } + + static async validateServerName(serverName: string): ValidatedServerConfig { + const result = await AutoDiscovery.findClientConfig(serverName); + return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result); + } + + static buildValidatedConfigFromDiscovery(serverName: string, discoveryResult): ValidatedServerConfig { + if (!discoveryResult || !discoveryResult["m.homeserver"]) { + // This shouldn't happen without major misconfiguration, so we'll log a bit of information + // in the log so we can find this bit of codee but otherwise tell teh user "it broke". + console.error("Ended up in a state of not knowing which homeserver to connect to."); + throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + } + + const hsResult = discoveryResult['m.homeserver']; + if (hsResult.state !== AutoDiscovery.SUCCESS) { + if (AutoDiscovery.ALL_ERRORS.indexOf(hsResult.error) !== -1) { + throw newTranslatableError(hsResult.error); + } + throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + } + + const isResult = discoveryResult['m.identity_server']; + let preferredIdentityUrl = "https://vector.im"; + if (isResult && isResult.state === AutoDiscovery.SUCCESS) { + preferredIdentityUrl = isResult["base_url"]; + } else if (isResult && isResult.state !== AutoDiscovery.PROMPT) { + console.error("Error determining preferred identity server URL:", isResult); + throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + } + + const preferredHomeserverUrl = hsResult["base_url"]; + let preferredHomeserverName = serverName ? serverName : hsResult["server_name"]; + + const url = new URL(preferredHomeserverUrl); + if (!preferredHomeserverName) preferredHomeserverName = url.hostname; + + // It should have been set by now, so check it + if (!preferredHomeserverName) { + console.error("Failed to parse homeserver name from homeserver URL"); + throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + } + + return makeType(ValidatedServerConfig, { + hsUrl: preferredHomeserverUrl, + hsName: preferredHomeserverName, + hsNameIsDifferent: url.hostname !== preferredHomeserverName, + isUrl: preferredIdentityUrl, + identityEnabled: !SdkConfig.get()['disable_identity_server'], + }); + } +} From 6b45e6031454dc74929a76d6c071c0200d65925a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 23:02:01 -0600 Subject: [PATCH 02/22] Update ServerTypeSelector for registration to use a server config --- .../structures/auth/Registration.js | 10 +------ src/components/views/auth/RegistrationForm.js | 27 ++++++----------- .../views/auth/ServerTypeSelector.js | 21 +++++++++---- src/utils/TypeUtils.js | 30 +++++++++++++++++++ 4 files changed, 55 insertions(+), 33 deletions(-) create mode 100644 src/utils/TypeUtils.js diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 708118bb22..c579b2082d 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -446,13 +446,6 @@ module.exports = React.createClass({ onEditServerDetailsClick = this.onEditServerDetailsClick; } - // If the current HS URL is the default HS URL, then we can label it - // with the default HS name (if it exists). - let hsName; - if (this.state.hsUrl === this.props.defaultHsUrl) { - hsName = this.props.defaultServerName; - } - return ; } }, diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 6e55581af0..f815ad081d 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -26,6 +26,7 @@ import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import { SAFE_LOCALPART_REGEX } from '../../../Registration'; import withValidation from '../elements/Validation'; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; const FIELD_EMAIL = 'field_email'; const FIELD_PHONE_NUMBER = 'field_phone_number'; @@ -51,11 +52,7 @@ module.exports = React.createClass({ onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise onEditServerDetailsClick: PropTypes.func, flows: PropTypes.arrayOf(PropTypes.object).isRequired, - // This is optional and only set if we used a server name to determine - // the HS URL via `.well-known` discovery. The server name is used - // instead of the HS URL when talking about "your account". - hsName: PropTypes.string, - hsUrl: PropTypes.string, + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, }, getDefaultProps: function() { @@ -499,20 +496,14 @@ module.exports = React.createClass({ }, render: function() { - let yourMatrixAccountText = _t('Create your Matrix account'); - if (this.props.hsName) { - yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', { - serverName: this.props.hsName, + let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', { + serverName: this.props.serverConfig.hsName, + }); + if (this.props.serverConfig.hsNameIsDifferent) { + // TODO: TravisR - Use tooltip to underline + yourMatrixAccountText = _t('Create your Matrix account on ', {}, { + 'underlinedServerName': () => {this.props.serverConfig.hsName}, }); - } else { - try { - const parsedHsUrl = new URL(this.props.hsUrl); - yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', { - serverName: parsedHsUrl.hostname, - }); - } catch (e) { - // ignore - } } let editLink = null; diff --git a/src/components/views/auth/ServerTypeSelector.js b/src/components/views/auth/ServerTypeSelector.js index 71d13da421..602de72f3f 100644 --- a/src/components/views/auth/ServerTypeSelector.js +++ b/src/components/views/auth/ServerTypeSelector.js @@ -19,6 +19,8 @@ import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import classnames from 'classnames'; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; +import {makeType} from "../../../utils/TypeUtils"; const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication'; @@ -32,8 +34,13 @@ export const TYPES = { label: () => _t('Free'), logo: () => , description: () => _t('Join millions for free on the largest public server'), - hsUrl: 'https://matrix.org', - isUrl: 'https://vector.im', + serverConfig: makeType(ValidatedServerConfig, { + hsUrl: "https://matrix.org", + hsName: "matrix.org", + hsNameIsDifferent: false, + isUrl: "https://vector.im", + identityEnabled: true, + }), }, PREMIUM: { id: PREMIUM, @@ -44,6 +51,7 @@ export const TYPES = { {sub} , }), + identityServerUrl: "https://vector.im", }, ADVANCED: { id: ADVANCED, @@ -56,10 +64,11 @@ export const TYPES = { }, }; -export function getTypeFromHsUrl(hsUrl) { +export function getTypeFromServerConfig(config) { + const {hsUrl} = config; if (!hsUrl) { return null; - } else if (hsUrl === TYPES.FREE.hsUrl) { + } else if (hsUrl === TYPES.FREE.serverConfig.hsUrl) { return FREE; } else if (new URL(hsUrl).hostname.endsWith('.modular.im')) { // This is an unlikely case to reach, as Modular defaults to hiding the @@ -76,7 +85,7 @@ export default class ServerTypeSelector extends React.PureComponent { selected: PropTypes.string, // Handler called when the selected type changes. onChange: PropTypes.func.isRequired, - } + }; constructor(props) { super(props); @@ -106,7 +115,7 @@ export default class ServerTypeSelector extends React.PureComponent { e.stopPropagation(); const type = e.currentTarget.dataset.id; this.updateSelectedType(type); - } + }; render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); diff --git a/src/utils/TypeUtils.js b/src/utils/TypeUtils.js new file mode 100644 index 0000000000..abdd0eb2a0 --- /dev/null +++ b/src/utils/TypeUtils.js @@ -0,0 +1,30 @@ +/* +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. +*/ + +/** + * Creates a class of a given type using the objects defined. This + * is a stopgap function while we don't have TypeScript interfaces. + * In future, we'd define the `type` as an interface and just cast + * it instead of cheating like we are here. + * @param {Type} Type The type of class to construct. + * @param {*} opts The options (properties) to set on the object. + * @returns {*} The created object. + */ +export function makeType(Type: any, opts: any) { + const c = new Type(); + Object.assign(c, opts); + return c; +} From 00ebb5e1fd1c5246e99d44400939960d23b6b70c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 23:04:06 -0600 Subject: [PATCH 03/22] Make registration work with server configs The general idea is that we throw the object around between components so they can pull off the details they care about. --- .../structures/auth/Registration.js | 103 ++++++++---------- 1 file changed, 45 insertions(+), 58 deletions(-) diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index c579b2082d..faab8190bd 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -17,16 +17,15 @@ limitations under the License. */ import Matrix from 'matrix-js-sdk'; - import Promise from 'bluebird'; import React from 'react'; import PropTypes from 'prop-types'; - import sdk from '../../../index'; import { _t, _td } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; import * as ServerType from '../../views/auth/ServerTypeSelector'; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; // Phases // Show controls to configure server details @@ -46,18 +45,7 @@ module.exports = React.createClass({ sessionId: PropTypes.string, makeRegistrationUrl: PropTypes.func.isRequired, idSid: PropTypes.string, - // The default server name to use when the user hasn't specified - // one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this - // via `.well-known` discovery. The server name is used instead of the - // HS URL when talking about "your account". - defaultServerName: PropTypes.string, - // An error passed along from higher up explaining that something - // went wrong when finding the defaultHsUrl. - defaultServerDiscoveryError: PropTypes.string, - customHsUrl: PropTypes.string, - customIsUrl: PropTypes.string, - defaultHsUrl: PropTypes.string, - defaultIsUrl: PropTypes.string, + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, brand: PropTypes.string, email: PropTypes.string, // registration shouldn't know or care how login is done. @@ -66,7 +54,7 @@ module.exports = React.createClass({ }, getInitialState: function() { - const serverType = ServerType.getTypeFromHsUrl(this.props.customHsUrl); + const serverType = ServerType.getTypeFromServerConfig(this.props.serverConfig); return { busy: false, @@ -87,8 +75,6 @@ module.exports = React.createClass({ // straight back into UI auth doingUIAuth: Boolean(this.props.sessionId), serverType, - hsUrl: this.props.customHsUrl, - isUrl: this.props.customIsUrl, // Phase of the overall registration dialog. phase: PHASE_REGISTRATION, flows: null, @@ -100,18 +86,22 @@ module.exports = React.createClass({ this._replaceClient(); }, - onServerConfigChange: function(config) { - const newState = {}; - if (config.hsUrl !== undefined) { - newState.hsUrl = config.hsUrl; + componentWillReceiveProps(newProps) { + if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && + newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; + + 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. + const serverType = ServerType.getTypeFromServerConfig(newProps.serverConfig); + if (serverType !== this.state.serverType) { + // Reset the phase to default phase for the server type. + this.setState({ + serverType, + phase: this.getDefaultPhaseForServerType(serverType), + }); } - if (config.isUrl !== undefined) { - newState.isUrl = config.isUrl; - } - this.props.onServerConfigChange(config); - this.setState(newState, () => { - this._replaceClient(); - }); }, getDefaultPhaseForServerType(type) { @@ -136,19 +126,17 @@ module.exports = React.createClass({ // the new type. switch (type) { case ServerType.FREE: { - const { hsUrl, isUrl } = ServerType.TYPES.FREE; - this.onServerConfigChange({ - hsUrl, - isUrl, - }); + const { serverConfig } = ServerType.TYPES.FREE; + this.props.onServerConfigChange(serverConfig); break; } case ServerType.PREMIUM: + // We can accept whatever server config was the default here as this essentially + // acts as a slightly different "custom server"/ADVANCED option. + break; case ServerType.ADVANCED: - this.onServerConfigChange({ - hsUrl: this.props.defaultHsUrl, - isUrl: this.props.defaultIsUrl, - }); + // Use the default config from the config + this.props.onServerConfigChange(SdkConfig.get()["validated_server_config"]); break; } @@ -158,13 +146,15 @@ module.exports = React.createClass({ }); }, - _replaceClient: async function() { + _replaceClient: async function(serverConfig) { this.setState({ errorText: null, }); + if (!serverConfig) serverConfig = this.props.serverConfig; + const {hsUrl, isUrl} = serverConfig; this._matrixClient = Matrix.createClient({ - baseUrl: this.state.hsUrl, - idBaseUrl: this.state.isUrl, + baseUrl: hsUrl, + idBaseUrl: isUrl, }); try { await this._makeRegisterRequest({}); @@ -189,12 +179,6 @@ module.exports = React.createClass({ }, onFormSubmit: function(formVals) { - // Don't allow the user to register if there's a discovery error - // Without this, the user could end up registering on the wrong homeserver. - if (this.props.defaultServerDiscoveryError) { - this.setState({errorText: this.props.defaultServerDiscoveryError}); - return; - } this.setState({ errorText: "", busy: true, @@ -207,7 +191,7 @@ module.exports = React.createClass({ if (!success) { let msg = response.message || response.toString(); // can we give a better error message? - if (response.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') { + if (response.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { const errorTop = messageForResourceLimitError( response.data.limit_type, response.data.admin_contact, { @@ -302,8 +286,13 @@ module.exports = React.createClass({ }); }, - onServerDetailsNextPhaseClick(ev) { + async onServerDetailsNextPhaseClick(ev) { ev.stopPropagation(); + // TODO: TravisR - Capture the user's input somehow else + if (this._serverConfigRef) { + // Just to make sure the user's input gets captured + await this._serverConfigRef.validateServer(); + } this.setState({ phase: PHASE_REGISTRATION, }); @@ -371,20 +360,17 @@ module.exports = React.createClass({ break; case ServerType.PREMIUM: serverDetails = this._serverConfigRef = r} + serverConfig={this.props.serverConfig} + onServerConfigChange={this.props.onServerConfigChange} delayTimeMs={250} />; break; case ServerType.ADVANCED: serverDetails = this._serverConfigRef = r} + serverConfig={this.props.serverConfig} + onServerConfigChange={this.props.onServerConfigChange} delayTimeMs={250} />; break; @@ -392,6 +378,7 @@ module.exports = React.createClass({ let nextButton = null; if (PHASES_ENABLED) { + // TODO: TravisR - Pull out server discovery from ServerConfig to disable the next button? nextButton = @@ -466,7 +453,7 @@ module.exports = React.createClass({ const AuthPage = sdk.getComponent('auth.AuthPage'); let errorText; - const err = this.state.errorText || this.props.defaultServerDiscoveryError; + const err = this.state.errorText; if (err) { errorText =
{ err }
; } From b6e027f5cb101d5f70f05161852744eaf32bc401 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 23:05:59 -0600 Subject: [PATCH 04/22] Make password resets use server config objects Like registration, the idea is that the object is passed around between components so they can take details they need. --- .../structures/auth/ForgotPassword.js | 88 +++++-------------- 1 file changed, 23 insertions(+), 65 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 46071f0a9c..5316235fe0 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -21,8 +21,8 @@ import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import Modal from "../../../Modal"; import SdkConfig from "../../../SdkConfig"; - import PasswordReset from "../../../PasswordReset"; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; // Phases // Show controls to configure server details @@ -40,28 +40,14 @@ module.exports = React.createClass({ displayName: 'ForgotPassword', propTypes: { - // The default server name to use when the user hasn't specified - // one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this - // via `.well-known` discovery. The server name is used instead of the - // HS URL when talking about "your account". - defaultServerName: PropTypes.string, - // An error passed along from higher up explaining that something - // went wrong when finding the defaultHsUrl. - defaultServerDiscoveryError: PropTypes.string, - - defaultHsUrl: PropTypes.string, - defaultIsUrl: PropTypes.string, - customHsUrl: PropTypes.string, - customIsUrl: PropTypes.string, - + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, + onServerConfigChange: PropTypes.func.isRequired, onLoginClick: PropTypes.func, onComplete: PropTypes.func.isRequired, }, getInitialState: function() { return { - enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl, - enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl, phase: PHASE_FORGOT, email: "", password: "", @@ -70,11 +56,11 @@ module.exports = React.createClass({ }; }, - submitPasswordReset: function(hsUrl, identityUrl, email, password) { + submitPasswordReset: function(email, password) { this.setState({ phase: PHASE_SENDING_EMAIL, }); - this.reset = new PasswordReset(hsUrl, identityUrl); + this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl); this.reset.resetPassword(email, password).done(() => { this.setState({ phase: PHASE_EMAIL_SENT, @@ -103,13 +89,6 @@ module.exports = React.createClass({ onSubmitForm: function(ev) { ev.preventDefault(); - // Don't allow the user to register if there's a discovery error - // Without this, the user could end up registering on the wrong homeserver. - if (this.props.defaultServerDiscoveryError) { - this.setState({errorText: this.props.defaultServerDiscoveryError}); - return; - } - if (!this.state.email) { this.showErrorDialog(_t('The email address linked to your account must be entered.')); } else if (!this.state.password || !this.state.password2) { @@ -132,10 +111,7 @@ module.exports = React.createClass({ button: _t('Continue'), onFinished: (confirmed) => { if (confirmed) { - this.submitPasswordReset( - this.state.enteredHsUrl, this.state.enteredIsUrl, - this.state.email, this.state.password, - ); + this.submitPasswordReset(this.state.email, this.state.password); } }, }); @@ -148,19 +124,13 @@ module.exports = React.createClass({ }); }, - onServerConfigChange: function(config) { - const newState = {}; - if (config.hsUrl !== undefined) { - newState.enteredHsUrl = config.hsUrl; - } - if (config.isUrl !== undefined) { - newState.enteredIsUrl = config.isUrl; - } - this.setState(newState); - }, - - onServerDetailsNextPhaseClick(ev) { + async onServerDetailsNextPhaseClick(ev) { ev.stopPropagation(); + // TODO: TravisR - Capture the user's input somehow else + if (this._serverConfigRef) { + // Just to make sure the user's input gets captured + await this._serverConfigRef.validateServer(); + } this.setState({ phase: PHASE_FORGOT, }); @@ -196,13 +166,12 @@ module.exports = React.createClass({ return null; } + // TODO: TravisR - Pull out server discovery from ServerConfig to disable the next button? return
this._serverConfigRef = r} + serverConfig={this.props.serverConfig} + onServerConfigChange={this.props.onServerConfigChange} delayTimeMs={0} /> { err }
; } - let yourMatrixAccountText = _t('Your Matrix account'); - if (this.state.enteredHsUrl === this.props.defaultHsUrl && this.props.defaultServerName) { - yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', { - serverName: this.props.defaultServerName, + let yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', { + serverName: this.props.serverConfig.hsName, + }); + if (this.props.serverConfig.hsNameIsDifferent) { + // TODO: TravisR - Use tooltip to underline + yourMatrixAccountText = _t('Your Matrix account on ', {}, { + 'underlinedServerName': () => {this.props.serverConfig.hsName}, }); - } else { - try { - const parsedHsUrl = new URL(this.state.enteredHsUrl); - yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', { - serverName: parsedHsUrl.hostname, - }); - } catch (e) { - errorText =
{_t( - "The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please " + - "enter a valid URL including the protocol prefix.", - { - hsUrl: this.state.enteredHsUrl, - })}
; - } } // If custom URLs are allowed, wire up the server details edit link. From 0b1a0c77b7fe9a9eef7f134041cb438ac6ad47c6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 23:07:40 -0600 Subject: [PATCH 05/22] Make login pass around server config objects Very similar to password resets and registration, the components pass around a server config for usage by other components. Login is a bit more complicated and needs a few more changes to pull the logic out to a more generic layer. --- src/components/structures/auth/Login.js | 189 ++++++--------------- src/components/views/auth/PasswordLogin.js | 77 ++++----- 2 files changed, 86 insertions(+), 180 deletions(-) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 2940346a4f..46bf0c2c76 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -25,7 +25,7 @@ import sdk from '../../../index'; import Login from '../../../Login'; import SdkConfig from '../../../SdkConfig'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; -import { AutoDiscovery } from "matrix-js-sdk"; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; // For validating phone numbers without country codes const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; @@ -59,19 +59,14 @@ module.exports = React.createClass({ propTypes: { onLoggedIn: PropTypes.func.isRequired, - // The default server name to use when the user hasn't specified - // one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this - // via `.well-known` discovery. The server name is used instead of the - // HS URL when talking about where to "sign in to". - defaultServerName: PropTypes.string, // An error passed along from higher up explaining that something - // went wrong when finding the defaultHsUrl. - defaultServerDiscoveryError: PropTypes.string, + // went wrong. May be replaced with a different error within the + // Login component. + errorText: PropTypes.string, + + // If true, the component will consider itself busy. + busy: PropTypes.bool, - customHsUrl: PropTypes.string, - customIsUrl: PropTypes.string, - defaultHsUrl: PropTypes.string, - defaultIsUrl: PropTypes.string, // 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. @@ -79,12 +74,13 @@ module.exports = React.createClass({ defaultDeviceDisplayName: PropTypes.string, - // login shouldn't know or care how registration is done. + // login shouldn't know or care how registration, password recovery, + // etc is done. onRegisterClick: PropTypes.func.isRequired, - - // login shouldn't care how password recovery is done. onForgotPasswordClick: PropTypes.func, onServerConfigChange: PropTypes.func.isRequired, + + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, }, getInitialState: function() { @@ -93,9 +89,6 @@ module.exports = React.createClass({ errorText: null, loginIncorrect: false, - enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl, - enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl, - // used for preserving form values when changing homeserver username: "", phoneCountry: null, @@ -105,10 +98,6 @@ module.exports = React.createClass({ phase: PHASE_LOGIN, // The current login flow, such as password, SSO, etc. currentFlow: "m.login.password", - - // .well-known discovery - discoveryError: "", - findingHomeserver: false, }; }, @@ -139,10 +128,17 @@ module.exports = React.createClass({ }); }, + isBusy: function() { + return this.state.busy || this.props.busy; + }, + + hasError: function() { + return this.state.errorText || this.props.errorText; + }, + onPasswordLogin: function(username, phoneCountry, phoneNumber, password) { - // Prevent people from submitting their password when homeserver - // discovery went wrong - if (this.state.discoveryError || this.props.defaultServerDiscoveryError) return; + // Prevent people from submitting their password when something isn't right. + if (this.isBusy() || this.hasError()) return; this.setState({ busy: true, @@ -164,7 +160,7 @@ module.exports = React.createClass({ const usingEmail = username.indexOf("@") > 0; if (error.httpStatus === 400 && usingEmail) { errorText = _t('This homeserver does not support login using email address.'); - } else if (error.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') { + } else if (error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { const errorTop = messageForResourceLimitError( error.data.limit_type, error.data.admin_contact, { @@ -194,11 +190,10 @@ module.exports = React.createClass({
{ _t('Incorrect username and/or password.') }
- { _t('Please note you are logging into the %(hs)s server, not matrix.org.', - { - hs: this.props.defaultHsUrl.replace(/^https?:\/\//, ''), - }) - } + {_t( + 'Please note you are logging into the %(hs)s server, not matrix.org.', + {hs: this.props.serverConfig.hsName}, + )}
); @@ -235,9 +230,9 @@ module.exports = React.createClass({ onUsernameBlur: function(username) { this.setState({ username: username, - discoveryError: null, + errorText: null, }); - if (username[0] === "@") { + if (username[0] === "@" && false) { // TODO: TravisR - Restore this const serverName = username.split(':').slice(1).join(':'); try { // we have to append 'https://' to make the URL constructor happy @@ -246,7 +241,7 @@ module.exports = React.createClass({ this._tryWellKnownDiscovery(url.hostname); } catch (e) { console.error("Problem parsing URL or unhandled error doing .well-known discovery:", e); - this.setState({discoveryError: _t("Failed to perform homeserver discovery")}); + this.setState({errorText: _t("Failed to perform homeserver discovery")}); } } }, @@ -274,32 +269,19 @@ module.exports = React.createClass({ } }, - onServerConfigChange: function(config) { - const self = this; - const newState = { - errorText: null, // reset err messages - }; - if (config.hsUrl !== undefined) { - newState.enteredHsUrl = config.hsUrl; - } - if (config.isUrl !== undefined) { - newState.enteredIsUrl = config.isUrl; - } - - this.props.onServerConfigChange(config); - this.setState(newState, function() { - self._initLoginLogic(config.hsUrl || null, config.isUrl); - }); - }, - onRegisterClick: function(ev) { ev.preventDefault(); ev.stopPropagation(); this.props.onRegisterClick(); }, - onServerDetailsNextPhaseClick(ev) { + async onServerDetailsNextPhaseClick(ev) { ev.stopPropagation(); + // TODO: TravisR - Capture the user's input somehow else + if (this._serverConfigRef) { + // Just to make sure the user's input gets captured + await this._serverConfigRef.validateServer(); + } this.setState({ phase: PHASE_LOGIN, }); @@ -313,64 +295,13 @@ module.exports = React.createClass({ }); }, - _tryWellKnownDiscovery: async function(serverName) { - if (!serverName.trim()) { - // Nothing to discover - this.setState({ - discoveryError: "", - findingHomeserver: false, - }); - return; - } - - this.setState({findingHomeserver: true}); - try { - const discovery = await AutoDiscovery.findClientConfig(serverName); - - const state = discovery["m.homeserver"].state; - if (state !== AutoDiscovery.SUCCESS && state !== AutoDiscovery.PROMPT) { - this.setState({ - discoveryError: discovery["m.homeserver"].error, - findingHomeserver: false, - }); - } else if (state === AutoDiscovery.PROMPT) { - this.setState({ - discoveryError: "", - findingHomeserver: false, - }); - } else if (state === AutoDiscovery.SUCCESS) { - this.setState({ - discoveryError: "", - findingHomeserver: false, - }); - this.onServerConfigChange({ - hsUrl: discovery["m.homeserver"].base_url, - isUrl: discovery["m.identity_server"].state === AutoDiscovery.SUCCESS - ? discovery["m.identity_server"].base_url - : "", - }); - } else { - console.warn("Unknown state for m.homeserver in discovery response: ", discovery); - this.setState({ - discoveryError: _t("Unknown failure discovering homeserver"), - findingHomeserver: false, - }); - } - } catch (e) { - console.error(e); - this.setState({ - findingHomeserver: false, - discoveryError: _t("Unknown error discovering homeserver"), - }); - } - }, - _initLoginLogic: function(hsUrl, isUrl) { const self = this; - hsUrl = hsUrl || this.state.enteredHsUrl; - isUrl = isUrl || this.state.enteredIsUrl; + hsUrl = hsUrl || this.props.serverConfig.hsUrl; + isUrl = isUrl || this.props.serverConfig.isUrl; - const fallbackHsUrl = hsUrl === this.props.defaultHsUrl ? this.props.fallbackHsUrl : null; + // TODO: TravisR - Only use this if the homeserver is the default homeserver + const fallbackHsUrl = this.props.fallbackHsUrl; const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, { defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, @@ -378,8 +309,6 @@ module.exports = React.createClass({ this._loginLogic = loginLogic; this.setState({ - enteredHsUrl: hsUrl, - enteredIsUrl: isUrl, busy: true, loginIncorrect: false, }); @@ -445,8 +374,8 @@ module.exports = React.createClass({ if (err.cors === 'rejected') { if (window.location.protocol === 'https:' && - (this.state.enteredHsUrl.startsWith("http:") || - !this.state.enteredHsUrl.startsWith("http")) + (this.props.serverConfig.hsUrl.startsWith("http:") || + !this.props.serverConfig.hsUrl.startsWith("http")) ) { errorText = { _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " + @@ -469,9 +398,9 @@ module.exports = React.createClass({ "is not blocking requests.", {}, { 'a': (sub) => { - return { sub }; + return + { sub } + ; }, }, ) } @@ -495,19 +424,17 @@ module.exports = React.createClass({ } const serverDetails = this._serverConfigRef = r} + serverConfig={this.props.serverConfig} + onServerConfigChange={this.props.onServerConfigChange} delayTimeMs={250} />; let nextButton = null; if (PHASES_ENABLED) { + // TODO: TravisR - Pull out server discovery from ServerConfig to disable the next button? nextButton = + onClick={this.onServerDetailsNextPhaseClick}> {_t("Next")} ; } @@ -547,13 +474,6 @@ module.exports = React.createClass({ onEditServerDetailsClick = this.onEditServerDetailsClick; } - // If the current HS URL is the default HS URL, then we can label it - // with the default HS name (if it exists). - let hsName; - if (this.state.enteredHsUrl === this.props.defaultHsUrl) { - hsName = this.props.defaultServerName; - } - return ( + serverConfig={this.props.serverConfig} + disableSubmit={this.isBusy()} + /> ); }, @@ -595,9 +514,9 @@ module.exports = React.createClass({ const AuthPage = sdk.getComponent("auth.AuthPage"); const AuthHeader = sdk.getComponent("auth.AuthHeader"); const AuthBody = sdk.getComponent("auth.AuthBody"); - const loader = this.state.busy ?
: null; + const loader = this.isBusy() ?
: null; - const errorText = this.props.defaultServerDiscoveryError || this.state.discoveryError || this.state.errorText; + const errorText = this.state.errorText || this.props.errorText; let errorTextSection; if (errorText) { diff --git a/src/components/views/auth/PasswordLogin.js b/src/components/views/auth/PasswordLogin.js index ed3afede2f..90c607442f 100644 --- a/src/components/views/auth/PasswordLogin.js +++ b/src/components/views/auth/PasswordLogin.js @@ -1,6 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd +Copyright 2017,2019 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,11 +21,29 @@ import classNames from 'classnames'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; /** * A pure UI component which displays a username/password form. */ -class PasswordLogin extends React.Component { +export default class PasswordLogin extends React.Component { + static propTypes = { + onSubmit: PropTypes.func.isRequired, // fn(username, password) + onError: PropTypes.func, + onForgotPasswordClick: PropTypes.func, // fn() + initialUsername: PropTypes.string, + initialPhoneCountry: PropTypes.string, + initialPhoneNumber: PropTypes.string, + initialPassword: PropTypes.string, + onUsernameChanged: PropTypes.func, + onPhoneCountryChanged: PropTypes.func, + onPhoneNumberChanged: PropTypes.func, + onPasswordChanged: PropTypes.func, + loginIncorrect: PropTypes.bool, + disableSubmit: PropTypes.bool, + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, + }; + static defaultProps = { onError: function() {}, onEditServerDetailsClick: null, @@ -40,13 +58,12 @@ class PasswordLogin extends React.Component { initialPhoneNumber: "", initialPassword: "", loginIncorrect: false, - // This is optional and only set if we used a server name to determine - // the HS URL via `.well-known` discovery. The server name is used - // instead of the HS URL when talking about where to "sign in to". - hsName: null, - hsUrl: "", disableSubmit: false, - } + }; + + static LOGIN_FIELD_EMAIL = "login_field_email"; + static LOGIN_FIELD_MXID = "login_field_mxid"; + static LOGIN_FIELD_PHONE = "login_field_phone"; constructor(props) { super(props); @@ -258,20 +275,14 @@ class PasswordLogin extends React.Component {
; } - let signInToText = _t('Sign in to your Matrix account'); - if (this.props.hsName) { - signInToText = _t('Sign in to your Matrix account on %(serverName)s', { - serverName: this.props.hsName, + let signInToText = _t('Sign in to your Matrix account on %(serverName)s', { + serverName: this.props.serverConfig.hsName, + }); + if (this.props.serverConfig.hsNameIsDifferent) { + // TODO: TravisR - Use tooltip to underline + signInToText = _t('Sign in to your Matrix account on ', {}, { + 'underlinedServerName': () => {this.props.serverConfig.hsName}, }); - } else { - try { - const parsedHsUrl = new URL(this.props.hsUrl); - signInToText = _t('Sign in to your Matrix account on %(serverName)s', { - serverName: parsedHsUrl.hostname, - }); - } catch (e) { - // ignore - } } let editLink = null; @@ -353,27 +364,3 @@ class PasswordLogin extends React.Component { ); } } - -PasswordLogin.LOGIN_FIELD_EMAIL = "login_field_email"; -PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid"; -PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone"; - -PasswordLogin.propTypes = { - onSubmit: PropTypes.func.isRequired, // fn(username, password) - onError: PropTypes.func, - onForgotPasswordClick: PropTypes.func, // fn() - initialUsername: PropTypes.string, - initialPhoneCountry: PropTypes.string, - initialPhoneNumber: PropTypes.string, - initialPassword: PropTypes.string, - onUsernameChanged: PropTypes.func, - onPhoneCountryChanged: PropTypes.func, - onPhoneNumberChanged: PropTypes.func, - onPasswordChanged: PropTypes.func, - loginIncorrect: PropTypes.bool, - hsName: PropTypes.string, - hsUrl: PropTypes.string, - disableSubmit: PropTypes.bool, -}; - -module.exports = PasswordLogin; From 1f527e71b1296a5a5747160f1e0fbf7aea27bbfd Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 23:09:07 -0600 Subject: [PATCH 06/22] Bring server config juggling into MatrixChat This way the server config is consistent across login, password reset, and registration. This also brings the code into a more generic place for all 3 duplicated efforts. --- src/components/structures/MatrixChat.js | 155 +++--------------------- 1 file changed, 17 insertions(+), 138 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 277985ba1d..ca9ddec749 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -50,8 +50,7 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import { startAnyRegistrationFlow } from "../../Registration.js"; import { messageForSyncError } from '../../utils/ErrorUtils'; import ResizeNotifier from "../../utils/ResizeNotifier"; - -const AutoDiscovery = Matrix.AutoDiscovery; +import {ValidatedDiscoveryConfig} from "../../utils/AutoDiscoveryUtils"; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -181,16 +180,8 @@ export default React.createClass({ // Parameters used in the registration dance with the IS register_client_secret: null, register_session_id: null, - register_hs_url: null, - register_is_url: null, register_id_sid: null, - // Parameters used for setting up the authentication views - defaultServerName: this.props.config.default_server_name, - defaultHsUrl: this.props.config.default_hs_url, - defaultIsUrl: this.props.config.default_is_url, - defaultServerDiscoveryError: null, - // When showing Modal dialogs we need to set aria-hidden on the root app element // and disable it when there are no dialogs hideToSRUsers: false, @@ -211,42 +202,15 @@ export default React.createClass({ }; }, - getDefaultServerName: function() { - return this.state.defaultServerName; - }, - - getCurrentHsUrl: function() { - if (this.state.register_hs_url) { - return this.state.register_hs_url; - } else if (MatrixClientPeg.get()) { - return MatrixClientPeg.get().getHomeserverUrl(); - } else { - return this.getDefaultHsUrl(); - } - }, - - getDefaultHsUrl(defaultToMatrixDotOrg) { - defaultToMatrixDotOrg = typeof(defaultToMatrixDotOrg) !== 'boolean' ? true : defaultToMatrixDotOrg; - if (!this.state.defaultHsUrl && defaultToMatrixDotOrg) return "https://matrix.org"; - return this.state.defaultHsUrl; - }, - + // TODO: TravisR - Remove this or put it somewhere else getFallbackHsUrl: function() { return this.props.config.fallback_hs_url; }, - getCurrentIsUrl: function() { - if (this.state.register_is_url) { - return this.state.register_is_url; - } else if (MatrixClientPeg.get()) { - return MatrixClientPeg.get().getIdentityServerUrl(); - } else { - return this.getDefaultIsUrl(); - } - }, - - getDefaultIsUrl() { - return this.state.defaultIsUrl || "https://vector.im"; + getServerProperties() { + let props = this.state.serverConfig; + if (!props) props = SdkConfig.get()["validated_server_config"]; + return {serverConfig: props}; }, componentWillMount: function() { @@ -260,40 +224,6 @@ export default React.createClass({ MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; } - // Set up the default URLs (async) - if (this.getDefaultServerName() && !this.getDefaultHsUrl(false)) { - this.setState({loadingDefaultHomeserver: true}); - this._tryDiscoverDefaultHomeserver(this.getDefaultServerName()); - } else if (this.getDefaultServerName() && this.getDefaultHsUrl(false)) { - // Ideally we would somehow only communicate this to the server admins, but - // given this is at login time we can't really do much besides hope that people - // will check their settings. - this.setState({ - defaultServerName: null, // To un-hide any secrets people might be keeping - defaultServerDiscoveryError: _t( - "Invalid configuration: Cannot supply a default homeserver URL and " + - "a default server name", - ), - }); - } - - // Set a default HS with query param `hs_url` - const paramHs = this.props.startingFragmentQueryParams.hs_url; - if (paramHs) { - console.log('Setting register_hs_url ', paramHs); - this.setState({ - register_hs_url: paramHs, - }); - } - // Set a default IS with query param `is_url` - const paramIs = this.props.startingFragmentQueryParams.is_url; - if (paramIs) { - console.log('Setting register_is_url ', paramIs); - this.setState({ - register_is_url: paramIs, - }); - } - // a thing to call showScreen with once login completes. this is kept // outside this.state because updating it should never trigger a // rerender. @@ -374,8 +304,8 @@ export default React.createClass({ return Lifecycle.loadSession({ fragmentQueryParams: this.props.startingFragmentQueryParams, enableGuest: this.props.enableGuest, - guestHsUrl: this.getCurrentHsUrl(), - guestIsUrl: this.getCurrentIsUrl(), + guestHsUrl: this.getServerProperties().serverConfig.hsUrl, + guestIsUrl: this.getServerProperties().serverConfig.isUrl, defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, }); }).then((loadedSession) => { @@ -1823,44 +1753,7 @@ export default React.createClass({ }, onServerConfigChange(config) { - const newState = {}; - if (config.hsUrl) { - newState.register_hs_url = config.hsUrl; - } - if (config.isUrl) { - newState.register_is_url = config.isUrl; - } - this.setState(newState); - }, - - _tryDiscoverDefaultHomeserver: async function(serverName) { - try { - const discovery = await AutoDiscovery.findClientConfig(serverName); - const state = discovery["m.homeserver"].state; - if (state !== AutoDiscovery.SUCCESS) { - console.error("Failed to discover homeserver on startup:", discovery); - this.setState({ - defaultServerDiscoveryError: discovery["m.homeserver"].error, - loadingDefaultHomeserver: false, - }); - } else { - const hsUrl = discovery["m.homeserver"].base_url; - const isUrl = discovery["m.identity_server"].state === AutoDiscovery.SUCCESS - ? discovery["m.identity_server"].base_url - : "https://vector.im"; - this.setState({ - defaultHsUrl: hsUrl, - defaultIsUrl: isUrl, - loadingDefaultHomeserver: false, - }); - } - } catch (e) { - console.error(e); - this.setState({ - defaultServerDiscoveryError: _t("Unknown error discovering homeserver"), - loadingDefaultHomeserver: false, - }); - } + this.setState({serverConfig: config}); }, _makeRegistrationUrl: function(params) { @@ -1879,8 +1772,7 @@ export default React.createClass({ if ( this.state.view === VIEWS.LOADING || - this.state.view === VIEWS.LOGGING_IN || - this.state.loadingDefaultHomeserver + this.state.view === VIEWS.LOGGING_IN ) { const Spinner = sdk.getComponent('elements.Spinner'); return ( @@ -1958,18 +1850,13 @@ export default React.createClass({ sessionId={this.state.register_session_id} idSid={this.state.register_id_sid} email={this.props.startingFragmentQueryParams.email} - defaultServerName={this.getDefaultServerName()} - defaultServerDiscoveryError={this.state.defaultServerDiscoveryError} - defaultHsUrl={this.getDefaultHsUrl()} - defaultIsUrl={this.getDefaultIsUrl()} brand={this.props.config.brand} - customHsUrl={this.getCurrentHsUrl()} - customIsUrl={this.getCurrentIsUrl()} makeRegistrationUrl={this._makeRegistrationUrl} onLoggedIn={this.onRegistered} onLoginClick={this.onLoginClick} onServerConfigChange={this.onServerConfigChange} - /> + {...this.getServerProperties()} + /> ); } @@ -1978,14 +1865,11 @@ export default React.createClass({ const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword'); return ( + onLoginClick={this.onLoginClick} + onServerConfigChange={this.onServerConfigChange} + {...this.getServerProperties()} + /> ); } @@ -1995,16 +1879,11 @@ export default React.createClass({ ); } From bb6ee10d8c4ac46c8547ae3263a352ef77e6886b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 23:09:31 -0600 Subject: [PATCH 07/22] Add language features to support server config changes --- src/i18n/strings/en_EN.json | 13 ++++++------- src/languageHandler.js | 12 ++++++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index eaea057b36..35593708c8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -246,6 +246,8 @@ "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …", + "No homeserver URL provided": "No homeserver URL provided", + "Unexpected error resolving homeserver configuration": "Unexpected error resolving homeserver configuration", "This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.", "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.", "Please contact your service administrator to continue using the service.": "Please contact your service administrator to continue using the service.", @@ -1306,6 +1308,7 @@ "Code": "Code", "Submit": "Submit", "Start authentication": "Start authentication", + "Unable to validate homeserver/identity server": "Unable to validate homeserver/identity server", "Your Modular server": "Your Modular server", "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of modular.im.": "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of modular.im.", "Server Name": "Server Name", @@ -1318,8 +1321,8 @@ "Username": "Username", "Phone": "Phone", "Not sure of your password? Set a new one": "Not sure of your password? Set a new one", - "Sign in to your Matrix account": "Sign in to your Matrix account", "Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s", + "Sign in to your Matrix account on ": "Sign in to your Matrix account on ", "Change": "Change", "Sign in with": "Sign in with", "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?", @@ -1339,8 +1342,8 @@ "Email (optional)": "Email (optional)", "Confirm": "Confirm", "Phone (optional)": "Phone (optional)", - "Create your Matrix account": "Create your Matrix account", "Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s", + "Create your Matrix account on ": "Create your Matrix account on ", "Use an email address to recover your account.": "Use an email address to recover your account.", "Other users can invite you to rooms using your contact details.": "Other users can invite you to rooms using your contact details.", "Other servers": "Other servers", @@ -1407,7 +1410,6 @@ "This homeserver does not support communities": "This homeserver does not support communities", "Failed to load %(groupId)s": "Failed to load %(groupId)s", "Filter room names": "Filter room names", - "Invalid configuration: Cannot supply a default homeserver URL and a default server name": "Invalid configuration: Cannot supply a default homeserver URL and a default server name", "Failed to reject invitation": "Failed to reject invitation", "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.", "Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?", @@ -1423,7 +1425,6 @@ "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.", "You are logged in to another account": "You are logged in to another account", "Thank you for verifying your email! The account you're logged into here (%(sessionUserId)s) appears to be different from the account you've verified an email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, please log out first.": "Thank you for verifying your email! The account you're logged into here (%(sessionUserId)s) appears to be different from the account you've verified an email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, please log out first.", - "Unknown error discovering homeserver": "Unknown error discovering homeserver", "Logout": "Logout", "Your Communities": "Your Communities", "Did you know: you can use communities to filter your Riot.im experience!": "Did you know: you can use communities to filter your Riot.im experience!", @@ -1491,9 +1492,8 @@ "A new password must be entered.": "A new password must be entered.", "New passwords must match each other.": "New passwords must match each other.", "Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another device before resetting your password.": "Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another device before resetting your password.", - "Your Matrix account": "Your Matrix account", "Your Matrix account on %(serverName)s": "Your Matrix account on %(serverName)s", - "The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please enter a valid URL including the protocol prefix.": "The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please enter a valid URL including the protocol prefix.", + "Your Matrix account on ": "Your Matrix account on ", "A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.", "Send Reset Email": "Send Reset Email", "Sign in instead": "Sign in instead", @@ -1517,7 +1517,6 @@ "Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.", "Failed to perform homeserver discovery": "Failed to perform homeserver discovery", "The phone number entered looks invalid": "The phone number entered looks invalid", - "Unknown failure discovering homeserver": "Unknown failure discovering homeserver", "This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.", "Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.", "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or 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.", diff --git a/src/languageHandler.js b/src/languageHandler.js index 854ac079bc..bd3a8df721 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -32,6 +32,18 @@ counterpart.setSeparator('|'); // Fall back to English counterpart.setFallbackLocale('en'); +/** + * Helper function to create an error which has an English message + * with a translatedMessage property for use by the consumer. + * @param {string} message Message to translate. + * @returns {Error} The constructed error. + */ +export function newTranslatableError(message) { + const error = new Error(message); + error.translatedMessage = _t(message); + return error; +} + // Function which only purpose is to mark that a string is translatable // Does not actually do anything. It's helpful for automatic extraction of translatable strings export function _td(s) { From a4b64649029b1f51a2da2d9143ccade636039ccd Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 23:39:15 -0600 Subject: [PATCH 08/22] Appease the linter --- src/components/structures/MatrixChat.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index ca9ddec749..089c843e6f 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -50,7 +50,6 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import { startAnyRegistrationFlow } from "../../Registration.js"; import { messageForSyncError } from '../../utils/ErrorUtils'; import ResizeNotifier from "../../utils/ResizeNotifier"; -import {ValidatedDiscoveryConfig} from "../../utils/AutoDiscoveryUtils"; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. From ae63df95ea09ec4be2f9a50bf4988ed4d98a16ef Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 23:46:43 -0600 Subject: [PATCH 09/22] Fix tests to use new serverConfig prop --- test/components/structures/auth/Login-test.js | 4 ++-- test/components/structures/auth/Registration-test.js | 4 ++-- test/test-utils.js | 12 ++++++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/test/components/structures/auth/Login-test.js b/test/components/structures/auth/Login-test.js index ec95243a56..74451b922f 100644 --- a/test/components/structures/auth/Login-test.js +++ b/test/components/structures/auth/Login-test.js @@ -22,6 +22,7 @@ import ReactTestUtils from 'react-dom/test-utils'; import sdk from 'matrix-react-sdk'; import SdkConfig from '../../../../src/SdkConfig'; import * as TestUtils from '../../../test-utils'; +import {mkServerConfig} from "../../../test-utils"; const Login = sdk.getComponent( 'structures.auth.Login', @@ -44,8 +45,7 @@ describe('Login', function() { function render() { return ReactDOM.render( {}} onRegisterClick={() => {}} onServerConfigChange={() => {}} diff --git a/test/components/structures/auth/Registration-test.js b/test/components/structures/auth/Registration-test.js index a10201d465..6914ed71d7 100644 --- a/test/components/structures/auth/Registration-test.js +++ b/test/components/structures/auth/Registration-test.js @@ -22,6 +22,7 @@ import ReactTestUtils from 'react-dom/test-utils'; import sdk from 'matrix-react-sdk'; import SdkConfig from '../../../../src/SdkConfig'; import * as TestUtils from '../../../test-utils'; +import {mkServerConfig} from "../../../test-utils"; const Registration = sdk.getComponent( 'structures.auth.Registration', @@ -44,8 +45,7 @@ describe('Registration', function() { function render() { return ReactDOM.render( {}} onLoggedIn={() => {}} onLoginClick={() => {}} diff --git a/test/test-utils.js b/test/test-utils.js index f4f00effbb..54705434e2 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -7,6 +7,8 @@ import PropTypes from 'prop-types'; import peg from '../src/MatrixClientPeg'; import dis from '../src/dispatcher'; import jssdk from 'matrix-js-sdk'; +import {makeType} from "../src/utils/TypeUtils"; +import {ValidatedServerConfig} from "../src/utils/AutoDiscoveryUtils"; const MatrixEvent = jssdk.MatrixEvent; /** @@ -260,6 +262,16 @@ export function mkStubRoom(roomId = null) { }; } +export function mkServerConfig(hsUrl, isUrl) { + return makeType(ValidatedServerConfig, { + hsUrl, + hsName: "TEST_ENVIRONMENT", + hsNameIsDifferent: false, // yes, we lie + isUrl, + identityEnabled: true, + }); +} + export function getDispatchForStore(store) { // Mock the dispatcher by gut-wrenching. Stores can only __emitChange whilst a // dispatcher `_isDispatching` is true. From 4ada66d3195052a0617f5c49d4512d764da961cc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 23:59:54 -0600 Subject: [PATCH 10/22] Fix rogue instance of old hsUrl property --- src/components/views/auth/PasswordLogin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/auth/PasswordLogin.js b/src/components/views/auth/PasswordLogin.js index 90c607442f..f5b2aec210 100644 --- a/src/components/views/auth/PasswordLogin.js +++ b/src/components/views/auth/PasswordLogin.js @@ -212,7 +212,7 @@ export default class PasswordLogin extends React.Component { type="text" label={SdkConfig.get().disable_custom_urls ? _t("Username on %(hs)s", { - hs: this.props.hsUrl.replace(/^https?:\/\//, ''), + hs: this.props.serverConfig.hsName, }) : _t("Username")} value={this.state.username} onChange={this.onUsernameChanged} From 58b9eb4cb22220c9d90f21d966843199cad55535 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 3 May 2019 16:25:54 -0600 Subject: [PATCH 11/22] Add a serverConfig property to MatrixChat for unit tests --- src/components/structures/MatrixChat.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 089c843e6f..545f847718 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -50,6 +50,7 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import { startAnyRegistrationFlow } from "../../Registration.js"; import { messageForSyncError } from '../../utils/ErrorUtils'; import ResizeNotifier from "../../utils/ResizeNotifier"; +import {ValidatedServerConfig} from "../../utils/AutoDiscoveryUtils"; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -107,6 +108,7 @@ export default React.createClass({ propTypes: { config: PropTypes.object, + serverConfig: PropTypes.instanceOf(ValidatedServerConfig), ConferenceHandler: PropTypes.any, onNewScreen: PropTypes.func, registrationUrl: PropTypes.string, @@ -208,6 +210,7 @@ export default React.createClass({ getServerProperties() { let props = this.state.serverConfig; + if (!props) props = this.props.serverConfig; // for unit tests if (!props) props = SdkConfig.get()["validated_server_config"]; return {serverConfig: props}; }, From eab209a26b5bc6eb9cf13bc082fd506dea82e10a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 6 May 2019 12:00:48 -0600 Subject: [PATCH 12/22] Log in to the right homeserver when changing the homeserver --- src/components/structures/auth/Login.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 46bf0c2c76..af9370f2db 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -121,6 +121,14 @@ module.exports = React.createClass({ this._unmounted = true; }, + componentWillReceiveProps(newProps) { + if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && + 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); + }, + onPasswordLoginError: function(errorText) { this.setState({ errorText, From ee33a4e9ba6349136c68a5165c14e914b0d02135 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 13 May 2019 16:30:34 -0600 Subject: [PATCH 13/22] Refactor "Next" button into ServerConfig components TODO still remains about making ModularServerConfig extend ServerConfig instead of duplicating everything. See https://github.com/vector-im/riot-web/issues/9290 --- .../structures/auth/ForgotPassword.js | 30 +++----- src/components/structures/auth/Login.js | 38 ++++------ .../structures/auth/Registration.js | 31 +++----- .../views/auth/ModularServerConfig.js | 51 ++++++++++--- src/components/views/auth/ServerConfig.js | 71 ++++++++++++++----- 5 files changed, 125 insertions(+), 96 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 5316235fe0..a772e72c5a 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -124,13 +124,7 @@ module.exports = React.createClass({ }); }, - async onServerDetailsNextPhaseClick(ev) { - ev.stopPropagation(); - // TODO: TravisR - Capture the user's input somehow else - if (this._serverConfigRef) { - // Just to make sure the user's input gets captured - await this._serverConfigRef.validateServer(); - } + async onServerDetailsNextPhaseClick() { this.setState({ phase: PHASE_FORGOT, }); @@ -160,25 +154,19 @@ module.exports = React.createClass({ renderServerDetails() { const ServerConfig = sdk.getComponent("auth.ServerConfig"); - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); if (SdkConfig.get()['disable_custom_urls']) { return null; } - // TODO: TravisR - Pull out server discovery from ServerConfig to disable the next button? - return
- this._serverConfigRef = r} - serverConfig={this.props.serverConfig} - onServerConfigChange={this.props.onServerConfigChange} - delayTimeMs={0} /> - - {_t("Next")} - -
; + return ; }, renderForgot() { diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index af9370f2db..68b440d064 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -20,11 +20,11 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import { _t, _td } from '../../../languageHandler'; +import {_t, _td} from '../../../languageHandler'; import sdk from '../../../index'; import Login from '../../../Login'; import SdkConfig from '../../../SdkConfig'; -import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; +import {messageForResourceLimitError} from '../../../utils/ErrorUtils'; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; // For validating phone numbers without country codes @@ -283,13 +283,7 @@ module.exports = React.createClass({ this.props.onRegisterClick(); }, - async onServerDetailsNextPhaseClick(ev) { - ev.stopPropagation(); - // TODO: TravisR - Capture the user's input somehow else - if (this._serverConfigRef) { - // Just to make sure the user's input gets captured - await this._serverConfigRef.validateServer(); - } + async onServerDetailsNextPhaseClick() { this.setState({ phase: PHASE_LOGIN, }); @@ -421,7 +415,6 @@ module.exports = React.createClass({ renderServerComponent() { const ServerConfig = sdk.getComponent("auth.ServerConfig"); - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); if (SdkConfig.get()['disable_custom_urls']) { return null; @@ -431,26 +424,19 @@ module.exports = React.createClass({ return null; } - const serverDetails = this._serverConfigRef = r} + const serverDetailsProps = {}; + if (PHASES_ENABLED) { + serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick; + serverDetailsProps.submitText = _t("Next"); + serverDetailsProps.submitClass = "mx_Login_submit"; + } + + return ; - - let nextButton = null; - if (PHASES_ENABLED) { - // TODO: TravisR - Pull out server discovery from ServerConfig to disable the next button? - nextButton = - {_t("Next")} - ; - } - - return
- {serverDetails} - {nextButton} -
; }, renderLoginComponentForStep() { diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index faab8190bd..f516816033 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -286,13 +286,7 @@ module.exports = React.createClass({ }); }, - async onServerDetailsNextPhaseClick(ev) { - ev.stopPropagation(); - // TODO: TravisR - Capture the user's input somehow else - if (this._serverConfigRef) { - // Just to make sure the user's input gets captured - await this._serverConfigRef.validateServer(); - } + async onServerDetailsNextPhaseClick() { this.setState({ phase: PHASE_REGISTRATION, }); @@ -337,7 +331,6 @@ module.exports = React.createClass({ const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector"); const ServerConfig = sdk.getComponent("auth.ServerConfig"); const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig"); - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); if (SdkConfig.get()['disable_custom_urls']) { return null; @@ -354,45 +347,41 @@ module.exports = React.createClass({
; } + const serverDetailsProps = {}; + if (PHASES_ENABLED) { + serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick; + serverDetailsProps.submitText = _t("Next"); + serverDetailsProps.submitClass = "mx_Login_submit"; + } + let serverDetails = null; switch (this.state.serverType) { case ServerType.FREE: break; case ServerType.PREMIUM: serverDetails = this._serverConfigRef = r} serverConfig={this.props.serverConfig} onServerConfigChange={this.props.onServerConfigChange} delayTimeMs={250} + {...serverDetailsProps} />; break; case ServerType.ADVANCED: serverDetails = this._serverConfigRef = r} serverConfig={this.props.serverConfig} onServerConfigChange={this.props.onServerConfigChange} delayTimeMs={250} + {...serverDetailsProps} />; break; } - let nextButton = null; - if (PHASES_ENABLED) { - // TODO: TravisR - Pull out server discovery from ServerConfig to disable the next button? - nextButton = - {_t("Next")} - ; - } - return
{serverDetails} - {nextButton}
; }, diff --git a/src/components/views/auth/ModularServerConfig.js b/src/components/views/auth/ModularServerConfig.js index ea22577dbd..5a3bc23596 100644 --- a/src/components/views/auth/ModularServerConfig.js +++ b/src/components/views/auth/ModularServerConfig.js @@ -41,6 +41,16 @@ export default class ModularServerConfig extends React.PureComponent { serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, delayTimeMs: PropTypes.number, // time to wait before invoking onChanged + + // Called after the component calls onServerConfigChange + onAfterSubmit: PropTypes.func, + + // Optional text for the submit button. If falsey, no button will be shown. + submitText: PropTypes.string, + + // Optional class for the submit button. Only applies if the submit button + // is to be rendered. + submitClass: PropTypes.string, }; static defaultProps = { @@ -119,6 +129,16 @@ export default class ModularServerConfig extends React.PureComponent { this.setState({ hsUrl }); }; + onSubmit = async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + await this.validateServer(); + + if (this.props.onAfterSubmit) { + this.props.onAfterSubmit(); + } + }; + _waitThenInvoke(existingTimeoutId, fn) { if (existingTimeoutId) { clearTimeout(existingTimeoutId); @@ -128,6 +148,16 @@ export default class ModularServerConfig extends React.PureComponent { render() { const Field = sdk.getComponent('elements.Field'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + const submitButton = this.props.submitText + ? {this.props.submitText} + : null; return (
@@ -141,15 +171,18 @@ export default class ModularServerConfig extends React.PureComponent { , }, )} -
- -
+
+
+ +
+ {submitButton} +
); } diff --git a/src/components/views/auth/ServerConfig.js b/src/components/views/auth/ServerConfig.js index 096e461efe..3967f49f18 100644 --- a/src/components/views/auth/ServerConfig.js +++ b/src/components/views/auth/ServerConfig.js @@ -30,12 +30,22 @@ import SdkConfig from "../../../SdkConfig"; export default class ServerConfig extends React.PureComponent { static propTypes = { - onServerConfigChange: PropTypes.func, + onServerConfigChange: PropTypes.func.isRequired, // The current configuration that the user is expecting to change. serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, delayTimeMs: PropTypes.number, // time to wait before invoking onChanged + + // Called after the component calls onServerConfigChange + onAfterSubmit: PropTypes.func, + + // Optional text for the submit button. If falsey, no button will be shown. + submitText: PropTypes.string, + + // Optional class for the submit button. Only applies if the submit button + // is to be rendered. + submitClass: PropTypes.string, }; static defaultProps = { @@ -124,6 +134,16 @@ export default class ServerConfig extends React.PureComponent { this.setState({ isUrl }); }; + onSubmit = async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + await this.validateServer(); + + if (this.props.onAfterSubmit) { + this.props.onAfterSubmit(); + } + }; + _waitThenInvoke(existingTimeoutId, fn) { if (existingTimeoutId) { clearTimeout(existingTimeoutId); @@ -138,11 +158,21 @@ export default class ServerConfig extends React.PureComponent { render() { const Field = sdk.getComponent('elements.Field'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const errorText = this.state.errorText ? {this.state.errorText} : null; + const submitButton = this.props.submitText + ? {this.props.submitText} + : null; + return (

{_t("Other servers")}

@@ -152,24 +182,27 @@ export default class ServerConfig extends React.PureComponent { , })} {errorText} -
- - -
+
+
+ + +
+ {submitButton} +
); } From e4576dac28cad4e5e18b04d4f5a1dbd433dfa59f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 13 May 2019 17:16:40 -0600 Subject: [PATCH 14/22] Render underlines and tooltips on custom server names in auth pages See https://github.com/vector-im/riot-web/issues/9290 --- res/css/_components.scss | 1 + res/css/structures/auth/_Login.scss | 4 ++ res/css/views/elements/_TextWithTooltip.scss | 19 +++++++ .../structures/auth/ForgotPassword.js | 11 +++- src/components/views/auth/PasswordLogin.js | 11 +++- src/components/views/auth/RegistrationForm.js | 11 +++- .../views/elements/TextWithTooltip.js | 56 +++++++++++++++++++ src/components/views/elements/Tooltip.js | 4 ++ 8 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 res/css/views/elements/_TextWithTooltip.scss create mode 100644 src/components/views/elements/TextWithTooltip.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 6e681894e3..ff22ad9eab 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -97,6 +97,7 @@ @import "./views/elements/_RoleButton.scss"; @import "./views/elements/_Spinner.scss"; @import "./views/elements/_SyntaxHighlight.scss"; +@import "./views/elements/_TextWithTooltip.scss"; @import "./views/elements/_ToggleSwitch.scss"; @import "./views/elements/_ToolTipButton.scss"; @import "./views/elements/_Tooltip.scss"; diff --git a/res/css/structures/auth/_Login.scss b/res/css/structures/auth/_Login.scss index 2cf6276557..4eff5c33e4 100644 --- a/res/css/structures/auth/_Login.scss +++ b/res/css/structures/auth/_Login.scss @@ -79,3 +79,7 @@ limitations under the License. .mx_Login_type_dropdown { min-width: 200px; } + +.mx_Login_underlinedServerName { + border-bottom: 1px dashed $accent-color; +} diff --git a/res/css/views/elements/_TextWithTooltip.scss b/res/css/views/elements/_TextWithTooltip.scss new file mode 100644 index 0000000000..5b34e7a3be --- /dev/null +++ b/res/css/views/elements/_TextWithTooltip.scss @@ -0,0 +1,19 @@ +/* +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_TextWithTooltip_tooltip { + display: none; +} \ No newline at end of file diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 5316235fe0..91ed1aa3ae 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -194,9 +194,16 @@ module.exports = React.createClass({ serverName: this.props.serverConfig.hsName, }); if (this.props.serverConfig.hsNameIsDifferent) { - // TODO: TravisR - Use tooltip to underline + const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip"); + yourMatrixAccountText = _t('Your Matrix account on ', {}, { - 'underlinedServerName': () => {this.props.serverConfig.hsName}, + 'underlinedServerName': () => { + return ; + }, }); } diff --git a/src/components/views/auth/PasswordLogin.js b/src/components/views/auth/PasswordLogin.js index f5b2aec210..80716b766c 100644 --- a/src/components/views/auth/PasswordLogin.js +++ b/src/components/views/auth/PasswordLogin.js @@ -279,9 +279,16 @@ export default class PasswordLogin extends React.Component { serverName: this.props.serverConfig.hsName, }); if (this.props.serverConfig.hsNameIsDifferent) { - // TODO: TravisR - Use tooltip to underline + const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip"); + signInToText = _t('Sign in to your Matrix account on ', {}, { - 'underlinedServerName': () => {this.props.serverConfig.hsName}, + 'underlinedServerName': () => { + return ; + }, }); } diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 0eecc6b826..be0142e6c6 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -516,9 +516,16 @@ module.exports = React.createClass({ serverName: this.props.serverConfig.hsName, }); if (this.props.serverConfig.hsNameIsDifferent) { - // TODO: TravisR - Use tooltip to underline + const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip"); + yourMatrixAccountText = _t('Create your Matrix account on ', {}, { - 'underlinedServerName': () => {this.props.serverConfig.hsName}, + 'underlinedServerName': () => { + return ; + }, }); } diff --git a/src/components/views/elements/TextWithTooltip.js b/src/components/views/elements/TextWithTooltip.js new file mode 100644 index 0000000000..757bcc9891 --- /dev/null +++ b/src/components/views/elements/TextWithTooltip.js @@ -0,0 +1,56 @@ +/* + 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. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import sdk from '../../../index'; + +export default class TextWithTooltip extends React.Component { + static propTypes = { + class: PropTypes.string, + tooltip: PropTypes.string.isRequired, + }; + + constructor() { + super(); + + this.state = { + hover: false, + }; + } + + onMouseOver = () => { + this.setState({hover: true}); + }; + + onMouseOut = () => { + this.setState({hover: false}); + }; + + render() { + const Tooltip = sdk.getComponent("elements.Tooltip"); + + return ( + + {this.props.children} + + + ); + } +} \ No newline at end of file diff --git a/src/components/views/elements/Tooltip.js b/src/components/views/elements/Tooltip.js index 1cc82978ed..1d6b54f413 100644 --- a/src/components/views/elements/Tooltip.js +++ b/src/components/views/elements/Tooltip.js @@ -79,6 +79,10 @@ module.exports = React.createClass({ let offset = 0; if (parentBox.height > MIN_TOOLTIP_HEIGHT) { offset = Math.floor((parentBox.height - MIN_TOOLTIP_HEIGHT) / 2); + } else { + // The tooltip is larger than the parent height: figure out what offset + // we need so that we're still centered. + offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT); } style.top = (parentBox.top - 2) + window.pageYOffset + offset; style.left = 6 + parentBox.right + window.pageXOffset; From 25e3f7888e5453cb25415e2c27ecf09f3c0c4b04 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 13 May 2019 18:31:43 -0600 Subject: [PATCH 15/22] newline for the linter --- src/components/views/elements/TextWithTooltip.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/TextWithTooltip.js b/src/components/views/elements/TextWithTooltip.js index 757bcc9891..61c3a2125a 100644 --- a/src/components/views/elements/TextWithTooltip.js +++ b/src/components/views/elements/TextWithTooltip.js @@ -53,4 +53,4 @@ export default class TextWithTooltip extends React.Component { ); } -} \ No newline at end of file +} From bb163576360e0defc3265f12fc08c5949f97ee69 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 14 May 2019 13:06:56 -0600 Subject: [PATCH 16/22] Flag all generated configs as non-default by default The app is expected to flag a particular config themselves as default. This is primarily intended so that other parts of the app can determine what to do based on whether or not the config is a default config. See https://github.com/vector-im/riot-web/issues/9290 --- src/utils/AutoDiscoveryUtils.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.js index 318c706136..0850039344 100644 --- a/src/utils/AutoDiscoveryUtils.js +++ b/src/utils/AutoDiscoveryUtils.js @@ -26,6 +26,8 @@ export class ValidatedServerConfig { isUrl: string; identityEnabled: boolean; + + isDefault: boolean; } export default class AutoDiscoveryUtils { @@ -99,6 +101,7 @@ export default class AutoDiscoveryUtils { hsNameIsDifferent: url.hostname !== preferredHomeserverName, isUrl: preferredIdentityUrl, identityEnabled: !SdkConfig.get()['disable_identity_server'], + isDefault: false, }); } } From 34719b9a2e8f4734d9a9367823142ffdb7a32655 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 14 May 2019 13:10:02 -0600 Subject: [PATCH 17/22] Only expose the fallback_hs_url if the homeserver is the default HS See https://github.com/vector-im/riot-web/issues/9290 --- src/components/structures/MatrixChat.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 4198980a17..38f597f673 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -203,9 +203,12 @@ export default React.createClass({ }; }, - // TODO: TravisR - Remove this or put it somewhere else getFallbackHsUrl: function() { - return this.props.config.fallback_hs_url; + if (this.props.serverConfig.isDefault) { + return this.props.config.fallback_hs_url; + } else { + return null; + } }, getServerProperties() { From 9b5830bb080a25781cac66e1807f16a87a9e1ed5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 14 May 2019 15:01:22 -0600 Subject: [PATCH 18/22] Restore use of full mxid login See https://github.com/vector-im/riot-web/issues/9290 --- src/components/structures/auth/Login.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index af9370f2db..3fc7aad50d 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -25,7 +25,7 @@ import sdk from '../../../index'; import Login from '../../../Login'; import SdkConfig from '../../../SdkConfig'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; -import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; +import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; // For validating phone numbers without country codes const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; @@ -235,21 +235,25 @@ module.exports = React.createClass({ this.setState({ username: username }); }, - onUsernameBlur: function(username) { + onUsernameBlur: async function(username) { this.setState({ username: username, + busy: true, // unset later by the result of onServerConfigChange errorText: null, }); - if (username[0] === "@" && false) { // TODO: TravisR - Restore this + if (username[0] === "@") { const serverName = username.split(':').slice(1).join(':'); try { - // we have to append 'https://' to make the URL constructor happy - // otherwise we get things like 'protocol: matrix.org, pathname: 8448' - const url = new URL("https://" + serverName); - this._tryWellKnownDiscovery(url.hostname); + const result = await AutoDiscoveryUtils.validateServerName(serverName); + this.props.onServerConfigChange(result); } catch (e) { console.error("Problem parsing URL or unhandled error doing .well-known discovery:", e); - this.setState({errorText: _t("Failed to perform homeserver discovery")}); + + let message = _t("Failed to perform homeserver discovery"); + if (e.translatedMessage) { + message = e.translatedMessage; + } + this.setState({errorText: message, busy: false}); } } }, From 6e3b06f3646b83d4e271e51ca894c718474d8c27 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 15 May 2019 13:55:50 -0600 Subject: [PATCH 19/22] Human de-linting --- src/components/structures/auth/ForgotPassword.js | 3 ++- src/components/views/auth/PasswordLogin.js | 5 +++-- src/components/views/auth/RegistrationForm.js | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 91ed1aa3ae..42ca23256c 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -200,7 +200,8 @@ module.exports = React.createClass({ 'underlinedServerName': () => { return ; }, diff --git a/src/components/views/auth/PasswordLogin.js b/src/components/views/auth/PasswordLogin.js index 80716b766c..825bffdc84 100644 --- a/src/components/views/auth/PasswordLogin.js +++ b/src/components/views/auth/PasswordLogin.js @@ -284,8 +284,9 @@ export default class PasswordLogin extends React.Component { signInToText = _t('Sign in to your Matrix account on ', {}, { 'underlinedServerName': () => { return ; }, diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index be0142e6c6..b1af6ea42c 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -522,7 +522,8 @@ module.exports = React.createClass({ 'underlinedServerName': () => { return ; }, From 595b490fd7130c0a53e61902723627358e60ce58 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 20 May 2019 20:00:11 -0600 Subject: [PATCH 20/22] Don't act busy on the login page for moving your cursor If you were in the username field and simply tabbed out without entering anything, the form would become "busy" and not let you submit. We should only be doing this if we have work to do, like .well-known discovery of the homeserver. --- src/components/structures/auth/Login.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 393c640604..aed8e9fca0 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -236,12 +236,13 @@ module.exports = React.createClass({ }, onUsernameBlur: async function(username) { + const doWellknownLookup = username[0] === "@"; this.setState({ username: username, - busy: true, // unset later by the result of onServerConfigChange + busy: doWellknownLookup, // unset later by the result of onServerConfigChange errorText: null, }); - if (username[0] === "@") { + if (doWellknownLookup) { const serverName = username.split(':').slice(1).join(':'); try { const result = await AutoDiscoveryUtils.validateServerName(serverName); From 7ecab350628f4454058d25406dc2b5c414ca52ca Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 20 May 2019 20:02:02 -0600 Subject: [PATCH 21/22] Add a null guard for serverConfig This is often null while the component is on its first render, and is called during that render. It is eventually populated by React, and the function re-called - we just have to be patient. --- src/components/structures/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 38f597f673..ac328f8387 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -204,7 +204,7 @@ export default React.createClass({ }, getFallbackHsUrl: function() { - if (this.props.serverConfig.isDefault) { + if (this.props.serverConfig && this.props.serverConfig.isDefault) { return this.props.config.fallback_hs_url; } else { return null; From ea1c778bb161305e2bd50ede69068cc32e36c550 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 22 May 2019 23:35:05 -0600 Subject: [PATCH 22/22] Fix copyright --- src/components/views/auth/PasswordLogin.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/auth/PasswordLogin.js b/src/components/views/auth/PasswordLogin.js index 825bffdc84..fe6fd74499 100644 --- a/src/components/views/auth/PasswordLogin.js +++ b/src/components/views/auth/PasswordLogin.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017,2019 Vector Creations Ltd +Copyright 2017 Vector Creations Ltd +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.