From d71922ca515cb1020faa59f578ed098ce3247c50 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 23 Feb 2022 09:22:37 -0700 Subject: [PATCH] Support social login & password on soft logout page (#7879) * Code style: Modernize * Make Soft Logout page support Social Sign On Fixes https://github.com/vector-im/element-web/issues/21099 This commit does a few things: * Moves rendering of the flows to functions * Adds a new login view enum for Password + SSO (mirroring logic from registration) * Makes an absolute mess of the resulting diff * Lint & i18n * Remove spurious typing --- src/components/structures/auth/SoftLogout.tsx | 183 +++++++++++------- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 111 insertions(+), 74 deletions(-) diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index 86e6711359..f24230806a 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019-2021 The Matrix.org Foundation C.I.C. +Copyright 2019-2022 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. @@ -16,6 +16,7 @@ limitations under the License. import React from 'react'; import { logger } from "matrix-js-sdk/src/logger"; +import { Optional } from "matrix-events-sdk"; import { _t } from '../../../languageHandler'; import dis from '../../../dispatcher/dispatcher'; @@ -34,18 +35,19 @@ import Spinner from "../../views/elements/Spinner"; import AuthHeader from "../../views/auth/AuthHeader"; import AuthBody from "../../views/auth/AuthBody"; -const LOGIN_VIEW = { - LOADING: 1, - PASSWORD: 2, - CAS: 3, // SSO, but old - SSO: 4, - UNSUPPORTED: 5, -}; +enum LoginView { + Loading, + Password, + CAS, // SSO, but old + SSO, + PasswordWithSocialSignOn, + Unsupported, +} -const FLOWS_TO_VIEWS = { - "m.login.password": LOGIN_VIEW.PASSWORD, - "m.login.cas": LOGIN_VIEW.CAS, - "m.login.sso": LOGIN_VIEW.SSO, +const STATIC_FLOWS_TO_VIEWS = { + "m.login.password": LoginView.Password, + "m.login.cas": LoginView.CAS, + "m.login.sso": LoginView.SSO, }; interface IProps { @@ -60,7 +62,7 @@ interface IProps { } interface IState { - loginView: number; + loginView: LoginView; keyBackupNeeded: boolean; busy: boolean; password: string; @@ -70,11 +72,11 @@ interface IState { @replaceableComponent("structures.auth.SoftLogout") export default class SoftLogout extends React.Component { - constructor(props) { + public constructor(props: IProps) { super(props); this.state = { - loginView: LOGIN_VIEW.LOADING, + loginView: LoginView.Loading, keyBackupNeeded: true, // assume we do while we figure it out (see componentDidMount) busy: false, password: "", @@ -83,7 +85,7 @@ export default class SoftLogout extends React.Component { }; } - componentDidMount(): void { + public componentDidMount(): void { // We've ended up here when we don't need to - navigate to login if (!Lifecycle.isSoftLogout()) { dis.dispatch({ action: "start_login" }); @@ -100,7 +102,7 @@ export default class SoftLogout extends React.Component { } } - onClearAll = () => { + private onClearAll = () => { Modal.createTrackedDialog('Clear Data', 'Soft Logout', ConfirmWipeDeviceDialog, { onFinished: (wipeData) => { if (!wipeData) return; @@ -115,7 +117,7 @@ export default class SoftLogout extends React.Component { const queryParams = this.props.realQueryParams; const hasAllParams = queryParams && queryParams['loginToken']; if (hasAllParams) { - this.setState({ loginView: LOGIN_VIEW.LOADING }); + this.setState({ loginView: LoginView.Loading }); this.trySsoLogin(); return; } @@ -124,21 +126,23 @@ export default class SoftLogout extends React.Component { // care about login flows here, unless it is the single flow we support. const client = MatrixClientPeg.get(); const flows = (await client.loginFlows()).flows; - const loginViews = flows.map(f => FLOWS_TO_VIEWS[f.type]); + const loginViews = flows.map(f => STATIC_FLOWS_TO_VIEWS[f.type]); - const chosenView = loginViews.filter(f => !!f)[0] || LOGIN_VIEW.UNSUPPORTED; + const isSocialSignOn = loginViews.includes(LoginView.Password) && loginViews.includes(LoginView.SSO); + const firstView = loginViews.filter(f => !!f)[0] || LoginView.Unsupported; + const chosenView = isSocialSignOn ? LoginView.PasswordWithSocialSignOn : firstView; this.setState({ flows, loginView: chosenView }); } - onPasswordChange = (ev) => { + private onPasswordChange = (ev) => { this.setState({ password: ev.target.value }); }; - onForgotPassword = () => { + private onForgotPassword = () => { dis.dispatch({ action: 'start_password_recovery' }); }; - onPasswordLogin = async (ev) => { + private onPasswordLogin = async (ev) => { ev.preventDefault(); ev.stopPropagation(); @@ -178,7 +182,7 @@ export default class SoftLogout extends React.Component { }); }; - async trySsoLogin() { + private async trySsoLogin() { this.setState({ busy: true }); const hsUrl = localStorage.getItem(SSO_HOMESERVER_URL_KEY); @@ -194,7 +198,7 @@ export default class SoftLogout extends React.Component { credentials = await sendLoginRequest(hsUrl, isUrl, loginType, loginParams); } catch (e) { logger.error(e); - this.setState({ busy: false, loginView: LOGIN_VIEW.UNSUPPORTED }); + this.setState({ busy: false, loginView: LoginView.Unsupported }); return; } @@ -202,12 +206,62 @@ export default class SoftLogout extends React.Component { if (this.props.onTokenLoginCompleted) this.props.onTokenLoginCompleted(); }).catch((e) => { logger.error(e); - this.setState({ busy: false, loginView: LOGIN_VIEW.UNSUPPORTED }); + this.setState({ busy: false, loginView: LoginView.Unsupported }); }); } + private renderPasswordForm(introText: Optional): JSX.Element { + let error: JSX.Element = null; + if (this.state.errorText) { + error = { this.state.errorText }; + } + + return ( +
+ { introText ?

{ introText }

: null } + { error } + + + { _t("Sign In") } + + + { _t("Forgotten your password?") } + + + ); + } + + private renderSsoForm(introText: Optional): JSX.Element { + const loginType = this.state.loginView === LoginView.CAS ? "cas" : "sso"; + const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType) as ISSOFlow; + + return ( +
+ { introText ?

{ introText }

: null } + flow.type === "m.login.password")} + /> +
+ ); + } + private renderSignInSection() { - if (this.state.loginView === LOGIN_VIEW.LOADING) { + if (this.state.loginView === LoginView.Loading) { return ; } @@ -218,62 +272,45 @@ export default class SoftLogout extends React.Component { "Without them, you won't be able to read all of your secure messages in any session."); } - if (this.state.loginView === LOGIN_VIEW.PASSWORD) { - let error = null; - if (this.state.errorText) { - error = { this.state.errorText }; - } - + if (this.state.loginView === LoginView.Password) { if (!introText) { introText = _t("Enter your password to sign in and regain access to your account."); } // else we already have a message and should use it (key backup warning) - return ( -
-

{ introText }

- { error } - - - { _t("Sign In") } - - - { _t("Forgotten your password?") } - - - ); + return this.renderPasswordForm(introText); } - if (this.state.loginView === LOGIN_VIEW.SSO || this.state.loginView === LOGIN_VIEW.CAS) { + if (this.state.loginView === LoginView.SSO || this.state.loginView === LoginView.CAS) { if (!introText) { 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) as ISSOFlow; + return this.renderSsoForm(introText); + } - return ( -
-

{ introText }

- flow.type === "m.login.password")} - /> -
- ); + if (this.state.loginView === LoginView.PasswordWithSocialSignOn) { + if (!introText) { + introText = _t("Sign in and regain access to your account."); + } + + // We render both forms with no intro/error to ensure the layout looks reasonably + // okay enough. + // + // Note: "mx_AuthBody_centered" text taken from registration page. + return <> +

{ introText }

+ { this.renderSsoForm(null) } +

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

+ { this.renderPasswordForm(null) } + ; } // Default: assume unsupported/error @@ -287,7 +324,7 @@ export default class SoftLogout extends React.Component { ); } - render() { + public render() { return ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3345e96794..5175e2f6dd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3286,9 +3286,9 @@ "Failed to re-authenticate due to a homeserver problem": "Failed to re-authenticate due to a homeserver problem", "Incorrect password": "Incorrect password", "Failed to re-authenticate": "Failed to re-authenticate", + "Forgotten your password?": "Forgotten your password?", "Regain access to your account and recover encryption keys stored in this session. Without them, you won't be able to read all of your secure messages in any session.": "Regain access to your account and recover encryption keys stored in this session. Without them, you won't be able to read all of your secure messages in any session.", "Enter your password to sign in and regain access to your account.": "Enter your password to sign in and regain access to your account.", - "Forgotten your password?": "Forgotten your password?", "Sign in and regain access to your account.": "Sign in and regain access to your account.", "You cannot sign in to your account. Please contact your homeserver admin for more information.": "You cannot sign in to your account. Please contact your homeserver admin for more information.", "You're signed out": "You're signed out",