summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2018-06-12 16:22:45 +0200
committerAndrej Mihajlov <and@mullvad.net>2018-06-12 16:22:45 +0200
commita7da96bc03b3705c4d99bbc55e9f1a04f2030c25 (patch)
tree25f9e6012bad2efc917342450f4ae055deb6fe6e
parent55b5d390bc377b9ff6cdf5a0b1641ea2fae23719 (diff)
parent247e9807b25ac5033ab5cfa55cc40a5201b21523 (diff)
downloadmullvadvpn-a7da96bc03b3705c4d99bbc55e9f1a04f2030c25.tar.xz
mullvadvpn-a7da96bc03b3705c4d99bbc55e9f1a04f2030c25.zip
Merge branch 'refresh-expiry-time'
-rw-r--r--CHANGELOG.md1
-rw-r--r--app/components/Account.js60
-rw-r--r--app/containers/AccountPage.js10
-rw-r--r--app/lib/app-visibility.js28
-rw-r--r--app/lib/backend.js19
-rw-r--r--app/main.js58
-rw-r--r--app/redux/account/actions.js20
-rw-r--r--app/redux/account/reducers.js7
-rw-r--r--app/redux/store.js24
-rw-r--r--test/components/Account.spec.js39
-rw-r--r--test/ipc.spec.js4
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', () => {