diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2018-09-06 12:42:26 +0300 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2018-09-13 15:40:09 +0300 |
| commit | c2a547f0d07add5acd229954548a2f254801c605 (patch) | |
| tree | e80670b9e610b5d87f7f6012f6d6955bba15ed49 | |
| parent | 7cb1310ef768141ed718f3ee0832b0303e4179c9 (diff) | |
| download | mullvadvpn-c2a547f0d07add5acd229954548a2f254801c605.tar.xz mullvadvpn-c2a547f0d07add5acd229954548a2f254801c605.zip | |
Show banner when connection is blocked
6 files changed, 182 insertions, 110 deletions
diff --git a/gui/packages/desktop/src/renderer/components/BlockingInternetBanner.js b/gui/packages/desktop/src/renderer/components/BlockingInternetBanner.js new file mode 100644 index 0000000000..901d699fc5 --- /dev/null +++ b/gui/packages/desktop/src/renderer/components/BlockingInternetBanner.js @@ -0,0 +1,70 @@ +// @flow + +import * as React from 'react'; +import { View, Text, Component, Styles } from 'reactxp'; +import { colors } from '../../config'; + +const styles = { + container: Styles.createViewStyle({ + flexDirection: 'row', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + backdropFilter: 'blur(4px)', + paddingTop: 8, + paddingLeft: 20, + paddingRight: 20, + paddingBottom: 8, + }), + icon: Styles.createViewStyle({ + width: 10, + height: 10, + flex: 0, + borderRadius: 5, + marginTop: 4, + marginRight: 8, + backgroundColor: colors.red, + }), + textContainer: Styles.createViewStyle({ + flex: 1, + }), + title: Styles.createTextStyle({ + fontFamily: 'Open Sans', + fontSize: 12, + fontWeight: '800', + lineHeight: 17, + color: colors.white60, + }), + subtitle: Styles.createTextStyle({ + fontFamily: 'Open Sans', + fontSize: 12, + fontWeight: '800', + lineHeight: 17, + color: colors.white40, + }), +}; + +export class BannerTitle extends Component { + render() { + return <Text style={styles.title}>{this.props.children}</Text>; + } +} + +export class BannerSubtitle extends Component { + render() { + return React.Children.count(this.props.children) > 0 ? ( + <Text style={styles.subtitle}>{this.props.children}</Text> + ) : null; + } +} + +export default class BlockingInternetBanner extends Component<{ + children: Array<React.Element<typeof BannerTitle> | React.Element<typeof BannerSubtitle>>, +}> { + render() { + return ( + <View style={styles.container}> + <View style={styles.icon} /> + <View style={styles.textContainer}>{this.props.children}</View> + </View> + ); + } +} diff --git a/gui/packages/desktop/src/renderer/components/Connect.js b/gui/packages/desktop/src/renderer/components/Connect.js index 5849d71249..f04f33a492 100644 --- a/gui/packages/desktop/src/renderer/components/Connect.js +++ b/gui/packages/desktop/src/renderer/components/Connect.js @@ -6,12 +6,14 @@ import { Component, Clipboard, Text, View, Types } from 'reactxp'; import { Accordion } from '@mullvad/components'; import { Layout, Container, Header } from './Layout'; import { SettingsBarButton, Brand } from './HeaderBar'; +import BlockingInternetBanner, { BannerTitle, BannerSubtitle } from './BlockingInternetBanner'; import * as AppButton from './AppButton'; import Img from './Img'; import Map from './Map'; import styles from './ConnectStyles'; -import { BlockedError, NoCreditError, NoInternetError } from '../errors'; +import { NoCreditError, NoInternetError } from '../errors'; import WindowStateObserver from '../lib/window-state-observer'; +import type { BlockReason, TunnelState } from '../lib/daemon-rpc'; import type { HeaderBarStyle } from './HeaderBar'; import type { ConnectionReduxState } from '../redux/connection/reducers'; @@ -29,29 +31,50 @@ type Props = { }; type State = { + banner: { + visible: boolean, + title: string, + subtitle: string, + }, showCopyIPMessage: boolean, }; +function getBlockReasonMessage(reason: BlockReason): string { + switch (reason) { + case 'ipv6_unavailable': + return 'Could not configure IPv6, please enable it on your system or disable it in the app'; + case 'set_security_policy_error': + return 'Failed to apply security policy'; + case 'start_tunnel_error': + return 'Failed to start tunnel connection'; + case 'no_matching_relay': + return 'No relay server matches the current settings'; + default: + return `Unknown error: ${(reason: empty)}`; + } +} + export default class Connect extends Component<Props, State> { state = { + banner: { + visible: false, + title: '', + subtitle: '', + }, showCopyIPMessage: false, }; _copyTimer: ?TimeoutID; _windowStateObserver = new WindowStateObserver(); - shouldComponentUpdate(nextProps: Props, nextState: State) { - const { connection: prevConnection, ...otherPrevProps } = this.props; - const { connection: nextConnection, ...otherNextProps } = nextProps; + constructor(props: Props) { + super(); - const prevState = this.state; - - return ( - // shallow compare the connection - !shallowCompare(prevConnection, nextConnection) || - !shallowCompare(otherPrevProps, otherNextProps) || - prevState.showCopyIPMessage !== nextState.showCopyIPMessage - ); + const connection = props.connection; + this.state = { + ...this.state, + banner: this.getBannerState(connection.status, connection.blockReason), + }; } componentDidMount() { @@ -70,6 +93,20 @@ export default class Connect extends Component<Props, State> { this._windowStateObserver.dispose(); } + componentDidUpdate(oldProps: Props, _oldState: State) { + const oldConnection = oldProps.connection; + const newConnection = this.props.connection; + + if ( + oldConnection.status !== newConnection.status || + oldConnection.blockReason !== newConnection.blockReason + ) { + this.setState({ + banner: this.getBannerState(newConnection.status, newConnection.blockReason), + }); + } + } + render() { const error = this.checkForErrors(); const child = error ? this.renderError(error) : this.renderMap(); @@ -85,6 +122,33 @@ export default class Connect extends Component<Props, State> { ); } + getBannerState( + tunnelState: TunnelState, + blockReason: ?BlockReason, + ): $PropertyType<State, 'banner'> { + switch (tunnelState) { + case 'connecting': + return { + visible: true, + title: 'BLOCKING INTERNET', + subtitle: '', + }; + + case 'blocked': + return { + visible: true, + title: 'BLOCKING INTERNET', + subtitle: blockReason ? getBlockReasonMessage(blockReason) : '', + }; + + default: + return { + ...this.state.banner, + visible: false, + }; + } + } + renderError(error: Error) { let title = ''; let message = ''; @@ -99,11 +163,6 @@ export default class Connect extends Component<Props, State> { message = 'Your internet connection will be secured when you get back online'; } - if (error instanceof BlockedError) { - title = 'Blocked'; - message = error.message; - } - return ( <View style={styles.connect}> <View style={styles.status_icon}> @@ -134,7 +193,7 @@ export default class Connect extends Component<Props, State> { center: [longitude, latitude], // do not show the marker when connecting showMarker: status !== 'connecting', - markerStyle: status === 'connected' ? 'secure' : 'unsecure', + markerStyle: status === 'connected' || status === 'blocked' ? 'secure' : 'unsecure', // zoom in when connected zoomLevel: status === 'connected' ? 'low' : 'medium', // a magic offset to align marker with spinner @@ -154,7 +213,13 @@ export default class Connect extends Component<Props, State> { } renderMap() { - let [isConnecting, isConnected, isDisconnected, isDisconnecting] = [false, false, false, false]; + let [isConnecting, isConnected, isDisconnected, isDisconnecting, isBlocked] = [ + false, + false, + false, + false, + false, + ]; switch (this.props.connection.status) { case 'connecting': isConnecting = true; @@ -168,6 +233,9 @@ export default class Connect extends Component<Props, State> { case 'disconnecting': isDisconnecting = true; break; + case 'blocked': + isBlocked = true; + break; } return ( @@ -176,7 +244,15 @@ export default class Connect extends Component<Props, State> { <Map style={{ width: '100%', height: '100%' }} {...this._getMapProps()} /> </View> <View style={styles.container}> - {this._renderIsBlockingInternetMessage()} + <Accordion + style={styles.blocking_container} + height={this.state.banner.visible ? 'auto' : 0} + testName={'blockingAccordion'}> + <BlockingInternetBanner> + <BannerTitle>{this.state.banner.title}</BannerTitle> + <BannerSubtitle>{this.state.banner.subtitle}</BannerSubtitle> + </BlockingInternetBanner> + </Accordion> {/* show spinner when connecting */} {isConnecting ? ( @@ -250,15 +326,15 @@ export default class Connect extends Component<Props, State> { </View> ) : null} - {/* footer when connecting */} - {isConnecting ? ( + {/* footer when connecting or blocked */} + {isConnecting || isBlocked ? ( <View style={styles.footer}> <AppButton.TransparentButton style={styles.switch_location_button} onPress={this.props.onSelectLocation}> {'Switch location'} </AppButton.TransparentButton> - <AppButton.RedTransparentButton onPress={this.props.onDisconnect}> + <AppButton.RedTransparentButton onPress={this.props.onDisconnect} testName="cancel"> {'Cancel'} </AppButton.RedTransparentButton> </View> @@ -290,19 +366,6 @@ export default class Connect extends Component<Props, State> { ); } - _renderIsBlockingInternetMessage() { - return ( - <Accordion - style={styles.blocking_container} - height={this.props.connection.status === 'connecting' ? 'auto' : 0}> - <Text style={styles.blocking_message}> - <Text style={styles.blocking_icon}> </Text> - <Text>BLOCKING INTERNET</Text> - </Text> - </Accordion> - ); - } - // Handlers onExternalLink(type: string) { @@ -341,9 +404,10 @@ export default class Connect extends Component<Props, State> { networkSecurityStyle(): Types.Style { const classes = [styles.status_security]; - if (this.props.connection.status === 'connected') { + const { status } = this.props.connection; + if (status === 'connected' || status === 'blocked') { classes.push(styles.status_security__secure); - } else if (this.props.connection.status === 'disconnected') { + } else if (status === 'disconnected' || status === 'disconnecting') { classes.push(styles.status_security__unsecured); } return classes; @@ -353,6 +417,8 @@ export default class Connect extends Component<Props, State> { switch (this.props.connection.status) { case 'connected': return 'SECURE CONNECTION'; + case 'blocked': + return 'BLOCKED CONNECTION'; case 'connecting': return 'CREATING SECURE CONNECTION'; default: @@ -380,16 +446,6 @@ export default class Connect extends Component<Props, State> { return new NoCreditError(); } - // Tunnel is blocked due to an error? - if (this.props.connection.status === 'blocked') { - return new BlockedError(this.props.connection.blockReason); - } - return null; } } - -function shallowCompare(lhs: Object, rhs: Object) { - const keys = Object.keys(lhs); - return keys.length === Object.keys(rhs).length && keys.every((key) => lhs[key] === rhs[key]); -} diff --git a/gui/packages/desktop/src/renderer/components/ConnectStyles.js b/gui/packages/desktop/src/renderer/components/ConnectStyles.js index 5417a64176..64633975fd 100644 --- a/gui/packages/desktop/src/renderer/components/ConnectStyles.js +++ b/gui/packages/desktop/src/renderer/components/ConnectStyles.js @@ -29,20 +29,6 @@ export default { paddingLeft: 24, paddingRight: 24, }), - blocking_container: Styles.createViewStyle({ - width: '100%', - position: 'absolute', - }), - blocking_icon: Styles.createViewStyle({ - width: 10, - height: 10, - flex: 0, - display: 'flex', - borderRadius: 5, - marginTop: 4, - marginRight: 8, - backgroundColor: colors.red, - }), status: Styles.createViewStyle({ paddingTop: 0, paddingLeft: 24, @@ -61,20 +47,9 @@ export default { switch_location_button: Styles.createViewStyle({ marginBottom: 16, }), - - blocking_message: Styles.createTextStyle({ - display: 'flex', - flexDirection: 'row', - fontFamily: 'Open Sans', - fontSize: 12, - fontWeight: '800', - lineHeight: 17, - paddingTop: 8, - paddingLeft: 20, - paddingRight: 20, - paddingBottom: 8, - color: colors.white60, - backgroundColor: colors.blue, + blocking_container: Styles.createViewStyle({ + width: '100%', + position: 'absolute', }), server_label: Styles.createTextStyle({ fontFamily: 'DINPro', diff --git a/gui/packages/desktop/src/renderer/errors.js b/gui/packages/desktop/src/renderer/errors.js index a6f103357a..67f87501dd 100644 --- a/gui/packages/desktop/src/renderer/errors.js +++ b/gui/packages/desktop/src/renderer/errors.js @@ -1,28 +1,5 @@ // @flow -import type { BlockReason } from './lib/daemon-rpc'; - -export class BlockedError extends Error { - constructor(reason: BlockReason) { - const message = (function() { - switch (reason) { - case 'ipv6_unavailable': - return 'Could not configure IPv6, please enable it on your system or disable it in the app'; - case 'set_security_policy_error': - return 'Failed to apply security policy'; - case 'start_tunnel_error': - return 'Failed to start tunnel connection'; - case 'no_matching_relay': - return 'No relay server matches the current settings'; - default: - return `Unknown error: ${(reason: empty)}`; - } - })(); - - super(message); - } -} - export class NoCreditError extends Error { constructor() { super("Account doesn't have enough credit available for connection"); @@ -47,12 +24,6 @@ export class InvalidAccountError extends Error { } } -export class NoAccountError extends Error { - constructor() { - super('No account was set'); - } -} - export class CommunicationError extends Error { constructor() { super('api.mullvad.net is blocked, please check your firewall'); diff --git a/gui/packages/desktop/src/renderer/redux/connection/actions.js b/gui/packages/desktop/src/renderer/redux/connection/actions.js index fb920673e0..6a54a8926b 100644 --- a/gui/packages/desktop/src/renderer/redux/connection/actions.js +++ b/gui/packages/desktop/src/renderer/redux/connection/actions.js @@ -1,6 +1,6 @@ // @flow -import type { Ip } from '../../lib/daemon-rpc'; +import type { BlockReason, Ip } from '../../lib/daemon-rpc'; type ConnectingAction = { type: 'CONNECTING', @@ -20,7 +20,7 @@ type DisconnectingAction = { type BlockedAction = { type: 'BLOCKED', - reason: string, + reason: BlockReason, }; type NewLocationAction = { @@ -77,7 +77,7 @@ function disconnecting(): DisconnectingAction { }; } -function blocked(reason: string): BlockedAction { +function blocked(reason: BlockReason): BlockedAction { return { type: 'BLOCKED', reason, diff --git a/gui/packages/desktop/src/renderer/redux/connection/reducers.js b/gui/packages/desktop/src/renderer/redux/connection/reducers.js index 9381d5e037..82374dbb2c 100644 --- a/gui/packages/desktop/src/renderer/redux/connection/reducers.js +++ b/gui/packages/desktop/src/renderer/redux/connection/reducers.js @@ -1,7 +1,7 @@ // @flow import type { ReduxAction } from '../store'; -import type { TunnelState, Ip } from '../../lib/daemon-rpc'; +import type { BlockReason, TunnelState, Ip } from '../../lib/daemon-rpc'; export type ConnectionReduxState = { status: TunnelState, @@ -11,7 +11,7 @@ export type ConnectionReduxState = { longitude: ?number, country: ?string, city: ?string, - blockReason: ?string, + blockReason: ?BlockReason, }; const initialState: ConnectionReduxState = { |
