summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2020-06-15 20:19:14 +0200
committerOskar Nyberg <oskar@mullvad.net>2020-06-24 11:24:34 +0200
commit60f8742caaf908c3ab1113bbe6903d12ca961aa3 (patch)
treee4aef6a24979c54e9189d6c12f64a99ca8389243 /gui/src
parent2341e2eea669ed29e1146d012fa7a4e5dc9bebf6 (diff)
downloadmullvadvpn-60f8742caaf908c3ab1113bbe6903d12ca961aa3.tar.xz
mullvadvpn-60f8742caaf908c3ab1113bbe6903d12ca961aa3.zip
Add system notification for when account has expired
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/main/index.ts51
-rw-r--r--gui/src/renderer/components/NotificationArea.tsx6
-rw-r--r--gui/src/shared/notifications/account-expired.ts34
-rw-r--r--gui/src/shared/notifications/close-to-account-expiry.ts (renamed from gui/src/shared/notifications/account-expiry.ts)10
-rw-r--r--gui/src/shared/notifications/notification.ts3
5 files changed, 75 insertions, 29 deletions
diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts
index a0e17fd9be..668b975b46 100644
--- a/gui/src/main/index.ts
+++ b/gui/src/main/index.ts
@@ -2,6 +2,7 @@ import { execFile } from 'child_process';
import { app, BrowserWindow, ipcMain, Menu, nativeImage, screen, shell, 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 { hasExpired } from '../shared/account-expiry';
@@ -27,17 +28,18 @@ import { loadTranslations, messages } from '../shared/gettext';
import { SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state';
import { IpcMainEventChannel } from '../shared/ipc-event-channel';
import {
- AccountExpiryNotificationProvider,
- InconsistentVersionNotificationProvider,
- UnsupportedVersionNotificationProvider,
-} from '../shared/notifications/notification';
-import {
backupLogFile,
getLogsDirectory,
getMainLogFile,
getRendererLogFile,
setupLogging,
} from '../shared/logging';
+import {
+ AccountExpiredNotificationProvider,
+ CloseToAccountExpiryNotificationProvider,
+ InconsistentVersionNotificationProvider,
+ UnsupportedVersionNotificationProvider,
+} from '../shared/notifications/notification';
import consumePromise from '../shared/promise';
import { Scheduler } from '../shared/scheduler';
import AccountDataCache from './account-data-cache';
@@ -165,7 +167,7 @@ class ApplicationMain {
private wireguardPublicKey?: IWireguardPublicKey;
- private accountExpiryNotificationTimeout?: NodeJS.Timeout;
+ private accountExpiryNotificationScheduler = new Scheduler();
private accountDataCache = new AccountDataCache(
(accountToken) => {
@@ -178,7 +180,7 @@ class ApplicationMain {
IpcMainEventChannel.account.notify(this.windowController.webContents, accountData);
}
- this.notifyOfAccountExpiry();
+ this.handleAccountExpiry();
},
);
@@ -1165,12 +1167,8 @@ class ApplicationMain {
try {
await this.daemonRpc.setAccount();
- if (this.accountExpiryNotificationTimeout) {
- global.clearTimeout(this.accountExpiryNotificationTimeout);
- this.accountExpiryNotificationTimeout = undefined;
- }
-
this.autoConnectFallbackScheduler.cancel();
+ this.accountExpiryNotificationScheduler.cancel();
} catch (error) {
log.info(`Failed to logout: ${error.message}`);
@@ -1220,19 +1218,30 @@ class ApplicationMain {
}
}
- private notifyOfAccountExpiry() {
+ private handleAccountExpiry() {
if (this.accountData) {
- const notificationProvider = new AccountExpiryNotificationProvider({
+ const expiredNotification = new AccountExpiredNotificationProvider({
+ accountExpiry: this.accountData.expiry,
+ tunnelState: this.tunnelState,
+ });
+ const closeToExpiryNotification = new CloseToAccountExpiryNotificationProvider({
accountExpiry: this.accountData.expiry,
locale: this.locale,
- tooSoon: this.accountExpiryNotificationTimeout !== undefined,
+ tooSoon: this.accountExpiryNotificationScheduler.isRunning,
});
- if (notificationProvider.mayDisplay()) {
- this.notificationController.notify(notificationProvider.getSystemNotification());
- this.accountExpiryNotificationTimeout = global.setTimeout(() => {
- this.accountExpiryNotificationTimeout = undefined;
- this.notifyOfAccountExpiry();
- }, 12 * 60 * 60 * 1000); // Every 12 hours
+
+ if (expiredNotification.mayDisplay()) {
+ this.accountExpiryNotificationScheduler.cancel();
+ this.notificationController.notify(expiredNotification.getSystemNotification());
+ } else if (closeToExpiryNotification.mayDisplay()) {
+ this.notificationController.notify(closeToExpiryNotification.getSystemNotification());
+
+ const twelveHours = 12 * 60 * 60 * 1000;
+ const remainingMilliseconds = moment(this.accountData.expiry).diff(new Date());
+ const delay = Math.min(twelveHours, remainingMilliseconds);
+ this.accountExpiryNotificationScheduler.schedule(() => {
+ this.handleAccountExpiry();
+ }, delay);
}
}
}
diff --git a/gui/src/renderer/components/NotificationArea.tsx b/gui/src/renderer/components/NotificationArea.tsx
index b5153a54aa..a6d4699e6a 100644
--- a/gui/src/renderer/components/NotificationArea.tsx
+++ b/gui/src/renderer/components/NotificationArea.tsx
@@ -4,7 +4,7 @@ import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { Types } from 'reactxp';
import {
- AccountExpiryNotificationProvider,
+ CloseToAccountExpiryNotificationProvider,
BlockWhenDisconnectedNotificationProvider,
ConnectingNotificationProvider,
ErrorNotificationProvider,
@@ -51,7 +51,9 @@ export default function NotificationArea(props: IProps) {
];
if (accountExpiry) {
- notificationProviders.push(new AccountExpiryNotificationProvider({ accountExpiry, locale }));
+ notificationProviders.push(
+ new CloseToAccountExpiryNotificationProvider({ accountExpiry, locale }),
+ );
}
const notificationProvider = notificationProviders.find((notification) =>
diff --git a/gui/src/shared/notifications/account-expired.ts b/gui/src/shared/notifications/account-expired.ts
new file mode 100644
index 0000000000..ea600aa577
--- /dev/null
+++ b/gui/src/shared/notifications/account-expired.ts
@@ -0,0 +1,34 @@
+import { links } from '../../config.json';
+import { hasExpired } from '../account-expiry';
+import { TunnelState } from '../daemon-rpc-types';
+import { messages } from '../gettext';
+import { SystemNotification, SystemNotificationProvider } from './notification';
+
+interface AccountExpiredNotificaitonContext {
+ accountExpiry: string;
+ tunnelState: TunnelState;
+}
+
+export class AccountExpiredNotificationProvider implements SystemNotificationProvider {
+ public constructor(private context: AccountExpiredNotificaitonContext) {}
+
+ public mayDisplay() {
+ // Only show when disconnected since the error state handles this if the connection is closed
+ // due to account expiry.
+ return (
+ this.context.tunnelState.state === 'disconnected' && hasExpired(this.context.accountExpiry)
+ );
+ }
+
+ public getSystemNotification(): SystemNotification {
+ return {
+ message: messages.pgettext(
+ 'notifications',
+ 'You have no more VPN time left on this account.',
+ ),
+ critical: true,
+ presentOnce: { value: true, name: this.constructor.name },
+ action: { type: 'open-url', url: links.purchase, withAuth: true },
+ };
+ }
+}
diff --git a/gui/src/shared/notifications/account-expiry.ts b/gui/src/shared/notifications/close-to-account-expiry.ts
index db536c7405..3cf3d82826 100644
--- a/gui/src/shared/notifications/account-expiry.ts
+++ b/gui/src/shared/notifications/close-to-account-expiry.ts
@@ -2,7 +2,7 @@ import moment from 'moment';
import { sprintf } from 'sprintf-js';
import { links } from '../../config.json';
import { messages } from '../../shared/gettext';
-import { hasExpired, formatRemainingTime } from '../account-expiry';
+import { formatDurationUntilExpiry, formatRemainingTime, hasExpired } from '../account-expiry';
import {
InAppNotification,
InAppNotificationProvider,
@@ -10,15 +10,15 @@ import {
SystemNotificationProvider,
} from './notification';
-interface AccountExpiryContext {
+interface CloseToAccountExpiryNotificationContext {
accountExpiry: string;
locale: string;
tooSoon?: boolean;
}
-export class AccountExpiryNotificationProvider
+export class CloseToAccountExpiryNotificationProvider
implements InAppNotificationProvider, SystemNotificationProvider {
- public constructor(private context: AccountExpiryContext) {}
+ public constructor(private context: CloseToAccountExpiryNotificationContext) {}
public mayDisplay() {
const willHaveExpiredInThreeDays = moment(this.context.accountExpiry).isSameOrBefore(
@@ -37,7 +37,7 @@ export class AccountExpiryNotificationProvider
// TRANSLATORS: %(duration)s - remaining time, e.g. "2 days"
messages.pgettext('notifications', 'Account credit expires in %(duration)s'),
{
- duration: formatRemainingTime(this.context.accountExpiry, this.context.locale),
+ duration: formatDurationUntilExpiry(this.context.accountExpiry, this.context.locale),
},
);
diff --git a/gui/src/shared/notifications/notification.ts b/gui/src/shared/notifications/notification.ts
index ea067ea45b..98b50d4d24 100644
--- a/gui/src/shared/notifications/notification.ts
+++ b/gui/src/shared/notifications/notification.ts
@@ -29,7 +29,8 @@ export interface InAppNotificationProvider extends NotificationProvider {
getInAppNotification(): InAppNotification | undefined;
}
-export * from './account-expiry';
+export * from './account-expired';
+export * from './close-to-account-expiry';
export * from './block-when-disconnected';
export * from './connected';
export * from './connecting';