aboutsummaryrefslogtreecommitdiffstats
path: root/ui
diff options
context:
space:
mode:
Diffstat (limited to 'ui')
-rw-r--r--ui/app/account-detail.js177
-rw-r--r--ui/app/accounts.js108
-rw-r--r--ui/app/actions.js88
-rw-r--r--ui/app/app.js212
-rw-r--r--ui/app/components/account-export.js36
-rw-r--r--ui/app/components/account-panel.js2
-rw-r--r--ui/app/components/drop-menu-item.js31
-rw-r--r--ui/app/components/editable-label.js52
-rw-r--r--ui/app/components/eth-balance.js40
-rw-r--r--ui/app/components/identicon.js55
-rw-r--r--ui/app/components/panel.js22
-rw-r--r--ui/app/components/transaction-list.js178
-rw-r--r--ui/app/conf-tx.js3
-rw-r--r--ui/app/css/fonts.css46
-rw-r--r--ui/app/css/index.css297
-rw-r--r--ui/app/css/lib.css53
-rw-r--r--ui/app/css/transitions.css48
-rw-r--r--ui/app/first-time/init-menu.js38
-rw-r--r--ui/app/loading.js3
-rw-r--r--ui/app/reducers/app.js30
-rw-r--r--ui/app/reducers/metamask.js25
-rw-r--r--ui/app/send.js209
-rw-r--r--ui/app/unlock.js21
-rw-r--r--ui/app/util.js62
-rw-r--r--ui/design/02a-metamask-AccDetails-OverTransaction.jpgbin0 -> 122075 bytes
-rw-r--r--ui/design/02b-metamask-AccDetails-Send.jpgbin0 -> 110143 bytes
-rw-r--r--ui/design/05-metamask-Menu.jpgbin0 -> 130264 bytes
27 files changed, 1262 insertions, 574 deletions
diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js
index 2775e24fb..bae44ec85 100644
--- a/ui/app/account-detail.js
+++ b/ui/app/account-detail.js
@@ -5,11 +5,15 @@ const h = require('react-hyperscript')
const connect = require('react-redux').connect
const copyToClipboard = require('copy-to-clipboard')
const actions = require('./actions')
+const addressSummary = require('./util').addressSummary
const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
-const AccountPanel = require('./components/account-panel')
+const Identicon = require('./components/identicon')
+const EtherBalance = 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')
module.exports = connect(mapStateToProps)(AccountDetailScreen)
@@ -30,75 +34,131 @@ function AccountDetailScreen() {
}
AccountDetailScreen.prototype.render = function() {
- var state = this.props
- var selected = state.address || Object.keys(state.accounts)[0]
- var identity = state.identities[selected]
- var account = state.accounts[selected]
- var accountDetail = state.accountDetail
- var transactions = state.transactions
+ var props = this.props
+ var selected = props.address || Object.keys(props.accounts)[0]
+ var identity = props.identities[selected]
+ var account = props.accounts[selected]
+ var accountDetail = props.accountDetail
+ var transactions = props.transactions
return (
- h('.account-detail-section.flex-column.flex-grow', {
- style: {
- width: '330px',
- },
- }, [
-
- // subtitle and nav
- h('.section-title.flex-row.flex-center', [
- h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
- onClick: this.navigateToAccounts.bind(this),
- }),
- h('h2.page-subtitle', 'Account Detail'),
- ]),
+ h('.account-detail-section.flex-column.flex-grow', [
- // account summary, with embedded action buttons
- h(AccountPanel, {
- showFullAddress: true,
- identity: identity,
- account: account,
- key: 'accountPanel'
- }),
-
- h('div', {
+ // identicon, label, balance, etc
+ h('.account-data-subsection.flex-column.flex-grow', {
style: {
- display: 'flex',
- }
+ margin: '0 20px',
+ },
}, [
- h('button', {
- onClick: () => {
- copyToClipboard(identity.address)
+ // header - identicon + nav
+ h('.flex-row.flex-space-between', {
+ style: {
+ marginTop: 28,
},
- }, 'COPY ADDR'),
-
- h('button', {
- onClick: () => {
- this.props.dispatch(actions.showSendPage())
+ }, [
+
+ // invisible placeholder for later
+ h('i.fa.fa-users.fa-lg.color-orange', {
+ style: {
+ visibility: 'hidden',
+ },
+ }),
+
+ // large identicon
+ h('.identicon-wrapper.flex-column.flex-center.select-none', [
+ h(Identicon, {
+ diameter: 62,
+ address: selected,
+ }),
+ ]),
+
+ // small accounts nav
+ h('i.fa.fa-users.fa-lg.cursor-pointer.color-orange', {
+ onClick: this.navigateToAccounts.bind(this),
+ }),
+ ]),
+
+ h('.flex-center', {
+ style: {
+ height: '62px',
+ paddingTop: '8px',
+ }
+ }, [
+ h(EditableLabel, {
+ textValue: identity ? identity.name : '',
+ state: {
+ isEditingLabel: false,
+ },
+ saveText: (text) => {
+ props.dispatch(actions.saveAccountLabel(selected, text))
+ },
+ }, [
+
+ // What is shown when not editing:
+ h('h2.font-medium.color-forest', identity && identity.name)
+ ]),
+ ]),
+
+ // address and getter actions
+ h('.flex-row.flex-space-between', {
+ style: {
+ marginBottom: 16,
},
- }, 'SEND'),
+ }, [
+
+ h('div', {
+ style: {
+ lineHeight: '16px',
+ },
+ }, addressSummary(selected)),
+
+ h('i.fa.fa-download.fa-md.cursor-pointer.color-orange', {
+ onClick: () => this.requestAccountExport(selected),
+ }),
+
+ h('i.fa.fa-qrcode.fa-md.cursor-disabled.color-orange', {
+ onClick: () => console.warn('QRCode not implented...'),
+ }),
+
+ h('i.fa.fa-clipboard.fa-md.cursor-pointer.color-orange', {
+ onClick: () => copyToClipboard(ethUtil.toChecksumAddress(selected)),
+ }),
+
+ ]),
+
+ // balance + send
+ h('.flex-row.flex-space-between', [
+
+ h(EtherBalance, {
+ value: account && account.balance,
+ style: {
+ lineHeight: '50px',
+ },
+ }),
+
+ h('button', {
+ onClick: () => this.props.dispatch(actions.showSendPage()),
+ style: {
+ margin: 10,
+ },
+ }, 'SEND ETH'),
+
+ ]),
- h('button', {
- onClick: () => {
- this.requestAccountExport(identity.address)
- },
- }, 'EXPORT'),
]),
+ // subview (tx history, pk export confirm)
h(ReactCSSTransitionGroup, {
- transitionName: "main",
+ className: 'css-transition-group',
+ transitionName: 'main',
transitionEnterTimeout: 300,
transitionLeaveTimeout: 300,
}, [
this.subview(),
]),
- // transaction table
- /*
- h('section.flex-column', [
- h('span', 'your transaction history will go here.'),
- ]),
- */
+
])
)
}
@@ -126,10 +186,15 @@ AccountDetailScreen.prototype.transactionList = function() {
var state = this.props
var transactions = state.transactions
- return transactionList(transactions
- .filter(tx => tx.txParams.from === state.address)
- .filter(tx => tx.txParams.metamaskNetworkId === state.networkVersion)
- .sort((a, b) => b.time - a.time), state.networkVersion)
+ var txsToRender = transactions
+ // only transactions that are from the current address
+ .filter(tx => tx.txParams.from === state.address)
+ // only transactions that are on the current network
+ .filter(tx => tx.txParams.metamaskNetworkId === state.networkVersion)
+ // sort by recency
+ .sort((a, b) => b.time - a.time)
+
+ return transactionList(txsToRender, state.networkVersion)
}
AccountDetailScreen.prototype.navigateToAccounts = function(event){
diff --git a/ui/app/accounts.js b/ui/app/accounts.js
index 16f37dc67..dbf4ee0fa 100644
--- a/ui/app/accounts.js
+++ b/ui/app/accounts.js
@@ -3,9 +3,13 @@ const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const extend = require('xtend')
+const Identicon = require('./components/identicon')
const actions = require('./actions')
-const AccountPanel = require('./components/account-panel')
+const EtherBalance = require('./components/eth-balance')
const valuesFor = require('./util').valuesFor
+const addressSummary = require('./util').addressSummary
+const formatBalance = require('./util').formatBalance
+const findDOMNode = require('react-dom').findDOMNode
module.exports = connect(mapStateToProps)(AccountsScreen)
@@ -17,6 +21,7 @@ function mapStateToProps(state) {
unconfTxs: state.metamask.unconfTxs,
selectedAddress: state.metamask.selectedAddress,
currentDomain: state.appState.currentDomain,
+ scrollToBottom: state.appState.scrollToBottom,
}
}
@@ -33,37 +38,52 @@ AccountsScreen.prototype.render = function() {
var actions = {
onSelect: this.onSelect.bind(this),
onShowDetail: this.onShowDetail.bind(this),
+ revealAccount: this.onRevealAccount.bind(this),
+ goHome: this.goHome.bind(this),
}
return (
- h('.accounts-section.flex-column.flex-grow', [
+ h('.accounts-section.flex-grow', [
// subtitle and nav
- h('.section-title.flex-column.flex-center', [
- h('h2.page-subtitle', 'Accounts'),
+ h('.section-title.flex-center', [
+ h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
+ onClick: actions.goHome,
+ }),
+ h('h2.page-subtitle', 'Select Account'),
]),
- // current domain
- /* AUDIT
- * Temporarily removed
- * since accounts are currently injected
- * regardless of the current domain.
- */
- h('.current-domain-panel.flex-center.font-small', [
- h('span', 'Selected address is visible to all sites you visit.'),
- // h('span', state.currentDomain),
- ]),
+ h('hr.horizontal-line'),
// identity selection
h('section.identity-section.flex-column', {
style: {
- maxHeight: '290px',
+ height: '418px',
overflowY: 'auto',
overflowX: 'hidden',
}
},
- identityList.map(renderAccountPanel)
- ),
+ [
+ identityList.map(renderAccountPanel),
+
+ h('hr.horizontal-line', {key: 'horizontal-line1'}),
+ h('div.footer.hover-white.pointer', {
+ key: 'reveal-account-bar',
+ onClick:() => {
+ actions.revealAccount()
+ },
+ style: {
+ display: 'flex',
+ flex: '1 0 auto',
+ height: '40px',
+ paddint: '10px',
+ justifyContent: 'center',
+ alignItems: 'center',
+ }
+ }, [
+ h('i.fa.fa-chevron-down.fa-lg', {key: ''}),
+ ]),
+ ]),
unconfTxList.length ? (
@@ -77,10 +97,7 @@ AccountsScreen.prototype.render = function() {
) : (
null
),
-
-
])
-
)
function renderAccountPanel(identity){
@@ -94,7 +111,48 @@ AccountsScreen.prototype.render = function() {
isSelected: false,
isFauceting: isFauceting,
})
- return h(AccountPanel, componentState)
+ const selectedClass = isSelected ? '.selected' : ''
+
+ return (
+ h(`.accounts-list-option.flex-row.flex-space-between.pointer.hover-white${selectedClass}`, {
+ key: `account-panel-${identity.address}`,
+ style: {
+ flex: '1 0 auto',
+ },
+ onClick: (event) => actions.onShowDetail(identity.address, event),
+ }, [
+
+ h('.identicon-wrapper.flex-column.flex-center.select-none', [
+ h(Identicon, {
+ address: identity.address
+ }),
+ ]),
+
+ // account address, balance
+ h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [
+
+ h('span', identity.name),
+ h('span.font-small', addressSummary(identity.address)),
+ // h('span.font-small', formatBalance(account.balance)),
+ h(EtherBalance, {
+ value: account.balance,
+ }),
+
+ ]),
+
+ ])
+ )
+ }
+}
+
+// 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
}
}
@@ -114,3 +172,11 @@ AccountsScreen.prototype.onShowDetail = function(address, event){
event.stopPropagation()
this.props.dispatch(actions.showAccountDetail(address))
}
+
+AccountsScreen.prototype.onRevealAccount = function() {
+ this.props.dispatch(actions.revealAccount())
+}
+
+AccountsScreen.prototype.goHome = function() {
+ this.props.dispatch(actions.goHome())
+}
diff --git a/ui/app/actions.js b/ui/app/actions.js
index dbcf3e577..9ff05c460 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -1,6 +1,11 @@
var actions = {
GO_HOME: 'GO_HOME',
goHome: goHome,
+ // menu state
+ TOGGLE_MENU: 'TOGGLE_MENU',
+ toggleMenu: toggleMenu,
+ SET_MENU_STATE: 'SET_MENU_STATE',
+ closeMenu: closeMenu,
// remote state
UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE',
updateMetamaskState: updateMetamaskState,
@@ -43,6 +48,8 @@ var actions = {
SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE',
SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE',
SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE',
+ REVEAL_ACCOUNT: 'REVEAL_ACCOUNT',
+ revealAccount: revealAccount,
// account detail screen
SHOW_SEND_PAGE: 'SHOW_SEND_PAGE',
showSendPage: showSendPage,
@@ -52,6 +59,8 @@ var actions = {
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',
@@ -105,6 +114,21 @@ function goHome() {
}
}
+// menu state
+
+function toggleMenu() {
+ return {
+ type: this.TOGGLE_MENU,
+ }
+}
+
+function closeMenu() {
+ return {
+ type: this.SET_MENU_STATE,
+ value: false,
+ }
+}
+
// async actions
function tryUnlockMetamask(password) {
@@ -114,7 +138,7 @@ function tryUnlockMetamask(password) {
if (err) {
dispatch(this.unlockFailed())
} else {
- dispatch(this.unlockMetamask())
+ dispatch(this.unlockMetamask(selectedAccount))
}
})
}
@@ -133,12 +157,12 @@ function recoverFromSeed(password, seed) {
return (dispatch) => {
// dispatch(this.createNewVaultInProgress())
dispatch(this.showLoadingIndication())
- _accountManager.recoverFromSeed(password, seed, (err, selectedAccount) => {
+ _accountManager.recoverFromSeed(password, seed, (err, metamaskState) => {
dispatch(this.hideLoadingIndication())
if (err) return dispatch(this.displayWarning(err.message))
- dispatch(this.goHome())
- dispatch(this.unlockMetamask())
+ var account = Object.keys(metamaskState.identities)[0]
+ dispatch(this.unlockMetamask(account))
})
}
}
@@ -155,6 +179,19 @@ function setSelectedAddress(address) {
}
}
+function revealAccount() {
+ return (dispatch) => {
+ dispatch(this.showLoadingIndication())
+ _accountManager.revealAccount((err) => {
+ dispatch(this.hideLoadingIndication())
+ if (err) return dispatch(this.displayWarning(err.message))
+ dispatch({
+ type: this.REVEAL_ACCOUNT,
+ })
+ })
+ }
+}
+
function signMsg(msgData) {
return (dispatch) => {
dispatch(this.showLoadingIndication())
@@ -271,9 +308,10 @@ function unlockFailed() {
}
}
-function unlockMetamask() {
+function unlockMetamask(account) {
return {
type: this.UNLOCK_METAMASK,
+ value: account,
}
}
@@ -297,11 +335,13 @@ function lockMetamask() {
function showAccountDetail(address) {
return (dispatch) => {
- _accountManager.setSelectedAddress(address)
-
- dispatch({
- type: this.SHOW_ACCOUNT_DETAIL,
- value: address,
+ dispatch(this.showLoadingIndication())
+ _accountManager.setSelectedAddress(address, (err, address) => {
+ dispatch(this.hideLoadingIndication())
+ dispatch({
+ type: this.SHOW_ACCOUNT_DETAIL,
+ value: address,
+ })
})
}
}
@@ -312,19 +352,19 @@ function backToAccountDetail(address) {
value: address,
}
}
-function clearSeedWordCache() {
+function clearSeedWordCache(account) {
return {
- type: this.CLEAR_SEED_WORD_CACHE
+ type: this.CLEAR_SEED_WORD_CACHE,
+ value: account,
}
}
function confirmSeedWords() {
return (dispatch) => {
dispatch(this.showLoadingIndication())
- _accountManager.clearSeedWordCache((err, accounts) => {
- dispatch(this.clearSeedWordCache())
- console.log('Seed word cache cleared.')
- dispatch(this.showAccountDetail(accounts[0].address))
+ _accountManager.clearSeedWordCache((err, account) => {
+ console.log('Seed word cache cleared. ' + account)
+ dispatch(this.showAccountDetail(account))
})
}
}
@@ -443,6 +483,22 @@ function showPrivateKey(key) {
}
}
+function saveAccountLabel(account, label) {
+ return (dispatch) => {
+ dispatch(this.showLoadingIndication())
+ _accountManager.saveAccountLabel(account, label, (err) => {
+ dispatch(this.hideLoadingIndication())
+ if (err) {
+ return dispatch(this.showWarning(err.message))
+ }
+ dispatch({
+ type: this.SAVE_ACCOUNT_LABEL,
+ value: { account, label },
+ })
+ })
+ }
+}
+
function showSendPage() {
return {
type: this.SHOW_SEND_PAGE,
diff --git a/ui/app/app.js b/ui/app/app.js
index a4ce40881..511012fab 100644
--- a/ui/app/app.js
+++ b/ui/app/app.js
@@ -24,6 +24,9 @@ const ConfigScreen = require('./config')
const InfoScreen = require('./info')
const LoadingIndicator = require('./loading')
const txHelper = require('../lib/tx-helper')
+const SandwichExpando = require('sandwich-expando')
+const MenuDroppo = require('menu-droppo')
+const DropMenuItem = require('./components/drop-menu-item')
module.exports = connect(mapStateToProps)(App)
@@ -42,6 +45,7 @@ function mapStateToProps(state) {
seedWords: state.metamask.seedWords,
unconfTxs: state.metamask.unconfTxs,
unconfMsgs: state.metamask.unconfMsgs,
+ menuOpen: state.appState.menuOpen,
}
}
@@ -50,15 +54,6 @@ App.prototype.render = function() {
var state = this.props
var view = state.currentView.name
var transForward = state.transForward
- var shouldHaveFooter = true
- switch (view) {
- case 'restoreVault':
- shouldHaveFooter = false;
- case 'createVault':
- shouldHaveFooter = false;
- case 'createVaultComplete':
- shouldHaveFooter = false;
- }
return (
@@ -67,16 +62,13 @@ App.prototype.render = function() {
// Windows was showing a vertical scroll bar:
overflow: 'hidden',
}
- },
- [
+ }, [
h(LoadingIndicator),
- // top row
- h('.app-header.flex-column.flex-center', {
- }, [
- h('h1', 'MetaMask'),
- ]),
+ // app bar
+ this.renderAppBar(),
+ this.renderDropdown(),
// panel content
h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), {
@@ -86,7 +78,8 @@ App.prototype.render = function() {
}
}, [
h(ReactCSSTransitionGroup, {
- transitionName: "main",
+ className: 'css-transition-group',
+ transitionName: 'main',
transitionEnterTimeout: 300,
transitionLeaveTimeout: 300,
}, [
@@ -95,71 +88,148 @@ App.prototype.render = function() {
]),
// footer
- h('.app-footer.flex-row.flex-space-around', {
+ // h('.app-footer.flex-row.flex-space-around', {
+ // style: {
+ // display: shouldHaveFooter ? 'flex' : 'none',
+ // alignItems: 'center',
+ // height: '56px',
+ // }
+ // }, [
+
+ // // settings icon
+ // h('i.fa.fa-cog.fa-lg' + (view === 'config' ? '.active' : '.cursor-pointer'), {
+ // style: {
+ // opacity: state.isUnlocked ? '1.0' : '0.0',
+ // transition: 'opacity 200ms ease-in',
+ // //transform: `translateX(${state.isUnlocked ? '0px' : '-100px'})`,
+ // },
+ // onClick: function(ev) {
+ // state.dispatch(actions.showConfigPage())
+ // },
+ // }),
+
+ // // toggle
+ // onOffToggle({
+ // toggleMetamaskActive: this.toggleMetamaskActive.bind(this),
+ // isUnlocked: state.isUnlocked,
+ // }),
+
+ // // help
+ // h('i.fa.fa-question.fa-lg.cursor-pointer', {
+ // style: {
+ // opacity: state.isUnlocked ? '1.0' : '0.0',
+ // },
+ // onClick() { state.dispatch(actions.showInfoPage()) }
+ // }),
+ // ]),
+
+ ])
+ )
+}
+
+App.prototype.renderAppBar = function(){
+ var state = this.props
+
+ return (
+
+ h('div', [
+
+ h('.app-header.flex-row.flex-space-between', {
style: {
- display: shouldHaveFooter ? 'flex' : 'none',
alignItems: 'center',
- height: '56px',
- }
- }, [
+ visibility: state.isUnlocked ? 'visible' : 'none',
+ background: state.isUnlocked ? 'white' : 'none',
+ height: '36px',
+ position: 'relative',
+ zIndex: 1,
+ },
+ }, state.isUnlocked && [
- // settings icon
- h('i.fa.fa-cog.fa-lg' + (view === 'config' ? '.active' : '.cursor-pointer'), {
- style: {
- opacity: state.isUnlocked ? '1.0' : '0.0',
- transition: 'opacity 200ms ease-in',
- //transform: `translateX(${state.isUnlocked ? '0px' : '-100px'})`,
- },
- onClick: function(ev) {
- state.dispatch(actions.showConfigPage())
- },
+ // mini logo
+ h('img', {
+ height: 24,
+ width: 24,
+ src: '/images/icon-128.png',
}),
- // toggle
- onOffToggle({
- toggleMetamaskActive: this.toggleMetamaskActive.bind(this),
- isUnlocked: state.isUnlocked,
- }),
+ // metamask name
+ h('h1', 'MetaMask'),
- // help
- h('i.fa.fa-question.fa-lg.cursor-pointer', {
- style: {
- opacity: state.isUnlocked ? '1.0' : '0.0',
+ // hamburger
+ h(SandwichExpando, {
+ width: 16,
+ barHeight: 2,
+ padding: 0,
+ isOpen: state.menuOpen,
+ color: 'rgb(247,146,30)',
+ onClick: (event) => {
+ event.preventDefault()
+ event.stopPropagation()
+ this.props.dispatch(actions.toggleMenu())
},
- onClick() { state.dispatch(actions.showInfoPage()) }
}),
]),
])
)
}
-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.renderDropdown = function() {
+ const props = this.props
+ return h(MenuDroppo, {
+ isOpen: props.menuOpen,
+ onClickOutside: (event) => {
+ this.props.dispatch(actions.closeMenu())
+ },
+ style: {
+ position: 'fixed',
+ right: 0,
+ zIndex: 0,
+ },
+ 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.props.dispatch(actions.closeMenu()),
+ action:() => this.props.dispatch(actions.showConfigPage()),
+ icon: h('i.fa.fa-gear.fa-lg', { ariaHidden: true }),
+ }),
+
+ h(DropMenuItem, {
+ label: 'Lock Account',
+ closeMenu:() => this.props.dispatch(actions.closeMenu()),
+ action:() => this.props.dispatch(actions.lockMetamask()),
+ icon: h('i.fa.fa-lock.fa-lg', { ariaHidden: true }),
+ }),
+
+ h(DropMenuItem, {
+ label: 'Help',
+ closeMenu:() => this.props.dispatch(actions.closeMenu()),
+ action:() => this.props.dispatch(actions.showInfoPage()),
+ icon: h('i.fa.fa-question.fa-lg', { ariaHidden: true }),
+ }),
+ ])
}
-App.prototype.renderPrimary = function(state){
- var state = this.props
+App.prototype.renderPrimary = function(){
+ var props = this.props
- // If seed words haven't been dismissed yet, show them still.
- /*
- if (state.seedWords) {
+ if (props.seedWords) {
return h(CreateVaultCompleteScreen, {key: 'createVaultComplete'})
}
- */
// show initialize screen
- if (!state.isInitialized) {
+ if (!props.isInitialized) {
// show current view
- switch (state.currentView.name) {
+ switch (props.currentView.name) {
case 'createVault':
return h(CreateVaultScreen, {key: 'createVault'})
@@ -167,6 +237,9 @@ App.prototype.renderPrimary = function(state){
case 'restoreVault':
return h(RestoreVaultScreen, {key: 'restoreVault'})
+ case 'createVaultComplete':
+ return h(CreateVaultCompleteScreen, {key: 'createVaultComplete'})
+
default:
return h(InitializeMenuScreen, {key: 'menuScreenInit'})
@@ -174,15 +247,12 @@ App.prototype.renderPrimary = function(state){
}
// show unlock screen
- if (!state.isUnlocked) {
+ if (!props.isUnlocked) {
return h(UnlockScreen, {key: 'locked'})
}
// show current view
- switch (state.currentView.name) {
-
- case 'createVaultComplete':
- return h(CreateVaultCompleteScreen, {key: 'created-vault'})
+ switch (props.currentView.name) {
case 'accounts':
return h(AccountsScreen, {key: 'accounts'})
@@ -214,6 +284,18 @@ App.prototype.renderPrimary = function(state){
}
}
+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.hasPendingTxs = function() {
var state = this.props
var unconfTxs = state.unconfTxs
diff --git a/ui/app/components/account-export.js b/ui/app/components/account-export.js
index f79a533ba..eab9baf65 100644
--- a/ui/app/components/account-export.js
+++ b/ui/app/components/account-export.js
@@ -1,6 +1,7 @@
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')
module.exports = ExportAccountView
@@ -31,19 +32,28 @@ ExportAccountView.prototype.render = function() {
and you should only do it if you know what you're doing.`
var confirmation = `If you're absolutely sure, type "I understand" below and
submit.`
- return h('div', { key: 'exporting' }, [
- h('p.error', warning),
- h('p', confirmation),
- h('input#exportAccount', {
- onKeyPress: this.onExportKeyPress.bind(this),
- }),
- h('button', {
- onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }),
- }, 'Submit'),
- h('button', {
- onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address))
- }, 'Cancel'),
- ])
+ return (
+
+ h('div', {
+ key: 'exporting',
+ style: {
+ margin: '0 20px',
+ },
+ }, [
+ h('p.error', warning),
+ h('p', confirmation),
+ h('input#exportAccount', {
+ onKeyPress: this.onExportKeyPress.bind(this),
+ }),
+ h('button', {
+ onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }),
+ }, 'Submit'),
+ h('button', {
+ onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address))
+ }, 'Cancel'),
+ ])
+
+ )
}
if (accountExported) {
diff --git a/ui/app/components/account-panel.js b/ui/app/components/account-panel.js
index c1450b516..6bae095d1 100644
--- a/ui/app/components/account-panel.js
+++ b/ui/app/components/account-panel.js
@@ -4,7 +4,7 @@ const Component = require('react').Component
const h = require('react-hyperscript')
const addressSummary = require('../util').addressSummary
const formatBalance = require('../util').formatBalance
-const Identicon = require('identicon.js')
+const Identicon = require('./identicon')
const Panel = require('./panel')
diff --git a/ui/app/components/drop-menu-item.js b/ui/app/components/drop-menu-item.js
new file mode 100644
index 000000000..c8e61278c
--- /dev/null
+++ b/ui/app/components/drop-menu-item.js
@@ -0,0 +1,31 @@
+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: 'Transat Medium',
+ color: 'rgb(125, 128, 130)',
+ cursor: 'pointer',
+ },
+ }, [
+ this.props.icon,
+ this.props.label,
+ ])
+}
diff --git a/ui/app/components/editable-label.js b/ui/app/components/editable-label.js
new file mode 100644
index 000000000..20e24a9c7
--- /dev/null
+++ b/ui/app/components/editable-label.js
@@ -0,0 +1,52 @@
+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
+ let state = this.state
+
+ if (state && state.isEditingLabel) {
+
+ return h('div.editable-label', [
+ h('input', {
+ defaultValue: props.textValue,
+ onKeyPress:(event) => {
+ this.saveIfEnter(event)
+ },
+ }),
+ h('button', {
+ onClick:() => this.saveText(),
+ }, 'Save')
+ ])
+
+ } else {
+ return h('div', {
+ 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
+ this.props.saveText(text)
+ this.setState({ isEditingLabel: false, textLabel: text })
+}
diff --git a/ui/app/components/eth-balance.js b/ui/app/components/eth-balance.js
new file mode 100644
index 000000000..3f88ef2d4
--- /dev/null
+++ b/ui/app/components/eth-balance.js
@@ -0,0 +1,40 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const parseBalance = require('../util').parseBalance
+
+module.exports = EthBalanceComponent
+
+inherits(EthBalanceComponent, Component)
+function EthBalanceComponent() {
+ Component.call(this)
+}
+
+EthBalanceComponent.prototype.render = function() {
+ var state = this.props
+ var parsedAmount = parseBalance(state.value)
+ var beforeDecimal = parsedAmount[0]
+ var afterDecimal = parsedAmount[1]
+ var value = beforeDecimal+(afterDecimal ? '.'+afterDecimal : '')
+ var style = state.style
+
+ return (
+
+ h('.ether-balance', {
+ style: style,
+ }, [
+ h('.ether-balance-amount', {
+ style: {
+ display: 'inline',
+ },
+ }, value),
+ h('.ether-balance-label', {
+ style: {
+ display: 'inline',
+ marginLeft: 6,
+ },
+ }, 'ETH'),
+ ])
+
+ )
+}
diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js
new file mode 100644
index 000000000..ef625cc62
--- /dev/null
+++ b/ui/app/components/identicon.js
@@ -0,0 +1,55 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const jazzicon = require('jazzicon')
+const findDOMNode = require('react-dom').findDOMNode
+
+module.exports = IdenticonComponent
+
+inherits(IdenticonComponent, Component)
+function IdenticonComponent() {
+ Component.call(this)
+
+ this.defaultDiameter = 46
+}
+
+IdenticonComponent.prototype.render = function() {
+ var state = this.props
+ var diameter = state.diameter || this.defaultDiameter
+ return (
+ h('div', {
+ key: 'identicon-' + this.props.address,
+ style: {
+ display: 'inline-block',
+ height: diameter,
+ width: diameter,
+ borderRadius: diameter / 2,
+ overflow: 'hidden',
+ },
+ })
+ )
+}
+
+IdenticonComponent.prototype.componentDidMount = function(){
+ var state = this.props
+ var address = state.address
+
+ if (!address) return
+ var numericRepresentation = jsNumberForAddress(address)
+
+ var container = findDOMNode(this)
+ // jazzicon with hack to fix inline svg error
+ var diameter = state.diameter || this.defaultDiameter
+ var identicon = jazzicon(diameter, numericRepresentation)
+ var identiconSrc = identicon.innerHTML
+ var dataUri = 'data:image/svg+xml;charset=utf-8,'+encodeURIComponent(identiconSrc)
+ var img = document.createElement('img')
+ img.src = dataUri
+ container.appendChild(img)
+}
+
+function jsNumberForAddress(address) {
+ var addr = address.slice(2, 10)
+ var seed = parseInt(addr, 16)
+ return seed
+}
diff --git a/ui/app/components/panel.js b/ui/app/components/panel.js
index 25e6b7f0f..5d72d6068 100644
--- a/ui/app/components/panel.js
+++ b/ui/app/components/panel.js
@@ -2,7 +2,7 @@ const inherits = require('util').inherits
const ethUtil = require('ethereumjs-util')
const Component = require('react').Component
const h = require('react-hyperscript')
-const Identicon = require('identicon.js')
+const Identicon = require('./identicon')
module.exports = Panel
@@ -18,26 +18,22 @@ Panel.prototype.render = function() {
var identity = state.identity || {}
var account = state.account || {}
var isFauceting = state.isFauceting
+ var style = {
+ flex: '1 0 auto',
+ }
- var identicon = new Identicon(state.identiconKey, 46).toString()
- var identiconSrc = `data:image/png;base64,${identicon}`
+ if (state.onClick) style.cursor = 'pointer'
return (
h('.identity-panel.flex-row.flex-space-between', {
- style: {
- flex: '1 0 auto',
- },
+ style,
onClick: state.onClick,
}, [
// account identicon
h('.identicon-wrapper.flex-column.select-none', [
- h('img.identicon', {
- src: identiconSrc,
- style: {
- border: 'none',
- borderRadius: '20px',
- }
+ h(Identicon, {
+ address: state.identiconKey,
}),
h('span.font-small', state.identiconLabel),
]),
@@ -49,7 +45,7 @@ Panel.prototype.render = function() {
return h('.flex-row.flex-space-between', {
key: '' + Math.round(Math.random() * 1000000),
}, [
- h('label.font-small', attr.key),
+ h('label.font-small.no-select', attr.key),
h('span.font-small', attr.value),
])
}),
diff --git a/ui/app/components/transaction-list.js b/ui/app/components/transaction-list.js
index 3e153aecf..f85aab70f 100644
--- a/ui/app/components/transaction-list.js
+++ b/ui/app/components/transaction-list.js
@@ -1,55 +1,159 @@
const h = require('react-hyperscript')
+const vreme = new (require('vreme'))
const formatBalance = require('../util').formatBalance
const addressSummary = require('../util').addressSummary
const explorerLink = require('../../lib/explorer-link')
const Panel = require('./panel')
+const Identicon = require('./identicon')
+const EtherBalance = require('./eth-balance')
+
module.exports = function(transactions, network) {
- return h('section', [
+ 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('.current-domain-panel.flex-center.font-small', [
- h('span', 'Transactions'),
- ]),
+ h('h3.flex-center.text-transform-uppercase', {
+ style: {
+ background: '#EBEBEB',
+ color: '#AEAEAE',
+ },
+ }, [
+ 'Transactions',
+ ]),
- h('.tx-list', {
+ h('.tx-list', {
style: {
overflowY: 'auto',
- height: '180px',
+ height: '204px',
+ padding: '0 20px',
textAlign: 'center',
},
- },
-
- [
+ }, (
- transactions.map((transaction) => {
- console.dir(transaction)
-
- var panelOpts = {
- key: `tx-${transaction.hash}`,
- identiconKey: transaction.txParams.to,
+ transactions.length ?
+ transactions.map(renderTransaction)
+ :
+ [h('.flex-center', {
style: {
- cursor: 'pointer',
- },
- onClick: (event) => {
- var url = explorerLink(transaction.hash, parseInt(network))
- chrome.tabs.create({ url });
+ height: '100%',
},
- attributes: [
- {
- key: 'TO',
- value: addressSummary(transaction.txParams.to),
- },
- {
- key: 'VALUE',
- value: formatBalance(transaction.txParams.value),
- },
- ]
- }
-
- return h(Panel, panelOpts)
- })
- ]
+ }, 'No transaction history...')]
+
+ ))
+
+ ])
+
+ )
+
+
+ function renderTransaction(transaction, i){
+
+ var txParams = transaction.txParams
+ var date = formatDate(transaction.time)
+
+ return (
+
+ h(`.transaction-list-item.flex-row.flex-space-between${transaction.hash ? '.pointer' : ''}`, {
+ key: `tx-${transaction.id + i}`,
+ onClick: (event) => {
+ if (!transaction.hash) return
+ var url = explorerLink(transaction.hash, parseInt(network))
+ chrome.tabs.create({ url })
+ },
+ style: {
+ padding: '20px 0',
+ },
+ }, [
+
+ // large identicon
+ h('.identicon-wrapper.flex-column.flex-center.select-none', [
+ identicon(txParams, transaction),
+ ]),
+
+ h('.flex-column', [
+
+ h('div', date),
+
+ recipientField(txParams, transaction),
+
+ ]),
+
+ h(EtherBalance, {
+ value: txParams.value,
+ }),
+ ])
+
)
+ }
+}
+
+function recipientField(txParams, transaction) {
+ if (txParams.to) {
+ return h('div', {
+ style: {
+ fontSize: 'small',
+ color: '#ABA9AA',
+ },
+ }, [
+ addressSummary(txParams.to),
+ failIfFailed(transaction),
+ ])
+
+ } else {
+
+ return h('div', {
+ style: {
+ fontSize: 'small',
+ color: '#ABA9AA',
+ },
+ },[
+ 'Contract Published',
+ failIfFailed(transaction),
+ ])
+
+ }
+}
+
+function formatDate(date){
+ return vreme.format(new Date(date), 'March 16 2014 14:30')
+}
+
+function identicon(txParams, transaction) {
+ if (transaction.status === 'rejected') {
+ return h('i.fa.fa-exclamation-triangle.fa-lg.error', {
+ 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',
+ }
+ })
+ }
+}
- ])
- }
+function failIfFailed(transaction) {
+ if (transaction.status === 'rejected') {
+ return h('span.error', ' (Failed)')
+ }
+}
diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js
index 8ab79c3b9..9092c85c9 100644
--- a/ui/app/conf-tx.js
+++ b/ui/app/conf-tx.js
@@ -77,7 +77,8 @@ ConfirmTxScreen.prototype.render = function() {
warningIfExists(state.warning),
h(ReactCSSTransitionGroup, {
- transitionName: "main",
+ className: 'css-transition-group',
+ transitionName: 'main',
transitionEnterTimeout: 300,
transitionLeaveTimeout: 300,
}, [
diff --git a/ui/app/css/fonts.css b/ui/app/css/fonts.css
index dd1a755fb..b528cb9ab 100644
--- a/ui/app/css/fonts.css
+++ b/ui/app/css/fonts.css
@@ -1,2 +1,46 @@
@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); \ No newline at end of file
+@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css);
+
+@font-face {
+ font-family: 'Transat Standard';
+ src: url('/fonts/Transat Standard/transat_standard-webfont.eot');
+ src: url('/fonts/Transat Standard/transat_standard-webfont.eot?#iefix') format('embedded-opentype'),
+ url('/fonts/Transat Standard/transat_standard-webfont.woff') format('woff'),
+ url('/fonts/Transat Standard/transat_standard-webfont.ttf') format('truetype'),
+ url('/fonts/Transat Standard/transat_standard-webfont.svg#ywftsvg') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Transat Black';
+ src: url('/fonts/Transat Black/transat_black-webfont.eot');
+ src: url('/fonts/Transat Black/transat_black-webfont.eot?#iefix') format('embedded-opentype'),
+ url('/fonts/Transat Black/transat_black-webfont.woff') format('woff'),
+ url('/fonts/Transat Black/transat_black-webfont.ttf') format('truetype'),
+ url('/fonts/Transat Black/transat_black-webfont.svg#ywftsvg') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Transat Medium';
+ src: url('/fonts/Transat Medium/transat_medium-webfont.eot');
+ src: url('/fonts/Transat Medium/transat_medium-webfont.eot?#iefix') format('embedded-opentype'),
+ url('/fonts/Transat Medium/transat_medium-webfont.woff') format('woff'),
+ url('/fonts/Transat Medium/transat_medium-webfont.ttf') format('truetype'),
+ url('/fonts/Transat Medium/transat_medium-webfont.svg#ywftsvg') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Transat Light';
+ src: url('/fonts/Transat Light/transat_light-webfont.eot');
+ src: url('/fonts/Transat Light/transat_light-webfont.eot?#iefix') format('embedded-opentype'),
+ url('/fonts/Transat Light/transat_light-webfont.woff') format('woff'),
+ url('/fonts/Transat Light/transat_light-webfont.ttf') format('truetype'),
+ url('/fonts/Transat Light/transat_light-webfont.svg#ywftsvg') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
diff --git a/ui/app/css/index.css b/ui/app/css/index.css
index 4871a650f..d6d1f91ac 100644
--- a/ui/app/css/index.css
+++ b/ui/app/css/index.css
@@ -14,11 +14,15 @@ application specific styles
}
html, body {
- /*font-family: 'Open Sans', Arial, sans-serif;*/
- font-family: 'Roboto', 'Noto', sans-serif;
+ font-family: 'Transat Standard', Arial;
color: #4D4D4D;
font-weight: 300;
line-height: 1.4em;
+ background: #F7F7F7;
+}
+
+input:focus, textarea:focus {
+ outline: none;
}
#app-content {
@@ -29,18 +33,18 @@ html, body {
}
button {
+ font-family: 'Transat Black';
outline: none;
cursor: pointer;
- margin: 10px;
- padding: 6px;
+ /*margin: 10px;*/
+ padding: 8px 12px;
border: none;
- border-radius: 3px;
background: #F7861C;
- font-weight: 500;
color: white;
transform-origin: center center;
transition: transform 50ms ease-in;
}
+
button:hover {
transform: scale(1.1);
}
@@ -48,26 +52,6 @@ button:active {
transform: scale(0.95);
}
-button.primary {
- margin: 10px;
- padding: 6px;
- border: none;
- border-radius: 3px;
- background: #F7861C;
- font-weight: 500;
- color: white;
-}
-
-input, textarea {
- width: 300px;
- padding: 6px;
- border-radius: 6px;
- border-style: solid;
- outline: none;
- border: 1px solid #F5A623;
- background: #FAF6F0;
-}
-
a {
text-decoration: none;
color: inherit;
@@ -85,6 +69,16 @@ app
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: 'Transat Standard';
+ text-transform: uppercase;
+}
+
button.btn-thin {
border: 1px solid;
border-color: #4D4D4D;
@@ -98,23 +92,25 @@ button.btn-thin {
}
.app-header {
- padding-top: 20px;
+ padding: 6px 8px;
}
.app-header h1 {
- font-size: 2em;
- font-weight: 300;
- height: 42px;
+ font-family: 'Transat Medium';
+ text-transform: uppercase;
+ color: #AEAEAE;
}
h2.page-subtitle {
+ font-family: 'Transat Light';
+ text-transform: uppercase;
+ color: #AEAEAE;
font-size: 1em;
- font-weight: 500;
- height: 24px;
- color: #F3C83E;
+ margin: 12px;
}
.app-primary {
+
}
.app-footer {
@@ -216,33 +212,70 @@ app sections
margin: -2px 8px 0px -8px;
}
-.unlock-screen label {
- color: #F3C83E;
- font-weight: 500;
+.unlock-screen #metamask-mascot-container {
+ margin-top: 24px;
+}
+
+.unlock-screen h1 {
+ margin-top: -28px;
+ margin-bottom: 42px;
}
.unlock-screen input[type=password] {
- width: 60%;
- height: 22px;
- padding: 2px;
- border-radius: 4px;
- border: 2px solid #F3C83E;
- background: #FAF6F0;
+ width: 260px;
+ /*height: 36px;
+ margin-bottom: 24px;
+ padding: 8px;*/
}
-.unlock-screen input[type=password]:focus {
- outline: none;
- border: 3px solid #F3C83E;
+/* 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;
+}
+
+
+
/* accounts */
.accounts-section {
- margin: 0 20px;
+ margin: 0 0px;
+}
+
+.accounts-section .horizontal-line {
+ margin: 0px 18px;
}
-.current-domain-panel {
- border: 1px solid #B7B7B7;
+.accounts-list-option {
+ height: 120px;
+}
+
+.accounts-list-option .identicon-wrapper {
+ width: 100px;
}
.unconftx-link {
@@ -289,8 +322,7 @@ app sections
/* accounts screen */
.identity-section {
- border: 2px solid #4D4D4D;
- margin: 0;
+
}
.identity-section .identity-panel {
@@ -298,9 +330,6 @@ app sections
border-bottom: 1px solid #B1B1B1;
cursor: pointer;
}
-.identity-section .identity-panel:hover {
- background: #F9F9F9;
-}
.identity-section .identity-panel.selected {
background: white;
@@ -311,10 +340,15 @@ app sections
border-color: orange;
}
+.identity-section .accounts-list-option:hover,
+.identity-section .accounts-list-option.selected {
+ background:white;
+}
+
/* account detail screen */
.account-detail-section {
- margin: 0 20px;
+
}
/* tx confirm */
@@ -333,157 +367,28 @@ app sections
background: #FAF6F0;
}
+/* Send Screen */
-/*
-react toggle
-*/
-
-/* overrides */
-
-.react-toggle-track-check {
- display: none;
-}
-.react-toggle-track-x {
- display: none;
-}
-
-/* modified original */
-
-.react-toggle {
- display: inline-block;
- position: relative;
- cursor: pointer;
- background-color: transparent;
- border: 0;
- padding: 0;
-
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -khtml-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-
- -webkit-tap-highlight-color: rgba(0,0,0,0);
- -webkit-tap-highlight-color: transparent;
-}
-
-.react-toggle-screenreader-only {
- border: 0;
- clip: rect(0 0 0 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- width: 1px;
-}
-
-.react-toggle--disabled {
- opacity: 0.5;
- -webkit-transition: opacity 0.25s;
- transition: opacity 0.25s;
-}
-
-.react-toggle-track {
- width: 50px;
- height: 24px;
- padding: 0;
- border-radius: 30px;
- background-color: #4D4D4D;
- -webkit-transition: all 0.2s ease;
- -moz-transition: all 0.2s ease;
- transition: all 0.2s ease;
-}
-
-.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track {
- background-color: #000000;
-}
-
-.react-toggle--checked .react-toggle-track {
- background-color: rgb(255, 174, 41);
-}
-
-.react-toggle.react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track {
- background-color: rgb(243, 151, 0);
-}
+.send-screen {
-.react-toggle-track-check {
- position: absolute;
- width: 14px;
- height: 10px;
- top: 0px;
- bottom: 0px;
- margin-top: auto;
- margin-bottom: auto;
- line-height: 0;
- left: 8px;
- opacity: 0;
- -webkit-transition: opacity 0.25s ease;
- -moz-transition: opacity 0.25s ease;
- transition: opacity 0.25s ease;
}
-.react-toggle--checked .react-toggle-track-check {
- opacity: 1;
- -webkit-transition: opacity 0.25s ease;
- -moz-transition: opacity 0.25s ease;
- transition: opacity 0.25s ease;
-}
-
-.react-toggle-track-x {
- position: absolute;
- width: 10px;
- height: 10px;
- top: 0px;
- bottom: 0px;
- margin-top: auto;
- margin-bottom: auto;
- line-height: 0;
- right: 10px;
- opacity: 1;
- -webkit-transition: opacity 0.25s ease;
- -moz-transition: opacity 0.25s ease;
- transition: opacity 0.25s ease;
+.send-screen section {
+ margin: 8px 16px;
}
-.react-toggle--checked .react-toggle-track-x {
- opacity: 0;
+.send-screen input {
+ width: 100%;
+ font-size: 12px;
+ letter-spacing: 0.1em;
}
-.react-toggle-thumb {
- transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms;
- position: absolute;
- top: 1px;
- left: 1px;
- width: 22px;
- height: 22px;
- border: 1px solid #4D4D4D;
- border-radius: 50%;
- background-color: #FAFAFA;
-
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
+/* Ether Balance Widget */
- -webkit-transition: all 0.25s ease;
- -moz-transition: all 0.25s ease;
- transition: all 0.25s ease;
+.ether-balance-amount {
+ color: #F7861C;
}
-.react-toggle--checked .react-toggle-thumb {
- left: 27px;
- border-color: #828282;
+.ether-balance-label {
+ color: #ABA9AA;
}
-/*
- .react-toggle--focus .react-toggle-thumb {
- -webkit-box-shadow: 0px 0px 3px 2px #0099E0;
- -moz-box-shadow: 0px 0px 3px 2px #0099E0;
- box-shadow: 0px 0px 2px 3px #0099E0;
- }
-
- .react-toggle:active .react-toggle-thumb {
- -webkit-box-shadow: 0px 0px 5px 5px #0099E0;
- -moz-box-shadow: 0px 0px 5px 5px #0099E0;
- box-shadow: 0px 0px 5px 5px #0099E0;
- }
diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css
index b6b26402b..d9719b1e3 100644
--- a/ui/app/css/lib.css
+++ b/ui/app/css/lib.css
@@ -1,3 +1,13 @@
+/* color */
+
+.color-orange {
+ color: #F7861C;
+}
+
+.color-forest {
+ color: #0A5448;
+}
+
/* lib */
.full-width {
@@ -47,6 +57,10 @@
flex: none;
}
+.flex-basis-auto {
+ flex-basis: auto;
+}
+
.flex-grow {
flex: 1 1 auto;
}
@@ -86,13 +100,16 @@
}
.select-none {
- cursor: default;
+ 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;
@@ -105,6 +122,10 @@
transform: scale(0.95);
}
+.cursor-disabled {
+ cursor: not-allowed;
+}
+
.margin-bottom-sml {
margin-bottom: 20px;
}
@@ -121,23 +142,27 @@
font-weight: bold;
}
+.text-transform-uppercase {
+ text-transform: uppercase;
+}
+
.font-small {
font-size: 12px;
}
-/* Send Screen */
-.send-screen {
- margin: 0 20px;
+.font-medium {
+ font-size: 1.2em;
}
-.send-screen section {
- margin: 7px;
- display: flex;
- flex-direction: row;
- justify-content: center;
-}
-.send-screen details {
- width: 100%;
+
+hr.horizontal-line {
+ display: block;
+ height: 1px;
+ border: 0;
+ border-top: 1px solid #ccc;
+ margin: 1em 0;
+ padding: 0;
}
-.send-screen section input {
- width: 100%;
+
+.hover-white:hover {
+ background: white;
}
diff --git a/ui/app/css/transitions.css b/ui/app/css/transitions.css
index e2225a98d..393a944f9 100644
--- a/ui/app/css/transitions.css
+++ b/ui/app/css/transitions.css
@@ -1,48 +1,42 @@
-/* initial positions */
-.app-primary.from-right .main-enter {
- transform: translateX(400px);
+/* universal */
+.app-primary .main-enter {
position: absolute;
width: 100%;
- transition: transform 300ms ease-in-out;
-}
-.app-primary.from-left .main-enter {
- transform: translateX(-400px);
- position: absolute;
- width: 100%;
- transition: transform 300ms ease-in-out;
}
/* center position */
-.app-primary .main-enter.main-enter-active,
-.app-primary .main-leave {
- transform: translateX(0px);
- position: absolute;
- width: 100%;
- transition: transform 300ms ease-in-out;
+.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;
}
-/* final positions */
+/* exited positions */
.app-primary.from-left .main-leave-active {
- transform: translateX(400px);
- position: absolute;
- width: 100%;
- transition: transform 300ms ease-in-out;
+ transform: translateX(360px);
+ transition: transform 300ms ease-in;
}
.app-primary.from-right .main-leave-active {
- transform: translateX(-400px);
- position: absolute;
- width: 100%;
- transition: transform 300ms ease-in-out;
+ transform: translateX(-360px);
+ transition: transform 300ms ease-in;
}
/* loader transitions */
.loader-enter, .loader-leave-active {
opacity: 0.0;
- transition: opacity 150 ease-in-out;
+ transition: opacity 150 ease-in;
}
.loader-enter-active, .loader-leave {
opacity: 1.0;
- transition: opacity 150 ease-in-out;
+ 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
index 11b01a88b..2d54e7e19 100644
--- a/ui/app/first-time/init-menu.js
+++ b/ui/app/first-time/init-menu.js
@@ -29,15 +29,6 @@ InitializeMenuScreen.prototype.render = function() {
switch (state.currentView.name) {
- case 'createVault':
- return h(CreateVaultScreen)
-
- case 'createVaultComplete':
- return h(CreateVaultCompleteScreen)
-
- case 'restoreVault':
- return this.renderRestoreVault()
-
default:
return this.renderMenu()
@@ -55,12 +46,12 @@ InitializeMenuScreen.prototype.renderMenu = function() {
h('.initialize-screen.flex-column.flex-center.flex-grow', [
- h('h2.page-subtitle', 'Welcome!'),
-
h(Mascot, {
animationEventEmitter: this.animationEventEmitter,
}),
+ h('h2.page-subtitle', 'MetaMask'),
+
h('button.btn-thin', {
onClick: this.showCreateVault.bind(this),
}, 'Create New Vault'),
@@ -80,31 +71,6 @@ InitializeMenuScreen.prototype.renderMenu = function() {
)
}
-InitializeMenuScreen.prototype.renderRestoreVault = function() {
- var state = this.props
- return (
-
- h('.initialize-screen.flex-column.flex-center.flex-grow', [
-
- // subtitle and nav
- h('.section-title.flex-row.flex-center', [
- h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
- onClick: this.showInitializeMenu.bind(this),
- }),
- h('h2.page-subtitle', 'Restore Vault'),
- ]),
-
-
- h('h3', 'Coming soon....'),
- // h('textarea.twelve-word-phrase', {
- // value: 'hey ho what the actual hello rubber duck bumbersnatch crumplezone frankenfurter',
- // }),
-
- ])
-
- )
-}
-
// InitializeMenuScreen.prototype.splitWor = function() {
// this.props.dispatch(actions.showInitializeMenu())
// }
diff --git a/ui/app/loading.js b/ui/app/loading.js
index 9288256de..f6279d5cf 100644
--- a/ui/app/loading.js
+++ b/ui/app/loading.js
@@ -23,7 +23,8 @@ LoadingIndicator.prototype.render = function() {
return (
h(ReactCSSTransitionGroup, {
- transitionName: "loader",
+ className: 'css-transition-group',
+ transitionName: 'loader',
transitionEnterTimeout: 150,
transitionLeaveTimeout: 150,
}, [
diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js
index 309351956..a29a8f79c 100644
--- a/ui/app/reducers/app.js
+++ b/ui/app/reducers/app.js
@@ -22,6 +22,7 @@ function reduceApp(state, action) {
var seedWords = state.metamask.seedWords
var appState = extend({
+ menuOpen: false,
currentView: seedWords ? seedConfView : defaultView,
accountDetail: {
subview: 'transactions',
@@ -34,6 +35,16 @@ function reduceApp(state, action) {
switch (action.type) {
+ case actions.TOGGLE_MENU:
+ return extend(appState, {
+ menuOpen: !appState.menuOpen,
+ })
+
+ case actions.SET_MENU_STATE:
+ return extend(appState, {
+ menuOpen: action.value,
+ })
+
// intialize
case actions.SHOW_CREATE_VAULT:
@@ -154,7 +165,7 @@ function reduceApp(state, action) {
accountExport: 'none',
privateKey: '',
},
- transForward: true,
+ transForward: false,
})
case actions.BACK_TO_ACCOUNT_DETAIL:
@@ -177,9 +188,15 @@ function reduceApp(state, action) {
currentView: {
name: seedWords ? 'createVaultComplete' : 'accounts',
},
- transForward: appState.currentView.name == 'locked',
+ transForward: true,
isLoading: false,
warning: null,
+ scrollToBottom: false,
+ })
+
+ case actions.REVEAL_ACCOUNT:
+ return extend(appState, {
+ scrollToBottom: true,
})
case actions.SHOW_CONF_TX_PAGE:
@@ -278,10 +295,13 @@ function reduceApp(state, action) {
case actions.CLEAR_SEED_WORD_CACHE:
return extend(appState, {
transForward: true,
- currentView: {
- name: 'accounts',
- },
+ currentView: {},
isLoading: false,
+ accountDetail: {
+ subview: 'transactions',
+ accountExport: 'none',
+ privateKey: '',
+ },
})
case actions.DISPLAY_WARNING:
diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js
index 9398f1497..a45327189 100644
--- a/ui/app/reducers/metamask.js
+++ b/ui/app/reducers/metamask.js
@@ -29,6 +29,7 @@ function reduceMetamask(state, action) {
return extend(metamaskState, {
isUnlocked: true,
isInitialized: true,
+ selectedAccount: action.value,
})
case actions.LOCK_METAMASK:
@@ -69,18 +70,38 @@ function reduceMetamask(state, action) {
}
return newState
+ case actions.SHOW_NEW_VAULT_SEED:
+ return extend(metamaskState, {
+ isUnlocked: true,
+ isInitialized: false,
+ })
+
case actions.CLEAR_SEED_WORD_CACHE:
var newState = extend(metamaskState, {
+ isUnlocked: true,
isInitialized: true,
+ selectedAccount: action.value,
})
delete newState.seedWords
return newState
- case actions.CREATE_NEW_VAULT_IN_PROGRESS:
- return extend(metamaskState, {
+ case actions.SHOW_ACCOUNT_DETAIL:
+ const newState = extend(metamaskState, {
isUnlocked: true,
isInitialized: true,
+ selectedAccount: action.value,
+ 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 })
default:
return metamaskState
diff --git a/ui/app/send.js b/ui/app/send.js
index ff8ef4d65..ba4e5bfff 100644
--- a/ui/app/send.js
+++ b/ui/app/send.js
@@ -2,10 +2,13 @@ const inherits = require('util').inherits
const Component = require('react').Component
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 AccountPanel = require('./components/account-panel')
+const formatBalance = require('./util').formatBalance
+const addressSummary = require('./util').addressSummary
+const EtherBalance = require('./components/eth-balance')
const ethUtil = require('ethereumjs-util')
module.exports = connect(mapStateToProps)(SendTransactionScreen)
@@ -18,6 +21,8 @@ function mapStateToProps(state) {
warning: state.appState.warning,
}
+ 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
@@ -32,95 +37,190 @@ function SendTransactionScreen() {
SendTransactionScreen.prototype.render = function() {
var state = this.props
+ var address = state.address
var account = state.account
var identity = state.identity
return (
+
h('.send-screen.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: this.back.bind(this),
- }),
- h('h2.page-subtitle', 'Send Transaction'),
+ //
+ // Sender Profile
+ //
+
+ h('.account-data-subsection.flex-column.flex-grow', {
+ style: {
+ margin: '0 20px',
+ },
+ }, [
+
+ // header - identicon + nav
+ h('.flex-row.flex-space-between', {
+ style: {
+ marginTop: 28,
+ },
+ }, [
+
+ // invisible placeholder for later
+ 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,
+ }),
+ ]),
+
+ // small accounts nav
+ h('i.fa.fa-users.fa-lg.cursor-pointer.color-orange', {
+ onClick: this.navigateToAccounts.bind(this),
+ }),
+
+ ]),
+
+ // account label
+ h('h2.font-medium.color-forest.flex-center', {
+ style: {
+ paddingTop: 8,
+ marginBottom: 8,
+ },
+ }, identity && identity.name),
+
+ // address and getter actions
+ h('.flex-row.flex-center', {
+ style: {
+ marginBottom: 8,
+ },
+ }, [
+
+ h('div', {
+ style: {
+ lineHeight: '16px',
+ },
+ }, addressSummary(address)),
+
+ ]),
+
+ // balance
+ h('.flex-row.flex-center', [
+
+ // h('div', formatBalance(account && account.balance)),
+ h(EtherBalance, {
+ value: account && account.balance,
+ })
+
+ ]),
+
+ ]),
+
+ //
+ // Required Fields
+ //
+
+ h('h3.flex-center.text-transform-uppercase', {
+ style: {
+ background: '#EBEBEB',
+ color: '#AEAEAE',
+ marginTop: 32,
+ marginBottom: 16,
+ },
+ }, [
+ 'Send Transaction',
]),
- h(AccountPanel, {
- showFullAddress: true,
- identity: identity,
- account: account,
- }),
+ // error message
+ state.error && h('span.error.flex-center', state.error),
- h('section.recipient', [
- h('input.address', {
+ // 'to' field
+ h('section.flex-row.flex-center', [
+ h('input.large-input', {
+ name: 'address',
placeholder: 'Recipient Address',
})
]),
- h('section.ammount', [
- h('input.ether', {
+ // 'amount' and send button
+ h('section.flex-row.flex-center', [
+
+ h('input.large-input', {
+ name: 'amount',
placeholder: 'Amount',
type: 'number',
- style: { marginRight: '6px' }
+ style: {
+ marginRight: 6,
+ },
}),
- h('select.currency', {
- name: 'currency',
- }, [
- h('option', { value: 'ether' }, 'Ether (1e18 wei)'),
- h('option', { value: 'wei' }, 'Wei'),
- ]),
- ]),
- h('section.data', [
- h('details', [
- h('summary', {
- style: {cursor: 'pointer'},
- }, 'Advanced'),
- h('textarea.txData', {
- type: 'textarea',
- placeholder: 'Transaction data (optional)',
- style: {
- height: '100px',
- width: '100%',
- resize: 'none',
- }
- })
- ])
+ h('button.primary', {
+ onClick: this.onSubmit.bind(this),
+ style: {
+ textTransform: 'uppercase',
+ },
+ }, 'Send')
+
]),
- h('section', {
+ //
+ // Optional Fields
+ //
+
+ h('h3.flex-center.text-transform-uppercase', {
+ style: {
+ background: '#EBEBEB',
+ color: '#AEAEAE',
+ marginTop: 16,
+ marginBottom: 16,
+ },
}, [
- h('button', {
- onClick: this.onSubmit.bind(this),
- }, 'Send')
+ 'Tranasactional Data (optional)',
+ ]),
+
+ // 'data' field
+ h('section.flex-row.flex-center', [
+ h('input.large-input', {
+ name: 'txData',
+ placeholder: '0x01234',
+ style: {
+ width: '100%',
+ resize: 'none',
+ }
+ }),
]),
- state.warning ? h('span.error', state.warning.split('.')[0]) : null,
])
+
)
}
+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.onSubmit = function(event) {
- var recipient = document.querySelector('input.address').value
+SendTransactionScreen.prototype.onSubmit = function() {
- var inputAmount = parseFloat(document.querySelector('input.ether').value)
- var currency = document.querySelector('select.currency').value
- var value = util.normalizeNumberToWei(inputAmount, currency)
-
- var balance = this.props.balance
+ const recipient = document.querySelector('input[name="address"]').value
+ 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
if (value.gt(balance)) {
var message = 'Insufficient funds.'
return this.props.dispatch(actions.displayWarning(message))
}
- if (recipient.length !== 42) {
- var message = 'Recipient address is the incorrect length.'
+
+ if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) {
+ var message = 'Recipient address is invalid.'
return this.props.dispatch(actions.displayWarning(message))
}
@@ -128,12 +228,11 @@ SendTransactionScreen.prototype.onSubmit = function(event) {
this.props.dispatch(actions.showLoadingIndication())
var txParams = {
- to: recipient,
from: this.props.address,
value: '0x' + value.toString(16),
}
- var txData = document.querySelector('textarea.txData').value
+ if (recipient) txParams.to = ethUtil.addHexPrefix(recipient)
if (txData) txParams.data = txData
this.props.dispatch(actions.signTx(txParams))
diff --git a/ui/app/unlock.js b/ui/app/unlock.js
index 8aac1b1ff..687bb5e52 100644
--- a/ui/app/unlock.js
+++ b/ui/app/unlock.js
@@ -29,19 +29,25 @@ UnlockScreen.prototype.render = function() {
h('.unlock-screen.flex-column.flex-center.flex-grow', [
- h('h2.page-subtitle', 'Welcome!'),
-
h(Mascot, {
animationEventEmitter: this.animationEventEmitter,
}),
- h('label', {
- htmlFor: 'password-box',
- }, 'Enter Password:'),
+ h('h1', {
+ style: {
+ fontSize: '1.4em',
+ textTransform: 'uppercase',
+ color: '#7F8082',
+ },
+ }, 'MetaMask'),
- h('input', {
+ h('input.large-input', {
type: 'password',
id: 'password-box',
+ placeholder: 'enter password',
+ style: {
+
+ },
onKeyPress: this.onKeyPress.bind(this),
onInput: this.inputChanged.bind(this),
}),
@@ -54,6 +60,9 @@ UnlockScreen.prototype.render = function() {
h('button.primary.cursor-pointer', {
onClick: this.onSubmit.bind(this),
+ style: {
+ margin: 10,
+ },
}, 'Unlock'),
])
diff --git a/ui/app/util.js b/ui/app/util.js
index 5dbcffa7e..81a029350 100644
--- a/ui/app/util.js
+++ b/ui/app/util.js
@@ -21,13 +21,17 @@ for (var currency in valueTable) {
module.exports = {
valuesFor: valuesFor,
addressSummary: addressSummary,
+ isAllOneCase: isAllOneCase,
+ isValidAddress: isValidAddress,
numericBalance: numericBalance,
+ parseBalance: parseBalance,
formatBalance: formatBalance,
dataSize: dataSize,
readableDate: readableDate,
ethToWei: ethToWei,
weiToEth: weiToEth,
normalizeToWei: normalizeToWei,
+ normalizeEthStringToWei: normalizeEthStringToWei,
normalizeNumberToWei: normalizeNumberToWei,
valueTable: valueTable,
bnTable: bnTable,
@@ -41,7 +45,21 @@ function valuesFor(obj) {
}
function addressSummary(address) {
- return address ? address.slice(0,2+8)+'...'+address.slice(-4) : '...'
+ if (!address) return ''
+ var checked = ethUtil.toChecksumAddress(address)
+ return checked ? checked.slice(0,2+8)+'...'+checked.slice(-4) : '...'
+}
+
+function isValidAddress(address) {
+ var prefixed = ethUtil.addHexPrefix(address)
+ 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
@@ -65,16 +83,30 @@ function weiToEth(bn) {
return eth
}
-var decimalsToKeep = 4
-function formatBalance(balance) {
- if (!balance || balance === '0x0') return 'None'
+// Takes hex, returns [beforeDecimal, afterDecimal]
+function parseBalance(balance, decimalsToKeep) {
+ if (decimalsToKeep === undefined) decimalsToKeep = 4
+ if (!balance || balance === '0x0') return ['0', '']
var wei = numericBalance(balance)
var padded = wei.toString(10)
var len = padded.length
- var nonZeroIndex = padded.match(/[^0]/) && padded.match(/[^0]/).index
+ var match = padded.match(/[^0]/)
+ var nonZeroIndex = match && match.index
var beforeDecimal = padded.substr(nonZeroIndex ? nonZeroIndex : 0, len - 18) || '0'
var afterDecimal = padded.substr(len - 18, decimalsToKeep)
- return `${beforeDecimal}.${afterDecimal} ETH`
+ return [beforeDecimal, afterDecimal]
+}
+
+// Takes wei hex, returns "None" or "${formattedAmount} ETH"
+function formatBalance(balance) {
+ var parsed = parseBalance(balance)
+ var beforeDecimal = parsed[0]
+ var afterDecimal = parsed[1]
+ if (beforeDecimal === '0' && afterDecimal === '') return 'None'
+ var result = beforeDecimal
+ if (afterDecimal) result += '.'+afterDecimal
+ result += ' ETH'
+ return result
}
function dataSize(data) {
@@ -91,9 +123,23 @@ function normalizeToWei(amount, currency) {
return amount
}
-var multiple = new ethUtil.BN('1000', 10)
+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 * 1000
+ var enlarged = n * 10000
var amount = new ethUtil.BN(String(enlarged), 10)
return normalizeToWei(amount, currency).div(multiple)
}
diff --git a/ui/design/02a-metamask-AccDetails-OverTransaction.jpg b/ui/design/02a-metamask-AccDetails-OverTransaction.jpg
new file mode 100644
index 000000000..8a06be6b9
--- /dev/null
+++ b/ui/design/02a-metamask-AccDetails-OverTransaction.jpg
Binary files differ
diff --git a/ui/design/02b-metamask-AccDetails-Send.jpg b/ui/design/02b-metamask-AccDetails-Send.jpg
new file mode 100644
index 000000000..10f2d27fd
--- /dev/null
+++ b/ui/design/02b-metamask-AccDetails-Send.jpg
Binary files differ
diff --git a/ui/design/05-metamask-Menu.jpg b/ui/design/05-metamask-Menu.jpg
new file mode 100644
index 000000000..0a43d7b2a
--- /dev/null
+++ b/ui/design/05-metamask-Menu.jpg
Binary files differ