Skip to content

Commit d00e3b2

Browse files
hultheraksooo
authored andcommitted
Merge branch 'add-spinner-while-fda-check-is-being-performed-des-1554'
1 parent 1139f5c commit d00e3b2

File tree

6 files changed

+136
-60
lines changed

6 files changed

+136
-60
lines changed

desktop/packages/mullvad-vpn/locales/messages.pot

+4
Original file line numberDiff line numberDiff line change
@@ -1748,6 +1748,10 @@ msgctxt "tray-icon-tooltip"
17481748
msgid "Connecting. %(location)s"
17491749
msgstr ""
17501750

1751+
msgctxt "troubleshoot"
1752+
msgid "Disable split tunneling"
1753+
msgstr ""
1754+
17511755
msgctxt "troubleshoot"
17521756
msgid "Enable “Full Disk Access” for “Mullvad VPN” in the macOS system settings."
17531757
msgstr ""

desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx

+77-33
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
ErrorNotificationProvider,
1212
InAppNotificationAction,
1313
InAppNotificationProvider,
14-
InAppNotificationTroubleshootInfo,
1514
InconsistentVersionNotificationProvider,
1615
ReconnectingNotificationProvider,
1716
UnsupportedVersionNotificationProvider,
@@ -43,7 +42,7 @@ interface IProps {
4342
}
4443

