diff --git a/README.md b/README.md
index 5ed26e0cfd3a385cb22e1c761c64e888cf179e2b..b2af6a0e4aee77c8202ab213a9971bd77dfaa86c 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,35 @@
-# parity.js
+# Parity Wallet
-JavaScript APIs and UIs for Parity.
+The Electron app of Parity Wallet user interface.
-## development
+## Development
0. Install [Node](https://nodejs.org/) if not already available
-0. Change to the `js` directory inside `parity/`
0. Install the npm modules via `npm install`
0. Parity should be run with `parity --ui-no-validation [...options]` (where `options` can be `--chain testnet`)
0. Start the development environment via `npm start`
-0. Connect to the [UI](http://localhost:3000)
+0. Connect to [http://localhost:3000](http://localhost:3000)
+
+If you wish to develop directly inside the Electron app, you can run parallely
+
+0. `npm run electron:dev`
+
+This will launch an Electron app listening to localhost:3000.
+
+Finally, to test the Electron app in an almost-production environment, run
+
+0. `npm run electron`
+
+This will *not* listen to localhost:3000, but will instead bundle the whole app, and launch an Electron instance which launches the bundled app. If everything works using this command, there's a good chance that the final binary will work too.
+
+## Create Electron binaries on Travis
+
+Travis will do the dirty work of creating installers on all platforms. To do so, simply push your code to the `ci-package` branch.
+
+For example, if you do your modifications on your personal branch `my-branch` and want to build binaries for this branch, run:
+
+0. `git checkout ci-package`
+0. `git reset --hard my-branch` (copy my-branch to ci-package)
+0. `git push --force ci-package` (will overwrite the previous ci-package, but that's all right)
+
+Travis will trigger the binaries build when on `ci-package` branch. The binaries will then be uploaded to https://github.com/Parity-JS/shell/releases as drafts.
diff --git a/package-lock.json b/package-lock.json
index 1b97895f19920900410b451f99bd2781fece9709..12906dbc2e38220526a622dae86f5fcaecb1a1ca 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3573,6 +3573,11 @@
"delayed-stream": "1.0.0"
}
},
+ "command-exists": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.2.tgz",
+ "integrity": "sha1-EoGcZPr5VEbsCuB/5sr7brNwiyI="
+ },
"commander": {
"version": "2.12.2",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz",
diff --git a/package.json b/package.json
index 6bebe03aff634ace50b9fdf3fdcc63dcac06b891..385520e6d0fca2ea7e456e12390d6a8950d6702f 100644
--- a/package.json
+++ b/package.json
@@ -24,8 +24,9 @@
"Parity"
],
"scripts": {
- "build": "npm run build:inject && npm run build:app && npm run build:embed",
+ "build": "npm run build:inject && npm run build:app && npm run build:electron && npm run build:embed",
"build:app": "webpack --config webpack/app",
+ "build:electron": "webpack --config webpack/electron",
"build:inject": "webpack --config webpack/inject",
"build:embed": "cross-env EMBED=1 node webpack/embed",
"build:i18n": "npm run clean && npm run build && babel-node ./scripts/build-i18n.js",
@@ -42,7 +43,7 @@
"lint:js": "eslint --ignore-path .gitignore ./src/",
"lint:js:cached": "eslint --cache --ignore-path .gitignore ./src/",
"lint:js:fix": "eslint --fix --ignore-path .gitignore ./src/",
- "package": "npm run build && electron-builder --config package.electron.json",
+ "package": "npm run ci:build && electron-builder --config package.electron.json",
"start": "npm run clean && npm install && npm run build:inject && npm run start:app",
"start:app": "node webpack/dev.server",
"test": "cross-env NODE_ENV=test mocha 'src/**/*.spec.js'",
@@ -154,6 +155,7 @@
"@parity/plugin-signer-qr": "github:parity-js/plugin-signer-qr#c275ba13524e9f6759079fabd10faf49cc00cfc0",
"@parity/shared": "2.2.25",
"@parity/ui": "3.1.4",
+ "command-exists": "1.2.2",
"is-electron": "2.1.0",
"keythereum": "1.0.2",
"lodash.flatten": "4.4.0",
diff --git a/src/Connection/connection.css b/src/Connection/connection.css
index 62aea01c6aaeaca4f079873ba441f1d0deac1003..a327eefb3260da8a96a5428cf9f6788092663fc3 100644
--- a/src/Connection/connection.css
+++ b/src/Connection/connection.css
@@ -37,6 +37,7 @@
.body {
box-shadow: rgba(0, 0, 0, 0.25) 0 14px 45px, rgba(0, 0, 0, 0.22) 0 10px 18px;
color: rgb(208, 208, 208);
+ background-color: #405161;
margin: 0 auto;
max-width: 40em;
padding: 2em 4em;
diff --git a/src/Connection/connection.js b/src/Connection/connection.js
index 0817f25cea5aee3bf50038f6bf0b5b32fe9147ab..2cc2073e9d45285c5ed8d1596f92e30bb95b826d 100644
--- a/src/Connection/connection.js
+++ b/src/Connection/connection.js
@@ -15,16 +15,24 @@
// along with Parity. If not, see .
import React, { Component } from 'react';
-import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
+import { FormattedMessage } from 'react-intl';
+import isElectron from 'is-electron';
import PropTypes from 'prop-types';
import GradientBg from '@parity/ui/lib/GradientBg';
+import Icon from 'semantic-ui-react/dist/commonjs/elements/Icon';
import Input from '@parity/ui/lib/Form/Input';
import { CompareIcon, ComputerIcon, DashboardIcon, KeyIcon } from '@parity/ui/lib/Icons';
import styles from './connection.css';
+let electron;
+
+if (isElectron()) {
+ electron = window.require('electron');
+}
+
class Connection extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
@@ -38,12 +46,44 @@ class Connection extends Component {
state = {
loading: false,
+ parityInstallLocation: true,
token: '',
validToken: false
}
+ componentDidMount () {
+ if (isElectron()) {
+ const { ipcRenderer, remote } = electron;
+ const parityInstallLocation = remote.getGlobal('parityInstallLocation');
+
+ this.setState({ parityInstallLocation });
+
+ // Run parity if parityInstallLocation !== null and not connected yet
+ if (!parityInstallLocation) { return; }
+
+ // After 3s, check if ui is still isConnecting
+ // If yes, then try to run `parity`
+ // The reason why we do this after 3s, is that even when parity is
+ // running, isConnecting is true on componentWillMount (lag to ping the
+ // node). -Amaury 13.03.2018
+ // TODO Find a more reliable way to know if parity is running or not
+ setTimeout(() => {
+ if (!this.props.isConnecting) { return; }
+ console.log('Launching parity.');
+ ipcRenderer.send('asynchronous-message', 'run-parity');
+ }, 3000);
+ }
+ }
+
+ handleOpenWebsite = () => {
+ const { shell } = electron;
+
+ shell.openExternal('https://parity.io');
+ }
+
render () {
const { isConnecting, isConnected, needsToken } = this.props;
+ const { parityInstallLocation } = this.state;
if (!isConnecting && isConnected) {
return null;
@@ -65,14 +105,18 @@ class Connection extends Component {
{
needsToken
?
- :
+ : parityInstallLocation
+ ?
+ :
}
{
needsToken
? this.renderSigner()
- : this.renderPing()
+ : parityInstallLocation
+ ? this.renderPing()
+ : this.renderParityNotInstalled()
}
@@ -135,6 +179,18 @@ class Connection extends Component {
);
}
+ renderParityNotInstalled () {
+ return (
+
+ https://parity.io } }
+ />
+
+ );
+ }
+
renderPing () {
return (
diff --git a/src/index.electron.js b/src/index.electron.js
index 0e9c56f57c69e277ae81f59746973af524e5632f..0d9be070d5642f43a7aa05f05f57e530f1414274 100644
--- a/src/index.electron.js
+++ b/src/index.electron.js
@@ -14,18 +14,27 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
-const argv = require('yargs').argv;
+// eslint-disable-next-line
+const dynamicRequire = typeof __non_webpack_require__ === 'undefined' ? require : __non_webpack_require__; // Dynamic require https://github.com/yargs/yargs/issues/781
+
+const argv = dynamicRequire('yargs').argv;
const electron = require('electron');
const path = require('path');
const pick = require('lodash/pick');
+const { spawn } = require('child_process');
const url = require('url');
-const { app, BrowserWindow, Menu, session } = electron;
+const parityInstallLocation = require('./util/parityInstallLocation');
+
+const { app, BrowserWindow, ipcMain, Menu, session, shell } = electron;
let mainWindow;
// Will send these variables to renderers via IPC
global.dirName = __dirname;
Object.assign(global, pick(argv, ['wsInterface', 'wsPort']));
+parityInstallLocation()
+ .then((location) => { global.parityInstallLocation = location; })
+ .catch(() => { });
function createWindow () {
mainWindow = new BrowserWindow({
@@ -42,13 +51,21 @@ function createWindow () {
// TODO Check if file exists?
mainWindow.loadURL(
url.format({
- pathname: path.join(__dirname, '../.build/index.html'),
+ pathname: path.join(__dirname, '..', '.build', 'index.html'),
protocol: 'file:',
slashes: true
})
);
}
+ // Listen to messages from renderer process
+ ipcMain.on('asynchronous-message', (event, arg) => {
+ // Run an instance of parity if we receive the `run-parity` message
+ if (arg === 'run-parity') {
+ spawn(global.parityInstallLocation);
+ }
+ });
+
// Create the Application's main menu
// https://github.com/electron/electron/blob/master/docs/api/menu.md#examples
const template = [
@@ -88,12 +105,50 @@ function createWindow () {
submenu: [
{
label: 'Learn More',
- click () { require('electron').shell.openExternal('https://parity.io'); }
+ click () { shell.openExternal('https://parity.io'); }
}
]
}
];
+ if (process.platform === 'darwin') {
+ template.unshift({
+ label: app.getName(),
+ submenu: [
+ { role: 'about' },
+ { type: 'separator' },
+ { role: 'services', submenu: [] },
+ { type: 'separator' },
+ { role: 'hide' },
+ { role: 'hideothers' },
+ { role: 'unhide' },
+ { type: 'separator' },
+ { role: 'quit' }
+ ]
+ });
+
+ // Edit menu
+ template[1].submenu.push(
+ { type: 'separator' },
+ {
+ label: 'Speech',
+ submenu: [
+ { role: 'startspeaking' },
+ { role: 'stopspeaking' }
+ ]
+ }
+ );
+
+ // Window menu
+ template[3].submenu = [
+ { role: 'close' },
+ { role: 'minimize' },
+ { role: 'zoom' },
+ { type: 'separator' },
+ { role: 'front' }
+ ];
+ }
+
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
diff --git a/src/util/parityInstallLocation.js b/src/util/parityInstallLocation.js
new file mode 100644
index 0000000000000000000000000000000000000000..5cc148656ce6623c038f97ded8340ca127608179
--- /dev/null
+++ b/src/util/parityInstallLocation.js
@@ -0,0 +1,49 @@
+// Copyright 2015-2017 Parity Technologies (UK) Ltd.
+// This file is part of Parity.
+
+// Parity is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity. If not, see .
+
+const commandExists = require('command-exists');
+const fs = require('fs');
+const util = require('util');
+
+const promiseAny = require('./promiseAny');
+
+const fsExists = util.promisify(fs.stat);
+
+// Locations to test if parity binary exists
+// TODO This could be improved
+const locations = {
+ linux: ['/bin/parity', '/usr/bin/parity', '/usr/local/bin/parity'],
+ darwin: ['/Applications/Parity Ethereum.app/Contents/MacOS/parity'],
+ win32: ['C:\\Program Files\\Parity Technologies\\Parity\\parity.exe']
+};
+
+/**
+ * This function checks if parity has been installed on the local machine:
+ * - first check if the program is in $PATH, using `command-exists`
+ * - then check the OS default installation dir if a parity folder exists
+ * This function should run in node env.
+ * Returns a string which is the command to run parity.
+ */
+const parityInstallLocation = () => {
+ return commandExists('parity') // First test is `parity` command exists
+ .then(() => 'parity') // If yes, return `parity` as command
+ .catch(() => promiseAny(locations[process.platform].map(
+ location => fsExists(location).then(() => location) // Then test if OS-specific locations contain parity
+ )))
+ .catch(() => null); // Return null if no parity is found
+};
+
+module.exports = parityInstallLocation;
diff --git a/src/util/promiseAny.js b/src/util/promiseAny.js
new file mode 100644
index 0000000000000000000000000000000000000000..c850f70ab032d315e5a4931b72153c7fa35d61bd
--- /dev/null
+++ b/src/util/promiseAny.js
@@ -0,0 +1,40 @@
+// Copyright 2015-2017 Parity Technologies (UK) Ltd.
+// This file is part of Parity.
+
+// Parity is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity. If not, see .
+
+/**
+ * Resolves when the 1st promise in an array of promises resolves
+ * @see https://stackoverflow.com/questions/37234191/resolve-es6-promise-with-first-success
+ * @param {Array} promises
+ */
+const promiseAny = (promises) => {
+ return Promise.all(promises.map(p => {
+ // If a request fails, count that as a resolution so it will keep
+ // waiting for other possible successes. If a request succeeds,
+ // treat it as a rejection so Promise.all immediately bails out.
+ return p.then(
+ val => Promise.reject(val),
+ err => Promise.resolve(err)
+ );
+ }))
+ .then(
+ // If '.all' resolved, we've just got an array of errors.
+ errors => Promise.reject(errors),
+ // If '.all' rejected, we've got the result we wanted.
+ val => Promise.resolve(val)
+ );
+};
+
+module.exports = promiseAny;
diff --git a/webpack/app.js b/webpack/app.js
index 1e0aeb5f6b491364cc46480fe584453eb729de05..ea554a4a5163d9cc256e50c8c1150315903c92ae 100644
--- a/webpack/app.js
+++ b/webpack/app.js
@@ -174,7 +174,7 @@ module.exports = {
plugins,
new HtmlWebpackPlugin({
- title: 'Parity',
+ title: 'Parity Wallet',
filename: 'index.html',
template: './index.parity.ejs',
favicon: FAVICON,
@@ -195,10 +195,6 @@ module.exports = {
from: path.join(__dirname, '../src/error_pages.css'),
to: 'styles.css'
},
- {
- from: path.join(__dirname, '../src/index.electron.js'),
- to: 'electron.js'
- },
{
from: path.join(__dirname, '../package.electron.json'),
to: 'package.json'
diff --git a/webpack/electron.js b/webpack/electron.js
new file mode 100644
index 0000000000000000000000000000000000000000..907e3ccfbe23e0217d2b89980b5efe4b85e1d211
--- /dev/null
+++ b/webpack/electron.js
@@ -0,0 +1,61 @@
+
+// Copyright 2015-2017 Parity Technologies (UK) Ltd.
+// This file is part of Parity.
+
+// Parity is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity. If not, see .
+
+const path = require('path');
+
+const rulesEs6 = require('./rules/es6');
+const rulesParity = require('./rules/parity');
+const Shared = require('./shared');
+
+const DEST = process.env.BUILD_DEST || '.build';
+const ENV = process.env.NODE_ENV || 'development';
+
+const isProd = ENV === 'production';
+
+module.exports = {
+ cache: !isProd,
+ devtool: '#source-map',
+ context: path.join(__dirname, '../src'),
+ entry: { electron: './index.electron.js' },
+ output: {
+ path: path.join(__dirname, '../', DEST),
+ filename: '[name].js'
+ },
+ node: {
+ __dirname: false
+ },
+ target: 'electron-main',
+
+ module: {
+ rules: [
+ rulesParity,
+ rulesEs6,
+ {
+ test: /\.js$/,
+ exclude: /node_modules/,
+ use: [{
+ loader: 'happypack/loader',
+ options: {
+ id: 'babel'
+ }
+ }]
+ }
+ ]
+ },
+
+ plugins: Shared.getPlugins()
+};