From 5eb3d5d485b17b98b19443d8def2f03dec9b38ef Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 3 Jul 2017 15:39:25 -0700 Subject: Make folder for responsive UI --- ui/app/account-detail.js | 311 ------- ui/app/accounts/account-list-item.js | 91 -- ui/app/accounts/import/index.js | 100 -- ui/app/accounts/import/json.js | 100 -- ui/app/accounts/import/private-key.js | 67 -- ui/app/accounts/import/seed.js | 30 - ui/app/accounts/index.js | 164 ---- ui/app/actions.js | 1031 --------------------- ui/app/add-token.js | 219 ----- ui/app/app.js | 591 ------------ ui/app/components/account-export.js | 122 --- ui/app/components/account-info-link.js | 41 - ui/app/components/account-panel.js | 86 -- ui/app/components/balance.js | 89 -- ui/app/components/binary-renderer.js | 46 - ui/app/components/bn-as-decimal-input.js | 174 ---- ui/app/components/buy-button-subview.js | 197 ---- ui/app/components/coinbase-form.js | 63 -- ui/app/components/copyButton.js | 59 -- ui/app/components/copyable.js | 46 - ui/app/components/custom-radio-list.js | 60 -- ui/app/components/drop-menu-item.js | 59 -- ui/app/components/editable-label.js | 51 - ui/app/components/ens-input.js | 170 ---- ui/app/components/eth-balance.js | 89 -- ui/app/components/fiat-value.js | 63 -- ui/app/components/hex-as-decimal-input.js | 154 --- ui/app/components/identicon.js | 72 -- ui/app/components/loading.js | 53 -- ui/app/components/mascot.js | 59 -- ui/app/components/mini-account-panel.js | 74 -- ui/app/components/network.js | 125 --- ui/app/components/notice.js | 126 --- ui/app/components/pending-msg-details.js | 50 - ui/app/components/pending-msg.js | 56 -- ui/app/components/pending-personal-msg-details.js | 60 -- ui/app/components/pending-personal-msg.js | 47 - ui/app/components/pending-tx.js | 480 ---------- ui/app/components/qr-code.js | 79 -- ui/app/components/range-slider.js | 58 -- ui/app/components/shapeshift-form.js | 306 ------ ui/app/components/shift-list-item.js | 204 ---- ui/app/components/tab-bar.js | 36 - ui/app/components/template.js | 18 - ui/app/components/token-cell.js | 72 -- ui/app/components/token-list.js | 194 ---- ui/app/components/tooltip.js | 22 - ui/app/components/transaction-list-item-icon.js | 68 -- ui/app/components/transaction-list-item.js | 165 ---- ui/app/components/transaction-list.js | 79 -- ui/app/conf-tx.js | 213 ----- ui/app/config.js | 211 ----- ui/app/conversion.json | 207 ----- ui/app/css/debug.css | 21 - ui/app/css/fonts.css | 36 - ui/app/css/index.css | 667 ------------- ui/app/css/lib.css | 268 ------ ui/app/css/reset.css | 48 - ui/app/css/transitions.css | 42 - ui/app/first-time/init-menu.js | 179 ---- ui/app/img/identicon-tardigrade.png | Bin 141119 -> 0 bytes ui/app/img/identicon-walrus.png | Bin 388973 -> 0 bytes ui/app/info.js | 154 --- ui/app/keychains/hd/create-vault-complete.js | 78 -- ui/app/keychains/hd/recover-seed/confirmation.js | 118 --- ui/app/keychains/hd/restore-vault.js | 152 --- ui/app/new-keychain.js | 29 - ui/app/reducers.js | 52 -- ui/app/reducers/app.js | 585 ------------ ui/app/reducers/identities.js | 15 - ui/app/reducers/metamask.js | 137 --- ui/app/root.js | 22 - ui/app/send.js | 288 ------ ui/app/settings.js | 59 -- ui/app/store.js | 21 - ui/app/template.js | 30 - ui/app/unlock.js | 118 --- ui/app/util.js | 217 ----- 78 files changed, 10743 deletions(-) delete mode 100644 ui/app/account-detail.js delete mode 100644 ui/app/accounts/account-list-item.js delete mode 100644 ui/app/accounts/import/index.js delete mode 100644 ui/app/accounts/import/json.js delete mode 100644 ui/app/accounts/import/private-key.js delete mode 100644 ui/app/accounts/import/seed.js delete mode 100644 ui/app/accounts/index.js delete mode 100644 ui/app/actions.js delete mode 100644 ui/app/add-token.js delete mode 100644 ui/app/app.js delete mode 100644 ui/app/components/account-export.js delete mode 100644 ui/app/components/account-info-link.js delete mode 100644 ui/app/components/account-panel.js delete mode 100644 ui/app/components/balance.js delete mode 100644 ui/app/components/binary-renderer.js delete mode 100644 ui/app/components/bn-as-decimal-input.js delete mode 100644 ui/app/components/buy-button-subview.js delete mode 100644 ui/app/components/coinbase-form.js delete mode 100644 ui/app/components/copyButton.js delete mode 100644 ui/app/components/copyable.js delete mode 100644 ui/app/components/custom-radio-list.js delete mode 100644 ui/app/components/drop-menu-item.js delete mode 100644 ui/app/components/editable-label.js delete mode 100644 ui/app/components/ens-input.js delete mode 100644 ui/app/components/eth-balance.js delete mode 100644 ui/app/components/fiat-value.js delete mode 100644 ui/app/components/hex-as-decimal-input.js delete mode 100644 ui/app/components/identicon.js delete mode 100644 ui/app/components/loading.js delete mode 100644 ui/app/components/mascot.js delete mode 100644 ui/app/components/mini-account-panel.js delete mode 100644 ui/app/components/network.js delete mode 100644 ui/app/components/notice.js delete mode 100644 ui/app/components/pending-msg-details.js delete mode 100644 ui/app/components/pending-msg.js delete mode 100644 ui/app/components/pending-personal-msg-details.js delete mode 100644 ui/app/components/pending-personal-msg.js delete mode 100644 ui/app/components/pending-tx.js delete mode 100644 ui/app/components/qr-code.js delete mode 100644 ui/app/components/range-slider.js delete mode 100644 ui/app/components/shapeshift-form.js delete mode 100644 ui/app/components/shift-list-item.js delete mode 100644 ui/app/components/tab-bar.js delete mode 100644 ui/app/components/template.js delete mode 100644 ui/app/components/token-cell.js delete mode 100644 ui/app/components/token-list.js delete mode 100644 ui/app/components/tooltip.js delete mode 100644 ui/app/components/transaction-list-item-icon.js delete mode 100644 ui/app/components/transaction-list-item.js delete mode 100644 ui/app/components/transaction-list.js delete mode 100644 ui/app/conf-tx.js delete mode 100644 ui/app/config.js delete mode 100644 ui/app/conversion.json delete mode 100644 ui/app/css/debug.css delete mode 100644 ui/app/css/fonts.css delete mode 100644 ui/app/css/index.css delete mode 100644 ui/app/css/lib.css delete mode 100644 ui/app/css/reset.css delete mode 100644 ui/app/css/transitions.css delete mode 100644 ui/app/first-time/init-menu.js delete mode 100644 ui/app/img/identicon-tardigrade.png delete mode 100644 ui/app/img/identicon-walrus.png delete mode 100644 ui/app/info.js delete mode 100644 ui/app/keychains/hd/create-vault-complete.js delete mode 100644 ui/app/keychains/hd/recover-seed/confirmation.js delete mode 100644 ui/app/keychains/hd/restore-vault.js delete mode 100644 ui/app/new-keychain.js delete mode 100644 ui/app/reducers.js delete mode 100644 ui/app/reducers/app.js delete mode 100644 ui/app/reducers/identities.js delete mode 100644 ui/app/reducers/metamask.js delete mode 100644 ui/app/root.js delete mode 100644 ui/app/send.js delete mode 100644 ui/app/settings.js delete mode 100644 ui/app/store.js delete mode 100644 ui/app/template.js delete mode 100644 ui/app/unlock.js delete mode 100644 ui/app/util.js (limited to 'ui/app') diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js deleted file mode 100644 index bed05a7fb..000000000 --- a/ui/app/account-detail.js +++ /dev/null @@ -1,311 +0,0 @@ -const inherits = require('util').inherits -const extend = require('xtend') -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const CopyButton = require('./components/copyButton') -const AccountInfoLink = require('./components/account-info-link') -const actions = require('./actions') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') -const valuesFor = require('./util').valuesFor - -const Identicon = require('./components/identicon') -const EthBalance = require('./components/eth-balance') -const TransactionList = require('./components/transaction-list') -const ExportAccountView = require('./components/account-export') -const ethUtil = require('ethereumjs-util') -const EditableLabel = require('./components/editable-label') -const Tooltip = require('./components/tooltip') -const TabBar = require('./components/tab-bar') -const TokenList = require('./components/token-list') - -module.exports = connect(mapStateToProps)(AccountDetailScreen) - -function mapStateToProps (state) { - return { - metamask: state.metamask, - identities: state.metamask.identities, - accounts: state.metamask.accounts, - address: state.metamask.selectedAddress, - accountDetail: state.appState.accountDetail, - network: state.metamask.network, - unapprovedMsgs: valuesFor(state.metamask.unapprovedMsgs), - shapeShiftTxList: state.metamask.shapeShiftTxList, - transactions: state.metamask.selectedAddressTxList || [], - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - currentAccountTab: state.metamask.currentAccountTab, - tokens: state.metamask.tokens, - } -} - -inherits(AccountDetailScreen, Component) -function AccountDetailScreen () { - Component.call(this) -} - -AccountDetailScreen.prototype.render = function () { - var props = this.props - var selected = props.address || Object.keys(props.accounts)[0] - var checksumAddress = selected && ethUtil.toChecksumAddress(selected) - var identity = props.identities[selected] - var account = props.accounts[selected] - const { network, conversionRate, currentCurrency } = props - - return ( - - h('.account-detail-section', [ - - // identicon, label, balance, etc - h('.account-data-subsection', { - style: { - margin: '0 20px', - }, - }, [ - - // header - identicon + nav - h('div', { - style: { - paddingTop: '20px', - display: 'flex', - justifyContent: 'flex-start', - alignItems: 'flex-start', - }, - }, [ - - // large identicon and addresses - h('.identicon-wrapper.select-none', [ - h(Identicon, { - diameter: 62, - address: selected, - }), - ]), - h('flex-column', { - style: { - lineHeight: '10px', - marginLeft: '15px', - }, - }, [ - h(EditableLabel, { - textValue: identity ? identity.name : '', - state: { - isEditingLabel: false, - }, - saveText: (text) => { - props.dispatch(actions.saveAccountLabel(selected, text)) - }, - }, [ - - // What is shown when not editing + edit text: - h('label.editing-label', [h('.edit-text', 'edit')]), - h('h2.font-medium.color-forest', {name: 'edit'}, identity && identity.name), - ]), - h('.flex-row', { - style: { - width: '15em', - justifyContent: 'space-between', - alignItems: 'baseline', - }, - }, [ - - // address - - h('div', { - style: { - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingTop: '3px', - width: '5em', - fontSize: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - marginTop: '10px', - marginBottom: '15px', - color: '#AEAEAE', - }, - }, checksumAddress), - - // copy and export - - h('.flex-row', { - style: { - justifyContent: 'flex-end', - }, - }, [ - - h(AccountInfoLink, { selected, network }), - - h(CopyButton, { - value: checksumAddress, - }), - - h(Tooltip, { - title: 'QR Code', - }, [ - h('i.fa.fa-qrcode.pointer.pop-hover', { - onClick: () => props.dispatch(actions.showQrView(selected, identity ? identity.name : '')), - style: { - fontSize: '18px', - position: 'relative', - color: 'rgb(247, 134, 28)', - top: '5px', - marginLeft: '3px', - marginRight: '3px', - }, - }), - ]), - - h(Tooltip, { - title: 'Export Private Key', - }, [ - h('div', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - h('img.cursor-pointer.color-orange', { - src: 'images/key-32.png', - onClick: () => this.requestAccountExport(selected), - style: { - height: '19px', - }, - }), - ]), - ]), - ]), - ]), - - // account ballence - - ]), - ]), - h('.flex-row', { - style: { - justifyContent: 'space-between', - alignItems: 'flex-start', - }, - }, [ - - h(EthBalance, { - value: account && account.balance, - conversionRate, - currentCurrency, - style: { - lineHeight: '7px', - marginTop: '10px', - }, - }), - - h('button', { - onClick: () => props.dispatch(actions.buyEthView(selected)), - style: { - marginBottom: '20px', - marginRight: '8px', - position: 'absolute', - left: '219px', - }, - }, 'BUY'), - - h('button', { - onClick: () => props.dispatch(actions.showSendPage()), - style: { - marginBottom: '20px', - marginRight: '8px', - }, - }, 'SEND'), - - ]), - ]), - - // subview (tx history, pk export confirm, buy eth warning) - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.subview(), - ]), - - ]) - ) -} - -AccountDetailScreen.prototype.subview = function () { - var subview - try { - subview = this.props.accountDetail.subview - } catch (e) { - subview = null - } - - switch (subview) { - case 'transactions': - return this.tabSections() - case 'export': - var state = extend({key: 'export'}, this.props) - return h(ExportAccountView, state) - default: - return this.tabSections() - } -} - -AccountDetailScreen.prototype.tabSections = function () { - const { currentAccountTab } = this.props - - return h('section.tabSection', [ - - h(TabBar, { - tabs: [ - { content: 'Sent', key: 'history' }, - { content: 'Tokens', key: 'tokens' }, - ], - defaultTab: currentAccountTab || 'history', - tabSelected: (key) => { - this.props.dispatch(actions.setCurrentAccountTab(key)) - }, - }), - - this.tabSwitchView(), - ]) -} - -AccountDetailScreen.prototype.tabSwitchView = function () { - const props = this.props - const { address, network } = props - const { currentAccountTab, tokens } = this.props - - switch (currentAccountTab) { - case 'tokens': - return h(TokenList, { - userAddress: address, - network, - tokens, - addToken: () => this.props.dispatch(actions.showAddTokenPage()), - }) - default: - return this.transactionList() - } -} - -AccountDetailScreen.prototype.transactionList = function () { - const {transactions, unapprovedMsgs, address, - network, shapeShiftTxList, conversionRate } = this.props - - return h(TransactionList, { - transactions: transactions.sort((a, b) => b.time - a.time), - network, - unapprovedMsgs, - conversionRate, - address, - shapeShiftTxList, - viewPendingTx: (txId) => { - this.props.dispatch(actions.viewPendingTx(txId)) - }, - }) -} - -AccountDetailScreen.prototype.requestAccountExport = function () { - this.props.dispatch(actions.requestExportAccount()) -} diff --git a/ui/app/accounts/account-list-item.js b/ui/app/accounts/account-list-item.js deleted file mode 100644 index 10a0b6cc7..000000000 --- a/ui/app/accounts/account-list-item.js +++ /dev/null @@ -1,91 +0,0 @@ -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 deleted file mode 100644 index 97b387229..000000000 --- a/ui/app/accounts/import/index.js +++ /dev/null @@ -1,100 +0,0 @@ -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 deleted file mode 100644 index 158a3c923..000000000 --- a/ui/app/accounts/import/json.js +++ /dev/null @@ -1,100 +0,0 @@ -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 deleted file mode 100644 index 68ccee58e..000000000 --- a/ui/app/accounts/import/private-key.js +++ /dev/null @@ -1,67 +0,0 @@ -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 deleted file mode 100644 index b4a7c0afa..000000000 --- a/ui/app/accounts/import/seed.js +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index ac2615cd7..000000000 --- a/ui/app/accounts/index.js +++ /dev/null @@ -1,164 +0,0 @@ -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()) -} diff --git a/ui/app/actions.js b/ui/app/actions.js deleted file mode 100644 index d99291e46..000000000 --- a/ui/app/actions.js +++ /dev/null @@ -1,1031 +0,0 @@ -const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url') - -var actions = { - _setBackgroundConnection: _setBackgroundConnection, - - GO_HOME: 'GO_HOME', - goHome: goHome, - // menu state - getNetworkStatus: 'getNetworkStatus', - // transition state - TRANSITION_FORWARD: 'TRANSITION_FORWARD', - TRANSITION_BACKWARD: 'TRANSITION_BACKWARD', - transitionForward, - transitionBackward, - // remote state - UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE', - updateMetamaskState: updateMetamaskState, - // notices - MARK_NOTICE_READ: 'MARK_NOTICE_READ', - markNoticeRead: markNoticeRead, - SHOW_NOTICE: 'SHOW_NOTICE', - showNotice: showNotice, - CLEAR_NOTICES: 'CLEAR_NOTICES', - clearNotices: clearNotices, - markAccountsFound, - // intialize screen - CREATE_NEW_VAULT_IN_PROGRESS: 'CREATE_NEW_VAULT_IN_PROGRESS', - SHOW_CREATE_VAULT: 'SHOW_CREATE_VAULT', - SHOW_RESTORE_VAULT: 'SHOW_RESTORE_VAULT', - FORGOT_PASSWORD: 'FORGOT_PASSWORD', - forgotPassword: forgotPassword, - SHOW_INIT_MENU: 'SHOW_INIT_MENU', - SHOW_NEW_VAULT_SEED: 'SHOW_NEW_VAULT_SEED', - SHOW_INFO_PAGE: 'SHOW_INFO_PAGE', - SHOW_IMPORT_PAGE: 'SHOW_IMPORT_PAGE', - unlockMetamask: unlockMetamask, - unlockFailed: unlockFailed, - showCreateVault: showCreateVault, - showRestoreVault: showRestoreVault, - showInitializeMenu: showInitializeMenu, - showImportPage, - createNewVaultAndKeychain: createNewVaultAndKeychain, - createNewVaultAndRestore: createNewVaultAndRestore, - createNewVaultInProgress: createNewVaultInProgress, - addNewKeyring, - importNewAccount, - addNewAccount, - NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', - navigateToNewAccountScreen, - showNewVaultSeed: showNewVaultSeed, - showInfoPage: showInfoPage, - // seed recovery actions - REVEAL_SEED_CONFIRMATION: 'REVEAL_SEED_CONFIRMATION', - revealSeedConfirmation: revealSeedConfirmation, - requestRevealSeed: requestRevealSeed, - // unlock screen - UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS', - UNLOCK_FAILED: 'UNLOCK_FAILED', - UNLOCK_METAMASK: 'UNLOCK_METAMASK', - LOCK_METAMASK: 'LOCK_METAMASK', - tryUnlockMetamask: tryUnlockMetamask, - lockMetamask: lockMetamask, - unlockInProgress: unlockInProgress, - // error handling - displayWarning: displayWarning, - DISPLAY_WARNING: 'DISPLAY_WARNING', - HIDE_WARNING: 'HIDE_WARNING', - hideWarning: hideWarning, - // accounts screen - SET_SELECTED_ACCOUNT: 'SET_SELECTED_ACCOUNT', - SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL', - SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE', - SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE', - SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE', - SET_CURRENT_FIAT: 'SET_CURRENT_FIAT', - setCurrentCurrency: setCurrentCurrency, - setCurrentAccountTab, - // account detail screen - SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', - showSendPage: showSendPage, - ADD_TO_ADDRESS_BOOK: 'ADD_TO_ADDRESS_BOOK', - addToAddressBook: addToAddressBook, - REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT', - requestExportAccount: requestExportAccount, - EXPORT_ACCOUNT: 'EXPORT_ACCOUNT', - exportAccount: exportAccount, - SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY', - showPrivateKey: showPrivateKey, - SAVE_ACCOUNT_LABEL: 'SAVE_ACCOUNT_LABEL', - saveAccountLabel: saveAccountLabel, - // tx conf screen - COMPLETED_TX: 'COMPLETED_TX', - TRANSACTION_ERROR: 'TRANSACTION_ERROR', - NEXT_TX: 'NEXT_TX', - PREVIOUS_TX: 'PREV_TX', - signMsg: signMsg, - cancelMsg: cancelMsg, - signPersonalMsg, - cancelPersonalMsg, - sendTx: sendTx, - signTx: signTx, - updateAndApproveTx, - cancelTx: cancelTx, - completedTx: completedTx, - txError: txError, - nextTx: nextTx, - previousTx: previousTx, - viewPendingTx: viewPendingTx, - VIEW_PENDING_TX: 'VIEW_PENDING_TX', - // app messages - confirmSeedWords: confirmSeedWords, - showAccountDetail: showAccountDetail, - BACK_TO_ACCOUNT_DETAIL: 'BACK_TO_ACCOUNT_DETAIL', - backToAccountDetail: backToAccountDetail, - showAccountsPage: showAccountsPage, - showConfTxPage: showConfTxPage, - // config screen - SHOW_CONFIG_PAGE: 'SHOW_CONFIG_PAGE', - SET_RPC_TARGET: 'SET_RPC_TARGET', - SET_DEFAULT_RPC_TARGET: 'SET_DEFAULT_RPC_TARGET', - SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', - USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER', - useEtherscanProvider: useEtherscanProvider, - showConfigPage, - SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', - showAddTokenPage, - addToken, - setRpcTarget: setRpcTarget, - setDefaultRpcTarget: setDefaultRpcTarget, - setProviderType: setProviderType, - // loading overlay - SHOW_LOADING: 'SHOW_LOADING_INDICATION', - HIDE_LOADING: 'HIDE_LOADING_INDICATION', - showLoadingIndication: showLoadingIndication, - hideLoadingIndication: hideLoadingIndication, - // buy Eth with coinbase - BUY_ETH: 'BUY_ETH', - buyEth: buyEth, - buyEthView: buyEthView, - BUY_ETH_VIEW: 'BUY_ETH_VIEW', - COINBASE_SUBVIEW: 'COINBASE_SUBVIEW', - coinBaseSubview: coinBaseSubview, - SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW', - shapeShiftSubview: shapeShiftSubview, - PAIR_UPDATE: 'PAIR_UPDATE', - pairUpdate: pairUpdate, - coinShiftRquest: coinShiftRquest, - SHOW_SUB_LOADING_INDICATION: 'SHOW_SUB_LOADING_INDICATION', - showSubLoadingIndication: showSubLoadingIndication, - HIDE_SUB_LOADING_INDICATION: 'HIDE_SUB_LOADING_INDICATION', - hideSubLoadingIndication: hideSubLoadingIndication, -// QR STUFF: - SHOW_QR: 'SHOW_QR', - showQrView: showQrView, - reshowQrCode: reshowQrCode, - SHOW_QR_VIEW: 'SHOW_QR_VIEW', -// FORGOT PASSWORD: - BACK_TO_INIT_MENU: 'BACK_TO_INIT_MENU', - goBackToInitView: goBackToInitView, - RECOVERY_IN_PROGRESS: 'RECOVERY_IN_PROGRESS', - BACK_TO_UNLOCK_VIEW: 'BACK_TO_UNLOCK_VIEW', - backToUnlockView: backToUnlockView, - // SHOWING KEYCHAIN - SHOW_NEW_KEYCHAIN: 'SHOW_NEW_KEYCHAIN', - showNewKeychain: showNewKeychain, - - callBackgroundThenUpdate, - forceUpdateMetamaskState, -} - -module.exports = actions - -var background = null -function _setBackgroundConnection (backgroundConnection) { - background = backgroundConnection -} - -function goHome () { - return { - type: actions.GO_HOME, - } -} - -// async actions - -function tryUnlockMetamask (password) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - dispatch(actions.unlockInProgress()) - log.debug(`background.submitPassword`) - background.submitPassword(password, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - dispatch(actions.unlockFailed(err.message)) - } else { - dispatch(actions.transitionForward()) - forceUpdateMetamaskState(dispatch) - } - }) - } -} - -function transitionForward () { - return { - type: this.TRANSITION_FORWARD, - } -} - -function transitionBackward () { - return { - type: this.TRANSITION_BACKWARD, - } -} - -function confirmSeedWords () { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.clearSeedWordCache`) - background.clearSeedWordCache((err, account) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - - log.info('Seed word cache cleared. ' + account) - dispatch(actions.showAccountDetail(account)) - }) - } -} - -function createNewVaultAndRestore (password, seed) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.createNewVaultAndRestore`) - background.createNewVaultAndRestore(password, seed, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.showAccountsPage()) - }) - } -} - -function createNewVaultAndKeychain (password) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.createNewVaultAndKeychain`) - background.createNewVaultAndKeychain(password, (err) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - log.debug(`background.placeSeedWords`) - background.placeSeedWords((err) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch(actions.hideLoadingIndication()) - forceUpdateMetamaskState(dispatch) - }) - }) - } -} - -function revealSeedConfirmation () { - return { - type: this.REVEAL_SEED_CONFIRMATION, - } -} - -function requestRevealSeed (password) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.submitPassword`) - background.submitPassword(password, (err) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - log.debug(`background.placeSeedWords`) - background.placeSeedWords((err, result) => { - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.hideLoadingIndication()) - dispatch(actions.showNewVaultSeed(result)) - }) - }) - } -} - -function addNewKeyring (type, opts) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.addNewKeyring`) - background.addNewKeyring(type, opts, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.showAccountsPage()) - }) - } -} - -function importNewAccount (strategy, args) { - return (dispatch) => { - dispatch(actions.showLoadingIndication('This may take a while, be patient.')) - log.debug(`background.importAccountWithStrategy`) - background.importAccountWithStrategy(strategy, args, (err) => { - if (err) return dispatch(actions.displayWarning(err.message)) - log.debug(`background.getState`) - background.getState((err, newState) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch(actions.updateMetamaskState(newState)) - dispatch({ - type: actions.SHOW_ACCOUNT_DETAIL, - value: newState.selectedAddress, - }) - }) - }) - } -} - -function navigateToNewAccountScreen () { - return { - type: this.NEW_ACCOUNT_SCREEN, - } -} - -function addNewAccount () { - log.debug(`background.addNewAccount`) - return callBackgroundThenUpdate(background.addNewAccount) -} - -function showInfoPage () { - return { - type: actions.SHOW_INFO_PAGE, - } -} - -function setCurrentCurrency (currencyCode) { - return (dispatch) => { - dispatch(this.showLoadingIndication()) - log.debug(`background.setCurrentCurrency`) - background.setCurrentCurrency(currencyCode, (err, data) => { - dispatch(this.hideLoadingIndication()) - if (err) { - log.error(err.stack) - return dispatch(actions.displayWarning(err.message)) - } - dispatch({ - type: this.SET_CURRENT_FIAT, - value: { - currentCurrency: data.currentCurrency, - conversionRate: data.conversionRate, - conversionDate: data.conversionDate, - }, - }) - }) - } -} - -function signMsg (msgData) { - log.debug('action - signMsg') - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - - log.debug(`actions calling background.signMessage`) - background.signMessage(msgData, (err, newState) => { - log.debug('signMessage called back') - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.hideLoadingIndication()) - - if (err) log.error(err) - if (err) return dispatch(actions.displayWarning(err.message)) - - dispatch(actions.completedTx(msgData.metamaskId)) - }) - } -} - -function signPersonalMsg (msgData) { - log.debug('action - signPersonalMsg') - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - - log.debug(`actions calling background.signPersonalMessage`) - background.signPersonalMessage(msgData, (err, newState) => { - log.debug('signPersonalMessage called back') - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.hideLoadingIndication()) - - if (err) log.error(err) - if (err) return dispatch(actions.displayWarning(err.message)) - - dispatch(actions.completedTx(msgData.metamaskId)) - }) - } -} - -function signTx (txData) { - return (dispatch) => { - global.ethQuery.sendTransaction(txData, (err, data) => { - dispatch(actions.hideLoadingIndication()) - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.hideWarning()) - }) - dispatch(this.showConfTxPage()) - } -} - -function sendTx (txData) { - log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`) - return (dispatch) => { - log.debug(`actions calling background.approveTransaction`) - background.approveTransaction(txData.id, (err) => { - if (err) { - dispatch(actions.txError(err)) - return log.error(err.message) - } - dispatch(actions.completedTx(txData.id)) - }) - } -} - -function updateAndApproveTx (txData) { - log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData)) - return (dispatch) => { - log.debug(`actions calling background.updateAndApproveTx`) - background.updateAndApproveTransaction(txData, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - dispatch(actions.txError(err)) - return log.error(err.message) - } - dispatch(actions.completedTx(txData.id)) - }) - } -} - -function completedTx (id) { - return { - type: actions.COMPLETED_TX, - value: id, - } -} - -function txError (err) { - return { - type: actions.TRANSACTION_ERROR, - message: err.message, - } -} - -function cancelMsg (msgData) { - log.debug(`background.cancelMessage`) - background.cancelMessage(msgData.id) - return actions.completedTx(msgData.id) -} - -function cancelPersonalMsg (msgData) { - const id = msgData.id - background.cancelPersonalMessage(id) - return actions.completedTx(id) -} - -function cancelTx (txData) { - log.debug(`background.cancelTransaction`) - background.cancelTransaction(txData.id) - return actions.completedTx(txData.id) -} - -// -// initialize screen -// - -function showCreateVault () { - return { - type: actions.SHOW_CREATE_VAULT, - } -} - -function showRestoreVault () { - return { - type: actions.SHOW_RESTORE_VAULT, - } -} - -function forgotPassword () { - return { - type: actions.FORGOT_PASSWORD, - } -} - -function showInitializeMenu () { - return { - type: actions.SHOW_INIT_MENU, - } -} - -function showImportPage () { - return { - type: actions.SHOW_IMPORT_PAGE, - } -} - -function createNewVaultInProgress () { - return { - type: actions.CREATE_NEW_VAULT_IN_PROGRESS, - } -} - -function showNewVaultSeed (seed) { - return { - type: actions.SHOW_NEW_VAULT_SEED, - value: seed, - } -} - -function backToUnlockView () { - return { - type: actions.BACK_TO_UNLOCK_VIEW, - } -} - -function showNewKeychain () { - return { - type: actions.SHOW_NEW_KEYCHAIN, - } -} - -// -// unlock screen -// - -function unlockInProgress () { - return { - type: actions.UNLOCK_IN_PROGRESS, - } -} - -function unlockFailed (message) { - return { - type: actions.UNLOCK_FAILED, - value: message, - } -} - -function unlockMetamask (account) { - return { - type: actions.UNLOCK_METAMASK, - value: account, - } -} - -function updateMetamaskState (newState) { - return { - type: actions.UPDATE_METAMASK_STATE, - value: newState, - } -} - -function lockMetamask () { - log.debug(`background.setLocked`) - return callBackgroundThenUpdate(background.setLocked) -} - -function setCurrentAccountTab (newTabName) { - log.debug(`background.setCurrentAccountTab: ${newTabName}`) - return callBackgroundThenUpdateNoSpinner(background.setCurrentAccountTab, newTabName) -} - -function showAccountDetail (address) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.setSelectedAddress`) - background.setSelectedAddress(address, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch({ - type: actions.SHOW_ACCOUNT_DETAIL, - value: address, - }) - }) - } -} - -function backToAccountDetail (address) { - return { - type: actions.BACK_TO_ACCOUNT_DETAIL, - value: address, - } -} - -function showAccountsPage () { - return { - type: actions.SHOW_ACCOUNTS_PAGE, - } -} - -function showConfTxPage (transForward = true) { - return { - type: actions.SHOW_CONF_TX_PAGE, - transForward: transForward, - } -} - -function nextTx () { - return { - type: actions.NEXT_TX, - } -} - -function viewPendingTx (txId) { - return { - type: actions.VIEW_PENDING_TX, - value: txId, - } -} - -function previousTx () { - return { - type: actions.PREVIOUS_TX, - } -} - -function showConfigPage (transitionForward = true) { - return { - type: actions.SHOW_CONFIG_PAGE, - value: transitionForward, - } -} - -function showAddTokenPage (transitionForward = true) { - return { - type: actions.SHOW_ADD_TOKEN_PAGE, - value: transitionForward, - } -} - -function addToken (address, symbol, decimals) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - background.addToken(address, symbol, decimals, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - setTimeout(() => { - dispatch(actions.goHome()) - }, 250) - }) - } -} - -function goBackToInitView () { - return { - type: actions.BACK_TO_INIT_MENU, - } -} - -// -// notice -// - -function markNoticeRead (notice) { - return (dispatch) => { - dispatch(this.showLoadingIndication()) - log.debug(`background.markNoticeRead`) - background.markNoticeRead(notice, (err, notice) => { - dispatch(this.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err)) - } - if (notice) { - return dispatch(actions.showNotice(notice)) - } else { - dispatch(this.clearNotices()) - return { - type: actions.SHOW_ACCOUNTS_PAGE, - } - } - }) - } -} - -function showNotice (notice) { - return { - type: actions.SHOW_NOTICE, - value: notice, - } -} - -function clearNotices () { - return { - type: actions.CLEAR_NOTICES, - } -} - -function markAccountsFound () { - log.debug(`background.markAccountsFound`) - return callBackgroundThenUpdate(background.markAccountsFound) -} - -// -// config -// - -// default rpc target refers to localhost:8545 in this instance. -function setDefaultRpcTarget (rpcList) { - log.debug(`background.setDefaultRpcTarget`) - return (dispatch) => { - background.setDefaultRpc((err, result) => { - if (err) { - log.error(err) - return dispatch(self.displayWarning('Had a problem changing networks.')) - } - }) - } -} - -function setRpcTarget (newRpc) { - log.debug(`background.setRpcTarget`) - return (dispatch) => { - background.setCustomRpc(newRpc, (err, result) => { - if (err) { - log.error(err) - return dispatch(self.displayWarning('Had a problem changing networks!')) - } - }) - } -} - -// Calls the addressBookController to add a new address. -function addToAddressBook (recipient, nickname) { - log.debug(`background.addToAddressBook`) - return (dispatch) => { - background.setAddressBook(recipient, nickname, (err, result) => { - if (err) { - log.error(err) - return dispatch(self.displayWarning('Address book failed to update')) - } - }) - } -} - -function setProviderType (type) { - log.debug(`background.setProviderType`) - background.setProviderType(type) - return { - type: actions.SET_PROVIDER_TYPE, - value: type, - } -} - -function useEtherscanProvider () { - log.debug(`background.useEtherscanProvider`) - background.useEtherscanProvider() - return { - type: actions.USE_ETHERSCAN_PROVIDER, - } -} - -function showLoadingIndication (message) { - return { - type: actions.SHOW_LOADING, - value: message, - } -} - -function hideLoadingIndication () { - return { - type: actions.HIDE_LOADING, - } -} - -function showSubLoadingIndication () { - return { - type: actions.SHOW_SUB_LOADING_INDICATION, - } -} - -function hideSubLoadingIndication () { - return { - type: actions.HIDE_SUB_LOADING_INDICATION, - } -} - -function displayWarning (text) { - return { - type: actions.DISPLAY_WARNING, - value: text, - } -} - -function hideWarning () { - return { - type: actions.HIDE_WARNING, - } -} - -function requestExportAccount () { - return { - type: actions.REQUEST_ACCOUNT_EXPORT, - } -} - -function exportAccount (password, address) { - var self = this - - return function (dispatch) { - dispatch(self.showLoadingIndication()) - - log.debug(`background.submitPassword`) - background.submitPassword(password, function (err) { - if (err) { - log.error('Error in submiting password.') - dispatch(self.hideLoadingIndication()) - return dispatch(self.displayWarning('Incorrect Password.')) - } - log.debug(`background.exportAccount`) - background.exportAccount(address, function (err, result) { - dispatch(self.hideLoadingIndication()) - - if (err) { - log.error(err) - return dispatch(self.displayWarning('Had a problem exporting the account.')) - } - - dispatch(self.showPrivateKey(result)) - }) - }) - } -} - -function showPrivateKey (key) { - return { - type: actions.SHOW_PRIVATE_KEY, - value: key, - } -} - -function saveAccountLabel (account, label) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.saveAccountLabel`) - background.saveAccountLabel(account, label, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch({ - type: actions.SAVE_ACCOUNT_LABEL, - value: { account, label }, - }) - }) - } -} - -function showSendPage () { - return { - type: actions.SHOW_SEND_PAGE, - } -} - -function buyEth (opts) { - return (dispatch) => { - const url = getBuyEthUrl(opts) - global.platform.openWindow({ url }) - dispatch({ - type: actions.BUY_ETH, - }) - } -} - -function buyEthView (address) { - return { - type: actions.BUY_ETH_VIEW, - value: address, - } -} - -function coinBaseSubview () { - return { - type: actions.COINBASE_SUBVIEW, - } -} - -function pairUpdate (coin) { - return (dispatch) => { - dispatch(actions.showSubLoadingIndication()) - dispatch(actions.hideWarning()) - shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { - dispatch(actions.hideSubLoadingIndication()) - dispatch({ - type: actions.PAIR_UPDATE, - value: { - marketinfo: mktResponse, - }, - }) - }) - } -} - -function shapeShiftSubview (network) { - var pair = 'btc_eth' - - return (dispatch) => { - dispatch(actions.showSubLoadingIndication()) - shapeShiftRequest('marketinfo', {pair}, (mktResponse) => { - shapeShiftRequest('getcoins', {}, (response) => { - dispatch(actions.hideSubLoadingIndication()) - if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) - dispatch({ - type: actions.SHAPESHIFT_SUBVIEW, - value: { - marketinfo: mktResponse, - coinOptions: response, - }, - }) - }) - }) - } -} - -function coinShiftRquest (data, marketData) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - shapeShiftRequest('shift', { method: 'POST', data}, (response) => { - dispatch(actions.hideLoadingIndication()) - if (response.error) return dispatch(actions.displayWarning(response.error)) - var message = ` - Deposit your ${response.depositType} to the address bellow:` - log.debug(`background.createShapeShiftTx`) - background.createShapeShiftTx(response.deposit, response.depositType) - dispatch(actions.showQrView(response.deposit, [message].concat(marketData))) - }) - } -} - -function showQrView (data, message) { - return { - type: actions.SHOW_QR_VIEW, - value: { - message: message, - data: data, - }, - } -} -function reshowQrCode (data, coin) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { - if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) - - var message = [ - `Deposit your ${coin} to the address bellow:`, - `Deposit Limit: ${mktResponse.limit}`, - `Deposit Minimum:${mktResponse.minimum}`, - ] - - dispatch(actions.hideLoadingIndication()) - return dispatch(actions.showQrView(data, message)) - }) - } -} - -function shapeShiftRequest (query, options, cb) { - var queryResponse, method - !options ? options = {} : null - options.method ? method = options.method : method = 'GET' - - var requestListner = function (request) { - queryResponse = JSON.parse(this.responseText) - cb ? cb(queryResponse) : null - return queryResponse - } - - var shapShiftReq = new XMLHttpRequest() - shapShiftReq.addEventListener('load', requestListner) - shapShiftReq.open(method, `https://shapeshift.io/${query}/${options.pair ? options.pair : ''}`, true) - - if (options.method === 'POST') { - var jsonObj = JSON.stringify(options.data) - shapShiftReq.setRequestHeader('Content-Type', 'application/json') - return shapShiftReq.send(jsonObj) - } else { - return shapShiftReq.send() - } -} - -// Call Background Then Update -// -// A function generator for a common pattern wherein: -// We show loading indication. -// We call a background method. -// We hide loading indication. -// If it errored, we show a warning. -// If it didn't, we update the state. -function callBackgroundThenUpdateNoSpinner (method, ...args) { - return (dispatch) => { - method.call(background, ...args, (err) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - forceUpdateMetamaskState(dispatch) - }) - } -} - -function callBackgroundThenUpdate (method, ...args) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - method.call(background, ...args, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - forceUpdateMetamaskState(dispatch) - }) - } -} - -function forceUpdateMetamaskState (dispatch) { - log.debug(`background.getState`) - background.getState((err, newState) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch(actions.updateMetamaskState(newState)) - }) -} diff --git a/ui/app/add-token.js b/ui/app/add-token.js deleted file mode 100644 index b303b5c0d..000000000 --- a/ui/app/add-token.js +++ /dev/null @@ -1,219 +0,0 @@ -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 ethUtil = require('ethereumjs-util') -const abi = require('human-standard-token-abi') -const Eth = require('ethjs-query') -const EthContract = require('ethjs-contract') - -const emptyAddr = '0x0000000000000000000000000000000000000000' - -module.exports = connect(mapStateToProps)(AddTokenScreen) - -function mapStateToProps (state) { - return { - } -} - -inherits(AddTokenScreen, Component) -function AddTokenScreen () { - this.state = { - warning: null, - address: null, - symbol: 'TOKEN', - decimals: 18, - } - Component.call(this) -} - -AddTokenScreen.prototype.render = function () { - const state = this.state - const props = this.props - const { warning, symbol, decimals } = state - - return ( - h('.flex-column.flex-grow', [ - - // subtitle and nav - 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', 'Add Token'), - ]), - - h('.error', { - style: { - display: warning ? 'block' : 'none', - padding: '0 20px', - textAlign: 'center', - }, - }, warning), - - // conf view - h('.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-space-around', { - style: { - padding: '20px', - }, - }, [ - - h('div', [ - h('span', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Token Address'), - ]), - - h('section.flex-row.flex-center', [ - h('input#token-address', { - name: 'address', - placeholder: 'Token Address', - onChange: this.tokenAddressDidChange.bind(this), - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - }), - ]), - - h('div', [ - h('span', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Token Sybmol'), - ]), - - h('div', { style: {display: 'flex'} }, [ - h('input#token_symbol', { - placeholder: `Like "ETH"`, - value: symbol, - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - onChange: (event) => { - var element = event.target - var symbol = element.value - this.setState({ symbol }) - }, - }), - ]), - - h('div', [ - h('span', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Decimals of Precision'), - ]), - - h('div', { style: {display: 'flex'} }, [ - h('input#token_decimals', { - value: decimals, - type: 'number', - min: 0, - max: 36, - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - onChange: (event) => { - var element = event.target - var decimals = element.value.trim() - this.setState({ decimals }) - }, - }), - ]), - - h('button', { - style: { - alignSelf: 'center', - }, - onClick: (event) => { - const valid = this.validateInputs() - if (!valid) return - - const { address, symbol, decimals } = this.state - this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) - }, - }, 'Add'), - ]), - ]), - ]) - ) -} - -AddTokenScreen.prototype.componentWillMount = function () { - if (typeof global.ethereumProvider === 'undefined') return - - this.eth = new Eth(global.ethereumProvider) - this.contract = new EthContract(this.eth) - this.TokenContract = this.contract(abi) -} - -AddTokenScreen.prototype.tokenAddressDidChange = function (event) { - const el = event.target - const address = el.value.trim() - if (ethUtil.isValidAddress(address) && address !== emptyAddr) { - this.setState({ address }) - this.attemptToAutoFillTokenParams(address) - } -} - -AddTokenScreen.prototype.validateInputs = function () { - let msg = '' - const state = this.state - const { address, symbol, decimals } = state - - const validAddress = ethUtil.isValidAddress(address) - if (!validAddress) { - msg += 'Address is invalid. ' - } - - const validDecimals = decimals >= 0 && decimals < 36 - if (!validDecimals) { - msg += 'Decimals must be at least 0, and not over 36. ' - } - - const symbolLen = symbol.trim().length - const validSymbol = symbolLen > 0 && symbolLen < 10 - if (!validSymbol) { - msg += 'Symbol must be between 0 and 10 characters.' - } - - const isValid = validAddress && validDecimals - - if (!isValid) { - this.setState({ - warning: msg, - }) - } else { - this.setState({ warning: null }) - } - - return isValid -} - -AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { - const contract = this.TokenContract.at(address) - - const results = await Promise.all([ - contract.symbol(), - contract.decimals(), - ]) - - const [ symbol, decimals ] = results - if (symbol && decimals) { - console.log('SETTING SYMBOL AND DECIMALS', { symbol, decimals }) - this.setState({ symbol: symbol[0], decimals: decimals[0].toString() }) - } -} - diff --git a/ui/app/app.js b/ui/app/app.js deleted file mode 100644 index 1a63002e1..000000000 --- a/ui/app/app.js +++ /dev/null @@ -1,591 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('./actions') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') -// init -const InitializeMenuScreen = require('./first-time/init-menu') -const NewKeyChainScreen = require('./new-keychain') -// unlock -const UnlockScreen = require('./unlock') -// accounts -const AccountsScreen = require('./accounts') -const AccountDetailScreen = require('./account-detail') -const SendTransactionScreen = require('./send') -const ConfirmTxScreen = require('./conf-tx') -// notice -const NoticeScreen = require('./components/notice') -const generateLostAccountsNotice = require('../lib/lost-accounts-notice') -// other views -const ConfigScreen = require('./config') -const AddTokenScreen = require('./add-token') -const Import = require('./accounts/import') -const InfoScreen = require('./info') -const Loading = require('./components/loading') -const SandwichExpando = require('sandwich-expando') -const MenuDroppo = require('menu-droppo') -const DropMenuItem = require('./components/drop-menu-item') -const NetworkIndicator = require('./components/network') -const Tooltip = require('./components/tooltip') -const BuyView = require('./components/buy-button-subview') -const QrView = require('./components/qr-code') -const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete') -const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') -const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation') - -module.exports = connect(mapStateToProps)(App) - -inherits(App, Component) -function App () { Component.call(this) } - -function mapStateToProps (state) { - return { - // state from plugin - isLoading: state.appState.isLoading, - loadingMessage: state.appState.loadingMessage, - noActiveNotices: state.metamask.noActiveNotices, - isInitialized: state.metamask.isInitialized, - isUnlocked: state.metamask.isUnlocked, - currentView: state.appState.currentView, - activeAddress: state.appState.activeAddress, - transForward: state.appState.transForward, - seedWords: state.metamask.seedWords, - unapprovedTxs: state.metamask.unapprovedTxs, - unapprovedMsgs: state.metamask.unapprovedMsgs, - menuOpen: state.appState.menuOpen, - network: state.metamask.network, - provider: state.metamask.provider, - forgottenPassword: state.appState.forgottenPassword, - lastUnreadNotice: state.metamask.lastUnreadNotice, - lostAccounts: state.metamask.lostAccounts, - frequentRpcList: state.metamask.frequentRpcList || [], - } -} - -App.prototype.render = function () { - var props = this.props - const { isLoading, loadingMessage, transForward, network } = props - const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' - const loadMessage = loadingMessage || isLoadingNetwork ? - `Connecting to ${this.getNetworkName()}` : null - - log.debug('Main ui render function') - - return ( - - h('.flex-column.flex-grow.full-height', { - style: { - // Windows was showing a vertical scroll bar: - overflow: 'hidden', - position: 'relative', - }, - }, [ - - // app bar - this.renderAppBar(), - this.renderNetworkDropdown(), - this.renderDropdown(), - - h(Loading, { - isLoading: isLoading || isLoadingNetwork, - loadingMessage: loadMessage, - }), - - // panel content - h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), { - style: { - height: '380px', - width: '360px', - }, - }, [ - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.renderPrimary(), - ]), - ]), - ]) - ) -} - -App.prototype.renderAppBar = function () { - if (window.METAMASK_UI_TYPE === 'notification') { - return null - } - - const props = this.props - const state = this.state || {} - const isNetworkMenuOpen = state.isNetworkMenuOpen || false - - return ( - - h('div', [ - - h('.app-header.flex-row.flex-space-between', { - style: { - alignItems: 'center', - visibility: props.isUnlocked ? 'visible' : 'none', - background: props.isUnlocked ? 'white' : 'none', - height: '36px', - position: 'relative', - zIndex: 12, - }, - }, [ - - h('div.left-menu-section', { - style: { - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - }, - }, [ - - // mini logo - h('img', { - height: 24, - width: 24, - src: '/images/icon-128.png', - }), - - h(NetworkIndicator, { - network: this.props.network, - provider: this.props.provider, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen }) - }, - }), - ]), - - // metamask name - props.isUnlocked && h('h1', { - style: { - position: 'relative', - left: '9px', - }, - }, 'MetaMask'), - - props.isUnlocked && h('div', { - style: { - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - }, - }, [ - - // small accounts nav - props.isUnlocked && h(Tooltip, { title: 'Switch Accounts' }, [ - h('img.cursor-pointer.color-orange', { - src: 'images/switch_acc.svg', - style: { - width: '23.5px', - marginRight: '8px', - }, - onClick: (event) => { - event.stopPropagation() - this.props.dispatch(actions.showAccountsPage()) - }, - }), - ]), - - // hamburger - props.isUnlocked && h(SandwichExpando, { - width: 16, - barHeight: 2, - padding: 0, - isOpen: state.isMainMenuOpen, - color: 'rgb(247,146,30)', - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - this.setState({ isMainMenuOpen: !state.isMainMenuOpen }) - }, - }), - ]), - ]), - ]) - ) -} - -App.prototype.renderNetworkDropdown = function () { - const props = this.props - const rpcList = props.frequentRpcList - const state = this.state || {} - const isOpen = state.isNetworkMenuOpen - - return h(MenuDroppo, { - isOpen, - onClickOutside: (event) => { - this.setState({ isNetworkMenuOpen: !isOpen }) - }, - zIndex: 11, - style: { - position: 'absolute', - left: 0, - top: '36px', - }, - innerStyle: { - background: 'white', - boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', - }, - }, [ // DROP MENU ITEMS - h('style', ` - .drop-menu-item:hover { background:rgb(235, 235, 235); } - .drop-menu-item i { margin: 11px; } - `), - - h(DropMenuItem, { - label: 'Main Ethereum Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setProviderType('mainnet')), - icon: h('.menu-icon.diamond'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Ropsten Test Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setProviderType('ropsten')), - icon: h('.menu-icon.red-dot'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Kovan Test Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false}), - action: () => props.dispatch(actions.setProviderType('kovan')), - icon: h('.menu-icon.hollow-diamond'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Rinkeby Test Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false}), - action: () => props.dispatch(actions.setProviderType('rinkeby')), - icon: h('.menu-icon.golden-square'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Localhost 8545', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setDefaultRpcTarget(rpcList)), - icon: h('i.fa.fa-question-circle.fa-lg'), - activeNetworkRender: props.provider.rpcTarget, - }), - - this.renderCustomOption(props.provider), - this.renderCommonRpc(rpcList, props.provider), - - h(DropMenuItem, { - label: 'Custom RPC', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => this.props.dispatch(actions.showConfigPage()), - icon: h('i.fa.fa-question-circle.fa-lg'), - }), - - ]) -} - -App.prototype.renderDropdown = function () { - const state = this.state || {} - const isOpen = state.isMainMenuOpen - - return h(MenuDroppo, { - isOpen: isOpen, - zIndex: 11, - onClickOutside: (event) => { - this.setState({ isMainMenuOpen: !isOpen }) - }, - style: { - position: 'absolute', - right: 0, - top: '36px', - }, - innerStyle: { - background: 'white', - boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', - }, - }, [ // DROP MENU ITEMS - h('style', ` - .drop-menu-item:hover { background:rgb(235, 235, 235); } - .drop-menu-item i { margin: 11px; } - `), - - h(DropMenuItem, { - label: 'Settings', - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.showConfigPage()), - icon: h('i.fa.fa-gear.fa-lg'), - }), - - h(DropMenuItem, { - label: 'Import Account', - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.showImportPage()), - icon: h('i.fa.fa-arrow-circle-o-up.fa-lg'), - }), - - h(DropMenuItem, { - label: 'Lock', - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.lockMetamask()), - icon: h('i.fa.fa-lock.fa-lg'), - }), - - h(DropMenuItem, { - label: 'Info/Help', - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.showInfoPage()), - icon: h('i.fa.fa-question.fa-lg'), - }), - ]) -} - -App.prototype.renderBackButton = function (style, justArrow = false) { - var props = this.props - return ( - h('.flex-row', { - key: 'leftArrow', - style: style, - onClick: () => props.dispatch(actions.goBackToInitView()), - }, [ - h('i.fa.fa-arrow-left.cursor-pointer'), - justArrow ? null : h('div.cursor-pointer', { - style: { - marginLeft: '3px', - }, - onClick: () => props.dispatch(actions.goBackToInitView()), - }, 'BACK'), - ]) - ) -} - -App.prototype.renderPrimary = function () { - log.debug('rendering primary') - var props = this.props - - // notices - if (!props.noActiveNotices) { - log.debug('rendering notice screen for unread notices.') - return h(NoticeScreen, { - notice: props.lastUnreadNotice, - key: 'NoticeScreen', - onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)), - }) - } else if (props.lostAccounts && props.lostAccounts.length > 0) { - log.debug('rendering notice screen for lost accounts view.') - return h(NoticeScreen, { - notice: generateLostAccountsNotice(props.lostAccounts), - key: 'LostAccountsNotice', - onConfirm: () => props.dispatch(actions.markAccountsFound()), - }) - } - - if (props.seedWords) { - log.debug('rendering seed words') - return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'}) - } - - // show initialize screen - if (!props.isInitialized || props.forgottenPassword) { - // show current view - log.debug('rendering an initialize screen') - switch (props.currentView.name) { - - case 'restoreVault': - log.debug('rendering restore vault screen') - return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) - - default: - log.debug('rendering menu screen') - return h(InitializeMenuScreen, {key: 'menuScreenInit'}) - } - } - - // show unlock screen - if (!props.isUnlocked) { - switch (props.currentView.name) { - - case 'restoreVault': - log.debug('rendering restore vault screen') - return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) - - case 'config': - log.debug('rendering config screen from unlock screen.') - return h(ConfigScreen, {key: 'config'}) - - default: - log.debug('rendering locked screen') - return h(UnlockScreen, {key: 'locked'}) - } - } - - // show current view - switch (props.currentView.name) { - - case 'accounts': - log.debug('rendering accounts screen') - return h(AccountsScreen, {key: 'accounts'}) - - case 'accountDetail': - log.debug('rendering account detail screen') - return h(AccountDetailScreen, {key: 'account-detail'}) - - case 'sendTransaction': - log.debug('rendering send tx screen') - return h(SendTransactionScreen, {key: 'send-transaction'}) - - case 'newKeychain': - log.debug('rendering new keychain screen') - return h(NewKeyChainScreen, {key: 'new-keychain'}) - - case 'confTx': - log.debug('rendering confirm tx screen') - return h(ConfirmTxScreen, {key: 'confirm-tx'}) - - case 'add-token': - log.debug('rendering add-token screen from unlock screen.') - return h(AddTokenScreen, {key: 'add-token'}) - - case 'config': - log.debug('rendering config screen') - return h(ConfigScreen, {key: 'config'}) - - case 'import-menu': - log.debug('rendering import screen') - return h(Import, {key: 'import-menu'}) - - case 'reveal-seed-conf': - log.debug('rendering reveal seed confirmation screen') - return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'}) - - case 'info': - log.debug('rendering info screen') - return h(InfoScreen, {key: 'info'}) - - case 'buyEth': - log.debug('rendering buy ether screen') - return h(BuyView, {key: 'buyEthView'}) - - case 'qr': - log.debug('rendering show qr screen') - return h('div', { - style: { - position: 'absolute', - height: '100%', - top: '0px', - left: '0px', - }, - }, [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { - onClick: () => props.dispatch(actions.backToAccountDetail(props.activeAddress)), - style: { - marginLeft: '10px', - marginTop: '50px', - }, - }), - h('div', { - style: { - position: 'absolute', - left: '44px', - width: '285px', - }, - }, [ - h(QrView, {key: 'qr'}), - ]), - ]) - - default: - log.debug('rendering default, account detail screen') - return h(AccountDetailScreen, {key: 'account-detail'}) - } -} - -App.prototype.toggleMetamaskActive = function () { - if (!this.props.isUnlocked) { - // currently inactive: redirect to password box - var passwordBox = document.querySelector('input[type=password]') - if (!passwordBox) return - passwordBox.focus() - } else { - // currently active: deactivate - this.props.dispatch(actions.lockMetamask(false)) - } -} - -App.prototype.renderCustomOption = function (provider) { - const { rpcTarget, type } = provider - if (type !== 'rpc') return null - - // Concatenate long URLs - let label = rpcTarget - if (rpcTarget.length > 31) { - label = label.substr(0, 34) + '...' - } - - switch (rpcTarget) { - - case 'http://localhost:8545': - return null - - default: - return h(DropMenuItem, { - label, - key: rpcTarget, - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - icon: h('i.fa.fa-question-circle.fa-lg'), - activeNetworkRender: 'custom', - }) - } -} - -App.prototype.getNetworkName = function () { - const { provider } = this.props - const providerName = provider.type - - let name - - if (providerName === 'mainnet') { - name = 'Main Ethereum Network' - } else if (providerName === 'ropsten') { - name = 'Ropsten Test Network' - } else if (providerName === 'kovan') { - name = 'Kovan Test Network' - } else if (providerName === 'rinkeby') { - name = 'Rinkeby Test Network' - } else { - name = 'Unknown Private Network' - } - - return name -} - -App.prototype.renderCommonRpc = function (rpcList, provider) { - const { rpcTarget } = provider - const props = this.props - - return rpcList.map((rpc) => { - if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { - return null - } else { - return h(DropMenuItem, { - label: rpc, - key: rpc, - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setRpcTarget(rpc)), - icon: h('i.fa.fa-question-circle.fa-lg'), - activeNetworkRender: rpc, - }) - } - }) -} diff --git a/ui/app/components/account-export.js b/ui/app/components/account-export.js deleted file mode 100644 index 394d878f7..000000000 --- a/ui/app/components/account-export.js +++ /dev/null @@ -1,122 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const copyToClipboard = require('copy-to-clipboard') -const actions = require('../actions') -const ethUtil = require('ethereumjs-util') -const connect = require('react-redux').connect - -module.exports = connect(mapStateToProps)(ExportAccountView) - -inherits(ExportAccountView, Component) -function ExportAccountView () { - Component.call(this) -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -ExportAccountView.prototype.render = function () { - var state = this.props - var accountDetail = state.accountDetail - - if (!accountDetail) return h('div') - var accountExport = accountDetail.accountExport - - var notExporting = accountExport === 'none' - var exportRequested = accountExport === 'requested' - var accountExported = accountExport === 'completed' - - if (notExporting) return h('div') - - if (exportRequested) { - var warning = `Export private keys at your own risk.` - return ( - h('div', { - style: { - display: 'inline-block', - textAlign: 'center', - }, - }, - [ - h('div', { - key: 'exporting', - style: { - margin: '0 20px', - }, - }, [ - h('p.error', warning), - h('input#exportAccount.sizing-input', { - type: 'password', - placeholder: 'confirm password', - onKeyPress: this.onExportKeyPress.bind(this), - style: { - position: 'relative', - top: '1.5px', - marginBottom: '7px', - }, - }), - ]), - h('div', { - key: 'buttons', - style: { - margin: '0 20px', - }, - }, - [ - h('button', { - onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }), - style: { - marginRight: '10px', - }, - }, 'Submit'), - h('button', { - onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), - }, 'Cancel'), - ]), - (this.props.warning) && ( - h('span.error', { - style: { - margin: '20px', - }, - }, this.props.warning.split('-')) - ), - ]) - ) - } - - if (accountExported) { - return h('div.privateKey', { - style: { - margin: '0 20px', - }, - }, [ - h('label', 'Your private key (click to copy):'), - h('p.error.cursor-pointer', { - style: { - textOverflow: 'ellipsis', - overflow: 'hidden', - webkitUserSelect: 'text', - width: '100%', - }, - onClick: function (event) { - copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey)) - }, - }, ethUtil.stripHexPrefix(accountDetail.privateKey)), - h('button', { - onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), - }, 'Done'), - ]) - } -} - -ExportAccountView.prototype.onExportKeyPress = function (event) { - if (event.key !== 'Enter') return - event.preventDefault() - - var input = document.getElementById('exportAccount').value - this.props.dispatch(actions.exportAccount(input, this.props.address)) -} diff --git a/ui/app/components/account-info-link.js b/ui/app/components/account-info-link.js deleted file mode 100644 index 6526ab502..000000000 --- a/ui/app/components/account-info-link.js +++ /dev/null @@ -1,41 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const Tooltip = require('./tooltip') -const genAccountLink = require('../../lib/account-link') - -module.exports = AccountInfoLink - -inherits(AccountInfoLink, Component) -function AccountInfoLink () { - Component.call(this) -} - -AccountInfoLink.prototype.render = function () { - const { selected, network } = this.props - const title = 'View account on Etherscan' - const url = genAccountLink(selected, network) - - if (!url) { - return null - } - - return h('.account-info-link', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - - h(Tooltip, { - title, - }, [ - h('i.fa.fa-info-circle.cursor-pointer.color-orange', { - style: { - margin: '5px', - }, - onClick () { global.platform.openWindow({ url }) }, - }), - ]), - ]) -} diff --git a/ui/app/components/account-panel.js b/ui/app/components/account-panel.js deleted file mode 100644 index abaaf8163..000000000 --- a/ui/app/components/account-panel.js +++ /dev/null @@ -1,86 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const Identicon = require('./identicon') -const formatBalance = require('../util').formatBalance -const addressSummary = require('../util').addressSummary - -module.exports = AccountPanel - - -inherits(AccountPanel, Component) -function AccountPanel () { - Component.call(this) -} - -AccountPanel.prototype.render = function () { - var state = this.props - var identity = state.identity || {} - var account = state.account || {} - var isFauceting = state.isFauceting - - var panelState = { - key: `accountPanel${identity.address}`, - identiconKey: identity.address, - identiconLabel: identity.name || '', - attributes: [ - { - key: 'ADDRESS', - value: addressSummary(identity.address), - }, - balanceOrFaucetingIndication(account, isFauceting), - ], - } - - return ( - - h('.identity-panel.flex-row.flex-space-between', { - style: { - flex: '1 0 auto', - cursor: panelState.onClick ? 'pointer' : undefined, - }, - onClick: panelState.onClick, - }, [ - - // account identicon - h('.identicon-wrapper.flex-column.select-none', [ - h(Identicon, { - address: panelState.identiconKey, - imageify: state.imageifyIdenticons, - }), - h('span.font-small', panelState.identiconLabel.substring(0, 7) + '...'), - ]), - - // account address, balance - h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [ - - panelState.attributes.map((attr) => { - return h('.flex-row.flex-space-between', { - key: '' + Math.round(Math.random() * 1000000), - }, [ - h('label.font-small.no-select', attr.key), - h('span.font-small', attr.value), - ]) - }), - ]), - - ]) - - ) -} - -function balanceOrFaucetingIndication (account, isFauceting) { - // Temporarily deactivating isFauceting indication - // because it shows fauceting for empty restored accounts. - if (/* isFauceting*/ false) { - return { - key: 'Account is auto-funding.', - value: 'Please wait.', - } - } else { - return { - key: 'BALANCE', - value: formatBalance(account.balance), - } - } -} diff --git a/ui/app/components/balance.js b/ui/app/components/balance.js deleted file mode 100644 index 57ca84564..000000000 --- a/ui/app/components/balance.js +++ /dev/null @@ -1,89 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const formatBalance = require('../util').formatBalance -const generateBalanceObject = require('../util').generateBalanceObject -const Tooltip = require('./tooltip.js') -const FiatValue = require('./fiat-value.js') - -module.exports = EthBalanceComponent - -inherits(EthBalanceComponent, Component) -function EthBalanceComponent () { - Component.call(this) -} - -EthBalanceComponent.prototype.render = function () { - var props = this.props - let { value } = props - var style = props.style - var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true - value = value ? formatBalance(value, 6, needsParse) : '...' - var width = props.width - - return ( - - h('.ether-balance.ether-balance-amount', { - style: style, - }, [ - h('div', { - style: { - display: 'inline', - width: width, - }, - }, this.renderBalance(value)), - ]) - - ) -} -EthBalanceComponent.prototype.renderBalance = function (value) { - var props = this.props - if (value === 'None') return value - if (value === '...') return value - var balanceObj = generateBalanceObject(value, props.shorten ? 1 : 3) - var balance - var splitBalance = value.split(' ') - var ethNumber = splitBalance[0] - var ethSuffix = splitBalance[1] - const showFiat = 'showFiat' in props ? props.showFiat : true - - if (props.shorten) { - balance = balanceObj.shortBalance - } else { - balance = balanceObj.balance - } - - var label = balanceObj.label - - return ( - h(Tooltip, { - position: 'bottom', - title: `${ethNumber} ${ethSuffix}`, - }, h('div.flex-column', [ - h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('div', { - style: { - width: '100%', - textAlign: 'right', - }, - }, this.props.incoming ? `+${balance}` : balance), - h('div', { - style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - }, - }, label), - ]), - - showFiat ? h(FiatValue, { value: props.value }) : null, - ])) - ) -} diff --git a/ui/app/components/binary-renderer.js b/ui/app/components/binary-renderer.js deleted file mode 100644 index 0b6a1f5c2..000000000 --- a/ui/app/components/binary-renderer.js +++ /dev/null @@ -1,46 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') -const extend = require('xtend') - -module.exports = BinaryRenderer - -inherits(BinaryRenderer, Component) -function BinaryRenderer () { - Component.call(this) -} - -BinaryRenderer.prototype.render = function () { - const props = this.props - const { value, style } = props - const text = this.hexToText(value) - - const defaultStyle = extend({ - width: '315px', - maxHeight: '210px', - resize: 'none', - border: 'none', - background: 'white', - padding: '3px', - }, style) - - return ( - h('textarea.font-small', { - readOnly: true, - style: defaultStyle, - defaultValue: text, - }) - ) -} - -BinaryRenderer.prototype.hexToText = function (hex) { - try { - const stripped = ethUtil.stripHexPrefix(hex) - const buff = Buffer.from(stripped, 'hex') - return buff.toString('utf8') - } catch (e) { - return hex - } -} - diff --git a/ui/app/components/bn-as-decimal-input.js b/ui/app/components/bn-as-decimal-input.js deleted file mode 100644 index f3ace4720..000000000 --- a/ui/app/components/bn-as-decimal-input.js +++ /dev/null @@ -1,174 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const extend = require('xtend') - -module.exports = BnAsDecimalInput - -inherits(BnAsDecimalInput, Component) -function BnAsDecimalInput () { - this.state = { invalid: null } - Component.call(this) -} - -/* Bn as Decimal Input - * - * A component for allowing easy, decimal editing - * of a passed in bn string value. - * - * On change, calls back its `onChange` function parameter - * and passes it an updated bn string. - */ - -BnAsDecimalInput.prototype.render = function () { - const props = this.props - const state = this.state - - const { value, scale, precision, onChange, min, max } = props - - const suffix = props.suffix - const style = props.style - const valueString = value.toString(10) - const newValue = this.downsize(valueString, scale, precision) - - return ( - h('.flex-column', [ - h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('input.hex-input', { - type: 'number', - step: 'any', - required: true, - min, - max, - style: extend({ - display: 'block', - textAlign: 'right', - backgroundColor: 'transparent', - border: '1px solid #bdbdbd', - - }, style), - value: newValue, - onBlur: (event) => { - this.updateValidity(event) - }, - onChange: (event) => { - this.updateValidity(event) - const value = (event.target.value === '') ? '' : event.target.value - - - const scaledNumber = this.upsize(value, scale, precision) - const precisionBN = new BN(scaledNumber, 10) - onChange(precisionBN, event.target.checkValidity()) - }, - onInvalid: (event) => { - const msg = this.constructWarning() - if (msg === state.invalid) { - return - } - this.setState({ invalid: msg }) - event.preventDefault() - return false - }, - }), - h('div', { - style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - marginRight: '6px', - width: '20px', - }, - }, suffix), - ]), - - state.invalid ? h('span.error', { - style: { - position: 'absolute', - right: '0px', - textAlign: 'right', - transform: 'translateY(26px)', - padding: '3px', - background: 'rgba(255,255,255,0.85)', - zIndex: '1', - textTransform: 'capitalize', - border: '2px solid #E20202', - }, - }, state.invalid) : null, - ]) - ) -} - -BnAsDecimalInput.prototype.setValid = function (message) { - this.setState({ invalid: null }) -} - -BnAsDecimalInput.prototype.updateValidity = function (event) { - const target = event.target - const value = this.props.value - const newValue = target.value - - if (value === newValue) { - return - } - - const valid = target.checkValidity() - - if (valid) { - this.setState({ invalid: null }) - } -} - -BnAsDecimalInput.prototype.constructWarning = function () { - const { name, min, max } = this.props - let message = name ? name + ' ' : '' - - if (min && max) { - message += `must be greater than or equal to ${min} and less than or equal to ${max}.` - } else if (min) { - message += `must be greater than or equal to ${min}.` - } else if (max) { - message += `must be less than or equal to ${max}.` - } else { - message += 'Invalid input.' - } - - return message -} - - -BnAsDecimalInput.prototype.downsize = function (number, scale, precision) { - // if there is no scaling, simply return the number - if (scale === 0) { - return Number(number) - } else { - // if the scale is the same as the precision, account for this edge case. - var decimals = (scale === precision) ? -1 : scale - precision - return Number(number.slice(0, -scale) + '.' + number.slice(-scale, decimals)) - } -} - -BnAsDecimalInput.prototype.upsize = function (number, scale, precision) { - var stringArray = number.toString().split('.') - var decimalLength = stringArray[1] ? stringArray[1].length : 0 - var newString = stringArray[0] - - // If there is scaling and decimal parts exist, integrate them in. - if ((scale !== 0) && (decimalLength !== 0)) { - newString += stringArray[1].slice(0, precision) - } - - // Add 0s to account for the upscaling. - for (var i = decimalLength; i < scale; i++) { - newString += '0' - } - return newString -} diff --git a/ui/app/components/buy-button-subview.js b/ui/app/components/buy-button-subview.js deleted file mode 100644 index 87084f92d..000000000 --- a/ui/app/components/buy-button-subview.js +++ /dev/null @@ -1,197 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const connect = require('react-redux').connect -const actions = require('../actions') -const CoinbaseForm = require('./coinbase-form') -const ShapeshiftForm = require('./shapeshift-form') -const Loading = require('./loading') -const AccountPanel = require('./account-panel') -const RadioList = require('./custom-radio-list') - -module.exports = connect(mapStateToProps)(BuyButtonSubview) - -function mapStateToProps (state) { - return { - identity: state.appState.identity, - account: state.metamask.accounts[state.appState.buyView.buyAddress], - warning: state.appState.warning, - buyView: state.appState.buyView, - network: state.metamask.network, - provider: state.metamask.provider, - context: state.appState.currentView.context, - isSubLoading: state.appState.isSubLoading, - } -} - -inherits(BuyButtonSubview, Component) -function BuyButtonSubview () { - Component.call(this) -} - -BuyButtonSubview.prototype.render = function () { - const props = this.props - const isLoading = props.isSubLoading - - return ( - h('.buy-eth-section.flex-column', { - style: { - alignItems: 'center', - }, - }, [ - // back button - h('.flex-row', { - style: { - alignItems: 'center', - justifyContent: 'center', - }, - }, [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { - onClick: this.backButtonContext.bind(this), - style: { - position: 'absolute', - left: '10px', - }, - }), - h('h2.text-transform-uppercase.flex-center', { - style: { - width: '100vw', - background: 'rgb(235, 235, 235)', - color: 'rgb(174, 174, 174)', - paddingTop: '4px', - paddingBottom: '4px', - }, - }, 'Buy Eth'), - ]), - h('div', { - style: { - position: 'absolute', - top: '57vh', - left: '49vw', - }, - }, [ - h(Loading, {isLoading}), - ]), - h('div', { - style: { - width: '80%', - }, - }, [ - h(AccountPanel, { - showFullAddress: true, - identity: props.identity, - account: props.account, - }), - ]), - h('h3.text-transform-uppercase', { - style: { - paddingLeft: '15px', - fontFamily: 'Montserrat Light', - width: '100vw', - background: 'rgb(235, 235, 235)', - color: 'rgb(174, 174, 174)', - paddingTop: '4px', - paddingBottom: '4px', - }, - }, 'Select Service'), - h('.flex-row.selected-exchange', { - style: { - position: 'relative', - right: '35px', - marginTop: '20px', - marginBottom: '20px', - }, - }, [ - h(RadioList, { - defaultFocus: props.buyView.subview, - labels: [ - 'Coinbase', - 'ShapeShift', - ], - subtext: { - 'Coinbase': 'Crypto/FIAT (USA only)', - 'ShapeShift': 'Crypto', - }, - onClick: this.radioHandler.bind(this), - }), - ]), - h('h3.text-transform-uppercase', { - style: { - paddingLeft: '15px', - fontFamily: 'Montserrat Light', - width: '100vw', - background: 'rgb(235, 235, 235)', - color: 'rgb(174, 174, 174)', - paddingTop: '4px', - paddingBottom: '4px', - }, - }, props.buyView.subview), - this.formVersionSubview(), - ]) - ) -} - -BuyButtonSubview.prototype.formVersionSubview = function () { - const network = this.props.network - if (network === '1') { - if (this.props.buyView.formView.coinbase) { - return h(CoinbaseForm, this.props) - } else if (this.props.buyView.formView.shapeshift) { - return h(ShapeshiftForm, this.props) - } - } else { - return h('div.flex-column', { - style: { - alignItems: 'center', - margin: '50px', - }, - }, [ - h('h3.text-transform-uppercase', { - style: { - width: '225px', - marginBottom: '15px', - }, - }, 'In order to access this feature, please switch to the Main Network'), - ((network === '3') || (network === '4') || (network === '42')) ? h('h3.text-transform-uppercase', 'or go to the') : null, - (network === '3') ? h('button.text-transform-uppercase', { - onClick: () => this.props.dispatch(actions.buyEth({ network })), - style: { - marginTop: '15px', - }, - }, 'Ropsten Test Faucet') : null, - (network === '4') ? h('button.text-transform-uppercase', { - onClick: () => this.props.dispatch(actions.buyEth({ network })), - style: { - marginTop: '15px', - }, - }, 'Rinkeby Test Faucet') : null, - (network === '42') ? h('button.text-transform-uppercase', { - onClick: () => this.props.dispatch(actions.buyEth({ network })), - style: { - marginTop: '15px', - }, - }, 'Kovan Test Faucet') : null, - ]) - } -} - -BuyButtonSubview.prototype.navigateTo = function (url) { - global.platform.openWindow({ url }) -} - -BuyButtonSubview.prototype.backButtonContext = function () { - if (this.props.context === 'confTx') { - this.props.dispatch(actions.showConfTxPage(false)) - } else { - this.props.dispatch(actions.goHome()) - } -} - -BuyButtonSubview.prototype.radioHandler = function (event) { - switch (event.target.title) { - case 'Coinbase': - return this.props.dispatch(actions.coinBaseSubview()) - case 'ShapeShift': - return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type)) - } -} diff --git a/ui/app/components/coinbase-form.js b/ui/app/components/coinbase-form.js deleted file mode 100644 index f44d86045..000000000 --- a/ui/app/components/coinbase-form.js +++ /dev/null @@ -1,63 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const connect = require('react-redux').connect -const actions = require('../actions') - -module.exports = connect(mapStateToProps)(CoinbaseForm) - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -inherits(CoinbaseForm, Component) - -function CoinbaseForm () { - Component.call(this) -} - -CoinbaseForm.prototype.render = function () { - var props = this.props - - return h('.flex-column', { - style: { - marginTop: '35px', - padding: '25px', - width: '100%', - }, - }, [ - h('.flex-row', { - style: { - justifyContent: 'space-around', - margin: '33px', - marginTop: '0px', - }, - }, [ - h('button.btn-green', { - onClick: this.toCoinbase.bind(this), - }, 'Continue to Coinbase'), - - h('button.btn-red', { - onClick: () => props.dispatch(actions.backTobuyView(props.accounts.address)), - }, 'Cancel'), - ]), - ]) -} - -CoinbaseForm.prototype.toCoinbase = function () { - const props = this.props - const address = props.buyView.buyAddress - props.dispatch(actions.buyEth({ network: '1', address, amount: 0 })) -} - -CoinbaseForm.prototype.renderLoading = function () { - return h('img', { - style: { - width: '27px', - marginRight: '-27px', - }, - src: 'images/loading.svg', - }) -} diff --git a/ui/app/components/copyButton.js b/ui/app/components/copyButton.js deleted file mode 100644 index a25d0719c..000000000 --- a/ui/app/components/copyButton.js +++ /dev/null @@ -1,59 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const copyToClipboard = require('copy-to-clipboard') - -const Tooltip = require('./tooltip') - -module.exports = CopyButton - -inherits(CopyButton, Component) -function CopyButton () { - Component.call(this) -} - -// As parameters, accepts: -// "value", which is the value to copy (mandatory) -// "title", which is the text to show on hover (optional, defaults to 'Copy') -CopyButton.prototype.render = function () { - const props = this.props - const state = this.state || {} - - const value = props.value - const copied = state.copied - - const message = copied ? 'Copied' : props.title || ' Copy ' - - return h('.copy-button', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - - h(Tooltip, { - title: message, - }, [ - h('i.fa.fa-clipboard.cursor-pointer.color-orange', { - style: { - margin: '5px', - }, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - copyToClipboard(value) - this.debounceRestore() - }, - }), - ]), - - ]) -} - -CopyButton.prototype.debounceRestore = function () { - this.setState({ copied: true }) - clearTimeout(this.timeout) - this.timeout = setTimeout(() => { - this.setState({ copied: false }) - }, 850) -} diff --git a/ui/app/components/copyable.js b/ui/app/components/copyable.js deleted file mode 100644 index a4f6f4bc6..000000000 --- a/ui/app/components/copyable.js +++ /dev/null @@ -1,46 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const Tooltip = require('./tooltip') -const copyToClipboard = require('copy-to-clipboard') - -module.exports = Copyable - -inherits(Copyable, Component) -function Copyable () { - Component.call(this) - this.state = { - copied: false, - } -} - -Copyable.prototype.render = function () { - const props = this.props - const state = this.state - const { value, children } = props - const { copied } = state - - return h(Tooltip, { - title: copied ? 'Copied!' : 'Copy', - position: 'bottom', - }, h('span', { - style: { - cursor: 'pointer', - }, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - copyToClipboard(value) - this.debounceRestore() - }, - }, children)) -} - -Copyable.prototype.debounceRestore = function () { - this.setState({ copied: true }) - clearTimeout(this.timeout) - this.timeout = setTimeout(() => { - this.setState({ copied: false }) - }, 850) -} diff --git a/ui/app/components/custom-radio-list.js b/ui/app/components/custom-radio-list.js deleted file mode 100644 index a4c525396..000000000 --- a/ui/app/components/custom-radio-list.js +++ /dev/null @@ -1,60 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = RadioList - -inherits(RadioList, Component) -function RadioList () { - Component.call(this) -} - -RadioList.prototype.render = function () { - const props = this.props - const activeClass = '.custom-radio-selected' - const inactiveClass = '.custom-radio-inactive' - const { - labels, - defaultFocus, - } = props - - - return ( - h('.flex-row', { - style: { - fontSize: '12px', - }, - }, [ - h('.flex-column.custom-radios', { - style: { - marginRight: '5px', - }, - }, - labels.map((lable, i) => { - let isSelcted = (this.state !== null) - isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable) - return h(isSelcted ? activeClass : inactiveClass, { - title: lable, - onClick: (event) => { - this.setState({selected: event.target.title}) - props.onClick(event) - }, - }) - }) - ), - h('.text', {}, - labels.map((lable) => { - if (props.subtext) { - return h('.flex-row', {}, [ - h('.radio-titles', lable), - h('.radio-titles-subtext', `- ${props.subtext[lable]}`), - ]) - } else { - return h('.radio-titles', lable) - } - }) - ), - ]) - ) -} - diff --git a/ui/app/components/drop-menu-item.js b/ui/app/components/drop-menu-item.js deleted file mode 100644 index e42948209..000000000 --- a/ui/app/components/drop-menu-item.js +++ /dev/null @@ -1,59 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = DropMenuItem - -inherits(DropMenuItem, Component) -function DropMenuItem () { - Component.call(this) -} - -DropMenuItem.prototype.render = function () { - return h('li.drop-menu-item', { - onClick: () => { - this.props.closeMenu() - this.props.action() - }, - style: { - listStyle: 'none', - padding: '6px 16px 6px 5px', - fontFamily: 'Montserrat Regular', - color: 'rgb(125, 128, 130)', - cursor: 'pointer', - display: 'flex', - justifyContent: 'flex-start', - }, - }, [ - this.props.icon, - this.props.label, - this.activeNetworkRender(), - ]) -} - -DropMenuItem.prototype.activeNetworkRender = function () { - const activeNetwork = this.props.activeNetworkRender - const { provider } = this.props - const providerType = provider ? provider.type : null - if (activeNetwork === undefined) return - - switch (this.props.label) { - case 'Main Ethereum Network': - if (providerType === 'mainnet') return h('.check', '✓') - break - case 'Ropsten Test Network': - if (providerType === 'ropsten') return h('.check', '✓') - break - case 'Kovan Test Network': - if (providerType === 'kovan') return h('.check', '✓') - break - case 'Rinkeby Test Network': - if (providerType === 'rinkeby') return h('.check', '✓') - break - case 'Localhost 8545': - if (activeNetwork === 'http://localhost:8545') return h('.check', '✓') - break - default: - if (activeNetwork === 'custom') return h('.check', '✓') - } -} diff --git a/ui/app/components/editable-label.js b/ui/app/components/editable-label.js deleted file mode 100644 index 41936f5e0..000000000 --- a/ui/app/components/editable-label.js +++ /dev/null @@ -1,51 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const findDOMNode = require('react-dom').findDOMNode - -module.exports = EditableLabel - -inherits(EditableLabel, Component) -function EditableLabel () { - Component.call(this) -} - -EditableLabel.prototype.render = function () { - const props = this.props - const state = this.state - - if (state && state.isEditingLabel) { - return h('div.editable-label', [ - h('input.sizing-input', { - defaultValue: props.textValue, - maxLength: '20', - onKeyPress: (event) => { - this.saveIfEnter(event) - }, - }), - h('button.editable-button', { - onClick: () => this.saveText(), - }, 'Save'), - ]) - } else { - return h('div.name-label', { - onClick: (event) => { - this.setState({ isEditingLabel: true }) - }, - }, this.props.children) - } -} - -EditableLabel.prototype.saveIfEnter = function (event) { - if (event.key === 'Enter') { - this.saveText() - } -} - -EditableLabel.prototype.saveText = function () { - var container = findDOMNode(this) - var text = container.querySelector('.editable-label input').value - var truncatedText = text.substring(0, 20) - this.props.saveText(truncatedText) - this.setState({ isEditingLabel: false, textLabel: truncatedText }) -} diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js deleted file mode 100644 index 3a33ebf74..000000000 --- a/ui/app/components/ens-input.js +++ /dev/null @@ -1,170 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const extend = require('xtend') -const debounce = require('debounce') -const copyToClipboard = require('copy-to-clipboard') -const ENS = require('ethjs-ens') -const networkMap = require('ethjs-ens/lib/network-map.json') -const ensRE = /.+\.eth$/ -const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' - - -module.exports = EnsInput - -inherits(EnsInput, Component) -function EnsInput () { - Component.call(this) -} - -EnsInput.prototype.render = function () { - const props = this.props - const opts = extend(props, { - list: 'addresses', - onChange: () => { - const network = this.props.network - const networkHasEnsSupport = getNetworkEnsSupport(network) - if (!networkHasEnsSupport) return - - const recipient = document.querySelector('input[name="address"]').value - if (recipient.match(ensRE) === null) { - return this.setState({ - loadingEns: false, - ensResolution: null, - ensFailure: null, - }) - } - - this.setState({ - loadingEns: true, - }) - this.checkName() - }, - }) - return h('div', { - style: { width: '100%' }, - }, [ - h('input.large-input', opts), - // The address book functionality. - h('datalist#addresses', - [ - // Corresponds to the addresses owned. - Object.keys(props.identities).map((key) => { - const identity = props.identities[key] - return h('option', { - value: identity.address, - label: identity.name, - key: identity.address, - }) - }), - // Corresponds to previously sent-to addresses. - props.addressBook.map((identity) => { - return h('option', { - value: identity.address, - label: identity.name, - key: identity.address, - }) - }), - ]), - this.ensIcon(), - ]) -} - -EnsInput.prototype.componentDidMount = function () { - const network = this.props.network - const networkHasEnsSupport = getNetworkEnsSupport(network) - this.setState({ ensResolution: ZERO_ADDRESS }) - - if (networkHasEnsSupport) { - const provider = global.ethereumProvider - this.ens = new ENS({ provider, network }) - this.checkName = debounce(this.lookupEnsName.bind(this), 200) - } -} - -EnsInput.prototype.lookupEnsName = function () { - const recipient = document.querySelector('input[name="address"]').value - const { ensResolution } = this.state - - log.info(`ENS attempting to resolve name: ${recipient}`) - this.ens.lookup(recipient.trim()) - .then((address) => { - if (address === ZERO_ADDRESS) throw new Error('No address has been set for this name.') - if (address !== ensResolution) { - this.setState({ - loadingEns: false, - ensResolution: address, - nickname: recipient.trim(), - hoverText: address + '\nClick to Copy', - ensFailure: false, - }) - } - }) - .catch((reason) => { - log.error(reason) - return this.setState({ - loadingEns: false, - ensResolution: ZERO_ADDRESS, - ensFailure: true, - hoverText: reason.message, - }) - }) -} - -EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { - const state = this.state || {} - const ensResolution = state.ensResolution - // If an address is sent without a nickname, meaning not from ENS or from - // the user's own accounts, a default of a one-space string is used. - const nickname = state.nickname || ' ' - if (prevState && ensResolution && this.props.onChange && - ensResolution !== prevState.ensResolution) { - this.props.onChange(ensResolution, nickname) - } -} - -EnsInput.prototype.ensIcon = function (recipient) { - const { hoverText } = this.state || {} - return h('span', { - title: hoverText, - style: { - position: 'absolute', - padding: '9px', - transform: 'translatex(-40px)', - }, - }, this.ensIconContents(recipient)) -} - -EnsInput.prototype.ensIconContents = function (recipient) { - const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: ZERO_ADDRESS} - - if (loadingEns) { - return h('img', { - src: 'images/loading.svg', - style: { - width: '30px', - height: '30px', - transform: 'translateY(-6px)', - }, - }) - } - - if (ensFailure) { - return h('i.fa.fa-warning.fa-lg.warning') - } - - if (ensResolution && (ensResolution !== ZERO_ADDRESS)) { - return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', { - style: { color: 'green' }, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - copyToClipboard(ensResolution) - }, - }) - } -} - -function getNetworkEnsSupport (network) { - return Boolean(networkMap[network]) -} diff --git a/ui/app/components/eth-balance.js b/ui/app/components/eth-balance.js deleted file mode 100644 index 4f538fd31..000000000 --- a/ui/app/components/eth-balance.js +++ /dev/null @@ -1,89 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const formatBalance = require('../util').formatBalance -const generateBalanceObject = require('../util').generateBalanceObject -const Tooltip = require('./tooltip.js') -const FiatValue = require('./fiat-value.js') - -module.exports = EthBalanceComponent - -inherits(EthBalanceComponent, Component) -function EthBalanceComponent () { - Component.call(this) -} - -EthBalanceComponent.prototype.render = function () { - var props = this.props - let { value } = props - const { style, width } = props - var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true - value = value ? formatBalance(value, 6, needsParse) : '...' - - return ( - - h('.ether-balance.ether-balance-amount', { - style, - }, [ - h('div', { - style: { - display: 'inline', - width, - }, - }, this.renderBalance(value)), - ]) - - ) -} -EthBalanceComponent.prototype.renderBalance = function (value) { - var props = this.props - const { conversionRate, shorten, incoming, currentCurrency } = props - if (value === 'None') return value - if (value === '...') return value - var balanceObj = generateBalanceObject(value, shorten ? 1 : 3) - var balance - var splitBalance = value.split(' ') - var ethNumber = splitBalance[0] - var ethSuffix = splitBalance[1] - const showFiat = 'showFiat' in props ? props.showFiat : true - - if (shorten) { - balance = balanceObj.shortBalance - } else { - balance = balanceObj.balance - } - - var label = balanceObj.label - - return ( - h(Tooltip, { - position: 'bottom', - title: `${ethNumber} ${ethSuffix}`, - }, h('div.flex-column', [ - h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('div', { - style: { - width: '100%', - textAlign: 'right', - }, - }, incoming ? `+${balance}` : balance), - h('div', { - style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - }, - }, label), - ]), - - showFiat ? h(FiatValue, { value: props.value, conversionRate, currentCurrency }) : null, - ])) - ) -} diff --git a/ui/app/components/fiat-value.js b/ui/app/components/fiat-value.js deleted file mode 100644 index 8a64a1cfc..000000000 --- a/ui/app/components/fiat-value.js +++ /dev/null @@ -1,63 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const formatBalance = require('../util').formatBalance - -module.exports = FiatValue - -inherits(FiatValue, Component) -function FiatValue () { - Component.call(this) -} - -FiatValue.prototype.render = function () { - const props = this.props - const { conversionRate, currentCurrency } = props - - const value = formatBalance(props.value, 6) - - if (value === 'None') return value - var fiatDisplayNumber, fiatTooltipNumber - var splitBalance = value.split(' ') - - if (conversionRate !== 0) { - fiatTooltipNumber = Number(splitBalance[0]) * conversionRate - fiatDisplayNumber = fiatTooltipNumber.toFixed(2) - } else { - fiatDisplayNumber = 'N/A' - fiatTooltipNumber = 'Unknown' - } - - return fiatDisplay(fiatDisplayNumber, currentCurrency) -} - -function fiatDisplay (fiatDisplayNumber, fiatSuffix) { - if (fiatDisplayNumber !== 'N/A') { - return h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('div', { - style: { - width: '100%', - textAlign: 'right', - fontSize: '12px', - color: '#333333', - }, - }, fiatDisplayNumber), - h('div', { - style: { - color: '#AEAEAE', - marginLeft: '5px', - fontSize: '12px', - }, - }, fiatSuffix), - ]) - } else { - return h('div') - } -} diff --git a/ui/app/components/hex-as-decimal-input.js b/ui/app/components/hex-as-decimal-input.js deleted file mode 100644 index 4a71e9585..000000000 --- a/ui/app/components/hex-as-decimal-input.js +++ /dev/null @@ -1,154 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const extend = require('xtend') - -module.exports = HexAsDecimalInput - -inherits(HexAsDecimalInput, Component) -function HexAsDecimalInput () { - this.state = { invalid: null } - Component.call(this) -} - -/* Hex as Decimal Input - * - * A component for allowing easy, decimal editing - * of a passed in hex string value. - * - * On change, calls back its `onChange` function parameter - * and passes it an updated hex string. - */ - -HexAsDecimalInput.prototype.render = function () { - const props = this.props - const state = this.state - - const { value, onChange, min, max } = props - - const toEth = props.toEth - const suffix = props.suffix - const decimalValue = decimalize(value, toEth) - const style = props.style - - return ( - h('.flex-column', [ - h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('input.hex-input', { - type: 'number', - required: true, - min: min, - max: max, - style: extend({ - display: 'block', - textAlign: 'right', - backgroundColor: 'transparent', - border: '1px solid #bdbdbd', - - }, style), - value: parseInt(decimalValue), - onBlur: (event) => { - this.updateValidity(event) - }, - onChange: (event) => { - this.updateValidity(event) - const hexString = (event.target.value === '') ? '' : hexify(event.target.value) - onChange(hexString) - }, - onInvalid: (event) => { - const msg = this.constructWarning() - if (msg === state.invalid) { - return - } - this.setState({ invalid: msg }) - event.preventDefault() - return false - }, - }), - h('div', { - style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - marginRight: '6px', - width: '20px', - }, - }, suffix), - ]), - - state.invalid ? h('span.error', { - style: { - position: 'absolute', - right: '0px', - textAlign: 'right', - transform: 'translateY(26px)', - padding: '3px', - background: 'rgba(255,255,255,0.85)', - zIndex: '1', - textTransform: 'capitalize', - border: '2px solid #E20202', - }, - }, state.invalid) : null, - ]) - ) -} - -HexAsDecimalInput.prototype.setValid = function (message) { - this.setState({ invalid: null }) -} - -HexAsDecimalInput.prototype.updateValidity = function (event) { - const target = event.target - const value = this.props.value - const newValue = target.value - - if (value === newValue) { - return - } - - const valid = target.checkValidity() - if (valid) { - this.setState({ invalid: null }) - } -} - -HexAsDecimalInput.prototype.constructWarning = function () { - const { name, min, max } = this.props - let message = name ? name + ' ' : '' - - if (min && max) { - message += `must be greater than or equal to ${min} and less than or equal to ${max}.` - } else if (min) { - message += `must be greater than or equal to ${min}.` - } else if (max) { - message += `must be less than or equal to ${max}.` - } else { - message += 'Invalid input.' - } - - return message -} - -function hexify (decimalString) { - const hexBN = new BN(parseInt(decimalString), 10) - return '0x' + hexBN.toString('hex') -} - -function decimalize (input, toEth) { - if (input === '') { - return '' - } else { - const strippedInput = ethUtil.stripHexPrefix(input) - const inputBN = new BN(strippedInput, 'hex') - return inputBN.toString(10) - } -} diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js deleted file mode 100644 index c754bc6ba..000000000 --- a/ui/app/components/identicon.js +++ /dev/null @@ -1,72 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const isNode = require('detect-node') -const findDOMNode = require('react-dom').findDOMNode -const jazzicon = require('jazzicon') -const iconFactoryGen = require('../../lib/icon-factory') -const iconFactory = iconFactoryGen(jazzicon) - -module.exports = IdenticonComponent - -inherits(IdenticonComponent, Component) -function IdenticonComponent () { - Component.call(this) - - this.defaultDiameter = 46 -} - -IdenticonComponent.prototype.render = function () { - var props = this.props - var diameter = props.diameter || this.defaultDiameter - return ( - h('div', { - key: 'identicon-' + this.props.address, - style: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - height: diameter, - width: diameter, - borderRadius: diameter / 2, - overflow: 'hidden', - }, - }) - ) -} - -IdenticonComponent.prototype.componentDidMount = function () { - var props = this.props - const { address } = props - - if (!address) return - - var container = findDOMNode(this) - - var diameter = props.diameter || this.defaultDiameter - if (!isNode) { - var img = iconFactory.iconForAddress(address, diameter) - container.appendChild(img) - } -} - -IdenticonComponent.prototype.componentDidUpdate = function () { - var props = this.props - const { address } = props - - if (!address) return - - var container = findDOMNode(this) - - var children = container.children - for (var i = 0; i < children.length; i++) { - container.removeChild(children[i]) - } - - var diameter = props.diameter || this.defaultDiameter - if (!isNode) { - var img = iconFactory.iconForAddress(address, diameter) - container.appendChild(img) - } -} - diff --git a/ui/app/components/loading.js b/ui/app/components/loading.js deleted file mode 100644 index 87d6f5d20..000000000 --- a/ui/app/components/loading.js +++ /dev/null @@ -1,53 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') - - -inherits(LoadingIndicator, Component) -module.exports = LoadingIndicator - -function LoadingIndicator () { - Component.call(this) -} - -LoadingIndicator.prototype.render = function () { - const { isLoading, loadingMessage } = this.props - - return ( - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'loader', - transitionEnterTimeout: 150, - transitionLeaveTimeout: 150, - }, [ - - isLoading ? h('div', { - style: { - zIndex: 10, - position: 'absolute', - flexDirection: 'column', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - height: '100%', - width: '100%', - background: 'rgba(255, 255, 255, 0.8)', - }, - }, [ - h('img', { - src: 'images/loading.svg', - }), - - h('br'), - - showMessageIfAny(loadingMessage), - ]) : null, - ]) - ) -} - -function showMessageIfAny (loadingMessage) { - if (!loadingMessage) return null - return h('span', loadingMessage) -} diff --git a/ui/app/components/mascot.js b/ui/app/components/mascot.js deleted file mode 100644 index 973ec2cad..000000000 --- a/ui/app/components/mascot.js +++ /dev/null @@ -1,59 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const metamaskLogo = require('metamask-logo') -const debounce = require('debounce') - -module.exports = Mascot - -inherits(Mascot, Component) -function Mascot () { - Component.call(this) - this.logo = metamaskLogo({ - followMouse: true, - pxNotRatio: true, - width: 200, - height: 200, - }) - - this.refollowMouse = debounce(this.logo.setFollowMouse.bind(this.logo, true), 1000) - this.unfollowMouse = this.logo.setFollowMouse.bind(this.logo, false) -} - -Mascot.prototype.render = function () { - // this is a bit hacky - // the event emitter is on `this.props` - // and we dont get that until render - this.handleAnimationEvents() - - return h('#metamask-mascot-container', { - style: { zIndex: 0 }, - }) -} - -Mascot.prototype.componentDidMount = function () { - var targetDivId = 'metamask-mascot-container' - var container = document.getElementById(targetDivId) - container.appendChild(this.logo.container) -} - -Mascot.prototype.componentWillUnmount = function () { - this.animations = this.props.animationEventEmitter - this.animations.removeAllListeners() - this.logo.container.remove() - this.logo.stopAnimation() -} - -Mascot.prototype.handleAnimationEvents = function () { - // only setup listeners once - if (this.animations) return - this.animations = this.props.animationEventEmitter - this.animations.on('point', this.lookAt.bind(this)) - this.animations.on('setFollowMouse', this.logo.setFollowMouse.bind(this.logo)) -} - -Mascot.prototype.lookAt = function (target) { - this.unfollowMouse() - this.logo.lookAt(target) - this.refollowMouse() -} diff --git a/ui/app/components/mini-account-panel.js b/ui/app/components/mini-account-panel.js deleted file mode 100644 index c09cf5b7a..000000000 --- a/ui/app/components/mini-account-panel.js +++ /dev/null @@ -1,74 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const Identicon = require('./identicon') - -module.exports = AccountPanel - - -inherits(AccountPanel, Component) -function AccountPanel () { - Component.call(this) -} - -AccountPanel.prototype.render = function () { - var props = this.props - var picOrder = props.picOrder || 'left' - const { imageSeed } = props - - return ( - - h('.identity-panel.flex-row.flex-left', { - style: { - cursor: props.onClick ? 'pointer' : undefined, - }, - onClick: props.onClick, - }, [ - - this.genIcon(imageSeed, picOrder), - - h('div.flex-column.flex-justify-center', { - style: { - lineHeight: '15px', - order: 2, - display: 'flex', - alignItems: picOrder === 'left' ? 'flex-begin' : 'flex-end', - }, - }, this.props.children), - ]) - ) -} - -AccountPanel.prototype.genIcon = function (seed, picOrder) { - const props = this.props - - // When there is no seed value, this is a contract creation. - // We then show the contract icon. - if (!seed) { - return h('.identicon-wrapper.flex-column.select-none', { - style: { - order: picOrder === 'left' ? 1 : 3, - }, - }, [ - h('i.fa.fa-file-text-o.fa-lg', { - style: { - fontSize: '42px', - transform: 'translate(0px, -16px)', - }, - }), - ]) - } - - // If there was a seed, we return an identicon for that address. - return h('.identicon-wrapper.flex-column.select-none', { - style: { - order: picOrder === 'left' ? 1 : 3, - }, - }, [ - h(Identicon, { - address: seed, - imageify: props.imageifyIdenticons, - }), - ]) -} - diff --git a/ui/app/components/network.js b/ui/app/components/network.js deleted file mode 100644 index d5d3e18cd..000000000 --- a/ui/app/components/network.js +++ /dev/null @@ -1,125 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = Network - -inherits(Network, Component) - -function Network () { - Component.call(this) -} - -Network.prototype.render = function () { - const props = this.props - const networkNumber = props.network - let providerName - try { - providerName = props.provider.type - } catch (e) { - providerName = null - } - let iconName, hoverText - - if (networkNumber === 'loading') { - return h('span', { - style: { - display: 'flex', - alignItems: 'center', - flexDirection: 'row', - }, - onClick: (event) => this.props.onClick(event), - }, [ - h('img', { - title: 'Attempting to connect to blockchain.', - style: { - width: '27px', - }, - src: 'images/loading.svg', - }), - h('i.fa.fa-sort-desc'), - ]) - - } else if (providerName === 'mainnet') { - hoverText = 'Main Ethereum Network' - iconName = 'ethereum-network' - } else if (providerName === 'ropsten') { - hoverText = 'Ropsten Test Network' - iconName = 'ropsten-test-network' - } else if (parseInt(networkNumber) === 3) { - hoverText = 'Ropsten Test Network' - iconName = 'ropsten-test-network' - } else if (providerName === 'kovan') { - hoverText = 'Kovan Test Network' - iconName = 'kovan-test-network' - } else if (providerName === 'rinkeby') { - hoverText = 'Rinkeby Test Network' - iconName = 'rinkeby-test-network' - } else { - hoverText = 'Unknown Private Network' - iconName = 'unknown-private-network' - } - - return ( - h('#network_component.pointer', { - title: hoverText, - onClick: (event) => this.props.onClick(event), - }, [ - (function () { - switch (iconName) { - case 'ethereum-network': - return h('.network-indicator', [ - h('.menu-icon.diamond'), - h('.network-name', { - style: { - color: '#039396', - }}, - 'Ethereum Main Net'), - ]) - case 'ropsten-test-network': - return h('.network-indicator', [ - h('.menu-icon.red-dot'), - h('.network-name', { - style: { - color: '#ff6666', - }}, - 'Ropsten Test Net'), - ]) - case 'kovan-test-network': - return h('.network-indicator', [ - h('.menu-icon.hollow-diamond'), - h('.network-name', { - style: { - color: '#690496', - }}, - 'Kovan Test Net'), - ]) - case 'rinkeby-test-network': - return h('.network-indicator', [ - h('.menu-icon.golden-square'), - h('.network-name', { - style: { - color: '#e7a218', - }}, - 'Rinkeby Test Net'), - ]) - default: - return h('.network-indicator', [ - h('i.fa.fa-question-circle.fa-lg', { - style: { - margin: '10px', - color: 'rgb(125, 128, 130)', - }, - }), - - h('.network-name', { - style: { - color: '#AEAEAE', - }}, - 'Private Network'), - ]) - } - })(), - ]) - ) -} diff --git a/ui/app/components/notice.js b/ui/app/components/notice.js deleted file mode 100644 index d9f0067cd..000000000 --- a/ui/app/components/notice.js +++ /dev/null @@ -1,126 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const ReactMarkdown = require('react-markdown') -const linker = require('extension-link-enabler') -const findDOMNode = require('react-dom').findDOMNode - -module.exports = Notice - -inherits(Notice, Component) -function Notice () { - Component.call(this) -} - -Notice.prototype.render = function () { - const { notice, onConfirm } = this.props - const { title, date, body } = notice - const state = this.state || { disclaimerDisabled: true } - const disabled = state.disclaimerDisabled - - return ( - h('.flex-column.flex-center.flex-grow', [ - h('h3.flex-center.text-transform-uppercase.terms-header', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - width: '100%', - fontSize: '20px', - textAlign: 'center', - padding: 6, - }, - }, [ - title, - ]), - - h('h5.flex-center.text-transform-uppercase.terms-header', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginBottom: 24, - width: '100%', - fontSize: '20px', - textAlign: 'center', - padding: 6, - }, - }, [ - date, - ]), - - h('style', ` - - .markdown { - overflow-x: hidden; - } - - .markdown h1, .markdown h2, .markdown h3 { - margin: 10px 0; - font-weight: bold; - } - - .markdown strong { - font-weight: bold; - } - .markdown em { - font-style: italic; - } - - .markdown p { - margin: 10px 0; - } - - .markdown a { - color: #df6b0e; - } - - `), - - h('div.markdown', { - onScroll: (e) => { - var object = e.currentTarget - if (object.offsetHeight + object.scrollTop + 100 >= object.scrollHeight) { - this.setState({disclaimerDisabled: false}) - } - }, - style: { - background: 'rgb(235, 235, 235)', - height: '310px', - padding: '6px', - width: '90%', - overflowY: 'scroll', - scroll: 'auto', - }, - }, [ - h(ReactMarkdown, { - className: 'notice-box', - source: body, - skipHtml: true, - }), - ]), - - h('button', { - disabled, - onClick: () => { - this.setState({disclaimerDisabled: true}) - onConfirm() - }, - style: { - marginTop: '18px', - }, - }, 'Accept'), - ]) - ) -} - -Notice.prototype.componentDidMount = function () { - var node = findDOMNode(this) - linker.setupListener(node) - if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) { - this.setState({disclaimerDisabled: false}) - } -} - -Notice.prototype.componentWillUnmount = function () { - var node = findDOMNode(this) - linker.teardownListener(node) -} diff --git a/ui/app/components/pending-msg-details.js b/ui/app/components/pending-msg-details.js deleted file mode 100644 index 16308d121..000000000 --- a/ui/app/components/pending-msg-details.js +++ /dev/null @@ -1,50 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const AccountPanel = require('./account-panel') - -module.exports = PendingMsgDetails - -inherits(PendingMsgDetails, Component) -function PendingMsgDetails () { - Component.call(this) -} - -PendingMsgDetails.prototype.render = function () { - var state = this.props - var msgData = state.txData - - var msgParams = msgData.msgParams || {} - var address = msgParams.from || state.selectedAddress - var identity = state.identities[address] || { address: address } - var account = state.accounts[address] || { address: address } - - return ( - h('div', { - key: msgData.id, - style: { - margin: '10px 20px', - }, - }, [ - - // account that will sign - h(AccountPanel, { - showFullAddress: true, - identity: identity, - account: account, - imageifyIdenticons: state.imageifyIdenticons, - }), - - // message data - h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-row.flex-space-between', [ - h('label.font-small', 'MESSAGE'), - h('span.font-small', msgParams.data), - ]), - ]), - - ]) - ) -} - diff --git a/ui/app/components/pending-msg.js b/ui/app/components/pending-msg.js deleted file mode 100644 index b2cac164a..000000000 --- a/ui/app/components/pending-msg.js +++ /dev/null @@ -1,56 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const PendingTxDetails = require('./pending-msg-details') - -module.exports = PendingMsg - -inherits(PendingMsg, Component) -function PendingMsg () { - Component.call(this) -} - -PendingMsg.prototype.render = function () { - var state = this.props - var msgData = state.txData - - return ( - - h('div', { - key: msgData.id, - }, [ - - // header - h('h3', { - style: { - fontWeight: 'bold', - textAlign: 'center', - }, - }, 'Sign Message'), - - h('.error', { - style: { - margin: '10px', - }, - }, `Signing this message can have - dangerous side effects. Only sign messages from - sites you fully trust with your entire account. - This will be fixed in a future version.`), - - // message details - h(PendingTxDetails, state), - - // sign + cancel - h('.flex-row.flex-space-around', [ - h('button', { - onClick: state.cancelMessage, - }, 'Cancel'), - h('button', { - onClick: state.signMessage, - }, 'Sign'), - ]), - ]) - - ) -} - diff --git a/ui/app/components/pending-personal-msg-details.js b/ui/app/components/pending-personal-msg-details.js deleted file mode 100644 index 1050513f2..000000000 --- a/ui/app/components/pending-personal-msg-details.js +++ /dev/null @@ -1,60 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const AccountPanel = require('./account-panel') -const BinaryRenderer = require('./binary-renderer') - -module.exports = PendingMsgDetails - -inherits(PendingMsgDetails, Component) -function PendingMsgDetails () { - Component.call(this) -} - -PendingMsgDetails.prototype.render = function () { - var state = this.props - var msgData = state.txData - - var msgParams = msgData.msgParams || {} - var address = msgParams.from || state.selectedAddress - var identity = state.identities[address] || { address: address } - var account = state.accounts[address] || { address: address } - - var { data } = msgParams - - return ( - h('div', { - key: msgData.id, - style: { - margin: '10px 20px', - }, - }, [ - - // account that will sign - h(AccountPanel, { - showFullAddress: true, - identity: identity, - account: account, - imageifyIdenticons: state.imageifyIdenticons, - }), - - // message data - h('div', { - style: { - height: '260px', - }, - }, [ - h('label.font-small', { style: { display: 'block' } }, 'MESSAGE'), - h(BinaryRenderer, { - value: data, - style: { - height: '215px', - }, - }), - ]), - - ]) - ) -} - diff --git a/ui/app/components/pending-personal-msg.js b/ui/app/components/pending-personal-msg.js deleted file mode 100644 index 4542adb28..000000000 --- a/ui/app/components/pending-personal-msg.js +++ /dev/null @@ -1,47 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const PendingTxDetails = require('./pending-personal-msg-details') - -module.exports = PendingMsg - -inherits(PendingMsg, Component) -function PendingMsg () { - Component.call(this) -} - -PendingMsg.prototype.render = function () { - var state = this.props - var msgData = state.txData - - return ( - - h('div', { - key: msgData.id, - }, [ - - // header - h('h3', { - style: { - fontWeight: 'bold', - textAlign: 'center', - }, - }, 'Sign Message'), - - // message details - h(PendingTxDetails, state), - - // sign + cancel - h('.flex-row.flex-space-around', [ - h('button', { - onClick: state.cancelPersonalMessage, - }, 'Cancel'), - h('button', { - onClick: state.signPersonalMessage, - }, 'Sign'), - ]), - ]) - - ) -} - diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js deleted file mode 100644 index d7d602f31..000000000 --- a/ui/app/components/pending-tx.js +++ /dev/null @@ -1,480 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const actions = require('../actions') -const clone = require('clone') - -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const hexToBn = require('../../../app/scripts/lib/hex-to-bn') -const util = require('../util') -const MiniAccountPanel = require('./mini-account-panel') -const Copyable = require('./copyable') -const EthBalance = require('./eth-balance') -const addressSummary = util.addressSummary -const nameForAddress = require('../../lib/contract-namer') -const BNInput = require('./bn-as-decimal-input') - -const MIN_GAS_PRICE_GWEI_BN = new BN(2) -const GWEI_FACTOR = new BN(1e9) -const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) -const MIN_GAS_LIMIT_BN = new BN(21000) - -module.exports = PendingTx -inherits(PendingTx, Component) -function PendingTx () { - Component.call(this) - this.state = { - valid: true, - txData: null, - submitting: false, - } -} - -PendingTx.prototype.render = function () { - const props = this.props - const { currentCurrency, blockGasLimit } = props - - const conversionRate = props.conversionRate - const txMeta = this.gatherTxMeta() - const txParams = txMeta.txParams || {} - - // Account Details - const address = txParams.from || props.selectedAddress - const identity = props.identities[address] || { address: address } - const account = props.accounts[address] - const balance = account ? account.balance : '0x0' - - // recipient check - const isValidAddress = !txParams.to || util.isValidAddress(txParams.to) - - // Gas - const gas = txParams.gas - const gasBn = hexToBn(gas) - const gasLimit = new BN(parseInt(blockGasLimit)) - const safeGasLimit = this.bnMultiplyByFraction(gasLimit, 19, 20).toString(10) - - // Gas Price - const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) - const gasPriceBn = hexToBn(gasPrice) - - const txFeeBn = gasBn.mul(gasPriceBn) - const valueBn = hexToBn(txParams.value) - const maxCost = txFeeBn.add(valueBn) - - const dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0 - - const balanceBn = hexToBn(balance) - const insufficientBalance = balanceBn.lt(maxCost) - - this.inputs = [] - - return ( - - h('div', { - key: txMeta.id, - }, [ - - h('form#pending-tx-form', { - onSubmit: this.onSubmit.bind(this), - - }, [ - - // tx info - h('div', [ - - h('.flex-row.flex-center', { - style: { - maxWidth: '100%', - }, - }, [ - - h(MiniAccountPanel, { - imageSeed: address, - picOrder: 'right', - }, [ - h('span.font-small', { - style: { - fontFamily: 'Montserrat Bold, Montserrat, sans-serif', - }, - }, identity.name), - - h(Copyable, { - value: ethUtil.toChecksumAddress(address), - }, [ - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, addressSummary(address, 6, 4, false)), - ]), - - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, [ - h(EthBalance, { - value: balance, - conversionRate, - currentCurrency, - inline: true, - labelColor: '#F7861C', - }), - ]), - ]), - - forwardCarrat(), - - this.miniAccountPanelForRecipient(), - ]), - - h('style', ` - .table-box { - margin: 7px 0px 0px 0px; - width: 100%; - } - .table-box .row { - margin: 0px; - background: rgb(236,236,236); - display: flex; - justify-content: space-between; - font-family: Montserrat Light, sans-serif; - font-size: 13px; - padding: 5px 25px; - } - .table-box .row .value { - font-family: Montserrat Regular; - } - `), - - h('.table-box', [ - - // Ether Value - // Currently not customizable, but easily modified - // in the way that gas and gasLimit currently are. - h('.row', [ - h('.cell.label', 'Amount'), - h(EthBalance, { value: txParams.value, currentCurrency, conversionRate }), - ]), - - // Gas Limit (customizable) - h('.cell.row', [ - h('.cell.label', 'Gas Limit'), - h('.cell.value', { - }, [ - h(BNInput, { - name: 'Gas Limit', - value: gasBn, - precision: 0, - scale: 0, - // The hard lower limit for gas. - min: MIN_GAS_LIMIT_BN.toString(10), - max: safeGasLimit, - suffix: 'UNITS', - style: { - position: 'relative', - top: '5px', - }, - onChange: this.gasLimitChanged.bind(this), - - ref: (hexInput) => { this.inputs.push(hexInput) }, - }), - ]), - ]), - - // Gas Price (customizable) - h('.cell.row', [ - h('.cell.label', 'Gas Price'), - h('.cell.value', { - }, [ - h(BNInput, { - name: 'Gas Price', - value: gasPriceBn, - precision: 9, - scale: 9, - suffix: 'GWEI', - min: MIN_GAS_PRICE_GWEI_BN.toString(10), - style: { - position: 'relative', - top: '5px', - }, - onChange: this.gasPriceChanged.bind(this), - ref: (hexInput) => { this.inputs.push(hexInput) }, - }), - ]), - ]), - - // Max Transaction Fee (calculated) - h('.cell.row', [ - h('.cell.label', 'Max Transaction Fee'), - h(EthBalance, { value: txFeeBn.toString(16), currentCurrency, conversionRate }), - ]), - - h('.cell.row', { - style: { - fontFamily: 'Montserrat Regular', - background: 'white', - padding: '10px 25px', - }, - }, [ - h('.cell.label', 'Max Total'), - h('.cell.value', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - h(EthBalance, { - value: maxCost.toString(16), - currentCurrency, - conversionRate, - inline: true, - labelColor: 'black', - fontSize: '16px', - }), - ]), - ]), - - // Data size row: - h('.cell.row', { - style: { - background: '#f7f7f7', - paddingBottom: '0px', - }, - }, [ - h('.cell.label'), - h('.cell.value', { - style: { - fontFamily: 'Montserrat Light', - fontSize: '11px', - }, - }, `Data included: ${dataLength} bytes`), - ]), - ]), // End of Table - - ]), - - h('style', ` - .conf-buttons button { - margin-left: 10px; - text-transform: uppercase; - } - `), - - txMeta.simulationFails ? - h('.error', { - style: { - marginLeft: 50, - fontSize: '0.9em', - }, - }, 'Transaction Error. Exception thrown in contract code.') - : null, - - !isValidAddress ? - h('.error', { - style: { - marginLeft: 50, - fontSize: '0.9em', - }, - }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.') - : null, - - insufficientBalance ? - h('span.error', { - style: { - marginLeft: 50, - fontSize: '0.9em', - }, - }, 'Insufficient balance for transaction') - : null, - - // send + cancel - h('.flex-row.flex-space-around.conf-buttons', { - style: { - display: 'flex', - justifyContent: 'flex-end', - margin: '14px 25px', - }, - }, [ - - - insufficientBalance ? - h('button.btn-green', { - onClick: props.buyEth, - }, 'Buy Ether') - : null, - - h('button', { - onClick: (event) => { - this.resetGasFields() - event.preventDefault() - }, - }, 'Reset'), - - // Accept Button - h('input.confirm.btn-green', { - type: 'submit', - value: 'SUBMIT', - style: { marginLeft: '10px' }, - disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, - }), - - h('button.cancel.btn-red', { - onClick: props.cancelTransaction, - }, 'Reject'), - ]), - ]), - ]) - ) -} - -PendingTx.prototype.miniAccountPanelForRecipient = function () { - const props = this.props - const txData = props.txData - const txParams = txData.txParams || {} - const isContractDeploy = !('to' in txParams) - - // If it's not a contract deploy, send to the account - if (!isContractDeploy) { - return h(MiniAccountPanel, { - imageSeed: txParams.to, - picOrder: 'left', - }, [ - - h('span.font-small', { - style: { - fontFamily: 'Montserrat Bold, Montserrat, sans-serif', - }, - }, nameForAddress(txParams.to, props.identities)), - - h(Copyable, { - value: ethUtil.toChecksumAddress(txParams.to), - }, [ - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, addressSummary(txParams.to, 6, 4, false)), - ]), - - ]) - } else { - return h(MiniAccountPanel, { - picOrder: 'left', - }, [ - - h('span.font-small', { - style: { - fontFamily: 'Montserrat Bold, Montserrat, sans-serif', - }, - }, 'New Contract'), - - ]) - } -} - -PendingTx.prototype.gasPriceChanged = function (newBN, valid) { - log.info(`Gas price changed to: ${newBN.toString(10)}`) - const txMeta = this.gatherTxMeta() - txMeta.txParams.gasPrice = '0x' + newBN.toString('hex') - this.setState({ - txData: clone(txMeta), - valid, - }) -} - -PendingTx.prototype.gasLimitChanged = function (newBN, valid) { - log.info(`Gas limit changed to ${newBN.toString(10)}`) - const txMeta = this.gatherTxMeta() - txMeta.txParams.gas = '0x' + newBN.toString('hex') - this.setState({ - txData: clone(txMeta), - valid, - }) -} - -PendingTx.prototype.resetGasFields = function () { - log.debug(`pending-tx resetGasFields`) - - this.inputs.forEach((hexInput) => { - if (hexInput) { - hexInput.setValid() - } - }) - - this.setState({ - txData: null, - valid: true, - }) -} - -PendingTx.prototype.onSubmit = function (event) { - event.preventDefault() - const txMeta = this.gatherTxMeta() - const valid = this.checkValidity() - this.setState({ valid, submitting: true }) - if (valid && this.verifyGasParams()) { - this.props.sendTransaction(txMeta, event) - } else { - this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) - this.setState({ submitting: false }) - } -} - -PendingTx.prototype.checkValidity = function () { - const form = this.getFormEl() - const valid = form.checkValidity() - return valid -} - -PendingTx.prototype.getFormEl = function () { - const form = document.querySelector('form#pending-tx-form') - // Stub out form for unit tests: - if (!form) { - return { checkValidity () { return true } } - } - return form -} - -// After a customizable state value has been updated, -PendingTx.prototype.gatherTxMeta = function () { - log.debug(`pending-tx gatherTxMeta`) - const props = this.props - const state = this.state - const txData = clone(state.txData) || clone(props.txData) - - log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) - return txData -} - -PendingTx.prototype.verifyGasParams = function () { - // We call this in case the gas has not been modified at all - if (!this.state) { return true } - return ( - this._notZeroOrEmptyString(this.state.gas) && - this._notZeroOrEmptyString(this.state.gasPrice) - ) -} - -PendingTx.prototype._notZeroOrEmptyString = function (obj) { - return obj !== '' && obj !== '0x0' -} - -PendingTx.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) { - const numBN = new BN(numerator) - const denomBN = new BN(denominator) - return targetBN.mul(numBN).div(denomBN) -} - -function forwardCarrat () { - return ( - h('img', { - src: 'images/forward-carrat.svg', - style: { - padding: '5px 6px 0px 10px', - height: '37px', - }, - }) - ) -} diff --git a/ui/app/components/qr-code.js b/ui/app/components/qr-code.js deleted file mode 100644 index 06b9aed9b..000000000 --- a/ui/app/components/qr-code.js +++ /dev/null @@ -1,79 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const qrCode = require('qrcode-npm').qrcode -const inherits = require('util').inherits -const connect = require('react-redux').connect -const isHexPrefixed = require('ethereumjs-util').isHexPrefixed -const CopyButton = require('./copyButton') - -module.exports = connect(mapStateToProps)(QrCodeView) - -function mapStateToProps (state) { - return { - Qr: state.appState.Qr, - buyView: state.appState.buyView, - warning: state.appState.warning, - } -} - -inherits(QrCodeView, Component) - -function QrCodeView () { - Component.call(this) -} - -QrCodeView.prototype.render = function () { - const props = this.props - const Qr = props.Qr - const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}` - const qrImage = qrCode(4, 'M') - qrImage.addData(address) - qrImage.make() - return h('.main-container.flex-column', { - key: 'qr', - style: { - justifyContent: 'center', - paddingBottom: '45px', - paddingLeft: '45px', - paddingRight: '45px', - alignItems: 'center', - }, - }, [ - Array.isArray(Qr.message) ? h('.message-container', this.renderMultiMessage()) : h('.qr-header', Qr.message), - - this.props.warning ? this.props.warning && h('span.error.flex-center', { - style: { - textAlign: 'center', - width: '229px', - height: '82px', - }, - }, - this.props.warning) : null, - - h('#qr-container.flex-column', { - style: { - marginTop: '25px', - marginBottom: '15px', - }, - dangerouslySetInnerHTML: { - __html: qrImage.createTableTag(4), - }, - }), - h('.flex-row', [ - h('h3.ellip-address', { - style: { - width: '247px', - }, - }, Qr.data), - h(CopyButton, { - value: Qr.data, - }), - ]), - ]) -} - -QrCodeView.prototype.renderMultiMessage = function () { - var Qr = this.props.Qr - var multiMessage = Qr.message.map((message) => h('.qr-message', message)) - return multiMessage -} diff --git a/ui/app/components/range-slider.js b/ui/app/components/range-slider.js deleted file mode 100644 index 823f5eb01..000000000 --- a/ui/app/components/range-slider.js +++ /dev/null @@ -1,58 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = RangeSlider - -inherits(RangeSlider, Component) -function RangeSlider () { - Component.call(this) -} - -RangeSlider.prototype.render = function () { - const state = this.state || {} - const props = this.props - const onInput = props.onInput || function () {} - const name = props.name - const { - min = 0, - max = 100, - increment = 1, - defaultValue = 50, - mirrorInput = false, - } = this.props.options - const {container, input, range} = props.style - - return ( - h('.flex-row', { - style: container, - }, [ - h('input', { - type: 'range', - name: name, - min: min, - max: max, - step: increment, - style: range, - value: state.value || defaultValue, - onChange: mirrorInput ? this.mirrorInputs.bind(this, event) : onInput, - }), - - // Mirrored input for range - mirrorInput ? h('input.large-input', { - type: 'number', - name: `${name}Mirror`, - min: min, - max: max, - value: state.value || defaultValue, - step: increment, - style: input, - onChange: this.mirrorInputs.bind(this, event), - }) : null, - ]) - ) -} - -RangeSlider.prototype.mirrorInputs = function (event) { - this.setState({value: event.target.value}) -} diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js deleted file mode 100644 index e0a720426..000000000 --- a/ui/app/components/shapeshift-form.js +++ /dev/null @@ -1,306 +0,0 @@ -const PersistentForm = require('../../lib/persistent-form') -const h = require('react-hyperscript') -const inherits = require('util').inherits -const connect = require('react-redux').connect -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') -const actions = require('../actions') -const Qr = require('./qr-code') -const isValidAddress = require('../util').isValidAddress -module.exports = connect(mapStateToProps)(ShapeshiftForm) - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - isSubLoading: state.appState.isSubLoading, - qrRequested: state.appState.qrRequested, - } -} - -inherits(ShapeshiftForm, PersistentForm) - -function ShapeshiftForm () { - PersistentForm.call(this) - this.persistentFormParentId = 'shapeshift-buy-form' -} - -ShapeshiftForm.prototype.render = function () { - return h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain(), - ]) -} - -ShapeshiftForm.prototype.renderMain = function () { - const marketinfo = this.props.buyView.formView.marketinfo - const coinOptions = this.props.buyView.formView.coinOptions - var coin = marketinfo.pair.split('_')[0].toUpperCase() - - return h('.flex-column', { - style: { - // marginTop: '10px', - padding: '25px', - paddingTop: '5px', - width: '100%', - minHeight: '215px', - alignItems: 'center', - overflowY: 'auto', - }, - }, [ - h('.flex-row', { - style: { - justifyContent: 'center', - alignItems: 'baseline', - height: '42px', - }, - }, [ - h('img', { - src: coinOptions[coin].image, - width: '25px', - height: '25px', - style: { - marginRight: '5px', - }, - }), - - h('.input-container', [ - h('input#fromCoin.buy-inputs.ex-coins', { - type: 'text', - list: 'coinList', - autoFocus: true, - dataset: { - persistentFormId: 'input-coin', - }, - style: { - boxSizing: 'border-box', - }, - onChange: this.handleLiveInput.bind(this), - defaultValue: 'BTC', - }), - - this.renderCoinList(), - - h('i.fa.fa-pencil-square-o.edit-text', { - style: { - fontSize: '12px', - color: '#F7861C', - position: 'relative', - bottom: '48px', - left: '106px', - }, - }), - ]), - - h('.icon-control', [ - h('i.fa.fa-refresh.fa-4.orange', { - style: { - bottom: '5px', - left: '5px', - color: '#F7861C', - }, - onClick: this.updateCoin.bind(this), - }), - h('i.fa.fa-chevron-right.fa-4.orange', { - style: { - position: 'relative', - bottom: '26px', - left: '10px', - color: '#F7861C', - }, - onClick: this.updateCoin.bind(this), - }), - ]), - - h('#toCoin.ex-coins', marketinfo.pair.split('_')[1].toUpperCase()), - - h('img', { - src: coinOptions[marketinfo.pair.split('_')[1].toUpperCase()].image, - width: '25px', - height: '25px', - style: { - marginLeft: '5px', - }, - }), - ]), - h('.flex-column', { - style: { - alignItems: 'flex-start', - }, - }, [ - this.props.warning ? this.props.warning && h('span.error.flex-center', { - style: { - textAlign: 'center', - width: '229px', - height: '82px', - }, - }, - this.props.warning) : this.renderInfo(), - ]), - - h(this.activeToggle('.input-container'), { - style: { - padding: '10px', - paddingTop: '0px', - width: '100%', - }, - }, [ - - h('div', `${coin} Address:`), - - h('input#fromCoinAddress.buy-inputs', { - type: 'text', - placeholder: `Your ${coin} Refund Address`, - dataset: { - persistentFormId: 'refund-address', - }, - style: { - boxSizing: 'border-box', - width: '227px', - height: '30px', - padding: ' 5px ', - }, - }), - - h('i.fa.fa-pencil-square-o.edit-text', { - style: { - fontSize: '12px', - color: '#F7861C', - position: 'relative', - bottom: '10px', - right: '11px', - }, - }), - h('.flex-row', { - style: { - justifyContent: 'flex-end', - }, - }, [ - h('button', { - onClick: this.shift.bind(this), - style: { - marginTop: '10px', - position: 'relative', - bottom: '40px', - }, - }, - 'Submit'), - ]), - ]), - ]) -} - -ShapeshiftForm.prototype.shift = function () { - var props = this.props - var withdrawal = this.props.buyView.buyAddress - var returnAddress = document.getElementById('fromCoinAddress').value - var pair = this.props.buyView.formView.marketinfo.pair - var data = { - 'withdrawal': withdrawal, - 'pair': pair, - 'returnAddress': returnAddress, - // Public api key - 'apiKey': '803d1f5df2ed1b1476e4b9e6bcd089e34d8874595dda6a23b67d93c56ea9cc2445e98a6748b219b2b6ad654d9f075f1f1db139abfa93158c04e825db122c14b6', - } - var message = [ - `Deposit Limit: ${props.buyView.formView.marketinfo.limit}`, - `Deposit Minimum:${props.buyView.formView.marketinfo.minimum}`, - ] - if (isValidAddress(withdrawal)) { - this.props.dispatch(actions.coinShiftRquest(data, message)) - } -} - -ShapeshiftForm.prototype.renderCoinList = function () { - var list = Object.keys(this.props.buyView.formView.coinOptions).map((item) => { - return h('option', { - value: item, - }, item) - }) - - return h('datalist#coinList', { - onClick: (event) => { - event.preventDefault() - }, - }, list) -} - -ShapeshiftForm.prototype.updateCoin = function (event) { - event.preventDefault() - const props = this.props - var coinOptions = this.props.buyView.formView.coinOptions - var coin = document.getElementById('fromCoin').value - - if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { - var message = 'Not a valid coin' - return props.dispatch(actions.displayWarning(message)) - } else { - return props.dispatch(actions.pairUpdate(coin)) - } -} - -ShapeshiftForm.prototype.handleLiveInput = function () { - const props = this.props - var coinOptions = this.props.buyView.formView.coinOptions - var coin = document.getElementById('fromCoin').value - - if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { - return null - } else { - return props.dispatch(actions.pairUpdate(coin)) - } -} - -ShapeshiftForm.prototype.renderInfo = function () { - const marketinfo = this.props.buyView.formView.marketinfo - const coinOptions = this.props.buyView.formView.coinOptions - var coin = marketinfo.pair.split('_')[0].toUpperCase() - - return h('span', { - style: { - }, - }, [ - h('h3.flex-row.text-transform-uppercase', { - style: { - color: '#868686', - paddingTop: '4px', - justifyContent: 'space-around', - textAlign: 'center', - fontSize: '17px', - }, - }, `Market Info for ${marketinfo.pair.replace('_', ' to ').toUpperCase()}:`), - h('.marketinfo', ['Status : ', `${coinOptions[coin].status}`]), - h('.marketinfo', ['Exchange Rate: ', `${marketinfo.rate}`]), - h('.marketinfo', ['Limit: ', `${marketinfo.limit}`]), - h('.marketinfo', ['Minimum : ', `${marketinfo.minimum}`]), - ]) -} - -ShapeshiftForm.prototype.activeToggle = function (elementType) { - if (!this.props.buyView.formView.response || this.props.warning) return elementType - return `${elementType}.inactive` -} - -ShapeshiftForm.prototype.renderLoading = function () { - return h('span', { - style: { - position: 'absolute', - left: '70px', - bottom: '194px', - background: 'transparent', - width: '229px', - height: '82px', - display: 'flex', - justifyContent: 'center', - }, - }, [ - h('img', { - style: { - width: '60px', - }, - src: 'images/loading.svg', - }), - ]) -} diff --git a/ui/app/components/shift-list-item.js b/ui/app/components/shift-list-item.js deleted file mode 100644 index 32bfbeda4..000000000 --- a/ui/app/components/shift-list-item.js +++ /dev/null @@ -1,204 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const vreme = new (require('vreme')) -const explorerLink = require('../../lib/explorer-link') -const actions = require('../actions') -const addressSummary = require('../util').addressSummary - -const CopyButton = require('./copyButton') -const EthBalance = require('./eth-balance') -const Tooltip = require('./tooltip') - - -module.exports = connect(mapStateToProps)(ShiftListItem) - -function mapStateToProps (state) { - return { - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - } -} - -inherits(ShiftListItem, Component) - -function ShiftListItem () { - Component.call(this) -} - -ShiftListItem.prototype.render = function () { - return ( - h('.transaction-list-item.flex-row', { - style: { - paddingTop: '20px', - paddingBottom: '20px', - justifyContent: 'space-around', - alignItems: 'center', - }, - }, [ - h('div', { - style: { - width: '0px', - position: 'relative', - bottom: '19px', - }, - }, [ - h('img', { - src: 'https://info.shapeshift.io/sites/default/files/logo.png', - style: { - height: '35px', - width: '132px', - position: 'absolute', - clip: 'rect(0px,23px,34px,0px)', - }, - }), - ]), - - this.renderInfo(), - this.renderUtilComponents(), - ]) - ) -} - -function formatDate (date) { - return vreme.format(new Date(date), 'March 16 2014 14:30') -} - -ShiftListItem.prototype.renderUtilComponents = function () { - var props = this.props - const { conversionRate, currentCurrency } = props - - switch (props.response.status) { - case 'no_deposits': - return h('.flex-row', [ - h(CopyButton, { - value: this.props.depositAddress, - }), - h(Tooltip, { - title: 'QR Code', - }, [ - h('i.fa.fa-qrcode.pointer.pop-hover', { - onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType)), - style: { - margin: '5px', - marginLeft: '23px', - marginRight: '12px', - fontSize: '20px', - color: '#F7861C', - }, - }), - ]), - ]) - case 'received': - return h('.flex-row') - - case 'complete': - return h('.flex-row', [ - h(CopyButton, { - value: this.props.response.transaction, - }), - h(EthBalance, { - value: `${props.response.outgoingCoin}`, - conversionRate, - currentCurrency, - width: '55px', - shorten: true, - needsParse: false, - incoming: true, - style: { - fontSize: '15px', - color: '#01888C', - }, - }), - ]) - - case 'failed': - return '' - default: - return '' - } -} - -ShiftListItem.prototype.renderInfo = function () { - var props = this.props - switch (props.response.status) { - case 'no_deposits': - return h('.flex-column', { - style: { - width: '200px', - overflow: 'hidden', - }, - }, [ - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, `${props.depositType} to ETH via ShapeShift`), - h('div', 'No deposits received'), - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, formatDate(props.time)), - ]) - case 'received': - return h('.flex-column', { - style: { - width: '200px', - overflow: 'hidden', - }, - }, [ - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, `${props.depositType} to ETH via ShapeShift`), - h('div', 'Conversion in progress'), - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, formatDate(props.time)), - ]) - case 'complete': - var url = explorerLink(props.response.transaction, parseInt('1')) - - return h('.flex-column.pointer', { - style: { - width: '200px', - overflow: 'hidden', - }, - onClick: () => global.platform.openWindow({ url }), - }, [ - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, 'From ShapeShift'), - h('div', formatDate(props.time)), - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, addressSummary(props.response.transaction)), - ]) - - case 'failed': - return h('span.error', '(Failed)') - default: - return '' - } -} diff --git a/ui/app/components/tab-bar.js b/ui/app/components/tab-bar.js deleted file mode 100644 index 6295e7dd9..000000000 --- a/ui/app/components/tab-bar.js +++ /dev/null @@ -1,36 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = TabBar - -inherits(TabBar, Component) -function TabBar () { - Component.call(this) -} - -TabBar.prototype.render = function () { - const props = this.props - const state = this.state || {} - const { tabs = [], defaultTab, tabSelected } = props - const { subview = defaultTab } = state - - return ( - h('.flex-row.space-around.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - paddingTop: '4px', - }, - }, tabs.map((tab) => { - const { key, content } = tab - return h(subview === key ? '.activeForm' : '.inactiveForm.pointer', { - onClick: () => { - this.setState({ subview: key }) - tabSelected(key) - }, - }, content) - })) - ) -} - diff --git a/ui/app/components/template.js b/ui/app/components/template.js deleted file mode 100644 index b6ed8eaa0..000000000 --- a/ui/app/components/template.js +++ /dev/null @@ -1,18 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = NewComponent - -inherits(NewComponent, Component) -function NewComponent () { - Component.call(this) -} - -NewComponent.prototype.render = function () { - const props = this.props - - return ( - h('span', props.message) - ) -} diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js deleted file mode 100644 index 19d7139bb..000000000 --- a/ui/app/components/token-cell.js +++ /dev/null @@ -1,72 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const Identicon = require('./identicon') -const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') - -module.exports = TokenCell - -inherits(TokenCell, Component) -function TokenCell () { - Component.call(this) -} - -TokenCell.prototype.render = function () { - const props = this.props - const { address, symbol, string, network, userAddress } = props - - return ( - h('li.token-cell', { - style: { cursor: network === '1' ? 'pointer' : 'default' }, - onClick: this.view.bind(this, address, userAddress, network), - }, [ - - h(Identicon, { - diameter: 50, - address, - network, - }), - - h('h3', `${string || 0} ${symbol}`), - - h('span', { style: { flex: '1 0 auto' } }), - - /* - h('button', { - onClick: this.send.bind(this, address), - }, 'SEND'), - */ - - ]) - ) -} - -TokenCell.prototype.send = function (address, event) { - event.preventDefault() - event.stopPropagation() - const url = tokenFactoryFor(address) - if (url) { - navigateTo(url) - } -} - -TokenCell.prototype.view = function (address, userAddress, network, event) { - const url = etherscanLinkFor(address, userAddress, network) - if (url) { - navigateTo(url) - } -} - -function navigateTo (url) { - global.platform.openWindow({ url }) -} - -function etherscanLinkFor (tokenAddress, address, network) { - const prefix = prefixForNetwork(network) - return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}` -} - -function tokenFactoryFor (tokenAddress) { - return `https://tokenfactory.surge.sh/#/token/${tokenAddress}` -} - diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js deleted file mode 100644 index fed7e9f7a..000000000 --- a/ui/app/components/token-list.js +++ /dev/null @@ -1,194 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const TokenTracker = require('eth-token-tracker') -const TokenCell = require('./token-cell.js') -const normalizeAddress = require('eth-sig-util').normalize - -const defaultTokens = [] -/* -const contracts = require('eth-contract-metadata') -for (const address in contracts) { - const contract = contracts[address] - if (contract.erc20) { - contract.address = address - defaultTokens.push(contract) - } -} -*/ - -module.exports = TokenList - -inherits(TokenList, Component) -function TokenList () { - this.state = { - tokens: [], - isLoading: true, - network: null, - } - Component.call(this) -} - -TokenList.prototype.render = function () { - const state = this.state - const { tokens, isLoading, error } = state - const { userAddress, network } = this.props - - if (isLoading) { - return this.message('Loading') - } - - if (error) { - log.error(error) - return this.message('There was a problem loading your token balances.') - } - - const tokenViews = tokens.map((tokenData) => { - tokenData.network = network - tokenData.userAddress = userAddress - return h(TokenCell, tokenData) - }) - - return h('div', [ - h('ol', { - style: { - height: '260px', - overflowY: 'auto', - display: 'flex', - flexDirection: 'column', - }, - }, [ - h('style', ` - - li.token-cell { - display: flex; - flex-direction: row; - align-items: center; - padding: 10px; - } - - li.token-cell > h3 { - margin-left: 12px; - } - - li.token-cell:hover { - background: white; - cursor: pointer; - } - - `), - ...tokenViews, - tokenViews.length ? null : this.message('No Tokens Found.'), - ]), - this.addTokenButtonElement(), - ]) -} - -TokenList.prototype.addTokenButtonElement = function () { - return h('div', [ - h('div.footer.hover-white.pointer', { - key: 'reveal-account-bar', - onClick: () => { - this.props.addToken() - }, - style: { - display: 'flex', - height: '40px', - padding: '10px', - justifyContent: 'center', - alignItems: 'center', - }, - }, [ - h('i.fa.fa-plus.fa-lg'), - ]), - ]) -} - -TokenList.prototype.message = function (body) { - return h('div', { - style: { - display: 'flex', - height: '250px', - alignItems: 'center', - justifyContent: 'center', - padding: '30px', - }, - }, body) -} - -TokenList.prototype.componentDidMount = function () { - this.createFreshTokenTracker() -} - -TokenList.prototype.createFreshTokenTracker = function () { - if (this.tracker) { - // Clean up old trackers when refreshing: - this.tracker.stop() - this.tracker.removeListener('update', this.balanceUpdater) - this.tracker.removeListener('error', this.showError) - } - - if (!global.ethereumProvider) return - const { userAddress } = this.props - this.tracker = new TokenTracker({ - userAddress, - provider: global.ethereumProvider, - tokens: uniqueMergeTokens(defaultTokens, this.props.tokens), - pollingInterval: 8000, - }) - - - // Set up listener instances for cleaning up - this.balanceUpdater = this.updateBalances.bind(this) - this.showError = (error) => { - this.setState({ error, isLoading: false }) - } - this.tracker.on('update', this.balanceUpdater) - this.tracker.on('error', this.showError) - - this.tracker.updateBalances() - .then(() => { - this.updateBalances(this.tracker.serialize()) - }) - .catch((reason) => { - log.error(`Problem updating balances`, reason) - this.setState({ isLoading: false }) - }) -} - -TokenList.prototype.componentWillUpdate = function (nextProps) { - if (nextProps.network === 'loading') return - const oldNet = this.props.network - const newNet = nextProps.network - - if (oldNet && newNet && newNet !== oldNet) { - this.setState({ isLoading: true }) - this.createFreshTokenTracker() - } -} - -TokenList.prototype.updateBalances = function (tokens) { - const heldTokens = tokens.filter(token => { - return token.balance !== '0' && token.string !== '0.000' - }) - this.setState({ tokens: heldTokens, isLoading: false }) -} - -TokenList.prototype.componentWillUnmount = function () { - if (!this.tracker) return - this.tracker.stop() -} - -function uniqueMergeTokens (tokensA, tokensB) { - const uniqueAddresses = [] - const result = [] - tokensA.concat(tokensB).forEach((token) => { - const normal = normalizeAddress(token.address) - if (!uniqueAddresses.includes(normal)) { - uniqueAddresses.push(normal) - result.push(token) - } - }) - return result -} - diff --git a/ui/app/components/tooltip.js b/ui/app/components/tooltip.js deleted file mode 100644 index edbc074bb..000000000 --- a/ui/app/components/tooltip.js +++ /dev/null @@ -1,22 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ReactTooltip = require('react-tooltip-component') - -module.exports = Tooltip - -inherits(Tooltip, Component) -function Tooltip () { - Component.call(this) -} - -Tooltip.prototype.render = function () { - const props = this.props - const { position, title, children } = props - - return h(ReactTooltip, { - position: position || 'left', - title, - fixed: false, - }, children) -} diff --git a/ui/app/components/transaction-list-item-icon.js b/ui/app/components/transaction-list-item-icon.js deleted file mode 100644 index 431054340..000000000 --- a/ui/app/components/transaction-list-item-icon.js +++ /dev/null @@ -1,68 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const Tooltip = require('./tooltip') - -const Identicon = require('./identicon') - -module.exports = TransactionIcon - -inherits(TransactionIcon, Component) -function TransactionIcon () { - Component.call(this) -} - -TransactionIcon.prototype.render = function () { - const { transaction, txParams, isMsg } = this.props - switch (transaction.status) { - case 'unapproved': - return h(!isMsg ? '.unapproved-tx-icon' : 'i.fa.fa-certificate.fa-lg') - - case 'rejected': - return h('i.fa.fa-exclamation-triangle.fa-lg.warning', { - style: { - width: '24px', - }, - }) - - case 'failed': - return h('i.fa.fa-exclamation-triangle.fa-lg.error', { - style: { - width: '24px', - }, - }) - - case 'submitted': - return h(Tooltip, { - title: 'Pending', - position: 'bottom', - }, [ - h('i.fa.fa-ellipsis-h', { - style: { - fontSize: '27px', - }, - }), - ]) - } - - if (isMsg) { - return h('i.fa.fa-certificate.fa-lg', { - style: { - width: '24px', - }, - }) - } - - if (txParams.to) { - return h(Identicon, { - diameter: 24, - address: txParams.to || transaction.hash, - }) - } else { - return h('i.fa.fa-file-text-o.fa-lg', { - style: { - width: '24px', - }, - }) - } -} diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js deleted file mode 100644 index dbda66a31..000000000 --- a/ui/app/components/transaction-list-item.js +++ /dev/null @@ -1,165 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const EthBalance = require('./eth-balance') -const addressSummary = require('../util').addressSummary -const explorerLink = require('../../lib/explorer-link') -const CopyButton = require('./copyButton') -const vreme = new (require('vreme')) -const Tooltip = require('./tooltip') -const numberToBN = require('number-to-bn') - -const TransactionIcon = require('./transaction-list-item-icon') -const ShiftListItem = require('./shift-list-item') -module.exports = TransactionListItem - -inherits(TransactionListItem, Component) -function TransactionListItem () { - Component.call(this) -} - -TransactionListItem.prototype.render = function () { - const { transaction, network, conversionRate, currentCurrency } = this.props - if (transaction.key === 'shapeshift') { - if (network === '1') return h(ShiftListItem, transaction) - } - var date = formatDate(transaction.time) - - let isLinkable = false - const numericNet = parseInt(network) - isLinkable = numericNet === 1 || numericNet === 3 || numericNet === 4 || numericNet === 42 - - var isMsg = ('msgParams' in transaction) - var isTx = ('txParams' in transaction) - var isPending = transaction.status === 'unapproved' - let txParams - if (isTx) { - txParams = transaction.txParams - } else if (isMsg) { - txParams = transaction.msgParams - } - - const nonce = txParams.nonce ? numberToBN(txParams.nonce).toString(10) : '' - - const isClickable = ('hash' in transaction && isLinkable) || isPending - return ( - h(`.transaction-list-item.flex-row.flex-space-between${isClickable ? '.pointer' : ''}`, { - onClick: (event) => { - if (isPending) { - this.props.showTx(transaction.id) - } - event.stopPropagation() - if (!transaction.hash || !isLinkable) return - var url = explorerLink(transaction.hash, parseInt(network)) - global.platform.openWindow({ url }) - }, - style: { - padding: '20px 0', - }, - }, [ - - h('.identicon-wrapper.flex-column.flex-center.select-none', [ - h('.pop-hover', { - onClick: (event) => { - event.stopPropagation() - if (!isTx || isPending) return - var url = `https://metamask.github.io/eth-tx-viz/?tx=${transaction.hash}` - global.platform.openWindow({ url }) - }, - }, [ - h(TransactionIcon, { txParams, transaction, isTx, isMsg }), - ]), - ]), - - h(Tooltip, { - title: 'Transaction Number', - position: 'bottom', - }, [ - h('span', { - style: { - display: 'flex', - cursor: 'normal', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - padding: '10px', - }, - }, nonce), - ]), - - h('.flex-column', {style: {width: '200px', overflow: 'hidden'}}, [ - domainField(txParams), - h('div', date), - recipientField(txParams, transaction, isTx, isMsg), - ]), - - // Places a copy button if tx is successful, else places a placeholder empty div. - transaction.hash ? h(CopyButton, { value: transaction.hash }) : h('div', {style: { display: 'flex', alignItems: 'center', width: '26px' }}), - - isTx ? h(EthBalance, { - value: txParams.value, - conversionRate, - currentCurrency, - width: '55px', - shorten: true, - showFiat: false, - style: {fontSize: '15px'}, - }) : h('.flex-column'), - ]) - ) -} - -function domainField (txParams) { - return h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - overflow: 'hidden', - textOverflow: 'ellipsis', - width: '100%', - }, - }, [ - txParams.origin, - ]) -} - -function recipientField (txParams, transaction, isTx, isMsg) { - let message - - if (isMsg) { - message = 'Signature Requested' - } else if (txParams.to) { - message = addressSummary(txParams.to) - } else { - message = 'Contract Published' - } - - return h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - }, - }, [ - message, - failIfFailed(transaction), - ]) -} - -function formatDate (date) { - return vreme.format(new Date(date), 'March 16 2014 14:30') -} - -function failIfFailed (transaction) { - if (transaction.status === 'rejected') { - return h('span.error', ' (Rejected)') - } - if (transaction.err) { - return h(Tooltip, { - title: transaction.err.message, - position: 'bottom', - }, [ - h('span.error', ' (Failed)'), - ]) - } -} diff --git a/ui/app/components/transaction-list.js b/ui/app/components/transaction-list.js deleted file mode 100644 index 3b4ba741e..000000000 --- a/ui/app/components/transaction-list.js +++ /dev/null @@ -1,79 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const TransactionListItem = require('./transaction-list-item') - -module.exports = TransactionList - - -inherits(TransactionList, Component) -function TransactionList () { - Component.call(this) -} - -TransactionList.prototype.render = function () { - const { transactions, network, unapprovedMsgs, conversionRate } = this.props - - var shapeShiftTxList - if (network === '1') { - shapeShiftTxList = this.props.shapeShiftTxList - } - const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList) - .sort((a, b) => b.time - a.time) - - return ( - - h('section.transaction-list', [ - - h('style', ` - .transaction-list .transaction-list-item:not(:last-of-type) { - border-bottom: 1px solid #D4D4D4; - } - .transaction-list .transaction-list-item .ether-balance-label { - display: block !important; - font-size: small; - } - `), - - h('.tx-list', { - style: { - overflowY: 'auto', - height: '300px', - padding: '0 20px', - textAlign: 'center', - }, - }, [ - - txsToRender.length - ? txsToRender.map((transaction, i) => { - let key - switch (transaction.key) { - case 'shapeshift': - const { depositAddress, time } = transaction - key = `shift-tx-${depositAddress}-${time}-${i}` - break - default: - key = `tx-${transaction.id}-${i}` - } - return h(TransactionListItem, { - transaction, i, network, key, - conversionRate, - showTx: (txId) => { - this.props.viewPendingTx(txId) - }, - }) - }) - : h('.flex-center', { - style: { - flexDirection: 'column', - height: '100%', - }, - }, [ - 'No transaction history.', - ]), - ]), - ]) - ) -} - diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js deleted file mode 100644 index 747d3ce2b..000000000 --- a/ui/app/conf-tx.js +++ /dev/null @@ -1,213 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') -const NetworkIndicator = require('./components/network') -const txHelper = require('../lib/tx-helper') -const isPopupOrNotification = require('../../app/scripts/lib/is-popup-or-notification') - -const PendingTx = require('./components/pending-tx') -const PendingMsg = require('./components/pending-msg') -const PendingPersonalMsg = require('./components/pending-personal-msg') -const Loading = require('./components/loading') - -module.exports = connect(mapStateToProps)(ConfirmTxScreen) - -function mapStateToProps (state) { - return { - identities: state.metamask.identities, - accounts: state.metamask.accounts, - selectedAddress: state.metamask.selectedAddress, - unapprovedTxs: state.metamask.unapprovedTxs, - unapprovedMsgs: state.metamask.unapprovedMsgs, - unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs, - index: state.appState.currentView.context, - warning: state.appState.warning, - network: state.metamask.network, - provider: state.metamask.provider, - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - blockGasLimit: state.metamask.currentBlockGasLimit, - } -} - -inherits(ConfirmTxScreen, Component) -function ConfirmTxScreen () { - Component.call(this) -} - -ConfirmTxScreen.prototype.render = function () { - const props = this.props - const { network, provider, unapprovedTxs, currentCurrency, - unapprovedMsgs, unapprovedPersonalMsgs, conversionRate, blockGasLimit } = props - - var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) - - var txData = unconfTxList[props.index] || {} - var txParams = txData.params || {} - var isNotification = isPopupOrNotification() === 'notification' - - - log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) - if (unconfTxList.length === 0) return h(Loading, { isLoading: true }) - - return ( - - h('.flex-column.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - !isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.goHome.bind(this), - }) : null, - h('h2.page-subtitle', 'Confirm Transaction'), - isNotification ? h(NetworkIndicator, { - network: network, - provider: provider, - }) : null, - ]), - - h('h3', { - style: { - alignSelf: 'center', - display: unconfTxList.length > 1 ? 'block' : 'none', - }, - }, [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - style: { - display: props.index === 0 ? 'none' : 'inline-block', - }, - onClick: () => props.dispatch(actions.previousTx()), - }), - ` ${props.index + 1} of ${unconfTxList.length} `, - h('i.fa.fa-arrow-right.fa-lg.cursor-pointer', { - style: { - display: props.index + 1 === unconfTxList.length ? 'none' : 'inline-block', - }, - onClick: () => props.dispatch(actions.nextTx()), - }), - ]), - - warningIfExists(props.warning), - - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - - currentTxView({ - // Properties - txData: txData, - key: txData.id, - selectedAddress: props.selectedAddress, - accounts: props.accounts, - identities: props.identities, - conversionRate, - currentCurrency, - blockGasLimit, - // Actions - buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), - sendTransaction: this.sendTransaction.bind(this), - cancelTransaction: this.cancelTransaction.bind(this, txData), - signMessage: this.signMessage.bind(this, txData), - signPersonalMessage: this.signPersonalMessage.bind(this, txData), - cancelMessage: this.cancelMessage.bind(this, txData), - cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), - }), - - ]), - ]) - ) -} - -function currentTxView (opts) { - log.info('rendering current tx view') - const { txData } = opts - const { txParams, msgParams, type } = txData - - if (txParams) { - log.debug('txParams detected, rendering pending tx') - return h(PendingTx, opts) - } else if (msgParams) { - log.debug('msgParams detected, rendering pending msg') - - if (type === 'eth_sign') { - log.debug('rendering eth_sign message') - return h(PendingMsg, opts) - } else if (type === 'personal_sign') { - log.debug('rendering personal_sign message') - return h(PendingPersonalMsg, opts) - } - } -} - -ConfirmTxScreen.prototype.buyEth = function (address, event) { - event.preventDefault() - this.props.dispatch(actions.buyEthView(address)) -} - -ConfirmTxScreen.prototype.sendTransaction = function (txData, event) { - this.stopPropagation(event) - this.props.dispatch(actions.updateAndApproveTx(txData)) -} - -ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) { - this.stopPropagation(event) - event.preventDefault() - this.props.dispatch(actions.cancelTx(txData)) -} - -ConfirmTxScreen.prototype.signMessage = function (msgData, event) { - log.info('conf-tx.js: signing message') - var params = msgData.msgParams - params.metamaskId = msgData.id - this.stopPropagation(event) - this.props.dispatch(actions.signMsg(params)) -} - -ConfirmTxScreen.prototype.stopPropagation = function (event) { - if (event.stopPropagation) { - event.stopPropagation() - } -} - -ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) { - log.info('conf-tx.js: signing personal message') - var params = msgData.msgParams - params.metamaskId = msgData.id - this.stopPropagation(event) - this.props.dispatch(actions.signPersonalMsg(params)) -} - -ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) { - log.info('canceling message') - this.stopPropagation(event) - this.props.dispatch(actions.cancelMsg(msgData)) -} - -ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) { - log.info('canceling personal message') - this.stopPropagation(event) - this.props.dispatch(actions.cancelPersonalMsg(msgData)) -} - -ConfirmTxScreen.prototype.goHome = function (event) { - this.stopPropagation(event) - this.props.dispatch(actions.goHome()) -} - -function warningIfExists (warning) { - if (warning && - // Do not display user rejections on this screen: - warning.indexOf('User denied transaction signature') === -1) { - return h('.error', { - style: { - margin: 'auto', - }, - }, warning) - } -} diff --git a/ui/app/config.js b/ui/app/config.js deleted file mode 100644 index 62785c49b..000000000 --- a/ui/app/config.js +++ /dev/null @@ -1,211 +0,0 @@ -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 currencies = require('./conversion.json').rows -const validUrl = require('valid-url') -const copyToClipboard = require('copy-to-clipboard') - -module.exports = connect(mapStateToProps)(ConfigScreen) - -function mapStateToProps (state) { - return { - metamask: state.metamask, - warning: state.appState.warning, - } -} - -inherits(ConfigScreen, Component) -function ConfigScreen () { - Component.call(this) -} - -ConfigScreen.prototype.render = function () { - var state = this.props - var metamaskState = state.metamask - var warning = state.warning - - return ( - h('.flex-column.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: (event) => { - state.dispatch(actions.goHome()) - }, - }), - h('h2.page-subtitle', 'Settings'), - ]), - - h('.error', { - style: { - display: warning ? 'block' : 'none', - padding: '0 20px', - textAlign: 'center', - }, - }, warning), - - // conf view - h('.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-space-around', { - style: { - padding: '20px', - }, - }, [ - - currentProviderDisplay(metamaskState), - - h('div', { style: {display: 'flex'} }, [ - h('input#new_rpc', { - placeholder: 'New RPC URL', - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - onKeyPress (event) { - if (event.key === 'Enter') { - var element = event.target - var newRpc = element.value - rpcValidation(newRpc, state) - } - }, - }), - h('button', { - style: { - alignSelf: 'center', - }, - onClick (event) { - event.preventDefault() - var element = document.querySelector('input#new_rpc') - var newRpc = element.value - rpcValidation(newRpc, state) - }, - }, 'Save'), - ]), - - h('hr.horizontal-line'), - - currentConversionInformation(metamaskState, state), - - h('hr.horizontal-line'), - - h('div', { - style: { - marginTop: '20px', - }, - }, [ - h('p', { - style: { - fontFamily: 'Montserrat Light', - fontSize: '13px', - }, - }, `State logs contain your public account addresses and sent transactions.`), - h('br'), - h('button', { - style: { - alignSelf: 'center', - }, - onClick (event) { - copyToClipboard(window.logState()) - }, - }, 'Copy State Logs'), - ]), - - h('hr.horizontal-line'), - - h('div', { - style: { - marginTop: '20px', - }, - }, [ - h('button', { - style: { - alignSelf: 'center', - }, - onClick (event) { - event.preventDefault() - state.dispatch(actions.revealSeedConfirmation()) - }, - }, 'Reveal Seed Words'), - ]), - - ]), - ]), - ]) - ) -} - -function rpcValidation (newRpc, state) { - if (validUrl.isWebUri(newRpc)) { - state.dispatch(actions.setRpcTarget(newRpc)) - } else { - var appendedRpc = `http://${newRpc}` - if (validUrl.isWebUri(appendedRpc)) { - state.dispatch(actions.displayWarning('URIs require the appropriate HTTP/HTTPS prefix.')) - } else { - state.dispatch(actions.displayWarning('Invalid RPC URI')) - } - } -} - -function currentConversionInformation (metamaskState, state) { - var currentCurrency = metamaskState.currentCurrency - var conversionDate = metamaskState.conversionDate - return h('div', [ - h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, 'Current Conversion'), - h('span', {style: { fontWeight: 'bold', paddingRight: '10px', fontSize: '13px'}}, `Updated ${Date(conversionDate)}`), - h('select#currentCurrency', { - onChange (event) { - event.preventDefault() - var element = document.getElementById('currentCurrency') - var newCurrency = element.value - state.dispatch(actions.setCurrentCurrency(newCurrency)) - }, - defaultValue: currentCurrency, - }, currencies.map((currency) => { - return h('option', {key: currency.code, value: currency.code}, `${currency.code} - ${currency.name}`) - }) - ), - ]) -} - -function currentProviderDisplay (metamaskState) { - var provider = metamaskState.provider - var title, value - - switch (provider.type) { - - case 'mainnet': - title = 'Current Network' - value = 'Main Ethereum Network' - break - - case 'ropsten': - title = 'Current Network' - value = 'Ropsten Test Network' - break - - case 'kovan': - title = 'Current Network' - value = 'Kovan Test Network' - break - - case 'rinkeby': - title = 'Current Network' - value = 'Rinkeby Test Network' - break - - default: - title = 'Current RPC' - value = metamaskState.provider.rpcTarget - } - - return h('div', [ - h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, title), - h('span', value), - ]) -} diff --git a/ui/app/conversion.json b/ui/app/conversion.json deleted file mode 100644 index 155ffc4fc..000000000 --- a/ui/app/conversion.json +++ /dev/null @@ -1,207 +0,0 @@ -{ - "rows": [ - { - "code": "REP", - "name": "Augur", - "statuses": [ - "primary" - ] - }, - { - "code": "BCN", - "name": "Bytecoin", - "statuses": [ - "primary" - ] - }, - { - "code": "BTC", - "name": "Bitcoin", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "BTS", - "name": "BitShares", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "BLK", - "name": "Blackcoin", - "statuses": [ - "primary" - ] - }, - { - "code": "GBP", - "name": "British Pound Sterling", - "statuses": [ - "secondary" - ] - }, - { - "code": "CAD", - "name": "Canadian Dollar", - "statuses": [ - "secondary" - ] - }, - { - "code": "CNY", - "name": "Chinese Yuan", - "statuses": [ - "secondary" - ] - }, - { - "code": "DSH", - "name": "Dashcoin", - "statuses": [ - "primary" - ] - }, - { - "code": "DOGE", - "name": "Dogecoin", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "ETC", - "name": "Ethereum Classic", - "statuses": [ - "primary" - ] - }, - { - "code": "EUR", - "name": "Euro", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "GNO", - "name": "GNO", - "statuses": [ - "primary" - ] - }, - { - "code": "GNT", - "name": "GNT", - "statuses": [ - "primary" - ] - }, - { - "code": "JPY", - "name": "Japanese Yen", - "statuses": [ - "secondary" - ] - }, - { - "code": "LTC", - "name": "Litecoin", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "MAID", - "name": "MaidSafeCoin", - "statuses": [ - "primary" - ] - }, - { - "code": "XEM", - "name": "NEM", - "statuses": [ - "primary" - ] - }, - { - "code": "XLM", - "name": "Stellar", - "statuses": [ - "primary" - ] - }, - { - "code": "XMR", - "name": "Monero", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "XRP", - "name": "Ripple", - "statuses": [ - "primary" - ] - }, - { - "code": "RUR", - "name": "Ruble", - "statuses": [ - "secondary" - ] - }, - { - "code": "STEEM", - "name": "Steem", - "statuses": [ - "primary" - ] - }, - { - "code": "STRAT", - "name": "STRAT", - "statuses": [ - "primary" - ] - }, - { - "code": "UAH", - "name": "Ukrainian Hryvnia", - "statuses": [ - "secondary" - ] - }, - { - "code": "USD", - "name": "US Dollar", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "WAVES", - "name": "WAVES", - "statuses": [ - "primary" - ] - }, - { - "code": "ZEC", - "name": "Zcash", - "statuses": [ - "primary" - ] - } - ] -} diff --git a/ui/app/css/debug.css b/ui/app/css/debug.css deleted file mode 100644 index 3e125bcd4..000000000 --- a/ui/app/css/debug.css +++ /dev/null @@ -1,21 +0,0 @@ -/* -debug / dev -*/ - -#app-content { - border: 2px solid green; -} - -#design-container { - position: absolute; - left: 360px; - top: -42px; - width: calc(100vw - 360px); - height: 100vh; - overflow: scroll; -} - -#design-container img { - width: 2000px; - margin-right: 600px; -} \ No newline at end of file diff --git a/ui/app/css/fonts.css b/ui/app/css/fonts.css deleted file mode 100644 index 3b9f581b9..000000000 --- a/ui/app/css/fonts.css +++ /dev/null @@ -1,36 +0,0 @@ -@import url(https://fonts.googleapis.com/css?family=Roboto:300,500); -@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css); - -@font-face { - font-family: 'Montserrat Regular'; - src: url('/fonts/Montserrat/Montserrat-Regular.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-Regular.ttf') format('truetype'); - font-weight: normal; - font-style: normal; - font-size: 'small'; - -} - -@font-face { - font-family: 'Montserrat Bold'; - src: url('/fonts/Montserrat/Montserrat-Bold.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-Bold.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'Montserrat Light'; - src: url('/fonts/Montserrat/Montserrat-Light.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-Light.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'Montserrat UltraLight'; - src: url('/fonts/Montserrat/Montserrat-UltraLight.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-UltraLight.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} diff --git a/ui/app/css/index.css b/ui/app/css/index.css deleted file mode 100644 index 808aafb4c..000000000 --- a/ui/app/css/index.css +++ /dev/null @@ -1,667 +0,0 @@ -/* -faint orange (textfield shades) #FAF6F0 -light orange (button shades): #F5C26D -dark orange (text): #F5A623 -borders/font/any gray: #4A4A4A -*/ - -/* -application specific styles -*/ - -* { - box-sizing: border-box; -} - -html, body { - font-family: 'Montserrat Regular', Arial; - color: #4D4D4D; - font-weight: 300; - line-height: 1.4em; - background: #F7F7F7; -} - -input:focus, textarea:focus { - outline: none; -} - -#app-content { - overflow-x: hidden; - min-width: 357px; - width: 360px; - height: 500px; -} - -button, input[type="submit"] { - font-family: 'Montserrat Bold'; - outline: none; - cursor: pointer; - padding: 8px 12px; - border: none; - color: white; - transform-origin: center center; - transition: transform 50ms ease-in; - /* default orange */ - background: rgba(247, 134, 28, 1); - box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); -} - -.btn-green, input[type="submit"].btn-green { - background: rgba(106, 195, 96, 1); - box-shadow: 0px 3px 6px rgba(106, 195, 96, 0.36); -} - -.btn-red { - background: rgba(254, 35, 17, 1); - box-shadow: 0px 3px 6px rgba(254, 35, 17, 0.36); -} - -button[disabled], input[type="submit"][disabled] { - cursor: not-allowed; - background: rgba(197, 197, 197, 1); - box-shadow: 0px 3px 6px rgba(197, 197, 197, 0.36); -} - -button.spaced { - margin: 2px; -} - -button:not([disabled]):hover, input[type="submit"]:not([disabled]):hover { - transform: scale(1.1); -} -button:not([disabled]):active, input[type="submit"]:not([disabled]):active { - transform: scale(0.95); -} - -a { - text-decoration: none; - color: inherit; -} - -a:hover{ - color: #df6b0e; -} - -/* -app -*/ - -.active { - color: #909090; -} - -button.primary { - padding: 8px 12px; - background: #F7861C; - box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); - color: white; - font-size: 1.1em; - font-family: 'Montserrat Regular'; - text-transform: uppercase; -} - -button.btn-thin { - border: 1px solid; - border-color: #4D4D4D; - color: #4D4D4D; - background: rgb(255, 174, 41); - border-radius: 4px; - min-width: 200px; - margin: 12px 0; - padding: 6px; - font-size: 13px; -} - -.app-header { - padding: 6px 8px; -} - -.app-header h1 { - font-family: 'Montserrat Regular'; - text-transform: uppercase; - color: #AEAEAE; -} - -h2.page-subtitle { - font-family: 'Montserrat Regular'; - text-transform: uppercase; - color: #AEAEAE; - font-size: 1em; - margin: 12px; -} - -.app-primary { - -} - -.app-footer { - padding-bottom: 10px; - align-items: center; -} - -.identicon { - height: 46px; - width: 46px; - background-size: cover; - border-radius: 100%; - border: 3px solid gray; -} - -textarea.twelve-word-phrase { - padding: 12px; - width: 300px; - height: 140px; - font-size: 16px; - background: white; - resize: none; -} - -.network-indicator { - display: flex; - align-items: center; - font-size: 0.6em; - -} - -.network-name { - width: 5.2em; - line-height: 9px; - text-rendering: geometricPrecision; -} - -.check { - margin-left: 7px; - color: #F7861C; - flex: 1 0 auto; - display: flex; - justify-content: flex-end; -} -/* -app sections -*/ - -/* initialize */ - -.initialize-screen hr { - width: 60px; - margin: 12px; - border-color: #F7861C; - border-style: solid; -} - -.initialize-screen label { - margin-top: 20px; -} - -.initialize-screen button.create-vault { - margin-top: 40px; -} - -.initialize-screen .warning { - font-size: 14px; - margin: 0 16px; -} - -/* unlock */ -.error { - color: #E20202; -} - -.warning { - color: #FFAE00; -} - -.lock { - width: 50px; - height: 50px; -} - -.lock.locked { - transform: scale(1.5); - opacity: 0.0; - transition: opacity 400ms ease-in, transform 400ms ease-in; -} -.lock.unlocked { - transform: scale(1); - opacity: 1; - transition: opacity 500ms ease-out, transform 500ms ease-out, background 200ms ease-in; -} - -.lock.locked .lock-top { - transform: scaleX(1) translateX(0); - transition: transform 250ms ease-in; -} -.lock.unlocked .lock-top { - transform: scaleX(-1) translateX(-12px); - transition: transform 250ms ease-in; -} -.lock.unlocked:hover { - border-radius: 4px; - background: #e5e5e5; - border: 1px solid #b1b1b1; -} -.lock.unlocked:active { - background: #c3c3c3; -} - -.section-title .fa-arrow-left { - margin: -2px 8px 0px -8px; -} - -.unlock-screen #metamask-mascot-container { - margin-top: 24px; -} - -.unlock-screen h1 { - margin-top: -28px; - margin-bottom: 42px; -} - -.unlock-screen input[type=password] { - width: 260px; - /*height: 36px; - margin-bottom: 24px; - padding: 8px;*/ -} - -.sizing-input{ - font-size: 14px; - height: 30px; - padding-left: 5px; -} -.editable-label{ - display: flex; -} -/* Webkit */ -.unlock-screen input::-webkit-input-placeholder { - text-align: center; - font-size: 1.2em; -} -/* Firefox 18- */ -.unlock-screen input:-moz-placeholder { - text-align: center; - font-size: 1.2em; -} -/* Firefox 19+ */ -.unlock-screen input::-moz-placeholder { - text-align: center; - font-size: 1.2em; -} -/* IE */ -.unlock-screen input:-ms-input-placeholder { - text-align: center; - font-size: 1.2em; -} - -input.large-input, textarea.large-input { - /*margin-bottom: 24px;*/ - padding: 8px; -} - -input.large-input { - height: 36px; -} - -.letter-spacey { - letter-spacing: 0.1em; -} - - - -/* accounts */ - -.accounts-section { - margin: 0 0px; -} - -.accounts-section .horizontal-line { - margin: 0px 18px; -} - -.accounts-list-option { - height: 120px; -} - -.accounts-list-option .identicon-wrapper { - width: 100px; -} - -.unconftx-link { - margin-top: 24px; - cursor: pointer; -} - -.unconftx-link .fa-arrow-right { - margin: 0px -8px 0px 8px; -} - -/* identity panel */ - -.identity-panel { - font-weight: 500; -} - -.identity-panel .identicon-wrapper { - margin: 4px; - margin-top: 8px; - display: flex; - align-items: center; -} - -.identity-panel .identicon-wrapper span { - margin: 0 auto; -} - -.identity-panel .identity-data { - margin: 8px 8px 8px 18px; -} - -.identity-panel i { - margin-top: 32px; - margin-right: 6px; - color: #B9B9B9; -} - -.identity-panel .arrow-right { - padding-left: 18px; - width: 42px; - min-width: 18px; - height: 100%; -} - -.identity-copy.flex-column { - flex: 0.25 0 auto; - justify-content: center; -} - -/* accounts screen */ - -.identity-section { - -} - -.identity-section .identity-panel { - background: #E9E9E9; - border-bottom: 1px solid #B1B1B1; - cursor: pointer; -} - -.identity-section .identity-panel.selected { - background: white; - color: #F3C83E; -} - -.identity-section .identity-panel.selected .identicon { - border-color: orange; -} - -.identity-section .accounts-list-option:hover, -.identity-section .accounts-list-option.selected { - background:white; -} - -/* account detail screen */ - -.account-detail-section { - -} -.name-label{ - -} - -.unapproved-tx-icon { - height: 16px; - width: 16px; - background: rgb(47, 174, 244); - border-color: #AEAEAE; - border-radius: 13px; -} - -.edit-text { - height: 100%; - visibility: hidden; -} -.editing-label { - display: flex; - justify-content: flex-start; - margin-left: 50px; - margin-bottom: 2px; - font-size: 11px; - text-rendering: geometricPrecision; - color: #F7861C; -} -.name-label:hover .edit-text { - visibility: visible; -} -/* tx confirm */ - -.unconftx-section input[type=password] { - height: 22px; - padding: 2px; - margin: 12px; - margin-bottom: 24px; - border-radius: 4px; - border: 2px solid #F3C83E; - background: #FAF6F0; -} - -/* Send Screen */ - -.send-screen { - -} - -.send-screen section { - margin: 8px 16px; -} - -.send-screen input { - width: 100%; - font-size: 12px; -} - -/* Ether Balance Widget */ - -.ether-balance-amount { - color: #F7861C; -} - -.ether-balance-label { - color: #ABA9AA; -} - -/* Info screen */ -.info-gray{ - font-family: 'Montserrat Regular'; - text-transform: uppercase; - color: #AEAEAE; -} - -.icon-size{ - width: 20px; -} - -.info{ - font-family: 'Montserrat Regular', Arial; - padding-bottom: 10px; - display: inline-block; - padding-left: 5px; -} - -/* buy eth warning screen */ -.custom-radios { - justify-content: space-around; - align-items: center; -} - - -.custom-radio-selected { - width: 17px; - height: 17px; - border: solid; - border-style: double; - border-radius: 15px; - border-width: 5px; - background: rgba(247, 134, 28, 1); - border-color: #F7F7F7; -} - -.custom-radio-inactive { - width: 14px; - height: 14px; - border: solid; - border-width: 1px; - border-radius: 24px; - border-color: #AEAEAE; -} - -.radio-titles { - color: rgba(247, 134, 28, 1); -} - -.radio-titles-subtext { - -} - -.selected-exchange { - -} - -.buy-radio { - -} - -.eth-warning{ - transition: opacity 400ms ease-in, transform 400ms ease-in; -} - -.buy-subview{ - transition: opacity 400ms ease-in, transform 400ms ease-in; -} - -.input-container:hover .edit-text{ - visibility: visible; -} - -.buy-inputs{ - font-family: 'Montserrat Light'; - font-size: 13px; - height: 20px; - background: transparent; - box-sizing: border-box; - border: solid; - border-color: transparent; - border-width: 0.5px; - border-radius: 2px; - -} -.input-container:hover .buy-inputs{ - box-sizing: inherit; - border: solid; - border-color: #F7861C; - border-width: 0.5px; - border-radius: 2px; -} - -.buy-inputs:focus{ - border: solid; - border-color: #F7861C; - border-width: 0.5px; - border-radius: 2px; -} - -.activeForm { - background: #F7F7F7; - border: none; - border-radius: 8px 8px 0px 0px; - width: 50%; - text-align: center; - padding-bottom: 4px; - -} - -.inactiveForm { - border: none; - border-radius: 8px 8px 0px 0px; - width: 50%; - text-align: center; - padding-bottom: 4px; -} - -.ex-coins { - font-family: 'Montserrat Regular'; - text-transform: uppercase; - text-align: center; - font-size: 33px; - width: 118px; - height: 42px; - padding: 1px; - color: #4D4D4D; -} - -.marketinfo{ - font-family: 'Montserrat light'; - color: #AEAEAE; - font-size: 15px; - line-height: 17px; -} - -#fromCoin::-webkit-calendar-picker-indicator { - display: none; -} - -#coinList { - width: 400px; - height: 500px; - overflow: scroll; -} - -.icon-control .fa-refresh{ - visibility: hidden; -} - -.icon-control:hover .fa-refresh{ - visibility: visible; -} - -.icon-control:hover .fa-chevron-right{ - visibility: hidden; -} - -.inactive { - color: #AEAEAE; -} - -.inactive button{ - background: #AEAEAE; - color: white; -} - -.ellip-address { - overflow: hidden; - text-overflow: ellipsis; - width: 5em; - font-size: 14px; - font-family: "Montserrat Light"; - margin-left: 5px; -} - -.qr-header { - font-size: 25px; - margin-top: 40px; -} - -.qr-message { - font-size: 12px; - color: #F7861C; -} - -div.message-container > div:first-child { - margin-top: 18px; - font-size: 15px; - color: #4D4D4D; -} - -.pop-hover:hover { - transform: scale(1.1); -} diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css deleted file mode 100644 index 910a24ee2..000000000 --- a/ui/app/css/lib.css +++ /dev/null @@ -1,268 +0,0 @@ -/* color */ - -.color-orange { - color: #F7861C; -} - -.color-forest { - color: #0A5448; -} - -/* lib */ - -.full-width { - width: 100%; -} - -.full-height { - height: 100%; -} - -.flex-column { - display: flex; - flex-direction: column; -} - -.space-between { - justify-content: space-between; -} - -.space-around { - justify-content: space-around; -} - -.flex-column-bottom { - display: flex; - flex-direction: column-reverse; -} - -.flex-row { - display: flex; - flex-direction: row; -} - -.flex-space-between { - justify-content: space-between; -} - -.flex-space-around { - justify-content: space-around; -} - -.flex-right { - display: flex; - flex-direction: row; - justify-content: flex-end; -} - -.flex-left { - display: flex; - flex-direction: row; - justify-content: flex-start; -} - -.flex-fixed { - flex: none; -} - -.flex-basis-auto { - flex-basis: auto; -} - -.flex-grow { - flex: 1 1 auto; -} - -.flex-wrap { - flex-wrap: wrap; -} - -.flex-center { - display: flex; - justify-content: center; - align-items: center; -} - -.flex-justify-center { - justify-content: center; -} - -.flex-align-center { - align-items: center; -} - -.flex-self-end { - align-self: flex-end; -} - -.flex-self-stretch { - align-self: stretch; -} - -.flex-vertical { - flex-direction: column; -} - -.z-bump { - z-index: 1; -} - -.select-none { - cursor: inherit; - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; -} - -.pointer { - cursor: pointer; -} -.cursor-pointer { - cursor: pointer; - transform-origin: center center; - transition: transform 50ms ease-in-out; -} -.cursor-pointer:hover { - transform: scale(1.1); -} -.cursor-pointer:active { - transform: scale(0.95); -} - -.cursor-disabled { - cursor: not-allowed; -} - -.margin-bottom-sml { - margin-bottom: 20px; -} - -.margin-bottom-med { - margin-bottom: 40px; -} - -.margin-right-left { - margin: 0 20px; -} - -.bold { - font-weight: bold; -} - -.text-transform-uppercase { - text-transform: uppercase; -} - -.font-small { - font-size: 12px; -} - -.font-medium { - font-size: 1.2em; -} - -hr.horizontal-line { - display: block; - height: 1px; - border: 0; - border-top: 1px solid #ccc; - margin: 1em 0; - padding: 0; -} - -.hover-white:hover { - background: white; -} - -.red-dot { - background: #E91550; - color: white; - border-radius: 10px; -} - -.diamond { - transform: rotate(45deg); - background: #038789; -} - -.hollow-diamond { - transform: rotate(45deg); - border: 3px solid #690496; -} - -.golden-square { - background: #EBB33F; -} - -.pending-dot { - background: red; - left: 14px; - top: 14px; - color: white; - border-radius: 10px; - height: 20px; - min-width: 20px; - position: relative; - display: flex; - align-items: center; - justify-content: center; - padding: 4px; - z-index: 1; -} - -.keyring-label { - z-index: 1; - font-size: 11px; - background: rgba(255,0,0,0.8); - bottom: -47px; - color: white; - border-radius: 10px; - height: 20px; - min-width: 20px; - position: relative; - display: flex; - align-items: center; - justify-content: center; - padding: 4px; -} - -.ether-balance { - display: flex; - align-items: center; -} - -.menu-icon { - display: inline-block; - height: 9px; - min-width: 9px; - margin: 13px; -} -.ether-icon { - background: rgb(0, 163, 68); - border-radius: 20px; -} -.testnet-icon { - background: #2465E1; -} - -.drop-menu-item { - display: flex; - align-items: center; -} - -.invisible { - visibility: hidden; -} - -.one-line-concat { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.critical-error { - text-align: center; - margin-top: 20px; - color: red; -} diff --git a/ui/app/css/reset.css b/ui/app/css/reset.css deleted file mode 100644 index 9ce89e8bc..000000000 --- a/ui/app/css/reset.css +++ /dev/null @@ -1,48 +0,0 @@ -/* http://meyerweb.com/eric/tools/css/reset/ - v2.0 | 20110126 - License: none (public domain) -*/ - -html, body, div, span, applet, object, iframe, -h1, h2, h3, h4, h5, h6, p, blockquote, pre, -a, abbr, acronym, address, big, cite, code, -del, dfn, em, img, ins, kbd, q, s, samp, -small, strike, strong, sub, sup, tt, var, -b, u, i, center, -dl, dt, dd, ol, ul, li, -fieldset, form, label, legend, -table, caption, tbody, tfoot, thead, tr, th, td, -article, aside, canvas, details, embed, -figure, figcaption, footer, header, hgroup, -menu, nav, output, ruby, section, summary, -time, mark, audio, video { - margin: 0; - padding: 0; - border: 0; - font-size: 100%; - font: inherit; - vertical-align: baseline; -} -/* HTML5 display-role reset for older browsers */ -article, aside, details, figcaption, figure, -footer, header, hgroup, menu, nav, section { - display: block; -} -body { - line-height: 1; -} -ol, ul { - list-style: none; -} -blockquote, q { - quotes: none; -} -blockquote:before, blockquote:after, -q:before, q:after { - content: ''; - content: none; -} -table { - border-collapse: collapse; - border-spacing: 0; -} \ No newline at end of file diff --git a/ui/app/css/transitions.css b/ui/app/css/transitions.css deleted file mode 100644 index 393a944f9..000000000 --- a/ui/app/css/transitions.css +++ /dev/null @@ -1,42 +0,0 @@ -/* universal */ -.app-primary .main-enter { - position: absolute; - width: 100%; -} - -/* center position */ -.app-primary.from-right .main-enter-active, -.app-primary.from-left .main-enter-active { - overflow-x: hidden; - transform: translateX(0px); - transition: transform 300ms ease-in; -} - -/* exited positions */ -.app-primary.from-left .main-leave-active { - transform: translateX(360px); - transition: transform 300ms ease-in; -} -.app-primary.from-right .main-leave-active { - transform: translateX(-360px); - transition: transform 300ms ease-in; -} - -/* loader transitions */ -.loader-enter, .loader-leave-active { - opacity: 0.0; - transition: opacity 150 ease-in; -} -.loader-enter-active, .loader-leave { - opacity: 1.0; - transition: opacity 150 ease-in; -} - -/* entering positions */ -.app-primary.from-right .main-enter:not(.main-enter-active) { - transform: translateX(360px); -} -.app-primary.from-left .main-enter:not(.main-enter-active) { - transform: translateX(-360px); -} - diff --git a/ui/app/first-time/init-menu.js b/ui/app/first-time/init-menu.js deleted file mode 100644 index cc7c51bd3..000000000 --- a/ui/app/first-time/init-menu.js +++ /dev/null @@ -1,179 +0,0 @@ -const inherits = require('util').inherits -const EventEmitter = require('events').EventEmitter -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const Mascot = require('../components/mascot') -const actions = require('../actions') -const Tooltip = require('../components/tooltip') -const getCaretCoordinates = require('textarea-caret') - -module.exports = connect(mapStateToProps)(InitializeMenuScreen) - -inherits(InitializeMenuScreen, Component) -function InitializeMenuScreen () { - Component.call(this) - this.animationEventEmitter = new EventEmitter() -} - -function mapStateToProps (state) { - return { - // state from plugin - currentView: state.appState.currentView, - warning: state.appState.warning, - } -} - -InitializeMenuScreen.prototype.render = function () { - var state = this.props - - switch (state.currentView.name) { - - default: - return this.renderMenu(state) - - } -} - -// InitializeMenuScreen.prototype.componentDidMount = function(){ -// document.getElementById('password-box').focus() -// } - -InitializeMenuScreen.prototype.renderMenu = function (state) { - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - h(Mascot, { - animationEventEmitter: this.animationEventEmitter, - }), - - h('h1', { - style: { - fontSize: '1.3em', - textTransform: 'uppercase', - color: '#7F8082', - marginBottom: 10, - }, - }, 'MetaMask'), - - - h('div', [ - h('h3', { - style: { - fontSize: '0.8em', - color: '#7F8082', - display: 'inline', - }, - }, 'Encrypt your new DEN'), - - h(Tooltip, { - title: 'Your DEN is your password-encrypted storage within MetaMask.', - }, [ - h('i.fa.fa-question-circle.pointer', { - style: { - fontSize: '18px', - position: 'relative', - color: 'rgb(247, 134, 28)', - top: '2px', - marginLeft: '4px', - }, - }), - ]), - ]), - - h('span.in-progress-notification', state.warning), - - // password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box', - placeholder: 'New Password (min 8 chars)', - onInput: this.inputChanged.bind(this), - style: { - width: 260, - marginTop: 12, - }, - }), - - // confirm password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box-confirm', - placeholder: 'Confirm Password', - onKeyPress: this.createVaultOnEnter.bind(this), - onInput: this.inputChanged.bind(this), - style: { - width: 260, - marginTop: 16, - }, - }), - - - h('button.primary', { - onClick: this.createNewVaultAndKeychain.bind(this), - style: { - margin: 12, - }, - }, 'Create'), - - h('.flex-row.flex-center.flex-grow', [ - h('p.pointer', { - onClick: this.showRestoreVault.bind(this), - style: { - fontSize: '0.8em', - color: 'rgb(247, 134, 28)', - textDecoration: 'underline', - }, - }, 'Import Existing DEN'), - ]), - - ]) - ) -} - -InitializeMenuScreen.prototype.createVaultOnEnter = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.createNewVaultAndKeychain() - } -} - -InitializeMenuScreen.prototype.componentDidMount = function () { - document.getElementById('password-box').focus() -} - -InitializeMenuScreen.prototype.showRestoreVault = function () { - this.props.dispatch(actions.showRestoreVault()) -} - -InitializeMenuScreen.prototype.createNewVaultAndKeychain = function () { - var passwordBox = document.getElementById('password-box') - var password = passwordBox.value - var passwordConfirmBox = document.getElementById('password-box-confirm') - var passwordConfirm = passwordConfirmBox.value - - if (password.length < 8) { - this.warning = 'password not long enough' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - if (password !== passwordConfirm) { - this.warning = 'passwords don\'t match' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - - this.props.dispatch(actions.createNewVaultAndKeychain(password)) -} - -InitializeMenuScreen.prototype.inputChanged = function (event) { - // tell mascot to look at page action - var element = event.target - var boundingRect = element.getBoundingClientRect() - var coordinates = getCaretCoordinates(element, element.selectionEnd) - this.animationEventEmitter.emit('point', { - x: boundingRect.left + coordinates.left - element.scrollLeft, - y: boundingRect.top + coordinates.top - element.scrollTop, - }) -} diff --git a/ui/app/img/identicon-tardigrade.png b/ui/app/img/identicon-tardigrade.png deleted file mode 100644 index 1742a32b8..000000000 Binary files a/ui/app/img/identicon-tardigrade.png and /dev/null differ diff --git a/ui/app/img/identicon-walrus.png b/ui/app/img/identicon-walrus.png deleted file mode 100644 index d58fae912..000000000 Binary files a/ui/app/img/identicon-walrus.png and /dev/null differ diff --git a/ui/app/info.js b/ui/app/info.js deleted file mode 100644 index e8470de97..000000000 --- a/ui/app/info.js +++ /dev/null @@ -1,154 +0,0 @@ -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)(InfoScreen) - -function mapStateToProps (state) { - return {} -} - -inherits(InfoScreen, Component) -function InfoScreen () { - Component.call(this) -} - -InfoScreen.prototype.render = function () { - const state = this.props - const version = global.platform.getVersion() - - return ( - h('.flex-column.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: (event) => { - state.dispatch(actions.goHome()) - }, - }), - h('h2.page-subtitle', 'Info'), - ]), - - // main view - h('.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-space-around', { - style: { - padding: '20px', - }, - }, [ - // current version number - - h('.info.info-gray', [ - h('div', 'Metamask'), - h('div', { - style: { - marginBottom: '10px', - }, - }, `Version: ${version}`), - ]), - - h('div', { - style: { - marginBottom: '5px', - }}, - [ - h('div', [ - h('a', { - href: 'https://metamask.io/privacy.html', - target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, - }, [ - h('div.info', 'Privacy Policy'), - ]), - ]), - h('div', [ - h('a', { - href: 'https://metamask.io/terms.html', - target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, - }, [ - h('div.info', 'Terms of Use'), - ]), - ]), - h('div', [ - h('a', { - href: 'https://metamask.io/attributions.html', - target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, - }, [ - h('div.info', 'Attributions'), - ]), - ]), - ] - ), - - h('hr', { - style: { - margin: '10px 0 ', - width: '7em', - }, - }), - - h('div', { - style: { - paddingLeft: '30px', - }}, - [ - h('div.fa.fa-github', [ - h('a.info', { - href: 'https://github.com/MetaMask/faq', - target: '_blank', - }, 'Need Help? Read our FAQ!'), - ]), - h('div', [ - h('a', { - href: 'https://metamask.io/', - target: '_blank', - }, [ - h('img.icon-size', { - src: 'images/icon-128.png', - style: { - // IE6-9 - filter: 'grayscale(100%)', - // Microsoft Edge and Firefox 35+ - WebkitFilter: 'grayscale(100%)', - }, - }), - h('div.info', 'Visit our web site'), - ]), - ]), - h('div.fa.fa-slack', [ - h('a.info', { - href: 'http://slack.metamask.io', - target: '_blank', - }, 'Join the conversation on Slack'), - ]), - - h('div.fa.fa-twitter', [ - h('a.info', { - href: 'https://twitter.com/metamask_io', - target: '_blank', - }, 'Follow us on Twitter'), - ]), - - h('div.fa.fa-envelope', [ - h('a.info', { - target: '_blank', - style: { width: '85vw' }, - href: 'mailto:help@metamask.io?subject=Feedback', - }, 'Email us!'), - ]), - ]), - ]), - ]), - ]) - ) -} - -InfoScreen.prototype.navigateTo = function (url) { - global.platform.openWindow({ url }) -} - diff --git a/ui/app/keychains/hd/create-vault-complete.js b/ui/app/keychains/hd/create-vault-complete.js deleted file mode 100644 index a318a9b50..000000000 --- a/ui/app/keychains/hd/create-vault-complete.js +++ /dev/null @@ -1,78 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('../../actions') - -module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen) - -inherits(CreateVaultCompleteScreen, Component) -function CreateVaultCompleteScreen () { - Component.call(this) -} - -function mapStateToProps (state) { - return { - seed: state.appState.currentView.seedWords, - cachedSeed: state.metamask.seedWords, - } -} - -CreateVaultCompleteScreen.prototype.render = function () { - var state = this.props - var seed = state.seed || state.cachedSeed || '' - - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - // // subtitle and nav - // h('.section-title.flex-row.flex-center', [ - // h('h2.page-subtitle', 'Vault Created'), - // ]), - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginTop: 36, - marginBottom: 8, - width: '100%', - fontSize: '20px', - padding: 6, - }, - }, [ - 'Vault Created', - ]), - - h('div', { - style: { - width: '360px', - height: '78px', - fontSize: '1em', - marginTop: '10px', - textAlign: 'center', - }, - }, [ - h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'), - ]), - - h('textarea.twelve-word-phrase', { - readOnly: true, - value: seed, - }), - - h('button.primary', { - onClick: () => this.confirmSeedWords(), - style: { - margin: '24px', - fontSize: '0.9em', - }, - }, 'I\'ve copied it somewhere safe'), - ]) - ) -} - -CreateVaultCompleteScreen.prototype.confirmSeedWords = function () { - this.props.dispatch(actions.confirmSeedWords()) -} diff --git a/ui/app/keychains/hd/recover-seed/confirmation.js b/ui/app/keychains/hd/recover-seed/confirmation.js deleted file mode 100644 index 4ccbec9fc..000000000 --- a/ui/app/keychains/hd/recover-seed/confirmation.js +++ /dev/null @@ -1,118 +0,0 @@ -const inherits = require('util').inherits - -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('../../../actions') - -module.exports = connect(mapStateToProps)(RevealSeedConfirmation) - -inherits(RevealSeedConfirmation, Component) -function RevealSeedConfirmation () { - Component.call(this) -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -RevealSeedConfirmation.prototype.render = function () { - const props = this.props - - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginBottom: 24, - width: '100%', - fontSize: '20px', - padding: 6, - }, - }, [ - 'Reveal Seed Words', - ]), - - h('.div', { - style: { - display: 'flex', - flexDirection: 'column', - padding: '20px', - justifyContent: 'center', - }, - }, [ - - h('h4', 'Do not recover your seed words in a public place! These words can be used to steal all your accounts.'), - - // confirmation - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box', - placeholder: 'Enter your password to confirm', - onKeyPress: this.checkConfirmation.bind(this), - style: { - width: 260, - marginTop: '12px', - }, - }), - - h('.flex-row.flex-space-between', { - style: { - marginTop: 30, - width: '50%', - }, - }, [ - // cancel - h('button.primary', { - onClick: this.goHome.bind(this), - }, 'CANCEL'), - - // submit - h('button.primary', { - onClick: this.revealSeedWords.bind(this), - }, 'OK'), - - ]), - - (props.warning) && ( - h('span.error', { - style: { - margin: '20px', - }, - }, props.warning.split('-')) - ), - - props.inProgress && ( - h('span.in-progress-notification', 'Generating Seed...') - ), - ]), - ]) - ) -} - -RevealSeedConfirmation.prototype.componentDidMount = function () { - document.getElementById('password-box').focus() -} - -RevealSeedConfirmation.prototype.goHome = function () { - this.props.dispatch(actions.showConfigPage(false)) -} - -// create vault - -RevealSeedConfirmation.prototype.checkConfirmation = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.revealSeedWords() - } -} - -RevealSeedConfirmation.prototype.revealSeedWords = function () { - var password = document.getElementById('password-box').value - this.props.dispatch(actions.requestRevealSeed(password)) -} diff --git a/ui/app/keychains/hd/restore-vault.js b/ui/app/keychains/hd/restore-vault.js deleted file mode 100644 index 06e51d9b3..000000000 --- a/ui/app/keychains/hd/restore-vault.js +++ /dev/null @@ -1,152 +0,0 @@ -const inherits = require('util').inherits -const PersistentForm = require('../../../lib/persistent-form') -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('../../actions') - -module.exports = connect(mapStateToProps)(RestoreVaultScreen) - -inherits(RestoreVaultScreen, PersistentForm) -function RestoreVaultScreen () { - PersistentForm.call(this) -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - forgottenPassword: state.appState.forgottenPassword, - } -} - -RestoreVaultScreen.prototype.render = function () { - var state = this.props - this.persistentFormParentId = 'restore-vault-form' - - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginBottom: 24, - width: '100%', - fontSize: '20px', - padding: 6, - }, - }, [ - 'Restore Vault', - ]), - - // wallet seed entry - h('h3', 'Wallet Seed'), - h('textarea.twelve-word-phrase.letter-spacey', { - dataset: { - persistentFormId: 'wallet-seed', - }, - placeholder: 'Enter your secret twelve word phrase here to restore your vault.', - }), - - // password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box', - placeholder: 'New Password (min 8 chars)', - dataset: { - persistentFormId: 'password', - }, - style: { - width: 260, - marginTop: 12, - }, - }), - - // confirm password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box-confirm', - placeholder: 'Confirm Password', - onKeyPress: this.createOnEnter.bind(this), - dataset: { - persistentFormId: 'password-confirmation', - }, - style: { - width: 260, - marginTop: 16, - }, - }), - - (state.warning) && ( - h('span.error.in-progress-notification', state.warning) - ), - - // submit - - h('.flex-row.flex-space-between', { - style: { - marginTop: 30, - width: '50%', - }, - }, [ - - // cancel - h('button.primary', { - onClick: this.showInitializeMenu.bind(this), - }, 'CANCEL'), - - // submit - h('button.primary', { - onClick: this.createNewVaultAndRestore.bind(this), - }, 'OK'), - - ]), - ]) - - ) -} - -RestoreVaultScreen.prototype.showInitializeMenu = function () { - if (this.props.forgottenPassword) { - this.props.dispatch(actions.backToUnlockView()) - } else { - this.props.dispatch(actions.showInitializeMenu()) - } -} - -RestoreVaultScreen.prototype.createOnEnter = function (event) { - if (event.key === 'Enter') { - this.createNewVaultAndRestore() - } -} - -RestoreVaultScreen.prototype.createNewVaultAndRestore = function () { - // check password - var passwordBox = document.getElementById('password-box') - var password = passwordBox.value - var passwordConfirmBox = document.getElementById('password-box-confirm') - var passwordConfirm = passwordConfirmBox.value - if (password.length < 8) { - this.warning = 'Password not long enough' - - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - if (password !== passwordConfirm) { - this.warning = 'Passwords don\'t match' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - // check seed - var seedBox = document.querySelector('textarea.twelve-word-phrase') - var seed = seedBox.value.trim() - if (seed.split(' ').length !== 12) { - this.warning = 'seed phrases are 12 words long' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - // submit - this.warning = null - this.props.dispatch(actions.displayWarning(this.warning)) - this.props.dispatch(actions.createNewVaultAndRestore(password, seed)) -} diff --git a/ui/app/new-keychain.js b/ui/app/new-keychain.js deleted file mode 100644 index cc9633166..000000000 --- a/ui/app/new-keychain.js +++ /dev/null @@ -1,29 +0,0 @@ -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)(NewKeychain) - -function mapStateToProps (state) { - return {} -} - -inherits(NewKeychain, Component) -function NewKeychain () { - Component.call(this) -} - -NewKeychain.prototype.render = function () { - // const props = this.props - - return ( - h('div', { - style: { - background: 'blue', - }, - }, [ - h('h1', `Here's a list!!!!`), - ]) - ) -} diff --git a/ui/app/reducers.js b/ui/app/reducers.js deleted file mode 100644 index 11efca529..000000000 --- a/ui/app/reducers.js +++ /dev/null @@ -1,52 +0,0 @@ -const extend = require('xtend') - -// -// Sub-Reducers take in the complete state and return their sub-state -// -const reduceIdentities = require('./reducers/identities') -const reduceMetamask = require('./reducers/metamask') -const reduceApp = require('./reducers/app') - -window.METAMASK_CACHED_LOG_STATE = null - -module.exports = rootReducer - -function rootReducer (state, action) { - // clone - state = extend(state) - - if (action.type === 'GLOBAL_FORCE_UPDATE') { - return action.value - } - - // - // Identities - // - - state.identities = reduceIdentities(state, action) - - // - // MetaMask - // - - state.metamask = reduceMetamask(state, action) - - // - // AppState - // - - state.appState = reduceApp(state, action) - - window.METAMASK_CACHED_LOG_STATE = state - return state -} - -window.logState = function () { - var stateString = JSON.stringify(window.METAMASK_CACHED_LOG_STATE, removeSeedWords, 2) - console.log(stateString) - return stateString -} - -function removeSeedWords (key, value) { - return key === 'seedWords' ? undefined : value -} diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js deleted file mode 100644 index 2fcc9bfe0..000000000 --- a/ui/app/reducers/app.js +++ /dev/null @@ -1,585 +0,0 @@ -const extend = require('xtend') -const actions = require('../actions') -const txHelper = require('../../lib/tx-helper') - -module.exports = reduceApp - - -function reduceApp (state, action) { - log.debug('App Reducer got ' + action.type) - // clone and defaults - const selectedAddress = state.metamask.selectedAddress - const hasUnconfActions = checkUnconfActions(state) - let name = 'accounts' - if (selectedAddress) { - name = 'accountDetail' - } - if (hasUnconfActions) { - log.debug('pending txs detected, defaulting to conf-tx view.') - name = 'confTx' - } - - var defaultView = { - name, - detailView: null, - context: selectedAddress, - } - - // confirm seed words - var seedWords = state.metamask.seedWords - var seedConfView = { - name: 'createVaultComplete', - seedWords, - } - - // default state - var appState = extend({ - shouldClose: false, - menuOpen: false, - currentView: seedWords ? seedConfView : defaultView, - accountDetail: { - subview: 'transactions', - }, - transForward: true, // Used to render transition direction - isLoading: false, // Used to display loading indicator - warning: null, // Used to display error text - }, state.appState) - - switch (action.type) { - - // transition methods - - case actions.TRANSITION_FORWARD: - return extend(appState, { - transForward: true, - }) - - case actions.TRANSITION_BACKWARD: - return extend(appState, { - transForward: false, - }) - - // intialize - - case actions.SHOW_CREATE_VAULT: - return extend(appState, { - currentView: { - name: 'createVault', - }, - transForward: true, - warning: null, - }) - - case actions.SHOW_RESTORE_VAULT: - return extend(appState, { - currentView: { - name: 'restoreVault', - }, - transForward: true, - forgottenPassword: true, - }) - - case actions.FORGOT_PASSWORD: - return extend(appState, { - currentView: { - name: 'restoreVault', - }, - transForward: false, - forgottenPassword: true, - }) - - case actions.SHOW_INIT_MENU: - return extend(appState, { - currentView: defaultView, - transForward: false, - }) - - case actions.SHOW_CONFIG_PAGE: - return extend(appState, { - currentView: { - name: 'config', - context: appState.currentView.context, - }, - transForward: action.value, - }) - - case actions.SHOW_ADD_TOKEN_PAGE: - return extend(appState, { - currentView: { - name: 'add-token', - context: appState.currentView.context, - }, - transForward: action.value, - }) - - case actions.SHOW_IMPORT_PAGE: - - return extend(appState, { - currentView: { - name: 'import-menu', - }, - transForward: true, - }) - - case actions.SHOW_INFO_PAGE: - return extend(appState, { - currentView: { - name: 'info', - context: appState.currentView.context, - }, - transForward: true, - }) - - case actions.CREATE_NEW_VAULT_IN_PROGRESS: - return extend(appState, { - currentView: { - name: 'createVault', - inProgress: true, - }, - transForward: true, - isLoading: true, - }) - - case actions.SHOW_NEW_VAULT_SEED: - return extend(appState, { - currentView: { - name: 'createVaultComplete', - seedWords: action.value, - }, - transForward: true, - isLoading: false, - }) - - case actions.NEW_ACCOUNT_SCREEN: - return extend(appState, { - currentView: { - name: 'new-account', - context: appState.currentView.context, - }, - transForward: true, - }) - - case actions.SHOW_SEND_PAGE: - return extend(appState, { - currentView: { - name: 'sendTransaction', - context: appState.currentView.context, - }, - transForward: true, - warning: null, - }) - - case actions.SHOW_NEW_KEYCHAIN: - return extend(appState, { - currentView: { - name: 'newKeychain', - context: appState.currentView.context, - }, - transForward: true, - }) - - // unlock - - case actions.UNLOCK_METAMASK: - return extend(appState, { - forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, - detailView: {}, - transForward: true, - isLoading: false, - warning: null, - }) - - case actions.LOCK_METAMASK: - return extend(appState, { - currentView: defaultView, - transForward: false, - warning: null, - }) - - case actions.BACK_TO_INIT_MENU: - return extend(appState, { - warning: null, - transForward: false, - forgottenPassword: true, - currentView: { - name: 'InitMenu', - }, - }) - - case actions.BACK_TO_UNLOCK_VIEW: - return extend(appState, { - warning: null, - transForward: true, - forgottenPassword: false, - currentView: { - name: 'UnlockScreen', - }, - }) - // reveal seed words - - case actions.REVEAL_SEED_CONFIRMATION: - return extend(appState, { - currentView: { - name: 'reveal-seed-conf', - }, - transForward: true, - warning: null, - }) - - // accounts - - case actions.SET_SELECTED_ACCOUNT: - return extend(appState, { - activeAddress: action.value, - }) - - case actions.GO_HOME: - return extend(appState, { - currentView: extend(appState.currentView, { - name: 'accountDetail', - }), - accountDetail: { - subview: 'transactions', - accountExport: 'none', - privateKey: '', - }, - transForward: false, - warning: null, - }) - - case actions.SHOW_ACCOUNT_DETAIL: - return extend(appState, { - forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, - currentView: { - name: 'accountDetail', - context: action.value, - }, - accountDetail: { - subview: 'transactions', - accountExport: 'none', - privateKey: '', - }, - transForward: false, - }) - - case actions.BACK_TO_ACCOUNT_DETAIL: - return extend(appState, { - currentView: { - name: 'accountDetail', - context: action.value, - }, - accountDetail: { - subview: 'transactions', - accountExport: 'none', - privateKey: '', - }, - transForward: false, - }) - - case actions.SHOW_ACCOUNTS_PAGE: - return extend(appState, { - currentView: { - name: seedWords ? 'createVaultComplete' : 'accounts', - seedWords, - }, - transForward: true, - isLoading: false, - warning: null, - scrollToBottom: false, - forgottenPassword: false, - }) - - case actions.SHOW_NOTICE: - return extend(appState, { - transForward: true, - isLoading: false, - }) - - case actions.REVEAL_ACCOUNT: - return extend(appState, { - scrollToBottom: true, - }) - - case actions.SHOW_CONF_TX_PAGE: - return extend(appState, { - currentView: { - name: 'confTx', - context: 0, - }, - transForward: action.transForward, - warning: null, - isLoading: false, - }) - - case actions.SHOW_CONF_MSG_PAGE: - return extend(appState, { - currentView: { - name: hasUnconfActions ? 'confTx' : 'account-detail', - context: 0, - }, - transForward: true, - warning: null, - isLoading: false, - }) - - case actions.COMPLETED_TX: - log.debug('reducing COMPLETED_TX for tx ' + action.value) - const otherUnconfActions = getUnconfActionList(state) - .filter(tx => tx.id !== action.value) - const hasOtherUnconfActions = otherUnconfActions.length > 0 - - if (hasOtherUnconfActions) { - log.debug('reducer detected txs - rendering confTx view') - return extend(appState, { - transForward: false, - currentView: { - name: 'confTx', - context: 0, - }, - warning: null, - }) - } else { - log.debug('attempting to close popup') - return extend(appState, { - // indicate notification should close - shouldClose: true, - transForward: false, - warning: null, - currentView: { - name: 'accountDetail', - context: state.metamask.selectedAddress, - }, - accountDetail: { - subview: 'transactions', - }, - }) - } - - case actions.NEXT_TX: - return extend(appState, { - transForward: true, - currentView: { - name: 'confTx', - context: ++appState.currentView.context, - warning: null, - }, - }) - - case actions.VIEW_PENDING_TX: - const context = indexForPending(state, action.value) - return extend(appState, { - transForward: true, - currentView: { - name: 'confTx', - context, - warning: null, - }, - }) - - case actions.PREVIOUS_TX: - return extend(appState, { - transForward: false, - currentView: { - name: 'confTx', - context: --appState.currentView.context, - warning: null, - }, - }) - - case actions.TRANSACTION_ERROR: - return extend(appState, { - currentView: { - name: 'confTx', - errorMessage: 'There was a problem submitting this transaction.', - }, - }) - - case actions.UNLOCK_FAILED: - return extend(appState, { - warning: action.value || 'Incorrect password. Try again.', - }) - - case actions.SHOW_LOADING: - return extend(appState, { - isLoading: true, - loadingMessage: action.value, - }) - - case actions.HIDE_LOADING: - return extend(appState, { - isLoading: false, - }) - - case actions.SHOW_SUB_LOADING_INDICATION: - return extend(appState, { - isSubLoading: true, - }) - - case actions.HIDE_SUB_LOADING_INDICATION: - return extend(appState, { - isSubLoading: false, - }) - case actions.CLEAR_SEED_WORD_CACHE: - return extend(appState, { - transForward: true, - currentView: {}, - isLoading: false, - accountDetail: { - subview: 'transactions', - accountExport: 'none', - privateKey: '', - }, - }) - - case actions.DISPLAY_WARNING: - return extend(appState, { - warning: action.value, - isLoading: false, - }) - - case actions.HIDE_WARNING: - return extend(appState, { - warning: undefined, - }) - - case actions.REQUEST_ACCOUNT_EXPORT: - return extend(appState, { - transForward: true, - currentView: { - name: 'accountDetail', - context: appState.currentView.context, - }, - accountDetail: { - subview: 'export', - accountExport: 'requested', - }, - }) - - case actions.EXPORT_ACCOUNT: - return extend(appState, { - accountDetail: { - subview: 'export', - accountExport: 'completed', - }, - }) - - case actions.SHOW_PRIVATE_KEY: - return extend(appState, { - accountDetail: { - subview: 'export', - accountExport: 'completed', - privateKey: action.value, - }, - }) - - case actions.BUY_ETH_VIEW: - return extend(appState, { - transForward: true, - currentView: { - name: 'buyEth', - context: appState.currentView.name, - }, - identity: state.metamask.identities[action.value], - buyView: { - subview: 'Coinbase', - amount: '15.00', - buyAddress: action.value, - formView: { - coinbase: true, - shapeshift: false, - }, - }, - }) - - case actions.COINBASE_SUBVIEW: - return extend(appState, { - buyView: { - subview: 'Coinbase', - formView: { - coinbase: true, - shapeshift: false, - }, - buyAddress: appState.buyView.buyAddress, - amount: appState.buyView.amount, - }, - }) - - case actions.SHAPESHIFT_SUBVIEW: - return extend(appState, { - buyView: { - subview: 'ShapeShift', - formView: { - coinbase: false, - shapeshift: true, - marketinfo: action.value.marketinfo, - coinOptions: action.value.coinOptions, - }, - buyAddress: appState.buyView.buyAddress, - amount: appState.buyView.amount, - }, - }) - - case actions.PAIR_UPDATE: - return extend(appState, { - buyView: { - subview: 'ShapeShift', - formView: { - coinbase: false, - shapeshift: true, - marketinfo: action.value.marketinfo, - coinOptions: appState.buyView.formView.coinOptions, - }, - buyAddress: appState.buyView.buyAddress, - amount: appState.buyView.amount, - warning: null, - }, - }) - - case actions.SHOW_QR: - return extend(appState, { - qrRequested: true, - transForward: true, - - Qr: { - message: action.value.message, - data: action.value.data, - }, - }) - - case actions.SHOW_QR_VIEW: - return extend(appState, { - currentView: { - name: 'qr', - context: appState.currentView.context, - }, - transForward: true, - Qr: { - message: action.value.message, - data: action.value.data, - }, - }) - default: - return appState - } -} - -function checkUnconfActions (state) { - const unconfActionList = getUnconfActionList(state) - const hasUnconfActions = unconfActionList.length > 0 - return hasUnconfActions -} - -function getUnconfActionList (state) { - const { unapprovedTxs, unapprovedMsgs, - unapprovedPersonalMsgs, network } = state.metamask - - const unconfActionList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) - return unconfActionList -} - -function indexForPending (state, txId) { - const unconfTxList = getUnconfActionList(state) - const match = unconfTxList.find((tx) => tx.id === txId) - const index = unconfTxList.indexOf(match) - return index -} diff --git a/ui/app/reducers/identities.js b/ui/app/reducers/identities.js deleted file mode 100644 index 341a404e7..000000000 --- a/ui/app/reducers/identities.js +++ /dev/null @@ -1,15 +0,0 @@ -const extend = require('xtend') - -module.exports = reduceIdentities - -function reduceIdentities (state, action) { - // clone + defaults - var idState = extend({ - - }, state.identities) - - switch (action.type) { - default: - return idState - } -} diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js deleted file mode 100644 index e0c416c2d..000000000 --- a/ui/app/reducers/metamask.js +++ /dev/null @@ -1,137 +0,0 @@ -const extend = require('xtend') -const actions = require('../actions') - -module.exports = reduceMetamask - -function reduceMetamask (state, action) { - let newState - - // clone + defaults - var metamaskState = extend({ - isInitialized: false, - isUnlocked: false, - rpcTarget: 'https://rawtestrpc.metamask.io/', - identities: {}, - unapprovedTxs: {}, - noActiveNotices: true, - lastUnreadNotice: undefined, - frequentRpcList: [], - addressBook: [], - }, state.metamask) - - switch (action.type) { - - case actions.SHOW_ACCOUNTS_PAGE: - newState = extend(metamaskState) - delete newState.seedWords - return newState - - case actions.SHOW_NOTICE: - return extend(metamaskState, { - noActiveNotices: false, - lastUnreadNotice: action.value, - }) - - case actions.CLEAR_NOTICES: - return extend(metamaskState, { - noActiveNotices: true, - }) - - case actions.UPDATE_METAMASK_STATE: - return extend(metamaskState, action.value) - - case actions.UNLOCK_METAMASK: - return extend(metamaskState, { - isUnlocked: true, - isInitialized: true, - selectedAddress: action.value, - }) - - case actions.LOCK_METAMASK: - return extend(metamaskState, { - isUnlocked: false, - }) - - case actions.SET_RPC_LIST: - return extend(metamaskState, { - frequentRpcList: action.value, - }) - - case actions.SET_RPC_TARGET: - return extend(metamaskState, { - provider: { - type: 'rpc', - rpcTarget: action.value, - }, - }) - - case actions.SET_PROVIDER_TYPE: - return extend(metamaskState, { - provider: { - type: action.value, - }, - }) - - case actions.COMPLETED_TX: - var stringId = String(action.id) - newState = extend(metamaskState, { - unapprovedTxs: {}, - unapprovedMsgs: {}, - }) - for (const id in metamaskState.unapprovedTxs) { - if (id !== stringId) { - newState.unapprovedTxs[id] = metamaskState.unapprovedTxs[id] - } - } - for (const id in metamaskState.unapprovedMsgs) { - if (id !== stringId) { - newState.unapprovedMsgs[id] = metamaskState.unapprovedMsgs[id] - } - } - return newState - - case actions.SHOW_NEW_VAULT_SEED: - return extend(metamaskState, { - isUnlocked: true, - isInitialized: false, - seedWords: action.value, - }) - - case actions.CLEAR_SEED_WORD_CACHE: - newState = extend(metamaskState, { - isUnlocked: true, - isInitialized: true, - selectedAddress: action.value, - }) - delete newState.seedWords - return newState - - case actions.SHOW_ACCOUNT_DETAIL: - newState = extend(metamaskState, { - isUnlocked: true, - isInitialized: true, - selectedAddress: action.value, - }) - delete newState.seedWords - return newState - - case actions.SAVE_ACCOUNT_LABEL: - const account = action.value.account - const name = action.value.label - var id = {} - id[account] = extend(metamaskState.identities[account], { name }) - var identities = extend(metamaskState.identities, id) - return extend(metamaskState, { identities }) - - case actions.SET_CURRENT_FIAT: - return extend(metamaskState, { - currentCurrency: action.value.currentCurrency, - conversionRate: action.value.conversionRate, - conversionDate: action.value.conversionDate, - }) - - default: - return metamaskState - - } -} diff --git a/ui/app/root.js b/ui/app/root.js deleted file mode 100644 index 9e7314b20..000000000 --- a/ui/app/root.js +++ /dev/null @@ -1,22 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const Provider = require('react-redux').Provider -const h = require('react-hyperscript') -const App = require('./app') - -module.exports = Root - -inherits(Root, Component) -function Root () { Component.call(this) } - -Root.prototype.render = function () { - return ( - - h(Provider, { - store: this.props.store, - }, [ - h(App), - ]) - - ) -} diff --git a/ui/app/send.js b/ui/app/send.js deleted file mode 100644 index a21a219eb..000000000 --- a/ui/app/send.js +++ /dev/null @@ -1,288 +0,0 @@ -const inherits = require('util').inherits -const PersistentForm = require('../lib/persistent-form') -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const Identicon = require('./components/identicon') -const actions = require('./actions') -const util = require('./util') -const numericBalance = require('./util').numericBalance -const addressSummary = require('./util').addressSummary -const isHex = require('./util').isHex -const EthBalance = require('./components/eth-balance') -const EnsInput = require('./components/ens-input') -const ethUtil = require('ethereumjs-util') -module.exports = connect(mapStateToProps)(SendTransactionScreen) - -function mapStateToProps (state) { - var result = { - address: state.metamask.selectedAddress, - accounts: state.metamask.accounts, - identities: state.metamask.identities, - warning: state.appState.warning, - network: state.metamask.network, - addressBook: state.metamask.addressBook, - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - } - - result.error = result.warning && result.warning.split('.')[0] - - result.account = result.accounts[result.address] - result.identity = result.identities[result.address] - result.balance = result.account ? numericBalance(result.account.balance) : null - - return result -} - -inherits(SendTransactionScreen, PersistentForm) -function SendTransactionScreen () { - PersistentForm.call(this) -} - -SendTransactionScreen.prototype.render = function () { - this.persistentFormParentId = 'send-tx-form' - - const props = this.props - const { - address, - account, - identity, - network, - identities, - addressBook, - conversionRate, - currentCurrency, - } = props - - return ( - - h('.send-screen.flex-column.flex-grow', [ - - // - // Sender Profile - // - - h('.account-data-subsection.flex-row.flex-grow', { - style: { - margin: '0 20px', - }, - }, [ - - // header - identicon + nav - h('.flex-row.flex-space-between', { - style: { - marginTop: '15px', - }, - }, [ - // back button - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { - onClick: this.back.bind(this), - }), - - // large identicon - h('.identicon-wrapper.flex-column.flex-center.select-none', [ - h(Identicon, { - diameter: 62, - address: address, - }), - ]), - - // invisible place holder - h('i.fa.fa-users.fa-lg.invisible', { - style: { - marginTop: '28px', - }, - }), - - ]), - - // account label - - h('.flex-column', { - style: { - marginTop: '10px', - alignItems: 'flex-start', - }, - }, [ - h('h2.font-medium.color-forest.flex-center', { - style: { - paddingTop: '8px', - marginBottom: '8px', - }, - }, identity && identity.name), - - // address and getter actions - h('.flex-row.flex-center', { - style: { - marginBottom: '8px', - }, - }, [ - - h('div', { - style: { - lineHeight: '16px', - }, - }, addressSummary(address)), - - ]), - - // balance - h('.flex-row.flex-center', [ - - h(EthBalance, { - value: account && account.balance, - conversionRate, - currentCurrency, - }), - - ]), - ]), - ]), - - // - // Required Fields - // - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginTop: '15px', - marginBottom: '16px', - }, - }, [ - 'Send Transaction', - ]), - - // error message - props.error && h('span.error.flex-center', props.error), - - // 'to' field - h('section.flex-row.flex-center', [ - h(EnsInput, { - name: 'address', - placeholder: 'Recipient Address', - onChange: this.recipientDidChange.bind(this), - network, - identities, - addressBook, - }), - ]), - - // 'amount' and send button - h('section.flex-row.flex-center', [ - - h('input.large-input', { - name: 'amount', - placeholder: 'Amount', - type: 'number', - style: { - marginRight: '6px', - }, - dataset: { - persistentFormId: 'tx-amount', - }, - }), - - h('button.primary', { - onClick: this.onSubmit.bind(this), - style: { - textTransform: 'uppercase', - }, - }, 'Next'), - - ]), - - // - // Optional Fields - // - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginTop: '16px', - marginBottom: '16px', - }, - }, [ - 'Transaction Data (optional)', - ]), - - // 'data' field - h('section.flex-column.flex-center', [ - h('input.large-input', { - name: 'txData', - placeholder: '0x01234', - style: { - width: '100%', - resize: 'none', - }, - dataset: { - persistentFormId: 'tx-data', - }, - }), - ]), - ]) - ) -} - -SendTransactionScreen.prototype.navigateToAccounts = function (event) { - event.stopPropagation() - this.props.dispatch(actions.showAccountsPage()) -} - -SendTransactionScreen.prototype.back = function () { - var address = this.props.address - this.props.dispatch(actions.backToAccountDetail(address)) -} - -SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickname) { - this.setState({ - recipient: recipient, - nickname: nickname, - }) -} - -SendTransactionScreen.prototype.onSubmit = function () { - const state = this.state || {} - const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '') - const nickname = state.nickname || ' ' - const input = document.querySelector('input[name="amount"]').value - const value = util.normalizeEthStringToWei(input) - const txData = document.querySelector('input[name="txData"]').value - const balance = this.props.balance - let message - - if (value.gt(balance)) { - message = 'Insufficient funds.' - return this.props.dispatch(actions.displayWarning(message)) - } - - if (input < 0) { - message = 'Can not send negative amounts of ETH.' - return this.props.dispatch(actions.displayWarning(message)) - } - - if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) { - message = 'Recipient address is invalid.' - return this.props.dispatch(actions.displayWarning(message)) - } - - if (!isHex(ethUtil.stripHexPrefix(txData)) && txData) { - message = 'Transaction data must be hex string.' - return this.props.dispatch(actions.displayWarning(message)) - } - - this.props.dispatch(actions.hideWarning()) - - this.props.dispatch(actions.addToAddressBook(recipient, nickname)) - - var txParams = { - from: this.props.address, - value: '0x' + value.toString(16), - } - - if (recipient) txParams.to = ethUtil.addHexPrefix(recipient) - if (txData) txParams.data = txData - - this.props.dispatch(actions.signTx(txParams)) -} diff --git a/ui/app/settings.js b/ui/app/settings.js deleted file mode 100644 index 454cc95e0..000000000 --- a/ui/app/settings.js +++ /dev/null @@ -1,59 +0,0 @@ -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)(AppSettingsPage) - -function mapStateToProps (state) { - return {} -} - -inherits(AppSettingsPage, Component) -function AppSettingsPage () { - Component.call(this) -} - -AppSettingsPage.prototype.render = function () { - return ( - - h('.account-detail-section.flex-column.flex-grow', [ - - // subtitle and nav - h('.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.navigateToAccounts.bind(this), - }), - h('h2.page-subtitle', 'Settings'), - ]), - - h('label', { - htmlFor: 'settings-rpc-endpoint', - }, 'RPC Endpoint:'), - h('input', { - type: 'url', - id: 'settings-rpc-endpoint', - onKeyPress: this.onKeyPress.bind(this), - }), - - ]) - - ) -} - -AppSettingsPage.prototype.componentDidMount = function () { - document.querySelector('input').focus() -} - -AppSettingsPage.prototype.onKeyPress = function (event) { - // get submit event - if (event.key === 'Enter') { - // this.submitPassword(event) - } -} - -AppSettingsPage.prototype.navigateToAccounts = function (event) { - event.stopPropagation() - this.props.dispatch(actions.showAccountsPage()) -} diff --git a/ui/app/store.js b/ui/app/store.js deleted file mode 100644 index ba9e58b49..000000000 --- a/ui/app/store.js +++ /dev/null @@ -1,21 +0,0 @@ -const createStore = require('redux').createStore -const applyMiddleware = require('redux').applyMiddleware -const thunkMiddleware = require('redux-thunk') -const rootReducer = require('./reducers') -const createLogger = require('redux-logger') - -global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' - -module.exports = configureStore - -const loggerMiddleware = createLogger({ - predicate: () => global.METAMASK_DEBUG, -}) - -const middlewares = [thunkMiddleware, loggerMiddleware] - -const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore) - -function configureStore (initialState) { - return createStoreWithMiddleware(rootReducer, initialState) -} diff --git a/ui/app/template.js b/ui/app/template.js deleted file mode 100644 index d15b30fd2..000000000 --- a/ui/app/template.js +++ /dev/null @@ -1,30 +0,0 @@ -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)(COMPONENTNAME) - -function mapStateToProps (state) { - return {} -} - -inherits(COMPONENTNAME, Component) -function COMPONENTNAME () { - Component.call(this) -} - -COMPONENTNAME.prototype.render = function () { - const props = this.props - - return ( - h('div', { - style: { - background: 'blue', - }, - }, [ - `Hello, ${props.sender}`, - ]) - ) -} - diff --git a/ui/app/unlock.js b/ui/app/unlock.js deleted file mode 100644 index 1aee3c5d0..000000000 --- a/ui/app/unlock.js +++ /dev/null @@ -1,118 +0,0 @@ -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 getCaretCoordinates = require('textarea-caret') -const EventEmitter = require('events').EventEmitter - -const Mascot = require('./components/mascot') - -module.exports = connect(mapStateToProps)(UnlockScreen) - -inherits(UnlockScreen, Component) -function UnlockScreen () { - Component.call(this) - this.animationEventEmitter = new EventEmitter() -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -UnlockScreen.prototype.render = function () { - const state = this.props - const warning = state.warning - return ( - h('.flex-column', [ - h('.unlock-screen.flex-column.flex-center.flex-grow', [ - - h(Mascot, { - animationEventEmitter: this.animationEventEmitter, - }), - - h('h1', { - style: { - fontSize: '1.4em', - textTransform: 'uppercase', - color: '#7F8082', - }, - }, 'MetaMask'), - - h('input.large-input', { - type: 'password', - id: 'password-box', - placeholder: 'enter password', - style: { - - }, - onKeyPress: this.onKeyPress.bind(this), - onInput: this.inputChanged.bind(this), - }), - - h('.error', { - style: { - display: warning ? 'block' : 'none', - padding: '0 20px', - textAlign: 'center', - }, - }, warning), - - h('button.primary.cursor-pointer', { - onClick: this.onSubmit.bind(this), - style: { - margin: 10, - }, - }, 'Unlock'), - ]), - - h('.flex-row.flex-center.flex-grow', [ - h('p.pointer', { - onClick: () => this.props.dispatch(actions.forgotPassword()), - style: { - fontSize: '0.8em', - color: 'rgb(247, 134, 28)', - textDecoration: 'underline', - }, - }, 'I forgot my password.'), - ]), - ]) - ) -} - -UnlockScreen.prototype.componentDidMount = function () { - document.getElementById('password-box').focus() -} - -UnlockScreen.prototype.onSubmit = function (event) { - const input = document.getElementById('password-box') - const password = input.value - this.props.dispatch(actions.tryUnlockMetamask(password)) -} - -UnlockScreen.prototype.onKeyPress = function (event) { - if (event.key === 'Enter') { - this.submitPassword(event) - } -} - -UnlockScreen.prototype.submitPassword = function (event) { - var element = event.target - var password = element.value - // reset input - element.value = '' - this.props.dispatch(actions.tryUnlockMetamask(password)) -} - -UnlockScreen.prototype.inputChanged = function (event) { - // tell mascot to look at page action - var element = event.target - var boundingRect = element.getBoundingClientRect() - var coordinates = getCaretCoordinates(element, element.selectionEnd) - this.animationEventEmitter.emit('point', { - x: boundingRect.left + coordinates.left - element.scrollLeft, - y: boundingRect.top + coordinates.top - element.scrollTop, - }) -} diff --git a/ui/app/util.js b/ui/app/util.js deleted file mode 100644 index ac3f42c6b..000000000 --- a/ui/app/util.js +++ /dev/null @@ -1,217 +0,0 @@ -const ethUtil = require('ethereumjs-util') - -var valueTable = { - wei: '1000000000000000000', - kwei: '1000000000000000', - mwei: '1000000000000', - gwei: '1000000000', - szabo: '1000000', - finney: '1000', - ether: '1', - kether: '0.001', - mether: '0.000001', - gether: '0.000000001', - tether: '0.000000000001', -} -var bnTable = {} -for (var currency in valueTable) { - bnTable[currency] = new ethUtil.BN(valueTable[currency], 10) -} - -module.exports = { - valuesFor: valuesFor, - addressSummary: addressSummary, - miniAddressSummary: miniAddressSummary, - isAllOneCase: isAllOneCase, - isValidAddress: isValidAddress, - numericBalance: numericBalance, - parseBalance: parseBalance, - formatBalance: formatBalance, - generateBalanceObject: generateBalanceObject, - dataSize: dataSize, - readableDate: readableDate, - normalizeToWei: normalizeToWei, - normalizeEthStringToWei: normalizeEthStringToWei, - normalizeNumberToWei: normalizeNumberToWei, - valueTable: valueTable, - bnTable: bnTable, - isHex: isHex, -} - -function valuesFor (obj) { - if (!obj) return [] - return Object.keys(obj) - .map(function (key) { return obj[key] }) -} - -function addressSummary (address, firstSegLength = 10, lastSegLength = 4, includeHex = true) { - if (!address) return '' - let checked = ethUtil.toChecksumAddress(address) - if (!includeHex) { - checked = ethUtil.stripHexPrefix(checked) - } - return checked ? checked.slice(0, firstSegLength) + '...' + checked.slice(checked.length - lastSegLength) : '...' -} - -function miniAddressSummary (address) { - if (!address) return '' - var checked = ethUtil.toChecksumAddress(address) - return checked ? checked.slice(0, 4) + '...' + checked.slice(-4) : '...' -} - -function isValidAddress (address) { - var prefixed = ethUtil.addHexPrefix(address) - if (address === '0x0000000000000000000000000000000000000000') return false - return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) -} - -function isAllOneCase (address) { - if (!address) return true - var lower = address.toLowerCase() - var upper = address.toUpperCase() - return address === lower || address === upper -} - -// Takes wei Hex, returns wei BN, even if input is null -function numericBalance (balance) { - if (!balance) return new ethUtil.BN(0, 16) - var stripped = ethUtil.stripHexPrefix(balance) - return new ethUtil.BN(stripped, 16) -} - -// Takes hex, returns [beforeDecimal, afterDecimal] -function parseBalance (balance) { - var beforeDecimal, afterDecimal - const wei = numericBalance(balance) - var weiString = wei.toString() - const trailingZeros = /0+$/ - - beforeDecimal = weiString.length > 18 ? weiString.slice(0, weiString.length - 18) : '0' - afterDecimal = ('000000000000000000' + wei).slice(-18).replace(trailingZeros, '') - if (afterDecimal === '') { afterDecimal = '0' } - return [beforeDecimal, afterDecimal] -} - -// Takes wei hex, returns an object with three properties. -// Its "formatted" property is what we generally use to render values. -function formatBalance (balance, decimalsToKeep, needsParse = true) { - var parsed = needsParse ? parseBalance(balance) : balance.split('.') - var beforeDecimal = parsed[0] - var afterDecimal = parsed[1] - var formatted = 'None' - if (decimalsToKeep === undefined) { - if (beforeDecimal === '0') { - if (afterDecimal !== '0') { - var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits - if (sigFigs) { afterDecimal = sigFigs[0] } - formatted = '0.' + afterDecimal + ' ETH' - } - } else { - formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ' ETH' - } - } else { - afterDecimal += Array(decimalsToKeep).join('0') - formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ' ETH' - } - return formatted -} - - -function generateBalanceObject (formattedBalance, decimalsToKeep = 1) { - var balance = formattedBalance.split(' ')[0] - var label = formattedBalance.split(' ')[1] - var beforeDecimal = balance.split('.')[0] - var afterDecimal = balance.split('.')[1] - var shortBalance = shortenBalance(balance, decimalsToKeep) - - if (beforeDecimal === '0' && afterDecimal.substr(0, 5) === '00000') { - // eslint-disable-next-line eqeqeq - if (afterDecimal == 0) { - balance = '0' - } else { - balance = '<1.0e-5' - } - } else if (beforeDecimal !== '0') { - balance = `${beforeDecimal}.${afterDecimal.slice(0, decimalsToKeep)}` - } - - return { balance, label, shortBalance } -} - -function shortenBalance (balance, decimalsToKeep = 1) { - var truncatedValue - var convertedBalance = parseFloat(balance) - if (convertedBalance > 1000000) { - truncatedValue = (balance / 1000000).toFixed(decimalsToKeep) - return `${truncatedValue}m` - } else if (convertedBalance > 1000) { - truncatedValue = (balance / 1000).toFixed(decimalsToKeep) - return `${truncatedValue}k` - } else if (convertedBalance === 0) { - return '0' - } else if (convertedBalance < 0.001) { - return '<0.001' - } else if (convertedBalance < 1) { - var stringBalance = convertedBalance.toString() - if (stringBalance.split('.')[1].length > 3) { - return convertedBalance.toFixed(3) - } else { - return stringBalance - } - } else { - return convertedBalance.toFixed(decimalsToKeep) - } -} - -function dataSize (data) { - var size = data ? ethUtil.stripHexPrefix(data).length : 0 - return size + ' bytes' -} - -// Takes a BN and an ethereum currency name, -// returns a BN in wei -function normalizeToWei (amount, currency) { - try { - return amount.mul(bnTable.wei).div(bnTable[currency]) - } catch (e) {} - return amount -} - -function normalizeEthStringToWei (str) { - const parts = str.split('.') - let eth = new ethUtil.BN(parts[0], 10).mul(bnTable.wei) - if (parts[1]) { - var decimal = parts[1] - while (decimal.length < 18) { - decimal += '0' - } - const decimalBN = new ethUtil.BN(decimal, 10) - eth = eth.add(decimalBN) - } - return eth -} - -var multiple = new ethUtil.BN('10000', 10) -function normalizeNumberToWei (n, currency) { - var enlarged = n * 10000 - var amount = new ethUtil.BN(String(enlarged), 10) - return normalizeToWei(amount, currency).div(multiple) -} - -function readableDate (ms) { - var date = new Date(ms) - var month = date.getMonth() - var day = date.getDate() - var year = date.getFullYear() - var hours = date.getHours() - var minutes = '0' + date.getMinutes() - var seconds = '0' + date.getSeconds() - - var dateStr = `${month}/${day}/${year}` - var time = `${hours}:${minutes.substr(-2)}:${seconds.substr(-2)}` - return `${dateStr} ${time}` -} - -function isHex (str) { - return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/)) -} -- cgit v1.2.3 From 86d367957fe8ac04462f716fe0ba2bfa4e5ff3f6 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 20 Jul 2017 12:38:38 -0700 Subject: Move responsive ui into its own folder for easier merges --- ui/app/account-detail.js | 311 +++++++ ui/app/accounts/account-list-item.js | 91 ++ ui/app/accounts/import/index.js | 100 ++ ui/app/accounts/import/json.js | 100 ++ ui/app/accounts/import/private-key.js | 67 ++ ui/app/accounts/import/seed.js | 30 + ui/app/accounts/index.js | 164 ++++ ui/app/actions.js | 1031 +++++++++++++++++++++ ui/app/add-token.js | 219 +++++ ui/app/app.js | 591 ++++++++++++ ui/app/components/account-export.js | 122 +++ ui/app/components/account-info-link.js | 41 + ui/app/components/account-panel.js | 86 ++ ui/app/components/balance.js | 89 ++ ui/app/components/binary-renderer.js | 46 + ui/app/components/bn-as-decimal-input.js | 174 ++++ ui/app/components/buy-button-subview.js | 197 ++++ ui/app/components/coinbase-form.js | 63 ++ ui/app/components/copyButton.js | 59 ++ ui/app/components/copyable.js | 46 + ui/app/components/custom-radio-list.js | 60 ++ ui/app/components/drop-menu-item.js | 59 ++ ui/app/components/editable-label.js | 51 + ui/app/components/ens-input.js | 170 ++++ ui/app/components/eth-balance.js | 89 ++ ui/app/components/fiat-value.js | 63 ++ ui/app/components/hex-as-decimal-input.js | 154 +++ ui/app/components/identicon.js | 72 ++ ui/app/components/loading.js | 53 ++ ui/app/components/mascot.js | 59 ++ ui/app/components/mini-account-panel.js | 74 ++ ui/app/components/network.js | 124 +++ ui/app/components/notice.js | 126 +++ ui/app/components/pending-msg-details.js | 50 + ui/app/components/pending-msg.js | 56 ++ ui/app/components/pending-personal-msg-details.js | 60 ++ ui/app/components/pending-personal-msg.js | 47 + ui/app/components/pending-tx.js | 480 ++++++++++ ui/app/components/qr-code.js | 79 ++ ui/app/components/range-slider.js | 58 ++ ui/app/components/shapeshift-form.js | 306 ++++++ ui/app/components/shift-list-item.js | 204 ++++ ui/app/components/tab-bar.js | 36 + ui/app/components/template.js | 18 + ui/app/components/token-cell.js | 72 ++ ui/app/components/token-list.js | 192 ++++ ui/app/components/tooltip.js | 22 + ui/app/components/transaction-list-item-icon.js | 68 ++ ui/app/components/transaction-list-item.js | 165 ++++ ui/app/components/transaction-list.js | 79 ++ ui/app/conf-tx.js | 213 +++++ ui/app/config.js | 211 +++++ ui/app/conversion.json | 207 +++++ ui/app/css/debug.css | 21 + ui/app/css/fonts.css | 36 + ui/app/css/index.css | 667 +++++++++++++ ui/app/css/lib.css | 268 ++++++ ui/app/css/reset.css | 48 + ui/app/css/transitions.css | 42 + ui/app/first-time/init-menu.js | 179 ++++ ui/app/img/identicon-tardigrade.png | Bin 0 -> 141119 bytes ui/app/img/identicon-walrus.png | Bin 0 -> 388973 bytes ui/app/info.js | 154 +++ ui/app/keychains/hd/create-vault-complete.js | 78 ++ ui/app/keychains/hd/recover-seed/confirmation.js | 118 +++ ui/app/keychains/hd/restore-vault.js | 152 +++ ui/app/new-keychain.js | 29 + ui/app/reducers.js | 52 ++ ui/app/reducers/app.js | 585 ++++++++++++ ui/app/reducers/identities.js | 15 + ui/app/reducers/metamask.js | 137 +++ ui/app/root.js | 22 + ui/app/send.js | 288 ++++++ ui/app/settings.js | 59 ++ ui/app/store.js | 21 + ui/app/template.js | 30 + ui/app/unlock.js | 118 +++ ui/app/util.js | 217 +++++ 78 files changed, 10740 insertions(+) create mode 100644 ui/app/account-detail.js create mode 100644 ui/app/accounts/account-list-item.js create mode 100644 ui/app/accounts/import/index.js create mode 100644 ui/app/accounts/import/json.js create mode 100644 ui/app/accounts/import/private-key.js create mode 100644 ui/app/accounts/import/seed.js create mode 100644 ui/app/accounts/index.js create mode 100644 ui/app/actions.js create mode 100644 ui/app/add-token.js create mode 100644 ui/app/app.js create mode 100644 ui/app/components/account-export.js create mode 100644 ui/app/components/account-info-link.js create mode 100644 ui/app/components/account-panel.js create mode 100644 ui/app/components/balance.js create mode 100644 ui/app/components/binary-renderer.js create mode 100644 ui/app/components/bn-as-decimal-input.js create mode 100644 ui/app/components/buy-button-subview.js create mode 100644 ui/app/components/coinbase-form.js create mode 100644 ui/app/components/copyButton.js create mode 100644 ui/app/components/copyable.js create mode 100644 ui/app/components/custom-radio-list.js create mode 100644 ui/app/components/drop-menu-item.js create mode 100644 ui/app/components/editable-label.js create mode 100644 ui/app/components/ens-input.js create mode 100644 ui/app/components/eth-balance.js create mode 100644 ui/app/components/fiat-value.js create mode 100644 ui/app/components/hex-as-decimal-input.js create mode 100644 ui/app/components/identicon.js create mode 100644 ui/app/components/loading.js create mode 100644 ui/app/components/mascot.js create mode 100644 ui/app/components/mini-account-panel.js create mode 100644 ui/app/components/network.js create mode 100644 ui/app/components/notice.js create mode 100644 ui/app/components/pending-msg-details.js create mode 100644 ui/app/components/pending-msg.js create mode 100644 ui/app/components/pending-personal-msg-details.js create mode 100644 ui/app/components/pending-personal-msg.js create mode 100644 ui/app/components/pending-tx.js create mode 100644 ui/app/components/qr-code.js create mode 100644 ui/app/components/range-slider.js create mode 100644 ui/app/components/shapeshift-form.js create mode 100644 ui/app/components/shift-list-item.js create mode 100644 ui/app/components/tab-bar.js create mode 100644 ui/app/components/template.js create mode 100644 ui/app/components/token-cell.js create mode 100644 ui/app/components/token-list.js create mode 100644 ui/app/components/tooltip.js create mode 100644 ui/app/components/transaction-list-item-icon.js create mode 100644 ui/app/components/transaction-list-item.js create mode 100644 ui/app/components/transaction-list.js create mode 100644 ui/app/conf-tx.js create mode 100644 ui/app/config.js create mode 100644 ui/app/conversion.json create mode 100644 ui/app/css/debug.css create mode 100644 ui/app/css/fonts.css create mode 100644 ui/app/css/index.css create mode 100644 ui/app/css/lib.css create mode 100644 ui/app/css/reset.css create mode 100644 ui/app/css/transitions.css create mode 100644 ui/app/first-time/init-menu.js create mode 100644 ui/app/img/identicon-tardigrade.png create mode 100644 ui/app/img/identicon-walrus.png create mode 100644 ui/app/info.js create mode 100644 ui/app/keychains/hd/create-vault-complete.js create mode 100644 ui/app/keychains/hd/recover-seed/confirmation.js create mode 100644 ui/app/keychains/hd/restore-vault.js create mode 100644 ui/app/new-keychain.js create mode 100644 ui/app/reducers.js create mode 100644 ui/app/reducers/app.js create mode 100644 ui/app/reducers/identities.js create mode 100644 ui/app/reducers/metamask.js create mode 100644 ui/app/root.js create mode 100644 ui/app/send.js create mode 100644 ui/app/settings.js create mode 100644 ui/app/store.js create mode 100644 ui/app/template.js create mode 100644 ui/app/unlock.js create mode 100644 ui/app/util.js (limited to 'ui/app') diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js new file mode 100644 index 000000000..bed05a7fb --- /dev/null +++ b/ui/app/account-detail.js @@ -0,0 +1,311 @@ +const inherits = require('util').inherits +const extend = require('xtend') +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const CopyButton = require('./components/copyButton') +const AccountInfoLink = require('./components/account-info-link') +const actions = require('./actions') +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +const valuesFor = require('./util').valuesFor + +const Identicon = require('./components/identicon') +const EthBalance = require('./components/eth-balance') +const TransactionList = require('./components/transaction-list') +const ExportAccountView = require('./components/account-export') +const ethUtil = require('ethereumjs-util') +const EditableLabel = require('./components/editable-label') +const Tooltip = require('./components/tooltip') +const TabBar = require('./components/tab-bar') +const TokenList = require('./components/token-list') + +module.exports = connect(mapStateToProps)(AccountDetailScreen) + +function mapStateToProps (state) { + return { + metamask: state.metamask, + identities: state.metamask.identities, + accounts: state.metamask.accounts, + address: state.metamask.selectedAddress, + accountDetail: state.appState.accountDetail, + network: state.metamask.network, + unapprovedMsgs: valuesFor(state.metamask.unapprovedMsgs), + shapeShiftTxList: state.metamask.shapeShiftTxList, + transactions: state.metamask.selectedAddressTxList || [], + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + currentAccountTab: state.metamask.currentAccountTab, + tokens: state.metamask.tokens, + } +} + +inherits(AccountDetailScreen, Component) +function AccountDetailScreen () { + Component.call(this) +} + +AccountDetailScreen.prototype.render = function () { + var props = this.props + var selected = props.address || Object.keys(props.accounts)[0] + var checksumAddress = selected && ethUtil.toChecksumAddress(selected) + var identity = props.identities[selected] + var account = props.accounts[selected] + const { network, conversionRate, currentCurrency } = props + + return ( + + h('.account-detail-section', [ + + // identicon, label, balance, etc + h('.account-data-subsection', { + style: { + margin: '0 20px', + }, + }, [ + + // header - identicon + nav + h('div', { + style: { + paddingTop: '20px', + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'flex-start', + }, + }, [ + + // large identicon and addresses + h('.identicon-wrapper.select-none', [ + h(Identicon, { + diameter: 62, + address: selected, + }), + ]), + h('flex-column', { + style: { + lineHeight: '10px', + marginLeft: '15px', + }, + }, [ + h(EditableLabel, { + textValue: identity ? identity.name : '', + state: { + isEditingLabel: false, + }, + saveText: (text) => { + props.dispatch(actions.saveAccountLabel(selected, text)) + }, + }, [ + + // What is shown when not editing + edit text: + h('label.editing-label', [h('.edit-text', 'edit')]), + h('h2.font-medium.color-forest', {name: 'edit'}, identity && identity.name), + ]), + h('.flex-row', { + style: { + width: '15em', + justifyContent: 'space-between', + alignItems: 'baseline', + }, + }, [ + + // address + + h('div', { + style: { + overflow: 'hidden', + textOverflow: 'ellipsis', + paddingTop: '3px', + width: '5em', + fontSize: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + marginTop: '10px', + marginBottom: '15px', + color: '#AEAEAE', + }, + }, checksumAddress), + + // copy and export + + h('.flex-row', { + style: { + justifyContent: 'flex-end', + }, + }, [ + + h(AccountInfoLink, { selected, network }), + + h(CopyButton, { + value: checksumAddress, + }), + + h(Tooltip, { + title: 'QR Code', + }, [ + h('i.fa.fa-qrcode.pointer.pop-hover', { + onClick: () => props.dispatch(actions.showQrView(selected, identity ? identity.name : '')), + style: { + fontSize: '18px', + position: 'relative', + color: 'rgb(247, 134, 28)', + top: '5px', + marginLeft: '3px', + marginRight: '3px', + }, + }), + ]), + + h(Tooltip, { + title: 'Export Private Key', + }, [ + h('div', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + h('img.cursor-pointer.color-orange', { + src: 'images/key-32.png', + onClick: () => this.requestAccountExport(selected), + style: { + height: '19px', + }, + }), + ]), + ]), + ]), + ]), + + // account ballence + + ]), + ]), + h('.flex-row', { + style: { + justifyContent: 'space-between', + alignItems: 'flex-start', + }, + }, [ + + h(EthBalance, { + value: account && account.balance, + conversionRate, + currentCurrency, + style: { + lineHeight: '7px', + marginTop: '10px', + }, + }), + + h('button', { + onClick: () => props.dispatch(actions.buyEthView(selected)), + style: { + marginBottom: '20px', + marginRight: '8px', + position: 'absolute', + left: '219px', + }, + }, 'BUY'), + + h('button', { + onClick: () => props.dispatch(actions.showSendPage()), + style: { + marginBottom: '20px', + marginRight: '8px', + }, + }, 'SEND'), + + ]), + ]), + + // subview (tx history, pk export confirm, buy eth warning) + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + this.subview(), + ]), + + ]) + ) +} + +AccountDetailScreen.prototype.subview = function () { + var subview + try { + subview = this.props.accountDetail.subview + } catch (e) { + subview = null + } + + switch (subview) { + case 'transactions': + return this.tabSections() + case 'export': + var state = extend({key: 'export'}, this.props) + return h(ExportAccountView, state) + default: + return this.tabSections() + } +} + +AccountDetailScreen.prototype.tabSections = function () { + const { currentAccountTab } = this.props + + return h('section.tabSection', [ + + h(TabBar, { + tabs: [ + { content: 'Sent', key: 'history' }, + { content: 'Tokens', key: 'tokens' }, + ], + defaultTab: currentAccountTab || 'history', + tabSelected: (key) => { + this.props.dispatch(actions.setCurrentAccountTab(key)) + }, + }), + + this.tabSwitchView(), + ]) +} + +AccountDetailScreen.prototype.tabSwitchView = function () { + const props = this.props + const { address, network } = props + const { currentAccountTab, tokens } = this.props + + switch (currentAccountTab) { + case 'tokens': + return h(TokenList, { + userAddress: address, + network, + tokens, + addToken: () => this.props.dispatch(actions.showAddTokenPage()), + }) + default: + return this.transactionList() + } +} + +AccountDetailScreen.prototype.transactionList = function () { + const {transactions, unapprovedMsgs, address, + network, shapeShiftTxList, conversionRate } = this.props + + return h(TransactionList, { + transactions: transactions.sort((a, b) => b.time - a.time), + network, + unapprovedMsgs, + conversionRate, + address, + shapeShiftTxList, + viewPendingTx: (txId) => { + this.props.dispatch(actions.viewPendingTx(txId)) + }, + }) +} + +AccountDetailScreen.prototype.requestAccountExport = function () { + this.props.dispatch(actions.requestExportAccount()) +} 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()) +} diff --git a/ui/app/actions.js b/ui/app/actions.js new file mode 100644 index 000000000..d99291e46 --- /dev/null +++ b/ui/app/actions.js @@ -0,0 +1,1031 @@ +const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url') + +var actions = { + _setBackgroundConnection: _setBackgroundConnection, + + GO_HOME: 'GO_HOME', + goHome: goHome, + // menu state + getNetworkStatus: 'getNetworkStatus', + // transition state + TRANSITION_FORWARD: 'TRANSITION_FORWARD', + TRANSITION_BACKWARD: 'TRANSITION_BACKWARD', + transitionForward, + transitionBackward, + // remote state + UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE', + updateMetamaskState: updateMetamaskState, + // notices + MARK_NOTICE_READ: 'MARK_NOTICE_READ', + markNoticeRead: markNoticeRead, + SHOW_NOTICE: 'SHOW_NOTICE', + showNotice: showNotice, + CLEAR_NOTICES: 'CLEAR_NOTICES', + clearNotices: clearNotices, + markAccountsFound, + // intialize screen + CREATE_NEW_VAULT_IN_PROGRESS: 'CREATE_NEW_VAULT_IN_PROGRESS', + SHOW_CREATE_VAULT: 'SHOW_CREATE_VAULT', + SHOW_RESTORE_VAULT: 'SHOW_RESTORE_VAULT', + FORGOT_PASSWORD: 'FORGOT_PASSWORD', + forgotPassword: forgotPassword, + SHOW_INIT_MENU: 'SHOW_INIT_MENU', + SHOW_NEW_VAULT_SEED: 'SHOW_NEW_VAULT_SEED', + SHOW_INFO_PAGE: 'SHOW_INFO_PAGE', + SHOW_IMPORT_PAGE: 'SHOW_IMPORT_PAGE', + unlockMetamask: unlockMetamask, + unlockFailed: unlockFailed, + showCreateVault: showCreateVault, + showRestoreVault: showRestoreVault, + showInitializeMenu: showInitializeMenu, + showImportPage, + createNewVaultAndKeychain: createNewVaultAndKeychain, + createNewVaultAndRestore: createNewVaultAndRestore, + createNewVaultInProgress: createNewVaultInProgress, + addNewKeyring, + importNewAccount, + addNewAccount, + NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', + navigateToNewAccountScreen, + showNewVaultSeed: showNewVaultSeed, + showInfoPage: showInfoPage, + // seed recovery actions + REVEAL_SEED_CONFIRMATION: 'REVEAL_SEED_CONFIRMATION', + revealSeedConfirmation: revealSeedConfirmation, + requestRevealSeed: requestRevealSeed, + // unlock screen + UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS', + UNLOCK_FAILED: 'UNLOCK_FAILED', + UNLOCK_METAMASK: 'UNLOCK_METAMASK', + LOCK_METAMASK: 'LOCK_METAMASK', + tryUnlockMetamask: tryUnlockMetamask, + lockMetamask: lockMetamask, + unlockInProgress: unlockInProgress, + // error handling + displayWarning: displayWarning, + DISPLAY_WARNING: 'DISPLAY_WARNING', + HIDE_WARNING: 'HIDE_WARNING', + hideWarning: hideWarning, + // accounts screen + SET_SELECTED_ACCOUNT: 'SET_SELECTED_ACCOUNT', + SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL', + SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE', + SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE', + SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE', + SET_CURRENT_FIAT: 'SET_CURRENT_FIAT', + setCurrentCurrency: setCurrentCurrency, + setCurrentAccountTab, + // account detail screen + SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', + showSendPage: showSendPage, + ADD_TO_ADDRESS_BOOK: 'ADD_TO_ADDRESS_BOOK', + addToAddressBook: addToAddressBook, + REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT', + requestExportAccount: requestExportAccount, + EXPORT_ACCOUNT: 'EXPORT_ACCOUNT', + exportAccount: exportAccount, + SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY', + showPrivateKey: showPrivateKey, + SAVE_ACCOUNT_LABEL: 'SAVE_ACCOUNT_LABEL', + saveAccountLabel: saveAccountLabel, + // tx conf screen + COMPLETED_TX: 'COMPLETED_TX', + TRANSACTION_ERROR: 'TRANSACTION_ERROR', + NEXT_TX: 'NEXT_TX', + PREVIOUS_TX: 'PREV_TX', + signMsg: signMsg, + cancelMsg: cancelMsg, + signPersonalMsg, + cancelPersonalMsg, + sendTx: sendTx, + signTx: signTx, + updateAndApproveTx, + cancelTx: cancelTx, + completedTx: completedTx, + txError: txError, + nextTx: nextTx, + previousTx: previousTx, + viewPendingTx: viewPendingTx, + VIEW_PENDING_TX: 'VIEW_PENDING_TX', + // app messages + confirmSeedWords: confirmSeedWords, + showAccountDetail: showAccountDetail, + BACK_TO_ACCOUNT_DETAIL: 'BACK_TO_ACCOUNT_DETAIL', + backToAccountDetail: backToAccountDetail, + showAccountsPage: showAccountsPage, + showConfTxPage: showConfTxPage, + // config screen + SHOW_CONFIG_PAGE: 'SHOW_CONFIG_PAGE', + SET_RPC_TARGET: 'SET_RPC_TARGET', + SET_DEFAULT_RPC_TARGET: 'SET_DEFAULT_RPC_TARGET', + SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', + USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER', + useEtherscanProvider: useEtherscanProvider, + showConfigPage, + SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', + showAddTokenPage, + addToken, + setRpcTarget: setRpcTarget, + setDefaultRpcTarget: setDefaultRpcTarget, + setProviderType: setProviderType, + // loading overlay + SHOW_LOADING: 'SHOW_LOADING_INDICATION', + HIDE_LOADING: 'HIDE_LOADING_INDICATION', + showLoadingIndication: showLoadingIndication, + hideLoadingIndication: hideLoadingIndication, + // buy Eth with coinbase + BUY_ETH: 'BUY_ETH', + buyEth: buyEth, + buyEthView: buyEthView, + BUY_ETH_VIEW: 'BUY_ETH_VIEW', + COINBASE_SUBVIEW: 'COINBASE_SUBVIEW', + coinBaseSubview: coinBaseSubview, + SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW', + shapeShiftSubview: shapeShiftSubview, + PAIR_UPDATE: 'PAIR_UPDATE', + pairUpdate: pairUpdate, + coinShiftRquest: coinShiftRquest, + SHOW_SUB_LOADING_INDICATION: 'SHOW_SUB_LOADING_INDICATION', + showSubLoadingIndication: showSubLoadingIndication, + HIDE_SUB_LOADING_INDICATION: 'HIDE_SUB_LOADING_INDICATION', + hideSubLoadingIndication: hideSubLoadingIndication, +// QR STUFF: + SHOW_QR: 'SHOW_QR', + showQrView: showQrView, + reshowQrCode: reshowQrCode, + SHOW_QR_VIEW: 'SHOW_QR_VIEW', +// FORGOT PASSWORD: + BACK_TO_INIT_MENU: 'BACK_TO_INIT_MENU', + goBackToInitView: goBackToInitView, + RECOVERY_IN_PROGRESS: 'RECOVERY_IN_PROGRESS', + BACK_TO_UNLOCK_VIEW: 'BACK_TO_UNLOCK_VIEW', + backToUnlockView: backToUnlockView, + // SHOWING KEYCHAIN + SHOW_NEW_KEYCHAIN: 'SHOW_NEW_KEYCHAIN', + showNewKeychain: showNewKeychain, + + callBackgroundThenUpdate, + forceUpdateMetamaskState, +} + +module.exports = actions + +var background = null +function _setBackgroundConnection (backgroundConnection) { + background = backgroundConnection +} + +function goHome () { + return { + type: actions.GO_HOME, + } +} + +// async actions + +function tryUnlockMetamask (password) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + dispatch(actions.unlockInProgress()) + log.debug(`background.submitPassword`) + background.submitPassword(password, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.unlockFailed(err.message)) + } else { + dispatch(actions.transitionForward()) + forceUpdateMetamaskState(dispatch) + } + }) + } +} + +function transitionForward () { + return { + type: this.TRANSITION_FORWARD, + } +} + +function transitionBackward () { + return { + type: this.TRANSITION_BACKWARD, + } +} + +function confirmSeedWords () { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.clearSeedWordCache`) + background.clearSeedWordCache((err, account) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + + log.info('Seed word cache cleared. ' + account) + dispatch(actions.showAccountDetail(account)) + }) + } +} + +function createNewVaultAndRestore (password, seed) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.createNewVaultAndRestore`) + background.createNewVaultAndRestore(password, seed, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.showAccountsPage()) + }) + } +} + +function createNewVaultAndKeychain (password) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.createNewVaultAndKeychain`) + background.createNewVaultAndKeychain(password, (err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + log.debug(`background.placeSeedWords`) + background.placeSeedWords((err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch(actions.hideLoadingIndication()) + forceUpdateMetamaskState(dispatch) + }) + }) + } +} + +function revealSeedConfirmation () { + return { + type: this.REVEAL_SEED_CONFIRMATION, + } +} + +function requestRevealSeed (password) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.submitPassword`) + background.submitPassword(password, (err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + log.debug(`background.placeSeedWords`) + background.placeSeedWords((err, result) => { + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.hideLoadingIndication()) + dispatch(actions.showNewVaultSeed(result)) + }) + }) + } +} + +function addNewKeyring (type, opts) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.addNewKeyring`) + background.addNewKeyring(type, opts, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.showAccountsPage()) + }) + } +} + +function importNewAccount (strategy, args) { + return (dispatch) => { + dispatch(actions.showLoadingIndication('This may take a while, be patient.')) + log.debug(`background.importAccountWithStrategy`) + background.importAccountWithStrategy(strategy, args, (err) => { + if (err) return dispatch(actions.displayWarning(err.message)) + log.debug(`background.getState`) + background.getState((err, newState) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch(actions.updateMetamaskState(newState)) + dispatch({ + type: actions.SHOW_ACCOUNT_DETAIL, + value: newState.selectedAddress, + }) + }) + }) + } +} + +function navigateToNewAccountScreen () { + return { + type: this.NEW_ACCOUNT_SCREEN, + } +} + +function addNewAccount () { + log.debug(`background.addNewAccount`) + return callBackgroundThenUpdate(background.addNewAccount) +} + +function showInfoPage () { + return { + type: actions.SHOW_INFO_PAGE, + } +} + +function setCurrentCurrency (currencyCode) { + return (dispatch) => { + dispatch(this.showLoadingIndication()) + log.debug(`background.setCurrentCurrency`) + background.setCurrentCurrency(currencyCode, (err, data) => { + dispatch(this.hideLoadingIndication()) + if (err) { + log.error(err.stack) + return dispatch(actions.displayWarning(err.message)) + } + dispatch({ + type: this.SET_CURRENT_FIAT, + value: { + currentCurrency: data.currentCurrency, + conversionRate: data.conversionRate, + conversionDate: data.conversionDate, + }, + }) + }) + } +} + +function signMsg (msgData) { + log.debug('action - signMsg') + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + + log.debug(`actions calling background.signMessage`) + background.signMessage(msgData, (err, newState) => { + log.debug('signMessage called back') + dispatch(actions.updateMetamaskState(newState)) + dispatch(actions.hideLoadingIndication()) + + if (err) log.error(err) + if (err) return dispatch(actions.displayWarning(err.message)) + + dispatch(actions.completedTx(msgData.metamaskId)) + }) + } +} + +function signPersonalMsg (msgData) { + log.debug('action - signPersonalMsg') + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + + log.debug(`actions calling background.signPersonalMessage`) + background.signPersonalMessage(msgData, (err, newState) => { + log.debug('signPersonalMessage called back') + dispatch(actions.updateMetamaskState(newState)) + dispatch(actions.hideLoadingIndication()) + + if (err) log.error(err) + if (err) return dispatch(actions.displayWarning(err.message)) + + dispatch(actions.completedTx(msgData.metamaskId)) + }) + } +} + +function signTx (txData) { + return (dispatch) => { + global.ethQuery.sendTransaction(txData, (err, data) => { + dispatch(actions.hideLoadingIndication()) + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.hideWarning()) + }) + dispatch(this.showConfTxPage()) + } +} + +function sendTx (txData) { + log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`) + return (dispatch) => { + log.debug(`actions calling background.approveTransaction`) + background.approveTransaction(txData.id, (err) => { + if (err) { + dispatch(actions.txError(err)) + return log.error(err.message) + } + dispatch(actions.completedTx(txData.id)) + }) + } +} + +function updateAndApproveTx (txData) { + log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData)) + return (dispatch) => { + log.debug(`actions calling background.updateAndApproveTx`) + background.updateAndApproveTransaction(txData, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.txError(err)) + return log.error(err.message) + } + dispatch(actions.completedTx(txData.id)) + }) + } +} + +function completedTx (id) { + return { + type: actions.COMPLETED_TX, + value: id, + } +} + +function txError (err) { + return { + type: actions.TRANSACTION_ERROR, + message: err.message, + } +} + +function cancelMsg (msgData) { + log.debug(`background.cancelMessage`) + background.cancelMessage(msgData.id) + return actions.completedTx(msgData.id) +} + +function cancelPersonalMsg (msgData) { + const id = msgData.id + background.cancelPersonalMessage(id) + return actions.completedTx(id) +} + +function cancelTx (txData) { + log.debug(`background.cancelTransaction`) + background.cancelTransaction(txData.id) + return actions.completedTx(txData.id) +} + +// +// initialize screen +// + +function showCreateVault () { + return { + type: actions.SHOW_CREATE_VAULT, + } +} + +function showRestoreVault () { + return { + type: actions.SHOW_RESTORE_VAULT, + } +} + +function forgotPassword () { + return { + type: actions.FORGOT_PASSWORD, + } +} + +function showInitializeMenu () { + return { + type: actions.SHOW_INIT_MENU, + } +} + +function showImportPage () { + return { + type: actions.SHOW_IMPORT_PAGE, + } +} + +function createNewVaultInProgress () { + return { + type: actions.CREATE_NEW_VAULT_IN_PROGRESS, + } +} + +function showNewVaultSeed (seed) { + return { + type: actions.SHOW_NEW_VAULT_SEED, + value: seed, + } +} + +function backToUnlockView () { + return { + type: actions.BACK_TO_UNLOCK_VIEW, + } +} + +function showNewKeychain () { + return { + type: actions.SHOW_NEW_KEYCHAIN, + } +} + +// +// unlock screen +// + +function unlockInProgress () { + return { + type: actions.UNLOCK_IN_PROGRESS, + } +} + +function unlockFailed (message) { + return { + type: actions.UNLOCK_FAILED, + value: message, + } +} + +function unlockMetamask (account) { + return { + type: actions.UNLOCK_METAMASK, + value: account, + } +} + +function updateMetamaskState (newState) { + return { + type: actions.UPDATE_METAMASK_STATE, + value: newState, + } +} + +function lockMetamask () { + log.debug(`background.setLocked`) + return callBackgroundThenUpdate(background.setLocked) +} + +function setCurrentAccountTab (newTabName) { + log.debug(`background.setCurrentAccountTab: ${newTabName}`) + return callBackgroundThenUpdateNoSpinner(background.setCurrentAccountTab, newTabName) +} + +function showAccountDetail (address) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.setSelectedAddress`) + background.setSelectedAddress(address, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch({ + type: actions.SHOW_ACCOUNT_DETAIL, + value: address, + }) + }) + } +} + +function backToAccountDetail (address) { + return { + type: actions.BACK_TO_ACCOUNT_DETAIL, + value: address, + } +} + +function showAccountsPage () { + return { + type: actions.SHOW_ACCOUNTS_PAGE, + } +} + +function showConfTxPage (transForward = true) { + return { + type: actions.SHOW_CONF_TX_PAGE, + transForward: transForward, + } +} + +function nextTx () { + return { + type: actions.NEXT_TX, + } +} + +function viewPendingTx (txId) { + return { + type: actions.VIEW_PENDING_TX, + value: txId, + } +} + +function previousTx () { + return { + type: actions.PREVIOUS_TX, + } +} + +function showConfigPage (transitionForward = true) { + return { + type: actions.SHOW_CONFIG_PAGE, + value: transitionForward, + } +} + +function showAddTokenPage (transitionForward = true) { + return { + type: actions.SHOW_ADD_TOKEN_PAGE, + value: transitionForward, + } +} + +function addToken (address, symbol, decimals) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + background.addToken(address, symbol, decimals, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + setTimeout(() => { + dispatch(actions.goHome()) + }, 250) + }) + } +} + +function goBackToInitView () { + return { + type: actions.BACK_TO_INIT_MENU, + } +} + +// +// notice +// + +function markNoticeRead (notice) { + return (dispatch) => { + dispatch(this.showLoadingIndication()) + log.debug(`background.markNoticeRead`) + background.markNoticeRead(notice, (err, notice) => { + dispatch(this.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err)) + } + if (notice) { + return dispatch(actions.showNotice(notice)) + } else { + dispatch(this.clearNotices()) + return { + type: actions.SHOW_ACCOUNTS_PAGE, + } + } + }) + } +} + +function showNotice (notice) { + return { + type: actions.SHOW_NOTICE, + value: notice, + } +} + +function clearNotices () { + return { + type: actions.CLEAR_NOTICES, + } +} + +function markAccountsFound () { + log.debug(`background.markAccountsFound`) + return callBackgroundThenUpdate(background.markAccountsFound) +} + +// +// config +// + +// default rpc target refers to localhost:8545 in this instance. +function setDefaultRpcTarget (rpcList) { + log.debug(`background.setDefaultRpcTarget`) + return (dispatch) => { + background.setDefaultRpc((err, result) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem changing networks.')) + } + }) + } +} + +function setRpcTarget (newRpc) { + log.debug(`background.setRpcTarget`) + return (dispatch) => { + background.setCustomRpc(newRpc, (err, result) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem changing networks!')) + } + }) + } +} + +// Calls the addressBookController to add a new address. +function addToAddressBook (recipient, nickname) { + log.debug(`background.addToAddressBook`) + return (dispatch) => { + background.setAddressBook(recipient, nickname, (err, result) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Address book failed to update')) + } + }) + } +} + +function setProviderType (type) { + log.debug(`background.setProviderType`) + background.setProviderType(type) + return { + type: actions.SET_PROVIDER_TYPE, + value: type, + } +} + +function useEtherscanProvider () { + log.debug(`background.useEtherscanProvider`) + background.useEtherscanProvider() + return { + type: actions.USE_ETHERSCAN_PROVIDER, + } +} + +function showLoadingIndication (message) { + return { + type: actions.SHOW_LOADING, + value: message, + } +} + +function hideLoadingIndication () { + return { + type: actions.HIDE_LOADING, + } +} + +function showSubLoadingIndication () { + return { + type: actions.SHOW_SUB_LOADING_INDICATION, + } +} + +function hideSubLoadingIndication () { + return { + type: actions.HIDE_SUB_LOADING_INDICATION, + } +} + +function displayWarning (text) { + return { + type: actions.DISPLAY_WARNING, + value: text, + } +} + +function hideWarning () { + return { + type: actions.HIDE_WARNING, + } +} + +function requestExportAccount () { + return { + type: actions.REQUEST_ACCOUNT_EXPORT, + } +} + +function exportAccount (password, address) { + var self = this + + return function (dispatch) { + dispatch(self.showLoadingIndication()) + + log.debug(`background.submitPassword`) + background.submitPassword(password, function (err) { + if (err) { + log.error('Error in submiting password.') + dispatch(self.hideLoadingIndication()) + return dispatch(self.displayWarning('Incorrect Password.')) + } + log.debug(`background.exportAccount`) + background.exportAccount(address, function (err, result) { + dispatch(self.hideLoadingIndication()) + + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem exporting the account.')) + } + + dispatch(self.showPrivateKey(result)) + }) + }) + } +} + +function showPrivateKey (key) { + return { + type: actions.SHOW_PRIVATE_KEY, + value: key, + } +} + +function saveAccountLabel (account, label) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.saveAccountLabel`) + background.saveAccountLabel(account, label, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch({ + type: actions.SAVE_ACCOUNT_LABEL, + value: { account, label }, + }) + }) + } +} + +function showSendPage () { + return { + type: actions.SHOW_SEND_PAGE, + } +} + +function buyEth (opts) { + return (dispatch) => { + const url = getBuyEthUrl(opts) + global.platform.openWindow({ url }) + dispatch({ + type: actions.BUY_ETH, + }) + } +} + +function buyEthView (address) { + return { + type: actions.BUY_ETH_VIEW, + value: address, + } +} + +function coinBaseSubview () { + return { + type: actions.COINBASE_SUBVIEW, + } +} + +function pairUpdate (coin) { + return (dispatch) => { + dispatch(actions.showSubLoadingIndication()) + dispatch(actions.hideWarning()) + shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { + dispatch(actions.hideSubLoadingIndication()) + dispatch({ + type: actions.PAIR_UPDATE, + value: { + marketinfo: mktResponse, + }, + }) + }) + } +} + +function shapeShiftSubview (network) { + var pair = 'btc_eth' + + return (dispatch) => { + dispatch(actions.showSubLoadingIndication()) + shapeShiftRequest('marketinfo', {pair}, (mktResponse) => { + shapeShiftRequest('getcoins', {}, (response) => { + dispatch(actions.hideSubLoadingIndication()) + if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) + dispatch({ + type: actions.SHAPESHIFT_SUBVIEW, + value: { + marketinfo: mktResponse, + coinOptions: response, + }, + }) + }) + }) + } +} + +function coinShiftRquest (data, marketData) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + shapeShiftRequest('shift', { method: 'POST', data}, (response) => { + dispatch(actions.hideLoadingIndication()) + if (response.error) return dispatch(actions.displayWarning(response.error)) + var message = ` + Deposit your ${response.depositType} to the address bellow:` + log.debug(`background.createShapeShiftTx`) + background.createShapeShiftTx(response.deposit, response.depositType) + dispatch(actions.showQrView(response.deposit, [message].concat(marketData))) + }) + } +} + +function showQrView (data, message) { + return { + type: actions.SHOW_QR_VIEW, + value: { + message: message, + data: data, + }, + } +} +function reshowQrCode (data, coin) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { + if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) + + var message = [ + `Deposit your ${coin} to the address bellow:`, + `Deposit Limit: ${mktResponse.limit}`, + `Deposit Minimum:${mktResponse.minimum}`, + ] + + dispatch(actions.hideLoadingIndication()) + return dispatch(actions.showQrView(data, message)) + }) + } +} + +function shapeShiftRequest (query, options, cb) { + var queryResponse, method + !options ? options = {} : null + options.method ? method = options.method : method = 'GET' + + var requestListner = function (request) { + queryResponse = JSON.parse(this.responseText) + cb ? cb(queryResponse) : null + return queryResponse + } + + var shapShiftReq = new XMLHttpRequest() + shapShiftReq.addEventListener('load', requestListner) + shapShiftReq.open(method, `https://shapeshift.io/${query}/${options.pair ? options.pair : ''}`, true) + + if (options.method === 'POST') { + var jsonObj = JSON.stringify(options.data) + shapShiftReq.setRequestHeader('Content-Type', 'application/json') + return shapShiftReq.send(jsonObj) + } else { + return shapShiftReq.send() + } +} + +// Call Background Then Update +// +// A function generator for a common pattern wherein: +// We show loading indication. +// We call a background method. +// We hide loading indication. +// If it errored, we show a warning. +// If it didn't, we update the state. +function callBackgroundThenUpdateNoSpinner (method, ...args) { + return (dispatch) => { + method.call(background, ...args, (err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + forceUpdateMetamaskState(dispatch) + }) + } +} + +function callBackgroundThenUpdate (method, ...args) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + method.call(background, ...args, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + forceUpdateMetamaskState(dispatch) + }) + } +} + +function forceUpdateMetamaskState (dispatch) { + log.debug(`background.getState`) + background.getState((err, newState) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch(actions.updateMetamaskState(newState)) + }) +} diff --git a/ui/app/add-token.js b/ui/app/add-token.js new file mode 100644 index 000000000..b303b5c0d --- /dev/null +++ b/ui/app/add-token.js @@ -0,0 +1,219 @@ +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 ethUtil = require('ethereumjs-util') +const abi = require('human-standard-token-abi') +const Eth = require('ethjs-query') +const EthContract = require('ethjs-contract') + +const emptyAddr = '0x0000000000000000000000000000000000000000' + +module.exports = connect(mapStateToProps)(AddTokenScreen) + +function mapStateToProps (state) { + return { + } +} + +inherits(AddTokenScreen, Component) +function AddTokenScreen () { + this.state = { + warning: null, + address: null, + symbol: 'TOKEN', + decimals: 18, + } + Component.call(this) +} + +AddTokenScreen.prototype.render = function () { + const state = this.state + const props = this.props + const { warning, symbol, decimals } = state + + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + 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', 'Add Token'), + ]), + + h('.error', { + style: { + display: warning ? 'block' : 'none', + padding: '0 20px', + textAlign: 'center', + }, + }, warning), + + // conf view + h('.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-space-around', { + style: { + padding: '20px', + }, + }, [ + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Token Address'), + ]), + + h('section.flex-row.flex-center', [ + h('input#token-address', { + name: 'address', + placeholder: 'Token Address', + onChange: this.tokenAddressDidChange.bind(this), + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + }), + ]), + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Token Sybmol'), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('input#token_symbol', { + placeholder: `Like "ETH"`, + value: symbol, + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onChange: (event) => { + var element = event.target + var symbol = element.value + this.setState({ symbol }) + }, + }), + ]), + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Decimals of Precision'), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('input#token_decimals', { + value: decimals, + type: 'number', + min: 0, + max: 36, + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onChange: (event) => { + var element = event.target + var decimals = element.value.trim() + this.setState({ decimals }) + }, + }), + ]), + + h('button', { + style: { + alignSelf: 'center', + }, + onClick: (event) => { + const valid = this.validateInputs() + if (!valid) return + + const { address, symbol, decimals } = this.state + this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) + }, + }, 'Add'), + ]), + ]), + ]) + ) +} + +AddTokenScreen.prototype.componentWillMount = function () { + if (typeof global.ethereumProvider === 'undefined') return + + this.eth = new Eth(global.ethereumProvider) + this.contract = new EthContract(this.eth) + this.TokenContract = this.contract(abi) +} + +AddTokenScreen.prototype.tokenAddressDidChange = function (event) { + const el = event.target + const address = el.value.trim() + if (ethUtil.isValidAddress(address) && address !== emptyAddr) { + this.setState({ address }) + this.attemptToAutoFillTokenParams(address) + } +} + +AddTokenScreen.prototype.validateInputs = function () { + let msg = '' + const state = this.state + const { address, symbol, decimals } = state + + const validAddress = ethUtil.isValidAddress(address) + if (!validAddress) { + msg += 'Address is invalid. ' + } + + const validDecimals = decimals >= 0 && decimals < 36 + if (!validDecimals) { + msg += 'Decimals must be at least 0, and not over 36. ' + } + + const symbolLen = symbol.trim().length + const validSymbol = symbolLen > 0 && symbolLen < 10 + if (!validSymbol) { + msg += 'Symbol must be between 0 and 10 characters.' + } + + const isValid = validAddress && validDecimals + + if (!isValid) { + this.setState({ + warning: msg, + }) + } else { + this.setState({ warning: null }) + } + + return isValid +} + +AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { + const contract = this.TokenContract.at(address) + + const results = await Promise.all([ + contract.symbol(), + contract.decimals(), + ]) + + const [ symbol, decimals ] = results + if (symbol && decimals) { + console.log('SETTING SYMBOL AND DECIMALS', { symbol, decimals }) + this.setState({ symbol: symbol[0], decimals: decimals[0].toString() }) + } +} + diff --git a/ui/app/app.js b/ui/app/app.js new file mode 100644 index 000000000..1a63002e1 --- /dev/null +++ b/ui/app/app.js @@ -0,0 +1,591 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('./actions') +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +// init +const InitializeMenuScreen = require('./first-time/init-menu') +const NewKeyChainScreen = require('./new-keychain') +// unlock +const UnlockScreen = require('./unlock') +// accounts +const AccountsScreen = require('./accounts') +const AccountDetailScreen = require('./account-detail') +const SendTransactionScreen = require('./send') +const ConfirmTxScreen = require('./conf-tx') +// notice +const NoticeScreen = require('./components/notice') +const generateLostAccountsNotice = require('../lib/lost-accounts-notice') +// other views +const ConfigScreen = require('./config') +const AddTokenScreen = require('./add-token') +const Import = require('./accounts/import') +const InfoScreen = require('./info') +const Loading = require('./components/loading') +const SandwichExpando = require('sandwich-expando') +const MenuDroppo = require('menu-droppo') +const DropMenuItem = require('./components/drop-menu-item') +const NetworkIndicator = require('./components/network') +const Tooltip = require('./components/tooltip') +const BuyView = require('./components/buy-button-subview') +const QrView = require('./components/qr-code') +const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete') +const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') +const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation') + +module.exports = connect(mapStateToProps)(App) + +inherits(App, Component) +function App () { Component.call(this) } + +function mapStateToProps (state) { + return { + // state from plugin + isLoading: state.appState.isLoading, + loadingMessage: state.appState.loadingMessage, + noActiveNotices: state.metamask.noActiveNotices, + isInitialized: state.metamask.isInitialized, + isUnlocked: state.metamask.isUnlocked, + currentView: state.appState.currentView, + activeAddress: state.appState.activeAddress, + transForward: state.appState.transForward, + seedWords: state.metamask.seedWords, + unapprovedTxs: state.metamask.unapprovedTxs, + unapprovedMsgs: state.metamask.unapprovedMsgs, + menuOpen: state.appState.menuOpen, + network: state.metamask.network, + provider: state.metamask.provider, + forgottenPassword: state.appState.forgottenPassword, + lastUnreadNotice: state.metamask.lastUnreadNotice, + lostAccounts: state.metamask.lostAccounts, + frequentRpcList: state.metamask.frequentRpcList || [], + } +} + +App.prototype.render = function () { + var props = this.props + const { isLoading, loadingMessage, transForward, network } = props + const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' + const loadMessage = loadingMessage || isLoadingNetwork ? + `Connecting to ${this.getNetworkName()}` : null + + log.debug('Main ui render function') + + return ( + + h('.flex-column.flex-grow.full-height', { + style: { + // Windows was showing a vertical scroll bar: + overflow: 'hidden', + position: 'relative', + }, + }, [ + + // app bar + this.renderAppBar(), + this.renderNetworkDropdown(), + this.renderDropdown(), + + h(Loading, { + isLoading: isLoading || isLoadingNetwork, + loadingMessage: loadMessage, + }), + + // panel content + h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), { + style: { + height: '380px', + width: '360px', + }, + }, [ + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + this.renderPrimary(), + ]), + ]), + ]) + ) +} + +App.prototype.renderAppBar = function () { + if (window.METAMASK_UI_TYPE === 'notification') { + return null + } + + const props = this.props + const state = this.state || {} + const isNetworkMenuOpen = state.isNetworkMenuOpen || false + + return ( + + h('div', [ + + h('.app-header.flex-row.flex-space-between', { + style: { + alignItems: 'center', + visibility: props.isUnlocked ? 'visible' : 'none', + background: props.isUnlocked ? 'white' : 'none', + height: '36px', + position: 'relative', + zIndex: 12, + }, + }, [ + + h('div.left-menu-section', { + style: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }, + }, [ + + // mini logo + h('img', { + height: 24, + width: 24, + src: '/images/icon-128.png', + }), + + h(NetworkIndicator, { + network: this.props.network, + provider: this.props.provider, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen }) + }, + }), + ]), + + // metamask name + props.isUnlocked && h('h1', { + style: { + position: 'relative', + left: '9px', + }, + }, 'MetaMask'), + + props.isUnlocked && h('div', { + style: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }, + }, [ + + // small accounts nav + props.isUnlocked && h(Tooltip, { title: 'Switch Accounts' }, [ + h('img.cursor-pointer.color-orange', { + src: 'images/switch_acc.svg', + style: { + width: '23.5px', + marginRight: '8px', + }, + onClick: (event) => { + event.stopPropagation() + this.props.dispatch(actions.showAccountsPage()) + }, + }), + ]), + + // hamburger + props.isUnlocked && h(SandwichExpando, { + width: 16, + barHeight: 2, + padding: 0, + isOpen: state.isMainMenuOpen, + color: 'rgb(247,146,30)', + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + this.setState({ isMainMenuOpen: !state.isMainMenuOpen }) + }, + }), + ]), + ]), + ]) + ) +} + +App.prototype.renderNetworkDropdown = function () { + const props = this.props + const rpcList = props.frequentRpcList + const state = this.state || {} + const isOpen = state.isNetworkMenuOpen + + return h(MenuDroppo, { + isOpen, + onClickOutside: (event) => { + this.setState({ isNetworkMenuOpen: !isOpen }) + }, + zIndex: 11, + style: { + position: 'absolute', + left: 0, + top: '36px', + }, + innerStyle: { + background: 'white', + boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', + }, + }, [ // DROP MENU ITEMS + h('style', ` + .drop-menu-item:hover { background:rgb(235, 235, 235); } + .drop-menu-item i { margin: 11px; } + `), + + h(DropMenuItem, { + label: 'Main Ethereum Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setProviderType('mainnet')), + icon: h('.menu-icon.diamond'), + activeNetworkRender: props.network, + provider: props.provider, + }), + + h(DropMenuItem, { + label: 'Ropsten Test Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setProviderType('ropsten')), + icon: h('.menu-icon.red-dot'), + activeNetworkRender: props.network, + provider: props.provider, + }), + + h(DropMenuItem, { + label: 'Kovan Test Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false}), + action: () => props.dispatch(actions.setProviderType('kovan')), + icon: h('.menu-icon.hollow-diamond'), + activeNetworkRender: props.network, + provider: props.provider, + }), + + h(DropMenuItem, { + label: 'Rinkeby Test Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false}), + action: () => props.dispatch(actions.setProviderType('rinkeby')), + icon: h('.menu-icon.golden-square'), + activeNetworkRender: props.network, + provider: props.provider, + }), + + h(DropMenuItem, { + label: 'Localhost 8545', + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setDefaultRpcTarget(rpcList)), + icon: h('i.fa.fa-question-circle.fa-lg'), + activeNetworkRender: props.provider.rpcTarget, + }), + + this.renderCustomOption(props.provider), + this.renderCommonRpc(rpcList, props.provider), + + h(DropMenuItem, { + label: 'Custom RPC', + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => this.props.dispatch(actions.showConfigPage()), + icon: h('i.fa.fa-question-circle.fa-lg'), + }), + + ]) +} + +App.prototype.renderDropdown = function () { + const state = this.state || {} + const isOpen = state.isMainMenuOpen + + return h(MenuDroppo, { + isOpen: isOpen, + zIndex: 11, + onClickOutside: (event) => { + this.setState({ isMainMenuOpen: !isOpen }) + }, + style: { + position: 'absolute', + right: 0, + top: '36px', + }, + innerStyle: { + background: 'white', + boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', + }, + }, [ // DROP MENU ITEMS + h('style', ` + .drop-menu-item:hover { background:rgb(235, 235, 235); } + .drop-menu-item i { margin: 11px; } + `), + + h(DropMenuItem, { + label: 'Settings', + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + action: () => this.props.dispatch(actions.showConfigPage()), + icon: h('i.fa.fa-gear.fa-lg'), + }), + + h(DropMenuItem, { + label: 'Import Account', + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + action: () => this.props.dispatch(actions.showImportPage()), + icon: h('i.fa.fa-arrow-circle-o-up.fa-lg'), + }), + + h(DropMenuItem, { + label: 'Lock', + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + action: () => this.props.dispatch(actions.lockMetamask()), + icon: h('i.fa.fa-lock.fa-lg'), + }), + + h(DropMenuItem, { + label: 'Info/Help', + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + action: () => this.props.dispatch(actions.showInfoPage()), + icon: h('i.fa.fa-question.fa-lg'), + }), + ]) +} + +App.prototype.renderBackButton = function (style, justArrow = false) { + var props = this.props + return ( + h('.flex-row', { + key: 'leftArrow', + style: style, + onClick: () => props.dispatch(actions.goBackToInitView()), + }, [ + h('i.fa.fa-arrow-left.cursor-pointer'), + justArrow ? null : h('div.cursor-pointer', { + style: { + marginLeft: '3px', + }, + onClick: () => props.dispatch(actions.goBackToInitView()), + }, 'BACK'), + ]) + ) +} + +App.prototype.renderPrimary = function () { + log.debug('rendering primary') + var props = this.props + + // notices + if (!props.noActiveNotices) { + log.debug('rendering notice screen for unread notices.') + return h(NoticeScreen, { + notice: props.lastUnreadNotice, + key: 'NoticeScreen', + onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)), + }) + } else if (props.lostAccounts && props.lostAccounts.length > 0) { + log.debug('rendering notice screen for lost accounts view.') + return h(NoticeScreen, { + notice: generateLostAccountsNotice(props.lostAccounts), + key: 'LostAccountsNotice', + onConfirm: () => props.dispatch(actions.markAccountsFound()), + }) + } + + if (props.seedWords) { + log.debug('rendering seed words') + return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'}) + } + + // show initialize screen + if (!props.isInitialized || props.forgottenPassword) { + // show current view + log.debug('rendering an initialize screen') + switch (props.currentView.name) { + + case 'restoreVault': + log.debug('rendering restore vault screen') + return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) + + default: + log.debug('rendering menu screen') + return h(InitializeMenuScreen, {key: 'menuScreenInit'}) + } + } + + // show unlock screen + if (!props.isUnlocked) { + switch (props.currentView.name) { + + case 'restoreVault': + log.debug('rendering restore vault screen') + return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) + + case 'config': + log.debug('rendering config screen from unlock screen.') + return h(ConfigScreen, {key: 'config'}) + + default: + log.debug('rendering locked screen') + return h(UnlockScreen, {key: 'locked'}) + } + } + + // show current view + switch (props.currentView.name) { + + case 'accounts': + log.debug('rendering accounts screen') + return h(AccountsScreen, {key: 'accounts'}) + + case 'accountDetail': + log.debug('rendering account detail screen') + return h(AccountDetailScreen, {key: 'account-detail'}) + + case 'sendTransaction': + log.debug('rendering send tx screen') + return h(SendTransactionScreen, {key: 'send-transaction'}) + + case 'newKeychain': + log.debug('rendering new keychain screen') + return h(NewKeyChainScreen, {key: 'new-keychain'}) + + case 'confTx': + log.debug('rendering confirm tx screen') + return h(ConfirmTxScreen, {key: 'confirm-tx'}) + + case 'add-token': + log.debug('rendering add-token screen from unlock screen.') + return h(AddTokenScreen, {key: 'add-token'}) + + case 'config': + log.debug('rendering config screen') + return h(ConfigScreen, {key: 'config'}) + + case 'import-menu': + log.debug('rendering import screen') + return h(Import, {key: 'import-menu'}) + + case 'reveal-seed-conf': + log.debug('rendering reveal seed confirmation screen') + return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'}) + + case 'info': + log.debug('rendering info screen') + return h(InfoScreen, {key: 'info'}) + + case 'buyEth': + log.debug('rendering buy ether screen') + return h(BuyView, {key: 'buyEthView'}) + + case 'qr': + log.debug('rendering show qr screen') + return h('div', { + style: { + position: 'absolute', + height: '100%', + top: '0px', + left: '0px', + }, + }, [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: () => props.dispatch(actions.backToAccountDetail(props.activeAddress)), + style: { + marginLeft: '10px', + marginTop: '50px', + }, + }), + h('div', { + style: { + position: 'absolute', + left: '44px', + width: '285px', + }, + }, [ + h(QrView, {key: 'qr'}), + ]), + ]) + + default: + log.debug('rendering default, account detail screen') + return h(AccountDetailScreen, {key: 'account-detail'}) + } +} + +App.prototype.toggleMetamaskActive = function () { + if (!this.props.isUnlocked) { + // currently inactive: redirect to password box + var passwordBox = document.querySelector('input[type=password]') + if (!passwordBox) return + passwordBox.focus() + } else { + // currently active: deactivate + this.props.dispatch(actions.lockMetamask(false)) + } +} + +App.prototype.renderCustomOption = function (provider) { + const { rpcTarget, type } = provider + if (type !== 'rpc') return null + + // Concatenate long URLs + let label = rpcTarget + if (rpcTarget.length > 31) { + label = label.substr(0, 34) + '...' + } + + switch (rpcTarget) { + + case 'http://localhost:8545': + return null + + default: + return h(DropMenuItem, { + label, + key: rpcTarget, + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + icon: h('i.fa.fa-question-circle.fa-lg'), + activeNetworkRender: 'custom', + }) + } +} + +App.prototype.getNetworkName = function () { + const { provider } = this.props + const providerName = provider.type + + let name + + if (providerName === 'mainnet') { + name = 'Main Ethereum Network' + } else if (providerName === 'ropsten') { + name = 'Ropsten Test Network' + } else if (providerName === 'kovan') { + name = 'Kovan Test Network' + } else if (providerName === 'rinkeby') { + name = 'Rinkeby Test Network' + } else { + name = 'Unknown Private Network' + } + + return name +} + +App.prototype.renderCommonRpc = function (rpcList, provider) { + const { rpcTarget } = provider + const props = this.props + + return rpcList.map((rpc) => { + if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { + return null + } else { + return h(DropMenuItem, { + label: rpc, + key: rpc, + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setRpcTarget(rpc)), + icon: h('i.fa.fa-question-circle.fa-lg'), + activeNetworkRender: rpc, + }) + } + }) +} diff --git a/ui/app/components/account-export.js b/ui/app/components/account-export.js new file mode 100644 index 000000000..394d878f7 --- /dev/null +++ b/ui/app/components/account-export.js @@ -0,0 +1,122 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const copyToClipboard = require('copy-to-clipboard') +const actions = require('../actions') +const ethUtil = require('ethereumjs-util') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(ExportAccountView) + +inherits(ExportAccountView, Component) +function ExportAccountView () { + Component.call(this) +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +ExportAccountView.prototype.render = function () { + var state = this.props + var accountDetail = state.accountDetail + + if (!accountDetail) return h('div') + var accountExport = accountDetail.accountExport + + var notExporting = accountExport === 'none' + var exportRequested = accountExport === 'requested' + var accountExported = accountExport === 'completed' + + if (notExporting) return h('div') + + if (exportRequested) { + var warning = `Export private keys at your own risk.` + return ( + h('div', { + style: { + display: 'inline-block', + textAlign: 'center', + }, + }, + [ + h('div', { + key: 'exporting', + style: { + margin: '0 20px', + }, + }, [ + h('p.error', warning), + h('input#exportAccount.sizing-input', { + type: 'password', + placeholder: 'confirm password', + onKeyPress: this.onExportKeyPress.bind(this), + style: { + position: 'relative', + top: '1.5px', + marginBottom: '7px', + }, + }), + ]), + h('div', { + key: 'buttons', + style: { + margin: '0 20px', + }, + }, + [ + h('button', { + onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }), + style: { + marginRight: '10px', + }, + }, 'Submit'), + h('button', { + onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), + }, 'Cancel'), + ]), + (this.props.warning) && ( + h('span.error', { + style: { + margin: '20px', + }, + }, this.props.warning.split('-')) + ), + ]) + ) + } + + if (accountExported) { + return h('div.privateKey', { + style: { + margin: '0 20px', + }, + }, [ + h('label', 'Your private key (click to copy):'), + h('p.error.cursor-pointer', { + style: { + textOverflow: 'ellipsis', + overflow: 'hidden', + webkitUserSelect: 'text', + width: '100%', + }, + onClick: function (event) { + copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey)) + }, + }, ethUtil.stripHexPrefix(accountDetail.privateKey)), + h('button', { + onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), + }, 'Done'), + ]) + } +} + +ExportAccountView.prototype.onExportKeyPress = function (event) { + if (event.key !== 'Enter') return + event.preventDefault() + + var input = document.getElementById('exportAccount').value + this.props.dispatch(actions.exportAccount(input, this.props.address)) +} diff --git a/ui/app/components/account-info-link.js b/ui/app/components/account-info-link.js new file mode 100644 index 000000000..6526ab502 --- /dev/null +++ b/ui/app/components/account-info-link.js @@ -0,0 +1,41 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Tooltip = require('./tooltip') +const genAccountLink = require('../../lib/account-link') + +module.exports = AccountInfoLink + +inherits(AccountInfoLink, Component) +function AccountInfoLink () { + Component.call(this) +} + +AccountInfoLink.prototype.render = function () { + const { selected, network } = this.props + const title = 'View account on Etherscan' + const url = genAccountLink(selected, network) + + if (!url) { + return null + } + + return h('.account-info-link', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + + h(Tooltip, { + title, + }, [ + h('i.fa.fa-info-circle.cursor-pointer.color-orange', { + style: { + margin: '5px', + }, + onClick () { global.platform.openWindow({ url }) }, + }), + ]), + ]) +} diff --git a/ui/app/components/account-panel.js b/ui/app/components/account-panel.js new file mode 100644 index 000000000..abaaf8163 --- /dev/null +++ b/ui/app/components/account-panel.js @@ -0,0 +1,86 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const Identicon = require('./identicon') +const formatBalance = require('../util').formatBalance +const addressSummary = require('../util').addressSummary + +module.exports = AccountPanel + + +inherits(AccountPanel, Component) +function AccountPanel () { + Component.call(this) +} + +AccountPanel.prototype.render = function () { + var state = this.props + var identity = state.identity || {} + var account = state.account || {} + var isFauceting = state.isFauceting + + var panelState = { + key: `accountPanel${identity.address}`, + identiconKey: identity.address, + identiconLabel: identity.name || '', + attributes: [ + { + key: 'ADDRESS', + value: addressSummary(identity.address), + }, + balanceOrFaucetingIndication(account, isFauceting), + ], + } + + return ( + + h('.identity-panel.flex-row.flex-space-between', { + style: { + flex: '1 0 auto', + cursor: panelState.onClick ? 'pointer' : undefined, + }, + onClick: panelState.onClick, + }, [ + + // account identicon + h('.identicon-wrapper.flex-column.select-none', [ + h(Identicon, { + address: panelState.identiconKey, + imageify: state.imageifyIdenticons, + }), + h('span.font-small', panelState.identiconLabel.substring(0, 7) + '...'), + ]), + + // account address, balance + h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [ + + panelState.attributes.map((attr) => { + return h('.flex-row.flex-space-between', { + key: '' + Math.round(Math.random() * 1000000), + }, [ + h('label.font-small.no-select', attr.key), + h('span.font-small', attr.value), + ]) + }), + ]), + + ]) + + ) +} + +function balanceOrFaucetingIndication (account, isFauceting) { + // Temporarily deactivating isFauceting indication + // because it shows fauceting for empty restored accounts. + if (/* isFauceting*/ false) { + return { + key: 'Account is auto-funding.', + value: 'Please wait.', + } + } else { + return { + key: 'BALANCE', + value: formatBalance(account.balance), + } + } +} diff --git a/ui/app/components/balance.js b/ui/app/components/balance.js new file mode 100644 index 000000000..57ca84564 --- /dev/null +++ b/ui/app/components/balance.js @@ -0,0 +1,89 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance +const generateBalanceObject = require('../util').generateBalanceObject +const Tooltip = require('./tooltip.js') +const FiatValue = require('./fiat-value.js') + +module.exports = EthBalanceComponent + +inherits(EthBalanceComponent, Component) +function EthBalanceComponent () { + Component.call(this) +} + +EthBalanceComponent.prototype.render = function () { + var props = this.props + let { value } = props + var style = props.style + var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true + value = value ? formatBalance(value, 6, needsParse) : '...' + var width = props.width + + return ( + + h('.ether-balance.ether-balance-amount', { + style: style, + }, [ + h('div', { + style: { + display: 'inline', + width: width, + }, + }, this.renderBalance(value)), + ]) + + ) +} +EthBalanceComponent.prototype.renderBalance = function (value) { + var props = this.props + if (value === 'None') return value + if (value === '...') return value + var balanceObj = generateBalanceObject(value, props.shorten ? 1 : 3) + var balance + var splitBalance = value.split(' ') + var ethNumber = splitBalance[0] + var ethSuffix = splitBalance[1] + const showFiat = 'showFiat' in props ? props.showFiat : true + + if (props.shorten) { + balance = balanceObj.shortBalance + } else { + balance = balanceObj.balance + } + + var label = balanceObj.label + + return ( + h(Tooltip, { + position: 'bottom', + title: `${ethNumber} ${ethSuffix}`, + }, h('div.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + }, + }, this.props.incoming ? `+${balance}` : balance), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + }, + }, label), + ]), + + showFiat ? h(FiatValue, { value: props.value }) : null, + ])) + ) +} diff --git a/ui/app/components/binary-renderer.js b/ui/app/components/binary-renderer.js new file mode 100644 index 000000000..0b6a1f5c2 --- /dev/null +++ b/ui/app/components/binary-renderer.js @@ -0,0 +1,46 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const extend = require('xtend') + +module.exports = BinaryRenderer + +inherits(BinaryRenderer, Component) +function BinaryRenderer () { + Component.call(this) +} + +BinaryRenderer.prototype.render = function () { + const props = this.props + const { value, style } = props + const text = this.hexToText(value) + + const defaultStyle = extend({ + width: '315px', + maxHeight: '210px', + resize: 'none', + border: 'none', + background: 'white', + padding: '3px', + }, style) + + return ( + h('textarea.font-small', { + readOnly: true, + style: defaultStyle, + defaultValue: text, + }) + ) +} + +BinaryRenderer.prototype.hexToText = function (hex) { + try { + const stripped = ethUtil.stripHexPrefix(hex) + const buff = Buffer.from(stripped, 'hex') + return buff.toString('utf8') + } catch (e) { + return hex + } +} + diff --git a/ui/app/components/bn-as-decimal-input.js b/ui/app/components/bn-as-decimal-input.js new file mode 100644 index 000000000..f3ace4720 --- /dev/null +++ b/ui/app/components/bn-as-decimal-input.js @@ -0,0 +1,174 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const extend = require('xtend') + +module.exports = BnAsDecimalInput + +inherits(BnAsDecimalInput, Component) +function BnAsDecimalInput () { + this.state = { invalid: null } + Component.call(this) +} + +/* Bn as Decimal Input + * + * A component for allowing easy, decimal editing + * of a passed in bn string value. + * + * On change, calls back its `onChange` function parameter + * and passes it an updated bn string. + */ + +BnAsDecimalInput.prototype.render = function () { + const props = this.props + const state = this.state + + const { value, scale, precision, onChange, min, max } = props + + const suffix = props.suffix + const style = props.style + const valueString = value.toString(10) + const newValue = this.downsize(valueString, scale, precision) + + return ( + h('.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('input.hex-input', { + type: 'number', + step: 'any', + required: true, + min, + max, + style: extend({ + display: 'block', + textAlign: 'right', + backgroundColor: 'transparent', + border: '1px solid #bdbdbd', + + }, style), + value: newValue, + onBlur: (event) => { + this.updateValidity(event) + }, + onChange: (event) => { + this.updateValidity(event) + const value = (event.target.value === '') ? '' : event.target.value + + + const scaledNumber = this.upsize(value, scale, precision) + const precisionBN = new BN(scaledNumber, 10) + onChange(precisionBN, event.target.checkValidity()) + }, + onInvalid: (event) => { + const msg = this.constructWarning() + if (msg === state.invalid) { + return + } + this.setState({ invalid: msg }) + event.preventDefault() + return false + }, + }), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + marginRight: '6px', + width: '20px', + }, + }, suffix), + ]), + + state.invalid ? h('span.error', { + style: { + position: 'absolute', + right: '0px', + textAlign: 'right', + transform: 'translateY(26px)', + padding: '3px', + background: 'rgba(255,255,255,0.85)', + zIndex: '1', + textTransform: 'capitalize', + border: '2px solid #E20202', + }, + }, state.invalid) : null, + ]) + ) +} + +BnAsDecimalInput.prototype.setValid = function (message) { + this.setState({ invalid: null }) +} + +BnAsDecimalInput.prototype.updateValidity = function (event) { + const target = event.target + const value = this.props.value + const newValue = target.value + + if (value === newValue) { + return + } + + const valid = target.checkValidity() + + if (valid) { + this.setState({ invalid: null }) + } +} + +BnAsDecimalInput.prototype.constructWarning = function () { + const { name, min, max } = this.props + let message = name ? name + ' ' : '' + + if (min && max) { + message += `must be greater than or equal to ${min} and less than or equal to ${max}.` + } else if (min) { + message += `must be greater than or equal to ${min}.` + } else if (max) { + message += `must be less than or equal to ${max}.` + } else { + message += 'Invalid input.' + } + + return message +} + + +BnAsDecimalInput.prototype.downsize = function (number, scale, precision) { + // if there is no scaling, simply return the number + if (scale === 0) { + return Number(number) + } else { + // if the scale is the same as the precision, account for this edge case. + var decimals = (scale === precision) ? -1 : scale - precision + return Number(number.slice(0, -scale) + '.' + number.slice(-scale, decimals)) + } +} + +BnAsDecimalInput.prototype.upsize = function (number, scale, precision) { + var stringArray = number.toString().split('.') + var decimalLength = stringArray[1] ? stringArray[1].length : 0 + var newString = stringArray[0] + + // If there is scaling and decimal parts exist, integrate them in. + if ((scale !== 0) && (decimalLength !== 0)) { + newString += stringArray[1].slice(0, precision) + } + + // Add 0s to account for the upscaling. + for (var i = decimalLength; i < scale; i++) { + newString += '0' + } + return newString +} diff --git a/ui/app/components/buy-button-subview.js b/ui/app/components/buy-button-subview.js new file mode 100644 index 000000000..87084f92d --- /dev/null +++ b/ui/app/components/buy-button-subview.js @@ -0,0 +1,197 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../actions') +const CoinbaseForm = require('./coinbase-form') +const ShapeshiftForm = require('./shapeshift-form') +const Loading = require('./loading') +const AccountPanel = require('./account-panel') +const RadioList = require('./custom-radio-list') + +module.exports = connect(mapStateToProps)(BuyButtonSubview) + +function mapStateToProps (state) { + return { + identity: state.appState.identity, + account: state.metamask.accounts[state.appState.buyView.buyAddress], + warning: state.appState.warning, + buyView: state.appState.buyView, + network: state.metamask.network, + provider: state.metamask.provider, + context: state.appState.currentView.context, + isSubLoading: state.appState.isSubLoading, + } +} + +inherits(BuyButtonSubview, Component) +function BuyButtonSubview () { + Component.call(this) +} + +BuyButtonSubview.prototype.render = function () { + const props = this.props + const isLoading = props.isSubLoading + + return ( + h('.buy-eth-section.flex-column', { + style: { + alignItems: 'center', + }, + }, [ + // back button + h('.flex-row', { + style: { + alignItems: 'center', + justifyContent: 'center', + }, + }, [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: this.backButtonContext.bind(this), + style: { + position: 'absolute', + left: '10px', + }, + }), + h('h2.text-transform-uppercase.flex-center', { + style: { + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, 'Buy Eth'), + ]), + h('div', { + style: { + position: 'absolute', + top: '57vh', + left: '49vw', + }, + }, [ + h(Loading, {isLoading}), + ]), + h('div', { + style: { + width: '80%', + }, + }, [ + h(AccountPanel, { + showFullAddress: true, + identity: props.identity, + account: props.account, + }), + ]), + h('h3.text-transform-uppercase', { + style: { + paddingLeft: '15px', + fontFamily: 'Montserrat Light', + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, 'Select Service'), + h('.flex-row.selected-exchange', { + style: { + position: 'relative', + right: '35px', + marginTop: '20px', + marginBottom: '20px', + }, + }, [ + h(RadioList, { + defaultFocus: props.buyView.subview, + labels: [ + 'Coinbase', + 'ShapeShift', + ], + subtext: { + 'Coinbase': 'Crypto/FIAT (USA only)', + 'ShapeShift': 'Crypto', + }, + onClick: this.radioHandler.bind(this), + }), + ]), + h('h3.text-transform-uppercase', { + style: { + paddingLeft: '15px', + fontFamily: 'Montserrat Light', + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, props.buyView.subview), + this.formVersionSubview(), + ]) + ) +} + +BuyButtonSubview.prototype.formVersionSubview = function () { + const network = this.props.network + if (network === '1') { + if (this.props.buyView.formView.coinbase) { + return h(CoinbaseForm, this.props) + } else if (this.props.buyView.formView.shapeshift) { + return h(ShapeshiftForm, this.props) + } + } else { + return h('div.flex-column', { + style: { + alignItems: 'center', + margin: '50px', + }, + }, [ + h('h3.text-transform-uppercase', { + style: { + width: '225px', + marginBottom: '15px', + }, + }, 'In order to access this feature, please switch to the Main Network'), + ((network === '3') || (network === '4') || (network === '42')) ? h('h3.text-transform-uppercase', 'or go to the') : null, + (network === '3') ? h('button.text-transform-uppercase', { + onClick: () => this.props.dispatch(actions.buyEth({ network })), + style: { + marginTop: '15px', + }, + }, 'Ropsten Test Faucet') : null, + (network === '4') ? h('button.text-transform-uppercase', { + onClick: () => this.props.dispatch(actions.buyEth({ network })), + style: { + marginTop: '15px', + }, + }, 'Rinkeby Test Faucet') : null, + (network === '42') ? h('button.text-transform-uppercase', { + onClick: () => this.props.dispatch(actions.buyEth({ network })), + style: { + marginTop: '15px', + }, + }, 'Kovan Test Faucet') : null, + ]) + } +} + +BuyButtonSubview.prototype.navigateTo = function (url) { + global.platform.openWindow({ url }) +} + +BuyButtonSubview.prototype.backButtonContext = function () { + if (this.props.context === 'confTx') { + this.props.dispatch(actions.showConfTxPage(false)) + } else { + this.props.dispatch(actions.goHome()) + } +} + +BuyButtonSubview.prototype.radioHandler = function (event) { + switch (event.target.title) { + case 'Coinbase': + return this.props.dispatch(actions.coinBaseSubview()) + case 'ShapeShift': + return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type)) + } +} diff --git a/ui/app/components/coinbase-form.js b/ui/app/components/coinbase-form.js new file mode 100644 index 000000000..f44d86045 --- /dev/null +++ b/ui/app/components/coinbase-form.js @@ -0,0 +1,63 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../actions') + +module.exports = connect(mapStateToProps)(CoinbaseForm) + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +inherits(CoinbaseForm, Component) + +function CoinbaseForm () { + Component.call(this) +} + +CoinbaseForm.prototype.render = function () { + var props = this.props + + return h('.flex-column', { + style: { + marginTop: '35px', + padding: '25px', + width: '100%', + }, + }, [ + h('.flex-row', { + style: { + justifyContent: 'space-around', + margin: '33px', + marginTop: '0px', + }, + }, [ + h('button.btn-green', { + onClick: this.toCoinbase.bind(this), + }, 'Continue to Coinbase'), + + h('button.btn-red', { + onClick: () => props.dispatch(actions.backTobuyView(props.accounts.address)), + }, 'Cancel'), + ]), + ]) +} + +CoinbaseForm.prototype.toCoinbase = function () { + const props = this.props + const address = props.buyView.buyAddress + props.dispatch(actions.buyEth({ network: '1', address, amount: 0 })) +} + +CoinbaseForm.prototype.renderLoading = function () { + return h('img', { + style: { + width: '27px', + marginRight: '-27px', + }, + src: 'images/loading.svg', + }) +} diff --git a/ui/app/components/copyButton.js b/ui/app/components/copyButton.js new file mode 100644 index 000000000..a25d0719c --- /dev/null +++ b/ui/app/components/copyButton.js @@ -0,0 +1,59 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const copyToClipboard = require('copy-to-clipboard') + +const Tooltip = require('./tooltip') + +module.exports = CopyButton + +inherits(CopyButton, Component) +function CopyButton () { + Component.call(this) +} + +// As parameters, accepts: +// "value", which is the value to copy (mandatory) +// "title", which is the text to show on hover (optional, defaults to 'Copy') +CopyButton.prototype.render = function () { + const props = this.props + const state = this.state || {} + + const value = props.value + const copied = state.copied + + const message = copied ? 'Copied' : props.title || ' Copy ' + + return h('.copy-button', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + + h(Tooltip, { + title: message, + }, [ + h('i.fa.fa-clipboard.cursor-pointer.color-orange', { + style: { + margin: '5px', + }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(value) + this.debounceRestore() + }, + }), + ]), + + ]) +} + +CopyButton.prototype.debounceRestore = function () { + this.setState({ copied: true }) + clearTimeout(this.timeout) + this.timeout = setTimeout(() => { + this.setState({ copied: false }) + }, 850) +} diff --git a/ui/app/components/copyable.js b/ui/app/components/copyable.js new file mode 100644 index 000000000..a4f6f4bc6 --- /dev/null +++ b/ui/app/components/copyable.js @@ -0,0 +1,46 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const Tooltip = require('./tooltip') +const copyToClipboard = require('copy-to-clipboard') + +module.exports = Copyable + +inherits(Copyable, Component) +function Copyable () { + Component.call(this) + this.state = { + copied: false, + } +} + +Copyable.prototype.render = function () { + const props = this.props + const state = this.state + const { value, children } = props + const { copied } = state + + return h(Tooltip, { + title: copied ? 'Copied!' : 'Copy', + position: 'bottom', + }, h('span', { + style: { + cursor: 'pointer', + }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(value) + this.debounceRestore() + }, + }, children)) +} + +Copyable.prototype.debounceRestore = function () { + this.setState({ copied: true }) + clearTimeout(this.timeout) + this.timeout = setTimeout(() => { + this.setState({ copied: false }) + }, 850) +} diff --git a/ui/app/components/custom-radio-list.js b/ui/app/components/custom-radio-list.js new file mode 100644 index 000000000..a4c525396 --- /dev/null +++ b/ui/app/components/custom-radio-list.js @@ -0,0 +1,60 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = RadioList + +inherits(RadioList, Component) +function RadioList () { + Component.call(this) +} + +RadioList.prototype.render = function () { + const props = this.props + const activeClass = '.custom-radio-selected' + const inactiveClass = '.custom-radio-inactive' + const { + labels, + defaultFocus, + } = props + + + return ( + h('.flex-row', { + style: { + fontSize: '12px', + }, + }, [ + h('.flex-column.custom-radios', { + style: { + marginRight: '5px', + }, + }, + labels.map((lable, i) => { + let isSelcted = (this.state !== null) + isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable) + return h(isSelcted ? activeClass : inactiveClass, { + title: lable, + onClick: (event) => { + this.setState({selected: event.target.title}) + props.onClick(event) + }, + }) + }) + ), + h('.text', {}, + labels.map((lable) => { + if (props.subtext) { + return h('.flex-row', {}, [ + h('.radio-titles', lable), + h('.radio-titles-subtext', `- ${props.subtext[lable]}`), + ]) + } else { + return h('.radio-titles', lable) + } + }) + ), + ]) + ) +} + diff --git a/ui/app/components/drop-menu-item.js b/ui/app/components/drop-menu-item.js new file mode 100644 index 000000000..e42948209 --- /dev/null +++ b/ui/app/components/drop-menu-item.js @@ -0,0 +1,59 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = DropMenuItem + +inherits(DropMenuItem, Component) +function DropMenuItem () { + Component.call(this) +} + +DropMenuItem.prototype.render = function () { + return h('li.drop-menu-item', { + onClick: () => { + this.props.closeMenu() + this.props.action() + }, + style: { + listStyle: 'none', + padding: '6px 16px 6px 5px', + fontFamily: 'Montserrat Regular', + color: 'rgb(125, 128, 130)', + cursor: 'pointer', + display: 'flex', + justifyContent: 'flex-start', + }, + }, [ + this.props.icon, + this.props.label, + this.activeNetworkRender(), + ]) +} + +DropMenuItem.prototype.activeNetworkRender = function () { + const activeNetwork = this.props.activeNetworkRender + const { provider } = this.props + const providerType = provider ? provider.type : null + if (activeNetwork === undefined) return + + switch (this.props.label) { + case 'Main Ethereum Network': + if (providerType === 'mainnet') return h('.check', '✓') + break + case 'Ropsten Test Network': + if (providerType === 'ropsten') return h('.check', '✓') + break + case 'Kovan Test Network': + if (providerType === 'kovan') return h('.check', '✓') + break + case 'Rinkeby Test Network': + if (providerType === 'rinkeby') return h('.check', '✓') + break + case 'Localhost 8545': + if (activeNetwork === 'http://localhost:8545') return h('.check', '✓') + break + default: + if (activeNetwork === 'custom') return h('.check', '✓') + } +} diff --git a/ui/app/components/editable-label.js b/ui/app/components/editable-label.js new file mode 100644 index 000000000..41936f5e0 --- /dev/null +++ b/ui/app/components/editable-label.js @@ -0,0 +1,51 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const findDOMNode = require('react-dom').findDOMNode + +module.exports = EditableLabel + +inherits(EditableLabel, Component) +function EditableLabel () { + Component.call(this) +} + +EditableLabel.prototype.render = function () { + const props = this.props + const state = this.state + + if (state && state.isEditingLabel) { + return h('div.editable-label', [ + h('input.sizing-input', { + defaultValue: props.textValue, + maxLength: '20', + onKeyPress: (event) => { + this.saveIfEnter(event) + }, + }), + h('button.editable-button', { + onClick: () => this.saveText(), + }, 'Save'), + ]) + } else { + return h('div.name-label', { + onClick: (event) => { + this.setState({ isEditingLabel: true }) + }, + }, this.props.children) + } +} + +EditableLabel.prototype.saveIfEnter = function (event) { + if (event.key === 'Enter') { + this.saveText() + } +} + +EditableLabel.prototype.saveText = function () { + var container = findDOMNode(this) + var text = container.querySelector('.editable-label input').value + var truncatedText = text.substring(0, 20) + this.props.saveText(truncatedText) + this.setState({ isEditingLabel: false, textLabel: truncatedText }) +} diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js new file mode 100644 index 000000000..3a33ebf74 --- /dev/null +++ b/ui/app/components/ens-input.js @@ -0,0 +1,170 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const extend = require('xtend') +const debounce = require('debounce') +const copyToClipboard = require('copy-to-clipboard') +const ENS = require('ethjs-ens') +const networkMap = require('ethjs-ens/lib/network-map.json') +const ensRE = /.+\.eth$/ +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + + +module.exports = EnsInput + +inherits(EnsInput, Component) +function EnsInput () { + Component.call(this) +} + +EnsInput.prototype.render = function () { + const props = this.props + const opts = extend(props, { + list: 'addresses', + onChange: () => { + const network = this.props.network + const networkHasEnsSupport = getNetworkEnsSupport(network) + if (!networkHasEnsSupport) return + + const recipient = document.querySelector('input[name="address"]').value + if (recipient.match(ensRE) === null) { + return this.setState({ + loadingEns: false, + ensResolution: null, + ensFailure: null, + }) + } + + this.setState({ + loadingEns: true, + }) + this.checkName() + }, + }) + return h('div', { + style: { width: '100%' }, + }, [ + h('input.large-input', opts), + // The address book functionality. + h('datalist#addresses', + [ + // Corresponds to the addresses owned. + Object.keys(props.identities).map((key) => { + const identity = props.identities[key] + return h('option', { + value: identity.address, + label: identity.name, + key: identity.address, + }) + }), + // Corresponds to previously sent-to addresses. + props.addressBook.map((identity) => { + return h('option', { + value: identity.address, + label: identity.name, + key: identity.address, + }) + }), + ]), + this.ensIcon(), + ]) +} + +EnsInput.prototype.componentDidMount = function () { + const network = this.props.network + const networkHasEnsSupport = getNetworkEnsSupport(network) + this.setState({ ensResolution: ZERO_ADDRESS }) + + if (networkHasEnsSupport) { + const provider = global.ethereumProvider + this.ens = new ENS({ provider, network }) + this.checkName = debounce(this.lookupEnsName.bind(this), 200) + } +} + +EnsInput.prototype.lookupEnsName = function () { + const recipient = document.querySelector('input[name="address"]').value + const { ensResolution } = this.state + + log.info(`ENS attempting to resolve name: ${recipient}`) + this.ens.lookup(recipient.trim()) + .then((address) => { + if (address === ZERO_ADDRESS) throw new Error('No address has been set for this name.') + if (address !== ensResolution) { + this.setState({ + loadingEns: false, + ensResolution: address, + nickname: recipient.trim(), + hoverText: address + '\nClick to Copy', + ensFailure: false, + }) + } + }) + .catch((reason) => { + log.error(reason) + return this.setState({ + loadingEns: false, + ensResolution: ZERO_ADDRESS, + ensFailure: true, + hoverText: reason.message, + }) + }) +} + +EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { + const state = this.state || {} + const ensResolution = state.ensResolution + // If an address is sent without a nickname, meaning not from ENS or from + // the user's own accounts, a default of a one-space string is used. + const nickname = state.nickname || ' ' + if (prevState && ensResolution && this.props.onChange && + ensResolution !== prevState.ensResolution) { + this.props.onChange(ensResolution, nickname) + } +} + +EnsInput.prototype.ensIcon = function (recipient) { + const { hoverText } = this.state || {} + return h('span', { + title: hoverText, + style: { + position: 'absolute', + padding: '9px', + transform: 'translatex(-40px)', + }, + }, this.ensIconContents(recipient)) +} + +EnsInput.prototype.ensIconContents = function (recipient) { + const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: ZERO_ADDRESS} + + if (loadingEns) { + return h('img', { + src: 'images/loading.svg', + style: { + width: '30px', + height: '30px', + transform: 'translateY(-6px)', + }, + }) + } + + if (ensFailure) { + return h('i.fa.fa-warning.fa-lg.warning') + } + + if (ensResolution && (ensResolution !== ZERO_ADDRESS)) { + return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', { + style: { color: 'green' }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(ensResolution) + }, + }) + } +} + +function getNetworkEnsSupport (network) { + return Boolean(networkMap[network]) +} diff --git a/ui/app/components/eth-balance.js b/ui/app/components/eth-balance.js new file mode 100644 index 000000000..4f538fd31 --- /dev/null +++ b/ui/app/components/eth-balance.js @@ -0,0 +1,89 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance +const generateBalanceObject = require('../util').generateBalanceObject +const Tooltip = require('./tooltip.js') +const FiatValue = require('./fiat-value.js') + +module.exports = EthBalanceComponent + +inherits(EthBalanceComponent, Component) +function EthBalanceComponent () { + Component.call(this) +} + +EthBalanceComponent.prototype.render = function () { + var props = this.props + let { value } = props + const { style, width } = props + var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true + value = value ? formatBalance(value, 6, needsParse) : '...' + + return ( + + h('.ether-balance.ether-balance-amount', { + style, + }, [ + h('div', { + style: { + display: 'inline', + width, + }, + }, this.renderBalance(value)), + ]) + + ) +} +EthBalanceComponent.prototype.renderBalance = function (value) { + var props = this.props + const { conversionRate, shorten, incoming, currentCurrency } = props + if (value === 'None') return value + if (value === '...') return value + var balanceObj = generateBalanceObject(value, shorten ? 1 : 3) + var balance + var splitBalance = value.split(' ') + var ethNumber = splitBalance[0] + var ethSuffix = splitBalance[1] + const showFiat = 'showFiat' in props ? props.showFiat : true + + if (shorten) { + balance = balanceObj.shortBalance + } else { + balance = balanceObj.balance + } + + var label = balanceObj.label + + return ( + h(Tooltip, { + position: 'bottom', + title: `${ethNumber} ${ethSuffix}`, + }, h('div.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + }, + }, incoming ? `+${balance}` : balance), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + }, + }, label), + ]), + + showFiat ? h(FiatValue, { value: props.value, conversionRate, currentCurrency }) : null, + ])) + ) +} diff --git a/ui/app/components/fiat-value.js b/ui/app/components/fiat-value.js new file mode 100644 index 000000000..8a64a1cfc --- /dev/null +++ b/ui/app/components/fiat-value.js @@ -0,0 +1,63 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance + +module.exports = FiatValue + +inherits(FiatValue, Component) +function FiatValue () { + Component.call(this) +} + +FiatValue.prototype.render = function () { + const props = this.props + const { conversionRate, currentCurrency } = props + + const value = formatBalance(props.value, 6) + + if (value === 'None') return value + var fiatDisplayNumber, fiatTooltipNumber + var splitBalance = value.split(' ') + + if (conversionRate !== 0) { + fiatTooltipNumber = Number(splitBalance[0]) * conversionRate + fiatDisplayNumber = fiatTooltipNumber.toFixed(2) + } else { + fiatDisplayNumber = 'N/A' + fiatTooltipNumber = 'Unknown' + } + + return fiatDisplay(fiatDisplayNumber, currentCurrency) +} + +function fiatDisplay (fiatDisplayNumber, fiatSuffix) { + if (fiatDisplayNumber !== 'N/A') { + return h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + fontSize: '12px', + color: '#333333', + }, + }, fiatDisplayNumber), + h('div', { + style: { + color: '#AEAEAE', + marginLeft: '5px', + fontSize: '12px', + }, + }, fiatSuffix), + ]) + } else { + return h('div') + } +} diff --git a/ui/app/components/hex-as-decimal-input.js b/ui/app/components/hex-as-decimal-input.js new file mode 100644 index 000000000..4a71e9585 --- /dev/null +++ b/ui/app/components/hex-as-decimal-input.js @@ -0,0 +1,154 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const extend = require('xtend') + +module.exports = HexAsDecimalInput + +inherits(HexAsDecimalInput, Component) +function HexAsDecimalInput () { + this.state = { invalid: null } + Component.call(this) +} + +/* Hex as Decimal Input + * + * A component for allowing easy, decimal editing + * of a passed in hex string value. + * + * On change, calls back its `onChange` function parameter + * and passes it an updated hex string. + */ + +HexAsDecimalInput.prototype.render = function () { + const props = this.props + const state = this.state + + const { value, onChange, min, max } = props + + const toEth = props.toEth + const suffix = props.suffix + const decimalValue = decimalize(value, toEth) + const style = props.style + + return ( + h('.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('input.hex-input', { + type: 'number', + required: true, + min: min, + max: max, + style: extend({ + display: 'block', + textAlign: 'right', + backgroundColor: 'transparent', + border: '1px solid #bdbdbd', + + }, style), + value: parseInt(decimalValue), + onBlur: (event) => { + this.updateValidity(event) + }, + onChange: (event) => { + this.updateValidity(event) + const hexString = (event.target.value === '') ? '' : hexify(event.target.value) + onChange(hexString) + }, + onInvalid: (event) => { + const msg = this.constructWarning() + if (msg === state.invalid) { + return + } + this.setState({ invalid: msg }) + event.preventDefault() + return false + }, + }), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + marginRight: '6px', + width: '20px', + }, + }, suffix), + ]), + + state.invalid ? h('span.error', { + style: { + position: 'absolute', + right: '0px', + textAlign: 'right', + transform: 'translateY(26px)', + padding: '3px', + background: 'rgba(255,255,255,0.85)', + zIndex: '1', + textTransform: 'capitalize', + border: '2px solid #E20202', + }, + }, state.invalid) : null, + ]) + ) +} + +HexAsDecimalInput.prototype.setValid = function (message) { + this.setState({ invalid: null }) +} + +HexAsDecimalInput.prototype.updateValidity = function (event) { + const target = event.target + const value = this.props.value + const newValue = target.value + + if (value === newValue) { + return + } + + const valid = target.checkValidity() + if (valid) { + this.setState({ invalid: null }) + } +} + +HexAsDecimalInput.prototype.constructWarning = function () { + const { name, min, max } = this.props + let message = name ? name + ' ' : '' + + if (min && max) { + message += `must be greater than or equal to ${min} and less than or equal to ${max}.` + } else if (min) { + message += `must be greater than or equal to ${min}.` + } else if (max) { + message += `must be less than or equal to ${max}.` + } else { + message += 'Invalid input.' + } + + return message +} + +function hexify (decimalString) { + const hexBN = new BN(parseInt(decimalString), 10) + return '0x' + hexBN.toString('hex') +} + +function decimalize (input, toEth) { + if (input === '') { + return '' + } else { + const strippedInput = ethUtil.stripHexPrefix(input) + const inputBN = new BN(strippedInput, 'hex') + return inputBN.toString(10) + } +} diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js new file mode 100644 index 000000000..c754bc6ba --- /dev/null +++ b/ui/app/components/identicon.js @@ -0,0 +1,72 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const isNode = require('detect-node') +const findDOMNode = require('react-dom').findDOMNode +const jazzicon = require('jazzicon') +const iconFactoryGen = require('../../lib/icon-factory') +const iconFactory = iconFactoryGen(jazzicon) + +module.exports = IdenticonComponent + +inherits(IdenticonComponent, Component) +function IdenticonComponent () { + Component.call(this) + + this.defaultDiameter = 46 +} + +IdenticonComponent.prototype.render = function () { + var props = this.props + var diameter = props.diameter || this.defaultDiameter + return ( + h('div', { + key: 'identicon-' + this.props.address, + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: diameter, + width: diameter, + borderRadius: diameter / 2, + overflow: 'hidden', + }, + }) + ) +} + +IdenticonComponent.prototype.componentDidMount = function () { + var props = this.props + const { address } = props + + if (!address) return + + var container = findDOMNode(this) + + var diameter = props.diameter || this.defaultDiameter + if (!isNode) { + var img = iconFactory.iconForAddress(address, diameter) + container.appendChild(img) + } +} + +IdenticonComponent.prototype.componentDidUpdate = function () { + var props = this.props + const { address } = props + + if (!address) return + + var container = findDOMNode(this) + + var children = container.children + for (var i = 0; i < children.length; i++) { + container.removeChild(children[i]) + } + + var diameter = props.diameter || this.defaultDiameter + if (!isNode) { + var img = iconFactory.iconForAddress(address, diameter) + container.appendChild(img) + } +} + diff --git a/ui/app/components/loading.js b/ui/app/components/loading.js new file mode 100644 index 000000000..87d6f5d20 --- /dev/null +++ b/ui/app/components/loading.js @@ -0,0 +1,53 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') + + +inherits(LoadingIndicator, Component) +module.exports = LoadingIndicator + +function LoadingIndicator () { + Component.call(this) +} + +LoadingIndicator.prototype.render = function () { + const { isLoading, loadingMessage } = this.props + + return ( + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'loader', + transitionEnterTimeout: 150, + transitionLeaveTimeout: 150, + }, [ + + isLoading ? h('div', { + style: { + zIndex: 10, + position: 'absolute', + flexDirection: 'column', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '100%', + width: '100%', + background: 'rgba(255, 255, 255, 0.8)', + }, + }, [ + h('img', { + src: 'images/loading.svg', + }), + + h('br'), + + showMessageIfAny(loadingMessage), + ]) : null, + ]) + ) +} + +function showMessageIfAny (loadingMessage) { + if (!loadingMessage) return null + return h('span', loadingMessage) +} diff --git a/ui/app/components/mascot.js b/ui/app/components/mascot.js new file mode 100644 index 000000000..973ec2cad --- /dev/null +++ b/ui/app/components/mascot.js @@ -0,0 +1,59 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const metamaskLogo = require('metamask-logo') +const debounce = require('debounce') + +module.exports = Mascot + +inherits(Mascot, Component) +function Mascot () { + Component.call(this) + this.logo = metamaskLogo({ + followMouse: true, + pxNotRatio: true, + width: 200, + height: 200, + }) + + this.refollowMouse = debounce(this.logo.setFollowMouse.bind(this.logo, true), 1000) + this.unfollowMouse = this.logo.setFollowMouse.bind(this.logo, false) +} + +Mascot.prototype.render = function () { + // this is a bit hacky + // the event emitter is on `this.props` + // and we dont get that until render + this.handleAnimationEvents() + + return h('#metamask-mascot-container', { + style: { zIndex: 0 }, + }) +} + +Mascot.prototype.componentDidMount = function () { + var targetDivId = 'metamask-mascot-container' + var container = document.getElementById(targetDivId) + container.appendChild(this.logo.container) +} + +Mascot.prototype.componentWillUnmount = function () { + this.animations = this.props.animationEventEmitter + this.animations.removeAllListeners() + this.logo.container.remove() + this.logo.stopAnimation() +} + +Mascot.prototype.handleAnimationEvents = function () { + // only setup listeners once + if (this.animations) return + this.animations = this.props.animationEventEmitter + this.animations.on('point', this.lookAt.bind(this)) + this.animations.on('setFollowMouse', this.logo.setFollowMouse.bind(this.logo)) +} + +Mascot.prototype.lookAt = function (target) { + this.unfollowMouse() + this.logo.lookAt(target) + this.refollowMouse() +} diff --git a/ui/app/components/mini-account-panel.js b/ui/app/components/mini-account-panel.js new file mode 100644 index 000000000..c09cf5b7a --- /dev/null +++ b/ui/app/components/mini-account-panel.js @@ -0,0 +1,74 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const Identicon = require('./identicon') + +module.exports = AccountPanel + + +inherits(AccountPanel, Component) +function AccountPanel () { + Component.call(this) +} + +AccountPanel.prototype.render = function () { + var props = this.props + var picOrder = props.picOrder || 'left' + const { imageSeed } = props + + return ( + + h('.identity-panel.flex-row.flex-left', { + style: { + cursor: props.onClick ? 'pointer' : undefined, + }, + onClick: props.onClick, + }, [ + + this.genIcon(imageSeed, picOrder), + + h('div.flex-column.flex-justify-center', { + style: { + lineHeight: '15px', + order: 2, + display: 'flex', + alignItems: picOrder === 'left' ? 'flex-begin' : 'flex-end', + }, + }, this.props.children), + ]) + ) +} + +AccountPanel.prototype.genIcon = function (seed, picOrder) { + const props = this.props + + // When there is no seed value, this is a contract creation. + // We then show the contract icon. + if (!seed) { + return h('.identicon-wrapper.flex-column.select-none', { + style: { + order: picOrder === 'left' ? 1 : 3, + }, + }, [ + h('i.fa.fa-file-text-o.fa-lg', { + style: { + fontSize: '42px', + transform: 'translate(0px, -16px)', + }, + }), + ]) + } + + // If there was a seed, we return an identicon for that address. + return h('.identicon-wrapper.flex-column.select-none', { + style: { + order: picOrder === 'left' ? 1 : 3, + }, + }, [ + h(Identicon, { + address: seed, + imageify: props.imageifyIdenticons, + }), + ]) +} + diff --git a/ui/app/components/network.js b/ui/app/components/network.js new file mode 100644 index 000000000..698a0bbb9 --- /dev/null +++ b/ui/app/components/network.js @@ -0,0 +1,124 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = Network + +inherits(Network, Component) + +function Network () { + Component.call(this) +} + +Network.prototype.render = function () { + const props = this.props + const networkNumber = props.network + let providerName + try { + providerName = props.provider.type + } catch (e) { + providerName = null + } + let iconName, hoverText + + if (networkNumber === 'loading') { + return h('span', { + style: { + display: 'flex', + alignItems: 'center', + flexDirection: 'row', + }, + onClick: (event) => this.props.onClick(event), + }, [ + h('img', { + title: 'Attempting to connect to blockchain.', + style: { + width: '27px', + }, + src: 'images/loading.svg', + }), + h('i.fa.fa-sort-desc'), + ]) + } else if (providerName === 'mainnet') { + hoverText = 'Main Ethereum Network' + iconName = 'ethereum-network' + } else if (providerName === 'ropsten') { + hoverText = 'Ropsten Test Network' + iconName = 'ropsten-test-network' + } else if (parseInt(networkNumber) === 3) { + hoverText = 'Ropsten Test Network' + iconName = 'ropsten-test-network' + } else if (providerName === 'kovan') { + hoverText = 'Kovan Test Network' + iconName = 'kovan-test-network' + } else if (providerName === 'rinkeby') { + hoverText = 'Rinkeby Test Network' + iconName = 'rinkeby-test-network' + } else { + hoverText = 'Unknown Private Network' + iconName = 'unknown-private-network' + } + + return ( + h('#network_component.pointer', { + title: hoverText, + onClick: (event) => this.props.onClick(event), + }, [ + (function () { + switch (iconName) { + case 'ethereum-network': + return h('.network-indicator', [ + h('.menu-icon.diamond'), + h('.network-name', { + style: { + color: '#039396', + }}, + 'Ethereum Main Net'), + ]) + case 'ropsten-test-network': + return h('.network-indicator', [ + h('.menu-icon.red-dot'), + h('.network-name', { + style: { + color: '#ff6666', + }}, + 'Ropsten Test Net'), + ]) + case 'kovan-test-network': + return h('.network-indicator', [ + h('.menu-icon.hollow-diamond'), + h('.network-name', { + style: { + color: '#690496', + }}, + 'Kovan Test Net'), + ]) + case 'rinkeby-test-network': + return h('.network-indicator', [ + h('.menu-icon.golden-square'), + h('.network-name', { + style: { + color: '#e7a218', + }}, + 'Rinkeby Test Net'), + ]) + default: + return h('.network-indicator', [ + h('i.fa.fa-question-circle.fa-lg', { + style: { + margin: '10px', + color: 'rgb(125, 128, 130)', + }, + }), + + h('.network-name', { + style: { + color: '#AEAEAE', + }}, + 'Private Network'), + ]) + } + })(), + ]) + ) +} diff --git a/ui/app/components/notice.js b/ui/app/components/notice.js new file mode 100644 index 000000000..d9f0067cd --- /dev/null +++ b/ui/app/components/notice.js @@ -0,0 +1,126 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const ReactMarkdown = require('react-markdown') +const linker = require('extension-link-enabler') +const findDOMNode = require('react-dom').findDOMNode + +module.exports = Notice + +inherits(Notice, Component) +function Notice () { + Component.call(this) +} + +Notice.prototype.render = function () { + const { notice, onConfirm } = this.props + const { title, date, body } = notice + const state = this.state || { disclaimerDisabled: true } + const disabled = state.disclaimerDisabled + + return ( + h('.flex-column.flex-center.flex-grow', [ + h('h3.flex-center.text-transform-uppercase.terms-header', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + width: '100%', + fontSize: '20px', + textAlign: 'center', + padding: 6, + }, + }, [ + title, + ]), + + h('h5.flex-center.text-transform-uppercase.terms-header', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + textAlign: 'center', + padding: 6, + }, + }, [ + date, + ]), + + h('style', ` + + .markdown { + overflow-x: hidden; + } + + .markdown h1, .markdown h2, .markdown h3 { + margin: 10px 0; + font-weight: bold; + } + + .markdown strong { + font-weight: bold; + } + .markdown em { + font-style: italic; + } + + .markdown p { + margin: 10px 0; + } + + .markdown a { + color: #df6b0e; + } + + `), + + h('div.markdown', { + onScroll: (e) => { + var object = e.currentTarget + if (object.offsetHeight + object.scrollTop + 100 >= object.scrollHeight) { + this.setState({disclaimerDisabled: false}) + } + }, + style: { + background: 'rgb(235, 235, 235)', + height: '310px', + padding: '6px', + width: '90%', + overflowY: 'scroll', + scroll: 'auto', + }, + }, [ + h(ReactMarkdown, { + className: 'notice-box', + source: body, + skipHtml: true, + }), + ]), + + h('button', { + disabled, + onClick: () => { + this.setState({disclaimerDisabled: true}) + onConfirm() + }, + style: { + marginTop: '18px', + }, + }, 'Accept'), + ]) + ) +} + +Notice.prototype.componentDidMount = function () { + var node = findDOMNode(this) + linker.setupListener(node) + if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) { + this.setState({disclaimerDisabled: false}) + } +} + +Notice.prototype.componentWillUnmount = function () { + var node = findDOMNode(this) + linker.teardownListener(node) +} diff --git a/ui/app/components/pending-msg-details.js b/ui/app/components/pending-msg-details.js new file mode 100644 index 000000000..16308d121 --- /dev/null +++ b/ui/app/components/pending-msg-details.js @@ -0,0 +1,50 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const AccountPanel = require('./account-panel') + +module.exports = PendingMsgDetails + +inherits(PendingMsgDetails, Component) +function PendingMsgDetails () { + Component.call(this) +} + +PendingMsgDetails.prototype.render = function () { + var state = this.props + var msgData = state.txData + + var msgParams = msgData.msgParams || {} + var address = msgParams.from || state.selectedAddress + var identity = state.identities[address] || { address: address } + var account = state.accounts[address] || { address: address } + + return ( + h('div', { + key: msgData.id, + style: { + margin: '10px 20px', + }, + }, [ + + // account that will sign + h(AccountPanel, { + showFullAddress: true, + identity: identity, + account: account, + imageifyIdenticons: state.imageifyIdenticons, + }), + + // message data + h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-row.flex-space-between', [ + h('label.font-small', 'MESSAGE'), + h('span.font-small', msgParams.data), + ]), + ]), + + ]) + ) +} + diff --git a/ui/app/components/pending-msg.js b/ui/app/components/pending-msg.js new file mode 100644 index 000000000..b2cac164a --- /dev/null +++ b/ui/app/components/pending-msg.js @@ -0,0 +1,56 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const PendingTxDetails = require('./pending-msg-details') + +module.exports = PendingMsg + +inherits(PendingMsg, Component) +function PendingMsg () { + Component.call(this) +} + +PendingMsg.prototype.render = function () { + var state = this.props + var msgData = state.txData + + return ( + + h('div', { + key: msgData.id, + }, [ + + // header + h('h3', { + style: { + fontWeight: 'bold', + textAlign: 'center', + }, + }, 'Sign Message'), + + h('.error', { + style: { + margin: '10px', + }, + }, `Signing this message can have + dangerous side effects. Only sign messages from + sites you fully trust with your entire account. + This will be fixed in a future version.`), + + // message details + h(PendingTxDetails, state), + + // sign + cancel + h('.flex-row.flex-space-around', [ + h('button', { + onClick: state.cancelMessage, + }, 'Cancel'), + h('button', { + onClick: state.signMessage, + }, 'Sign'), + ]), + ]) + + ) +} + diff --git a/ui/app/components/pending-personal-msg-details.js b/ui/app/components/pending-personal-msg-details.js new file mode 100644 index 000000000..1050513f2 --- /dev/null +++ b/ui/app/components/pending-personal-msg-details.js @@ -0,0 +1,60 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const AccountPanel = require('./account-panel') +const BinaryRenderer = require('./binary-renderer') + +module.exports = PendingMsgDetails + +inherits(PendingMsgDetails, Component) +function PendingMsgDetails () { + Component.call(this) +} + +PendingMsgDetails.prototype.render = function () { + var state = this.props + var msgData = state.txData + + var msgParams = msgData.msgParams || {} + var address = msgParams.from || state.selectedAddress + var identity = state.identities[address] || { address: address } + var account = state.accounts[address] || { address: address } + + var { data } = msgParams + + return ( + h('div', { + key: msgData.id, + style: { + margin: '10px 20px', + }, + }, [ + + // account that will sign + h(AccountPanel, { + showFullAddress: true, + identity: identity, + account: account, + imageifyIdenticons: state.imageifyIdenticons, + }), + + // message data + h('div', { + style: { + height: '260px', + }, + }, [ + h('label.font-small', { style: { display: 'block' } }, 'MESSAGE'), + h(BinaryRenderer, { + value: data, + style: { + height: '215px', + }, + }), + ]), + + ]) + ) +} + diff --git a/ui/app/components/pending-personal-msg.js b/ui/app/components/pending-personal-msg.js new file mode 100644 index 000000000..4542adb28 --- /dev/null +++ b/ui/app/components/pending-personal-msg.js @@ -0,0 +1,47 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const PendingTxDetails = require('./pending-personal-msg-details') + +module.exports = PendingMsg + +inherits(PendingMsg, Component) +function PendingMsg () { + Component.call(this) +} + +PendingMsg.prototype.render = function () { + var state = this.props + var msgData = state.txData + + return ( + + h('div', { + key: msgData.id, + }, [ + + // header + h('h3', { + style: { + fontWeight: 'bold', + textAlign: 'center', + }, + }, 'Sign Message'), + + // message details + h(PendingTxDetails, state), + + // sign + cancel + h('.flex-row.flex-space-around', [ + h('button', { + onClick: state.cancelPersonalMessage, + }, 'Cancel'), + h('button', { + onClick: state.signPersonalMessage, + }, 'Sign'), + ]), + ]) + + ) +} + diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js new file mode 100644 index 000000000..d7d602f31 --- /dev/null +++ b/ui/app/components/pending-tx.js @@ -0,0 +1,480 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const actions = require('../actions') +const clone = require('clone') + +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const hexToBn = require('../../../app/scripts/lib/hex-to-bn') +const util = require('../util') +const MiniAccountPanel = require('./mini-account-panel') +const Copyable = require('./copyable') +const EthBalance = require('./eth-balance') +const addressSummary = util.addressSummary +const nameForAddress = require('../../lib/contract-namer') +const BNInput = require('./bn-as-decimal-input') + +const MIN_GAS_PRICE_GWEI_BN = new BN(2) +const GWEI_FACTOR = new BN(1e9) +const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) +const MIN_GAS_LIMIT_BN = new BN(21000) + +module.exports = PendingTx +inherits(PendingTx, Component) +function PendingTx () { + Component.call(this) + this.state = { + valid: true, + txData: null, + submitting: false, + } +} + +PendingTx.prototype.render = function () { + const props = this.props + const { currentCurrency, blockGasLimit } = props + + const conversionRate = props.conversionRate + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + + // Account Details + const address = txParams.from || props.selectedAddress + const identity = props.identities[address] || { address: address } + const account = props.accounts[address] + const balance = account ? account.balance : '0x0' + + // recipient check + const isValidAddress = !txParams.to || util.isValidAddress(txParams.to) + + // Gas + const gas = txParams.gas + const gasBn = hexToBn(gas) + const gasLimit = new BN(parseInt(blockGasLimit)) + const safeGasLimit = this.bnMultiplyByFraction(gasLimit, 19, 20).toString(10) + + // Gas Price + const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) + const gasPriceBn = hexToBn(gasPrice) + + const txFeeBn = gasBn.mul(gasPriceBn) + const valueBn = hexToBn(txParams.value) + const maxCost = txFeeBn.add(valueBn) + + const dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0 + + const balanceBn = hexToBn(balance) + const insufficientBalance = balanceBn.lt(maxCost) + + this.inputs = [] + + return ( + + h('div', { + key: txMeta.id, + }, [ + + h('form#pending-tx-form', { + onSubmit: this.onSubmit.bind(this), + + }, [ + + // tx info + h('div', [ + + h('.flex-row.flex-center', { + style: { + maxWidth: '100%', + }, + }, [ + + h(MiniAccountPanel, { + imageSeed: address, + picOrder: 'right', + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, identity.name), + + h(Copyable, { + value: ethUtil.toChecksumAddress(address), + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, addressSummary(address, 6, 4, false)), + ]), + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, [ + h(EthBalance, { + value: balance, + conversionRate, + currentCurrency, + inline: true, + labelColor: '#F7861C', + }), + ]), + ]), + + forwardCarrat(), + + this.miniAccountPanelForRecipient(), + ]), + + h('style', ` + .table-box { + margin: 7px 0px 0px 0px; + width: 100%; + } + .table-box .row { + margin: 0px; + background: rgb(236,236,236); + display: flex; + justify-content: space-between; + font-family: Montserrat Light, sans-serif; + font-size: 13px; + padding: 5px 25px; + } + .table-box .row .value { + font-family: Montserrat Regular; + } + `), + + h('.table-box', [ + + // Ether Value + // Currently not customizable, but easily modified + // in the way that gas and gasLimit currently are. + h('.row', [ + h('.cell.label', 'Amount'), + h(EthBalance, { value: txParams.value, currentCurrency, conversionRate }), + ]), + + // Gas Limit (customizable) + h('.cell.row', [ + h('.cell.label', 'Gas Limit'), + h('.cell.value', { + }, [ + h(BNInput, { + name: 'Gas Limit', + value: gasBn, + precision: 0, + scale: 0, + // The hard lower limit for gas. + min: MIN_GAS_LIMIT_BN.toString(10), + max: safeGasLimit, + suffix: 'UNITS', + style: { + position: 'relative', + top: '5px', + }, + onChange: this.gasLimitChanged.bind(this), + + ref: (hexInput) => { this.inputs.push(hexInput) }, + }), + ]), + ]), + + // Gas Price (customizable) + h('.cell.row', [ + h('.cell.label', 'Gas Price'), + h('.cell.value', { + }, [ + h(BNInput, { + name: 'Gas Price', + value: gasPriceBn, + precision: 9, + scale: 9, + suffix: 'GWEI', + min: MIN_GAS_PRICE_GWEI_BN.toString(10), + style: { + position: 'relative', + top: '5px', + }, + onChange: this.gasPriceChanged.bind(this), + ref: (hexInput) => { this.inputs.push(hexInput) }, + }), + ]), + ]), + + // Max Transaction Fee (calculated) + h('.cell.row', [ + h('.cell.label', 'Max Transaction Fee'), + h(EthBalance, { value: txFeeBn.toString(16), currentCurrency, conversionRate }), + ]), + + h('.cell.row', { + style: { + fontFamily: 'Montserrat Regular', + background: 'white', + padding: '10px 25px', + }, + }, [ + h('.cell.label', 'Max Total'), + h('.cell.value', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + h(EthBalance, { + value: maxCost.toString(16), + currentCurrency, + conversionRate, + inline: true, + labelColor: 'black', + fontSize: '16px', + }), + ]), + ]), + + // Data size row: + h('.cell.row', { + style: { + background: '#f7f7f7', + paddingBottom: '0px', + }, + }, [ + h('.cell.label'), + h('.cell.value', { + style: { + fontFamily: 'Montserrat Light', + fontSize: '11px', + }, + }, `Data included: ${dataLength} bytes`), + ]), + ]), // End of Table + + ]), + + h('style', ` + .conf-buttons button { + margin-left: 10px; + text-transform: uppercase; + } + `), + + txMeta.simulationFails ? + h('.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Transaction Error. Exception thrown in contract code.') + : null, + + !isValidAddress ? + h('.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.') + : null, + + insufficientBalance ? + h('span.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Insufficient balance for transaction') + : null, + + // send + cancel + h('.flex-row.flex-space-around.conf-buttons', { + style: { + display: 'flex', + justifyContent: 'flex-end', + margin: '14px 25px', + }, + }, [ + + + insufficientBalance ? + h('button.btn-green', { + onClick: props.buyEth, + }, 'Buy Ether') + : null, + + h('button', { + onClick: (event) => { + this.resetGasFields() + event.preventDefault() + }, + }, 'Reset'), + + // Accept Button + h('input.confirm.btn-green', { + type: 'submit', + value: 'SUBMIT', + style: { marginLeft: '10px' }, + disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, + }), + + h('button.cancel.btn-red', { + onClick: props.cancelTransaction, + }, 'Reject'), + ]), + ]), + ]) + ) +} + +PendingTx.prototype.miniAccountPanelForRecipient = function () { + const props = this.props + const txData = props.txData + const txParams = txData.txParams || {} + const isContractDeploy = !('to' in txParams) + + // If it's not a contract deploy, send to the account + if (!isContractDeploy) { + return h(MiniAccountPanel, { + imageSeed: txParams.to, + picOrder: 'left', + }, [ + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, nameForAddress(txParams.to, props.identities)), + + h(Copyable, { + value: ethUtil.toChecksumAddress(txParams.to), + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, addressSummary(txParams.to, 6, 4, false)), + ]), + + ]) + } else { + return h(MiniAccountPanel, { + picOrder: 'left', + }, [ + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, 'New Contract'), + + ]) + } +} + +PendingTx.prototype.gasPriceChanged = function (newBN, valid) { + log.info(`Gas price changed to: ${newBN.toString(10)}`) + const txMeta = this.gatherTxMeta() + txMeta.txParams.gasPrice = '0x' + newBN.toString('hex') + this.setState({ + txData: clone(txMeta), + valid, + }) +} + +PendingTx.prototype.gasLimitChanged = function (newBN, valid) { + log.info(`Gas limit changed to ${newBN.toString(10)}`) + const txMeta = this.gatherTxMeta() + txMeta.txParams.gas = '0x' + newBN.toString('hex') + this.setState({ + txData: clone(txMeta), + valid, + }) +} + +PendingTx.prototype.resetGasFields = function () { + log.debug(`pending-tx resetGasFields`) + + this.inputs.forEach((hexInput) => { + if (hexInput) { + hexInput.setValid() + } + }) + + this.setState({ + txData: null, + valid: true, + }) +} + +PendingTx.prototype.onSubmit = function (event) { + event.preventDefault() + const txMeta = this.gatherTxMeta() + const valid = this.checkValidity() + this.setState({ valid, submitting: true }) + if (valid && this.verifyGasParams()) { + this.props.sendTransaction(txMeta, event) + } else { + this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) + this.setState({ submitting: false }) + } +} + +PendingTx.prototype.checkValidity = function () { + const form = this.getFormEl() + const valid = form.checkValidity() + return valid +} + +PendingTx.prototype.getFormEl = function () { + const form = document.querySelector('form#pending-tx-form') + // Stub out form for unit tests: + if (!form) { + return { checkValidity () { return true } } + } + return form +} + +// After a customizable state value has been updated, +PendingTx.prototype.gatherTxMeta = function () { + log.debug(`pending-tx gatherTxMeta`) + const props = this.props + const state = this.state + const txData = clone(state.txData) || clone(props.txData) + + log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) + return txData +} + +PendingTx.prototype.verifyGasParams = function () { + // We call this in case the gas has not been modified at all + if (!this.state) { return true } + return ( + this._notZeroOrEmptyString(this.state.gas) && + this._notZeroOrEmptyString(this.state.gasPrice) + ) +} + +PendingTx.prototype._notZeroOrEmptyString = function (obj) { + return obj !== '' && obj !== '0x0' +} + +PendingTx.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) { + const numBN = new BN(numerator) + const denomBN = new BN(denominator) + return targetBN.mul(numBN).div(denomBN) +} + +function forwardCarrat () { + return ( + h('img', { + src: 'images/forward-carrat.svg', + style: { + padding: '5px 6px 0px 10px', + height: '37px', + }, + }) + ) +} diff --git a/ui/app/components/qr-code.js b/ui/app/components/qr-code.js new file mode 100644 index 000000000..06b9aed9b --- /dev/null +++ b/ui/app/components/qr-code.js @@ -0,0 +1,79 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const qrCode = require('qrcode-npm').qrcode +const inherits = require('util').inherits +const connect = require('react-redux').connect +const isHexPrefixed = require('ethereumjs-util').isHexPrefixed +const CopyButton = require('./copyButton') + +module.exports = connect(mapStateToProps)(QrCodeView) + +function mapStateToProps (state) { + return { + Qr: state.appState.Qr, + buyView: state.appState.buyView, + warning: state.appState.warning, + } +} + +inherits(QrCodeView, Component) + +function QrCodeView () { + Component.call(this) +} + +QrCodeView.prototype.render = function () { + const props = this.props + const Qr = props.Qr + const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}` + const qrImage = qrCode(4, 'M') + qrImage.addData(address) + qrImage.make() + return h('.main-container.flex-column', { + key: 'qr', + style: { + justifyContent: 'center', + paddingBottom: '45px', + paddingLeft: '45px', + paddingRight: '45px', + alignItems: 'center', + }, + }, [ + Array.isArray(Qr.message) ? h('.message-container', this.renderMultiMessage()) : h('.qr-header', Qr.message), + + this.props.warning ? this.props.warning && h('span.error.flex-center', { + style: { + textAlign: 'center', + width: '229px', + height: '82px', + }, + }, + this.props.warning) : null, + + h('#qr-container.flex-column', { + style: { + marginTop: '25px', + marginBottom: '15px', + }, + dangerouslySetInnerHTML: { + __html: qrImage.createTableTag(4), + }, + }), + h('.flex-row', [ + h('h3.ellip-address', { + style: { + width: '247px', + }, + }, Qr.data), + h(CopyButton, { + value: Qr.data, + }), + ]), + ]) +} + +QrCodeView.prototype.renderMultiMessage = function () { + var Qr = this.props.Qr + var multiMessage = Qr.message.map((message) => h('.qr-message', message)) + return multiMessage +} diff --git a/ui/app/components/range-slider.js b/ui/app/components/range-slider.js new file mode 100644 index 000000000..823f5eb01 --- /dev/null +++ b/ui/app/components/range-slider.js @@ -0,0 +1,58 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = RangeSlider + +inherits(RangeSlider, Component) +function RangeSlider () { + Component.call(this) +} + +RangeSlider.prototype.render = function () { + const state = this.state || {} + const props = this.props + const onInput = props.onInput || function () {} + const name = props.name + const { + min = 0, + max = 100, + increment = 1, + defaultValue = 50, + mirrorInput = false, + } = this.props.options + const {container, input, range} = props.style + + return ( + h('.flex-row', { + style: container, + }, [ + h('input', { + type: 'range', + name: name, + min: min, + max: max, + step: increment, + style: range, + value: state.value || defaultValue, + onChange: mirrorInput ? this.mirrorInputs.bind(this, event) : onInput, + }), + + // Mirrored input for range + mirrorInput ? h('input.large-input', { + type: 'number', + name: `${name}Mirror`, + min: min, + max: max, + value: state.value || defaultValue, + step: increment, + style: input, + onChange: this.mirrorInputs.bind(this, event), + }) : null, + ]) + ) +} + +RangeSlider.prototype.mirrorInputs = function (event) { + this.setState({value: event.target.value}) +} diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js new file mode 100644 index 000000000..e0a720426 --- /dev/null +++ b/ui/app/components/shapeshift-form.js @@ -0,0 +1,306 @@ +const PersistentForm = require('../../lib/persistent-form') +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +const actions = require('../actions') +const Qr = require('./qr-code') +const isValidAddress = require('../util').isValidAddress +module.exports = connect(mapStateToProps)(ShapeshiftForm) + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + isSubLoading: state.appState.isSubLoading, + qrRequested: state.appState.qrRequested, + } +} + +inherits(ShapeshiftForm, PersistentForm) + +function ShapeshiftForm () { + PersistentForm.call(this) + this.persistentFormParentId = 'shapeshift-buy-form' +} + +ShapeshiftForm.prototype.render = function () { + return h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain(), + ]) +} + +ShapeshiftForm.prototype.renderMain = function () { + const marketinfo = this.props.buyView.formView.marketinfo + const coinOptions = this.props.buyView.formView.coinOptions + var coin = marketinfo.pair.split('_')[0].toUpperCase() + + return h('.flex-column', { + style: { + // marginTop: '10px', + padding: '25px', + paddingTop: '5px', + width: '100%', + minHeight: '215px', + alignItems: 'center', + overflowY: 'auto', + }, + }, [ + h('.flex-row', { + style: { + justifyContent: 'center', + alignItems: 'baseline', + height: '42px', + }, + }, [ + h('img', { + src: coinOptions[coin].image, + width: '25px', + height: '25px', + style: { + marginRight: '5px', + }, + }), + + h('.input-container', [ + h('input#fromCoin.buy-inputs.ex-coins', { + type: 'text', + list: 'coinList', + autoFocus: true, + dataset: { + persistentFormId: 'input-coin', + }, + style: { + boxSizing: 'border-box', + }, + onChange: this.handleLiveInput.bind(this), + defaultValue: 'BTC', + }), + + this.renderCoinList(), + + h('i.fa.fa-pencil-square-o.edit-text', { + style: { + fontSize: '12px', + color: '#F7861C', + position: 'relative', + bottom: '48px', + left: '106px', + }, + }), + ]), + + h('.icon-control', [ + h('i.fa.fa-refresh.fa-4.orange', { + style: { + bottom: '5px', + left: '5px', + color: '#F7861C', + }, + onClick: this.updateCoin.bind(this), + }), + h('i.fa.fa-chevron-right.fa-4.orange', { + style: { + position: 'relative', + bottom: '26px', + left: '10px', + color: '#F7861C', + }, + onClick: this.updateCoin.bind(this), + }), + ]), + + h('#toCoin.ex-coins', marketinfo.pair.split('_')[1].toUpperCase()), + + h('img', { + src: coinOptions[marketinfo.pair.split('_')[1].toUpperCase()].image, + width: '25px', + height: '25px', + style: { + marginLeft: '5px', + }, + }), + ]), + h('.flex-column', { + style: { + alignItems: 'flex-start', + }, + }, [ + this.props.warning ? this.props.warning && h('span.error.flex-center', { + style: { + textAlign: 'center', + width: '229px', + height: '82px', + }, + }, + this.props.warning) : this.renderInfo(), + ]), + + h(this.activeToggle('.input-container'), { + style: { + padding: '10px', + paddingTop: '0px', + width: '100%', + }, + }, [ + + h('div', `${coin} Address:`), + + h('input#fromCoinAddress.buy-inputs', { + type: 'text', + placeholder: `Your ${coin} Refund Address`, + dataset: { + persistentFormId: 'refund-address', + }, + style: { + boxSizing: 'border-box', + width: '227px', + height: '30px', + padding: ' 5px ', + }, + }), + + h('i.fa.fa-pencil-square-o.edit-text', { + style: { + fontSize: '12px', + color: '#F7861C', + position: 'relative', + bottom: '10px', + right: '11px', + }, + }), + h('.flex-row', { + style: { + justifyContent: 'flex-end', + }, + }, [ + h('button', { + onClick: this.shift.bind(this), + style: { + marginTop: '10px', + position: 'relative', + bottom: '40px', + }, + }, + 'Submit'), + ]), + ]), + ]) +} + +ShapeshiftForm.prototype.shift = function () { + var props = this.props + var withdrawal = this.props.buyView.buyAddress + var returnAddress = document.getElementById('fromCoinAddress').value + var pair = this.props.buyView.formView.marketinfo.pair + var data = { + 'withdrawal': withdrawal, + 'pair': pair, + 'returnAddress': returnAddress, + // Public api key + 'apiKey': '803d1f5df2ed1b1476e4b9e6bcd089e34d8874595dda6a23b67d93c56ea9cc2445e98a6748b219b2b6ad654d9f075f1f1db139abfa93158c04e825db122c14b6', + } + var message = [ + `Deposit Limit: ${props.buyView.formView.marketinfo.limit}`, + `Deposit Minimum:${props.buyView.formView.marketinfo.minimum}`, + ] + if (isValidAddress(withdrawal)) { + this.props.dispatch(actions.coinShiftRquest(data, message)) + } +} + +ShapeshiftForm.prototype.renderCoinList = function () { + var list = Object.keys(this.props.buyView.formView.coinOptions).map((item) => { + return h('option', { + value: item, + }, item) + }) + + return h('datalist#coinList', { + onClick: (event) => { + event.preventDefault() + }, + }, list) +} + +ShapeshiftForm.prototype.updateCoin = function (event) { + event.preventDefault() + const props = this.props + var coinOptions = this.props.buyView.formView.coinOptions + var coin = document.getElementById('fromCoin').value + + if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { + var message = 'Not a valid coin' + return props.dispatch(actions.displayWarning(message)) + } else { + return props.dispatch(actions.pairUpdate(coin)) + } +} + +ShapeshiftForm.prototype.handleLiveInput = function () { + const props = this.props + var coinOptions = this.props.buyView.formView.coinOptions + var coin = document.getElementById('fromCoin').value + + if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { + return null + } else { + return props.dispatch(actions.pairUpdate(coin)) + } +} + +ShapeshiftForm.prototype.renderInfo = function () { + const marketinfo = this.props.buyView.formView.marketinfo + const coinOptions = this.props.buyView.formView.coinOptions + var coin = marketinfo.pair.split('_')[0].toUpperCase() + + return h('span', { + style: { + }, + }, [ + h('h3.flex-row.text-transform-uppercase', { + style: { + color: '#868686', + paddingTop: '4px', + justifyContent: 'space-around', + textAlign: 'center', + fontSize: '17px', + }, + }, `Market Info for ${marketinfo.pair.replace('_', ' to ').toUpperCase()}:`), + h('.marketinfo', ['Status : ', `${coinOptions[coin].status}`]), + h('.marketinfo', ['Exchange Rate: ', `${marketinfo.rate}`]), + h('.marketinfo', ['Limit: ', `${marketinfo.limit}`]), + h('.marketinfo', ['Minimum : ', `${marketinfo.minimum}`]), + ]) +} + +ShapeshiftForm.prototype.activeToggle = function (elementType) { + if (!this.props.buyView.formView.response || this.props.warning) return elementType + return `${elementType}.inactive` +} + +ShapeshiftForm.prototype.renderLoading = function () { + return h('span', { + style: { + position: 'absolute', + left: '70px', + bottom: '194px', + background: 'transparent', + width: '229px', + height: '82px', + display: 'flex', + justifyContent: 'center', + }, + }, [ + h('img', { + style: { + width: '60px', + }, + src: 'images/loading.svg', + }), + ]) +} diff --git a/ui/app/components/shift-list-item.js b/ui/app/components/shift-list-item.js new file mode 100644 index 000000000..32bfbeda4 --- /dev/null +++ b/ui/app/components/shift-list-item.js @@ -0,0 +1,204 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const vreme = new (require('vreme')) +const explorerLink = require('../../lib/explorer-link') +const actions = require('../actions') +const addressSummary = require('../util').addressSummary + +const CopyButton = require('./copyButton') +const EthBalance = require('./eth-balance') +const Tooltip = require('./tooltip') + + +module.exports = connect(mapStateToProps)(ShiftListItem) + +function mapStateToProps (state) { + return { + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + } +} + +inherits(ShiftListItem, Component) + +function ShiftListItem () { + Component.call(this) +} + +ShiftListItem.prototype.render = function () { + return ( + h('.transaction-list-item.flex-row', { + style: { + paddingTop: '20px', + paddingBottom: '20px', + justifyContent: 'space-around', + alignItems: 'center', + }, + }, [ + h('div', { + style: { + width: '0px', + position: 'relative', + bottom: '19px', + }, + }, [ + h('img', { + src: 'https://info.shapeshift.io/sites/default/files/logo.png', + style: { + height: '35px', + width: '132px', + position: 'absolute', + clip: 'rect(0px,23px,34px,0px)', + }, + }), + ]), + + this.renderInfo(), + this.renderUtilComponents(), + ]) + ) +} + +function formatDate (date) { + return vreme.format(new Date(date), 'March 16 2014 14:30') +} + +ShiftListItem.prototype.renderUtilComponents = function () { + var props = this.props + const { conversionRate, currentCurrency } = props + + switch (props.response.status) { + case 'no_deposits': + return h('.flex-row', [ + h(CopyButton, { + value: this.props.depositAddress, + }), + h(Tooltip, { + title: 'QR Code', + }, [ + h('i.fa.fa-qrcode.pointer.pop-hover', { + onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType)), + style: { + margin: '5px', + marginLeft: '23px', + marginRight: '12px', + fontSize: '20px', + color: '#F7861C', + }, + }), + ]), + ]) + case 'received': + return h('.flex-row') + + case 'complete': + return h('.flex-row', [ + h(CopyButton, { + value: this.props.response.transaction, + }), + h(EthBalance, { + value: `${props.response.outgoingCoin}`, + conversionRate, + currentCurrency, + width: '55px', + shorten: true, + needsParse: false, + incoming: true, + style: { + fontSize: '15px', + color: '#01888C', + }, + }), + ]) + + case 'failed': + return '' + default: + return '' + } +} + +ShiftListItem.prototype.renderInfo = function () { + var props = this.props + switch (props.response.status) { + case 'no_deposits': + return h('.flex-column', { + style: { + width: '200px', + overflow: 'hidden', + }, + }, [ + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, `${props.depositType} to ETH via ShapeShift`), + h('div', 'No deposits received'), + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, formatDate(props.time)), + ]) + case 'received': + return h('.flex-column', { + style: { + width: '200px', + overflow: 'hidden', + }, + }, [ + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, `${props.depositType} to ETH via ShapeShift`), + h('div', 'Conversion in progress'), + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, formatDate(props.time)), + ]) + case 'complete': + var url = explorerLink(props.response.transaction, parseInt('1')) + + return h('.flex-column.pointer', { + style: { + width: '200px', + overflow: 'hidden', + }, + onClick: () => global.platform.openWindow({ url }), + }, [ + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, 'From ShapeShift'), + h('div', formatDate(props.time)), + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, addressSummary(props.response.transaction)), + ]) + + case 'failed': + return h('span.error', '(Failed)') + default: + return '' + } +} diff --git a/ui/app/components/tab-bar.js b/ui/app/components/tab-bar.js new file mode 100644 index 000000000..6295e7dd9 --- /dev/null +++ b/ui/app/components/tab-bar.js @@ -0,0 +1,36 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = TabBar + +inherits(TabBar, Component) +function TabBar () { + Component.call(this) +} + +TabBar.prototype.render = function () { + const props = this.props + const state = this.state || {} + const { tabs = [], defaultTab, tabSelected } = props + const { subview = defaultTab } = state + + return ( + h('.flex-row.space-around.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + paddingTop: '4px', + }, + }, tabs.map((tab) => { + const { key, content } = tab + return h(subview === key ? '.activeForm' : '.inactiveForm.pointer', { + onClick: () => { + this.setState({ subview: key }) + tabSelected(key) + }, + }, content) + })) + ) +} + diff --git a/ui/app/components/template.js b/ui/app/components/template.js new file mode 100644 index 000000000..b6ed8eaa0 --- /dev/null +++ b/ui/app/components/template.js @@ -0,0 +1,18 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = NewComponent + +inherits(NewComponent, Component) +function NewComponent () { + Component.call(this) +} + +NewComponent.prototype.render = function () { + const props = this.props + + return ( + h('span', props.message) + ) +} diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js new file mode 100644 index 000000000..19d7139bb --- /dev/null +++ b/ui/app/components/token-cell.js @@ -0,0 +1,72 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Identicon = require('./identicon') +const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') + +module.exports = TokenCell + +inherits(TokenCell, Component) +function TokenCell () { + Component.call(this) +} + +TokenCell.prototype.render = function () { + const props = this.props + const { address, symbol, string, network, userAddress } = props + + return ( + h('li.token-cell', { + style: { cursor: network === '1' ? 'pointer' : 'default' }, + onClick: this.view.bind(this, address, userAddress, network), + }, [ + + h(Identicon, { + diameter: 50, + address, + network, + }), + + h('h3', `${string || 0} ${symbol}`), + + h('span', { style: { flex: '1 0 auto' } }), + + /* + h('button', { + onClick: this.send.bind(this, address), + }, 'SEND'), + */ + + ]) + ) +} + +TokenCell.prototype.send = function (address, event) { + event.preventDefault() + event.stopPropagation() + const url = tokenFactoryFor(address) + if (url) { + navigateTo(url) + } +} + +TokenCell.prototype.view = function (address, userAddress, network, event) { + const url = etherscanLinkFor(address, userAddress, network) + if (url) { + navigateTo(url) + } +} + +function navigateTo (url) { + global.platform.openWindow({ url }) +} + +function etherscanLinkFor (tokenAddress, address, network) { + const prefix = prefixForNetwork(network) + return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}` +} + +function tokenFactoryFor (tokenAddress) { + return `https://tokenfactory.surge.sh/#/token/${tokenAddress}` +} + diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js new file mode 100644 index 000000000..20cfa897e --- /dev/null +++ b/ui/app/components/token-list.js @@ -0,0 +1,192 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const TokenTracker = require('eth-token-tracker') +const TokenCell = require('./token-cell.js') +const normalizeAddress = require('eth-sig-util').normalize + +const defaultTokens = [] +const contracts = require('eth-contract-metadata') +for (const address in contracts) { + const contract = contracts[address] + if (contract.erc20) { + contract.address = address + defaultTokens.push(contract) + } +} + +module.exports = TokenList + +inherits(TokenList, Component) +function TokenList () { + this.state = { + tokens: [], + isLoading: true, + network: null, + } + Component.call(this) +} + +TokenList.prototype.render = function () { + const state = this.state + const { tokens, isLoading, error } = state + const { userAddress, network } = this.props + + if (isLoading) { + return this.message('Loading') + } + + if (error) { + log.error(error) + return this.message('There was a problem loading your token balances.') + } + + const tokenViews = tokens.map((tokenData) => { + tokenData.network = network + tokenData.userAddress = userAddress + return h(TokenCell, tokenData) + }) + + return h('div', [ + h('ol', { + style: { + height: '260px', + overflowY: 'auto', + display: 'flex', + flexDirection: 'column', + }, + }, [ + h('style', ` + + li.token-cell { + display: flex; + flex-direction: row; + align-items: center; + padding: 10px; + } + + li.token-cell > h3 { + margin-left: 12px; + } + + li.token-cell:hover { + background: white; + cursor: pointer; + } + + `), + ...tokenViews, + tokenViews.length ? null : this.message('No Tokens Found.'), + ]), + this.addTokenButtonElement(), + ]) +} + +TokenList.prototype.addTokenButtonElement = function () { + return h('div', [ + h('div.footer.hover-white.pointer', { + key: 'reveal-account-bar', + onClick: () => { + this.props.addToken() + }, + style: { + display: 'flex', + height: '40px', + padding: '10px', + justifyContent: 'center', + alignItems: 'center', + }, + }, [ + h('i.fa.fa-plus.fa-lg'), + ]), + ]) +} + +TokenList.prototype.message = function (body) { + return h('div', { + style: { + display: 'flex', + height: '250px', + alignItems: 'center', + justifyContent: 'center', + padding: '30px', + }, + }, body) +} + +TokenList.prototype.componentDidMount = function () { + this.createFreshTokenTracker() +} + +TokenList.prototype.createFreshTokenTracker = function () { + if (this.tracker) { + // Clean up old trackers when refreshing: + this.tracker.stop() + this.tracker.removeListener('update', this.balanceUpdater) + this.tracker.removeListener('error', this.showError) + } + + if (!global.ethereumProvider) return + const { userAddress } = this.props + this.tracker = new TokenTracker({ + userAddress, + provider: global.ethereumProvider, + tokens: uniqueMergeTokens(defaultTokens, this.props.tokens), + pollingInterval: 8000, + }) + + + // Set up listener instances for cleaning up + this.balanceUpdater = this.updateBalances.bind(this) + this.showError = (error) => { + this.setState({ error, isLoading: false }) + } + this.tracker.on('update', this.balanceUpdater) + this.tracker.on('error', this.showError) + + this.tracker.updateBalances() + .then(() => { + this.updateBalances(this.tracker.serialize()) + }) + .catch((reason) => { + log.error(`Problem updating balances`, reason) + this.setState({ isLoading: false }) + }) +} + +TokenList.prototype.componentWillUpdate = function (nextProps) { + if (nextProps.network === 'loading') return + const oldNet = this.props.network + const newNet = nextProps.network + + if (oldNet && newNet && newNet !== oldNet) { + this.setState({ isLoading: true }) + this.createFreshTokenTracker() + } +} + +TokenList.prototype.updateBalances = function (tokens) { + const heldTokens = tokens.filter(token => { + return token.balance !== '0' && token.string !== '0.000' + }) + this.setState({ tokens: heldTokens, isLoading: false }) +} + +TokenList.prototype.componentWillUnmount = function () { + if (!this.tracker) return + this.tracker.stop() +} + +function uniqueMergeTokens (tokensA, tokensB) { + const uniqueAddresses = [] + const result = [] + tokensA.concat(tokensB).forEach((token) => { + const normal = normalizeAddress(token.address) + if (!uniqueAddresses.includes(normal)) { + uniqueAddresses.push(normal) + result.push(token) + } + }) + return result +} + diff --git a/ui/app/components/tooltip.js b/ui/app/components/tooltip.js new file mode 100644 index 000000000..edbc074bb --- /dev/null +++ b/ui/app/components/tooltip.js @@ -0,0 +1,22 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ReactTooltip = require('react-tooltip-component') + +module.exports = Tooltip + +inherits(Tooltip, Component) +function Tooltip () { + Component.call(this) +} + +Tooltip.prototype.render = function () { + const props = this.props + const { position, title, children } = props + + return h(ReactTooltip, { + position: position || 'left', + title, + fixed: false, + }, children) +} diff --git a/ui/app/components/transaction-list-item-icon.js b/ui/app/components/transaction-list-item-icon.js new file mode 100644 index 000000000..431054340 --- /dev/null +++ b/ui/app/components/transaction-list-item-icon.js @@ -0,0 +1,68 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Tooltip = require('./tooltip') + +const Identicon = require('./identicon') + +module.exports = TransactionIcon + +inherits(TransactionIcon, Component) +function TransactionIcon () { + Component.call(this) +} + +TransactionIcon.prototype.render = function () { + const { transaction, txParams, isMsg } = this.props + switch (transaction.status) { + case 'unapproved': + return h(!isMsg ? '.unapproved-tx-icon' : 'i.fa.fa-certificate.fa-lg') + + case 'rejected': + return h('i.fa.fa-exclamation-triangle.fa-lg.warning', { + style: { + width: '24px', + }, + }) + + case 'failed': + return h('i.fa.fa-exclamation-triangle.fa-lg.error', { + style: { + width: '24px', + }, + }) + + case 'submitted': + return h(Tooltip, { + title: 'Pending', + position: 'bottom', + }, [ + h('i.fa.fa-ellipsis-h', { + style: { + fontSize: '27px', + }, + }), + ]) + } + + if (isMsg) { + return h('i.fa.fa-certificate.fa-lg', { + style: { + width: '24px', + }, + }) + } + + if (txParams.to) { + return h(Identicon, { + diameter: 24, + address: txParams.to || transaction.hash, + }) + } else { + return h('i.fa.fa-file-text-o.fa-lg', { + style: { + width: '24px', + }, + }) + } +} diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js new file mode 100644 index 000000000..dbda66a31 --- /dev/null +++ b/ui/app/components/transaction-list-item.js @@ -0,0 +1,165 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const EthBalance = require('./eth-balance') +const addressSummary = require('../util').addressSummary +const explorerLink = require('../../lib/explorer-link') +const CopyButton = require('./copyButton') +const vreme = new (require('vreme')) +const Tooltip = require('./tooltip') +const numberToBN = require('number-to-bn') + +const TransactionIcon = require('./transaction-list-item-icon') +const ShiftListItem = require('./shift-list-item') +module.exports = TransactionListItem + +inherits(TransactionListItem, Component) +function TransactionListItem () { + Component.call(this) +} + +TransactionListItem.prototype.render = function () { + const { transaction, network, conversionRate, currentCurrency } = this.props + if (transaction.key === 'shapeshift') { + if (network === '1') return h(ShiftListItem, transaction) + } + var date = formatDate(transaction.time) + + let isLinkable = false + const numericNet = parseInt(network) + isLinkable = numericNet === 1 || numericNet === 3 || numericNet === 4 || numericNet === 42 + + var isMsg = ('msgParams' in transaction) + var isTx = ('txParams' in transaction) + var isPending = transaction.status === 'unapproved' + let txParams + if (isTx) { + txParams = transaction.txParams + } else if (isMsg) { + txParams = transaction.msgParams + } + + const nonce = txParams.nonce ? numberToBN(txParams.nonce).toString(10) : '' + + const isClickable = ('hash' in transaction && isLinkable) || isPending + return ( + h(`.transaction-list-item.flex-row.flex-space-between${isClickable ? '.pointer' : ''}`, { + onClick: (event) => { + if (isPending) { + this.props.showTx(transaction.id) + } + event.stopPropagation() + if (!transaction.hash || !isLinkable) return + var url = explorerLink(transaction.hash, parseInt(network)) + global.platform.openWindow({ url }) + }, + style: { + padding: '20px 0', + }, + }, [ + + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + h('.pop-hover', { + onClick: (event) => { + event.stopPropagation() + if (!isTx || isPending) return + var url = `https://metamask.github.io/eth-tx-viz/?tx=${transaction.hash}` + global.platform.openWindow({ url }) + }, + }, [ + h(TransactionIcon, { txParams, transaction, isTx, isMsg }), + ]), + ]), + + h(Tooltip, { + title: 'Transaction Number', + position: 'bottom', + }, [ + h('span', { + style: { + display: 'flex', + cursor: 'normal', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + padding: '10px', + }, + }, nonce), + ]), + + h('.flex-column', {style: {width: '200px', overflow: 'hidden'}}, [ + domainField(txParams), + h('div', date), + recipientField(txParams, transaction, isTx, isMsg), + ]), + + // Places a copy button if tx is successful, else places a placeholder empty div. + transaction.hash ? h(CopyButton, { value: transaction.hash }) : h('div', {style: { display: 'flex', alignItems: 'center', width: '26px' }}), + + isTx ? h(EthBalance, { + value: txParams.value, + conversionRate, + currentCurrency, + width: '55px', + shorten: true, + showFiat: false, + style: {fontSize: '15px'}, + }) : h('.flex-column'), + ]) + ) +} + +function domainField (txParams) { + return h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + overflow: 'hidden', + textOverflow: 'ellipsis', + width: '100%', + }, + }, [ + txParams.origin, + ]) +} + +function recipientField (txParams, transaction, isTx, isMsg) { + let message + + if (isMsg) { + message = 'Signature Requested' + } else if (txParams.to) { + message = addressSummary(txParams.to) + } else { + message = 'Contract Published' + } + + return h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + }, + }, [ + message, + failIfFailed(transaction), + ]) +} + +function formatDate (date) { + return vreme.format(new Date(date), 'March 16 2014 14:30') +} + +function failIfFailed (transaction) { + if (transaction.status === 'rejected') { + return h('span.error', ' (Rejected)') + } + if (transaction.err) { + return h(Tooltip, { + title: transaction.err.message, + position: 'bottom', + }, [ + h('span.error', ' (Failed)'), + ]) + } +} diff --git a/ui/app/components/transaction-list.js b/ui/app/components/transaction-list.js new file mode 100644 index 000000000..3b4ba741e --- /dev/null +++ b/ui/app/components/transaction-list.js @@ -0,0 +1,79 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const TransactionListItem = require('./transaction-list-item') + +module.exports = TransactionList + + +inherits(TransactionList, Component) +function TransactionList () { + Component.call(this) +} + +TransactionList.prototype.render = function () { + const { transactions, network, unapprovedMsgs, conversionRate } = this.props + + var shapeShiftTxList + if (network === '1') { + shapeShiftTxList = this.props.shapeShiftTxList + } + const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList) + .sort((a, b) => b.time - a.time) + + return ( + + h('section.transaction-list', [ + + h('style', ` + .transaction-list .transaction-list-item:not(:last-of-type) { + border-bottom: 1px solid #D4D4D4; + } + .transaction-list .transaction-list-item .ether-balance-label { + display: block !important; + font-size: small; + } + `), + + h('.tx-list', { + style: { + overflowY: 'auto', + height: '300px', + padding: '0 20px', + textAlign: 'center', + }, + }, [ + + txsToRender.length + ? txsToRender.map((transaction, i) => { + let key + switch (transaction.key) { + case 'shapeshift': + const { depositAddress, time } = transaction + key = `shift-tx-${depositAddress}-${time}-${i}` + break + default: + key = `tx-${transaction.id}-${i}` + } + return h(TransactionListItem, { + transaction, i, network, key, + conversionRate, + showTx: (txId) => { + this.props.viewPendingTx(txId) + }, + }) + }) + : h('.flex-center', { + style: { + flexDirection: 'column', + height: '100%', + }, + }, [ + 'No transaction history.', + ]), + ]), + ]) + ) +} + diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js new file mode 100644 index 000000000..63b77ef7f --- /dev/null +++ b/ui/app/conf-tx.js @@ -0,0 +1,213 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') +const NetworkIndicator = require('./components/network') +const txHelper = require('../lib/tx-helper') +const isPopupOrNotification = require('../../../app/scripts/lib/is-popup-or-notification') + +const PendingTx = require('./components/pending-tx') +const PendingMsg = require('./components/pending-msg') +const PendingPersonalMsg = require('./components/pending-personal-msg') +const Loading = require('./components/loading') + +module.exports = connect(mapStateToProps)(ConfirmTxScreen) + +function mapStateToProps (state) { + return { + identities: state.metamask.identities, + accounts: state.metamask.accounts, + selectedAddress: state.metamask.selectedAddress, + unapprovedTxs: state.metamask.unapprovedTxs, + unapprovedMsgs: state.metamask.unapprovedMsgs, + unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs, + index: state.appState.currentView.context, + warning: state.appState.warning, + network: state.metamask.network, + provider: state.metamask.provider, + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + blockGasLimit: state.metamask.currentBlockGasLimit, + } +} + +inherits(ConfirmTxScreen, Component) +function ConfirmTxScreen () { + Component.call(this) +} + +ConfirmTxScreen.prototype.render = function () { + const props = this.props + const { network, provider, unapprovedTxs, currentCurrency, + unapprovedMsgs, unapprovedPersonalMsgs, conversionRate, blockGasLimit } = props + + var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) + + var txData = unconfTxList[props.index] || {} + var txParams = txData.params || {} + var isNotification = isPopupOrNotification() === 'notification' + + + log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) + if (unconfTxList.length === 0) return h(Loading, { isLoading: true }) + + return ( + + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + !isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: this.goHome.bind(this), + }) : null, + h('h2.page-subtitle', 'Confirm Transaction'), + isNotification ? h(NetworkIndicator, { + network: network, + provider: provider, + }) : null, + ]), + + h('h3', { + style: { + alignSelf: 'center', + display: unconfTxList.length > 1 ? 'block' : 'none', + }, + }, [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + style: { + display: props.index === 0 ? 'none' : 'inline-block', + }, + onClick: () => props.dispatch(actions.previousTx()), + }), + ` ${props.index + 1} of ${unconfTxList.length} `, + h('i.fa.fa-arrow-right.fa-lg.cursor-pointer', { + style: { + display: props.index + 1 === unconfTxList.length ? 'none' : 'inline-block', + }, + onClick: () => props.dispatch(actions.nextTx()), + }), + ]), + + warningIfExists(props.warning), + + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + + currentTxView({ + // Properties + txData: txData, + key: txData.id, + selectedAddress: props.selectedAddress, + accounts: props.accounts, + identities: props.identities, + conversionRate, + currentCurrency, + blockGasLimit, + // Actions + buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), + sendTransaction: this.sendTransaction.bind(this), + cancelTransaction: this.cancelTransaction.bind(this, txData), + signMessage: this.signMessage.bind(this, txData), + signPersonalMessage: this.signPersonalMessage.bind(this, txData), + cancelMessage: this.cancelMessage.bind(this, txData), + cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), + }), + + ]), + ]) + ) +} + +function currentTxView (opts) { + log.info('rendering current tx view') + const { txData } = opts + const { txParams, msgParams, type } = txData + + if (txParams) { + log.debug('txParams detected, rendering pending tx') + return h(PendingTx, opts) + } else if (msgParams) { + log.debug('msgParams detected, rendering pending msg') + + if (type === 'eth_sign') { + log.debug('rendering eth_sign message') + return h(PendingMsg, opts) + } else if (type === 'personal_sign') { + log.debug('rendering personal_sign message') + return h(PendingPersonalMsg, opts) + } + } +} + +ConfirmTxScreen.prototype.buyEth = function (address, event) { + event.preventDefault() + this.props.dispatch(actions.buyEthView(address)) +} + +ConfirmTxScreen.prototype.sendTransaction = function (txData, event) { + this.stopPropagation(event) + this.props.dispatch(actions.updateAndApproveTx(txData)) +} + +ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) { + this.stopPropagation(event) + event.preventDefault() + this.props.dispatch(actions.cancelTx(txData)) +} + +ConfirmTxScreen.prototype.signMessage = function (msgData, event) { + log.info('conf-tx.js: signing message') + var params = msgData.msgParams + params.metamaskId = msgData.id + this.stopPropagation(event) + this.props.dispatch(actions.signMsg(params)) +} + +ConfirmTxScreen.prototype.stopPropagation = function (event) { + if (event.stopPropagation) { + event.stopPropagation() + } +} + +ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) { + log.info('conf-tx.js: signing personal message') + var params = msgData.msgParams + params.metamaskId = msgData.id + this.stopPropagation(event) + this.props.dispatch(actions.signPersonalMsg(params)) +} + +ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) { + log.info('canceling message') + this.stopPropagation(event) + this.props.dispatch(actions.cancelMsg(msgData)) +} + +ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) { + log.info('canceling personal message') + this.stopPropagation(event) + this.props.dispatch(actions.cancelPersonalMsg(msgData)) +} + +ConfirmTxScreen.prototype.goHome = function (event) { + this.stopPropagation(event) + this.props.dispatch(actions.goHome()) +} + +function warningIfExists (warning) { + if (warning && + // Do not display user rejections on this screen: + warning.indexOf('User denied transaction signature') === -1) { + return h('.error', { + style: { + margin: 'auto', + }, + }, warning) + } +} diff --git a/ui/app/config.js b/ui/app/config.js new file mode 100644 index 000000000..62785c49b --- /dev/null +++ b/ui/app/config.js @@ -0,0 +1,211 @@ +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 currencies = require('./conversion.json').rows +const validUrl = require('valid-url') +const copyToClipboard = require('copy-to-clipboard') + +module.exports = connect(mapStateToProps)(ConfigScreen) + +function mapStateToProps (state) { + return { + metamask: state.metamask, + warning: state.appState.warning, + } +} + +inherits(ConfigScreen, Component) +function ConfigScreen () { + Component.call(this) +} + +ConfigScreen.prototype.render = function () { + var state = this.props + var metamaskState = state.metamask + var warning = state.warning + + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + state.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Settings'), + ]), + + h('.error', { + style: { + display: warning ? 'block' : 'none', + padding: '0 20px', + textAlign: 'center', + }, + }, warning), + + // conf view + h('.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-space-around', { + style: { + padding: '20px', + }, + }, [ + + currentProviderDisplay(metamaskState), + + h('div', { style: {display: 'flex'} }, [ + h('input#new_rpc', { + placeholder: 'New RPC URL', + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onKeyPress (event) { + if (event.key === 'Enter') { + var element = event.target + var newRpc = element.value + rpcValidation(newRpc, state) + } + }, + }), + h('button', { + style: { + alignSelf: 'center', + }, + onClick (event) { + event.preventDefault() + var element = document.querySelector('input#new_rpc') + var newRpc = element.value + rpcValidation(newRpc, state) + }, + }, 'Save'), + ]), + + h('hr.horizontal-line'), + + currentConversionInformation(metamaskState, state), + + h('hr.horizontal-line'), + + h('div', { + style: { + marginTop: '20px', + }, + }, [ + h('p', { + style: { + fontFamily: 'Montserrat Light', + fontSize: '13px', + }, + }, `State logs contain your public account addresses and sent transactions.`), + h('br'), + h('button', { + style: { + alignSelf: 'center', + }, + onClick (event) { + copyToClipboard(window.logState()) + }, + }, 'Copy State Logs'), + ]), + + h('hr.horizontal-line'), + + h('div', { + style: { + marginTop: '20px', + }, + }, [ + h('button', { + style: { + alignSelf: 'center', + }, + onClick (event) { + event.preventDefault() + state.dispatch(actions.revealSeedConfirmation()) + }, + }, 'Reveal Seed Words'), + ]), + + ]), + ]), + ]) + ) +} + +function rpcValidation (newRpc, state) { + if (validUrl.isWebUri(newRpc)) { + state.dispatch(actions.setRpcTarget(newRpc)) + } else { + var appendedRpc = `http://${newRpc}` + if (validUrl.isWebUri(appendedRpc)) { + state.dispatch(actions.displayWarning('URIs require the appropriate HTTP/HTTPS prefix.')) + } else { + state.dispatch(actions.displayWarning('Invalid RPC URI')) + } + } +} + +function currentConversionInformation (metamaskState, state) { + var currentCurrency = metamaskState.currentCurrency + var conversionDate = metamaskState.conversionDate + return h('div', [ + h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, 'Current Conversion'), + h('span', {style: { fontWeight: 'bold', paddingRight: '10px', fontSize: '13px'}}, `Updated ${Date(conversionDate)}`), + h('select#currentCurrency', { + onChange (event) { + event.preventDefault() + var element = document.getElementById('currentCurrency') + var newCurrency = element.value + state.dispatch(actions.setCurrentCurrency(newCurrency)) + }, + defaultValue: currentCurrency, + }, currencies.map((currency) => { + return h('option', {key: currency.code, value: currency.code}, `${currency.code} - ${currency.name}`) + }) + ), + ]) +} + +function currentProviderDisplay (metamaskState) { + var provider = metamaskState.provider + var title, value + + switch (provider.type) { + + case 'mainnet': + title = 'Current Network' + value = 'Main Ethereum Network' + break + + case 'ropsten': + title = 'Current Network' + value = 'Ropsten Test Network' + break + + case 'kovan': + title = 'Current Network' + value = 'Kovan Test Network' + break + + case 'rinkeby': + title = 'Current Network' + value = 'Rinkeby Test Network' + break + + default: + title = 'Current RPC' + value = metamaskState.provider.rpcTarget + } + + return h('div', [ + h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, title), + h('span', value), + ]) +} diff --git a/ui/app/conversion.json b/ui/app/conversion.json new file mode 100644 index 000000000..155ffc4fc --- /dev/null +++ b/ui/app/conversion.json @@ -0,0 +1,207 @@ +{ + "rows": [ + { + "code": "REP", + "name": "Augur", + "statuses": [ + "primary" + ] + }, + { + "code": "BCN", + "name": "Bytecoin", + "statuses": [ + "primary" + ] + }, + { + "code": "BTC", + "name": "Bitcoin", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "BTS", + "name": "BitShares", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "BLK", + "name": "Blackcoin", + "statuses": [ + "primary" + ] + }, + { + "code": "GBP", + "name": "British Pound Sterling", + "statuses": [ + "secondary" + ] + }, + { + "code": "CAD", + "name": "Canadian Dollar", + "statuses": [ + "secondary" + ] + }, + { + "code": "CNY", + "name": "Chinese Yuan", + "statuses": [ + "secondary" + ] + }, + { + "code": "DSH", + "name": "Dashcoin", + "statuses": [ + "primary" + ] + }, + { + "code": "DOGE", + "name": "Dogecoin", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "ETC", + "name": "Ethereum Classic", + "statuses": [ + "primary" + ] + }, + { + "code": "EUR", + "name": "Euro", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "GNO", + "name": "GNO", + "statuses": [ + "primary" + ] + }, + { + "code": "GNT", + "name": "GNT", + "statuses": [ + "primary" + ] + }, + { + "code": "JPY", + "name": "Japanese Yen", + "statuses": [ + "secondary" + ] + }, + { + "code": "LTC", + "name": "Litecoin", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "MAID", + "name": "MaidSafeCoin", + "statuses": [ + "primary" + ] + }, + { + "code": "XEM", + "name": "NEM", + "statuses": [ + "primary" + ] + }, + { + "code": "XLM", + "name": "Stellar", + "statuses": [ + "primary" + ] + }, + { + "code": "XMR", + "name": "Monero", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "XRP", + "name": "Ripple", + "statuses": [ + "primary" + ] + }, + { + "code": "RUR", + "name": "Ruble", + "statuses": [ + "secondary" + ] + }, + { + "code": "STEEM", + "name": "Steem", + "statuses": [ + "primary" + ] + }, + { + "code": "STRAT", + "name": "STRAT", + "statuses": [ + "primary" + ] + }, + { + "code": "UAH", + "name": "Ukrainian Hryvnia", + "statuses": [ + "secondary" + ] + }, + { + "code": "USD", + "name": "US Dollar", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "WAVES", + "name": "WAVES", + "statuses": [ + "primary" + ] + }, + { + "code": "ZEC", + "name": "Zcash", + "statuses": [ + "primary" + ] + } + ] +} diff --git a/ui/app/css/debug.css b/ui/app/css/debug.css new file mode 100644 index 000000000..3e125bcd4 --- /dev/null +++ b/ui/app/css/debug.css @@ -0,0 +1,21 @@ +/* +debug / dev +*/ + +#app-content { + border: 2px solid green; +} + +#design-container { + position: absolute; + left: 360px; + top: -42px; + width: calc(100vw - 360px); + height: 100vh; + overflow: scroll; +} + +#design-container img { + width: 2000px; + margin-right: 600px; +} \ No newline at end of file diff --git a/ui/app/css/fonts.css b/ui/app/css/fonts.css new file mode 100644 index 000000000..3b9f581b9 --- /dev/null +++ b/ui/app/css/fonts.css @@ -0,0 +1,36 @@ +@import url(https://fonts.googleapis.com/css?family=Roboto:300,500); +@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css); + +@font-face { + font-family: 'Montserrat Regular'; + src: url('/fonts/Montserrat/Montserrat-Regular.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; + font-size: 'small'; + +} + +@font-face { + font-family: 'Montserrat Bold'; + src: url('/fonts/Montserrat/Montserrat-Bold.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-Bold.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Montserrat Light'; + src: url('/fonts/Montserrat/Montserrat-Light.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-Light.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Montserrat UltraLight'; + src: url('/fonts/Montserrat/Montserrat-UltraLight.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-UltraLight.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} diff --git a/ui/app/css/index.css b/ui/app/css/index.css new file mode 100644 index 000000000..808aafb4c --- /dev/null +++ b/ui/app/css/index.css @@ -0,0 +1,667 @@ +/* +faint orange (textfield shades) #FAF6F0 +light orange (button shades): #F5C26D +dark orange (text): #F5A623 +borders/font/any gray: #4A4A4A +*/ + +/* +application specific styles +*/ + +* { + box-sizing: border-box; +} + +html, body { + font-family: 'Montserrat Regular', Arial; + color: #4D4D4D; + font-weight: 300; + line-height: 1.4em; + background: #F7F7F7; +} + +input:focus, textarea:focus { + outline: none; +} + +#app-content { + overflow-x: hidden; + min-width: 357px; + width: 360px; + height: 500px; +} + +button, input[type="submit"] { + font-family: 'Montserrat Bold'; + outline: none; + cursor: pointer; + padding: 8px 12px; + border: none; + color: white; + transform-origin: center center; + transition: transform 50ms ease-in; + /* default orange */ + background: rgba(247, 134, 28, 1); + box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); +} + +.btn-green, input[type="submit"].btn-green { + background: rgba(106, 195, 96, 1); + box-shadow: 0px 3px 6px rgba(106, 195, 96, 0.36); +} + +.btn-red { + background: rgba(254, 35, 17, 1); + box-shadow: 0px 3px 6px rgba(254, 35, 17, 0.36); +} + +button[disabled], input[type="submit"][disabled] { + cursor: not-allowed; + background: rgba(197, 197, 197, 1); + box-shadow: 0px 3px 6px rgba(197, 197, 197, 0.36); +} + +button.spaced { + margin: 2px; +} + +button:not([disabled]):hover, input[type="submit"]:not([disabled]):hover { + transform: scale(1.1); +} +button:not([disabled]):active, input[type="submit"]:not([disabled]):active { + transform: scale(0.95); +} + +a { + text-decoration: none; + color: inherit; +} + +a:hover{ + color: #df6b0e; +} + +/* +app +*/ + +.active { + color: #909090; +} + +button.primary { + padding: 8px 12px; + background: #F7861C; + box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); + color: white; + font-size: 1.1em; + font-family: 'Montserrat Regular'; + text-transform: uppercase; +} + +button.btn-thin { + border: 1px solid; + border-color: #4D4D4D; + color: #4D4D4D; + background: rgb(255, 174, 41); + border-radius: 4px; + min-width: 200px; + margin: 12px 0; + padding: 6px; + font-size: 13px; +} + +.app-header { + padding: 6px 8px; +} + +.app-header h1 { + font-family: 'Montserrat Regular'; + text-transform: uppercase; + color: #AEAEAE; +} + +h2.page-subtitle { + font-family: 'Montserrat Regular'; + text-transform: uppercase; + color: #AEAEAE; + font-size: 1em; + margin: 12px; +} + +.app-primary { + +} + +.app-footer { + padding-bottom: 10px; + align-items: center; +} + +.identicon { + height: 46px; + width: 46px; + background-size: cover; + border-radius: 100%; + border: 3px solid gray; +} + +textarea.twelve-word-phrase { + padding: 12px; + width: 300px; + height: 140px; + font-size: 16px; + background: white; + resize: none; +} + +.network-indicator { + display: flex; + align-items: center; + font-size: 0.6em; + +} + +.network-name { + width: 5.2em; + line-height: 9px; + text-rendering: geometricPrecision; +} + +.check { + margin-left: 7px; + color: #F7861C; + flex: 1 0 auto; + display: flex; + justify-content: flex-end; +} +/* +app sections +*/ + +/* initialize */ + +.initialize-screen hr { + width: 60px; + margin: 12px; + border-color: #F7861C; + border-style: solid; +} + +.initialize-screen label { + margin-top: 20px; +} + +.initialize-screen button.create-vault { + margin-top: 40px; +} + +.initialize-screen .warning { + font-size: 14px; + margin: 0 16px; +} + +/* unlock */ +.error { + color: #E20202; +} + +.warning { + color: #FFAE00; +} + +.lock { + width: 50px; + height: 50px; +} + +.lock.locked { + transform: scale(1.5); + opacity: 0.0; + transition: opacity 400ms ease-in, transform 400ms ease-in; +} +.lock.unlocked { + transform: scale(1); + opacity: 1; + transition: opacity 500ms ease-out, transform 500ms ease-out, background 200ms ease-in; +} + +.lock.locked .lock-top { + transform: scaleX(1) translateX(0); + transition: transform 250ms ease-in; +} +.lock.unlocked .lock-top { + transform: scaleX(-1) translateX(-12px); + transition: transform 250ms ease-in; +} +.lock.unlocked:hover { + border-radius: 4px; + background: #e5e5e5; + border: 1px solid #b1b1b1; +} +.lock.unlocked:active { + background: #c3c3c3; +} + +.section-title .fa-arrow-left { + margin: -2px 8px 0px -8px; +} + +.unlock-screen #metamask-mascot-container { + margin-top: 24px; +} + +.unlock-screen h1 { + margin-top: -28px; + margin-bottom: 42px; +} + +.unlock-screen input[type=password] { + width: 260px; + /*height: 36px; + margin-bottom: 24px; + padding: 8px;*/ +} + +.sizing-input{ + font-size: 14px; + height: 30px; + padding-left: 5px; +} +.editable-label{ + display: flex; +} +/* Webkit */ +.unlock-screen input::-webkit-input-placeholder { + text-align: center; + font-size: 1.2em; +} +/* Firefox 18- */ +.unlock-screen input:-moz-placeholder { + text-align: center; + font-size: 1.2em; +} +/* Firefox 19+ */ +.unlock-screen input::-moz-placeholder { + text-align: center; + font-size: 1.2em; +} +/* IE */ +.unlock-screen input:-ms-input-placeholder { + text-align: center; + font-size: 1.2em; +} + +input.large-input, textarea.large-input { + /*margin-bottom: 24px;*/ + padding: 8px; +} + +input.large-input { + height: 36px; +} + +.letter-spacey { + letter-spacing: 0.1em; +} + + + +/* accounts */ + +.accounts-section { + margin: 0 0px; +} + +.accounts-section .horizontal-line { + margin: 0px 18px; +} + +.accounts-list-option { + height: 120px; +} + +.accounts-list-option .identicon-wrapper { + width: 100px; +} + +.unconftx-link { + margin-top: 24px; + cursor: pointer; +} + +.unconftx-link .fa-arrow-right { + margin: 0px -8px 0px 8px; +} + +/* identity panel */ + +.identity-panel { + font-weight: 500; +} + +.identity-panel .identicon-wrapper { + margin: 4px; + margin-top: 8px; + display: flex; + align-items: center; +} + +.identity-panel .identicon-wrapper span { + margin: 0 auto; +} + +.identity-panel .identity-data { + margin: 8px 8px 8px 18px; +} + +.identity-panel i { + margin-top: 32px; + margin-right: 6px; + color: #B9B9B9; +} + +.identity-panel .arrow-right { + padding-left: 18px; + width: 42px; + min-width: 18px; + height: 100%; +} + +.identity-copy.flex-column { + flex: 0.25 0 auto; + justify-content: center; +} + +/* accounts screen */ + +.identity-section { + +} + +.identity-section .identity-panel { + background: #E9E9E9; + border-bottom: 1px solid #B1B1B1; + cursor: pointer; +} + +.identity-section .identity-panel.selected { + background: white; + color: #F3C83E; +} + +.identity-section .identity-panel.selected .identicon { + border-color: orange; +} + +.identity-section .accounts-list-option:hover, +.identity-section .accounts-list-option.selected { + background:white; +} + +/* account detail screen */ + +.account-detail-section { + +} +.name-label{ + +} + +.unapproved-tx-icon { + height: 16px; + width: 16px; + background: rgb(47, 174, 244); + border-color: #AEAEAE; + border-radius: 13px; +} + +.edit-text { + height: 100%; + visibility: hidden; +} +.editing-label { + display: flex; + justify-content: flex-start; + margin-left: 50px; + margin-bottom: 2px; + font-size: 11px; + text-rendering: geometricPrecision; + color: #F7861C; +} +.name-label:hover .edit-text { + visibility: visible; +} +/* tx confirm */ + +.unconftx-section input[type=password] { + height: 22px; + padding: 2px; + margin: 12px; + margin-bottom: 24px; + border-radius: 4px; + border: 2px solid #F3C83E; + background: #FAF6F0; +} + +/* Send Screen */ + +.send-screen { + +} + +.send-screen section { + margin: 8px 16px; +} + +.send-screen input { + width: 100%; + font-size: 12px; +} + +/* Ether Balance Widget */ + +.ether-balance-amount { + color: #F7861C; +} + +.ether-balance-label { + color: #ABA9AA; +} + +/* Info screen */ +.info-gray{ + font-family: 'Montserrat Regular'; + text-transform: uppercase; + color: #AEAEAE; +} + +.icon-size{ + width: 20px; +} + +.info{ + font-family: 'Montserrat Regular', Arial; + padding-bottom: 10px; + display: inline-block; + padding-left: 5px; +} + +/* buy eth warning screen */ +.custom-radios { + justify-content: space-around; + align-items: center; +} + + +.custom-radio-selected { + width: 17px; + height: 17px; + border: solid; + border-style: double; + border-radius: 15px; + border-width: 5px; + background: rgba(247, 134, 28, 1); + border-color: #F7F7F7; +} + +.custom-radio-inactive { + width: 14px; + height: 14px; + border: solid; + border-width: 1px; + border-radius: 24px; + border-color: #AEAEAE; +} + +.radio-titles { + color: rgba(247, 134, 28, 1); +} + +.radio-titles-subtext { + +} + +.selected-exchange { + +} + +.buy-radio { + +} + +.eth-warning{ + transition: opacity 400ms ease-in, transform 400ms ease-in; +} + +.buy-subview{ + transition: opacity 400ms ease-in, transform 400ms ease-in; +} + +.input-container:hover .edit-text{ + visibility: visible; +} + +.buy-inputs{ + font-family: 'Montserrat Light'; + font-size: 13px; + height: 20px; + background: transparent; + box-sizing: border-box; + border: solid; + border-color: transparent; + border-width: 0.5px; + border-radius: 2px; + +} +.input-container:hover .buy-inputs{ + box-sizing: inherit; + border: solid; + border-color: #F7861C; + border-width: 0.5px; + border-radius: 2px; +} + +.buy-inputs:focus{ + border: solid; + border-color: #F7861C; + border-width: 0.5px; + border-radius: 2px; +} + +.activeForm { + background: #F7F7F7; + border: none; + border-radius: 8px 8px 0px 0px; + width: 50%; + text-align: center; + padding-bottom: 4px; + +} + +.inactiveForm { + border: none; + border-radius: 8px 8px 0px 0px; + width: 50%; + text-align: center; + padding-bottom: 4px; +} + +.ex-coins { + font-family: 'Montserrat Regular'; + text-transform: uppercase; + text-align: center; + font-size: 33px; + width: 118px; + height: 42px; + padding: 1px; + color: #4D4D4D; +} + +.marketinfo{ + font-family: 'Montserrat light'; + color: #AEAEAE; + font-size: 15px; + line-height: 17px; +} + +#fromCoin::-webkit-calendar-picker-indicator { + display: none; +} + +#coinList { + width: 400px; + height: 500px; + overflow: scroll; +} + +.icon-control .fa-refresh{ + visibility: hidden; +} + +.icon-control:hover .fa-refresh{ + visibility: visible; +} + +.icon-control:hover .fa-chevron-right{ + visibility: hidden; +} + +.inactive { + color: #AEAEAE; +} + +.inactive button{ + background: #AEAEAE; + color: white; +} + +.ellip-address { + overflow: hidden; + text-overflow: ellipsis; + width: 5em; + font-size: 14px; + font-family: "Montserrat Light"; + margin-left: 5px; +} + +.qr-header { + font-size: 25px; + margin-top: 40px; +} + +.qr-message { + font-size: 12px; + color: #F7861C; +} + +div.message-container > div:first-child { + margin-top: 18px; + font-size: 15px; + color: #4D4D4D; +} + +.pop-hover:hover { + transform: scale(1.1); +} diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css new file mode 100644 index 000000000..910a24ee2 --- /dev/null +++ b/ui/app/css/lib.css @@ -0,0 +1,268 @@ +/* color */ + +.color-orange { + color: #F7861C; +} + +.color-forest { + color: #0A5448; +} + +/* lib */ + +.full-width { + width: 100%; +} + +.full-height { + height: 100%; +} + +.flex-column { + display: flex; + flex-direction: column; +} + +.space-between { + justify-content: space-between; +} + +.space-around { + justify-content: space-around; +} + +.flex-column-bottom { + display: flex; + flex-direction: column-reverse; +} + +.flex-row { + display: flex; + flex-direction: row; +} + +.flex-space-between { + justify-content: space-between; +} + +.flex-space-around { + justify-content: space-around; +} + +.flex-right { + display: flex; + flex-direction: row; + justify-content: flex-end; +} + +.flex-left { + display: flex; + flex-direction: row; + justify-content: flex-start; +} + +.flex-fixed { + flex: none; +} + +.flex-basis-auto { + flex-basis: auto; +} + +.flex-grow { + flex: 1 1 auto; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.flex-center { + display: flex; + justify-content: center; + align-items: center; +} + +.flex-justify-center { + justify-content: center; +} + +.flex-align-center { + align-items: center; +} + +.flex-self-end { + align-self: flex-end; +} + +.flex-self-stretch { + align-self: stretch; +} + +.flex-vertical { + flex-direction: column; +} + +.z-bump { + z-index: 1; +} + +.select-none { + cursor: inherit; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.pointer { + cursor: pointer; +} +.cursor-pointer { + cursor: pointer; + transform-origin: center center; + transition: transform 50ms ease-in-out; +} +.cursor-pointer:hover { + transform: scale(1.1); +} +.cursor-pointer:active { + transform: scale(0.95); +} + +.cursor-disabled { + cursor: not-allowed; +} + +.margin-bottom-sml { + margin-bottom: 20px; +} + +.margin-bottom-med { + margin-bottom: 40px; +} + +.margin-right-left { + margin: 0 20px; +} + +.bold { + font-weight: bold; +} + +.text-transform-uppercase { + text-transform: uppercase; +} + +.font-small { + font-size: 12px; +} + +.font-medium { + font-size: 1.2em; +} + +hr.horizontal-line { + display: block; + height: 1px; + border: 0; + border-top: 1px solid #ccc; + margin: 1em 0; + padding: 0; +} + +.hover-white:hover { + background: white; +} + +.red-dot { + background: #E91550; + color: white; + border-radius: 10px; +} + +.diamond { + transform: rotate(45deg); + background: #038789; +} + +.hollow-diamond { + transform: rotate(45deg); + border: 3px solid #690496; +} + +.golden-square { + background: #EBB33F; +} + +.pending-dot { + background: red; + left: 14px; + top: 14px; + color: white; + border-radius: 10px; + height: 20px; + min-width: 20px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + z-index: 1; +} + +.keyring-label { + z-index: 1; + font-size: 11px; + background: rgba(255,0,0,0.8); + bottom: -47px; + color: white; + border-radius: 10px; + height: 20px; + min-width: 20px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; +} + +.ether-balance { + display: flex; + align-items: center; +} + +.menu-icon { + display: inline-block; + height: 9px; + min-width: 9px; + margin: 13px; +} +.ether-icon { + background: rgb(0, 163, 68); + border-radius: 20px; +} +.testnet-icon { + background: #2465E1; +} + +.drop-menu-item { + display: flex; + align-items: center; +} + +.invisible { + visibility: hidden; +} + +.one-line-concat { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.critical-error { + text-align: center; + margin-top: 20px; + color: red; +} diff --git a/ui/app/css/reset.css b/ui/app/css/reset.css new file mode 100644 index 000000000..9ce89e8bc --- /dev/null +++ b/ui/app/css/reset.css @@ -0,0 +1,48 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} \ No newline at end of file diff --git a/ui/app/css/transitions.css b/ui/app/css/transitions.css new file mode 100644 index 000000000..393a944f9 --- /dev/null +++ b/ui/app/css/transitions.css @@ -0,0 +1,42 @@ +/* universal */ +.app-primary .main-enter { + position: absolute; + width: 100%; +} + +/* center position */ +.app-primary.from-right .main-enter-active, +.app-primary.from-left .main-enter-active { + overflow-x: hidden; + transform: translateX(0px); + transition: transform 300ms ease-in; +} + +/* exited positions */ +.app-primary.from-left .main-leave-active { + transform: translateX(360px); + transition: transform 300ms ease-in; +} +.app-primary.from-right .main-leave-active { + transform: translateX(-360px); + transition: transform 300ms ease-in; +} + +/* loader transitions */ +.loader-enter, .loader-leave-active { + opacity: 0.0; + transition: opacity 150 ease-in; +} +.loader-enter-active, .loader-leave { + opacity: 1.0; + transition: opacity 150 ease-in; +} + +/* entering positions */ +.app-primary.from-right .main-enter:not(.main-enter-active) { + transform: translateX(360px); +} +.app-primary.from-left .main-enter:not(.main-enter-active) { + transform: translateX(-360px); +} + diff --git a/ui/app/first-time/init-menu.js b/ui/app/first-time/init-menu.js new file mode 100644 index 000000000..cc7c51bd3 --- /dev/null +++ b/ui/app/first-time/init-menu.js @@ -0,0 +1,179 @@ +const inherits = require('util').inherits +const EventEmitter = require('events').EventEmitter +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const Mascot = require('../components/mascot') +const actions = require('../actions') +const Tooltip = require('../components/tooltip') +const getCaretCoordinates = require('textarea-caret') + +module.exports = connect(mapStateToProps)(InitializeMenuScreen) + +inherits(InitializeMenuScreen, Component) +function InitializeMenuScreen () { + Component.call(this) + this.animationEventEmitter = new EventEmitter() +} + +function mapStateToProps (state) { + return { + // state from plugin + currentView: state.appState.currentView, + warning: state.appState.warning, + } +} + +InitializeMenuScreen.prototype.render = function () { + var state = this.props + + switch (state.currentView.name) { + + default: + return this.renderMenu(state) + + } +} + +// InitializeMenuScreen.prototype.componentDidMount = function(){ +// document.getElementById('password-box').focus() +// } + +InitializeMenuScreen.prototype.renderMenu = function (state) { + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + h(Mascot, { + animationEventEmitter: this.animationEventEmitter, + }), + + h('h1', { + style: { + fontSize: '1.3em', + textTransform: 'uppercase', + color: '#7F8082', + marginBottom: 10, + }, + }, 'MetaMask'), + + + h('div', [ + h('h3', { + style: { + fontSize: '0.8em', + color: '#7F8082', + display: 'inline', + }, + }, 'Encrypt your new DEN'), + + h(Tooltip, { + title: 'Your DEN is your password-encrypted storage within MetaMask.', + }, [ + h('i.fa.fa-question-circle.pointer', { + style: { + fontSize: '18px', + position: 'relative', + color: 'rgb(247, 134, 28)', + top: '2px', + marginLeft: '4px', + }, + }), + ]), + ]), + + h('span.in-progress-notification', state.warning), + + // password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'New Password (min 8 chars)', + onInput: this.inputChanged.bind(this), + style: { + width: 260, + marginTop: 12, + }, + }), + + // confirm password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box-confirm', + placeholder: 'Confirm Password', + onKeyPress: this.createVaultOnEnter.bind(this), + onInput: this.inputChanged.bind(this), + style: { + width: 260, + marginTop: 16, + }, + }), + + + h('button.primary', { + onClick: this.createNewVaultAndKeychain.bind(this), + style: { + margin: 12, + }, + }, 'Create'), + + h('.flex-row.flex-center.flex-grow', [ + h('p.pointer', { + onClick: this.showRestoreVault.bind(this), + style: { + fontSize: '0.8em', + color: 'rgb(247, 134, 28)', + textDecoration: 'underline', + }, + }, 'Import Existing DEN'), + ]), + + ]) + ) +} + +InitializeMenuScreen.prototype.createVaultOnEnter = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.createNewVaultAndKeychain() + } +} + +InitializeMenuScreen.prototype.componentDidMount = function () { + document.getElementById('password-box').focus() +} + +InitializeMenuScreen.prototype.showRestoreVault = function () { + this.props.dispatch(actions.showRestoreVault()) +} + +InitializeMenuScreen.prototype.createNewVaultAndKeychain = function () { + var passwordBox = document.getElementById('password-box') + var password = passwordBox.value + var passwordConfirmBox = document.getElementById('password-box-confirm') + var passwordConfirm = passwordConfirmBox.value + + if (password.length < 8) { + this.warning = 'password not long enough' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + if (password !== passwordConfirm) { + this.warning = 'passwords don\'t match' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + + this.props.dispatch(actions.createNewVaultAndKeychain(password)) +} + +InitializeMenuScreen.prototype.inputChanged = function (event) { + // tell mascot to look at page action + var element = event.target + var boundingRect = element.getBoundingClientRect() + var coordinates = getCaretCoordinates(element, element.selectionEnd) + this.animationEventEmitter.emit('point', { + x: boundingRect.left + coordinates.left - element.scrollLeft, + y: boundingRect.top + coordinates.top - element.scrollTop, + }) +} diff --git a/ui/app/img/identicon-tardigrade.png b/ui/app/img/identicon-tardigrade.png new file mode 100644 index 000000000..1742a32b8 Binary files /dev/null and b/ui/app/img/identicon-tardigrade.png differ diff --git a/ui/app/img/identicon-walrus.png b/ui/app/img/identicon-walrus.png new file mode 100644 index 000000000..d58fae912 Binary files /dev/null and b/ui/app/img/identicon-walrus.png differ diff --git a/ui/app/info.js b/ui/app/info.js new file mode 100644 index 000000000..e8470de97 --- /dev/null +++ b/ui/app/info.js @@ -0,0 +1,154 @@ +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)(InfoScreen) + +function mapStateToProps (state) { + return {} +} + +inherits(InfoScreen, Component) +function InfoScreen () { + Component.call(this) +} + +InfoScreen.prototype.render = function () { + const state = this.props + const version = global.platform.getVersion() + + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + state.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Info'), + ]), + + // main view + h('.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-space-around', { + style: { + padding: '20px', + }, + }, [ + // current version number + + h('.info.info-gray', [ + h('div', 'Metamask'), + h('div', { + style: { + marginBottom: '10px', + }, + }, `Version: ${version}`), + ]), + + h('div', { + style: { + marginBottom: '5px', + }}, + [ + h('div', [ + h('a', { + href: 'https://metamask.io/privacy.html', + target: '_blank', + onClick (event) { this.navigateTo(event.target.href) }, + }, [ + h('div.info', 'Privacy Policy'), + ]), + ]), + h('div', [ + h('a', { + href: 'https://metamask.io/terms.html', + target: '_blank', + onClick (event) { this.navigateTo(event.target.href) }, + }, [ + h('div.info', 'Terms of Use'), + ]), + ]), + h('div', [ + h('a', { + href: 'https://metamask.io/attributions.html', + target: '_blank', + onClick (event) { this.navigateTo(event.target.href) }, + }, [ + h('div.info', 'Attributions'), + ]), + ]), + ] + ), + + h('hr', { + style: { + margin: '10px 0 ', + width: '7em', + }, + }), + + h('div', { + style: { + paddingLeft: '30px', + }}, + [ + h('div.fa.fa-github', [ + h('a.info', { + href: 'https://github.com/MetaMask/faq', + target: '_blank', + }, 'Need Help? Read our FAQ!'), + ]), + h('div', [ + h('a', { + href: 'https://metamask.io/', + target: '_blank', + }, [ + h('img.icon-size', { + src: 'images/icon-128.png', + style: { + // IE6-9 + filter: 'grayscale(100%)', + // Microsoft Edge and Firefox 35+ + WebkitFilter: 'grayscale(100%)', + }, + }), + h('div.info', 'Visit our web site'), + ]), + ]), + h('div.fa.fa-slack', [ + h('a.info', { + href: 'http://slack.metamask.io', + target: '_blank', + }, 'Join the conversation on Slack'), + ]), + + h('div.fa.fa-twitter', [ + h('a.info', { + href: 'https://twitter.com/metamask_io', + target: '_blank', + }, 'Follow us on Twitter'), + ]), + + h('div.fa.fa-envelope', [ + h('a.info', { + target: '_blank', + style: { width: '85vw' }, + href: 'mailto:help@metamask.io?subject=Feedback', + }, 'Email us!'), + ]), + ]), + ]), + ]), + ]) + ) +} + +InfoScreen.prototype.navigateTo = function (url) { + global.platform.openWindow({ url }) +} + diff --git a/ui/app/keychains/hd/create-vault-complete.js b/ui/app/keychains/hd/create-vault-complete.js new file mode 100644 index 000000000..a318a9b50 --- /dev/null +++ b/ui/app/keychains/hd/create-vault-complete.js @@ -0,0 +1,78 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('../../actions') + +module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen) + +inherits(CreateVaultCompleteScreen, Component) +function CreateVaultCompleteScreen () { + Component.call(this) +} + +function mapStateToProps (state) { + return { + seed: state.appState.currentView.seedWords, + cachedSeed: state.metamask.seedWords, + } +} + +CreateVaultCompleteScreen.prototype.render = function () { + var state = this.props + var seed = state.seed || state.cachedSeed || '' + + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + // // subtitle and nav + // h('.section-title.flex-row.flex-center', [ + // h('h2.page-subtitle', 'Vault Created'), + // ]), + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginTop: 36, + marginBottom: 8, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + 'Vault Created', + ]), + + h('div', { + style: { + width: '360px', + height: '78px', + fontSize: '1em', + marginTop: '10px', + textAlign: 'center', + }, + }, [ + h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'), + ]), + + h('textarea.twelve-word-phrase', { + readOnly: true, + value: seed, + }), + + h('button.primary', { + onClick: () => this.confirmSeedWords(), + style: { + margin: '24px', + fontSize: '0.9em', + }, + }, 'I\'ve copied it somewhere safe'), + ]) + ) +} + +CreateVaultCompleteScreen.prototype.confirmSeedWords = function () { + this.props.dispatch(actions.confirmSeedWords()) +} diff --git a/ui/app/keychains/hd/recover-seed/confirmation.js b/ui/app/keychains/hd/recover-seed/confirmation.js new file mode 100644 index 000000000..4ccbec9fc --- /dev/null +++ b/ui/app/keychains/hd/recover-seed/confirmation.js @@ -0,0 +1,118 @@ +const inherits = require('util').inherits + +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('../../../actions') + +module.exports = connect(mapStateToProps)(RevealSeedConfirmation) + +inherits(RevealSeedConfirmation, Component) +function RevealSeedConfirmation () { + Component.call(this) +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +RevealSeedConfirmation.prototype.render = function () { + const props = this.props + + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + 'Reveal Seed Words', + ]), + + h('.div', { + style: { + display: 'flex', + flexDirection: 'column', + padding: '20px', + justifyContent: 'center', + }, + }, [ + + h('h4', 'Do not recover your seed words in a public place! These words can be used to steal all your accounts.'), + + // confirmation + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'Enter your password to confirm', + onKeyPress: this.checkConfirmation.bind(this), + style: { + width: 260, + marginTop: '12px', + }, + }), + + h('.flex-row.flex-space-between', { + style: { + marginTop: 30, + width: '50%', + }, + }, [ + // cancel + h('button.primary', { + onClick: this.goHome.bind(this), + }, 'CANCEL'), + + // submit + h('button.primary', { + onClick: this.revealSeedWords.bind(this), + }, 'OK'), + + ]), + + (props.warning) && ( + h('span.error', { + style: { + margin: '20px', + }, + }, props.warning.split('-')) + ), + + props.inProgress && ( + h('span.in-progress-notification', 'Generating Seed...') + ), + ]), + ]) + ) +} + +RevealSeedConfirmation.prototype.componentDidMount = function () { + document.getElementById('password-box').focus() +} + +RevealSeedConfirmation.prototype.goHome = function () { + this.props.dispatch(actions.showConfigPage(false)) +} + +// create vault + +RevealSeedConfirmation.prototype.checkConfirmation = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.revealSeedWords() + } +} + +RevealSeedConfirmation.prototype.revealSeedWords = function () { + var password = document.getElementById('password-box').value + this.props.dispatch(actions.requestRevealSeed(password)) +} diff --git a/ui/app/keychains/hd/restore-vault.js b/ui/app/keychains/hd/restore-vault.js new file mode 100644 index 000000000..06e51d9b3 --- /dev/null +++ b/ui/app/keychains/hd/restore-vault.js @@ -0,0 +1,152 @@ +const inherits = require('util').inherits +const PersistentForm = require('../../../lib/persistent-form') +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('../../actions') + +module.exports = connect(mapStateToProps)(RestoreVaultScreen) + +inherits(RestoreVaultScreen, PersistentForm) +function RestoreVaultScreen () { + PersistentForm.call(this) +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + forgottenPassword: state.appState.forgottenPassword, + } +} + +RestoreVaultScreen.prototype.render = function () { + var state = this.props + this.persistentFormParentId = 'restore-vault-form' + + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + 'Restore Vault', + ]), + + // wallet seed entry + h('h3', 'Wallet Seed'), + h('textarea.twelve-word-phrase.letter-spacey', { + dataset: { + persistentFormId: 'wallet-seed', + }, + placeholder: 'Enter your secret twelve word phrase here to restore your vault.', + }), + + // password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'New Password (min 8 chars)', + dataset: { + persistentFormId: 'password', + }, + style: { + width: 260, + marginTop: 12, + }, + }), + + // confirm password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box-confirm', + placeholder: 'Confirm Password', + onKeyPress: this.createOnEnter.bind(this), + dataset: { + persistentFormId: 'password-confirmation', + }, + style: { + width: 260, + marginTop: 16, + }, + }), + + (state.warning) && ( + h('span.error.in-progress-notification', state.warning) + ), + + // submit + + h('.flex-row.flex-space-between', { + style: { + marginTop: 30, + width: '50%', + }, + }, [ + + // cancel + h('button.primary', { + onClick: this.showInitializeMenu.bind(this), + }, 'CANCEL'), + + // submit + h('button.primary', { + onClick: this.createNewVaultAndRestore.bind(this), + }, 'OK'), + + ]), + ]) + + ) +} + +RestoreVaultScreen.prototype.showInitializeMenu = function () { + if (this.props.forgottenPassword) { + this.props.dispatch(actions.backToUnlockView()) + } else { + this.props.dispatch(actions.showInitializeMenu()) + } +} + +RestoreVaultScreen.prototype.createOnEnter = function (event) { + if (event.key === 'Enter') { + this.createNewVaultAndRestore() + } +} + +RestoreVaultScreen.prototype.createNewVaultAndRestore = function () { + // check password + var passwordBox = document.getElementById('password-box') + var password = passwordBox.value + var passwordConfirmBox = document.getElementById('password-box-confirm') + var passwordConfirm = passwordConfirmBox.value + if (password.length < 8) { + this.warning = 'Password not long enough' + + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + if (password !== passwordConfirm) { + this.warning = 'Passwords don\'t match' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + // check seed + var seedBox = document.querySelector('textarea.twelve-word-phrase') + var seed = seedBox.value.trim() + if (seed.split(' ').length !== 12) { + this.warning = 'seed phrases are 12 words long' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + // submit + this.warning = null + this.props.dispatch(actions.displayWarning(this.warning)) + this.props.dispatch(actions.createNewVaultAndRestore(password, seed)) +} diff --git a/ui/app/new-keychain.js b/ui/app/new-keychain.js new file mode 100644 index 000000000..cc9633166 --- /dev/null +++ b/ui/app/new-keychain.js @@ -0,0 +1,29 @@ +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)(NewKeychain) + +function mapStateToProps (state) { + return {} +} + +inherits(NewKeychain, Component) +function NewKeychain () { + Component.call(this) +} + +NewKeychain.prototype.render = function () { + // const props = this.props + + return ( + h('div', { + style: { + background: 'blue', + }, + }, [ + h('h1', `Here's a list!!!!`), + ]) + ) +} diff --git a/ui/app/reducers.js b/ui/app/reducers.js new file mode 100644 index 000000000..11efca529 --- /dev/null +++ b/ui/app/reducers.js @@ -0,0 +1,52 @@ +const extend = require('xtend') + +// +// Sub-Reducers take in the complete state and return their sub-state +// +const reduceIdentities = require('./reducers/identities') +const reduceMetamask = require('./reducers/metamask') +const reduceApp = require('./reducers/app') + +window.METAMASK_CACHED_LOG_STATE = null + +module.exports = rootReducer + +function rootReducer (state, action) { + // clone + state = extend(state) + + if (action.type === 'GLOBAL_FORCE_UPDATE') { + return action.value + } + + // + // Identities + // + + state.identities = reduceIdentities(state, action) + + // + // MetaMask + // + + state.metamask = reduceMetamask(state, action) + + // + // AppState + // + + state.appState = reduceApp(state, action) + + window.METAMASK_CACHED_LOG_STATE = state + return state +} + +window.logState = function () { + var stateString = JSON.stringify(window.METAMASK_CACHED_LOG_STATE, removeSeedWords, 2) + console.log(stateString) + return stateString +} + +function removeSeedWords (key, value) { + return key === 'seedWords' ? undefined : value +} diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js new file mode 100644 index 000000000..2fcc9bfe0 --- /dev/null +++ b/ui/app/reducers/app.js @@ -0,0 +1,585 @@ +const extend = require('xtend') +const actions = require('../actions') +const txHelper = require('../../lib/tx-helper') + +module.exports = reduceApp + + +function reduceApp (state, action) { + log.debug('App Reducer got ' + action.type) + // clone and defaults + const selectedAddress = state.metamask.selectedAddress + const hasUnconfActions = checkUnconfActions(state) + let name = 'accounts' + if (selectedAddress) { + name = 'accountDetail' + } + if (hasUnconfActions) { + log.debug('pending txs detected, defaulting to conf-tx view.') + name = 'confTx' + } + + var defaultView = { + name, + detailView: null, + context: selectedAddress, + } + + // confirm seed words + var seedWords = state.metamask.seedWords + var seedConfView = { + name: 'createVaultComplete', + seedWords, + } + + // default state + var appState = extend({ + shouldClose: false, + menuOpen: false, + currentView: seedWords ? seedConfView : defaultView, + accountDetail: { + subview: 'transactions', + }, + transForward: true, // Used to render transition direction + isLoading: false, // Used to display loading indicator + warning: null, // Used to display error text + }, state.appState) + + switch (action.type) { + + // transition methods + + case actions.TRANSITION_FORWARD: + return extend(appState, { + transForward: true, + }) + + case actions.TRANSITION_BACKWARD: + return extend(appState, { + transForward: false, + }) + + // intialize + + case actions.SHOW_CREATE_VAULT: + return extend(appState, { + currentView: { + name: 'createVault', + }, + transForward: true, + warning: null, + }) + + case actions.SHOW_RESTORE_VAULT: + return extend(appState, { + currentView: { + name: 'restoreVault', + }, + transForward: true, + forgottenPassword: true, + }) + + case actions.FORGOT_PASSWORD: + return extend(appState, { + currentView: { + name: 'restoreVault', + }, + transForward: false, + forgottenPassword: true, + }) + + case actions.SHOW_INIT_MENU: + return extend(appState, { + currentView: defaultView, + transForward: false, + }) + + case actions.SHOW_CONFIG_PAGE: + return extend(appState, { + currentView: { + name: 'config', + context: appState.currentView.context, + }, + transForward: action.value, + }) + + case actions.SHOW_ADD_TOKEN_PAGE: + return extend(appState, { + currentView: { + name: 'add-token', + context: appState.currentView.context, + }, + transForward: action.value, + }) + + case actions.SHOW_IMPORT_PAGE: + + return extend(appState, { + currentView: { + name: 'import-menu', + }, + transForward: true, + }) + + case actions.SHOW_INFO_PAGE: + return extend(appState, { + currentView: { + name: 'info', + context: appState.currentView.context, + }, + transForward: true, + }) + + case actions.CREATE_NEW_VAULT_IN_PROGRESS: + return extend(appState, { + currentView: { + name: 'createVault', + inProgress: true, + }, + transForward: true, + isLoading: true, + }) + + case actions.SHOW_NEW_VAULT_SEED: + return extend(appState, { + currentView: { + name: 'createVaultComplete', + seedWords: action.value, + }, + transForward: true, + isLoading: false, + }) + + case actions.NEW_ACCOUNT_SCREEN: + return extend(appState, { + currentView: { + name: 'new-account', + context: appState.currentView.context, + }, + transForward: true, + }) + + case actions.SHOW_SEND_PAGE: + return extend(appState, { + currentView: { + name: 'sendTransaction', + context: appState.currentView.context, + }, + transForward: true, + warning: null, + }) + + case actions.SHOW_NEW_KEYCHAIN: + return extend(appState, { + currentView: { + name: 'newKeychain', + context: appState.currentView.context, + }, + transForward: true, + }) + + // unlock + + case actions.UNLOCK_METAMASK: + return extend(appState, { + forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, + detailView: {}, + transForward: true, + isLoading: false, + warning: null, + }) + + case actions.LOCK_METAMASK: + return extend(appState, { + currentView: defaultView, + transForward: false, + warning: null, + }) + + case actions.BACK_TO_INIT_MENU: + return extend(appState, { + warning: null, + transForward: false, + forgottenPassword: true, + currentView: { + name: 'InitMenu', + }, + }) + + case actions.BACK_TO_UNLOCK_VIEW: + return extend(appState, { + warning: null, + transForward: true, + forgottenPassword: false, + currentView: { + name: 'UnlockScreen', + }, + }) + // reveal seed words + + case actions.REVEAL_SEED_CONFIRMATION: + return extend(appState, { + currentView: { + name: 'reveal-seed-conf', + }, + transForward: true, + warning: null, + }) + + // accounts + + case actions.SET_SELECTED_ACCOUNT: + return extend(appState, { + activeAddress: action.value, + }) + + case actions.GO_HOME: + return extend(appState, { + currentView: extend(appState.currentView, { + name: 'accountDetail', + }), + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + transForward: false, + warning: null, + }) + + case actions.SHOW_ACCOUNT_DETAIL: + return extend(appState, { + forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, + currentView: { + name: 'accountDetail', + context: action.value, + }, + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + transForward: false, + }) + + case actions.BACK_TO_ACCOUNT_DETAIL: + return extend(appState, { + currentView: { + name: 'accountDetail', + context: action.value, + }, + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + transForward: false, + }) + + case actions.SHOW_ACCOUNTS_PAGE: + return extend(appState, { + currentView: { + name: seedWords ? 'createVaultComplete' : 'accounts', + seedWords, + }, + transForward: true, + isLoading: false, + warning: null, + scrollToBottom: false, + forgottenPassword: false, + }) + + case actions.SHOW_NOTICE: + return extend(appState, { + transForward: true, + isLoading: false, + }) + + case actions.REVEAL_ACCOUNT: + return extend(appState, { + scrollToBottom: true, + }) + + case actions.SHOW_CONF_TX_PAGE: + return extend(appState, { + currentView: { + name: 'confTx', + context: 0, + }, + transForward: action.transForward, + warning: null, + isLoading: false, + }) + + case actions.SHOW_CONF_MSG_PAGE: + return extend(appState, { + currentView: { + name: hasUnconfActions ? 'confTx' : 'account-detail', + context: 0, + }, + transForward: true, + warning: null, + isLoading: false, + }) + + case actions.COMPLETED_TX: + log.debug('reducing COMPLETED_TX for tx ' + action.value) + const otherUnconfActions = getUnconfActionList(state) + .filter(tx => tx.id !== action.value) + const hasOtherUnconfActions = otherUnconfActions.length > 0 + + if (hasOtherUnconfActions) { + log.debug('reducer detected txs - rendering confTx view') + return extend(appState, { + transForward: false, + currentView: { + name: 'confTx', + context: 0, + }, + warning: null, + }) + } else { + log.debug('attempting to close popup') + return extend(appState, { + // indicate notification should close + shouldClose: true, + transForward: false, + warning: null, + currentView: { + name: 'accountDetail', + context: state.metamask.selectedAddress, + }, + accountDetail: { + subview: 'transactions', + }, + }) + } + + case actions.NEXT_TX: + return extend(appState, { + transForward: true, + currentView: { + name: 'confTx', + context: ++appState.currentView.context, + warning: null, + }, + }) + + case actions.VIEW_PENDING_TX: + const context = indexForPending(state, action.value) + return extend(appState, { + transForward: true, + currentView: { + name: 'confTx', + context, + warning: null, + }, + }) + + case actions.PREVIOUS_TX: + return extend(appState, { + transForward: false, + currentView: { + name: 'confTx', + context: --appState.currentView.context, + warning: null, + }, + }) + + case actions.TRANSACTION_ERROR: + return extend(appState, { + currentView: { + name: 'confTx', + errorMessage: 'There was a problem submitting this transaction.', + }, + }) + + case actions.UNLOCK_FAILED: + return extend(appState, { + warning: action.value || 'Incorrect password. Try again.', + }) + + case actions.SHOW_LOADING: + return extend(appState, { + isLoading: true, + loadingMessage: action.value, + }) + + case actions.HIDE_LOADING: + return extend(appState, { + isLoading: false, + }) + + case actions.SHOW_SUB_LOADING_INDICATION: + return extend(appState, { + isSubLoading: true, + }) + + case actions.HIDE_SUB_LOADING_INDICATION: + return extend(appState, { + isSubLoading: false, + }) + case actions.CLEAR_SEED_WORD_CACHE: + return extend(appState, { + transForward: true, + currentView: {}, + isLoading: false, + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + }) + + case actions.DISPLAY_WARNING: + return extend(appState, { + warning: action.value, + isLoading: false, + }) + + case actions.HIDE_WARNING: + return extend(appState, { + warning: undefined, + }) + + case actions.REQUEST_ACCOUNT_EXPORT: + return extend(appState, { + transForward: true, + currentView: { + name: 'accountDetail', + context: appState.currentView.context, + }, + accountDetail: { + subview: 'export', + accountExport: 'requested', + }, + }) + + case actions.EXPORT_ACCOUNT: + return extend(appState, { + accountDetail: { + subview: 'export', + accountExport: 'completed', + }, + }) + + case actions.SHOW_PRIVATE_KEY: + return extend(appState, { + accountDetail: { + subview: 'export', + accountExport: 'completed', + privateKey: action.value, + }, + }) + + case actions.BUY_ETH_VIEW: + return extend(appState, { + transForward: true, + currentView: { + name: 'buyEth', + context: appState.currentView.name, + }, + identity: state.metamask.identities[action.value], + buyView: { + subview: 'Coinbase', + amount: '15.00', + buyAddress: action.value, + formView: { + coinbase: true, + shapeshift: false, + }, + }, + }) + + case actions.COINBASE_SUBVIEW: + return extend(appState, { + buyView: { + subview: 'Coinbase', + formView: { + coinbase: true, + shapeshift: false, + }, + buyAddress: appState.buyView.buyAddress, + amount: appState.buyView.amount, + }, + }) + + case actions.SHAPESHIFT_SUBVIEW: + return extend(appState, { + buyView: { + subview: 'ShapeShift', + formView: { + coinbase: false, + shapeshift: true, + marketinfo: action.value.marketinfo, + coinOptions: action.value.coinOptions, + }, + buyAddress: appState.buyView.buyAddress, + amount: appState.buyView.amount, + }, + }) + + case actions.PAIR_UPDATE: + return extend(appState, { + buyView: { + subview: 'ShapeShift', + formView: { + coinbase: false, + shapeshift: true, + marketinfo: action.value.marketinfo, + coinOptions: appState.buyView.formView.coinOptions, + }, + buyAddress: appState.buyView.buyAddress, + amount: appState.buyView.amount, + warning: null, + }, + }) + + case actions.SHOW_QR: + return extend(appState, { + qrRequested: true, + transForward: true, + + Qr: { + message: action.value.message, + data: action.value.data, + }, + }) + + case actions.SHOW_QR_VIEW: + return extend(appState, { + currentView: { + name: 'qr', + context: appState.currentView.context, + }, + transForward: true, + Qr: { + message: action.value.message, + data: action.value.data, + }, + }) + default: + return appState + } +} + +function checkUnconfActions (state) { + const unconfActionList = getUnconfActionList(state) + const hasUnconfActions = unconfActionList.length > 0 + return hasUnconfActions +} + +function getUnconfActionList (state) { + const { unapprovedTxs, unapprovedMsgs, + unapprovedPersonalMsgs, network } = state.metamask + + const unconfActionList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) + return unconfActionList +} + +function indexForPending (state, txId) { + const unconfTxList = getUnconfActionList(state) + const match = unconfTxList.find((tx) => tx.id === txId) + const index = unconfTxList.indexOf(match) + return index +} diff --git a/ui/app/reducers/identities.js b/ui/app/reducers/identities.js new file mode 100644 index 000000000..341a404e7 --- /dev/null +++ b/ui/app/reducers/identities.js @@ -0,0 +1,15 @@ +const extend = require('xtend') + +module.exports = reduceIdentities + +function reduceIdentities (state, action) { + // clone + defaults + var idState = extend({ + + }, state.identities) + + switch (action.type) { + default: + return idState + } +} diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js new file mode 100644 index 000000000..e0c416c2d --- /dev/null +++ b/ui/app/reducers/metamask.js @@ -0,0 +1,137 @@ +const extend = require('xtend') +const actions = require('../actions') + +module.exports = reduceMetamask + +function reduceMetamask (state, action) { + let newState + + // clone + defaults + var metamaskState = extend({ + isInitialized: false, + isUnlocked: false, + rpcTarget: 'https://rawtestrpc.metamask.io/', + identities: {}, + unapprovedTxs: {}, + noActiveNotices: true, + lastUnreadNotice: undefined, + frequentRpcList: [], + addressBook: [], + }, state.metamask) + + switch (action.type) { + + case actions.SHOW_ACCOUNTS_PAGE: + newState = extend(metamaskState) + delete newState.seedWords + return newState + + case actions.SHOW_NOTICE: + return extend(metamaskState, { + noActiveNotices: false, + lastUnreadNotice: action.value, + }) + + case actions.CLEAR_NOTICES: + return extend(metamaskState, { + noActiveNotices: true, + }) + + case actions.UPDATE_METAMASK_STATE: + return extend(metamaskState, action.value) + + case actions.UNLOCK_METAMASK: + return extend(metamaskState, { + isUnlocked: true, + isInitialized: true, + selectedAddress: action.value, + }) + + case actions.LOCK_METAMASK: + return extend(metamaskState, { + isUnlocked: false, + }) + + case actions.SET_RPC_LIST: + return extend(metamaskState, { + frequentRpcList: action.value, + }) + + case actions.SET_RPC_TARGET: + return extend(metamaskState, { + provider: { + type: 'rpc', + rpcTarget: action.value, + }, + }) + + case actions.SET_PROVIDER_TYPE: + return extend(metamaskState, { + provider: { + type: action.value, + }, + }) + + case actions.COMPLETED_TX: + var stringId = String(action.id) + newState = extend(metamaskState, { + unapprovedTxs: {}, + unapprovedMsgs: {}, + }) + for (const id in metamaskState.unapprovedTxs) { + if (id !== stringId) { + newState.unapprovedTxs[id] = metamaskState.unapprovedTxs[id] + } + } + for (const id in metamaskState.unapprovedMsgs) { + if (id !== stringId) { + newState.unapprovedMsgs[id] = metamaskState.unapprovedMsgs[id] + } + } + return newState + + case actions.SHOW_NEW_VAULT_SEED: + return extend(metamaskState, { + isUnlocked: true, + isInitialized: false, + seedWords: action.value, + }) + + case actions.CLEAR_SEED_WORD_CACHE: + newState = extend(metamaskState, { + isUnlocked: true, + isInitialized: true, + selectedAddress: action.value, + }) + delete newState.seedWords + return newState + + case actions.SHOW_ACCOUNT_DETAIL: + newState = extend(metamaskState, { + isUnlocked: true, + isInitialized: true, + selectedAddress: action.value, + }) + delete newState.seedWords + return newState + + case actions.SAVE_ACCOUNT_LABEL: + const account = action.value.account + const name = action.value.label + var id = {} + id[account] = extend(metamaskState.identities[account], { name }) + var identities = extend(metamaskState.identities, id) + return extend(metamaskState, { identities }) + + case actions.SET_CURRENT_FIAT: + return extend(metamaskState, { + currentCurrency: action.value.currentCurrency, + conversionRate: action.value.conversionRate, + conversionDate: action.value.conversionDate, + }) + + default: + return metamaskState + + } +} diff --git a/ui/app/root.js b/ui/app/root.js new file mode 100644 index 000000000..9e7314b20 --- /dev/null +++ b/ui/app/root.js @@ -0,0 +1,22 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const Provider = require('react-redux').Provider +const h = require('react-hyperscript') +const App = require('./app') + +module.exports = Root + +inherits(Root, Component) +function Root () { Component.call(this) } + +Root.prototype.render = function () { + return ( + + h(Provider, { + store: this.props.store, + }, [ + h(App), + ]) + + ) +} diff --git a/ui/app/send.js b/ui/app/send.js new file mode 100644 index 000000000..a21a219eb --- /dev/null +++ b/ui/app/send.js @@ -0,0 +1,288 @@ +const inherits = require('util').inherits +const PersistentForm = require('../lib/persistent-form') +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const Identicon = require('./components/identicon') +const actions = require('./actions') +const util = require('./util') +const numericBalance = require('./util').numericBalance +const addressSummary = require('./util').addressSummary +const isHex = require('./util').isHex +const EthBalance = require('./components/eth-balance') +const EnsInput = require('./components/ens-input') +const ethUtil = require('ethereumjs-util') +module.exports = connect(mapStateToProps)(SendTransactionScreen) + +function mapStateToProps (state) { + var result = { + address: state.metamask.selectedAddress, + accounts: state.metamask.accounts, + identities: state.metamask.identities, + warning: state.appState.warning, + network: state.metamask.network, + addressBook: state.metamask.addressBook, + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + } + + result.error = result.warning && result.warning.split('.')[0] + + result.account = result.accounts[result.address] + result.identity = result.identities[result.address] + result.balance = result.account ? numericBalance(result.account.balance) : null + + return result +} + +inherits(SendTransactionScreen, PersistentForm) +function SendTransactionScreen () { + PersistentForm.call(this) +} + +SendTransactionScreen.prototype.render = function () { + this.persistentFormParentId = 'send-tx-form' + + const props = this.props + const { + address, + account, + identity, + network, + identities, + addressBook, + conversionRate, + currentCurrency, + } = props + + return ( + + h('.send-screen.flex-column.flex-grow', [ + + // + // Sender Profile + // + + h('.account-data-subsection.flex-row.flex-grow', { + style: { + margin: '0 20px', + }, + }, [ + + // header - identicon + nav + h('.flex-row.flex-space-between', { + style: { + marginTop: '15px', + }, + }, [ + // back button + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: this.back.bind(this), + }), + + // large identicon + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + h(Identicon, { + diameter: 62, + address: address, + }), + ]), + + // invisible place holder + h('i.fa.fa-users.fa-lg.invisible', { + style: { + marginTop: '28px', + }, + }), + + ]), + + // account label + + h('.flex-column', { + style: { + marginTop: '10px', + alignItems: 'flex-start', + }, + }, [ + h('h2.font-medium.color-forest.flex-center', { + style: { + paddingTop: '8px', + marginBottom: '8px', + }, + }, identity && identity.name), + + // address and getter actions + h('.flex-row.flex-center', { + style: { + marginBottom: '8px', + }, + }, [ + + h('div', { + style: { + lineHeight: '16px', + }, + }, addressSummary(address)), + + ]), + + // balance + h('.flex-row.flex-center', [ + + h(EthBalance, { + value: account && account.balance, + conversionRate, + currentCurrency, + }), + + ]), + ]), + ]), + + // + // Required Fields + // + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginTop: '15px', + marginBottom: '16px', + }, + }, [ + 'Send Transaction', + ]), + + // error message + props.error && h('span.error.flex-center', props.error), + + // 'to' field + h('section.flex-row.flex-center', [ + h(EnsInput, { + name: 'address', + placeholder: 'Recipient Address', + onChange: this.recipientDidChange.bind(this), + network, + identities, + addressBook, + }), + ]), + + // 'amount' and send button + h('section.flex-row.flex-center', [ + + h('input.large-input', { + name: 'amount', + placeholder: 'Amount', + type: 'number', + style: { + marginRight: '6px', + }, + dataset: { + persistentFormId: 'tx-amount', + }, + }), + + h('button.primary', { + onClick: this.onSubmit.bind(this), + style: { + textTransform: 'uppercase', + }, + }, 'Next'), + + ]), + + // + // Optional Fields + // + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginTop: '16px', + marginBottom: '16px', + }, + }, [ + 'Transaction Data (optional)', + ]), + + // 'data' field + h('section.flex-column.flex-center', [ + h('input.large-input', { + name: 'txData', + placeholder: '0x01234', + style: { + width: '100%', + resize: 'none', + }, + dataset: { + persistentFormId: 'tx-data', + }, + }), + ]), + ]) + ) +} + +SendTransactionScreen.prototype.navigateToAccounts = function (event) { + event.stopPropagation() + this.props.dispatch(actions.showAccountsPage()) +} + +SendTransactionScreen.prototype.back = function () { + var address = this.props.address + this.props.dispatch(actions.backToAccountDetail(address)) +} + +SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickname) { + this.setState({ + recipient: recipient, + nickname: nickname, + }) +} + +SendTransactionScreen.prototype.onSubmit = function () { + const state = this.state || {} + const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '') + const nickname = state.nickname || ' ' + const input = document.querySelector('input[name="amount"]').value + const value = util.normalizeEthStringToWei(input) + const txData = document.querySelector('input[name="txData"]').value + const balance = this.props.balance + let message + + if (value.gt(balance)) { + message = 'Insufficient funds.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if (input < 0) { + message = 'Can not send negative amounts of ETH.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) { + message = 'Recipient address is invalid.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if (!isHex(ethUtil.stripHexPrefix(txData)) && txData) { + message = 'Transaction data must be hex string.' + return this.props.dispatch(actions.displayWarning(message)) + } + + this.props.dispatch(actions.hideWarning()) + + this.props.dispatch(actions.addToAddressBook(recipient, nickname)) + + var txParams = { + from: this.props.address, + value: '0x' + value.toString(16), + } + + if (recipient) txParams.to = ethUtil.addHexPrefix(recipient) + if (txData) txParams.data = txData + + this.props.dispatch(actions.signTx(txParams)) +} diff --git a/ui/app/settings.js b/ui/app/settings.js new file mode 100644 index 000000000..454cc95e0 --- /dev/null +++ b/ui/app/settings.js @@ -0,0 +1,59 @@ +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)(AppSettingsPage) + +function mapStateToProps (state) { + return {} +} + +inherits(AppSettingsPage, Component) +function AppSettingsPage () { + Component.call(this) +} + +AppSettingsPage.prototype.render = function () { + return ( + + h('.account-detail-section.flex-column.flex-grow', [ + + // subtitle and nav + h('.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: this.navigateToAccounts.bind(this), + }), + h('h2.page-subtitle', 'Settings'), + ]), + + h('label', { + htmlFor: 'settings-rpc-endpoint', + }, 'RPC Endpoint:'), + h('input', { + type: 'url', + id: 'settings-rpc-endpoint', + onKeyPress: this.onKeyPress.bind(this), + }), + + ]) + + ) +} + +AppSettingsPage.prototype.componentDidMount = function () { + document.querySelector('input').focus() +} + +AppSettingsPage.prototype.onKeyPress = function (event) { + // get submit event + if (event.key === 'Enter') { + // this.submitPassword(event) + } +} + +AppSettingsPage.prototype.navigateToAccounts = function (event) { + event.stopPropagation() + this.props.dispatch(actions.showAccountsPage()) +} diff --git a/ui/app/store.js b/ui/app/store.js new file mode 100644 index 000000000..ba9e58b49 --- /dev/null +++ b/ui/app/store.js @@ -0,0 +1,21 @@ +const createStore = require('redux').createStore +const applyMiddleware = require('redux').applyMiddleware +const thunkMiddleware = require('redux-thunk') +const rootReducer = require('./reducers') +const createLogger = require('redux-logger') + +global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' + +module.exports = configureStore + +const loggerMiddleware = createLogger({ + predicate: () => global.METAMASK_DEBUG, +}) + +const middlewares = [thunkMiddleware, loggerMiddleware] + +const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore) + +function configureStore (initialState) { + return createStoreWithMiddleware(rootReducer, initialState) +} diff --git a/ui/app/template.js b/ui/app/template.js new file mode 100644 index 000000000..d15b30fd2 --- /dev/null +++ b/ui/app/template.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)(COMPONENTNAME) + +function mapStateToProps (state) { + return {} +} + +inherits(COMPONENTNAME, Component) +function COMPONENTNAME () { + Component.call(this) +} + +COMPONENTNAME.prototype.render = function () { + const props = this.props + + return ( + h('div', { + style: { + background: 'blue', + }, + }, [ + `Hello, ${props.sender}`, + ]) + ) +} + diff --git a/ui/app/unlock.js b/ui/app/unlock.js new file mode 100644 index 000000000..1aee3c5d0 --- /dev/null +++ b/ui/app/unlock.js @@ -0,0 +1,118 @@ +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 getCaretCoordinates = require('textarea-caret') +const EventEmitter = require('events').EventEmitter + +const Mascot = require('./components/mascot') + +module.exports = connect(mapStateToProps)(UnlockScreen) + +inherits(UnlockScreen, Component) +function UnlockScreen () { + Component.call(this) + this.animationEventEmitter = new EventEmitter() +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +UnlockScreen.prototype.render = function () { + const state = this.props + const warning = state.warning + return ( + h('.flex-column', [ + h('.unlock-screen.flex-column.flex-center.flex-grow', [ + + h(Mascot, { + animationEventEmitter: this.animationEventEmitter, + }), + + h('h1', { + style: { + fontSize: '1.4em', + textTransform: 'uppercase', + color: '#7F8082', + }, + }, 'MetaMask'), + + h('input.large-input', { + type: 'password', + id: 'password-box', + placeholder: 'enter password', + style: { + + }, + onKeyPress: this.onKeyPress.bind(this), + onInput: this.inputChanged.bind(this), + }), + + h('.error', { + style: { + display: warning ? 'block' : 'none', + padding: '0 20px', + textAlign: 'center', + }, + }, warning), + + h('button.primary.cursor-pointer', { + onClick: this.onSubmit.bind(this), + style: { + margin: 10, + }, + }, 'Unlock'), + ]), + + h('.flex-row.flex-center.flex-grow', [ + h('p.pointer', { + onClick: () => this.props.dispatch(actions.forgotPassword()), + style: { + fontSize: '0.8em', + color: 'rgb(247, 134, 28)', + textDecoration: 'underline', + }, + }, 'I forgot my password.'), + ]), + ]) + ) +} + +UnlockScreen.prototype.componentDidMount = function () { + document.getElementById('password-box').focus() +} + +UnlockScreen.prototype.onSubmit = function (event) { + const input = document.getElementById('password-box') + const password = input.value + this.props.dispatch(actions.tryUnlockMetamask(password)) +} + +UnlockScreen.prototype.onKeyPress = function (event) { + if (event.key === 'Enter') { + this.submitPassword(event) + } +} + +UnlockScreen.prototype.submitPassword = function (event) { + var element = event.target + var password = element.value + // reset input + element.value = '' + this.props.dispatch(actions.tryUnlockMetamask(password)) +} + +UnlockScreen.prototype.inputChanged = function (event) { + // tell mascot to look at page action + var element = event.target + var boundingRect = element.getBoundingClientRect() + var coordinates = getCaretCoordinates(element, element.selectionEnd) + this.animationEventEmitter.emit('point', { + x: boundingRect.left + coordinates.left - element.scrollLeft, + y: boundingRect.top + coordinates.top - element.scrollTop, + }) +} diff --git a/ui/app/util.js b/ui/app/util.js new file mode 100644 index 000000000..ac3f42c6b --- /dev/null +++ b/ui/app/util.js @@ -0,0 +1,217 @@ +const ethUtil = require('ethereumjs-util') + +var valueTable = { + wei: '1000000000000000000', + kwei: '1000000000000000', + mwei: '1000000000000', + gwei: '1000000000', + szabo: '1000000', + finney: '1000', + ether: '1', + kether: '0.001', + mether: '0.000001', + gether: '0.000000001', + tether: '0.000000000001', +} +var bnTable = {} +for (var currency in valueTable) { + bnTable[currency] = new ethUtil.BN(valueTable[currency], 10) +} + +module.exports = { + valuesFor: valuesFor, + addressSummary: addressSummary, + miniAddressSummary: miniAddressSummary, + isAllOneCase: isAllOneCase, + isValidAddress: isValidAddress, + numericBalance: numericBalance, + parseBalance: parseBalance, + formatBalance: formatBalance, + generateBalanceObject: generateBalanceObject, + dataSize: dataSize, + readableDate: readableDate, + normalizeToWei: normalizeToWei, + normalizeEthStringToWei: normalizeEthStringToWei, + normalizeNumberToWei: normalizeNumberToWei, + valueTable: valueTable, + bnTable: bnTable, + isHex: isHex, +} + +function valuesFor (obj) { + if (!obj) return [] + return Object.keys(obj) + .map(function (key) { return obj[key] }) +} + +function addressSummary (address, firstSegLength = 10, lastSegLength = 4, includeHex = true) { + if (!address) return '' + let checked = ethUtil.toChecksumAddress(address) + if (!includeHex) { + checked = ethUtil.stripHexPrefix(checked) + } + return checked ? checked.slice(0, firstSegLength) + '...' + checked.slice(checked.length - lastSegLength) : '...' +} + +function miniAddressSummary (address) { + if (!address) return '' + var checked = ethUtil.toChecksumAddress(address) + return checked ? checked.slice(0, 4) + '...' + checked.slice(-4) : '...' +} + +function isValidAddress (address) { + var prefixed = ethUtil.addHexPrefix(address) + if (address === '0x0000000000000000000000000000000000000000') return false + return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) +} + +function isAllOneCase (address) { + if (!address) return true + var lower = address.toLowerCase() + var upper = address.toUpperCase() + return address === lower || address === upper +} + +// Takes wei Hex, returns wei BN, even if input is null +function numericBalance (balance) { + if (!balance) return new ethUtil.BN(0, 16) + var stripped = ethUtil.stripHexPrefix(balance) + return new ethUtil.BN(stripped, 16) +} + +// Takes hex, returns [beforeDecimal, afterDecimal] +function parseBalance (balance) { + var beforeDecimal, afterDecimal + const wei = numericBalance(balance) + var weiString = wei.toString() + const trailingZeros = /0+$/ + + beforeDecimal = weiString.length > 18 ? weiString.slice(0, weiString.length - 18) : '0' + afterDecimal = ('000000000000000000' + wei).slice(-18).replace(trailingZeros, '') + if (afterDecimal === '') { afterDecimal = '0' } + return [beforeDecimal, afterDecimal] +} + +// Takes wei hex, returns an object with three properties. +// Its "formatted" property is what we generally use to render values. +function formatBalance (balance, decimalsToKeep, needsParse = true) { + var parsed = needsParse ? parseBalance(balance) : balance.split('.') + var beforeDecimal = parsed[0] + var afterDecimal = parsed[1] + var formatted = 'None' + if (decimalsToKeep === undefined) { + if (beforeDecimal === '0') { + if (afterDecimal !== '0') { + var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits + if (sigFigs) { afterDecimal = sigFigs[0] } + formatted = '0.' + afterDecimal + ' ETH' + } + } else { + formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ' ETH' + } + } else { + afterDecimal += Array(decimalsToKeep).join('0') + formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ' ETH' + } + return formatted +} + + +function generateBalanceObject (formattedBalance, decimalsToKeep = 1) { + var balance = formattedBalance.split(' ')[0] + var label = formattedBalance.split(' ')[1] + var beforeDecimal = balance.split('.')[0] + var afterDecimal = balance.split('.')[1] + var shortBalance = shortenBalance(balance, decimalsToKeep) + + if (beforeDecimal === '0' && afterDecimal.substr(0, 5) === '00000') { + // eslint-disable-next-line eqeqeq + if (afterDecimal == 0) { + balance = '0' + } else { + balance = '<1.0e-5' + } + } else if (beforeDecimal !== '0') { + balance = `${beforeDecimal}.${afterDecimal.slice(0, decimalsToKeep)}` + } + + return { balance, label, shortBalance } +} + +function shortenBalance (balance, decimalsToKeep = 1) { + var truncatedValue + var convertedBalance = parseFloat(balance) + if (convertedBalance > 1000000) { + truncatedValue = (balance / 1000000).toFixed(decimalsToKeep) + return `${truncatedValue}m` + } else if (convertedBalance > 1000) { + truncatedValue = (balance / 1000).toFixed(decimalsToKeep) + return `${truncatedValue}k` + } else if (convertedBalance === 0) { + return '0' + } else if (convertedBalance < 0.001) { + return '<0.001' + } else if (convertedBalance < 1) { + var stringBalance = convertedBalance.toString() + if (stringBalance.split('.')[1].length > 3) { + return convertedBalance.toFixed(3) + } else { + return stringBalance + } + } else { + return convertedBalance.toFixed(decimalsToKeep) + } +} + +function dataSize (data) { + var size = data ? ethUtil.stripHexPrefix(data).length : 0 + return size + ' bytes' +} + +// Takes a BN and an ethereum currency name, +// returns a BN in wei +function normalizeToWei (amount, currency) { + try { + return amount.mul(bnTable.wei).div(bnTable[currency]) + } catch (e) {} + return amount +} + +function normalizeEthStringToWei (str) { + const parts = str.split('.') + let eth = new ethUtil.BN(parts[0], 10).mul(bnTable.wei) + if (parts[1]) { + var decimal = parts[1] + while (decimal.length < 18) { + decimal += '0' + } + const decimalBN = new ethUtil.BN(decimal, 10) + eth = eth.add(decimalBN) + } + return eth +} + +var multiple = new ethUtil.BN('10000', 10) +function normalizeNumberToWei (n, currency) { + var enlarged = n * 10000 + var amount = new ethUtil.BN(String(enlarged), 10) + return normalizeToWei(amount, currency).div(multiple) +} + +function readableDate (ms) { + var date = new Date(ms) + var month = date.getMonth() + var day = date.getDate() + var year = date.getFullYear() + var hours = date.getHours() + var minutes = '0' + date.getMinutes() + var seconds = '0' + date.getSeconds() + + var dateStr = `${month}/${day}/${year}` + var time = `${hours}:${minutes.substr(-2)}:${seconds.substr(-2)}` + return `${dateStr} ${time}` +} + +function isHex (str) { + return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/)) +} -- cgit v1.2.3 From 38dccab12e4140bb085f3ea17e642e55f54d68a1 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 20 Jul 2017 12:54:08 -0700 Subject: Fix reference --- ui/app/conf-tx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ui/app') diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 63b77ef7f..747d3ce2b 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -6,7 +6,7 @@ const connect = require('react-redux').connect const actions = require('./actions') const NetworkIndicator = require('./components/network') const txHelper = require('../lib/tx-helper') -const isPopupOrNotification = require('../../../app/scripts/lib/is-popup-or-notification') +const isPopupOrNotification = require('../../app/scripts/lib/is-popup-or-notification') const PendingTx = require('./components/pending-tx') const PendingMsg = require('./components/pending-msg') -- cgit v1.2.3 From a22adec66fd0c541eb350ea424a6b00d179eedaf Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 24 Jul 2017 17:04:13 -0700 Subject: Replace ui with responsive-ui --- ui/app/account-detail.js | 120 ++++++------ ui/app/accounts/account-list-item.js | 91 --------- ui/app/accounts/index.js | 164 ---------------- ui/app/add-token.js | 2 +- ui/app/app.js | 272 +++++++++++++-------------- ui/app/components/account-dropdowns.js | 227 ++++++++++++++++++++++ ui/app/components/account-info-link.js | 41 ---- ui/app/components/drop-menu-item.js | 59 ------ ui/app/components/dropdown.js | 89 +++++++++ ui/app/components/editable-label.js | 7 +- ui/app/components/pending-tx.js | 2 +- ui/app/components/transaction-list.js | 8 +- ui/app/css/index.css | 15 +- ui/app/css/lib.css | 4 + ui/app/info.js | 10 +- ui/app/keychains/hd/create-vault-complete.js | 2 - 16 files changed, 537 insertions(+), 576 deletions(-) delete mode 100644 ui/app/accounts/account-list-item.js delete mode 100644 ui/app/accounts/index.js create mode 100644 ui/app/components/account-dropdowns.js delete mode 100644 ui/app/components/account-info-link.js delete mode 100644 ui/app/components/drop-menu-item.js create mode 100644 ui/app/components/dropdown.js (limited to 'ui/app') diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index bed05a7fb..18c867153 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -3,21 +3,18 @@ const extend = require('xtend') const Component = require('react').Component const h = require('react-hyperscript') const connect = require('react-redux').connect -const CopyButton = require('./components/copyButton') -const AccountInfoLink = require('./components/account-info-link') const actions = require('./actions') const ReactCSSTransitionGroup = require('react-addons-css-transition-group') const valuesFor = require('./util').valuesFor - const Identicon = require('./components/identicon') const EthBalance = require('./components/eth-balance') const TransactionList = require('./components/transaction-list') const ExportAccountView = require('./components/account-export') const ethUtil = require('ethereumjs-util') const EditableLabel = require('./components/editable-label') -const Tooltip = require('./components/tooltip') const TabBar = require('./components/tab-bar') const TokenList = require('./components/token-list') +const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns module.exports = connect(mapStateToProps)(AccountDetailScreen) @@ -54,12 +51,18 @@ AccountDetailScreen.prototype.render = function () { return ( - h('.account-detail-section', [ + h('.account-detail-section', { + style: { + height: '100%', + maxWidth: '850px', + }, + }, [ // identicon, label, balance, etc h('.account-data-subsection', { style: { margin: '0 20px', + flex: '1 0 auto', }, }, [ @@ -84,6 +87,7 @@ AccountDetailScreen.prototype.render = function () { style: { lineHeight: '10px', marginLeft: '15px', + width: '100%', }, }, [ h(EditableLabel, { @@ -98,7 +102,42 @@ AccountDetailScreen.prototype.render = function () { // What is shown when not editing + edit text: h('label.editing-label', [h('.edit-text', 'edit')]), - h('h2.font-medium.color-forest', {name: 'edit'}, identity && identity.name), + h( + 'div', + { + style: { + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center', + }, + }, + [ + h( + 'h2.font-medium.color-forest', + { + name: 'edit', + style: { + }, + }, + [ + identity && identity.name, + ] + ), + h( + AccountDropdowns, + { + style: { + marginRight: '8px', + marginLeft: 'auto', + cursor: 'pointer', + }, + selected, + network, + identities: props.identities, + }, + ), + ] + ), ]), h('.flex-row', { style: { @@ -124,56 +163,6 @@ AccountDetailScreen.prototype.render = function () { color: '#AEAEAE', }, }, checksumAddress), - - // copy and export - - h('.flex-row', { - style: { - justifyContent: 'flex-end', - }, - }, [ - - h(AccountInfoLink, { selected, network }), - - h(CopyButton, { - value: checksumAddress, - }), - - h(Tooltip, { - title: 'QR Code', - }, [ - h('i.fa.fa-qrcode.pointer.pop-hover', { - onClick: () => props.dispatch(actions.showQrView(selected, identity ? identity.name : '')), - style: { - fontSize: '18px', - position: 'relative', - color: 'rgb(247, 134, 28)', - top: '5px', - marginLeft: '3px', - marginRight: '3px', - }, - }), - ]), - - h(Tooltip, { - title: 'Export Private Key', - }, [ - h('div', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - h('img.cursor-pointer.color-orange', { - src: 'images/key-32.png', - onClick: () => this.requestAccountExport(selected), - style: { - height: '19px', - }, - }), - ]), - ]), - ]), ]), // account ballence @@ -197,14 +186,11 @@ AccountDetailScreen.prototype.render = function () { }, }), + h('.flex-grow'), + h('button', { onClick: () => props.dispatch(actions.buyEthView(selected)), - style: { - marginBottom: '20px', - marginRight: '8px', - position: 'absolute', - left: '219px', - }, + style: { marginRight: '10px' }, }, 'BUY'), h('button', { @@ -254,7 +240,11 @@ AccountDetailScreen.prototype.subview = function () { AccountDetailScreen.prototype.tabSections = function () { const { currentAccountTab } = this.props - return h('section.tabSection', [ + return h('section.tabSection', { + style: { + height: '100%', + }, + }, [ h(TabBar, { tabs: [ @@ -305,7 +295,3 @@ AccountDetailScreen.prototype.transactionList = function () { }, }) } - -AccountDetailScreen.prototype.requestAccountExport = function () { - this.props.dispatch(actions.requestExportAccount()) -} diff --git a/ui/app/accounts/account-list-item.js b/ui/app/accounts/account-list-item.js deleted file mode 100644 index 10a0b6cc7..000000000 --- a/ui/app/accounts/account-list-item.js +++ /dev/null @@ -1,91 +0,0 @@ -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/index.js b/ui/app/accounts/index.js deleted file mode 100644 index ac2615cd7..000000000 --- a/ui/app/accounts/index.js +++ /dev/null @@ -1,164 +0,0 @@ -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()) -} diff --git a/ui/app/add-token.js b/ui/app/add-token.js index 15ef7a852..b303b5c0d 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -86,7 +86,7 @@ AddTokenScreen.prototype.render = function () { h('div', [ h('span', { style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Token Symbol'), + }, 'Token Sybmol'), ]), h('div', { style: {display: 'flex'} }, [ diff --git a/ui/app/app.js b/ui/app/app.js index 1a63002e1..d1a20f079 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -10,7 +10,6 @@ const NewKeyChainScreen = require('./new-keychain') // unlock const UnlockScreen = require('./unlock') // accounts -const AccountsScreen = require('./accounts') const AccountDetailScreen = require('./account-detail') const SendTransactionScreen = require('./send') const ConfirmTxScreen = require('./conf-tx') @@ -24,10 +23,9 @@ const Import = require('./accounts/import') const InfoScreen = require('./info') const Loading = require('./components/loading') const SandwichExpando = require('sandwich-expando') -const MenuDroppo = require('menu-droppo') -const DropMenuItem = require('./components/drop-menu-item') +const Dropdown = require('./components/dropdown').Dropdown +const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem const NetworkIndicator = require('./components/network') -const Tooltip = require('./components/tooltip') const BuyView = require('./components/buy-button-subview') const QrView = require('./components/qr-code') const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete') @@ -79,6 +77,8 @@ App.prototype.render = function () { // Windows was showing a vertical scroll bar: overflow: 'hidden', position: 'relative', + height: '100%', + alignItems: 'center', }, }, [ @@ -95,8 +95,8 @@ App.prototype.render = function () { // panel content h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), { style: { - height: '380px', - width: '360px', + height: '100%', + maxWidth: '850px', }, }, [ h(ReactCSSTransitionGroup, { @@ -123,14 +123,18 @@ App.prototype.renderAppBar = function () { return ( - h('div', [ + h('div', { + style: { + width: '100%' + }, + }, [ h('.app-header.flex-row.flex-space-between', { style: { alignItems: 'center', visibility: props.isUnlocked ? 'visible' : 'none', background: props.isUnlocked ? 'white' : 'none', - height: '36px', + height: '38px', position: 'relative', zIndex: 12, }, @@ -178,21 +182,6 @@ App.prototype.renderAppBar = function () { }, }, [ - // small accounts nav - props.isUnlocked && h(Tooltip, { title: 'Switch Accounts' }, [ - h('img.cursor-pointer.color-orange', { - src: 'images/switch_acc.svg', - style: { - width: '23.5px', - marginRight: '8px', - }, - onClick: (event) => { - event.stopPropagation() - this.props.dispatch(actions.showAccountsPage()) - }, - }), - ]), - // hamburger props.isUnlocked && h(SandwichExpando, { width: 16, @@ -214,11 +203,12 @@ App.prototype.renderAppBar = function () { App.prototype.renderNetworkDropdown = function () { const props = this.props + const { provider: { type: providerType, rpcTarget: activeNetwork } } = props const rpcList = props.frequentRpcList const state = this.state || {} const isOpen = state.isNetworkMenuOpen - return h(MenuDroppo, { + return h(Dropdown, { isOpen, onClickOutside: (event) => { this.setState({ isNetworkMenuOpen: !isOpen }) @@ -226,72 +216,92 @@ App.prototype.renderNetworkDropdown = function () { zIndex: 11, style: { position: 'absolute', - left: 0, + left: '2px', top: '36px', }, - innerStyle: { - background: 'white', - boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', - }, - }, [ // DROP MENU ITEMS - h('style', ` - .drop-menu-item:hover { background:rgb(235, 235, 235); } - .drop-menu-item i { margin: 11px; } - `), - - h(DropMenuItem, { - label: 'Main Ethereum Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setProviderType('mainnet')), - icon: h('.menu-icon.diamond'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Ropsten Test Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setProviderType('ropsten')), - icon: h('.menu-icon.red-dot'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Kovan Test Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false}), - action: () => props.dispatch(actions.setProviderType('kovan')), - icon: h('.menu-icon.hollow-diamond'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Rinkeby Test Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false}), - action: () => props.dispatch(actions.setProviderType('rinkeby')), - icon: h('.menu-icon.golden-square'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Localhost 8545', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setDefaultRpcTarget(rpcList)), - icon: h('i.fa.fa-question-circle.fa-lg'), - activeNetworkRender: props.provider.rpcTarget, - }), + innerStyle: {}, + }, [ + + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setProviderType('mainnet')), + }, + [ + h('.menu-icon.diamond'), + 'Main Ethereum Network', + providerType === 'mainnet' ? h('.check', '✓') : null, + ] + ), + + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setProviderType('ropsten')), + }, + [ + h('.menu-icon.red-dot'), + 'Ropsten Test Network', + providerType === 'ropsten' ? h('.check', '✓') : null, + ] + ), + + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setProviderType('kovan')), + }, + [ + h('.menu-icon.hollow-diamond'), + 'Kovan Test Network', + providerType === 'kovan' ? h('.check', '✓') : null, + ] + ), + + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setProviderType('rinkeby')), + }, + [ + h('.menu-icon.golden-square'), + 'Rinkeby Test Network', + providerType === 'rinkeby' ? h('.check', '✓') : null, + ] + ), + + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setDefaultRpcTarget(rpcList)), + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + 'Localhost 8545', + activeNetwork === 'http://localhost:8545' ? h('.check', '✓') : null, + ] + ), this.renderCustomOption(props.provider), this.renderCommonRpc(rpcList, props.provider), - h(DropMenuItem, { - label: 'Custom RPC', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => this.props.dispatch(actions.showConfigPage()), - icon: h('i.fa.fa-question-circle.fa-lg'), - }), + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => this.props.dispatch(actions.showConfigPage()), + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + 'Custom RPC', + activeNetwork === 'custom' ? h('.check', '✓') : null, + ] + ), ]) } @@ -300,7 +310,7 @@ App.prototype.renderDropdown = function () { const state = this.state || {} const isOpen = state.isMainMenuOpen - return h(MenuDroppo, { + return h(Dropdown, { isOpen: isOpen, zIndex: 11, onClickOutside: (event) => { @@ -308,46 +318,30 @@ App.prototype.renderDropdown = function () { }, style: { position: 'absolute', - right: 0, - top: '36px', + right: '2px', + top: '38px', }, - innerStyle: { - background: 'white', - boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', - }, - }, [ // DROP MENU ITEMS - h('style', ` - .drop-menu-item:hover { background:rgb(235, 235, 235); } - .drop-menu-item i { margin: 11px; } - `), - - h(DropMenuItem, { - label: 'Settings', + innerStyle: {}, + }, [ + h(DropdownMenuItem, { closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.showConfigPage()), - icon: h('i.fa.fa-gear.fa-lg'), - }), + onClick: () => { this.props.dispatch(actions.showConfigPage()) }, + }, 'Settings'), - h(DropMenuItem, { - label: 'Import Account', + h(DropdownMenuItem, { closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.showImportPage()), - icon: h('i.fa.fa-arrow-circle-o-up.fa-lg'), - }), + onClick: () => { this.props.dispatch(actions.showImportPage()) }, + }, 'Import Account'), - h(DropMenuItem, { - label: 'Lock', + h(DropdownMenuItem, { closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.lockMetamask()), - icon: h('i.fa.fa-lock.fa-lg'), - }), + onClick: () => { this.props.dispatch(actions.lockMetamask()) }, + }, 'Lock'), - h(DropMenuItem, { - label: 'Info/Help', + h(DropdownMenuItem, { closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.showInfoPage()), - icon: h('i.fa.fa-question.fa-lg'), - }), + onClick: () => { this.props.dispatch(actions.showInfoPage()) }, + }, 'Info/Help'), ]) } @@ -433,10 +427,6 @@ App.prototype.renderPrimary = function () { // show current view switch (props.currentView.name) { - case 'accounts': - log.debug('rendering accounts screen') - return h(AccountsScreen, {key: 'accounts'}) - case 'accountDetail': log.debug('rendering account detail screen') return h(AccountDetailScreen, {key: 'account-detail'}) @@ -539,13 +529,18 @@ App.prototype.renderCustomOption = function (provider) { return null default: - return h(DropMenuItem, { - label, - key: rpcTarget, - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - icon: h('i.fa.fa-question-circle.fa-lg'), - activeNetworkRender: 'custom', - }) + return h( + DropdownMenuItem, + { + key: rpcTarget, + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + label, + h('.check', '✓'), + ] + ) } } @@ -578,14 +573,19 @@ App.prototype.renderCommonRpc = function (rpcList, provider) { if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { return null } else { - return h(DropMenuItem, { - label: rpc, - key: rpc, - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setRpcTarget(rpc)), - icon: h('i.fa.fa-question-circle.fa-lg'), - activeNetworkRender: rpc, - }) + return h( + DropdownMenuItem, + { + key: rpc, + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setRpcTarget(rpc)), + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + rpc, + h('.check', '✓'), + ] + ) } }) } diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js new file mode 100644 index 000000000..d1d319477 --- /dev/null +++ b/ui/app/components/account-dropdowns.js @@ -0,0 +1,227 @@ +const Component = require('react').Component +const PropTypes = require('react').PropTypes +const h = require('react-hyperscript') +const actions = require('../actions') +const genAccountLink = require('../../lib/account-link.js') +const connect = require('react-redux').connect +const Dropdown = require('./dropdown').Dropdown +const DropdownMenuItem = require('./dropdown').DropdownMenuItem +const Identicon = require('./identicon') +const ethUtil = require('ethereumjs-util') +const copyToClipboard = require('copy-to-clipboard') + +class AccountDropdowns extends Component { + constructor (props) { + super(props) + this.state = { + accountSelectorActive: false, + optionsMenuActive: false, + } + } + + renderAccounts () { + const { identities, selected } = this.props + + return Object.keys(identities).map((key) => { + const identity = identities[key] + const isSelected = identity.address === selected + + return h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + this.props.actions.showAccountDetail(identity.address) + }, + }, + [ + h( + Identicon, + { + address: identity.address, + diameter: 16, + }, + ), + h('span', { style: { marginLeft: '10px' } }, identity.name || ''), + h('span', { style: { marginLeft: '10px' } }, isSelected ? h('.check', '✓') : null), + ] + ) + }) + } + + renderAccountSelector () { + const { actions } = this.props + const { accountSelectorActive } = this.state + + return h( + Dropdown, + { + style: { + marginLeft: '-125px', + minWidth: '180px', + }, + isOpen: accountSelectorActive, + onClickOutside: () => { this.setState({ accountSelectorActive: false }) }, + }, + [ + ...this.renderAccounts(), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => actions.addNewAccount(), + }, + [ + h( + Identicon, + { + diameter: 16, + }, + ), + h('span', { style: { marginLeft: '10px' } }, 'Create Account'), + ], + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => actions.showImportPage(), + }, + [ + h( + Identicon, + { + diameter: 16, + }, + ), + h('span', { style: { marginLeft: '10px' } }, 'Import Account'), + ] + ), + ] + ) + } + + renderAccountOptions () { + const { actions } = this.props + const { optionsMenuActive } = this.state + + return h( + Dropdown, + { + style: { + marginLeft: '-162px', + minWidth: '180px', + }, + isOpen: optionsMenuActive, + onClickOutside: () => { this.setState({ optionsMenuActive: false }) }, + }, + [ + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => actions.showConfigPage(), + }, + 'Account Settings', + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + const { selected, network } = this.props + const url = genAccountLink(selected, network) + global.platform.openWindow({ url }) + }, + }, + 'View account on Etherscan', + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + const { selected } = this.props + const checkSumAddress = selected && ethUtil.toChecksumAddress(selected) + copyToClipboard(checkSumAddress) + }, + }, + 'Copy Address to clipboard', + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + actions.requestAccountExport() + }, + }, + 'Export Private Key', + ), + ] + ) + } + + render () { + const { style } = this.props + const { optionsMenuActive, accountSelectorActive } = this.state + + return h( + 'span', + { + style: style, + }, + [ + h( + 'i.fa.fa-angle-down', + { + style: {}, + onClick: (event) => { + event.stopPropagation() + this.setState({ + accountSelectorActive: !accountSelectorActive, + optionsMenuActive: false, + }) + }, + }, + this.renderAccountSelector(), + ), + h( + 'i.fa.fa-ellipsis-h', + { + style: { 'marginLeft': '10px'}, + onClick: (event) => { + event.stopPropagation() + this.setState({ + accountSelectorActive: false, + optionsMenuActive: !optionsMenuActive, + }) + }, + }, + this.renderAccountOptions() + ), + ] + ) + } +} + +AccountDropdowns.propTypes = { + identities: PropTypes.objectOf(PropTypes.object), + selected: PropTypes.string, +} + +const mapDispatchToProps = (dispatch) => { + return { + actions: { + showConfigPage: () => dispatch(actions.showConfigPage()), + requestAccountExport: () => dispatch(actions.requestExportAccount()), + showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)), + addNewAccount: () => dispatch(actions.addNewAccount()), + showImportPage: () => dispatch(actions.showImportPage()), + }, + } +} + +module.exports = { + AccountDropdowns: connect(null, mapDispatchToProps)(AccountDropdowns), +} diff --git a/ui/app/components/account-info-link.js b/ui/app/components/account-info-link.js deleted file mode 100644 index 6526ab502..000000000 --- a/ui/app/components/account-info-link.js +++ /dev/null @@ -1,41 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const Tooltip = require('./tooltip') -const genAccountLink = require('../../lib/account-link') - -module.exports = AccountInfoLink - -inherits(AccountInfoLink, Component) -function AccountInfoLink () { - Component.call(this) -} - -AccountInfoLink.prototype.render = function () { - const { selected, network } = this.props - const title = 'View account on Etherscan' - const url = genAccountLink(selected, network) - - if (!url) { - return null - } - - return h('.account-info-link', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - - h(Tooltip, { - title, - }, [ - h('i.fa.fa-info-circle.cursor-pointer.color-orange', { - style: { - margin: '5px', - }, - onClick () { global.platform.openWindow({ url }) }, - }), - ]), - ]) -} diff --git a/ui/app/components/drop-menu-item.js b/ui/app/components/drop-menu-item.js deleted file mode 100644 index e42948209..000000000 --- a/ui/app/components/drop-menu-item.js +++ /dev/null @@ -1,59 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = DropMenuItem - -inherits(DropMenuItem, Component) -function DropMenuItem () { - Component.call(this) -} - -DropMenuItem.prototype.render = function () { - return h('li.drop-menu-item', { - onClick: () => { - this.props.closeMenu() - this.props.action() - }, - style: { - listStyle: 'none', - padding: '6px 16px 6px 5px', - fontFamily: 'Montserrat Regular', - color: 'rgb(125, 128, 130)', - cursor: 'pointer', - display: 'flex', - justifyContent: 'flex-start', - }, - }, [ - this.props.icon, - this.props.label, - this.activeNetworkRender(), - ]) -} - -DropMenuItem.prototype.activeNetworkRender = function () { - const activeNetwork = this.props.activeNetworkRender - const { provider } = this.props - const providerType = provider ? provider.type : null - if (activeNetwork === undefined) return - - switch (this.props.label) { - case 'Main Ethereum Network': - if (providerType === 'mainnet') return h('.check', '✓') - break - case 'Ropsten Test Network': - if (providerType === 'ropsten') return h('.check', '✓') - break - case 'Kovan Test Network': - if (providerType === 'kovan') return h('.check', '✓') - break - case 'Rinkeby Test Network': - if (providerType === 'rinkeby') return h('.check', '✓') - break - case 'Localhost 8545': - if (activeNetwork === 'http://localhost:8545') return h('.check', '✓') - break - default: - if (activeNetwork === 'custom') return h('.check', '✓') - } -} diff --git a/ui/app/components/dropdown.js b/ui/app/components/dropdown.js new file mode 100644 index 000000000..e77b4c40c --- /dev/null +++ b/ui/app/components/dropdown.js @@ -0,0 +1,89 @@ +const Component = require('react').Component +const PropTypes = require('react').PropTypes +const h = require('react-hyperscript') +const MenuDroppo = require('menu-droppo') + +const noop = () => {} + +class Dropdown extends Component { + render () { + const { isOpen, onClickOutside, style, children } = this.props + + return h( + MenuDroppo, + { + isOpen, + zIndex: 11, + onClickOutside, + style, + innerStyle: { + borderRadius: '4px', + padding: '8px 16px', + background: 'rgba(0, 0, 0, 0.8)', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + }, + }, + [ + h( + 'style', + ` + li.dropdown-menu-item:hover { color:rgb(225, 225, 225); } + li.dropdown-menu-item { color: rgb(185, 185, 185); } + ` + ), + ...children, + ] + ) + } +} + +Dropdown.defaultProps = { + isOpen: false, + onClick: noop, +} + +Dropdown.propTypes = { + isOpen: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, + children: PropTypes.node, + style: PropTypes.object.isRequired, +} + +class DropdownMenuItem extends Component { + render () { + const { onClick, closeMenu, children } = this.props + + return h( + 'li.dropdown-menu-item', + { + onClick: () => { + onClick() + closeMenu() + }, + style: { + listStyle: 'none', + padding: '8px 0px 8px 0px', + fontSize: '12px', + fontStyle: 'normal', + fontFamily: 'Montserrat Regular', + cursor: 'pointer', + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center', + }, + }, + children + ) + } +} + +DropdownMenuItem.propTypes = { + closeMenu: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, + children: PropTypes.node, +} + +module.exports = { + Dropdown, + DropdownMenuItem, +} diff --git a/ui/app/components/editable-label.js b/ui/app/components/editable-label.js index 41936f5e0..167be7eaf 100644 --- a/ui/app/components/editable-label.js +++ b/ui/app/components/editable-label.js @@ -30,7 +30,12 @@ EditableLabel.prototype.render = function () { } else { return h('div.name-label', { onClick: (event) => { - this.setState({ isEditingLabel: true }) + const nameAttribute = event.target.getAttribute('name') + // checks for class to handle smaller CTA above the account name + const classAttribute = event.target.getAttribute('class') + if (nameAttribute === 'edit' || classAttribute === 'edit-text') { + this.setState({ isEditingLabel: true }) + } }, }, this.props.children) } diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 5324ccd64..d7d602f31 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -15,7 +15,7 @@ const addressSummary = util.addressSummary const nameForAddress = require('../../lib/contract-namer') const BNInput = require('./bn-as-decimal-input') -const MIN_GAS_PRICE_GWEI_BN = new BN(1) +const MIN_GAS_PRICE_GWEI_BN = new BN(2) const GWEI_FACTOR = new BN(1e9) const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) const MIN_GAS_LIMIT_BN = new BN(21000) diff --git a/ui/app/components/transaction-list.js b/ui/app/components/transaction-list.js index 3b4ba741e..ae6aaec8c 100644 --- a/ui/app/components/transaction-list.js +++ b/ui/app/components/transaction-list.js @@ -24,7 +24,11 @@ TransactionList.prototype.render = function () { return ( - h('section.transaction-list', [ + h('section.transaction-list', { + style: { + height: '100%', + }, + }, [ h('style', ` .transaction-list .transaction-list-item:not(:last-of-type) { @@ -39,7 +43,7 @@ TransactionList.prototype.render = function () { h('.tx-list', { style: { overflowY: 'auto', - height: '300px', + height: '100%', padding: '0 20px', textAlign: 'center', }, diff --git a/ui/app/css/index.css b/ui/app/css/index.css index 808aafb4c..2ae92bbd6 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -19,6 +19,15 @@ html, body { font-weight: 300; line-height: 1.4em; background: #F7F7F7; + width: 100%; + height: 100%; + margin: 0; + padding: 0; +} + +.css-transition-group { + flex: 1; + height: 100%; } input:focus, textarea:focus { @@ -28,8 +37,7 @@ input:focus, textarea:focus { #app-content { overflow-x: hidden; min-width: 357px; - width: 360px; - height: 500px; + height: 100%; } button, input[type="submit"] { @@ -403,7 +411,8 @@ input.large-input { /* account detail screen */ .account-detail-section { - + display: flex; + flex-wrap: wrap; } .name-label{ diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css index 910a24ee2..b0ca958a2 100644 --- a/ui/app/css/lib.css +++ b/ui/app/css/lib.css @@ -232,6 +232,10 @@ hr.horizontal-line { align-items: center; } +.tabSection { + min-width: 350px; +} + .menu-icon { display: inline-block; height: 9px; diff --git a/ui/app/info.js b/ui/app/info.js index cb2e41f5b..e8470de97 100644 --- a/ui/app/info.js +++ b/ui/app/info.js @@ -97,17 +97,11 @@ InfoScreen.prototype.render = function () { paddingLeft: '30px', }}, [ - h('div.fa.fa-support', [ - h('a.info', { - href: 'http://metamask.consensyssupport.happyfox.com', - target: '_blank', - }, 'Visit our Support Center'), - ]), h('div.fa.fa-github', [ h('a.info', { - href: 'https://github.com/MetaMask/metamask-extension/issues/new', + href: 'https://github.com/MetaMask/faq', target: '_blank', - }, 'Found a bug? Report it!'), + }, 'Need Help? Read our FAQ!'), ]), h('div', [ h('a', { diff --git a/ui/app/keychains/hd/create-vault-complete.js b/ui/app/keychains/hd/create-vault-complete.js index a318a9b50..c32751fff 100644 --- a/ui/app/keychains/hd/create-vault-complete.js +++ b/ui/app/keychains/hd/create-vault-complete.js @@ -47,8 +47,6 @@ CreateVaultCompleteScreen.prototype.render = function () { h('div', { style: { - width: '360px', - height: '78px', fontSize: '1em', marginTop: '10px', textAlign: 'center', -- cgit v1.2.3 From 7a04643d8ea8a47cc159fc69c941e588e8b873cc Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 24 Jul 2017 17:27:27 -0700 Subject: Restore sort order fix --- ui/app/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ui/app') diff --git a/ui/app/app.js b/ui/app/app.js index d1a20f079..973cb756c 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -125,7 +125,7 @@ App.prototype.renderAppBar = function () { h('div', { style: { - width: '100%' + width: '100%', }, }, [ -- cgit v1.2.3 From 0ea6749dbc923a6e796b1de4bbd301d931739b9d Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 25 Jul 2017 18:22:31 -0700 Subject: Lots of flex rearrangement on account detail view Includes removal of ReactCssTransitionGroup for a simpler UI refactor. --- ui/app/account-detail.js | 25 +++--------------- ui/app/app.js | 31 ++++++---------------- ui/app/components/loading.js | 47 ++++++++++++++-------------------- ui/app/components/shapeshift-form.js | 10 +------- ui/app/components/tab-bar.js | 1 + ui/app/components/token-list.js | 40 +++++++++++++++++++++-------- ui/app/components/transaction-list.js | 12 +++++---- ui/app/conf-tx.js | 48 ++++++++++++++--------------------- ui/app/css/index.css | 46 ++++++++++++++++++++++++++++----- ui/app/css/lib.css | 1 + 10 files changed, 129 insertions(+), 132 deletions(-) (limited to 'ui/app') diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index 18c867153..24561c32e 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -4,7 +4,6 @@ const Component = require('react').Component const h = require('react-hyperscript') const connect = require('react-redux').connect const actions = require('./actions') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') const valuesFor = require('./util').valuesFor const Identicon = require('./components/identicon') const EthBalance = require('./components/eth-balance') @@ -51,14 +50,9 @@ AccountDetailScreen.prototype.render = function () { return ( - h('.account-detail-section', { - style: { - height: '100%', - maxWidth: '850px', - }, - }, [ + h('.account-detail-section.full-flex-height', [ - // identicon, label, balance, etc + // identicon, label, balance, etc h('.account-data-subsection', { style: { margin: '0 20px', @@ -205,14 +199,7 @@ AccountDetailScreen.prototype.render = function () { ]), // subview (tx history, pk export confirm, buy eth warning) - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.subview(), - ]), + this.subview(), ]) ) @@ -240,11 +227,7 @@ AccountDetailScreen.prototype.subview = function () { AccountDetailScreen.prototype.tabSections = function () { const { currentAccountTab } = this.props - return h('section.tabSection', { - style: { - height: '100%', - }, - }, [ + return h('section.tabSection.full-flex-height.grow-tenx', [ h(TabBar, { tabs: [ diff --git a/ui/app/app.js b/ui/app/app.js index 973cb756c..6da48b9b6 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -3,7 +3,6 @@ const Component = require('react').Component const connect = require('react-redux').connect const h = require('react-hyperscript') const actions = require('./actions') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') // init const InitializeMenuScreen = require('./first-time/init-menu') const NewKeyChainScreen = require('./new-keychain') @@ -67,17 +66,15 @@ App.prototype.render = function () { const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' const loadMessage = loadingMessage || isLoadingNetwork ? `Connecting to ${this.getNetworkName()}` : null - log.debug('Main ui render function') return ( - h('.flex-column.flex-grow.full-height', { + h('.flex-column.full-height', { style: { // Windows was showing a vertical scroll bar: overflow: 'hidden', position: 'relative', - height: '100%', alignItems: 'center', }, }, [ @@ -93,20 +90,12 @@ App.prototype.render = function () { }), // panel content - h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), { + h('.app-primary' + (transForward ? '.from-right' : '.from-left'), { style: { - height: '100%', maxWidth: '850px', }, }, [ - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.renderPrimary(), - ]), + this.renderPrimary(), ]), ]) ) @@ -123,10 +112,8 @@ App.prototype.renderAppBar = function () { return ( - h('div', { - style: { - width: '100%', - }, + h('.full-width', { + height: '38px', }, [ h('.app-header.flex-row.flex-space-between', { @@ -328,11 +315,6 @@ App.prototype.renderDropdown = function () { onClick: () => { this.props.dispatch(actions.showConfigPage()) }, }, 'Settings'), - h(DropdownMenuItem, { - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - onClick: () => { this.props.dispatch(actions.showImportPage()) }, - }, 'Import Account'), - h(DropdownMenuItem, { closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), onClick: () => { this.props.dispatch(actions.lockMetamask()) }, @@ -515,6 +497,8 @@ App.prototype.toggleMetamaskActive = function () { App.prototype.renderCustomOption = function (provider) { const { rpcTarget, type } = provider + const props = this.props + if (type !== 'rpc') return null // Concatenate long URLs @@ -533,6 +517,7 @@ App.prototype.renderCustomOption = function (provider) { DropdownMenuItem, { key: rpcTarget, + onClick: () => props.dispatch(actions.setCustomRpc(rpcTarget)), closeMenu: () => this.setState({ isNetworkMenuOpen: false }), }, [ diff --git a/ui/app/components/loading.js b/ui/app/components/loading.js index 87d6f5d20..92d17ccd6 100644 --- a/ui/app/components/loading.js +++ b/ui/app/components/loading.js @@ -1,7 +1,6 @@ const inherits = require('util').inherits const Component = require('react').Component const h = require('react-hyperscript') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') inherits(LoadingIndicator, Component) @@ -15,35 +14,27 @@ LoadingIndicator.prototype.render = function () { const { isLoading, loadingMessage } = this.props return ( - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'loader', - transitionEnterTimeout: 150, - transitionLeaveTimeout: 150, + isLoading ? h('div', { + style: { + zIndex: 10, + position: 'absolute', + flexDirection: 'column', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '100%', + width: '100%', + background: 'rgba(255, 255, 255, 0.8)', + }, }, [ + h('img', { + src: 'images/loading.svg', + }), - isLoading ? h('div', { - style: { - zIndex: 10, - position: 'absolute', - flexDirection: 'column', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - height: '100%', - width: '100%', - background: 'rgba(255, 255, 255, 0.8)', - }, - }, [ - h('img', { - src: 'images/loading.svg', - }), - - h('br'), - - showMessageIfAny(loadingMessage), - ]) : null, - ]) + h('br'), + + showMessageIfAny(loadingMessage), + ]) : null ) } diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js index e0a720426..76a265d63 100644 --- a/ui/app/components/shapeshift-form.js +++ b/ui/app/components/shapeshift-form.js @@ -2,7 +2,6 @@ const PersistentForm = require('../../lib/persistent-form') const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') const actions = require('../actions') const Qr = require('./qr-code') const isValidAddress = require('../util').isValidAddress @@ -24,14 +23,7 @@ function ShapeshiftForm () { } ShapeshiftForm.prototype.render = function () { - return h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain(), - ]) + return this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain() } ShapeshiftForm.prototype.renderMain = function () { diff --git a/ui/app/components/tab-bar.js b/ui/app/components/tab-bar.js index 6295e7dd9..bef444a48 100644 --- a/ui/app/components/tab-bar.js +++ b/ui/app/components/tab-bar.js @@ -21,6 +21,7 @@ TabBar.prototype.render = function () { background: '#EBEBEB', color: '#AEAEAE', paddingTop: '4px', + minHeight: '30px', }, }, tabs.map((tab) => { const { key, content } = tab diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index 20cfa897e..79ec3f351 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -47,10 +47,11 @@ TokenList.prototype.render = function () { return h(TokenCell, tokenData) }) - return h('div', [ - h('ol', { + return h('.full-flex-height', [ + this.renderTokenStatusBar(), + + h('ol.full-flex-height.flex-column', { style: { - height: '260px', overflowY: 'auto', display: 'flex', flexDirection: 'column', @@ -63,6 +64,7 @@ TokenList.prototype.render = function () { flex-direction: row; align-items: center; padding: 10px; + min-height: 50px; } li.token-cell > h3 { @@ -76,17 +78,35 @@ TokenList.prototype.render = function () { `), ...tokenViews, - tokenViews.length ? null : this.message('No Tokens Found.'), + h('.flex-grow'), ]), - this.addTokenButtonElement(), ]) } -TokenList.prototype.addTokenButtonElement = function () { - return h('div', [ - h('div.footer.hover-white.pointer', { +TokenList.prototype.renderTokenStatusBar = function () { + const { tokens } = this.state + + let msg + if (tokens.length > 0) { + msg = `You own ${tokens.length} tokens` + } else { + msg = `No tokens found` + } + + return h('div', { + style: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + minHeight: '70px', + padding: '10px', + }, + }, [ + h('span', msg), + h('button', { key: 'reveal-account-bar', - onClick: () => { + onClick: (event) => { + event.preventDefault() this.props.addToken() }, style: { @@ -97,7 +117,7 @@ TokenList.prototype.addTokenButtonElement = function () { alignItems: 'center', }, }, [ - h('i.fa.fa-plus.fa-lg'), + 'ADD TOKEN', ]), ]) } diff --git a/ui/app/components/transaction-list.js b/ui/app/components/transaction-list.js index ae6aaec8c..192931486 100644 --- a/ui/app/components/transaction-list.js +++ b/ui/app/components/transaction-list.js @@ -24,9 +24,9 @@ TransactionList.prototype.render = function () { return ( - h('section.transaction-list', { + h('section.transaction-list.full-flex-height', { style: { - height: '100%', + justifyContent: 'center', }, }, [ @@ -68,13 +68,15 @@ TransactionList.prototype.render = function () { }, }) }) - : h('.flex-center', { + : h('.flex-center.full-flex-height', { style: { flexDirection: 'column', - height: '100%', + justifyContent: 'center', }, }, [ - 'No transaction history.', + h('p', { + marginTop: '50px', + }, 'No transaction history.'), ]), ]), ]) diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 747d3ce2b..34727ff78 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -1,6 +1,5 @@ const inherits = require('util').inherits const Component = require('react').Component -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') const h = require('react-hyperscript') const connect = require('react-redux').connect const actions = require('./actions') @@ -92,34 +91,25 @@ ConfirmTxScreen.prototype.render = function () { warningIfExists(props.warning), - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - - currentTxView({ - // Properties - txData: txData, - key: txData.id, - selectedAddress: props.selectedAddress, - accounts: props.accounts, - identities: props.identities, - conversionRate, - currentCurrency, - blockGasLimit, - // Actions - buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), - sendTransaction: this.sendTransaction.bind(this), - cancelTransaction: this.cancelTransaction.bind(this, txData), - signMessage: this.signMessage.bind(this, txData), - signPersonalMessage: this.signPersonalMessage.bind(this, txData), - cancelMessage: this.cancelMessage.bind(this, txData), - cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), - }), - - ]), + currentTxView({ + // Properties + txData: txData, + key: txData.id, + selectedAddress: props.selectedAddress, + accounts: props.accounts, + identities: props.identities, + conversionRate, + currentCurrency, + blockGasLimit, + // Actions + buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), + sendTransaction: this.sendTransaction.bind(this), + cancelTransaction: this.cancelTransaction.bind(this, txData), + signMessage: this.signMessage.bind(this, txData), + signPersonalMessage: this.signPersonalMessage.bind(this, txData), + cancelMessage: this.cancelMessage.bind(this, txData), + cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), + }), ]) ) } diff --git a/ui/app/css/index.css b/ui/app/css/index.css index 2ae92bbd6..00d4bea93 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -25,19 +25,48 @@ html, body { padding: 0; } -.css-transition-group { - flex: 1; - height: 100%; +html { + min-height: 400px; +} + +.app-root { + overflow: hidden; + position: relative +} + +.app-primary { + display: flex; } input:focus, textarea:focus { outline: none; } +.full-size { + height: 100%; + width: 100%; +} + +.full-width { + width: 100%; +} + +.full-height { + height: 100%; +} + +.full-flex-height { + display: flex; + flex: 1 1 auto; + flex-direction: column; +} + #app-content { overflow-x: hidden; min-width: 357px; height: 100%; + display: flex; + flex-direction: column; } button, input[type="submit"] { @@ -138,10 +167,6 @@ h2.page-subtitle { margin: 12px; } -.app-primary { - -} - .app-footer { padding-bottom: 10px; align-items: center; @@ -413,7 +438,14 @@ input.large-input { .account-detail-section { display: flex; flex-wrap: wrap; + overflow-y: auto; + flex-direction: inherit; } + +.grow-tenx { + flex-grow: 10; +} + .name-label{ } diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css index b0ca958a2..98570859a 100644 --- a/ui/app/css/lib.css +++ b/ui/app/css/lib.css @@ -270,3 +270,4 @@ hr.horizontal-line { margin-top: 20px; color: red; } + -- cgit v1.2.3 From 0fdbb8096259d622ff92f4ebb0b83a135c2f705c Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 26 Jul 2017 12:31:08 -0700 Subject: Remove Account Settings item from optionsMenu, unnecessary --- ui/app/components/account-dropdowns.js | 8 -------- 1 file changed, 8 deletions(-) (limited to 'ui/app') diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index d1d319477..ec223ea29 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -116,14 +116,6 @@ class AccountDropdowns extends Component { onClickOutside: () => { this.setState({ optionsMenuActive: false }) }, }, [ - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => actions.showConfigPage(), - }, - 'Account Settings', - ), h( DropdownMenuItem, { -- cgit v1.2.3 From 4c0f827946dfb6ea115d628480a332d6f732ca20 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 26 Jul 2017 12:43:18 -0700 Subject: Make account selection dropdown menu scrollable when too large for view --- ui/app/components/account-dropdowns.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'ui/app') diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index ec223ea29..61f32f713 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -59,6 +59,8 @@ class AccountDropdowns extends Component { style: { marginLeft: '-125px', minWidth: '180px', + overflowY: 'auto', + maxHeight: '300px', }, isOpen: accountSelectorActive, onClickOutside: () => { this.setState({ accountSelectorActive: false }) }, -- cgit v1.2.3 From 8006d798ee8d1993ef4b06cce25480f0aea6c4f4 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 26 Jul 2017 13:02:08 -0700 Subject: Re-center network dropdown --- ui/app/app.js | 4 +++- ui/app/components/dropdown.js | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) (limited to 'ui/app') diff --git a/ui/app/app.js b/ui/app/app.js index 6da48b9b6..4ecc6d6d5 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -206,7 +206,9 @@ App.prototype.renderNetworkDropdown = function () { left: '2px', top: '36px', }, - innerStyle: {}, + innerStyle: { + padding: '2px 16px 2px 0px', + }, }, [ h( diff --git a/ui/app/components/dropdown.js b/ui/app/components/dropdown.js index e77b4c40c..70ed388f4 100644 --- a/ui/app/components/dropdown.js +++ b/ui/app/components/dropdown.js @@ -7,7 +7,7 @@ const noop = () => {} class Dropdown extends Component { render () { - const { isOpen, onClickOutside, style, children } = this.props + const { isOpen, onClickOutside, style, innerStyle, children } = this.props return h( MenuDroppo, @@ -21,6 +21,7 @@ class Dropdown extends Component { padding: '8px 16px', background: 'rgba(0, 0, 0, 0.8)', boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + ...innerStyle, }, }, [ -- cgit v1.2.3 From 8fc0a025f62722e9cced11e22600e0093e64e4f5 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 26 Jul 2017 14:36:22 -0700 Subject: Set initial scale for mobile. --- ui/app/components/transaction-list.js | 4 +++- ui/app/css/index.css | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'ui/app') diff --git a/ui/app/components/transaction-list.js b/ui/app/components/transaction-list.js index 192931486..69b72614c 100644 --- a/ui/app/components/transaction-list.js +++ b/ui/app/components/transaction-list.js @@ -75,7 +75,9 @@ TransactionList.prototype.render = function () { }, }, [ h('p', { - marginTop: '50px', + style: { + marginTop: '50px', + }, }, 'No transaction history.'), ]), ]), diff --git a/ui/app/css/index.css b/ui/app/css/index.css index 00d4bea93..05bdb33af 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -26,7 +26,7 @@ html, body { } html { - min-height: 400px; + min-height: 500px; } .app-root { -- cgit v1.2.3 From bc65484e1bd15fb6ccc9b94f23c991a170b8d39c Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 26 Jul 2017 15:10:50 -0700 Subject: Remove object spread syntax --- ui/app/components/dropdown.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) (limited to 'ui/app') diff --git a/ui/app/components/dropdown.js b/ui/app/components/dropdown.js index 70ed388f4..759800fd6 100644 --- a/ui/app/components/dropdown.js +++ b/ui/app/components/dropdown.js @@ -2,6 +2,7 @@ const Component = require('react').Component const PropTypes = require('react').PropTypes const h = require('react-hyperscript') const MenuDroppo = require('menu-droppo') +const extend = require('xtend') const noop = () => {} @@ -9,6 +10,13 @@ class Dropdown extends Component { render () { const { isOpen, onClickOutside, style, innerStyle, children } = this.props + const innerStyleDefaults = extend({ + borderRadius: '4px', + padding: '8px 16px', + background: 'rgba(0, 0, 0, 0.8)', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + }, innerStyle) + return h( MenuDroppo, { @@ -16,13 +24,7 @@ class Dropdown extends Component { zIndex: 11, onClickOutside, style, - innerStyle: { - borderRadius: '4px', - padding: '8px 16px', - background: 'rgba(0, 0, 0, 0.8)', - boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', - ...innerStyle, - }, + innerStyle: innerStyleDefaults, }, [ h( -- cgit v1.2.3 From c071591adb7b5190c21769114f3c98fd2a5b5ec8 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 26 Jul 2017 15:30:41 -0700 Subject: Fix custom provider indication --- ui/app/actions.js | 2 +- ui/app/app.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'ui/app') diff --git a/ui/app/actions.js b/ui/app/actions.js index d99291e46..fca34d624 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -719,7 +719,7 @@ function setDefaultRpcTarget (rpcList) { } function setRpcTarget (newRpc) { - log.debug(`background.setRpcTarget`) + log.debug(`background.setRpcTarget: ${newRpc}`) return (dispatch) => { background.setCustomRpc(newRpc, (err, result) => { if (err) { diff --git a/ui/app/app.js b/ui/app/app.js index 4ecc6d6d5..f67335797 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -519,7 +519,7 @@ App.prototype.renderCustomOption = function (provider) { DropdownMenuItem, { key: rpcTarget, - onClick: () => props.dispatch(actions.setCustomRpc(rpcTarget)), + onClick: () => props.dispatch(actions.setRpcTarget(rpcTarget)), closeMenu: () => this.setState({ isNetworkMenuOpen: false }), }, [ @@ -553,24 +553,24 @@ App.prototype.getNetworkName = function () { } App.prototype.renderCommonRpc = function (rpcList, provider) { - const { rpcTarget } = provider const props = this.props + const rpcTarget = provider.rpcTarget return rpcList.map((rpc) => { if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { return null } else { + return h( DropdownMenuItem, { - key: rpc, closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setRpcTarget(rpc)), + onClick: () => props.dispatch(actions.setRpcTarget(rpc)), }, [ h('i.fa.fa-question-circle.fa-lg.menu-icon'), rpc, - h('.check', '✓'), + rpcTarget === rpc ? h('.check', '✓') : null, ] ) } -- cgit v1.2.3 From 21bde25780d4b228a273a75fa3e04cfe96b410e6 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 09:19:03 -0700 Subject: Fix spelling --- ui/app/add-token.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ui/app') diff --git a/ui/app/add-token.js b/ui/app/add-token.js index b303b5c0d..15ef7a852 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -86,7 +86,7 @@ AddTokenScreen.prototype.render = function () { h('div', [ h('span', { style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Token Sybmol'), + }, 'Token Symbol'), ]), h('div', { style: {display: 'flex'} }, [ -- cgit v1.2.3 From 1d9fea86546026aa09f6e040a9b0ef551d5659fa Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 13:57:01 -0700 Subject: Fix info page formatting --- ui/app/info.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) (limited to 'ui/app') diff --git a/ui/app/info.js b/ui/app/info.js index e8470de97..38576b284 100644 --- a/ui/app/info.js +++ b/ui/app/info.js @@ -20,7 +20,11 @@ InfoScreen.prototype.render = function () { const version = global.platform.getVersion() return ( - h('.flex-column.flex-grow', [ + h('.flex-column.flex-grow', { + style: { + maxWidth: '400px', + }, + }, [ // subtitle and nav h('.section-title.flex-row.flex-center', [ @@ -103,6 +107,7 @@ InfoScreen.prototype.render = function () { target: '_blank', }, 'Need Help? Read our FAQ!'), ]), + h('div', [ h('a', { href: 'https://metamask.io/', @@ -120,6 +125,7 @@ InfoScreen.prototype.render = function () { h('div.info', 'Visit our web site'), ]), ]), + h('div.fa.fa-slack', [ h('a.info', { href: 'http://slack.metamask.io', @@ -127,11 +133,13 @@ InfoScreen.prototype.render = function () { }, 'Join the conversation on Slack'), ]), - h('div.fa.fa-twitter', [ - h('a.info', { - href: 'https://twitter.com/metamask_io', - target: '_blank', - }, 'Follow us on Twitter'), + h('div', [ + h('.fa.fa-twitter', [ + h('a.info', { + href: 'https://twitter.com/metamask_io', + target: '_blank', + }, 'Follow us on Twitter'), + ]) ]), h('div.fa.fa-envelope', [ -- cgit v1.2.3 From e73c7b907a111a12bcae3c04da9c3c99931f9e72 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 13:58:17 -0700 Subject: Restore support link --- ui/app/info.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'ui/app') diff --git a/ui/app/info.js b/ui/app/info.js index 38576b284..899841c83 100644 --- a/ui/app/info.js +++ b/ui/app/info.js @@ -101,11 +101,11 @@ InfoScreen.prototype.render = function () { paddingLeft: '30px', }}, [ - h('div.fa.fa-github', [ + h('div.fa.fa-support', [ h('a.info', { - href: 'https://github.com/MetaMask/faq', + href: 'http://metamask.consensyssupport.happyfox.com', target: '_blank', - }, 'Need Help? Read our FAQ!'), + }, 'Visit our Support Center'), ]), h('div', [ @@ -139,7 +139,7 @@ InfoScreen.prototype.render = function () { href: 'https://twitter.com/metamask_io', target: '_blank', }, 'Follow us on Twitter'), - ]) + ]), ]), h('div.fa.fa-envelope', [ -- cgit v1.2.3 From f884477abbf427ac390e682fd6380c6a826f0745 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 14:07:36 -0700 Subject: Remove unused parameter --- ui/app/actions.js | 2 +- ui/app/app.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'ui/app') diff --git a/ui/app/actions.js b/ui/app/actions.js index fca34d624..0a9d347aa 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -706,7 +706,7 @@ function markAccountsFound () { // // default rpc target refers to localhost:8545 in this instance. -function setDefaultRpcTarget (rpcList) { +function setDefaultRpcTarget () { log.debug(`background.setDefaultRpcTarget`) return (dispatch) => { background.setDefaultRpc((err, result) => { diff --git a/ui/app/app.js b/ui/app/app.js index f67335797..aadaa6f03 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -267,7 +267,7 @@ App.prototype.renderNetworkDropdown = function () { DropdownMenuItem, { closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), - onClick: () => props.dispatch(actions.setDefaultRpcTarget(rpcList)), + onClick: () => props.dispatch(actions.setDefaultRpcTarget()), }, [ h('i.fa.fa-question-circle.fa-lg.menu-icon'), -- cgit v1.2.3 From 56496f1ea08c1f4f2786e3d1be11026adf0f5900 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 14:13:53 -0700 Subject: Fix loading indication placement --- ui/app/components/loading.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ui/app') diff --git a/ui/app/components/loading.js b/ui/app/components/loading.js index 92d17ccd6..933321983 100644 --- a/ui/app/components/loading.js +++ b/ui/app/components/loading.js @@ -14,7 +14,7 @@ LoadingIndicator.prototype.render = function () { const { isLoading, loadingMessage } = this.props return ( - isLoading ? h('div', { + isLoading ? h('.full-flex-height', { style: { zIndex: 10, position: 'absolute', -- cgit v1.2.3 From 9a1cf2a0d424c54c760fd18644a2fad7c582959a Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 14:15:56 -0700 Subject: Ensure loading indication is full screen --- ui/app/components/loading.js | 1 + 1 file changed, 1 insertion(+) (limited to 'ui/app') diff --git a/ui/app/components/loading.js b/ui/app/components/loading.js index 933321983..163792584 100644 --- a/ui/app/components/loading.js +++ b/ui/app/components/loading.js @@ -16,6 +16,7 @@ LoadingIndicator.prototype.render = function () { return ( isLoading ? h('.full-flex-height', { style: { + left: '0px', zIndex: 10, position: 'absolute', flexDirection: 'column', -- cgit v1.2.3 From 8ba32d5ea8cbd30b85cade9fccaaa6c0f3f5cd04 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 14:58:51 -0700 Subject: Set min gas price to 1 gwei --- ui/app/components/pending-tx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ui/app') diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index d7d602f31..5324ccd64 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -15,7 +15,7 @@ const addressSummary = util.addressSummary const nameForAddress = require('../../lib/contract-namer') const BNInput = require('./bn-as-decimal-input') -const MIN_GAS_PRICE_GWEI_BN = new BN(2) +const MIN_GAS_PRICE_GWEI_BN = new BN(1) const GWEI_FACTOR = new BN(1e9) const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) const MIN_GAS_LIMIT_BN = new BN(21000) -- cgit v1.2.3 From 87c26eb5bc30941a85de98f885d4eb9a752cf9d6 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 18:07:42 -0700 Subject: Correct token pluralization for one token --- ui/app/components/token-list.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'ui/app') diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index 79ec3f351..5ea31ae8d 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -87,7 +87,9 @@ TokenList.prototype.renderTokenStatusBar = function () { const { tokens } = this.state let msg - if (tokens.length > 0) { + if (tokens.length === 1) { + msg = `You own 1 token` + } else if (tokens.length === 1) { msg = `You own ${tokens.length} tokens` } else { msg = `No tokens found` -- cgit v1.2.3 From a25c4f34c0063a0f3d3b1989d9303cb75952d443 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 18:32:32 -0700 Subject: Limit max width of seed word conf screen --- ui/app/keychains/hd/recover-seed/confirmation.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'ui/app') diff --git a/ui/app/keychains/hd/recover-seed/confirmation.js b/ui/app/keychains/hd/recover-seed/confirmation.js index 4ccbec9fc..4335186a5 100644 --- a/ui/app/keychains/hd/recover-seed/confirmation.js +++ b/ui/app/keychains/hd/recover-seed/confirmation.js @@ -23,7 +23,9 @@ RevealSeedConfirmation.prototype.render = function () { return ( - h('.initialize-screen.flex-column.flex-center.flex-grow', [ + h('.initialize-screen.flex-column.flex-center.flex-grow', { + style: { maxWidth: '420px' }, + }, [ h('h3.flex-center.text-transform-uppercase', { style: { @@ -61,7 +63,7 @@ RevealSeedConfirmation.prototype.render = function () { }, }), - h('.flex-row.flex-space-between', { + h('.flex-row.flex-start', { style: { marginTop: 30, width: '50%', @@ -74,6 +76,7 @@ RevealSeedConfirmation.prototype.render = function () { // submit h('button.primary', { + style: { marginLeft: '10px' }, onClick: this.revealSeedWords.bind(this), }, 'OK'), -- cgit v1.2.3 From d1828b6dc98c95a284350f0d22bf4b6be08ebacd Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 18:43:09 -0700 Subject: Fix react warning --- ui/app/app.js | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'ui/app') diff --git a/ui/app/app.js b/ui/app/app.js index aadaa6f03..b251baefd 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -214,6 +214,7 @@ App.prototype.renderNetworkDropdown = function () { h( DropdownMenuItem, { + key: 'main', closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => props.dispatch(actions.setProviderType('mainnet')), }, @@ -227,6 +228,7 @@ App.prototype.renderNetworkDropdown = function () { h( DropdownMenuItem, { + key: 'ropsten', closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => props.dispatch(actions.setProviderType('ropsten')), }, @@ -240,6 +242,7 @@ App.prototype.renderNetworkDropdown = function () { h( DropdownMenuItem, { + key: 'kovan', closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => props.dispatch(actions.setProviderType('kovan')), }, @@ -253,6 +256,7 @@ App.prototype.renderNetworkDropdown = function () { h( DropdownMenuItem, { + key: 'rinkeby', closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => props.dispatch(actions.setProviderType('rinkeby')), }, @@ -266,6 +270,7 @@ App.prototype.renderNetworkDropdown = function () { h( DropdownMenuItem, { + key: 'default', closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => props.dispatch(actions.setDefaultRpcTarget()), }, @@ -564,6 +569,7 @@ App.prototype.renderCommonRpc = function (rpcList, provider) { return h( DropdownMenuItem, { + key: `common${rpc}`, closeMenu: () => this.setState({ isNetworkMenuOpen: false }), onClick: () => props.dispatch(actions.setRpcTarget(rpc)), }, -- cgit v1.2.3 From 24d375aaf1717f1ae743fbe6e83eb50f0e6a4b95 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Fri, 28 Jul 2017 15:31:03 -0700 Subject: Fix dropdown toggle behavior - account dropdowns --- ui/app/components/account-dropdowns.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) (limited to 'ui/app') diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index 61f32f713..2813f4752 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -17,6 +17,8 @@ class AccountDropdowns extends Component { accountSelectorActive: false, optionsMenuActive: false, } + this.accountSelectorToggleClassName = 'fa-angle-down'; + this.optionsMenuToggleClassName = 'fa-ellipsis-h'; } renderAccounts () { @@ -63,7 +65,13 @@ class AccountDropdowns extends Component { maxHeight: '300px', }, isOpen: accountSelectorActive, - onClickOutside: () => { this.setState({ accountSelectorActive: false }) }, + onClickOutside: (event) => { + const { classList } = event.target + const isNotToggleElement = !classList.contains(this.accountSelectorToggleClassName) + if (accountSelectorActive && isNotToggleElement) { + this.setState({ accountSelectorActive: false }) + } + }, }, [ ...this.renderAccounts(), @@ -115,7 +123,13 @@ class AccountDropdowns extends Component { minWidth: '180px', }, isOpen: optionsMenuActive, - onClickOutside: () => { this.setState({ optionsMenuActive: false }) }, + onClickOutside: () => { + const { classList } = event.target + const isNotToggleElement = !classList.contains(this.optionsMenuToggleClassName) + if (optionsMenuActive && isNotToggleElement) { + this.setState({ optionsMenuActive: false }) + } + }, }, [ h( -- cgit v1.2.3 From 34834c108dafd1de75f26a09d78feb8949ef5e56 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Fri, 28 Jul 2017 15:40:26 -0700 Subject: Fix dropdown toggle behavior - settings --- ui/app/app.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'ui/app') diff --git a/ui/app/app.js b/ui/app/app.js index b251baefd..f293e89bd 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -308,7 +308,11 @@ App.prototype.renderDropdown = function () { isOpen: isOpen, zIndex: 11, onClickOutside: (event) => { - this.setState({ isMainMenuOpen: !isOpen }) + const { classList } = event.target + const isNotToggleElement = !classList.contains('sandwich-expando') + if (isNotToggleElement) { + this.setState({ isMainMenuOpen: false }) + } }, style: { position: 'absolute', -- cgit v1.2.3 From 4044b58b5a7133caeefd0f3c0a16478387fe7247 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Fri, 28 Jul 2017 15:55:55 -0700 Subject: Fix dropdown behavior - network --- ui/app/app.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'ui/app') diff --git a/ui/app/app.js b/ui/app/app.js index f293e89bd..8fad0f7d6 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -198,7 +198,17 @@ App.prototype.renderNetworkDropdown = function () { return h(Dropdown, { isOpen, onClickOutside: (event) => { - this.setState({ isNetworkMenuOpen: !isOpen }) + const { classList } = event.target + const isNotToggleElement = [ + classList.contains('menu-icon'), + classList.contains('network-name'), + classList.contains('network-indicator'), + ].filter(bool => bool).length === 0; + // classes from three constituent nodes of the toggle element + + if (isNotToggleElement) { + this.setState({ isNetworkMenuOpen: false }) + } }, zIndex: 11, style: { -- cgit v1.2.3 From 4115c25d8f2e186a575de7904a91b3717da5e800 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 1 Aug 2017 13:21:02 -0700 Subject: lint fix --- ui/app/app.js | 2 +- ui/app/components/account-dropdowns.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'ui/app') diff --git a/ui/app/app.js b/ui/app/app.js index 8fad0f7d6..297a2f621 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -203,7 +203,7 @@ App.prototype.renderNetworkDropdown = function () { classList.contains('menu-icon'), classList.contains('network-name'), classList.contains('network-indicator'), - ].filter(bool => bool).length === 0; + ].filter(bool => bool).length === 0 // classes from three constituent nodes of the toggle element if (isNotToggleElement) { diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index 2813f4752..4ef9a5c14 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -17,8 +17,8 @@ class AccountDropdowns extends Component { accountSelectorActive: false, optionsMenuActive: false, } - this.accountSelectorToggleClassName = 'fa-angle-down'; - this.optionsMenuToggleClassName = 'fa-ellipsis-h'; + this.accountSelectorToggleClassName = 'fa-angle-down' + this.optionsMenuToggleClassName = 'fa-ellipsis-h' } renderAccounts () { -- cgit v1.2.3 From 6176a4b1bf773bfe73d00bec1e4da1f95ffaf7d5 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Thu, 3 Aug 2017 15:32:44 -0700 Subject: Add QR functionality --- ui/app/components/account-dropdowns.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'ui/app') diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index 4ef9a5c14..79e6cff59 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -144,6 +144,18 @@ class AccountDropdowns extends Component { }, 'View account on Etherscan', ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + const { selected, identities } = this.props + var identity = identities[selected] + actions.showQrView(selected, identity ? identity.name : '') + }, + }, + 'Show QR Code', + ), h( DropdownMenuItem, { @@ -226,6 +238,7 @@ const mapDispatchToProps = (dispatch) => { showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)), addNewAccount: () => dispatch(actions.addNewAccount()), showImportPage: () => dispatch(actions.showImportPage()), + showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)), }, } } -- cgit v1.2.3 From ce2e9872cf7470dcf019db742bf5a1011fc1d300 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Thu, 3 Aug 2017 15:51:20 -0700 Subject: Remove 100% properties that were messing up sizing. --- ui/app/css/index.css | 2 -- 1 file changed, 2 deletions(-) (limited to 'ui/app') diff --git a/ui/app/css/index.css b/ui/app/css/index.css index 05bdb33af..e9a70b3a2 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -19,8 +19,6 @@ html, body { font-weight: 300; line-height: 1.4em; background: #F7F7F7; - width: 100%; - height: 100%; margin: 0; padding: 0; } -- cgit v1.2.3 From b4f621c980916bdbcaa03099cbff4e53adc375b4 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Thu, 3 Aug 2017 16:00:44 -0700 Subject: Add maximum width for private key reveal --- ui/app/components/account-export.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ui/app') diff --git a/ui/app/components/account-export.js b/ui/app/components/account-export.js index 394d878f7..330f73805 100644 --- a/ui/app/components/account-export.js +++ b/ui/app/components/account-export.js @@ -100,7 +100,7 @@ ExportAccountView.prototype.render = function () { textOverflow: 'ellipsis', overflow: 'hidden', webkitUserSelect: 'text', - width: '100%', + maxWidth: '275px', }, onClick: function (event) { copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey)) -- cgit v1.2.3 From 77908e1181a8fd043c1e9bdc3807b9dd8a0d3ab7 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Thu, 3 Aug 2017 16:30:57 -0700 Subject: Fix wonky widths for notices. --- ui/app/app.js | 2 +- ui/app/components/notice.js | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) (limited to 'ui/app') diff --git a/ui/app/app.js b/ui/app/app.js index 297a2f621..e23caa72b 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -92,7 +92,7 @@ App.prototype.render = function () { // panel content h('.app-primary' + (transForward ? '.from-right' : '.from-left'), { style: { - maxWidth: '850px', + width: '100%', }, }, [ this.renderPrimary(), diff --git a/ui/app/components/notice.js b/ui/app/components/notice.js index d9f0067cd..636e82ba1 100644 --- a/ui/app/components/notice.js +++ b/ui/app/components/notice.js @@ -19,7 +19,11 @@ Notice.prototype.render = function () { const disabled = state.disclaimerDisabled return ( - h('.flex-column.flex-center.flex-grow', [ + h('.flex-column.flex-center.flex-grow', { + style: { + width: '100%', + }, + }, [ h('h3.flex-center.text-transform-uppercase.terms-header', { style: { background: '#EBEBEB', -- cgit v1.2.3 From 24a13d116ce484a834eea641a75bc4aa626c52c0 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Thu, 3 Aug 2017 16:35:14 -0700 Subject: Fix fox positioning. --- ui/app/unlock.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'ui/app') diff --git a/ui/app/unlock.js b/ui/app/unlock.js index 1aee3c5d0..9bacd5124 100644 --- a/ui/app/unlock.js +++ b/ui/app/unlock.js @@ -26,7 +26,11 @@ UnlockScreen.prototype.render = function () { const state = this.props const warning = state.warning return ( - h('.flex-column', [ + h('.flex-column', { + style: { + width: 'inherit', + }, + }, [ h('.unlock-screen.flex-column.flex-center.flex-grow', [ h(Mascot, { -- cgit v1.2.3 From 67f10d660f1a7f5875253eb0de097e1fab9ad912 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Thu, 3 Aug 2017 17:03:14 -0700 Subject: Adjust account checksumAddress location up --- ui/app/account-detail.js | 1 + 1 file changed, 1 insertion(+) (limited to 'ui/app') diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index 24561c32e..a233c6861 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -145,6 +145,7 @@ AccountDetailScreen.prototype.render = function () { h('div', { style: { + marginTop: '-10px', overflow: 'hidden', textOverflow: 'ellipsis', paddingTop: '3px', -- cgit v1.2.3 From ecb09fcc0aa801f553cd83659c06c429b5cfbf33 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Thu, 3 Aug 2017 17:08:19 -0700 Subject: Disable account selection dropdown from account details --- ui/app/account-detail.js | 3 +-- ui/app/components/account-dropdowns.js | 20 +++++++++++++++----- 2 files changed, 16 insertions(+), 7 deletions(-) (limited to 'ui/app') diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index a233c6861..22a883096 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -128,6 +128,7 @@ AccountDetailScreen.prototype.render = function () { selected, network, identities: props.identities, + enableAccountOptions: true, }, ), ] @@ -145,7 +146,6 @@ AccountDetailScreen.prototype.render = function () { h('div', { style: { - marginTop: '-10px', overflow: 'hidden', textOverflow: 'ellipsis', paddingTop: '3px', @@ -153,7 +153,6 @@ AccountDetailScreen.prototype.render = function () { fontSize: '13px', fontFamily: 'Montserrat Light', textRendering: 'geometricPrecision', - marginTop: '10px', marginBottom: '15px', color: '#AEAEAE', }, diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index 79e6cff59..8d8cb211e 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -183,7 +183,7 @@ class AccountDropdowns extends Component { } render () { - const { style } = this.props + const { style, enableAccountsSelector, enableAccountOptions } = this.props const { optionsMenuActive, accountSelectorActive } = this.state return h( @@ -192,10 +192,12 @@ class AccountDropdowns extends Component { style: style, }, [ - h( + enableAccountsSelector && h( 'i.fa.fa-angle-down', { - style: {}, + style: { + fontSize: '1.8em', + }, onClick: (event) => { event.stopPropagation() this.setState({ @@ -206,10 +208,13 @@ class AccountDropdowns extends Component { }, this.renderAccountSelector(), ), - h( + enableAccountOptions && h( 'i.fa.fa-ellipsis-h', { - style: { 'marginLeft': '10px'}, + style: { + marginRight: '0.5em', + fontSize: '1.8em', + }, onClick: (event) => { event.stopPropagation() this.setState({ @@ -225,6 +230,11 @@ class AccountDropdowns extends Component { } } +AccountDropdowns.defaultProps = { + enableAccountsSelector: false, + enableAccountOptions: false, +} + AccountDropdowns.propTypes = { identities: PropTypes.objectOf(PropTypes.object), selected: PropTypes.string, -- cgit v1.2.3 From 850e9b63d297c9da2403bb3796445df3cd500600 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Thu, 3 Aug 2017 17:17:00 -0700 Subject: Move accountselector menu-droppo up to app-header --- ui/app/app.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) (limited to 'ui/app') diff --git a/ui/app/app.js b/ui/app/app.js index e23caa72b..bafc13d32 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -30,6 +30,8 @@ const QrView = require('./components/qr-code') const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete') const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation') +const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns +const ethUtil = require('ethereumjs-util') module.exports = connect(mapStateToProps)(App) @@ -37,6 +39,14 @@ inherits(App, Component) function App () { Component.call(this) } function mapStateToProps (state) { + const { + identities, + accounts, + address, + } = state.metamask + let selected = address || Object.keys(accounts)[0] + // let checksumAddress = selected && ethUtil.toChecksumAddress(selected) + return { // state from plugin isLoading: state.appState.isLoading, @@ -57,6 +67,10 @@ function mapStateToProps (state) { lastUnreadNotice: state.metamask.lastUnreadNotice, lostAccounts: state.metamask.lostAccounts, frequentRpcList: state.metamask.frequentRpcList || [], + + // state needed to get account dropdown temporarily rendering from app bar + identities, + selected, } } @@ -169,6 +183,14 @@ App.prototype.renderAppBar = function () { }, }, [ + props.isUnlocked && h(AccountDropdowns, { + style: {}, + enableAccountsSelector: true, + identities: this.props.identities, + selected: this.props.selected, + network: this.props.network, + }, []), + // hamburger props.isUnlocked && h(SandwichExpando, { width: 16, -- cgit v1.2.3 From aff213e845dcea49946f9a26d4b44a35082cd5f3 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Thu, 3 Aug 2017 17:49:23 -0700 Subject: Position account switcher icon back in app header --- ui/app/components/account-dropdowns.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'ui/app') diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index 8d8cb211e..906b3315b 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -193,10 +193,14 @@ class AccountDropdowns extends Component { }, [ enableAccountsSelector && h( - 'i.fa.fa-angle-down', + 'div.cursor-pointer.color-orange', { style: { - fontSize: '1.8em', + background: 'url(images/switch_acc.svg) white center center no-repeat', + height: '25px', + width: '25px', + transform: 'scale(0.75)', + marginRight: '3px', }, onClick: (event) => { event.stopPropagation() -- cgit v1.2.3 From 4d6a289629fdfb404251386564720a3a586fe79a Mon Sep 17 00:00:00 2001 From: sdtsui Date: Thu, 3 Aug 2017 17:50:00 -0700 Subject: Add note with previous fa-angle-down for future refactor --- ui/app/components/account-dropdowns.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'ui/app') diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index 906b3315b..33daecd38 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -193,9 +193,11 @@ class AccountDropdowns extends Component { }, [ enableAccountsSelector && h( + // 'i.fa.fa-angle-down', 'div.cursor-pointer.color-orange', { style: { + // fontSize: '1.8em', background: 'url(images/switch_acc.svg) white center center no-repeat', height: '25px', width: '25px', -- cgit v1.2.3 From 10d3a519c881763d4b79876bb1fde6c8243c3427 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Thu, 3 Aug 2017 18:35:10 -0700 Subject: De-dupe click handler for sandwich-expando --- ui/app/app.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) (limited to 'ui/app') diff --git a/ui/app/app.js b/ui/app/app.js index bafc13d32..fda48f41d 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -193,15 +193,17 @@ App.prototype.renderAppBar = function () { // hamburger props.isUnlocked && h(SandwichExpando, { + className: 'sandwich-expando', width: 16, barHeight: 2, padding: 0, isOpen: state.isMainMenuOpen, color: 'rgb(247,146,30)', - onClick: (event) => { - event.preventDefault() + onClick: () => { event.stopPropagation() - this.setState({ isMainMenuOpen: !state.isMainMenuOpen }) + this.setState({ + isMainMenuOpen: !state.isMainMenuOpen, + }) }, }), ]), @@ -340,9 +342,13 @@ App.prototype.renderDropdown = function () { isOpen: isOpen, zIndex: 11, onClickOutside: (event) => { - const { classList } = event.target - const isNotToggleElement = !classList.contains('sandwich-expando') - if (isNotToggleElement) { + const classList = event.target.classList + const parentClassList = event.target.parentElement.classList + + const isToggleElement = classList.contains('sandwich-expando') || + parentClassList.contains('sandwich-expando') + + if (isOpen && !isToggleElement) { this.setState({ isMainMenuOpen: false }) } }, -- cgit v1.2.3 From 086441e41c3a06cb062a475b59b542aa4ea6f185 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Thu, 3 Aug 2017 18:50:29 -0700 Subject: Increase size of network dropdown --- ui/app/app.js | 18 ++++++++++++++++++ ui/app/components/dropdown.js | 3 ++- ui/app/css/index.css | 2 +- ui/app/css/lib.css | 9 +++++++-- 4 files changed, 28 insertions(+), 4 deletions(-) (limited to 'ui/app') diff --git a/ui/app/app.js b/ui/app/app.js index fda48f41d..2566a7515 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -251,6 +251,9 @@ App.prototype.renderNetworkDropdown = function () { key: 'main', closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => props.dispatch(actions.setProviderType('mainnet')), + style: { + fontSize: '18px' + }, }, [ h('.menu-icon.diamond'), @@ -265,6 +268,9 @@ App.prototype.renderNetworkDropdown = function () { key: 'ropsten', closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => props.dispatch(actions.setProviderType('ropsten')), + style: { + fontSize: '18px' + }, }, [ h('.menu-icon.red-dot'), @@ -279,6 +285,9 @@ App.prototype.renderNetworkDropdown = function () { key: 'kovan', closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => props.dispatch(actions.setProviderType('kovan')), + style: { + fontSize: '18px' + }, }, [ h('.menu-icon.hollow-diamond'), @@ -293,6 +302,9 @@ App.prototype.renderNetworkDropdown = function () { key: 'rinkeby', closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => props.dispatch(actions.setProviderType('rinkeby')), + style: { + fontSize: '18px' + }, }, [ h('.menu-icon.golden-square'), @@ -307,6 +319,9 @@ App.prototype.renderNetworkDropdown = function () { key: 'default', closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => props.dispatch(actions.setDefaultRpcTarget()), + style: { + fontSize: '18px' + }, }, [ h('i.fa.fa-question-circle.fa-lg.menu-icon'), @@ -323,6 +338,9 @@ App.prototype.renderNetworkDropdown = function () { { closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => this.props.dispatch(actions.showConfigPage()), + style: { + fontSize: '18px' + }, }, [ h('i.fa.fa-question-circle.fa-lg.menu-icon'), diff --git a/ui/app/components/dropdown.js b/ui/app/components/dropdown.js index 759800fd6..993a104ee 100644 --- a/ui/app/components/dropdown.js +++ b/ui/app/components/dropdown.js @@ -54,7 +54,7 @@ Dropdown.propTypes = { class DropdownMenuItem extends Component { render () { - const { onClick, closeMenu, children } = this.props + const { onClick, closeMenu, children, style } = this.props return h( 'li.dropdown-menu-item', @@ -73,6 +73,7 @@ class DropdownMenuItem extends Component { display: 'flex', justifyContent: 'flex-start', alignItems: 'center', + ...style, }, }, children diff --git a/ui/app/css/index.css b/ui/app/css/index.css index e9a70b3a2..49b432a1f 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -201,7 +201,7 @@ textarea.twelve-word-phrase { } .check { - margin-left: 7px; + margin-left: 12px; color: #F7861C; flex: 1 0 auto; display: flex; diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css index 98570859a..6fff4f56a 100644 --- a/ui/app/css/lib.css +++ b/ui/app/css/lib.css @@ -238,10 +238,15 @@ hr.horizontal-line { .menu-icon { display: inline-block; - height: 9px; - min-width: 9px; + height: 12px; + min-width: 12px; margin: 13px; } + +i.fa.fa-question-circle.fa-lg.menu-icon { + font-size: 18px; +} + .ether-icon { background: rgb(0, 163, 68); border-radius: 20px; -- cgit v1.2.3 From 36d8c3dd3984d37fb72115464f862d89d1559763 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Thu, 3 Aug 2017 18:55:11 -0700 Subject: Increase size of settings dropdown and account settings dropdown --- ui/app/components/account-dropdowns.js | 2 +- ui/app/components/dropdown.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'ui/app') diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index 33daecd38..faad31422 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -119,7 +119,7 @@ class AccountDropdowns extends Component { Dropdown, { style: { - marginLeft: '-162px', + marginLeft: '-215px', minWidth: '180px', }, isOpen: optionsMenuActive, diff --git a/ui/app/components/dropdown.js b/ui/app/components/dropdown.js index 993a104ee..b99e0aa9a 100644 --- a/ui/app/components/dropdown.js +++ b/ui/app/components/dropdown.js @@ -66,7 +66,7 @@ class DropdownMenuItem extends Component { style: { listStyle: 'none', padding: '8px 0px 8px 0px', - fontSize: '12px', + fontSize: '18px', fontStyle: 'normal', fontFamily: 'Montserrat Regular', cursor: 'pointer', -- cgit v1.2.3 From 28fd016d12a73c92a2fa8492de4072520d094f95 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Thu, 3 Aug 2017 19:08:36 -0700 Subject: Increase size of accountSelection dropdown --- ui/app/components/account-dropdowns.js | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) (limited to 'ui/app') diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index faad31422..f0ebd9f25 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -35,17 +35,23 @@ class AccountDropdowns extends Component { onClick: () => { this.props.actions.showAccountDetail(identity.address) }, + style: { + fontSize: '24px', + } }, [ h( Identicon, { address: identity.address, - diameter: 16, + diameter: 32, + style: { + marginLeft: '10px', + }, }, ), - h('span', { style: { marginLeft: '10px' } }, identity.name || ''), - h('span', { style: { marginLeft: '10px' } }, isSelected ? h('.check', '✓') : null), + h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, identity.name || ''), + h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null), ] ) }) @@ -59,10 +65,12 @@ class AccountDropdowns extends Component { Dropdown, { style: { - marginLeft: '-125px', + marginLeft: '-220px', + marginTop: '38px', minWidth: '180px', overflowY: 'auto', maxHeight: '300px', + width: '285px', }, isOpen: accountSelectorActive, onClickOutside: (event) => { @@ -85,10 +93,13 @@ class AccountDropdowns extends Component { h( Identicon, { - diameter: 16, + style: { + marginLeft: '10px' + }, + diameter: 32, }, ), - h('span', { style: { marginLeft: '10px' } }, 'Create Account'), + h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, 'Create Account'), ], ), h( @@ -101,10 +112,13 @@ class AccountDropdowns extends Component { h( Identicon, { - diameter: 16, + style: { + marginLeft: '10px' + }, + diameter: 32, }, ), - h('span', { style: { marginLeft: '10px' } }, 'Import Account'), + h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, 'Import Account'), ] ), ] -- cgit v1.2.3 From 6f14f4b717c7f1d86611994473ff7c33059218c6 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Thu, 3 Aug 2017 19:10:37 -0700 Subject: Allow new accounts selector to handle clicks --- ui/app/components/account-dropdowns.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'ui/app') diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index f0ebd9f25..b4ae9f606 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -17,7 +17,7 @@ class AccountDropdowns extends Component { accountSelectorActive: false, optionsMenuActive: false, } - this.accountSelectorToggleClassName = 'fa-angle-down' + this.accountSelectorToggleClassName = 'accounts-selector' this.optionsMenuToggleClassName = 'fa-ellipsis-h' } @@ -208,7 +208,7 @@ class AccountDropdowns extends Component { [ enableAccountsSelector && h( // 'i.fa.fa-angle-down', - 'div.cursor-pointer.color-orange', + 'div.cursor-pointer.color-orange.accounts-selector', { style: { // fontSize: '1.8em', -- cgit v1.2.3 From baee076348a913529834f7d57239e2ded460aa0a Mon Sep 17 00:00:00 2001 From: sdtsui Date: Thu, 3 Aug 2017 19:17:46 -0700 Subject: Lint ui/app --- ui/app/app.js | 17 +++++++---------- ui/app/components/account-dropdowns.js | 6 +++--- ui/app/components/notice.js | 2 +- 3 files changed, 11 insertions(+), 14 deletions(-) (limited to 'ui/app') diff --git a/ui/app/app.js b/ui/app/app.js index 2566a7515..0592496fc 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -31,7 +31,6 @@ const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete') const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation') const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns -const ethUtil = require('ethereumjs-util') module.exports = connect(mapStateToProps)(App) @@ -44,8 +43,7 @@ function mapStateToProps (state) { accounts, address, } = state.metamask - let selected = address || Object.keys(accounts)[0] - // let checksumAddress = selected && ethUtil.toChecksumAddress(selected) + const selected = address || Object.keys(accounts)[0] return { // state from plugin @@ -252,7 +250,7 @@ App.prototype.renderNetworkDropdown = function () { closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => props.dispatch(actions.setProviderType('mainnet')), style: { - fontSize: '18px' + fontSize: '18px', }, }, [ @@ -269,7 +267,7 @@ App.prototype.renderNetworkDropdown = function () { closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => props.dispatch(actions.setProviderType('ropsten')), style: { - fontSize: '18px' + fontSize: '18px', }, }, [ @@ -286,7 +284,7 @@ App.prototype.renderNetworkDropdown = function () { closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => props.dispatch(actions.setProviderType('kovan')), style: { - fontSize: '18px' + fontSize: '18px', }, }, [ @@ -303,7 +301,7 @@ App.prototype.renderNetworkDropdown = function () { closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => props.dispatch(actions.setProviderType('rinkeby')), style: { - fontSize: '18px' + fontSize: '18px', }, }, [ @@ -320,7 +318,7 @@ App.prototype.renderNetworkDropdown = function () { closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => props.dispatch(actions.setDefaultRpcTarget()), style: { - fontSize: '18px' + fontSize: '18px', }, }, [ @@ -339,7 +337,7 @@ App.prototype.renderNetworkDropdown = function () { closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => this.props.dispatch(actions.showConfigPage()), style: { - fontSize: '18px' + fontSize: '18px', }, }, [ @@ -625,7 +623,6 @@ App.prototype.renderCommonRpc = function (rpcList, provider) { if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { return null } else { - return h( DropdownMenuItem, { diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index b4ae9f606..fc96ce6c5 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -37,7 +37,7 @@ class AccountDropdowns extends Component { }, style: { fontSize: '24px', - } + }, }, [ h( @@ -94,7 +94,7 @@ class AccountDropdowns extends Component { Identicon, { style: { - marginLeft: '10px' + marginLeft: '10px', }, diameter: 32, }, @@ -113,7 +113,7 @@ class AccountDropdowns extends Component { Identicon, { style: { - marginLeft: '10px' + marginLeft: '10px', }, diameter: 32, }, diff --git a/ui/app/components/notice.js b/ui/app/components/notice.js index 636e82ba1..c26505193 100644 --- a/ui/app/components/notice.js +++ b/ui/app/components/notice.js @@ -21,7 +21,7 @@ Notice.prototype.render = function () { return ( h('.flex-column.flex-center.flex-grow', { style: { - width: '100%', + width: '100%', }, }, [ h('h3.flex-center.text-transform-uppercase.terms-header', { -- cgit v1.2.3 From 5ddb40f60cfd300f97aba82158fa239d1c80f9bc Mon Sep 17 00:00:00 2001 From: sdtsui Date: Thu, 3 Aug 2017 19:23:22 -0700 Subject: Adjust top and bottom padding of accountSwitcher --- ui/app/components/account-dropdowns.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'ui/app') diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index fc96ce6c5..b61a4996a 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -24,7 +24,7 @@ class AccountDropdowns extends Component { renderAccounts () { const { identities, selected } = this.props - return Object.keys(identities).map((key) => { + return Object.keys(identities).map((key, index) => { const identity = identities[key] const isSelected = identity.address === selected @@ -36,6 +36,7 @@ class AccountDropdowns extends Component { this.props.actions.showAccountDetail(identity.address) }, style: { + marginTop: index === 0 ? '10px' : '', fontSize: '24px', }, }, @@ -118,7 +119,13 @@ class AccountDropdowns extends Component { diameter: 32, }, ), - h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, 'Import Account'), + h('span', { + style: { + marginLeft: '20px', + fontSize: '24px', + marginButtom: '20px', + }, + }, 'Import Account'), ] ), ] -- cgit v1.2.3 From 777eb865df2d8dae15eb34589bdd47817aab8486 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Thu, 3 Aug 2017 20:02:49 -0700 Subject: Adjust padding of accountSwitcher dropdown --- ui/app/components/account-dropdowns.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'ui/app') diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index b61a4996a..da92619e1 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -36,7 +36,7 @@ class AccountDropdowns extends Component { this.props.actions.showAccountDetail(identity.address) }, style: { - marginTop: index === 0 ? '10px' : '', + marginTop: index === 0 ? '5px' : '', fontSize: '24px', }, }, @@ -66,12 +66,15 @@ class AccountDropdowns extends Component { Dropdown, { style: { - marginLeft: '-220px', + marginLeft: '-238px', marginTop: '38px', minWidth: '180px', overflowY: 'auto', maxHeight: '300px', - width: '285px', + width: '300px', + }, + innerStyle: { + padding: '8px 25px', }, isOpen: accountSelectorActive, onClickOutside: (event) => { @@ -123,7 +126,7 @@ class AccountDropdowns extends Component { style: { marginLeft: '20px', fontSize: '24px', - marginButtom: '20px', + marginBottom: '5px', }, }, 'Import Account'), ] -- cgit v1.2.3 From 29dcadc346fec8c2ea66c84c72d6c65bc565162f Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 3 Aug 2017 21:32:08 -0700 Subject: ui - dropdown - use Object.assign syntax to appease coverage parser --- ui/app/components/dropdown.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'ui/app') diff --git a/ui/app/components/dropdown.js b/ui/app/components/dropdown.js index b99e0aa9a..8cdfc13e8 100644 --- a/ui/app/components/dropdown.js +++ b/ui/app/components/dropdown.js @@ -63,7 +63,7 @@ class DropdownMenuItem extends Component { onClick() closeMenu() }, - style: { + style: Object.assign({ listStyle: 'none', padding: '8px 0px 8px 0px', fontSize: '18px', @@ -73,8 +73,7 @@ class DropdownMenuItem extends Component { display: 'flex', justifyContent: 'flex-start', alignItems: 'center', - ...style, - }, + }, style), }, children ) -- cgit v1.2.3 From 781ac00eac5d947b2c88159d38267386992a05f2 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Fri, 4 Aug 2017 11:31:07 -0700 Subject: Re-enable css transitions for dropdowns in header, needs menu-droppo library update --- ui/app/app.js | 2 ++ ui/app/components/account-dropdowns.js | 1 + ui/app/components/dropdown.js | 4 +++- 3 files changed, 6 insertions(+), 1 deletion(-) (limited to 'ui/app') diff --git a/ui/app/app.js b/ui/app/app.js index 0592496fc..620b4617a 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -218,6 +218,7 @@ App.prototype.renderNetworkDropdown = function () { const isOpen = state.isNetworkMenuOpen return h(Dropdown, { + useCssTransition: true, isOpen, onClickOutside: (event) => { const { classList } = event.target @@ -355,6 +356,7 @@ App.prototype.renderDropdown = function () { const isOpen = state.isMainMenuOpen return h(Dropdown, { + useCssTransition: true, isOpen: isOpen, zIndex: 11, onClickOutside: (event) => { diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index da92619e1..b23600e9b 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -65,6 +65,7 @@ class AccountDropdowns extends Component { return h( Dropdown, { + useCssTransition: true, // Hardcoded because account selector is temporarily in app-header style: { marginLeft: '-238px', marginTop: '38px', diff --git a/ui/app/components/dropdown.js b/ui/app/components/dropdown.js index 8cdfc13e8..ef1a274c3 100644 --- a/ui/app/components/dropdown.js +++ b/ui/app/components/dropdown.js @@ -8,7 +8,7 @@ const noop = () => {} class Dropdown extends Component { render () { - const { isOpen, onClickOutside, style, innerStyle, children } = this.props + const { isOpen, onClickOutside, style, innerStyle, children, useCssTransition } = this.props const innerStyleDefaults = extend({ borderRadius: '4px', @@ -20,6 +20,7 @@ class Dropdown extends Component { return h( MenuDroppo, { + useCssTransition, isOpen, zIndex: 11, onClickOutside, @@ -43,6 +44,7 @@ class Dropdown extends Component { Dropdown.defaultProps = { isOpen: false, onClick: noop, + useCssTransition: false, } Dropdown.propTypes = { -- cgit v1.2.3 From 4d967ebea99b8c1a899a91e379a1fa12015b7e53 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Fri, 4 Aug 2017 11:47:36 -0700 Subject: Bring menu-droppo component into project, remove as a dependency --- ui/app/components/dropdown.js | 2 +- ui/app/components/menu-droppo.js | 137 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 ui/app/components/menu-droppo.js (limited to 'ui/app') diff --git a/ui/app/components/dropdown.js b/ui/app/components/dropdown.js index ef1a274c3..34c7149ee 100644 --- a/ui/app/components/dropdown.js +++ b/ui/app/components/dropdown.js @@ -1,7 +1,7 @@ const Component = require('react').Component const PropTypes = require('react').PropTypes const h = require('react-hyperscript') -const MenuDroppo = require('menu-droppo') +const MenuDroppo = require('./menu-droppo') const extend = require('xtend') const noop = () => {} diff --git a/ui/app/components/menu-droppo.js b/ui/app/components/menu-droppo.js new file mode 100644 index 000000000..a9370a7c8 --- /dev/null +++ b/ui/app/components/menu-droppo.js @@ -0,0 +1,137 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const findDOMNode = require('react-dom').findDOMNode +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') + +module.exports = MenuDroppoComponent + + +inherits(MenuDroppoComponent, Component) +function MenuDroppoComponent() { + Component.call(this) +} + +MenuDroppoComponent.prototype.render = function() { + + const speed = this.props.speed || '300ms' + const useCssTransition = this.props.useCssTransition + const zIndex = ('zIndex' in this.props) ? this.props.zIndex : 0 + + this.manageListeners() + + let style = this.props.style || {} + if (!('position' in style)) { + style.position = 'fixed' + } + style.zIndex = zIndex + + return ( + h('.menu-droppo-container', { + style, + }, [ + h('style', ` + .menu-droppo-enter { + transition: transform ${speed} ease-in-out; + transform: translateY(-200%); + } + + .menu-droppo-enter.menu-droppo-enter-active { + transition: transform ${speed} ease-in-out; + transform: translateY(0%); + } + + .menu-droppo-leave { + transition: transform ${speed} ease-in-out; + transform: translateY(0%); + } + + .menu-droppo-leave.menu-droppo-leave-active { + transition: transform ${speed} ease-in-out; + transform: translateY(-200%); + } + `), + + !!useCssTransition + ? h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'menu-droppo', + transitionEnterTimeout: parseInt(speed), + transitionLeaveTimeout: parseInt(speed), + }, this.renderPrimary()) + : this.renderPrimary() + ]) + ) +} + +MenuDroppoComponent.prototype.renderPrimary = function() { + const isOpen = this.props.isOpen + if (!isOpen) { + return null + return h('span', { + key: 'menu-droppo-null', + style: { + height: '0px', + } + }) + } + + let innerStyle = this.props.innerStyle || {} + + return ( + h('.menu-droppo', { + key: 'menu-droppo-drawer', + style: innerStyle, + }, + [ this.props.children ]) + ) +} + +MenuDroppoComponent.prototype.manageListeners = function() { + const isOpen = this.props.isOpen + const onClickOutside = this.props.onClickOutside + + if (isOpen) { + this.outsideClickHandler = onClickOutside + } else if (isOpen) { + this.outsideClickHandler = null + } +} + +MenuDroppoComponent.prototype.componentDidMount = function() { + if (this && document.body) { + this.globalClickHandler = this.globalClickOccurred.bind(this); + document.body.addEventListener('click', this.globalClickHandler) + var container = findDOMNode(this) + this.container = container + } +} + +MenuDroppoComponent.prototype.componentWillUnmount = function() { + if (this && document.body) { + document.body.removeEventListener('click', this.globalClickHandler) + } +} + +MenuDroppoComponent.prototype.globalClickOccurred = function(event) { + const target = event.target + const container = findDOMNode(this) + const isOpen = this.props.isOpen + + if (target !== container && + !isDescendant(this.container, event.target) && + this.outsideClickHandler) { + this.outsideClickHandler(event) + } +} + +function isDescendant(parent, child) { + var node = child.parentNode; + while (node != null) { + if (node == parent) { + return true; + } + node = node.parentNode; + } + return false; +} -- cgit v1.2.3 From 0cd72db2d2aa575420befa687156cf2bd271b6dd Mon Sep 17 00:00:00 2001 From: sdtsui Date: Fri, 4 Aug 2017 12:57:16 -0700 Subject: Adds early breakpoint from @frankiebee 's + @kumavis 's CR --- ui/app/css/lib.css | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'ui/app') diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css index 6fff4f56a..e7b3bab05 100644 --- a/ui/app/css/lib.css +++ b/ui/app/css/lib.css @@ -276,3 +276,27 @@ i.fa.fa-question-circle.fa-lg.menu-icon { color: red; } +// Account Subsection + Tab Section Breakpoints Hack: +// Resolves issue from @frankiebee in: +// https://github.com/MetaMask/metamask-extension/pull/1835 +// Please remove this when integrating new designs +@media screen and (min-width: 575px) and (max-width: 800px) { + .account-data-subsection { + flex: 0 0 auto !important; // needs to remove default flex settings + width: 40%; + margin-left: 10px !important; + margin-right: 10px !important; + } + + .tabSection { + flex: 0 0 auto !important; // needs to remove default flex settings + width: 49%; + min-width: 305px; // min-width needs to be low for an early break + margin-left: 10px !important; + margin-right: 10px !important; + } + + .name-label { + width: 80%; // repositions dropdown after early break + } +} -- cgit v1.2.3 From 8cc3ae5988d652b73217e80df93a685d7e5a8422 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Fri, 4 Aug 2017 13:19:06 -0700 Subject: Cleanup breakpoint css + comment on hackiness --- ui/app/css/lib.css | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) (limited to 'ui/app') diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css index e7b3bab05..81647d1c1 100644 --- a/ui/app/css/lib.css +++ b/ui/app/css/lib.css @@ -276,27 +276,30 @@ i.fa.fa-question-circle.fa-lg.menu-icon { color: red; } -// Account Subsection + Tab Section Breakpoints Hack: -// Resolves issue from @frankiebee in: -// https://github.com/MetaMask/metamask-extension/pull/1835 -// Please remove this when integrating new designs +/* + Hacky breakpoint fix for account + tab sections + Resolves issue from @frankiebee in + https://github.com/MetaMask/metamask-extension/pull/1835 + Please remove this when integrating new designs + */ + @media screen and (min-width: 575px) and (max-width: 800px) { .account-data-subsection { - flex: 0 0 auto !important; // needs to remove default flex settings - width: 40%; - margin-left: 10px !important; + flex: 0 0 auto !important; // reset flex + margin-left: 10px !important; // create additional horizontal space margin-right: 10px !important; + width: 40%; } .tabSection { - flex: 0 0 auto !important; // needs to remove default flex settings - width: 49%; - min-width: 305px; // min-width needs to be low for an early break + flex: 0 0 auto !important; margin-left: 10px !important; margin-right: 10px !important; + min-width: 285px; + width: 49%; } .name-label { - width: 80%; // repositions dropdown after early break + width: 80%; } } -- cgit v1.2.3 From 186bcec4fb5edede504d6aa56b4f67426a6056ec Mon Sep 17 00:00:00 2001 From: sdtsui Date: Fri, 4 Aug 2017 13:31:18 -0700 Subject: Lint menu-droppo --- ui/app/components/menu-droppo.js | 55 ++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 31 deletions(-) (limited to 'ui/app') diff --git a/ui/app/components/menu-droppo.js b/ui/app/components/menu-droppo.js index a9370a7c8..66ab18954 100644 --- a/ui/app/components/menu-droppo.js +++ b/ui/app/components/menu-droppo.js @@ -8,12 +8,11 @@ module.exports = MenuDroppoComponent inherits(MenuDroppoComponent, Component) -function MenuDroppoComponent() { +function MenuDroppoComponent () { Component.call(this) } -MenuDroppoComponent.prototype.render = function() { - +MenuDroppoComponent.prototype.render = function () { const speed = this.props.speed || '300ms' const useCssTransition = this.props.useCssTransition const zIndex = ('zIndex' in this.props) ? this.props.zIndex : 0 @@ -52,31 +51,25 @@ MenuDroppoComponent.prototype.render = function() { } `), - !!useCssTransition + useCssTransition ? h(ReactCSSTransitionGroup, { className: 'css-transition-group', transitionName: 'menu-droppo', transitionEnterTimeout: parseInt(speed), transitionLeaveTimeout: parseInt(speed), }, this.renderPrimary()) - : this.renderPrimary() + : this.renderPrimary(), ]) ) } -MenuDroppoComponent.prototype.renderPrimary = function() { +MenuDroppoComponent.prototype.renderPrimary = function () { const isOpen = this.props.isOpen if (!isOpen) { return null - return h('span', { - key: 'menu-droppo-null', - style: { - height: '0px', - } - }) } - let innerStyle = this.props.innerStyle || {} + const innerStyle = this.props.innerStyle || {} return ( h('.menu-droppo', { @@ -87,7 +80,7 @@ MenuDroppoComponent.prototype.renderPrimary = function() { ) } -MenuDroppoComponent.prototype.manageListeners = function() { +MenuDroppoComponent.prototype.manageListeners = function () { const isOpen = this.props.isOpen const onClickOutside = this.props.onClickOutside @@ -98,40 +91,40 @@ MenuDroppoComponent.prototype.manageListeners = function() { } } -MenuDroppoComponent.prototype.componentDidMount = function() { +MenuDroppoComponent.prototype.componentDidMount = function () { if (this && document.body) { - this.globalClickHandler = this.globalClickOccurred.bind(this); + this.globalClickHandler = this.globalClickOccurred.bind(this) document.body.addEventListener('click', this.globalClickHandler) var container = findDOMNode(this) this.container = container } } -MenuDroppoComponent.prototype.componentWillUnmount = function() { +MenuDroppoComponent.prototype.componentWillUnmount = function () { if (this && document.body) { document.body.removeEventListener('click', this.globalClickHandler) } } -MenuDroppoComponent.prototype.globalClickOccurred = function(event) { +MenuDroppoComponent.prototype.globalClickOccurred = function (event) { const target = event.target const container = findDOMNode(this) - const isOpen = this.props.isOpen if (target !== container && - !isDescendant(this.container, event.target) && - this.outsideClickHandler) { - this.outsideClickHandler(event) + !isDescendant(this.container, event.target) && + this.outsideClickHandler) { + this.outsideClickHandler(event) } } -function isDescendant(parent, child) { - var node = child.parentNode; - while (node != null) { - if (node == parent) { - return true; - } - node = node.parentNode; - } - return false; +function isDescendant (parent, child) { + var node = child.parentNode + while (node !== null) { + if (node === parent) { + return true + } + node = node.parentNode + } + + return false } -- cgit v1.2.3 From b5251d22a6da21b918a0bfd98e6e572bc80a58f5 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Fri, 4 Aug 2017 23:34:26 -0700 Subject: Fix integration test failures: unnecessary sandwich-expando event stopPropagation --- ui/app/app.js | 1 - 1 file changed, 1 deletion(-) (limited to 'ui/app') diff --git a/ui/app/app.js b/ui/app/app.js index 620b4617a..4565bdd37 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -198,7 +198,6 @@ App.prototype.renderAppBar = function () { isOpen: state.isMainMenuOpen, color: 'rgb(247,146,30)', onClick: () => { - event.stopPropagation() this.setState({ isMainMenuOpen: !state.isMainMenuOpen, }) -- cgit v1.2.3