diff --git a/res/css/_components.scss b/res/css/_components.scss index 445ed70ff41..707f73247d7 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -45,8 +45,6 @@ @import "./views/auth/_InteractiveAuthEntryComponents.scss"; @import "./views/auth/_LanguageSelector.scss"; @import "./views/auth/_PassphraseField.scss"; -@import "./views/auth/_ServerConfig.scss"; -@import "./views/auth/_ServerTypeSelector.scss"; @import "./views/auth/_Welcome.scss"; @import "./views/avatars/_BaseAvatar.scss"; @import "./views/avatars/_DecoratedRoomAvatar.scss"; @@ -78,11 +76,13 @@ @import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_ModalWidgetDialog.scss"; @import "./views/dialogs/_NewSessionReviewDialog.scss"; +@import "./views/dialogs/_RegistrationEmailPromptDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomSettingsDialogBridges.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; @import "./views/dialogs/_RoomUpgradeWarningDialog.scss"; @import "./views/dialogs/_ServerOfflineDialog.scss"; +@import "./views/dialogs/_ServerPickerDialog.scss"; @import "./views/dialogs/_SetEmailDialog.scss"; @import "./views/dialogs/_SettingsDialog.scss"; @import "./views/dialogs/_ShareDialog.scss"; @@ -124,6 +124,8 @@ @import "./views/elements/_RichText.scss"; @import "./views/elements/_RoleButton.scss"; @import "./views/elements/_RoomAliasField.scss"; +@import "./views/elements/_SSOButtons.scss"; +@import "./views/elements/_ServerPicker.scss"; @import "./views/elements/_Slider.scss"; @import "./views/elements/_Spinner.scss"; @import "./views/elements/_StyledCheckbox.scss"; diff --git a/res/css/structures/auth/_Login.scss b/res/css/structures/auth/_Login.scss index 02436833a2e..a8cb7d7eee7 100644 --- a/res/css/structures/auth/_Login.scss +++ b/res/css/structures/auth/_Login.scss @@ -18,7 +18,7 @@ limitations under the License. .mx_Login_submit { @mixin mx_DialogButton; width: 100%; - margin-top: 35px; + margin-top: 24px; margin-bottom: 24px; box-sizing: border-box; text-align: center; @@ -33,12 +33,6 @@ limitations under the License. cursor: default; } -.mx_AuthBody a.mx_Login_sso_link:link, -.mx_AuthBody a.mx_Login_sso_link:hover, -.mx_AuthBody a.mx_Login_sso_link:visited { - color: $button-primary-fg-color; -} - .mx_Login_loader { display: inline; position: relative; @@ -91,6 +85,8 @@ limitations under the License. } div.mx_AccessibleButton_kind_link.mx_Login_forgot { + display: block; + margin: 0 auto; // style it as a link font-size: inherit; padding: 0; diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss index 0ba0d10e068..8f0c758e7af 100644 --- a/res/css/views/auth/_AuthBody.scss +++ b/res/css/views/auth/_AuthBody.scss @@ -37,6 +37,10 @@ limitations under the License. color: $authpage-primary-color; } + h3.mx_AuthBody_centered { + text-align: center; + } + a:link, a:hover, a:visited { @@ -96,12 +100,6 @@ limitations under the License. } } -.mx_AuthBody_editServerDetails { - padding-left: 1em; - font-size: $font-12px; - font-weight: normal; -} - .mx_AuthBody_fieldRow { display: flex; margin-bottom: 10px; @@ -146,6 +144,14 @@ limitations under the License. display: block; text-align: center; width: 100%; + + > a { + font-weight: $font-semi-bold; + } +} + +.mx_SSOButtons + .mx_AuthBody_changeFlow { + margin-top: 24px; } .mx_AuthBody_spinner { diff --git a/res/css/views/auth/_ServerTypeSelector.scss b/res/css/views/auth/_ServerTypeSelector.scss deleted file mode 100644 index fbd3d2655de..00000000000 --- a/res/css/views/auth/_ServerTypeSelector.scss +++ /dev/null @@ -1,69 +0,0 @@ -/* -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_ServerTypeSelector { - display: flex; - margin-bottom: 28px; -} - -.mx_ServerTypeSelector_type { - margin: 0 5px; -} - -.mx_ServerTypeSelector_type:first-child { - margin-left: 0; -} - -.mx_ServerTypeSelector_type:last-child { - margin-right: 0; -} - -.mx_ServerTypeSelector_label { - text-align: center; - font-weight: 600; - color: $authpage-primary-color; - margin: 8px 0; -} - -.mx_ServerTypeSelector_type .mx_AccessibleButton { - padding: 10px; - border: 1px solid $input-border-color; - border-radius: 4px; -} - -.mx_ServerTypeSelector_type.mx_ServerTypeSelector_type_selected .mx_AccessibleButton { - border-color: $input-valid-border-color; -} - -.mx_ServerTypeSelector_logo { - display: flex; - justify-content: center; - height: 18px; - margin-bottom: 12px; - font-weight: 600; - color: $authpage-primary-color; -} - -.mx_ServerTypeSelector_logo > div { - display: flex; - width: 70%; - align-items: center; - justify-content: space-evenly; -} - -.mx_ServerTypeSelector_description { - font-size: $font-10px; -} diff --git a/res/css/views/auth/_ServerConfig.scss b/res/css/views/dialogs/_RegistrationEmailPromptDialog.scss similarity index 57% rename from res/css/views/auth/_ServerConfig.scss rename to res/css/views/dialogs/_RegistrationEmailPromptDialog.scss index a7e0057ab33..31fc6d7a047 100644 --- a/res/css/views/auth/_ServerConfig.scss +++ b/res/css/views/dialogs/_RegistrationEmailPromptDialog.scss @@ -1,6 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,21 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_ServerConfig_help:link { - opacity: 0.8; -} - -.mx_ServerConfig_error { - display: block; - color: $warning-color; -} +.mx_RegistrationEmailPromptDialog { + width: 417px; -.mx_ServerConfig_identityServer { - transform: scaleY(0); - transform-origin: top; - transition: transform 0.25s; + .mx_Dialog_content { + margin-bottom: 24px; + color: $tertiary-fg-color; + } - &.mx_ServerConfig_identityServer_shown { - transform: scaleY(1); + .mx_Dialog_primary { + width: 100%; } } diff --git a/res/css/views/dialogs/_ServerPickerDialog.scss b/res/css/views/dialogs/_ServerPickerDialog.scss new file mode 100644 index 00000000000..b01b49d7af3 --- /dev/null +++ b/res/css/views/dialogs/_ServerPickerDialog.scss @@ -0,0 +1,78 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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_ServerPickerDialog { + width: 468px; + box-sizing: border-box; + + .mx_Dialog_content { + margin-bottom: 0; + + > p { + color: $secondary-fg-color; + font-size: $font-14px; + margin: 16px 0; + + &:first-of-type { + margin-bottom: 40px; + } + + &:last-of-type { + margin: 0 24px 24px; + } + } + + > h4 { + font-size: $font-15px; + font-weight: $font-semi-bold; + color: $secondary-fg-color; + margin-left: 8px; + } + + > a { + color: $accent-color; + margin-left: 8px; + } + } + + .mx_ServerPickerDialog_otherHomeserverRadio { + input[type="radio"] + div { + margin-top: auto; + margin-bottom: auto; + } + } + + .mx_ServerPickerDialog_otherHomeserver { + border-top: none; + border-left: none; + border-right: none; + border-radius: unset; + + > input { + padding-left: 0; + } + + > label { + margin-left: 0; + } + } + + .mx_AccessibleButton_kind_primary { + width: calc(100% - 64px); + margin: 0 8px; + padding: 15px 18px; + } +} diff --git a/res/css/views/elements/_SSOButtons.scss b/res/css/views/elements/_SSOButtons.scss new file mode 100644 index 00000000000..f762468c7f8 --- /dev/null +++ b/res/css/views/elements/_SSOButtons.scss @@ -0,0 +1,49 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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_SSOButtons { + display: flex; + justify-content: center; + + .mx_SSOButton { + position: relative; + width: 100%; + padding-left: 32px; + padding-right: 32px; + + > img { + object-fit: contain; + position: absolute; + left: 8px; + top: 4px; + } + } + + .mx_SSOButton_mini { + box-sizing: border-box; + width: 50px; // 48px + 1px border on all sides + height: 50px; // 48px + 1px border on all sides + + > img { + left: 12px; + top: 12px; + } + + & + .mx_SSOButton_mini { + margin-left: 24px; + } + } +} diff --git a/res/css/views/elements/_ServerPicker.scss b/res/css/views/elements/_ServerPicker.scss new file mode 100644 index 00000000000..ae1e445a9fb --- /dev/null +++ b/res/css/views/elements/_ServerPicker.scss @@ -0,0 +1,88 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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_ServerPicker { + margin-bottom: 14px; + border-bottom: 1px solid rgba(141, 151, 165, 0.2); + display: grid; + grid-template-columns: auto min-content; + grid-template-rows: auto auto auto; + font-size: $font-14px; + line-height: $font-20px; + + > h3 { + font-weight: $font-semi-bold; + margin: 0 0 20px; + grid-column: 1; + grid-row: 1; + } + + .mx_ServerPicker_help { + width: 20px; + height: 20px; + background-color: $icon-button-color; + border-radius: 10px; + grid-column: 2; + grid-row: 1; + margin-left: auto; + text-align: center; + color: #ffffff; + font-size: 16px; + position: relative; + + &::before { + content: ''; + width: 24px; + height: 24px; + position: absolute; + top: -2px; + left: -2px; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/element-icons/i.svg'); + background: #ffffff; + } + } + + .mx_ServerPicker_server { + color: $primary-fg-color; + grid-column: 1; + grid-row: 2; + margin-bottom: 16px; + } + + .mx_ServerPicker_change { + padding: 0; + font-size: inherit; + grid-column: 2; + grid-row: 2; + } + + .mx_ServerPicker_desc { + margin-top: -12px; + color: $tertiary-fg-color; + grid-column: 1 / 2; + grid-row: 3; + margin-bottom: 16px; + } +} + +.mx_ServerPicker_helpDialog { + .mx_Dialog_content { + width: 456px; + } +} diff --git a/res/img/element-icons/i.svg b/res/img/element-icons/i.svg new file mode 100644 index 00000000000..6674f1ed8d4 --- /dev/null +++ b/res/img/element-icons/i.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 1b7ff9598d7..08fe2e9f572 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -217,7 +217,7 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28); /* align images in buttons (eg spinners) */ vertical-align: middle; border: 0px; - border-radius: 4px; + border-radius: 8px; font-family: $font-family; font-size: $font-14px; color: $button-fg-color; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 932a37b46e8..3e3c299af95 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -208,7 +208,7 @@ $composer-shadow-color: tranparent; /* align images in buttons (eg spinners) */ vertical-align: middle; border: 0px; - border-radius: 4px; + border-radius: 8px; font-family: $font-family; font-size: $font-14px; color: $button-fg-color; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index dba8fa6415e..085d6d7f103 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -331,7 +331,7 @@ $composer-shadow-color: tranparent; /* align images in buttons (eg spinners) */ vertical-align: middle; border: 0px; - border-radius: 4px; + border-radius: 8px; font-family: $font-family; font-size: $font-14px; color: $button-fg-color; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index f89b9f2c755..4cfeeae05ed 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -335,7 +335,7 @@ $composer-shadow-color: rgba(0, 0, 0, 0.04); /* align images in buttons (eg spinners) */ vertical-align: middle; border: 0px; - border-radius: 4px; + border-radius: 8px; font-family: $font-family; font-size: $font-14px; color: $button-fg-color; diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index 0a1f06f0b3b..4f7c7126e9e 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -248,15 +248,16 @@ export default abstract class BasePlatform { * @param {MatrixClient} mxClient the matrix client using which we should start the flow * @param {"sso"|"cas"} loginType the type of SSO it is, CAS/SSO. * @param {string} fragmentAfterLogin the hash to pass to the app during sso callback. + * @param {string} idpId The ID of the Identity Provider being targeted, optional. */ - startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: string) { + startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: string, idpId?: string) { // persist hs url and is url for when the user is returned to the app with the login token localStorage.setItem(SSO_HOMESERVER_URL_KEY, mxClient.getHomeserverUrl()); if (mxClient.getIdentityServerUrl()) { localStorage.setItem(SSO_ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl()); } const callbackUrl = this.getSSOCallbackUrl(fragmentAfterLogin); - window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO + window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType, idpId); // redirect to SSO } onKeyDown(ev: KeyboardEvent): boolean { diff --git a/src/Login.ts b/src/Login.ts index ae4aa226edb..281906d8610 100644 --- a/src/Login.ts +++ b/src/Login.ts @@ -29,10 +29,25 @@ interface ILoginOptions { } // TODO: Move this to JS SDK -interface ILoginFlow { - type: string; +interface IPasswordFlow { + type: "m.login.password"; } +export interface IIdentityProvider { + id: string; + name: string; + icon?: string; +} + +export interface ISSOFlow { + type: "m.login.sso" | "m.login.cas"; + // eslint-disable-next-line camelcase + identity_providers: IIdentityProvider[]; + "org.matrix.msc2858.identity_providers": IIdentityProvider[]; // Unstable prefix for MSC2858 +} + +export type LoginFlow = ISSOFlow | IPasswordFlow; + // TODO: Move this to JS SDK /* eslint-disable camelcase */ interface ILoginParams { @@ -48,9 +63,8 @@ export default class Login { private hsUrl: string; private isUrl: string; private fallbackHsUrl: string; - private currentFlowIndex: number; // TODO: Flows need a type in JS SDK - private flows: Array; + private flows: Array; private defaultDeviceDisplayName: string; private tempClient: MatrixClient; @@ -63,7 +77,6 @@ export default class Login { this.hsUrl = hsUrl; this.isUrl = isUrl; this.fallbackHsUrl = fallbackHsUrl; - this.currentFlowIndex = 0; this.flows = []; this.defaultDeviceDisplayName = opts.defaultDeviceDisplayName; this.tempClient = null; // memoize @@ -100,27 +113,13 @@ export default class Login { }); } - public async getFlows(): Promise> { + public async getFlows(): Promise> { const client = this.createTemporaryClient(); const { flows } = await client.loginFlows(); this.flows = flows; - this.currentFlowIndex = 0; - // technically the UI should display options for all flows for the - // user to then choose one, so return all the flows here. return this.flows; } - public chooseFlow(flowIndex): void { - this.currentFlowIndex = flowIndex; - } - - public getCurrentFlowStep(): string { - // technically the flow can have multiple steps, but no one does this - // for login so we can ignore it. - const flowStep = this.flows[this.currentFlowIndex]; - return flowStep ? flowStep.type : null; - } - public loginViaPassword( username: string, phoneCountry: string, diff --git a/src/PasswordReset.js b/src/PasswordReset.js index 9472ddc6332..b38a9de9607 100644 --- a/src/PasswordReset.js +++ b/src/PasswordReset.js @@ -40,10 +40,6 @@ export default class PasswordReset { this.identityServerDomain = identityUrl ? identityUrl.split("://")[1] : null; } - doesServerRequireIdServerParam() { - return this.client.doesServerRequireIdServerParam(); - } - /** * Attempt to reset the user's password. This will trigger a side-effect of * sending an email to the provided email address. @@ -78,9 +74,6 @@ export default class PasswordReset { sid: this.sessionId, client_secret: this.clientSecret, }; - if (await this.doesServerRequireIdServerParam()) { - creds.id_server = this.identityServerDomain; - } try { await this.client.setPassword({ diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 9fede15aa6c..32b961296b1 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -2009,6 +2009,7 @@ export default class MatrixChat extends React.PureComponent { onLoginClick={this.onLoginClick} onServerConfigChange={this.onServerConfigChange} defaultDeviceDisplayName={this.props.defaultDeviceDisplayName} + fragmentAfterLogin={fragmentAfterLogin} {...this.getServerProperties()} /> ); diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index f9f5263f7e8..5a39fe9fd9a 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -21,16 +21,14 @@ import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import * as sdk from '../../../index'; import Modal from "../../../Modal"; -import SdkConfig from "../../../SdkConfig"; import PasswordReset from "../../../PasswordReset"; import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import classNames from 'classnames'; import AuthPage from "../../views/auth/AuthPage"; import CountlyAnalytics from "../../../CountlyAnalytics"; +import ServerPicker from "../../views/elements/ServerPicker"; // Phases -// Show controls to configure server details -const PHASE_SERVER_DETAILS = 0; // Show the forgot password inputs const PHASE_FORGOT = 1; // Email is in the process of being sent @@ -62,7 +60,6 @@ export default class ForgotPassword extends React.Component { serverIsAlive: true, serverErrorIsFatal: false, serverDeadError: "", - serverRequiresIdServer: null, }; constructor(props) { @@ -93,12 +90,8 @@ export default class ForgotPassword extends React.Component { serverConfig.isUrl, ); - const pwReset = new PasswordReset(serverConfig.hsUrl, serverConfig.isUrl); - const serverRequiresIdServer = await pwReset.doesServerRequireIdServerParam(); - this.setState({ serverIsAlive: true, - serverRequiresIdServer, }); } catch (e) { this.setState(AutoDiscoveryUtils.authComponentStateForError(e, "forgot_password")); @@ -177,20 +170,6 @@ export default class ForgotPassword extends React.Component { }); }; - onServerDetailsNextPhaseClick = async () => { - this.setState({ - phase: PHASE_FORGOT, - }); - }; - - onEditServerDetailsClick = ev => { - ev.preventDefault(); - ev.stopPropagation(); - this.setState({ - phase: PHASE_SERVER_DETAILS, - }); - }; - onLoginClick = ev => { ev.preventDefault(); ev.stopPropagation(); @@ -205,24 +184,6 @@ export default class ForgotPassword extends React.Component { }); } - renderServerDetails() { - const ServerConfig = sdk.getComponent("auth.ServerConfig"); - - if (SdkConfig.get()['disable_custom_urls']) { - return null; - } - - return ; - } - renderForgot() { const Field = sdk.getComponent('elements.Field'); @@ -246,57 +207,13 @@ export default class ForgotPassword extends React.Component { ); } - let yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', { - serverName: this.props.serverConfig.hsName, - }); - if (this.props.serverConfig.hsNameIsDifferent) { - const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip"); - - yourMatrixAccountText = _t('Your Matrix account on ', {}, { - 'underlinedServerName': () => { - return ; - }, - }); - } - - // If custom URLs are allowed, wire up the server details edit link. - let editLink = null; - if (!SdkConfig.get()['disable_custom_urls']) { - editLink = - {_t('Change')} - ; - } - - if (!this.props.serverConfig.isUrl && this.state.serverRequiresIdServer) { - return
-

- {yourMatrixAccountText} - {editLink} -

- {_t( - "No identity server is configured: " + - "add one in server settings to reset your password.", - )} - - {_t('Sign in instead')} - -
; - } - return
{errorText} {serverDeadSection} -

