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.', )}
    ); }