diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot index 517423d42078..4d588a5a9eda 100644 --- a/gui/locales/messages.pot +++ b/gui/locales/messages.pot @@ -1600,6 +1600,10 @@ msgctxt "split-tunneling-view" msgid "Please try again or send a problem report." msgstr "" +msgctxt "split-tunneling-view" +msgid "To use split tunneling please enable “Full disk access” for “Mullvad VPN” in the macOS system settings." +msgstr "" + #. Error message showed in a dialog when an application fails to launch. msgctxt "split-tunneling-view" msgid "Unable to launch selection. %(detailedErrorMessage)s" diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index c86bed047f13..8cea0d4008a6 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -438,6 +438,13 @@ export class DaemonRpc extends GrpcClient { await this.callBool(this.client.setSplitTunnelState, enabled); } + public async needFullDiskPermissions(): Promise { + const needFullDiskPermissions = await this.callEmpty( + this.client.needFullDiskPermissions, + ); + return needFullDiskPermissions.getValue(); + } + public async checkVolumes(): Promise { await this.callEmpty(this.client.checkVolumes); } diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index 400fe39d2ad7..c9067c78e218 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -832,6 +832,9 @@ class ApplicationMain splitTunneling!.removeApplicationFromCache(application); return Promise.resolve(); }); + IpcMainEventChannel.macOsSplitTunneling.handleNeedFullDiskPermissions(() => { + return this.daemonRpc.needFullDiskPermissions(); + }); IpcMainEventChannel.app.handleQuit(() => this.disconnectAndQuit()); IpcMainEventChannel.app.handleOpenUrl(async (url) => { diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index d36c27aa14c8..ddbb43aab7b1 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -345,6 +345,8 @@ export default class AppRenderer { IpcRendererEventChannel.splitTunneling.addApplication(application); public forgetManuallyAddedSplitTunnelingApplication = (application: ISplitTunnelingApplication) => IpcRendererEventChannel.splitTunneling.forgetManuallyAddedApplication(application); + public needFullDiskPermissions = () => + IpcRendererEventChannel.macOsSplitTunneling.needFullDiskPermissions(); public setObfuscationSettings = (obfuscationSettings: ObfuscationSettings) => IpcRendererEventChannel.settings.setObfuscationSettings(obfuscationSettings); public setEnableDaita = (value: boolean) => diff --git a/gui/src/renderer/components/SmallButton.tsx b/gui/src/renderer/components/SmallButton.tsx index e88d71995248..c91fbbdb204c 100644 --- a/gui/src/renderer/components/SmallButton.tsx +++ b/gui/src/renderer/components/SmallButton.tsx @@ -53,6 +53,10 @@ const StyledSmallButton = styled.button(smallText, (prop alignItems: 'center', justifyContent: 'center', + '&&:not(& + &&)': { + marginLeft: '0px', + }, + [`${SmallButtonGroupStart} &&`]: { marginLeft: 0, marginRight: `${BUTTON_GROUP_GAP}px`, diff --git a/gui/src/renderer/components/SplitTunnelingSettings.tsx b/gui/src/renderer/components/SplitTunnelingSettings.tsx index ed999ba867a5..7e8830e6f89d 100644 --- a/gui/src/renderer/components/SplitTunnelingSettings.tsx +++ b/gui/src/renderer/components/SplitTunnelingSettings.tsx @@ -43,6 +43,7 @@ import { StyledPageCover, StyledSearchBar, StyledSpinnerRow, + StyledSystemSettingsButton, } from './SplitTunnelingSettingsStyles'; import Switch from './Switch'; @@ -313,9 +314,12 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro removeSplitTunnelingApplication, forgetManuallyAddedSplitTunnelingApplication, getSplitTunnelingApplications, + needFullDiskPermissions, setSplitTunnelingState, } = useAppContext(); - const splitTunnelingEnabled = useSelector((state: IReduxState) => state.settings.splitTunneling); + const splitTunnelingEnabledValue = useSelector( + (state: IReduxState) => state.settings.splitTunneling, + ); const splitTunnelingApplications = useSelector( (state: IReduxState) => state.settings.splitTunnelingApplications, ); @@ -323,6 +327,23 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro const [searchTerm, setSearchTerm] = useState(''); const [applications, setApplications] = useState(); + const [splitTunnelingAvailable, setSplitTunnelingAvailable] = useState( + window.env.platform === 'darwin' ? undefined : true, + ); + + const splitTunnelingEnabled = splitTunnelingEnabledValue && (splitTunnelingAvailable ?? false); + + const fetchNeedFullDiskPermissions = useCallback(async () => { + const needPermissions = await needFullDiskPermissions(); + setSplitTunnelingAvailable(!needPermissions); + }, [needFullDiskPermissions]); + + useEffect((): void | (() => void) => { + if (window.env.platform === 'darwin') { + void fetchNeedFullDiskPermissions(); + } + }, [fetchNeedFullDiskPermissions]); + const onMount = useEffectEvent(async () => { const { fromCache, applications } = await getSplitTunnelingApplications(); setApplications(applications); @@ -441,14 +462,25 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro {strings.splitTunneling} - + - - {messages.pgettext( - 'split-tunneling-view', - 'Choose the apps you want to exclude from the VPN tunnel.', - )} - + + {splitTunnelingAvailable ? ( + + {messages.pgettext( + 'split-tunneling-view', + 'Choose the apps you want to exclude from the VPN tunnel.', + )} + + ) : null} {splitTunnelingEnabled && ( @@ -495,6 +527,34 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro ); } +interface MacOsSplitTunnelingAvailabilityProps { + needFullDiskPermissions: boolean; +} + +function MacOsSplitTunnelingAvailability({ + needFullDiskPermissions, +}: MacOsSplitTunnelingAvailabilityProps) { + const { showFullDiskAccessSettings } = useAppContext(); + + return ( + <> + {needFullDiskPermissions === true ? ( + <> + + {messages.pgettext( + 'split-tunneling-view', + 'To use split tunneling please enable “Full disk access” for “Mullvad VPN” in the macOS system settings.', + )} + + + Open System Settings + + + ) : null} + + ); +} + interface IApplicationListProps { applications: T[] | undefined; rowRenderer: (application: T) => React.ReactElement; diff --git a/gui/src/renderer/components/SplitTunnelingSettingsStyles.tsx b/gui/src/renderer/components/SplitTunnelingSettingsStyles.tsx index 1aea5108a14b..a2019fba8dcc 100644 --- a/gui/src/renderer/components/SplitTunnelingSettingsStyles.tsx +++ b/gui/src/renderer/components/SplitTunnelingSettingsStyles.tsx @@ -8,6 +8,7 @@ import ImageView from './ImageView'; import { NavigationScrollbars } from './NavigationBar'; import SearchBar from './SearchBar'; import { HeaderTitle } from './SettingsHeader'; +import { SmallButton } from './SmallButton'; export const StyledPageCover = styled.div<{ $show: boolean }>((props) => ({ position: 'absolute', @@ -122,3 +123,8 @@ export const StyledSearchBar = styled(SearchBar)({ marginRight: measurements.viewMargin, marginBottom: measurements.buttonVerticalMargin, }); + +export const StyledSystemSettingsButton = styled(SmallButton)({ + width: '100%', + marginTop: '24px', +}); diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts index 954dce168014..a2282e28498b 100644 --- a/gui/src/shared/ipc-schema.ts +++ b/gui/src/shared/ipc-schema.ts @@ -240,6 +240,9 @@ export const ipcSchema = { getApplications: invoke(), launchApplication: invoke(), }, + macOsSplitTunneling: { + needFullDiskPermissions: invoke(), + }, splitTunneling: { '': notifyRenderer(), setState: invoke(),