diff options
| author | anderklander <anderklander@gmail.com> | 2018-03-23 08:41:13 +0100 |
|---|---|---|
| committer | anderklander <anderklander@gmail.com> | 2018-04-11 13:45:41 +0200 |
| commit | 70fb24b51b159195fb5ba5e854459ae47ab01f90 (patch) | |
| tree | 2c252d6df011335b3df520e77d25077d63f420d6 | |
| parent | c1cb78f55c8e346c1d2c097df4bf368c56e91abf (diff) | |
| download | mullvadvpn-70fb24b51b159195fb5ba5e854459ae47ab01f90.tar.xz mullvadvpn-70fb24b51b159195fb5ba5e854459ae47ab01f90.zip | |
Connect in reactxp
| -rw-r--r-- | app/components/Connect.css | 190 | ||||
| -rw-r--r-- | app/components/Connect.js | 200 | ||||
| -rw-r--r-- | app/components/ConnectStyles.js | 147 | ||||
| -rw-r--r-- | test/components/Connect.spec.js | 65 |
4 files changed, 279 insertions, 323 deletions
diff --git a/app/components/Connect.css b/app/components/Connect.css deleted file mode 100644 index 592e824ea2..0000000000 --- a/app/components/Connect.css +++ /dev/null @@ -1,190 +0,0 @@ -.connect { - height: 100%; - position: relative; -} - -.connect__map { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 0; -} - -.connect__container { - display: flex; - flex-direction: column; - height: 100%; - position: relative; /* need this for z-index to work to cover map */ - z-index: 1; -} - -.connect__footer { - display: flex; - flex-direction: column; - padding: 42px 24px 24px; -} - -.connect__row + .connect__row{ - margin-top: 16px; -} - -.connect__blocking-container { - color: rgba(255,255,255,0.8); - max-height: 0px; - height: 100%; - width: 100%; - overflow: hidden; - - text-transform: uppercase; - - position: absolute; - border-bottom: 1px solid rgba(255,255,255,0.8); - transition: max-height 0.5s; -} -.connect__blocking-container.show { - max-height: 36px; -} -.connect__blocking-container.hide { - max-height: 0px; - border-bottom: 0px solid rgb(208, 2, 27); - transition: max-height 0.5s, border-bottom 0.1s 0.4s; -} - -.connect__blocking-message { - display: flex; - flex-direction: row; - margin: 8px 20px; - font-family: "Open Sans"; - font-size: 12px; - font-weight: 800; - line-height: 17px; -} - -.connect__blocking-icon { - width: 10px; - height: 10px; - border-radius: 5px; - margin-top: 4px; - margin-right: 8px; - - background-color: rgb(208, 2, 27); -} - -.connect__server { - padding: 7px 12px 9px; - background-color: rgba(255,255,255,0.2); - border-radius: 4px; - display: flex; - flex-direction: row; - align-items: center; - backdrop-filter: blur(4px); -} - -.connect__server-label { - flex: 1 1 auto; - text-align: center; -} - -.connect__server-chevron { - flex: 0 0 auto; - width: 7px; - margin-left: -7px; /* let .connect__server-label extend to occupy the entire space */ -} - -.connect__footer-button { - display: block; - width:100%; - border: 0; - padding: 7px 12px 9px; - border-radius: 4px; - font-family: DINPro; - font-size: 20px; - font-weight: 900; - text-align: center; - line-height: 26px; - color: #FFFFFF; -} - -.connect__status { - padding: 0 24px; - margin-top: 94px; - margin-bottom: auto; -} - -.connect__error-title { - font-family: DINPro; - font-size: 32px; - font-weight: 900; - line-height: 1.25; - color: #fff; - margin-bottom: 8px; -} - -.connect__error-message { - font-family: "Open Sans"; - font-size: 13px; - font-weight: 600; - line-height: normal; - color: #fff; - margin-bottom: 24px; -} - -.connect__status-security { - font-family: "Open Sans"; - font-size: 16px; - font-weight: 800; - line-height: 22px; - margin-bottom: 4px; - color: #FFFFFF; - text-transform: uppercase; -} - -.connect__status-security--unsecured { - color: #D0021B; -} - -.connect__status-security--secure { - color: #44AD4D; -} - -.connect__status-location { - font-family: DINPro; - font-size: 38px; - font-weight: 900; - line-height: 1.16em; - max-height: calc(1.16em * 2); - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - letter-spacing: -0.9px; - color: #FFFFFF; - margin-bottom: 4px; -} - -.connect__status-location-icon { - display: inline-block; - margin-right: 8px; -} - -.connect__status-ipaddress { - font-family: "Open Sans"; - font-size: 16px; - font-weight: 800; - line-height: normal; - color: #FFFFFF; -} - -.connect__status-ipaddress--invisible { - visibility: hidden; -} - -.connect__status-icon { - text-align: center; - margin-bottom: 32px; -} - -.connect__status-icon--hidden { - visibility: hidden; -} diff --git a/app/components/Connect.js b/app/components/Connect.js index 3408e40b8d..632b04ea21 100644 --- a/app/components/Connect.js +++ b/app/components/Connect.js @@ -3,12 +3,15 @@ import moment from 'moment'; import * as React from 'react'; import { Layout, Container, Header } from './Layout'; +import { Text, View, Animated, Styles, Types } from 'reactxp'; +import Img from './Img'; +import { TransparentButton, GreenButton, RedButton, Label } from './styled'; +import styles from './ConnectStyles'; +import { colors } from '../config'; + import { BackendError } from '../lib/backend'; import Map from './Map'; -import ExternalLinkSVG from '../assets/images/icon-extLink.svg'; -import ChevronRightSVG from '../assets/images/icon-chevron.svg'; - import type { HeaderBarStyle } from './HeaderBar'; import type { ConnectionReduxState } from '../redux/connection/reducers'; @@ -66,7 +69,7 @@ export default class Connect extends React.Component<ConnectProps, ConnectState> return ( <Layout> - <Header style={ this.headerStyle() } showSettings={ true } onSettings={ this.props.onSettings } /> + <Header style={ this.headerStyle() } showSettings={ true } onSettings={ this.props.onSettings } testName='header'/> <Container> { child } </Container> @@ -76,28 +79,28 @@ export default class Connect extends React.Component<ConnectProps, ConnectState> renderError(error: BackendError) { return ( - <div className="connect"> - <div className="connect__status"> - <div className="connect__status-icon"> - <img src="./assets/images/icon-fail.svg" alt="" /> - </div> - <div className="connect__error-title"> + <View style={styles.connect}> + <View style={styles.status}> + <View style={styles.status_icon}> + <Img source="icon-fail" height="60" width="60" alt="" /> + </View> + <View style={styles.error_title}> { error.title } - </div> - <div className="connect__error-message"> + </View> + <View style={styles.error_message}> { error.message } - </div> + </View> { error.type === 'NO_CREDIT' ? - <div> - <button className="button button--positive" onClick={ this.onExternalLink.bind(this, 'purchase') }> - <span className="button-label">Buy more time</span> - <ExternalLinkSVG className="button-icon button-icon--16" /> - </button> - </div> + <View> + <GreenButton onPress={ this.onExternalLink.bind(this, 'purchase') }> + <Label>Buy more time</Label> + <Img source='icon-extLink' height='16' width='16' /> + </GreenButton> + </View> : null } - </div> - </div> + </View> + </View> ); } @@ -150,20 +153,23 @@ export default class Connect extends React.Component<ConnectProps, ConnectState> } return ( - <div className="connect"> - <div className="connect__map"> + <View style={styles.connect}> + <View style={styles.map}> <Map style={{ width: '100%', height: '100%' }} { ...this._getMapProps() } /> - </div> - <div className="connect__container"> + </View> + <View style={styles.container}> { this._renderIsBlockingInternetMessage() } - <div className="connect__status"> + <View style={styles.status}> { /* show spinner when connecting */ } - <div className={ this.spinnerClass() }> - <img src="./assets/images/icon-spinner.svg" alt="" ref={ this._updateMapOffset } /> - </div> + { isConnecting ? + <View style={ styles.status_icon }> + <Img source="icon-spinner" height="60" width="60" alt="" ref={ this._updateMapOffset } /> + </View> + : null + } - <div className={ this.networkSecurityClass() }>{ this.networkSecurityMessage() }</div> + <View style={ this.networkSecurityStyle() } testName='networkSecurityMessage'>{ this.networkSecurityMessage() }</View> { /* ********************************** @@ -173,19 +179,19 @@ export default class Connect extends React.Component<ConnectProps, ConnectState> { /* location when connecting or disconnected */ } { isConnecting || isDisconnected ? - <div className="connect__status-location"> - <span>{ this.props.connection.country }</span> - </div> + <Text style={styles.status_location} testName='location'> + { this.props.connection.country } + </Text> : null } { /* location when connected */ } { isConnected ? - <div className="connect__status-location"> + <Text style={styles.status_location} testName='location'> { this.props.connection.city } { this.props.connection.city && <br/> } { this.props.connection.country } - </div> + </Text> :null } @@ -195,15 +201,15 @@ export default class Connect extends React.Component<ConnectProps, ConnectState> ********************************** */ } - <div className={ this.ipAddressClass() } onClick={ this.onIPAddressClick.bind(this) }> + <Text style={ this.ipAddressStyle() } onPress={ this.onIPAddressClick.bind(this) }> { (isConnected || isDisconnected) ? ( - <span>{ + <Text testName='ipAddress'>{ this.state.showCopyIPMessage ? 'IP copied to clipboard!' : this.props.connection.ip - }</span>) : null } - </div> - </div> + }</Text>) : null } + </Text> + </View> { /* @@ -214,46 +220,31 @@ export default class Connect extends React.Component<ConnectProps, ConnectState> { /* footer when disconnected */ } { isDisconnected ? - <div className="connect__footer"> - <div className="connect__row"> - <button className="connect__server button button--neutral button--blur" onClick={ this.props.onSelectLocation }> - <div className="connect__server-label">{ this.props.selectedRelayName }</div> - <div className="connect__server-chevron"><ChevronRightSVG /></div> - </button> - </div> - - <div className="connect__row"> - <button className="button button--positive" onClick={ this.props.onConnect }>Secure my connection</button> - </div> - </div> + <View style={styles.footer}> + <TransparentButton onPress={ this.props.onSelectLocation }> + <Label>{ this.props.selectedRelayName }</Label> + <Img height='12' width='7' source='icon-chevron' /> + </TransparentButton> + <GreenButton onPress={ this.props.onConnect } testName='secureConnection'>Secure my connection</GreenButton> + </View> : null } { /* footer when connecting */ } { isConnecting ? - <div className="connect__footer"> - <div className="connect__row"> - <button className="button button--neutral button--blur" onClick={ this.props.onSelectLocation }>Switch location</button> - </div> - - <div className="connect__row"> - <button className="button button--negative-light button--blur" onClick={ this.props.onDisconnect }>Cancel</button> - </div> - </div> + <View style={styles.footer}> + <TransparentButton onPress={ this.props.onSelectLocation }>Switch location</TransparentButton> + <RedButton onPress={ this.props.onDisconnect }>Cancel</RedButton> + </View> : null } { /* footer when connected */ } { isConnected ? - <div className="connect__footer"> - <div className="connect__row"> - <button className="button button--neutral button--blur" onClick={ this.props.onSelectLocation }>Switch location</button> - </div> - - <div className="connect__row"> - <button className="button button--negative-light button--blur" onClick={ this.props.onDisconnect }>Disconnect</button> - </div> - </div> + <View style={styles.footer}> + <TransparentButton onPress={ this.props.onSelectLocation }>Switch location</TransparentButton> + <RedButton onPress={ this.props.onDisconnect } testName='disconnect'>Disconnect</RedButton> + </View> : null } @@ -263,23 +254,39 @@ export default class Connect extends React.Component<ConnectProps, ConnectState> ********************************** */ } - </div> - </div> + </View> + </View> ); } + _getBlockingMessageAnimation(animationValue: Animated.Value, toValue: number){ + return Animated.timing(animationValue, { + toValue: toValue, + easing: Animated.Easing.InOut(), + duration: 250, + useNativeDriver: true, + }); + } + + _renderIsBlockingInternetMessage() { - let animationClass = 'hide'; + let messageHeight = 0; + let messageHeightValue = Animated.createValue(0); if (this.props.connection.status === 'connecting') { - animationClass = 'show'; + messageHeight = styles.blockingMessageHeight; } - return <div className={`connect__blocking-container ${animationClass}`}> - <div className="connect__blocking-message"> - <div className="connect__blocking-icon"> </div> - blocking internet - </div> - </div>; + this._getBlockingMessageAnimation(messageHeightValue, messageHeight).start(); + + return <Animated.View style={ + Styles.createAnimatedViewStyle({ + height: messageHeight, + backgroundColor: colors.blue})} > + <Text style={styles.blocking_message}> + <Text style={styles.blocking_icon}> </Text> + <Text>BLOCKING INTERNET</Text> + </Text> + </Animated.View>; } // Handlers @@ -308,39 +315,30 @@ export default class Connect extends React.Component<ConnectProps, ConnectState> throw new Error('Invalid ConnectionState'); } - networkSecurityClass(): string { - let classes = ['connect__status-security']; + networkSecurityStyle(): Types.Style { + let classes = [styles.status_security]; if(this.props.connection.status === 'connected') { - classes.push('connect__status-security--secure'); + classes.push(styles.status_security__secure); } else if(this.props.connection.status === 'disconnected') { - classes.push('connect__status-security--unsecured'); + classes.push(styles.status_security__unsecured); } - - return classes.join(' '); + return classes; } networkSecurityMessage(): string { switch(this.props.connection.status) { - case 'connected': return 'Secure connection'; - case 'connecting': return 'Creating secure connection'; - default: return 'Unsecured connection'; - } - } - - spinnerClass(): string { - var classes = ['connect__status-icon']; - if(this.props.connection.status !== 'connecting') { - classes.push('connect__status-icon--hidden'); + case 'connected': return 'SECURE CONNECTION'; + case 'connecting': return 'CREATING SECURE CONNECTION'; + default: return 'UNSECURED CONNECTION'; } - return classes.join(' '); } - ipAddressClass(): string { - var classes = ['connect__status-ipaddress']; + ipAddressStyle(): Types.Style { + var classes = [styles.status_ipaddress]; if(this.props.connection.status === 'connecting') { - classes.push('connect__status-ipaddress--invisible'); + classes.push(styles.status_ipaddress__invisible); } - return classes.join(' '); + return classes; } displayError(): ?BackendError { diff --git a/app/components/ConnectStyles.js b/app/components/ConnectStyles.js new file mode 100644 index 0000000000..030d33b07c --- /dev/null +++ b/app/components/ConnectStyles.js @@ -0,0 +1,147 @@ +// @flow +import { createViewStyles, createTextStyles } from '../lib/styles'; +import { colors } from '../config'; + +export default { + ...createViewStyles({ + connect: { + height: '100%', + flex: 1, + }, + map: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + zIndex: 0, + height: '100%', + width: '100%', + }, + container: { + flexDirection: 'column', + flex: 1, + position: 'relative', /* need this for z-index to work to cover map */ + zIndex: 1, + }, + footer:{ + flex: 0, + marginBottom: 16, + }, + blocking_container: { + color: colors.white80, + overflow: 'hidden', + position: 'absolute', + }, + blocking_icon: { + width: 10, + height: 10, + flex: 0, + display: 'flex', + borderRadius: 5, + marginTop: 4, + marginRight: 8, + backgroundColor: colors.red, + }, + server: { + paddingTop: 7, + paddingLeft: 12, + paddingRight: 12, + paddingBottom: 9, + backgroundColor: colors.white20, + borderRadius: 4, + flexDirection: 'row', + alignItems: 'center', + }, + status: { + justifyContent: 'center', + paddingTop: 0, + paddingLeft: 24, + paddingRight: 24, + paddingBottom: 0, + marginTop: 94, + flex: 1, + }, + status_icon: { + alignSelf: 'center', + width: 60, + height: 60, + marginBottom: 32, + }, + }), + ...createTextStyles({ + blocking_message: { + display: 'flex', + flexDirection: 'row', + fontFamily: 'Open Sans', + fontSize: 12, + fontWeight: '800', + lineHeight: 17, + marginTop: 8, + marginLeft: 20, + marginRight: 20, + marginBottom: 8, + color: colors.white60, + backgroundColor: colors.blue, + }, + server_label: { + fontFamily: 'DINPro', + fontSize: 32, + fontWeight: '900', + lineHeight: 44, + letterSpacing: -0.7, + color: colors.white, + marginBottom: 7, + flex:0, + }, + error_title: { + fontFamily: 'DINPro', + fontSize: 32, + fontWeight: '900', + lineHeight: 40, + color: colors.white, + marginBottom: 8, + }, + error_message: { + fontFamily: 'Open Sans', + fontSize: 13, + fontWeight: '600', + color: colors.white, + marginBottom: 24, + }, + status_security: { + fontFamily: 'Open Sans', + fontSize: 16, + fontWeight: '800', + lineHeight: 22, + marginBottom: 4, + color: colors.white, + }, + status_security__secure: { + color: colors.green, + }, + status_security__unsecured: { + color: colors.red, + }, + status_ipaddress: { + fontFamily: 'Open Sans', + fontSize: 16, + fontWeight: '800', + color: colors.white, + }, + status_ipaddress__invisible: { + opacity: 0, + }, + status_location: { + fontFamily: 'DINPro', + fontSize: 38, + fontWeight: '900', + lineHeight: 40, + overflow: 'hidden', + letterSpacing: -0.9, + color: colors.white, + marginBottom: 4, + }, + }), + blockingMessageHeight: 36, +};
\ No newline at end of file diff --git a/test/components/Connect.spec.js b/test/components/Connect.spec.js index 9c6ed0d109..59079082c5 100644 --- a/test/components/Connect.spec.js +++ b/test/components/Connect.spec.js @@ -2,10 +2,9 @@ import { expect } from 'chai'; import React from 'react'; -import { mount } from 'enzyme'; +import { shallow } from 'enzyme'; import Connect from '../../app/components/Connect'; -import Header from '../../app/components/HeaderBar'; import type { ConnectProps } from '../../app/components/Connect'; @@ -19,13 +18,12 @@ describe('components/Connect', () => { } }); - const header = component.find(Header); - const securityMessage = component.find('.connect__status-security--unsecured'); - const connectButton = component.find('.button .button--positive'); - + const header = getComponent(component, 'header'); + const securityMessage = getComponent(component, 'networkSecurityMessage'); + const connectButton = getComponent(component, 'secureConnection'); expect(header.prop('style')).to.equal('error'); - expect(securityMessage.text().toLowerCase()).to.contain('unsecured'); - expect(connectButton.text()).to.equal('Secure my connection'); + expect(securityMessage.html()).to.contain('UNSECURED'); + expect(connectButton.html()).to.contain('Secure my connection'); }); it('shows secured hints when connected', () => { @@ -36,13 +34,12 @@ describe('components/Connect', () => { } }); - const header = component.find(Header); - const securityMessage = component.find('.connect__status-security--secure'); - const disconnectButton = component.find('.button .button--negative-light'); - + const header = getComponent(component, 'header'); + const securityMessage = getComponent(component, 'networkSecurityMessage'); + const disconnectButton = getComponent(component, 'disconnect'); expect(header.prop('style')).to.equal('success'); - expect(securityMessage.text().toLowerCase()).to.contain('secure'); - expect(disconnectButton.text()).to.equal('Disconnect'); + expect(securityMessage.html()).to.contain('SECURE '); + expect(disconnectButton.html()).to.contain('Disconnect'); }); it('shows the connection location when connecting', () => { @@ -54,12 +51,12 @@ describe('components/Connect', () => { city: 'Oslo', } }); - const countryAndCity = component.find('.connect__status-location'); - const ipAddr = component.find('.connect__status-ipaddress'); + const countryAndCity = getComponent(component, 'location'); + const ipAddr = getComponent(component, 'ipAddress'); - expect(countryAndCity.text()).to.contain('Norway'); - expect(countryAndCity.text()).not.to.contain('Oslo'); - expect(ipAddr.text()).to.be.empty; + 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', () => { @@ -72,12 +69,12 @@ describe('components/Connect', () => { ip: '4.3.2.1', } }); - const countryAndCity = component.find('.connect__status-location'); - const ipAddr = component.find('.connect__status-ipaddress'); + const countryAndCity = getComponent(component, 'location'); + const ipAddr = getComponent(component, 'ipAddress'); - expect(countryAndCity.text()).to.contain('Norway'); - expect(countryAndCity.text()).to.contain('Oslo'); - expect(ipAddr.text()).to.contain('4.3.2.1'); + 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', () => { @@ -90,12 +87,12 @@ describe('components/Connect', () => { ip: '4.3.2.1', } }); - const countryAndCity = component.find('.connect__status-location'); - const ipAddr = component.find('.connect__status-ipaddress'); + const countryAndCity = getComponent(component, 'location'); + const ipAddr = getComponent(component, 'ipAddress'); - expect(countryAndCity.text()).to.contain('Norway'); - expect(countryAndCity.text()).to.not.contain('Oslo'); - expect(ipAddr.text()).to.contain('4.3.2.1'); + expect(countryAndCity.html()).to.contain('Norway'); + expect(countryAndCity.html()).to.not.contain('Oslo'); + expect(ipAddr.html()).to.contain('4.3.2.1'); }); it('invokes the onConnect prop', (done) => { @@ -106,9 +103,9 @@ describe('components/Connect', () => { status: 'disconnected', } }); - const connectButton = component.find('.button .button--positive'); + const connectButton = getComponent(component, 'secureConnection'); - connectButton.simulate('click'); + connectButton.prop('onPress')(); }); }); @@ -134,5 +131,9 @@ const defaultProps: ConnectProps = { function renderWithProps(customProps: $Shape<ConnectProps>) { const props = { ...defaultProps, ...customProps }; - return mount( <Connect { ...props } /> ); + return shallow( <Connect { ...props } /> ); } + +function getComponent(container, testName) { + return container.findWhere( n => n.prop('testName') === testName); +}
\ No newline at end of file |
