summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2022-07-15 17:02:50 +0200
committerOskar Nyberg <oskar@mullvad.net>2022-07-22 14:36:05 +0200
commit42a4f632cd0dc77031cd2d7d94085bd8b6fb1284 (patch)
tree30bae23efd2936b2a7a0beeed65d03855e0b8fc5
parent060400307569b0fe8432c608389f9f6476fd4e2c (diff)
downloadmullvadvpn-42a4f632cd0dc77031cd2d7d94085bd8b6fb1284.tar.xz
mullvadvpn-42a4f632cd0dc77031cd2d7d94085bd8b6fb1284.zip
Move VPN related settings to its own view
-rw-r--r--gui/src/renderer/app.tsx12
-rw-r--r--gui/src/renderer/components/AppRouter.tsx4
-rw-r--r--gui/src/renderer/components/CustomDnsSettings.tsx50
-rw-r--r--gui/src/renderer/components/Login.tsx5
-rw-r--r--gui/src/renderer/components/OpenVPNSettings.tsx2
-rw-r--r--gui/src/renderer/components/VpnSettings.tsx722
-rw-r--r--gui/src/renderer/components/WireguardSettings.tsx4
-rw-r--r--gui/src/renderer/lib/routes.ts2
-rw-r--r--gui/src/shared/localization-contexts.ts3
9 files changed, 756 insertions, 48 deletions
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index ddc0ba22ce..7d269b9bdd 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -418,11 +418,11 @@ export default class AppRenderer {
void this.openUrl(`${link}?token=${token}`);
};
- public async setAllowLan(allowLan: boolean) {
+ public setAllowLan = async (allowLan: boolean) => {
const actions = this.reduxActions;
await IpcRendererEventChannel.settings.setAllowLan(allowLan);
actions.settings.updateAllowLan(allowLan);
- }
+ };
public setShowBetaReleases = async (showBetaReleases: boolean) => {
const actions = this.reduxActions;
@@ -430,11 +430,11 @@ export default class AppRenderer {
actions.settings.updateShowBetaReleases(showBetaReleases);
};
- public async setEnableIpv6(enableIpv6: boolean) {
+ public setEnableIpv6 = async (enableIpv6: boolean) => {
const actions = this.reduxActions;
await IpcRendererEventChannel.settings.setEnableIpv6(enableIpv6);
actions.settings.updateEnableIpv6(enableIpv6);
- }
+ };
public async setBridgeState(bridgeState: BridgeState) {
const actions = this.reduxActions;
@@ -468,11 +468,11 @@ export default class AppRenderer {
IpcRendererEventChannel.guiSettings.setEnableSystemNotifications(flag);
}
- public setAutoStart(autoStart: boolean): Promise<void> {
+ public setAutoStart = (autoStart: boolean): Promise<void> => {
this.storeAutoStart(autoStart);
return IpcRendererEventChannel.autoStart.set(autoStart);
- }
+ };
public setStartMinimized(startMinimized: boolean) {
IpcRendererEventChannel.guiSettings.setStartMinimized(startMinimized);
diff --git a/gui/src/renderer/components/AppRouter.tsx b/gui/src/renderer/components/AppRouter.tsx
index 186bf069b9..e9355775b2 100644
--- a/gui/src/renderer/components/AppRouter.tsx
+++ b/gui/src/renderer/components/AppRouter.tsx
@@ -3,7 +3,6 @@ import * as React from 'react';
import { Route, Switch } from 'react-router';
import AccountPage from '../containers/AccountPage';
-import AdvancedSettingsPage from '../containers/AdvancedSettingsPage';
import LoginPage from '../containers/LoginPage';
import OpenVPNSettingsPage from '../containers/OpenVPNSettingsPage';
import ProblemReportPage from '../containers/ProblemReportPage';
@@ -30,6 +29,7 @@ import SplitTunnelingSettings from './SplitTunnelingSettings';
import Support from './Support';
import TooManyDevices from './TooManyDevices';
import TransitionContainer, { TransitionView } from './TransitionContainer';
+import VpnSettings from './VpnSettings';
interface IAppRoutesState {
currentLocation: IHistoryProps['history']['location'];
@@ -91,7 +91,7 @@ class AppRouter extends React.Component<IHistoryProps & IAppContext, IAppRoutesS
<Route exact path={RoutePath.selectLanguage} component={SelectLanguagePage} />
<Route exact path={RoutePath.accountSettings} component={AccountPage} />
<Route exact path={RoutePath.interfaceSettings} component={InterfaceSettings} />
- <Route exact path={RoutePath.advancedSettings} component={AdvancedSettingsPage} />
+ <Route exact path={RoutePath.vpnSettings} component={VpnSettings} />
<Route exact path={RoutePath.wireguardSettings} component={WireguardSettingsPage} />
<Route exact path={RoutePath.openVpnSettings} component={OpenVPNSettingsPage} />
<Route exact path={RoutePath.splitTunneling} component={SplitTunnelingSettings} />
diff --git a/gui/src/renderer/components/CustomDnsSettings.tsx b/gui/src/renderer/components/CustomDnsSettings.tsx
index 0e9dccc79e..99362eed93 100644
--- a/gui/src/renderer/components/CustomDnsSettings.tsx
+++ b/gui/src/renderer/components/CustomDnsSettings.tsx
@@ -6,7 +6,6 @@ import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
import { IpAddress } from '../lib/ip';
import { useBoolean, useMounted } from '../lib/utilityHooks';
-import { formatMarkdown } from '../markdown-formatter';
import { useSelector } from '../redux/store';
import Accordion from './Accordion';
import * as AppButton from './AppButton';
@@ -221,7 +220,7 @@ export default function CustomDnsSettings() {
<AriaInputGroup>
<AriaLabel>
<Cell.InputLabel>
- {messages.pgettext('advanced-settings-view', 'Use custom DNS server')}
+ {messages.pgettext('vpn-settings-view', 'Use custom DNS server')}
</Cell.InputLabel>
</AriaLabel>
<AriaInput>
@@ -254,7 +253,7 @@ export default function CustomDnsSettings() {
{inputVisible && (
<div ref={inputContainerRef}>
<Cell.RowInput
- placeholder={messages.pgettext('advanced-settings-view', 'Enter IP')}
+ placeholder={messages.pgettext('vpn-settings-view', 'Enter IP')}
onSubmit={onAdd}
onChange={setValid}
invalid={invalid}
@@ -271,7 +270,7 @@ export default function CustomDnsSettings() {
disabled={inputVisible}
tabIndex={-1}>
<StyledAddCustomDnsLabel tabIndex={-1}>
- {messages.pgettext('advanced-settings-view', 'Add a server')}
+ {messages.pgettext('vpn-settings-view', 'Add a server')}
</StyledAddCustomDnsLabel>
<Cell.Icon
source="icon-add"
@@ -286,11 +285,18 @@ export default function CustomDnsSettings() {
<StyledCustomDnsFooter>
<Cell.FooterText>
- {featureAvailable ? (
- messages.pgettext('advanced-settings-view', 'Enable to add at least one DNS server.')
- ) : (
- <DisabledMessage />
- )}
+ {featureAvailable
+ ? messages.pgettext('vpn-settings-view', 'Enable to add at least one DNS server.')
+ : // This line makes sure that the next one isn't prefixed by the color.
+ // TRANSLATORS: This is displayed when either or both of the block ads/trackers settings are
+ // TRANSLATORS: turned on which makes the custom DNS setting disabled. The text enclosed in "**"
+ // TRANSLATORS: will appear bold.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(preferencesPageName)s - The page title showed on top in the preferences page.
+ messages.pgettext(
+ 'vpn-settings-view',
+ 'Disable all content blockers to activate this setting.',
+ )}
</Cell.FooterText>
</StyledCustomDnsFooter>
@@ -304,22 +310,6 @@ export default function CustomDnsSettings() {
);
}
-function DisabledMessage() {
- const preferencesPageName = messages.pgettext('preferences-nav', 'Preferences');
-
- // TRANSLATORS: This is displayed when either or both of the block ads/trackers settings are
- // TRANSLATORS: turned on which makes the custom DNS setting disabled. The text enclosed in "**"
- // TRANSLATORS: will appear bold.
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(preferencesPageName)s - The page title showed on top in the preferences page.
- const customDnsDisabledMessage = messages.pgettext(
- 'preferences-view',
- 'Disable all content blockers (under %(preferencesPageName)s) to activate this setting.',
- );
-
- return formatMarkdown(sprintf(customDnsDisabledMessage, { preferencesPageName }));
-}
-
interface ICellListItemProps {
willShowConfirmationDialog: React.RefObject<boolean>;
onRemove: (application: string) => void;
@@ -372,7 +362,7 @@ function CellListItem(props: ICellListItemProps) {
<div ref={inputContainerRef}>
<Cell.RowInput
initialValue={props.children}
- placeholder={messages.pgettext('advanced-settings-view', 'Enter IP')}
+ placeholder={messages.pgettext('vpn-settings-view', 'Enter IP')}
onSubmit={onSubmit}
onChange={setValid}
invalid={invalid}
@@ -417,7 +407,7 @@ function ConfirmationDialog(props: IConfirmationDialogProps) {
let message;
if (props.isLocal.current) {
message = messages.pgettext(
- 'advanced-settings-view',
+ 'vpn-settings-view',
'The DNS server you want to add is a private IP. You must ensure that your network interfaces are configured to use it.',
);
} else {
@@ -426,12 +416,12 @@ function ConfirmationDialog(props: IConfirmationDialogProps) {
// TRANSLATORS: %(tunnelProtocol)s - the name of the tunnel protocol setting
// TRANSLATORS: %(wireguard)s - will be replaced with "WireGuard"
messages.pgettext(
- 'advanced-settings-view',
+ 'vpn-settings-view',
'The DNS server you want to add is public and will only work with %(wireguard)s. To ensure that it always works, set the "%(tunnelProtocol)s" (in Advanced settings) to %(wireguard)s.',
),
{
wireguard: strings.wireguard,
- tunnelProtocol: messages.pgettext('advanced-settings-view', 'Tunnel protocol'),
+ tunnelProtocol: messages.pgettext('vpn-settings-view', 'Tunnel protocol'),
},
);
}
@@ -441,7 +431,7 @@ function ConfirmationDialog(props: IConfirmationDialogProps) {
type={ModalAlertType.caution}
buttons={[
<AppButton.RedButton key="confirm" onClick={props.confirm}>
- {messages.pgettext('advanced-settings-view', 'Add anyway')}
+ {messages.pgettext('vpn-settings-view', 'Add anyway')}
</AppButton.RedButton>,
<AppButton.BlueButton key="back" onClick={props.abort}>
{messages.gettext('Back')}
diff --git a/gui/src/renderer/components/Login.tsx b/gui/src/renderer/components/Login.tsx
index 282f49d01c..c2c8b9d177 100644
--- a/gui/src/renderer/components/Login.tsx
+++ b/gui/src/renderer/components/Login.tsx
@@ -442,10 +442,7 @@ function BlockMessage() {
}
}, [blockWhenDisconnected, tunnelState, setBlockWhenDisconnected, disconnectTunnel]);
- const alwaysRequireVpnSettingsName = messages.pgettext(
- 'advanced-settings-view',
- 'Always require VPN',
- );
+ const alwaysRequireVpnSettingsName = messages.pgettext('vpn-settings-view', 'Lockdown mode');
const message = formatMarkdown(
blockWhenDisconnected
? sprintf(
diff --git a/gui/src/renderer/components/OpenVPNSettings.tsx b/gui/src/renderer/components/OpenVPNSettings.tsx
index 4aac83014f..a6e3d551a1 100644
--- a/gui/src/renderer/components/OpenVPNSettings.tsx
+++ b/gui/src/renderer/components/OpenVPNSettings.tsx
@@ -321,7 +321,7 @@ export default class OpenVpnSettings extends React.Component<IProps, IState> {
'To activate Bridge mode, go back and change **%(tunnelProtocol)s** to **%(openvpn)s**.',
),
{
- tunnelProtocol: messages.pgettext('advanced-settings-view', 'Tunnel protocol'),
+ tunnelProtocol: messages.pgettext('vpn-settings-view', 'Tunnel protocol'),
openvpn: strings.openvpn,
},
),
diff --git a/gui/src/renderer/components/VpnSettings.tsx b/gui/src/renderer/components/VpnSettings.tsx
new file mode 100644
index 0000000000..03274c0b2a
--- /dev/null
+++ b/gui/src/renderer/components/VpnSettings.tsx
@@ -0,0 +1,722 @@
+import { useCallback, useMemo } from 'react';
+import { sprintf } from 'sprintf-js';
+import styled from 'styled-components';
+
+import { colors, strings } from '../../config.json';
+import { IDnsOptions, TunnelProtocol } from '../../shared/daemon-rpc-types';
+import { messages } from '../../shared/gettext';
+import log from '../../shared/logging';
+import RelaySettingsBuilder from '../../shared/relay-settings-builder';
+import { useAppContext } from '../context';
+import { useHistory } from '../lib/history';
+import { RoutePath } from '../lib/routes';
+import { useBoolean } from '../lib/utilityHooks';
+import { formatMarkdown } from '../markdown-formatter';
+import { RelaySettingsRedux } from '../redux/settings/reducers';
+import { useSelector } from '../redux/store';
+import * as AppButton from './AppButton';
+import { AriaDescription, AriaDetails, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup';
+import * as Cell from './cell';
+import Selector, { ISelectorItem } from './cell/Selector';
+import CustomDnsSettings from './CustomDnsSettings';
+import InfoButton, { InfoIcon } from './InfoButton';
+import { BackAction } from './KeyboardNavigation';
+import { Container, Layout } from './Layout';
+import { ModalAlert, ModalAlertType, ModalMessage } from './Modal';
+import {
+ NavigationBar,
+ NavigationContainer,
+ NavigationItems,
+ NavigationScrollbars,
+ TitleBarItem,
+} from './NavigationBar';
+import SettingsHeader, { HeaderTitle } from './SettingsHeader';
+
+const StyledContainer = styled(Container)({
+ backgroundColor: colors.darkBlue,
+});
+
+const StyledContent = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ flex: 1,
+ marginBottom: '2px',
+});
+
+const StyledInfoIcon = styled(InfoIcon)({
+ marginRight: '16px',
+});
+
+const StyledSelectorContainer = styled.div({
+ flex: 0,
+});
+
+export default function VpnSettings() {
+ const { pop } = useHistory();
+
+ return (
+ <BackAction action={pop}>
+ <Layout>
+ <StyledContainer>
+ <NavigationContainer>
+ <NavigationBar>
+ <NavigationItems>
+ <TitleBarItem>
+ {
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('vpn-settings-view', 'VPN settings')
+ }
+ </TitleBarItem>
+ </NavigationItems>
+ </NavigationBar>
+
+ <NavigationScrollbars>
+ <SettingsHeader>
+ <HeaderTitle>{messages.pgettext('vpn-settings-view', 'VPN settings')}</HeaderTitle>
+ </SettingsHeader>
+
+ <StyledContent>
+ <Cell.Group>
+ <AutoStart />
+ <AutoConnect />
+ </Cell.Group>
+
+ <Cell.Group>
+ <AllowLan />
+ </Cell.Group>
+
+ <Cell.Group>
+ <BlockAds />
+ <BlockTrackers />
+ <BlockMalware />
+ <BlockGambling />
+ <BlockAdultContent />
+ </Cell.Group>
+
+ <Cell.Group>
+ <EnableIpv6 />
+ </Cell.Group>
+
+ <Cell.Group>
+ <KillSwitchInfo />
+ <LockdownMode />
+ </Cell.Group>
+
+ <Cell.Group>
+ <TunnelProtocolSetting />
+ </Cell.Group>
+
+ <Cell.Group>
+ <WireguardSettingsButton />
+ <OpenVpnSettingsButton />
+ </Cell.Group>
+
+ <Cell.Group>
+ <CustomDnsSettings />
+ </Cell.Group>
+ </StyledContent>
+ </NavigationScrollbars>
+ </NavigationContainer>
+ </StyledContainer>
+ </Layout>
+ </BackAction>
+ );
+}
+
+function AutoStart() {
+ const autoStart = useSelector((state) => state.settings.autoStart);
+ const { setAutoStart: setAutoStartImpl } = useAppContext();
+
+ const setAutoStart = useCallback(
+ async (autoStart: boolean) => {
+ try {
+ await setAutoStartImpl(autoStart);
+ } catch (e) {
+ const error = e as Error;
+ log.error(`Cannot set auto-start: ${error.message}`);
+ }
+ },
+ [setAutoStartImpl],
+ );
+
+ return (
+ <AriaInputGroup>
+ <Cell.Container>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('vpn-settings-view', 'Launch app on start-up')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.Switch isOn={autoStart} onChange={setAutoStart} />
+ </AriaInput>
+ </Cell.Container>
+ </AriaInputGroup>
+ );
+}
+
+function AutoConnect() {
+ const autoConnect = useSelector((state) => state.settings.guiSettings.autoConnect);
+ const { setAutoConnect } = useAppContext();
+
+ return (
+ <AriaInputGroup>
+ <Cell.Container>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('vpn-settings-view', 'Auto-connect')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.Switch isOn={autoConnect} onChange={setAutoConnect} />
+ </AriaInput>
+ </Cell.Container>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {messages.pgettext(
+ 'vpn-settings-view',
+ 'Automatically connect to a server when the app launches.',
+ )}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
+ );
+}
+
+function AllowLan() {
+ const allowLan = useSelector((state) => state.settings.allowLan);
+ const { setAllowLan } = useAppContext();
+
+ return (
+ <AriaInputGroup>
+ <Cell.Container>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('vpn-settings-view', 'Local network sharing')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.Switch isOn={allowLan} onChange={setAllowLan} />
+ </AriaInput>
+ </Cell.Container>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {messages.pgettext(
+ 'vpn-settings-view',
+ 'Allows access to other devices on the same network for sharing, printing etc.',
+ )}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
+ );
+}
+
+function useDns(setting: keyof IDnsOptions['defaultOptions']) {
+ const dns = useSelector((state) => state.settings.dns);
+ const { setDnsOptions } = useAppContext();
+
+ const updateBlockSetting = useCallback(
+ (enabled: boolean) =>
+ setDnsOptions({
+ ...dns,
+ defaultOptions: {
+ ...dns.defaultOptions,
+ [setting]: enabled,
+ },
+ }),
+ [dns, setDnsOptions],
+ );
+
+ return [dns, updateBlockSetting] as const;
+}
+
+function BlockAds() {
+ const [dns, setBlockAds] = useDns('blockAds');
+
+ return (
+ <AriaInputGroup>
+ <Cell.Container disabled={dns.state === 'custom'}>
+ <AriaLabel>
+ <Cell.InputLabel>{messages.pgettext('vpn-settings-view', 'Block ads')}</Cell.InputLabel>
+ </AriaLabel>
+ <AriaDetails>
+ <InfoButton>
+ <ModalMessage>
+ {messages.pgettext(
+ 'vpn-settings-view',
+ 'When enabled, this feature stops the device from contacting certain known ad domains.',
+ )}
+ </ModalMessage>
+ <ModalMessage>
+ {messages.pgettext(
+ 'vpn-settings-view',
+ 'Warning: This might cause issues on certain websites, services, and programs.',
+ )}
+ </ModalMessage>
+ </InfoButton>
+ </AriaDetails>
+ <AriaInput>
+ <Cell.Switch
+ isOn={dns.state === 'default' && dns.defaultOptions.blockAds}
+ onChange={setBlockAds}
+ />
+ </AriaInput>
+ </Cell.Container>
+ </AriaInputGroup>
+ );
+}
+
+function BlockTrackers() {
+ const [dns, setBlockTrackers] = useDns('blockTrackers');
+
+ return (
+ <AriaInputGroup>
+ <Cell.Container disabled={dns.state === 'custom'}>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('vpn-settings-view', 'Block trackers')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaDetails>
+ <InfoButton>
+ <ModalMessage>
+ {messages.pgettext(
+ 'vpn-settings-view',
+ 'When enabled, this feature stops the device from contacting certain domains known to track users.',
+ )}
+ </ModalMessage>
+ <ModalMessage>
+ {messages.pgettext(
+ 'vpn-settings-view',
+ 'Warning: This might cause issues on certain websites, services, and programs.',
+ )}
+ </ModalMessage>
+ </InfoButton>
+ </AriaDetails>
+ <AriaInput>
+ <Cell.Switch
+ isOn={dns.state === 'default' && dns.defaultOptions.blockTrackers}
+ onChange={setBlockTrackers}
+ />
+ </AriaInput>
+ </Cell.Container>
+ </AriaInputGroup>
+ );
+}
+
+function BlockMalware() {
+ const [dns, setBlockMalware] = useDns('blockMalware');
+
+ return (
+ <AriaInputGroup>
+ <Cell.Container disabled={dns.state === 'custom'}>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('vpn-settings-view', 'Block malware')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaDetails>
+ <InfoButton>
+ <ModalMessage>
+ {messages.pgettext(
+ 'vpn-settings-view',
+ 'When enabled, this feature stops the device from contacting certain domains known to host malware.',
+ )}
+ </ModalMessage>
+ <ModalMessage>
+ {messages.pgettext(
+ 'vpn-settings-view',
+ 'Warning: This is not an anti-virus and should not be treated as such, this is just an extra layer of protection.',
+ )}
+ </ModalMessage>
+ </InfoButton>
+ </AriaDetails>
+ <AriaInput>
+ <Cell.Switch
+ isOn={dns.state === 'default' && dns.defaultOptions.blockMalware}
+ onChange={setBlockMalware}
+ />
+ </AriaInput>
+ </Cell.Container>
+ </AriaInputGroup>
+ );
+}
+
+function BlockGambling() {
+ const [dns, setBlockGambling] = useDns('blockGambling');
+
+ return (
+ <AriaInputGroup>
+ <Cell.Container disabled={dns.state === 'custom'}>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('vpn-settings-view', 'Block gambling')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaDetails>
+ <InfoButton
+ message={messages.pgettext(
+ 'vpn-settings-view',
+ 'When enabled, this feature stops the device from contacting certain websites and services known to host gambling content.',
+ )}
+ />
+ </AriaDetails>
+ <AriaInput>
+ <Cell.Switch
+ isOn={dns.state === 'default' && dns.defaultOptions.blockGambling}
+ onChange={setBlockGambling}
+ />
+ </AriaInput>
+ </Cell.Container>
+ </AriaInputGroup>
+ );
+}
+
+function BlockAdultContent() {
+ const [dns, setBlockAdultContent] = useDns('blockAdultContent');
+
+ return (
+ <AriaInputGroup>
+ <Cell.Container disabled={dns.state === 'custom'}>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('vpn-settings-view', 'Block adult content')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaDetails>
+ <InfoButton
+ message={messages.pgettext(
+ 'vpn-settings-view',
+ 'When enabled, this feature stops the device from contacting certain websites and services known to host adult content.',
+ )}
+ />
+ </AriaDetails>
+ <AriaInput>
+ <Cell.Switch
+ isOn={dns.state === 'default' && dns.defaultOptions.blockAdultContent}
+ onChange={setBlockAdultContent}
+ />
+ </AriaInput>
+ </Cell.Container>
+ {dns.state === 'custom' && <CustomDnsEnabledFooter />}
+ </AriaInputGroup>
+ );
+}
+
+function CustomDnsEnabledFooter() {
+ const customDnsFeatureName = messages.pgettext('vpn-settings-view', 'Use custom DNS server');
+
+ // TRANSLATORS: This is displayed when the custom DNS setting is turned on which makes the block
+ // TRANSLATORS: ads/trackers settings disabled. The text enclosed in "**" will appear bold.
+ // TRANSLATORS: Advanced settings refer to the name of the page with the title "Advanced".
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(customDnsFeatureName)s - The name displayed next to the custom DNS toggle.
+ const blockingDisabledText = messages.pgettext(
+ 'vpn-settings-view',
+ 'Disable **%(customDnsFeatureName)s** (under Advanced settings) to activate these settings.',
+ );
+
+ return (
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {formatMarkdown(sprintf(blockingDisabledText, { customDnsFeatureName }))}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ );
+}
+
+function EnableIpv6() {
+ const enableIpv6 = useSelector((state) => state.settings.enableIpv6);
+ const { setEnableIpv6: setEnableIpv6Impl } = useAppContext();
+
+ const setEnableIpv6 = useCallback(
+ async (enableIpv6: boolean) => {
+ try {
+ await setEnableIpv6Impl(enableIpv6);
+ } catch (e) {
+ const error = e as Error;
+ log.error('Failed to update enable IPv6', error.message);
+ }
+ },
+ [setEnableIpv6Impl],
+ );
+
+ return (
+ <AriaInputGroup>
+ <Cell.Container>
+ <AriaLabel>
+ <Cell.InputLabel>{messages.pgettext('vpn-settings-view', 'Enable IPv6')}</Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.Switch isOn={enableIpv6} onChange={setEnableIpv6} />
+ </AriaInput>
+ </Cell.Container>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {messages.pgettext(
+ 'vpn-settings-view',
+ 'Enable IPv6 communication through the tunnel.',
+ )}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
+ );
+}
+
+function KillSwitchInfo() {
+ const [killSwitchInfoVisible, showKillSwitchInfo, hideKillSwitchInfo] = useBoolean(false);
+
+ return (
+ <>
+ <Cell.CellButton onClick={showKillSwitchInfo}>
+ <AriaInputGroup>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('vpn-settings-view', 'Kill switch')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <StyledInfoIcon />
+ <AriaInput>
+ <Cell.Switch isOn disabled />
+ </AriaInput>
+ </AriaInputGroup>
+ </Cell.CellButton>
+ <ModalAlert
+ isOpen={killSwitchInfoVisible}
+ type={ModalAlertType.info}
+ buttons={[
+ <AppButton.BlueButton key="back" onClick={hideKillSwitchInfo}>
+ {messages.gettext('Got it!')}
+ </AppButton.BlueButton>,
+ ]}
+ close={hideKillSwitchInfo}>
+ <ModalMessage>
+ {messages.pgettext(
+ 'vpn-settings-view',
+ 'This built-in feature prevents your traffic from leaking outside of the VPN tunnel if your network suddenly stops working or if the tunnel fails, it does this by blocking your traffic until your connection is reestablished.',
+ )}
+ </ModalMessage>
+ <ModalMessage>
+ {messages.pgettext(
+ 'vpn-settings-view',
+ 'The difference between the Kill Switch and Lockdown Mode is that the Kill Switch will prevent any leaks from happening during automatic tunnel reconnects, software crashes and similar accidents. With Lockdown Mode enabled, you must be connected to a Mullvad VPN server to be able to reach the internet. Manually disconnecting or quitting the app will block your connection.',
+ )}
+ </ModalMessage>
+ </ModalAlert>
+ </>
+ );
+}
+
+function LockdownMode() {
+ const blockWhenDisconnected = useSelector((state) => state.settings.blockWhenDisconnected);
+ const { setBlockWhenDisconnected: setBlockWhenDisconnectedImpl } = useAppContext();
+
+ const [confirmationDialogVisible, showConfirmationDialog, hideConfirmationDialog] = useBoolean(
+ false,
+ );
+
+ const setBlockWhenDisconnected = useCallback(
+ async (blockWhenDisconnected: boolean) => {
+ try {
+ await setBlockWhenDisconnectedImpl(blockWhenDisconnected);
+ } catch (e) {
+ const error = e as Error;
+ log.error('Failed to update block when disconnected', error.message);
+ }
+ },
+ [setBlockWhenDisconnectedImpl],
+ );
+
+ const setLockDownMode = useCallback(
+ async (newValue: boolean) => {
+ if (newValue) {
+ showConfirmationDialog();
+ } else {
+ await setBlockWhenDisconnected(false);
+ }
+ },
+ [setBlockWhenDisconnected, showConfirmationDialog],
+ );
+
+ const confirmLockdownMode = useCallback(async () => {
+ hideConfirmationDialog();
+ await setBlockWhenDisconnected(true);
+ }, [hideConfirmationDialog, setBlockWhenDisconnected]);
+
+ return (
+ <>
+ <AriaInputGroup>
+ <Cell.Container>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('vpn-settings-view', 'Lockdown mode')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaDetails>
+ <InfoButton>
+ <ModalMessage>
+ {messages.pgettext(
+ 'vpn-settings-view',
+ 'The difference between the Kill Switch and Lockdown Mode is that the Kill Switch will prevent any leaks from happening during automatic tunnel reconnects, software crashes and similar accidents.',
+ )}
+ </ModalMessage>
+ <ModalMessage>
+ {messages.pgettext(
+ 'vpn-settings-view',
+ 'With Lockdown Mode enabled, you must be connected to a Mullvad VPN server to be able to reach the internet. Manually disconnecting or quitting the app will block your connection.',
+ )}
+ </ModalMessage>
+ </InfoButton>
+ </AriaDetails>
+ <AriaInput>
+ <Cell.Switch isOn={blockWhenDisconnected} onChange={setLockDownMode} />
+ </AriaInput>
+ </Cell.Container>
+ </AriaInputGroup>
+ <ModalAlert
+ isOpen={confirmationDialogVisible}
+ type={ModalAlertType.caution}
+ buttons={[
+ <AppButton.RedButton key="confirm" onClick={confirmLockdownMode}>
+ {messages.gettext('Enable anyway')}
+ </AppButton.RedButton>,
+ <AppButton.BlueButton key="back" onClick={hideConfirmationDialog}>
+ {messages.gettext('Back')}
+ </AppButton.BlueButton>,
+ ]}
+ close={hideConfirmationDialog}>
+ <ModalMessage>
+ {messages.pgettext(
+ 'vpn-settings-view',
+ 'Attention: enabling this will always require a Mullvad VPN connection in order to reach the internet.',
+ )}
+ </ModalMessage>
+ <ModalMessage>
+ {messages.pgettext(
+ 'vpn-settings-view',
+ 'The app’s built-in kill switch is always on. This setting will additionally block the internet if clicking Disconnect or Quit.',
+ )}
+ </ModalMessage>
+ </ModalAlert>
+ </>
+ );
+}
+
+function TunnelProtocolSetting() {
+ const tunnelProtocol = useSelector((state) =>
+ mapRelaySettingsToProtocol(state.settings.relaySettings),
+ );
+ const { updateRelaySettings } = useAppContext();
+
+ const setTunnelProtocol = useCallback(async (tunnelProtocol: TunnelProtocol | undefined) => {
+ const relayUpdate = RelaySettingsBuilder.normal()
+ .tunnel.tunnelProtocol((config) => {
+ if (tunnelProtocol) {
+ config.tunnelProtocol.exact(tunnelProtocol);
+ } else {
+ config.tunnelProtocol.any();
+ }
+ })
+ .build();
+ try {
+ await updateRelaySettings(relayUpdate);
+ } catch (e) {
+ const error = e as Error;
+ log.error('Failed to update tunnel protocol constraints', error.message);
+ }
+ }, []);
+
+ const tunnelProtocolItems: Array<ISelectorItem<TunnelProtocol | undefined>> = useMemo(
+ () => [
+ {
+ label: messages.gettext('Automatic'),
+ value: undefined,
+ },
+ {
+ label: strings.wireguard,
+ value: 'wireguard',
+ },
+ {
+ label: strings.openvpn,
+ value: 'openvpn',
+ },
+ ],
+ [],
+ );
+
+ return (
+ <AriaInputGroup>
+ <StyledSelectorContainer>
+ <Selector
+ title={messages.pgettext('vpn-settings-view', 'Tunnel protocol')}
+ values={tunnelProtocolItems}
+ value={tunnelProtocol}
+ onSelect={setTunnelProtocol}
+ />
+ </StyledSelectorContainer>
+ </AriaInputGroup>
+ );
+}
+
+function mapRelaySettingsToProtocol(relaySettings: RelaySettingsRedux) {
+ if ('normal' in relaySettings) {
+ const { tunnelProtocol } = relaySettings.normal;
+ return tunnelProtocol === 'any' ? undefined : tunnelProtocol;
+ // since the GUI doesn't display custom settings, just display the default ones.
+ // If the user sets any settings, then those will be applied.
+ } else if ('customTunnelEndpoint' in relaySettings) {
+ return undefined;
+ } else {
+ throw new Error('Unknown type of relay settings.');
+ }
+}
+
+function WireguardSettingsButton() {
+ const history = useHistory();
+ const tunnelProtocol = useSelector((state) =>
+ mapRelaySettingsToProtocol(state.settings.relaySettings),
+ );
+
+ const navigate = useCallback(() => history.push(RoutePath.wireguardSettings), [history]);
+
+ return (
+ <Cell.CellNavigationButton onClick={navigate} disabled={tunnelProtocol === 'openvpn'}>
+ <Cell.Label>
+ {sprintf(
+ // TRANSLATORS: %(wireguard)s will be replaced with the string "WireGuard"
+ messages.pgettext('vpn-settings-view', '%(wireguard)s settings'),
+ { wireguard: strings.wireguard },
+ )}
+ </Cell.Label>
+ </Cell.CellNavigationButton>
+ );
+}
+
+function OpenVpnSettingsButton() {
+ const history = useHistory();
+ const tunnelProtocol = useSelector((state) =>
+ mapRelaySettingsToProtocol(state.settings.relaySettings),
+ );
+
+ const navigate = useCallback(() => history.push(RoutePath.openVpnSettings), [history]);
+
+ return (
+ <Cell.CellNavigationButton onClick={navigate} disabled={tunnelProtocol === 'wireguard'}>
+ <Cell.Label>
+ {sprintf(
+ // TRANSLATORS: %(openvpn)s will be replaced with the string "OpenVPN"
+ messages.pgettext('vpn-settings-view', '%(openvpn)s settings'),
+ { openvpn: strings.openvpn },
+ )}
+ </Cell.Label>
+ </Cell.CellNavigationButton>
+ );
+}
diff --git a/gui/src/renderer/components/WireguardSettings.tsx b/gui/src/renderer/components/WireguardSettings.tsx
index 20c3009574..285d28b0c4 100644
--- a/gui/src/renderer/components/WireguardSettings.tsx
+++ b/gui/src/renderer/components/WireguardSettings.tsx
@@ -159,7 +159,7 @@ export default class WireguardSettings extends React.Component<IProps, IState> {
<Cell.InputLabel>
{
// TRANSLATORS: The label next to the multihop settings toggle.
- messages.pgettext('advanced-settings-view', 'Enable multihop')
+ messages.pgettext('vpn-settings-view', 'Enable multihop')
}
</Cell.InputLabel>
</AriaLabel>
@@ -179,7 +179,7 @@ export default class WireguardSettings extends React.Component<IProps, IState> {
// TRANSLATORS: Available placeholders:
// TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard"
messages.pgettext(
- 'advanced-settings-view',
+ 'vpn-settings-view',
'Increases anonymity by routing your traffic into one %(wireguard)s server and out another, making it harder to trace.',
),
{ wireguard: strings.wireguard },
diff --git a/gui/src/renderer/lib/routes.ts b/gui/src/renderer/lib/routes.ts
index 43c3c9b4fb..a997a97cd3 100644
--- a/gui/src/renderer/lib/routes.ts
+++ b/gui/src/renderer/lib/routes.ts
@@ -16,7 +16,7 @@ export enum RoutePath {
selectLanguage = '/settings/language',
accountSettings = '/settings/account',
interfaceSettings = '/settings/interface',
- advancedSettings = '/settings/advanced',
+ vpnSettings = '/settings/vpn',
wireguardSettings = '/settings/advanced/wireguard',
openVpnSettings = '/settings/advanced/openvpn',
splitTunneling = '/settings/advanced/split-tunneling',
diff --git a/gui/src/shared/localization-contexts.ts b/gui/src/shared/localization-contexts.ts
index 336c0c9fe1..980e79917a 100644
--- a/gui/src/shared/localization-contexts.ts
+++ b/gui/src/shared/localization-contexts.ts
@@ -23,8 +23,7 @@ export type LocalizationContexts =
| 'redeem-voucher-view'
| 'redeem-voucher-alert'
| 'interface-settings-view'
- | 'advanced-settings-view'
- | 'advanced-settings-nav'
+ | 'vpn-settings-view'
| 'wireguard-settings-view'
| 'wireguard-settings-nav'
| 'openvpn-settings-view'