diff --git a/examples/trivial/index.js b/examples/trivial/index.js
index 5752e6bda7..2be9054954 100644
--- a/examples/trivial/index.js
+++ b/examples/trivial/index.js
@@ -24,7 +24,52 @@ var React = require("react");
// maps cannot pass through two stages).
var MatrixReactSdk = require("../../src/index");
-React.render(
- ,
+// Here, we do some crude URL analysis to allow
+// deep-linking. We only support registration
+// deep-links in this example.
+function routeUrl(location) {
+ if (location.hash.indexOf('#/register') == 0) {
+ var hashparts = location.hash.split('?');
+ var params = {};
+ if (hashparts.length == 2) {
+ var pairs = hashparts[1].split('&');
+ for (var i = 0; i < pairs.length; ++i) {
+ var parts = pairs[i].split('=');
+ if (parts.length != 2) continue;
+ params[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]);
+ }
+ }
+ window.matrixChat.showScreen('register', params);
+ }
+}
+
+var loaded = false;
+
+window.onload = function() {
+ routeUrl(window.location);
+ loaded = true;
+}
+
+// This will be called whenever the SDK changes screens,
+// so a web page can update the URL bar appropriately.
+var onNewScreen = function(screen) {
+ if (!loaded) return;
+ window.location.hash = '#/'+screen;
+}
+
+// We use this to work out what URL the SDK should
+// pass through when registering to allow the user to
+// click back to the client having registered.
+// It's up to us to recognise if we're loaded with
+// this URL and tell MatrixClient to resume registration.
+var makeRegistrationUrl = function() {
+ return window.location.protocol + '//' +
+ window.location.host +
+ window.location.pathname +
+ '#/register';
+}
+
+window.matrixChat = React.render(
+ ,
document.getElementById('matrixchat')
);
diff --git a/skins/base/views/organisms/RoomView.js b/skins/base/views/organisms/RoomView.js
index 81a59c2e82..8de03a84a7 100644
--- a/skins/base/views/organisms/RoomView.js
+++ b/skins/base/views/organisms/RoomView.js
@@ -38,6 +38,12 @@ module.exports = React.createClass({
mixins: [RoomViewController],
render: function() {
+ if (!this.state.room) {
+ return (
+
+ );
+ }
+
var myUserId = MatrixClientPeg.get().credentials.userId;
if (this.state.room.currentState.members[myUserId].membership == 'invite') {
if (this.state.joining) {
diff --git a/skins/base/views/pages/MatrixChat.js b/skins/base/views/pages/MatrixChat.js
index f9e199e6e8..0231af4420 100644
--- a/skins/base/views/pages/MatrixChat.js
+++ b/skins/base/views/pages/MatrixChat.js
@@ -23,6 +23,7 @@ var RoomList = ComponentBroker.get('organisms/RoomList');
var RoomView = ComponentBroker.get('organisms/RoomView');
var MatrixToolbar = ComponentBroker.get('molecules/MatrixToolbar');
var Login = ComponentBroker.get('templates/Login');
+var Register = ComponentBroker.get('templates/Register');
var MatrixChatController = require("../../../../src/controllers/pages/MatrixChat");
@@ -51,6 +52,14 @@ module.exports = React.createClass({
return (
);
+ } else if (this.state.screen == 'register') {
+ return (
+
+ );
} else {
return (
diff --git a/skins/base/views/templates/Login.js b/skins/base/views/templates/Login.js
index 6cd721e20b..ceae07ec41 100644
--- a/skins/base/views/templates/Login.js
+++ b/skins/base/views/templates/Login.js
@@ -40,6 +40,7 @@ module.exports = React.createClass({
Please log in:
{this.componentForStep(this.state.step)}
{this.state.errorText}
+ Create a new account
);
}
diff --git a/skins/base/views/templates/Register.js b/skins/base/views/templates/Register.js
new file mode 100644
index 0000000000..676fa2e273
--- /dev/null
+++ b/skins/base/views/templates/Register.js
@@ -0,0 +1,55 @@
+/*
+Copyright 2015 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.
+*/
+
+'use strict';
+
+var React = require('react');
+
+var ComponentBroker = require("../../../../src/ComponentBroker");
+
+var Loader = require("react-loader");
+
+var RegisterController = require("../../../../src/controllers/templates/Register");
+
+module.exports = React.createClass({
+ displayName: 'Register',
+ mixins: [RegisterController],
+
+ registerContent: function() {
+ if (this.state.busy) {
+ return (
+
+ );
+ } else {
+ return (
+
+ );
+ }
+ },
+
+ render: function() {
+ return (
+
+ {this.registerContent()}
+
+ );
+ }
+});
diff --git a/src/ComponentBroker.js b/src/ComponentBroker.js
index 1332627afa..6445e9472f 100644
--- a/src/ComponentBroker.js
+++ b/src/ComponentBroker.js
@@ -85,6 +85,7 @@ require('../skins/base/views/molecules/MemberTile');
require('../skins/base/views/organisms/RoomList');
require('../skins/base/views/organisms/RoomView');
require('../skins/base/views/templates/Login');
+require('../skins/base/views/templates/Register');
require('../skins/base/views/organisms/Notifier');
require('../skins/base/views/organisms/CreateRoom');
require('../skins/base/views/molecules/UserSelector');
diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js
index a1c820ee1a..0b6c26496f 100644
--- a/src/MatrixClientPeg.js
+++ b/src/MatrixClientPeg.js
@@ -24,11 +24,13 @@ var matrixClient = null;
var localStorage = window.localStorage;
if (localStorage) {
var hs_url = localStorage.getItem("mx_hs_url");
+ var is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org';
var access_token = localStorage.getItem("mx_access_token");
var user_id = localStorage.getItem("mx_user_id");
if (access_token && user_id && hs_url) {
matrixClient = Matrix.createClient({
baseUrl: hs_url,
+ idBaseUrl: is_url,
accessToken: access_token,
userId: user_id
});
@@ -44,8 +46,11 @@ module.exports = {
matrixClient = cli;
},
- replaceUsingUrl: function(hs_url) {
- matrixClient = Matrix.createClient(hs_url);
+ replaceUsingUrls: function(hs_url, is_url) {
+ matrixClient = Matrix.createClient({
+ baseUrl: hs_url,
+ idBaseUrl: is_url
+ });
}
};
diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js
index 6049eee0e1..10b375cdad 100644
--- a/src/controllers/organisms/RoomView.js
+++ b/src/controllers/organisms/RoomView.js
@@ -36,7 +36,7 @@ var tileTypes = {
module.exports = {
getInitialState: function() {
return {
- room: MatrixClientPeg.get().getRoom(this.props.roomId),
+ room: this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null,
messageCap: INITIAL_SIZE
}
},
diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js
index 717f91e7a4..44a4df0752 100644
--- a/src/controllers/pages/MatrixChat.js
+++ b/src/controllers/pages/MatrixChat.js
@@ -44,6 +44,11 @@ module.exports = {
this.focusComposer = false;
document.addEventListener("keydown", this.onKeyDown);
window.addEventListener("focus", this.onFocus);
+ if (this.state.logged_in) {
+ this.notifyNewScreen('');
+ } else {
+ this.notifyNewScreen('login');
+ }
},
componentWillUnmount: function() {
@@ -63,14 +68,45 @@ module.exports = {
switch (payload.action) {
case 'logout':
- this.setState({
+ this.replaceState({
logged_in: false,
ready: false
});
+ localStorage.removeItem("mx_hs_url");
+ localStorage.removeItem("mx_user_id");
+ localStorage.removeItem("mx_access_token");
Notifier.stop();
MatrixClientPeg.get().removeAllListeners();
MatrixClientPeg.replace(null);
break;
+ case 'start_registration':
+ if (this.state.logged_in) return;
+ var newState = payload.params || {};
+ newState.screen = 'register';
+ if (
+ payload.params &&
+ payload.params.client_secret &&
+ payload.params.session_id &&
+ payload.params.hs_url &&
+ payload.params.is_url &&
+ payload.params.sid
+ ) {
+ newState.register_client_secret = payload.params.client_secret;
+ newState.register_session_id = payload.params.session_id;
+ newState.register_hs_url = payload.params.hs_url;
+ newState.register_is_url = payload.params.is_url;
+ newState.register_id_sid = payload.params.sid;
+ }
+ this.replaceState(newState);
+ this.notifyNewScreen('register');
+ break;
+ case 'start_login':
+ if (this.state.logged_in) return;
+ this.replaceState({
+ screen: 'login'
+ });
+ this.notifyNewScreen('login');
+ break;
case 'view_room':
this.focusComposer = true;
this.setState({
@@ -99,8 +135,12 @@ module.exports = {
},
onLoggedIn: function() {
- this.setState({logged_in: true});
+ this.setState({
+ screen: undefined,
+ logged_in: true
+ });
this.startMatrixClient();
+ this.notifyNewScreen('');
},
startMatrixClient: function() {
@@ -137,6 +177,26 @@ module.exports = {
onFocus: function(ev) {
dis.dispatch({action: 'focus_composer'});
+ },
+
+ showScreen(screen, params) {
+ if (screen == 'register') {
+ dis.dispatch({
+ action: 'start_registration',
+ params: params
+ });
+ } else if (screen == 'login') {
+ dis.dispatch({
+ action: 'start_login',
+ params: params
+ });
+ }
+ },
+
+ notifyNewScreen: function(screen) {
+ if (this.props.onNewScreen) {
+ this.props.onNewScreen(screen);
+ }
}
};
diff --git a/src/controllers/templates/Login.js b/src/controllers/templates/Login.js
index f3c58cf2fb..714fb2720a 100644
--- a/src/controllers/templates/Login.js
+++ b/src/controllers/templates/Login.js
@@ -20,6 +20,7 @@ var React = require('react');
var MatrixClientPeg = require("../../MatrixClientPeg");
var Matrix = require("matrix-js-sdk");
+var dis = require("../../dispatcher");
var ComponentBroker = require("../../ComponentBroker");
@@ -41,8 +42,14 @@ module.exports = {
onHSChosen: function(ev) {
ev.preventDefault();
- MatrixClientPeg.replaceUsingUrl(this.refs.serverConfig.getHsUrl());
- this.setState({hs_url: this.refs.serverConfig.getHsUrl()});
+ MatrixClientPeg.replaceUsingUrls(
+ this.refs.serverConfig.getHsUrl(),
+ this.refs.serverConfig.getIsUrl()
+ );
+ this.setState({
+ hs_url: this.refs.serverConfig.getHsUrl(),
+ is_url: this.refs.serverConfig.getIsUrl()
+ });
this.setStep("fetch_stages");
var cli = MatrixClientPeg.get();
this.setState({busy: true});
@@ -71,12 +78,14 @@ module.exports = {
// XXX: we assume this means we're logged in, but there could be a next stage
MatrixClientPeg.replace(Matrix.createClient({
baseUrl: that.state.hs_url,
+ idBaseUrl: that.state.is_url,
userId: data.user_id,
accessToken: data.access_token
}));
var localStorage = window.localStorage;
if (localStorage) {
localStorage.setItem("mx_hs_url", that.state.hs_url);
+ localStorage.setItem("mx_is_url", that.state.is_url);
localStorage.setItem("mx_user_id", data.user_id);
localStorage.setItem("mx_access_token", data.access_token);
} else {
@@ -85,9 +94,6 @@ module.exports = {
if (that.props.onLoggedIn) {
that.props.onLoggedIn();
}
- /*dis.dispatch({
- 'action': 'logged_in'
- });*/
}, function(error) {
that.setStep("stage_m.login.password");
that.setState({errorText: 'Login failed.'});
@@ -118,4 +124,11 @@ module.exports = {
);
}
},
+
+ showRegister: function(ev) {
+ ev.preventDefault();
+ dis.dispatch({
+ action: 'start_registration'
+ });
+ }
};
diff --git a/src/controllers/templates/Register.js b/src/controllers/templates/Register.js
new file mode 100644
index 0000000000..650fdd6b2d
--- /dev/null
+++ b/src/controllers/templates/Register.js
@@ -0,0 +1,346 @@
+/*
+Copyright 2015 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.
+*/
+
+'use strict';
+
+var React = require('react');
+
+var MatrixClientPeg = require("../../MatrixClientPeg");
+var Matrix = require("matrix-js-sdk");
+var dis = require("../../dispatcher");
+
+var ComponentBroker = require("../../ComponentBroker");
+
+var ServerConfig = ComponentBroker.get("molecules/ServerConfig");
+
+module.exports = {
+ getInitialState: function() {
+ return {
+ step: 'initial',
+ busy: false,
+ currentStep: 0,
+ totalSteps: 1
+ };
+ },
+
+ componentWillMount: function() {
+ this.readNewProps();
+ },
+
+ componentWillReceiveProps: function() {
+ this.readNewProps();
+ },
+
+ readNewProps: function() {
+ if (this.props.clientSecret && this.props.hsUrl &&
+ this.props.isUrl && this.props.sessionId &&
+ this.props.idSid) {
+ this.authSessionId = this.props.sessionId;
+ MatrixClientPeg.replaceUsingUrls(
+ this.props.hsUrl,
+ this.props.isUrl
+ );
+ this.setState({
+ hs_url: this.props.hsUrl,
+ is_url: this.props.isUrl
+ });
+ this.savedParams = {client_secret: this.props.clientSecret};
+ this.setState({busy: true});
+
+ var isLocation = document.createElement('a');
+ isLocation.href = this.props.isUrl;
+
+ var auth = {
+ type: 'm.login.email.identity',
+ threepid_creds: {
+ sid: this.props.idSid,
+ client_secret: this.savedParams.client_secret,
+ id_server: isLocation.host
+ }
+ };
+ this.tryRegister(auth);
+ }
+ },
+
+ componentDidUpdate: function() {
+ // Just putting a script tag into the returned jsx doesn't work, annoyingly,
+ // so we do this instead.
+ if (this.refs.recaptchaContainer) {
+ var scriptTag = document.createElement('script');
+ window.mx_on_recaptcha_loaded = this.onCaptchaLoaded;
+ scriptTag.setAttribute('src', "https://www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit");
+ this.refs.recaptchaContainer.getDOMNode().appendChild(scriptTag);
+ }
+ },
+
+ setStep: function(step) {
+ this.setState({ step: step, errorText: '', busy: false });
+ },
+
+ getSupportedStageTypes: function() {
+ return ['m.login.email.identity', 'm.login.recaptcha'];
+ },
+
+ chooseFlow: function(flows) {
+ // this is fairly simple right now
+ var supportedTypes = this.getSupportedStageTypes();
+
+ var emailFlow = null;
+ var otherFlow = null;
+ for (var flowI = 0; flowI < flows.length; ++flowI) {
+ var flow = flows[flowI];
+ var flowHasEmail = false;
+ var flowSupported = true;
+ for (var stageI = 0; stageI < flow.stages.length; ++stageI) {
+ var stage = flow.stages[stageI];
+
+ if (supportedTypes.indexOf(stage) == -1) {
+ flowSupported = false;
+ }
+
+ if (stage == 'm.login.email.identity') {
+ flowHasEmail = true;
+ }
+ }
+ if (flowSupported) {
+ if (flowHasEmail) {
+ emailFlow = flow;
+ } else {
+ otherFlow = flow;
+ }
+ }
+ }
+
+ if (
+ this.savedParams.email != '' ||
+ this.completedStages.indexOf('m.login.email.identity' > -1)
+ ) {
+ return emailFlow;
+ } else {
+ return otherFlow;
+ }
+ },
+
+ firstUncompletedStageIndex: function(flow) {
+ if (this.completedStages === undefined) return 0;
+ for (var i = 0; i < flow.stages.length; ++i) {
+ if (this.completedStages.indexOf(flow.stages[i]) == -1) {
+ return i;
+ }
+ }
+ },
+
+ numCompletedStages: function(flow) {
+ if (this.completedStages === undefined) return 0;
+ var nCompleted = 0;
+ for (var i = 0; i < flow.stages.length; ++i) {
+ if (this.completedStages.indexOf(flow.stages[i]) > -1) {
+ ++nCompleted;
+ }
+ }
+ return nCompleted;
+ },
+
+ onInitialStageSubmit: function(ev) {
+ ev.preventDefault();
+ MatrixClientPeg.replaceUsingUrls(
+ this.refs.serverConfig.getHsUrl(),
+ this.refs.serverConfig.getIsUrl()
+ );
+ this.setState({
+ hs_url: this.refs.serverConfig.getHsUrl(),
+ is_url: this.refs.serverConfig.getIsUrl()
+ });
+ var cli = MatrixClientPeg.get();
+ this.setState({busy: true});
+ var self = this;
+
+ this.savedParams = {
+ email: this.refs.email.getDOMNode().value,
+ username: this.refs.username.getDOMNode().value,
+ password: this.refs.password.getDOMNode().value
+ };
+
+ this.tryRegister();
+ },
+
+ startStage: function(stageName) {
+ var self = this;
+ this.setStep('stage_'+stageName);
+ switch(stageName) {
+ case 'm.login.email.identity':
+ self.setState({
+ busy: true
+ });
+ var cli = MatrixClientPeg.get();
+ this.savedParams.client_secret = cli.generateClientSecret();
+ this.savedParams.send_attempt = 1;
+
+ var nextLink = this.props.registrationUrl +
+ '?client_secret=' +
+ encodeURIComponent(this.savedParams.client_secret) +
+ "&hs_url=" +
+ encodeURIComponent(this.state.hs_url) +
+ "&is_url=" +
+ encodeURIComponent(this.state.is_url) +
+ "&session_id=" +
+ encodeURIComponent(this.authSessionId);
+
+ cli.requestEmailToken(
+ this.savedParams.email,
+ this.savedParams.client_secret,
+ this.savedParams.send_attempt,
+ nextLink
+ ).done(function(response) {
+ self.setState({
+ busy: false,
+ });
+ self.setStep('stage_m.login.email.identity');
+ }, function(error) {
+ self.setState({
+ busy: false,
+ errorText: 'Unable to contact the given Home Server'
+ });
+ });
+ break;
+ case 'm.login.recaptcha':
+ if (!this.authParams || !this.authParams['m.login.recaptcha'].public_key) {
+ this.setState({
+ errorText: "This server has not supplied enough information for Recaptcha authentication"
+ });
+ }
+ break;
+ }
+ },
+
+ onRegistered: function(user_id, access_token) {
+ MatrixClientPeg.replace(Matrix.createClient({
+ baseUrl: this.state.hs_url,
+ idBaseUrl: this.state.is_url,
+ userId: user_id,
+ accessToken: access_token
+ }));
+ var localStorage = window.localStorage;
+ if (localStorage) {
+ localStorage.setItem("mx_hs_url", this.state.hs_url);
+ localStorage.setItem("mx_user_id", user_id);
+ localStorage.setItem("mx_access_token", access_token);
+ } else {
+ console.warn("No local storage available: can't persist session!");
+ }
+ if (this.props.onLoggedIn) {
+ this.props.onLoggedIn();
+ }
+ },
+
+ componentForStep: function(step) {
+ switch (step) {
+ case 'initial':
+ return (
+
+
+
+ );
+ // XXX: clearly these should be separate organisms
+ case 'stage_m.login.email.identity':
+ return (
+
+ Please check your email to continue registration.
+
+ );
+ case 'stage_m.login.recaptcha':
+ return (
+
+ This Home Server would like to make sure you're not a robot
+
+
+ );
+ }
+ },
+
+ onCaptchaLoaded: function() {
+ if (this.refs.recaptchaContainer) {
+ var sitekey = this.authParams['m.login.recaptcha'].public_key;
+ global.grecaptcha.render('mx_recaptcha', {
+ 'sitekey': sitekey,
+ 'callback': this.onCaptchaDone
+ });
+ }
+ },
+
+ onCaptchaDone: function(captcha_response) {
+ this.tryRegister({
+ type: 'm.login.recaptcha',
+ response: captcha_response
+ });
+ },
+
+ tryRegister: function(auth) {
+ var self = this;
+ MatrixClientPeg.get().register(
+ this.savedParams.username,
+ this.savedParams.password,
+ this.authSessionId,
+ auth
+ ).done(function(result) {
+ self.onRegistered(result.user_id, result.access_token);
+ }, function(error) {
+ if (error.httpStatus == 401 && error.data.flows) {
+ self.authParams = error.data.params;
+ self.authSessionId = error.data.session;
+
+ self.completedStages = error.data.completed;
+
+ var flow = self.chooseFlow(error.data.flows);
+
+ var flowStage = self.firstUncompletedStageIndex(flow);
+ var numDone = self.numCompletedStages(flow);
+
+ self.setState({
+ busy: false,
+ flows: flow,
+ currentStep: 1+numDone,
+ totalSteps: flow.stages.length+1,
+ flowStage: flowStage
+ });
+ self.startStage(flow.stages[flowStage]);
+ } else {
+ var errorText = "Unable to contact the given Home Server";
+ if (error.httpStatus == 401) {
+ errorText = "Authorisation failed!";
+ }
+ self.setStep("initial");
+ self.setState({
+ busy: false,
+ errorText: errorText
+ });
+ }
+ });
+ },
+
+ showLogin: function(ev) {
+ ev.preventDefault();
+ dis.dispatch({
+ action: 'start_login'
+ });
+ }
+};