summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJoakim Hulthe <joakim.hulthe@mullvad.net>2024-12-18 15:31:57 +0100
committerJoakim Hulthe <joakim.hulthe@mullvad.net>2024-12-18 15:31:57 +0100
commit2c472944dc3521cbd694e77766e54d04faebe880 (patch)
treed7a92cd897506f1f78866637fba887c6cc4086e1
parent7cb8b8c16cbcb2938823294c1d6f78bab4040c9e (diff)
parent64828f69ed57ea9b02aba037997efb4d41d3a2b2 (diff)
downloadmullvadvpn-2c472944dc3521cbd694e77766e54d04faebe880.tar.xz
mullvadvpn-2c472944dc3521cbd694e77766e54d04faebe880.zip
Merge branch 'add-spinner-while-fda-check-is-being-performed-des-1554'
-rw-r--r--desktop/packages/mullvad-vpn/locales/messages.pot4
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx110
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx74
-rw-r--r--desktop/packages/mullvad-vpn/src/shared/notifications/error.ts10
-rw-r--r--desktop/packages/mullvad-vpn/src/shared/notifications/notification.ts1
5 files changed, 133 insertions, 66 deletions
diff --git a/desktop/packages/mullvad-vpn/locales/messages.pot b/desktop/packages/mullvad-vpn/locales/messages.pot
index be9d787aa5..1b5f981cc5 100644
--- a/desktop/packages/mullvad-vpn/locales/messages.pot
+++ b/desktop/packages/mullvad-vpn/locales/messages.pot
@@ -1781,6 +1781,10 @@ msgid "Connecting. %(location)s"
msgstr ""
msgctxt "troubleshoot"
+msgid "Disable split tunneling"
+msgstr ""
+
+msgctxt "troubleshoot"
msgid "Enable “Full Disk Access” for “Mullvad VPN” in the macOS system settings."
msgstr ""
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx
index ab9965250a..df3c78d4b0 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx
@@ -9,7 +9,6 @@ import {
ErrorNotificationProvider,
InAppNotificationAction,
InAppNotificationProvider,
- InAppNotificationTroubleshootInfo,
InconsistentVersionNotificationProvider,
ReconnectingNotificationProvider,
UnsupportedVersionNotificationProvider,
@@ -47,7 +46,7 @@ interface IProps {
}
export default function NotificationArea(props: IProps) {
- const { showFullDiskAccessSettings } = useAppContext();
+ const { showFullDiskAccessSettings, reconnectTunnel } = useAppContext();
const account = useSelector((state: IReduxState) => state.account);
const locale = useSelector((state: IReduxState) => state.userInterface.locale);
@@ -75,6 +74,15 @@ export default function NotificationArea(props: IProps) {
setDisplayedChangelog();
}, [setDisplayedChangelog]);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ const { setSplitTunnelingState } = useAppContext();
+ const disableSplitTunneling = useCallback(async () => {
+ setIsModalOpen(false);
+ await setSplitTunnelingState(false);
+ await reconnectTunnel();
+ }, [reconnectTunnel, setSplitTunnelingState]);
+
const notificationProviders: InAppNotificationProvider[] = [
new ConnectingNotificationProvider({ tunnelState }),
new ReconnectingNotificationProvider(tunnelState),
@@ -83,7 +91,13 @@ export default function NotificationArea(props: IProps) {
blockWhenDisconnected,
hasExcludedApps,
}),
- new ErrorNotificationProvider({ tunnelState, hasExcludedApps, showFullDiskAccessSettings }),
+
+ new ErrorNotificationProvider({
+ tunnelState,
+ hasExcludedApps,
+ showFullDiskAccessSettings,
+ disableSplitTunneling,
+ }),
new InconsistentVersionNotificationProvider({ consistent: version.consistent }),
new UnsupportedVersionNotificationProvider(version),
];
@@ -140,7 +154,13 @@ export default function NotificationArea(props: IProps) {
)}
</NotificationSubtitle>
</NotificationContent>
- {notification.action && <NotificationActionWrapper action={notification.action} />}
+ {notification.action && (
+ <NotificationActionWrapper
+ action={notification.action}
+ isModalOpen={isModalOpen}
+ setIsModalOpen={setIsModalOpen}
+ />
+ )}
</NotificationBanner>
);
} else {
@@ -153,46 +173,51 @@ export default function NotificationArea(props: IProps) {
return <NotificationBanner className={props.className} aria-hidden={true} />;
}
-interface INotificationActionWrapperProps {
+interface NotificationActionWrapperProps {
action: InAppNotificationAction;
+ isModalOpen: boolean;
+ setIsModalOpen: (isOpen: boolean) => void;
}
-function NotificationActionWrapper(props: INotificationActionWrapperProps) {
+function NotificationActionWrapper({
+ action,
+ isModalOpen,
+ setIsModalOpen,
+}: NotificationActionWrapperProps) {
const { push } = useHistory();
const { openLinkWithAuth, openUrl } = useAppContext();
- const [troubleshootInfo, setTroubleshootInfo] = useState<InAppNotificationTroubleshootInfo>();
+
+ const closeTroubleshootModal = useCallback(() => setIsModalOpen(false), [setIsModalOpen]);
const handleClick = useCallback(() => {
- if (props.action) {
- switch (props.action.type) {
+ if (action) {
+ switch (action.type) {
case 'open-url':
- if (props.action.withAuth) {
- return openLinkWithAuth(props.action.url);
+ if (action.withAuth) {
+ return openLinkWithAuth(action.url);
} else {
- return openUrl(props.action.url);
+ return openUrl(action.url);
}
case 'troubleshoot-dialog':
- setTroubleshootInfo(props.action.troubleshoot);
+ setIsModalOpen(true);
break;
case 'close':
- props.action.close();
+ action.close();
break;
}
}
return Promise.resolve();
- }, [openLinkWithAuth, openUrl, props.action]);
+ }, [action, setIsModalOpen, openLinkWithAuth, openUrl]);
const goToProblemReport = useCallback(() => {
- setTroubleshootInfo(undefined);
+ closeTroubleshootModal();
push(RoutePath.problemReport, { transition: transitions.show });
- }, [push]);
-
- const closeTroubleshootInfo = useCallback(() => setTroubleshootInfo(undefined), []);
+ }, [closeTroubleshootModal, push]);
let actionComponent: React.ReactElement | undefined;
- if (props.action) {
- switch (props.action.type) {
+ if (action) {
+ switch (action.type) {
case 'open-url':
actionComponent = <NotificationOpenLinkAction onClick={handleClick} />;
break;
@@ -208,7 +233,11 @@ function NotificationActionWrapper(props: INotificationActionWrapperProps) {
}
}
- const problemReportButton = troubleshootInfo?.buttons ? (
+ if (action.type !== 'troubleshoot-dialog') {
+ return <NotificationActions>{actionComponent}</NotificationActions>;
+ }
+
+ const problemReportButton = action.troubleshoot?.buttons ? (
<AppButton.BlueButton key="problem-report" onClick={goToProblemReport}>
{messages.pgettext('in-app-notifications', 'Send problem report')}
</AppButton.BlueButton>
@@ -220,17 +249,32 @@ function NotificationActionWrapper(props: INotificationActionWrapperProps) {
let buttons = [
problemReportButton,
- <AppButton.BlueButton key="back" onClick={closeTroubleshootInfo}>
+ <AppButton.BlueButton key="back" onClick={closeTroubleshootModal}>
{messages.gettext('Back')}
</AppButton.BlueButton>,
];
- if (troubleshootInfo?.buttons) {
- const actionButtons = troubleshootInfo.buttons.map((button) => (
- <AppButton.GreenButton key={button.label} onClick={button.action}>
- {button.label}
- </AppButton.GreenButton>
- ));
+ if (action.troubleshoot?.buttons) {
+ const actionButtons = action.troubleshoot.buttons.map(({ variant, label, action }) => {
+ if (variant === 'success')
+ return (
+ <AppButton.GreenButton key={label} onClick={action}>
+ {label}
+ </AppButton.GreenButton>
+ );
+ else if (variant === 'destructive')
+ return (
+ <AppButton.RedButton key={label} onClick={action}>
+ {label}
+ </AppButton.RedButton>
+ );
+ else
+ return (
+ <AppButton.BlueButton key={label} onClick={action}>
+ {label}
+ </AppButton.BlueButton>
+ );
+ });
buttons = actionButtons.concat(buttons);
}
@@ -239,14 +283,14 @@ function NotificationActionWrapper(props: INotificationActionWrapperProps) {
<>
<NotificationActions>{actionComponent}</NotificationActions>
<ModalAlert
- isOpen={troubleshootInfo !== undefined}
+ isOpen={isModalOpen}
type={ModalAlertType.info}
buttons={buttons}
- close={closeTroubleshootInfo}>
- <ModalMessage>{troubleshootInfo?.details}</ModalMessage>
+ close={closeTroubleshootModal}>
+ <ModalMessage>{action.troubleshoot?.details}</ModalMessage>
<ModalMessage>
<ModalMessageList>
- {troubleshootInfo?.steps.map((step) => <li key={step}>{step}</li>)}
+ {action.troubleshoot?.steps.map((step) => <li key={step}>{step}</li>)}
</ModalMessageList>
</ModalMessage>
<ModalMessage>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx
index 4070b7dba5..a24790e66a 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx
@@ -66,12 +66,10 @@ export default function SplitTunneling() {
</NavigationBar>
<StyledNavigationScrollbars ref={scrollbarsRef}>
- <Flex $flexDirection="column" $flex={1}>
- <PlatformSpecificSplitTunnelingSettings
- setBrowsing={setBrowsing}
- scrollToTop={scrollToTop}
- />
- </Flex>
+ <PlatformSpecificSplitTunnelingSettings
+ setBrowsing={setBrowsing}
+ scrollToTop={scrollToTop}
+ />
</StyledNavigationScrollbars>
</NavigationContainer>
</SettingsContainer>
@@ -315,9 +313,7 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
needFullDiskPermissions,
setSplitTunnelingState,
} = useAppContext();
- const splitTunnelingEnabledValue = useSelector(
- (state: IReduxState) => state.settings.splitTunneling,
- );
+ const splitTunnelingEnabled = useSelector((state: IReduxState) => state.settings.splitTunneling);
const splitTunnelingApplications = useSelector(
(state: IReduxState) => state.settings.splitTunnelingApplications,
);
@@ -325,15 +321,18 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
const [searchTerm, setSearchTerm] = useState('');
const [applications, setApplications] = useState<ISplitTunnelingApplication[]>();
+ const [loadingDiskPermissions, setLoadingDiskPermissions] = useState(false);
const [splitTunnelingAvailable, setSplitTunnelingAvailable] = useState(
window.env.platform === 'darwin' ? undefined : true,
);
- const splitTunnelingEnabled = splitTunnelingEnabledValue && (splitTunnelingAvailable ?? false);
+ const canEditSplitTunneling = splitTunnelingEnabled && (splitTunnelingAvailable ?? false);
const fetchNeedFullDiskPermissions = useCallback(async () => {
+ setLoadingDiskPermissions(true);
const needPermissions = await needFullDiskPermissions();
setSplitTunnelingAvailable(!needPermissions);
+ setLoadingDiskPermissions(false);
}, [needFullDiskPermissions]);
useEffect((): void | (() => void) => {
@@ -375,12 +374,12 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
const addApplication = useCallback(
async (application: ISplitTunnelingApplication | string) => {
- if (!splitTunnelingEnabled) {
+ if (!canEditSplitTunneling) {
await setSplitTunnelingState(true);
}
await addSplitTunnelingApplication(application);
},
- [addSplitTunnelingApplication, splitTunnelingEnabled, setSplitTunnelingState],
+ [addSplitTunnelingApplication, canEditSplitTunneling, setSplitTunnelingState],
);
const addBrowsedForApplication = useCallback(
@@ -403,12 +402,12 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
const removeApplication = useCallback(
async (application: ISplitTunnelingApplication) => {
- if (!splitTunnelingEnabled) {
+ if (!canEditSplitTunneling) {
await setSplitTunnelingState(true);
}
removeSplitTunnelingApplication(application);
},
- [removeSplitTunnelingApplication, setSplitTunnelingState, splitTunnelingEnabled],
+ [removeSplitTunnelingApplication, setSplitTunnelingState, canEditSplitTunneling],
);
const filePickerCallback = useFilePicker(
@@ -440,9 +439,9 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
[addApplication, forgetManuallyAddedApplicationAndUpdate],
);
- const showSplitSection = splitTunnelingEnabled && filteredSplitApplications.length > 0;
+ const showSplitSection = canEditSplitTunneling && filteredSplitApplications.length > 0;
const showNonSplitSection =
- splitTunnelingEnabled &&
+ canEditSplitTunneling &&
(!filteredNonSplitApplications || filteredNonSplitApplications.length > 0);
const excludedTitle = (
@@ -462,26 +461,37 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
<HeaderTitle>{strings.splitTunneling}</HeaderTitle>
<Switch
isOn={splitTunnelingEnabled}
- disabled={!splitTunnelingAvailable}
+ disabled={
+ !splitTunnelingEnabled && (!splitTunnelingAvailable || loadingDiskPermissions)
+ }
onChange={setSplitTunnelingState}
/>
</Flex>
- <MacOsSplitTunnelingAvailability
- needFullDiskPermissions={
- window.env.platform === 'darwin' && splitTunnelingAvailable === false
- }
- />
- {splitTunnelingAvailable ? (
- <HeaderSubTitle>
- {messages.pgettext(
- 'split-tunneling-view',
- 'Choose the apps you want to exclude from the VPN tunnel.',
+ {!loadingDiskPermissions && (
+ <>
+ <MacOsSplitTunnelingAvailability
+ needFullDiskPermissions={
+ window.env.platform === 'darwin' && splitTunnelingAvailable === false
+ }
+ />
+ {splitTunnelingAvailable && (
+ <HeaderSubTitle>
+ {messages.pgettext(
+ 'split-tunneling-view',
+ 'Choose the apps you want to exclude from the VPN tunnel.',
+ )}
+ </HeaderSubTitle>
)}
- </HeaderSubTitle>
- ) : null}
+ </>
+ )}
</SettingsHeader>
+ {loadingDiskPermissions && (
+ <Flex $justifyContent="center" $margin={{ top: Spacings.spacing6 }}>
+ <ImageView source="icon-spinner" height={48} />
+ </Flex>
+ )}
- {splitTunnelingEnabled && (
+ {canEditSplitTunneling && (
<StyledSearchBar searchTerm={searchTerm} onSearch={setSearchTerm} />
)}
@@ -505,7 +515,7 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
</Cell.Section>
</Accordion>
- {splitTunnelingEnabled && searchTerm !== '' && !showSplitSection && !showNonSplitSection && (
+ {canEditSplitTunneling && searchTerm !== '' && !showSplitSection && !showNonSplitSection && (
<StyledNoResult>
<StyledNoResultText>
{formatHtml(
@@ -516,7 +526,7 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
</StyledNoResult>
)}
- {splitTunnelingEnabled && (
+ {canEditSplitTunneling && (
<StyledBrowseButton onClick={addWithFilePicker}>
{messages.pgettext('split-tunneling-view', 'Find another app')}
</StyledBrowseButton>
diff --git a/desktop/packages/mullvad-vpn/src/shared/notifications/error.ts b/desktop/packages/mullvad-vpn/src/shared/notifications/error.ts
index af82748d7b..42e550c466 100644
--- a/desktop/packages/mullvad-vpn/src/shared/notifications/error.ts
+++ b/desktop/packages/mullvad-vpn/src/shared/notifications/error.ts
@@ -13,6 +13,7 @@ import {
InAppNotification,
InAppNotificationAction,
InAppNotificationProvider,
+ InAppNotificationTroubleshootButton,
SystemNotification,
SystemNotificationCategory,
SystemNotificationProvider,
@@ -23,6 +24,7 @@ interface ErrorNotificationContext {
tunnelState: TunnelState;
hasExcludedApps: boolean;
showFullDiskAccessSettings?: () => void;
+ disableSplitTunneling?: () => void;
}
export class ErrorNotificationProvider
@@ -276,12 +278,18 @@ export class ErrorNotificationProvider
},
};
} else if (errorState.cause === ErrorStateCause.needFullDiskPermissions) {
- let troubleshootButtons = undefined;
+ let troubleshootButtons: InAppNotificationTroubleshootButton[] | undefined = undefined;
if (this.context.showFullDiskAccessSettings) {
troubleshootButtons = [
{
label: messages.pgettext('troubleshoot', 'Open system settings'),
action: () => this.context.showFullDiskAccessSettings?.(),
+ variant: 'success',
+ },
+ {
+ label: messages.pgettext('troubleshoot', 'Disable split tunneling'),
+ action: () => this.context.disableSplitTunneling?.(),
+ variant: 'destructive',
},
];
}
diff --git a/desktop/packages/mullvad-vpn/src/shared/notifications/notification.ts b/desktop/packages/mullvad-vpn/src/shared/notifications/notification.ts
index 921a029a06..48561c555b 100644
--- a/desktop/packages/mullvad-vpn/src/shared/notifications/notification.ts
+++ b/desktop/packages/mullvad-vpn/src/shared/notifications/notification.ts
@@ -16,6 +16,7 @@ export interface InAppNotificationTroubleshootInfo {
export interface InAppNotificationTroubleshootButton {
label: string;
action: () => void;
+ variant?: 'primary' | 'success' | 'destructive';
}
export type InAppNotificationAction =