import { AnimatePresence } from 'motion/react';
import { useCallback, useState } from 'react';
import { messages } from '../../shared/gettext';
import log from '../../shared/logging';
import {
CloseToAccountExpiryNotificationProvider,
ConnectingNotificationProvider,
ErrorNotificationProvider,
InAppNotificationAction,
InAppNotificationProvider,
InconsistentVersionNotificationProvider,
LockdownModeNotificationProvider,
ReconnectingNotificationProvider,
UnsupportedVersionNotificationProvider,
} from '../../shared/notifications';
import { RoutePath } from '../../shared/routes';
import { useAppContext } from '../context';
import {
useAppUpgradeDownloadProgressValue,
useAppUpgradeEventType,
useHasAppUpgradeError,
} from '../hooks';
import useActions from '../lib/actionsHook';
import { Button } from '../lib/components';
import { TransitionType, useHistory } from '../lib/history';
import {
AppUpgradeErrorNotificationProvider,
AppUpgradeProgressNotificationProvider,
AppUpgradeReadyNotificationProvider,
NewDeviceNotificationProvider,
NewVersionNotificationProvider,
NoOpenVpnServerAvailableNotificationProvider,
OpenVpnSupportEndingNotificationProvider,
UnsupportedWireGuardPortNotificationProvider,
} from '../lib/notifications';
import { AppUpgradeAvailableNotificationProvider } from '../lib/notifications/app-upgrade-available';
import { useTunnelProtocol } from '../lib/relay-settings-hooks';
import { useMounted } from '../lib/utility-hooks';
import accountActions from '../redux/account/actions';
import { convertEventTypeToStep } from '../redux/app-upgrade/helpers';
import { useAppUpgradeError, useVersionSuggestedUpgrade } from '../redux/hooks';
import { IReduxState, useSelector } from '../redux/store';
import { ModalAlert, ModalAlertType, ModalMessage, ModalMessageList } from './Modal';
import {
NotificationActions,
NotificationBanner,
NotificationCloseAction,
NotificationContent,
NotificationIndicator,
NotificationOpenLinkAction,
NotificationTitle,
NotificationTroubleshootDialogAction,
} from './NotificationBanner';
import { NotificationSubtitle } from './NotificationSubtitle';
interface IProps {
className?: string;
}
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);
const connection = useSelector((state: IReduxState) => state.connection);
const version = useSelector((state: IReduxState) => state.version);
const tunnelProtocol = useTunnelProtocol();
const fullRelayList = useSelector((state) => state.settings.relayLocations);
const allowedPortRanges = useSelector((state) => state.settings.wireguardEndpointData.portRanges);
const relaySettings = useSelector((state) => state.settings.relaySettings);
const lockdownModeSetting = useSelector((state: IReduxState) => state.settings.lockdownMode);
const hasExcludedApps = useSelector(
(state: IReduxState) =>
state.settings.splitTunneling && state.settings.splitTunnelingApplications.length > 0,
);
const { hideNewDeviceBanner } = useActions(accountActions);
const { setDisplayedChangelog, setDismissedUpgrade, appUpgrade, appUpgradeInstallerStart } =
useAppContext();
const currentVersion = useSelector((state) => state.version.current);
const displayedForVersion = useSelector(
(state) => state.settings.guiSettings.changelogDisplayedForVersion,
);
const changelog = useSelector((state) => state.userInterface.changelog);
const close = useCallback(() => {
setDisplayedChangelog();
}, [setDisplayedChangelog]);
const [isModalOpen, setIsModalOpen] = useState(false);
const { setSplitTunnelingState } = useAppContext();
const disableSplitTunneling = useCallback(async () => {
setIsModalOpen(false);
await setSplitTunnelingState(false);
}, [setSplitTunnelingState]);
const updateDismissedForVersion = useSelector(
(state) => state.settings.guiSettings.updateDismissedForVersion,
);
const hasAppUpgradeError = useHasAppUpgradeError();
const { error } = useAppUpgradeError();
const restartAppUpgrade = useCallback(() => {
appUpgrade();
}, [appUpgrade]);
const restartAppUpgradeInstaller = useCallback(() => {
appUpgradeInstallerStart();
}, [appUpgradeInstallerStart]);
const { suggestedUpgrade } = useVersionSuggestedUpgrade();
const appUpgradeDownloadProgressValue = useAppUpgradeDownloadProgressValue();
const appUpgradeEventType = useAppUpgradeEventType();
const appUpgradeStep = convertEventTypeToStep(appUpgradeEventType);
const notificationProviders: InAppNotificationProvider[] = [
new ConnectingNotificationProvider({ tunnelState }),
new ReconnectingNotificationProvider(tunnelState),
new LockdownModeNotificationProvider({
tunnelState,
lockdownModeSetting,
hasExcludedApps,
}),
new AppUpgradeErrorNotificationProvider({
hasAppUpgradeError,
appUpgradeError: error,
restartAppUpgrade,
restartAppUpgradeInstaller,
}),
new AppUpgradeReadyNotificationProvider({
appUpgradeEventType,
suggestedUpgradeVersion: suggestedUpgrade?.version,
}),
new AppUpgradeProgressNotificationProvider({
appUpgradeStep,
appUpgradeEventType,
appUpgradeDownloadProgressValue,
}),
new NoOpenVpnServerAvailableNotificationProvider({
connection,
tunnelProtocol,
relayLocations: fullRelayList,
}),
new UnsupportedWireGuardPortNotificationProvider({
connection,
relaySettings,
tunnelProtocol,
allowedPortRanges,
}),
new ErrorNotificationProvider({
tunnelState,
hasExcludedApps,
showFullDiskAccessSettings,
disableSplitTunneling,
}),
new InconsistentVersionNotificationProvider({ consistent: version.consistent }),
new UnsupportedVersionNotificationProvider(version),
];
if (account.expiry) {
notificationProviders.push(
new CloseToAccountExpiryNotificationProvider({ accountExpiry: account.expiry, locale }),
);
}
notificationProviders.push(
new NewDeviceNotificationProvider({
shouldDisplay: account.status.type === 'ok' && account.status.newDeviceBanner,
deviceName: account.deviceName ?? '',
close: hideNewDeviceBanner,
}),
new NewVersionNotificationProvider({
currentVersion,
displayedForVersion,
changelog,
close,
}),
new AppUpgradeAvailableNotificationProvider({
platform: window.env.platform,
suggestedUpgradeVersion: suggestedUpgrade?.version,
suggestedIsBeta: version.suggestedIsBeta,
updateDismissedForVersion,
close: setDismissedUpgrade,
}),
new OpenVpnSupportEndingNotificationProvider({ tunnelProtocol }),
);
const notificationProvider = notificationProviders.find((notification) =>
notification.mayDisplay(),
);
const notification = notificationProvider?.getInAppNotification();
if (notificationProvider) {
if (!notification) {
log.error(
`Notification providers mayDisplay() returned true but getInAppNotification() returned undefined for ${notificationProvider.constructor.name}`,
);
}
}
// We only want to animate notifications after first mount,
// so as to prevent an animation from animating in when the
// app has just started.
const mounted = useMounted();
const isMounted = mounted();
return (
{notification && (
{notification.title}
{notification.action && (
)}
)}
);
}
interface NotificationActionWrapperProps {
action: InAppNotificationAction;
isModalOpen: boolean;
setIsModalOpen: (isOpen: boolean) => void;
}
function NotificationActionWrapper({
action,
isModalOpen,
setIsModalOpen,
}: NotificationActionWrapperProps) {
const { push } = useHistory();
const { openUrlWithAuth, openUrl } = useAppContext();
const closeTroubleshootModal = useCallback(() => setIsModalOpen(false), [setIsModalOpen]);
const handleClick = useCallback(() => {
if (action) {
switch (action.type) {
case 'navigate-external':
if (action.link.withAuth) {
return openUrlWithAuth(action.link.to);
} else {
return openUrl(action.link.to);
}
case 'troubleshoot-dialog':
setIsModalOpen(true);
break;
case 'close':
action.close();
break;
}
}
return Promise.resolve();
}, [action, setIsModalOpen, openUrlWithAuth, openUrl]);
const goToProblemReport = useCallback(() => {
closeTroubleshootModal();
push(RoutePath.problemReport, { transition: TransitionType.show });
}, [closeTroubleshootModal, push]);
let actionComponent: React.ReactElement | undefined;
if (action) {
switch (action.type) {
case 'navigate-external':
actionComponent = ;
break;
case 'troubleshoot-dialog':
actionComponent = (
<>
>
);
break;
case 'close':
actionComponent = ;
}
}
if (action.type !== 'troubleshoot-dialog') {
return {actionComponent};
}
const problemReportButton = action.troubleshoot?.buttons ? (
) : (
);
let buttons = [
problemReportButton,
,
];
if (action.troubleshoot?.buttons) {
const actionButtons = action.troubleshoot.buttons.map(({ variant, label, action }) => (
));
buttons = actionButtons.concat(buttons);
}
return (
<>
{actionComponent}
{action.troubleshoot?.details}
{action.troubleshoot?.steps.map((step) => {step})}
{messages.pgettext(
'troubleshoot',
'If these steps do not work please send a problem report.',
)}
>
);
}