aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app/accounts
diff options
context:
space:
mode:
Diffstat (limited to 'ui/app/accounts')
-rw-r--r--ui/app/accounts/account-list-item.js91
-rw-r--r--ui/app/accounts/import/index.js100
-rw-r--r--ui/app/accounts/import/json.js100
-rw-r--r--ui/app/accounts/import/private-key.js67
-rw-r--r--ui/app/accounts/import/seed.js30
-rw-r--r--ui/app/accounts/index.js164
6 files changed, 552 insertions, 0 deletions
diff --git a/ui/app/accounts/account-list-item.js b/ui/app/accounts/account-list-item.js
new file mode 100644
index 000000000..10a0b6cc7
--- /dev/null
+++ b/ui/app/accounts/account-list-item.js
@@ -0,0 +1,91 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const ethUtil = require('ethereumjs-util')
+
+const EthBalance = require('../components/eth-balance')
+const CopyButton = require('../components/copyButton')
+const Identicon = require('../components/identicon')
+
+module.exports = AccountListItem
+
+inherits(AccountListItem, Component)
+function AccountListItem () {
+ Component.call(this)
+}
+
+AccountListItem.prototype.render = function () {
+ const { identity, selectedAddress, accounts, onShowDetail,
+ conversionRate, currentCurrency } = this.props
+
+ const checksumAddress = identity && identity.address && ethUtil.toChecksumAddress(identity.address)
+ const isSelected = selectedAddress === identity.address
+ const account = accounts[identity.address]
+ const selectedClass = isSelected ? '.selected' : ''
+
+ return (
+ h(`.accounts-list-option.flex-row.flex-space-between.pointer.hover-white${selectedClass}`, {
+ key: `account-panel-${identity.address}`,
+ onClick: (event) => onShowDetail(identity.address, event),
+ }, [
+
+ h('.identicon-wrapper.flex-column.flex-center.select-none', [
+ this.pendingOrNot(),
+ this.indicateIfLoose(),
+ h(Identicon, {
+ address: identity.address,
+ imageify: true,
+ }),
+ ]),
+
+ // account address, balance
+ h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', {
+ style: {
+ width: '200px',
+ },
+ }, [
+ h('span', identity.name),
+ h('span.font-small', {
+ style: {
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ },
+ }, checksumAddress),
+ h(EthBalance, {
+ value: account && account.balance,
+ currentCurrency,
+ conversionRate,
+ style: {
+ lineHeight: '7px',
+ marginTop: '10px',
+ },
+ }),
+ ]),
+
+ // copy button
+ h('.identity-copy.flex-column', {
+ style: {
+ margin: '0 20px',
+ },
+ }, [
+ h(CopyButton, {
+ value: checksumAddress,
+ }),
+ ]),
+ ])
+ )
+}
+
+AccountListItem.prototype.indicateIfLoose = function () {
+ try { // Sometimes keyrings aren't loaded yet:
+ const type = this.props.keyring.type
+ const isLoose = type !== 'HD Key Tree'
+ return isLoose ? h('.keyring-label', 'LOOSE') : null
+ } catch (e) { return }
+}
+
+AccountListItem.prototype.pendingOrNot = function () {
+ const pending = this.props.pending
+ if (pending.length === 0) return null
+ return h('.pending-dot', pending.length)
+}
diff --git a/ui/app/accounts/import/index.js b/ui/app/accounts/import/index.js
new file mode 100644
index 000000000..97b387229
--- /dev/null
+++ b/ui/app/accounts/import/index.js
@@ -0,0 +1,100 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../../actions')
+import Select from 'react-select'
+
+// Subviews
+const JsonImportView = require('./json.js')
+const PrivateKeyImportView = require('./private-key.js')
+
+const menuItems = [
+ 'Private Key',
+ 'JSON File',
+]
+
+module.exports = connect(mapStateToProps)(AccountImportSubview)
+
+function mapStateToProps (state) {
+ return {
+ menuItems,
+ }
+}
+
+inherits(AccountImportSubview, Component)
+function AccountImportSubview () {
+ Component.call(this)
+}
+
+AccountImportSubview.prototype.render = function () {
+ const props = this.props
+ const state = this.state || {}
+ const { menuItems } = props
+ const { type } = state
+
+ return (
+ h('div', {
+ style: {
+ },
+ }, [
+ h('.section-title.flex-row.flex-center', [
+ h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
+ onClick: (event) => {
+ props.dispatch(actions.goHome())
+ },
+ }),
+ h('h2.page-subtitle', 'Import Accounts'),
+ ]),
+ h('div', {
+ style: {
+ padding: '10px',
+ color: 'rgb(174, 174, 174)',
+ },
+ }, [
+
+ h('h3', { style: { padding: '3px' } }, 'SELECT TYPE'),
+
+ h('style', `
+ .has-value.Select--single > .Select-control .Select-value .Select-value-label, .Select-value-label {
+ color: rgb(174,174,174);
+ }
+ `),
+
+ h(Select, {
+ name: 'import-type-select',
+ clearable: false,
+ value: type || menuItems[0],
+ options: menuItems.map((type) => {
+ return {
+ value: type,
+ label: type,
+ }
+ }),
+ onChange: (opt) => {
+ this.setState({ type: opt.value })
+ },
+ }),
+ ]),
+
+ this.renderImportView(),
+ ])
+ )
+}
+
+AccountImportSubview.prototype.renderImportView = function () {
+ const props = this.props
+ const state = this.state || {}
+ const { type } = state
+ const { menuItems } = props
+ const current = type || menuItems[0]
+
+ switch (current) {
+ case 'Private Key':
+ return h(PrivateKeyImportView)
+ case 'JSON File':
+ return h(JsonImportView)
+ default:
+ return h(JsonImportView)
+ }
+}
diff --git a/ui/app/accounts/import/json.js b/ui/app/accounts/import/json.js
new file mode 100644
index 000000000..158a3c923
--- /dev/null
+++ b/ui/app/accounts/import/json.js
@@ -0,0 +1,100 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../../actions')
+const FileInput = require('react-simple-file-input').default
+
+const HELP_LINK = 'https://github.com/MetaMask/faq/blob/master/README.md#q-i-cant-use-the-import-feature-for-uploading-a-json-file-the-window-keeps-closing-when-i-try-to-select-a-file'
+
+module.exports = connect(mapStateToProps)(JsonImportSubview)
+
+function mapStateToProps (state) {
+ return {
+ error: state.appState.warning,
+ }
+}
+
+inherits(JsonImportSubview, Component)
+function JsonImportSubview () {
+ Component.call(this)
+}
+
+JsonImportSubview.prototype.render = function () {
+ const { error } = this.props
+
+ return (
+ h('div', {
+ style: {
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ padding: '5px 15px 0px 15px',
+ },
+ }, [
+
+ h('p', 'Used by a variety of different clients'),
+ h('a.warning', { href: HELP_LINK, target: '_blank' }, 'File import not working? Click here!'),
+
+ h(FileInput, {
+ readAs: 'text',
+ onLoad: this.onLoad.bind(this),
+ style: {
+ margin: '20px 0px 12px 20px',
+ fontSize: '15px',
+ },
+ }),
+
+ h('input.large-input.letter-spacey', {
+ type: 'password',
+ placeholder: 'Enter password',
+ id: 'json-password-box',
+ onKeyPress: this.createKeyringOnEnter.bind(this),
+ style: {
+ width: 260,
+ marginTop: 12,
+ },
+ }),
+
+ h('button.primary', {
+ onClick: this.createNewKeychain.bind(this),
+ style: {
+ margin: 12,
+ },
+ }, 'Import'),
+
+ error ? h('span.error', error) : null,
+ ])
+ )
+}
+
+JsonImportSubview.prototype.onLoad = function (event, file) {
+ this.setState({file: file, fileContents: event.target.result})
+}
+
+JsonImportSubview.prototype.createKeyringOnEnter = function (event) {
+ if (event.key === 'Enter') {
+ event.preventDefault()
+ this.createNewKeychain()
+ }
+}
+
+JsonImportSubview.prototype.createNewKeychain = function () {
+ const state = this.state
+ const { fileContents } = state
+
+ if (!fileContents) {
+ const message = 'You must select a file to import.'
+ return this.props.dispatch(actions.displayWarning(message))
+ }
+
+ const passwordInput = document.getElementById('json-password-box')
+ const password = passwordInput.value
+
+ if (!password) {
+ const message = 'You must enter a password for the selected file.'
+ return this.props.dispatch(actions.displayWarning(message))
+ }
+
+ this.props.dispatch(actions.importNewAccount('JSON File', [ fileContents, password ]))
+}
diff --git a/ui/app/accounts/import/private-key.js b/ui/app/accounts/import/private-key.js
new file mode 100644
index 000000000..68ccee58e
--- /dev/null
+++ b/ui/app/accounts/import/private-key.js
@@ -0,0 +1,67 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../../actions')
+
+module.exports = connect(mapStateToProps)(PrivateKeyImportView)
+
+function mapStateToProps (state) {
+ return {
+ error: state.appState.warning,
+ }
+}
+
+inherits(PrivateKeyImportView, Component)
+function PrivateKeyImportView () {
+ Component.call(this)
+}
+
+PrivateKeyImportView.prototype.render = function () {
+ const { error } = this.props
+
+ return (
+ h('div', {
+ style: {
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ padding: '5px 15px 0px 15px',
+ },
+ }, [
+ h('span', 'Paste your private key string here'),
+
+ h('input.large-input.letter-spacey', {
+ type: 'password',
+ id: 'private-key-box',
+ onKeyPress: this.createKeyringOnEnter.bind(this),
+ style: {
+ width: 260,
+ marginTop: 12,
+ },
+ }),
+
+ h('button.primary', {
+ onClick: this.createNewKeychain.bind(this),
+ style: {
+ margin: 12,
+ },
+ }, 'Import'),
+
+ error ? h('span.error', error) : null,
+ ])
+ )
+}
+
+PrivateKeyImportView.prototype.createKeyringOnEnter = function (event) {
+ if (event.key === 'Enter') {
+ event.preventDefault()
+ this.createNewKeychain()
+ }
+}
+
+PrivateKeyImportView.prototype.createNewKeychain = function () {
+ const input = document.getElementById('private-key-box')
+ const privateKey = input.value
+ this.props.dispatch(actions.importNewAccount('Private Key', [ privateKey ]))
+}
diff --git a/ui/app/accounts/import/seed.js b/ui/app/accounts/import/seed.js
new file mode 100644
index 000000000..b4a7c0afa
--- /dev/null
+++ b/ui/app/accounts/import/seed.js
@@ -0,0 +1,30 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+
+module.exports = connect(mapStateToProps)(SeedImportSubview)
+
+function mapStateToProps (state) {
+ return {}
+}
+
+inherits(SeedImportSubview, Component)
+function SeedImportSubview () {
+ Component.call(this)
+}
+
+SeedImportSubview.prototype.render = function () {
+ return (
+ h('div', {
+ style: {
+ },
+ }, [
+ `Paste your seed phrase here!`,
+ h('textarea'),
+ h('br'),
+ h('button', 'Submit'),
+ ])
+ )
+}
+
diff --git a/ui/app/accounts/index.js b/ui/app/accounts/index.js
new file mode 100644
index 000000000..ac2615cd7
--- /dev/null
+++ b/ui/app/accounts/index.js
@@ -0,0 +1,164 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../actions')
+const valuesFor = require('../util').valuesFor
+const findDOMNode = require('react-dom').findDOMNode
+const AccountListItem = require('./account-list-item')
+
+module.exports = connect(mapStateToProps)(AccountsScreen)
+
+function mapStateToProps (state) {
+ const pendingTxs = valuesFor(state.metamask.unapprovedTxs)
+ .filter(txMeta => txMeta.metamaskNetworkId === state.metamask.network)
+ const pendingMsgs = valuesFor(state.metamask.unapprovedMsgs)
+ const pending = pendingTxs.concat(pendingMsgs)
+
+ return {
+ accounts: state.metamask.accounts,
+ identities: state.metamask.identities,
+ unapprovedTxs: state.metamask.unapprovedTxs,
+ selectedAddress: state.metamask.selectedAddress,
+ scrollToBottom: state.appState.scrollToBottom,
+ pending,
+ keyrings: state.metamask.keyrings,
+ conversionRate: state.metamask.conversionRate,
+ currentCurrency: state.metamask.currentCurrency,
+ }
+}
+
+inherits(AccountsScreen, Component)
+function AccountsScreen () {
+ Component.call(this)
+}
+
+AccountsScreen.prototype.render = function () {
+ const props = this.props
+ const { keyrings, conversionRate, currentCurrency } = props
+ const identityList = valuesFor(props.identities)
+ const unapprovedTxList = valuesFor(props.unapprovedTxs)
+
+ return (
+
+ h('.accounts-section.flex-grow', [
+
+ // subtitle and nav
+ h('.section-title.flex-center', [
+ h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
+ onClick: this.goHome.bind(this),
+ }),
+ h('h2.page-subtitle', 'Select Account'),
+ ]),
+
+ h('hr.horizontal-line'),
+
+ // identity selection
+ h('section.identity-section', {
+ style: {
+ height: '418px',
+ overflowY: 'auto',
+ overflowX: 'hidden',
+ },
+ },
+ [
+ identityList.map((identity) => {
+ const pending = this.props.pending.filter((txOrMsg) => {
+ if ('txParams' in txOrMsg) {
+ return txOrMsg.txParams.from === identity.address
+ } else if ('msgParams' in txOrMsg) {
+ return txOrMsg.msgParams.from === identity.address
+ } else {
+ return false
+ }
+ })
+
+ const simpleAddress = identity.address.substring(2).toLowerCase()
+ const keyring = keyrings.find((kr) => {
+ return kr.accounts.includes(simpleAddress) ||
+ kr.accounts.includes(identity.address)
+ })
+
+ return h(AccountListItem, {
+ key: `acct-panel-${identity.address}`,
+ identity,
+ selectedAddress: this.props.selectedAddress,
+ conversionRate,
+ currentCurrency,
+ accounts: this.props.accounts,
+ onShowDetail: this.onShowDetail.bind(this),
+ pending,
+ keyring,
+ })
+ }),
+
+ h('hr.horizontal-line'),
+ h('div.footer.hover-white.pointer', {
+ key: 'reveal-account-bar',
+ onClick: () => {
+ this.addNewAccount()
+ },
+ style: {
+ display: 'flex',
+ height: '40px',
+ padding: '10px',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ }, [
+ h('i.fa.fa-plus.fa-lg', {key: ''}),
+ ]),
+ h('hr.horizontal-line'),
+ ]),
+
+ unapprovedTxList.length ? (
+
+ h('.unconftx-link.flex-row.flex-center', {
+ onClick: this.navigateToConfTx.bind(this),
+ }, [
+ h('span', 'Unconfirmed Txs'),
+ h('i.fa.fa-arrow-right.fa-lg'),
+ ])
+
+ ) : (
+ null
+ ),
+ ])
+ )
+}
+
+// If a new account was revealed, scroll to the bottom
+AccountsScreen.prototype.componentDidUpdate = function () {
+ const scrollToBottom = this.props.scrollToBottom
+
+ if (scrollToBottom) {
+ var container = findDOMNode(this)
+ var scrollable = container.querySelector('.identity-section')
+ scrollable.scrollTop = scrollable.scrollHeight
+ }
+}
+
+AccountsScreen.prototype.navigateToConfTx = function () {
+ event.stopPropagation()
+ this.props.dispatch(actions.showConfTxPage())
+}
+
+AccountsScreen.prototype.onShowDetail = function (address, event) {
+ event.stopPropagation()
+ this.props.dispatch(actions.showAccountDetail(address))
+}
+
+AccountsScreen.prototype.addNewAccount = function () {
+ this.props.dispatch(actions.addNewAccount(0))
+}
+
+/* An optional view proposed in this design:
+ * https://consensys.quip.com/zZVrAysM5znY
+AccountsScreen.prototype.addNewAccount = function () {
+ this.props.dispatch(actions.navigateToNewAccountScreen())
+}
+*/
+
+AccountsScreen.prototype.goHome = function () {
+ this.props.dispatch(actions.goHome())
+}