diff options
| author | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-09-18 13:06:54 +0200 |
|---|---|---|
| committer | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-09-18 13:06:54 +0200 |
| commit | e529ab2eb44515f726cda080941793933eb59b2f (patch) | |
| tree | a74434c9bdab26ae15b2de3c7b7b630f233cd603 | |
| parent | d86e2d5ceff307ffe7446ec1a412efde8ed9f31f (diff) | |
| parent | 1cd875c883aea8ec7345c7724afef5f8f2392bca (diff) | |
| download | mullvadvpn-e529ab2eb44515f726cda080941793933eb59b2f.tar.xz mullvadvpn-e529ab2eb44515f726cda080941793933eb59b2f.zip | |
Merge branch 'implement-ui-for-split-tunneling-not-being-supported-des-2222'
28 files changed, 416 insertions, 22 deletions
diff --git a/desktop/packages/mullvad-vpn/locales/messages.pot b/desktop/packages/mullvad-vpn/locales/messages.pot index 3eafee0b0e..ba8742fb5d 100644 --- a/desktop/packages/mullvad-vpn/locales/messages.pot +++ b/desktop/packages/mullvad-vpn/locales/messages.pot @@ -2128,6 +2128,13 @@ msgctxt "split-tunneling-view" msgid "%(applicationName)s is problematic and can’t be excluded from the VPN tunnel." msgstr "" +#. Information about split tunneling not being supported on the system. +#. Available placeholders: +#. %(splitTunneling)s - will be replaced with Split tunneling +msgctxt "split-tunneling-view" +msgid "%(splitTunneling)s is not supported by your system." +msgstr "" + msgctxt "split-tunneling-view" msgid "Add" msgstr "" @@ -2140,6 +2147,11 @@ msgctxt "split-tunneling-view" msgid "Choose the apps you want to exclude from the VPN tunnel." msgstr "" +#. Link for learning more +msgctxt "split-tunneling-view" +msgid "Click here to learn more" +msgstr "" + msgctxt "split-tunneling-view" msgid "Click on an app to launch it. Its traffic will bypass the VPN tunnel until you close it." msgstr "" @@ -2187,6 +2199,14 @@ msgctxt "split-tunneling-view" msgid "Restart Mullvad Service" msgstr "" +#. Information about split tunneling being unavailable due to +#. missing support in the user's operating system. +#. Available placeholders: +#. %(splitTunneling)s - will be replaced with Split tunneling +msgctxt "split-tunneling-view" +msgid "To use %(splitTunneling)s, please change to a Linux kernel version that supports cgroup v1." +msgstr "" + msgctxt "split-tunneling-view" msgid "To use split tunneling please enable “Full disk access” for “Mullvad VPN” in the macOS system settings." msgstr "" diff --git a/desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts b/desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts index ab76aba907..5c8e6de6e1 100644 --- a/desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts +++ b/desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts @@ -498,9 +498,13 @@ export class DaemonRpc extends GrpcClient { await this.callBool(this.client.setSplitTunnelState, enabled); } - public async splitTunnelIsEnabled(): Promise<boolean> { - const isEnabled = await this.callEmpty<BoolValue>(this.client.splitTunnelIsEnabled); - return isEnabled.getValue(); + public async linuxSplitTunnelIsSupported(): Promise<boolean> { + try { + const isEnabled = await this.callEmpty<BoolValue>(this.client.splitTunnelIsEnabled); + return isEnabled.getValue(); + } catch { + return false; + } } public async needFullDiskPermissions(): Promise<boolean> { diff --git a/desktop/packages/mullvad-vpn/src/main/index.ts b/desktop/packages/mullvad-vpn/src/main/index.ts index 9c354f4dd4..0b582c3cd6 100644 --- a/desktop/packages/mullvad-vpn/src/main/index.ts +++ b/desktop/packages/mullvad-vpn/src/main/index.ts @@ -848,6 +848,10 @@ class ApplicationMain return Promise.resolve(this.translations); }); + IpcMainEventChannel.linuxSplitTunneling.handleIsSplitTunnelingSupported(() => { + return this.daemonRpc.linuxSplitTunnelIsSupported(); + }); + IpcMainEventChannel.linuxSplitTunneling.handleGetApplications(() => { return this.linuxSplitTunneling!.getApplications(this.locale); }); diff --git a/desktop/packages/mullvad-vpn/src/renderer/app.tsx b/desktop/packages/mullvad-vpn/src/renderer/app.tsx index 44ee6d8084..69ae19338d 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/app.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/app.tsx @@ -445,6 +445,9 @@ export default class AppRenderer { public daemonPrepareRestart = (shutdown: boolean): void => { IpcRendererEventChannel.daemon.prepareRestart(shutdown); }; + public getLinuxSplitTunnelingSupported = () => { + return IpcRendererEventChannel.linuxSplitTunneling.isSplitTunnelingSupported(); + }; public getAppUpgradeCacheDir = () => IpcRendererEventChannel.app.getUpgradeCacheDir(); public tryStartDaemon = () => { diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/SearchBar.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/SearchBar.tsx index e7a355c8b4..de0c36902e 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/SearchBar.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/SearchBar.tsx @@ -29,6 +29,12 @@ export const StyledSearchInput = styled.input.attrs({ type: 'text' })({ color: colors.blue, backgroundColor: colors.white, }, + '&&:disabled': { + backgroundColor: colors.whiteOnDarkBlue5, + '&&::placeholder': { + color: colors.whiteAlpha20, + }, + }, }); export const StyledClearButton = styled(IconButton)({ @@ -53,17 +59,21 @@ export const StyledSearchIcon = styled(Icon)({ [`${StyledSearchInput}:focus ~ &&`]: { backgroundColor: colors.blue, }, + [`${StyledSearchInput}:disabled ~ &&`]: { + backgroundColor: colors.whiteAlpha20, + }, }); export interface ISearchBarProps { searchTerm: string; + disabled?: boolean; onSearch: (searchTerm: string) => void; className?: string; disableAutoFocus?: boolean; } export default function SearchBar(props: ISearchBarProps) { - const { onSearch } = props; + const { disabled, onSearch } = props; const inputRef = useStyledRef<HTMLInputElement>(); @@ -96,6 +106,7 @@ export default function SearchBar(props: ISearchBarProps) { return ( <StyledSearchContainer className={props.className}> <StyledSearchInput + disabled={disabled} ref={inputRef} value={props.searchTerm} onInput={onInput} diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/LinuxSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/LinuxSettings.tsx index fc9a7c3ca3..50a4d2d617 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/LinuxSettings.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/LinuxSettings.tsx @@ -1,7 +1,6 @@ import { useEffect } from 'react'; import { strings } from '../../../../../../shared/constants'; -import { messages } from '../../../../../../shared/gettext'; import { useAppContext } from '../../../../../context'; import { Flex, Spinner } from '../../../../../lib/components'; import { FlexColumn } from '../../../../../lib/components/flex-column'; @@ -10,19 +9,36 @@ import { useEffectEvent } from '../../../../../lib/utility-hooks'; import SettingsHeader, { HeaderSubTitle, HeaderTitle } from '../../../../SettingsHeader'; import { ApplicationSearchBar } from '../application-search-bar'; import { ApplicationSearchNoResult } from '../application-search-no-result'; -import { LaunchErrorDialog, LinuxApplicationList, OpenFilePickerButton } from './components'; +import { + HeaderDescription, + LaunchErrorDialog, + LinuxApplicationList, + OpenFilePickerButton, + UnsupportedDialog, +} from './components'; import { useShowLinuxApplicationList, useShowNoSearchResult, useShowSpinner } from './hooks'; import { LinuxSettingsContextProvider, useLinuxSettingsContext } from './LinuxSettingsContext'; function LinuxSettingsInner() { - const { getLinuxSplitTunnelingApplications } = useAppContext(); - const { searchTerm, setApplications, setSearchTerm } = useLinuxSettingsContext(); + const { getLinuxSplitTunnelingSupported, getLinuxSplitTunnelingApplications } = useAppContext(); + const { + splitTunnelingSupported, + searchTerm, + setApplications, + setSearchTerm, + setSplitTunnelingSupported, + } = useLinuxSettingsContext(); const runAfterTransition = useAfterTransition(); const showLinuxApplicationList = useShowLinuxApplicationList(); const showNoSearchResult = useShowNoSearchResult(); const showSpinner = useShowSpinner(); - const updateApplications = useEffectEvent(() => { + const onMount = useEffectEvent(() => { + runAfterTransition(async () => { + const linuxSplitTunnelingSupported = await getLinuxSplitTunnelingSupported(); + setSplitTunnelingSupported(linuxSplitTunnelingSupported); + }); + runAfterTransition(async () => { const applications = await getLinuxSplitTunnelingApplications(); setApplications(applications); @@ -34,20 +50,21 @@ function LinuxSettingsInner() { // Enable these rules again when eslint can lint useEffectEvent properly. // eslint-disable-next-line react-compiler/react-compiler // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => void updateApplications(), []); + useEffect(() => void onMount(), []); return ( <> <SettingsHeader> <HeaderTitle>{strings.splitTunneling}</HeaderTitle> <HeaderSubTitle> - {messages.pgettext( - 'split-tunneling-view', - 'Click on an app to launch it. Its traffic will bypass the VPN tunnel until you close it.', - )} + <HeaderDescription /> </HeaderSubTitle> </SettingsHeader> - <ApplicationSearchBar searchTerm={searchTerm} onSearch={setSearchTerm} /> + <ApplicationSearchBar + disabled={!splitTunnelingSupported} + searchTerm={searchTerm} + onSearch={setSearchTerm} + /> {showNoSearchResult && <ApplicationSearchNoResult searchTerm={searchTerm} />} <FlexColumn $gap="medium"> {showLinuxApplicationList && <LinuxApplicationList />} @@ -61,6 +78,7 @@ function LinuxSettingsInner() { </Flex> </FlexColumn> <LaunchErrorDialog /> + <UnsupportedDialog /> </> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/LinuxSettingsContext.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/LinuxSettingsContext.tsx index 5af8fa2277..5e041c0141 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/LinuxSettingsContext.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/LinuxSettingsContext.tsx @@ -13,6 +13,10 @@ type LinuxSettingsContext = { setApplications: (value: ILinuxSplitTunnelingApplication[]) => void; setBrowseError: (value?: string) => void; setSearchTerm: (value: string) => void; + setShowUnsupportedDialog: (value: boolean) => void; + setSplitTunnelingSupported: (value: boolean) => void; + showUnsupportedDialog: boolean; + splitTunnelingSupported?: boolean; }; const LinuxSettingsContext = React.createContext<LinuxSettingsContext | undefined>(undefined); @@ -29,6 +33,10 @@ export function LinuxSettingsContextProvider({ children }: LinuxSettingsContextP const [applications, setApplications] = useState<ILinuxSplitTunnelingApplication[]>(); const [browseError, setBrowseError] = useState<string>(); const [searchTerm, setSearchTerm] = useState(''); + const [splitTunnelingSupported, setSplitTunnelingSupported] = useState<boolean | undefined>( + undefined, + ); + const [showUnsupportedDialog, setShowUnsupportedDialog] = useState(false); const value = useMemo( () => ({ @@ -38,8 +46,23 @@ export function LinuxSettingsContextProvider({ children }: LinuxSettingsContextP setApplications, setBrowseError, setSearchTerm, + setShowUnsupportedDialog, + setSplitTunnelingSupported, + showUnsupportedDialog, + splitTunnelingSupported, }), - [applications, browseError, searchTerm, setApplications, setBrowseError, setSearchTerm], + [ + applications, + browseError, + searchTerm, + setApplications, + setBrowseError, + setSearchTerm, + setShowUnsupportedDialog, + setSplitTunnelingSupported, + showUnsupportedDialog, + splitTunnelingSupported, + ], ); return <LinuxSettingsContext value={value}>{children}</LinuxSettingsContext>; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/HeaderDescription.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/HeaderDescription.tsx new file mode 100644 index 0000000000..bb6a602731 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/HeaderDescription.tsx @@ -0,0 +1,59 @@ +import { sprintf } from 'sprintf-js'; + +import { strings } from '../../../../../../../../shared/constants'; +import { messages } from '../../../../../../../../shared/gettext'; +import { Icon, Link } from '../../../../../../../lib/components'; +import { FlexColumn } from '../../../../../../../lib/components/flex-column'; +import { FlexRow } from '../../../../../../../lib/components/flex-row'; +import { useLinuxSettingsContext } from '../../LinuxSettingsContext'; +import { useShowUnsupportedDialog } from './hooks'; + +export function HeaderDescription() { + const { splitTunnelingSupported } = useLinuxSettingsContext(); + const message = sprintf( + // TRANSLATORS: Information about split tunneling not being supported on the system. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(splitTunneling)s - will be replaced with Split tunneling + messages.pgettext( + 'split-tunneling-view', + '%(splitTunneling)s is not supported by your system.', + ), + { + splitTunneling: strings.splitTunneling, + }, + ); + const showUnsupportedDialog = useShowUnsupportedDialog(); + + if (splitTunnelingSupported === false) { + return ( + <FlexRow> + <FlexColumn $justifyContent="center" $margin={{ right: 'small' }}> + <Icon size="small" color="whiteAlpha60" icon="info-circle" /> + </FlexColumn> + <FlexColumn> + <span> + {message} + + <Link + aria-description={message} + as="button" + onClick={showUnsupportedDialog} + variant="labelTiny"> + <Link.Text> + { + // TRANSLATORS: Link for learning more + messages.pgettext('split-tunneling-view', 'Click here to learn more') + } + </Link.Text> + </Link> + </span> + </FlexColumn> + </FlexRow> + ); + } + + return messages.pgettext( + 'split-tunneling-view', + 'Click on an app to launch it. Its traffic will bypass the VPN tunnel until you close it.', + ); +} diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/hooks/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/hooks/index.ts new file mode 100644 index 0000000000..4b2b16cffb --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/hooks/index.ts @@ -0,0 +1 @@ +export * from './use-show-unsupported-dialog'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/hooks/use-show-unsupported-dialog.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/hooks/use-show-unsupported-dialog.ts new file mode 100644 index 0000000000..fd4bbc531a --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/hooks/use-show-unsupported-dialog.ts @@ -0,0 +1,13 @@ +import { useCallback } from 'react'; + +import { useLinuxSettingsContext } from '../../../LinuxSettingsContext'; + +export function useShowUnsupportedDialog() { + const { setShowUnsupportedDialog } = useLinuxSettingsContext(); + + const showUnsupportedDialog = useCallback(() => { + setShowUnsupportedDialog(true); + }, [setShowUnsupportedDialog]); + + return showUnsupportedDialog; +} diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/index.ts new file mode 100644 index 0000000000..52c084e8ff --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/index.ts @@ -0,0 +1 @@ +export * from './HeaderDescription'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/index.ts index 6dd9b7e1cb..15401396b3 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/index.ts +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/index.ts @@ -1,3 +1,5 @@ +export * from './header-description'; export * from './launch-error-dialog'; export * from './linux-application-list'; export * from './open-file-picker-button'; +export * from './unsupported-dialog'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/LinuxApplicationList.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/LinuxApplicationList.tsx index 817c6e10c2..31fc18409d 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/LinuxApplicationList.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/LinuxApplicationList.tsx @@ -17,5 +17,11 @@ export function LinuxApplicationList() { const filteredApplications = useFilteredApplications(); - return <ApplicationList applications={filteredApplications} rowRenderer={rowRenderer} />; + return ( + <ApplicationList + data-testid="linux-applications" + applications={filteredApplications} + rowRenderer={rowRenderer} + /> + ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/components/linux-application-row/hooks/use-disabled.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/components/linux-application-row/hooks/use-disabled.ts index 052c610b3a..0086cbe977 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/components/linux-application-row/hooks/use-disabled.ts +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/components/linux-application-row/hooks/use-disabled.ts @@ -1,9 +1,12 @@ +import { useLinuxSettingsContext } from '../../../../../LinuxSettingsContext'; import { useApplication } from './use-application'; export function useDisabled() { + const { splitTunnelingSupported } = useLinuxSettingsContext(); const application = useApplication(); - const disabled = application.warning === 'launches-elsewhere'; + const disabled = + splitTunnelingSupported === false || application.warning === 'launches-elsewhere'; return disabled; } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/components/linux-application-row/hooks/use-launch-application.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/components/linux-application-row/hooks/use-launch-application.ts index 1b91848e6b..593f55c6bb 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/components/linux-application-row/hooks/use-launch-application.ts +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/components/linux-application-row/hooks/use-launch-application.ts @@ -1,20 +1,31 @@ import { useCallback } from 'react'; +import { useLinuxSettingsContext } from '../../../../../LinuxSettingsContext'; import { useLinuxApplicationRowContext } from '../LinuxApplicationRowContext'; import { useHasApplicationWarning } from './use-has-application-warning'; export function useLaunchApplication() { const { application, onSelect, setShowWarningDialog } = useLinuxApplicationRowContext(); + const { setShowUnsupportedDialog, splitTunnelingSupported } = useLinuxSettingsContext(); const hasApplicationWarning = useHasApplicationWarning(); const launchApplication = useCallback(() => { - if (hasApplicationWarning) { + if (splitTunnelingSupported === false) { + setShowUnsupportedDialog(true); + } else if (hasApplicationWarning) { setShowWarningDialog(true); } else { setShowWarningDialog(false); onSelect?.(application); } - }, [application, hasApplicationWarning, onSelect, setShowWarningDialog]); + }, [ + application, + hasApplicationWarning, + onSelect, + setShowUnsupportedDialog, + setShowWarningDialog, + splitTunnelingSupported, + ]); return launchApplication; } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/OpenFilePickerButton.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/OpenFilePickerButton.tsx index 0b7fe7bec1..5b570f10f2 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/OpenFilePickerButton.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/OpenFilePickerButton.tsx @@ -1,12 +1,13 @@ import { messages } from '../../../../../../../../shared/gettext'; import { Button } from '../../../../../../../lib/components'; -import { useLaunchWithFilePicker } from './hooks'; +import { useDisabled, useLaunchWithFilePicker } from './hooks'; export function OpenFilePickerButton() { + const disabled = useDisabled(); const launchWithFilePicker = useLaunchWithFilePicker(); return ( - <Button onClick={launchWithFilePicker}> + <Button disabled={disabled} onClick={launchWithFilePicker}> <Button.Text> { // TRANSLATORS: Button label for browsing applications with split tunneling. diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/hooks/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/hooks/index.ts index 502bc77f87..cc11951cd4 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/hooks/index.ts +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/hooks/index.ts @@ -1 +1,2 @@ +export * from './use-disabled'; export * from './use-launch-with-file-picker'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/hooks/use-disabled.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/hooks/use-disabled.ts new file mode 100644 index 0000000000..ea1146ab10 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/hooks/use-disabled.ts @@ -0,0 +1,9 @@ +import { useLinuxSettingsContext } from '../../../LinuxSettingsContext'; + +export function useDisabled() { + const { splitTunnelingSupported } = useLinuxSettingsContext(); + + const disabled = splitTunnelingSupported === false; + + return disabled; +} diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/unsupported-dialog/UnsupportedDialog.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/unsupported-dialog/UnsupportedDialog.tsx new file mode 100644 index 0000000000..220596e4bb --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/unsupported-dialog/UnsupportedDialog.tsx @@ -0,0 +1,45 @@ +import { useCallback } from 'react'; +import { sprintf } from 'sprintf-js'; + +import { strings } from '../../../../../../../../shared/constants'; +import { messages } from '../../../../../../../../shared/gettext'; +import { Button } from '../../../../../../../lib/components'; +import { ModalAlert, ModalAlertType } from '../../../../../../Modal'; +import { useLinuxSettingsContext } from '../../LinuxSettingsContext'; + +export function UnsupportedDialog() { + const { showUnsupportedDialog, setShowUnsupportedDialog } = useLinuxSettingsContext(); + const hideUnsupportedDialog = useCallback(() => { + setShowUnsupportedDialog(false); + }, [setShowUnsupportedDialog]); + + const unsupportedMessage = sprintf( + // TRANSLATORS: Information about split tunneling being unavailable due to + // TRANSLATORS: missing support in the user's operating system. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(splitTunneling)s - will be replaced with Split tunneling + messages.pgettext( + 'split-tunneling-view', + 'To use %(splitTunneling)s, please change to a Linux kernel version that supports cgroup v1.', + ), + { + splitTunneling: strings.splitTunneling, + }, + ); + + const buttons = [ + <Button key="cancel" onClick={hideUnsupportedDialog}> + <Button.Text>{messages.gettext('Got it!')}</Button.Text> + </Button>, + ]; + + return ( + <ModalAlert + isOpen={showUnsupportedDialog} + type={ModalAlertType.info} + message={unsupportedMessage} + buttons={buttons} + close={hideUnsupportedDialog} + /> + ); +} diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/unsupported-dialog/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/unsupported-dialog/index.ts new file mode 100644 index 0000000000..499647a433 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/unsupported-dialog/index.ts @@ -0,0 +1 @@ +export * from './UnsupportedDialog'; diff --git a/desktop/packages/mullvad-vpn/src/shared/ipc-schema.ts b/desktop/packages/mullvad-vpn/src/shared/ipc-schema.ts index 9a9580fc13..cd33b994fe 100644 --- a/desktop/packages/mullvad-vpn/src/shared/ipc-schema.ts +++ b/desktop/packages/mullvad-vpn/src/shared/ipc-schema.ts @@ -253,6 +253,7 @@ export const ipcSchema = { log: send<ILogEntry>(), }, linuxSplitTunneling: { + isSplitTunnelingSupported: invoke<void, boolean>(), getApplications: invoke<void, ILinuxSplitTunnelingApplication[]>(), launchApplication: invoke<ILinuxSplitTunnelingApplication | string, LaunchApplicationResult>(), }, diff --git a/desktop/packages/mullvad-vpn/test/e2e/mocked/split-tunneling/split-tunneling.spec.ts b/desktop/packages/mullvad-vpn/test/e2e/mocked/split-tunneling/split-tunneling.spec.ts new file mode 100644 index 0000000000..ee776261f0 --- /dev/null +++ b/desktop/packages/mullvad-vpn/test/e2e/mocked/split-tunneling/split-tunneling.spec.ts @@ -0,0 +1,92 @@ +import { expect, test } from '@playwright/test'; +import { Page } from 'playwright'; + +import { RoutePath } from '../../../../src/shared/routes'; +import { RoutesObjectModel } from '../../route-object-models'; +import { MockedTestUtils, startMockedApp } from '../mocked-utils'; + +let page: Page; +let util: MockedTestUtils; +let routes: RoutesObjectModel; + +test.describe('Split tunneling', () => { + test.beforeAll(async () => { + ({ page, util } = await startMockedApp()); + routes = new RoutesObjectModel(page, util); + + await util.waitForRoute(RoutePath.main); + await routes.main.gotoSettings(); + await routes.settings.gotoSplitTunnelingSettings(); + }); + + test.afterAll(async () => { + await page.close(); + }); + + test.describe('Linux Split tunneling unsupported', () => { + if (process.platform !== 'linux') { + test.skip(); + } + + test.beforeAll(async () => { + await util.ipc.linuxSplitTunneling.isSplitTunnelingSupported.handle(false); + await util.ipc.linuxSplitTunneling.getApplications.handle([ + { + absolutepath: '/app', + exec: 'app', + name: 'app', + type: 'app', + icon: '', + warning: undefined, + }, + { + absolutepath: '/launches-elsewhere', + exec: 'launches-elsewhere', + name: 'launches-elsewhere', + type: 'launches-elsewhere', + icon: '', + warning: 'launches-elsewhere', + }, + { + absolutepath: '/launches-in-existing-process', + exec: 'launches-in-existing-process', + name: 'launches-in-existing-process', + type: 'launches-in-existing-process', + icon: '', + warning: 'launches-in-existing-process', + }, + ]); + }); + + test('App should show unsupported dialog when link in header is clicked', async () => { + // Open the unsupported dialog + await routes.splitTunnelingSettings.openUnsupportedDialog(); + const unsupportedText = + routes.splitTunnelingSettings.getSplitTunnelingUnsupportedDialogText(); + await expect(unsupportedText).toBeVisible(); + + // Close the unsupported dialog + await routes.splitTunnelingSettings.closeUnsupportedDialog(); + await expect(unsupportedText).not.toBeVisible(); + }); + + test('App list items should be shown even when split tunneling is unsupported', async () => { + // Apps should be shown if split tunneling is unsupported + const linuxApplications = routes.splitTunnelingSettings.getLinuxApplications(); + await expect(linuxApplications).toHaveCount(3); + }); + + test('App list items should show unsupported dialog when clicked', async () => { + // Ensure clicking an application in the list makes the unsupported dialog visible + const linuxApplications = routes.splitTunnelingSettings.getLinuxApplications(); + await linuxApplications.first().click(); + const unsupportedText = + routes.splitTunnelingSettings.getSplitTunnelingUnsupportedDialogText(); + await expect(unsupportedText).toBeVisible(); + + // Close the unsupported dialog + await routes.splitTunnelingSettings.closeUnsupportedDialog(); + await expect(unsupportedText).not.toBeVisible(); + }); + }); +}); diff --git a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/routes-object-model.ts b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/routes-object-model.ts index ec58260dd3..9cf32e5caf 100644 --- a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/routes-object-model.ts +++ b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/routes-object-model.ts @@ -10,6 +10,7 @@ import { MultihopSettingsRouteObjectModel } from './multihop-settings'; import { SelectLanguageRouteObjectModel } from './select-language'; import { SelectLocationRouteObjectModel } from './select-location'; import { SettingsRouteObjectModel } from './settings/settings-route-object-model'; +import { SplitTunnelingSettingsRouteObjectModel } from './split-tunneling-settings'; import { UdpOverTcpSettingsRouteObjectModel } from './udp-over-tcp-settings'; import { UserInterfaceSettingsRouteObjectModel } from './user-interface-settings'; import { VpnSettingsRouteObjectModel } from './vpn-settings'; @@ -29,6 +30,7 @@ export class RoutesObjectModel { readonly udpOverTcpSettings: UdpOverTcpSettingsRouteObjectModel; readonly multihopSettings: MultihopSettingsRouteObjectModel; readonly daitaSettings: DaitaSettingsRouteObjectModel; + readonly splitTunnelingSettings: SplitTunnelingSettingsRouteObjectModel; constructor(page: Page, utils: TestUtils) { this.selectLanguage = new SelectLanguageRouteObjectModel(page, utils); @@ -44,5 +46,6 @@ export class RoutesObjectModel { this.udpOverTcpSettings = new UdpOverTcpSettingsRouteObjectModel(page, utils); this.multihopSettings = new MultihopSettingsRouteObjectModel(page, utils); this.daitaSettings = new DaitaSettingsRouteObjectModel(page, utils); + this.splitTunnelingSettings = new SplitTunnelingSettingsRouteObjectModel(page, utils); } } diff --git a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/settings/selectors.ts b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/settings/selectors.ts index abf5527eda..8cc021fdcc 100644 --- a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/settings/selectors.ts +++ b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/settings/selectors.ts @@ -5,4 +5,5 @@ export const createSelectors = (page: Page) => ({ daitaSettingsButton: () => page.getByRole('button', { name: 'Daita' }), userInterfaceButton: () => page.getByRole('button', { name: 'User interface settings' }), vpnSettingsButton: () => page.getByRole('button', { name: 'VPN settings' }), + splitTunnelingSettingsButton: () => page.getByRole('button', { name: 'Split tunneling' }), }); diff --git a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/settings/settings-route-object-model.ts b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/settings/settings-route-object-model.ts index 59b8609260..13692a2b27 100644 --- a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/settings/settings-route-object-model.ts +++ b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/settings/settings-route-object-model.ts @@ -34,4 +34,9 @@ export class SettingsRouteObjectModel { await this.selectors.daitaSettingsButton().click(); await this.utils.waitForRoute(RoutePath.daitaSettings); } + + async gotoSplitTunnelingSettings() { + await this.selectors.splitTunnelingSettingsButton().click(); + await this.utils.waitForRoute(RoutePath.splitTunneling); + } } diff --git a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/split-tunneling-settings/index.ts b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/split-tunneling-settings/index.ts new file mode 100644 index 0000000000..1a647dd9e5 --- /dev/null +++ b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/split-tunneling-settings/index.ts @@ -0,0 +1,2 @@ +export * from './split-tunneling-settings-route-object-model'; +export * from './selectors'; diff --git a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/split-tunneling-settings/selectors.ts b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/split-tunneling-settings/selectors.ts new file mode 100644 index 0000000000..f7208eab3a --- /dev/null +++ b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/split-tunneling-settings/selectors.ts @@ -0,0 +1,17 @@ +import { type Page } from 'playwright'; + +export const createSelectors = (page: Page) => ({ + splitTunnelingUnsupportedDialogOpenLink: () => + page.getByRole('button', { + name: 'Click here to learn more', + }), + splitTunnelingUnsupportedDialogCloseButton: () => + page.getByRole('button', { + name: 'Got it!', + }), + splitTunnelingUnsupportedDialogText: () => + page.getByText( + 'To use Split tunneling, please change to a Linux kernel version that supports cgroup v1.', + ), + linuxApplications: () => page.getByTestId('linux-applications').locator('button'), +}); diff --git a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/split-tunneling-settings/split-tunneling-settings-route-object-model.ts b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/split-tunneling-settings/split-tunneling-settings-route-object-model.ts new file mode 100644 index 0000000000..65589304cb --- /dev/null +++ b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/split-tunneling-settings/split-tunneling-settings-route-object-model.ts @@ -0,0 +1,37 @@ +import { Page } from 'playwright'; + +import { RoutePath } from '../../../../src/shared/routes'; +import { type TestUtils } from '../../utils'; +import { createSelectors } from './selectors'; + +export class SplitTunnelingSettingsRouteObjectModel { + readonly page: Page; + readonly utils: TestUtils; + readonly selectors: ReturnType<typeof createSelectors>; + + constructor(page: Page, utils: TestUtils) { + this.page = page; + this.utils = utils; + this.selectors = createSelectors(page); + } + + async waitForRoute() { + await this.utils.waitForRoute(RoutePath.splitTunneling); + } + + getLinuxApplications() { + return this.selectors.linuxApplications(); + } + + getSplitTunnelingUnsupportedDialogText() { + return this.selectors.splitTunnelingUnsupportedDialogText(); + } + + closeUnsupportedDialog() { + return this.selectors.splitTunnelingUnsupportedDialogCloseButton().click(); + } + + openUnsupportedDialog() { + return this.selectors.splitTunnelingUnsupportedDialogOpenLink().click(); + } +} |
