This operation requires additional authentication.
+ {this._renderCurrentStage()}
+ {error}
+
+
+ {submitButton}
+ {cancelButton}
+
+
+ );
+ },
+});
diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js
new file mode 100644
index 0000000000..23e2b442ef
--- /dev/null
+++ b/src/components/views/login/InteractiveAuthEntryComponents.js
@@ -0,0 +1,212 @@
+/*
+Copyright 2016 OpenMarket 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 React from 'react';
+
+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
+ * for an auth stage. (The intention is that they could also be used for other
+ * components, such as the registration flow).
+ *
+ * Call getEntryComponentForLoginType() to get a component suitable for a
+ * particular login type. Each component requires the same properties:
+ *
+ * loginType: the login type of the auth stage being attempted
+ * authSessionId: session id from the server
+ * 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
+ * focus: set the input focus appropriately in the form.
+ */
+
+export const PasswordAuthEntry = React.createClass({
+ displayName: 'PasswordAuthEntry',
+
+ statics: {
+ LOGIN_TYPE: "m.login.password",
+ },
+
+ propTypes: {
+ submitAuthDict: React.PropTypes.func.isRequired,
+ setSubmitButtonEnabled: React.PropTypes.func.isRequired,
+ errorText: React.PropTypes.string,
+ },
+
+ componentWillMount: function() {
+ this.props.setSubmitButtonEnabled(false);
+ },
+
+ focus: function() {
+ if (this.refs.passwordField) {
+ this.refs.passwordField.focus();
+ }
+ },
+
+ onSubmitClick: function() {
+ this.props.submitAuthDict({
+ type: PasswordAuthEntry.LOGIN_TYPE,
+ user: MatrixClientPeg.get().credentials.userId,
+ password: this.refs.passwordField.value,
+ });
+ },
+
+ _onPasswordFieldChange: function (ev) {
+ // enable the submit button iff the password is non-empty
+ this.props.setSubmitButtonEnabled(Boolean(ev.target.value));
+ },
+
+ render: function() {
+ let passwordBoxClass = null;
+
+ if (this.props.errorText) {
+ passwordBoxClass = 'error';
+ }
+
+ return (
+
+ );
+ },
+});
+
+export const FallbackAuthEntry = React.createClass({
+ displayName: 'FallbackAuthEntry',
+
+ propTypes: {
+ authSessionId: React.PropTypes.string.isRequired,
+ loginType: React.PropTypes.string.isRequired,
+ submitAuthDict: React.PropTypes.func.isRequired,
+ setSubmitButtonEnabled: React.PropTypes.func.isRequired,
+ errorText: React.PropTypes.string,
+ },
+
+ componentWillMount: function() {
+ // 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);
+ },
+
+ componentWillUnmount: function() {
+ window.removeEventListener("message", this._onReceiveMessage);
+ if (this._popupWindow) {
+ this._popupWindow.close();
+ }
+ },
+
+ onSubmitClick: function() {
+ var url = MatrixClientPeg.get().getFallbackAuthUrl(
+ this.props.loginType,
+ this.props.authSessionId
+ );
+ this._popupWindow = window.open(url);
+ this.props.setSubmitButtonEnabled(false);
+ },
+
+ _onReceiveMessage: function(event) {
+ if (
+ event.data === "authDone" &&
+ event.origin === MatrixClientPeg.get().getHomeserverUrl()
+ ) {
+ this.props.submitAuthDict({});
+ }
+ },
+
+ render: function() {
+ return (
+
+ Click "Submit" to authenticate
+
+ {this.props.errorText}
+
+
+ );
+ },
+});
+
+const AuthEntryComponents = [
+ PasswordAuthEntry,
+ RecaptchaAuthEntry,
+];
+
+export function getEntryComponentForLoginType(loginType) {
+ for (var c of AuthEntryComponents) {
+ if (c.LOGIN_TYPE == loginType) {
+ return c;
+ }
+ }
+ return FallbackAuthEntry;
+};
diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js
new file mode 100644
index 0000000000..35daace0f8
--- /dev/null
+++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js
@@ -0,0 +1,108 @@
+/*
+Copyright 2016 OpenMarket 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 expect from 'expect';
+import q from 'q';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import ReactTestUtils from 'react-addons-test-utils';
+import sinon from 'sinon';
+
+import sdk from 'matrix-react-sdk';
+import MatrixClientPeg from 'MatrixClientPeg';
+
+import * as test_utils from '../../../test-utils';
+
+const InteractiveAuthDialog = sdk.getComponent(
+ 'views.dialogs.InteractiveAuthDialog'
+);
+
+describe('InteractiveAuthDialog', function () {
+ var parentDiv;
+ var sandbox;
+
+ beforeEach(function() {
+ test_utils.beforeEach(this);
+ sandbox = test_utils.stubClient(sandbox);
+ parentDiv = document.createElement('div');
+ document.body.appendChild(parentDiv);
+ });
+
+ afterEach(function() {
+ ReactDOM.unmountComponentAtNode(parentDiv);
+ parentDiv.remove();
+ sandbox.restore();
+ });
+
+ it('Should successfully complete a password flow', function(done) {
+ const onFinished = sinon.spy();
+ const doRequest = sinon.stub().returns(q({a:1}));
+
+ // tell the stub matrixclient to return a real userid
+ var client = MatrixClientPeg.get();
+ client.credentials = {userId: "@user:id"};
+
+ const dlg = ReactDOM.render(
+ , parentDiv);
+
+ // at this point there should be a password box
+ const passwordNode = ReactTestUtils.findRenderedDOMComponentWithTag(
+ dlg, "input"
+ );
+ expect(passwordNode.type).toEqual("password");
+
+ // 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
+ // trigger a request
+ passwordNode.value = "s3kr3t";
+ ReactTestUtils.Simulate.change(passwordNode);
+ expect(submitNode.disabled).toBe(false);
+ ReactTestUtils.Simulate.keyDown(passwordNode, {
+ key: "Enter", keyCode: 13, which: 13,
+ });
+
+ expect(doRequest.callCount).toEqual(1);
+ expect(doRequest.calledWithExactly({
+ session: "sess",
+ type: "m.login.password",
+ password: "s3kr3t",
+ user: "@user:id",
+ })).toBe(true);
+
+ // the submit button should now be disabled (and be a spinner)
+ expect(submitNode.disabled).toBe(true);
+
+ // let the request complete
+ q.delay(1).then(() => {
+ expect(onFinished.callCount).toEqual(1);
+ expect(onFinished.calledWithExactly(true, {a:1})).toBe(true);
+ }).done(done, done);
+ });
+});