summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--gui/packages/components/package.json3
-rw-r--r--gui/packages/components/src/ClipboardLabel.tsx51
-rw-r--r--gui/packages/components/src/SecuredLabel.tsx62
-rw-r--r--gui/packages/components/src/index.ts2
-rw-r--r--gui/packages/components/tsconfig.json3
-rw-r--r--gui/packages/desktop/src/renderer/components/Account.js35
-rw-r--r--gui/packages/desktop/src/renderer/components/Connect.js501
-rw-r--r--gui/packages/desktop/src/renderer/components/ConnectStyles.js42
-rw-r--r--gui/packages/desktop/test/components/Connect.spec.js239
-rw-r--r--gui/yarn.lock10
10 files changed, 437 insertions, 511 deletions
diff --git a/gui/packages/components/package.json b/gui/packages/components/package.json
index d1f78a7fb0..c737428e43 100644
--- a/gui/packages/components/package.json
+++ b/gui/packages/components/package.json
@@ -18,6 +18,7 @@
"@types/enzyme-adapter-react-16": "^1.0.3",
"@types/jsdom": "^11.0.6",
"@types/mocha": "^5.2.5",
+ "@types/node": "^10.10.1",
"@types/react": "^16.4.11",
"chai": "^4.1.2",
"enzyme": "^3.3.0",
@@ -32,7 +33,7 @@
"ts-node": "^7.0.1",
"tslint": "^5.11.0",
"tslint-config-prettier": "^1.15.0",
- "typescript": "^3.0.1"
+ "typescript": "^3.0.3"
},
"dependencies": {},
"peerDependencies": {
diff --git a/gui/packages/components/src/ClipboardLabel.tsx b/gui/packages/components/src/ClipboardLabel.tsx
new file mode 100644
index 0000000000..9cabe96ffd
--- /dev/null
+++ b/gui/packages/components/src/ClipboardLabel.tsx
@@ -0,0 +1,51 @@
+import * as React from 'react';
+import { Clipboard, Component, Text, Types } from 'reactxp';
+
+interface IProps {
+ value: string;
+ delay: number;
+ message: string;
+ style?: Types.TextStyleRuleSet;
+}
+
+interface IState {
+ showsMessage: boolean;
+}
+
+export default class ClipboardLabel extends Component<IProps, IState> {
+ public static defaultProps: Partial<IProps> = {
+ delay: 3000,
+ message: 'Copied!',
+ };
+
+ public state: IState = {
+ showsMessage: false,
+ };
+
+ private timer: NodeJS.Timer | null = null;
+
+ public componentWillUnmount() {
+ if (this.timer) {
+ clearTimeout(this.timer);
+ }
+ }
+
+ public render() {
+ return (
+ <Text style={this.props.style} onPress={this.handlePress}>
+ {this.state.showsMessage ? this.props.message : this.props.value}
+ </Text>
+ );
+ }
+
+ private handlePress = () => {
+ if (this.timer) {
+ clearTimeout(this.timer);
+ }
+
+ this.timer = setTimeout(() => this.setState({ showsMessage: false }), this.props.delay);
+ this.setState({ showsMessage: true });
+
+ Clipboard.setText(this.props.value);
+ };
+}
diff --git a/gui/packages/components/src/SecuredLabel.tsx b/gui/packages/components/src/SecuredLabel.tsx
new file mode 100644
index 0000000000..9bf9ce0cdc
--- /dev/null
+++ b/gui/packages/components/src/SecuredLabel.tsx
@@ -0,0 +1,62 @@
+import * as React from 'react';
+import { Clipboard, Component, Styles, Text, Types } from 'reactxp';
+
+export enum SecuredDisplayStyle {
+ secured,
+ blocked,
+ securing,
+ unsecured,
+}
+
+interface IProps {
+ displayStyle: SecuredDisplayStyle;
+ style: Types.TextStyleRuleSet;
+}
+
+const styles = {
+ securing: Styles.createTextStyle({
+ color: 'rgb(255, 255, 255)', // white
+ }),
+ secured: Styles.createTextStyle({
+ color: 'rgb(68, 173, 77)', // green
+ }),
+ unsecured: Styles.createTextStyle({
+ color: 'rgb(208, 2, 27)', // red
+ }),
+};
+
+export default class SecuredLabel extends Component<IProps> {
+ public render() {
+ return <Text style={[this.props.style, this.getTextStyle()]}>{this.getText()}</Text>;
+ }
+
+ private getText() {
+ switch (this.props.displayStyle) {
+ case SecuredDisplayStyle.secured:
+ return 'SECURE CONNECTION';
+
+ case SecuredDisplayStyle.blocked:
+ return 'BLOCKED CONNECTION';
+
+ case SecuredDisplayStyle.securing:
+ return 'CREATING SECURE CONNECTION';
+
+ case SecuredDisplayStyle.unsecured:
+ return 'UNSECURED CONNECTION';
+ }
+ }
+
+ private getTextStyle() {
+ switch (this.props.displayStyle) {
+ case SecuredDisplayStyle.secured:
+ case SecuredDisplayStyle.blocked:
+ return styles.secured;
+
+ case SecuredDisplayStyle.securing:
+ return styles.securing;
+
+ case SecuredDisplayStyle.unsecured:
+ return styles.unsecured;
+ }
+ }
+}
diff --git a/gui/packages/components/src/index.ts b/gui/packages/components/src/index.ts
index 2a416a4f1d..7d5ad156f0 100644
--- a/gui/packages/components/src/index.ts
+++ b/gui/packages/components/src/index.ts
@@ -1 +1,3 @@
export { default as Accordion } from './Accordion';
+export { default as ClipboardLabel } from './ClipboardLabel';
+export { default as SecuredLabel, SecuredDisplayStyle } from './SecuredLabel';
diff --git a/gui/packages/components/tsconfig.json b/gui/packages/components/tsconfig.json
index 9ed6e8e8d9..59dcc25146 100644
--- a/gui/packages/components/tsconfig.json
+++ b/gui/packages/components/tsconfig.json
@@ -5,7 +5,8 @@
"module": "commonjs",
"jsx": "react",
"strict": true,
- "skipLibCheck": true
+ "skipLibCheck": true,
+ "types": ["node"]
},
"include": [
"./src/*.ts",
diff --git a/gui/packages/desktop/src/renderer/components/Account.js b/gui/packages/desktop/src/renderer/components/Account.js
index bdfaec893e..319bbe3c58 100644
--- a/gui/packages/desktop/src/renderer/components/Account.js
+++ b/gui/packages/desktop/src/renderer/components/Account.js
@@ -2,7 +2,8 @@
import moment from 'moment';
import * as React from 'react';
-import { Component, Clipboard, Text, View } from 'reactxp';
+import { Component, Text, View } from 'reactxp';
+import { ClipboardLabel } from '@mullvad/components';
import * as AppButton from './AppButton';
import { Layout, Container } from './Layout';
import NavigationBar, { BackBarItem } from './NavigationBar';
@@ -21,15 +22,7 @@ type Props = {
onBuyMore: () => void,
};
-type State = {
- showAccountTokenCopiedMessage: boolean,
-};
-
-export default class Account extends Component<Props, State> {
- state = {
- showAccountTokenCopiedMessage: false,
- };
-
+export default class Account extends Component<Props> {
_copyTimer: ?TimeoutID;
componentWillUnmount() {
@@ -38,18 +31,6 @@ export default class Account extends Component<Props, State> {
}
}
- onAccountTokenClick() {
- if (this._copyTimer) {
- clearTimeout(this._copyTimer);
- }
- this._copyTimer = setTimeout(
- () => this.setState({ showAccountTokenCopiedMessage: false }),
- 3000,
- );
- this.setState({ showAccountTokenCopiedMessage: true });
- Clipboard.setText(this.props.accountToken);
- }
-
render() {
return (
<Layout>
@@ -68,13 +49,11 @@ export default class Account extends Component<Props, State> {
<View style={styles.account__main}>
<View style={styles.account__row}>
<Text style={styles.account__row_label}>Account ID</Text>
- <Text
+ <ClipboardLabel
style={styles.account__row_value}
- onPress={this.onAccountTokenClick.bind(this)}>
- {this.state.showAccountTokenCopiedMessage
- ? 'COPIED TO CLIPBOARD!'
- : this.props.accountToken}
- </Text>
+ value={this.props.accountToken}
+ message={'COPIED TO CLIPBOARD!'}
+ />
</View>
<View style={styles.account__row}>
diff --git a/gui/packages/desktop/src/renderer/components/Connect.js b/gui/packages/desktop/src/renderer/components/Connect.js
index ef5fb2e3a7..29d7ccc8e1 100644
--- a/gui/packages/desktop/src/renderer/components/Connect.js
+++ b/gui/packages/desktop/src/renderer/components/Connect.js
@@ -2,8 +2,8 @@
import moment from 'moment';
import * as React from 'react';
-import { Component, Clipboard, Text, View, Types } from 'reactxp';
-import { Accordion } from '@mullvad/components';
+import { Component, Text, View } from 'reactxp';
+import { Accordion, ClipboardLabel, SecuredLabel, SecuredDisplayStyle } from '@mullvad/components';
import { Layout, Container, Header } from './Layout';
import { SettingsBarButton, Brand } from './HeaderBar';
import BlockingInternetBanner, { BannerTitle, BannerSubtitle } from './BlockingInternetBanner';
@@ -12,7 +12,7 @@ import Img from './Img';
import Map from './Map';
import styles from './ConnectStyles';
import { NoCreditError, NoInternetError } from '../errors';
-import type { BlockReason, TunnelStateTransition } from '../lib/daemon-rpc';
+import type { BlockReason, TunnelState, TunnelStateTransition } from '../lib/daemon-rpc';
import type { HeaderBarStyle } from './HeaderBar';
import type { ConnectionReduxState } from '../redux/connection/reducers';
@@ -28,15 +28,6 @@ type Props = {
onExternalLink: (type: string) => void,
};
-type State = {
- banner: {
- visible: boolean,
- title: string,
- subtitle: string,
- },
- showCopyIPMessage: boolean,
-};
-
function getBlockReasonMessage(blockReason: BlockReason): string {
switch (blockReason.reason) {
case 'auth_failed': {
@@ -58,48 +49,7 @@ function getBlockReasonMessage(blockReason: BlockReason): string {
}
}
-export default class Connect extends Component<Props, State> {
- state = {
- banner: {
- visible: false,
- title: '',
- subtitle: '',
- },
- showCopyIPMessage: false,
- };
-
- _copyTimer: ?TimeoutID;
-
- constructor(props: Props) {
- super();
-
- const connection = props.connection;
- this.state = {
- ...this.state,
- banner: this.getBannerState(connection.status),
- };
- }
-
- componentWillUnmount() {
- if (this._copyTimer) {
- clearTimeout(this._copyTimer);
- }
- }
-
- componentDidUpdate(oldProps: Props, _oldState: State) {
- const oldConnection = oldProps.connection;
- const newConnection = this.props.connection;
-
- if (
- oldConnection.status.state !== newConnection.status.state ||
- oldConnection.status.details !== newConnection.status.details
- ) {
- this.setState({
- banner: this.getBannerState(newConnection.status),
- });
- }
- }
-
+export default class Connect extends Component<Props> {
render() {
const error = this.checkForErrors();
const child = error ? this.renderError(error) : this.renderMap();
@@ -115,30 +65,6 @@ export default class Connect extends Component<Props, State> {
);
}
- getBannerState(tunnelState: TunnelStateTransition): $PropertyType<State, 'banner'> {
- switch (tunnelState.state) {
- case 'connecting':
- return {
- visible: true,
- title: 'BLOCKING INTERNET',
- subtitle: '',
- };
-
- case 'blocked':
- return {
- visible: true,
- title: 'BLOCKING INTERNET',
- subtitle: getBlockReasonMessage(tunnelState.details),
- };
-
- default:
- return {
- ...this.state.banner,
- visible: false,
- };
- }
- }
-
renderError(error: Error) {
let title = '';
let message = '';
@@ -158,7 +84,7 @@ export default class Connect extends Component<Props, State> {
<View style={styles.status_icon}>
<Img source="icon-fail" height={60} width={60} alt="" />
</View>
- <View style={styles.status}>
+ <View style={styles.body}>
<View style={styles.error_title}>{title}</View>
<View style={styles.error_message}>{message}</View>
{error instanceof NoCreditError ? (
@@ -221,7 +147,7 @@ export default class Connect extends Component<Props, State> {
case 'nothing':
return 'unsecure';
default:
- throw new Error(`Invalid action after disconnection: $(status.details: empty)}`);
+ throw new Error(`Invalid action after disconnection: ${(status.details: empty)}`);
}
default:
throw new Error(`Invalid connection status: ${(status.state: empty)}`);
@@ -229,154 +155,32 @@ export default class Connect extends Component<Props, State> {
}
renderMap() {
- let [isConnecting, isConnected, isDisconnected, isDisconnecting, isBlocked] = [
- false,
- false,
- false,
- false,
- false,
- ];
- switch (this.props.connection.status.state) {
- case 'connecting':
- isConnecting = true;
- break;
- case 'connected':
- isConnected = true;
- break;
- case 'disconnected':
- isDisconnected = true;
- break;
- case 'disconnecting':
- isDisconnecting = true;
- break;
- case 'blocked':
- isBlocked = true;
- break;
- }
-
return (
<View style={styles.connect}>
<View style={styles.map}>
<Map style={{ width: '100%', height: '100%' }} {...this._getMapProps()} />
</View>
<View style={styles.container}>
- <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>
+ <TunnelBanner tunnelState={this.props.connection.status} />
{/* show spinner when connecting */}
- {isConnecting ? (
+ {this.props.connection.status.state === 'connecting' ? (
<View style={styles.status_icon}>
<Img source="icon-spinner" height={60} width={60} alt="" />
</View>
) : null}
- <View style={styles.status}>
- <View style={this.networkSecurityStyle()} testName="networkSecurityMessage">
- {this.networkSecurityMessage()}
- </View>
-
- {/*
- **********************************
- Begin: Location block
- **********************************
- */}
-
- {/* location when connecting, disconnecting or disconnected */}
- {isConnecting || isDisconnecting || isDisconnected ? (
- <Text style={styles.status_location} testName="location">
- {this.props.connection.country}
- </Text>
- ) : null}
-
- {/* location when connected */}
- {isConnected ? (
- <Text style={styles.status_location} testName="location">
- {this.props.connection.city}
- {this.props.connection.city && <br />}
- {this.props.connection.country}
- </Text>
- ) : null}
-
- {/*
- **********************************
- End: Location block
- **********************************
- */}
-
- <Text style={this.ipAddressStyle()} onPress={this.onIPAddressClick.bind(this)}>
- {isConnected || isDisconnecting || isDisconnected ? (
- <Text testName="ipAddress">
- {this.state.showCopyIPMessage
- ? 'IP copied to clipboard!'
- : this.props.connection.ip}
- </Text>
- ) : null}
- </Text>
- </View>
-
- {/*
- **********************************
- Begin: Footer block
- **********************************
- */}
-
- {/* footer when disconnecting or disconnected */}
- {isDisconnecting || isDisconnected ? (
- <View style={styles.footer}>
- <AppButton.TransparentButton
- style={styles.switch_location_button}
- onPress={this.props.onSelectLocation}>
- <AppButton.Label>{this.props.selectedRelayName}</AppButton.Label>
- <Img height={12} width={7} source="icon-chevron" />
- </AppButton.TransparentButton>
- <AppButton.GreenButton onPress={this.props.onConnect} testName="secureConnection">
- {'Secure my connection'}
- </AppButton.GreenButton>
- </View>
- ) : null}
-
- {/* 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} testName="cancel">
- {'Cancel'}
- </AppButton.RedTransparentButton>
- </View>
- ) : null}
-
- {/* footer when connected */}
- {isConnected ? (
- <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}
- testName="disconnect">
- {'Disconnect'}
- </AppButton.RedTransparentButton>
- </View>
- ) : null}
-
- {/*
- **********************************
- End: Footer block
- **********************************
- */}
+ <TunnelControl
+ style={styles.tunnel_control}
+ tunnelState={this.props.connection.status.state}
+ selectedRelayName={this.props.selectedRelayName}
+ city={this.props.connection.city}
+ country={this.props.connection.country}
+ ip={this.props.connection.ip}
+ onConnect={this.props.onConnect}
+ onDisconnect={this.props.onDisconnect}
+ onSelectLocation={this.props.onSelectLocation}
+ />
</View>
</View>
);
@@ -388,19 +192,6 @@ export default class Connect extends Component<Props, State> {
this.props.onExternalLink(type);
}
- onIPAddressClick() {
- if (this._copyTimer) {
- clearTimeout(this._copyTimer);
- }
- this._copyTimer = setTimeout(() => this.setState({ showCopyIPMessage: false }), 3000);
- this.setState({ showCopyIPMessage: true });
-
- const { ip } = this.props.connection;
- if (ip) {
- Clipboard.setText(ip);
- }
- }
-
// Private
headerBarStyle(): HeaderBarStyle {
@@ -427,38 +218,6 @@ export default class Connect extends Component<Props, State> {
}
}
- networkSecurityStyle(): Types.Style {
- const classes = [styles.status_security];
- const { state } = this.props.connection.status;
- if (state === 'connected' || state === 'blocked') {
- classes.push(styles.status_security__secure);
- } else if (state === 'disconnected' || state === 'disconnecting') {
- classes.push(styles.status_security__unsecured);
- }
- return classes;
- }
-
- networkSecurityMessage(): string {
- switch (this.props.connection.status.state) {
- case 'connected':
- return 'SECURE CONNECTION';
- case 'blocked':
- return 'BLOCKED CONNECTION';
- case 'connecting':
- return 'CREATING SECURE CONNECTION';
- default:
- return 'UNSECURED CONNECTION';
- }
- }
-
- ipAddressStyle(): Types.Style {
- var classes = [styles.status_ipaddress];
- if (this.props.connection.status.state === 'connecting') {
- classes.push(styles.status_ipaddress__invisible);
- }
- return classes;
- }
-
checkForErrors(): ?Error {
// Offline?
if (!this.props.connection.isOnline) {
@@ -474,3 +233,225 @@ export default class Connect extends Component<Props, State> {
return null;
}
}
+
+type TunnelBannerProps = {
+ tunnelState: TunnelStateTransition,
+};
+
+type TunnerBannerState = {
+ visible: boolean,
+ title: string,
+ subtitle: string,
+};
+
+export class TunnelBanner extends Component<TunnelBannerProps, TunnerBannerState> {
+ state = {
+ visible: false,
+ title: '',
+ subtitle: '',
+ };
+
+ constructor(props) {
+ super();
+ this.state = this._deriveState(props.tunnelState);
+ }
+
+ componentDidUpdate(oldProps, _oldState) {
+ if (
+ oldProps.tunnelState.state !== this.props.tunnelState.state ||
+ oldProps.tunnelState.details !== this.props.tunnelState.details
+ ) {
+ const nextState = this._deriveState(this.props.tunnelState);
+ this.setState(nextState);
+ }
+ }
+
+ render() {
+ return (
+ <Accordion style={styles.blocking_container} height={this.state.visible ? 'auto' : 0}>
+ <BlockingInternetBanner>
+ <BannerTitle>{this.state.title}</BannerTitle>
+ <BannerSubtitle>{this.state.subtitle}</BannerSubtitle>
+ </BlockingInternetBanner>
+ </Accordion>
+ );
+ }
+
+ _deriveState(tunnelState: TunnelStateTransition) {
+ switch (tunnelState.state) {
+ case 'connecting':
+ return {
+ visible: true,
+ title: 'BLOCKING INTERNET',
+ subtitle: '',
+ };
+
+ case 'blocked':
+ return {
+ visible: true,
+ title: 'BLOCKING INTERNET',
+ subtitle: getBlockReasonMessage(tunnelState.details),
+ };
+
+ default:
+ return {
+ ...this.state,
+ visible: false,
+ };
+ }
+ }
+}
+
+type TunnelControlProps = {
+ tunnelState: TunnelState,
+ selectedRelayName: string,
+ city: ?string,
+ country: ?string,
+ ip: ?string,
+ onConnect: () => void,
+ onDisconnect: () => void,
+ onSelectLocation: () => void,
+};
+
+export function TunnelControl(props: TunnelControlProps) {
+ const Location = ({ children }) => <View style={styles.status_location}>{children}</View>;
+ const City = () => <Text style={styles.status_location_text}>{props.city}</Text>;
+ const Country = () => <Text style={styles.status_location_text}>{props.country}</Text>;
+ const Ip = () => (
+ <ClipboardLabel
+ style={styles.status_ipaddress}
+ value={props.ip || ''}
+ message={'IP copied to clipboard!'}
+ />
+ );
+
+ const SwitchLocation = () => {
+ return (
+ <AppButton.TransparentButton
+ style={styles.switch_location_button}
+ onPress={props.onSelectLocation}>
+ <AppButton.Label>{'Switch location'}</AppButton.Label>
+ </AppButton.TransparentButton>
+ );
+ };
+
+ const SelectedLocation = () => (
+ <AppButton.TransparentButton
+ style={styles.switch_location_button}
+ onPress={props.onSelectLocation}
+ testName={'SelectedLocation'}>
+ <AppButton.Label>{props.selectedRelayName}</AppButton.Label>
+ <Img height={12} width={7} source="icon-chevron" />
+ </AppButton.TransparentButton>
+ );
+
+ const Connect = () => (
+ <AppButton.GreenButton onPress={props.onConnect} testName="secureConnection">
+ {'Secure my connection'}
+ </AppButton.GreenButton>
+ );
+
+ const Disconnect = () => (
+ <AppButton.RedTransparentButton onPress={props.onDisconnect} testName="disconnect">
+ {'Disconnect'}
+ </AppButton.RedTransparentButton>
+ );
+
+ const Cancel = () => (
+ <AppButton.RedTransparentButton onPress={props.onDisconnect} testName="cancel">
+ {'Cancel'}
+ </AppButton.RedTransparentButton>
+ );
+
+ const Secured = ({ displayStyle }) => (
+ <SecuredLabel style={styles.status_security} displayStyle={displayStyle} />
+ );
+ const Wrapper = ({ children }) => <View style={props.style}>{children}</View>;
+ const Body = ({ children }) => <View style={styles.body}>{children}</View>;
+ const Footer = ({ children }) => <View style={styles.footer}>{children}</View>;
+
+ switch (props.tunnelState) {
+ case 'connecting':
+ return (
+ <Wrapper>
+ <Body>
+ <Secured displayStyle={SecuredDisplayStyle.securing} />
+ <Location>
+ <City />
+ </Location>
+ </Body>
+ <Footer>
+ <SwitchLocation />
+ <Cancel />
+ </Footer>
+ </Wrapper>
+ );
+ case 'connected':
+ return (
+ <Wrapper>
+ <Body>
+ <Secured displayStyle={SecuredDisplayStyle.secured} />
+ <Location>
+ <City />
+ <Country />
+ </Location>
+ <Ip />
+ </Body>
+ <Footer>
+ <SwitchLocation />
+ <Disconnect />
+ </Footer>
+ </Wrapper>
+ );
+
+ case 'blocked':
+ return (
+ <Wrapper>
+ <Body>
+ <Secured displayStyle={SecuredDisplayStyle.blocked} />
+ </Body>
+ <Footer>
+ <SwitchLocation />
+ <Cancel />
+ </Footer>
+ </Wrapper>
+ );
+
+ case 'disconnecting':
+ return (
+ <Wrapper>
+ <Body>
+ <Secured displayStyle={SecuredDisplayStyle.secured} />
+ <Location>
+ <Country />
+ </Location>
+ <Ip />
+ </Body>
+ <Footer>
+ <SelectedLocation />
+ <Connect />
+ </Footer>
+ </Wrapper>
+ );
+
+ case 'disconnected':
+ return (
+ <Wrapper>
+ <Body>
+ <Secured displayStyle={SecuredDisplayStyle.unsecured} />
+ <Location>
+ <Country />
+ </Location>
+ <Ip />
+ </Body>
+ <Footer>
+ <SelectedLocation />
+ <Connect />
+ </Footer>
+ </Wrapper>
+ );
+
+ default:
+ throw new Error(`Unknown TunnelState: ${(props.tunnelState: empty)}`);
+ }
+}
diff --git a/gui/packages/desktop/src/renderer/components/ConnectStyles.js b/gui/packages/desktop/src/renderer/components/ConnectStyles.js
index 64633975fd..b882afb930 100644
--- a/gui/packages/desktop/src/renderer/components/ConnectStyles.js
+++ b/gui/packages/desktop/src/renderer/components/ConnectStyles.js
@@ -7,6 +7,9 @@ export default {
connect: Styles.createViewStyle({
flex: 1,
}),
+ tunnel_control: Styles.createViewStyle({
+ flex: 1,
+ }),
map: Styles.createViewStyle({
position: 'absolute',
top: 0,
@@ -23,13 +26,7 @@ export default {
position: 'relative' /* need this for z-index to work to cover map */,
zIndex: 1,
}),
- footer: Styles.createViewStyle({
- flex: 0,
- paddingBottom: 16,
- paddingLeft: 24,
- paddingRight: 24,
- }),
- status: Styles.createViewStyle({
+ body: Styles.createViewStyle({
paddingTop: 0,
paddingLeft: 24,
paddingRight: 24,
@@ -37,6 +34,12 @@ export default {
marginTop: 186,
flex: 1,
}),
+ footer: Styles.createViewStyle({
+ flex: 0,
+ paddingBottom: 16,
+ paddingLeft: 24,
+ paddingRight: 24,
+ }),
status_icon: Styles.createViewStyle({
position: 'absolute',
alignSelf: 'center',
@@ -51,16 +54,6 @@ export default {
width: '100%',
position: 'absolute',
}),
- server_label: Styles.createTextStyle({
- fontFamily: 'DINPro',
- fontSize: 32,
- fontWeight: '900',
- lineHeight: 44,
- letterSpacing: -0.7,
- color: colors.white,
- marginBottom: 7,
- flex: 0,
- }),
error_title: Styles.createTextStyle({
fontFamily: 'DINPro',
fontSize: 32,
@@ -82,13 +75,6 @@ export default {
fontWeight: '800',
lineHeight: 22,
marginBottom: 4,
- color: colors.white,
- }),
- status_security__secure: Styles.createTextStyle({
- color: colors.green,
- }),
- status_security__unsecured: Styles.createTextStyle({
- color: colors.red,
}),
status_ipaddress: Styles.createTextStyle({
fontFamily: 'Open Sans',
@@ -96,10 +82,11 @@ export default {
fontWeight: '800',
color: colors.white,
}),
- status_ipaddress__invisible: Styles.createTextStyle({
- opacity: 0,
- }),
status_location: Styles.createTextStyle({
+ flexDirection: 'column',
+ marginBottom: 4,
+ }),
+ status_location_text: Styles.createTextStyle({
fontFamily: 'DINPro',
fontSize: 38,
fontWeight: '900',
@@ -107,6 +94,5 @@ export default {
overflow: 'hidden',
letterSpacing: -0.9,
color: colors.white,
- marginBottom: 4,
}),
};
diff --git a/gui/packages/desktop/test/components/Connect.spec.js b/gui/packages/desktop/test/components/Connect.spec.js
index f144b249ee..c6a55ffd4c 100644
--- a/gui/packages/desktop/test/components/Connect.spec.js
+++ b/gui/packages/desktop/test/components/Connect.spec.js
@@ -3,206 +3,65 @@
import * as React from 'react';
import { shallow } from 'enzyme';
-import Connect from '../../src/renderer/components/Connect';
-
-type ConnectProps = React.ElementProps<typeof Connect>;
+import { TunnelBanner } from '../../src/renderer/components/Connect';
describe('components/Connect', () => {
- it('shows unsecured hints when disconnected', () => {
- const component = renderWithProps({
- connection: {
- ...defaultProps.connection,
- status: { state: 'disconnected' },
- },
- });
-
- const header = getComponent(component, 'header');
- const securityMessage = getComponent(component, 'networkSecurityMessage');
- const connectButton = getComponent(component, 'secureConnection');
- expect(header.prop('barStyle')).to.equal('error');
- expect(securityMessage.html()).to.contain('UNSECURED');
- expect(connectButton.html()).to.contain('Secure my connection');
- });
-
- it('shows secured hints when connected', () => {
- const component = renderWithProps({
- connection: {
- ...defaultProps.connection,
- status: { state: 'connected' },
- },
- });
-
- const header = getComponent(component, 'header');
- const securityMessage = getComponent(component, 'networkSecurityMessage');
- const disconnectButton = getComponent(component, 'disconnect');
- expect(header.prop('barStyle')).to.equal('success');
- expect(securityMessage.html()).to.contain('SECURE ');
- expect(disconnectButton.html()).to.contain('Disconnect');
- });
-
- it('shows blocked hints when blocked', () => {
- const component = renderWithProps({
- connection: {
- ...defaultProps.connection,
- status: { state: 'blocked', details: { reason: 'no_matching_relay' } },
- },
- });
-
- const header = getComponent(component, 'header');
- const securityMessage = getComponent(component, 'networkSecurityMessage');
- const cancelButton = getComponent(component, 'cancel');
- expect(header.prop('barStyle')).to.equal('success');
- expect(securityMessage.html()).to.contain('BLOCKED ');
- expect(cancelButton.html()).to.contain('Cancel');
- });
-
- it('shows the connection location when connecting', () => {
- const component = renderWithProps({
- connection: {
- ...defaultProps.connection,
- status: { state: 'connecting' },
- country: 'Norway',
- city: 'Oslo',
- ip: '4.3.2.1',
- },
- });
- const countryAndCity = getComponent(component, 'location');
- const ipAddr = getComponent(component, 'ipAddress');
-
- expect(countryAndCity.html()).to.contain('Norway');
- expect(countryAndCity.html()).not.to.contain('Oslo');
- expect(ipAddr.length).to.equal(0);
- });
-
- it('shows the connection location when connected', () => {
- const component = renderWithProps({
- connection: {
- ...defaultProps.connection,
- status: { state: 'connected' },
- country: 'Norway',
- city: 'Oslo',
- ip: '4.3.2.1',
- },
+ describe('TunnelBanner', () => {
+ it('invisible when disconnecting', () => {
+ for (const reason of ['nothing', 'block', 'reconnect']) {
+ const component = shallow(
+ <TunnelBanner
+ tunnelState={{
+ state: 'disconnecting',
+ details: { reason },
+ }}
+ />,
+ );
+ expect(component.state('visible')).to.be.false;
+ }
});
- const countryAndCity = getComponent(component, 'location');
- const ipAddr = getComponent(component, 'ipAddress');
- expect(countryAndCity.html()).to.contain('Norway');
- expect(countryAndCity.html()).to.contain('Oslo');
- expect(ipAddr.html()).to.contain('4.3.2.1');
- });
-
- it('shows the connection location when disconnected', () => {
- const component = renderWithProps({
- connection: {
- ...defaultProps.connection,
- status: { state: 'disconnected' },
- country: 'Norway',
- city: 'Oslo',
- ip: '4.3.2.1',
- },
+ it('invisible when connected or disconnected', () => {
+ for (const state of ['connected', 'disconnected']) {
+ const component = shallow(
+ <TunnelBanner
+ tunnelState={{
+ state,
+ }}
+ />,
+ );
+ expect(component.state('visible')).to.be.false;
+ }
});
- const countryAndCity = getComponent(component, 'location');
- const ipAddr = getComponent(component, 'ipAddress');
- expect(countryAndCity.html()).to.contain('Norway');
- expect(countryAndCity.html()).to.not.contain('Oslo');
- expect(ipAddr.html()).to.contain('4.3.2.1');
- });
+ it('visible when connecting', () => {
+ const component = shallow(
+ <TunnelBanner
+ tunnelState={{
+ state: 'connecting',
+ }}
+ />,
+ );
- it('invokes the onConnect prop', (done) => {
- const component = renderWithProps({
- onConnect: () => done(),
- connection: {
- ...defaultProps.connection,
- status: { state: 'disconnected' },
- },
+ expect(component.state('visible')).to.be.true;
+ expect(component.state('title')).to.not.be.empty;
});
- const connectButton = getComponent(component, 'secureConnection');
-
- connectButton.prop('onPress')();
- });
- it('hides the blocking internet message when connected or disconnected', () => {
- for (const state of ['connected', 'disconnected']) {
- const component = renderWithProps({
- connection: {
- ...defaultProps.connection,
- status: { state },
- },
- });
- const blockingAccordion = getComponent(component, 'blockingAccordion');
+ it('visible when blocked', () => {
+ const component = shallow(
+ <TunnelBanner
+ tunnelState={{
+ state: 'blocked',
+ details: {
+ reason: 'no_matching_relay',
+ },
+ }}
+ />,
+ );
- expect(blockingAccordion.prop('height')).to.equal(0);
- }
- });
-
- it('hides the blocking internet message when disconnecting', () => {
- for (const afterDisconnect of ['nothing', 'block', 'reconnect']) {
- const component = renderWithProps({
- connection: {
- ...defaultProps.connection,
- status: { state: 'disconnecting', details: afterDisconnect },
- },
- });
- const blockingAccordion = getComponent(component, 'blockingAccordion');
-
- expect(blockingAccordion.prop('height')).to.equal(0);
- }
- });
-
- it('shows the blocking internet message when connecting', () => {
- const component = renderWithProps({
- connection: {
- ...defaultProps.connection,
- status: { state: 'connecting' },
- country: 'Norway',
- city: 'Oslo',
- },
- });
- const blockingAccordion = getComponent(component, 'blockingAccordion');
-
- expect(blockingAccordion.prop('height')).to.equal('auto');
- });
-
- it('shows the blocking internet message when blocked', () => {
- const component = renderWithProps({
- connection: {
- ...defaultProps.connection,
- status: { state: 'blocked', details: { reason: 'no_matching_relay' } },
- },
+ expect(component.state('visible')).to.be.true;
+ expect(component.state('title')).to.not.be.empty;
+ expect(component.state('subtitle')).to.not.be.empty;
});
- const blockingAccordion = getComponent(component, 'blockingAccordion');
- expect(blockingAccordion.prop('height')).to.equal('auto');
- expect(blockingAccordion.dive().html()).to.contain('No relay server');
});
});
-
-const defaultProps: ConnectProps = {
- onSettings: () => {},
- onSelectLocation: () => {},
- onConnect: () => {},
- onDisconnect: () => {},
- onExternalLink: () => {},
- accountExpiry: '',
- selectedRelayName: '',
- connection: {
- status: { state: 'disconnected' },
- isOnline: true,
- ip: null,
- latitude: null,
- longitude: null,
- country: null,
- city: null,
- },
- updateAccountExpiry: () => Promise.resolve(),
-};
-
-function renderWithProps(customProps: $Shape<ConnectProps>) {
- const props = { ...defaultProps, ...customProps };
- return shallow(<Connect {...props} />);
-}
-
-function getComponent(container, testName) {
- return container.findWhere((n) => n.prop('testName') === testName);
-}
diff --git a/gui/yarn.lock b/gui/yarn.lock
index 102a3b710e..2ef6d7a4aa 100644
--- a/gui/yarn.lock
+++ b/gui/yarn.lock
@@ -646,6 +646,10 @@
version "10.7.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.7.1.tgz#b704d7c259aa40ee052eec678758a68d07132a2e"
+"@types/node@^10.10.1":
+ version "10.10.1"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.10.1.tgz#d5c96ca246a418404914d180b7fdd625ad18eca6"
+
"@types/node@^8.0.24":
version "8.10.26"
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.26.tgz#950e3d4e6b316ba6e1ae4e84d9155aba67f88c2f"
@@ -7423,9 +7427,9 @@ typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
-typescript@^3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.1.tgz#43738f29585d3a87575520a4b93ab6026ef11fdb"
+typescript@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.3.tgz#4853b3e275ecdaa27f78fda46dc273a7eb7fc1c8"
ua-parser-js@0.7.17:
version "0.7.17"