summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar <oskar@mullvad.net>2025-06-30 11:53:51 +0200
committerOskar <oskar@mullvad.net>2025-06-30 11:53:51 +0200
commit91e70c3ea3038251445d2fcf32c72ceaf5122430 (patch)
treec7c5de502a44075a32e7fca12cd738e9fe7822dd
parent31d2c4d704bd00e3a1624291ba138d799529068e (diff)
parentc163e18f355ffddc6c0162a1dd2dad73240ebdf6 (diff)
downloadmullvadvpn-91e70c3ea3038251445d2fcf32c72ceaf5122430.tar.xz
mullvadvpn-91e70c3ea3038251445d2fcf32c72ceaf5122430.zip
Merge branch 'start-system-service-from-ui-des-1466'
-rw-r--r--desktop/packages/mullvad-vpn/locales/messages.pot50
-rw-r--r--desktop/packages/mullvad-vpn/src/main/user-interface.ts53
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/app.tsx18
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/AppRouter.tsx5
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/Launch.tsx140
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/Modal.tsx2
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/LaunchView.tsx31
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/Footer.tsx20
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/default-footer/DefaultFooter.tsx30
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/default-footer/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/footer-text/FooterText.tsx7
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/footer-text/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/hooks/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/hooks/useContent.tsx10
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/macos-permission-footer/MacOsSystemSettingsFooter.tsx39
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/macos-permission-footer/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/restart-daemon-footer/RestartDaemonFooter.tsx43
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/restart-daemon-footer/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/index.ts6
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/status-text/StatusText.tsx6
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/status-text/hooks/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/status-text/hooks/useStatusText.tsx46
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/status-text/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/troubleshooting-modal/TroubleshootingModal.tsx70
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/troubleshooting-modal/hooks/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/troubleshooting-modal/hooks/useTroubleshootingSteps.tsx50
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/troubleshooting-modal/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/launch/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/typography/Text.tsx7
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/view/View.tsx27
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/view/components/Container.tsx24
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/view/components/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/view/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/redux/userinterface/actions.ts16
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/redux/userinterface/hooks/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/redux/userinterface/hooks/useUserInterfaceDaemonStatus.ts7
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/redux/userinterface/reducers.ts9
-rw-r--r--desktop/packages/mullvad-vpn/src/shared/ipc-schema.ts3
-rw-r--r--desktop/packages/mullvad-vpn/src/shared/ipc-types.ts2
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/mocked/launch/ipc.ts17
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/mocked/launch/launch.spec.ts62
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/route-object-models/launch/index.ts2
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/route-object-models/launch/launch-route-object-model.ts21
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/route-object-models/launch/selectors.ts9
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/route-object-models/routes-object-model.ts3
48 files changed, 695 insertions, 156 deletions
diff --git a/desktop/packages/mullvad-vpn/locales/messages.pot b/desktop/packages/mullvad-vpn/locales/messages.pot
index 71041171f0..70c4619373 100644
--- a/desktop/packages/mullvad-vpn/locales/messages.pot
+++ b/desktop/packages/mullvad-vpn/locales/messages.pot
@@ -1385,41 +1385,79 @@ msgctxt "in-app-notifications"
msgid "Welcome, this device is now called <b>%(deviceName)s</b>. For more details see the info button in Account."
msgstr ""
+#. Status text app is trying to connect to the system service.
msgctxt "launch-view"
msgid "Connecting to Mullvad system service..."
msgstr ""
+#. Button label for opening dialog with troubleshooting details.
msgctxt "launch-view"
-msgid "Disable third party antivirus software."
+msgid "Details"
msgstr ""
+#. List item in troubleshooting modal advising user disable third party antivirus.
+msgctxt "launch-view"
+msgid "Disabling third party antivirus software"
+msgstr ""
+
+#. Status text shown when app fails to start.
+msgctxt "launch-view"
+msgid "Failed to start the app, please try again or click “Details” for more info"
+msgstr ""
+
+#. Message in troubleshooting modal advising user to send a problem report if the steps do not work.
msgctxt "launch-view"
msgid "If these steps do not work please send a problem report."
msgstr ""
+#. Message in launch view when the background process permissions have been revoked.
msgctxt "launch-view"
msgid "Permission for the Mullvad VPN service has been revoked. Please go to System Settings and allow Mullvad VPN under the “Allow in the Background” setting."
msgstr ""
+#. List item in troubleshooting modal advising user to reinstall the app.
msgctxt "launch-view"
-msgid "Reinstalling the app."
+msgid "Reinstalling the app"
msgstr ""
+#. List item in troubleshooting modal advising user to restart background process.
msgctxt "launch-view"
-msgid "Restarting your computer."
+msgid "Restarting the Mullvad background process"
msgstr ""
-#. Button label for problem report view.
+#. List item in troubleshooting modal instructing user how to restart the background process.
+msgctxt "launch-view"
+msgid "Restarting the Mullvad background process by clicking \"Back\", then \"Try again\""
+msgstr ""
+
+#. List item in troubleshooting modal advising user to restart their computer.
+msgctxt "launch-view"
+msgid "Restarting your computer"
+msgstr ""
+
+#. Button label for sending a problem report.
msgctxt "launch-view"
msgid "Send problem report"
msgstr ""
+#. Status text shown when app is starting.
+msgctxt "launch-view"
+msgid "Starting up...."
+msgstr ""
+
+#. Message in troubleshooting modal when the background process failed to start.
msgctxt "launch-view"
-msgid "The system service component of the app hasn’t started or can’t be contacted. The system service is responsible for the security, kill switch, and the VPN tunnel. To troubleshoot please try:"
+msgid "The Mullvad background process failed to start. The background process is responsible for the security, kill switch, and the VPN tunnel. Please try:"
+msgstr ""
+
+#. Button label for trying to restart the daemon again.
+msgctxt "launch-view"
+msgid "Try again"
msgstr ""
+#. Message in launch view when the mullvad service cannot be contacted.
msgctxt "launch-view"
-msgid "Unable to contact the Mullvad system service, your connection might be unsecure. Please troubleshoot or send a problem report by clicking the Learn more button."
+msgid "Unable to contact the Mullvad system service, your connection might be unsecure. Please troubleshoot or send a problem report by clicking the \"Learn more\" button."
msgstr ""
#. This is a warning message shown when the app is blocking the users
diff --git a/desktop/packages/mullvad-vpn/src/main/user-interface.ts b/desktop/packages/mullvad-vpn/src/main/user-interface.ts
index 0c295aad77..19b45e22cf 100644
--- a/desktop/packages/mullvad-vpn/src/main/user-interface.ts
+++ b/desktop/packages/mullvad-vpn/src/main/user-interface.ts
@@ -1,4 +1,4 @@
-import { exec } from 'child_process';
+import { exec, spawn } from 'child_process';
import { app, BrowserWindow, dialog, Menu, nativeImage, screen, Tray } from 'electron';
import path from 'path';
import { sprintf } from 'sprintf-js';
@@ -18,6 +18,7 @@ import {
} from './ipc-event-channel';
import { WebContentsConsoleInput } from './logging';
import { isMacOs11OrNewer } from './platform-version';
+import { resolveBin } from './proc';
import TrayIconController, { TrayIconType } from './tray-icon-controller';
import WindowController, { WindowControllerDelegate } from './window-controller';
@@ -62,6 +63,56 @@ export default class UserInterface implements WindowControllerDelegate {
}
public registerIpcListeners() {
+ IpcMainEventChannel.daemon.handleTryStart(() => {
+ IpcMainEventChannel.daemon.notifyTryStartEvent?.('start-requested');
+
+ try {
+ const SETUP_PATH = `"\\"${resolveBin('mullvad-setup')}\\""`;
+ const SYSTEM_ROOT_PATH = process.env.SYSTEMROOT || process.env.windir || 'C:\\Windows';
+ const PWSH_PATH = `${SYSTEM_ROOT_PATH}\\System32\\WindowsPowershell\\v1.0\\powershell.exe`;
+
+ const child = spawn(
+ PWSH_PATH,
+ [
+ '-Command',
+ 'Start-Process',
+ SETUP_PATH,
+ 'start-service',
+ '-Verb',
+ 'RunAs',
+ '-WindowStyle',
+ 'Hidden',
+ '-Wait',
+ ],
+ {
+ detached: false,
+ stdio: 'ignore',
+ windowsVerbatimArguments: true,
+ },
+ );
+ child.once('error', (error) => {
+ log.error(`"mullvad-setup.exe start-service" failed: ${error.message}`);
+ IpcMainEventChannel.daemon.notifyTryStartEvent?.('stopped');
+ });
+
+ child.once('exit', (code) => {
+ if (code !== 0) {
+ log.error(
+ `"mullvad-setup.exe start-service" exited unexpectedly with exit code: ${code}`,
+ );
+ IpcMainEventChannel.daemon.notifyTryStartEvent?.('stopped');
+ } else {
+ log.info('"mullvad-setup.exe start-service" succeeded');
+ // 'running' is set from onDaemonConnected event handler
+ }
+ });
+ } catch (e) {
+ const error = e as Error;
+ log.error(`Failed to run "mullvad-setup.exe start-service". Error: ${error.message}`);
+ IpcMainEventChannel.daemon.notifyTryStartEvent?.('stopped');
+ }
+ });
+
IpcMainEventChannel.app.handleShowOpenDialog(async (options) => {
this.browsingFiles = true;
const response = await dialog.showOpenDialog({
diff --git a/desktop/packages/mullvad-vpn/src/renderer/app.tsx b/desktop/packages/mullvad-vpn/src/renderer/app.tsx
index 8c39327f40..e705f76661 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/app.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/app.tsx
@@ -36,7 +36,12 @@ import {
} from '../shared/daemon-rpc-types';
import { messages, relayLocations } from '../shared/gettext';
import { IGuiSettingsState, SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state';
-import { IChangelog, ICurrentAppVersionInfo, IHistoryObject } from '../shared/ipc-types';
+import {
+ DaemonStatus,
+ IChangelog,
+ ICurrentAppVersionInfo,
+ IHistoryObject,
+} from '../shared/ipc-types';
import log, { ConsoleOutput } from '../shared/logging';
import { LogLevel } from '../shared/logging-types';
import { RoutePath } from '../shared/routes';
@@ -176,6 +181,10 @@ export default class AppRenderer {
this.setRelayListPair(relayListPair);
});
+ IpcRendererEventChannel.daemon.listenTryStartEvent((status: DaemonStatus) => {
+ this.reduxActions.userInterface.setDaemonStatus(status);
+ });
+
IpcRendererEventChannel.app.listenUpgradeEvent((appUpgradeEvent) => {
this.reduxActions.appUpgrade.setAppUpgradeEvent(appUpgradeEvent);
@@ -436,6 +445,11 @@ export default class AppRenderer {
public daemonPrepareRestart = (shutdown: boolean): void => {
IpcRendererEventChannel.daemon.prepareRestart(shutdown);
};
+
+ public tryStartDaemon = () => {
+ if (window.env.platform === 'win32') IpcRendererEventChannel.daemon.tryStart();
+ };
+
public appUpgrade = () => {
const reduxState = this.reduxStore.getState();
const appUpgradeError = reduxState.appUpgrade.error;
@@ -804,12 +818,14 @@ export default class AppRenderer {
this.connectedToDaemon = true;
this.reduxActions.userInterface.setConnectedToDaemon(true);
this.reduxActions.userInterface.setDaemonAllowed(true);
+ this.reduxActions.userInterface.setDaemonStatus('running');
this.resetNavigation();
}
private onDaemonDisconnected() {
this.connectedToDaemon = false;
this.reduxActions.userInterface.setConnectedToDaemon(false);
+ this.reduxActions.userInterface.setDaemonStatus('stopped');
this.resetNavigation();
}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/AppRouter.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/AppRouter.tsx
index aba4887f2e..492199c76f 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/AppRouter.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/AppRouter.tsx
@@ -21,7 +21,6 @@ import {
import ExpiredAccountErrorView from './ExpiredAccountErrorView';
import Filter from './Filter';
import Focus, { IFocusHandle } from './Focus';
-import Launch from './Launch';
import MainView from './main-view/MainView';
import MultihopSettings from './MultihopSettings';
import OpenVpnSettings from './OpenVpnSettings';
@@ -35,7 +34,7 @@ import Support from './Support';
import TooManyDevices from './TooManyDevices';
import UdpOverTcp from './UdpOverTcp';
import UserInterfaceSettings from './UserInterfaceSettings';
-import { AppInfoView, AppUpgradeView, ChangelogView, SettingsView } from './views';
+import { AppInfoView, AppUpgradeView, ChangelogView, LaunchView, SettingsView } from './views';
import VpnSettings from './VpnSettings';
import WireguardSettings from './WireguardSettings';
@@ -50,7 +49,7 @@ export default function AppRouter() {
return (
<Focus ref={focusRef}>
<Switch key={currentLocation.key} location={currentLocation}>
- <Route exact path={RoutePath.launch} component={Launch} />
+ <Route exact path={RoutePath.launch} component={LaunchView} />
<Route exact path={RoutePath.login} component={LoginPage} />
<Route exact path={RoutePath.tooManyDevices} component={TooManyDevices} />
<Route exact path={RoutePath.deviceRevoked} component={DeviceRevokedView} />
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/Launch.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/Launch.tsx
deleted file mode 100644
index 2f68f9395b..0000000000
--- a/desktop/packages/mullvad-vpn/src/renderer/components/Launch.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-import { useCallback } from 'react';
-import styled from 'styled-components';
-
-import { messages } from '../../shared/gettext';
-import { RoutePath } from '../../shared/routes';
-import { useAppContext } from '../context';
-import { Button } from '../lib/components';
-import { colors } from '../lib/foundations';
-import { TransitionType, useHistory } from '../lib/history';
-import { useBoolean } from '../lib/utility-hooks';
-import { useSelector } from '../redux/store';
-import { measurements, tinyText } from './common-styles';
-import ErrorView from './ErrorView';
-import { Footer } from './Layout';
-import { ModalAlert, ModalAlertType, ModalMessage, ModalMessageList } from './Modal';
-
-export default function Launch() {
- const daemonAllowed = useSelector((state) => state.userInterface.daemonAllowed);
- const footer = daemonAllowed === false ? <MacOsPermissionFooter /> : <DefaultFooter />;
-
- return (
- <ErrorView footer={footer}>
- {messages.pgettext('launch-view', 'Connecting to Mullvad system service...')}
- </ErrorView>
- );
-}
-
-const StyledFooter = styled(Footer)({
- backgroundColor: colors.blue,
- transition: 'opacity 250ms ease-in-out',
-});
-
-const StyledFooterInner = styled.div({
- display: 'flex',
- flexDirection: 'column',
- flex: 1,
- backgroundColor: colors.darkBlue,
- borderRadius: '8px',
- margin: 0,
- padding: '16px',
-});
-
-const StyledFooterMessage = styled.span(tinyText, {
- color: colors.white,
- margin: `8px 0 ${measurements.buttonVerticalMargin} 0`,
-});
-
-function MacOsPermissionFooter() {
- const { showLaunchDaemonSettings } = useAppContext();
-
- const openSettings = useCallback(async () => {
- await showLaunchDaemonSettings();
- }, [showLaunchDaemonSettings]);
-
- return (
- <StyledFooter>
- <StyledFooterInner>
- <StyledFooterMessage>
- {messages.pgettext(
- 'launch-view',
- 'Permission for the Mullvad VPN service has been revoked. Please go to System Settings and allow Mullvad VPN under the “Allow in the Background” setting.',
- )}
- </StyledFooterMessage>
- <Button onClick={openSettings}>
- <Button.Text>
- {
- // TRANSLATORS: Button label for system settings.
- messages.gettext('Go to System Settings')
- }
- </Button.Text>
- </Button>
- </StyledFooterInner>
- </StyledFooter>
- );
-}
-
-function DefaultFooter() {
- const { push } = useHistory();
- const [dialogVisible, showDialog, hideDialog] = useBoolean();
-
- const openSendProblemReport = useCallback(() => {
- hideDialog();
- push(RoutePath.problemReport, { transition: TransitionType.show });
- }, [hideDialog, push]);
-
- return (
- <>
- <StyledFooter>
- <StyledFooterInner>
- <StyledFooterMessage>
- {messages.pgettext(
- 'launch-view',
- 'Unable to contact the Mullvad system service, your connection might be unsecure. Please troubleshoot or send a problem report by clicking the Learn more button.',
- )}
- </StyledFooterMessage>
- <Button onClick={showDialog}>
- <Button.Text>{messages.gettext('Learn more')}</Button.Text>
- </Button>
- </StyledFooterInner>
- </StyledFooter>
- <ModalAlert
- isOpen={dialogVisible}
- type={ModalAlertType.info}
- close={hideDialog}
- buttons={[
- <Button variant="success" key="problem-report" onClick={openSendProblemReport}>
- <Button.Text>
- {
- // TRANSLATORS: Button label for problem report view.
- messages.pgettext('launch-view', 'Send problem report')
- }
- </Button.Text>
- </Button>,
- <Button key="back" onClick={hideDialog}>
- <Button.Text>{messages.gettext('Back')}</Button.Text>
- </Button>,
- ]}>
- <ModalMessage>
- {messages.pgettext(
- 'launch-view',
- 'The system service component of the app hasn’t started or can’t be contacted. The system service is responsible for the security, kill switch, and the VPN tunnel. To troubleshoot please try:',
- )}
- </ModalMessage>
- <ModalMessage>
- <ModalMessageList>
- <li>{messages.pgettext('launch-view', 'Restarting your computer.')}</li>
- <li>{messages.pgettext('launch-view', 'Reinstalling the app.')}</li>
- <li>{messages.pgettext('launch-view', 'Disable third party antivirus software.')}</li>
- </ModalMessageList>
- </ModalMessage>
- <ModalMessage>
- {messages.pgettext(
- 'launch-view',
- 'If these steps do not work please send a problem report.',
- )}
- </ModalMessage>
- </ModalAlert>
- </>
- );
-}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/Modal.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/Modal.tsx
index c978ed4891..ba2077ec03 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/Modal.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/Modal.tsx
@@ -360,5 +360,5 @@ export const ModalMessage = styled.span(tinyText, {
export const ModalMessageList = styled.ul({
listStyle: 'disc outside',
paddingLeft: '20px',
- color: colors.whiteAlpha80,
+ color: colors.whiteAlpha60,
});
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/index.ts
index 686a4bbbd3..2ce9d17013 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/views/index.ts
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/index.ts
@@ -1,4 +1,5 @@
export * from './app-info';
export * from './app-upgrade';
+export * from './launch';
export * from './changelog';
export * from './settings';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/LaunchView.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/LaunchView.tsx
new file mode 100644
index 0000000000..815d3dcf80
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/LaunchView.tsx
@@ -0,0 +1,31 @@
+import { Flex, Logo } from '../../../lib/components';
+import { View } from '../../../lib/components/view';
+import { AppMainHeader } from '../../app-main-header';
+import { Footer, StatusText } from './components';
+
+export function LaunchView() {
+ return (
+ <View>
+ <AppMainHeader logoVariant="none">
+ <AppMainHeader.SettingsButton />
+ </AppMainHeader>
+ <View.Container size="4" $flex={1}>
+ <Flex
+ $flexDirection="column"
+ $flex={1}
+ $margin={{ vertical: 'large' }}
+ $alignItems="center"
+ $gap="medium">
+ <Flex $flexDirection="column" $gap="medium">
+ <Logo variant="icon" size="2" />
+ <Logo variant="text" size="2" />
+ </Flex>
+ <StatusText />
+ </Flex>
+ <Flex $margin={{ vertical: 'large' }}>
+ <Footer />
+ </Flex>
+ </View.Container>
+ </View>
+ );
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/Footer.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/Footer.tsx
new file mode 100644
index 0000000000..7a08723916
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/Footer.tsx
@@ -0,0 +1,20 @@
+import styled from 'styled-components';
+
+import { Flex } from '../../../../../lib/components';
+import { colors, Radius } from '../../../../../lib/foundations';
+import { useContent } from './components/hooks';
+
+const StyledLaunchFooter = styled(Flex)`
+ width: 100%;
+ background-color: ${colors.darkBlue};
+ border-radius: ${Radius.radius8};
+`;
+
+export function Footer() {
+ const content = useContent();
+ return (
+ <StyledLaunchFooter $flexDirection="column" $padding="medium">
+ {content}
+ </StyledLaunchFooter>
+ );
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/default-footer/DefaultFooter.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/default-footer/DefaultFooter.tsx
new file mode 100644
index 0000000000..3a64f7c19d
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/default-footer/DefaultFooter.tsx
@@ -0,0 +1,30 @@
+import { messages } from '../../../../../../../../shared/gettext';
+import { Button } from '../../../../../../../lib/components';
+import { FlexColumn } from '../../../../../../../lib/components/flex-column';
+import { useBoolean } from '../../../../../../../lib/utility-hooks';
+import { TroubleshootingModal } from '../../../troubleshooting-modal';
+import { FooterText } from '../footer-text';
+
+export function DefaultLaunchFooter() {
+ const [dialogOpen, showDialog, hideDialog] = useBoolean();
+
+ return (
+ <>
+ <FlexColumn $gap="medium">
+ <FooterText>
+ {
+ // TRANSLATORS: Message in launch view when the mullvad service cannot be contacted.
+ messages.pgettext(
+ 'launch-view',
+ 'Unable to contact the Mullvad system service, your connection might be unsecure. Please troubleshoot or send a problem report by clicking the "Learn more" button.',
+ )
+ }
+ </FooterText>
+ <Button onClick={showDialog}>
+ <Button.Text>{messages.gettext('Learn more')}</Button.Text>
+ </Button>
+ </FlexColumn>
+ <TroubleshootingModal isOpen={dialogOpen} onClose={hideDialog} />
+ </>
+ );
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/default-footer/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/default-footer/index.ts
new file mode 100644
index 0000000000..59528bba29
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/default-footer/index.ts
@@ -0,0 +1 @@
+export * from './DefaultFooter';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/footer-text/FooterText.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/footer-text/FooterText.tsx
new file mode 100644
index 0000000000..61e605fb81
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/footer-text/FooterText.tsx
@@ -0,0 +1,7 @@
+import { Text, TextProps } from '../../../../../../../lib/components';
+
+export type FooterTextProps = TextProps;
+
+export function FooterText(props: FooterTextProps) {
+ return <Text variant="labelTiny" {...props}></Text>;
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/footer-text/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/footer-text/index.ts
new file mode 100644
index 0000000000..09cf8c8567
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/footer-text/index.ts
@@ -0,0 +1 @@
+export * from './FooterText';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/hooks/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/hooks/index.ts
new file mode 100644
index 0000000000..951561b9d2
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/hooks/index.ts
@@ -0,0 +1 @@
+export * from './useContent';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/hooks/useContent.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/hooks/useContent.tsx
new file mode 100644
index 0000000000..8bcda7c0c6
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/hooks/useContent.tsx
@@ -0,0 +1,10 @@
+import { useSelector } from '../../../../../../../redux/store';
+import { DefaultLaunchFooter, MacOsPermissionFooter, RestartDaemonFooter } from '../../..';
+
+export const useContent = () => {
+ const platform = window.env.platform;
+ const daemonAllowed = useSelector((state) => state.userInterface.daemonAllowed);
+ if (platform === 'darwin' && daemonAllowed === false) return <MacOsPermissionFooter />;
+ if (platform === 'win32') return <RestartDaemonFooter />;
+ return <DefaultLaunchFooter />;
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/index.ts
new file mode 100644
index 0000000000..3c88ff6018
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/index.ts
@@ -0,0 +1 @@
+export * from './footer-text';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/macos-permission-footer/MacOsSystemSettingsFooter.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/macos-permission-footer/MacOsSystemSettingsFooter.tsx
new file mode 100644
index 0000000000..fa680bacdf
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/macos-permission-footer/MacOsSystemSettingsFooter.tsx
@@ -0,0 +1,39 @@
+import { useCallback } from 'react';
+
+import { messages } from '../../../../../../../../shared/gettext';
+import { useAppContext } from '../../../../../../../context';
+import { Button } from '../../../../../../../lib/components';
+import { FlexColumn } from '../../../../../../../lib/components/flex-column';
+import { FooterText } from '../footer-text';
+
+export function MacOsPermissionFooter() {
+ const { showLaunchDaemonSettings } = useAppContext();
+
+ const openSettings = useCallback(async () => {
+ await showLaunchDaemonSettings();
+ }, [showLaunchDaemonSettings]);
+
+ return (
+ <>
+ <FlexColumn $gap="medium">
+ <FooterText>
+ {
+ // TRANSLATORS: Message in launch view when the background process permissions have been revoked.
+ messages.pgettext(
+ 'launch-view',
+ 'Permission for the Mullvad VPN service has been revoked. Please go to System Settings and allow Mullvad VPN under the “Allow in the Background” setting.',
+ )
+ }
+ </FooterText>
+ <Button onClick={openSettings}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for system settings.
+ messages.gettext('Go to System Settings')
+ }
+ </Button.Text>
+ </Button>
+ </FlexColumn>
+ </>
+ );
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/macos-permission-footer/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/macos-permission-footer/index.ts
new file mode 100644
index 0000000000..f0ffcd17b5
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/macos-permission-footer/index.ts
@@ -0,0 +1 @@
+export * from './MacOsSystemSettingsFooter';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/restart-daemon-footer/RestartDaemonFooter.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/restart-daemon-footer/RestartDaemonFooter.tsx
new file mode 100644
index 0000000000..88647c26dd
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/restart-daemon-footer/RestartDaemonFooter.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+
+import { messages } from '../../../../../../../../shared/gettext';
+import { useAppContext } from '../../../../../../../context';
+import { Button } from '../../../../../../../lib/components';
+import { FlexColumn } from '../../../../../../../lib/components/flex-column';
+import { useBoolean } from '../../../../../../../lib/utility-hooks';
+import { useUserInterfaceDaemonStatus } from '../../../../../../../redux/hooks';
+import { TroubleshootingModal } from '../../../troubleshooting-modal';
+
+export function RestartDaemonFooter() {
+ const { tryStartDaemon } = useAppContext();
+ const { daemonStatus } = useUserInterfaceDaemonStatus();
+ const [dialogOpen, showDialog, hideDialog] = useBoolean();
+
+ const handleTryAgain = React.useCallback(() => {
+ tryStartDaemon();
+ }, [tryStartDaemon]);
+
+ return (
+ <>
+ <FlexColumn $gap="medium">
+ <Button onClick={handleTryAgain} disabled={daemonStatus && daemonStatus !== 'stopped'}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for trying to restart the daemon again.
+ messages.pgettext('launch-view', 'Try again')
+ }
+ </Button.Text>
+ </Button>
+ <Button onClick={showDialog}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for opening dialog with troubleshooting details.
+ messages.pgettext('launch-view', 'Details')
+ }
+ </Button.Text>
+ </Button>
+ </FlexColumn>
+ <TroubleshootingModal isOpen={dialogOpen} onClose={hideDialog} />
+ </>
+ );
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/restart-daemon-footer/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/restart-daemon-footer/index.ts
new file mode 100644
index 0000000000..81ebd79ab8
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/components/restart-daemon-footer/index.ts
@@ -0,0 +1 @@
+export * from './RestartDaemonFooter';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/index.ts
new file mode 100644
index 0000000000..ddcc5a9cd1
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/footer/index.ts
@@ -0,0 +1 @@
+export * from './Footer';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/index.ts
new file mode 100644
index 0000000000..e132e2b52e
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/index.ts
@@ -0,0 +1,6 @@
+export * from './footer';
+export * from './footer/components/default-footer';
+export * from './footer/components/macos-permission-footer';
+export * from './footer/components/restart-daemon-footer';
+export * from './status-text';
+export * from './troubleshooting-modal';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/status-text/StatusText.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/status-text/StatusText.tsx
new file mode 100644
index 0000000000..feb8a4e1fe
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/status-text/StatusText.tsx
@@ -0,0 +1,6 @@
+import { useStatusText } from './hooks';
+
+export function StatusText() {
+ const status = useStatusText();
+ return <>{status}</>;
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/status-text/hooks/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/status-text/hooks/index.ts
new file mode 100644
index 0000000000..fd5511b107
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/status-text/hooks/index.ts
@@ -0,0 +1 @@
+export * from './useStatusText';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/status-text/hooks/useStatusText.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/status-text/hooks/useStatusText.tsx
new file mode 100644
index 0000000000..716662ac90
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/status-text/hooks/useStatusText.tsx
@@ -0,0 +1,46 @@
+import { messages } from '../../../../../../../shared/gettext';
+import { BodySmall, Flex, Spinner } from '../../../../../../lib/components';
+import { FlexColumn } from '../../../../../../lib/components/flex-column';
+import { useUserInterfaceDaemonStatus } from '../../../../../../redux/hooks';
+
+export const useStatusText = () => {
+ const { daemonStatus } = useUserInterfaceDaemonStatus();
+
+ let statusMessage = (
+ <BodySmall color="whiteAlpha40" textAlign="center" role="alert">
+ {
+ // TRANSLATORS: Status text app is trying to connect to the system service.
+ messages.pgettext('launch-view', 'Connecting to Mullvad system service...')
+ }
+ </BodySmall>
+ );
+ if (window.env.platform === 'win32') {
+ if (daemonStatus === 'start-requested') {
+ statusMessage = (
+ <FlexColumn $alignItems="center" $gap="big">
+ <BodySmall color="whiteAlpha40" textAlign="center" role="alert">
+ {
+ // TRANSLATORS: Status text shown when app is starting.
+ messages.pgettext('launch-view', 'Starting up....')
+ }
+ </BodySmall>
+ <Spinner size="medium" />
+ </FlexColumn>
+ );
+ } else {
+ statusMessage = (
+ <BodySmall color="whiteAlpha40" textAlign="center" role="alert">
+ {
+ // TRANSLATORS: Status text shown when app fails to start.
+ messages.pgettext(
+ 'launch-view',
+ 'Failed to start the app, please try again or click “Details” for more info',
+ )
+ }
+ </BodySmall>
+ );
+ }
+ }
+
+ return <Flex $justifyContent="center">{statusMessage}</Flex>;
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/status-text/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/status-text/index.ts
new file mode 100644
index 0000000000..63c9750133
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/status-text/index.ts
@@ -0,0 +1 @@
+export * from './StatusText';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/troubleshooting-modal/TroubleshootingModal.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/troubleshooting-modal/TroubleshootingModal.tsx
new file mode 100644
index 0000000000..80aa4e1791
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/troubleshooting-modal/TroubleshootingModal.tsx
@@ -0,0 +1,70 @@
+import { useCallback } from 'react';
+import styled from 'styled-components';
+
+import { messages } from '../../../../../../shared/gettext';
+import { RoutePath } from '../../../../../../shared/routes';
+import { Button } from '../../../../../lib/components';
+import { TransitionType, useHistory } from '../../../../../lib/history';
+import { ModalAlert, ModalAlertType, ModalMessage, ModalMessageList } from '../../../../Modal';
+import { useTroubleshootingSteps } from './hooks';
+
+export type TroubleshootingModalProps = {
+ isOpen: boolean;
+ onClose: () => void;
+};
+
+const StyledModalMessage = styled(ModalMessage)`
+ margin-top: 0;
+`;
+
+export function TroubleshootingModal({ isOpen, onClose }: TroubleshootingModalProps) {
+ const { push } = useHistory();
+ const openSendProblemReport = useCallback(() => {
+ onClose();
+ push(RoutePath.problemReport, { transition: TransitionType.show });
+ }, [onClose, push]);
+
+ const steps = useTroubleshootingSteps();
+
+ return (
+ <ModalAlert
+ isOpen={isOpen}
+ type={ModalAlertType.info}
+ close={onClose}
+ buttons={[
+ <Button variant="success" key="problem-report" onClick={openSendProblemReport}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for sending a problem report.
+ messages.pgettext('launch-view', 'Send problem report')
+ }
+ </Button.Text>
+ </Button>,
+ <Button key="back" onClick={onClose}>
+ <Button.Text>{messages.gettext('Back')}</Button.Text>
+ </Button>,
+ ]}>
+ <ModalMessage>
+ {
+ // TRANSLATORS: Message in troubleshooting modal when the background process failed to start.
+ messages.pgettext(
+ 'launch-view',
+ 'The Mullvad background process failed to start. The background process is responsible for the security, kill switch, and the VPN tunnel. Please try:',
+ )
+ }
+ </ModalMessage>
+ <StyledModalMessage>
+ <ModalMessageList>{steps}</ModalMessageList>
+ </StyledModalMessage>
+ <ModalMessage>
+ {
+ // TRANSLATORS: Message in troubleshooting modal advising user to send a problem report if the steps do not work.
+ messages.pgettext(
+ 'launch-view',
+ 'If these steps do not work please send a problem report.',
+ )
+ }
+ </ModalMessage>
+ </ModalAlert>
+ );
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/troubleshooting-modal/hooks/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/troubleshooting-modal/hooks/index.ts
new file mode 100644
index 0000000000..44fa712e78
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/troubleshooting-modal/hooks/index.ts
@@ -0,0 +1 @@
+export * from './useTroubleshootingSteps';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/troubleshooting-modal/hooks/useTroubleshootingSteps.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/troubleshooting-modal/hooks/useTroubleshootingSteps.tsx
new file mode 100644
index 0000000000..afca701bc3
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/troubleshooting-modal/hooks/useTroubleshootingSteps.tsx
@@ -0,0 +1,50 @@
+import { messages } from '../../../../../../../shared/gettext';
+
+export const useTroubleshootingSteps = () => {
+ let restartBackgroundProcessStep = <></>;
+ if (window.env.platform === 'win32') {
+ restartBackgroundProcessStep = (
+ <li>
+ {
+ // TRANSLATORS: List item in troubleshooting modal instructing user how to restart the background process.
+ messages.pgettext(
+ 'launch-view',
+ 'Restarting the Mullvad background process by clicking "Back", then "Try again"',
+ )
+ }
+ </li>
+ );
+ } else {
+ restartBackgroundProcessStep = (
+ <li>
+ {
+ // TRANSLATORS: List item in troubleshooting modal advising user to restart background process.
+ messages.pgettext('launch-view', 'Restarting the Mullvad background process')
+ }
+ </li>
+ );
+ }
+ return (
+ <>
+ {restartBackgroundProcessStep}
+ <li>
+ {
+ // TRANSLATORS: List item in troubleshooting modal advising user to restart their computer.
+ messages.pgettext('launch-view', 'Restarting your computer')
+ }
+ </li>
+ <li>
+ {
+ // TRANSLATORS: List item in troubleshooting modal advising user to reinstall the app.
+ messages.pgettext('launch-view', 'Reinstalling the app')
+ }
+ </li>
+ <li>
+ {
+ // TRANSLATORS: List item in troubleshooting modal advising user disable third party antivirus.
+ messages.pgettext('launch-view', 'Disabling third party antivirus software')
+ }
+ </li>
+ </>
+ );
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/troubleshooting-modal/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/troubleshooting-modal/index.ts
new file mode 100644
index 0000000000..e7f1b5b679
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/components/troubleshooting-modal/index.ts
@@ -0,0 +1 @@
+export * from './TroubleshootingModal';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/index.ts
new file mode 100644
index 0000000000..dfe554378f
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/launch/index.ts
@@ -0,0 +1 @@
+export * from './LaunchView';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/typography/Text.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/typography/Text.tsx
index dff1a9dc08..aa4b0f1fca 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/typography/Text.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/typography/Text.tsx
@@ -7,18 +7,20 @@ import { PolymorphicProps, TransientProps } from '../../types';
type TextBaseProps = {
variant?: Typography;
color?: Colors;
+ textAlign?: React.CSSProperties['textAlign'];
};
export type TextProps<T extends React.ElementType = 'span'> = PolymorphicProps<T, TextBaseProps>;
const StyledText = styled.span<TransientProps<TextBaseProps>>(
- ({ $variant = 'bodySmall', $color = 'white' }) => {
+ ({ $variant = 'bodySmall', $color = 'white', $textAlign }) => {
const { fontFamily, fontSize, fontWeight, lineHeight } = typography[$variant];
const color = colors[$color];
return `
--color: ${color};
color: var(--color);
+ text-align: ${$textAlign || undefined};
font-family: ${fontFamily};
font-size: ${fontSize};
font-weight: ${fontWeight};
@@ -30,9 +32,10 @@ const StyledText = styled.span<TransientProps<TextBaseProps>>(
export const Text = <T extends React.ElementType = 'span'>({
variant,
color,
+ textAlign,
...props
}: TextProps<T>) => {
- return <StyledText $variant={variant} $color={color} {...props} />;
+ return <StyledText $variant={variant} $color={color} $textAlign={textAlign} {...props} />;
};
Text.displayName = 'Text';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/view/View.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/view/View.tsx
new file mode 100644
index 0000000000..37c1592ead
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/view/View.tsx
@@ -0,0 +1,27 @@
+import styled from 'styled-components';
+
+import { Colors, colors, ColorVariables } from '../../foundations';
+import { Flex, FlexProps } from '../flex';
+import { Container } from './components';
+
+export type ViewProps = FlexProps & {
+ backgroundColor?: Colors;
+};
+
+export const StyledView = styled(Flex)<{ $backgroundColor?: ColorVariables }>`
+ height: 100vh;
+ max-width: 100%;
+ background-color: ${({ $backgroundColor }) => $backgroundColor || undefined};
+`;
+
+function View({ backgroundColor = 'blue', ...props }: ViewProps) {
+ return (
+ <StyledView $backgroundColor={colors[backgroundColor]} $flexDirection="column" {...props} />
+ );
+}
+
+const ViewNamespace = Object.assign(View, {
+ Container: Container,
+});
+
+export { ViewNamespace as View };
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/view/components/Container.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/view/components/Container.tsx
new file mode 100644
index 0000000000..69d2f5291b
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/view/components/Container.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import styled from 'styled-components';
+
+import { spacings } from '../../../foundations';
+import { Flex, FlexProps } from '../../flex';
+
+export interface ContainerProps extends FlexProps {
+ size?: '3' | '4';
+ children: React.ReactNode;
+}
+
+const sizes: Record<'3' | '4', string> = {
+ '3': `calc(100% - ${spacings.large} * 2)`,
+ '4': `calc(100% - ${spacings.medium} * 2)`,
+};
+
+const StyledFlex = styled(Flex)<{ $size: string }>((props) => ({
+ width: props.$size,
+ margin: 'auto',
+}));
+
+export function Container({ size = '4', ...props }: ContainerProps) {
+ return <StyledFlex $size={sizes[size]} $flexDirection="column" {...props} />;
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/view/components/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/view/components/index.ts
new file mode 100644
index 0000000000..8a1103ffa4
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/view/components/index.ts
@@ -0,0 +1 @@
+export * from './Container';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/view/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/view/index.ts
new file mode 100644
index 0000000000..450cf3c6ae
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/view/index.ts
@@ -0,0 +1 @@
+export * from './View';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/redux/userinterface/actions.ts b/desktop/packages/mullvad-vpn/src/renderer/redux/userinterface/actions.ts
index 83c0252452..00764e1375 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/redux/userinterface/actions.ts
+++ b/desktop/packages/mullvad-vpn/src/renderer/redux/userinterface/actions.ts
@@ -1,5 +1,5 @@
import { MacOsScrollbarVisibility } from '../../../shared/ipc-schema';
-import { IChangelog } from '../../../shared/ipc-types';
+import { DaemonStatus, IChangelog } from '../../../shared/ipc-types';
import { LocationType } from '../../components/select-location/select-location-types';
export interface IUpdateLocaleAction {
@@ -31,6 +31,11 @@ export interface ISetConnectedToDaemon {
connectedToDaemon: boolean;
}
+export interface ISetDaemonStatus {
+ type: 'SET_DAEMON_STATUS';
+ daemonStatus: DaemonStatus;
+}
+
export interface ISetDaemonAllowed {
type: 'SET_DAEMON_ALLOWED';
daemonAllowed: boolean;
@@ -63,6 +68,7 @@ export type UserInterfaceAction =
| ISetWindowFocusedAction
| ISetMacOsScrollbarVisibility
| ISetConnectedToDaemon
+ | ISetDaemonStatus
| ISetDaemonAllowed
| ISetChangelog
| ISetIsPerformingPostUpgrade
@@ -112,6 +118,13 @@ function setConnectedToDaemon(connectedToDaemon: boolean): ISetConnectedToDaemon
};
}
+function setDaemonStatus(daemonStatus: DaemonStatus): ISetDaemonStatus {
+ return {
+ type: 'SET_DAEMON_STATUS',
+ daemonStatus: daemonStatus,
+ };
+}
+
function setDaemonAllowed(daemonAllowed: boolean): ISetDaemonAllowed {
return {
type: 'SET_DAEMON_ALLOWED',
@@ -154,6 +167,7 @@ export default {
setWindowFocused,
setMacOsScrollbarVisibility,
setConnectedToDaemon,
+ setDaemonStatus,
setDaemonAllowed,
setChangelog,
setIsPerformingPostUpgrade,
diff --git a/desktop/packages/mullvad-vpn/src/renderer/redux/userinterface/hooks/index.ts b/desktop/packages/mullvad-vpn/src/renderer/redux/userinterface/hooks/index.ts
index 0b7ee7ff2a..528eae8cfe 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/redux/userinterface/hooks/index.ts
+++ b/desktop/packages/mullvad-vpn/src/renderer/redux/userinterface/hooks/index.ts
@@ -1,3 +1,4 @@
export * from './useUserInterfaceChangelog';
export * from './useUserInterfaceConnectedToDaemon';
+export * from './useUserInterfaceDaemonStatus';
export * from './useUserInterfaceIsMacOs13OrNewer';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/redux/userinterface/hooks/useUserInterfaceDaemonStatus.ts b/desktop/packages/mullvad-vpn/src/renderer/redux/userinterface/hooks/useUserInterfaceDaemonStatus.ts
new file mode 100644
index 0000000000..1a9424d771
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/redux/userinterface/hooks/useUserInterfaceDaemonStatus.ts
@@ -0,0 +1,7 @@
+import { useSelector } from '../../store';
+
+export const useUserInterfaceDaemonStatus = () => {
+ return {
+ daemonStatus: useSelector((state) => state.userInterface.daemonStatus),
+ };
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/redux/userinterface/reducers.ts b/desktop/packages/mullvad-vpn/src/renderer/redux/userinterface/reducers.ts
index 46bdfc6308..e1bbe6e0dd 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/redux/userinterface/reducers.ts
+++ b/desktop/packages/mullvad-vpn/src/renderer/redux/userinterface/reducers.ts
@@ -1,5 +1,5 @@
import { MacOsScrollbarVisibility } from '../../../shared/ipc-schema';
-import { IChangelog } from '../../../shared/ipc-types';
+import { DaemonStatus, IChangelog } from '../../../shared/ipc-types';
import { LocationType } from '../../components/select-location/select-location-types';
import { ReduxAction } from '../store';
@@ -10,6 +10,7 @@ export interface IUserInterfaceReduxState {
windowFocused: boolean;
macOsScrollbarVisibility?: MacOsScrollbarVisibility;
connectedToDaemon: boolean;
+ daemonStatus?: DaemonStatus;
daemonAllowed?: boolean;
changelog: IChangelog;
isPerformingPostUpgrade: boolean;
@@ -53,6 +54,12 @@ export default function (
case 'SET_CONNECTED_TO_DAEMON':
return { ...state, connectedToDaemon: action.connectedToDaemon };
+ case 'SET_DAEMON_STATUS':
+ return {
+ ...state,
+ daemonStatus: action.daemonStatus,
+ };
+
case 'SET_DAEMON_ALLOWED':
return { ...state, daemonAllowed: action.daemonAllowed };
diff --git a/desktop/packages/mullvad-vpn/src/shared/ipc-schema.ts b/desktop/packages/mullvad-vpn/src/shared/ipc-schema.ts
index 031d1fc593..4f2c9da0fd 100644
--- a/desktop/packages/mullvad-vpn/src/shared/ipc-schema.ts
+++ b/desktop/packages/mullvad-vpn/src/shared/ipc-schema.ts
@@ -37,6 +37,7 @@ import { MapData } from '../renderer/lib/3dmap';
import { AppUpgradeError, AppUpgradeEvent } from './app-upgrade';
import { invoke, invokeSync, notifyRenderer, send } from './ipc-helpers';
import {
+ DaemonStatus,
IChangelog,
ICurrentAppVersionInfo,
IHistoryObject,
@@ -143,6 +144,8 @@ export const ipcSchema = {
connected: notifyRenderer<void>(),
disconnected: notifyRenderer<void>(),
prepareRestart: send<boolean>(),
+ tryStart: send<void>(),
+ tryStartEvent: notifyRenderer<DaemonStatus>(),
},
relays: {
'': notifyRenderer<IRelayListWithEndpointData>(),
diff --git a/desktop/packages/mullvad-vpn/src/shared/ipc-types.ts b/desktop/packages/mullvad-vpn/src/shared/ipc-types.ts
index 40a32e42fa..c3fe8ef2b0 100644
--- a/desktop/packages/mullvad-vpn/src/shared/ipc-types.ts
+++ b/desktop/packages/mullvad-vpn/src/shared/ipc-types.ts
@@ -35,3 +35,5 @@ export interface IHistoryObject {
}
export type ScrollPositions = Record<string, [number, number]>;
+
+export type DaemonStatus = 'start-requested' | 'running' | 'stopped';
diff --git a/desktop/packages/mullvad-vpn/test/e2e/mocked/launch/ipc.ts b/desktop/packages/mullvad-vpn/test/e2e/mocked/launch/ipc.ts
new file mode 100644
index 0000000000..6e790c444f
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/test/e2e/mocked/launch/ipc.ts
@@ -0,0 +1,17 @@
+import { MockedTestUtils } from '../mocked-utils';
+
+export const createIpc = (util: MockedTestUtils) => {
+ const createMockResponse = <T>(channel: string, response: T) =>
+ util.sendMockIpcResponse<T>({
+ channel,
+ response,
+ });
+
+ return {
+ handle: {},
+ send: {
+ daemonDisconnected: () => createMockResponse('daemon-disconnected', {}),
+ daemonAllowed: (allowed: boolean) => createMockResponse('daemon-daemonAllowed', allowed),
+ },
+ };
+};
diff --git a/desktop/packages/mullvad-vpn/test/e2e/mocked/launch/launch.spec.ts b/desktop/packages/mullvad-vpn/test/e2e/mocked/launch/launch.spec.ts
new file mode 100644
index 0000000000..30e793d6b8
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/test/e2e/mocked/launch/launch.spec.ts
@@ -0,0 +1,62 @@
+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';
+import { createIpc } from './ipc';
+
+let page: Page;
+let util: MockedTestUtils;
+let routes: RoutesObjectModel;
+let ipc: ReturnType<typeof createIpc>;
+
+test.describe('Launch', () => {
+ test.beforeAll(async () => {
+ ({ page, util } = await startMockedApp());
+ ipc = createIpc(util);
+ routes = new RoutesObjectModel(page, util);
+ await util.waitForRoute(RoutePath.main);
+
+ await ipc.send.daemonDisconnected();
+ await routes.launch.waitForRoute();
+ });
+
+ test.afterAll(async () => {
+ await page.close();
+ });
+
+ test.describe('Linux', () => {
+ test.skip(() => process.platform !== 'linux');
+ test('Should display default footer', async () => {
+ const learnMoreButton = routes.launch.selectors.learnMoreButton();
+ await expect(learnMoreButton).toBeVisible();
+ const defaultFooterText = routes.launch.selectors.defaultFooterText();
+ await expect(defaultFooterText).toBeVisible();
+ });
+ });
+
+ test.describe('Windows', () => {
+ test.skip(() => process.platform !== 'win32');
+ test('Should display restart daemon footer', async () => {
+ const learnMoreButton = routes.launch.selectors.detailsButton();
+ await expect(learnMoreButton).toBeVisible();
+ const tryAgainButton = routes.launch.selectors.tryAgainButton();
+ await expect(tryAgainButton).toBeVisible();
+ });
+ });
+ test.describe('MacOS', () => {
+ test.skip(() => process.platform !== 'darwin');
+ test('Should display default footer', async () => {
+ const learnMoreButton = routes.launch.selectors.learnMoreButton();
+ await expect(learnMoreButton).toBeVisible();
+ const defaultFooterText = routes.launch.selectors.defaultFooterText();
+ await expect(defaultFooterText).toBeVisible();
+ });
+ test('Should display permission footer when daemon is not allowed', async () => {
+ await ipc.send.daemonAllowed(false);
+ const gotoSystemSettingsButton = routes.launch.selectors.gotoSystemSettingsButton();
+ await expect(gotoSystemSettingsButton).toBeVisible();
+ });
+ });
+});
diff --git a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/launch/index.ts b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/launch/index.ts
new file mode 100644
index 0000000000..2bf6c8100b
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/launch/index.ts
@@ -0,0 +1,2 @@
+export * from './launch-route-object-model';
+export * from './selectors';
diff --git a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/launch/launch-route-object-model.ts b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/launch/launch-route-object-model.ts
new file mode 100644
index 0000000000..105bb6c337
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/launch/launch-route-object-model.ts
@@ -0,0 +1,21 @@
+import { Page } from 'playwright';
+
+import { RoutePath } from '../../../../src/shared/routes';
+import { TestUtils } from '../../utils';
+import { createSelectors } from './selectors';
+
+export class LaunchRouteObjectModel {
+ readonly page: Page;
+ readonly utils: TestUtils;
+ readonly selectors: ReturnType<typeof createSelectors>;
+
+ constructor(page: Page, util: TestUtils) {
+ this.page = page;
+ this.utils = util;
+ this.selectors = createSelectors(page);
+ }
+
+ async waitForRoute() {
+ await this.utils.waitForRoute(RoutePath.launch);
+ }
+}
diff --git a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/launch/selectors.ts b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/launch/selectors.ts
new file mode 100644
index 0000000000..ef7d1d10bf
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/launch/selectors.ts
@@ -0,0 +1,9 @@
+import { Page } from 'playwright';
+
+export const createSelectors = (page: Page) => ({
+ defaultFooterText: () => page.getByText('Unable to contact the Mullvad system service'),
+ learnMoreButton: () => page.getByRole('button', { name: 'Learn more' }),
+ tryAgainButton: () => page.getByRole('button', { name: 'Try again' }),
+ detailsButton: () => page.getByRole('button', { name: 'Details' }),
+ gotoSystemSettingsButton: () => page.getByRole('button', { name: 'Go to system settings' }),
+});
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 eb84afec5e..dbb0e39803 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
@@ -2,6 +2,7 @@ import { Page } from 'playwright';
import { TestUtils } from '../utils';
import { FilterRouteObjectModel } from './filter';
+import { LaunchRouteObjectModel } from './launch';
import { MainRouteObjectModel } from './main';
import { SelectLanguageRouteObjectModel } from './select-language';
import { SelectLocationRouteObjectModel } from './select-location';
@@ -11,6 +12,7 @@ import { VpnSettingsRouteObjectModel } from './vpn-settings';
export class RoutesObjectModel {
readonly main: MainRouteObjectModel;
+ readonly launch: LaunchRouteObjectModel;
readonly settings: SettingsRouteObjectModel;
readonly userInterfaceSettings: UserInterfaceSettingsRouteObjectModel;
readonly selectLanguage: SelectLanguageRouteObjectModel;
@@ -21,6 +23,7 @@ export class RoutesObjectModel {
constructor(page: Page, utils: TestUtils) {
this.selectLanguage = new SelectLanguageRouteObjectModel(page, utils);
this.main = new MainRouteObjectModel(page, utils);
+ this.launch = new LaunchRouteObjectModel(page, utils);
this.settings = new SettingsRouteObjectModel(page, utils);
this.userInterfaceSettings = new UserInterfaceSettingsRouteObjectModel(page, utils);
this.filter = new FilterRouteObjectModel(page, utils);