summaryrefslogtreecommitdiffhomepage
path: root/gui/src/renderer/components
diff options
context:
space:
mode:
Diffstat (limited to 'gui/src/renderer/components')
-rw-r--r--gui/src/renderer/components/Connect.tsx6
-rw-r--r--gui/src/renderer/components/NotificationArea.tsx376
-rw-r--r--gui/src/renderer/components/NotificationBanner.tsx5
3 files changed, 86 insertions, 301 deletions
diff --git a/gui/src/renderer/components/Connect.tsx b/gui/src/renderer/components/Connect.tsx
index f4070c459f..4acab681bc 100644
--- a/gui/src/renderer/components/Connect.tsx
+++ b/gui/src/renderer/components/Connect.tsx
@@ -3,8 +3,8 @@ import { Component, Styles, View } from 'reactxp';
import styled from 'styled-components';
import AccountExpiry from '../../shared/account-expiry';
import ExpiredAccountErrorViewContainer from '../containers/ExpiredAccountErrorViewContainer';
-import NotificationAreaContainer from '../containers/NotificationAreaContainer';
-import { AuthFailureKind, parseAuthFailure } from '../lib/auth-failure';
+import NotificationArea from '../components/NotificationArea';
+import { AuthFailureKind, parseAuthFailure } from '../../shared/auth-failure';
import { LoginState } from '../redux/account/reducers';
import { IConnectionReduxState } from '../redux/connection/reducers';
import { Brand, HeaderBarStyle, SettingsBarButton } from './HeaderBar';
@@ -165,7 +165,7 @@ export default class Connect extends Component<IProps, IState> {
onSelectLocation={this.props.onSelectLocation}
/>
- <NotificationAreaContainer style={styles.notificationArea} />
+ <NotificationArea style={styles.notificationArea} />
</View>
</View>
);
diff --git a/gui/src/renderer/components/NotificationArea.tsx b/gui/src/renderer/components/NotificationArea.tsx
index 01607c3840..bc76ba0885 100644
--- a/gui/src/renderer/components/NotificationArea.tsx
+++ b/gui/src/renderer/components/NotificationArea.tsx
@@ -1,8 +1,23 @@
-import moment from 'moment';
-import * as React from 'react';
-import { Component, Types } from 'reactxp';
-import { sprintf } from 'sprintf-js';
-import { messages } from '../../shared/gettext';
+import { shell } from 'electron';
+import log from 'electron-log';
+import React, { useCallback } from 'react';
+import { useSelector } from 'react-redux';
+import { Types } from 'reactxp';
+import AccountExpiry from '../../shared/account-expiry';
+import {
+ AccountExpiryNotificationProvider,
+ BlockWhenDisconnectedNotificationProvider,
+ ConnectingNotificationProvider,
+ ErrorNotificationProvider,
+ InAppNotificationProvider,
+ InconsistentVersionNotificationProvider,
+ NotificationAction,
+ ReconnectingNotificationProvider,
+ UnsupportedVersionNotificationProvider,
+ UpdateAvailableNotificationProvider,
+} from '../../shared/notifications/notification';
+import { useAppContext } from '../context';
+import { IReduxState } from '../redux/store';
import {
NotificationActions,
NotificationBanner,
@@ -13,313 +28,82 @@ import {
NotificationTitle,
} from './NotificationBanner';
-import AccountExpiry from '../../shared/account-expiry';
-import { ErrorStateCause, TunnelParameterError, TunnelState } from '../../shared/daemon-rpc-types';
-import { parseAuthFailure } from '../lib/auth-failure';
-import { IVersionReduxState } from '../redux/version/reducers';
-
interface IProps {
style?: Types.ViewStyleRuleSet;
- accountExpiry?: AccountExpiry;
- tunnelState: TunnelState;
- version: IVersionReduxState;
- blockWhenDisconnected: boolean;
- onOpenDownloadLink: () => Promise<void>;
- onOpenBuyMoreLink: () => Promise<void>;
}
-type NotificationAreaPresentation =
- | { type: 'failure-unsecured'; reason: string }
- | { type: 'blocking'; reason: string }
- | { type: 'inconsistent-version' }
- | { type: 'unsupported-version'; upgradeVersion: string }
- | { type: 'update-available'; upgradeVersion: string }
- | { type: 'expires-soon'; timeLeft: string };
+export default function NotificationArea(props: IProps) {
+ const accountExpiry = useSelector((state: IReduxState) =>
+ state.account.expiry
+ ? new AccountExpiry(state.account.expiry, state.userInterface.locale)
+ : undefined,
+ );
+ const tunnelState = useSelector((state: IReduxState) => state.connection.status);
+ const version = useSelector((state: IReduxState) => state.version);
+ const blockWhenDisconnected = useSelector(
+ (state: IReduxState) => state.settings.blockWhenDisconnected,
+ );
-type State = NotificationAreaPresentation & {
- visible: boolean;
-};
+ const notificationProviders: InAppNotificationProvider[] = [
+ new ConnectingNotificationProvider({ tunnelState }),
+ new ReconnectingNotificationProvider(tunnelState),
+ new BlockWhenDisconnectedNotificationProvider({ tunnelState, blockWhenDisconnected }),
+ new ErrorNotificationProvider(tunnelState),
+ new InconsistentVersionNotificationProvider({ consistent: version.consistent }),
+ new UnsupportedVersionNotificationProvider(version),
+ new UpdateAvailableNotificationProvider(version),
+ ];
-function getTunnelParameterMessage(err: TunnelParameterError): string {
- switch (err) {
- /// TODO: once bridge constraints can be set, add a more descriptive error message
- case 'no_matching_bridge_relay':
- case 'no_matching_relay':
- return messages.pgettext(
- 'in-app-notifications',
- 'No relay server matches the current settings. You can try changing the location or the relay settings.',
- );
- case 'no_wireguard_key':
- return messages.pgettext(
- 'in-app-notifications',
- 'Valid WireGuard key is missing. Manage keys under Advanced settings.',
- );
- case 'custom_tunnel_host_resultion_error':
- return messages.pgettext(
- 'in-app-notifications',
- 'Failed to resolve host of custom tunnel. Consider changing the settings',
- );
+ if (accountExpiry) {
+ notificationProviders.push(new AccountExpiryNotificationProvider({ accountExpiry }));
}
-}
-function getErrorCauseMessage(blockReason: ErrorStateCause): string {
- switch (blockReason.reason) {
- case 'auth_failed':
- return parseAuthFailure(blockReason.details).message;
- case 'ipv6_unavailable':
- return messages.pgettext(
- 'in-app-notifications',
- 'Could not configure IPv6, please enable it on your system or disable it in the app',
- );
- case 'set_firewall_policy_error': {
- let extraMessage = null;
- switch (process.platform) {
- case 'linux':
- extraMessage = messages.pgettext('in-app-notifications', 'Your kernel may be outdated');
- break;
- case 'win32':
- extraMessage = messages.pgettext(
- 'in-app-notifications',
- 'This might be caused by third party security software',
- );
- break;
- }
- return `${messages.pgettext(
- 'in-app-notifications',
- 'Failed to apply firewall rules. The device might currently be unsecured',
- )}${extraMessage ? '. ' + extraMessage : ''}`;
- }
- case 'set_dns_error':
- return messages.pgettext('in-app-notifications', 'Failed to set system DNS server');
- case 'start_tunnel_error':
- return messages.pgettext('in-app-notifications', 'Failed to start tunnel connection');
- case 'tunnel_parameter_error':
- return getTunnelParameterMessage(blockReason.details);
- case 'is_offline':
- return messages.pgettext(
- 'in-app-notifications',
- 'This device is offline, no tunnels can be established',
+ const notificationProvider = notificationProviders.find((notification) =>
+ notification.mayDisplay(),
+ );
+
+ if (notificationProvider) {
+ const notification = notificationProvider.getInAppNotification();
+
+ if (notification) {
+ return (
+ <NotificationBanner style={props.style} visible>
+ <NotificationIndicator type={notification.indicator} />
+ <NotificationContent>
+ <NotificationTitle>{notification.title}</NotificationTitle>
+ <NotificationSubtitle>{notification.subtitle}</NotificationSubtitle>
+ </NotificationContent>
+ {notification.action && <NotificationActionWrapper action={notification.action} />}
+ </NotificationBanner>
);
- case 'tap_adapter_problem':
- return messages.pgettext(
- 'in-app-notifications',
- "Unable to detect a working TAP adapter on this device. If you've disabled it, enable it again. Otherwise, please reinstall the app",
+ } else {
+ log.error(
+ `Notification providers mayDisplay() returned true but getInAppNotification() returned undefined for ${notificationProvider.constructor.name}`,
);
+ }
}
-}
-function capitalizeFirstLetter(inputString: string): string {
- return inputString.charAt(0).toUpperCase() + inputString.slice(1);
+ return <NotificationBanner style={props.style} visible={false} />;
}
-export default class NotificationArea extends Component<IProps, State> {
- public static getDerivedStateFromProps(props: IProps, state: State) {
- const { accountExpiry, blockWhenDisconnected, tunnelState, version } = props;
-
- switch (tunnelState.state) {
- case 'connecting':
- return {
- visible: true,
- type: 'blocking',
- reason: '',
- };
-
- case 'error':
- if (tunnelState.details.isBlocking) {
- return {
- visible: true,
- type: 'blocking',
- reason: getErrorCauseMessage(tunnelState.details.cause),
- };
- } else {
- return {
- visible: true,
- type: 'failure-unsecured',
- reason: getErrorCauseMessage(tunnelState.details.cause),
- };
- }
-
- case 'disconnecting':
- if (tunnelState.details === 'reconnect') {
- return {
- visible: true,
- type: 'blocking',
- reason: '',
- };
- }
- // fallthrough
-
- case 'disconnected':
- if (blockWhenDisconnected) {
- return {
- visible: true,
- type: 'blocking',
- reason: messages.pgettext('in-app-notifications', '"Always require VPN" is enabled.'),
- };
- }
- // fallthrough
-
- default:
- if (!version.consistent) {
- return {
- visible: true,
- type: 'inconsistent-version',
- };
- }
-
- if (!version.supported && version.nextUpgrade) {
- return {
- visible: true,
- type: 'unsupported-version',
- upgradeVersion: version.nextUpgrade,
- };
- }
-
- if (version.nextUpgrade && version.nextUpgrade !== version.current) {
- return {
- visible: true,
- type: 'update-available',
- upgradeVersion: version.nextUpgrade,
- };
- }
+interface INotificationActionWrapperProps {
+ action: NotificationAction;
+}
- if (accountExpiry && accountExpiry.willHaveExpiredAt(moment().add(3, 'days').toDate())) {
- return {
- visible: true,
- type: 'expires-soon',
- timeLeft: capitalizeFirstLetter(accountExpiry.remainingTime()),
- };
- }
+function NotificationActionWrapper(props: INotificationActionWrapperProps) {
+ const { openLinkWithAuth } = useAppContext();
- return {
- ...state,
- visible: false,
- };
+ const handlePress = useCallback(() => {
+ if (props.action.withAuth) {
+ return openLinkWithAuth(props.action.url);
+ } else {
+ return shell.openExternal(props.action.url);
}
- }
+ }, []);
- public state: State = {
- type: 'blocking',
- reason: '',
- visible: false,
- };
-
- public render() {
- return (
- <NotificationBanner style={this.props.style} visible={this.state.visible}>
- {this.state.type === 'failure-unsecured' && (
- <React.Fragment>
- <NotificationIndicator type={'error'} />
- <NotificationContent>
- <NotificationTitle>
- {messages.pgettext('in-app-notifications', 'YOU MIGHT BE LEAKING NETWORK TRAFFIC')}
- </NotificationTitle>
- <NotificationSubtitle>
- {messages.pgettext(
- 'in-app-notifications',
- 'Failed to block all network traffic. Please troubleshoot or report the problem to us.',
- )}
- </NotificationSubtitle>
- </NotificationContent>
- </React.Fragment>
- )}
-
- {this.state.type === 'blocking' && (
- <React.Fragment>
- <NotificationIndicator type={'error'} />
- <NotificationContent>
- <NotificationTitle>
- {messages.pgettext('in-app-notifications', 'BLOCKING INTERNET')}
- </NotificationTitle>
- <NotificationSubtitle>{this.state.reason}</NotificationSubtitle>
- </NotificationContent>
- </React.Fragment>
- )}
-
- {this.state.type === 'inconsistent-version' && (
- <React.Fragment>
- <NotificationIndicator type={'error'} />
- <NotificationContent>
- <NotificationTitle>
- {messages.pgettext('in-app-notifications', 'INCONSISTENT VERSION')}
- </NotificationTitle>
- <NotificationSubtitle>
- {messages.pgettext(
- 'in-app-notifications',
- 'Inconsistent internal version information, please restart the app',
- )}
- </NotificationSubtitle>
- </NotificationContent>
- </React.Fragment>
- )}
-
- {this.state.type === 'unsupported-version' && (
- <React.Fragment>
- <NotificationIndicator type={'error'} />
- <NotificationContent>
- <NotificationTitle>
- {messages.pgettext('in-app-notifications', 'UNSUPPORTED VERSION')}
- </NotificationTitle>
- <NotificationSubtitle>
- {sprintf(
- // TRANSLATORS: The in-app banner displayed to the user when the running app becomes unsupported.
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(version)s - the newest available version of the app
- messages.pgettext(
- 'in-app-notifications',
- 'You are running an unsupported app version. Please upgrade to %(version)s now to ensure your security',
- ),
- { version: this.state.upgradeVersion },
- )}
- </NotificationSubtitle>
- </NotificationContent>
- <NotificationActions>
- <NotificationOpenLinkAction onPress={this.props.onOpenDownloadLink} />
- </NotificationActions>
- </React.Fragment>
- )}
-
- {this.state.type === 'update-available' && (
- <React.Fragment>
- <NotificationIndicator type={'warning'} />
- <NotificationContent>
- <NotificationTitle>
- {messages.pgettext('in-app-notifications', 'UPDATE AVAILABLE')}
- </NotificationTitle>
- <NotificationSubtitle>
- {sprintf(
- // TRANSLATORS: The in-app banner displayed to the user when the app update is available.
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(version)s - the newest available version of the app
- messages.pgettext(
- 'in-app-notifications',
- 'Install Mullvad VPN (%(version)s) to stay up to date',
- ),
- { version: this.state.upgradeVersion },
- )}
- </NotificationSubtitle>
- </NotificationContent>
- <NotificationActions>
- <NotificationOpenLinkAction onPress={this.props.onOpenDownloadLink} />
- </NotificationActions>
- </React.Fragment>
- )}
-
- {this.state.type === 'expires-soon' && (
- <React.Fragment>
- <NotificationIndicator type={'warning'} />
- <NotificationContent>
- <NotificationTitle>
- {messages.pgettext('in-app-notifications', 'ACCOUNT CREDIT EXPIRES SOON')}
- </NotificationTitle>
- <NotificationSubtitle>{this.state.timeLeft}</NotificationSubtitle>
- </NotificationContent>
- <NotificationActions>
- <NotificationOpenLinkAction onPress={this.props.onOpenBuyMoreLink} />
- </NotificationActions>
- </React.Fragment>
- )}
- </NotificationBanner>
- );
- }
+ return (
+ <NotificationActions>
+ <NotificationOpenLinkAction onPress={handlePress} />
+ </NotificationActions>
+ );
}
diff --git a/gui/src/renderer/components/NotificationBanner.tsx b/gui/src/renderer/components/NotificationBanner.tsx
index 5831676fcf..207c3f6081 100644
--- a/gui/src/renderer/components/NotificationBanner.tsx
+++ b/gui/src/renderer/components/NotificationBanner.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
import { Animated, Button, Component, Styles, Text, Types, UserInterface, View } from 'reactxp';
import { colors } from '../../config.json';
+import { InAppNotificationIndicatorType } from '../../shared/notifications/notification';
import consumePromise from '../../shared/promise';
import { BlockingButton } from './AppButton';
import ImageView from './ImageView';
@@ -151,7 +152,7 @@ export class NotificationActions extends Component<INotificationActionsProps> {
}
interface INotificationIndicatorProps {
- type: 'success' | 'warning' | 'error';
+ type: InAppNotificationIndicatorType;
children?: React.ReactNode;
}
@@ -162,7 +163,7 @@ export class NotificationIndicator extends Component<INotificationIndicatorProps
}
interface INotificationBannerProps {
- children: React.ReactNode; // Array<NotificationContent | NotificationActions>,
+ children?: React.ReactNode; // Array<NotificationContent | NotificationActions>,
style?: Types.ViewStyleRuleSet;
visible: boolean;
animationDuration: number;