summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2020-02-10 16:37:45 +0100
committerOskar Nyberg <oskar@mullvad.net>2020-02-11 09:00:53 +0100
commit83a1b073c6616330fa48468deaa13ade13ed711c (patch)
tree2bf5396b4cd93baa1b551e74b60d8f9a4a5ce493
parentc9b3e7f007b0dd340ea810bca05abef36a325b7f (diff)
downloadmullvadvpn-83a1b073c6616330fa48468deaa13ade13ed711c.tar.xz
mullvadvpn-83a1b073c6616330fa48468deaa13ade13ed711c.zip
Add account expiry warning notification
-rw-r--r--CHANGELOG.md3
-rw-r--r--gui/src/main/index.ts32
-rw-r--r--gui/src/main/notification-controller.ts18
-rw-r--r--gui/src/renderer/components/Account.tsx2
-rw-r--r--gui/src/renderer/components/Connect.tsx2
-rw-r--r--gui/src/renderer/components/NotificationArea.tsx2
-rw-r--r--gui/src/renderer/components/Settings.tsx2
-rw-r--r--gui/src/renderer/containers/ConnectPage.tsx2
-rw-r--r--gui/src/renderer/containers/NotificationAreaContainer.tsx2
-rw-r--r--gui/src/shared/account-expiry.ts (renamed from gui/src/renderer/lib/account-expiry.ts)8
-rw-r--r--gui/test/components/NotificationArea.spec.tsx2
11 files changed, 65 insertions, 10 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6155667724..dfd4ca89b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,8 +24,9 @@ Line wrap the file at 100 chars. Th
## [Unreleased]
### Added
-- Add reconnect button to the desktop app
+- Add reconnect button to the desktop app.
- Add monochrome option for the tray icon on Windows and Linux.
+- Show OS notification when account is close to expiry on desktop platforms.
#### Android
- Add option to enable or disable local network sharing.
diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts
index 4df3b4c7a2..32620c3574 100644
--- a/gui/src/main/index.ts
+++ b/gui/src/main/index.ts
@@ -2,8 +2,10 @@ import { execFile } from 'child_process';
import { app, BrowserWindow, ipcMain, Menu, nativeImage, screen, Tray } from 'electron';
import log from 'electron-log';
import mkdirp from 'mkdirp';
+import moment from 'moment';
import * as path from 'path';
import * as uuid from 'uuid';
+import AccountExpiry from '../shared/account-expiry';
import BridgeSettingsBuilder from '../shared/bridge-settings-builder';
import {
AccountToken,
@@ -145,6 +147,8 @@ class ApplicationMain {
private wireguardPublicKey?: IWireguardPublicKey;
+ private accountExpiryNotificationTimeout?: NodeJS.Timeout;
+
private accountDataCache = new AccountDataCache(
(accountToken) => {
return this.daemonRpc.getAccountData(accountToken);
@@ -155,6 +159,8 @@ class ApplicationMain {
if (this.windowController) {
IpcMainEventChannel.account.notify(this.windowController.webContents, accountData);
}
+
+ this.notifyOfAccountExpiry();
},
);
@@ -1081,6 +1087,11 @@ class ApplicationMain {
private async logout(): Promise<void> {
try {
await this.daemonRpc.setAccount();
+
+ if (this.accountExpiryNotificationTimeout) {
+ global.clearTimeout(this.accountExpiryNotificationTimeout);
+ this.accountExpiryNotificationTimeout = undefined;
+ }
} catch (error) {
log.info(`Failed to logout: ${error.message}`);
@@ -1132,6 +1143,27 @@ class ApplicationMain {
}
}
+ private notifyOfAccountExpiry() {
+ if (this.accountData) {
+ const accountExpiry = new AccountExpiry(this.accountData.expiry, this.locale);
+ if (
+ accountExpiry &&
+ !this.accountExpiryNotificationTimeout &&
+ accountExpiry.willHaveExpiredAt(
+ moment()
+ .add(3, 'days')
+ .toDate(),
+ )
+ ) {
+ this.notificationController.closeToExpiryNotification(accountExpiry);
+ this.accountExpiryNotificationTimeout = global.setTimeout(() => {
+ this.accountExpiryNotificationTimeout = undefined;
+ this.notifyOfAccountExpiry();
+ }, 12 * 60 * 60 * 1000); // Every 12 hours
+ }
+ }
+ }
+
private async updateAccountHistory(): Promise<void> {
try {
this.setAccountHistory(await this.daemonRpc.getAccountHistory());
diff --git a/gui/src/main/notification-controller.ts b/gui/src/main/notification-controller.ts
index 98852029f3..69c0f9ec30 100644
--- a/gui/src/main/notification-controller.ts
+++ b/gui/src/main/notification-controller.ts
@@ -3,6 +3,7 @@ import os from 'os';
import path from 'path';
import { sprintf } from 'sprintf-js';
import config from '../config.json';
+import AccountExpiry from '../shared/account-expiry';
import { TunnelState } from '../shared/daemon-rpc-types';
import { messages } from '../shared/gettext';
@@ -156,6 +157,23 @@ export default class NotificationController {
this.scheduleNotification(notification);
}
+ public closeToExpiryNotification(accountExpiry: AccountExpiry) {
+ const duration = accountExpiry.durationUntilExpiry();
+ const notification = new Notification({
+ title: this.notificationTitle,
+ body: sprintf(
+ // TRANSLATORS: The system notification displayed to the user when the account credit is close to expiry.
+ // TRANSLATORS: Available placeholder:
+ // TRANSLATORS: %(duration)s - remaining time, e.g. "2 days"
+ messages.pgettext('notifications', 'Account credit expires in %(duration)s'),
+ { duration },
+ ),
+ silent: true,
+ icon: this.notificationIcon,
+ });
+ this.scheduleNotification(notification);
+ }
+
public cancelPendingNotifications() {
for (const notification of this.pendingNotifications) {
notification.close();
diff --git a/gui/src/renderer/components/Account.tsx b/gui/src/renderer/components/Account.tsx
index 1390f78370..5b5a393f62 100644
--- a/gui/src/renderer/components/Account.tsx
+++ b/gui/src/renderer/components/Account.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import { Component, Text, View } from 'reactxp';
+import AccountExpiry from '../../shared/account-expiry';
import { messages } from '../../shared/gettext';
-import AccountExpiry from '../lib/account-expiry';
import styles from './AccountStyles';
import * as AppButton from './AppButton';
import ClipboardLabel from './ClipboardLabel';
diff --git a/gui/src/renderer/components/Connect.tsx b/gui/src/renderer/components/Connect.tsx
index 4cb63c6a5b..2308a91dff 100644
--- a/gui/src/renderer/components/Connect.tsx
+++ b/gui/src/renderer/components/Connect.tsx
@@ -1,8 +1,8 @@
import * as React from 'react';
import { Component, Styles, View } from 'reactxp';
import { links } from '../../config.json';
+import AccountExpiry from '../../shared/account-expiry';
import NotificationAreaContainer from '../containers/NotificationAreaContainer';
-import AccountExpiry from '../lib/account-expiry';
import { AuthFailureKind, parseAuthFailure } from '../lib/auth-failure';
import { IConnectionReduxState } from '../redux/connection/reducers';
import { IVersionReduxState } from '../redux/version/reducers';
diff --git a/gui/src/renderer/components/NotificationArea.tsx b/gui/src/renderer/components/NotificationArea.tsx
index c459695a30..243b406518 100644
--- a/gui/src/renderer/components/NotificationArea.tsx
+++ b/gui/src/renderer/components/NotificationArea.tsx
@@ -13,8 +13,8 @@ import {
NotificationTitle,
} from './NotificationBanner';
+import AccountExpiry from '../../shared/account-expiry';
import { ErrorStateCause, TunnelParameterError, TunnelState } from '../../shared/daemon-rpc-types';
-import AccountExpiry from '../lib/account-expiry';
import { parseAuthFailure } from '../lib/auth-failure';
import { IVersionReduxState } from '../redux/version/reducers';
diff --git a/gui/src/renderer/components/Settings.tsx b/gui/src/renderer/components/Settings.tsx
index dc7f875529..ee5cc8c340 100644
--- a/gui/src/renderer/components/Settings.tsx
+++ b/gui/src/renderer/components/Settings.tsx
@@ -1,8 +1,8 @@
import * as React from 'react';
import { Component, Text, View } from 'reactxp';
import { colors, links } from '../../config.json';
+import AccountExpiry from '../../shared/account-expiry';
import { messages } from '../../shared/gettext';
-import AccountExpiry from '../lib/account-expiry';
import * as AppButton from './AppButton';
import * as Cell from './Cell';
import { Container, Layout } from './Layout';
diff --git a/gui/src/renderer/containers/ConnectPage.tsx b/gui/src/renderer/containers/ConnectPage.tsx
index e4c37f19cc..de691b0db9 100644
--- a/gui/src/renderer/containers/ConnectPage.tsx
+++ b/gui/src/renderer/containers/ConnectPage.tsx
@@ -3,10 +3,10 @@ import log from 'electron-log';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { sprintf } from 'sprintf-js';
+import AccountExpiry from '../../shared/account-expiry';
import { messages } from '../../shared/gettext';
import Connect from '../components/Connect';
import withAppContext, { IAppContext } from '../context';
-import AccountExpiry from '../lib/account-expiry';
import { IRelayLocationRedux, RelaySettingsRedux } from '../redux/settings/reducers';
import { IReduxState, ReduxDispatch } from '../redux/store';
diff --git a/gui/src/renderer/containers/NotificationAreaContainer.tsx b/gui/src/renderer/containers/NotificationAreaContainer.tsx
index 814b055d97..f282c48fab 100644
--- a/gui/src/renderer/containers/NotificationAreaContainer.tsx
+++ b/gui/src/renderer/containers/NotificationAreaContainer.tsx
@@ -2,9 +2,9 @@ import { connect } from 'react-redux';
import { shell } from 'electron';
import { links } from '../../config.json';
+import AccountExpiry from '../../shared/account-expiry';
import NotificationArea from '../components/NotificationArea';
import withAppContext, { IAppContext } from '../context';
-import AccountExpiry from '../lib/account-expiry';
import { IReduxState, ReduxDispatch } from '../redux/store';
const mapStateToProps = (state: IReduxState, _props: IAppContext) => ({
diff --git a/gui/src/renderer/lib/account-expiry.ts b/gui/src/shared/account-expiry.ts
index 5393238dcd..8e4c9b2ece 100644
--- a/gui/src/renderer/lib/account-expiry.ts
+++ b/gui/src/shared/account-expiry.ts
@@ -1,6 +1,6 @@
import moment from 'moment';
import { sprintf } from 'sprintf-js';
-import { messages } from '../../shared/gettext';
+import { messages } from './gettext';
export default class AccountExpiry {
private expiry: moment.Moment;
@@ -21,8 +21,12 @@ export default class AccountExpiry {
return this.expiry.format('L LTS');
}
+ public durationUntilExpiry(): string {
+ return this.expiry.fromNow(true);
+ }
+
public remainingTime(): string {
- const duration = this.expiry.fromNow(true);
+ const duration = this.durationUntilExpiry();
return sprintf(
// TRANSLATORS: The remaining time left on the account displayed across the app.
diff --git a/gui/test/components/NotificationArea.spec.tsx b/gui/test/components/NotificationArea.spec.tsx
index 6aa7621724..d50a453c48 100644
--- a/gui/test/components/NotificationArea.spec.tsx
+++ b/gui/test/components/NotificationArea.spec.tsx
@@ -2,8 +2,8 @@ import moment from 'moment';
import * as React from 'react';
import { shallow } from 'enzyme';
import NotificationArea from '../../src/renderer/components/NotificationArea';
+import AccountExpiry from '../../src/shared/account-expiry';
import { AfterDisconnect } from '../../src/shared/daemon-rpc-types';
-import AccountExpiry from '../../src/renderer/lib/account-expiry';
import { expect } from 'chai';
describe('components/NotificationArea', () => {