diff options
| author | Markus Pettersson <markus.pettersson@mullvad.net> | 2025-03-19 15:14:41 +0100 |
|---|---|---|
| committer | Markus Pettersson <markus.pettersson@mullvad.net> | 2025-03-19 15:14:41 +0100 |
| commit | 01102af2d21cb2eadc36a422359a4eca1ed5fc17 (patch) | |
| tree | c43467f3e3976939e4a7192f43e5a53b00281918 | |
| parent | 1cb64dd714e0bfca67f90cce6c7d80633e60e417 (diff) | |
| parent | 306a5a8990f5de9b83bb0c78d5d04e3863263b53 (diff) | |
| download | mullvadvpn-01102af2d21cb2eadc36a422359a4eca1ed5fc17.tar.xz mullvadvpn-01102af2d21cb2eadc36a422359a4eca1ed5fc17.zip | |
Merge branch 'implement-openvpn-warnings-des-1583'
11 files changed, 420 insertions, 45 deletions
diff --git a/desktop/packages/mullvad-vpn/locales/messages.pot b/desktop/packages/mullvad-vpn/locales/messages.pot index 2cc943c454..d4a794e4ad 100644 --- a/desktop/packages/mullvad-vpn/locales/messages.pot +++ b/desktop/packages/mullvad-vpn/locales/messages.pot @@ -324,6 +324,17 @@ msgctxt "accessibility" msgid "Forget account number %(accountNumber)s" msgstr "" +#. Accessibility label for link to blog post about OpenVPN support ending. +msgctxt "accessibility" +msgid "Go to blog post to read more, opens externally" +msgstr "" + +#. Accessibility label for link to VPN settings where +#. the user can change tunnel protocol. +msgctxt "accessibility" +msgid "Go to VPN settings to change tunnel protocol" +msgstr "" + #. Provided to accessibility tools such as screenreaders to describe #. the button which obscures the account number. msgctxt "accessibility" @@ -900,6 +911,29 @@ msgctxt "in-app-notifications" msgid "%(duration)s. Buy more credit." msgstr "" +#. First part of notification subtitle when there are no openVPN servers available. +#. Will be followed by a link to VPN settings. +#. Available placeholders: +#. %(openVpn)s - Will be replaced with OpenVPN +msgctxt "in-app-notifications" +msgid "%(openVpn)s support has ended. Please update the app or" +msgstr "" + +#. Notification title indicating that OpenVPN support is ending. +#. Available placeholders: +#. %(openVpn)s - Will be replaced with OPENVPN +msgctxt "in-app-notifications" +msgid "%(openVpn)s SUPPORT IS ENDING" +msgstr "" + +#. First part of notification subtitle when there are no openVPN servers +#. matching current settings. Will be followed by a link to VPN settings. +#. Available placeholders: +#. %(openVpn)s - Will be replaced with OpenVPN +msgctxt "in-app-notifications" +msgid "%(openVpn)s support is ending. Switch location or" +msgstr "" + msgctxt "in-app-notifications" msgid "ACCOUNT CREDIT EXPIRES SOON" msgstr "" @@ -916,6 +950,14 @@ msgctxt "in-app-notifications" msgid "BLOCKING INTERNET" msgstr "" +#. Link following the first part of the notification subtitle. +#. Will navigate the user to the VPN settings. +#. Available placeholders: +#. %(wireGuard)s - Will be replaced with WireGuard +msgctxt "in-app-notifications" +msgid "change tunnel protocol to %(wireGuard)s" +msgstr "" + msgctxt "in-app-notifications" msgid "Click here to see what’s new." msgstr "" @@ -937,10 +979,37 @@ msgctxt "in-app-notifications" msgid "NEW VERSION INSTALLED" msgstr "" +#. Notification title when there are no openVPN servers +#. matching current settings. +#. Available placeholders: +#. %(openVpn)s - Will be replaced with OPENVPN +msgctxt "in-app-notifications" +msgid "NO %(openVpn)s SERVER AVAILABLE" +msgstr "" + +#. Notification title when there are no openVPN servers available. +#. Available placeholders: +#. %(openVpn)s - Will be replaced with OPENVPN +msgctxt "in-app-notifications" +msgid "NO %(openVpn)s SERVERS AVAILABLE" +msgstr "" + +#. Notification subtitle indicating that OpenVPN support is ending. +#. Available placeholders: +#. %(wireGuard)s - Will be replaced with WireGuard +msgctxt "in-app-notifications" +msgid "Please change tunnel protocol to %(wireGuard)s." +msgstr "" + msgctxt "in-app-notifications" msgid "Please quit and restart the app." msgstr "" +#. Link in notication to a blog post about OpenVPN support ending. +msgctxt "in-app-notifications" +msgid "Read more" +msgstr "" + msgctxt "in-app-notifications" msgid "Send problem report" msgstr "" @@ -1946,6 +2015,13 @@ msgctxt "vpn-settings-view" msgid "Attention: this setting cannot be used in combination with <b>%(customDnsFeatureName)s</b>" msgstr "" +#. Footer text for tunnel protocol selector when OpenVPN is selected. +#. Available placeholders: +#. %(openvpn)s - Will be replaced with OpenVPN +msgctxt "vpn-settings-view" +msgid "Attention: We are removing support for %(openVpn)s." +msgstr "" + msgctxt "vpn-settings-view" msgid "Auto-connect" msgstr "" @@ -2020,6 +2096,12 @@ msgctxt "vpn-settings-view" msgid "Malware" msgstr "" +#. Link in tunnel protocol selector footer to blog post +#. about OpenVPN support ending. +msgctxt "vpn-settings-view" +msgid "Read more" +msgstr "" + msgctxt "vpn-settings-view" msgid "Server IP override" msgstr "" diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx index b0588e6c0d..9a7ad3c928 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx @@ -16,18 +16,18 @@ import { } from '../../shared/notifications'; import { useAppContext } from '../context'; import useActions from '../lib/actionsHook'; -import { Colors } from '../lib/foundations'; import { transitions, useHistory } from '../lib/history'; -import { formatHtml } from '../lib/html-formatter'; import { NewDeviceNotificationProvider, NewVersionNotificationProvider, + NoOpenVpnServerAvailableNotificationProvider, + OpenVpnSupportEndingNotificationProvider, } from '../lib/notifications'; +import { useTunnelProtocol } from '../lib/relay-settings-hooks'; import { RoutePath } from '../lib/routes'; import accountActions from '../redux/account/actions'; import { IReduxState, useSelector } from '../redux/store'; import * as AppButton from './AppButton'; -import { InternalLink } from './InternalLink'; import { ModalAlert, ModalAlertType, ModalMessage, ModalMessageList } from './Modal'; import { NotificationActions, @@ -36,10 +36,10 @@ import { NotificationContent, NotificationIndicator, NotificationOpenLinkAction, - NotificationSubtitle, NotificationTitle, NotificationTroubleshootDialogAction, } from './NotificationBanner'; +import { NotificationSubtitle } from './NotificationSubtitle'; interface IProps { className?: string; @@ -52,6 +52,10 @@ export default function NotificationArea(props: IProps) { const locale = useSelector((state: IReduxState) => state.userInterface.locale); const tunnelState = useSelector((state: IReduxState) => state.connection.status); const version = useSelector((state: IReduxState) => state.version); + const tunnelProtocol = useTunnelProtocol(); + const reduxConnection = useSelector((state) => state.connection); + const fullRelayList = useSelector((state) => state.settings.relayLocations); + const blockWhenDisconnected = useSelector( (state: IReduxState) => state.settings.blockWhenDisconnected, ); @@ -90,7 +94,11 @@ export default function NotificationArea(props: IProps) { blockWhenDisconnected, hasExcludedApps, }), - + new NoOpenVpnServerAvailableNotificationProvider({ + tunnelProtocol, + tunnelState: reduxConnection.status, + relayLocations: fullRelayList, + }), new ErrorNotificationProvider({ tunnelState, hasExcludedApps, @@ -120,6 +128,7 @@ export default function NotificationArea(props: IProps) { close, }), new UpdateAvailableNotificationProvider(version), + new OpenVpnSupportEndingNotificationProvider({ tunnelProtocol }), ); const notificationProvider = notificationProviders.find((notification) => @@ -140,18 +149,10 @@ export default function NotificationArea(props: IProps) { <NotificationTitle data-testid="notificationTitle"> {notification.title} </NotificationTitle> - <NotificationSubtitle data-testid="notificationSubTitle"> - {notification.subtitleAction?.type === 'navigate-internal' ? ( - <InternalLink - variant="labelTiny" - color={Colors.white60} - {...notification.subtitleAction.link}> - {formatHtml(notification.subtitle ?? '')} - </InternalLink> - ) : ( - formatHtml(notification.subtitle ?? '') - )} - </NotificationSubtitle> + <NotificationSubtitle + data-testid="notificationSubTitle" + subtitle={notification.subtitle} + /> </NotificationContent> {notification.action && ( <NotificationActionWrapper diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationBanner.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationBanner.tsx index 9ce77d8e78..3f6da5eb09 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationBanner.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationBanner.tsx @@ -15,18 +15,6 @@ export const NotificationTitle = styled.span(tinyText, { color: Colors.white, }); -export const NotificationSubtitleText = styled.span(tinyText, { - color: Colors.white60, -}); - -interface INotificationSubtitleProps { - children?: React.ReactNode; -} - -export function NotificationSubtitle(props: INotificationSubtitleProps) { - return React.Children.count(props.children) > 0 ? <NotificationSubtitleText {...props} /> : null; -} - export const NotificationActionButton = styled(AppButton.SimpleButton)({ flex: 1, justifyContent: 'center', diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationSubtitle.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationSubtitle.tsx new file mode 100644 index 0000000000..fc5a4600d1 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationSubtitle.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import styled from 'styled-components'; + +import { InAppNotificationSubtitle } from '../../shared/notifications'; +import { Icon, LabelTiny } from '../lib/components'; +import { Colors } from '../lib/foundations'; +import { formatHtml } from '../lib/html-formatter'; +import { ExternalLink } from './ExternalLink'; +import { InternalLink } from './InternalLink'; + +export type NotificationSubtitleProps = { + subtitle?: string | InAppNotificationSubtitle[]; +}; + +const StyledIcon = styled(Icon)` + display: inline-flex; + vertical-align: middle; +`; + +const formatSubtitle = (subtitle: InAppNotificationSubtitle) => { + const content = formatHtml(subtitle.content); + if (subtitle.action) { + switch (subtitle.action.type) { + case 'navigate-internal': + return ( + <InternalLink variant="labelTiny" {...subtitle.action.link}> + {content} + </InternalLink> + ); + case 'navigate-external': + return ( + <ExternalLink variant="labelTiny" {...subtitle.action.link}> + {content} + <StyledIcon icon="external" size="small" /> + </ExternalLink> + ); + default: + break; + } + } + return content; +}; + +export const NotificationSubtitle = ({ subtitle, ...props }: NotificationSubtitleProps) => { + if (!subtitle) { + return null; + } + + if (!Array.isArray(subtitle)) { + return ( + <LabelTiny color={Colors.white60} {...props}> + {formatHtml(subtitle)} + </LabelTiny> + ); + } + + return ( + <LabelTiny color={Colors.white60} {...props}> + {subtitle.map((subtitle, index, arr) => { + const content = formatSubtitle(subtitle); + + return ( + <React.Fragment key={subtitle.content}> + {content} + {index !== arr.length - 1 && ' '} + </React.Fragment> + ); + })} + </LabelTiny> + ); +}; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/VpnSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/VpnSettings.tsx index a3b0174e98..a4077a6128 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/VpnSettings.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/VpnSettings.tsx @@ -2,11 +2,12 @@ import { useCallback, useMemo } from 'react'; import { sprintf } from 'sprintf-js'; import styled from 'styled-components'; -import { strings } from '../../shared/constants'; +import { strings, urls } from '../../shared/constants'; import { IDnsOptions, TunnelProtocol } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import log from '../../shared/logging'; import { useAppContext } from '../context'; +import { Flex, Icon } from '../lib/components'; import { useRelaySettingsUpdater } from '../lib/constraint-updater'; import { Colors, spacings } from '../lib/foundations'; import { useHistory } from '../lib/history'; @@ -22,6 +23,7 @@ import { AriaDescription, AriaDetails, AriaInput, AriaInputGroup, AriaLabel } fr import * as Cell from './cell'; import Selector, { SelectorItem } from './cell/Selector'; import CustomDnsSettings from './CustomDnsSettings'; +import { ExternalLink } from './ExternalLink'; import InfoButton from './InfoButton'; import { BackAction } from './KeyboardNavigation'; import { Layout, SettingsContainer, SettingsContent, SettingsGroup, SettingsStack } from './Layout'; @@ -725,7 +727,7 @@ function TunnelProtocolSetting() { value={tunnelProtocol} onSelect={setTunnelProtocol} /> - {openVpnDisabled ? ( + {openVpnDisabled && ( <Cell.CellFooter> <AriaDescription> <Cell.CellFooterText> @@ -739,7 +741,35 @@ function TunnelProtocolSetting() { </Cell.CellFooterText> </AriaDescription> </Cell.CellFooter> - ) : null} + )} + {tunnelProtocol === 'openvpn' && ( + <Cell.CellFooter> + <AriaDescription> + <Cell.CellFooterText> + {sprintf( + // TRANSLATORS: Footer text for tunnel protocol selector when OpenVPN is selected. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(openvpn)s - Will be replaced with OpenVPN + messages.pgettext( + 'vpn-settings-view', + 'Attention: We are removing support for %(openVpn)s.', + ), + { openVpn: strings.openvpn }, + )}{' '} + </Cell.CellFooterText> + </AriaDescription> + <ExternalLink variant="labelTiny" to={urls.removingOpenVpnBlog}> + <Flex> + {sprintf( + // TRANSLATORS: Link in tunnel protocol selector footer to blog post + // TRANSLATORS: about OpenVPN support ending. + messages.pgettext('vpn-settings-view', 'Read more'), + )} + <Icon icon="external" size="small" /> + </Flex> + </ExternalLink> + </Cell.CellFooter> + )} </AriaInputGroup> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/notifications/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/notifications/index.ts index a03af00d06..d949ff0428 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/lib/notifications/index.ts +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/notifications/index.ts @@ -1,2 +1,4 @@ export * from './new-device'; export * from './new-version'; +export * from './open-vpn-support-ending'; +export * from './no-open-vpn-server-available'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/notifications/new-version.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/notifications/new-version.ts index 337ba016c5..0bccec27a0 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/lib/notifications/new-version.ts +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/notifications/new-version.ts @@ -27,18 +27,22 @@ export class NewVersionNotificationProvider implements InAppNotificationProvider indicator: 'success', action: { type: 'close', close: this.context.close }, title, - subtitle, - subtitleAction: { - type: 'navigate-internal', - link: { - to: RoutePath.changelog, - onClick: this.context.close, - 'aria-label': messages.pgettext( - 'accessibility', - 'New version installed, click here to see the changelog', - ), + subtitle: [ + { + content: subtitle, + action: { + type: 'navigate-internal', + link: { + to: RoutePath.changelog, + onClick: this.context.close, + 'aria-label': messages.pgettext( + 'accessibility', + 'New version installed, click here to see the changelog', + ), + }, + }, }, - }, + ], }; } } diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/notifications/no-open-vpn-server-available.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/notifications/no-open-vpn-server-available.ts new file mode 100644 index 0000000000..dc8819835a --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/notifications/no-open-vpn-server-available.ts @@ -0,0 +1,122 @@ +import { sprintf } from 'sprintf-js'; + +import { strings } from '../../../shared/constants'; +import { + ErrorStateCause, + TunnelParameterError, + TunnelProtocol, + TunnelState, +} from '../../../shared/daemon-rpc-types'; +import { messages } from '../../../shared/gettext'; +import { + InAppNotification, + InAppNotificationProvider, + InAppNotificationSubtitle, +} from '../../../shared/notifications'; +import { IRelayLocationCountryRedux } from '../../redux/settings/reducers'; +import { RoutePath } from '../routes'; + +interface NoOpenVpnServerAvailableNotificationContext { + tunnelProtocol: TunnelProtocol; + tunnelState: TunnelState; + relayLocations: IRelayLocationCountryRedux[]; +} + +export class NoOpenVpnServerAvailableNotificationProvider implements InAppNotificationProvider { + public constructor(private context: NoOpenVpnServerAvailableNotificationContext) {} + + public mayDisplay = () => { + const { tunnelState } = this.context; + return ( + tunnelState.state === 'error' && + tunnelState.details.cause === ErrorStateCause.tunnelParameterError && + tunnelState.details.parameterError === TunnelParameterError.noMatchingRelay + ); + }; + + public getInAppNotification(): InAppNotification { + let title: string = ''; + const subtitle: InAppNotificationSubtitle[] = []; + const capitalizedOpenVpn = strings.openvpn.toUpperCase(); + if (this.anyOpenVpnLocationsEnabled()) { + title = sprintf( + // TRANSLATORS: Notification title when there are no openVPN servers + // TRANSLATORS: matching current settings. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(openVpn)s - Will be replaced with OPENVPN + messages.pgettext('in-app-notifications', 'NO %(openVpn)s SERVER AVAILABLE'), + { openVpn: capitalizedOpenVpn }, + ); + subtitle.push({ + content: sprintf( + // TRANSLATORS: First part of notification subtitle when there are no openVPN servers + // TRANSLATORS: matching current settings. Will be followed by a link to VPN settings. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(openVpn)s - Will be replaced with OpenVPN + messages.pgettext( + 'in-app-notifications', + '%(openVpn)s support is ending. Switch location or', + ), + { openVpn: strings.openvpn }, + ), + }); + } else { + title = sprintf( + // TRANSLATORS: Notification title when there are no openVPN servers available. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(openVpn)s - Will be replaced with OPENVPN + messages.pgettext('in-app-notifications', 'NO %(openVpn)s SERVERS AVAILABLE'), + { openVpn: capitalizedOpenVpn }, + ); + subtitle.push({ + content: sprintf( + // TRANSLATORS: First part of notification subtitle when there are no openVPN servers available. + // TRANSLATORS: Will be followed by a link to VPN settings. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(openVpn)s - Will be replaced with OpenVPN + messages.pgettext( + 'in-app-notifications', + '%(openVpn)s support has ended. Please update the app or', + ), + { openVpn: strings.openvpn }, + ), + }); + } + subtitle.push({ + content: sprintf( + // TRANSLATORS: Link following the first part of the notification subtitle. + // TRANSLATORS: Will navigate the user to the VPN settings. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(wireGuard)s - Will be replaced with WireGuard + messages.pgettext('in-app-notifications', 'change tunnel protocol to %(wireGuard)s'), + { wireGuard: strings.wireguard }, + ), + action: { + type: 'navigate-internal', + link: { + to: RoutePath.vpnSettings, + 'aria-label': + // TRANSLATORS: Accessibility label for link to VPN settings where + // TRANSLATORS: the user can change tunnel protocol. + messages.pgettext('accessibility', 'Go to VPN settings to change tunnel protocol'), + }, + }, + }); + + return { + indicator: 'error', + title, + subtitle, + }; + } + + private anyOpenVpnLocationsEnabled() { + return this.context.relayLocations.some((location) => { + return location.cities.some((city) => { + return city.relays.some((relay) => { + return relay.endpointType === 'openvpn' && relay.active; + }); + }); + }); + } +} diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/notifications/open-vpn-support-ending.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/notifications/open-vpn-support-ending.ts new file mode 100644 index 0000000000..8ddecd84b5 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/notifications/open-vpn-support-ending.ts @@ -0,0 +1,65 @@ +import { sprintf } from 'sprintf-js'; + +import { strings, urls } from '../../../shared/constants'; +import { TunnelProtocol } from '../../../shared/daemon-rpc-types'; +import { messages } from '../../../shared/gettext'; +import { InAppNotification, InAppNotificationProvider } from '../../../shared/notifications'; + +interface OpenVpnSupportEndingNotificationContext { + tunnelProtocol: TunnelProtocol; +} + +export class OpenVpnSupportEndingNotificationProvider implements InAppNotificationProvider { + public constructor(private context: OpenVpnSupportEndingNotificationContext) {} + + public mayDisplay = () => { + return this.context.tunnelProtocol === 'openvpn'; + }; + + public getInAppNotification(): InAppNotification { + const capitalizedOpenVpn = strings.openvpn.toUpperCase(); + return { + indicator: 'warning', + title: sprintf( + // TRANSLATORS: Notification title indicating that OpenVPN support is ending. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(openVpn)s - Will be replaced with OPENVPN + messages.pgettext('in-app-notifications', '%(openVpn)s SUPPORT IS ENDING'), + { + openVpn: capitalizedOpenVpn, + }, + ), + subtitle: [ + { + content: sprintf( + // TRANSLATORS: Notification subtitle indicating that OpenVPN support is ending. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(wireGuard)s - Will be replaced with WireGuard + messages.pgettext( + 'in-app-notifications', + 'Please change tunnel protocol to %(wireGuard)s.', + ), + { wireGuard: strings.wireguard }, + ), + }, + { + content: + // TRANSLATORS: Link in notication to a blog post about OpenVPN support ending. + messages.pgettext('in-app-notifications', 'Read more'), + action: { + type: 'navigate-external', + link: { + to: urls.removingOpenVpnBlog, + 'aria-label': + // TRANSLATORS: Accessibility label for link to blog post about OpenVPN support ending. + messages.pgettext( + 'accessibility', + 'Go to blog post to read more, opens externally', + ), + }, + }, + }, + ], + }; + } +} diff --git a/desktop/packages/mullvad-vpn/src/shared/constants/urls.ts b/desktop/packages/mullvad-vpn/src/shared/constants/urls.ts index 24851e0a4c..f9a77a92b0 100644 --- a/desktop/packages/mullvad-vpn/src/shared/constants/urls.ts +++ b/desktop/packages/mullvad-vpn/src/shared/constants/urls.ts @@ -5,6 +5,7 @@ export const urls = { faq: 'https://mullvad.net/help/tag/mullvad-app/', privacyGuide: 'https://mullvad.net/help/first-steps-towards-online-privacy/', download: 'https://mullvad.net/download/vpn/', + removingOpenVpnBlog: 'https://mullvad.net/en/blog/removing-openvpn-15th-january-2026', } as const; type BaseUrl = (typeof urls)[keyof typeof urls]; diff --git a/desktop/packages/mullvad-vpn/src/shared/notifications/notification.ts b/desktop/packages/mullvad-vpn/src/shared/notifications/notification.ts index 80a40deef2..fa31004d6a 100644 --- a/desktop/packages/mullvad-vpn/src/shared/notifications/notification.ts +++ b/desktop/packages/mullvad-vpn/src/shared/notifications/notification.ts @@ -1,3 +1,4 @@ +import { ExternalLinkProps } from '../../renderer/components/ExternalLink'; import { InternalLinkProps } from '../../renderer/components/InternalLink'; import { Url } from '../constants'; @@ -33,6 +34,10 @@ export type InAppNotificationAction = | { type: 'navigate-internal'; link: Pick<InternalLinkProps, 'to' | 'onClick' | 'aria-label'>; + } + | { + type: 'navigate-external'; + link: Pick<ExternalLinkProps, 'to' | 'onClick' | 'aria-label'>; }; export type InAppNotificationIndicatorType = 'success' | 'warning' | 'error'; @@ -69,8 +74,12 @@ export interface InAppNotification { indicator?: InAppNotificationIndicatorType; action?: InAppNotificationAction; title: string; - subtitle?: string; - subtitleAction?: InAppNotificationAction; + subtitle?: string | InAppNotificationSubtitle[]; +} + +export interface InAppNotificationSubtitle { + content: string; + action?: InAppNotificationAction; } export interface SystemNotificationProvider extends NotificationProvider { |
