diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot
index a2685f87077d..e1a3d61497e0 100644
--- a/gui/locales/messages.pot
+++ b/gui/locales/messages.pot
@@ -1087,7 +1087,11 @@ msgid "Disconnected and unsecure"
msgstr ""
msgctxt "notifications"
-msgid "Failed to enable split tunneling. Please try again or disable it."
+msgid "Failed to enable split tunneling."
+msgstr ""
+
+msgctxt "notifications"
+msgid "Failed to enable split tunneling. Please try reconnecting or disable split tunneling."
msgstr ""
msgctxt "notifications"
@@ -1682,6 +1686,14 @@ msgctxt "tray-icon-tooltip"
msgid "Connecting. %(location)s"
msgstr ""
+msgctxt "troubleshoot"
+msgid "Enable “Full Disk Access” for “Mullvad VPN” in the macOS system settings."
+msgstr ""
+
+msgctxt "troubleshoot"
+msgid "Failed to enable split tunneling. This is because the app is missing system permissions. What you can do:"
+msgstr ""
+
msgctxt "troubleshoot"
msgid "If these steps do not work please send a problem report."
msgstr ""
@@ -1690,6 +1702,10 @@ msgctxt "troubleshoot"
msgid "Make sure you have NF tables support."
msgstr ""
+msgctxt "troubleshoot"
+msgid "Open system settings"
+msgstr ""
+
msgctxt "troubleshoot"
msgid "This error can happen when something other than Mullvad is actively updating the DNS."
msgstr ""
diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts
index c51524f1ee2f..e8f8d96e1806 100644
--- a/gui/src/main/daemon-rpc.ts
+++ b/gui/src/main/daemon-rpc.ts
@@ -1047,10 +1047,9 @@ function convertFromTunnelStateError(state: grpcTypes.ErrorState.AsObject): Erro
cause: ErrorStateCause.splitTunnelError,
};
case grpcTypes.ErrorState.Cause.NEED_FULL_DISK_PERMISSIONS:
- // TODO: handle correctly
return {
...baseError,
- cause: ErrorStateCause.splitTunnelError,
+ cause: ErrorStateCause.needFullDiskPermissions,
};
case grpcTypes.ErrorState.Cause.VPN_PERMISSION_DENIED:
// VPN_PERMISSION_DENIED is only ever created on Android
diff --git a/gui/src/main/user-interface.ts b/gui/src/main/user-interface.ts
index 3d6e04f8e7cd..e3dd5a50eda0 100644
--- a/gui/src/main/user-interface.ts
+++ b/gui/src/main/user-interface.ts
@@ -74,12 +74,20 @@ export default class UserInterface implements WindowControllerDelegate {
});
IpcMainEventChannel.app.handleShowLaunchDaemonSettings(async () => {
+ try {
+ await execAsync('open x-apple.systempreferences:com.apple.LoginItems-Settings.extension');
+ } catch (error) {
+ log.error(`Failed to open launch daemon settings: ${error}`);
+ }
+ });
+
+ IpcMainEventChannel.app.handleShowFullDiskAccessSettings(async () => {
try {
await execAsync(
- 'open -W x-apple.systempreferences:com.apple.LoginItems-Settings.extension',
+ 'open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"',
);
} catch (error) {
- log.error(`Failed to open launch daemon settings: ${error}`);
+ log.error(`Failed to open Full Disk Access settings: ${error}`);
}
});
}
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index 3aa73698dc82..e62d65358e24 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -529,6 +529,10 @@ export default class AppRenderer {
await IpcRendererEventChannel.app.showLaunchDaemonSettings();
}
+ public showFullDiskAccessSettings = async () => {
+ await IpcRendererEventChannel.app.showFullDiskAccessSettings();
+ };
+
public async sendProblemReport(
email: string,
message: string,
diff --git a/gui/src/renderer/components/NotificationArea.tsx b/gui/src/renderer/components/NotificationArea.tsx
index a2a89bcd84b0..79c234f71fab 100644
--- a/gui/src/renderer/components/NotificationArea.tsx
+++ b/gui/src/renderer/components/NotificationArea.tsx
@@ -43,6 +43,8 @@ interface IProps {
}
export default function NotificationArea(props: IProps) {
+ const { showFullDiskAccessSettings } = useAppContext();
+
const account = useSelector((state: IReduxState) => state.account);
const locale = useSelector((state: IReduxState) => state.userInterface.locale);
const tunnelState = useSelector((state: IReduxState) => state.connection.status);
@@ -65,7 +67,7 @@ export default function NotificationArea(props: IProps) {
blockWhenDisconnected,
hasExcludedApps,
}),
- new ErrorNotificationProvider({ tunnelState, hasExcludedApps }),
+ new ErrorNotificationProvider({ tunnelState, hasExcludedApps, showFullDiskAccessSettings }),
new InconsistentVersionNotificationProvider({ consistent: version.consistent }),
new UnsupportedVersionNotificationProvider(version),
];
@@ -168,20 +170,40 @@ function NotificationActionWrapper(props: INotificationActionWrapperProps) {
}
}
+ const problemReportButton = troubleshootInfo?.buttons ? (
+
+ {messages.pgettext('in-app-notifications', 'Send problem report')}
+
+ ) : (
+
+ {messages.pgettext('in-app-notifications', 'Send problem report')}
+
+ );
+
+ let buttons = [
+ problemReportButton,
+
+ {messages.gettext('Back')}
+ ,
+ ];
+
+ if (troubleshootInfo?.buttons) {
+ const actionButtons = troubleshootInfo.buttons.map((button) => (
+
+ {button.label}
+
+ ));
+
+ buttons = actionButtons.concat(buttons);
+ }
+
return (
<>
{actionComponent}
- {messages.pgettext('in-app-notifications', 'Send problem report')}
- ,
-
- {messages.gettext('Back')}
- ,
- ]}
+ buttons={buttons}
close={closeTroubleshootInfo}>
{troubleshootInfo?.details}
diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts
index 761dd01a4cac..ce0f3a896876 100644
--- a/gui/src/shared/daemon-rpc-types.ts
+++ b/gui/src/shared/daemon-rpc-types.ts
@@ -48,6 +48,7 @@ export enum ErrorStateCause {
tunnelParameterError,
isOffline,
splitTunnelError,
+ needFullDiskPermissions,
}
export enum AuthFailedError {
@@ -71,7 +72,8 @@ export type ErrorState =
| ErrorStateCause.setDnsError
| ErrorStateCause.startTunnelError
| ErrorStateCause.isOffline
- | ErrorStateCause.splitTunnelError;
+ | ErrorStateCause.splitTunnelError
+ | ErrorStateCause.needFullDiskPermissions;
blockingError?: FirewallPolicyError;
}
| {
diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts
index e33736bcf864..acbb6366d262 100644
--- a/gui/src/shared/ipc-schema.ts
+++ b/gui/src/shared/ipc-schema.ts
@@ -162,6 +162,7 @@ export const ipcSchema = {
openUrl: invoke(),
showOpenDialog: invoke(),
showLaunchDaemonSettings: invoke(),
+ showFullDiskAccessSettings: invoke(),
getPathBaseName: invoke(),
},
tunnel: {
diff --git a/gui/src/shared/notifications/error.ts b/gui/src/shared/notifications/error.ts
index 1d36b7e1ded0..387ed146a956 100644
--- a/gui/src/shared/notifications/error.ts
+++ b/gui/src/shared/notifications/error.ts
@@ -22,6 +22,7 @@ import {
interface ErrorNotificationContext {
tunnelState: TunnelState;
hasExcludedApps: boolean;
+ showFullDiskAccessSettings?: () => void;
}
export class ErrorNotificationProvider
@@ -32,7 +33,7 @@ export class ErrorNotificationProvider
public getSystemNotification(): SystemNotification | undefined {
if (this.context.tunnelState.state === 'error') {
- let message = getMessage(this.context.tunnelState.details);
+ let message = this.getMessage(this.context.tunnelState.details);
if (!this.context.tunnelState.details.blockingError && this.context.hasExcludedApps) {
message = `${message} ${sprintf(
messages.pgettext(
@@ -58,7 +59,7 @@ export class ErrorNotificationProvider
public getInAppNotification(): InAppNotification | undefined {
if (this.context.tunnelState.state === 'error') {
- let subtitle = getMessage(this.context.tunnelState.details);
+ let subtitle = this.getMessage(this.context.tunnelState.details);
if (!this.context.tunnelState.details.blockingError && this.context.hasExcludedApps) {
subtitle = `${subtitle} ${sprintf(
messages.pgettext(
@@ -78,231 +79,259 @@ export class ErrorNotificationProvider
? messages.pgettext('in-app-notifications', 'NETWORK TRAFFIC MIGHT BE LEAKING')
: messages.pgettext('in-app-notifications', 'BLOCKING INTERNET'),
subtitle,
- action: getActions(this.context.tunnelState.details) ?? undefined,
+ action: this.getActions(this.context.tunnelState.details) ?? undefined,
};
} else {
return undefined;
}
}
-}
-
-function getMessage(errorState: ErrorState): string {
- if (errorState.blockingError) {
- if (errorState.cause === ErrorStateCause.setFirewallPolicyError) {
- switch (process.platform ?? window.env.platform) {
- case 'win32':
- return messages.pgettext(
- 'notifications',
- 'Unable to block all network traffic. Try temporarily disabling any third-party antivirus or security software or send a problem report.',
- );
- case 'linux':
- return messages.pgettext(
- 'notifications',
- 'Unable to block all network traffic. Try updating your kernel or send a problem report.',
- );
- }
- }
-
- return messages.pgettext(
- 'notifications',
- 'Unable to block all network traffic. Please troubleshoot or send a problem report.',
- );
- } else {
- switch (errorState.cause) {
- case ErrorStateCause.authFailed:
- switch (errorState.authFailedError) {
- case AuthFailedError.invalidAccount:
- return messages.pgettext(
- 'auth-failure',
- 'You are logged in with an invalid account number. Please log out and try another one.',
- );
-
- case AuthFailedError.expiredAccount:
- return messages.pgettext('auth-failure', 'Blocking internet: account is out of time');
- case AuthFailedError.tooManyConnections:
- return messages.pgettext(
- 'auth-failure',
- 'Too many simultaneous connections on this account. Disconnect another device or try connecting again shortly.',
- );
-
- case AuthFailedError.unknown:
- default:
- return messages.pgettext(
- 'auth-failure',
- 'Unable to authenticate account. Please send a problem report.',
- );
- }
- case ErrorStateCause.ipv6Unavailable:
- return messages.pgettext(
- 'notifications',
- 'Could not configure IPv6. Disable it in the app or enable it on your device.',
- );
- case ErrorStateCause.setFirewallPolicyError:
+ private getMessage(errorState: ErrorState): string {
+ if (errorState.blockingError) {
+ if (errorState.cause === ErrorStateCause.setFirewallPolicyError) {
switch (process.platform ?? window.env.platform) {
case 'win32':
return messages.pgettext(
'notifications',
- 'Unable to apply firewall rules. Try temporarily disabling any third-party antivirus or security software.',
+ 'Unable to block all network traffic. Try temporarily disabling any third-party antivirus or security software or send a problem report.',
);
case 'linux':
return messages.pgettext(
'notifications',
- 'Unable to apply firewall rules. Try updating your kernel.',
+ 'Unable to block all network traffic. Try updating your kernel or send a problem report.',
);
- default:
- return messages.pgettext('notifications', 'Unable to apply firewall rules.');
}
- case ErrorStateCause.setDnsError:
- return messages.pgettext(
- 'notifications',
- 'Unable to set system DNS server. Please send a problem report.',
- );
- case ErrorStateCause.startTunnelError:
- return messages.pgettext(
- 'notifications',
- 'Unable to start tunnel connection. Please send a problem report.',
- );
- case ErrorStateCause.createTunnelDeviceError:
- if (errorState.osError === 4319) {
+ }
+
+ return messages.pgettext(
+ 'notifications',
+ 'Unable to block all network traffic. Please troubleshoot or send a problem report.',
+ );
+ } else {
+ switch (errorState.cause) {
+ case ErrorStateCause.authFailed:
+ switch (errorState.authFailedError) {
+ case AuthFailedError.invalidAccount:
+ return messages.pgettext(
+ 'auth-failure',
+ 'You are logged in with an invalid account number. Please log out and try another one.',
+ );
+
+ case AuthFailedError.expiredAccount:
+ return messages.pgettext('auth-failure', 'Blocking internet: account is out of time');
+
+ case AuthFailedError.tooManyConnections:
+ return messages.pgettext(
+ 'auth-failure',
+ 'Too many simultaneous connections on this account. Disconnect another device or try connecting again shortly.',
+ );
+
+ case AuthFailedError.unknown:
+ default:
+ return messages.pgettext(
+ 'auth-failure',
+ 'Unable to authenticate account. Please send a problem report.',
+ );
+ }
+ case ErrorStateCause.ipv6Unavailable:
return messages.pgettext(
'notifications',
- 'Unable to start tunnel connection. This could be because of conflicts with VMware, please troubleshoot.',
+ 'Could not configure IPv6. Disable it in the app or enable it on your device.',
);
- }
+ case ErrorStateCause.setFirewallPolicyError:
+ switch (process.platform ?? window.env.platform) {
+ case 'win32':
+ return messages.pgettext(
+ 'notifications',
+ 'Unable to apply firewall rules. Try temporarily disabling any third-party antivirus or security software.',
+ );
+ case 'linux':
+ return messages.pgettext(
+ 'notifications',
+ 'Unable to apply firewall rules. Try updating your kernel.',
+ );
+ default:
+ return messages.pgettext('notifications', 'Unable to apply firewall rules.');
+ }
+ case ErrorStateCause.setDnsError:
+ return messages.pgettext(
+ 'notifications',
+ 'Unable to set system DNS server. Please send a problem report.',
+ );
+ case ErrorStateCause.startTunnelError:
+ return messages.pgettext(
+ 'notifications',
+ 'Unable to start tunnel connection. Please send a problem report.',
+ );
+ case ErrorStateCause.createTunnelDeviceError:
+ if (errorState.osError === 4319) {
+ return messages.pgettext(
+ 'notifications',
+ 'Unable to start tunnel connection. This could be because of conflicts with VMware, please troubleshoot.',
+ );
+ }
+ return messages.pgettext(
+ 'notifications',
+ 'Unable to start tunnel connection. Please send a problem report.',
+ );
+ case ErrorStateCause.tunnelParameterError:
+ return this.getTunnelParameterMessage(errorState.parameterError);
+ case ErrorStateCause.isOffline:
+ return messages.pgettext(
+ 'notifications',
+ 'Your device is offline. The tunnel will automatically connect once your device is back online.',
+ );
+ case ErrorStateCause.needFullDiskPermissions:
+ return messages.pgettext('notifications', 'Failed to enable split tunneling.');
+ case ErrorStateCause.splitTunnelError:
+ switch (process.platform ?? window.env.platform) {
+ case 'darwin':
+ return messages.pgettext(
+ 'notifications',
+ 'Failed to enable split tunneling. Please try reconnecting or disable split tunneling.',
+ );
+ default:
+ return messages.pgettext(
+ 'notifications',
+ 'Unable to communicate with Mullvad kernel driver. Try reconnecting or send a problem report.',
+ );
+ }
+ }
+ }
+ }
+
+ private getTunnelParameterMessage(error: TunnelParameterError): string {
+ switch (error) {
+ /// TODO: once bridge constraints can be set, add a more descriptive error message
+ case TunnelParameterError.noMatchingBridgeRelay:
+ case TunnelParameterError.noMatchingRelay:
return messages.pgettext(
'notifications',
- 'Unable to start tunnel connection. Please send a problem report.',
+ 'No servers match your settings, try changing server or other settings.',
+ );
+ case TunnelParameterError.noWireguardKey:
+ return sprintf(
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(wireguard)s - will be replaced with "WireGuard"
+ messages.pgettext(
+ 'notifications',
+ 'Valid %(wireguard)s key is missing. Manage keys under Advanced settings.',
+ ),
+ { wireguard: strings.wireguard },
);
- case ErrorStateCause.tunnelParameterError:
- return getTunnelParameterMessage(errorState.parameterError);
- case ErrorStateCause.isOffline:
+ case TunnelParameterError.customTunnelHostResolutionError:
return messages.pgettext(
'notifications',
- 'Your device is offline. The tunnel will automatically connect once your device is back online.',
+ 'Unable to resolve host of custom tunnel. Try changing your settings.',
);
- case ErrorStateCause.splitTunnelError:
- switch (process.platform ?? window.env.platform) {
- case 'darwin':
- return messages.pgettext(
- 'notifications',
- 'Failed to enable split tunneling. Please try again or disable it.',
- );
- default:
- return messages.pgettext(
- 'notifications',
- 'Unable to communicate with Mullvad kernel driver. Try reconnecting or send a problem report.',
- );
- }
}
}
-}
-function getTunnelParameterMessage(error: TunnelParameterError): string {
- switch (error) {
- /// TODO: once bridge constraints can be set, add a more descriptive error message
- case TunnelParameterError.noMatchingBridgeRelay:
- case TunnelParameterError.noMatchingRelay:
- return messages.pgettext(
- 'notifications',
- 'No servers match your settings, try changing server or other settings.',
- );
- case TunnelParameterError.noWireguardKey:
- return sprintf(
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(wireguard)s - will be replaced with "WireGuard"
- messages.pgettext(
- 'notifications',
- 'Valid %(wireguard)s key is missing. Manage keys under Advanced settings.',
- ),
- { wireguard: strings.wireguard },
- );
- case TunnelParameterError.customTunnelHostResolutionError:
- return messages.pgettext(
- 'notifications',
- 'Unable to resolve host of custom tunnel. Try changing your settings.',
- );
- }
-}
+ private getActions(errorState: ErrorState): InAppNotificationAction | void {
+ const platform = process.platform ?? window.env.platform;
+
+ if (errorState.cause === ErrorStateCause.setFirewallPolicyError && platform === 'linux') {
+ return {
+ type: 'troubleshoot-dialog',
+ troubleshoot: {
+ details: messages.pgettext('troubleshoot', 'This might be caused by an outdated kernel.'),
+ steps: [
+ messages.pgettext('troubleshoot', 'Update your kernel.'),
+ messages.pgettext('troubleshoot', 'Make sure you have NF tables support.'),
+ ],
+ },
+ };
+ } else if (errorState.cause === ErrorStateCause.setDnsError) {
+ const troubleshootSteps = [];
+ if (platform === 'darwin') {
+ troubleshootSteps.push(
+ messages.pgettext(
+ 'troubleshoot',
+ 'Try to turn Wi-Fi Calling off in the FaceTime app settings and restart the Mac.',
+ ),
+ messages.pgettext(
+ 'troubleshoot',
+ 'Uninstall or disable other DNS, networking and ads/website blocking apps.',
+ ),
+ );
+ } else if (platform === 'win32') {
+ troubleshootSteps.push(
+ messages.pgettext(
+ 'troubleshoot',
+ 'Uninstall or disable other DNS, networking and ads/website blocking apps.',
+ ),
+ );
+ }
-function getActions(errorState: ErrorState): InAppNotificationAction | void {
- const platform = process.platform ?? window.env.platform;
+ return {
+ type: 'troubleshoot-dialog',
+ troubleshoot: {
+ details: messages.pgettext(
+ 'troubleshoot',
+ 'This error can happen when something other than Mullvad is actively updating the DNS.',
+ ),
+ steps: troubleshootSteps,
+ },
+ };
+ } else if (errorState.cause === ErrorStateCause.needFullDiskPermissions) {
+ let troubleshootButtons = undefined;
+ if (this.context.showFullDiskAccessSettings) {
+ troubleshootButtons = [
+ {
+ label: messages.pgettext('troubleshoot', 'Open system settings'),
+ action: () => this.context.showFullDiskAccessSettings?.(),
+ },
+ ];
+ }
- if (errorState.cause === ErrorStateCause.setFirewallPolicyError && platform === 'linux') {
- return {
- type: 'troubleshoot-dialog',
- troubleshoot: {
- details: messages.pgettext('troubleshoot', 'This might be caused by an outdated kernel.'),
- steps: [
- messages.pgettext('troubleshoot', 'Update your kernel.'),
- messages.pgettext('troubleshoot', 'Make sure you have NF tables support.'),
- ],
- },
- };
- } else if (errorState.cause === ErrorStateCause.setDnsError) {
- const troubleshootSteps = [];
- if (platform === 'darwin') {
- troubleshootSteps.push(
- messages.pgettext(
- 'troubleshoot',
- 'Try to turn Wi-Fi Calling off in the FaceTime app settings and restart the Mac.',
- ),
- messages.pgettext(
- 'troubleshoot',
- 'Uninstall or disable other DNS, networking and ads/website blocking apps.',
- ),
- );
- } else if (platform === 'win32') {
- troubleshootSteps.push(
- messages.pgettext(
- 'troubleshoot',
- 'Uninstall or disable other DNS, networking and ads/website blocking apps.',
- ),
- );
+ return {
+ type: 'troubleshoot-dialog',
+ troubleshoot: {
+ details: messages.pgettext(
+ 'troubleshoot',
+ 'Failed to enable split tunneling. This is because the app is missing system permissions. What you can do:',
+ ),
+ steps: [
+ messages.pgettext(
+ 'troubleshoot',
+ 'Enable “Full Disk Access” for “Mullvad VPN” in the macOS system settings.',
+ ),
+ ],
+ buttons: troubleshootButtons,
+ },
+ };
+ } else if (platform === 'win32' && errorState.cause === ErrorStateCause.splitTunnelError) {
+ return {
+ type: 'troubleshoot-dialog',
+ troubleshoot: {
+ details: messages.pgettext(
+ 'troubleshoot',
+ 'Unable to communicate with Mullvad kernel driver.',
+ ),
+ steps: [
+ messages.pgettext('troubleshoot', 'Try reconnecting.'),
+ messages.pgettext('troubleshoot', 'Try restarting your device.'),
+ ],
+ },
+ };
+ } else if (
+ errorState.cause === ErrorStateCause.createTunnelDeviceError &&
+ errorState.osError === 4319
+ ) {
+ return {
+ type: 'troubleshoot-dialog',
+ troubleshoot: {
+ details: messages.pgettext(
+ 'troubleshoot',
+ 'Unable to start tunnel connection because of a failure when creating the tunnel device. This is often caused by conflicts with the VMware Bridge Protocol.',
+ ),
+ steps: [
+ messages.pgettext('troubleshoot', 'Try to reinstall VMware.'),
+ messages.pgettext('troubleshoot', 'Try to uninstall VMware.'),
+ ],
+ },
+ };
}
-
- return {
- type: 'troubleshoot-dialog',
- troubleshoot: {
- details: messages.pgettext(
- 'troubleshoot',
- 'This error can happen when something other than Mullvad is actively updating the DNS.',
- ),
- steps: troubleshootSteps,
- },
- };
- } else if (errorState.cause === ErrorStateCause.splitTunnelError) {
- // TODO: macos: handle this and full disk access error
- return {
- type: 'troubleshoot-dialog',
- troubleshoot: {
- details: messages.pgettext(
- 'troubleshoot',
- 'Unable to communicate with Mullvad kernel driver.',
- ),
- steps: [
- messages.pgettext('troubleshoot', 'Try reconnecting.'),
- messages.pgettext('troubleshoot', 'Try restarting your device.'),
- ],
- },
- };
- } else if (
- errorState.cause === ErrorStateCause.createTunnelDeviceError &&
- errorState.osError === 4319
- ) {
- return {
- type: 'troubleshoot-dialog',
- troubleshoot: {
- details: messages.pgettext(
- 'troubleshoot',
- 'Unable to start tunnel connection because of a failure when creating the tunnel device. This is often caused by conflicts with the VMware Bridge Protocol.',
- ),
- steps: [
- messages.pgettext('troubleshoot', 'Try to reinstall VMware.'),
- messages.pgettext('troubleshoot', 'Try to uninstall VMware.'),
- ],
- },
- };
}
}
diff --git a/gui/src/shared/notifications/notification.ts b/gui/src/shared/notifications/notification.ts
index 1957cc07fb0f..87166aab4d57 100644
--- a/gui/src/shared/notifications/notification.ts
+++ b/gui/src/shared/notifications/notification.ts
@@ -8,6 +8,12 @@ export type NotificationAction = {
export interface InAppNotificationTroubleshootInfo {
details: string;
steps: string[];
+ buttons?: Array;
+}
+
+export interface InAppNotificationTroubleshootButton {
+ label: string;
+ action: () => void;
}
export type InAppNotificationAction =