summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@codeispoetry.ru>2017-06-26 14:01:15 +0300
committerAndrej Mihajlov <and@codeispoetry.ru>2017-06-26 14:01:15 +0300
commitab9d8b2471dbcd54bb882b3b6722626f81b71053 (patch)
tree05b88ff17af4a3e318c26d2903f869a4baff58c1
parentdbafeda02cc3b490fb704ab7ba7b23a6cd5d9abe (diff)
parent9c878320781750d28a3958163974336cf28a0325 (diff)
downloadmullvadvpn-ab9d8b2471dbcd54bb882b3b6722626f81b71053.tar.xz
mullvadvpn-ab9d8b2471dbcd54bb882b3b6722626f81b71053.zip
Merge branch 'feature/flow-typed/connect-component'
-rw-r--r--app/components/Connect.js195
-rw-r--r--app/lib/backend.js20
-rw-r--r--app/reducers/user.js5
-rw-r--r--app/types.js2
4 files changed, 128 insertions, 94 deletions
diff --git a/app/components/Connect.js b/app/components/Connect.js
index abbbb19db9..6ffb516ec6 100644
--- a/app/components/Connect.js
+++ b/app/components/Connect.js
@@ -1,7 +1,6 @@
-import assert from 'assert';
+// @flow
import moment from 'moment';
import React, { Component } from 'react';
-import PropTypes from 'prop-types';
import { If, Then, Else } from 'react-if';
import ReactMapboxGl, { Marker } from 'react-mapbox-gl';
import cheapRuler from 'cheap-ruler';
@@ -10,74 +9,72 @@ import { mapbox as mapboxConfig } from '../config';
import { BackendError } from '../lib/backend';
import ExternalLinkSVG from '../assets/images/icon-extLink.svg';
-import type HeaderBarStyle from './HeaderBar';
+import type { Coordinate2d } from '../types';
+import type { ServerInfo } from '../lib/backend';
+import type { HeaderBarStyle } from './HeaderBar';
+import type { UserReduxState } from '../reducers/user';
+import type { ConnectReduxState } from '../reducers/connect';
+import type { SettingsReduxState } from '../reducers/settings';
+
+type DisplayLocation = {
+ location: Coordinate2d;
+ country: ?string;
+ city: ?string;
+};
export default class Connect extends Component {
- static propTypes = {
- settings: PropTypes.object.isRequired,
- onSettings: PropTypes.func.isRequired,
- onConnect: PropTypes.func.isRequired,
- onCopyIP: PropTypes.func.isRequired,
- onDisconnect: PropTypes.func.isRequired,
- onExternalLink: PropTypes.func.isRequired,
- getServerInfo: PropTypes.func.isRequired
+ props: {
+ user: UserReduxState,
+ connect: ConnectReduxState,
+ settings: SettingsReduxState,
+ onSettings: () => void,
+ onSelectLocation: () => void,
+ onConnect: (address: string) => void,
+ onCopyIP: () => void,
+ onDisconnect: () => void,
+ onExternalLink: (type: string) => void,
+ getServerInfo: (identifier: string) => ?ServerInfo
};
- constructor() {
- super();
-
- // timer used along with `state.showCopyIPMessage`
- this._copyTimer = null;
-
- this.state = {
- isFirstPass: true,
-
- // this flag is used together with timer to display
- // a message that IP address has been copied to clipboard
- showCopyIPMessage: false
- };
- }
+ state = {
+ isFirstPass: true,
+ showCopyIPMessage: false
+ };
- // Component Lifecycle
+ _copyTimer: ?number;
componentDidMount() {
this.setState({ isFirstPass: false });
}
componentWillUnmount() {
- this.setState({ isFirstPass: true });
- }
-
- render() {
- let error = null;
-
- // check if user out of time
- // this is by far the simplest implementation
- // later on backend will notify us and disconnect VPN etc..
- if(moment(this.props.user.paidUntil).isSameOrBefore(moment())) {
- error = new BackendError('NO_CREDIT');
+ if(this._copyTimer) {
+ clearTimeout(this._copyTimer);
+ this._copyTimer = null;
}
- // Offline?
- if(this.props.connect.isOnline === false) {
- error = new BackendError('NO_INTERNET');
- }
+ this.setState({
+ isFirstPass: true,
+ showCopyIPMessage: false
+ });
+ }
+
+ render(): React.Element<*> {
+ const error = this.displayError();
+ const child = error ? this.renderError(error) : this.renderMap();
return (
<Layout>
<Header style={ this.headerStyle() } showSettings={ true } onSettings={ this.props.onSettings } />
<Container>
- <If condition={ error !== null }>
- <Then>{ () => this.renderError(error) }</Then>
- <Else>{ ::this.renderMap }</Else>
- </If>
+ { child }
</Container>
</Layout>
);
}
- renderError(error) {
+ renderError(error: BackendError): React.Element<*> {
return (
<div className="connect">
<div className="connect__status">
@@ -90,7 +87,7 @@ export default class Connect extends Component {
<div className="connect__error-message">
{ error.message }
</div>
- <If condition={ error.code === 'NO_CREDIT' }>
+ <If condition={ error.type === 'NO_CREDIT' }>
<Then>
<div>
<button className="button button--positive" onClick={ this.onExternalLink.bind(this, 'purchase') }>
@@ -105,23 +102,28 @@ export default class Connect extends Component {
);
}
- renderMap() {
+ renderMap(): React.Element<*> {
const preferredServer = this.props.settings.preferredServer;
const serverInfo = this.props.getServerInfo(preferredServer);
+ if(!serverInfo) {
+ throw new Error('Server info cannot be null.');
+ }
- const isConnecting = this.props.connect.status === 'connecting';
- const isConnected = this.props.connect.status === 'connected';
- const isDisconnected = this.props.connect.status === 'disconnected';
+ let isConnecting = false;
+ let isConnected = false;
+ let isDisconnected = false;
+ switch(this.props.connect.status) {
+ case 'connecting': isConnecting = true; break;
+ case 'connected': isConnected = true; break;
+ case 'disconnected': isDisconnected = true; break;
+ }
const altitude = (isConnecting ? 300 : 100) * 1000;
-
const displayLocation = this.displayLocation();
- const bounds = this.getBounds(displayLocation.location, altitude);
-
- const userLocation = this.toLngLat(this.props.user.location);
- const serverLocation = this.toLngLat(serverInfo.location);
- const mapBounds = this.toLngLatBounds(bounds);
+ const mapBounds = this.calculateMapBounds(displayLocation.location, altitude);
const mapBoundsOptions = { offset: [0, -113], animate: !this.state.isFirstPass };
+ const userLocation = this.convertToMapCoordinate(this.props.user.location || [0, 0]);
+ const serverLocation = this.convertToMapCoordinate(serverInfo.location);
return (
<div className="connect">
@@ -224,7 +226,7 @@ export default class Connect extends Component {
**********************************
*/ }
- <div className={ this.ipAddressClass() } onClick={ ::this.onIPAddressClick }>
+ <div className={ this.ipAddressClass() } onClick={ this.onIPAddressClick.bind(this) }>
<If condition={ this.state.showCopyIPMessage }>
<Then><span>{ 'IP copied to clipboard!' }</span></Then>
<Else><span>{ this.props.connect.clientIp }</span></Else>
@@ -268,7 +270,7 @@ export default class Connect extends Component {
</div>
<div className="connect__row">
- <button className="button button--positive" onClick={ ::this.onConnect }>Secure my connection</button>
+ <button className="button button--positive" onClick={ this.onConnect.bind(this) }>Secure my connection</button>
</div>
</div>
</Then>
@@ -318,12 +320,14 @@ export default class Connect extends Component {
// Handlers
onConnect() {
- const server = this.props.settings.preferredServer;
- const serverInfo = this.props.getServerInfo(server);
- this.props.onConnect(serverInfo.address);
+ const { preferredServer } = this.props.settings;
+ const serverInfo = this.props.getServerInfo(preferredServer);
+ if(serverInfo) {
+ this.props.onConnect(serverInfo.address);
+ }
}
- onExternalLink(type) {
+ onExternalLink(type: string) {
this.props.onExternalLink(type);
}
@@ -344,9 +348,10 @@ export default class Connect extends Component {
case 'connected':
return 'success';
}
+ throw new Error('Invalid ConnectionState');
}
- networkSecurityClass() {
+ networkSecurityClass(): string {
let classes = ['connect__status-security'];
if(this.props.connect.status === 'connected') {
classes.push('connect__status-security--secure');
@@ -357,7 +362,7 @@ export default class Connect extends Component {
return classes.join(' ');
}
- networkSecurityMessage() {
+ networkSecurityMessage(): string {
switch(this.props.connect.status) {
case 'connected': return 'Secure connection';
case 'connecting': return 'Creating secure connection';
@@ -365,7 +370,7 @@ export default class Connect extends Component {
}
}
- spinnerClass() {
+ spinnerClass(): string {
var classes = ['connect__status-icon'];
if(this.props.connect.status !== 'connecting') {
classes.push('connect__status-icon--hidden');
@@ -373,7 +378,7 @@ export default class Connect extends Component {
return classes.join(' ');
}
- ipAddressClass() {
+ ipAddressClass(): string {
var classes = ['connect__status-ipaddress'];
if(this.props.connect.status === 'connecting') {
classes.push('connect__status-ipaddress--invisible');
@@ -381,34 +386,50 @@ export default class Connect extends Component {
return classes.join(' ');
}
- displayLocation() {
+ displayLocation(): DisplayLocation {
+ // return user location when disconnected
if(this.props.connect.status === 'disconnected') {
- const { location, country, city } = this.props.user;
- return { location, country, city };
+ let { location, country, city } = this.props.user;
+ return {
+ location: location || [0, 0],
+ country, city
+ };
+ } else { // otherwise server location
+ const preferredServer = this.props.settings.preferredServer;
+ const serverInfo = this.props.getServerInfo(preferredServer);
+ if(serverInfo) {
+ const { location, country, city } = serverInfo;
+ return { location, country, city };
+ }
+ throw new Error('Server location is not available.');
}
-
- const preferredServer = this.props.settings.preferredServer;
- return this.props.getServerInfo(preferredServer);
}
- // Geo helpers
+ displayError(): ?BackendError {
+ // Offline?
+ if(!this.props.connect.isOnline) {
+ return new BackendError('NO_INTERNET');
+ }
+
+ // No credit?
+ const { paidUntil } = this.props.user;
+ if(paidUntil && moment(paidUntil).isSameOrBefore(moment())) {
+ return new BackendError('NO_CREDIT');
+ }
- getBounds(center, altitude) {
- const ruler = cheapRuler(center[0], 'meters');
- return ruler.bufferPoint(center, altitude);
+ return null;
}
- toLngLat(pos) {
- assert(pos.length === 2, 'wrong number of coordinates in position');
- return [ pos[1], pos[0] ];
+ // Geo helpers
+
+ calculateMapBounds(center: Coordinate2d, altitude: number): [Coordinate2d, Coordinate2d] {
+ const bounds = cheapRuler(center[0], 'meters').bufferPoint(center, altitude);
+ // convert [lat,lng] bounds to [lng,lat]
+ return [ [bounds[1], bounds[0]], [bounds[3], bounds[2]] ];
}
- toLngLatBounds(bounds) {
- assert(bounds.length % 2 === 0, 'wrong number of sides in bounds');
- let result = [];
- for(let i = 0; i < bounds.length; i += 2) {
- result.push(bounds.slice(i, i + 2).reverse());
- }
- return result;
+ convertToMapCoordinate(pos: Coordinate2d): Coordinate2d {
+ // convert [lat,lng] bounds to [lng,lat]
+ return [pos[1], pos[0]];
}
}
diff --git a/app/lib/backend.js b/app/lib/backend.js
index a3574557a4..bf3a1382f1 100644
--- a/app/lib/backend.js
+++ b/app/lib/backend.js
@@ -7,6 +7,16 @@ import { IpcFacade, RealIpc } from './ipc-facade';
export type EventType = 'connect' | 'connecting' | 'disconnect' | 'login' | 'logging' | 'logout' | 'updatedIp' | 'updatedLocation' | 'updatedReachability';
export type ErrorType = 'NO_CREDIT' | 'NO_INTERNET' | 'INVALID_ACCOUNT';
+export type ServerInfo = {
+ address: string,
+ name: string,
+ city: string,
+ country: string,
+ location: [number, number]
+};
+
+export type ServerInfoList = { [string]: ServerInfo };
+
export class BackendError extends Error {
type: ErrorType;
title: string;
@@ -93,15 +103,15 @@ export class Backend {
});
}
- serverInfo(key: string) {
- switch(key) {
+ serverInfo(identifier: string): ?ServerInfo {
+ switch(identifier) {
case 'fastest': return this.fastestServer();
case 'nearest': return this.nearestServer();
- default: return servers[key];
+ default: return (servers: ServerInfoList)[identifier];
}
}
- fastestServer() {
+ fastestServer(): ServerInfo {
return {
address: 'uk.mullvad.net',
name: 'Fastest',
@@ -111,7 +121,7 @@ export class Backend {
};
}
- nearestServer() {
+ nearestServer(): ServerInfo {
return {
address: 'es.mullvad.net',
name: 'Nearest',
diff --git a/app/reducers/user.js b/app/reducers/user.js
index 47d9f97f5d..8222e60299 100644
--- a/app/reducers/user.js
+++ b/app/reducers/user.js
@@ -2,6 +2,7 @@
import { handleActions } from 'redux-actions';
import actions from '../actions/user';
+import type { Coordinate2d } from '../types';
import type { ReduxAction } from '../store';
import type { LoginState } from '../enums';
import type { BackendError } from '../lib/backend';
@@ -9,7 +10,7 @@ import type { BackendError } from '../lib/backend';
export type UserReduxState = {
account: ?string,
paidUntil: ?string, // ISO8601
- location: Array<number>,
+ location: ?Coordinate2d,
country: ?string,
city: ?string,
status: LoginState,
@@ -19,7 +20,7 @@ export type UserReduxState = {
const initialState: UserReduxState = {
account: null,
paidUntil: null,
- location: [0, 0],
+ location: null,
country: null,
city: null,
status: 'none',
diff --git a/app/types.js b/app/types.js
new file mode 100644
index 0000000000..a18b4c21da
--- /dev/null
+++ b/app/types.js
@@ -0,0 +1,2 @@
+// @flow
+export type Coordinate2d = [number, number];