summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMarkus Pettersson <markus.pettersson@mullvad.net>2025-03-19 15:14:41 +0100
committerMarkus Pettersson <markus.pettersson@mullvad.net>2025-03-19 15:14:41 +0100
commit01102af2d21cb2eadc36a422359a4eca1ed5fc17 (patch)
treec43467f3e3976939e4a7192f43e5a53b00281918
parent1cb64dd714e0bfca67f90cce6c7d80633e60e417 (diff)
parent306a5a8990f5de9b83bb0c78d5d04e3863263b53 (diff)
downloadmullvadvpn-01102af2d21cb2eadc36a422359a4eca1ed5fc17.tar.xz
mullvadvpn-01102af2d21cb2eadc36a422359a4eca1ed5fc17.zip
Merge branch 'implement-openvpn-warnings-des-1583'
-rw-r--r--desktop/packages/mullvad-vpn/locales/messages.pot82
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx35
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/NotificationBanner.tsx12
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/NotificationSubtitle.tsx71
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/VpnSettings.tsx36
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/notifications/index.ts2
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/notifications/new-version.ts26
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/notifications/no-open-vpn-server-available.ts122
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/notifications/open-vpn-support-ending.ts65
-rw-r--r--desktop/packages/mullvad-vpn/src/shared/constants/urls.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/shared/notifications/notification.ts13
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 {