diff --git a/desktop/packages/mullvad-vpn/locales/messages.pot b/desktop/packages/mullvad-vpn/locales/messages.pot
index be9d787aa558..1b5f981cc5b2 100644
--- a/desktop/packages/mullvad-vpn/locales/messages.pot
+++ b/desktop/packages/mullvad-vpn/locales/messages.pot
@@ -1780,6 +1780,10 @@ msgctxt "tray-icon-tooltip"
msgid "Connecting. %(location)s"
msgstr ""
+msgctxt "troubleshoot"
+msgid "Disable split tunneling"
+msgstr ""
+
msgctxt "troubleshoot"
msgid "Enable “Full Disk Access” for “Mullvad VPN” in the macOS system settings."
msgstr ""
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx
index ab9965250acb..df3c78d4b063 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx
@@ -9,7 +9,6 @@ import {
ErrorNotificationProvider,
InAppNotificationAction,
InAppNotificationProvider,
- InAppNotificationTroubleshootInfo,
InconsistentVersionNotificationProvider,
ReconnectingNotificationProvider,
UnsupportedVersionNotificationProvider,
@@ -47,7 +46,7 @@ interface IProps {
}
export default function NotificationArea(props: IProps) {
- const { showFullDiskAccessSettings } = useAppContext();
+ const { showFullDiskAccessSettings, reconnectTunnel } = useAppContext();
const account = useSelector((state: IReduxState) => state.account);
const locale = useSelector((state: IReduxState) => state.userInterface.locale);
@@ -75,6 +74,15 @@ export default function NotificationArea(props: IProps) {
setDisplayedChangelog();
}, [setDisplayedChangelog]);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ const { setSplitTunnelingState } = useAppContext();
+ const disableSplitTunneling = useCallback(async () => {
+ setIsModalOpen(false);
+ await setSplitTunnelingState(false);
+ await reconnectTunnel();
+ }, [reconnectTunnel, setSplitTunnelingState]);
+
const notificationProviders: InAppNotificationProvider[] = [
new ConnectingNotificationProvider({ tunnelState }),
new ReconnectingNotificationProvider(tunnelState),
@@ -83,7 +91,13 @@ export default function NotificationArea(props: IProps) {
blockWhenDisconnected,
hasExcludedApps,
}),
- new ErrorNotificationProvider({ tunnelState, hasExcludedApps, showFullDiskAccessSettings }),
+
+ new ErrorNotificationProvider({
+ tunnelState,
+ hasExcludedApps,
+ showFullDiskAccessSettings,
+ disableSplitTunneling,
+ }),
new InconsistentVersionNotificationProvider({ consistent: version.consistent }),
new UnsupportedVersionNotificationProvider(version),
];
@@ -140,7 +154,13 @@ export default function NotificationArea(props: IProps) {
)}
- {notification.action && }
+ {notification.action && (
+
+ )}
);
} else {
@@ -153,46 +173,51 @@ export default function NotificationArea(props: IProps) {
return ;
}
-interface INotificationActionWrapperProps {
+interface NotificationActionWrapperProps {
action: InAppNotificationAction;
+ isModalOpen: boolean;
+ setIsModalOpen: (isOpen: boolean) => void;
}
-function NotificationActionWrapper(props: INotificationActionWrapperProps) {
+function NotificationActionWrapper({
+ action,
+ isModalOpen,
+ setIsModalOpen,
+}: NotificationActionWrapperProps) {
const { push } = useHistory();
const { openLinkWithAuth, openUrl } = useAppContext();
- const [troubleshootInfo, setTroubleshootInfo] = useState();
+
+ const closeTroubleshootModal = useCallback(() => setIsModalOpen(false), [setIsModalOpen]);
const handleClick = useCallback(() => {
- if (props.action) {
- switch (props.action.type) {
+ if (action) {
+ switch (action.type) {
case 'open-url':
- if (props.action.withAuth) {
- return openLinkWithAuth(props.action.url);
+ if (action.withAuth) {
+ return openLinkWithAuth(action.url);
} else {
- return openUrl(props.action.url);
+ return openUrl(action.url);
}
case 'troubleshoot-dialog':
- setTroubleshootInfo(props.action.troubleshoot);
+ setIsModalOpen(true);
break;
case 'close':
- props.action.close();
+ action.close();
break;
}
}
return Promise.resolve();
- }, [openLinkWithAuth, openUrl, props.action]);
+ }, [action, setIsModalOpen, openLinkWithAuth, openUrl]);
const goToProblemReport = useCallback(() => {
- setTroubleshootInfo(undefined);
+ closeTroubleshootModal();
push(RoutePath.problemReport, { transition: transitions.show });
- }, [push]);
-
- const closeTroubleshootInfo = useCallback(() => setTroubleshootInfo(undefined), []);
+ }, [closeTroubleshootModal, push]);
let actionComponent: React.ReactElement | undefined;
- if (props.action) {
- switch (props.action.type) {
+ if (action) {
+ switch (action.type) {
case 'open-url':
actionComponent = ;
break;
@@ -208,7 +233,11 @@ function NotificationActionWrapper(props: INotificationActionWrapperProps) {
}
}
- const problemReportButton = troubleshootInfo?.buttons ? (
+ if (action.type !== 'troubleshoot-dialog') {
+ return {actionComponent};
+ }
+
+ const problemReportButton = action.troubleshoot?.buttons ? (
{messages.pgettext('in-app-notifications', 'Send problem report')}
@@ -220,17 +249,32 @@ function NotificationActionWrapper(props: INotificationActionWrapperProps) {
let buttons = [
problemReportButton,
-
+
{messages.gettext('Back')}
,
];
- if (troubleshootInfo?.buttons) {
- const actionButtons = troubleshootInfo.buttons.map((button) => (
-
- {button.label}
-
- ));
+ if (action.troubleshoot?.buttons) {
+ const actionButtons = action.troubleshoot.buttons.map(({ variant, label, action }) => {
+ if (variant === 'success')
+ return (
+
+ {label}
+
+ );
+ else if (variant === 'destructive')
+ return (
+
+ {label}
+
+ );
+ else
+ return (
+
+ {label}
+
+ );
+ });
buttons = actionButtons.concat(buttons);
}
@@ -239,14 +283,14 @@ function NotificationActionWrapper(props: INotificationActionWrapperProps) {
<>
{actionComponent}
- {troubleshootInfo?.details}
+ close={closeTroubleshootModal}>
+ {action.troubleshoot?.details}
- {troubleshootInfo?.steps.map((step) => {step})}
+ {action.troubleshoot?.steps.map((step) => {step})}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx
index 4070b7dba59c..a24790e66a5b 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx
@@ -66,12 +66,10 @@ export default function SplitTunneling() {
-
-
-
+
@@ -315,9 +313,7 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
needFullDiskPermissions,
setSplitTunnelingState,
} = useAppContext();
- const splitTunnelingEnabledValue = useSelector(
- (state: IReduxState) => state.settings.splitTunneling,
- );
+ const splitTunnelingEnabled = useSelector((state: IReduxState) => state.settings.splitTunneling);
const splitTunnelingApplications = useSelector(
(state: IReduxState) => state.settings.splitTunnelingApplications,
);
@@ -325,15 +321,18 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
const [searchTerm, setSearchTerm] = useState('');
const [applications, setApplications] = useState();
+ const [loadingDiskPermissions, setLoadingDiskPermissions] = useState(false);
const [splitTunnelingAvailable, setSplitTunnelingAvailable] = useState(
window.env.platform === 'darwin' ? undefined : true,
);
- const splitTunnelingEnabled = splitTunnelingEnabledValue && (splitTunnelingAvailable ?? false);
+ const canEditSplitTunneling = splitTunnelingEnabled && (splitTunnelingAvailable ?? false);
const fetchNeedFullDiskPermissions = useCallback(async () => {
+ setLoadingDiskPermissions(true);
const needPermissions = await needFullDiskPermissions();
setSplitTunnelingAvailable(!needPermissions);
+ setLoadingDiskPermissions(false);
}, [needFullDiskPermissions]);
useEffect((): void | (() => void) => {
@@ -375,12 +374,12 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
const addApplication = useCallback(
async (application: ISplitTunnelingApplication | string) => {
- if (!splitTunnelingEnabled) {
+ if (!canEditSplitTunneling) {
await setSplitTunnelingState(true);
}
await addSplitTunnelingApplication(application);
},
- [addSplitTunnelingApplication, splitTunnelingEnabled, setSplitTunnelingState],
+ [addSplitTunnelingApplication, canEditSplitTunneling, setSplitTunnelingState],
);
const addBrowsedForApplication = useCallback(
@@ -403,12 +402,12 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
const removeApplication = useCallback(
async (application: ISplitTunnelingApplication) => {
- if (!splitTunnelingEnabled) {
+ if (!canEditSplitTunneling) {
await setSplitTunnelingState(true);
}
removeSplitTunnelingApplication(application);
},
- [removeSplitTunnelingApplication, setSplitTunnelingState, splitTunnelingEnabled],
+ [removeSplitTunnelingApplication, setSplitTunnelingState, canEditSplitTunneling],
);
const filePickerCallback = useFilePicker(
@@ -440,9 +439,9 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
[addApplication, forgetManuallyAddedApplicationAndUpdate],
);
- const showSplitSection = splitTunnelingEnabled && filteredSplitApplications.length > 0;
+ const showSplitSection = canEditSplitTunneling && filteredSplitApplications.length > 0;
const showNonSplitSection =
- splitTunnelingEnabled &&
+ canEditSplitTunneling &&
(!filteredNonSplitApplications || filteredNonSplitApplications.length > 0);
const excludedTitle = (
@@ -462,26 +461,37 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
{strings.splitTunneling}
-
- {splitTunnelingAvailable ? (
-
- {messages.pgettext(
- 'split-tunneling-view',
- 'Choose the apps you want to exclude from the VPN tunnel.',
+ {!loadingDiskPermissions && (
+ <>
+
+ {splitTunnelingAvailable && (
+
+ {messages.pgettext(
+ 'split-tunneling-view',
+ 'Choose the apps you want to exclude from the VPN tunnel.',
+ )}
+
)}
-
- ) : null}
+ >
+ )}
+ {loadingDiskPermissions && (
+
+
+
+ )}
- {splitTunnelingEnabled && (
+ {canEditSplitTunneling && (
)}
@@ -505,7 +515,7 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
- {splitTunnelingEnabled && searchTerm !== '' && !showSplitSection && !showNonSplitSection && (
+ {canEditSplitTunneling && searchTerm !== '' && !showSplitSection && !showNonSplitSection && (
{formatHtml(
@@ -516,7 +526,7 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
)}
- {splitTunnelingEnabled && (
+ {canEditSplitTunneling && (
{messages.pgettext('split-tunneling-view', 'Find another app')}
diff --git a/desktop/packages/mullvad-vpn/src/shared/notifications/error.ts b/desktop/packages/mullvad-vpn/src/shared/notifications/error.ts
index af82748d7b04..42e550c46641 100644
--- a/desktop/packages/mullvad-vpn/src/shared/notifications/error.ts
+++ b/desktop/packages/mullvad-vpn/src/shared/notifications/error.ts
@@ -13,6 +13,7 @@ import {
InAppNotification,
InAppNotificationAction,
InAppNotificationProvider,
+ InAppNotificationTroubleshootButton,
SystemNotification,
SystemNotificationCategory,
SystemNotificationProvider,
@@ -23,6 +24,7 @@ interface ErrorNotificationContext {
tunnelState: TunnelState;
hasExcludedApps: boolean;
showFullDiskAccessSettings?: () => void;
+ disableSplitTunneling?: () => void;
}
export class ErrorNotificationProvider
@@ -276,12 +278,18 @@ export class ErrorNotificationProvider
},
};
} else if (errorState.cause === ErrorStateCause.needFullDiskPermissions) {
- let troubleshootButtons = undefined;
+ let troubleshootButtons: InAppNotificationTroubleshootButton[] | undefined = undefined;
if (this.context.showFullDiskAccessSettings) {
troubleshootButtons = [
{
label: messages.pgettext('troubleshoot', 'Open system settings'),
action: () => this.context.showFullDiskAccessSettings?.(),
+ variant: 'success',
+ },
+ {
+ label: messages.pgettext('troubleshoot', 'Disable split tunneling'),
+ action: () => this.context.disableSplitTunneling?.(),
+ variant: 'destructive',
},
];
}
diff --git a/desktop/packages/mullvad-vpn/src/shared/notifications/notification.ts b/desktop/packages/mullvad-vpn/src/shared/notifications/notification.ts
index 921a029a0678..48561c555b01 100644
--- a/desktop/packages/mullvad-vpn/src/shared/notifications/notification.ts
+++ b/desktop/packages/mullvad-vpn/src/shared/notifications/notification.ts
@@ -16,6 +16,7 @@ export interface InAppNotificationTroubleshootInfo {
export interface InAppNotificationTroubleshootButton {
label: string;
action: () => void;
+ variant?: 'primary' | 'success' | 'destructive';
}
export type InAppNotificationAction =