Merge remote-tracking branch 'origin/develop' into dbkr/hold_ui

This commit is contained in:
David Baker 2020-12-03 17:57:22 +00:00
commit 6916b5d7c6
92 changed files with 3118 additions and 1795 deletions

View file

@ -34,7 +34,6 @@ import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
import { MatrixClientPeg, IMatrixClientCreds } from "../../MatrixClientPeg";
import PlatformPeg from "../../PlatformPeg";
import SdkConfig from "../../SdkConfig";
import * as RoomListSorter from "../../RoomListSorter";
import dis from "../../dispatcher/dispatcher";
import Notifier from '../../Notifier';
@ -48,7 +47,6 @@ import * as Lifecycle from '../../Lifecycle';
// LifecycleStore is not used but does listen to and dispatch actions
import '../../stores/LifecycleStore';
import PageTypes from '../../PageTypes';
import { getHomePageUrl } from '../../utils/pages';
import createRoom from "../../createRoom";
import {_t, _td, getCurrentLanguage} from '../../languageHandler';
@ -591,7 +589,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
MatrixClientPeg.get().leave(payload.room_id).then(() => {
modal.close();
if (this.state.currentRoomId === payload.room_id) {
dis.dispatch({action: 'view_next_room'});
dis.dispatch({action: 'view_home_page'});
}
}, (err) => {
modal.close();
@ -620,9 +618,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
break;
}
case 'view_next_room':
this.viewNextRoom(1);
break;
case Action.ViewUserSettings: {
const tabPayload = payload as OpenToTabPayload;
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
@ -802,35 +797,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.notifyNewScreen('register');
}
// TODO: Move to RoomViewStore
private viewNextRoom(roomIndexDelta: number) {
const allRooms = RoomListSorter.mostRecentActivityFirst(
MatrixClientPeg.get().getRooms(),
);
// If there are 0 rooms or 1 room, view the home page because otherwise
// if there are 0, we end up trying to index into an empty array, and
// if there is 1, we end up viewing the same room.
if (allRooms.length < 2) {
dis.dispatch({
action: 'view_home_page',
});
return;
}
let roomIndex = -1;
for (let i = 0; i < allRooms.length; ++i) {
if (allRooms[i].roomId === this.state.currentRoomId) {
roomIndex = i;
break;
}
}
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
if (roomIndex < 0) roomIndex = allRooms.length - 1;
dis.dispatch({
action: 'view_room',
room_id: allRooms[roomIndex].roomId,
});
}
// switch view to the given room
//
// @param {Object} roomInfo Object containing data about the room to be joined
@ -1097,9 +1063,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private forgetRoom(roomId: string) {
MatrixClientPeg.get().forget(roomId).then(() => {
// Switch to another room view if we're currently viewing the historical room
// Switch to home page if we're currently viewing the forgotten room
if (this.state.currentRoomId === roomId) {
dis.dispatch({ action: "view_next_room" });
dis.dispatch({ action: "view_home_page" });
}
}).catch((err) => {
const errCode = err.errcode || _td("unknown error code");
@ -1233,12 +1199,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} else {
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'view_welcome_page'});
} else if (getHomePageUrl(this.props.config)) {
dis.dispatch({action: 'view_home_page'});
} else {
this.firstSyncPromise.promise.then(() => {
dis.dispatch({action: 'view_next_room'});
});
dis.dispatch({action: 'view_home_page'});
}
}
}
@ -2009,6 +1971,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
onLoginClick={this.onLoginClick}
onServerConfigChange={this.onServerConfigChange}
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
fragmentAfterLogin={fragmentAfterLogin}
{...this.getServerProperties()}
/>
);

View file

