diff options
| -rw-r--r-- | gui/packages/components/package.json | 3 | ||||
| -rw-r--r-- | gui/packages/components/src/ClipboardLabel.tsx | 51 | ||||
| -rw-r--r-- | gui/packages/components/src/SecuredLabel.tsx | 62 | ||||
| -rw-r--r-- | gui/packages/components/src/index.ts | 2 | ||||
| -rw-r--r-- | gui/packages/components/tsconfig.json | 3 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/components/Account.js | 35 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/components/Connect.js | 501 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/components/ConnectStyles.js | 42 | ||||
| -rw-r--r-- | gui/packages/desktop/test/components/Connect.spec.js | 239 | ||||
| -rw-r--r-- | gui/yarn.lock | 10 |
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" |
