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 =