- {yourMatrixAccountText} - {editLink} -

+
CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_focus")} onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_blur")} + autoComplete="new-password" /> CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_focus")} onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_blur")} + autoComplete="new-password" />
{_t( @@ -380,9 +299,6 @@ export default class ForgotPassword extends React.Component { let resetPasswordJsx; switch (this.state.phase) { - case PHASE_SERVER_DETAILS: - resetPasswordJsx = this.renderServerDetails(); - break; case PHASE_FORGOT: resetPasswordJsx = this.renderForgot(); break; diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 4cd8981a65d..606aeb44ab2 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016, 2017, 2018, 2019 New Vector Ltd +Copyright 2015, 2016, 2017, 2018, 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,30 +14,27 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {ComponentProps, ReactNode} from 'react'; +import React, {ReactNode} from 'react'; +import {MatrixError} from "matrix-js-sdk/src/http-api"; import {_t, _td} from '../../../languageHandler'; import * as sdk from '../../../index'; -import Login from '../../../Login'; +import Login, {ISSOFlow, LoginFlow} from '../../../Login'; import SdkConfig from '../../../SdkConfig'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import classNames from "classnames"; import AuthPage from "../../views/auth/AuthPage"; -import SSOButton from "../../views/elements/SSOButton"; import PlatformPeg from '../../../PlatformPeg'; import SettingsStore from "../../../settings/SettingsStore"; import {UIFeature} from "../../../settings/UIFeature"; import CountlyAnalytics from "../../../CountlyAnalytics"; import {IMatrixClientCreds} from "../../../MatrixClientPeg"; -import ServerConfig from "../../views/auth/ServerConfig"; import PasswordLogin from "../../views/auth/PasswordLogin"; -import SignInToText from "../../views/auth/SignInToText"; import InlineSpinner from "../../views/elements/InlineSpinner"; import Spinner from "../../views/elements/Spinner"; - -// Enable phases for login -const PHASES_ENABLED = true; +import SSOButtons from "../../views/elements/SSOButtons"; +import ServerPicker from "../../views/elements/ServerPicker"; // These are used in several places, and come from the js-sdk's autodiscovery // stuff. We define them here so that they'll be picked up by i18n. @@ -75,13 +72,6 @@ interface IProps { onServerConfigChange(config: ValidatedServerConfig): void; } -enum Phase { - // Show controls to configure server details - ServerDetails, - // Show the appropriate login flow(s) for the server - Login, -} - interface IState { busy: boolean; busyLoggingIn?: boolean; @@ -90,17 +80,13 @@ interface IState { // can we attempt to log in or are there validation errors? canTryLogin: boolean; + flows?: LoginFlow[]; + // used for preserving form values when changing homeserver username: string; phoneCountry?: string; phoneNumber: string; - // Phase of the overall login dialog. - phase: Phase; - // The current login flow, such as password, SSO, etc. - // we need to load the flows from the server - currentFlow?: string; - // We perform liveliness checks later, but for now suppress the errors. // We also track the server dead errors independently of the regular errors so // that we can render it differently, and override any other error the user may @@ -113,9 +99,10 @@ interface IState { /* * A wire component which glues together login UI components and Login logic */ -export default class LoginComponent extends React.Component { +export default class LoginComponent extends React.PureComponent { private unmounted = false; private loginLogic: Login; + private readonly stepRendererMap: Record ReactNode>; constructor(props) { @@ -127,11 +114,13 @@ export default class LoginComponent extends React.Component { errorText: null, loginIncorrect: false, canTryLogin: true, + + flows: null, + username: "", phoneCountry: null, phoneNumber: "", - phase: Phase.Login, - currentFlow: null, + serverIsAlive: true, serverErrorIsFatal: false, serverDeadError: "", @@ -351,13 +340,15 @@ export default class LoginComponent extends React.Component { }; onTryRegisterClick = ev => { - const step = this.getCurrentFlowStep(); - if (step === 'm.login.sso' || step === 'm.login.cas') { - // If we're showing SSO it means that registration is also probably disabled, - // so intercept the click and instead pretend the user clicked 'Sign in with SSO'. + const hasPasswordFlow = this.state.flows.find(flow => flow.type === "m.login.password"); + const ssoFlow = this.state.flows.find(flow => flow.type === "m.login.sso" || flow.type === "m.login.cas"); + // If has no password flow but an SSO flow guess that the user wants to register with SSO. + // TODO: instead hide the Register button if registration is disabled by checking with the server, + // has no specific errCode currently and uses M_FORBIDDEN. + if (ssoFlow && !hasPasswordFlow) { ev.preventDefault(); ev.stopPropagation(); - const ssoKind = step === 'm.login.sso' ? 'sso' : 'cas'; + const ssoKind = ssoFlow.type === 'm.login.sso' ? 'sso' : 'cas'; PlatformPeg.get().startSingleSignOn(this.loginLogic.createTemporaryClient(), ssoKind, this.props.fragmentAfterLogin); } else { @@ -366,20 +357,6 @@ export default class LoginComponent extends React.Component { } }; - private onServerDetailsNextPhaseClick = () => { - this.setState({ - phase: Phase.Login, - }); - }; - - private onEditServerDetailsClick = ev => { - ev.preventDefault(); - ev.stopPropagation(); - this.setState({ - phase: Phase.ServerDetails, - }); - }; - private async initLoginLogic({hsUrl, isUrl}: ValidatedServerConfig) { let isDefaultServer = false; if (this.props.serverConfig.isDefault @@ -397,7 +374,6 @@ export default class LoginComponent extends React.Component { this.setState({ busy: true, - currentFlow: null, // reset flow loginIncorrect: false, }); @@ -421,38 +397,22 @@ export default class LoginComponent extends React.Component { busy: false, ...AutoDiscoveryUtils.authComponentStateForError(e), }); - if (this.state.serverErrorIsFatal) { - // Server is dead: show server details prompt instead - this.setState({ - phase: Phase.ServerDetails, - }); - return; - } } loginLogic.getFlows().then((flows) => { // look for a flow where we understand all of the steps. - for (let i = 0; i < flows.length; i++ ) { - if (!this.isSupportedFlow(flows[i])) { - continue; - } + const supportedFlows = flows.filter(this.isSupportedFlow); - // we just pick the first flow where we support all the - // steps. (we don't have a UI for multiple logins so let's skip - // that for now). - loginLogic.chooseFlow(i); + if (supportedFlows.length > 0) { this.setState({ - currentFlow: this.getCurrentFlowStep(), + flows: supportedFlows, }); return; } - // we got to the end of the list without finding a suitable - // flow. + + // we got to the end of the list without finding a suitable flow. this.setState({ - errorText: _t( - "This homeserver doesn't offer any login flows which are " + - "supported by this client.", - ), + errorText: _t("This homeserver doesn't offer any login flows which are supported by this client."), }); }, (err) => { this.setState({ @@ -467,7 +427,7 @@ export default class LoginComponent extends React.Component { }); } - private isSupportedFlow(flow) { + private isSupportedFlow = (flow: LoginFlow): boolean => { // technically the flow can have multiple steps, but no one does this // for login and loginLogic doesn't support it so we can ignore it. if (!this.stepRendererMap[flow.type]) { @@ -475,20 +435,16 @@ export default class LoginComponent extends React.Component { return false; } return true; - } - - private getCurrentFlowStep() { - return this.loginLogic ? this.loginLogic.getCurrentFlowStep() : null; - } + }; - private errorTextFromError(err) { + private errorTextFromError(err: MatrixError): ReactNode { let errCode = err.errcode; if (!errCode && err.httpStatus) { errCode = "HTTP " + err.httpStatus; } - let errorText: ReactNode = _t("Error: Problem communicating with the given homeserver.") + - (errCode ? " (" + errCode + ")" : ""); + let errorText: ReactNode = _t("There was a problem communicating with the homeserver, " + + "please try again later.") + (errCode ? " (" + errCode + ")" : ""); if (err.cors === 'rejected') { if (window.location.protocol === 'https:' && @@ -526,61 +482,28 @@ export default class LoginComponent extends React.Component { return errorText; } - private renderServerComponent() { - if (SdkConfig.get()['disable_custom_urls']) { - return null; - } - - if (PHASES_ENABLED && this.state.phase !== Phase.ServerDetails) { - return null; - } - - const serverDetailsProps: ComponentProps = {}; - if (PHASES_ENABLED) { - serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick; - serverDetailsProps.submitText = _t("Next"); - serverDetailsProps.submitClass = "mx_Login_submit"; - } - - return ; - } - - private renderLoginComponentForStep() { - if (PHASES_ENABLED && this.state.phase !== Phase.Login) { - return null; - } - - const step = this.state.currentFlow; - - if (!step) { - return null; - } - - const stepRenderer = this.stepRendererMap[step]; - - if (stepRenderer) { - return stepRenderer(); - } - - return null; + renderLoginComponentForFlows() { + if (!this.state.flows) return null; + + // this is the ideal order we want to show the flows in + const order = [ + "m.login.password", + "m.login.sso", + ]; + + const flows = order.map(type => this.state.flows.find(flow => flow.type === type)).filter(Boolean); + return + { flows.map(flow => { + const stepRenderer = this.stepRendererMap[flow.type]; + return { stepRenderer() } + }) } + } private renderPasswordStep = () => { - let onEditServerDetailsClick = null; - // If custom URLs are allowed, wire up the server details edit link. - if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) { - onEditServerDetailsClick = this.onEditServerDetailsClick; - } - return ( { }; private renderSsoStep = loginType => { - let onEditServerDetailsClick = null; - // If custom URLs are allowed, wire up the server details edit link. - if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) { - onEditServerDetailsClick = this.onEditServerDetailsClick; - } - // XXX: This link does *not* have a target="_blank" because single sign-on relies on - // redirecting the user back to a URI once they're logged in. On the web, this means - // we use the same window and redirect back to Element. On Electron, this actually - // opens the SSO page in the Electron app itself due to - // https://github.com/electron/electron/issues/8841 and so happens to work. - // If this bug gets fixed, it will break SSO since it will open the SSO page in the - // user's browser, let them log into their SSO provider, then redirect their browser - // to vector://vector which, of course, will not work. + const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType) as ISSOFlow; + return ( -
- - - -
+ flow.type === "m.login.password")} + /> ); }; @@ -670,9 +578,11 @@ export default class LoginComponent extends React.Component {
; } else if (SettingsStore.getValue(UIFeature.Registration)) { footer = ( - - { _t('Create account') } - + + {_t("New? Create account", {}, { + a: sub => { sub }, + })} + ); } @@ -686,8 +596,11 @@ export default class LoginComponent extends React.Component { { errorTextSection } { serverDeadSection } - { this.renderServerComponent() } - { this.renderLoginComponentForStep() } + + { this.renderLoginComponentForFlows() } { footer } diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index f97f20cf598..e1a2fc55901 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -15,29 +15,21 @@ limitations under the License. */ import Matrix from 'matrix-js-sdk'; -import React, {ComponentProps, ReactNode} from 'react'; +import React, {ReactNode} from 'react'; import {MatrixClient} from "matrix-js-sdk/src/client"; import * as sdk from '../../../index'; import { _t, _td } from '../../../languageHandler'; -import SdkConfig from '../../../SdkConfig'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; -import * as ServerType from '../../views/auth/ServerTypeSelector'; import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import classNames from "classnames"; import * as Lifecycle from '../../../Lifecycle'; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import AuthPage from "../../views/auth/AuthPage"; -import Login from "../../../Login"; +import Login, {ISSOFlow} from "../../../Login"; import dis from "../../../dispatcher/dispatcher"; - -// Phases -enum Phase { - // Show controls to configure server details - ServerDetails = 0, - // Show the appropriate registration flow(s) for the server - Registration = 1, -} +import SSOButtons from "../../views/elements/SSOButtons"; +import ServerPicker from '../../views/elements/ServerPicker'; interface IProps { serverConfig: ValidatedServerConfig; @@ -47,6 +39,7 @@ interface IProps { clientSecret?: string; sessionId?: string; idSid?: string; + fragmentAfterLogin?: string; // Called when the user has logged in. Params: // - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken @@ -92,9 +85,6 @@ interface IState { // If set, we've registered but are not going to log // the user in to their new account automatically. completedNoSignin: boolean; - serverType: ServerType.FREE | ServerType.PREMIUM | ServerType.ADVANCED; - // Phase of the overall registration dialog. - phase: Phase; flows: { stages: string[]; }[]; @@ -109,23 +99,22 @@ interface IState { // Our matrix client - part of state because we can't render the UI auth // component without it. matrixClient?: MatrixClient; - // whether the HS requires an ID server to register with a threepid - serverRequiresIdServer?: boolean; // The user ID we've just registered registeredUsername?: string; // if a different user ID to the one we just registered is logged in, // this is the user ID that's logged in. differentLoggedInUserId?: string; + // the SSO flow definition, this is fetched from /login as that's the only + // place it is exposed. + ssoFlow?: ISSOFlow; } -// Enable phases for registration -const PHASES_ENABLED = true; - export default class Registration extends React.Component { + loginLogic: Login; + constructor(props) { super(props); - const serverType = ServerType.getTypeFromServerConfig(this.props.serverConfig); this.state = { busy: false, errorText: null, @@ -133,14 +122,17 @@ export default class Registration extends React.Component { email: this.props.email, }, doingUIAuth: Boolean(this.props.sessionId), - serverType, - phase: Phase.Registration, flows: null, completedNoSignin: false, serverIsAlive: true, serverErrorIsFatal: false, serverDeadError: "", }; + + const {hsUrl, isUrl} = this.props.serverConfig; + this.loginLogic = new Login(hsUrl, isUrl, null, { + defaultDeviceDisplayName: "Element login check", // We shouldn't ever be used + }); } componentDidMount() { @@ -154,61 +146,8 @@ export default class Registration extends React.Component { 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: Registration.getDefaultPhaseForServerType(serverType), - }); - } } - private static getDefaultPhaseForServerType(type: IState["serverType"]) { - switch (type) { - case ServerType.FREE: { - // Move directly to the registration phase since the server - // details are fixed. - return Phase.Registration; - } - case ServerType.PREMIUM: - case ServerType.ADVANCED: - return Phase.ServerDetails; - } - } - - private onServerTypeChange = (type: IState["serverType"]) => { - this.setState({ - serverType: type, - }); - - // When changing server types, set the HS / IS URLs to reasonable defaults for the - // the new type. - switch (type) { - case ServerType.FREE: { - const { 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: - // Use the default config from the config - this.props.onServerConfigChange(SdkConfig.get()["validated_server_config"]); - break; - } - - // Reset the phase to default phase for the server type. - this.setState({ - phase: Registration.getDefaultPhaseForServerType(type), - }); - }; - private async replaceClient(serverConfig: ValidatedServerConfig) { this.setState({ errorText: null, @@ -245,16 +184,20 @@ export default class Registration extends React.Component { idBaseUrl: isUrl, }); - let serverRequiresIdServer = true; + this.loginLogic.setHomeserverUrl(hsUrl); + this.loginLogic.setIdentityServerUrl(isUrl); + + let ssoFlow: ISSOFlow; try { - serverRequiresIdServer = await cli.doesServerRequireIdServerParam(); + const loginFlows = await this.loginLogic.getFlows(); + ssoFlow = loginFlows.find(f => f.type === "m.login.sso" || f.type === "m.login.cas") as ISSOFlow; } catch (e) { - console.log("Unable to determine is server needs id_server param", e); + console.error("Failed to get login flows to check for SSO support", e); } this.setState({ matrixClient: cli, - serverRequiresIdServer, + ssoFlow, busy: false, }); const showGenericError = (e) => { @@ -282,26 +225,16 @@ export default class Registration extends React.Component { // At this point registration is pretty much disabled, but before we do that let's // quickly check to see if the server supports SSO instead. If it does, we'll send // the user off to the login page to figure their account out. - try { - const loginLogic = new Login(hsUrl, isUrl, null, { - defaultDeviceDisplayName: "Element login check", // We shouldn't ever be used + if (ssoFlow) { + // Redirect to login page - server probably expects SSO only + dis.dispatch({action: 'start_login'}); + } else { + this.setState({ + serverErrorIsFatal: true, // fatal because user cannot continue on this server + errorText: _t("Registration has been disabled on this homeserver."), + // add empty flows array to get rid of spinner + flows: [], }); - const flows = await loginLogic.getFlows(); - const hasSsoFlow = flows.find(f => f.type === 'm.login.sso' || f.type === 'm.login.cas'); - if (hasSsoFlow) { - // Redirect to login page - server probably expects SSO only - dis.dispatch({action: 'start_login'}); - } else { - this.setState({ - serverErrorIsFatal: true, // fatal because user cannot continue on this server - errorText: _t("Registration has been disabled on this homeserver."), - // add empty flows array to get rid of spinner - flows: [], - }); - } - } catch (e) { - console.error("Failed to get login flows to check for SSO support", e); - showGenericError(e); } } else { console.log("Unable to query for supported registration methods.", e); @@ -365,6 +298,8 @@ export default class Registration extends React.Component { if (!msisdnAvailable) { msg = _t('This server does not support authentication with a phone number.'); } + } else if (response.errcode === "M_USER_IN_USE") { + msg = _t("That username already exists, please try another."); } this.setState({ busy: false, @@ -453,21 +388,6 @@ export default class Registration extends React.Component { this.setState({ busy: false, doingUIAuth: false, - phase: Phase.Registration, - }); - }; - - private onServerDetailsNextPhaseClick = async () => { - this.setState({ - phase: Phase.Registration, - }); - }; - - private onEditServerDetailsClick = ev => { - ev.preventDefault(); - ev.stopPropagation(); - this.setState({ - phase: Phase.ServerDetails, }); }; @@ -516,77 +436,7 @@ export default class Registration extends React.Component { } }; - private renderServerComponent() { - const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector"); - const ServerConfig = sdk.getComponent("auth.ServerConfig"); - const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig"); - - if (SdkConfig.get()['disable_custom_urls']) { - return null; - } - - // Hide the server picker once the user is doing UI Auth unless encountered a fatal server error - if (this.state.phase !== Phase.ServerDetails && this.state.doingUIAuth && !this.state.serverErrorIsFatal) { - return null; - } - - // If we're on a different phase, we only show the server type selector, - // which is always shown if we allow custom URLs at all. - // (if there's a fatal server error, we need to show the full server - // config as the user may need to change servers to resolve the error). - if (PHASES_ENABLED && this.state.phase !== Phase.ServerDetails && !this.state.serverErrorIsFatal) { - return
- -
; - } - - const serverDetailsProps: ComponentProps = {}; - 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 = ; - break; - case ServerType.ADVANCED: - serverDetails = ; - break; - } - - return
- - {serverDetails} -
; - } - private renderRegisterComponent() { - if (PHASES_ENABLED && this.state.phase !== Phase.Registration) { - return null; - } - const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth'); const Spinner = sdk.getComponent('elements.Spinner'); const RegistrationForm = sdk.getComponent('auth.RegistrationForm'); @@ -610,18 +460,48 @@ export default class Registration extends React.Component { ; } else if (this.state.flows.length) { - return ; + let ssoSection; + if (this.state.ssoFlow) { + let continueWithSection; + const providers = this.state.ssoFlow["org.matrix.msc2858.identity_providers"] + || this.state.ssoFlow["identity_providers"] || []; + // when there is only a single (or 0) providers we show a wide button with `Continue with X` text + if (providers.length > 1) { + // i18n: ssoButtons is a placeholder to help translators understand context + continueWithSection =

+ { _t("Continue with %(ssoButtons)s", { ssoButtons: "" }).trim() } +

; + } + + // i18n: ssoButtons & usernamePassword are placeholders to help translators understand context + ssoSection = + { continueWithSection } + +

