aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--app/_locales/en/messages.json33
-rw-r--r--app/images/connect-icon.svg11
-rw-r--r--app/scripts/lib/util.js2
-rw-r--r--app/scripts/metamask-controller.js70
-rw-r--r--app/scripts/platforms/extension.js7
-rw-r--r--package-lock.json81
-rw-r--r--package.json3
-rwxr-xr-xstart-trezor10
-rw-r--r--ui/app/actions.js42
-rw-r--r--ui/app/components/account-menu/index.js17
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/account-list.js128
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/connect-screen.js55
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/index.js154
-rw-r--r--ui/app/components/pages/create-account/index.js25
-rw-r--r--ui/app/components/pages/create-account/new-account.js2
-rw-r--r--ui/app/css/itcss/components/new-account.scss118
-rw-r--r--ui/app/routes.js2
17 files changed, 741 insertions, 19 deletions
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 621775592..e1f321c68 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -11,6 +11,9 @@
"accountName": {
"message": "Account Name"
},
+ "accountSelectionRequired": {
+ "message": "You need to select an account!"
+ },
"address": {
"message": "Address"
},
@@ -77,6 +80,9 @@
"borrowDharma": {
"message": "Borrow With Dharma (Beta)"
},
+ "browserNotSupported": {
+ "message": "Bummer! Your Browser is not supported..."
+ },
"builtInCalifornia": {
"message": "MetaMask is designed and built in California."
},
@@ -104,6 +110,9 @@
"close": {
"message": "Close"
},
+ "chromeRequiredForTrezor":{
+ "message": "You need to use Metamask on Google Chrome in order to connect to your TREZOR device."
+ },
"confirm": {
"message": "Confirm"
},
@@ -119,6 +128,18 @@
"confirmTransaction": {
"message": "Confirm Transaction"
},
+ "connectHardware": {
+ "message": "Connect Hardware"
+ },
+ "connect": {
+ "message": "Connect"
+ },
+ "connecting": {
+ "message": "Connecting..."
+ },
+ "connectToTrezor": {
+ "message": "Connect to Trezor"
+ },
"continue": {
"message": "Continue"
},
@@ -244,6 +265,9 @@
"done": {
"message": "Done"
},
+ "downloadGoogleChrome": {
+ "message": "Download Google Chrome"
+ },
"downloadStateLogs": {
"message": "Download State Logs"
},
@@ -612,6 +636,9 @@
"popularTokens": {
"message": "Popular Tokens"
},
+ "prev": {
+ "message": "Prev"
+ },
"privacyMsg": {
"message": "Privacy Policy"
},
@@ -796,6 +823,9 @@
"searchTokens": {
"message": "Search Tokens"
},
+ "selectAnAddress": {
+ "message": "Select an Address"
+ },
"sendTokensAnywhere": {
"message": "Send Tokens to anyone with an Ethereum account"
},
@@ -945,6 +975,9 @@
"unknownNetworkId": {
"message": "Unknown network ID"
},
+ "unlock": {
+ "message": "Unlock"
+ },
"unlockMessage": {
"message": "The decentralized web awaits"
},
diff --git a/app/images/connect-icon.svg b/app/images/connect-icon.svg
new file mode 100644
index 000000000..84540999a
--- /dev/null
+++ b/app/images/connect-icon.svg
@@ -0,0 +1,11 @@
+<svg width="288" height="288" xmlns="http://www.w3.org/2000/svg">
+
+ <g>
+ <title>background</title>
+ <rect fill="none" id="canvas_background" height="402" width="582" y="-1" x="-1"/>
+ </g>
+ <g>
+ <title>Layer 1</title>
+ <path fill="#ffffff" id="svg_1" d="m122,25l15,-21c4,-5 10,-5 14,0l16,22c4,5 2,10 -5,10l-12,0l0,118c0,3 3,3 5,1l25,-25c4,-4 6,-10 6,-16l0,-24c-7,0 -12,-5 -12,-12l0,-12c0,-6 5,-12 12,-12l12,0c7,0 12,5 12,12l0,12c0,7 -5,12 -12,12l0,24c0,10 -3,18 -10,25l-31,31c-4,4 -7,6 -7,16l0,49c12,3 21,13 21,26c0,15 -12,27 -27,27s-27,-12 -27,-27c0,-13 9,-23 21,-26l0,-13c0,-10 -3,-13 -7,-17l-31,-31c-6,-6 -10,-14 -10,-24l0,-25c-7,-2 -12,-9 -12,-17c0,-10 8,-18 18,-18s18,8 18,18c0,8 -5,15 -12,17l0,25c0,7 3,12 7,16l25,25c2,2 4,2 4,-1l0,-154l-12,0c-7,0 -9,-5 -4,-11z"/>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js
index 431d1e59c..51e9036cc 100644
--- a/app/scripts/lib/util.js
+++ b/app/scripts/lib/util.js
@@ -28,7 +28,7 @@ function getStack () {
*
*/
const getEnvironmentType = (url = window.location.href) => {
- if (url.match(/popup.html(?:\?.+)*$/)) {
+ if (url.match(/popup.html(?:#.*)*$/)) {
return ENVIRONMENT_TYPE_POPUP
} else if (url.match(/home.html(?:\?.+)*$/) || url.match(/home.html(?:#.*)*$/)) {
return ENVIRONMENT_TYPE_FULLSCREEN
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 450113acf..d70bac1c3 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -47,6 +47,7 @@ const percentile = require('percentile')
const seedPhraseVerifier = require('./lib/seed-phrase-verifier')
const cleanErrorStack = require('./lib/cleanErrorStack')
const log = require('loglevel')
+const TrezorKeyring = require('eth-trezor-keyring')
module.exports = class MetamaskController extends EventEmitter {
@@ -124,7 +125,9 @@ module.exports = class MetamaskController extends EventEmitter {
})
// key mgmt
+ const additionalKeyrings = [TrezorKeyring]
this.keyringController = new KeyringController({
+ keyringTypes: additionalKeyrings,
initState: initState.KeyringController,
getNetwork: this.networkController.getNetworkState.bind(this.networkController),
encryptor: opts.encryptor || undefined,
@@ -353,6 +356,10 @@ module.exports = class MetamaskController extends EventEmitter {
resetAccount: nodeify(this.resetAccount, this),
importAccountWithStrategy: nodeify(this.importAccountWithStrategy, this),
+ // trezor
+ connectHardware: nodeify(this.connectHardware, this),
+ unlockTrezorAccount: nodeify(this.unlockTrezorAccount, this),
+
// vault management
submitPassword: nodeify(this.submitPassword, this),
@@ -509,6 +516,69 @@ module.exports = class MetamaskController extends EventEmitter {
}
//
+ // Hardware
+ //
+
+ /**
+ * Fetch account list from a trezor device.
+ *
+ * @returns [] accounts
+ */
+ async connectHardware (deviceName, page) {
+
+ switch (deviceName) {
+ case 'trezor':
+ const keyringController = this.keyringController
+ let keyring = await keyringController.getKeyringsByType(
+ 'Trezor Hardware'
+ )[0]
+ if (!keyring) {
+ keyring = await this.keyringController.addNewKeyring('Trezor Hardware')
+ }
+ if (page === 0) {
+ keyring.page = 0
+ }
+ const accounts = page === -1 ? await keyring.getPreviousPage() : await keyring.getNextPage()
+ this.accountTracker.syncWithAddresses(accounts.map(a => a.address))
+ return accounts
+
+ default:
+ throw new Error('MetamaskController - Unknown device')
+ }
+ }
+
+ /**
+ * Imports an account from a trezor device.
+ *
+ * @returns {} keyState
+ */
+ async unlockTrezorAccount (index) {
+ const keyringController = this.keyringController
+ const keyring = await keyringController.getKeyringsByType(
+ 'Trezor Hardware'
+ )[0]
+ if (!keyring) {
+ throw new Error('MetamaskController - No Trezor Hardware Keyring found')
+ }
+
+ keyring.setAccountToUnlock(index)
+ const oldAccounts = await keyringController.getAccounts()
+ const keyState = await keyringController.addNewAccount(keyring)
+ const newAccounts = await keyringController.getAccounts()
+
+ this.preferencesController.setAddresses(newAccounts)
+ newAccounts.forEach(address => {
+ if (!oldAccounts.includes(address)) {
+ this.preferencesController.setSelectedAddress(address)
+ }
+ })
+
+ const { identities } = this.preferencesController.store.getState()
+ return { ...keyState, identities }
+ }
+
+
+ //
// Account Management
//
diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js
index f5cc255d1..f8dd767dc 100644
--- a/app/scripts/platforms/extension.js
+++ b/app/scripts/platforms/extension.js
@@ -17,8 +17,11 @@ class ExtensionPlatform {
return extension.runtime.getManifest().version
}
- openExtensionInBrowser () {
- const extensionURL = extension.runtime.getURL('home.html')
+ openExtensionInBrowser (route = null) {
+ let extensionURL = extension.runtime.getURL('home.html')
+ if (route) {
+ extensionURL += `#${route}`
+ }
this.openWindow({ url: extensionURL })
}
diff --git a/package-lock.json b/package-lock.json
index d2852f29a..a2b4fc34f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8454,9 +8454,9 @@
}
},
"eth-keyring-controller": {
- "version": "3.1.4",
- "resolved": "https://registry.npmjs.org/eth-keyring-controller/-/eth-keyring-controller-3.1.4.tgz",
- "integrity": "sha512-NNlVB/TBc8p9CblwECjPlUR+7MNQKiBa7tEFxIzZ9MjjNCEYPWDXTm0vJZzuDtVmFxYwIA53UD0QEn0QNxWNEQ==",
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/eth-keyring-controller/-/eth-keyring-controller-3.2.0.tgz",
+ "integrity": "sha512-GU1blKGftP6VDfI/5+9QZxPVkgm6VB7/UWTywd49rA/lNfm64mwTFjkBLG26S6UjdEJZiAU1kI1CdNWdAzZg5w==",
"dev": true,
"requires": {
"bip39": "^2.4.0",
@@ -8548,12 +8548,13 @@
"resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz",
"integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=",
"requires": {
+ "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7",
"ethereumjs-util": "^5.1.1"
},
"dependencies": {
"ethereumjs-abi": {
- "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#4ea2fdfed09e8f99117d9362d17c6b01b64a2bcf",
- "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#4ea2fdfed09e8f99117d9362d17c6b01b64a2bcf",
+ "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7",
+ "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
"requires": {
"bn.js": "^4.10.0",
"ethereumjs-util": "^5.0.0"
@@ -8774,6 +8775,63 @@
}
}
},
+ "eth-trezor-keyring": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/eth-trezor-keyring/-/eth-trezor-keyring-0.0.1.tgz",
+ "integrity": "sha512-guXGx4tiBukuRMkNSRa3YS003X6gDWnsVL6PZ7X/dPY7bbt1nRQYVonABh/gSZSJYnKws1CrvW5/ORAHd7i8Dg==",
+ "requires": {
+ "eth-sig-util": "^1.4.2",
+ "ethereumjs-tx": "^1.3.4",
+ "ethereumjs-util": "^5.1.5",
+ "events": "^2.0.0",
+ "hdkey": "0.8.0"
+ },
+ "dependencies": {
+ "ethereum-common": {
+ "version": "0.0.18",
+ "resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.0.18.tgz",
+ "integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8="
+ },
+ "ethereumjs-tx": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.6.tgz",
+ "integrity": "sha512-wzsEs0mCSLqdDjqSDg6AWh1hyL8H3R/pyZxehkcCXq5MJEFXWz+eJ2jSv+3yEaLy6tXrNP7dmqS3Kyb3zAONkg==",
+ "requires": {
+ "ethereum-common": "^0.0.18",
+ "ethereumjs-util": "^5.0.0"
+ }
+ },
+ "ethereumjs-util": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz",
+ "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==",
+ "requires": {
+ "bn.js": "^4.11.0",
+ "create-hash": "^1.1.2",
+ "ethjs-util": "^0.1.3",
+ "keccak": "^1.0.2",
+ "rlp": "^2.0.0",
+ "safe-buffer": "^5.1.1",
+ "secp256k1": "^3.0.1"
+ }
+ },
+ "events": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz",
+ "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg=="
+ },
+ "hdkey": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/hdkey/-/hdkey-0.8.0.tgz",
+ "integrity": "sha512-oYsdlK22eobT68N5faWI3776f6tOLyqxLLYwxMx+TP0rkWzuCs0oiOm2VbLWcxdpHFP4LtiRR8udaIX8VkEaZQ==",
+ "requires": {
+ "coinstring": "^2.0.0",
+ "safe-buffer": "^5.1.1",
+ "secp256k1": "^3.0.1"
+ }
+ }
+ }
+ },
"eth-tx-summary": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/eth-tx-summary/-/eth-tx-summary-3.2.1.tgz",
@@ -30647,6 +30705,7 @@
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+ "dev": true,
"requires": {
"is-typedarray": "^1.0.0"
}
@@ -31670,6 +31729,7 @@
"resolved": "https://registry.npmjs.org/web3/-/web3-0.20.3.tgz",
"integrity": "sha1-yqRDc9yIFayHZ73ba6cwc5ZMqos=",
"requires": {
+ "bignumber.js": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934",
"crypto-js": "^3.1.4",
"utf8": "^2.1.1",
"xhr2": "*",
@@ -31678,7 +31738,7 @@
"dependencies": {
"bignumber.js": {
"version": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934",
- "from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934"
+ "from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git"
}
}
},
@@ -32177,7 +32237,8 @@
"dev": true,
"requires": {
"underscore": "1.8.3",
- "web3-core-helpers": "1.0.0-beta.34"
+ "web3-core-helpers": "1.0.0-beta.34",
+ "websocket": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2"
},
"dependencies": {
"underscore": {
@@ -32188,7 +32249,8 @@
},
"websocket": {
"version": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2",
- "from": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2",
+ "from": "git://github.com/frozeman/WebSocket-Node.git#browserifyCompatible",
+ "dev": true,
"requires": {
"debug": "^2.2.0",
"nan": "^2.3.3",
@@ -33544,7 +33606,8 @@
"yaeti": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
- "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc="
+ "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=",
+ "dev": true
},
"yallist": {
"version": "2.1.2",
diff --git a/package.json b/package.json
index 6a5e63992..674ebd3a5 100644
--- a/package.json
+++ b/package.json
@@ -101,6 +101,7 @@
"eth-query": "^2.1.2",
"eth-sig-util": "^1.4.2",
"eth-token-tracker": "^1.1.4",
+ "eth-trezor-keyring": "0.0.1",
"ethereumjs-abi": "^0.6.4",
"ethereumjs-tx": "^1.3.0",
"ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
@@ -231,7 +232,7 @@
"eslint-plugin-mocha": "^5.0.0",
"eslint-plugin-react": "^7.4.0",
"eth-json-rpc-middleware": "^1.6.0",
- "eth-keyring-controller": "^3.1.4",
+ "eth-keyring-controller": "^3.2.0",
"file-loader": "^1.1.11",
"fs-promise": "^2.0.3",
"ganache-cli": "^6.1.0",
diff --git a/start-trezor b/start-trezor
new file mode 100755
index 000000000..1c4630b7c
--- /dev/null
+++ b/start-trezor
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+# stop any existing instance of trezord
+killall trezord
+
+# start the bridge for the simulator
+/Applications/Utilities/TREZOR\ Bridge/trezord -e 21324 >> /dev/null 2>&1 &
+
+# launch simulator
+/Users/bruno/repos/trezor-core/emu.sh \ No newline at end of file
diff --git a/ui/app/actions.js b/ui/app/actions.js
index ad890f565..f04de8fe8 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -78,6 +78,8 @@ var actions = {
addNewKeyring,
importNewAccount,
addNewAccount,
+ connectHardware,
+ unlockTrezorAccount,
NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN',
navigateToNewAccountScreen,
resetAccount,
@@ -599,6 +601,46 @@ function addNewAccount () {
}
}
+function connectHardware (deviceName, page) {
+ log.debug(`background.connectHardware`, deviceName, page)
+ return (dispatch, getState) => {
+ dispatch(actions.showLoadingIndication())
+ return new Promise((resolve, reject) => {
+ background.connectHardware(deviceName, page, (err, accounts) => {
+ if (err) {
+ log.error(err)
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch(actions.hideLoadingIndication())
+
+ forceUpdateMetamaskState(dispatch)
+ return resolve(accounts)
+ })
+ })
+ }
+}
+
+function unlockTrezorAccount (index) {
+ log.debug(`background.unlockTrezorAccount`, index)
+ return (dispatch, getState) => {
+ dispatch(actions.showLoadingIndication())
+ return new Promise((resolve, reject) => {
+ background.unlockTrezorAccount(index, (err, accounts) => {
+ if (err) {
+ log.error(err)
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch(actions.hideLoadingIndication())
+ return resolve()
+ })
+ })
+ }
+}
+
function showInfoPage () {
return {
type: actions.SHOW_INFO_PAGE,
diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js
index f34631ca8..be6963ac4 100644
--- a/ui/app/components/account-menu/index.js
+++ b/ui/app/components/account-menu/index.js
@@ -9,11 +9,16 @@ const actions = require('../../actions')
const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu')
const Identicon = require('../identicon')
const { formatBalance } = require('../../util')
+const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums')
+const { getEnvironmentType } = require('../../../../app/scripts/lib/util')
+
+
const {
SETTINGS_ROUTE,
INFO_ROUTE,
NEW_ACCOUNT_ROUTE,
IMPORT_ACCOUNT_ROUTE,
+ CONNECT_HARDWARE_ROUTE,
DEFAULT_ROUTE,
} = require('../../routes')
@@ -106,6 +111,18 @@ AccountMenu.prototype.render = function () {
icon: h('img.account-menu__item-icon', { src: 'images/import-account.svg' }),
text: this.context.t('importAccount'),
}),
+ h(Item, {
+ onClick: () => {
+ toggleAccountMenu()
+ if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
+ global.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE)
+ } else {
+ history.push(CONNECT_HARDWARE_ROUTE)
+ }
+ },
+ icon: h('img.account-menu__item-icon', { src: 'images/connect-icon.svg' }),
+ text: this.context.t('connectHardware'),
+ }),
h(Divider),
h(Item, {
onClick: () => {
diff --git a/ui/app/components/pages/create-account/connect-hardware/account-list.js b/ui/app/components/pages/create-account/connect-hardware/account-list.js
new file mode 100644
index 000000000..77e0af3ac
--- /dev/null
+++ b/ui/app/components/pages/create-account/connect-hardware/account-list.js
@@ -0,0 +1,128 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const genAccountLink = require('../../../../../lib/account-link.js')
+const { formatBalance } = require('../../../../util')
+
+class AccountList extends Component {
+ constructor (props, context) {
+ super(props)
+ }
+
+ getBalance (address) {
+ // Get the balance
+ const { accounts } = this.props
+ const balanceValue = accounts && accounts[address] ? accounts[address].balance : ''
+ const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...'
+ return formattedBalance
+ }
+
+ renderAccounts () {
+ return h('div.hw-account-list', [
+ h('div.hw-account-list__title_wrapper', [
+ h('div.hw-account-list__title', {}, [this.context.t('selectAnAddress')]),
+ h('div.hw-account-list__device', {}, ['Trezor - ETH']),
+ ]),
+ this.props.accounts.map((a, i) => {
+
+ return h('div.hw-account-list__item', { key: a.address }, [
+ h('span.hw-account-list__item__index', a.index + 1),
+ h('div.hw-account-list__item__radio', [
+ h('input', {
+ type: 'radio',
+ name: 'selectedAccount',
+ id: `address-${i}`,
+ value: a.index,
+ onChange: (e) => this.props.onAccountChange(e.target.value),
+ checked: this.props.selectedAccount === a.index.toString(),
+ }),
+ h(
+ 'label.hw-account-list__item__label',
+ {
+ htmlFor: `address-${i}`,
+ },
+ `${a.address.slice(0, 4)}...${a.address.slice(-4)}`
+ ),
+ ]),
+ h('span.hw-account-list__item__balance', `${this.getBalance(a.address)}`),
+ h(
+ 'a.hw-account-list__item__link',
+ {
+ href: genAccountLink(a.address, this.props.network),
+ target: '_blank',
+ title: this.context.t('etherscanView'),
+ },
+ h('img', { src: 'images/popout.svg' })
+ ),
+ ])
+ }),
+ ])
+ }
+
+ renderPagination () {
+ return h('div.hw-list-pagination', [
+ h(
+ 'button.btn-primary.hw-list-pagination__button',
+ {
+ onClick: () => this.props.getPage(-1),
+ },
+ `< ${this.context.t('prev')}`
+ ),
+
+ h(
+ 'button.btn-primary.hw-list-pagination__button',
+ {
+ onClick: () => this.props.getPage(1),
+ },
+ `${this.context.t('next')} >`
+ ),
+ ])
+ }
+
+ renderButtons () {
+ return h('div.new-account-create-form__buttons', {}, [
+ h(
+ 'button.btn-default.btn--large.new-account-create-form__button',
+ {
+ onClick: this.props.onCancel.bind(this),
+ },
+ [this.context.t('cancel')]
+ ),
+
+ h(
+ `button.btn-primary.btn--large.new-account-create-form__button ${this.props.selectedAccount === null ? '.btn-primary--disabled' : ''}`,
+ {
+ onClick: this.props.onUnlockAccount.bind(this),
+ },
+ [this.context.t('unlock')]
+ ),
+ ])
+ }
+
+ render () {
+ return h('div', {}, [
+ this.renderAccounts(),
+ this.renderPagination(),
+ this.renderButtons(),
+ ])
+ }
+
+}
+
+
+AccountList.propTypes = {
+ accounts: PropTypes.array.isRequired,
+ onAccountChange: PropTypes.func.isRequired,
+ getPage: PropTypes.func.isRequired,
+ network: PropTypes.string,
+ selectedAccount: PropTypes.string,
+ history: PropTypes.object,
+ onUnlockAccount: PropTypes.func,
+ onCancel: PropTypes.func,
+}
+
+AccountList.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = AccountList
diff --git a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js
new file mode 100644
index 000000000..ec6a11b40
--- /dev/null
+++ b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js
@@ -0,0 +1,55 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+
+class ConnectScreen extends Component {
+ constructor (props, context) {
+ super(props)
+ }
+
+ renderUnsupportedBrowser () {
+ return (
+ h('div', {}, [
+ h('div.hw-unsupported-browser', [
+ h('h3.hw-unsupported-browser__title', {}, this.context.t('browserNotSupported')),
+ h('p.hw-unsupported-browser__msg', {}, this.context.t('chromeRequiredForTrezor')),
+ ]),
+ h(
+ 'button.btn-primary.btn--large',
+ { onClick: () => global.platform.openWindow({
+ url: 'https://google.com/chrome',
+ }), style: { margin: 12 } },
+ this.context.t('downloadGoogleChrome')
+ ),
+ ])
+ )
+ }
+
+ renderConnectButton () {
+ return h(
+ 'button.btn-primary.btn--large',
+ { onClick: this.props.connectToTrezor.bind(this), style: { margin: 12 } },
+ this.props.btnText
+ )
+ }
+
+ render () {
+ const isChrome = window.navigator.userAgent.search('Chrome') !== -1
+ if (isChrome) {
+ return this.renderConnectButton()
+ }
+ return this.renderUnsupportedBrowser()
+ }
+}
+
+ConnectScreen.propTypes = {
+ connectToTrezor: PropTypes.func.isRequired,
+ btnText: PropTypes.string,
+}
+
+ConnectScreen.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = ConnectScreen
+
diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js
new file mode 100644
index 000000000..22c54d28c
--- /dev/null
+++ b/ui/app/components/pages/create-account/connect-hardware/index.js
@@ -0,0 +1,154 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../../../../actions')
+const ConnectScreen = require('./connect-screen')
+const AccountList = require('./account-list')
+const { DEFAULT_ROUTE } = require('../../../../routes')
+
+class ConnectHardwareForm extends Component {
+ constructor (props, context) {
+ super(props)
+ this.state = {
+ error: null,
+ response: null,
+ btnText: context.t('connectToTrezor'),
+ selectedAccount: null,
+ accounts: [],
+ }
+ }
+
+ connectToTrezor = () => {
+ if (this.state.accounts.length) {
+ return null
+ }
+ this.setState({ btnText: this.context.t('connecting')})
+ this.getPage(0)
+ }
+
+ onAccountChange = (account) => {
+ this.setState({selectedAccount: account.toString(), error: null})
+ }
+
+ getPage = (page) => {
+ this.props
+ .connectHardware('trezor', page)
+ .then(accounts => {
+ if (accounts.length) {
+ const newState = { accounts: accounts }
+ // Default to the first account
+ if (this.state.selectedAccount === null) {
+ const firstAccount = accounts[0]
+ newState.selectedAccount = firstAccount.index.toString()
+ // If the page doesn't contain the selected account, let's deselect it
+ } else if (!accounts.filter(a => a.index.toString() === '').lenght) {
+ newState.selectedAccount = null
+ }
+ this.setState(newState)
+ }
+ })
+ .catch(e => {
+ this.setState({ btnText: this.context.t('connectToTrezor') })
+ })
+ }
+
+ onUnlockAccount = () => {
+
+ if (this.state.selectedAccount === null) {
+ this.setState({ error: this.context.t('accountSelectionRequired') })
+ }
+
+ this.props.unlockTrezorAccount(this.state.selectedAccount)
+ .then(_ => {
+ this.props.history.push(DEFAULT_ROUTE)
+ }).catch(e => {
+ this.setState({ error: e.toString() })
+ })
+ }
+
+ onCancel = () => {
+ this.props.history.push(DEFAULT_ROUTE)
+ }
+
+ renderError () {
+ return this.state.error
+ ? h('span.error', { style: { marginBottom: 40 } }, this.state.error)
+ : null
+ }
+
+ renderContent () {
+ if (!this.state.accounts.length) {
+ return h(ConnectScreen, {
+ connectToTrezor: this.connectToTrezor,
+ btnText: this.state.btnText,
+ })
+ }
+
+ return h(AccountList, {
+ accounts: this.state.accounts,
+ selectedAccount: this.state.selectedAccount,
+ onAccountChange: this.onAccountChange,
+ network: this.props.network,
+ getPage: this.getPage,
+ history: this.props.history,
+ onUnlockAccount: this.onUnlockAccount,
+ onCancel: this.onCancel,
+ })
+ }
+
+ render () {
+ return h('div.new-account-create-form', [
+ this.renderError(),
+ this.renderContent(),
+ ])
+ }
+}
+
+ConnectHardwareForm.propTypes = {
+ hideModal: PropTypes.func,
+ showImportPage: PropTypes.func,
+ showConnectPage: PropTypes.func,
+ connectHardware: PropTypes.func,
+ unlockTrezorAccount: PropTypes.func,
+ numberOfExistingAccounts: PropTypes.number,
+ history: PropTypes.object,
+ t: PropTypes.func,
+ network: PropTypes.string,
+ accounts: PropTypes.object,
+}
+
+const mapStateToProps = state => {
+ const {
+ metamask: { network, selectedAddress, identities = {}, accounts = [] },
+ } = state
+ const numberOfExistingAccounts = Object.keys(identities).length
+
+ return {
+ network,
+ accounts,
+ address: selectedAddress,
+ numberOfExistingAccounts,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ connectHardware: (deviceName, page) => {
+ return dispatch(actions.connectHardware(deviceName, page))
+ },
+ unlockTrezorAccount: index => {
+ return dispatch(actions.unlockTrezorAccount(index))
+ },
+ showImportPage: () => dispatch(actions.showImportPage()),
+ showConnectPage: () => dispatch(actions.showConnectPage()),
+ }
+}
+
+ConnectHardwareForm.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(
+ ConnectHardwareForm
+)
diff --git a/ui/app/components/pages/create-account/index.js b/ui/app/components/pages/create-account/index.js
index 5681e43a9..d3de1ea01 100644
--- a/ui/app/components/pages/create-account/index.js
+++ b/ui/app/components/pages/create-account/index.js
@@ -8,7 +8,12 @@ const { getCurrentViewContext } = require('../../../selectors')
const classnames = require('classnames')
const NewAccountCreateForm = require('./new-account')
const NewAccountImportForm = require('./import-account')
-const { NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE } = require('../../../routes')
+const ConnectHardwareForm = require('./connect-hardware')
+const {
+ NEW_ACCOUNT_ROUTE,
+ IMPORT_ACCOUNT_ROUTE,
+ CONNECT_HARDWARE_ROUTE,
+} = require('../../../routes')
class CreateAccountPage extends Component {
renderTabs () {
@@ -36,6 +41,19 @@ class CreateAccountPage extends Component {
}, [
this.context.t('import'),
]),
+ h(
+ 'div.new-account__tabs__tab',
+ {
+ className: classnames('new-account__tabs__tab', {
+ 'new-account__tabs__selected': matchPath(location.pathname, {
+ path: CONNECT_HARDWARE_ROUTE,
+ exact: true,
+ }),
+ }),
+ onClick: () => history.push(CONNECT_HARDWARE_ROUTE),
+ },
+ this.context.t('connect')
+ ),
])
}
@@ -57,6 +75,11 @@ class CreateAccountPage extends Component {
path: IMPORT_ACCOUNT_ROUTE,
component: NewAccountImportForm,
}),
+ h(Route, {
+ exact: true,
+ path: CONNECT_HARDWARE_ROUTE,
+ component: ConnectHardwareForm,
+ }),
]),
]),
])
diff --git a/ui/app/components/pages/create-account/new-account.js b/ui/app/components/pages/create-account/new-account.js
index 9c94990e0..402b8f03b 100644
--- a/ui/app/components/pages/create-account/new-account.js
+++ b/ui/app/components/pages/create-account/new-account.js
@@ -62,6 +62,7 @@ class NewAccountCreateForm extends Component {
NewAccountCreateForm.propTypes = {
hideModal: PropTypes.func,
showImportPage: PropTypes.func,
+ showConnectPage: PropTypes.func,
createAccount: PropTypes.func,
numberOfExistingAccounts: PropTypes.number,
history: PropTypes.object,
@@ -92,6 +93,7 @@ const mapDispatchToProps = dispatch => {
})
},
showImportPage: () => dispatch(actions.showImportPage()),
+ showConnectPage: () => dispatch(actions.showConnectPage()),
}
}
diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss
index 293579058..551025df3 100644
--- a/ui/app/css/itcss/components/new-account.scss
+++ b/ui/app/css/itcss/components/new-account.scss
@@ -28,7 +28,6 @@
&__tab {
height: 54px;
- width: 75px;
padding: 15px 10px;
color: $dusty-gray;
font-family: Roboto;
@@ -38,10 +37,6 @@
cursor: pointer;
}
- &__tab:first-of-type {
- margin-right: 20px;
- }
-
&__tab:hover {
color: $black;
border-bottom: none;
@@ -158,6 +153,119 @@
}
}
+.hw-unsupported-browser {
+ &__title {
+ padding-top: 10px;
+ }
+
+ &__msg {
+ font-size: 14px;
+ color: #9b9b9b;
+ margin-top: 15px;
+ margin-bottom: 15px;
+ }
+}
+
+.hw-account-list {
+ display: flex;
+ flex: 1;
+ flex-flow: column;
+ width: 100%;
+
+ &__title_wrapper {
+ display: flex;
+ flex-direction: row;
+ flex: 1;
+ }
+
+ &__title {
+ margin-bottom: 23px;
+ align-self: flex-start;
+ color: $scorpion;
+ font-family: Roboto;
+ font-size: 16px;
+ line-height: 21px;
+ font-weight: bold;
+ display: flex;
+ flex: 1;
+ }
+
+ &__device {
+ margin-bottom: 23px;
+ align-self: flex-end;
+ color: $scorpion;
+ font-family: Roboto;
+ font-size: 16px;
+ line-height: 21px;
+ font-weight: normal;
+ display: flex;
+ }
+
+ &__item {
+ font-size: 15px;
+ flex-direction: row;
+ display: flex;
+ padding: 10px;
+ }
+
+ &__item:nth-of-type(even) {
+ background-color: #fbfbfb;
+ }
+
+ &__item:nth-of-type(odd) {
+ background: rgba(0, 0, 0, 0.03);
+ }
+
+ &__item:hover {
+ background-color: rgba(0, 0, 0, 0.06);
+ }
+
+ &__item__index {
+ display: flex;
+ margin-right: 20px;
+ }
+
+ &__item__radio {
+ display: flex;
+ flex: 1;
+ }
+
+ &__item__label {
+ margin-left: 10px;
+ display: flex;
+ flex: 1;
+ }
+
+ &__item__balance {
+ display: flex;
+ flex: 1;
+ justify-content: center;
+ }
+
+ &__item__link {
+ display: flex;
+ }
+
+ &__item__link img {
+ width: 15px;
+ height: 15px;
+ }
+}
+
+.hw-list-pagination {
+ display: flex;
+ align-self: flex-end;
+ margin-top: 10px;
+
+ &__button {
+ height: 25px;
+ flex: initial;
+ min-width: 90px;
+ font-size: 12px;
+ }
+}
+
+
.new-account-create-form {
display: flex;
flex-flow: column;
diff --git a/ui/app/routes.js b/ui/app/routes.js
index 0ff3f644d..04f71165f 100644
--- a/ui/app/routes.js
+++ b/ui/app/routes.js
@@ -9,6 +9,7 @@ const ADD_TOKEN_ROUTE = '/add-token'
const CONFIRM_ADD_TOKEN_ROUTE = '/confirm-add-token'
const NEW_ACCOUNT_ROUTE = '/new-account'
const IMPORT_ACCOUNT_ROUTE = '/new-account/import'
+const CONNECT_HARDWARE_ROUTE = '/new-account/connect'
const SEND_ROUTE = '/send'
const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction'
const SIGNATURE_REQUEST_ROUTE = '/confirm-transaction/signature-request'
@@ -35,6 +36,7 @@ module.exports = {
CONFIRM_ADD_TOKEN_ROUTE,
NEW_ACCOUNT_ROUTE,
IMPORT_ACCOUNT_ROUTE,
+ CONNECT_HARDWARE_ROUTE,
SEND_ROUTE,
CONFIRM_TRANSACTION_ROUTE,
NOTICE_ROUTE,