diff --git a/res/css/views/auth/_InteractiveAuthEntryComponents.scss b/res/css/views/auth/_InteractiveAuthEntryComponents.scss
index 85007aeecb..05cddf2c48 100644
--- a/res/css/views/auth/_InteractiveAuthEntryComponents.scss
+++ b/res/css/views/auth/_InteractiveAuthEntryComponents.scss
@@ -60,3 +60,14 @@ limitations under the License.
.mx_InteractiveAuthEntryComponents_passwordSection {
width: 300px;
}
+
+.mx_InteractiveAuthEntryComponents_sso_buttons {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ margin-top: 20px;
+
+ .mx_AccessibleButton {
+ margin-left: 5px;
+ }
+}
diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss
index b87071745d..0c48a07ccf 100644
--- a/res/css/views/elements/_AccessibleButton.scss
+++ b/res/css/views/elements/_AccessibleButton.scss
@@ -36,6 +36,13 @@ limitations under the License.
font-weight: 600;
}
+.mx_AccessibleButton_kind_primary_outline {
+ color: $button-primary-bg-color;
+ background-color: $button-secondary-bg-color;
+ border: 1px solid $button-primary-bg-color;
+ font-weight: 600;
+}
+
.mx_AccessibleButton_kind_secondary {
color: $accent-color;
font-weight: 600;
@@ -60,7 +67,15 @@ limitations under the License.
background-color: $button-danger-bg-color;
}
-.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled {
+.mx_AccessibleButton_kind_danger_outline {
+ color: $button-danger-bg-color;
+ background-color: $button-secondary-bg-color;
+ border: 1px solid $button-danger-bg-color;
+}
+
+.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled,
+mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled
+{
color: $button-danger-disabled-fg-color;
background-color: $button-danger-disabled-bg-color;
}
diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js
index f4adb5751f..2492bf79a0 100644
--- a/src/components/structures/InteractiveAuth.js
+++ b/src/components/structures/InteractiveAuth.js
@@ -1,6 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd.
-Copyright 2019 The Matrix.org Foundation C.I.C.
+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.
@@ -24,6 +24,8 @@ import getEntryComponentForLoginType from '../views/auth/InteractiveAuthEntryCom
import * as sdk from '../../index';
+export const ERROR_USER_CANCELLED = new Error("User cancelled auth session");
+
export default createReactClass({
displayName: 'InteractiveAuth',
@@ -47,7 +49,7 @@ export default createReactClass({
// @param {bool} status True if the operation requiring
// auth was completed sucessfully, false if canceled.
// @param {object} result The result of the authenticated call
- // if successful, otherwise the error object
+ // if successful, otherwise the error object.
// @param {object} extra Additional information about the UI Auth
// process:
// * emailSid {string} If email auth was performed, the sid of
@@ -75,6 +77,15 @@ export default createReactClass({
// is managed by some other party and should not be managed by
// the component itself.
continueIsManaged: PropTypes.bool,
+
+ // Called when the stage changes, or the stage's phase changes. First
+ // argument is the stage, second is the phase. Some stages do not have
+ // phases and will be counted as 0 (numeric).
+ onStagePhaseChange: PropTypes.func,
+
+ // continueText and continueKind are passed straight through to the AuthEntryComponent.
+ continueText: PropTypes.string,
+ continueKind: PropTypes.string,
},
getInitialState: function() {
@@ -204,6 +215,16 @@ export default createReactClass({
this._authLogic.submitAuthDict(authData);
},
+ _onPhaseChange: function(newPhase) {
+ if (this.props.onStagePhaseChange) {
+ this.props.onStagePhaseChange(this.state.authStage, newPhase || 0);
+ }
+ },
+
+ _onStageCancel: function() {
+ this.props.onAuthFinished(false, ERROR_USER_CANCELLED);
+ },
+
_renderCurrentStage: function() {
const stage = this.state.authStage;
if (!stage) {
@@ -232,6 +253,10 @@ export default createReactClass({
fail={this._onAuthStageFailed}
setEmailSid={this._setEmailSid}
showContinue={!this.props.continueIsManaged}
+ onPhaseChange={this._onPhaseChange}
+ continueText={this.props.continueText}
+ continueKind={this.props.continueKind}
+ onCancel={this._onStageCancel}
/>
);
},
diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js
index 26a293724a..d262a5525d 100644
--- a/src/components/views/auth/InteractiveAuthEntryComponents.js
+++ b/src/components/views/auth/InteractiveAuthEntryComponents.js
@@ -25,6 +25,7 @@ import classnames from 'classnames';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
+import AccessibleButton from "../elements/AccessibleButton";
/* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed
@@ -59,11 +60,21 @@ import SettingsStore from "../../../settings/SettingsStore";
* session to be failed and the process to go back to the start.
* setEmailSid: m.login.email.identity only: a function to be called with the
* email sid after a token is requested.
+ * onPhaseChange: A function which is called when the stage's phase changes. If
+ * the stage has no phases, call this with DEFAULT_PHASE. Takes
+ * one argument, the phase, and is always defined/required.
+ * continueText: For stages which have a continue button, the text to use.
+ * continueKind: For stages which have a continue button, the style of button to
+ * use. For example, 'danger' or 'primary'.
+ * onCancel A function with no arguments which is called by the stage if the
+ * user knowingly cancelled/dismissed the authentication attempt.
*
* Each component may also provide the following functions (beyond the standard React ones):
* focus: set the input focus appropriately in the form.
*/
+export const DEFAULT_PHASE = 0;
+
export const PasswordAuthEntry = createReactClass({
displayName: 'PasswordAuthEntry',
@@ -78,6 +89,11 @@ export const PasswordAuthEntry = createReactClass({
// is the auth logic currently waiting for something to
// happen?
busy: PropTypes.bool,
+ onPhaseChange: PropTypes.func.isRequired,
+ },
+
+ componentDidMount: function() {
+ this.props.onPhaseChange(DEFAULT_PHASE);
},
getInitialState: function() {
@@ -175,6 +191,11 @@ export const RecaptchaAuthEntry = createReactClass({
stageParams: PropTypes.object.isRequired,
errorText: PropTypes.string,
busy: PropTypes.bool,
+ onPhaseChange: PropTypes.func.isRequired,
+ },
+
+ componentDidMount: function() {
+ this.props.onPhaseChange(DEFAULT_PHASE);
},
_onCaptchaResponse: function(response) {
@@ -236,6 +257,11 @@ export const TermsAuthEntry = createReactClass({
errorText: PropTypes.string,
busy: PropTypes.bool,
showContinue: PropTypes.bool,
+ onPhaseChange: PropTypes.func.isRequired,
+ },
+
+ componentDidMount: function() {
+ this.props.onPhaseChange(DEFAULT_PHASE);
},
componentWillMount: function() {
@@ -378,6 +404,11 @@ export const EmailIdentityAuthEntry = createReactClass({
stageState: PropTypes.object.isRequired,
fail: PropTypes.func.isRequired,
setEmailSid: PropTypes.func.isRequired,
+ onPhaseChange: PropTypes.func.isRequired,
+ },
+
+ componentDidMount: function() {
+ this.props.onPhaseChange(DEFAULT_PHASE);
},
getInitialState: function() {
@@ -420,6 +451,11 @@ export const MsisdnAuthEntry = createReactClass({
clientSecret: PropTypes.func,
submitAuthDict: PropTypes.func.isRequired,
matrixClient: PropTypes.object,
+ onPhaseChange: PropTypes.func.isRequired,
+ },
+
+ componentDidMount: function() {
+ this.props.onPhaseChange(DEFAULT_PHASE);
},
getInitialState: function() {
@@ -571,13 +607,17 @@ export class SSOAuthEntry extends React.Component {
loginType: PropTypes.string.isRequired,
submitAuthDict: PropTypes.func.isRequired,
errorText: PropTypes.string,
+ onPhaseChange: PropTypes.func.isRequired,
+ continueText: PropTypes.string,
+ continueKind: PropTypes.string,
+ onCancel: PropTypes.func,
};
static LOGIN_TYPE = "m.login.sso";
static UNSTABLE_LOGIN_TYPE = "org.matrix.login.sso";
- static STAGE_PREAUTH = 1; // button to start SSO
- static STAGE_POSTAUTH = 2; // button to confirm SSO completed
+ static PHASE_PREAUTH = 1; // button to start SSO
+ static PHASE_POSTAUTH = 2; // button to confirm SSO completed
constructor(props) {
super(props);
@@ -589,39 +629,56 @@ export class SSOAuthEntry extends React.Component {
this.props.loginType,
this.props.authSessionId,
),
- stage: SSOAuthEntry.STAGE_PREAUTH,
+ phase: SSOAuthEntry.PHASE_PREAUTH,
};
}
- onStartAuthClick = (e) => {
- e.preventDefault();
- e.stopPropagation();
+ componentDidMount(): void {
+ this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH);
+ }
+ onStartAuthClick = () => {
// Note: We don't use PlatformPeg's startSsoAuth functions because we almost
// certainly will need to open the thing in a new tab to avoid loosing application
// context.
- window.open(e.target.href, '_blank');
- this.setState({stage: SSOAuthEntry.STAGE_POSTAUTH});
+ window.open(this.state.ssoUrl, '_blank');
+ this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH});
+ this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH);
};
- onConfirmClick = (e) => {
- e.preventDefault();
- e.stopPropagation();
-
+ onConfirmClick = () => {
this.props.submitAuthDict({});
};
render () {
- if (this.state.stage === SSOAuthEntry.STAGE_PREAUTH) {
- return
- {_t("Single Sign On")}
- ;
+ let continueButton = null;
+ const cancelButton = (
+