summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2018-07-05 16:19:24 +0200
committerAndrej Mihajlov <and@mullvad.net>2018-07-05 16:19:24 +0200
commitf5628a7a1a185e5d71cbeb8d1c3d56bd87ff30b5 (patch)
treeda38bcd09eaca01af7a5b4fd1adf041cf47aa8ef
parente288920bec3000b6cb0da7e16b95c27e4e34be6c (diff)
parent09adf211269ff66eb53af007289765fc53e59b15 (diff)
downloadmullvadvpn-f5628a7a1a185e5d71cbeb8d1c3d56bd87ff30b5.tar.xz
mullvadvpn-f5628a7a1a185e5d71cbeb8d1c3d56bd87ff30b5.zip
Merge branch 'remove-backend'
-rw-r--r--CHANGELOG.md3
-rw-r--r--app/app.js594
-rw-r--r--app/components/AccountInput.js14
-rw-r--r--app/components/Connect.js2
-rw-r--r--app/components/Login.js2
-rw-r--r--app/config.json2
-rw-r--r--app/containers/AccountPage.js8
-rw-r--r--app/containers/AdvancedSettingsPage.js5
-rw-r--r--app/containers/ConnectPage.js5
-rw-r--r--app/containers/LoginPage.js10
-rw-r--r--app/containers/PreferencesPage.js3
-rw-r--r--app/containers/SelectLocationPage.js7
-rw-r--r--app/errors.js61
-rw-r--r--app/index.js10
-rw-r--r--app/lib/backend.js631
-rw-r--r--app/lib/reconnection-backoff.js26
-rw-r--r--app/redux/account/actions.js6
-rw-r--r--app/routes.js4
-rw-r--r--package.json2
-rw-r--r--yarn.lock79
20 files changed, 700 insertions, 774 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 30e0931a8d..0838ba1620 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,9 @@ Line wrap the file at 100 chars. Th
## [Unreleased]
+### Fixed
+- Disable account input when logging in.
+
## [2018.2-beta1] - 2018-07-02
### Added
diff --git a/app/app.js b/app/app.js
index d2eacfe751..9cbc368cde 100644
--- a/app/app.js
+++ b/app/app.js
@@ -1,28 +1,435 @@
// @flow
import React from 'react';
-import { Component } from 'reactxp';
+import { bindActionCreators } from 'redux';
import { Provider } from 'react-redux';
-import { ConnectedRouter } from 'react-router-redux';
+import { ConnectedRouter, push as pushHistory } from 'react-router-redux';
import { createMemoryHistory } from 'history';
import { webFrame, ipcRenderer } from 'electron';
-import { log } from './lib/platform';
+
import makeRoutes from './routes';
-import configureStore from './redux/store';
-import { Backend } from './lib/backend';
+import { log } from './lib/platform';
+import ReconnectionBackoff from './lib/reconnection-backoff';
import { DaemonRpc } from './lib/daemon-rpc';
import { setShutdownHandler } from './shutdown-handler';
+import {
+ RemoteError as JsonRpcTransportRemoteError,
+ TimeOutError as JsonRpcTransportTimeOutError,
+} from './lib/jsonrpc-transport';
+import {
+ UnknownError,
+ NoAccountError,
+ CommunicationError,
+ InvalidAccountError,
+ NoDaemonError,
+} from './errors';
+
+import configureStore from './redux/store';
+import accountActions from './redux/account/actions';
+import connectionActions from './redux/connection/actions';
+import settingsActions from './redux/settings/actions';
+
+import type { RpcCredentials } from './lib/rpc-address-file';
+import type {
+ DaemonRpcProtocol,
+ ConnectionObserver as DaemonConnectionObserver,
+} from './lib/daemon-rpc';
+import type { ReduxStore } from './redux/store';
+import type { AccountToken, BackendState, RelaySettingsUpdate } from './lib/daemon-rpc';
import type { ConnectionState } from './redux/connection/reducers';
import type { TrayIconType } from './tray-icon-controller';
-import type { RpcCredentialsProvider, RpcCredentials } from './lib/backend';
-const initialState = null;
-const memoryHistory = createMemoryHistory();
-const store = configureStore(initialState, memoryHistory);
+export default class AppRenderer {
+ _daemonRpc: DaemonRpcProtocol = new DaemonRpc();
+ _reconnectBackoff = new ReconnectionBackoff();
+ _credentials: ?RpcCredentials;
+ _openConnectionObserver: ?DaemonConnectionObserver;
+ _closeConnectionObserver: ?DaemonConnectionObserver;
+ _memoryHistory = createMemoryHistory();
+ _reduxStore: ReduxStore;
+ _reduxActions: *;
+
+ constructor() {
+ const store = configureStore(null, this._memoryHistory);
+ const dispatch = store.dispatch;
+
+ this._reduxStore = store;
+ this._reduxActions = {
+ account: bindActionCreators(accountActions, dispatch),
+ connection: bindActionCreators(connectionActions, dispatch),
+ settings: bindActionCreators(settingsActions, dispatch),
+ history: bindActionCreators({ push: pushHistory }, dispatch),
+ };
+
+ this._openConnectionObserver = this._daemonRpc.addOpenConnectionObserver(() => {
+ this._onOpenConnection();
+ });
+
+ this._closeConnectionObserver = this._daemonRpc.addCloseConnectionObserver((error) => {
+ this._onCloseConnection(error);
+ });
+
+ this._setupReachability();
+
+ setShutdownHandler(async () => {
+ log.info('Executing a shutdown handler');
+ try {
+ await this.disconnectTunnel();
+ log.info('Disconnected the tunnel');
+ } catch (e) {
+ log.error(`Failed to shutdown tunnel: ${e.message}`);
+ }
+ });
+
+ // disable pinch to zoom
+ webFrame.setVisualZoomLevelLimits(1, 1);
+ }
+
+ dispose() {
+ const openConnectionObserver = this._openConnectionObserver;
+ const closeConnectionObserver = this._closeConnectionObserver;
+
+ if (openConnectionObserver) {
+ openConnectionObserver.unsubscribe();
+ this._openConnectionObserver = null;
+ }
+
+ if (closeConnectionObserver) {
+ closeConnectionObserver.unsubscribe();
+ this._closeConnectionObserver = null;
+ }
+ }
+
+ renderView() {
+ return (
+ <Provider store={this._reduxStore}>
+ <ConnectedRouter history={this._memoryHistory}>
+ {makeRoutes(this._reduxStore.getState, { app: this })}
+ </ConnectedRouter>
+ </Provider>
+ );
+ }
+
+ connect() {
+ this._connectToDaemon();
+ }
+
+ disconnect() {
+ this._daemonRpc.disconnect();
+ }
+
+ async login(accountToken: AccountToken) {
+ const actions = this._reduxActions;
+ actions.account.startLogin(accountToken);
+
+ log.debug('Attempting to login');
+
+ try {
+ const accountData = await this._daemonRpc.getAccountData(accountToken);
+ await this._daemonRpc.setAccount(accountToken);
+
+ actions.account.loginSuccessful(accountData.expiry);
+
+ // Redirect the user after some time to allow for
+ // the 'Login Successful' screen to be visible
+ setTimeout(() => {
+ actions.history.push('/connect');
+ log.debug('Autoconnecting...');
+ this.connectTunnel();
+ }, 1000);
+ } catch (error) {
+ log.error('Failed to log in,', error.message);
+
+ actions.account.loginFailed(this._rpcErrorToBackendError(error));
+ }
+ }
+
+ async _autologin() {
+ const actions = this._reduxActions;
+ actions.account.startLogin();
+
+ log.debug('Attempting to log in automatically');
+
+ try {
+ const accountToken = await this._daemonRpc.getAccount();
+ if (!accountToken) {
+ throw new NoAccountError();
+ }
+
+ log.debug(`The daemon had an account number stored: ${accountToken}`);
+ actions.account.startLogin(accountToken);
+
+ const accountData = await this._daemonRpc.getAccountData(accountToken);
+ log.debug('The stored account number still exists:', accountData);
+
+ actions.account.loginSuccessful(accountData.expiry);
+ actions.history.push('/connect');
+ } catch (e) {
+ log.warn('Unable to autologin,', e.message);
+
+ actions.account.autoLoginFailed();
+ actions.history.push('/');
+
+ throw e;
+ }
+ }
+
+ async logout() {
+ const actions = this._reduxActions;
+
+ try {
+ await Promise.all([
+ this.disconnectTunnel(),
+ this._daemonRpc.setAccount(null),
+ this._fetchAccountHistory(),
+ ]);
+ actions.account.loggedOut();
+ actions.history.push('/');
+ } catch (e) {
+ log.info('Failed to logout: ', e.message);
+ }
+ }
+
+ async connectTunnel() {
+ const actions = this._reduxActions;
+
+ try {
+ const currentState = await this._daemonRpc.getState();
+ if (currentState.state === 'secured') {
+ log.debug('Refusing to connect as connection is already secured');
+ actions.connection.connected();
+ } else {
+ actions.connection.connecting();
+ await this._daemonRpc.connectTunnel();
+ }
+ } catch (error) {
+ actions.connection.disconnected();
+ throw error;
+ }
+ }
+
+ disconnectTunnel() {
+ return this._daemonRpc.disconnectTunnel();
+ }
+
+ updateRelaySettings(relaySettings: RelaySettingsUpdate) {
+ return this._daemonRpc.updateRelaySettings(relaySettings);
+ }
+
+ async fetchRelaySettings() {
+ const actions = this._reduxActions;
+ const relaySettings = await this._daemonRpc.getRelaySettings();
+
+ log.debug('Got relay settings from daemon', JSON.stringify(relaySettings));
-class CredentialsProvider implements RpcCredentialsProvider {
- request(): Promise<RpcCredentials> {
+ if (relaySettings.normal) {
+ const payload = {};
+ const normal = relaySettings.normal;
+ const tunnel = normal.tunnel;
+ const location = normal.location;
+
+ if (location === 'any') {
+ payload.location = 'any';
+ } else {
+ payload.location = location.only;
+ }
+
+ if (tunnel === 'any') {
+ payload.port = 'any';
+ payload.protocol = 'any';
+ } else {
+ const { port, protocol } = tunnel.only.openvpn;
+ payload.port = port === 'any' ? port : port.only;
+ payload.protocol = protocol === 'any' ? protocol : protocol.only;
+ }
+
+ actions.settings.updateRelay({
+ normal: payload,
+ });
+ } else if (relaySettings.custom_tunnel_endpoint) {
+ const custom_tunnel_endpoint = relaySettings.custom_tunnel_endpoint;
+ const {
+ host,
+ tunnel: {
+ openvpn: { port, protocol },
+ },
+ } = custom_tunnel_endpoint;
+
+ actions.settings.updateRelay({
+ custom_tunnel_endpoint: {
+ host,
+ port,
+ protocol,
+ },
+ });
+ }
+ }
+
+ async updateAccountExpiry() {
+ const actions = this._reduxActions;
+ try {
+ const accountToken = await this._daemonRpc.getAccount();
+ if (!accountToken) {
+ throw new NoAccountError();
+ }
+ const accountData = await this._daemonRpc.getAccountData(accountToken);
+ actions.account.updateAccountExpiry(accountData.expiry);
+ } catch (e) {
+ log.error(`Failed to update account expiry: ${e.message}`);
+ }
+ }
+
+ async removeAccountFromHistory(accountToken: AccountToken): Promise<void> {
+ await this._daemonRpc.removeAccountFromHistory(accountToken);
+ await this._fetchAccountHistory();
+ }
+
+ async _fetchAccountHistory(): Promise<void> {
+ const actions = this._reduxActions;
+
+ const accountHistory = await this._daemonRpc.getAccountHistory();
+ actions.account.updateAccountHistory(accountHistory);
+ }
+
+ async _fetchRelayLocations() {
+ const actions = this._reduxActions;
+ const locations = await this._daemonRpc.getRelayLocations();
+
+ log.info('Got relay locations');
+
+ const storedLocations = locations.countries.map((country) => ({
+ name: country.name,
+ code: country.code,
+ hasActiveRelays: country.cities.some((city) => city.has_active_relays),
+ cities: country.cities.map((city) => ({
+ name: city.name,
+ code: city.code,
+ latitude: city.latitude,
+ longitude: city.longitude,
+ hasActiveRelays: city.has_active_relays,
+ })),
+ }));
+
+ actions.settings.updateRelayLocations(storedLocations);
+ }
+
+ async _fetchLocation() {
+ const actions = this._reduxActions;
+ const location = await this._daemonRpc.getLocation();
+
+ log.info('Got location from daemon');
+
+ const locationUpdate = {
+ ip: location.ip,
+ country: location.country,
+ city: location.city,
+ latitude: location.latitude,
+ longitude: location.longitude,
+ mullvadExitIp: location.mullvad_exit_ip,
+ };
+
+ actions.connection.newLocation(locationUpdate);
+ }
+
+ async setAllowLan(allowLan: boolean) {
+ const actions = this._reduxActions;
+ await this._daemonRpc.setAllowLan(allowLan);
+ actions.settings.updateAllowLan(allowLan);
+ }
+
+ async _fetchAllowLan() {
+ const actions = this._reduxActions;
+ const allowLan = await this._daemonRpc.getAllowLan();
+ actions.settings.updateAllowLan(allowLan);
+ }
+
+ async _fetchSecurityState() {
+ const securityState = await this._daemonRpc.getState();
+ const connectionState = this._securityStateToConnectionState(securityState);
+ this._updateConnectionState(connectionState);
+ }
+
+ async _connectToDaemon(): Promise<void> {
+ let credentials;
+ try {
+ credentials = await this._requestCredentials();
+ } catch (error) {
+ log.error(`Cannot request the RPC credentials: ${error.message}`);
+ return;
+ }
+
+ this._credentials = credentials;
+ this._daemonRpc.connect(credentials.connectionString);
+ }
+
+ async _onOpenConnection() {
+ this._reconnectBackoff.reset();
+
+ // authenticate once connected
+ const credentials = this._credentials;
+ try {
+ if (!credentials) {
+ throw new Error('Credentials cannot be unset after connection is established.');
+ }
+ await this._authenticate(credentials.sharedSecret);
+ } catch (error) {
+ log.error(`Cannot authenticate: ${error.message}`);
+ }
+
+ // autologin
+ try {
+ await this._autologin();
+ } catch (error) {
+ if (error instanceof NoAccountError) {
+ log.debug('No previously configured account set, showing window');
+ ipcRenderer.send('show-window');
+ } else {
+ log.error(`Failed to autologin: ${error.message}`);
+ }
+ }
+
+ // make sure to re-subscribe to state notifications when connection is re-established.
+ try {
+ await this._subscribeStateListener();
+ } catch (error) {
+ log.error(`Cannot subscribe for RPC notifications: ${error.message}`);
+ }
+
+ // fetch initial state
+ try {
+ await this._fetchInitialState();
+ } catch (error) {
+ log.error(`Cannot fetch initial state: ${error.message}`);
+ }
+
+ // auto connect the tunnel
+ try {
+ await this.connectTunnel();
+ } catch (error) {
+ log.error(`Cannot autoconnect the tunnel: ${error.message}`);
+ }
+ }
+
+ async _onCloseConnection(error: ?Error) {
+ if (error) {
+ log.debug(`Lost connection to daemon: ${error.message}`);
+
+ const recover = async () => {
+ try {
+ await this.connect();
+ } catch (error) {
+ log.error(`Failed to reconnect: ${error.message}`);
+ }
+ };
+
+ this._reconnectBackoff.attempt(() => {
+ recover();
+ });
+ }
+ }
+
+ _requestCredentials(): Promise<RpcCredentials> {
return new Promise((resolve, _reject) => {
ipcRenderer.once('daemon-connection-ready', (_event, credentials: RpcCredentials) => {
resolve(credentials);
@@ -30,64 +437,135 @@ class CredentialsProvider implements RpcCredentialsProvider {
ipcRenderer.send('discover-daemon-connection');
});
}
-}
-const rpc = new DaemonRpc();
-const credentialsProvider = new CredentialsProvider();
-const backend = new Backend(store, rpc, credentialsProvider);
-backend.connect();
+ /**
+ * Start reachability monitoring for online/offline detection
+ * This is currently done via HTML5 APIs but will be replaced later
+ * with proper mullvad-daemon integration.
+ */
+ _setupReachability() {
+ const actions = this._reduxActions;
-setShutdownHandler(async () => {
- log.info('Executing a shutdown handler');
+ window.addEventListener('online', () => {
+ actions.connection.online();
+ });
+ window.addEventListener('offline', () => {
+ actions.connection.offline();
+ });
- try {
- await backend.disconnectTunnel();
- log.info('Disconnected the tunnel');
- } catch (e) {
- log.error(`Failed to shutdown tunnel: ${e.message}`);
+ if (navigator.onLine) {
+ actions.connection.online();
+ } else {
+ actions.connection.offline();
+ }
}
-});
-/**
- * Get tray icon type based on connection state
- */
-const getIconType = (s: ConnectionState): TrayIconType => {
- switch (s) {
- case 'connected':
- return 'secured';
- case 'connecting':
- return 'securing';
- default:
- return 'unsecured';
+ async _subscribeStateListener() {
+ await this._daemonRpc.subscribeStateListener((newState, error) => {
+ if (error) {
+ log.error(`Received an error when processing the incoming state change: ${error.message}`);
+ }
+
+ if (newState) {
+ const connectionState = this._securityStateToConnectionState(newState);
+
+ log.debug(
+ `Got new state from daemon {state: ${newState.state}, target_state: ${
+ newState.target_state
+ }}, translated to '${connectionState}'`,
+ );
+
+ this._updateConnectionState(connectionState);
+ this._refreshStateOnChange();
+ }
+ });
}
-};
-/**
- * Update tray icon via IPC call
- */
-const updateTrayIcon = () => {
- const { connection } = store.getState();
+ _fetchInitialState() {
+ return Promise.all([
+ this._fetchSecurityState(),
+ this.fetchRelaySettings(),
+ this._fetchRelayLocations(),
+ this._fetchAllowLan(),
+ this._fetchLocation(),
+ this._fetchAccountHistory(),
+ ]);
+ }
- // TODO: Only update the tray icon if the connection status changed
- ipcRenderer.send('change-tray-icon', getIconType(connection.status));
-};
+ _updateTrayIcon(connectionState: ConnectionState) {
+ const iconTypes: { [ConnectionState]: TrayIconType } = {
+ connected: 'secured',
+ connecting: 'securing',
+ };
+ const type = iconTypes[connectionState] || 'unsecured';
-store.subscribe(updateTrayIcon);
+ ipcRenderer.send('change-tray-icon', type);
+ }
-// force update tray
-updateTrayIcon();
+ async _refreshStateOnChange() {
+ try {
+ await this._fetchLocation();
+ } catch (error) {
+ log.error(`Failed to fetch the location: ${error.message}`);
+ }
+ }
-// disable smart pinch.
-webFrame.setVisualZoomLevelLimits(1, 1);
+ _securityStateToConnectionState(backendState: BackendState): ConnectionState {
+ if (backendState.state === 'unsecured' && backendState.target_state === 'secured') {
+ return 'connecting';
+ } else if (backendState.state === 'secured' && backendState.target_state === 'secured') {
+ return 'connected';
+ } else if (backendState.target_state === 'unsecured') {
+ return 'disconnected';
+ }
+ throw new Error('Unsupported state/target state combination: ' + JSON.stringify(backendState));
+ }
-export default class App extends Component {
- render() {
- return (
- <Provider store={store}>
- <ConnectedRouter history={memoryHistory}>
- {makeRoutes(store.getState, { backend })}
- </ConnectedRouter>
- </Provider>
- );
+ _updateConnectionState(connectionState: ConnectionState) {
+ const actions = this._reduxActions;
+ switch (connectionState) {
+ case 'connecting':
+ actions.connection.connecting();
+ break;
+ case 'connected':
+ actions.connection.connected();
+ break;
+ case 'disconnected':
+ actions.connection.disconnected();
+ break;
+ }
+
+ this._updateTrayIcon(connectionState);
+ }
+
+ _rpcErrorToBackendError(e) {
+ if (e instanceof JsonRpcTransportRemoteError) {
+ switch (e.code) {
+ case -200: // Account doesn't exist
+ return new InvalidAccountError();
+ case -32603: // Internal error
+ // We treat all internal backend errors as the user cannot reach
+ // api.mullvad.net. This is not always true of course, but it is
+ // true so often that we choose to disregard the other edge cases
+ // for now.
+ return new CommunicationError();
+ }
+ } else if (e instanceof JsonRpcTransportTimeOutError) {
+ return new CommunicationError();
+ } else if (e instanceof NoDaemonError) {
+ return e;
+ }
+
+ return new UnknownError(e.message);
+ }
+
+ async _authenticate(sharedSecret: string) {
+ try {
+ await this._daemonRpc.authenticate(sharedSecret);
+ log.info('Authenticated with backend');
+ } catch (e) {
+ log.error(`Failed to authenticate with backend: ${e.message}`);
+ throw e;
+ }
}
}
diff --git a/app/components/AccountInput.js b/app/components/AccountInput.js
index 5a8fa47336..cb7cd258c5 100644
--- a/app/components/AccountInput.js
+++ b/app/components/AccountInput.js
@@ -1,7 +1,7 @@
// @flow
import * as React from 'react';
-import { formatAccount } from '../lib/formatters';
import { TextInput } from 'reactxp';
+import { formatAccount } from '../lib/formatters';
import { colors } from '../config';
// @TODO: move it into types.js
@@ -66,10 +66,13 @@ export default class AccountInput extends React.Component<AccountInputProps, Acc
}
shouldComponentUpdate(nextProps: AccountInputProps, nextState: AccountInputState) {
+ const mergedProps = { ...this.props, ...nextProps };
+ const hasPropChanges = Object.keys(mergedProps).some((key) => {
+ return this.props[key] !== nextProps[key];
+ });
+
return (
- this.props.value !== nextProps.value ||
- this.props.onEnter !== nextProps.onEnter ||
- this.props.onChange !== nextProps.onChange ||
+ hasPropChanges ||
this.state.value !== nextState.value ||
this.state.selectionRange[0] !== nextState.selectionRange[0] ||
this.state.selectionRange[1] !== nextState.selectionRange[1]
@@ -78,8 +81,7 @@ export default class AccountInput extends React.Component<AccountInputProps, Acc
render() {
const displayString = formatAccount(this.state.value || '');
- // eslint-disable-next-line no-unused-vars
- const { value, onChange, onEnter, ...otherProps } = this.props;
+ const { value: _value, onChange: _onChange, onEnter: _onEnter, ...otherProps } = this.props;
return (
<TextInput
{...otherProps}
diff --git a/app/components/Connect.js b/app/components/Connect.js
index d14b768d44..79723e5672 100644
--- a/app/components/Connect.js
+++ b/app/components/Connect.js
@@ -9,7 +9,7 @@ import { TransparentButton, RedTransparentButton, GreenButton, Label } from './s
import Accordion from './Accordion';
import styles from './ConnectStyles';
-import { NoCreditError, NoInternetError } from '../lib/backend';
+import { NoCreditError, NoInternetError } from '../errors';
import Map from './Map';
import type { HeaderBarStyle } from './HeaderBar';
diff --git a/app/components/Login.js b/app/components/Login.js
index e70d84b7aa..355396a77c 100644
--- a/app/components/Login.js
+++ b/app/components/Login.js
@@ -352,7 +352,7 @@ export default class Login extends Component<Props, State> {
onChange={this._onInputChange}
onEnter={this._onLogin}
value={this.props.accountToken || ''}
- disabled={!this._shouldEnableAccountInput()}
+ editable={this._shouldEnableAccountInput()}
autoFocus={true}
ref={(ref) => (this._accountInput = ref)}
testName="AccountInput"
diff --git a/app/config.json b/app/config.json
index fdbbfe7ae2..d634167048 100644
--- a/app/config.json
+++ b/app/config.json
@@ -22,10 +22,8 @@
"white20": "rgba(255, 255, 255, 0.2)",
"blue20": "rgba(41, 77, 115, 0.2)",
"blue40": "rgba(41, 77, 115, 0.4)",
- "blue80": "rgba(41, 77, 115, 0.5)",
"blue60": "rgba(41, 77, 115, 0.6)",
"blue80": "rgba(41, 77, 115, 0.8)",
- "blue80": "rgba(41, 77, 115, 0.9)",
"red95": "rgba(208, 2, 27, 0.95)",
"red40": "rgba(208, 2, 27, 0.40)",
"red45": "rgba(208, 2, 27, 0.45)",
diff --git a/app/containers/AccountPage.js b/app/containers/AccountPage.js
index 1c54167b50..a8b54b30a3 100644
--- a/app/containers/AccountPage.js
+++ b/app/containers/AccountPage.js
@@ -4,7 +4,6 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { push } from 'react-router-redux';
import Account from '../components/Account';
-import accountActions from '../redux/account/actions';
import { links } from '../config';
import { openLink } from '../lib/platform';
@@ -16,14 +15,11 @@ const mapStateToProps = (state: ReduxState) => ({
accountExpiry: state.account.expiry,
});
const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => {
- const { backend } = props;
const { push: pushHistory } = bindActionCreators({ push }, dispatch);
- const { logout } = bindActionCreators(accountActions, dispatch);
-
return {
- updateAccountExpiry: () => backend.updateAccountExpiry(),
+ updateAccountExpiry: () => props.app.updateAccountExpiry(),
onLogout: () => {
- logout(backend);
+ props.app.logout();
},
onClose: () => {
pushHistory('/settings');
diff --git a/app/containers/AdvancedSettingsPage.js b/app/containers/AdvancedSettingsPage.js
index 0c1312b87f..357cafdfee 100644
--- a/app/containers/AdvancedSettingsPage.js
+++ b/app/containers/AdvancedSettingsPage.js
@@ -26,7 +26,6 @@ const mapStateToProps = (state: ReduxState) => {
};
const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => {
- const { backend } = props;
return {
onClose: () => dispatch(push('/settings')),
@@ -47,8 +46,8 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) =>
.build();
try {
- await backend.updateRelaySettings(relayUpdate);
- await backend.fetchRelaySettings();
+ await props.app.updateRelaySettings(relayUpdate);
+ await props.app.fetchRelaySettings();
} catch (e) {
log.error('Failed to update relay settings', e.message);
}
diff --git a/app/containers/ConnectPage.js b/app/containers/ConnectPage.js
index 21cc1a7133..7966a2fa04 100644
--- a/app/containers/ConnectPage.js
+++ b/app/containers/ConnectPage.js
@@ -57,7 +57,6 @@ const mapStateToProps = (state: ReduxState) => {
const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => {
const { copyIPAddress } = bindActionCreators(connectActions, dispatch);
const history = bindActionCreators({ push: pushHistory }, dispatch);
- const { backend } = props;
return {
onSettings: () => {
@@ -68,7 +67,7 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) =>
},
onConnect: () => {
try {
- backend.connectTunnel();
+ props.app.connectTunnel();
} catch (error) {
log.error(`Failed to connect the tunnel: ${error.message}`);
}
@@ -78,7 +77,7 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) =>
},
onDisconnect: () => {
try {
- backend.disconnectTunnel();
+ props.app.disconnectTunnel();
} catch (error) {
log.error(`Failed to disconnect the tunnel: ${error.message}`);
}
diff --git a/app/containers/LoginPage.js b/app/containers/LoginPage.js
index fcf4dc8b7f..248c30ec1b 100644
--- a/app/containers/LoginPage.js
+++ b/app/containers/LoginPage.js
@@ -22,24 +22,20 @@ const mapStateToProps = (state: ReduxState) => {
};
const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => {
const { push: pushHistory } = bindActionCreators({ push }, dispatch);
- const { login, resetLoginError, updateAccountToken } = bindActionCreators(
- accountActions,
- dispatch,
- );
- const { backend } = props;
+ const { resetLoginError, updateAccountToken } = bindActionCreators(accountActions, dispatch);
return {
openSettings: () => {
pushHistory('/settings');
},
login: (account) => {
- login(backend, account);
+ props.app.login(account);
},
resetLoginError: () => {
resetLoginError();
},
openExternalLink: (type) => openLink(links[type]),
updateAccountToken: updateAccountToken,
- removeAccountTokenFromHistory: (token) => backend.removeAccountFromHistory(token),
+ removeAccountTokenFromHistory: (token) => props.app.removeAccountFromHistory(token),
};
};
diff --git a/app/containers/PreferencesPage.js b/app/containers/PreferencesPage.js
index d4eb608038..6ee813209a 100644
--- a/app/containers/PreferencesPage.js
+++ b/app/containers/PreferencesPage.js
@@ -13,12 +13,11 @@ const mapStateToProps = (state: ReduxState) => ({
});
const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => {
- const { backend } = props;
const { push: pushHistory } = bindActionCreators({ push }, dispatch);
return {
onClose: () => pushHistory('/settings'),
onChangeAllowLan: (allowLan) => {
- backend.setAllowLan(allowLan);
+ props.app.setAllowLan(allowLan);
},
};
};
diff --git a/app/containers/SelectLocationPage.js b/app/containers/SelectLocationPage.js
index 29b47c14a3..affe04dfc0 100644
--- a/app/containers/SelectLocationPage.js
+++ b/app/containers/SelectLocationPage.js
@@ -15,7 +15,6 @@ const mapStateToProps = (state: ReduxState) => ({
});
const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => {
const { push: pushHistory } = bindActionCreators({ push }, dispatch);
- const { backend } = props;
return {
onClose: () => pushHistory('/connect'),
onSelect: async (relayLocation) => {
@@ -24,9 +23,9 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) =>
.location.fromRaw(relayLocation)
.build();
- await backend.updateRelaySettings(relayUpdate);
- await backend.fetchRelaySettings();
- await backend.connectTunnel();
+ await props.app.updateRelaySettings(relayUpdate);
+ await props.app.fetchRelaySettings();
+ await props.app.connectTunnel();
pushHistory('/connect');
} catch (e) {
diff --git a/app/errors.js b/app/errors.js
new file mode 100644
index 0000000000..929fa52ab9
--- /dev/null
+++ b/app/errors.js
@@ -0,0 +1,61 @@
+export class NoCreditError extends Error {
+ constructor() {
+ super("Account doesn't have enough credit available for connection");
+ }
+
+ get userFriendlyTitle(): string {
+ return 'Out of time';
+ }
+
+ get userFriendlyMessage(): string {
+ return 'Buy more time, so you can continue using the internet securely';
+ }
+}
+
+export class NoInternetError extends Error {
+ constructor() {
+ super('Internet connectivity is currently unavailable');
+ }
+
+ get userFriendlyTitle(): string {
+ return 'Offline';
+ }
+
+ get userFriendlyMessage(): string {
+ return 'Your internet connection will be secured when you get back online';
+ }
+}
+
+export class NoDaemonError extends Error {
+ constructor() {
+ super('Could not connect to Mullvad daemon');
+ }
+}
+
+export class InvalidAccountError extends Error {
+ constructor() {
+ super('Invalid account number');
+ }
+}
+
+export class NoAccountError extends Error {
+ constructor() {
+ super('No account was set');
+ }
+}
+
+export class CommunicationError extends Error {
+ constructor() {
+ super('api.mullvad.net is blocked, please check your firewall');
+ }
+}
+
+export class UnknownError extends Error {
+ constructor(cause: string) {
+ super(`An unknown error occurred, ${cause}`);
+ }
+
+ get userFriendlyTitle(): string {
+ return 'Something went wrong';
+ }
+}
diff --git a/app/index.js b/app/index.js
index 3f17282c34..39553123fc 100644
--- a/app/index.js
+++ b/app/index.js
@@ -1,7 +1,13 @@
-import React from 'react';
+// @flow
+
import RX from 'reactxp';
import App from './app';
+const app = new App();
+const view = app.renderView();
+
+app.connect();
+
RX.App.initialize(true, true);
-RX.UserInterface.setMainView(<App />);
+RX.UserInterface.setMainView(view);
RX.UserInterface.useCustomScrollbars(true);
diff --git a/app/lib/backend.js b/app/lib/backend.js
deleted file mode 100644
index 3e128da0b1..0000000000
--- a/app/lib/backend.js
+++ /dev/null
@@ -1,631 +0,0 @@
-// @flow
-
-import { ipcRenderer } from 'electron';
-import { bindActionCreators } from 'redux';
-import { push as pushHistory } from 'react-router-redux';
-import {
- RemoteError as JsonRpcTransportRemoteError,
- TimeOutError as JsonRpcTransportTimeOutError,
-} from './jsonrpc-transport';
-import accountActions from '../redux/account/actions';
-import connectionActions from '../redux/connection/actions';
-import settingsActions from '../redux/settings/actions';
-import { log } from '../lib/platform';
-
-import type { RpcCredentials as OriginalRpcCredentials } from './rpc-address-file';
-import type {
- DaemonRpcProtocol,
- ConnectionObserver as DaemonConnectionObserver,
-} from './daemon-rpc';
-import type { ReduxStore } from '../redux/store';
-import type { AccountToken, BackendState, RelaySettingsUpdate } from './daemon-rpc';
-import type { ConnectionState } from '../redux/connection/reducers';
-
-export class NoCreditError extends Error {
- constructor() {
- super("Account doesn't have enough credit available for connection");
- }
-
- get userFriendlyTitle(): string {
- return 'Out of time';
- }
-
- get userFriendlyMessage(): string {
- return 'Buy more time, so you can continue using the internet securely';
- }
-}
-
-export class NoInternetError extends Error {
- constructor() {
- super('Internet connectivity is currently unavailable');
- }
-
- get userFriendlyTitle(): string {
- return 'Offline';
- }
-
- get userFriendlyMessage(): string {
- return 'Your internet connection will be secured when you get back online';
- }
-}
-
-export class NoDaemonError extends Error {
- constructor() {
- super('Could not connect to Mullvad daemon');
- }
-}
-
-export class InvalidAccountError extends Error {
- constructor() {
- super('Invalid account number');
- }
-}
-
-export class NoAccountError extends Error {
- constructor() {
- super('No account was set');
- }
-}
-
-export class CommunicationError extends Error {
- constructor() {
- super('api.mullvad.net is blocked, please check your firewall');
- }
-}
-
-export class UnknownError extends Error {
- constructor(cause: string) {
- super(`An unknown error occurred, ${cause}`);
- }
-
- get userFriendlyTitle(): string {
- return 'Something went wrong';
- }
-}
-
-export class CredentialsRequestError extends Error {
- _reason: Error;
-
- constructor(reason: Error) {
- super('Failed to request the RPC credentials');
- this._reason = reason;
- }
-
- get reason(): Error {
- return this._reason;
- }
-}
-
-export type RpcCredentials = OriginalRpcCredentials;
-export interface RpcCredentialsProvider {
- request(): Promise<RpcCredentials>;
-}
-
-/**
- * Backend implementation
- */
-export class Backend {
- _daemonRpc: DaemonRpcProtocol;
- _credentialsProvider: RpcCredentialsProvider;
- _reconnectBackoff = new ReconnectionBackoff();
- _credentials: ?RpcCredentials;
- _openConnectionObserver: ?DaemonConnectionObserver;
- _closeConnectionObserver: ?DaemonConnectionObserver;
- _reduxActions: *;
-
- constructor(
- store: ReduxStore,
- rpc: DaemonRpcProtocol,
- credentialsProvider: RpcCredentialsProvider,
- ) {
- this._daemonRpc = rpc;
- this._credentialsProvider = credentialsProvider;
-
- this._reduxActions = {
- account: bindActionCreators(accountActions, store.dispatch),
- connection: bindActionCreators(connectionActions, store.dispatch),
- settings: bindActionCreators(settingsActions, store.dispatch),
- history: bindActionCreators({ push: pushHistory }, store.dispatch),
- };
-
- this._openConnectionObserver = rpc.addOpenConnectionObserver(() => {
- this._onOpenConnection();
- });
-
- this._closeConnectionObserver = rpc.addCloseConnectionObserver((error) => {
- this._onCloseConnection(error);
- });
-
- this._setupReachability();
- }
-
- dispose() {
- const openConnectionObserver = this._openConnectionObserver;
- const closeConnectionObserver = this._closeConnectionObserver;
-
- if (openConnectionObserver) {
- openConnectionObserver.unsubscribe();
- this._openConnectionObserver = null;
- }
-
- if (closeConnectionObserver) {
- closeConnectionObserver.unsubscribe();
- this._closeConnectionObserver = null;
- }
- }
-
- connect() {
- this._connectToDaemon();
- }
-
- disconnect() {
- this._daemonRpc.disconnect();
- }
-
- async login(accountToken: AccountToken) {
- const actions = this._reduxActions;
- actions.account.startLogin(accountToken);
-
- log.debug('Attempting to login');
-
- try {
- const accountData = await this._daemonRpc.getAccountData(accountToken);
- await this._daemonRpc.setAccount(accountToken);
-
- actions.account.loginSuccessful(accountData.expiry);
-
- // Redirect the user after some time to allow for
- // the 'Login Successful' screen to be visible
- setTimeout(() => {
- actions.history.push('/connect');
- log.debug('Autoconnecting...');
- this.connectTunnel();
- }, 1000);
- } catch (error) {
- log.error('Failed to log in,', error.message);
-
- actions.account.loginFailed(this._rpcErrorToBackendError(error));
- }
- }
-
- async autologin() {
- const actions = this._reduxActions;
- actions.account.startLogin();
-
- log.debug('Attempting to log in automatically');
-
- try {
- const accountToken = await this._daemonRpc.getAccount();
- if (!accountToken) {
- throw new NoAccountError();
- }
-
- log.debug(`The backend had an account number stored: ${accountToken}`);
- actions.account.startLogin(accountToken);
-
- const accountData = await this._daemonRpc.getAccountData(accountToken);
- log.debug('The stored account number still exists:', accountData);
-
- actions.account.loginSuccessful(accountData.expiry);
- actions.history.push('/connect');
- } catch (e) {
- log.warn('Unable to autologin,', e.message);
-
- actions.account.autoLoginFailed();
- actions.history.push('/');
-
- throw e;
- }
- }
-
- async logout() {
- const actions = this._reduxActions;
-
- try {
- await Promise.all([
- this.disconnectTunnel(),
- this._daemonRpc.setAccount(null),
- this.fetchAccountHistory(),
- ]);
- actions.account.loggedOut();
- actions.history.push('/');
- } catch (e) {
- log.info('Failed to logout: ', e.message);
- }
- }
-
- async connectTunnel() {
- const actions = this._reduxActions;
-
- try {
- const currentState = await this._daemonRpc.getState();
- if (currentState.state === 'secured') {
- log.debug('Refusing to connect as connection is already secured');
- actions.connection.connected();
- } else {
- actions.connection.connecting();
- await this._daemonRpc.connectTunnel();
- }
- } catch (error) {
- actions.connection.disconnected();
- throw error;
- }
- }
-
- disconnectTunnel() {
- return this._daemonRpc.disconnectTunnel();
- }
-
- updateRelaySettings(relaySettings: RelaySettingsUpdate) {
- return this._daemonRpc.updateRelaySettings(relaySettings);
- }
-
- async fetchRelaySettings() {
- const actions = this._reduxActions;
- const relaySettings = await this._daemonRpc.getRelaySettings();
-
- log.debug('Got relay settings from backend', JSON.stringify(relaySettings));
-
- if (relaySettings.normal) {
- const payload = {};
- const normal = relaySettings.normal;
- const tunnel = normal.tunnel;
- const location = normal.location;
-
- if (location === 'any') {
- payload.location = 'any';
- } else {
- payload.location = location.only;
- }
-
- if (tunnel === 'any') {
- payload.port = 'any';
- payload.protocol = 'any';
- } else {
- const { port, protocol } = tunnel.only.openvpn;
- payload.port = port === 'any' ? port : port.only;
- payload.protocol = protocol === 'any' ? protocol : protocol.only;
- }
-
- actions.settings.updateRelay({
- normal: payload,
- });
- } else if (relaySettings.custom_tunnel_endpoint) {
- const custom_tunnel_endpoint = relaySettings.custom_tunnel_endpoint;
- const {
- host,
- tunnel: {
- openvpn: { port, protocol },
- },
- } = custom_tunnel_endpoint;
-
- actions.settings.updateRelay({
- custom_tunnel_endpoint: {
- host,
- port,
- protocol,
- },
- });
- }
- }
-
- async updateAccountExpiry() {
- const actions = this._reduxActions;
- try {
- const accountToken = await this._daemonRpc.getAccount();
- if (!accountToken) {
- throw new NoAccountError();
- }
- const accountData = await this._daemonRpc.getAccountData(accountToken);
- actions.account.updateAccountExpiry(accountData.expiry);
- } catch (e) {
- log.error(`Failed to update account expiry: ${e.message}`);
- }
- }
-
- async removeAccountFromHistory(accountToken: AccountToken): Promise<void> {
- await this._daemonRpc.removeAccountFromHistory(accountToken);
- await this.fetchAccountHistory();
- }
-
- async fetchAccountHistory(): Promise<void> {
- const actions = this._reduxActions;
-
- const accountHistory = await this._daemonRpc.getAccountHistory();
- actions.account.updateAccountHistory(accountHistory);
- }
-
- async fetchRelayLocations() {
- const actions = this._reduxActions;
- const locations = await this._daemonRpc.getRelayLocations();
-
- log.info('Got relay locations');
-
- const storedLocations = locations.countries.map((country) => ({
- name: country.name,
- code: country.code,
- hasActiveRelays: country.cities.some((city) => city.has_active_relays),
- cities: country.cities.map((city) => ({
- name: city.name,
- code: city.code,
- latitude: city.latitude,
- longitude: city.longitude,
- hasActiveRelays: city.has_active_relays,
- })),
- }));
-
- actions.settings.updateRelayLocations(storedLocations);
- }
-
- async fetchLocation() {
- const actions = this._reduxActions;
- const location = await this._daemonRpc.getLocation();
-
- log.info('Got location from daemon');
-
- const locationUpdate = {
- ip: location.ip,
- country: location.country,
- city: location.city,
- latitude: location.latitude,
- longitude: location.longitude,
- mullvadExitIp: location.mullvad_exit_ip,
- };
-
- actions.connection.newLocation(locationUpdate);
- }
-
- async setAllowLan(allowLan: boolean) {
- const actions = this._reduxActions;
- await this._daemonRpc.setAllowLan(allowLan);
- actions.settings.updateAllowLan(allowLan);
- }
-
- async fetchAllowLan() {
- const actions = this._reduxActions;
- const allowLan = await this._daemonRpc.getAllowLan();
- actions.settings.updateAllowLan(allowLan);
- }
-
- async fetchSecurityState() {
- const securityState = await this._daemonRpc.getState();
- const connectionState = this._securityStateToConnectionState(securityState);
- this._updateConnectionState(connectionState);
- }
-
- async _requestCredentials(): Promise<RpcCredentials> {
- try {
- return await this._credentialsProvider.request();
- } catch (providerError) {
- throw new CredentialsRequestError(providerError);
- }
- }
-
- async _connectToDaemon(): Promise<void> {
- let credentials;
- try {
- credentials = await this._requestCredentials();
- } catch (error) {
- log.error(`Cannot request the RPC credentials: ${error.message}`);
- return;
- }
-
- this._credentials = credentials;
- this._daemonRpc.connect(credentials.connectionString);
- }
-
- async _onOpenConnection() {
- this._reconnectBackoff.reset();
-
- // authenticate once connected
- const credentials = this._credentials;
- try {
- if (!credentials) {
- throw new Error('Credentials cannot be unset after connection is established.');
- }
- await this._authenticate(credentials.sharedSecret);
- } catch (error) {
- log.error(`Cannot authenticate: ${error.message}`);
- }
-
- // autologin
- try {
- await this.autologin();
- } catch (error) {
- if (error instanceof NoAccountError) {
- log.debug('No previously configured account set, showing window');
- ipcRenderer.send('show-window');
- } else {
- log.error(`Failed to autologin: ${error.message}`);
- }
- }
-
- // make sure to re-subscribe to state notifications when connection is re-established.
- try {
- await this._subscribeStateListener();
- } catch (error) {
- log.error(`Cannot subscribe for RPC notifications: ${error.message}`);
- }
-
- // fetch initial state
- try {
- await this._fetchInitialState();
- } catch (error) {
- log.error(`Cannot fetch initial state: ${error.message}`);
- }
-
- // auto connect the tunnel
- try {
- await this.connectTunnel();
- } catch (error) {
- log.error(`Cannot autoconnect the tunnel: ${error.message}`);
- }
- }
-
- async _onCloseConnection(error: ?Error) {
- if (error) {
- log.debug(`Lost connection to daemon: ${error.message}`);
-
- const recover = async () => {
- try {
- await this.connect();
- } catch (error) {
- log.error(`Failed to reconnect: ${error.message}`);
- }
- };
-
- this._reconnectBackoff.attempt(() => {
- recover();
- });
- }
- }
-
- /**
- * Start reachability monitoring for online/offline detection
- * This is currently done via HTML5 APIs but will be replaced later
- * with proper backend integration.
- */
- _setupReachability() {
- const actions = this._reduxActions;
-
- window.addEventListener('online', () => {
- actions.connection.online();
- });
- window.addEventListener('offline', () => {
- actions.connection.offline();
- });
-
- if (navigator.onLine) {
- actions.connection.online();
- } else {
- actions.connection.offline();
- }
- }
-
- async _subscribeStateListener() {
- await this._daemonRpc.subscribeStateListener((newState, error) => {
- if (error) {
- log.error(`Received an error when processing the incoming state change: ${error.message}`);
- }
-
- if (newState) {
- const connectionState = this._securityStateToConnectionState(newState);
-
- log.debug(
- `Got new state from backend {state: ${newState.state}, target_state: ${
- newState.target_state
- }}, translated to '${connectionState}'`,
- );
-
- this._updateConnectionState(connectionState);
- this._refreshStateOnChange();
- }
- });
- }
-
- async _fetchInitialState() {
- return Promise.all([
- this.fetchSecurityState(),
- this.fetchRelaySettings(),
- this.fetchRelayLocations(),
- this.fetchAllowLan(),
- this.fetchLocation(),
- this.fetchAccountHistory(),
- ]);
- }
-
- async _refreshStateOnChange() {
- try {
- await this.fetchLocation();
- } catch (error) {
- log.error(`Failed to fetch the location: ${error.message}`);
- }
- }
-
- _securityStateToConnectionState(backendState: BackendState): ConnectionState {
- if (backendState.state === 'unsecured' && backendState.target_state === 'secured') {
- return 'connecting';
- } else if (backendState.state === 'secured' && backendState.target_state === 'secured') {
- return 'connected';
- } else if (backendState.target_state === 'unsecured') {
- return 'disconnected';
- }
- throw new Error('Unsupported state/target state combination: ' + JSON.stringify(backendState));
- }
-
- _updateConnectionState(connectionState: ConnectionState) {
- const actions = this._reduxActions;
- switch (connectionState) {
- case 'connecting':
- actions.connection.connecting();
- break;
- case 'connected':
- actions.connection.connected();
- break;
- case 'disconnected':
- actions.connection.disconnected();
- break;
- }
- }
-
- _rpcErrorToBackendError(e) {
- if (e instanceof JsonRpcTransportRemoteError) {
- switch (e.code) {
- case -200: // Account doesn't exist
- return new InvalidAccountError();
- case -32603: // Internal error
- // We treat all internal backend errors as the user cannot reach
- // api.mullvad.net. This is not always true of course, but it is
- // true so often that we choose to disregard the other edge cases
- // for now.
- return new CommunicationError();
- }
- } else if (e instanceof JsonRpcTransportTimeOutError) {
- return new CommunicationError();
- } else if (e instanceof NoDaemonError) {
- return e;
- }
-
- return new UnknownError(e.message);
- }
-
- async _authenticate(sharedSecret: string) {
- try {
- await this._daemonRpc.authenticate(sharedSecret);
- log.info('Authenticated with backend');
- } catch (e) {
- log.error(`Failed to authenticate with backend: ${e.message}`);
- throw e;
- }
- }
-}
-
-/*
- * Used to calculate the time to wait before reconnecting
- * the websocket.
- *
- * It uses a linear backoff function that goes from 500ms
- * to 3000ms
- */
-class ReconnectionBackoff {
- _attempt: number;
-
- constructor() {
- this._attempt = 0;
- }
-
- attempt(handler: () => void) {
- setTimeout(handler, this._getIncreasedBackoff());
- }
-
- reset() {
- this._attempt = 0;
- }
-
- _getIncreasedBackoff() {
- if (this._attempt < 6) {
- this._attempt++;
- }
- return this._attempt * 500;
- }
-}
diff --git a/app/lib/reconnection-backoff.js b/app/lib/reconnection-backoff.js
new file mode 100644
index 0000000000..1cd8203dac
--- /dev/null
+++ b/app/lib/reconnection-backoff.js
@@ -0,0 +1,26 @@
+/*
+ * Used to calculate the time to wait before reconnecting to the daemon.
+ * It uses a linear backoff function that goes from 500ms to 3000ms.
+ */
+export default class ReconnectionBackoff {
+ _attempt: number;
+
+ constructor() {
+ this._attempt = 0;
+ }
+
+ attempt(handler: () => void) {
+ setTimeout(handler, this._getIncreasedBackoff());
+ }
+
+ reset() {
+ this._attempt = 0;
+ }
+
+ _getIncreasedBackoff() {
+ if (this._attempt < 6) {
+ this._attempt++;
+ }
+ return this._attempt * 500;
+ }
+}
diff --git a/app/redux/account/actions.js b/app/redux/account/actions.js
index df25c1e823..41fbfdee05 100644
--- a/app/redux/account/actions.js
+++ b/app/redux/account/actions.js
@@ -1,7 +1,6 @@
// @flow
import type { AccountToken } from '../../lib/daemon-rpc';
-import type { Backend } from '../../lib/backend';
type StartLoginAction = {
type: 'START_LOGIN',
@@ -109,12 +108,7 @@ function updateAccountExpiry(expiry: string): UpdateAccountExpiryAction {
};
}
-const login = (backend: Backend, account: string) => () => backend.login(account);
-const logout = (backend: Backend) => () => backend.logout();
-
export default {
- login,
- logout,
startLogin,
loginSuccessful,
loginFailed,
diff --git a/app/routes.js b/app/routes.js
index c48e03dac3..95ad24e876 100644
--- a/app/routes.js
+++ b/app/routes.js
@@ -15,10 +15,10 @@ import SelectLocationPage from './containers/SelectLocationPage';
import { getTransitionProps } from './transitions';
import type { ReduxGetState } from './redux/store';
-import type { Backend } from './lib/backend';
+import type App from './app';
export type SharedRouteProps = {
- backend: Backend,
+ app: App,
};
export default function makeRoutes(
diff --git a/package.json b/package.json
index e8ede62cc7..2128fbf2aa 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,7 @@
"react-router": "^4.2.0",
"react-router-redux": "^5.0.0-alpha.9",
"react-simple-maps": "^0.10.1",
- "reactxp": "1.3.0-rc.6",
+ "reactxp": "^1.3.0",
"redux": "^3.0.0",
"redux-thunk": "^2.2.0",
"uuid": "^3.0.1",
diff --git a/yarn.lock b/yarn.lock
index e25dbfd6d7..53d87f03bc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -86,14 +86,14 @@
lodash "^4.2.0"
to-fast-properties "^2.0.0"
+"@types/lodash@4.14.110":
+ version "4.14.110"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.110.tgz#fb07498f84152947f30ea09d89207ca07123461e"
+
"@types/lodash@^4.14.64":
version "4.14.91"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.91.tgz#794611b28056d16b5436059c6d800b39d573cd3a"
-"@types/lodash@^4.14.80":
- version "4.14.105"
- resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.105.tgz#9fcc4627a1f98f8f8fce79ddb2bff4afd97e959b"
-
"@types/node@*":
version "8.5.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.5.2.tgz#83b8103fa9a2c2e83d78f701a9aa7c9539739aa5"
@@ -102,16 +102,16 @@
version "8.9.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.9.4.tgz#dfd327582a06c114eb6e0441fa3d6fab35edad48"
-"@types/react-dom@^16.0.6":
+"@types/react-dom@16.0.6":
version "16.0.6"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.6.tgz#f1a65a4e7be8ed5d123f8b3b9eacc913e35a1a3c"
dependencies:
"@types/node" "*"
"@types/react" "*"
-"@types/react-native@^0.55.23":
- version "0.55.25"
- resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.55.25.tgz#68484f8c3f1ab0ccce21ea556dcbcf20140f0c63"
+"@types/react-native@0.55.26":
+ version "0.55.26"
+ resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.55.26.tgz#a7150ca15e0de7e435cc66a1ca44f6b506c99e40"
dependencies:
"@types/react" "*"
@@ -119,11 +119,9 @@
version "16.0.31"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.31.tgz#5285da62f3ac62b797f6d0729a1d6181f3180c3e"
-"@types/react@^16.3.17":
- version "16.3.17"
- resolved "https://registry.yarnpkg.com/@types/react/-/react-16.3.17.tgz#d59d1a632570b0713946ed9c2949d994773633c5"
- dependencies:
- csstype "^2.2.0"
+"@types/react@16.0.36":
+ version "16.0.36"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.36.tgz#ceb5639013bdb92a94147883052e69bb2c22c69b"
abbrev@1:
version "1.1.1"
@@ -374,7 +372,7 @@ assert-plus@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
-assert@^1.3.0:
+assert@1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
dependencies:
@@ -2141,10 +2139,6 @@ css-what@2.1:
version "2.1.0"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd"
-csstype@^2.2.0:
- version "2.5.3"
- resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.3.tgz#2504152e6e1cc59b32098b7f5d6a63f16294c1f7"
-
csurf@~1.8.3:
version "1.8.3"
resolved "https://registry.yarnpkg.com/csurf/-/csurf-1.8.3.tgz#23f2a13bf1d8fce1d0c996588394442cba86a56a"
@@ -4898,6 +4892,10 @@ lodash.templatesettings@^3.0.0:
lodash._reinterpolate "^3.0.0"
lodash.escape "^3.0.0"
+lodash@4.17.10, lodash@^4.17.10, lodash@^4.17.4:
+ version "4.17.10"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
+
lodash@^3.1.0, lodash@^3.10.1, lodash@^3.2.0, lodash@^3.3.1, lodash@^3.5.0:
version "3.10.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
@@ -4910,10 +4908,6 @@ lodash@^4.14.0, lodash@^4.15.0, lodash@^4.16.6, lodash@^4.2.1, lodash@^4.3.0, lo
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
-lodash@^4.17.10, lodash@^4.17.4:
- version "4.17.10"
- resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
-
log-symbols@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
@@ -6040,7 +6034,14 @@ promise@^7.1.1:
dependencies:
asap "~2.0.3"
-prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0:
+prop-types@15.6.2:
+ version "15.6.2"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
+ dependencies:
+ loose-envify "^1.3.1"
+ object-assign "^4.1.1"
+
+prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0:
version "15.6.0"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
dependencies:
@@ -6350,20 +6351,20 @@ react@^16.0.0:
object-assign "^4.1.1"
prop-types "^15.6.0"
-reactxp@1.3.0-rc.6:
- version "1.3.0-rc.6"
- resolved "https://registry.yarnpkg.com/reactxp/-/reactxp-1.3.0-rc.6.tgz#11e68d2e450926b7b51d507300041912eb3dfc61"
+reactxp@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/reactxp/-/reactxp-1.3.0.tgz#ccb3859b7713ea0cb921fa3155c84f02b36b0f17"
dependencies:
- "@types/lodash" "^4.14.80"
- "@types/react" "^16.3.17"
- "@types/react-dom" "^16.0.6"
- "@types/react-native" "^0.55.23"
- assert "^1.3.0"
- lodash "^4.17.4"
- prop-types "^15.5.9"
- rebound "^0.1.0"
- subscribableevent "^1.0.0"
- synctasks "^0.3.3"
+ "@types/lodash" "4.14.110"
+ "@types/react" "16.0.36"
+ "@types/react-dom" "16.0.6"
+ "@types/react-native" "0.55.26"
+ assert "1.4.1"
+ lodash "4.17.10"
+ prop-types "15.6.2"
+ rebound "0.1.0"
+ subscribableevent "1.0.0"
+ synctasks "0.3.3"
read-all-stream@^3.0.0:
version "3.1.0"
@@ -6520,7 +6521,7 @@ readline2@^1.0.1:
is-fullwidth-code-point "^1.0.0"
mute-stream "0.0.5"
-rebound@^0.1.0:
+rebound@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/rebound/-/rebound-0.1.0.tgz#0638c61a93666bb515a58a03e1cfb34021e88b72"
@@ -7387,7 +7388,7 @@ strip-outer@^1.0.0:
dependencies:
escape-string-regexp "^1.0.2"
-subscribableevent@^1.0.0:
+subscribableevent@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/subscribableevent/-/subscribableevent-1.0.0.tgz#bde9500fa9009c7740c924109bac6119cd9898e6"
dependencies:
@@ -7443,7 +7444,7 @@ sync-exec@~0.6.x:
version "0.6.2"
resolved "https://registry.yarnpkg.com/sync-exec/-/sync-exec-0.6.2.tgz#717d22cc53f0ce1def5594362f3a89a2ebb91105"
-synctasks@^0.3.3:
+synctasks@0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/synctasks/-/synctasks-0.3.3.tgz#1e3dde423b39d28bc940fdb7698d8b4b7a741e77"