Split out InterActiveAuthDialog
Into a component that does Interactive Auth and a dialog that wraps it, so we can do interactive auth not necessarily in a dialog. As a side effect: * Put the buttons for each auth stage in the stage itself. Some stages don't have submit buttons (and it's very possible other stages may have other buttons entirely, like 'resend') so it makes more sense for the buttons to live in the stage components themselves. Plus it saves the slightly evil calling-functions-on-react-children thing we were doing (and indeed extending that to show the submit button at all). * Give all BaseDialogs a cross in the top right to cancel. They were all dismissable by clicking outside or pressing esc, so this adds a more visually obvious way of dismissing them. Plus, it means our InteractiveAuthDialog can have a way of canceling the whole operation separate from buttons for the individual stages.
This commit is contained in:
parent
a21e71f59d
commit
79d9deb339
5 changed files with 201 additions and 165 deletions
|
@ -31,6 +31,8 @@ import structures$CreateRoom from './components/structures/CreateRoom';
|
|||
structures$CreateRoom && (module.exports.components['structures.CreateRoom'] = structures$CreateRoom);
|
||||
import structures$FilePanel from './components/structures/FilePanel';
|
||||
structures$FilePanel && (module.exports.components['structures.FilePanel'] = structures$FilePanel);
|
||||
import structures$InteractiveAuth from './components/structures/InteractiveAuth';
|
||||
structures$InteractiveAuth && (module.exports.components['structures.InteractiveAuth'] = structures$InteractiveAuth);
|
||||
import structures$LoggedInView from './components/structures/LoggedInView';
|
||||
structures$LoggedInView && (module.exports.components['structures.LoggedInView'] = structures$LoggedInView);
|
||||
import structures$MatrixChat from './components/structures/MatrixChat';
|
||||
|
|
153
src/components/structures/InteractiveAuth.js
Normal file
153
src/components/structures/InteractiveAuth.js
Normal file
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 Matrix from 'matrix-js-sdk';
|
||||
const InteractiveAuth = Matrix.InteractiveAuth;
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import sdk from '../../index';
|
||||
|
||||
import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'InteractiveAuth',
|
||||
|
||||
propTypes: {
|
||||
// response from initial request. If not supplied, will do a request on
|
||||
// mount.
|
||||
authData: React.PropTypes.shape({
|
||||
flows: React.PropTypes.array,
|
||||
params: React.PropTypes.object,
|
||||
session: React.PropTypes.string,
|
||||
}),
|
||||
|
||||
// callback
|
||||
makeRequest: React.PropTypes.func.isRequired,
|
||||
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
authStage: null,
|
||||
busy: false,
|
||||
errorText: null,
|
||||
stageErrorText: null,
|
||||
submitButtonEnabled: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
this._authLogic = new InteractiveAuth({
|
||||
authData: this.props.authData,
|
||||
doRequest: this._requestCallback,
|
||||
startAuthStage: this._startAuthStage,
|
||||
});
|
||||
|
||||
this._authLogic.attemptAuth().then((result) => {
|
||||
this.props.onFinished(true, result);
|
||||
}).catch((error) => {
|
||||
console.error("Error during user-interactive auth:", error);
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = error.message || error.toString();
|
||||
this.setState({
|
||||
errorText: msg
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
},
|
||||
|
||||
_startAuthStage: function(stageType, error) {
|
||||
this.setState({
|
||||
authStage: stageType,
|
||||
errorText: error ? error.error : null,
|
||||
}, this._setFocus);
|
||||
},
|
||||
|
||||
_requestCallback: function(auth) {
|
||||
this.setState({
|
||||
busy: true,
|
||||
errorText: null,
|
||||
stageErrorText: null,
|
||||
});
|
||||
return this.props.makeRequest(auth).finally(() => {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_setFocus: function() {
|
||||
if (this.refs.stageComponent && this.refs.stageComponent.focus) {
|
||||
this.refs.stageComponent.focus();
|
||||
}
|
||||
},
|
||||
|
||||
_onCancel: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
_submitAuthDict: function(authData) {
|
||||
this._authLogic.submitAuthDict(authData);
|
||||
},
|
||||
|
||||
_renderCurrentStage: function() {
|
||||
const stage = this.state.authStage;
|
||||
var StageComponent = getEntryComponentForLoginType(stage);
|
||||
return (
|
||||
<StageComponent ref="stageComponent"
|
||||
loginType={stage}
|
||||
authSessionId={this._authLogic.getSessionId()}
|
||||
stageParams={this._authLogic.getStageParams(stage)}
|
||||
submitAuthDict={this._submitAuthDict}
|
||||
errorText={this.state.stageErrorText}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
|
||||
let error = null;
|
||||
if (this.state.errorText) {
|
||||
error = (
|
||||
<div className="error">
|
||||
{this.state.errorText}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{this._renderCurrentStage()}
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
|
||||
import * as KeyCode from '../../../KeyCode';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
/**
|
||||
* Basic container for modal dialogs.
|
||||
|
@ -59,9 +60,23 @@ export default React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_onCancelClick: function(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div onKeyDown={this._onKeyDown} className={this.props.className}>
|
||||
<AccessibleButton onClick={this._onCancelClick}
|
||||
className="mx_Dialog_cancelButton"
|
||||
>
|
||||
<img
|
||||
src="img/cancel.svg" width="18" height="18"
|
||||
alt="Cancel" title="Cancel"
|
||||
/>
|
||||
</AccessibleButton>
|
||||
<div className='mx_Dialog_title'>
|
||||
{ this.props.title }
|
||||
</div>
|
||||
|
|
|
@ -15,13 +15,12 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
const InteractiveAuth = Matrix.InteractiveAuth;
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import sdk from '../../../index';
|
||||
|
||||
import {getEntryComponentForLoginType} from '../login/InteractiveAuthEntryComponents';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'InteractiveAuthDialog',
|
||||
|
@ -41,168 +40,33 @@ export default React.createClass({
|
|||
onFinished: React.PropTypes.func.isRequired,
|
||||
|
||||
title: React.PropTypes.string,
|
||||
submitButtonLabel: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Authentication",
|
||||
submitButtonLabel: "Submit",
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
authStage: null,
|
||||
busy: false,
|
||||
errorText: null,
|
||||
stageErrorText: null,
|
||||
submitButtonEnabled: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
this._authLogic = new InteractiveAuth({
|
||||
authData: this.props.authData,
|
||||
doRequest: this._requestCallback,
|
||||
startAuthStage: this._startAuthStage,
|
||||
});
|
||||
|
||||
this._authLogic.attemptAuth().then((result) => {
|
||||
this.props.onFinished(true, result);
|
||||
}).catch((error) => {
|
||||
console.error("Error during user-interactive auth:", error);
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = error.message || error.toString();
|
||||
this.setState({
|
||||
errorText: msg
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
},
|
||||
|
||||
_startAuthStage: function(stageType, error) {
|
||||
this.setState({
|
||||
authStage: stageType,
|
||||
errorText: error ? error.error : null,
|
||||
}, this._setFocus);
|
||||
},
|
||||
|
||||
_requestCallback: function(auth) {
|
||||
this.setState({
|
||||
busy: true,
|
||||
errorText: null,
|
||||
stageErrorText: null,
|
||||
});
|
||||
return this.props.makeRequest(auth).finally(() => {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_onEnterPressed: function(e) {
|
||||
if (this.state.submitButtonEnabled && !this.state.busy) {
|
||||
this._onSubmit();
|
||||
}
|
||||
},
|
||||
|
||||
_onSubmit: function() {
|
||||
if (this.refs.stageComponent && this.refs.stageComponent.onSubmitClick) {
|
||||
this.refs.stageComponent.onSubmitClick();
|
||||
}
|
||||
},
|
||||
|
||||
_setFocus: function() {
|
||||
if (this.refs.stageComponent && this.refs.stageComponent.focus) {
|
||||
this.refs.stageComponent.focus();
|
||||
}
|
||||
},
|
||||
|
||||
_onCancel: function() {
|
||||
_onCancelClick: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
_setSubmitButtonEnabled: function(enabled) {
|
||||
this.setState({
|
||||
submitButtonEnabled: enabled,
|
||||
});
|
||||
},
|
||||
|
||||
_submitAuthDict: function(authData) {
|
||||
this._authLogic.submitAuthDict(authData);
|
||||
},
|
||||
|
||||
_renderCurrentStage: function() {
|
||||
const stage = this.state.authStage;
|
||||
var StageComponent = getEntryComponentForLoginType(stage);
|
||||
return (
|
||||
<StageComponent ref="stageComponent"
|
||||
loginType={stage}
|
||||
authSessionId={this._authLogic.getSessionId()}
|
||||
stageParams={this._authLogic.getStageParams(stage)}
|
||||
submitAuthDict={this._submitAuthDict}
|
||||
setSubmitButtonEnabled={this._setSubmitButtonEnabled}
|
||||
errorText={this.state.stageErrorText}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth");
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
let error = null;
|
||||
if (this.state.errorText) {
|
||||
error = (
|
||||
<div className="error">
|
||||
{this.state.errorText}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const submitLabel = this.state.busy ? <Loader /> : this.props.submitButtonLabel;
|
||||
const submitEnabled = this.state.submitButtonEnabled && !this.state.busy;
|
||||
|
||||
const submitButton = (
|
||||
<button className="mx_Dialog_primary"
|
||||
onClick={this._onSubmit}
|
||||
disabled={!submitEnabled}
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
);
|
||||
|
||||
const cancelButton = (
|
||||
<button onClick={this._onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_InteractiveAuthDialog"
|
||||
onEnterPressed={this._onEnterPressed}
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>This operation requires additional authentication.</p>
|
||||
{this._renderCurrentStage()}
|
||||
{error}
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
{submitButton}
|
||||
{cancelButton}
|
||||
<div>
|
||||
<InteractiveAuth ref={this._collectInteractiveAuth}
|
||||
authData={this.props.authData}
|
||||
makeRequest={this.props.makeRequest}
|
||||
onFinished={this.props.onFinished}
|
||||
/>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
|
|
@ -32,7 +32,6 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
|||
* stageParams: params from the server for the stage being attempted
|
||||
* errorText: error message from a previous attempt to authenticate
|
||||
* submitAuthDict: a function which will be called with the new auth dict
|
||||
* setSubmitButtonEnabled: a function which will enable/disable the 'submit' button
|
||||
*
|
||||
* Each component may also provide the following functions (beyond the standard React ones):
|
||||
* onSubmitClick: handle a 'submit' button click
|
||||
|
@ -48,12 +47,13 @@ export const PasswordAuthEntry = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
submitAuthDict: React.PropTypes.func.isRequired,
|
||||
setSubmitButtonEnabled: React.PropTypes.func.isRequired,
|
||||
errorText: React.PropTypes.string,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.props.setSubmitButtonEnabled(false);
|
||||
getInitialState: function() {
|
||||
return {
|
||||
enableSubmit: false,
|
||||
};
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
|
@ -62,7 +62,7 @@ export const PasswordAuthEntry = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onSubmitClick: function() {
|
||||
_onSubmit: function() {
|
||||
this.props.submitAuthDict({
|
||||
type: PasswordAuthEntry.LOGIN_TYPE,
|
||||
user: MatrixClientPeg.get().credentials.userId,
|
||||
|
@ -72,7 +72,9 @@ export const PasswordAuthEntry = React.createClass({
|
|||
|
||||
_onPasswordFieldChange: function(ev) {
|
||||
// enable the submit button iff the password is non-empty
|
||||
this.props.setSubmitButtonEnabled(Boolean(ev.target.value));
|
||||
this.setState({
|
||||
enableSubmit: Boolean(this.refs.passwordField.value),
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
@ -86,12 +88,20 @@ export const PasswordAuthEntry = React.createClass({
|
|||
<div>
|
||||
<p>To continue, please enter your password.</p>
|
||||
<p>Password:</p>
|
||||
<input
|
||||
ref="passwordField"
|
||||
className={passwordBoxClass}
|
||||
onChange={this._onPasswordFieldChange}
|
||||
type="password"
|
||||
/>
|
||||
<form onSubmit={this._onSubmit}>
|
||||
<input
|
||||
ref="passwordField"
|
||||
className={passwordBoxClass}
|
||||
onChange={this._onPasswordFieldChange}
|
||||
type="password"
|
||||
/>
|
||||
<div className="mx_button_row">
|
||||
<input type="submit"
|
||||
className="mx_Dialog_primary"
|
||||
disabled={!this.state.enableSubmit}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<div className="error">
|
||||
{this.props.errorText}
|
||||
</div>
|
||||
|
@ -110,14 +120,9 @@ export const RecaptchaAuthEntry = React.createClass({
|
|||
propTypes: {
|
||||
submitAuthDict: React.PropTypes.func.isRequired,
|
||||
stageParams: React.PropTypes.object.isRequired,
|
||||
setSubmitButtonEnabled: React.PropTypes.func.isRequired,
|
||||
errorText: React.PropTypes.string,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.props.setSubmitButtonEnabled(false);
|
||||
},
|
||||
|
||||
_onCaptchaResponse: function(response) {
|
||||
this.props.submitAuthDict({
|
||||
type: RecaptchaAuthEntry.LOGIN_TYPE,
|
||||
|
@ -148,7 +153,6 @@ export const FallbackAuthEntry = React.createClass({
|
|||
authSessionId: React.PropTypes.string.isRequired,
|
||||
loginType: React.PropTypes.string.isRequired,
|
||||
submitAuthDict: React.PropTypes.func.isRequired,
|
||||
setSubmitButtonEnabled: React.PropTypes.func.isRequired,
|
||||
errorText: React.PropTypes.string,
|
||||
},
|
||||
|
||||
|
@ -156,7 +160,6 @@ export const FallbackAuthEntry = React.createClass({
|
|||
// we have to make the user click a button, as browsers will block
|
||||
// the popup if we open it immediately.
|
||||
this._popupWindow = null;
|
||||
this.props.setSubmitButtonEnabled(true);
|
||||
window.addEventListener("message", this._onReceiveMessage);
|
||||
},
|
||||
|
||||
|
@ -173,7 +176,6 @@ export const FallbackAuthEntry = React.createClass({
|
|||
this.props.authSessionId
|
||||
);
|
||||
this._popupWindow = window.open(url);
|
||||
this.props.setSubmitButtonEnabled(false);
|
||||
},
|
||||
|
||||
_onReceiveMessage: function(event) {
|
||||
|
|
Loading…
Reference in a new issue