@ -1332,7 +1332,7 @@ export default class RoomView extends React.Component<IProps, IState> {
rejecting: true,
});
this.context.leave(this.state.roomId).then(() => {
dis.dispatch({ action: 'view_next_room' });
dis.dispatch({ action: 'view_home_page' });
this.setState({
rejecting: false,
});
@ -1366,7 +1366,7 @@ export default class RoomView extends React.Component<IProps, IState> {
await this.context.setIgnoredUsers(ignoredUsers);
await this.context.leave(this.state.roomId);
dis.dispatch({ action: 'view_next_room' });
dis.dispatch({ action: 'view_home_page' });
this.setState({
rejecting: false,
});

View file

@ -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 <ServerConfig
serverConfig={this.props.serverConfig}
onServerConfigChange={this.props.onServerConfigChange}
delayTimeMs={0}
showIdentityServerIfRequiredByHomeserver={true}
onAfterSubmit={this.onServerDetailsNextPhaseClick}
submitText={_t("Next")}
submitClass="mx_Login_submit"
/>;
}
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 />', {}, {
'underlinedServerName': () => {
return <TextWithTooltip
class="mx_Login_underlinedServerName"
tooltip={this.props.serverConfig.hsUrl}
>
{this.props.serverConfig.hsName}
</TextWithTooltip>;
},
});
}
// If custom URLs are allowed, wire up the server details edit link.
let editLink = null;
if (!SdkConfig.get()['disable_custom_urls']) {
editLink = <a className="mx_AuthBody_editServerDetails"
href="#" onClick={this.onEditServerDetailsClick}
>
{_t('Change')}
</a>;
}
if (!this.props.serverConfig.isUrl && this.state.serverRequiresIdServer) {
return <div>
<h3>
{yourMatrixAccountText}
{editLink}
</h3>
{_t(
"No identity server is configured: " +
"add one in server settings to reset your password.",
)}
<a className="mx_AuthBody_changeFlow" onClick={this.onLoginClick} href="#">
{_t('Sign in instead')}
</a>
</div>;
}
return <div>
{errorText}
{serverDeadSection}
<h3>
{yourMatrixAccountText}
{editLink}
</h3>
<ServerPicker
serverConfig={this.props.serverConfig}
onServerConfigChange={this.props.onServerConfigChange}
/>
<form onSubmit={this.onSubmitForm}>
<div className="mx_AuthBody_fieldRow">
<Field
@ -319,6 +236,7 @@ export default class ForgotPassword extends React.Component {
onChange={this.onInputChanged.bind(this, "password")}
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_focus")}
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_blur")}
autoComplete="new-password"
/>
<Field
name="reset_password_confirm"
@ -328,6 +246,7 @@ export default class ForgotPassword extends React.Component {
onChange={this.onInputChanged.bind(this, "password2")}
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_focus")}
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_blur")}
autoComplete="new-password"
/>
</div>
<span>{_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;

View file

@ -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<IProps, IState> {
export default class LoginComponent extends React.PureComponent<IProps, IState> {
private unmounted = false;
private loginLogic: Login;
private readonly stepRendererMap: Record<string, () => ReactNode>;
constructor(props) {
@ -127,11 +114,13 @@ export default class LoginComponent extends React.Component<IProps, IState> {
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<IProps, IState> {
};
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<IProps, IState> {
}
};
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<IProps, IState> {
this.setState({
busy: true,
currentFlow: null, // reset flow
loginIncorrect: false,
});
@ -421,38 +397,22 @@ export default class LoginComponent extends React.Component<IProps, IState> {
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<IProps, IState> {
});
}
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<IProps, IState> {
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<IProps, IState> {
return errorText;
}
private renderServerComponent() {
if (SdkConfig.get()['disable_custom_urls']) {
return null;
}
renderLoginComponentForFlows() {
if (!this.state.flows) return null;
if (PHASES_ENABLED && this.state.phase !== Phase.ServerDetails) {
return null;
}
// this is the ideal order we want to show the flows in
const order = [
"m.login.password",
"m.login.sso",
];
const serverDetailsProps: ComponentProps<typeof ServerConfig> = {};
if (PHASES_ENABLED) {
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
serverDetailsProps.submitText = _t("Next");
serverDetailsProps.submitClass = "mx_Login_submit";
}
return <ServerConfig
serverConfig={this.props.serverConfig}
onServerConfigChange={this.props.onServerConfigChange}
delayTimeMs={250}
{...serverDetailsProps}
/>;
}
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;
const flows = order.map(type => this.state.flows.find(flow => flow.type === type)).filter(Boolean);
return <React.Fragment>
{ flows.map(flow => {
const stepRenderer = this.stepRendererMap[flow.type];
return <React.Fragment key={flow.type}>{ stepRenderer() }</React.Fragment>
}) }
</React.Fragment>
}
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 (
<PasswordLogin
onSubmit={this.onPasswordLogin}
onEditServerDetailsClick={onEditServerDetailsClick}
username={this.state.username}
phoneCountry={this.state.phoneCountry}
phoneNumber={this.state.phoneNumber}
@ -598,31 +521,16 @@ export default class LoginComponent extends React.Component<IProps, IState> {
};
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.
return (
<div>
<SignInToText serverConfig={this.props.serverConfig}
onEditServerDetailsClick={onEditServerDetailsClick} />
const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType) as ISSOFlow;
<SSOButton
className="mx_Login_sso_link mx_Login_submit"
matrixClient={this.loginLogic.createTemporaryClient()}
loginType={loginType}
fragmentAfterLogin={this.props.fragmentAfterLogin}
/>
</div>
return (
<SSOButtons
matrixClient={this.loginLogic.createTemporaryClient()}
flow={flow}
loginType={loginType}
fragmentAfterLogin={this.props.fragmentAfterLogin}
primary={!this.state.flows.find(flow => flow.type === "m.login.password")}
/>
);
};
@ -670,9 +578,11 @@ export default class LoginComponent extends React.Component<IProps, IState> {
</div>;
} else if (SettingsStore.getValue(UIFeature.Registration)) {
footer = (
<a className="mx_AuthBody_changeFlow" onClick={this.onTryRegisterClick} href="#">
{ _t('Create account') }
</a>
<span className="mx_AuthBody_changeFlow">
{_t("New? <a>Create account</a>", {}, {
a: sub => <a onClick={this.onTryRegisterClick} href="#">{ sub }</a>,
})}
</span>
);
}
@ -686,8 +596,11 @@ export default class LoginComponent extends React.Component<IProps, IState> {
</h2>
{ errorTextSection }
{ serverDeadSection }
{ this.renderServerComponent() }
{ this.renderLoginComponentForStep() }
<ServerPicker
serverConfig={this.props.serverConfig}
onServerConfigChange={this.props.onServerConfigChange}
/>
{ this.renderLoginComponentForFlows() }
{ footer }
</AuthBody>
</AuthPage>

View file

@ -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<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
// 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<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
}
};
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 <div>
<ServerTypeSelector
selected={this.state.serverType}
onChange={this.onServerTypeChange}
/>
</div>;
}
const serverDetailsProps: ComponentProps<typeof ServerConfig> = {};
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 = <ModularServerConfig
serverConfig={this.props.serverConfig}
onServerConfigChange={this.props.onServerConfigChange}
delayTimeMs={250}
{...serverDetailsProps}
/>;
break;
case ServerType.ADVANCED:
serverDetails = <ServerConfig
serverConfig={this.props.serverConfig}
onServerConfigChange={this.props.onServerConfigChange}
delayTimeMs={250}
showIdentityServerIfRequiredByHomeserver={true}
{...serverDetailsProps}
/>;
break;
}
return <div>
<ServerTypeSelector
selected={this.state.serverType}
onChange={this.onServerTypeChange}
/>
{serverDetails}
</div>;
}
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<IProps, IState> {
<Spinner />
</div>;
} else if (this.state.flows.length) {
return <RegistrationForm
defaultUsername={this.state.formVals.username}
defaultEmail={this.state.formVals.email}
defaultPhoneCountry={this.state.formVals.phoneCountry}
defaultPhoneNumber={this.state.formVals.phoneNumber}
defaultPassword={this.state.formVals.password}
onRegisterClick={this.onFormSubmit}
flows={this.state.flows}
serverConfig={this.props.serverConfig}
canSubmit={!this.state.serverErrorIsFatal}
serverRequiresIdServer={this.state.serverRequiresIdServer}
/>;
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 = <h3 className="mx_AuthBody_centered">
{ _t("Continue with %(ssoButtons)s", { ssoButtons: "" }).trim() }
</h3>;
}
// i18n: ssoButtons & usernamePassword are placeholders to help translators understand context
ssoSection = <React.Fragment>
{ continueWithSection }
<SSOButtons
matrixClient={this.loginLogic.createTemporaryClient()}
flow={this.state.ssoFlow}
loginType={this.state.ssoFlow.type === "m.login.sso" ? "sso" : "cas"}
fragmentAfterLogin={this.props.fragmentAfterLogin}
/>
<h3 className="mx_AuthBody_centered">
{ _t("%(ssoButtons)s Or %(usernamePassword)s", { ssoButtons: "", usernamePassword: ""}).trim() }
</h3>
</React.Fragment>;
}
return <React.Fragment>
{ ssoSection }
<RegistrationForm
defaultUsername={this.state.formVals.username}
defaultEmail={this.state.formVals.email}
defaultPhoneCountry={this.state.formVals.phoneCountry}
defaultPhoneNumber={this.state.formVals.phoneNumber}
defaultPassword={this.state.formVals.password}
onRegisterClick={this.onFormSubmit}
flows={this.state.flows}
serverConfig={this.props.serverConfig}
canSubmit={!this.state.serverErrorIsFatal}
/>
</React.Fragment>;
}
}
@ -650,13 +530,15 @@ export default class Registration extends React.Component<IProps, IState> {
);
}
const signIn = <a className="mx_AuthBody_changeFlow" onClick={this.onLoginClick} href="#">
{ _t('Sign in instead') }
</a>;
const signIn = <span className="mx_AuthBody_changeFlow">
{_t("Already have an account? <a>Sign in here</a>", {}, {
a: sub => <a onClick={this.onLoginClick} href="#">{ sub }</a>,
})}
</span>;
// 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 = <a className="mx_AuthBody_changeFlow" onClick={this.onGoToFormClicked} href="#">
{ _t('Go back') }
</a>;
@ -702,48 +584,16 @@ export default class Registration extends React.Component<IProps, IState> {
{ regDoneText }
</div>;
} 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 />', {}, {
'underlinedServerName': () => {
return <TextWithTooltip
class="mx_Login_underlinedServerName"
tooltip={this.props.serverConfig.hsUrl}
>
{this.props.serverConfig.hsName}
</TextWithTooltip>;
},
});
}
// 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 = (
<a className="mx_AuthBody_editServerDetails" href="#" onClick={this.onEditServerDetailsClick}>
{_t('Change')}
</a>
);
}
body = <div>
<h2>{ _t('Create your account') }</h2>
<h2>{ _t('Create account') }</h2>
{ errorText }
{ serverDeadSection }
{ this.renderServerComponent() }
{ this.state.phase !== Phase.ServerDetails && <h3>
{yourMatrixAccountText}
{editLink}
</h3> }
<ServerPicker
title={_t("Host account on")}
dialogTitle={_t("Decide where your account is hosted")}
serverConfig={this.props.serverConfig}
onServerConfigChange={this.state.doingUIAuth ? undefined : this.props.onServerConfigChange}
/>
{ this.renderRegisterComponent() }
{ goBack }
{ signIn }

View file

@ -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 (
<div>
<p>{introText}</p>
<SSOButton
<SSOButtons
matrixClient={MatrixClientPeg.get()}
loginType={this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso"}
flow={flow}
loginType={loginType}
fragmentAfterLogin={this.props.fragmentAfterLogin}
primary={!this.state.flows.find(flow => flow.type === "m.login.password")}
/>
</div>
);