aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app
diff options
context:
space:
mode:
authorDan Finlay <dan@danfinlay.com>2017-07-21 03:38:38 +0800
committerDan Finlay <dan@danfinlay.com>2017-07-21 03:38:38 +0800
commit86d367957fe8ac04462f716fe0ba2bfa4e5ff3f6 (patch)
treeb4a6805e5e2a4de48c880d80f4b87d1f3b560a18 /ui/app
parent199587383b022a17d56adcb56d6a99ceba71fec7 (diff)
downloadtangerine-wallet-browser-86d367957fe8ac04462f716fe0ba2bfa4e5ff3f6.tar
tangerine-wallet-browser-86d367957fe8ac04462f716fe0ba2bfa4e5ff3f6.tar.gz
tangerine-wallet-browser-86d367957fe8ac04462f716fe0ba2bfa4e5ff3f6.tar.bz2
tangerine-wallet-browser-86d367957fe8ac04462f716fe0ba2bfa4e5ff3f6.tar.lz
tangerine-wallet-browser-86d367957fe8ac04462f716fe0ba2bfa4e5ff3f6.tar.xz
tangerine-wallet-browser-86d367957fe8ac04462f716fe0ba2bfa4e5ff3f6.tar.zst
tangerine-wallet-browser-86d367957fe8ac04462f716fe0ba2bfa4e5ff3f6.zip
Move responsive ui into its own folder for easier merges
Diffstat (limited to 'ui/app')
-rw-r--r--ui/app/account-detail.js311
-rw-r--r--ui/app/accounts/account-list-item.js91
-rw-r--r--ui/app/accounts/import/index.js100
-rw-r--r--ui/app/accounts/import/json.js100
-rw-r--r--ui/app/accounts/import/private-key.js67
-rw-r--r--ui/app/accounts/import/seed.js30
-rw-r--r--ui/app/accounts/index.js164
-rw-r--r--ui/app/actions.js1031
-rw-r--r--ui/app/add-token.js219
-rw-r--r--ui/app/app.js591
-rw-r--r--ui/app/components/account-export.js122
-rw-r--r--ui/app/components/account-info-link.js41
-rw-r--r--ui/app/components/account-panel.js86
-rw-r--r--ui/app/components/balance.js89
-rw-r--r--ui/app/components/binary-renderer.js46
-rw-r--r--ui/app/components/bn-as-decimal-input.js174
-rw-r--r--ui/app/components/buy-button-subview.js197
-rw-r--r--ui/app/components/coinbase-form.js63
-rw-r--r--ui/app/components/copyButton.js59
-rw-r--r--ui/app/components/copyable.js46
-rw-r--r--ui/app/components/custom-radio-list.js60
-rw-r--r--ui/app/components/drop-menu-item.js59
-rw-r--r--ui/app/components/editable-label.js51
-rw-r--r--ui/app/components/ens-input.js170
-rw-r--r--ui/app/components/eth-balance.js89
-rw-r--r--ui/app/components/fiat-value.js63
-rw-r--r--ui/app/components/hex-as-decimal-input.js154
-rw-r--r--ui/app/components/identicon.js72
-rw-r--r--ui/app/components/loading.js53
-rw-r--r--ui/app/components/mascot.js59
-rw-r--r--ui/app/components/mini-account-panel.js74
-rw-r--r--ui/app/components/network.js124
-rw-r--r--ui/app/components/notice.js126
-rw-r--r--ui/app/components/pending-msg-details.js50
-rw-r--r--ui/app/components/pending-msg.js56
-rw-r--r--ui/app/components/pending-personal-msg-details.js60
-rw-r--r--ui/app/components/pending-personal-msg.js47
-rw-r--r--ui/app/components/pending-tx.js480
-rw-r--r--ui/app/components/qr-code.js79
-rw-r--r--ui/app/components/range-slider.js58
-rw-r--r--ui/app/components/shapeshift-form.js306
-rw-r--r--ui/app/components/shift-list-item.js204
-rw-r--r--ui/app/components/tab-bar.js36
-rw-r--r--ui/app/components/template.js18
-rw-r--r--ui/app/components/token-cell.js72
-rw-r--r--ui/app/components/token-list.js192
-rw-r--r--ui/app/components/tooltip.js22
-rw-r--r--ui/app/components/transaction-list-item-icon.js68
-rw-r--r--ui/app/components/transaction-list-item.js165
-rw-r--r--ui/app/components/transaction-list.js79
-rw-r--r--ui/app/conf-tx.js213
-rw-r--r--ui/app/config.js211
-rw-r--r--ui/app/conversion.json207
-rw-r--r--ui/app/css/debug.css21
-rw-r--r--ui/app/css/fonts.css36
-rw-r--r--ui/app/css/index.css667
-rw-r--r--ui/app/css/lib.css268
-rw-r--r--ui/app/css/reset.css48
-rw-r--r--ui/app/css/transitions.css42
-rw-r--r--ui/app/first-time/init-menu.js179
-rw-r--r--ui/app/img/identicon-tardigrade.pngbin0 -> 141119 bytes
-rw-r--r--ui/app/img/identicon-walrus.pngbin0 -> 388973 bytes
-rw-r--r--ui/app/info.js154
-rw-r--r--ui/app/keychains/hd/create-vault-complete.js78
-rw-r--r--ui/app/keychains/hd/recover-seed/confirmation.js118
-rw-r--r--ui/app/keychains/hd/restore-vault.js152
-rw-r--r--ui/app/new-keychain.js29
-rw-r--r--ui/app/reducers.js52
-rw-r--r--ui/app/reducers/app.js585
-rw-r--r--ui/app/reducers/identities.js15
-rw-r--r--ui/app/reducers/metamask.js137
-rw-r--r--ui/app/root.js22
-rw-r--r--ui/app/send.js288
-rw-r--r--ui/app/settings.js59
-rw-r--r--ui/app/store.js21
-rw-r--r--ui/app/template.js30
-rw-r--r--ui/app/unlock.js118
-rw-r--r--ui/app/util.js217
78 files changed, 10740 insertions, 0 deletions
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
--- /dev/null
+++ b/ui/app/img/identicon-tardigrade.png
Binary files differ
diff --git a/ui/app/img/identicon-walrus.png b/ui/app/img/identicon-walrus.png
new file mode 100644
index 000000000..d58fae912
--- /dev/null
+++ b/ui/app/img/identicon-walrus.png
Binary files 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]+$/))
+}