+ { _t("%(ssoButtons)s Or %(usernamePassword)s", { ssoButtons: "", usernamePassword: ""}).trim() } +

+
; + } + + return + { ssoSection } + + ; } } @@ -650,13 +530,15 @@ export default class Registration extends React.Component { ); } - const signIn = - { _t('Sign in instead') } - ; + const signIn = + {_t("Already have an account? Sign in here", {}, { + a: sub => { sub }, + })} + ; // Only show the 'go back' button if you're not looking at the form let goBack; - if ((PHASES_ENABLED && this.state.phase !== Phase.Registration) || this.state.doingUIAuth) { + if (this.state.doingUIAuth) { goBack = { _t('Go back') } ; @@ -702,48 +584,16 @@ export default class Registration extends React.Component { { regDoneText } ; } else { - let yourMatrixAccountText: ReactNode = _t('Create your Matrix account on %(serverName)s', { - serverName: this.props.serverConfig.hsName, - }); - if (this.props.serverConfig.hsNameIsDifferent) { - const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip"); - - yourMatrixAccountText = _t('Create your Matrix account on ', {}, { - 'underlinedServerName': () => { - return ; - }, - }); - } - - // If custom URLs are allowed, user is not doing UIA flows and they haven't selected the Free server type, - // wire up the server details edit link. - let editLink = null; - if (PHASES_ENABLED && - !SdkConfig.get()['disable_custom_urls'] && - this.state.serverType !== ServerType.FREE && - !this.state.doingUIAuth - ) { - editLink = ( - - {_t('Change')} - - ); - } - body =
-

