summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authoranderklander <anderklander@gmail.com>2018-03-23 08:41:13 +0100
committeranderklander <anderklander@gmail.com>2018-04-11 13:45:41 +0200
commit70fb24b51b159195fb5ba5e854459ae47ab01f90 (patch)
tree2c252d6df011335b3df520e77d25077d63f420d6
parentc1cb78f55c8e346c1d2c097df4bf368c56e91abf (diff)
downloadmullvadvpn-70fb24b51b159195fb5ba5e854459ae47ab01f90.tar.xz
mullvadvpn-70fb24b51b159195fb5ba5e854459ae47ab01f90.zip
Connect in reactxp
-rw-r--r--app/components/Connect.css190
-rw-r--r--app/components/Connect.js200
-rw-r--r--app/components/ConnectStyles.js147
-rw-r--r--test/components/Connect.spec.js65
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">&nbsp;</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}>&nbsp;</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