import moment from 'moment'; import * as React from 'react'; import { Component, Types } from 'reactxp'; import { sprintf } from 'sprintf-js'; import { messages } from '../../shared/gettext'; import { NotificationActions, NotificationBanner, NotificationContent, NotificationIndicator, NotificationOpenLinkAction, NotificationSubtitle, NotificationTitle, } from './NotificationBanner'; import AccountExpiry from '../../shared/account-expiry'; import { ErrorStateCause, TunnelParameterError, TunnelState } from '../../shared/daemon-rpc-types'; import { parseAuthFailure } from '../lib/auth-failure'; import { IVersionReduxState } from '../redux/version/reducers'; interface IProps { style?: Types.ViewStyleRuleSet; accountExpiry?: AccountExpiry; tunnelState: TunnelState; version: IVersionReduxState; blockWhenDisconnected: boolean; onOpenDownloadLink: () => Promise; onOpenBuyMoreLink: () => Promise; } type NotificationAreaPresentation = | { type: 'failure-unsecured'; reason: string } | { type: 'blocking'; reason: string } | { type: 'inconsistent-version' } | { type: 'unsupported-version'; upgradeVersion: string } | { type: 'update-available'; upgradeVersion: string } | { type: 'expires-soon'; timeLeft: string }; type State = NotificationAreaPresentation & { visible: boolean; }; function getTunnelParameterMessage(err: TunnelParameterError): string { switch (err) { /// TODO: once bridge constraints can be set, add a more descriptive error message case 'no_matching_bridge_relay': case 'no_matching_relay': return messages.pgettext( 'in-app-notifications', 'No relay server matches the current settings. You can try changing the location or the relay settings.', ); case 'no_wireguard_key': return messages.pgettext( 'in-app-notifications', 'WireGuard key not published to our servers. You can manage your key in Advanced settings.', ); case 'custom_tunnel_host_resultion_error': return messages.pgettext( 'in-app-notifications', 'Failed to resolve host of custom tunnel. Consider changing the settings', ); } } function getErrorCauseMessage(blockReason: ErrorStateCause): string { switch (blockReason.reason) { case 'auth_failed': return parseAuthFailure(blockReason.details).message; case 'ipv6_unavailable': return messages.pgettext( 'in-app-notifications', 'Could not configure IPv6, please enable it on your system or disable it in the app', ); case 'set_firewall_policy_error': { let extraMessage = null; switch (process.platform) { case 'linux': extraMessage = messages.pgettext('in-app-notifications', 'Your kernel may be outdated'); break; case 'win32': extraMessage = messages.pgettext( 'in-app-notifications', 'This might be caused by third party security software', ); break; } return `${messages.pgettext( 'in-app-notifications', 'Failed to apply firewall rules. The device might currently be unsecured', )}${extraMessage ? '. ' + extraMessage : ''}`; } case 'set_dns_error': return messages.pgettext('in-app-notifications', 'Failed to set system DNS server'); case 'start_tunnel_error': return messages.pgettext('in-app-notifications', 'Failed to start tunnel connection'); case 'tunnel_parameter_error': return getTunnelParameterMessage(blockReason.details); case 'is_offline': return messages.pgettext( 'in-app-notifications', 'This device is offline, no tunnels can be established', ); case 'tap_adapter_problem': return messages.pgettext( 'in-app-notifications', "Unable to detect a working TAP adapter on this device. If you've disabled it, enable it again. Otherwise, please reinstall the app", ); } } function capitalizeFirstLetter(inputString: string): string { return inputString.charAt(0).toUpperCase() + inputString.slice(1); } export default class NotificationArea extends Component { public static getDerivedStateFromProps(props: IProps, state: State) { const { accountExpiry, blockWhenDisconnected, tunnelState, version } = props; switch (tunnelState.state) { case 'connecting': return { visible: true, type: 'blocking', reason: '', }; case 'error': if (tunnelState.details.isBlocking) { return { visible: true, type: 'blocking', reason: getErrorCauseMessage(tunnelState.details.cause), }; } else { return { visible: true, type: 'failure-unsecured', reason: getErrorCauseMessage(tunnelState.details.cause), }; } case 'disconnecting': if (tunnelState.details === 'reconnect') { return { visible: true, type: 'blocking', reason: '', }; } // fallthrough case 'disconnected': if (blockWhenDisconnected) { return { visible: true, type: 'blocking', reason: '', }; } // fallthrough default: if (!version.consistent) { return { visible: true, type: 'inconsistent-version', }; } if (!version.currentIsSupported && version.nextUpgrade) { return { visible: true, type: 'unsupported-version', upgradeVersion: version.nextUpgrade, }; } if (version.currentIsOutdated && version.nextUpgrade) { return { visible: true, type: 'update-available', upgradeVersion: version.nextUpgrade, }; } if ( accountExpiry && accountExpiry.willHaveExpiredAt( moment() .add(3, 'days') .toDate(), ) ) { return { visible: true, type: 'expires-soon', timeLeft: capitalizeFirstLetter(accountExpiry.remainingTime()), }; } return { ...state, visible: false, }; } } public state: State = { type: 'blocking', reason: '', visible: false, }; public render() { return ( {this.state.type === 'failure-unsecured' && ( {messages.pgettext('in-app-notifications', 'FAILURE - UNSECURED')} {this.state.reason} )} {this.state.type === 'blocking' && ( {messages.pgettext('in-app-notifications', 'BLOCKING INTERNET')} {this.state.reason} )} {this.state.type === 'inconsistent-version' && ( {messages.pgettext('in-app-notifications', 'INCONSISTENT VERSION')} {messages.pgettext( 'in-app-notifications', 'Inconsistent internal version information, please restart the app', )} )} {this.state.type === 'unsupported-version' && ( {messages.pgettext('in-app-notifications', 'UNSUPPORTED VERSION')} {sprintf( // TRANSLATORS: The in-app banner displayed to the user when the running app becomes unsupported. // TRANSLATORS: Available placeholders: // TRANSLATORS: %(version)s - the newest available version of the app messages.pgettext( 'in-app-notifications', 'You are running an unsupported app version. Please upgrade to %(version)s now to ensure your security', ), { version: this.state.upgradeVersion }, )} )} {this.state.type === 'update-available' && ( {messages.pgettext('in-app-notifications', 'UPDATE AVAILABLE')} {sprintf( // TRANSLATORS: The in-app banner displayed to the user when the app update is available. // TRANSLATORS: Available placeholders: // TRANSLATORS: %(version)s - the newest available version of the app messages.pgettext( 'in-app-notifications', 'Install Mullvad VPN (%(version)s) to stay up to date', ), { version: this.state.upgradeVersion }, )} )} {this.state.type === 'expires-soon' && ( {messages.pgettext('in-app-notifications', 'ACCOUNT CREDIT EXPIRES SOON')} {this.state.timeLeft} )} ); } }