diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2018-06-12 16:22:45 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2018-06-12 16:22:45 +0200 |
| commit | a7da96bc03b3705c4d99bbc55e9f1a04f2030c25 (patch) | |
| tree | 25f9e6012bad2efc917342450f4ae055deb6fe6e | |
| parent | 55b5d390bc377b9ff6cdf5a0b1641ea2fae23719 (diff) | |
| parent | 247e9807b25ac5033ab5cfa55cc40a5201b21523 (diff) | |
| download | mullvadvpn-a7da96bc03b3705c4d99bbc55e9f1a04f2030c25.tar.xz mullvadvpn-a7da96bc03b3705c4d99bbc55e9f1a04f2030c25.zip | |
Merge branch 'refresh-expiry-time'
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | app/components/Account.js | 60 | ||||
| -rw-r--r-- | app/containers/AccountPage.js | 10 | ||||
| -rw-r--r-- | app/lib/app-visibility.js | 28 | ||||
| -rw-r--r-- | app/lib/backend.js | 19 | ||||
| -rw-r--r-- | app/main.js | 58 | ||||
| -rw-r--r-- | app/redux/account/actions.js | 20 | ||||
| -rw-r--r-- | app/redux/account/reducers.js | 7 | ||||
| -rw-r--r-- | app/redux/store.js | 24 | ||||
| -rw-r--r-- | test/components/Account.spec.js | 39 | ||||
| -rw-r--r-- | test/ipc.spec.js | 4 |
11 files changed, 196 insertions, 74 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 749fb4ec83..a555c212ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Line wrap the file at 100 chars. Th ## [Unreleased] ### Added +- Refresh account expiration when account view becomes visible. - Add `tunnel` subcommand to manage tunnel specific options in the CLI. - Add support for passing the `--mssfix` argument to OpenVPN tunnels. - Add details to mullvad CLI interface error for when it doesn't trust the RPC file. diff --git a/app/components/Account.js b/app/components/Account.js index b4eef86712..8639d0fe42 100644 --- a/app/components/Account.js +++ b/app/components/Account.js @@ -1,28 +1,60 @@ // @flow import moment from 'moment'; -import React from 'react'; +import * as React from 'react'; import { Component, Text, View } from 'reactxp'; import { Button, RedButton, GreenButton, Label } from './styled'; import { Layout, Container } from './Layout'; import styles from './AccountStyles'; import Img from './Img'; import { formatAccount } from '../lib/formatters'; +import AppVisiblityObserver from '../lib/app-visibility'; -import type { AccountReduxState } from '../redux/account/reducers'; +import type { AccountToken } from '../lib/ipc-facade'; export type AccountProps = { - account: AccountReduxState, + accountToken: AccountToken, + accountExpiry: string, + updateAccountExpiry: () => Promise<void>, onLogout: () => void, onClose: () => void, onBuyMore: () => void, }; -export default class Account extends Component { - props: AccountProps; +export type AccountState = { + isRefreshingExpiry: boolean, +}; + +export default class Account extends Component<AccountProps, AccountState> { + state = { + isRefreshingExpiry: false, + }; + + _appVisibilityObserver: ?AppVisiblityObserver; + + _isMounted = false; + + componentDidMount() { + this._isMounted = true; + this._refreshAccountExpiry(); + + this._appVisibilityObserver = new AppVisiblityObserver((isVisible) => { + if (isVisible) { + this._refreshAccountExpiry(); + } + }); + } + + componentWillUnmount() { + this._isMounted = false; + + if (this._appVisibilityObserver) { + this._appVisibilityObserver.dispose(); + } + } render() { - const expiry = moment(this.props.account.expiry); - const formattedAccountToken = formatAccount(this.props.account.accountToken || ''); + const expiry = moment(this.props.accountExpiry); + const formattedAccountToken = formatAccount(this.props.accountToken || ''); const formattedExpiry = expiry.format('hA, D MMMM YYYY').toUpperCase(); const isOutOfTime = expiry.isSameOrBefore(moment()); @@ -81,4 +113,18 @@ export default class Account extends Component { </Layout> ); } + + async _refreshAccountExpiry() { + this.setState({ isRefreshingExpiry: true }); + + try { + await this.props.updateAccountExpiry(); + } catch (e) { + // TODO: Report the error to user + } + + if (this._isMounted) { + this.setState({ isRefreshingExpiry: false }); + } + } } diff --git a/app/containers/AccountPage.js b/app/containers/AccountPage.js index 210ad4aaeb..1c54167b50 100644 --- a/app/containers/AccountPage.js +++ b/app/containers/AccountPage.js @@ -11,13 +11,19 @@ import { openLink } from '../lib/platform'; import type { ReduxState, ReduxDispatch } from '../redux/store'; import type { SharedRouteProps } from '../routes'; -const mapStateToProps = (state: ReduxState) => state; +const mapStateToProps = (state: ReduxState) => ({ + accountToken: state.account.accountToken, + accountExpiry: state.account.expiry, +}); const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => { + const { backend } = props; const { push: pushHistory } = bindActionCreators({ push }, dispatch); const { logout } = bindActionCreators(accountActions, dispatch); + return { + updateAccountExpiry: () => backend.updateAccountExpiry(), onLogout: () => { - logout(props.backend); + logout(backend); }, onClose: () => { pushHistory('/settings'); diff --git a/app/lib/app-visibility.js b/app/lib/app-visibility.js new file mode 100644 index 0000000000..d599f6941b --- /dev/null +++ b/app/lib/app-visibility.js @@ -0,0 +1,28 @@ +// @flow +import { ipcRenderer } from 'electron'; + +type EventHandler = (boolean) => any; + +export default class AppVisiblityObserver { + _handler: EventHandler; + + constructor(handler: EventHandler) { + this._handler = handler; + + ipcRenderer.on('show-window', this._handleShowEvent).on('hide-window', this._handleHideEvent); + } + + dispose() { + ipcRenderer + .removeListener('show-window', this._handleShowEvent) + .removeListener('hide-window', this._handleHideEvent); + } + + _handleShowEvent = (_event) => { + this._handler(true); + }; + + _handleHideEvent = (_event) => { + this._handler(false); + }; +} diff --git a/app/lib/backend.js b/app/lib/backend.js index b043a9b10e..dfe4fdd2c3 100644 --- a/app/lib/backend.js +++ b/app/lib/backend.js @@ -365,6 +365,25 @@ export class Backend { } } + async updateAccountExpiry() { + const ipc = this._ipc; + const store = this._store; + + try { + await this._ensureAuthenticated(); + + const accountToken = await this._ipc.getAccount(); + if (!accountToken) { + throw new BackendError('NO_ACCOUNT'); + } + + const accountData = await ipc.getAccountData(accountToken); + store.dispatch(accountActions.updateAccountExpiry(accountData.expiry)); + } catch (e) { + log.error(`Failed to update account expiry: ${e.message}`); + } + } + async removeAccountFromHistory(accountToken: AccountToken) { try { await this._ensureAuthenticated(); diff --git a/app/main.js b/app/main.js index 2ac7f0a8d1..b6fefc519e 100644 --- a/app/main.js +++ b/app/main.js @@ -110,7 +110,8 @@ const ApplicationMain = { tray.on('click', () => windowController.toggle()); - this._registerIpcEvents(); + this._registerWindowIpcEvents(window); + this._registerIpcListeners(); this._setAppMenu(); this._addContextMenu(window); @@ -140,7 +141,13 @@ const ApplicationMain = { window.loadFile('build/index.html'); }, - _registerIpcEvents() { + _registerWindowIpcEvents(window: BrowserWindow) { + // Notify renderer when window visibility changes. + window.on('show', () => window.webContents.send('show-window')); + window.on('hide', () => window.webContents.send('hide-window')); + }, + + _registerIpcListeners() { ipcMain.on('on-browser-window-ready', () => { this._pollConnectionInfoFile(); }); @@ -406,32 +413,35 @@ const ApplicationMain = { ]; // add inspect element on right click menu - window.webContents.on('context-menu', (_e: Event, props: { x: number, y: number }) => { - let inspectTemplate = [ - { - label: 'Inspect element', - click() { - window.openDevTools({ mode: 'detach' }); - window.inspectElement(props.x, props.y); + window.webContents.on( + 'context-menu', + (_e: Event, props: { x: number, y: number, isEditable: boolean }) => { + let inspectTemplate = [ + { + label: 'Inspect element', + click() { + window.openDevTools({ mode: 'detach' }); + window.inspectElement(props.x, props.y); + }, }, - }, - ]; + ]; - if (props.isEditable) { - let inputMenu = menuTemplate; + if (props.isEditable) { + let inputMenu = menuTemplate; - // mixin 'inspect element' into standard menu when in development mode - if (process.env.NODE_ENV === 'development') { - inputMenu = menuTemplate.concat([{ type: 'separator' }], inspectTemplate); - } + // mixin 'inspect element' into standard menu when in development mode + if (process.env.NODE_ENV === 'development') { + inputMenu = menuTemplate.concat([{ type: 'separator' }], inspectTemplate); + } - Menu.buildFromTemplate(inputMenu).popup(window); - } else if (process.env.NODE_ENV === 'development') { - // display inspect element for all non-editable - // elements when in development mode - Menu.buildFromTemplate(inspectTemplate).popup(window); - } - }); + Menu.buildFromTemplate(inputMenu).popup(window); + } else if (process.env.NODE_ENV === 'development') { + // display inspect element for all non-editable + // elements when in development mode + Menu.buildFromTemplate(inspectTemplate).popup(window); + } + }, + ); }, _createTray(): Tray { diff --git a/app/redux/account/actions.js b/app/redux/account/actions.js index ca2bbdc7b6..fdb9f44281 100644 --- a/app/redux/account/actions.js +++ b/app/redux/account/actions.js @@ -10,7 +10,7 @@ type StartLoginAction = { type LoginSuccessfulAction = { type: 'LOGIN_SUCCESSFUL', - expiry: string, + expiry?: string, }; type LoginFailedAction = { @@ -36,6 +36,11 @@ type UpdateAccountHistoryAction = { accountHistory: Array<AccountToken>, }; +type UpdateAccountExpiryAction = { + type: 'UPDATE_ACCOUNT_EXPIRY', + expiry: string, +}; + export type AccountAction = | StartLoginAction | LoginSuccessfulAction @@ -43,7 +48,8 @@ export type AccountAction = | LoggedOutAction | ResetLoginErrorAction | UpdateAccountTokenAction - | UpdateAccountHistoryAction; + | UpdateAccountHistoryAction + | UpdateAccountExpiryAction; function startLogin(accountToken?: AccountToken): StartLoginAction { return { @@ -55,7 +61,7 @@ function startLogin(accountToken?: AccountToken): StartLoginAction { function loginSuccessful(expiry: string): LoginSuccessfulAction { return { type: 'LOGIN_SUCCESSFUL', - expiry: expiry, + expiry, }; } @@ -96,6 +102,13 @@ function updateAccountHistory(accountHistory: Array<AccountToken>): UpdateAccoun }; } +function updateAccountExpiry(expiry: string): UpdateAccountExpiryAction { + return { + type: 'UPDATE_ACCOUNT_EXPIRY', + expiry, + }; +} + const login = (backend: Backend, account: string) => () => backend.login(account); const logout = (backend: Backend) => () => backend.logout(); @@ -110,4 +123,5 @@ export default { resetLoginError, updateAccountToken, updateAccountHistory, + updateAccountExpiry, }; diff --git a/app/redux/account/reducers.js b/app/redux/account/reducers.js index fe10dce742..f3485028d4 100644 --- a/app/redux/account/reducers.js +++ b/app/redux/account/reducers.js @@ -85,6 +85,13 @@ export default function( accountHistory: action.accountHistory, }, }; + case 'UPDATE_ACCOUNT_EXPIRY': + return { + ...state, + ...{ + expiry: action.expiry, + }, + }; } return state; diff --git a/app/redux/store.js b/app/redux/store.js index 8d74b605f6..9e15b3a325 100644 --- a/app/redux/store.js +++ b/app/redux/store.js @@ -3,22 +3,22 @@ import { createStore, applyMiddleware, combineReducers, compose } from 'redux'; import { routerMiddleware, routerReducer, push, replace } from 'react-router-redux'; import thunk from 'redux-thunk'; -import account from './account/reducers.js'; -import accountActions from './account/actions.js'; -import connection from './connection/reducers.js'; -import connectionActions from './connection/actions.js'; -import settings from './settings/reducers.js'; -import settingsActions from './settings/actions.js'; +import account from './account/reducers'; +import accountActions from './account/actions'; +import connection from './connection/reducers'; +import connectionActions from './connection/actions'; +import settings from './settings/reducers'; +import settingsActions from './settings/actions'; import type { Store } from 'redux'; import type { History } from 'history'; -import type { AccountReduxState } from './account/reducers.js'; -import type { ConnectionReduxState } from './connection/reducers.js'; -import type { SettingsReduxState } from './settings/reducers.js'; +import type { AccountReduxState } from './account/reducers'; +import type { ConnectionReduxState } from './connection/reducers'; +import type { SettingsReduxState } from './settings/reducers'; -import type { ConnectionAction } from './connection/actions.js'; -import type { AccountAction } from './account/actions.js'; -import type { SettingsAction } from './settings/actions.js'; +import type { ConnectionAction } from './connection/actions'; +import type { AccountAction } from './account/actions'; +import type { SettingsAction } from './settings/actions'; export type ReduxState = { account: AccountReduxState, diff --git a/test/components/Account.spec.js b/test/components/Account.spec.js index c9d0891022..c4b50a740c 100644 --- a/test/components/Account.spec.js +++ b/test/components/Account.spec.js @@ -6,30 +6,26 @@ import { shallow } from 'enzyme'; require('../setup/enzyme'); import Account from '../../app/components/Account'; -import type { AccountReduxState } from '../../app/redux/account/reducers'; import type { AccountProps } from '../../app/components/Account'; describe('components/Account', () => { - const state: AccountReduxState = { - accountToken: '1234', - accountHistory: [], - expiry: new Date('2038-01-01').toISOString(), - status: 'none', - error: null, - }; - - const makeProps = (state: AccountReduxState, mergeProps: $Shape<AccountProps>): AccountProps => { + const makeProps = (mergeProps: $Shape<AccountProps>): AccountProps => { const defaultProps: AccountProps = { - account: state, + accountToken: '1234', + accountExpiry: new Date('2038-01-01').toISOString(), + updateAccountExpiry: () => Promise.resolve(), onClose: () => {}, onLogout: () => {}, onBuyMore: () => {}, }; - return Object.assign({}, defaultProps, mergeProps); + return { + ...defaultProps, + ...mergeProps, + }; }; it('should call close callback', (done) => { - const props = makeProps(state, { + const props = makeProps({ onClose: () => done(), }); const component = getComponent(render(props), 'account__close'); @@ -37,7 +33,7 @@ describe('components/Account', () => { }); it('should call logout callback', (done) => { - const props = makeProps(state, { + const props = makeProps({ onLogout: () => done(), }); const component = getComponent(render(props), 'account__logout'); @@ -45,7 +41,7 @@ describe('components/Account', () => { }); it('should call "buy more" callback', (done) => { - const props = makeProps(state, { + const props = makeProps({ onBuyMore: () => done(), }); const component = getComponent(render(props), 'account__buymore'); @@ -53,20 +49,15 @@ describe('components/Account', () => { }); it('should display "out of time" message when account expired', () => { - const expiredState: AccountReduxState = { - accountToken: '1234', - accountHistory: [], - expiry: new Date('2001-01-01').toISOString(), - status: 'none', - error: null, - }; - const props = makeProps(expiredState, {}); + const props = makeProps({ + accountExpiry: new Date('2001-01-01').toISOString(), + }); const component = getComponent(render(props), 'account__out_of_time'); expect(component).to.have.length(1); }); it('should not display "out of time" message when account is active', () => { - const props = makeProps(state, {}); + const props = makeProps({}); const component = getComponent(render(props), 'account__out_of_time'); expect(component).to.have.length(0); }); diff --git a/test/ipc.spec.js b/test/ipc.spec.js index 15009bb639..43d6e9e2f4 100644 --- a/test/ipc.spec.js +++ b/test/ipc.spec.js @@ -1,10 +1,10 @@ // @flow -import Ipc from '../app/lib/jsonrpc-ws-ipc.js'; +import Ipc from '../app/lib/jsonrpc-ws-ipc'; import jsonrpc from 'jsonrpc-lite'; import { expect } from 'chai'; import assert from 'assert'; -import type { JsonRpcMessage } from '../app/lib/jsonrpc-ws-ipc.js'; +import type { JsonRpcMessage } from '../app/lib/jsonrpc-ws-ipc'; describe('The IPC server', () => { it('should send as soon as the websocket connects', () => { |
