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() +};