summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2018-08-08 16:53:23 +0200
committerAndrej Mihajlov <and@mullvad.net>2018-08-08 16:53:23 +0200
commitac98e48c68eadfdd7250eccd38aa492e6f830744 (patch)
tree05bfd7a9f05456220f11f40329093b127a5b20a2
parentd53fda746465c0bb6525371b9d780cbfac90f942 (diff)
parenta8f2831cd50e98b1d126b8c3169adcf1ef75b8a2 (diff)
downloadmullvadvpn-ac98e48c68eadfdd7250eccd38aa492e6f830744.tar.xz
mullvadvpn-ac98e48c68eadfdd7250eccd38aa492e6f830744.zip
Merge branch 'remove-auto-login'
-rw-r--r--.eslintrc15
-rw-r--r--CHANGELOG.md6
-rw-r--r--app/app.js148
-rw-r--r--app/components/Account.js27
-rw-r--r--app/components/AccountStyles.js1
-rw-r--r--app/components/Cell.js1
-rw-r--r--app/components/Connect.js41
-rw-r--r--app/components/CustomScrollbars.js2
-rw-r--r--app/components/HeaderBar.js163
-rw-r--r--app/components/HeaderBarPlatformStyles.android.js2
-rw-r--r--app/components/HeaderBarPlatformStyles.js16
-rw-r--r--app/components/HeaderBarStyles.js53
-rw-r--r--app/components/Img.js8
-rw-r--r--app/components/Launch.js58
-rw-r--r--app/components/Layout.js11
-rw-r--r--app/components/Login.js6
-rw-r--r--app/components/LoginStyles.js24
-rw-r--r--app/components/Settings.js43
-rw-r--r--app/containers/AccountPage.js6
-rw-r--r--app/containers/AdvancedSettingsPage.js9
-rw-r--r--app/containers/ConnectPage.js5
-rw-r--r--app/containers/LaunchPage.js23
-rw-r--r--app/containers/LoginPage.js4
-rw-r--r--app/containers/PreferencesPage.js8
-rw-r--r--app/containers/SelectLocationPage.js10
-rw-r--r--app/containers/SettingsPage.js23
-rw-r--r--app/containers/SupportPage.js6
-rw-r--r--app/lib/daemon-rpc.js2
-rw-r--r--app/lib/jsonrpc-transport.js117
-rw-r--r--app/lib/reconnection-backoff.js8
-rw-r--r--app/lib/window-state-observer.js64
-rw-r--r--app/redux/account/actions.js39
-rw-r--r--app/redux/account/reducers.js1
-rw-r--r--app/redux/connection/actions.js22
-rw-r--r--app/redux/daemon/actions.js24
-rw-r--r--app/redux/daemon/reducers.js33
-rw-r--r--app/redux/store.js14
-rw-r--r--app/routes.js29
-rw-r--r--init.js2
-rw-r--r--package.json14
-rw-r--r--scripts/serve.js63
-rw-r--r--test/components/Account.spec.js3
-rw-r--r--test/components/Connect.spec.js7
-rw-r--r--test/components/HeaderBar.spec.js44
-rw-r--r--test/components/Settings.spec.js219
-rw-r--r--test/jsonrpc-transport.spec.js128
-rw-r--r--yarn.lock166
47 files changed, 1057 insertions, 661 deletions
diff --git a/.eslintrc b/.eslintrc
index fdb929fb1d..0f54d38f8b 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -1,9 +1,14 @@
{
+ "root": true,
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:promise/recommended",
- "prettier"
+ "plugin:flowtype/recommended",
+ "prettier",
+ "prettier/flowtype",
+ "prettier/react",
+ "prettier/standard"
],
"parser": "babel-eslint",
"parserOptions": {
@@ -12,15 +17,17 @@
"modules": true
}
},
- "plugins": ["react", "flowtype", "promise"],
+ "plugins": [
+ "react",
+ "flowtype",
+ "promise"
+ ],
"rules": {
"prefer-const": "warn",
"no-console": "off",
"no-loop-func": "warn",
- "new-cap": "off",
"no-param-reassign": "warn",
"func-names": "off",
- "comma-spacing": "warn",
"no-unused-expressions": "error",
"no-unused-vars": [
"error",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 319ad014fe..38c14687b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,12 +29,13 @@ Line wrap the file at 100 chars. Th
- Account token can be copied to the clipboard by clicking on it in the account settings screen.
- Automatically scroll to selected country/city in locations view.
- Show system notifications when connection state changes.
+- Add launch view displayed when connecting to system service.
### Changed
- Format the expiry date and time using the system locale.
- Account tokens are now required to have at least ten digits.
-### macOS
+#### macOS
- Rename directores for settings, logs and cache from `mullvad-daemon` to `mullvad-vpn`.
#### Windows
@@ -46,7 +47,8 @@ Line wrap the file at 100 chars. Th
### Fixed
- Ignore empty strings as redaction requests in the problem report tool, to avoid adding redacted
markers between every character of the log message.
-
+- Previously logged in users won't be going through login view when restarting the app, instead
+ will be taken straight to main view.
## [2018.2-beta2] - 2018-07-18
### Added
diff --git a/app/app.js b/app/app.js
index 3171d69b76..369c51be2d 100644
--- a/app/app.js
+++ b/app/app.js
@@ -3,7 +3,11 @@
import React from 'react';
import { bindActionCreators } from 'redux';
import { Provider } from 'react-redux';
-import { ConnectedRouter, push as pushHistory } from 'connected-react-router';
+import {
+ ConnectedRouter,
+ push as pushHistory,
+ replace as replaceHistory,
+} from 'connected-react-router';
import { createMemoryHistory } from 'history';
import { webFrame, ipcRenderer } from 'electron';
@@ -19,10 +23,12 @@ import configureStore from './redux/store';
import accountActions from './redux/account/actions';
import connectionActions from './redux/connection/actions';
import settingsActions from './redux/settings/actions';
+import daemonActions from './redux/daemon/actions';
import type { RpcCredentials } from './lib/rpc-address-file';
import type {
DaemonRpcProtocol,
+ AccountData,
ConnectionObserver as DaemonConnectionObserver,
} from './lib/daemon-rpc';
import type { ReduxStore } from './redux/store';
@@ -40,6 +46,7 @@ export default class AppRenderer {
_memoryHistory = createMemoryHistory();
_reduxStore: ReduxStore;
_reduxActions: *;
+ _accountDataState = new AccountDataState();
constructor() {
const store = configureStore(null, this._memoryHistory);
@@ -50,7 +57,14 @@ export default class AppRenderer {
account: bindActionCreators(accountActions, dispatch),
connection: bindActionCreators(connectionActions, dispatch),
settings: bindActionCreators(settingsActions, dispatch),
- history: bindActionCreators({ push: pushHistory }, dispatch),
+ daemon: bindActionCreators(daemonActions, dispatch),
+ history: bindActionCreators(
+ {
+ push: pushHistory,
+ replace: replaceHistory,
+ },
+ dispatch,
+ ),
};
this._openConnectionObserver = this._daemonRpc.addOpenConnectionObserver(() => {
@@ -110,23 +124,27 @@ export default class AppRenderer {
async login(accountToken: AccountToken) {
const actions = this._reduxActions;
+ const history = this._memoryHistory;
actions.account.startLogin(accountToken);
- log.debug('Attempting to login');
+ log.debug('Logging in');
try {
const accountData = await this._daemonRpc.getAccountData(accountToken);
await this._daemonRpc.setAccount(accountToken);
- actions.account.loginSuccessful(accountData.expiry);
+ actions.account.updateAccountExpiry(accountData.expiry);
+ actions.account.loginSuccessful();
// Redirect the user after some time to allow for
// the 'Login Successful' screen to be visible
setTimeout(async () => {
- actions.history.push('/connect');
+ if (history.location.pathname === '/login') {
+ actions.history.replace('/connect');
+ }
try {
- log.debug('Auto-connecting the tunnel...');
+ log.debug('Auto-connecting the tunnel');
await this.connectTunnel();
} catch (error) {
log.error(`Failed to auto-connect the tunnel: ${error.message}`);
@@ -139,33 +157,33 @@ export default class AppRenderer {
}
}
- async _autologin() {
+ async _restoreSession() {
const actions = this._reduxActions;
- actions.account.startLogin();
+ const history = this._memoryHistory;
- log.debug('Attempting to log in automatically');
-
- try {
- const accountToken = await this._daemonRpc.getAccount();
- if (!accountToken) {
- throw new NoAccountError();
- }
+ log.debug('Restoring session');
- log.debug(`The daemon had an account number stored: ${accountToken}`);
- actions.account.startLogin(accountToken);
+ const accountToken = await this._daemonRpc.getAccount();
- const accountData = await this._daemonRpc.getAccountData(accountToken);
- log.debug('The stored account number still exists:', accountData);
+ if (accountToken) {
+ log.debug(`Got account token: ${accountToken}`);
+ actions.account.updateAccountToken(accountToken);
+ actions.account.loginSuccessful();
- actions.account.loginSuccessful(accountData.expiry);
- actions.history.push('/connect');
- } catch (e) {
- log.warn('Unable to autologin,', e.message);
+ // take user to main view if user is still at launch screen `/`
+ if (history.location.pathname === '/') {
+ actions.history.replace('/connect');
+ }
+ } else {
+ log.debug('No account set, showing login view.');
- actions.account.autoLoginFailed();
- actions.history.push('/');
+ // show window when account is not set
+ ipcRenderer.send('show-window');
- throw e;
+ // take user to `/login` screen if user is at launch screen `/`
+ if (history.location.pathname === '/') {
+ actions.history.replace('/login');
+ }
}
}
@@ -179,7 +197,10 @@ export default class AppRenderer {
this._fetchAccountHistory(),
]);
actions.account.loggedOut();
- actions.history.push('/');
+ actions.history.replace('/login');
+
+ // reset account data state on log out
+ this._accountDataState = new AccountDataState();
} catch (e) {
log.info('Failed to logout: ', e.message);
}
@@ -262,13 +283,29 @@ export default class AppRenderer {
async updateAccountExpiry() {
const actions = this._reduxActions;
+ const accountDataState = this._accountDataState;
+
+ // Bail if something else requested an update to account data
+ // or if account data cache was updated recently.
+ if (accountDataState.isUpdating() || !accountDataState.needsUpdate()) {
+ return;
+ }
+
try {
const accountToken = await this._daemonRpc.getAccount();
if (!accountToken) {
throw new NoAccountError();
}
- const accountData = await this._daemonRpc.getAccountData(accountToken);
- actions.account.updateAccountExpiry(accountData.expiry);
+
+ const accountData = await accountDataState.update(() => {
+ return this._daemonRpc.getAccountData(accountToken);
+ });
+
+ // Check if account token is still the same after receiving account data
+ const currentAccountToken = this._reduxStore.getState().account.accountToken;
+ if (currentAccountToken === accountToken) {
+ actions.account.updateAccountExpiry(accountData.expiry);
+ }
} catch (e) {
log.error(`Failed to update account expiry: ${e.message}`);
}
@@ -370,6 +407,10 @@ export default class AppRenderer {
}
async _onOpenConnection() {
+ // save to redux that the app connected to daemon
+ this._reduxActions.daemon.connected();
+
+ // reset the reconnect backoff when connection established.
this._reconnectBackoff.reset();
// authenticate once connected
@@ -383,16 +424,11 @@ export default class AppRenderer {
log.error(`Cannot authenticate: ${error.message}`);
}
- // autologin
+ // attempt to restore the session
try {
- await this._autologin();
+ await this._restoreSession();
} 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}`);
- }
+ log.error(`Failed to restore session: ${error.message}`);
}
// make sure to re-subscribe to state notifications when connection is re-established.
@@ -422,6 +458,12 @@ export default class AppRenderer {
}
async _onCloseConnection(error: ?Error) {
+ const actions = this._reduxActions;
+
+ // save to redux that the app disconnected from daemon
+ actions.daemon.disconnected();
+
+ // recover connection on error
if (error) {
log.debug(`Lost connection to daemon: ${error.message}`);
@@ -436,6 +478,9 @@ export default class AppRenderer {
this._reconnectBackoff.attempt(() => {
recover();
});
+
+ // take user back to the launch screen `/`.
+ actions.history.replace('/');
}
}
@@ -555,3 +600,32 @@ export default class AppRenderer {
}
}
}
+
+// Helper class to keep track of account data updates
+class AccountDataState {
+ _expiresAt: ?Date;
+ _isUpdating = false;
+
+ isUpdating() {
+ return this._isUpdating;
+ }
+
+ needsUpdate() {
+ return !this._expiresAt || this._expiresAt < new Date();
+ }
+
+ async update(fn: () => Promise<AccountData>): Promise<AccountData> {
+ this._isUpdating = true;
+
+ try {
+ const accountData = await fn();
+ this._expiresAt = new Date(Date.now() + 60 * 1000); // 60s expiration
+
+ return accountData;
+ } catch (error) {
+ throw error;
+ } finally {
+ this._isUpdating = false;
+ }
+ }
+}
diff --git a/app/components/Account.js b/app/components/Account.js
index 2edf277fa4..700f33af2b 100644
--- a/app/components/Account.js
+++ b/app/components/Account.js
@@ -1,7 +1,7 @@
// @flow
import moment from 'moment';
import * as React from 'react';
-import { Component, Text, View, App, Types } from 'reactxp';
+import { Component, Text, View } from 'reactxp';
import * as AppButton from './AppButton';
import { Layout, Container } from './Layout';
import NavigationBar, { BackBarItem } from './NavigationBar';
@@ -9,10 +9,11 @@ import SettingsHeader, { HeaderTitle } from './SettingsHeader';
import styles from './AccountStyles';
import Img from './Img';
import { formatAccount } from '../lib/formatters';
+import WindowStateObserver from '../lib/window-state-observer';
import type { AccountToken } from '../lib/daemon-rpc';
-export type AccountProps = {
+type Props = {
accountToken: AccountToken,
accountExpiry: string,
expiryLocale: string,
@@ -28,27 +29,23 @@ type State = {
showAccountTokenCopiedMessage: boolean,
};
-export default class Account extends Component<AccountProps, State> {
+export default class Account extends Component<Props, State> {
state = {
isRefreshingExpiry: false,
showAccountTokenCopiedMessage: false,
};
- _activationStateToken: ?Types.SubscriptionToken;
-
_isMounted = false;
-
_copyTimer: ?TimeoutID;
+ _windowStateObserver = new WindowStateObserver();
componentDidMount() {
this._isMounted = true;
this._refreshAccountExpiry();
- this._activationStateToken = App.activationStateChangedEvent.subscribe((activationState) => {
- if (activationState === Types.AppActivationState.Active) {
- this._refreshAccountExpiry();
- }
- });
+ this._windowStateObserver.onShow = () => {
+ this._refreshAccountExpiry();
+ };
}
componentWillUnmount() {
@@ -58,11 +55,7 @@ export default class Account extends Component<AccountProps, State> {
clearTimeout(this._copyTimer);
}
- const activationStateToken = this._activationStateToken;
- if (activationStateToken) {
- activationStateToken.unsubscribe();
- this._activationStateToken = null;
- }
+ this._windowStateObserver.dispose();
}
onAccountTokenClick() {
@@ -119,7 +112,7 @@ export default class Account extends Component<AccountProps, State> {
<Text style={styles.account__row_label}>Paid until</Text>
{isOutOfTime ? (
<Text style={styles.account__out_of_time} testName="account__out_of_time">
- OUT OF TIME
+ {'OUT OF TIME'}
</Text>
) : (
<Text style={styles.account__row_value}>{formattedExpiry}</Text>
diff --git a/app/components/AccountStyles.js b/app/components/AccountStyles.js
index 14fc478a54..a96a8fcff6 100644
--- a/app/components/AccountStyles.js
+++ b/app/components/AccountStyles.js
@@ -56,6 +56,7 @@ export default {
account__row_value: {
fontFamily: 'Open Sans',
fontSize: 16,
+ lineHeight: 19,
fontWeight: '800',
color: colors.white,
},
diff --git a/app/components/Cell.js b/app/components/Cell.js
index 2b3c06a3b1..80b783352f 100644
--- a/app/components/Cell.js
+++ b/app/components/Cell.js
@@ -19,6 +19,7 @@ const styles = {
flexDirection: 'row',
alignItems: 'center',
alignContent: 'center',
+ cursor: 'default',
},
cellHover: {
backgroundColor: colors.blue80,
diff --git a/app/components/Connect.js b/app/components/Connect.js
index 037ab02495..9d7f52f7b1 100644
--- a/app/components/Connect.js
+++ b/app/components/Connect.js
@@ -3,19 +3,20 @@
import moment from 'moment';
import * as React from 'react';
import { Layout, Container, Header } from './Layout';
+import { SettingsBarButton, Brand } from './HeaderBar';
import { Component, Text, View, Types } from 'reactxp';
import * as AppButton from './AppButton';
import Img from './Img';
import Accordion from './Accordion';
import styles from './ConnectStyles';
-
import { NoCreditError, NoInternetError } from '../errors';
import Map from './Map';
+import WindowStateObserver from '../lib/window-state-observer';
import type { HeaderBarStyle } from './HeaderBar';
import type { ConnectionReduxState } from '../redux/connection/reducers';
-export type ConnectProps = {
+type Props = {
connection: ConnectionReduxState,
accountExpiry: string,
selectedRelayName: string,
@@ -25,22 +26,24 @@ export type ConnectProps = {
onCopyIP: () => void,
onDisconnect: () => void,
onExternalLink: (type: string) => void,
+ updateAccountExpiry: () => Promise<void>,
};
-type ConnectState = {
+type State = {
showCopyIPMessage: boolean,
mapOffset: [number, number],
};
-export default class Connect extends Component<ConnectProps, ConnectState> {
+export default class Connect extends Component<Props, State> {
state = {
showCopyIPMessage: false,
mapOffset: [0, 0],
};
_copyTimer: ?TimeoutID;
+ _windowStateObserver = new WindowStateObserver();
- shouldComponentUpdate(nextProps: ConnectProps, nextState: ConnectState) {
+ shouldComponentUpdate(nextProps: Props, nextState: State) {
const { connection: prevConnection, ...otherPrevProps } = this.props;
const { connection: nextConnection, ...otherNextProps } = nextProps;
@@ -56,10 +59,20 @@ export default class Connect extends Component<ConnectProps, ConnectState> {
);
}
+ componentDidMount() {
+ this.props.updateAccountExpiry();
+
+ this._windowStateObserver.onShow = () => {
+ this.props.updateAccountExpiry();
+ };
+ }
+
componentWillUnmount() {
if (this._copyTimer) {
clearTimeout(this._copyTimer);
}
+
+ this._windowStateObserver.dispose();
}
render() {
@@ -68,12 +81,10 @@ export default class Connect extends Component<ConnectProps, ConnectState> {
return (
<Layout>
- <Header
- style={this.headerStyle()}
- showSettings={true}
- onSettings={this.props.onSettings}
- testName="header"
- />
+ <Header barStyle={this.headerBarStyle()} testName="header">
+ <Brand />
+ <SettingsBarButton onPress={this.props.onSettings} />
+ </Header>
<Container>{child}</Container>
</Layout>
);
@@ -324,15 +335,17 @@ export default class Connect extends Component<ConnectProps, ConnectState> {
// Private
- headerStyle(): HeaderBarStyle {
- switch (this.props.connection.status) {
+ headerBarStyle(): HeaderBarStyle {
+ const { status } = this.props.connection;
+ switch (status) {
case 'disconnected':
return 'error';
case 'connecting':
case 'connected':
return 'success';
+ default:
+ throw new Error(`Invalid ConnectionState: ${(status: empty)}`);
}
- throw new Error('Invalid ConnectionState');
}
networkSecurityStyle(): Types.Style {
diff --git a/app/components/CustomScrollbars.js b/app/components/CustomScrollbars.js
index be74bfddb5..a149faff75 100644
--- a/app/components/CustomScrollbars.js
+++ b/app/components/CustomScrollbars.js
@@ -176,7 +176,7 @@ export default class CustomScrollbars extends React.Component<Props, State> {
return offsetTop - (scrollable.offsetHeight - child.clientHeight) * 0.5;
default:
- throw new Error(`Unknown enum type for ScrollPosition: ${scrollPosition}`);
+ throw new Error(`Unknown enum type for ScrollPosition: ${(scrollPosition: empty)}`);
}
}
diff --git a/app/components/HeaderBar.js b/app/components/HeaderBar.js
index a46856eb46..02b1d86e31 100644
--- a/app/components/HeaderBar.js
+++ b/app/components/HeaderBar.js
@@ -1,55 +1,148 @@
// @flow
import React from 'react';
-import { Component, Text, Button, View } from 'reactxp';
-
+import { Component, Text, Button, View, Styles } from 'reactxp';
import Img from './Img';
-
-import styles from './HeaderBarStyles';
-import platformStyles from './HeaderBarPlatformStyles';
+import { colors } from '../config';
export type HeaderBarStyle = 'default' | 'defaultDark' | 'error' | 'success';
-export type HeaderBarProps = {
- style: HeaderBarStyle,
- showSettings: boolean,
- onSettings: ?() => void,
+type HeaderBarProps = {
+ barStyle: HeaderBarStyle,
+};
+
+const headerBarStyles = {
+ container: {
+ base: Styles.createViewStyle({
+ paddingTop: 12,
+ paddingBottom: 12,
+ paddingLeft: 12,
+ paddingRight: 12,
+ }),
+ platformOverride: {
+ darwin: Styles.createViewStyle({
+ paddingTop: 24,
+ }),
+ linux: Styles.createViewStyle({
+ WebkitAppRegion: 'drag',
+ }),
+ },
+ },
+ content: Styles.createViewStyle({
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'flex-end',
+ // the size of "brand" logo
+ minHeight: 51,
+ }),
+ barStyle: {
+ default: Styles.createViewStyle({
+ backgroundColor: colors.blue,
+ }),
+ defaultDark: Styles.createViewStyle({
+ backgroundColor: colors.darkBlue,
+ }),
+ error: Styles.createViewStyle({
+ backgroundColor: colors.red,
+ }),
+ success: Styles.createViewStyle({
+ backgroundColor: colors.green,
+ }),
+ },
};
export default class HeaderBar extends Component<HeaderBarProps> {
static defaultProps: HeaderBarProps = {
- style: 'default',
- showSettings: false,
- onSettings: null,
+ barStyle: 'default',
};
render() {
- const containerClass = [
- styles['headerbar'],
- platformStyles[process.platform],
- styles['style_' + this.props.style],
+ const style = [
+ headerBarStyles.container.base,
+ headerBarStyles.container.platformOverride[process.platform],
+ headerBarStyles.barStyle[this.props.barStyle],
+ this.props.style,
];
return (
- <View style={containerClass}>
- <View style={styles.container} testName="headerbar__container">
- <Img height={50} width={50} source="logo-icon" />
- <Text style={styles.title}>MULLVAD VPN</Text>
- </View>
+ <View style={style}>
+ <View style={headerBarStyles.content}>{this.props.children}</View>
+ </View>
+ );
+ }
+}
- {this.props.showSettings ? (
- <Button
- style={styles.settings}
- onPress={this.props.onSettings}
- testName="headerbar__settings">
- <Img
- height={24}
- width={24}
- source="icon-settings"
- style={[styles.settings_icon, platformStyles.settings_icon]}
- hoverStyle={styles.settings_icon_hover}
- />
- </Button>
- ) : null}
+const brandStyles = {
+ container: Styles.createViewStyle({
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ }),
+ title: Styles.createTextStyle({
+ fontFamily: 'DINPro',
+ fontSize: 24,
+ fontWeight: '900',
+ lineHeight: 30,
+ letterSpacing: -0.5,
+ color: colors.white60,
+ marginLeft: 8,
+ }),
+};
+
+export class Brand extends Component {
+ render() {
+ return (
+ <View style={brandStyles.container} testName="headerbar__container">
+ <Img width={50} height={50} source="logo-icon" />
+ <Text style={brandStyles.title}>{'MULLVAD VPN'}</Text>
</View>
);
}
}
+
+type SettingsButtonProps = {
+ onPress: ?() => void,
+};
+
+const settingsBarButtonStyles = {
+ container: {
+ base: Styles.createViewStyle({
+ cursor: 'default',
+ padding: 0,
+ marginLeft: 8,
+ }),
+ platformOverride: {
+ linux: Styles.createViewStyle({
+ WebkitAppRegion: 'no-drag',
+ }),
+ },
+ },
+ icon: {
+ normal: Styles.createViewStyle({
+ color: colors.white60,
+ }),
+ hover: Styles.createViewStyle({
+ color: colors.white,
+ }),
+ },
+};
+
+export class SettingsBarButton extends Component<SettingsButtonProps> {
+ render() {
+ return (
+ <Button
+ style={[
+ settingsBarButtonStyles.container.base,
+ settingsBarButtonStyles.container.platformOverride[process.platform],
+ ]}
+ onPress={this.props.onPress}
+ testName="headerbar__settings">
+ <Img
+ height={24}
+ width={24}
+ source="icon-settings"
+ style={settingsBarButtonStyles.icon.normal}
+ hoverStyle={settingsBarButtonStyles.icon.hover}
+ />
+ </Button>
+ );
+ }
+}
diff --git a/app/components/HeaderBarPlatformStyles.android.js b/app/components/HeaderBarPlatformStyles.android.js
deleted file mode 100644
index f2f97beb65..0000000000
--- a/app/components/HeaderBarPlatformStyles.android.js
+++ /dev/null
@@ -1,2 +0,0 @@
-import { createViewStyles } from '../lib/styles';
-export default { ...createViewStyles({}) };
diff --git a/app/components/HeaderBarPlatformStyles.js b/app/components/HeaderBarPlatformStyles.js
deleted file mode 100644
index c60967797a..0000000000
--- a/app/components/HeaderBarPlatformStyles.js
+++ /dev/null
@@ -1,16 +0,0 @@
-// @flow
-import { createViewStyles } from '../lib/styles';
-
-export default {
- ...createViewStyles({
- darwin: {
- paddingTop: 24,
- },
- linux: {
- WebkitAppRegion: 'drag',
- },
- settings_icon: {
- WebkitAppRegion: 'no-drag',
- },
- }),
-};
diff --git a/app/components/HeaderBarStyles.js b/app/components/HeaderBarStyles.js
deleted file mode 100644
index 7d3c94cdc9..0000000000
--- a/app/components/HeaderBarStyles.js
+++ /dev/null
@@ -1,53 +0,0 @@
-// @flow
-import { createTextStyles, createViewStyles } from '../lib/styles';
-import { colors } from '../config';
-
-export default {
- ...createViewStyles({
- headerbar: {
- paddingTop: 12,
- paddingBottom: 12,
- paddingLeft: 12,
- paddingRight: 12,
- backgroundColor: colors.blue,
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- },
- style_defaultDark: {
- backgroundColor: colors.darkBlue,
- },
- style_error: {
- backgroundColor: colors.red,
- },
- style_success: {
- backgroundColor: colors.green,
- },
- container: {
- display: 'flex',
- flexDirection: 'row',
- alignItems: 'center',
- },
- settings: {
- cursor: 'default',
- padding: 0,
- },
- settings_icon: {
- color: colors.white60,
- },
- settings_icon_hover: {
- color: colors.white,
- },
- }),
- ...createTextStyles({
- title: {
- fontFamily: 'DINPro',
- fontSize: 24,
- fontWeight: '900',
- lineHeight: 30,
- letterSpacing: -0.5,
- color: colors.white60,
- marginLeft: 8,
- },
- }),
-};
diff --git a/app/components/Img.js b/app/components/Img.js
index 1d6f6613f4..6b34f99360 100644
--- a/app/components/Img.js
+++ b/app/components/Img.js
@@ -4,6 +4,8 @@ import { View, Component, Types } from 'reactxp';
type Props = {
source: string,
+ width?: number,
+ heigth?: number,
tintColor?: string,
hoverStyle?: Types.ViewStyle,
disabled?: boolean,
@@ -20,7 +22,7 @@ export default class Img extends Component<Props, State> {
getHoverStyle = () => (this.state.hovered ? this.props.hoverStyle || null : null);
render() {
- const { source, style, onMouseEnter, onMouseLeave, ...otherProps } = this.props;
+ const { source, width, heigth, style, onMouseEnter, onMouseLeave, ...otherProps } = this.props;
const tintColor = this.props.tintColor;
const url = './assets/images/' + source + '.svg';
let image;
@@ -36,6 +38,8 @@ export default class Img extends Component<Props, State> {
}}>
<img
src={url}
+ width={width}
+ height={heigth}
style={{
visibility: 'hidden',
}}
@@ -43,7 +47,7 @@ export default class Img extends Component<Props, State> {
</div>
);
} else {
- image = <img src={url} />;
+ image = <img src={url} width={width} height={heigth} />;
}
return (
diff --git a/app/components/Launch.js b/app/components/Launch.js
new file mode 100644
index 0000000000..bab877dd53
--- /dev/null
+++ b/app/components/Launch.js
@@ -0,0 +1,58 @@
+// @flow
+import * as React from 'react';
+import { Component, Styles, View, Text } from 'reactxp';
+import { Layout, Container, Header } from './Layout';
+import { SettingsBarButton } from './HeaderBar';
+import Img from './Img';
+import { colors } from '../config';
+
+const styles = {
+ container: Styles.createViewStyle({
+ flex: 1,
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginTop: -150,
+ }),
+ logo: Styles.createViewStyle({
+ marginBottom: 4,
+ }),
+ title: Styles.createTextStyle({
+ fontFamily: 'DINPro',
+ fontSize: 24,
+ fontWeight: '900',
+ lineHeight: 30,
+ letterSpacing: -0.5,
+ color: colors.white60,
+ marginBottom: 4,
+ }),
+ subtitle: Styles.createTextStyle({
+ fontFamily: 'Open Sans',
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.white40,
+ }),
+};
+
+type Props = {
+ openSettings: () => void,
+};
+
+export default class Launch extends Component<Props> {
+ render() {
+ return (
+ <Layout>
+ <Header>
+ <SettingsBarButton onPress={this.props.openSettings} />
+ </Header>
+ <Container>
+ <View style={styles.container} testName="headerbar__container">
+ <Img height={120} width={120} source="logo-icon" style={styles.logo} />
+ <Text style={styles.title}>{'MULLVAD VPN'}</Text>
+ <Text style={styles.subtitle}>{'Connecting to daemon...'}</Text>
+ </View>
+ </Container>
+ </Layout>
+ );
+ }
+}
diff --git a/app/components/Layout.js b/app/components/Layout.js
index 2699b3e43d..1eb6984797 100644
--- a/app/components/Layout.js
+++ b/app/components/Layout.js
@@ -1,19 +1,16 @@
// @flow
import * as React from 'react';
-import HeaderBar from './HeaderBar';
import { View, Component } from 'reactxp';
-
-import type { HeaderBarProps } from './HeaderBar';
-
+import HeaderBar from './HeaderBar';
import styles from './LayoutStyles';
-export class Header extends Component<HeaderBarProps> {
+export class Header extends Component<React.ElementProps<typeof HeaderBar>> {
static defaultProps = HeaderBar.defaultProps;
render() {
return (
- <View style={styles.header}>
- <HeaderBar {...this.props} />
+ <View style={[styles.header, this.props.style]}>
+ <HeaderBar barStyle={this.props.barStyle}>{this.props.children}</HeaderBar>
</View>
);
}
diff --git a/app/components/Login.js b/app/components/Login.js
index d902bba1bc..b410f80553 100644
--- a/app/components/Login.js
+++ b/app/components/Login.js
@@ -2,6 +2,7 @@
import * as React from 'react';
import { Component, Text, View, Animated, Styles, UserInterface } from 'reactxp';
import { Layout, Container, Header } from './Layout';
+import { SettingsBarButton, Brand } from './HeaderBar';
import AccountInput from './AccountInput';
import Accordion from './Accordion';
import { formatAccount } from '../lib/formatters';
@@ -94,7 +95,10 @@ export default class Login extends Component<Props, State> {
render() {
return (
<Layout>
- <Header showSettings={true} onSettings={this.props.openSettings} />
+ <Header>
+ <Brand />
+ <SettingsBarButton onPress={this.props.openSettings} />
+ </Header>
<Container>
<View style={styles.login_form}>
{this._getStatusIcon()}
diff --git a/app/components/LoginStyles.js b/app/components/LoginStyles.js
index d9094a2f13..61e5026a71 100644
--- a/app/components/LoginStyles.js
+++ b/app/components/LoginStyles.js
@@ -14,9 +14,8 @@ export default {
},
status_icon: {
flex: 0,
- height: 48,
marginBottom: 30,
- justifyContent: 'center',
+ alignItems: 'center',
},
login_form: {
flex: 1,
@@ -88,13 +87,15 @@ export default {
backgroundColor: colors.darkBlue,
},
account_dropdown__item: {
- paddingTop: 10,
- paddingRight: 12,
- paddingLeft: 12,
- paddingBottom: 12,
+ paddingTop: 0,
+ paddingRight: 0,
+ paddingLeft: 0,
+ paddingBottom: 0,
marginBottom: 0,
flexDirection: 'row',
+ alignItems: 'stretch',
backgroundColor: colors.white60,
+ cursor: 'default',
},
account_dropdown__item_hover: {
backgroundColor: colors.white40,
@@ -102,6 +103,11 @@ export default {
account_dropdown__remove: {
justifyContent: 'center',
color: colors.blue40,
+ paddingTop: 10,
+ paddingRight: 12,
+ paddingBottom: 12,
+ paddingLeft: 12,
+ marginLeft: 0,
},
account_dropdown__remove_cell_hover: {
color: colors.blue60,
@@ -136,6 +142,7 @@ export default {
subtitle: {
fontFamily: 'Open Sans',
fontSize: 13,
+ lineHeight: 15,
fontWeight: '600',
letterSpacing: -0.2,
color: colors.white80,
@@ -165,6 +172,11 @@ export default {
borderWidth: 0,
textAlign: 'left',
marginLeft: 0,
+ paddingTop: 10,
+ paddingRight: 0,
+ paddingLeft: 12,
+ paddingBottom: 12,
+ cursor: 'default',
},
}),
};
diff --git a/app/components/Settings.js b/app/components/Settings.js
index 4a8784fe0e..0ed45c7685 100644
--- a/app/components/Settings.js
+++ b/app/components/Settings.js
@@ -10,14 +10,14 @@ import SettingsHeader, { HeaderTitle } from './SettingsHeader';
import CustomScrollbars from './CustomScrollbars';
import styles from './SettingsStyles';
import Img from './Img';
+import WindowStateObserver from '../lib/window-state-observer';
-import type { AccountReduxState } from '../redux/account/reducers';
-import type { SettingsReduxState } from '../redux/settings/reducers';
+import type { LoginState } from '../redux/account/reducers';
-export type SettingsProps = {
- account: AccountReduxState,
- settings: SettingsReduxState,
- version: string,
+type Props = {
+ loginState: LoginState,
+ accountExpiry: ?string,
+ appVersion: string,
onQuit: () => void,
onClose: () => void,
onViewAccount: () => void,
@@ -25,9 +25,24 @@ export type SettingsProps = {
onViewPreferences: () => void,
onViewAdvancedSettings: () => void,
onExternalLink: (type: string) => void,
+ updateAccountExpiry: () => Promise<void>,
};
-export default class Settings extends Component<SettingsProps> {
+export default class Settings extends Component<Props> {
+ _windowStateObserver = new WindowStateObserver();
+
+ componentDidMount() {
+ this.props.updateAccountExpiry();
+
+ this._windowStateObserver.onShow = () => {
+ this.props.updateAccountExpiry();
+ };
+ }
+
+ componentWillUnmount() {
+ this._windowStateObserver.dispose();
+ }
+
render() {
return (
<Layout>
@@ -60,17 +75,17 @@ export default class Settings extends Component<SettingsProps> {
}
_renderTopButtons() {
- const isLoggedIn = this.props.account.status === 'ok';
+ const isLoggedIn = this.props.loginState === 'ok';
if (!isLoggedIn) {
return null;
}
- let isOutOfTime = false,
- formattedExpiry = '';
- const expiryIso = this.props.account.expiry;
+ let isOutOfTime = false;
+ let formattedExpiry = '';
+ const expiryIso = this.props.accountExpiry;
if (isLoggedIn && expiryIso) {
- const expiry = moment(this.props.account.expiry);
+ const expiry = moment(expiryIso);
isOutOfTime = expiry.isSameOrBefore(moment());
formattedExpiry = (expiry.fromNow(true) + ' left').toUpperCase();
}
@@ -86,7 +101,7 @@ export default class Settings extends Component<SettingsProps> {
<Cell.SubText
testName="settings__account_paid_until_subtext"
style={styles.settings__account_paid_until_Label__error}>
- OUT OF TIME
+ {'OUT OF TIME'}
</Cell.SubText>
<Img height={12} width={7} source="icon-chevron" />
</Cell.CellButton>
@@ -136,7 +151,7 @@ export default class Settings extends Component<SettingsProps> {
// the version in package.json has to be semver, but we use a YEAR.release-channel
// version scheme. in package.json we thus have to write YEAR.release.X-channel and
// this function is responsible for removing .X part.
- return this.props.version
+ return this.props.appVersion
.replace('.0-', '-') // remove the .0 in 2018.1.0-beta9
.replace(/\.0$/, ''); // remove the .0 in 2018.1.0
}
diff --git a/app/containers/AccountPage.js b/app/containers/AccountPage.js
index 078c893748..712466f191 100644
--- a/app/containers/AccountPage.js
+++ b/app/containers/AccountPage.js
@@ -3,7 +3,7 @@
import { remote } from 'electron';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
-import { push } from 'connected-react-router';
+import { goBack } from 'connected-react-router';
import Account from '../components/Account';
import accountActions from '../redux/account/actions';
import { links } from '../config';
@@ -19,7 +19,7 @@ const mapStateToProps = (state: ReduxState) => ({
});
const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => {
const { copyAccountToken } = bindActionCreators(accountActions, dispatch);
- const { push: pushHistory } = bindActionCreators({ push }, dispatch);
+ const history = bindActionCreators({ goBack }, dispatch);
return {
updateAccountExpiry: () => props.app.updateAccountExpiry(),
onCopyAccountToken: () => copyAccountToken(),
@@ -27,7 +27,7 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) =>
props.app.logout();
},
onClose: () => {
- pushHistory('/settings');
+ history.goBack();
},
onBuyMore: () => openLink(links['purchase']),
};
diff --git a/app/containers/AdvancedSettingsPage.js b/app/containers/AdvancedSettingsPage.js
index 9f88228cc3..bf584096b9 100644
--- a/app/containers/AdvancedSettingsPage.js
+++ b/app/containers/AdvancedSettingsPage.js
@@ -1,7 +1,8 @@
// @flow
import { connect } from 'react-redux';
-import { push } from 'connected-react-router';
+import { goBack } from 'connected-react-router';
+import { bindActionCreators } from 'redux';
import { AdvancedSettings } from '../components/AdvancedSettings';
import RelaySettingsBuilder from '../lib/relay-settings-builder';
import { log } from '../lib/platform';
@@ -26,9 +27,11 @@ const mapStateToProps = (state: ReduxState) => {
};
const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => {
+ const history = bindActionCreators({ goBack }, dispatch);
return {
- onClose: () => dispatch(push('/settings')),
-
+ onClose: () => {
+ history.goBack();
+ },
onUpdate: async (protocol, port) => {
const relayUpdate = RelaySettingsBuilder.normal()
.tunnel.openvpn((openvpn) => {
diff --git a/app/containers/ConnectPage.js b/app/containers/ConnectPage.js
index 6f906f1c82..0ad43e9c0b 100644
--- a/app/containers/ConnectPage.js
+++ b/app/containers/ConnectPage.js
@@ -2,7 +2,7 @@
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
-import { push as pushHistory } from 'connected-react-router';
+import { push } from 'connected-react-router';
import { links } from '../config';
import Connect from '../components/Connect';
import connectActions from '../redux/connection/actions';
@@ -56,7 +56,7 @@ const mapStateToProps = (state: ReduxState) => {
const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => {
const { copyIPAddress } = bindActionCreators(connectActions, dispatch);
- const history = bindActionCreators({ push: pushHistory }, dispatch);
+ const history = bindActionCreators({ push }, dispatch);
return {
onSettings: () => {
@@ -83,6 +83,7 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) =>
}
},
onExternalLink: (type) => openLink(links[type]),
+ updateAccountExpiry: () => props.app.updateAccountExpiry(),
};
};
diff --git a/app/containers/LaunchPage.js b/app/containers/LaunchPage.js
new file mode 100644
index 0000000000..1b265dd030
--- /dev/null
+++ b/app/containers/LaunchPage.js
@@ -0,0 +1,23 @@
+// @flow
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+import { push } from 'connected-react-router';
+import Launch from '../components/Launch';
+
+import type { ReduxState, ReduxDispatch } from '../redux/store';
+import type { SharedRouteProps } from '../routes';
+
+const mapStateToProps = (_state: ReduxState) => ({});
+const mapDispatchToProps = (dispatch: ReduxDispatch, _props: SharedRouteProps) => {
+ const history = bindActionCreators({ push }, dispatch);
+ return {
+ openSettings: () => {
+ history.push('/settings');
+ },
+ };
+};
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(Launch);
diff --git a/app/containers/LoginPage.js b/app/containers/LoginPage.js
index 74823f3925..539f05c5ac 100644
--- a/app/containers/LoginPage.js
+++ b/app/containers/LoginPage.js
@@ -21,11 +21,11 @@ const mapStateToProps = (state: ReduxState) => {
};
};
const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => {
- const { push: pushHistory } = bindActionCreators({ push }, dispatch);
+ const history = bindActionCreators({ push }, dispatch);
const { resetLoginError, updateAccountToken } = bindActionCreators(accountActions, dispatch);
return {
openSettings: () => {
- pushHistory('/settings');
+ history.push('/settings');
},
login: (account) => {
props.app.login(account);
diff --git a/app/containers/PreferencesPage.js b/app/containers/PreferencesPage.js
index 8534a70d74..ee2709c767 100644
--- a/app/containers/PreferencesPage.js
+++ b/app/containers/PreferencesPage.js
@@ -2,7 +2,7 @@
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
-import { push } from 'connected-react-router';
+import { goBack } from 'connected-react-router';
import Preferences from '../components/Preferences';
import { log, getOpenAtLogin, setOpenAtLogin } from '../lib/platform';
@@ -15,9 +15,11 @@ const mapStateToProps = (state: ReduxState) => ({
});
const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => {
- const { push: pushHistory } = bindActionCreators({ push }, dispatch);
+ const history = bindActionCreators({ goBack }, dispatch);
return {
- onClose: () => pushHistory('/settings'),
+ onClose: () => {
+ history.goBack();
+ },
getAutoStart: () => {
return getOpenAtLogin();
},
diff --git a/app/containers/SelectLocationPage.js b/app/containers/SelectLocationPage.js
index 6307e9421b..037dd67a0b 100644
--- a/app/containers/SelectLocationPage.js
+++ b/app/containers/SelectLocationPage.js
@@ -2,7 +2,7 @@
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
-import { push } from 'connected-react-router';
+import { goBack } from 'connected-react-router';
import SelectLocation from '../components/SelectLocation';
import RelaySettingsBuilder from '../lib/relay-settings-builder';
import { log } from '../lib/platform';
@@ -14,9 +14,9 @@ const mapStateToProps = (state: ReduxState) => ({
settings: state.settings,
});
const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => {
- const { push: pushHistory } = bindActionCreators({ push }, dispatch);
+ const history = bindActionCreators({ goBack }, dispatch);
return {
- onClose: () => pushHistory('/connect'),
+ onClose: () => history.goBack(),
onSelect: async (relayLocation) => {
try {
const relayUpdate = RelaySettingsBuilder.normal()
@@ -27,9 +27,9 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) =>
await props.app.fetchRelaySettings();
await props.app.connectTunnel();
- pushHistory('/connect');
+ history.goBack();
} catch (e) {
- log.error('Failed to select server: ', e.message);
+ log.error(`Failed to select server: ${e.message}`);
}
},
};
diff --git a/app/containers/SettingsPage.js b/app/containers/SettingsPage.js
index 8ffd0691fd..06c0cd6060 100644
--- a/app/containers/SettingsPage.js
+++ b/app/containers/SettingsPage.js
@@ -2,7 +2,7 @@
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
-import { push } from 'connected-react-router';
+import { push, goBack } from 'connected-react-router';
import Settings from '../components/Settings';
import { links } from '../config';
import { getAppVersion, openLink, exit } from '../lib/platform';
@@ -11,20 +11,21 @@ import type { ReduxState, ReduxDispatch } from '../redux/store';
import type { SharedRouteProps } from '../routes';
const mapStateToProps = (state: ReduxState) => ({
- account: state.account,
- settings: state.settings,
- version: getAppVersion(),
+ loginState: state.account.status,
+ accountExpiry: state.account.expiry,
+ appVersion: getAppVersion(),
});
-const mapDispatchToProps = (dispatch: ReduxDispatch, _props: SharedRouteProps) => {
- const { push: pushHistory } = bindActionCreators({ push }, dispatch);
+const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => {
+ const history = bindActionCreators({ push, goBack }, dispatch);
return {
onQuit: () => exit(),
- onClose: () => pushHistory('/connect'),
- onViewAccount: () => pushHistory('/settings/account'),
- onViewSupport: () => pushHistory('/settings/support'),
- onViewPreferences: () => pushHistory('/settings/preferences'),
- onViewAdvancedSettings: () => pushHistory('/settings/advanced'),
+ onClose: () => history.goBack(),
+ onViewAccount: () => history.push('/settings/account'),
+ onViewSupport: () => history.push('/settings/support'),
+ onViewPreferences: () => history.push('/settings/preferences'),
+ onViewAdvancedSettings: () => history.push('/settings/advanced'),
onExternalLink: (type) => openLink(links[type]),
+ updateAccountExpiry: () => props.app.updateAccountExpiry(),
};
};
diff --git a/app/containers/SupportPage.js b/app/containers/SupportPage.js
index ffab2d2095..78d1b41ebe 100644
--- a/app/containers/SupportPage.js
+++ b/app/containers/SupportPage.js
@@ -1,7 +1,7 @@
// @flow
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
-import { push } from 'connected-react-router';
+import { goBack } from 'connected-react-router';
import Support from '../components/Support';
import { openItem } from '../lib/platform';
import { collectProblemReport, sendProblemReport } from '../lib/problem-report';
@@ -18,10 +18,10 @@ const mapStateToProps = (state: ReduxState) => ({
const mapDispatchToProps = (dispatch: ReduxDispatch, _props: SharedRouteProps) => {
const { saveReportForm, clearReportForm } = bindActionCreators(supportActions, dispatch);
- const { push: pushHistory } = bindActionCreators({ push }, dispatch);
+ const history = bindActionCreators({ goBack }, dispatch);
return {
- onClose: () => pushHistory('/settings'),
+ onClose: () => history.goBack(),
viewLog: (path) => openItem(path),
saveReportForm,
clearReportForm,
diff --git a/app/lib/daemon-rpc.js b/app/lib/daemon-rpc.js
index fb3915e068..163ea170d0 100644
--- a/app/lib/daemon-rpc.js
+++ b/app/lib/daemon-rpc.js
@@ -327,7 +327,7 @@ export class DaemonRpc implements DaemonRpcProtocol {
const validatedObject = validate(RelaySettingsSchema, response);
/* $FlowFixMe:
- There is no way to express the constraints with string literals, i.e:
+ There is no way to express constraints with string literals, i.e:
RelaySettingsSchema constraint:
oneOf(string, object)
diff --git a/app/lib/jsonrpc-transport.js b/app/lib/jsonrpc-transport.js
index 2fa98df988..cbf1e7dce8 100644
--- a/app/lib/jsonrpc-transport.js
+++ b/app/lib/jsonrpc-transport.js
@@ -123,7 +123,7 @@ const DEFAULT_TIMEOUT_MILLIS = 5000;
export default class JsonRpcTransport extends EventEmitter {
_unansweredRequests: Map<string, UnansweredRequest> = new Map();
_subscriptions: Map<string | number, (mixed) => void> = new Map();
- _websocket: ?WebSocket;
+ _webSocket: ?WebSocket;
_websocketFactory: (string) => WebSocket;
constructor(websocketFactory: ?(string) => WebSocket) {
@@ -133,46 +133,60 @@ export default class JsonRpcTransport extends EventEmitter {
}
/// Connect websocket
- connect(connectionString: string) {
- this.disconnect();
+ connect(connectionString: string): Promise<void> {
+ return new Promise((resolve, reject) => {
+ this.disconnect();
- log.info('Connecting to websocket', connectionString);
+ log.info('Connecting to websocket', connectionString);
- const websocket = this._websocketFactory(connectionString);
+ const webSocket = this._websocketFactory(connectionString);
- websocket.onopen = () => {
- log.info('Websocket is connected');
- this.emit('open');
- };
+ // A flag used to determine if Promise was resolved.
+ let isPromiseResolved = false;
- websocket.onmessage = (event) => {
- const data = event.data;
- if (typeof data === 'string') {
- this._onMessage(data);
- } else {
- log.error('Got invalid reply from the server', event);
- }
- };
+ webSocket.onopen = () => {
+ log.info('Websocket is connected');
+ this.emit('open');
+
+ // Resolve the Promise
+ resolve();
+ isPromiseResolved = true;
+ };
+
+ webSocket.onmessage = (event) => {
+ const data = event.data;
+ if (typeof data === 'string') {
+ this._onMessage(data);
+ } else {
+ log.error('Got invalid reply from the server', event);
+ }
+ };
- websocket.onclose = (event) => {
- log.info(`The websocket connection closed with code: ${event.code}`);
+ webSocket.onclose = (event) => {
+ log.info(`The websocket connection closed with code: ${event.code}`);
- // Remove all subscriptions since they are connection based
- this._subscriptions.clear();
+ // Remove all subscriptions since they are connection based
+ this._subscriptions.clear();
- // 1000 is a code used for normal connection closure.
- const connectionError = event.code === 1000 ? null : new ConnectionError(event.code);
+ // 1000 is a code used for normal connection closure.
+ const connectionError = event.code === 1000 ? null : new ConnectionError(event.code);
- this.emit('close', connectionError);
- };
+ this.emit('close', connectionError);
- this._websocket = websocket;
+ // Prevent rejecting a previously resolved Promise.
+ if (!isPromiseResolved) {
+ reject(connectionError);
+ }
+ };
+
+ this._webSocket = webSocket;
+ });
}
disconnect() {
- if (this._websocket) {
- this._websocket.close();
- this._websocket = null;
+ if (this._webSocket) {
+ this._webSocket.close();
+ this._webSocket = null;
}
}
@@ -195,19 +209,14 @@ export default class JsonRpcTransport extends EventEmitter {
}
}
- async send(
- action: string,
- data: mixed,
- timeout: number = DEFAULT_TIMEOUT_MILLIS,
- ): Promise<mixed> {
- let socket: WebSocket;
- try {
- socket = await this._getWebSocket();
- } catch (error) {
- throw error;
- }
-
+ send(action: string, data: mixed, timeout: number = DEFAULT_TIMEOUT_MILLIS): Promise<mixed> {
return new Promise((resolve, reject) => {
+ const webSocket = this._webSocket;
+ if (!webSocket) {
+ reject(new Error('Websocket is not connected.'));
+ return;
+ }
+
const id = uuid.v4();
const payload = this._prepareParams(data);
const timerId = setTimeout(() => this._onTimeout(id), timeout);
@@ -221,9 +230,14 @@ export default class JsonRpcTransport extends EventEmitter {
try {
log.silly('Sending message', id, action);
- socket.send(JSON.stringify(message));
+ webSocket.send(JSON.stringify(message));
} catch (error) {
log.error(`Failed sending RPC message "${action}": ${error.message}`);
+
+ // clean up on error
+ this._unansweredRequests.delete(id);
+ clearTimeout(timerId);
+
throw error;
}
});
@@ -245,25 +259,6 @@ export default class JsonRpcTransport extends EventEmitter {
}
}
- _getWebSocket(): Promise<WebSocket> {
- if (this._websocket && this._websocket.readyState === 1) {
- return Promise.resolve(this._websocket);
- } else {
- return new Promise((resolve, reject) => {
- log.debug('Waiting for websocket to connect');
-
- this.once('open', () => {
- const ws = this._websocket;
- if (ws) {
- resolve(ws);
- } else {
- reject(new Error('Internal error'));
- }
- });
- });
- }
- }
-
_onTimeout(requestId) {
const request = this._unansweredRequests.get(requestId);
diff --git a/app/lib/reconnection-backoff.js b/app/lib/reconnection-backoff.js
index 1cd8203dac..d777e86e57 100644
--- a/app/lib/reconnection-backoff.js
+++ b/app/lib/reconnection-backoff.js
@@ -1,13 +1,11 @@
+// @flow
+
/*
* 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 = 0;
attempt(handler: () => void) {
setTimeout(handler, this._getIncreasedBackoff());
diff --git a/app/lib/window-state-observer.js b/app/lib/window-state-observer.js
new file mode 100644
index 0000000000..67252d8f6a
--- /dev/null
+++ b/app/lib/window-state-observer.js
@@ -0,0 +1,64 @@
+// @flow
+
+import { remote } from 'electron';
+
+type EventListener = () => void;
+
+// Tiny helper for detecting the window state.
+export default class WindowStateObserver {
+ _onShow: ?EventListener;
+ _onHide: ?EventListener;
+
+ get onShow() {
+ return this._onShow;
+ }
+
+ set onShow(listener: ?EventListener) {
+ const currentWindow = remote.getCurrentWindow();
+ const oldListener = this._onShow;
+ if (oldListener) {
+ currentWindow.removeListener('show', oldListener);
+ }
+
+ if (listener) {
+ currentWindow.addListener('show', listener);
+ }
+
+ this._onShow = listener;
+ }
+
+ get onHide() {
+ return this._onHide;
+ }
+
+ set onHide(listener: ?EventListener) {
+ const currentWindow = remote.getCurrentWindow();
+ const oldListener = this._onHide;
+ if (oldListener) {
+ currentWindow.removeListener('hide', oldListener);
+ }
+
+ if (listener) {
+ currentWindow.addListener('hide', listener);
+ }
+
+ this._onHide = listener;
+ }
+
+ constructor() {
+ // Because BrowserWindow persists between page reloads,
+ // it's important to release event handlers when that happens.
+ window.addEventListener('beforeunload', this._onBeforeUnload);
+ }
+
+ _onBeforeUnload = () => {
+ this.dispose();
+ };
+
+ dispose() {
+ this.onShow = null;
+ this.onHide = null;
+
+ window.removeEventListener('beforeunload', this._onBeforeUnload);
+ }
+}
diff --git a/app/redux/account/actions.js b/app/redux/account/actions.js
index 3cc4b83681..ae42d91b6c 100644
--- a/app/redux/account/actions.js
+++ b/app/redux/account/actions.js
@@ -4,23 +4,13 @@ import { Clipboard } from 'reactxp';
import type { AccountToken } from '../../lib/daemon-rpc';
import type { ReduxThunk } from '../store';
-const copyAccountToken = (): ReduxThunk => {
- return (_, getState) => {
- const accountToken = getState().account.accountToken;
- if (accountToken) {
- Clipboard.setText(accountToken);
- }
- };
-};
-
type StartLoginAction = {
type: 'START_LOGIN',
- accountToken?: AccountToken,
+ accountToken: AccountToken,
};
type LoginSuccessfulAction = {
type: 'LOGIN_SUCCESSFUL',
- expiry?: string,
};
type LoginFailedAction = {
@@ -61,24 +51,23 @@ export type AccountAction =
| UpdateAccountHistoryAction
| UpdateAccountExpiryAction;
-function startLogin(accountToken?: AccountToken): StartLoginAction {
+function startLogin(accountToken: AccountToken): StartLoginAction {
return {
type: 'START_LOGIN',
accountToken: accountToken,
};
}
-function loginSuccessful(expiry: string): LoginSuccessfulAction {
+function loginSuccessful(): LoginSuccessfulAction {
return {
type: 'LOGIN_SUCCESSFUL',
- expiry,
};
}
function loginFailed(error: Error): LoginFailedAction {
return {
type: 'LOGIN_FAILED',
- error: error,
+ error,
};
}
@@ -88,10 +77,6 @@ function loggedOut(): LoggedOutAction {
};
}
-function autoLoginFailed(): LoggedOutAction {
- return loggedOut();
-}
-
function resetLoginError(): ResetLoginErrorAction {
return {
type: 'RESET_LOGIN_ERROR',
@@ -101,14 +86,14 @@ function resetLoginError(): ResetLoginErrorAction {
function updateAccountToken(token: AccountToken): UpdateAccountTokenAction {
return {
type: 'UPDATE_ACCOUNT_TOKEN',
- token: token,
+ token,
};
}
function updateAccountHistory(accountHistory: Array<AccountToken>): UpdateAccountHistoryAction {
return {
type: 'UPDATE_ACCOUNT_HISTORY',
- accountHistory: accountHistory,
+ accountHistory,
};
}
@@ -119,15 +104,23 @@ function updateAccountExpiry(expiry: string): UpdateAccountExpiryAction {
};
}
+function copyAccountToken(): ReduxThunk {
+ return (_, getState) => {
+ const accountToken = getState().account.accountToken;
+ if (accountToken) {
+ Clipboard.setText(accountToken);
+ }
+ };
+}
+
export default {
- copyAccountToken,
startLogin,
loginSuccessful,
loginFailed,
loggedOut,
- autoLoginFailed,
resetLoginError,
updateAccountToken,
updateAccountHistory,
updateAccountExpiry,
+ copyAccountToken,
};
diff --git a/app/redux/account/reducers.js b/app/redux/account/reducers.js
index dbd854a6c0..7263b51166 100644
--- a/app/redux/account/reducers.js
+++ b/app/redux/account/reducers.js
@@ -40,7 +40,6 @@ export default function(
...{
status: 'ok',
error: null,
- expiry: action.expiry,
},
};
case 'LOGIN_FAILED':
diff --git a/app/redux/connection/actions.js b/app/redux/connection/actions.js
index bb155aa485..915eedd273 100644
--- a/app/redux/connection/actions.js
+++ b/app/redux/connection/actions.js
@@ -4,21 +4,14 @@ import { Clipboard } from 'reactxp';
import type { ReduxThunk } from '../store';
import type { Ip } from '../../lib/daemon-rpc';
-const copyIPAddress = (): ReduxThunk => {
- return (_, getState) => {
- const ip = getState().connection.ip;
- if (ip) {
- Clipboard.setText(ip);
- }
- };
-};
-
type ConnectingAction = {
type: 'CONNECTING',
};
+
type ConnectedAction = {
type: 'CONNECTED',
};
+
type DisconnectedAction = {
type: 'DISCONNECTED',
};
@@ -88,12 +81,21 @@ function offline(): OfflineAction {
};
}
+function copyIPAddress(): ReduxThunk {
+ return (_, getState) => {
+ const ip = getState().connection.ip;
+ if (ip) {
+ Clipboard.setText(ip);
+ }
+ };
+}
+
export default {
- copyIPAddress,
newLocation,
connecting,
connected,
disconnected,
online,
offline,
+ copyIPAddress,
};
diff --git a/app/redux/daemon/actions.js b/app/redux/daemon/actions.js
new file mode 100644
index 0000000000..904cb3bd61
--- /dev/null
+++ b/app/redux/daemon/actions.js
@@ -0,0 +1,24 @@
+// @flow
+
+export type DaemonConnectedAction = {
+ type: 'DAEMON_CONNECTED',
+};
+export type DaemonDisconnectedAction = {
+ type: 'DAEMON_DISCONNECTED',
+};
+
+export type DaemonAction = DaemonConnectedAction | DaemonDisconnectedAction;
+
+function connected(): DaemonConnectedAction {
+ return {
+ type: 'DAEMON_CONNECTED',
+ };
+}
+
+function disconnected(): DaemonDisconnectedAction {
+ return {
+ type: 'DAEMON_DISCONNECTED',
+ };
+}
+
+export default { connected, disconnected };
diff --git a/app/redux/daemon/reducers.js b/app/redux/daemon/reducers.js
new file mode 100644
index 0000000000..5e9e707ff6
--- /dev/null
+++ b/app/redux/daemon/reducers.js
@@ -0,0 +1,33 @@
+// @flow
+
+import type { ReduxAction } from '../store';
+
+export type DaemonReduxState = {
+ isConnected: boolean,
+};
+
+const initialState: DaemonReduxState = {
+ isConnected: false,
+};
+
+export default function(
+ state: DaemonReduxState = initialState,
+ action: ReduxAction,
+): DaemonReduxState {
+ switch (action.type) {
+ case 'DAEMON_CONNECTED':
+ return {
+ ...state,
+ isConnected: true,
+ };
+
+ case 'DAEMON_DISCONNECTED':
+ return {
+ ...state,
+ isConnected: false,
+ };
+
+ default:
+ return state;
+ }
+}
diff --git a/app/redux/store.js b/app/redux/store.js
index 8afe22d716..e269d9902b 100644
--- a/app/redux/store.js
+++ b/app/redux/store.js
@@ -11,6 +11,8 @@ import settings from './settings/reducers';
import settingsActions from './settings/actions';
import support from './support/reducers';
import supportActions from './support/actions';
+import daemon from './daemon/reducers';
+import daemonActions from './daemon/actions';
import type { Store, StoreEnhancer } from 'redux';
import type { History } from 'history';
@@ -18,20 +20,28 @@ import type { AccountReduxState } from './account/reducers';
import type { ConnectionReduxState } from './connection/reducers';
import type { SettingsReduxState } from './settings/reducers';
import type { SupportReduxState } from './support/reducers';
+import type { DaemonReduxState } from './daemon/reducers';
import type { AccountAction } from './account/actions';
import type { ConnectionAction } from './connection/actions';
import type { SettingsAction } from './settings/actions';
import type { SupportAction } from './support/actions';
+import type { DaemonAction } from './daemon/actions';
export type ReduxState = {
account: AccountReduxState,
connection: ConnectionReduxState,
settings: SettingsReduxState,
support: SupportReduxState,
+ daemon: DaemonReduxState,
};
-export type ReduxAction = AccountAction | ConnectionAction | SettingsAction | SupportAction;
+export type ReduxAction =
+ | AccountAction
+ | ConnectionAction
+ | SettingsAction
+ | SupportAction
+ | DaemonAction;
export type ReduxStore = Store<ReduxState, ReduxAction, ReduxDispatch>;
export type ReduxGetState = () => ReduxState;
export type ReduxDispatch = (action: ReduxAction | ReduxThunk) => any;
@@ -48,6 +58,7 @@ export default function configureStore(
...connectionActions,
...settingsActions,
...supportActions,
+ ...daemonActions,
pushRoute: (route) => push(route),
replaceRoute: (route) => replace(route),
};
@@ -57,6 +68,7 @@ export default function configureStore(
connection,
settings,
support,
+ daemon,
};
const middlewares = [thunk, router];
diff --git a/app/routes.js b/app/routes.js
index 95ad24e876..107ee616be 100644
--- a/app/routes.js
+++ b/app/routes.js
@@ -4,6 +4,7 @@ import * as React from 'react';
import { Switch, Route, Redirect } from 'react-router';
import TransitionContainer from './components/TransitionContainer';
import PlatformWindow from './components/PlatformWindow';
+import LaunchPage from './containers/LaunchPage';
import LoginPage from './containers/LoginPage';
import ConnectPage from './containers/ConnectPage';
import SettingsPage from './containers/SettingsPage';
@@ -38,7 +39,6 @@ export default function makeRoutes(
// example: <PublicRoute path="/" component={ MyComponent } />
const PublicRoute = ({ component, ...otherProps }) => {
return (
- // $FlowFixMe: This has been fixed in Flow 0.71
<Route
{...otherProps}
render={(routeProps) => {
@@ -52,7 +52,6 @@ export default function makeRoutes(
// example: <PrivateRoute path="/protected" component={ MyComponent } />
const PrivateRoute = ({ component, ...otherProps }) => {
return (
- // $FlowFixMe: This has been fixed in Flow 0.71
<Route
{...otherProps}
render={(routeProps) => {
@@ -62,7 +61,7 @@ export default function makeRoutes(
if (isLoggedIn) {
return renderMergedProps(component, routeProps, otherProps);
} else {
- return <Redirect to={'/'} />;
+ return <Redirect to={'/login'} />;
}
}}
/>
@@ -74,7 +73,6 @@ export default function makeRoutes(
// example: <LoginRoute path="/login" component={ MyComponent } />
const LoginRoute = ({ component, ...otherProps }) => {
return (
- // $FlowFixMe: This has been fixed in Flow 0.71
<Route
{...otherProps}
render={(routeProps) => {
@@ -91,11 +89,29 @@ export default function makeRoutes(
);
};
+ // Renders launch route that is only available when daemon is not connected.
+ // Otherwise this route redirects user to /login.
+ // example: <LaunchRoute path="/" component={ MyComponent } />
+ const LaunchRoute = ({ component, ...otherProps }) => {
+ return (
+ <Route
+ {...otherProps}
+ render={(routeProps) => {
+ const { daemon } = getState();
+ if (daemon.isConnected) {
+ return <Redirect to={'/login'} />;
+ } else {
+ return renderMergedProps(component, routeProps, otherProps);
+ }
+ }}
+ />
+ );
+ };
+
// store previous route
let previousRoute: ?string;
return (
- // $FlowFixMe: This has been fixed in Flow 0.71
<Route
render={({ location }) => {
const toRoute = location.pathname;
@@ -107,7 +123,8 @@ export default function makeRoutes(
<PlatformWindow>
<TransitionContainer {...transitionProps}>
<Switch key={location.key} location={location}>
- <LoginRoute exact path="/" component={LoginPage} />
+ <LaunchRoute exact path="/" component={LaunchPage} />
+ <LoginRoute exact path="/login" component={LoginPage} />
<PrivateRoute exact path="/connect" component={ConnectPage} />
<PublicRoute exact path="/settings" component={SettingsPage} />
<PrivateRoute exact path="/settings/account" component={AccountPage} />
diff --git a/init.js b/init.js
index 0304e4341a..e489d4ef8e 100644
--- a/init.js
+++ b/init.js
@@ -1 +1 @@
-require('./build/main'); \ No newline at end of file
+require('./build/main');
diff --git a/package.json b/package.json
index 084a96b0ca..f2aa5b4425 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,7 @@
"react-redux": "^5.0.7",
"react-router": "^4.3.1",
"react-simple-maps": "^0.10.1",
- "reactxp": "^1.3.0",
+ "reactxp": "^1.3.3",
"redux": "^4.0.0",
"redux-thunk": "^2.3.0",
"uuid": "^3.0.1",
@@ -60,13 +60,13 @@
"enzyme-adapter-react-16": "^1.1.0",
"eslint": "^4.19.1",
"eslint-config-prettier": "^2.9.0",
- "eslint-plugin-flowtype": "^2.49.3",
- "eslint-plugin-react": "^7.9.1",
+ "eslint-plugin-flowtype": "^2.50.0",
+ "eslint-plugin-react": "^7.10.0",
"flow-bin": "^0.78.0",
- "flow-typed": "^2.4.0",
- "mock-socket": "^7.1.0",
+ "flow-typed": "^2.5.1",
+ "mock-socket": "^8.0.2",
"npm-run-all": "^4.0.1",
- "prettier": "1.13.7",
+ "prettier": "1.14.0",
"rimraf": "^2.5.4"
},
"scripts": {
@@ -90,6 +90,6 @@
"private:serve": "cross-env BABEL_ENV=electron babel-node scripts/serve.js",
"private:compile": "babel app/ --copy-files --out-dir build",
"private:clean": "rimraf build",
- "private:format": "cross-env prettier 'app/**/*.js' 'test/**/*.js'"
+ "private:format": "cross-env prettier 'app/**/*.js' 'test/**/*.js' 'scripts/*.js'"
}
}
diff --git a/scripts/serve.js b/scripts/serve.js
index b658b0dc70..03abeab2c2 100644
--- a/scripts/serve.js
+++ b/scripts/serve.js
@@ -15,38 +15,39 @@ const getClientUrl = (options) => {
return getRootUrl(options) + pathname;
};
-bsync.init({
- ui: false,
- // Port 35829 = LiveReload's default port 35729 + 100.
- // If the port is occupied, Browsersync uses next free port automatically.
- port: 35829,
- ghostMode: false,
- open: false,
- notify: false,
- logSnippet: false,
- socket: {
- // Use the actual port here.
- domain: getRootUrl
- }
-}, (err, bs) => {
- if (err) return console.error(err);
+bsync.init(
+ {
+ ui: false,
+ // Port 35829 = LiveReload's default port 35729 + 100.
+ // If the port is occupied, Browsersync uses next free port automatically.
+ port: 35829,
+ ghostMode: false,
+ open: false,
+ notify: false,
+ logSnippet: false,
+ socket: {
+ // Use the actual port here.
+ domain: getRootUrl,
+ },
+ },
+ (err, bs) => {
+ if (err) return console.error(err);
- const child = spawn(electron, ['.'], {
- env: {
- ...{
- NODE_ENV: 'development',
- BROWSER_SYNC_CLIENT_URL: getClientUrl(bs.options)
+ const child = spawn(electron, ['.'], {
+ env: {
+ ...{
+ NODE_ENV: 'development',
+ BROWSER_SYNC_CLIENT_URL: getClientUrl(bs.options),
+ },
+ ...process.env,
},
- ...process.env
- },
- stdio: 'inherit'
- });
+ stdio: 'inherit',
+ });
- child.on('close', () => {
- process.exit();
- });
+ child.on('close', () => {
+ process.exit();
+ });
- bsync
- .watch('build/**/*')
- .on('change', bsync.reload);
-});
+ bsync.watch('build/**/*').on('change', bsync.reload);
+ },
+);
diff --git a/test/components/Account.spec.js b/test/components/Account.spec.js
index a850c6af06..703ce3d27f 100644
--- a/test/components/Account.spec.js
+++ b/test/components/Account.spec.js
@@ -4,7 +4,8 @@ import * as React from 'react';
import { shallow } from 'enzyme';
import Account from '../../app/components/Account';
import { BackBarItem } from '../../app/components/NavigationBar';
-import type { AccountProps } from '../../app/components/Account';
+
+type AccountProps = React.ElementProps<typeof Account>;
describe('components/Account', () => {
const makeProps = (mergeProps: $Shape<AccountProps>): AccountProps => {
diff --git a/test/components/Connect.spec.js b/test/components/Connect.spec.js
index d21a8eb510..e445b2c788 100644
--- a/test/components/Connect.spec.js
+++ b/test/components/Connect.spec.js
@@ -5,7 +5,7 @@ import { shallow } from 'enzyme';
import Connect from '../../app/components/Connect';
-import type { ConnectProps } from '../../app/components/Connect';
+type ConnectProps = React.ElementProps<typeof Connect>;
describe('components/Connect', () => {
it('shows unsecured hints when disconnected', () => {
@@ -19,7 +19,7 @@ describe('components/Connect', () => {
const header = getComponent(component, 'header');
const securityMessage = getComponent(component, 'networkSecurityMessage');
const connectButton = getComponent(component, 'secureConnection');
- expect(header.prop('style')).to.equal('error');
+ expect(header.prop('barStyle')).to.equal('error');
expect(securityMessage.html()).to.contain('UNSECURED');
expect(connectButton.html()).to.contain('Secure my connection');
});
@@ -35,7 +35,7 @@ describe('components/Connect', () => {
const header = getComponent(component, 'header');
const securityMessage = getComponent(component, 'networkSecurityMessage');
const disconnectButton = getComponent(component, 'disconnect');
- expect(header.prop('style')).to.equal('success');
+ expect(header.prop('barStyle')).to.equal('success');
expect(securityMessage.html()).to.contain('SECURE ');
expect(disconnectButton.html()).to.contain('Disconnect');
});
@@ -125,6 +125,7 @@ const defaultProps: ConnectProps = {
country: null,
city: null,
},
+ updateAccountExpiry: () => Promise.resolve(),
};
function renderWithProps(customProps: $Shape<ConnectProps>) {
diff --git a/test/components/HeaderBar.spec.js b/test/components/HeaderBar.spec.js
deleted file mode 100644
index 09eab134ad..0000000000
--- a/test/components/HeaderBar.spec.js
+++ /dev/null
@@ -1,44 +0,0 @@
-// @flow
-
-import React from 'react';
-import { shallow } from 'enzyme';
-import HeaderBar from '../../app/components/HeaderBar';
-
-describe('components/HeaderBar', () => {
- it('should display settings button', () => {
- const component = render({
- showSettings: true,
- });
- const hasChildMatching = hasChild(component, 'headerbar__settings');
- expect(hasChildMatching).to.be.true;
- });
-
- it('should not display settings button', () => {
- const component = render({
- showSettings: false,
- });
- const hasChildMatching = hasChild(component, 'headerbar__settings');
- expect(hasChildMatching).to.be.false;
- });
-
- it('should call settings callback', (done) => {
- const component = render({
- showSettings: true,
- onSettings: () => done(),
- });
- const settingsButton = getComponent(component, 'headerbar__settings');
- settingsButton.simulate('press');
- });
-});
-
-function render(props) {
- return shallow(<HeaderBar {...props} />);
-}
-
-function getComponent(container, testName) {
- return container.findWhere((n) => n.prop('testName') === testName);
-}
-
-function hasChild(container, testName) {
- return getComponent(container, testName).length > 0;
-}
diff --git a/test/components/Settings.spec.js b/test/components/Settings.spec.js
index 9edf46fcc3..a9302a9ea7 100644
--- a/test/components/Settings.spec.js
+++ b/test/components/Settings.spec.js
@@ -5,180 +5,201 @@ import { shallow } from 'enzyme';
import Settings from '../../app/components/Settings';
import { CloseBarItem } from '../../app/components/NavigationBar';
-import type { AccountReduxState } from '../../app/redux/account/reducers';
-import type { SettingsReduxState } from '../../app/redux/settings/reducers';
-import type { SettingsProps } from '../../app/components/Settings';
+type SettingsProps = React.ElementProps<typeof Settings>;
describe('components/Settings', () => {
- const loggedOutAccountState: AccountReduxState = {
- accountToken: null,
- accountHistory: [],
- expiry: null,
- status: 'none',
- error: null,
- };
-
- const loggedInAccountState: AccountReduxState = {
- accountToken: '1234',
- accountHistory: [],
- expiry: new Date('2038-01-01').toISOString(),
- status: 'ok',
- error: null,
- };
-
- const unpaidAccountState: AccountReduxState = {
- accountToken: '1234',
- accountHistory: [],
- expiry: new Date('2001-01-01').toISOString(),
- status: 'ok',
- error: null,
- };
-
- const settingsState: SettingsReduxState = {
- relaySettings: {
- normal: {
- location: 'any',
- protocol: 'udp',
- port: 1301,
- },
- },
- relayLocations: [],
- autoConnect: false,
- allowLan: false,
- };
-
- const makeProps = (
- anAccountState: AccountReduxState,
- aSettingsState: SettingsReduxState,
- mergeProps: $Shape<SettingsProps> = {},
- ): SettingsProps => {
- const defaultProps: SettingsProps = {
- account: anAccountState,
- settings: aSettingsState,
- version: '',
- onQuit: () => {},
- onClose: () => {},
- onViewAccount: () => {},
- onViewSupport: () => {},
- onViewAdvancedSettings: () => {},
- onViewPreferences: () => {},
- onExternalLink: (_type) => {},
- };
- return Object.assign({}, defaultProps, mergeProps);
+ const defaultProps: SettingsProps = {
+ loginState: 'none',
+ accountExpiry: null,
+ appVersion: '',
+ onQuit: () => {},
+ onClose: () => {},
+ onViewAccount: () => {},
+ onViewSupport: () => {},
+ onViewAdvancedSettings: () => {},
+ onViewPreferences: () => {},
+ onExternalLink: (_type) => {},
+ updateAccountExpiry: () => Promise.resolve(),
};
it('should show quit button when logged out', () => {
- const props = makeProps(loggedOutAccountState, settingsState);
- const component = getComponent(render(props), 'settings__quit');
+ const props = {
+ ...defaultProps,
+ loginState: 'none',
+ accountExpiry: null,
+ };
+ const component = getComponent(shallow(<Settings {...props} />), 'settings__quit');
expect(component).to.have.length(1);
});
it('should show quit button when logged in', () => {
- const props = makeProps(loggedInAccountState, settingsState);
- const component = getComponent(render(props), 'settings__quit');
+ const props = {
+ ...defaultProps,
+ loginState: 'ok',
+ accountExpiry: new Date('2038-01-01').toISOString(),
+ };
+ const component = getComponent(shallow(<Settings {...props} />), 'settings__quit');
expect(component).to.have.length(1);
});
it('should show external links when logged out', () => {
- const props = makeProps(loggedOutAccountState, settingsState);
- const component = getComponent(render(props), 'settings__external_link');
+ const props = {
+ ...defaultProps,
+ loginState: 'none',
+ accountExpiry: null,
+ };
+ const component = getComponent(shallow(<Settings {...props} />), 'settings__external_link');
expect(component.length).to.be.above(0);
});
it('should show external links when logged in', () => {
- const props = makeProps(loggedInAccountState, settingsState);
- const component = getComponent(render(props), 'settings__external_link');
+ const props = {
+ ...defaultProps,
+ loginState: 'ok',
+ accountExpiry: new Date('2038-01-01').toISOString(),
+ };
+ const component = getComponent(shallow(<Settings {...props} />), 'settings__external_link');
expect(component.length).to.be.above(0);
});
it('should show account section when logged in', () => {
- const props = makeProps(loggedInAccountState, settingsState);
- const component = getComponent(render(props), 'settings__account');
+ const props = {
+ ...defaultProps,
+ loginState: 'ok',
+ accountExpiry: new Date('2038-01-01').toISOString(),
+ };
+ const component = getComponent(shallow(<Settings {...props} />), 'settings__account');
expect(component).to.have.length(1);
});
it('should hide account section when logged out', () => {
- const props = makeProps(loggedOutAccountState, settingsState);
- const elements = getComponent(render(props), 'settings__account');
+ const props = {
+ ...defaultProps,
+ loginState: 'none',
+ accountExpiry: null,
+ };
+ const elements = getComponent(shallow(<Settings {...props} />), 'settings__account');
expect(elements).to.have.length(0);
});
it('should hide account link when not logged in', () => {
- const props = makeProps(loggedOutAccountState, settingsState);
- const elements = getComponent(render(props), 'settings__view_account');
+ const props = {
+ ...defaultProps,
+ loginState: 'none',
+ accountExpiry: null,
+ };
+ const elements = getComponent(shallow(<Settings {...props} />), 'settings__view_account');
expect(elements).to.have.length(0);
});
it('should show out-of-time message for unpaid account', () => {
- const props = makeProps(unpaidAccountState, settingsState);
- const component = getComponent(render(props), 'settings__account_paid_until_subtext');
+ const props = {
+ ...defaultProps,
+ loginState: 'ok',
+ accountExpiry: new Date('2001-01-01').toISOString(),
+ };
+ const component = getComponent(
+ shallow(<Settings {...props} />),
+ 'settings__account_paid_until_subtext',
+ );
expect(component.children().text()).to.equal('OUT OF TIME');
});
it('should hide out-of-time message for paid account', () => {
- const props = makeProps(loggedInAccountState, settingsState);
- const component = getComponent(render(props), 'settings__account_paid_until_subtext');
+ const props = {
+ ...defaultProps,
+ loginState: 'ok',
+ accountExpiry: new Date('2038-01-01').toISOString(),
+ };
+ const component = getComponent(
+ shallow(<Settings {...props} />),
+ 'settings__account_paid_until_subtext',
+ );
expect(component.children().text()).not.to.equal('OUT OF TIME');
});
it('should call close callback', (done) => {
- const props = makeProps(loggedOutAccountState, settingsState, {
+ const props = {
+ ...defaultProps,
+ loginState: 'none',
+ accountExpiry: null,
onClose: () => done(),
- });
- const component = render(props)
+ };
+ const component = shallow(<Settings {...props} />)
.find(CloseBarItem)
.dive();
component.simulate('press');
});
it('should call quit callback', (done) => {
- const props = makeProps(loggedOutAccountState, settingsState, {
+ const props = {
+ ...defaultProps,
+ loginState: 'none',
+ accountExpiry: null,
onQuit: () => done(),
- });
- const component = getComponent(render(props), 'settings__quit');
+ };
+ const component = getComponent(shallow(<Settings {...props} />), 'settings__quit');
component.simulate('press');
});
it('should call account callback', (done) => {
- const props = makeProps(loggedInAccountState, settingsState, {
+ const props = {
+ ...defaultProps,
+ loginState: 'ok',
+ accountExpiry: new Date('2038-01-01').toISOString(),
onViewAccount: () => done(),
- });
- const component = getComponent(render(props), 'settings__account_paid_until_button');
+ };
+ const component = getComponent(
+ shallow(<Settings {...props} />),
+ 'settings__account_paid_until_button',
+ );
component.simulate('press');
});
it('should call advanced settings callback', (done) => {
- const props = makeProps(loggedInAccountState, settingsState, {
+ const props = {
+ ...defaultProps,
+ loginState: 'ok',
+ accountExpiry: new Date('2038-01-01').toISOString(),
onViewAdvancedSettings: () => done(),
- });
- const component = getComponent(render(props), 'settings__advanced');
+ };
+ const component = getComponent(shallow(<Settings {...props} />), 'settings__advanced');
component.simulate('press');
});
it('should call preferences callback', (done) => {
- const props = makeProps(loggedInAccountState, settingsState, {
+ const props = {
+ ...defaultProps,
+ loginState: 'ok',
+ accountExpiry: new Date('2038-01-01').toISOString(),
onViewPreferences: () => done(),
- });
- const component = getComponent(render(props), 'settings__preferences');
+ };
+ const component = getComponent(shallow(<Settings {...props} />), 'settings__preferences');
component.simulate('press');
});
it('should call support callback', (done) => {
- const props = makeProps(loggedInAccountState, settingsState, {
+ const props = {
+ ...defaultProps,
+ loginState: 'ok',
+ accountExpiry: new Date('2038-01-01').toISOString(),
onViewSupport: () => done(),
- });
- const component = getComponent(render(props), 'settings__view_support');
+ };
+ const component = getComponent(shallow(<Settings {...props} />), 'settings__view_support');
component.simulate('press');
});
it('should call external links callback', () => {
const collectedExternalLinkTypes: Array<string> = [];
- const props = makeProps(loggedOutAccountState, settingsState, {
+ const props = {
+ ...defaultProps,
+ loginState: 'none',
+ accountExpiry: null,
onExternalLink: (type) => {
collectedExternalLinkTypes.push(type);
},
- });
- const container = getComponent(render(props), 'settings__external_link');
+ };
+ const container = getComponent(shallow(<Settings {...props} />), 'settings__external_link');
container
.find({ testName: 'settings__external_link' })
.forEach((element) => element.simulate('press'));
@@ -187,10 +208,6 @@ describe('components/Settings', () => {
});
});
-function render(props) {
- return shallow(<Settings {...props} />);
-}
-
function getComponent(container, testName) {
return container.findWhere((n) => n.prop('testName') === testName);
}
diff --git a/test/jsonrpc-transport.spec.js b/test/jsonrpc-transport.spec.js
index 1f36be83a6..ca66514192 100644
--- a/test/jsonrpc-transport.spec.js
+++ b/test/jsonrpc-transport.spec.js
@@ -1,10 +1,8 @@
// @flow
-import JsonRpcTransport, {
- TimeOutError as JsonRpcTransportTimeOutError,
-} from '../app/lib/jsonrpc-transport';
import jsonrpc from 'jsonrpc-lite';
import { Server, WebSocket as MockWebSocket } from 'mock-socket';
+import JsonRpcTransport, { TimeOutError } from '../app/lib/jsonrpc-transport';
describe('JSON RPC transport', () => {
const WEBSOCKET_URL = 'ws://localhost:8080';
@@ -12,104 +10,94 @@ describe('JSON RPC transport', () => {
beforeEach(() => {
server = new Server(WEBSOCKET_URL);
- transport = new JsonRpcTransport((s) => new MockWebSocket(s));
+ transport = new JsonRpcTransport((url) => new MockWebSocket(url));
});
afterEach(() => {
server.close();
});
- it('should send as soon as the websocket connects', () => {
- server.on('message', (msg) => {
- const { payload } = jsonrpc.parse(msg);
-
- if (payload.method === 'hello') {
- server.send(JSON.stringify(jsonrpc.success(payload.id, 'ok')));
- }
- });
-
- const sendPromise = transport.send('hello');
-
- transport.connect(WEBSOCKET_URL);
-
- return expect(sendPromise).to.eventually.be.fulfilled;
- });
-
- it('should reject failed jsonrpc requests', () => {
- server.on('message', (msg) => {
- const { payload } = jsonrpc.parse(msg);
-
- if (payload.method === 'invalid-method') {
- server.send(
- JSON.stringify(
- jsonrpc.error(payload.id, new jsonrpc.JsonRpcError('Method not found', -32601)),
- ),
- );
- }
+ it('should reject failed jsonrpc requests', async () => {
+ server.on('connection', (socket) => {
+ socket.on('message', (msg) => {
+ const { payload } = jsonrpc.parse(msg);
+ if (payload.method === 'invalid-method') {
+ socket.send(
+ JSON.stringify(
+ jsonrpc.error(payload.id, new jsonrpc.JsonRpcError('Method not found', -32601)),
+ ),
+ );
+ }
+ });
});
+ await transport.connect(WEBSOCKET_URL);
const sendPromise = transport.send('invalid-method');
- transport.connect(WEBSOCKET_URL);
-
return expect(sendPromise).to.eventually.be.rejectedWith('Method not found');
});
- it('should route reply to correct promise', () => {
- server.on('message', (msg) => {
- const { payload } = jsonrpc.parse(msg);
-
- if (payload.method === 'a message') {
- server.send(JSON.stringify(jsonrpc.success(payload.id, 'a reply')));
- }
+ it('should route reply to correct promise', async () => {
+ server.on('connection', (socket) => {
+ socket.on('message', (msg) => {
+ const { payload } = jsonrpc.parse(msg);
+ if (payload.method === 'a message') {
+ socket.send(JSON.stringify(jsonrpc.success(payload.id, 'a reply')));
+ }
+ });
});
+ await transport.connect(WEBSOCKET_URL);
+
const decoyPromise = transport.send('a decoy', [], 100);
const messagePromise = transport.send('a message', [], 100);
- transport.connect(WEBSOCKET_URL);
-
return Promise.all([
expect(messagePromise).to.eventually.be.equal('a reply'),
- expect(decoyPromise).to.eventually.be.rejectedWith(JsonRpcTransportTimeOutError),
+ expect(decoyPromise).to.eventually.be.rejectedWith(TimeOutError),
]);
});
- it('should timeout if no response is returned', () => {
+ it('should timeout if no response is returned', async () => {
+ await transport.connect(WEBSOCKET_URL);
const sendPromise = transport.send('timeout-message', {}, 1);
- transport.connect(WEBSOCKET_URL);
-
- return expect(sendPromise).to.eventually.be.rejectedWith(
- JsonRpcTransportTimeOutError,
- 'Request timed out',
- );
+ return expect(sendPromise).to.eventually.be.rejectedWith(TimeOutError, 'Request timed out');
});
- it('should route notifications', () => {
- server.on('message', (msg) => {
- const { payload } = jsonrpc.parse(msg);
+ it('should route notifications', async () => {
+ server.on('connection', (socket) => {
+ socket.on('message', (msg) => {
+ const { payload } = jsonrpc.parse(msg);
+ if (payload.method === 'event_subscribe') {
+ socket.send(JSON.stringify(jsonrpc.success(payload.id, 1)));
+ }
+ });
+ });
+
+ await transport.connect(WEBSOCKET_URL);
- if (payload.method === 'event_subscribe') {
- server.send(JSON.stringify(jsonrpc.success(payload.id, 1)));
+ const eventPromiseHelper = (() => {
+ let borrowedResolve: ?(mixed) => void;
+ const promise = new Promise((resolve) => (borrowedResolve = resolve));
+ /* Flow does not understand that the body of Promise runs immediately.
+ see https://github.com/facebook/flow/issues/6711 */
+ if (!borrowedResolve) {
+ throw new Error();
}
- });
+ return {
+ resolve: borrowedResolve,
+ promise,
+ };
+ })();
- transport.connect(WEBSOCKET_URL);
+ await transport.subscribe('event', eventPromiseHelper.resolve);
- let subscribePromise;
- const eventPromise = new Promise((resolve) => {
- subscribePromise = transport.subscribe('event', resolve).then((value) => {
- server.send(
- JSON.stringify(jsonrpc.notification('event', { subscription: 1, result: 'beacon' })),
- );
- return value;
- });
- });
+ server.emit(
+ 'message',
+ JSON.stringify(jsonrpc.notification('event', { subscription: 1, result: 'beacon' })),
+ );
- return Promise.all([
- expect(subscribePromise).to.eventually.be.fulfilled,
- expect(eventPromise).to.eventually.be.equal('beacon'),
- ]);
+ return expect(eventPromiseHelper.promise).to.eventually.be.equal('beacon');
});
});
diff --git a/yarn.lock b/yarn.lock
index 6e29ef5472..d882450a94 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -86,6 +86,19 @@
lodash "^4.2.0"
to-fast-properties "^2.0.0"
+"@octokit/rest@^15.2.6":
+ version "15.9.5"
+ resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-15.9.5.tgz#e356d202bd0b517e381f705ad77d98ccb84e0c65"
+ dependencies:
+ before-after-hook "^1.1.0"
+ btoa-lite "^1.0.0"
+ debug "^3.1.0"
+ http-proxy-agent "^2.1.0"
+ https-proxy-agent "^2.2.0"
+ lodash "^4.17.4"
+ node-fetch "^2.1.1"
+ url-template "^2.0.8"
+
"@types/lodash@4.14.110":
version "4.14.110"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.110.tgz#fb07498f84152947f30ea09d89207ca07123461e"
@@ -170,6 +183,12 @@ after@0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
+agent-base@4, agent-base@^4.1.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9"
+ dependencies:
+ es6-promisify "^5.0.0"
+
ajv-keywords@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762"
@@ -372,7 +391,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.4.1:
+assert@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
dependencies:
@@ -1259,6 +1278,10 @@ beeper@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809"
+before-after-hook@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-1.1.0.tgz#83165e15a59460d13702cb8febd6a1807896db5a"
+
better-assert@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522"
@@ -1458,6 +1481,10 @@ bser@^2.0.0:
dependencies:
node-int64 "^0.4.0"
+btoa-lite@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337"
+
buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
@@ -2884,10 +2911,16 @@ es-to-primitive@^1.1.1:
is-date-object "^1.0.1"
is-symbol "^1.0.1"
-es6-promise@^4.0.5:
+es6-promise@^4.0.3, es6-promise@^4.0.5:
version "4.2.4"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29"
+es6-promisify@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
+ dependencies:
+ es6-promise "^4.0.3"
+
escape-html@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.2.tgz#d77d32fa98e38c2f41ae85e9278e0e0e6ba1022c"
@@ -2917,9 +2950,9 @@ eslint-config-prettier@^2.9.0:
dependencies:
get-stdin "^5.0.1"
-eslint-plugin-flowtype@^2.49.3:
- version "2.49.3"
- resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-2.49.3.tgz#ccca6ee5ba2027eb3ed36bc2ec8c9a842feee841"
+eslint-plugin-flowtype@^2.50.0:
+ version "2.50.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-2.50.0.tgz#953e262fa9b5d0fa76e178604892cf60dfb916da"
dependencies:
lodash "^4.17.10"
@@ -2927,14 +2960,14 @@ eslint-plugin-promise@^3.8.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.8.0.tgz#65ebf27a845e3c1e9d6f6a5622ddd3801694b621"
-eslint-plugin-react@^7.9.1:
- version "7.9.1"
- resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.9.1.tgz#101aadd15e7c7b431ed025303ac7b421a8e3dc15"
+eslint-plugin-react@^7.10.0:
+ version "7.10.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.10.0.tgz#af5c1fef31c4704db02098f9be18202993828b50"
dependencies:
doctrine "^2.1.0"
- has "^1.0.2"
+ has "^1.0.3"
jsx-ast-utils "^2.0.1"
- prop-types "^15.6.1"
+ prop-types "^15.6.2"
eslint-scope@^3.7.1, eslint-scope@~3.7.1:
version "3.7.1"
@@ -3354,14 +3387,14 @@ flow-bin@^0.78.0:
version "0.78.0"
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.78.0.tgz#df9fe7f9c9a2dfaff39083949fe2d831b41627b7"
-flow-typed@^2.4.0:
- version "2.4.0"
- resolved "https://registry.yarnpkg.com/flow-typed/-/flow-typed-2.4.0.tgz#3d2f48cf85df29df3bca6745b623726496ff4788"
+flow-typed@^2.5.1:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/flow-typed/-/flow-typed-2.5.1.tgz#0ff565cc94d2af8c557744ba364b6f14726a6b9f"
dependencies:
+ "@octokit/rest" "^15.2.6"
babel-polyfill "^6.26.0"
colors "^1.1.2"
fs-extra "^5.0.0"
- github "0.2.4"
glob "^7.1.2"
got "^7.1.0"
md5 "^2.1.0"
@@ -3603,12 +3636,6 @@ github-username@^2.0.0:
gh-got "^2.2.0"
meow "^3.5.0"
-github@0.2.4:
- version "0.2.4"
- resolved "https://registry.yarnpkg.com/github/-/github-0.2.4.tgz#24fa7f0e13fa11b946af91134c51982a91ce538b"
- dependencies:
- mime "^1.2.11"
-
glob-base@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
@@ -3938,7 +3965,7 @@ has@^1.0.1:
dependencies:
function-bind "^1.0.2"
-has@^1.0.2:
+has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
dependencies:
@@ -4063,6 +4090,13 @@ http-errors@~1.6.1:
setprototypeof "1.0.3"
statuses ">= 1.3.1 < 2"
+http-proxy-agent@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405"
+ dependencies:
+ agent-base "4"
+ debug "3.1.0"
+
http-proxy@1.15.2:
version "1.15.2"
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.15.2.tgz#642fdcaffe52d3448d2bda3b0079e9409064da31"
@@ -4086,6 +4120,13 @@ http-signature@~1.2.0:
jsprim "^1.2.2"
sshpk "^1.7.0"
+https-proxy-agent@^2.2.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0"
+ dependencies:
+ agent-base "^4.1.0"
+ debug "^3.1.0"
+
iconv-lite@0.4.11:
version "0.4.11"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.11.tgz#2ecb42fd294744922209a2e7c404dac8793d8ade"
@@ -4912,10 +4953,6 @@ 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, lodash@^4.17.5:
- 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"
@@ -4928,6 +4965,10 @@ lodash@^4.14.0, lodash@^4.15.0, lodash@^4.16.6, lodash@^4.3.0, lodash@^4.6.1:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
+lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5:
+ 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"
@@ -5194,7 +5235,7 @@ mime@1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53"
-mime@^1.2.11, mime@^1.3.4:
+mime@^1.3.4:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
@@ -5275,9 +5316,11 @@ mocha@^5.2.0:
mkdirp "0.5.1"
supports-color "5.4.0"
-mock-socket@^7.1.0:
- version "7.1.0"
- resolved "https://registry.yarnpkg.com/mock-socket/-/mock-socket-7.1.0.tgz#482ecccafb0f0e86b8905ba2aa57c1fe73ba262d"
+mock-socket@^8.0.2:
+ version "8.0.2"
+ resolved "https://registry.yarnpkg.com/mock-socket/-/mock-socket-8.0.2.tgz#899dbe376a33a10165341939e5dd4653532dcd13"
+ dependencies:
+ url-parse "^1.2.0"
moment@^2.20.1:
version "2.20.1"
@@ -5386,6 +5429,10 @@ node-fetch@^1.0.1, node-fetch@^1.3.3:
encoding "^0.1.11"
is-stream "^1.0.1"
+node-fetch@^2.1.1:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.2.0.tgz#4ee79bde909262f9775f731e3656d0db55ced5b5"
+
node-int64@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
@@ -5998,9 +6045,9 @@ preserve@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
-prettier@1.13.7:
- version "1.13.7"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.13.7.tgz#850f3b8af784a49a6ea2d2eaa7ed1428a34b7281"
+prettier@1.14.0:
+ version "1.14.0"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.14.0.tgz#847c235522035fd988100f1f43cf20a7d24f9372"
pretty-bytes@^1.0.2:
version "1.0.4"
@@ -6054,13 +6101,6 @@ promise@^7.1.1:
dependencies:
asap "~2.0.3"
-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.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"
@@ -6077,6 +6117,13 @@ prop-types@^15.6.1:
loose-envify "^1.3.1"
object-assign "^4.1.1"
+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"
+
ps-tree@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.1.0.tgz#b421b24140d6203f1ed3c76996b4427b08e8c014"
@@ -6111,6 +6158,10 @@ qs@~6.5.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
+querystringify@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.0.0.tgz#fa3ed6e68eb15159457c89b37bc6472833195755"
+
quickselect@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-1.0.1.tgz#1e6ceaa9db1ca7c75aafcc863c7bef2037ca62a1"
@@ -6375,20 +6426,20 @@ react@^16.0.0:
object-assign "^4.1.1"
prop-types "^15.6.0"
-reactxp@^1.3.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/reactxp/-/reactxp-1.3.0.tgz#ccb3859b7713ea0cb921fa3155c84f02b36b0f17"
+reactxp@^1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/reactxp/-/reactxp-1.3.3.tgz#fc950b9c63d5f78d2207341514231330f86309f2"
dependencies:
"@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"
+ 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"
@@ -6545,7 +6596,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"
@@ -6725,7 +6776,7 @@ require-uncached@^1.0.3:
caller-path "^0.1.0"
resolve-from "^1.0.0"
-requires-port@1.x.x:
+requires-port@1.x.x, requires-port@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
@@ -7400,7 +7451,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:
@@ -7456,7 +7507,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"
@@ -7841,6 +7892,17 @@ url-parse-lax@^1.0.0:
dependencies:
prepend-http "^1.0.1"
+url-parse@^1.2.0:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.3.tgz#bfaee455c889023219d757e045fa6a684ec36c15"
+ dependencies:
+ querystringify "^2.0.0"
+ requires-port "^1.0.0"
+
+url-template@^2.0.8:
+ version "2.0.8"
+ resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21"
+
url-to-options@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9"