summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--app/actions/user.js26
-rw-r--r--app/assets/images/icon-spinner.svg13
-rw-r--r--app/components/Login.css10
-rw-r--r--app/components/Login.js97
-rw-r--r--app/components/Tray.js19
-rw-r--r--app/constants.js7
-rw-r--r--app/containers/LoginPage.js14
-rw-r--r--app/reducers/user.js6
-rw-r--r--package.json2
9 files changed, 156 insertions, 38 deletions
diff --git a/app/actions/user.js b/app/actions/user.js
index a809669a60..1a229d850f 100644
--- a/app/actions/user.js
+++ b/app/actions/user.js
@@ -1,23 +1,25 @@
+import assert from 'assert';
import { createAction } from 'redux-actions';
+import { LoginState } from '../constants';
-const loginSuccess = createAction('user.loginSuccess', (account) => {
- return { account, loggedIn: true };
-});
+const loginChange = createAction('USER_LOGIN_CHANGE');
-const loginFailure = createAction('user.loginFailure', (account, error) => {
- return { account, error, loggedIn: false };
-});
-
-const login = (backend, account) => {
- return async (dispatch) => {
+const requestLogin = (backend, account) => {
+ return async (dispatch, getState) => {
try {
+ dispatch(loginChange({ account: account, status: LoginState.connecting }));
+
await backend.login(account);
- dispatch(loginSuccess(account));
+
+ dispatch(loginChange({ status: LoginState.ok }));
} catch(e) {
- dispatch(loginFailure(account, e));
+ dispatch(loginChange({ status: LoginState.failed, error: e }));
}
};
};
-export default { login, loginSuccess, loginFailure };
+export default {
+ requestLogin,
+ loginChange
+};
diff --git a/app/assets/images/icon-spinner.svg b/app/assets/images/icon-spinner.svg
new file mode 100644
index 0000000000..b5be945405
--- /dev/null
+++ b/app/assets/images/icon-spinner.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" id="loader-1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="40px" height="40px" viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
+ <path fill="#fff" d="M43.935,25.145c0-10.318-8.364-18.683-18.683-18.683c-10.318,0-18.683,8.365-18.683,18.683h4.068c0-8.071,6.543-14.615,14.615-14.615c8.072,0,14.615,6.543,14.615,14.615H43.935z">
+ <animateTransform attributeType="xml"
+ attributeName="transform"
+ type="rotate"
+ from="0 25 25"
+ to="360 25 25"
+ dur="0.6s"
+ repeatCount="indefinite"/>
+ </path>
+</svg> \ No newline at end of file
diff --git a/app/components/Login.css b/app/components/Login.css
index db38b20097..a10163c2af 100644
--- a/app/components/Login.css
+++ b/app/components/Login.css
@@ -10,6 +10,16 @@
flex: 0 0 auto;
}
+.login-footer--invisible {
+ visibility: hidden;
+}
+
+.login-form__status-icon {
+ text-align: center;
+ margin-bottom: 44px;
+ height: 36px;
+}
+
.login-footer__prompt {
font-family: "Open Sans";
font-size: 15px;
diff --git a/app/components/Login.js b/app/components/Login.js
index d503200a21..a9aaec81c5 100644
--- a/app/components/Login.js
+++ b/app/components/Login.js
@@ -1,30 +1,53 @@
import { shell } from 'electron';
import React, { Component, PropTypes } from 'react';
+import { If, Then, Else } from 'react-if';
import Layout from './Layout';
-import constants from '../constants';
+import { createAccountURL, LoginState } from '../constants';
export default class Login extends Component {
static propTypes = {
- onLogin: PropTypes.func.isRequired
+ user: PropTypes.object.isRequired,
+ onLogin: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onFirstChangeAfterFailure: PropTypes.func.isRequired
};
-
+
constructor(props) {
super(props);
- this.state = { account: '' };
+ this.state = { notifyOnFirstChangeAfterFailure: false };
+ }
+
+ componentWillReceiveProps(nextProps) {
+ const prev = this.props.user || {};
+ const next = nextProps.user || {};
+
+ if(prev.status !== next.status && next.status === LoginState.failed) {
+ this.setState({ notifyOnFirstChangeAfterFailure: true });
+ }
}
handleLogin() {
- this.props.onLogin(this.props.backend, this.state.account);
+ const { account } = this.props.user;
+ if(account.length > 0) {
+ this.props.onLogin(account);
+ }
}
handleCreateAccount() {
- shell.openExternal(constants.createAccountURL);
+ shell.openExternal(createAccountURL);
}
handleInputChange(e) {
const val = e.target.value.replace(/[^0-9]/g, '');
- this.setState({ account: val });
+
+ // notify delegate on first change after login failure
+ if(this.state.notifyOnFirstChangeAfterFailure) {
+ this.setState({ notifyOnFirstChangeAfterFailure: false });
+ this.props.onFirstChangeAfterFailure();
+ }
+
+ this.props.onChange(val);
}
handleInputKeyUp(e) {
@@ -43,25 +66,71 @@ export default class Login extends Component {
return val.replace(/([0-9]{4})/g, '$1 ').trim();
}
+ formTitle(s) {
+ switch(s) {
+ case LoginState.connecting: return "Logging in...";
+ case LoginState.failed: return "Login failed";
+ case LoginState.ok: return "Logged in";
+ default: return "Login";
+ }
+ }
+
+ formSubtitle(s, e) {
+ switch(s) {
+ case LoginState.failed: return e.message;
+ case LoginState.connecting: return 'Checking account number';
+ default: return 'Enter your account number';
+ }
+ }
+
render() {
+ console.log(this.props.user);
+ const { account, status, error } = this.props.user;
+ const title = this.formTitle(status);
+ const subtitle = this.formSubtitle(status, error);
+ const isConnecting = status === LoginState.connecting;
+ const isFailed = status === LoginState.failed;
+ const inputClass = ["login-form__input-field", isFailed ? "login-form__input-field--error" : ""].join(' ');
+ const footerClass = ["login-footer", isConnecting ? "login-footer--invisible" : ""].join(' ');
+
return (
<Layout>
<div className="login">
<div className="login-form">
<div>
- <div className="login-form__title">Login</div>
- <div className="login-form__subtitle">Enter your account number</div>
+
+ { /* show spinner when connecting */ }
+ <If condition={ isConnecting }>
+ <Then>
+ <div className="login-form__status-icon">
+ <img src="./assets/images/icon-spinner.svg" alt="" />
+ </div>
+ </Then>
+ </If>
+
+ { /* show error icon when failed */ }
+ <If condition={ isFailed }>
+ <Then>
+ <div className="login-form__status-icon">
+ <img src="./assets/images/icon-fail.svg" alt="" />
+ </div>
+ </Then>
+ </If>
+
+ <div className="login-form__title">{ title }</div>
+ <div className="login-form__subtitle">{ subtitle }</div>
<div className="login-form__input-wrap">
- <input className="login-form__input-field"
+ <input className={ inputClass }
type="text"
placeholder="0000 0000 0000"
- onChange={::this.handleInputChange}
- onKeyUp={::this.handleInputKeyUp}
- value={this.formattedAccount(this.state.account)} />
+ onChange={ ::this.handleInputChange }
+ onKeyUp={ ::this.handleInputKeyUp }
+ value={ this.formattedAccount(account) }
+ disabled={ isConnecting } />
</div>
</div>
</div>
- <div className="login-footer">
+ <div className={footerClass}>
<div className="login-footer__prompt">Don't have an account number?</div>
<button className="login-footer__button" onClick={::this.handleCreateAccount}>Create account</button>
</div>
diff --git a/app/components/Tray.js b/app/components/Tray.js
index 745fbc20d1..11075cab36 100644
--- a/app/components/Tray.js
+++ b/app/components/Tray.js
@@ -1,5 +1,6 @@
import React, { Component, PropTypes } from 'react';
import { TrayMenu, TrayItem } from '../lib/components/TrayMenu';
+import { shell } from 'electron';
export default class Tray extends Component {
@@ -11,16 +12,24 @@ export default class Tray extends Component {
this.props.login({ username: '', loggedIn: false });
this.props.history.push('/');
}
+
+ openPrivacyPolicy() {
+ shell.openExternal('https://mullvad.net/#privacy');
+ }
+
+ openHomepage() {
+ shell.openExternal('https://mullvad.net');
+ }
render() {
const loggedIn = this.props.user && this.props.user.loggedIn;
return (
- <TrayMenu tray={this.props.handle}>
- <TrayItem label="Log out" click={::this.logout} visible={loggedIn} />
- <TrayItem type="separator" visible={loggedIn} />
- <TrayItem label="Privacy Policy" />
- <TrayItem label="Visit homepage" />
+ <TrayMenu tray={ this.props.handle }>
+ <TrayItem label="Log out" click={ ::this.logout } visible={ loggedIn } />
+ <TrayItem type="separator" visible={ loggedIn } />
+ <TrayItem label="Privacy Policy" click={ ::this.openPrivacyPolicy } />
+ <TrayItem label="Visit homepage" click={ ::this.openHomepage } />
</TrayMenu>
);
}
diff --git a/app/constants.js b/app/constants.js
index 1e88112e9a..816b8761fb 100644
--- a/app/constants.js
+++ b/app/constants.js
@@ -1,3 +1,8 @@
+import Enum from 'es6-enum'
+
+const LoginState = Enum('none', 'connecting', 'failed', 'ok');
+
module.exports = {
- createAccountURL: 'https://mullvad.net/account/create/'
+ createAccountURL: 'https://mullvad.net/account/create/',
+ LoginState
};
diff --git a/app/containers/LoginPage.js b/app/containers/LoginPage.js
index 1eb28d3352..5d40b21340 100644
--- a/app/containers/LoginPage.js
+++ b/app/containers/LoginPage.js
@@ -2,15 +2,25 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Login from '../components/Login';
import userActions from '../actions/user';
+import { LoginState } from '../constants';
const mapStateToProps = (state) => {
return state;
};
-const mapDispatchToProps = (dispatch) => {
+const mapDispatchToProps = (dispatch, props) => {
const user = bindActionCreators(userActions, dispatch);
return {
- onLogin: user.login
+ onLogin: (account) => {
+ return user.requestLogin(props.backend, account);
+ },
+ onChange: (account) => {
+ return user.loginChange({ account });
+ },
+ onFirstChangeAfterFailure: () => {
+ console.log('onFirstChangeAfterFailure');
+ return user.loginChange({ status: LoginState.none, error: null })
+ }
};
};
diff --git a/app/reducers/user.js b/app/reducers/user.js
index 3e3565a15a..553155c7b8 100644
--- a/app/reducers/user.js
+++ b/app/reducers/user.js
@@ -3,10 +3,8 @@ import { handleActions } from 'redux-actions';
import actions from '../actions/user';
export default handleActions({
- [actions.loginSuccess]: (state, action) => {
- return { ...state, ...action.payload };
- },
- [actions.loginFailure]: (state, action) => {
+ [actions.loginChange]: (state, action) => {
+ console.log(action.payload);
return { ...state, ...action.payload };
}
}, {});
diff --git a/package.json b/package.json
index 7ac69b3be2..ec5236bada 100644
--- a/package.json
+++ b/package.json
@@ -11,8 +11,10 @@
"license": "MIT",
"dependencies": {
"babel-runtime": "^6.22.0",
+ "es6-enum": "^1.1.0",
"react": "^15.4.2",
"react-dom": "^15.4.2",
+ "react-if": "^2.1.0",
"react-redux": "^5.0.2",
"react-router": "^3.0.2",
"react-router-redux": "^4.0.7",