4544
export default function NotificationArea(props: IProps) {
46-
const { showFullDiskAccessSettings } = useAppContext();
45+
const { showFullDiskAccessSettings, reconnectTunnel } = useAppContext();
4746

4847
const account = useSelector((state: IReduxState) => state.account);
4948
const locale = useSelector((state: IReduxState) => state.userInterface.locale);
@@ -59,6 +58,15 @@ export default function NotificationArea(props: IProps) {
5958

6059
const { hideNewDeviceBanner } = useActions(accountActions);
6160

61+
const [isModalOpen, setIsModalOpen] = useState(false);
62+
63+
const { setSplitTunnelingState } = useAppContext();
64+
const disableSplitTunneling = useCallback(async () => {
65+
setIsModalOpen(false);
66+
await setSplitTunnelingState(false);
67+
await reconnectTunnel();
68+
}, [reconnectTunnel, setSplitTunnelingState]);
69+
6270
const notificationProviders: InAppNotificationProvider[] = [
6371
new ConnectingNotificationProvider({ tunnelState }),
6472
new ReconnectingNotificationProvider(tunnelState),
@@ -67,7 +75,13 @@ export default function NotificationArea(props: IProps) {
6775
blockWhenDisconnected,
6876
hasExcludedApps,
6977
}),
70-
new ErrorNotificationProvider({ tunnelState, hasExcludedApps, showFullDiskAccessSettings }),
78+
79+
new ErrorNotificationProvider({
80+
tunnelState,
81+
hasExcludedApps,
82+
showFullDiskAccessSettings,
83+
disableSplitTunneling,
84+
}),
7185
new InconsistentVersionNotificationProvider({ consistent: version.consistent }),
7286
new UnsupportedVersionNotificationProvider(version),
7387
];
@@ -109,7 +123,13 @@ export default function NotificationArea(props: IProps) {
109123
{formatHtml(notification.subtitle ?? '')}
110124
</NotificationSubtitle>
111125
</NotificationContent>
112-
{notification.action && <NotificationActionWrapper action={notification.action} />}
126+
{notification.action && (
127+
<NotificationActionWrapper
128+
action={notification.action}
129+
isModalOpen={isModalOpen}
130+
setIsModalOpen={setIsModalOpen}
131+
/>
132+
)}
113133
</NotificationBanner>
114134
);
115135
} else {
@@ -122,46 +142,51 @@ export default function NotificationArea(props: IProps) {
122142
return <NotificationBanner className={props.className} aria-hidden={true} />;
123143
}
124144

125-
interface INotificationActionWrapperProps {
145+
interface NotificationActionWrapperProps {
126146
action: InAppNotificationAction;
147+
isModalOpen: boolean;
148+
setIsModalOpen: (isOpen: boolean) => void;
127149
}
128150

129-
function NotificationActionWrapper(props: INotificationActionWrapperProps) {
151+
function NotificationActionWrapper({
152+
action,
153+
isModalOpen,
154+
setIsModalOpen,
155+
}: NotificationActionWrapperProps) {
130156
const { push } = useHistory();
131157
const { openLinkWithAuth, openUrl } = useAppContext();
132-
const [troubleshootInfo, setTroubleshootInfo] = useState<InAppNotificationTroubleshootInfo>();
158+
159+
const closeTroubleshootModal = useCallback(() => setIsModalOpen(false), [setIsModalOpen]);
133160

134161
const handleClick = useCallback(() => {
135-
if (props.action) {
136-
switch (props.action.type) {
162+
if (action) {
163+
switch (action.type) {
137164
case 'open-url':
138-
if (props.action.withAuth) {
139-
return openLinkWithAuth(props.action.url);
165+
if (action.withAuth) {
166+
return openLinkWithAuth(action.url);
140167
} else {
141-
return openUrl(props.action.url);
168+
return openUrl(action.url);
142169
}
143170
case 'troubleshoot-dialog':
144-
setTroubleshootInfo(props.action.troubleshoot);
171+
setIsModalOpen(true);
145172
break;
146173
case 'close':
147-
props.action.close();
174+
action.close();
148175
break;
149176
}
150177
}
151178

152179
return Promise.resolve();
153-
}, [openLinkWithAuth, openUrl, props.action]);
180+
}, [action, setIsModalOpen, openLinkWithAuth, openUrl]);
154181

155182
const goToProblemReport = useCallback(() => {
156-
setTroubleshootInfo(undefined);
183+
closeTroubleshootModal();
157184
push(RoutePath.problemReport, { transition: transitions.show });
158-
}, [push]);
159-
160-
const closeTroubleshootInfo = useCallback(() => setTroubleshootInfo(undefined), []);
185+
}, [closeTroubleshootModal, push]);
161186

162187
let actionComponent: React.ReactElement | undefined;
163-
if (props.action) {
164-
switch (props.action.type) {
188+
if (action) {
189+
switch (action.type) {
165190
case 'open-url':
166191
actionComponent = <NotificationOpenLinkAction onClick={handleClick} />;
167192
break;
@@ -177,7 +202,11 @@ function NotificationActionWrapper(props: INotificationActionWrapperProps) {
177202
}
178203
}
179204

180-
const problemReportButton = troubleshootInfo?.buttons ? (
205+
if (action.type !== 'troubleshoot-dialog') {
206+
return <NotificationActions>{actionComponent}</NotificationActions>;
207+
}
208+
209+
const problemReportButton = action.troubleshoot?.buttons ? (
181210
<AppButton.BlueButton key="problem-report" onClick={goToProblemReport}>
182211
{messages.pgettext('in-app-notifications', 'Send problem report')}
183212
</AppButton.BlueButton>
@@ -189,17 +218,32 @@ function NotificationActionWrapper(props: INotificationActionWrapperProps) {
189218

190219
let buttons = [
191220
problemReportButton,
192-
<AppButton.BlueButton key="back" onClick={closeTroubleshootInfo}>
221+
<AppButton.BlueButton key="back" onClick={closeTroubleshootModal}>
193222
{messages.gettext('Back')}
194223
</AppButton.BlueButton>,
195224
];
196225

197-
if (troubleshootInfo?.buttons) {
198-
const actionButtons = troubleshootInfo.buttons.map((button) => (
199-
<AppButton.GreenButton key={button.label} onClick={button.action}>
200-
{button.label}
201-
</AppButton.GreenButton>
202-
));
226+
if (action.troubleshoot?.buttons) {
227+
const actionButtons = action.troubleshoot.buttons.map(({ variant, label, action }) => {
228+
if (variant === 'success')
229+
return (
230+
<AppButton.GreenButton key={label} onClick={action}>
231+
{label}
232+
</AppButton.GreenButton>
233+
);
234+
else if (variant === 'destructive')
235+
return (
236+
<AppButton.RedButton key={label} onClick={action}>
237+
{label}
238+
</AppButton.RedButton>
239+
);
240+
else
241+
return (
242+
<AppButton.BlueButton key={label} onClick={action}>
243+
{label}
244+
</AppButton.BlueButton>
245+
);
246+
});
203247

204248
buttons = actionButtons.concat(buttons);
205249
}
@@ -208,14 +252,14 @@ function NotificationActionWrapper(props: INotificationActionWrapperProps) {
208252
<>
209253
<NotificationActions>{actionComponent}</NotificationActions>
210254
<ModalAlert
211-
isOpen={troubleshootInfo !== undefined}
255+
isOpen={isModalOpen}
212256
type={ModalAlertType.info}
213257
buttons={buttons}
214-
close={closeTroubleshootInfo}>
215-
<ModalMessage>{troubleshootInfo?.details}</ModalMessage>
258+
close={closeTroubleshootModal}>
259+
<ModalMessage>{action.troubleshoot?.details}</ModalMessage>
216260
<ModalMessage>
217261
<ModalMessageList>
218-
{troubleshootInfo?.steps.map((step) => <li key={step}>{step}</li>)}
262+
{action.troubleshoot?.steps.map((step) => <li key={step}>{step}</li>)}
219263
</ModalMessageList>
220264
</ModalMessage>
221265
<ModalMessage>

desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx

+39-26
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
StyledCellLabel,
3333
StyledCellWarningIcon,
3434
StyledContent,
35+
StyledFdaSpinner,
3536
StyledHeaderTitle,
3637
StyledHeaderTitleContainer,
3738
StyledIcon,
@@ -318,25 +319,26 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
318319
needFullDiskPermissions,
319320
setSplitTunnelingState,
320321
} = useAppContext();
321-
const splitTunnelingEnabledValue = useSelector(
322-
(state: IReduxState) => state.settings.splitTunneling,
323-
);
322+
const splitTunnelingEnabled = useSelector((state: IReduxState) => state.settings.splitTunneling);
324323
const splitTunnelingApplications = useSelector(
325324
(state: IReduxState) => state.settings.splitTunnelingApplications,
326325
);
327326

328327
const [searchTerm, setSearchTerm] = useState('');
329328
const [applications, setApplications] = useState<ISplitTunnelingApplication[]>();
330329

330+
const [loadingDiskPermissions, setLoadingDiskPermissions] = useState(false);
331331
const [splitTunnelingAvailable, setSplitTunnelingAvailable] = useState(
332332
window.env.platform === 'darwin' ? undefined : true,
333333
);
334334

335-
const splitTunnelingEnabled = splitTunnelingEnabledValue && (splitTunnelingAvailable ?? false);
335+
const canEditSplitTunneling = splitTunnelingEnabled && (splitTunnelingAvailable ?? false);
336336

337337
const fetchNeedFullDiskPermissions = useCallback(async () => {
338+
setLoadingDiskPermissions(true);
338339
const needPermissions = await needFullDiskPermissions();
339340
setSplitTunnelingAvailable(!needPermissions);
341+
setLoadingDiskPermissions(false);
340342
}, [needFullDiskPermissions]);
341343

342344
useEffect((): void | (() => void) => {
@@ -378,12 +380,12 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
378380

379381
const addApplication = useCallback(
380382
async (application: ISplitTunnelingApplication | string) => {
381-
if (!splitTunnelingEnabled) {
383+
if (!canEditSplitTunneling) {
382384
await setSplitTunnelingState(true);
383385
}
384386
await addSplitTunnelingApplication(application);
385387
},
386-
[addSplitTunnelingApplication, splitTunnelingEnabled, setSplitTunnelingState],
388+
[addSplitTunnelingApplication, canEditSplitTunneling, setSplitTunnelingState],
387389
);
388390

389391
const addBrowsedForApplication = useCallback(
@@ -406,12 +408,12 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
406408

407409
const removeApplication = useCallback(
408410
async (application: ISplitTunnelingApplication) => {
409-
if (!splitTunnelingEnabled) {
411+
if (!canEditSplitTunneling) {
410412
await setSplitTunnelingState(true);
411413
}
412414
removeSplitTunnelingApplication(application);
413415
},
414-
[removeSplitTunnelingApplication, setSplitTunnelingState, splitTunnelingEnabled],
416+
[removeSplitTunnelingApplication, setSplitTunnelingState, canEditSplitTunneling],
415417
);
416418

417419
const filePickerCallback = useFilePicker(
@@ -443,9 +445,9 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
443445
[addApplication, forgetManuallyAddedApplicationAndUpdate],
444446
);
445447

446-
const showSplitSection = splitTunnelingEnabled && filteredSplitApplications.length > 0;
448+
const showSplitSection = canEditSplitTunneling && filteredSplitApplications.length > 0;
447449
const showNonSplitSection =
448-
splitTunnelingEnabled &&
450+
canEditSplitTunneling &&
449451
(!filteredNonSplitApplications || filteredNonSplitApplications.length > 0);
450452

451453
const excludedTitle = (
@@ -465,26 +467,37 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
465467
<StyledHeaderTitle>{strings.splitTunneling}</StyledHeaderTitle>
466468
<Switch
467469
isOn={splitTunnelingEnabled}
468-
disabled={!splitTunnelingAvailable}
470+
disabled={
471+
!splitTunnelingEnabled && (!splitTunnelingAvailable || loadingDiskPermissions)
472+
}
469473
onChange={setSplitTunnelingState}
470474
/>
471475
</StyledHeaderTitleContainer>
472-
<MacOsSplitTunnelingAvailability
473-
needFullDiskPermissions={
474-
window.env.platform === 'darwin' && splitTunnelingAvailable === false
475-
}
476-
/>
477-
{splitTunnelingAvailable ? (
478-
<HeaderSubTitle>
479-
{messages.pgettext(
480-
'split-tunneling-view',
481-
'Choose the apps you want to exclude from the VPN tunnel.',
476+
{!loadingDiskPermissions && (
477+
<>
478+
<MacOsSplitTunnelingAvailability
479+
needFullDiskPermissions={
480+
window.env.platform === 'darwin' && splitTunnelingAvailable === false
481+
}
482+
/>
483+
{splitTunnelingAvailable && (
484+
<HeaderSubTitle>
485+
{messages.pgettext(
486+
'split-tunneling-view',
487+
'Choose the apps you want to exclude from the VPN tunnel.',
488+
)}
489+
</HeaderSubTitle>
482490
)}
483-
</HeaderSubTitle>
484-
) : null}
491+
</>
492+
)}
485493
</SettingsHeader>
494+
{loadingDiskPermissions && (
495+
<StyledFdaSpinner>
496+
<ImageView source="icon-spinner" height={48} />
497+
</StyledFdaSpinner>
498+
)}
486499

487-
{splitTunnelingEnabled && (
500+
{canEditSplitTunneling && (
488501
<StyledSearchBar searchTerm={searchTerm} onSearch={setSearchTerm} />
489502
)}
490503

@@ -508,7 +521,7 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
508521
</Cell.Section>
509522
</Accordion>
510523

511-
{splitTunnelingEnabled && searchTerm !== '' && !showSplitSection && !showNonSplitSection && (
524+
{canEditSplitTunneling && searchTerm !== '' && !showSplitSection && !showNonSplitSection && (
512525
<StyledNoResult>
513526
<StyledNoResultText>
514527
{formatHtml(
@@ -519,7 +532,7 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
519532
</StyledNoResult>
520533
)}
521534

522-
{splitTunnelingEnabled && (
535+
{canEditSplitTunneling && (
523536
<StyledBrowseButton onClick={addWithFilePicker}>
524537
{messages.pgettext('split-tunneling-view', 'Find another app')}
525538
</StyledBrowseButton>

desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettingsStyles.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,9 @@ export const WideSmallButton = styled(SmallButton)({
136136
export const Spacing = styled.div<{ height: string }>((props) => ({
137137
height: props.height,
138138
}));
139+
140+
export const StyledFdaSpinner = styled.div({
141+
display: 'flex',
142+
justifyContent: 'center',
143+
marginTop: '24px',
144+
});

0 commit comments

Comments
 (0)