diff options
| -rw-r--r-- | app/actions/user.js | 26 | ||||
| -rw-r--r-- | app/assets/images/icon-spinner.svg | 13 | ||||
| -rw-r--r-- | app/components/Login.css | 10 | ||||
| -rw-r--r-- | app/components/Login.js | 97 | ||||
| -rw-r--r-- | app/components/Tray.js | 19 | ||||
| -rw-r--r-- | app/constants.js | 7 | ||||
| -rw-r--r-- | app/containers/LoginPage.js | 14 | ||||
| -rw-r--r-- | app/reducers/user.js | 6 | ||||
| -rw-r--r-- | package.json | 2 |
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", |