{ _t('Create your account') }

+

{ _t('Create account') }

{ errorText } { serverDeadSection } - { this.renderServerComponent() } - { this.state.phase !== Phase.ServerDetails &&

- {yourMatrixAccountText} - {editLink} -

} + { this.renderRegisterComponent() } { goBack } { signIn } diff --git a/src/components/structures/auth/SoftLogout.js b/src/components/structures/auth/SoftLogout.js index a539c8c9ee5..fdc1aec96de 100644 --- a/src/components/structures/auth/SoftLogout.js +++ b/src/components/structures/auth/SoftLogout.js @@ -24,8 +24,8 @@ import Modal from '../../../Modal'; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {sendLoginRequest} from "../../../Login"; import AuthPage from "../../views/auth/AuthPage"; -import SSOButton from "../../views/elements/SSOButton"; import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "../../../BasePlatform"; +import SSOButtons from "../../views/elements/SSOButtons"; const LOGIN_VIEW = { LOADING: 1, @@ -101,10 +101,11 @@ export default class SoftLogout extends React.Component { // Note: we don't use the existing Login class because it is heavily flow-based. We don't // care about login flows here, unless it is the single flow we support. const client = MatrixClientPeg.get(); - const loginViews = (await client.loginFlows()).flows.map(f => FLOWS_TO_VIEWS[f.type]); + const flows = (await client.loginFlows()).flows; + const loginViews = flows.map(f => FLOWS_TO_VIEWS[f.type]); const chosenView = loginViews.filter(f => !!f)[0] || LOGIN_VIEW.UNSUPPORTED; - this.setState({loginView: chosenView}); + this.setState({ flows, loginView: chosenView }); } onPasswordChange = (ev) => { @@ -240,13 +241,18 @@ export default class SoftLogout extends React.Component { introText = _t("Sign in and regain access to your account."); } // else we already have a message and should use it (key backup warning) + const loginType = this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso"; + const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType); + return (

{introText}

- flow.type === "m.login.password")} />
); diff --git a/src/components/views/auth/CustomServerDialog.js b/src/components/views/auth/CustomServerDialog.js deleted file mode 100644 index 138f8c4689f..00000000000 --- a/src/components/views/auth/CustomServerDialog.js +++ /dev/null @@ -1,47 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -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 { _t } from '../../../languageHandler'; -import SdkConfig from '../../../SdkConfig'; - -export default class CustomServerDialog extends React.Component { - render() { - const brand = SdkConfig.get().brand; - return ( -
-
- { _t("Custom Server Options") } -
-
-

{_t( - "You can use the custom server options to sign into other " + - "Matrix servers by specifying a different homeserver URL. This " + - "allows you to use %(brand)s with an existing Matrix account on a " + - "different homeserver.", - { brand }, - )}

-
-
- -
-
- ); - } -} diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index 6628ca71205..60e57afc98f 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -18,7 +18,6 @@ limitations under the License. import React, {createRef} from 'react'; import PropTypes from 'prop-types'; -import url from 'url'; import classnames from 'classnames'; import * as sdk from '../../../index'; @@ -500,17 +499,11 @@ export class MsisdnAuthEntry extends React.Component { }); try { - const requiresIdServerParam = - await this.props.matrixClient.doesServerRequireIdServerParam(); let result; if (this._submitUrl) { result = await this.props.matrixClient.submitMsisdnTokenOtherUrl( this._submitUrl, this._sid, this.props.clientSecret, this.state.token, ); - } else if (requiresIdServerParam) { - result = await this.props.matrixClient.submitMsisdnToken( - this._sid, this.props.clientSecret, this.state.token, - ); } else { throw new Error("The registration with MSISDN flow is misconfigured"); } @@ -519,12 +512,6 @@ export class MsisdnAuthEntry extends React.Component { sid: this._sid, client_secret: this.props.clientSecret, }; - if (requiresIdServerParam) { - const idServerParsedUrl = url.parse( - this.props.matrixClient.getIdentityServerUrl(), - ); - creds.id_server = idServerParsedUrl.host; - } this.props.submitAuthDict({ type: MsisdnAuthEntry.LOGIN_TYPE, // TODO: Remove `threepid_creds` once servers support proper UIA diff --git a/src/components/views/auth/ModularServerConfig.js b/src/components/views/auth/ModularServerConfig.js deleted file mode 100644 index 28fd16379d5..00000000000 --- a/src/components/views/auth/ModularServerConfig.js +++ /dev/null @@ -1,124 +0,0 @@ -/* -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 * as 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'; -import ServerConfig from "./ServerConfig"; - -const MODULAR_URL = 'https://element.io/matrix-services' + - '?utm_source=element-web&utm_medium=web&utm_campaign=element-web-authentication'; - -// TODO: TravisR - Can this extend ServerConfig for most things? - -/* - * Configure the Modular server name. - * - * This is a variant of ServerConfig with only the HS field and different body - * text that is specific to the Modular case. - */ -export default class ModularServerConfig extends ServerConfig { - static propTypes = ServerConfig.propTypes; - - 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, - 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, - }); - - return null; - } - } - - 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); - } - - render() { - const Field = sdk.getComponent('elements.Field'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - - const submitButton = this.props.submitText - ? {this.props.submitText} - : null; - - return ( -
-

{_t("Your server")}

- {_t( - "Enter the location of your Element Matrix Services homeserver. It may use your own " + - "domain name or be a subdomain of element.io.", - {}, { - a: sub => - {sub} - , - }, - )} - -
- -
- {submitButton} - -
- ); - } -} diff --git a/src/components/views/auth/PasswordLogin.tsx b/src/components/views/auth/PasswordLogin.tsx index fced2e08d01..84e583c3a5e 100644 --- a/src/components/views/auth/PasswordLogin.tsx +++ b/src/components/views/auth/PasswordLogin.tsx @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016, 2017, 2019 New Vector Ltd. +Copyright 2015, 2016, 2017, 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -26,7 +26,6 @@ import withValidation from "../elements/Validation"; import * as Email from "../../../email"; import Field from "../elements/Field"; import CountryDropdown from "./CountryDropdown"; -import SignInToText from "./SignInToText"; // For validating phone numbers without country codes const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; @@ -47,7 +46,6 @@ interface IProps { onUsernameBlur?(username: string): void; onPhoneCountryChanged?(phoneCountry: string): void; onPhoneNumberChanged?(phoneNumber: string): void; - onEditServerDetailsClick?(): void; onForgotPasswordClick?(): void; } @@ -70,7 +68,6 @@ enum LoginField { */ export default class PasswordLogin extends React.PureComponent { static defaultProps = { - onEditServerDetailsClick: null, onUsernameChanged: function() {}, onUsernameBlur: function() {}, onPhoneCountryChanged: function() {}, @@ -296,7 +293,7 @@ export default class PasswordLogin extends React.PureComponent { }, { key: "number", test: ({ value }) => !value || PHONE_NUMBER_REGEX.test(value), - invalid: () => _t("Doesn't look like a valid phone number"), + invalid: () => _t("That phone number doesn't look quite right, please check and try again"), }, ], }); @@ -357,6 +354,7 @@ export default class PasswordLogin extends React.PureComponent { key="username_input" type="text" label={_t("Username")} + placeholder={_t("Username").toLocaleLowerCase()} value={this.props.username} onChange={this.onUsernameChanged} onFocus={this.onUsernameFocus} @@ -410,20 +408,14 @@ export default class PasswordLogin extends React.PureComponent { let forgotPasswordJsx; if (this.props.onForgotPasswordClick) { - forgotPasswordJsx = - {_t('Not sure of your password? Set a new one', {}, { - a: sub => ( - - {sub} - - ), - })} - ; + forgotPasswordJsx = + {_t("Forgot password?")} + ; } const pwFieldClass = classNames({ @@ -465,8 +457,6 @@ export default class PasswordLogin extends React.PureComponent { return (
-
{loginType} {loginField} diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx index f6fb3bb3ea1..a0c7ab7b4f7 100644 --- a/src/components/views/auth/RegistrationForm.tsx +++ b/src/components/views/auth/RegistrationForm.tsx @@ -28,6 +28,8 @@ import withValidation from '../elements/Validation'; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import PassphraseField from "./PassphraseField"; import CountlyAnalytics from "../../../CountlyAnalytics"; +import Field from '../elements/Field'; +import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog'; enum RegistrationField { Email = "field_email", @@ -51,7 +53,6 @@ interface IProps { }[]; serverConfig: ValidatedServerConfig; canSubmit?: boolean; - serverRequiresIdServer?: boolean; onRegisterClick(params: { username: string; @@ -104,6 +105,7 @@ export default class RegistrationForm extends React.PureComponent { ev.preventDefault(); + ev.persist(); if (!this.props.canSubmit) return; @@ -114,38 +116,24 @@ export default class RegistrationForm extends React.PureComponent { + if (confirmed) { + this.setState({ + email, + }, () => { + this.doSubmit(ev); + }); + } + }, + }); } else { // user can't set an e-mail so don't prompt them to this.doSubmit(ev); return; } - - CountlyAnalytics.instance.track("onboarding_registration_submit_warn"); - - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createTrackedDialog('If you don\'t specify an email address...', '', QuestionDialog, { - title: _t("Warning!"), - description: desc, - button: _t("Continue"), - onFinished: (confirmed) => { - if (confirmed) { - this.doSubmit(ev); - } - }, - }); } else { this.doSubmit(ev); } @@ -357,7 +345,7 @@ export default class RegistrationForm extends React.PureComponent !value || phoneNumberLooksValid(value), - invalid: () => _t("Doesn't look like a valid phone number"), + invalid: () => _t("That phone number doesn't look quite right, please check and try again"), }, ], }); @@ -416,11 +404,7 @@ export default class RegistrationForm extends React.PureComponent this[RegistrationField.PasswordConfirm] = field} @@ -493,7 +470,6 @@ export default class RegistrationForm extends React.PureComponent this[RegistrationField.Username] = field} type="text" autoFocus={true} label={_t("Username")} + placeholder={_t("Username").toLocaleLowerCase()} value={this.state.username} onChange={this.onUsernameChange} onValidate={this.onUsernameValidate} @@ -539,30 +515,22 @@ export default class RegistrationForm extends React.PureComponent - {_t( - "Set an email for account recovery. " + - "Use email or phone to optionally be discoverable by existing contacts.", - )} + { + _t("Add an email to be able to reset your password.") + } { + _t("Use email or phone to optionally be discoverable by existing contacts.") + }
; } else { emailHelperText =
- {_t( - "Set an email for account recovery. " + - "Use email to optionally be discoverable by existing contacts.", - )} + { + _t("Add an email to be able to reset your password.") + } { + _t("Use email to optionally be discoverable by existing contacts.") + }
; } } - const haveIs = Boolean(this.props.serverConfig.isUrl); - let noIsText = null; - if (this.props.serverRequiresIdServer && !haveIs) { - noIsText =
- {_t( - "No identity server is configured so you cannot add an email address in order to " + - "reset your password in the future.", - )} -
; - } return (
@@ -579,7 +547,6 @@ export default class RegistrationForm extends React.PureComponent { emailHelperText } - { noIsText } { registerButton }
diff --git a/src/components/views/auth/ServerConfig.js b/src/components/views/auth/ServerConfig.js deleted file mode 100644 index e04bf9e25a3..00000000000 --- a/src/components/views/auth/ServerConfig.js +++ /dev/null @@ -1,291 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -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 Modal from '../../../Modal'; -import * as sdk from '../../../index'; -import { _t } from '../../../languageHandler'; -import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; -import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils"; -import SdkConfig from "../../../SdkConfig"; -import { createClient } from 'matrix-js-sdk/src/matrix'; -import classNames from 'classnames'; -import CountlyAnalytics from "../../../CountlyAnalytics"; - -/* - * A pure UI component which displays the HS and IS to use. - */ - -export default class ServerConfig extends React.PureComponent { - static propTypes = { - 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, - - // Whether the flow this component is embedded in requires an identity - // server when the homeserver says it will need one. Default false. - showIdentityServerIfRequiredByHomeserver: PropTypes.bool, - }; - - static defaultProps = { - onServerConfigChange: function() {}, - delayTimeMs: 0, - }; - - constructor(props) { - super(props); - - this.state = { - busy: false, - errorText: "", - hsUrl: props.serverConfig.hsUrl, - isUrl: props.serverConfig.isUrl, - showIdentityServer: false, - }; - - CountlyAnalytics.instance.track("onboarding_custom_server"); - } - - // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase - if (newProps.serverConfig.hsUrl === this.state.hsUrl && - newProps.serverConfig.isUrl === this.state.isUrl) return; - - this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl); - } - - 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" - const result = this.validateAndApplyServer(this.state.hsUrl, this.state.isUrl); - if (!result) { - return result; - } - - // If the UI flow this component is embedded in requires an identity - // server when the homeserver says it will need one, check first and - // reveal this field if not already shown. - // XXX: This a backward compatibility path for homeservers that require - // an identity server to be passed during certain flows. - // See also https://github.com/matrix-org/synapse/pull/5868. - if ( - this.props.showIdentityServerIfRequiredByHomeserver && - !this.state.showIdentityServer && - await this.isIdentityServerRequiredByHomeserver() - ) { - this.setState({ - showIdentityServer: true, - }); - return null; - } - - return result; - } - - 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({ - hsUrl: defaultConfig.hsUrl, - isUrl: defaultConfig.isUrl, - busy: false, - errorText: "", - }); - this.props.onServerConfigChange(defaultConfig); - return defaultConfig; - } - - this.setState({ - 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); - - const stateForError = AutoDiscoveryUtils.authComponentStateForError(e); - if (!stateForError.isFatalError) { - this.setState({ - busy: false, - }); - // carry on anyway - const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl, true); - this.props.onServerConfigChange(result); - return result; - } else { - let message = _t("Unable to validate homeserver/identity server"); - if (e.translatedMessage) { - message = e.translatedMessage; - } - this.setState({ - busy: false, - errorText: message, - }); - - return null; - } - } - } - - async isIdentityServerRequiredByHomeserver() { - // XXX: We shouldn't have to create a whole new MatrixClient just to - // check if the homeserver requires an identity server... Should it be - // extracted to a static utils function...? - return createClient({ - baseUrl: this.state.hsUrl, - }).doesServerRequireIdServerParam(); - } - - onHomeserverBlur = (ev) => { - this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => { - this.validateServer(); - }); - }; - - onHomeserverChange = (ev) => { - const hsUrl = ev.target.value; - this.setState({ hsUrl }); - }; - - onIdentityServerBlur = (ev) => { - this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => { - this.validateServer(); - }); - }; - - onIdentityServerChange = (ev) => { - const isUrl = ev.target.value; - this.setState({ isUrl }); - }; - - onSubmit = async (ev) => { - ev.preventDefault(); - ev.stopPropagation(); - const result = await this.validateServer(); - if (!result) return; // Do not continue. - - if (this.props.onAfterSubmit) { - this.props.onAfterSubmit(); - } - }; - - _waitThenInvoke(existingTimeoutId, fn) { - if (existingTimeoutId) { - clearTimeout(existingTimeoutId); - } - return setTimeout(fn.bind(this), this.props.delayTimeMs); - } - - showHelpPopup = () => { - const CustomServerDialog = sdk.getComponent('auth.CustomServerDialog'); - Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog); - }; - - _renderHomeserverSection() { - const Field = sdk.getComponent('elements.Field'); - return
- {_t("Enter your custom homeserver URL What does this mean?", {}, { - a: sub => - {sub} - , - })} - -
; - } - - _renderIdentityServerSection() { - const Field = sdk.getComponent('elements.Field'); - const classes = classNames({ - "mx_ServerConfig_identityServer": true, - "mx_ServerConfig_identityServer_shown": this.state.showIdentityServer, - }); - return
- {_t("Enter your custom identity server URL What does this mean?", {}, { - a: sub => - {sub} - , - })} - -
; - } - - render() { - 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")}

- {errorText} - {this._renderHomeserverSection()} - {this._renderIdentityServerSection()} - {submitButton} -
- ); - } -} diff --git a/src/components/views/auth/ServerTypeSelector.js b/src/components/views/auth/ServerTypeSelector.js deleted file mode 100644 index 71e7ac7f0ea..00000000000 --- a/src/components/views/auth/ServerTypeSelector.js +++ /dev/null @@ -1,153 +0,0 @@ -/* -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 { _t } from '../../../languageHandler'; -import * as sdk from '../../../index'; -import classnames from 'classnames'; -import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; -import {makeType} from "../../../utils/TypeUtils"; - -const MODULAR_URL = 'https://element.io/matrix-services' + - '?utm_source=element-web&utm_medium=web&utm_campaign=element-web-authentication'; - -export const FREE = 'Free'; -export const PREMIUM = 'Premium'; -export const ADVANCED = 'Advanced'; - -export const TYPES = { - FREE: { - id: FREE, - label: () => _t('Free'), - logo: () => , - description: () => _t('Join millions for free on the largest public server'), - serverConfig: makeType(ValidatedServerConfig, { - hsUrl: "https://matrix-client.matrix.org", - hsName: "matrix.org", - hsNameIsDifferent: false, - isUrl: "https://vector.im", - }), - }, - PREMIUM: { - id: PREMIUM, - label: () => _t('Premium'), - logo: () => , - description: () => _t('Premium hosting for organisations Learn more', {}, { - a: sub => - {sub} - , - }), - identityServerUrl: "https://vector.im", - }, - ADVANCED: { - id: ADVANCED, - label: () => _t('Advanced'), - logo: () =>
- - {_t('Other')} -
, - description: () => _t('Find other public servers or use a custom server'), - }, -}; - -export function getTypeFromServerConfig(config) { - const {hsUrl} = config; - if (!hsUrl) { - return null; - } 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 - // server type selector. - return PREMIUM; - } else { - return ADVANCED; - } -} - -export default class ServerTypeSelector extends React.PureComponent { - static propTypes = { - // The default selected type. - selected: PropTypes.string, - // Handler called when the selected type changes. - onChange: PropTypes.func.isRequired, - }; - - constructor(props) { - super(props); - - const { - selected, - } = props; - - this.state = { - selected, - }; - } - - updateSelectedType(type) { - if (this.state.selected === type) { - return; - } - this.setState({ - selected: type, - }); - if (this.props.onChange) { - this.props.onChange(type); - } - } - - onClick = (e) => { - e.stopPropagation(); - const type = e.currentTarget.dataset.id; - this.updateSelectedType(type); - }; - - render() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - - const serverTypes = []; - for (const type of Object.values(TYPES)) { - const { id, label, logo, description } = type; - const classes = classnames( - "mx_ServerTypeSelector_type", - `mx_ServerTypeSelector_type_${id}`, - { - "mx_ServerTypeSelector_type_selected": id === this.state.selected, - }, - ); - - serverTypes.push(
-
- {label()} -
- -
- {logo()} -
-
- {description()} -
-
-
); - } - - return
- {serverTypes} -
; - } -} diff --git a/src/components/views/auth/SignInToText.js b/src/components/views/auth/SignInToText.js deleted file mode 100644 index 7564096b7db..00000000000 --- a/src/components/views/auth/SignInToText.js +++ /dev/null @@ -1,62 +0,0 @@ -/* -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -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 {_t} from "../../../languageHandler"; -import * as sdk from "../../../index"; -import PropTypes from "prop-types"; -import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; - -export default class SignInToText extends React.PureComponent { - static propTypes = { - serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, - onEditServerDetailsClick: PropTypes.func, - }; - - render() { - let signInToText = _t('Sign in to your Matrix account on %(serverName)s', { - serverName: this.props.serverConfig.hsName, - }); - if (this.props.serverConfig.hsNameIsDifferent) { - const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip"); - - signInToText = _t('Sign in to your Matrix account on ', {}, { - 'underlinedServerName': () => { - return ; - }, - }); - } - - let editLink = null; - if (this.props.onEditServerDetailsClick) { - editLink = - {_t('Change')} - ; - } - - return

- {signInToText} - {editLink} -

; - } -} diff --git a/src/components/views/dialogs/InfoDialog.js b/src/components/views/dialogs/InfoDialog.js index 8125bc3edd4..97ae968ff34 100644 --- a/src/components/views/dialogs/InfoDialog.js +++ b/src/components/views/dialogs/InfoDialog.js @@ -31,6 +31,7 @@ export default class InfoDialog extends React.Component { onFinished: PropTypes.func, hasCloseButton: PropTypes.bool, onKeyDown: PropTypes.func, + fixedWidth: PropTypes.bool, }; static defaultProps = { @@ -54,6 +55,7 @@ export default class InfoDialog extends React.Component { contentId='mx_Dialog_content' hasCancel={this.props.hasCloseButton} onKeyDown={this.props.onKeyDown} + fixedWidth={this.props.fixedWidth} >
{ this.props.description } diff --git a/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx b/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx new file mode 100644 index 00000000000..b7cc81c1134 --- /dev/null +++ b/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx @@ -0,0 +1,96 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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 * as React from "react"; + +import { _t } from '../../../languageHandler'; +import { IDialogProps } from "./IDialogProps"; +import {useRef, useState} from "react"; +import Field from "../elements/Field"; +import CountlyAnalytics from "../../../CountlyAnalytics"; +import withValidation from "../elements/Validation"; +import * as Email from "../../../email"; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; + +interface IProps extends IDialogProps { + onFinished(continued: boolean, email?: string): void; +} + +const validation = withValidation({ + rules: [ + { + key: "email", + test: ({ value }) => !value || Email.looksValid(value), + invalid: () => _t("Doesn't look like a valid email address"), + }, + ], +}); + +const RegistrationEmailPromptDialog: React.FC = ({onFinished}) => { + const [email, setEmail] = useState(""); + const fieldRef = useRef(); + + const onSubmit = async () => { + if (email) { + const valid = await fieldRef.current.validate({ allowEmpty: false }); + + if (!valid) { + fieldRef.current.focus(); + fieldRef.current.validate({ allowEmpty: false, focused: true }); + return; + } + } + + onFinished(true, email); + }; + + return onFinished(false)} + fixedWidth={false} + > +
+

{_t("Just a heads up, if you don't add an email and forget your password, you could " + + "permanently lose access to your account.", {}, { + b: sub => {sub}, + })}

+
+ { + setEmail(ev.target.value); + }} + onValidate={async fieldState => await validation(fieldState)} + onFocus={() => CountlyAnalytics.instance.track("onboarding_registration_email2_focus")} + onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email2_blur")} + /> + +
+ +
; +}; + +export default RegistrationEmailPromptDialog; diff --git a/src/components/views/dialogs/ServerPickerDialog.tsx b/src/components/views/dialogs/ServerPickerDialog.tsx new file mode 100644 index 00000000000..9eb819c98ee --- /dev/null +++ b/src/components/views/dialogs/ServerPickerDialog.tsx @@ -0,0 +1,203 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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, {createRef} from 'react'; + +import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; +import BaseDialog from './BaseDialog'; +import { _t } from '../../../languageHandler'; +import AccessibleButton from "../elements/AccessibleButton"; +import SdkConfig from "../../../SdkConfig"; +import Field from "../elements/Field"; +import StyledRadioButton from "../elements/StyledRadioButton"; +import TextWithTooltip from "../elements/TextWithTooltip"; +import withValidation, {IFieldState} from "../elements/Validation"; + +interface IProps { + title?: string; + serverConfig: ValidatedServerConfig; + onFinished(config?: ValidatedServerConfig): void; +} + +interface IState { + defaultChosen: boolean; + otherHomeserver: string; +} + +export default class ServerPickerDialog extends React.PureComponent { + private readonly defaultServer: ValidatedServerConfig; + private readonly fieldRef = createRef(); + private validatedConf: ValidatedServerConfig; + + constructor(props) { + super(props); + + const config = SdkConfig.get(); + this.defaultServer = config["validated_server_config"] as ValidatedServerConfig; + this.state = { + defaultChosen: this.props.serverConfig.isDefault, + otherHomeserver: this.props.serverConfig.isDefault ? "" : this.props.serverConfig.hsUrl, + }; + } + + private onDefaultChosen = () => { + this.setState({ defaultChosen: true }); + }; + + private onOtherChosen = () => { + this.setState({ defaultChosen: false }); + }; + + private onHomeserverChange = (ev) => { + this.setState({ otherHomeserver: ev.target.value }); + }; + + // 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" + private validate = withValidation({ + deriveData: async ({ value: hsUrl }) => { + // Always try and use the defaults first + const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"]; + if (defaultConfig.hsUrl === hsUrl) return {}; + + try { + this.validatedConf = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl); + return {}; + } catch (e) { + console.error(e); + + const stateForError = AutoDiscoveryUtils.authComponentStateForError(e); + if (!stateForError.isFatalError) { + // carry on anyway + this.validatedConf = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, null, true); + return {}; + } else { + let error = _t("Unable to validate homeserver/identity server"); + if (e.translatedMessage) { + error = e.translatedMessage; + } + return { error }; + } + } + }, + rules: [ + { + key: "required", + test: ({ value, allowEmpty }) => allowEmpty || !!value, + invalid: () => _t("Specify a homeserver"), + }, { + key: "valid", + test: async function({ value }, { error }) { + if (!value) return true; + return !error; + }, + invalid: function({ error }) { + return error; + }, + }, + ], + }); + + private onHomeserverValidate = (fieldState: IFieldState) => this.validate(fieldState); + + private onSubmit = async (ev) => { + ev.preventDefault(); + + const valid = await this.fieldRef.current.validate({ allowEmpty: false }); + + if (!valid) { + this.fieldRef.current.focus(); + this.fieldRef.current.validate({ allowEmpty: false, focused: true }); + return; + } + + this.props.onFinished(this.validatedConf); + }; + + public render() { + let text; + if (this.defaultServer.hsName === "matrix.org") { + text = _t("Matrix.org is the biggest public homeserver in the world, so it’s a good place for many."); + } + + let defaultServerName = this.defaultServer.hsName; + if (this.defaultServer.hsNameIsDifferent) { + defaultServerName = ( + + ); + } + + return +
+

+ {_t("We call the places you where you can host your account ‘homeservers’.")} {text} +

+ + + {defaultServerName} + + + + + +

+ {_t("Use your preferred Matrix homeserver if you have one, or host your own.")} +

+ + + {_t("Continue")} + + +

{_t("Learn more")}

+ + {_t("About homeservers")} + +
+
; + } +} diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index fb34f51b604..4335cc46ace 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -61,6 +61,10 @@ interface IProps { tooltipClassName?: string; // If specified, an additional class name to apply to the field container className?: string; + // On what events should validation occur; by default on all + validateOnFocus?: boolean; + validateOnBlur?: boolean; + validateOnChange?: boolean; // All other props pass through to the . } @@ -100,6 +104,9 @@ export default class Field extends React.PureComponent { public static readonly defaultProps = { element: "input", type: "text", + validateOnFocus: true, + validateOnBlur: true, + validateOnChange: true, }; /* @@ -137,9 +144,11 @@ export default class Field extends React.PureComponent { this.setState({ focused: true, }); - this.validate({ - focused: true, - }); + if (this.props.validateOnFocus) { + this.validate({ + focused: true, + }); + } // Parent component may have supplied its own `onFocus` as well if (this.props.onFocus) { this.props.onFocus(ev); @@ -147,7 +156,9 @@ export default class Field extends React.PureComponent { }; private onChange = (ev) => { - this.validateOnChange(); + if (this.props.validateOnChange) { + this.validateOnChange(); + } // Parent component may have supplied its own `onChange` as well if (this.props.onChange) { this.props.onChange(ev); @@ -158,16 +169,18 @@ export default class Field extends React.PureComponent { this.setState({ focused: false, }); - this.validate({ - focused: false, - }); + if (this.props.validateOnBlur) { + this.validate({ + focused: false, + }); + } // Parent component may have supplied its own `onBlur` as well if (this.props.onBlur) { this.props.onBlur(ev); } }; - private async validate({ focused, allowEmpty = true }: {focused: boolean, allowEmpty?: boolean}) { + public async validate({ focused, allowEmpty = true }: {focused?: boolean, allowEmpty?: boolean}) { if (!this.props.onValidate) { return; } @@ -196,6 +209,8 @@ export default class Field extends React.PureComponent { feedbackVisible: false, }); } + + return valid; } public render() { diff --git a/src/components/views/elements/SSOButton.js b/src/components/views/elements/SSOButton.js deleted file mode 100644 index 1126ae3cd77..00000000000 --- a/src/components/views/elements/SSOButton.js +++ /dev/null @@ -1,42 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -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 PlatformPeg from "../../../PlatformPeg"; -import AccessibleButton from "./AccessibleButton"; -import {_t} from "../../../languageHandler"; - -const SSOButton = ({matrixClient, loginType, fragmentAfterLogin, ...props}) => { - const onClick = () => { - PlatformPeg.get().startSingleSignOn(matrixClient, loginType, fragmentAfterLogin); - }; - - return ( - - {_t("Sign in with single sign-on")} - - ); -}; - -SSOButton.propTypes = { - matrixClient: PropTypes.object.isRequired, // does not use context as may use a temporary client - loginType: PropTypes.oneOf(["sso", "cas"]), // defaults to "sso" in base-apis - fragmentAfterLogin: PropTypes.string, -}; - -export default SSOButton; diff --git a/src/components/views/elements/SSOButtons.tsx b/src/components/views/elements/SSOButtons.tsx new file mode 100644 index 00000000000..a8bcc884125 --- /dev/null +++ b/src/components/views/elements/SSOButtons.tsx @@ -0,0 +1,110 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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 {MatrixClient} from "matrix-js-sdk/src/client"; + +import PlatformPeg from "../../../PlatformPeg"; +import AccessibleButton from "./AccessibleButton"; +import {_t} from "../../../languageHandler"; +import {IIdentityProvider, ISSOFlow} from "../../../Login"; +import classNames from "classnames"; + +interface ISSOButtonProps extends Omit { + idp: IIdentityProvider; + mini?: boolean; +} + +const SSOButton: React.FC = ({ + matrixClient, + loginType, + fragmentAfterLogin, + idp, + primary, + mini, + ...props +}) => { + const kind = primary ? "primary" : "primary_outline"; + const label = idp ? _t("Continue with %(provider)s", { provider: idp.name }) : _t("Sign in with single sign-on"); + + const onClick = () => { + PlatformPeg.get().startSingleSignOn(matrixClient, loginType, fragmentAfterLogin, idp.id); + }; + + let icon; + if (idp && idp.icon && idp.icon.startsWith("https://")) { + icon = {label}; + } + + const classes = classNames("mx_SSOButton", { + mx_SSOButton_mini: mini, + }); + + if (mini) { + // TODO fallback icon + return ( + + { icon } + + ); + } + + return ( + + { icon } + { label } + + ); +}; + +interface IProps { + matrixClient: MatrixClient; + flow: ISSOFlow; + loginType?: "sso" | "cas"; + fragmentAfterLogin?: string; + primary?: boolean; +} + +const SSOButtons: React.FC = ({matrixClient, flow, loginType, fragmentAfterLogin, primary}) => { + const providers = flow.identity_providers || flow["org.matrix.msc2858.identity_providers"] || []; + if (providers.length < 2) { + return
+ +
; + } + + return
+ { providers.map(idp => ( + + )) } +
; +}; + +export default SSOButtons; diff --git a/src/components/views/elements/ServerPicker.tsx b/src/components/views/elements/ServerPicker.tsx new file mode 100644 index 00000000000..7637ab7b8da --- /dev/null +++ b/src/components/views/elements/ServerPicker.tsx @@ -0,0 +1,93 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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 AccessibleButton from "./AccessibleButton"; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; +import {_t} from "../../../languageHandler"; +import TextWithTooltip from "./TextWithTooltip"; +import SdkConfig from "../../../SdkConfig"; +import Modal from "../../../Modal"; +import ServerPickerDialog from "../dialogs/ServerPickerDialog"; +import InfoDialog from "../dialogs/InfoDialog"; + +interface IProps { + title?: string; + dialogTitle?: string; + serverConfig: ValidatedServerConfig; + onServerConfigChange?(config: ValidatedServerConfig): void; +} + +const showPickerDialog = ( + title: string, + serverConfig: ValidatedServerConfig, + onFinished: (config: ValidatedServerConfig) => void, +) => { + Modal.createTrackedDialog("Server Picker", "", ServerPickerDialog, { title, serverConfig, onFinished }); +}; + +const onHelpClick = () => { + Modal.createTrackedDialog('Custom Server Dialog', '', InfoDialog, { + title: _t("Server Options"), + description: _t("You can use the custom server options to sign into other Matrix servers by specifying " + + "a different homeserver URL. This allows you to use Element with an existing Matrix account on " + + "a different homeserver."), + button: _t("Dismiss"), + hasCloseButton: false, + fixedWidth: false, + }, "mx_ServerPicker_helpDialog"); +}; + +const ServerPicker = ({ title, dialogTitle, serverConfig, onServerConfigChange }: IProps) => { + let editBtn; + if (!SdkConfig.get()["disable_custom_urls"] && onServerConfigChange) { + const onClick = () => { + showPickerDialog(dialogTitle, serverConfig, (config?: ValidatedServerConfig) => { + if (config) { + onServerConfigChange(config); + } + }); + }; + editBtn = + {_t("Edit")} + ; + } + + let serverName = serverConfig.hsName; + if (serverConfig.hsNameIsDifferent) { + serverName = ; + } + + let desc; + if (serverConfig.hsName === "matrix.org") { + desc = + {_t("Join millions for free on the largest public server")} + ; + } + + return
+

{title || _t("Homeserver")}

+ + {serverName} + { editBtn } + { desc } +
+} + +export default ServerPicker; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b33cbffb8fa..d44c01756a0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1897,6 +1897,11 @@ "This address is available to use": "This address is available to use", "This address is already in use": "This address is already in use", "Room directory": "Room directory", + "Server Options": "Server Options", + "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use Element with an existing Matrix account on a different homeserver.": "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use Element with an existing Matrix account on a different homeserver.", + "Join millions for free on the largest public server": "Join millions for free on the largest public server", + "Homeserver": "Homeserver", + "Continue with %(provider)s": "Continue with %(provider)s", "Sign in with single sign-on": "Sign in with single sign-on", "And %(count)s more...|other": "And %(count)s more...", "Home": "Home", @@ -2113,6 +2118,10 @@ "Use this session to verify your new one, granting it access to encrypted messages:": "Use this session to verify your new one, granting it access to encrypted messages:", "If you didn’t sign in to this session, your account may be compromised.": "If you didn’t sign in to this session, your account may be compromised.", "This wasn't me": "This wasn't me", + "Doesn't look like a valid email address": "Doesn't look like a valid email address", + "Continuing without email": "Continuing without email", + "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.": "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.", + "Email (optional)": "Email (optional)", "Please fill why you're reporting.": "Please fill why you're reporting.", "Report Content to Your Homeserver Administrator": "Report Content to Your Homeserver Administrator", "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.", @@ -2146,6 +2155,15 @@ "A connection error occurred while trying to contact the server.": "A connection error occurred while trying to contact the server.", "The server is not configured to indicate what the problem is (CORS).": "The server is not configured to indicate what the problem is (CORS).", "Recent changes that have not yet been received": "Recent changes that have not yet been received", + "Unable to validate homeserver/identity server": "Unable to validate homeserver/identity server", + "Specify a homeserver": "Specify a homeserver", + "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.", + "Sign into your homeserver": "Sign into your homeserver", + "We call the places you where you can host your account ‘homeservers’.": "We call the places you where you can host your account ‘homeservers’.", + "Other homeserver": "Other homeserver", + "Use your preferred Matrix homeserver if you have one, or host your own.": "Use your preferred Matrix homeserver if you have one, or host your own.", + "Learn more": "Learn more", + "About homeservers": "About homeservers", "Sign out and remove encryption keys?": "Sign out and remove encryption keys?", "Clear Storage and Sign Out": "Clear Storage and Sign Out", "Send Logs": "Send Logs", @@ -2274,8 +2292,6 @@ "powered by Matrix": "powered by Matrix", "This homeserver would like to make sure you are not a robot.": "This homeserver would like to make sure you are not a robot.", "Country Dropdown": "Country Dropdown", - "Custom Server Options": "Custom Server Options", - "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.": "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.", "Confirm your identity by entering your account password below.": "Confirm your identity by entering your account password below.", "Password": "Password", "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.", @@ -2289,48 +2305,30 @@ "Code": "Code", "Submit": "Submit", "Start authentication": "Start authentication", - "Unable to validate homeserver/identity server": "Unable to validate homeserver/identity server", - "Enter the location of your Element Matrix Services homeserver. It may use your own domain name or be a subdomain of element.io.": "Enter the location of your Element Matrix Services homeserver. It may use your own domain name or be a subdomain of element.io.", - "Server Name": "Server Name", "Enter password": "Enter password", "Nice, strong password!": "Nice, strong password!", "Password is allowed, but unsafe": "Password is allowed, but unsafe", "Keep going...": "Keep going...", "Enter username": "Enter username", "Enter email address": "Enter email address", - "Doesn't look like a valid email address": "Doesn't look like a valid email address", "Enter phone number": "Enter phone number", - "Doesn't look like a valid phone number": "Doesn't look like a valid phone number", + "That phone number doesn't look quite right, please check and try again": "That phone number doesn't look quite right, please check and try again", "Email": "Email", "Username": "Username", "Phone": "Phone", - "Not sure of your password? Set a new one": "Not sure of your password? Set a new one", + "Forgot password?": "Forgot password?", "Sign in with": "Sign in with", "Sign in": "Sign in", - "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "No identity server is configured so you cannot add an email address in order to reset your password in the future.", - "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?", "Use an email address to recover your account": "Use an email address to recover your account", "Enter email address (required on this homeserver)": "Enter email address (required on this homeserver)", "Other users can invite you to rooms using your contact details": "Other users can invite you to rooms using your contact details", "Enter phone number (required on this homeserver)": "Enter phone number (required on this homeserver)", "Use lowercase letters, numbers, dashes and underscores only": "Use lowercase letters, numbers, dashes and underscores only", - "Email (optional)": "Email (optional)", "Phone (optional)": "Phone (optional)", "Register": "Register", - "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.", - "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.", - "Enter your custom homeserver URL What does this mean?": "Enter your custom homeserver URL What does this mean?", - "Homeserver URL": "Homeserver URL", - "Enter your custom identity server URL What does this mean?": "Enter your custom identity server URL What does this mean?", - "Identity Server URL": "Identity Server URL", - "Other servers": "Other servers", - "Free": "Free", - "Join millions for free on the largest public server": "Join millions for free on the largest public server", - "Premium": "Premium", - "Premium hosting for organisations Learn more": "Premium hosting for organisations Learn more", - "Find other public servers or use a custom server": "Find other public servers or use a custom server", - "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 ", + "Add an email to be able to reset your password.": "Add an email to be able to reset your password.", + "Use email or phone to optionally be discoverable by existing contacts.": "Use email or phone to optionally be discoverable by existing contacts.", + "Use email to optionally be discoverable by existing contacts.": "Use email to optionally be discoverable by existing contacts.", "Sign in with SSO": "Sign in with SSO", "Couldn't load page": "Couldn't load page", "You must register to use this functionality": "You must register to use this functionality", @@ -2486,13 +2484,10 @@ "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 sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.", - "Your Matrix account on %(serverName)s": "Your Matrix account on %(serverName)s", - "Your Matrix account on ": "Your Matrix account on ", - "No identity server is configured: add one in server settings to reset your password.": "No identity server is configured: add one in server settings to reset your password.", - "Sign in instead": "Sign in instead", "New Password": "New Password", "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", "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.", "I have verified my email address": "I have verified my email address", "Your password has been reset.": "Your password has been reset.", @@ -2514,24 +2509,28 @@ "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", "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.", + "There was a problem communicating with the homeserver, please try again later.": "There was a problem communicating with the homeserver, please try again later.", "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.", "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.", "Syncing...": "Syncing...", "Signing In...": "Signing In...", "If you've joined lots of rooms, this might take a while": "If you've joined lots of rooms, this might take a while", - "Create account": "Create account", + "New? Create account": "New? Create account", "Unable to query for supported registration methods.": "Unable to query for supported registration methods.", "Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.", "This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.", + "That username already exists, please try another.": "That username already exists, please try another.", + "Continue with %(ssoButtons)s": "Continue with %(ssoButtons)s", + "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s Or %(usernamePassword)s", + "Already have an account? Sign in here": "Already have an account? Sign in here", "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).", "Continue with previous account": "Continue with previous account", "Log in to your new account.": "Log in to your new account.", "You can now close this window or log in to your new account.": "You can now close this window or log in to your new account.", "Registration Successful": "Registration Successful", - "Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s", - "Create your Matrix account on ": "Create your Matrix account on ", - "Create your account": "Create your account", + "Create account": "Create account", + "Host account on": "Host account on", + "Decide where your account is hosted": "Decide where your account is hosted", "Use Recovery Key or Passphrase": "Use Recovery Key or Passphrase", "Use Recovery Key": "Use Recovery Key", "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.", diff --git a/test/components/structures/auth/Login-test.js b/test/components/structures/auth/Login-test.js index 7ca210ff93f..0631e26cbda 100644 --- a/test/components/structures/auth/Login-test.js +++ b/test/components/structures/auth/Login-test.js @@ -52,7 +52,7 @@ describe('Login', function() { // Set non-empty flows & matrixClient to get past the loading spinner root.setState({ - currentFlow: "m.login.password", + flows: [{ type: "m.login.password" }], }); const form = ReactTestUtils.findRenderedComponentWithType( @@ -61,10 +61,7 @@ describe('Login', function() { ); expect(form).toBeTruthy(); - const changeServerLink = ReactTestUtils.findRenderedDOMComponentWithClass( - root, - 'mx_AuthBody_editServerDetails', - ); + const changeServerLink = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_ServerPicker_change'); expect(changeServerLink).toBeTruthy(); }); @@ -77,7 +74,7 @@ describe('Login', function() { // Set non-empty flows & matrixClient to get past the loading spinner root.setState({ - currentFlow: "m.login.password", + flows: [{ type: "m.login.password" }], }); const form = ReactTestUtils.findRenderedComponentWithType( @@ -86,10 +83,70 @@ describe('Login', function() { ); expect(form).toBeTruthy(); - const changeServerLinks = ReactTestUtils.scryRenderedDOMComponentsWithClass( - root, - 'mx_AuthBody_editServerDetails', - ); + const changeServerLinks = ReactTestUtils.scryRenderedDOMComponentsWithClass(root, 'mx_ServerPicker_change'); expect(changeServerLinks).toHaveLength(0); }); + + it("should show SSO button if that flow is available", () => { + jest.spyOn(SdkConfig, "get").mockReturnValue({ + disable_custom_urls: true, + }); + + const root = render(); + + // Set non-empty flows & matrixClient to get past the loading spinner + root.setState({ + flows: [{ type: "m.login.sso" }], + }); + + const ssoButton = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_SSOButton"); + expect(ssoButton).toBeTruthy(); + }); + + it("should show both SSO button and username+password if both are available", () => { + jest.spyOn(SdkConfig, "get").mockReturnValue({ + disable_custom_urls: true, + }); + + const root = render(); + + // Set non-empty flows & matrixClient to get past the loading spinner + root.setState({ + flows: [{ type: "m.login.password" }, { type: "m.login.sso" }], + }); + + const form = ReactTestUtils.findRenderedComponentWithType(root, sdk.getComponent('auth.PasswordLogin')); + expect(form).toBeTruthy(); + + const ssoButton = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_SSOButton"); + expect(ssoButton).toBeTruthy(); + }); + + it("should show multiple SSO buttons if multiple identity_providers are available", () => { + jest.spyOn(SdkConfig, "get").mockReturnValue({ + disable_custom_urls: true, + }); + + const root = render(); + + // Set non-empty flows & matrixClient to get past the loading spinner + root.setState({ + flows: [{ + type: "m.login.sso", + identity_providers: [{ + id: "a", + name: "Provider 1", + }, { + id: "b", + name: "Provider 2", + }, { + id: "c", + name: "Provider 3", + }], + }], + }); + + const ssoButtons = ReactTestUtils.scryRenderedDOMComponentsWithClass(root, "mx_SSOButton"); + expect(ssoButtons.length).toBe(3); + }); }); diff --git a/test/components/structures/auth/Registration-test.js b/test/components/structures/auth/Registration-test.js index bf26763a794..3e8e887329c 100644 --- a/test/components/structures/auth/Registration-test.js +++ b/test/components/structures/auth/Registration-test.js @@ -48,12 +48,9 @@ describe('Registration', function() { />, parentDiv); } - it('should show server type selector', function() { + it('should show server picker', function() { const root = render(); - const selector = ReactTestUtils.findRenderedComponentWithType( - root, - sdk.getComponent('auth.ServerTypeSelector'), - ); + const selector = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_ServerPicker"); expect(selector).toBeTruthy(); }); @@ -79,4 +76,27 @@ describe('Registration', function() { ); expect(form).toBeTruthy(); }); + + it("should show SSO options if those are available", () => { + jest.spyOn(SdkConfig, "get").mockReturnValue({ + disable_custom_urls: true, + }); + + const root = render(); + + // Set non-empty flows & matrixClient to get past the loading spinner + root.setState({ + flows: [{ + stages: [], + }], + ssoFlow: { + type: "m.login.sso", + }, + matrixClient: {}, + busy: false, + }); + + const ssoButton = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_SSOButton"); + expect(ssoButton).toBeTruthy(); + }); }); diff --git a/test/end-to-end-tests/src/usecases/signup.js b/test/end-to-end-tests/src/usecases/signup.js index ef8a259091d..804cee9599a 100644 --- a/test/end-to-end-tests/src/usecases/signup.js +++ b/test/end-to-end-tests/src/usecases/signup.js @@ -22,24 +22,12 @@ module.exports = async function signup(session, username, password, homeserver) await session.goto(session.url('/#/register')); // change the homeserver by clicking the advanced section if (homeserver) { - const advancedButton = await session.query('.mx_ServerTypeSelector_type_Advanced'); - await advancedButton.click(); + const changeButton = await session.query('.mx_ServerPicker_change'); + await changeButton.click(); - // depending on what HS is configured as the default, the advanced registration - // goes the HS/IS entry directly (for matrix.org) or takes you to the user/pass entry (not matrix.org). - // To work with both, we look for the "Change" link in the user/pass entry but don't fail when we can't find it - // As this link should be visible immediately, and to not slow down the case where it isn't present, - // pick a lower timeout of 5000ms - try { - const changeHsField = await session.query('.mx_AuthBody_editServerDetails', 5000); - if (changeHsField) { - await changeHsField.click(); - } - } catch (err) {} - - const hsInputField = await session.query('#mx_ServerConfig_hsUrl'); + const hsInputField = await session.query('.mx_ServerPickerDialog_otherHomeserver'); await session.replaceInputText(hsInputField, homeserver); - const nextButton = await session.query('.mx_Login_submit'); + const nextButton = await session.query('.mx_ServerPickerDialog_continue'); // accept homeserver await nextButton.click(); } @@ -68,7 +56,7 @@ module.exports = async function signup(session, username, password, homeserver) await registerButton.click(); //confirm dialog saying you cant log back in without e-mail - const continueButton = await session.query('.mx_QuestionDialog button.mx_Dialog_primary'); + const continueButton = await session.query('.mx_RegistrationEmailPromptDialog button.mx_Dialog_primary'); await continueButton.click(); //find the privacy policy checkbox and check it