Merge pull request #691 from matrix-org/dbkr/interactive_auth_nondialog

Split out InterActiveAuthDialog
This commit is contained in:
David Baker 2017-02-14 10:55:12 +00:00 committed by GitHub
commit 17b08aedfc
6 changed files with 239 additions and 183 deletions

View file

@ -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';

View file

@ -0,0 +1,152 @@
/*
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,
// callback called when the auth process has finished
// @param {bool} status True if the operation requiring
// auth was completed sucessfully, false if canceled.
// @param result The result of the authenticated call
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();
}
},
_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}
busy={this.state.busy}
/>
);
},
render: function() {
let error = null;
if (this.state.errorText) {
error = (
<div className="error">
{this.state.errorText}
</div>
);
}
return (
<div>
<div>
{this._renderCurrentStage()}
{error}
</div>
</div>
);
},
});

View file

@ -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,21 @@ export default React.createClass({
}
},
_onCancelClick: function(e) {
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>

View file

@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
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.
@ -15,13 +16,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 +41,29 @@ 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() {
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>
);

View file

@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
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.
@ -20,7 +21,7 @@ import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
/* This file contains a collection of components which are used by the
* InteractiveAuthDialog to prompt the user to enter the information needed
* InteractiveAuth to prompt the user to enter the information needed
* for an auth stage. (The intention is that they could also be used for other
* components, such as the registration flow).
*
@ -32,10 +33,10 @@ 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
* busy: a boolean indicating whether the auth logic is doing something
* the user needs to wait for.
*
* Each component may also provide the following functions (beyond the standard React ones):
* onSubmitClick: handle a 'submit' button click
* focus: set the input focus appropriately in the form.
*/
@ -48,12 +49,16 @@ export const PasswordAuthEntry = React.createClass({
propTypes: {
submitAuthDict: React.PropTypes.func.isRequired,
setSubmitButtonEnabled: React.PropTypes.func.isRequired,
errorText: React.PropTypes.string,
// is the auth logic currently waiting for something to
// happen?
busy: React.PropTypes.bool,
},
componentWillMount: function() {
this.props.setSubmitButtonEnabled(false);
getInitialState: function() {
return {
passwordValid: false,
};
},
focus: function() {
@ -62,7 +67,10 @@ export const PasswordAuthEntry = React.createClass({
}
},
onSubmitClick: function() {
_onSubmit: function(e) {
e.preventDefault();
if (this.props.busy) return;
this.props.submitAuthDict({
type: PasswordAuthEntry.LOGIN_TYPE,
user: MatrixClientPeg.get().credentials.userId,
@ -72,7 +80,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({
passwordValid: Boolean(this.refs.passwordField.value),
});
},
render: function() {
@ -82,16 +92,34 @@ export const PasswordAuthEntry = React.createClass({
passwordBoxClass = 'error';
}
let submitButtonOrSpinner;
if (this.props.busy) {
const Loader = sdk.getComponent("elements.Spinner");
submitButtonOrSpinner = <Loader />;
} else {
submitButtonOrSpinner = (
<input type="submit"
className="mx_Dialog_primary"
disabled={!this.state.passwordValid}
/>
);
}
return (
<div>
<p>To continue, please enter your password.</p>
<p>Password:</p>
<form onSubmit={this._onSubmit}>
<input
ref="passwordField"
className={passwordBoxClass}
onChange={this._onPasswordFieldChange}
type="password"
/>
<div className="mx_button_row">
{submitButtonOrSpinner}
</div>
</form>
<div className="error">
{this.props.errorText}
</div>
@ -110,14 +138,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 +171,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 +178,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);
},
@ -167,13 +188,12 @@ export const FallbackAuthEntry = React.createClass({
}
},
onSubmitClick: function() {
_onShowFallbackClick: function() {
var url = MatrixClientPeg.get().getFallbackAuthUrl(
this.props.loginType,
this.props.authSessionId
);
this._popupWindow = window.open(url);
this.props.setSubmitButtonEnabled(false);
},
_onReceiveMessage: function(event) {
@ -188,7 +208,7 @@ export const FallbackAuthEntry = React.createClass({
render: function() {
return (
<div>
Click "Submit" to authenticate
<a onClick={this._onShowFallbackClick}>Start authentication</a>
<div className="error">
{this.props.errorText}
</div>

View file

@ -67,16 +67,24 @@ describe('InteractiveAuthDialog', function () {
onFinished={onFinished}
/>, parentDiv);
// at this point there should be a password box
const passwordNode = ReactTestUtils.findRenderedDOMComponentWithTag(
// at this point there should be a password box and a submit button
const formNode = ReactTestUtils.findRenderedDOMComponentWithTag(dlg, "form");
const inputNodes = ReactTestUtils.scryRenderedDOMComponentsWithTag(
dlg, "input"
);
expect(passwordNode.type).toEqual("password");
let passwordNode;
let submitNode;
for (const node of inputNodes) {
if (node.type == 'password') {
passwordNode = node;
} else if (node.type == 'submit') {
submitNode = node;
}
}
expect(passwordNode).toExist();
expect(submitNode).toExist();
// submit should be disabled
const submitNode = ReactTestUtils.findRenderedDOMComponentWithClass(
dlg, "mx_Dialog_primary"
);
expect(submitNode.disabled).toBe(true);
// put something in the password box, and hit enter; that should
@ -84,9 +92,7 @@ describe('InteractiveAuthDialog', function () {
passwordNode.value = "s3kr3t";
ReactTestUtils.Simulate.change(passwordNode);
expect(submitNode.disabled).toBe(false);
ReactTestUtils.Simulate.keyDown(passwordNode, {
key: "Enter", keyCode: 13, which: 13,
});
ReactTestUtils.Simulate.submit(formNode, {});
expect(doRequest.callCount).toEqual(1);
expect(doRequest.calledWithExactly({
@ -96,8 +102,10 @@ describe('InteractiveAuthDialog', function () {
user: "@user:id",
})).toBe(true);
// the submit button should now be disabled (and be a spinner)
expect(submitNode.disabled).toBe(true);
// there should now be a spinner
ReactTestUtils.findRenderedComponentWithType(
dlg, sdk.getComponent('elements.Spinner'),
);
// let the request complete
q.delay(1).then(() => {