diff options
| author | Joakim Hulthe <joakim.hulthe@mullvad.net> | 2024-12-18 15:31:57 +0100 |
|---|---|---|
| committer | Oskar <oskar@mullvad.net> | 2024-12-19 09:46:21 +0100 |
| commit | d00e3b299bcea101e5a34cc760fb3c0d401562c5 (patch) | |
| tree | 5efa84076a627375a425d279cbee48cb8ed6a103 | |
| parent | 1139f5ca5a86a373fcf96c6d0088c264d4659d66 (diff) | |
| download | mullvadvpn-fda-fixes-backport.tar.xz mullvadvpn-fda-fixes-backport.zip | |
Merge branch 'add-spinner-while-fda-check-is-being-performed-des-1554'fda-fixes-backport
6 files changed, 136 insertions, 60 deletions
diff --git a/desktop/packages/mullvad-vpn/locales/messages.pot b/desktop/packages/mullvad-vpn/locales/messages.pot index 1633f99c56..7472de2eaa 100644 --- a/desktop/packages/mullvad-vpn/locales/messages.pot +++ b/desktop/packages/mullvad-vpn/locales/messages.pot @@ -1749,6 +1749,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 dac62db192..ed04e770a3 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx @@ -11,7 +11,6 @@ import { ErrorNotificationProvider, InAppNotificationAction, InAppNotificationProvider, - InAppNotificationTroubleshootInfo, InconsistentVersionNotificationProvider, ReconnectingNotificationProvider, UnsupportedVersionNotificationProvider, @@ -43,7 +42,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); @@ -59,6 +58,15 @@ export default function NotificationArea(props: IProps) { const { hideNewDeviceBanner } = useActions(accountActions); + 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), @@ -67,7 +75,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), ]; @@ -109,7 +123,13 @@ export default function NotificationArea(props: IProps) { {formatHtml(notification.subtitle ?? '')} </NotificationSubtitle> </NotificationContent> - {notification.action && <NotificationActionWrapper action={notification.action} />} + {notification.action && ( + <NotificationActionWrapper + action={notification.action} + isModalOpen={isModalOpen} + setIsModalOpen={setIsModalOpen} + /> + )} </NotificationBanner> ); } else { @@ -122,46 +142,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; @@ -177,7 +202,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> @@ -189,17 +218,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); } @@ -208,14 +252,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 072f62728c..d5044d012f 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx @@ -32,6 +32,7 @@ import { StyledCellLabel, StyledCellWarningIcon, StyledContent, + StyledFdaSpinner, StyledHeaderTitle, StyledHeaderTitleContainer, StyledIcon, @@ -318,9 +319,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, ); @@ -328,15 +327,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) => { @@ -378,12 +380,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( @@ -406,12 +408,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( @@ -443,9 +445,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 = ( @@ -465,26 +467,37 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro <StyledHeaderTitle>{strings.splitTunneling}</StyledHeaderTitle> <Switch isOn={splitTunnelingEnabled} - disabled={!splitTunnelingAvailable} + disabled={ + !splitTunnelingEnabled && (!splitTunnelingAvailable || loadingDiskPermissions) + } onChange={setSplitTunnelingState} /> </StyledHeaderTitleContainer> - <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 && ( + <StyledFdaSpinner> + <ImageView source="icon-spinner" height={48} /> + </StyledFdaSpinner> + )} - {splitTunnelingEnabled && ( + {canEditSplitTunneling && ( <StyledSearchBar searchTerm={searchTerm} onSearch={setSearchTerm} /> )} @@ -508,7 +521,7 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro </Cell.Section> </Accordion> - {splitTunnelingEnabled && searchTerm !== '' && !showSplitSection && !showNonSplitSection && ( + {canEditSplitTunneling && searchTerm !== '' && !showSplitSection && !showNonSplitSection && ( <StyledNoResult> <StyledNoResultText> {formatHtml( @@ -519,7 +532,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/renderer/components/SplitTunnelingSettingsStyles.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettingsStyles.tsx index a2046e61c9..28cb3fce21 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettingsStyles.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettingsStyles.tsx @@ -136,3 +136,9 @@ export const WideSmallButton = styled(SmallButton)({ export const Spacing = styled.div<{ height: string }>((props) => ({ height: props.height, })); + +export const StyledFdaSpinner = styled.div({ + display: 'flex', + justifyContent: 'center', + marginTop: '24px', +}); 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 87166aab4d..2b6af5b7d3 100644 --- a/desktop/packages/mullvad-vpn/src/shared/notifications/notification.ts +++ b/desktop/packages/mullvad-vpn/src/shared/notifications/notification.ts @@ -14,6 +14,7 @@ export interface InAppNotificationTroubleshootInfo { export interface InAppNotificationTroubleshootButton { label: string; action: () => void; + variant?: 'primary' | 'success' | 'destructive'; } export type InAppNotificationAction = |
