summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2023-02-21 18:46:12 +0100
committerOskar Nyberg <oskar@mullvad.net>2023-02-21 18:46:12 +0100
commit26a81c00ba35e12af86d48985de0cfc98032a3fe (patch)
tree6a742e4f548772b107f5b9ce71b2847244cbbca6
parent68b96d792092acf0e5127684201a85d7628c0632 (diff)
parent16d8551c7119aa14364e90253b051b7a26ed7653 (diff)
downloadmullvadvpn-26a81c00ba35e12af86d48985de0cfc98032a3fe.tar.xz
mullvadvpn-26a81c00ba35e12af86d48985de0cfc98032a3fe.zip
Merge branch 'improve-in-app-notifications'
-rw-r--r--CHANGELOG.md1
-rw-r--r--gui/locales/messages.pot53
-rw-r--r--gui/src/renderer/components/KeyboardNavigation.tsx41
-rw-r--r--gui/src/renderer/components/NavigationBar.tsx11
-rw-r--r--gui/src/renderer/components/NotificationArea.tsx97
-rw-r--r--gui/src/renderer/components/NotificationBanner.tsx37
-rw-r--r--gui/src/renderer/components/Settings.tsx2
-rw-r--r--gui/src/renderer/components/select-location/SelectLocation.tsx2
-rw-r--r--gui/src/shared/localization-contexts.ts3
-rw-r--r--gui/src/shared/notifications/error.ts74
-rw-r--r--gui/src/shared/notifications/notification.ts14
11 files changed, 278 insertions, 57 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd070d284b..5839eba589 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,7 @@ Line wrap the file at 100 chars. Th
- Add Kyber1024 KEM algorithm into the Post-Quantum secure key exchange algorithm. This means the
Quantum-resistant-tunnels feature now mixes both Classic McEliece and Kyber for added protection.
- Add notification dot to tray icon and system notification throttling.
+- Add troubleshooting information to some in-app notifications.
### Changed
- Update the Post-Quantum secure key exchange gRPC client to use the stabilized
diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot
index 3fe9fc4570..da6569a449 100644
--- a/gui/locales/messages.pot
+++ b/gui/locales/messages.pot
@@ -215,6 +215,9 @@ msgstr ""
msgid "This setting increases latency. Use only if needed."
msgstr ""
+msgid "Troubleshoot"
+msgstr ""
+
msgid "Try a different search."
msgstr ""
@@ -602,6 +605,10 @@ msgctxt "in-app-notifications"
msgid "Please quit and restart the app."
msgstr ""
+msgctxt "in-app-notifications"
+msgid "Send problem report"
+msgstr ""
+
#. The in-app banner displayed to the user when the app beta update is
#. available.
#. Available placeholders:
@@ -779,7 +786,7 @@ msgid "Lockdown mode active, connection blocked"
msgstr ""
msgctxt "notifications"
-msgid "No servers in your selected location match your settings."
+msgid "No servers match your settings, try changing server or other settings."
msgstr ""
msgctxt "notifications"
@@ -795,7 +802,7 @@ msgid "Unable to apply firewall rules."
msgstr ""
msgctxt "notifications"
-msgid "Unable to apply firewall rules. Try disabling any third-party antivirus or security software."
+msgid "Unable to apply firewall rules. Try temporarily disabling any third-party antivirus or security software."
msgstr ""
msgctxt "notifications"
@@ -807,7 +814,7 @@ msgid "Unable to block all network traffic. Please troubleshoot or send a proble
msgstr ""
msgctxt "notifications"
-msgid "Unable to block all network traffic. Try disabling any third-party antivirus or security software or send a problem report."
+msgid "Unable to block all network traffic. Try temporarily disabling any third-party antivirus or security software or send a problem report."
msgstr ""
msgctxt "notifications"
@@ -1231,6 +1238,46 @@ msgctxt "tray-icon-tooltip"
msgid "Connecting. %(location)s"
msgstr ""
+msgctxt "troubleshoot"
+msgid "If these steps do not work please send a problem report."
+msgstr ""
+
+msgctxt "troubleshoot"
+msgid "Make sure you have NF tables support."
+msgstr ""
+
+msgctxt "troubleshoot"
+msgid "This can happen because the kernel is old, or if you have removed a kernel."
+msgstr ""
+
+msgctxt "troubleshoot"
+msgid "This error can happen when something other than Mullvad is actively updating the DNS."
+msgstr ""
+
+msgctxt "troubleshoot"
+msgid "Try reconnecting."
+msgstr ""
+
+msgctxt "troubleshoot"
+msgid "Try restarting your device."
+msgstr ""
+
+msgctxt "troubleshoot"
+msgid "Try to turn Wi-Fi Calling off in the FaceTime app settings and restart the Mac."
+msgstr ""
+
+msgctxt "troubleshoot"
+msgid "Unable to communicate with Mullvad kernel driver."
+msgstr ""
+
+msgctxt "troubleshoot"
+msgid "Uninstall or disable other DNS, networking and ads/website blocking apps."
+msgstr ""
+
+msgctxt "troubleshoot"
+msgid "Update your kernel."
+msgstr ""
+
msgctxt "tunnel-control"
msgid "Secure my connection"
msgstr ""
diff --git a/gui/src/renderer/components/KeyboardNavigation.tsx b/gui/src/renderer/components/KeyboardNavigation.tsx
index 8e8bae5faf..b98f57b4ce 100644
--- a/gui/src/renderer/components/KeyboardNavigation.tsx
+++ b/gui/src/renderer/components/KeyboardNavigation.tsx
@@ -11,7 +11,7 @@ interface IKeyboardNavigationProps {
// Listens for and handles keyboard shortcuts
export default function KeyboardNavigation(props: IKeyboardNavigationProps) {
const history = useHistory();
- const [backAction, setBackAction] = useState<IBackActionConfiguration>();
+ const [backAction, setBackAction] = useState<BackActionFn>();
const location = useLocation();
const handleKeyDown = useCallback(
@@ -22,7 +22,7 @@ export default function KeyboardNavigation(props: IKeyboardNavigationProps) {
if (event.shiftKey) {
history.pop(true);
} else {
- backAction?.action();
+ backAction?.();
}
}
}
@@ -38,18 +38,12 @@ export default function KeyboardNavigation(props: IKeyboardNavigationProps) {
return <BackActionTracker registerBackAction={setBackAction}>{props.children}</BackActionTracker>;
}
-type BackActionIcon = 'back' | 'close';
type BackActionFn = () => void;
-interface IBackActionConfiguration {
- icon: BackActionIcon;
- action: BackActionFn;
-}
-
interface IBackActionContext {
- parentBackAction?: IBackActionConfiguration;
- registerBackAction: (backAction: IBackActionConfiguration) => void;
- removeBackAction: (backAction: IBackActionConfiguration) => void;
+ parentBackAction?: BackActionFn;
+ registerBackAction: (backAction: BackActionFn) => void;
+ removeBackAction: (backAction: BackActionFn) => void;
}
export const BackActionContext = React.createContext<IBackActionContext>({
@@ -63,7 +57,6 @@ export const BackActionContext = React.createContext<IBackActionContext>({
interface IBackActionProps {
disabled?: boolean;
- icon?: BackActionIcon;
action: BackActionFn;
children: React.ReactNode;
}
@@ -72,13 +65,9 @@ interface IBackActionProps {
// either by pressing the back button in the navigation bar or by pressing escape.
export function BackAction(props: IBackActionProps) {
const backActionContext = useContext(BackActionContext);
- const [childrenBackAction, setChildrenBackAction] = useState<IBackActionConfiguration>();
+ const [childrenBackAction, setChildrenBackAction] = useState<BackActionFn>();
- const parentBackAction = useMemo<IBackActionConfiguration>(
- () => ({ icon: props.icon ?? 'back', action: props.action }),
- [props.icon, props.action],
- );
- const backActionConfiguration = childrenBackAction ?? parentBackAction;
+ const backActionConfiguration = childrenBackAction ?? props.action;
// Every time the action or the disabled property changes the action needs to be reregistered.
useEffect((): (() => void) | void => {
@@ -91,29 +80,27 @@ export function BackAction(props: IBackActionProps) {
// Every back action keeps track of the back actions in its subtree. This makes it possible to
// always use the action furthest down in the tree.
return (
- <BackActionTracker
- registerBackAction={setChildrenBackAction}
- parentBackAction={parentBackAction}>
+ <BackActionTracker registerBackAction={setChildrenBackAction} parentBackAction={props.action}>
{props.children}
</BackActionTracker>
);
}
interface IBackActionTracker {
- parentBackAction?: IBackActionConfiguration;
- registerBackAction: (backAction: IBackActionConfiguration | undefined) => void;
+ parentBackAction?: BackActionFn;
+ registerBackAction: (backAction: BackActionFn | undefined) => void;
children: React.ReactNode;
}
// This component keeps track of all registered back actions in it's subtree and reports one of them
// to it's parent.
function BackActionTracker(props: IBackActionTracker) {
- const [backActions, setBackActions] = useState<Array<IBackActionConfiguration>>([]);
+ const [backActions, setBackActions] = useState<Array<BackActionFn>>([]);
- const registerBackAction = useCallback((backAction: IBackActionConfiguration) => {
+ const registerBackAction = useCallback((backAction: BackActionFn) => {
setBackActions((backActions) => [...backActions, backAction]);
}, []);
- const removeBackAction = useCallback((backAction: IBackActionConfiguration) => {
+ const removeBackAction = useCallback((backAction: BackActionFn) => {
setBackActions((backActions) => backActions.filter((action) => action !== backAction));
}, []);
const backActionContext = useMemo(
@@ -121,7 +108,7 @@ function BackActionTracker(props: IBackActionTracker) {
[backActions],
);
- useEffect(() => props.registerBackAction(backActions.at(0)), [backActions]);
+ useEffect(() => props.registerBackAction(() => backActions.at(0)), [backActions]);
return (
<BackActionContext.Provider value={backActionContext}>
diff --git a/gui/src/renderer/components/NavigationBar.tsx b/gui/src/renderer/components/NavigationBar.tsx
index 317a9bd26e..56c3d2b8a3 100644
--- a/gui/src/renderer/components/NavigationBar.tsx
+++ b/gui/src/renderer/components/NavigationBar.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useContext, useEffect, useLayoutEffect, useRef } from 'react';
+import React, { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
import { colors } from '../../config.json';
import { messages } from '../../shared/gettext';
@@ -184,13 +184,14 @@ export const TitleBarItem = React.memo(function TitleBarItemT(props: ITitleBarIt
});
export function BackBarItem() {
+ const history = useHistory();
+ const backIcon = useMemo(() => history.length > 2, []);
const { parentBackAction } = useContext(BackActionContext);
- const iconSource = parentBackAction?.icon === 'back' ? 'icon-back' : 'icon-close-down';
- const ariaLabel =
- parentBackAction?.icon === 'back' ? messages.gettext('Back') : messages.gettext('Close');
+ const iconSource = backIcon ? 'icon-back' : 'icon-close-down';
+ const ariaLabel = backIcon ? messages.gettext('Back') : messages.gettext('Close');
return (
- <StyledBackBarItemButton aria-label={ariaLabel} onClick={parentBackAction?.action}>
+ <StyledBackBarItemButton aria-label={ariaLabel} onClick={parentBackAction}>
<StyledBackBarItemIcon source={iconSource} tintColor={colors.white40} width={24} />
</StyledBackBarItemButton>
);
diff --git a/gui/src/renderer/components/NotificationArea.tsx b/gui/src/renderer/components/NotificationArea.tsx
index 317cc1e923..4923bacc7c 100644
--- a/gui/src/renderer/components/NotificationArea.tsx
+++ b/gui/src/renderer/components/NotificationArea.tsx
@@ -1,21 +1,29 @@
-import { useCallback } from 'react';
+import { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
+import styled from 'styled-components';
+import { colors } from '../../config.json';
+import { messages } from '../../shared/gettext';
import log from '../../shared/logging';
import {
BlockWhenDisconnectedNotificationProvider,
CloseToAccountExpiryNotificationProvider,
ConnectingNotificationProvider,
ErrorNotificationProvider,
+ InAppNotificationAction,
InAppNotificationProvider,
+ InAppNotificationTroubleshootInfo,
InconsistentVersionNotificationProvider,
- NotificationAction,
ReconnectingNotificationProvider,
UnsupportedVersionNotificationProvider,
UpdateAvailableNotificationProvider,
} from '../../shared/notifications/notification';
import { useAppContext } from '../context';
+import { transitions, useHistory } from '../lib/history';
+import { RoutePath } from '../lib/routes';
import { IReduxState } from '../redux/store';
+import * as AppButton from './AppButton';
+import { ModalAlert, ModalAlertType, ModalMessage } from './Modal';
import {
NotificationActions,
NotificationBanner,
@@ -24,6 +32,7 @@ import {
NotificationOpenLinkAction,
NotificationSubtitle,
NotificationTitle,
+ NotificationTroubleshootDialogAction,
} from './NotificationBanner';
interface IProps {
@@ -92,24 +101,92 @@ export default function NotificationArea(props: IProps) {
return <NotificationBanner className={props.className} aria-hidden={true} />;
}
+const TroubleshootList = styled.ul({
+ listStyle: 'disc outside',
+ paddingLeft: '20px',
+ color: colors.white80,
+});
+
interface INotificationActionWrapperProps {
- action: NotificationAction;
+ action: InAppNotificationAction;
}
function NotificationActionWrapper(props: INotificationActionWrapperProps) {
+ const history = useHistory();
const { openLinkWithAuth, openUrl } = useAppContext();
+ const [troubleshootInfo, setTroubleshootInfo] = useState<InAppNotificationTroubleshootInfo>();
const handleClick = useCallback(() => {
- if (props.action.withAuth) {
- return openLinkWithAuth(props.action.url);
- } else {
- return openUrl(props.action.url);
+ if (props.action) {
+ switch (props.action.type) {
+ case 'open-url':
+ if (props.action.withAuth) {
+ return openLinkWithAuth(props.action.url);
+ } else {
+ return openUrl(props.action.url);
+ }
+ case 'troubleshoot-dialog':
+ setTroubleshootInfo(props.action.troubleshoot);
+ break;
+ }
}
+
+ return Promise.resolve();
+ }, [props.action]);
+
+ const goToProblemReport = useCallback(() => {
+ setTroubleshootInfo(undefined);
+ history.push(RoutePath.problemReport, { transition: transitions.show });
}, []);
+ const closeTroubleshootInfo = useCallback(() => setTroubleshootInfo(undefined), []);
+
+ let actionComponent: React.ReactElement | undefined;
+ if (props.action) {
+ switch (props.action.type) {
+ case 'open-url':
+ actionComponent = <NotificationOpenLinkAction onClick={handleClick} />;
+ break;
+ case 'troubleshoot-dialog':
+ actionComponent = (
+ <>
+ <NotificationTroubleshootDialogAction onClick={handleClick} />
+ </>
+ );
+ break;
+ }
+ }
+
return (
- <NotificationActions>
- <NotificationOpenLinkAction onClick={handleClick} />
- </NotificationActions>
+ <>
+ <NotificationActions>{actionComponent}</NotificationActions>
+ <ModalAlert
+ isOpen={troubleshootInfo !== undefined}
+ type={ModalAlertType.info}
+ buttons={[
+ <AppButton.GreenButton key="problem-report" onClick={goToProblemReport}>
+ {messages.pgettext('in-app-notifications', 'Send problem report')}
+ </AppButton.GreenButton>,
+ <AppButton.BlueButton key="back" onClick={closeTroubleshootInfo}>
+ {messages.gettext('Back')}
+ </AppButton.BlueButton>,
+ ]}
+ close={closeTroubleshootInfo}>
+ <ModalMessage>{troubleshootInfo?.details}</ModalMessage>
+ <ModalMessage>
+ <TroubleshootList>
+ {troubleshootInfo?.steps.map((step) => (
+ <li key={step}>{step}</li>
+ ))}
+ </TroubleshootList>
+ </ModalMessage>
+ <ModalMessage>
+ {messages.pgettext(
+ 'troubleshoot',
+ 'If these steps do not work please send a problem report.',
+ )}
+ </ModalMessage>
+ </ModalAlert>
+ </>
);
}
diff --git a/gui/src/renderer/components/NotificationBanner.tsx b/gui/src/renderer/components/NotificationBanner.tsx
index 9f8c63e4b2..4f20d315c8 100644
--- a/gui/src/renderer/components/NotificationBanner.tsx
+++ b/gui/src/renderer/components/NotificationBanner.tsx
@@ -26,7 +26,7 @@ export function NotificationSubtitle(props: INotificationSubtitleProps) {
return React.Children.count(props.children) > 0 ? <NotificationSubtitleText {...props} /> : null;
}
-export const NotificationOpenLinkActionButton = styled(AppButton.SimpleButton)({
+export const NotificationActionButton = styled(AppButton.SimpleButton)({
flex: 1,
justifyContent: 'center',
cursor: 'default',
@@ -36,20 +36,25 @@ export const NotificationOpenLinkActionButton = styled(AppButton.SimpleButton)({
});
export const NotificationOpenLinkActionIcon = styled(ImageView)({
- [NotificationOpenLinkActionButton + ':hover &']: {
+ [NotificationActionButton + ':hover &']: {
+ backgroundColor: colors.white80,
+ },
+});
+
+export const NotificationTroubleshootDialogActionIcon = styled(ImageView)({
+ [NotificationActionButton + ':hover &']: {
backgroundColor: colors.white80,
},
});
interface INotifcationOpenLinkActionProps {
onClick: () => Promise<void>;
- children?: React.ReactNode;
}
export function NotificationOpenLinkAction(props: INotifcationOpenLinkActionProps) {
return (
<AppButton.BlockingButton onClick={props.onClick}>
- <NotificationOpenLinkActionButton
+ <NotificationActionButton
aria-describedby={NOTIFICATION_AREA_ID}
aria-label={messages.gettext('Open URL')}>
<NotificationOpenLinkActionIcon
@@ -58,11 +63,33 @@ export function NotificationOpenLinkAction(props: INotifcationOpenLinkActionProp
tintColor={colors.white60}
source="icon-extLink"
/>
- </NotificationOpenLinkActionButton>
+ </NotificationActionButton>
</AppButton.BlockingButton>
);
}
+interface INotifcationTroubleshootDialogActionProps {
+ onClick: () => Promise<void>;
+}
+
+export function NotificationTroubleshootDialogAction(
+ props: INotifcationTroubleshootDialogActionProps,
+) {
+ return (
+ <NotificationActionButton
+ aria-describedby={NOTIFICATION_AREA_ID}
+ aria-label={messages.gettext('Troubleshoot')}
+ onClick={props.onClick}>
+ <NotificationOpenLinkActionIcon
+ height={12}
+ width={12}
+ tintColor={colors.white60}
+ source="icon-info"
+ />
+ </NotificationActionButton>
+ );
+}
+
export const NotificationContent = styled.div.attrs({ id: NOTIFICATION_AREA_ID })({
display: 'flex',
flexDirection: 'column',
diff --git a/gui/src/renderer/components/Settings.tsx b/gui/src/renderer/components/Settings.tsx
index 76b4d6b4fd..1a316f81bd 100644
--- a/gui/src/renderer/components/Settings.tsx
+++ b/gui/src/renderer/components/Settings.tsx
@@ -38,7 +38,7 @@ export default function Support() {
const showSubSettings = loginState.type === 'ok' && connectedToDaemon;
return (
- <BackAction icon="close" action={history.pop}>
+ <BackAction action={history.pop}>
<Layout>
<SettingsContainer>
<NavigationContainer>
diff --git a/gui/src/renderer/components/select-location/SelectLocation.tsx b/gui/src/renderer/components/select-location/SelectLocation.tsx
index 9e9c60b7a5..bc397506a8 100644
--- a/gui/src/renderer/components/select-location/SelectLocation.tsx
+++ b/gui/src/renderer/components/select-location/SelectLocation.tsx
@@ -117,7 +117,7 @@ export default function SelectLocation() {
const showProvidersFilter = providers.length > 0;
const showFilters = showOwnershipFilter || showProvidersFilter;
return (
- <BackAction icon="close" action={onClose}>
+ <BackAction action={onClose}>
<Layout>
<SettingsContainer>
<NavigationContainer>
diff --git a/gui/src/shared/localization-contexts.ts b/gui/src/shared/localization-contexts.ts
index 8e8c3930be..d342a4210e 100644
--- a/gui/src/shared/localization-contexts.ts
+++ b/gui/src/shared/localization-contexts.ts
@@ -33,4 +33,5 @@ export type LocalizationContexts =
| 'support-view'
| 'select-language-nav'
| 'tray-icon-context-menu'
- | 'tray-icon-tooltip';
+ | 'tray-icon-tooltip'
+ | 'troubleshoot';
diff --git a/gui/src/shared/notifications/error.ts b/gui/src/shared/notifications/error.ts
index 67267a494e..9ca0921bf6 100644
--- a/gui/src/shared/notifications/error.ts
+++ b/gui/src/shared/notifications/error.ts
@@ -11,6 +11,7 @@ import {
import { messages } from '../gettext';
import {
InAppNotification,
+ InAppNotificationAction,
InAppNotificationProvider,
SystemNotification,
SystemNotificationCategory,
@@ -77,6 +78,7 @@ export class ErrorNotificationProvider
? messages.pgettext('in-app-notifications', 'NETWORK TRAFFIC MIGHT BE LEAKING')
: messages.pgettext('in-app-notifications', 'BLOCKING INTERNET'),
subtitle,
+ action: getActions(this.context.tunnelState.details) ?? undefined,
};
} else {
return undefined;
@@ -91,7 +93,7 @@ function getMessage(errorState: ErrorState): string {
case 'win32':
return messages.pgettext(
'notifications',
- 'Unable to block all network traffic. Try disabling any third-party antivirus or security software or send a problem report.',
+ 'Unable to block all network traffic. Try temporarily disabling any third-party antivirus or security software or send a problem report.',
);
case 'linux':
return messages.pgettext(
@@ -141,7 +143,7 @@ function getMessage(errorState: ErrorState): string {
case 'win32':
return messages.pgettext(
'notifications',
- 'Unable to apply firewall rules. Try disabling any third-party antivirus or security software.',
+ 'Unable to apply firewall rules. Try temporarily disabling any third-party antivirus or security software.',
);
case 'linux':
return messages.pgettext(
@@ -184,7 +186,7 @@ function getTunnelParameterMessage(error: TunnelParameterError): string {
case TunnelParameterError.noMatchingRelay:
return messages.pgettext(
'notifications',
- 'No servers in your selected location match your settings.',
+ 'No servers match your settings, try changing server or other settings.',
);
case TunnelParameterError.noWireguardKey:
return sprintf(
@@ -203,3 +205,69 @@ function getTunnelParameterMessage(error: TunnelParameterError): string {
);
}
}
+
+function getActions(errorState: ErrorState): InAppNotificationAction | void {
+ const platform = process.platform ?? window.env.platform;
+
+ if (errorState.cause === ErrorStateCause.setFirewallPolicyError && platform === 'linux') {
+ return {
+ type: 'troubleshoot-dialog',
+ troubleshoot: {
+ details: messages.pgettext(
+ 'troubleshoot',
+ 'This can happen because the kernel is old, or if you have removed a kernel.',
+ ),
+ steps: [
+ messages.pgettext('troubleshoot', 'Update your kernel.'),
+ messages.pgettext('troubleshoot', 'Make sure you have NF tables support.'),
+ ],
+ },
+ };
+ } else if (errorState.cause === ErrorStateCause.setDnsError) {
+ const troubleshootSteps = [];
+ if (platform === 'darwin') {
+ troubleshootSteps.push(
+ messages.pgettext(
+ 'troubleshoot',
+ 'Try to turn Wi-Fi Calling off in the FaceTime app settings and restart the Mac.',
+ ),
+ messages.pgettext(
+ 'troubleshoot',
+ 'Uninstall or disable other DNS, networking and ads/website blocking apps.',
+ ),
+ );
+ } else if (platform === 'win32') {
+ troubleshootSteps.push(
+ messages.pgettext(
+ 'troubleshoot',
+ 'Uninstall or disable other DNS, networking and ads/website blocking apps.',
+ ),
+ );
+ }
+
+ return {
+ type: 'troubleshoot-dialog',
+ troubleshoot: {
+ details: messages.pgettext(
+ 'troubleshoot',
+ 'This error can happen when something other than Mullvad is actively updating the DNS.',
+ ),
+ steps: troubleshootSteps,
+ },
+ };
+ } else if (errorState.cause === ErrorStateCause.splitTunnelError) {
+ return {
+ type: 'troubleshoot-dialog',
+ troubleshoot: {
+ details: messages.pgettext(
+ 'troubleshoot',
+ 'Unable to communicate with Mullvad kernel driver.',
+ ),
+ steps: [
+ messages.pgettext('troubleshoot', 'Try reconnecting.'),
+ messages.pgettext('troubleshoot', 'Try restarting your device.'),
+ ],
+ },
+ };
+ }
+}
diff --git a/gui/src/shared/notifications/notification.ts b/gui/src/shared/notifications/notification.ts
index 10b5b21fc8..88bddfa9f5 100644
--- a/gui/src/shared/notifications/notification.ts
+++ b/gui/src/shared/notifications/notification.ts
@@ -5,6 +5,18 @@ export type NotificationAction = {
withAuth?: boolean;
};
+export interface InAppNotificationTroubleshootInfo {
+ details: string;
+ steps: string[];
+}
+
+export type InAppNotificationAction =
+ | NotificationAction
+ | {
+ type: 'troubleshoot-dialog';
+ troubleshoot: InAppNotificationTroubleshootInfo;
+ };
+
export type InAppNotificationIndicatorType = 'success' | 'warning' | 'error';
export enum SystemNotificationSeverityType {
@@ -39,7 +51,7 @@ export interface InAppNotification {
indicator?: InAppNotificationIndicatorType;
title: string;
subtitle?: string;
- action?: NotificationAction;
+ action?: InAppNotificationAction;
}
export interface SystemNotificationProvider extends NotificationProvider {