diff options
Diffstat (limited to 'ui')
87 files changed, 7044 insertions, 1968 deletions
diff --git a/ui/app/account-and-transaction-details.js b/ui/app/account-and-transaction-details.js new file mode 100644 index 000000000..60293de77 --- /dev/null +++ b/ui/app/account-and-transaction-details.js @@ -0,0 +1,38 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +// Main Views +const TxView = require('./components/tx-view') +const WalletView = require('./components/wallet-view') + +module.exports = AccountAndTransactionDetails + +inherits(AccountAndTransactionDetails, Component) +function AccountAndTransactionDetails () { + Component.call(this) +} + +AccountAndTransactionDetails.prototype.render = function () { + return h('div', { + style: { + display: 'flex', + flex: '1 0 auto', + }, + }, [ + // wallet + h(WalletView, { + style: { + }, + responsiveDisplayClassname: '.lap-visible', + }, [ + ]), + + // transaction + h(TxView, { + style: { + }, + }, [ + ]), + ]) +} + diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index 02089ecd0..d5b9419a7 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -5,15 +5,10 @@ const h = require('react-hyperscript') const connect = require('react-redux').connect const actions = require('./actions') 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 TabBar = require('./components/tab-bar') const TokenList = require('./components/token-list') -const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns module.exports = connect(mapStateToProps)(AccountDetailScreen) @@ -40,179 +35,11 @@ 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.full-flex-height', [ - - // identicon, label, balance, etc - h('.account-data-subsection', { - style: { - margin: '0 20px', - flex: '1 0 auto', - }, - }, [ - - // 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', - width: '100%', - }, - }, [ - 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( - 'div', - { - style: { - display: 'flex', - justifyContent: 'flex-start', - alignItems: 'center', - }, - }, - [ - h( - 'div.font-medium.color-forest', - { - name: 'edit', - style: { - }, - }, - [ - h('h2', { - style: { - maxWidth: '180px', - overflow: 'hidden', - textOverflow: 'ellipsis', - padding: '5px 0px', - }, - }, [ - identity && identity.name, - ]), - ] - ), - h( - AccountDropdowns, - { - style: { - marginRight: '8px', - marginLeft: 'auto', - cursor: 'pointer', - }, - selected, - network, - identities: props.identities, - enableAccountOptions: true, - }, - ), - ] - ), - ]), - 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', - marginBottom: '15px', - color: '#AEAEAE', - }, - }, checksumAddress), - ]), - - // 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('.flex-grow'), - - h('button', { - onClick: () => props.dispatch(actions.buyEthView(selected)), - style: { marginRight: '10px' }, - }, 'BUY'), - - h('button', { - onClick: () => props.dispatch(actions.showSendPage()), - style: { - marginBottom: '20px', - marginRight: '8px', - }, - }, 'SEND'), - - ]), - ]), - - // subview (tx history, pk export confirm, buy eth warning) - this.subview(), - - ]) - ) -} +// Note: This component is no longer used. Leaving the file for reference: +// - structuring routing for add token +// - state required for TxList +// Delete file when those features are complete +AccountDetailScreen.prototype.render = function () {} AccountDetailScreen.prototype.subview = function () { var subview diff --git a/ui/app/actions.js b/ui/app/actions.js index e793e6a21..678c68a6a 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -1,3 +1,4 @@ +const abi = require('human-standard-token-abi') const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url') var actions = { @@ -5,6 +6,21 @@ var actions = { GO_HOME: 'GO_HOME', goHome: goHome, + // modal state + MODAL_OPEN: 'UI_MODAL_OPEN', + MODAL_CLOSE: 'UI_MODAL_CLOSE', + showModal: showModal, + hideModal: hideModal, + // sidebar state + SIDEBAR_OPEN: 'UI_SIDEBAR_OPEN', + SIDEBAR_CLOSE: 'UI_SIDEBAR_CLOSE', + showSidebar: showSidebar, + hideSidebar: hideSidebar, + // network dropdown open + NETWORK_DROPDOWN_OPEN: 'UI_NETWORK_DROPDOWN_OPEN', + NETWORK_DROPDOWN_CLOSE: 'UI_NETWORK_DROPDOWN_CLOSE', + showNetworkDropdown: showNetworkDropdown, + hideNetworkDropdown: hideNetworkDropdown, // menu state getNetworkStatus: 'getNetworkStatus', // transition state @@ -68,6 +84,8 @@ var actions = { hideWarning: hideWarning, // accounts screen SET_SELECTED_ACCOUNT: 'SET_SELECTED_ACCOUNT', + SET_SELECTED_TOKEN: 'SET_SELECTED_TOKEN', + setSelectedToken, SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL', SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE', SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE', @@ -78,6 +96,8 @@ var actions = { // account detail screen SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', showSendPage: showSendPage, + SHOW_SEND_TOKEN_PAGE: 'SHOW_SEND_TOKEN_PAGE', + showSendTokenPage, ADD_TO_ADDRESS_BOOK: 'ADD_TO_ADDRESS_BOOK', addToAddressBook: addToAddressBook, REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT', @@ -97,7 +117,9 @@ var actions = { cancelMsg: cancelMsg, signPersonalMsg, cancelPersonalMsg, + sendTx: sendTx, signTx: signTx, + signTokenTx: signTokenTx, updateAndApproveTx, cancelTx: cancelTx, completedTx: completedTx, @@ -142,6 +164,8 @@ var actions = { coinBaseSubview: coinBaseSubview, SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW', shapeShiftSubview: shapeShiftSubview, + UPDATE_TOKEN_EXCHANGE_RATE: 'UPDATE_TOKEN_EXCHANGE_RATE', + updateTokenExchangeRate, PAIR_UPDATE: 'PAIR_UPDATE', pairUpdate: pairUpdate, coinShiftRquest: coinShiftRquest, @@ -337,16 +361,16 @@ function showInfoPage () { function setCurrentCurrency (currencyCode) { return (dispatch) => { - dispatch(this.showLoadingIndication()) + dispatch(actions.showLoadingIndication()) log.debug(`background.setCurrentCurrency`) background.setCurrentCurrency(currencyCode, (err, data) => { - dispatch(this.hideLoadingIndication()) + dispatch(actions.hideLoadingIndication()) if (err) { log.error(err.stack) return dispatch(actions.displayWarning(err.message)) } dispatch({ - type: this.SET_CURRENT_FIAT, + type: actions.SET_CURRENT_FIAT, value: { currentCurrency: data.currentCurrency, conversionRate: data.conversionRate, @@ -400,10 +424,37 @@ function signTx (txData) { dispatch(actions.showLoadingIndication()) global.ethQuery.sendTransaction(txData, (err, data) => { dispatch(actions.hideLoadingIndication()) - if (err) dispatch(actions.displayWarning(err.message)) - dispatch(this.goHome()) + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.hideWarning()) }) - dispatch(actions.showConfTxPage()) + dispatch(actions.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 signTokenTx (tokenAddress, toAddress, amount, txData) { + return dispatch => { + dispatch(actions.showLoadingIndication()) + const token = global.eth.contract(abi).at(tokenAddress) + token.transfer(toAddress, amount, txData) + .catch(err => { + dispatch(actions.hideLoadingIndication()) + dispatch(actions.displayWarning(err.message)) + }) + dispatch(actions.showConfTxPage({})) } } @@ -568,6 +619,13 @@ function setCurrentAccountTab (newTabName) { return callBackgroundThenUpdateNoSpinner(background.setCurrentAccountTab, newTabName) } +function setSelectedToken (tokenAddress) { + return { + type: actions.SET_SELECTED_TOKEN, + value: tokenAddress || null, + } +} + function showAccountDetail (address) { return (dispatch) => { dispatch(actions.showLoadingIndication()) @@ -581,6 +639,7 @@ function showAccountDetail (address) { type: actions.SHOW_ACCOUNT_DETAIL, value: address, }) + dispatch(actions.setSelectedToken()) }) } } @@ -598,10 +657,11 @@ function showAccountsPage () { } } -function showConfTxPage (transForward = true) { +function showConfTxPage ({transForward = true, id}) { return { type: actions.SHOW_CONF_TX_PAGE, - transForward: transForward, + transForward, + id, } } @@ -631,10 +691,9 @@ function showConfigPage (transitionForward = true) { } } -function showAddTokenPage (transitionForward = true) { +function showAddTokenPage () { return { type: actions.SHOW_ADD_TOKEN_PAGE, - value: transitionForward, } } @@ -761,6 +820,46 @@ function useEtherscanProvider () { } } +function showNetworkDropdown () { + return { + type: actions.NETWORK_DROPDOWN_OPEN, + } +} + +function hideNetworkDropdown () { + return { + type: actions.NETWORK_DROPDOWN_CLOSE, + } +} + + +function showModal (payload) { + return { + type: actions.MODAL_OPEN, + payload, + } +} + +function hideModal (payload) { + return { + type: actions.MODAL_CLOSE, + payload, + } +} + +function showSidebar () { + return { + type: actions.SIDEBAR_OPEN, + } +} + +function hideSidebar () { + return { + type: actions.SIDEBAR_CLOSE, + } +} + + function showLoadingIndication (message) { return { type: actions.SHOW_LOADING, @@ -863,6 +962,12 @@ function showSendPage () { } } +function showSendTokenPage () { + return { + type: actions.SHOW_SEND_TOKEN_PAGE, + } +} + function buyEth (opts) { return (dispatch) => { const url = getBuyEthUrl(opts) @@ -989,6 +1094,28 @@ function shapeShiftRequest (query, options, cb) { } } +function updateTokenExchangeRate (token = '') { + const pair = `${token.toLowerCase()}_eth` + + return dispatch => { + if (!token) { + return + } + + shapeShiftRequest('marketinfo', { pair }, marketinfo => { + if (!marketinfo.error) { + dispatch({ + type: actions.UPDATE_TOKEN_EXCHANGE_RATE, + payload: { + pair, + marketinfo, + }, + }) + } + }) + } +} + // Call Background Then Update // // A function generator for a common pattern wherein: diff --git a/ui/app/add-token.js b/ui/app/add-token.js index 18adc7eb5..4374ee586 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -232,7 +232,6 @@ AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) 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 index ee800ea90..14e6a26e2 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -6,33 +6,37 @@ const actions = require('./actions') // init const InitializeMenuScreen = require('./first-time/init-menu') const NewKeyChainScreen = require('./new-keychain') -// unlock -const UnlockScreen = require('./unlock') // accounts -const AccountDetailScreen = require('./account-detail') +const MainContainer = require('./main-container') const SendTransactionScreen = require('./send') +const SendTokenScreen = require('./components/send-token') const ConfirmTxScreen = require('./conf-tx') // notice const NoticeScreen = require('./components/notice') const generateLostAccountsNotice = require('../lib/lost-accounts-notice') + +// slideout menu +const WalletView = require('./components/wallet-view') + // 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 Dropdown = require('./components/dropdown').Dropdown -const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem const NetworkIndicator = require('./components/network') 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') -const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +const NetworkDropdown = require('./components/dropdowns/network-dropdown') -module.exports = connect(mapStateToProps)(App) +// Global Modals +const Modal = require('./components/modals/index').Modal + +module.exports = connect(mapStateToProps, mapDispatchToProps)(App) inherits(App, Component) function App () { Component.call(this) } @@ -48,6 +52,8 @@ function mapStateToProps (state) { return { // state from plugin + networkDropdownOpen: state.appState.networkDropdownOpen, + sidebarOpen: state.appState.sidebarOpen, isLoading: state.appState.isLoading, loadingMessage: state.appState.loadingMessage, noActiveNotices: state.metamask.noActiveNotices, @@ -74,9 +80,18 @@ function mapStateToProps (state) { } } +function mapDispatchToProps (dispatch, ownProps) { + return { + dispatch, + hideSidebar: () => dispatch(actions.hideSidebar()), + showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()), + hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()), + } +} + App.prototype.render = function () { var props = this.props - const { isLoading, loadingMessage, transForward, network } = props + const { isLoading, loadingMessage, network } = props const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' const loadMessage = loadingMessage || isLoadingNetwork ? `Connecting to ${this.getNetworkName()}` : null @@ -86,316 +101,155 @@ App.prototype.render = function () { h('.flex-column.full-height', { style: { - // Windows was showing a vertical scroll bar: - overflow: 'hidden', + overflowX: 'hidden', position: 'relative', alignItems: 'center', }, }, [ + // global modal + h(Modal, {}, []), + // app bar this.renderAppBar(), - this.renderNetworkDropdown(), - this.renderDropdown(), + + // sidebar + this.renderSidebar(), + + // network dropdown + h(NetworkDropdown, { + provider: this.props.provider, + frequentRpcList: this.props.frequentRpcList, + }, []), h(Loading, { isLoading: isLoading || isLoadingNetwork, loadingMessage: loadMessage, }), - // panel content - h('.app-primary' + (transForward ? '.from-right' : '.from-left'), { - style: { - width: '100%', - }, - }, [ - this.renderPrimary(), - ]), + // content + this.renderPrimary(), ]) ) } +App.prototype.renderGlobalModal = function () { + return h(Modal, { + ref: 'modalRef', + }, [ + // h(BuyOptions, {}, []), + ]) +} + +App.prototype.renderSidebar = function () { + + return h('div', { + }, [ + h('style', ` + .sidebar-enter { + transition: transform 300ms ease-in-out; + transform: translateX(-100%); + } + .sidebar-enter.sidebar-enter-active { + transition: transform 300ms ease-in-out; + transform: translateX(0%); + } + .sidebar-leave { + transition: transform 200ms ease-out; + transform: translateX(0%); + } + .sidebar-leave.sidebar-leave-active { + transition: transform 200ms ease-out; + transform: translateX(-100%); + } + `), + + h(ReactCSSTransitionGroup, { + transitionName: 'sidebar', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 200, + }, [ + // A second instance of Walletview is used for non-mobile viewports + this.props.sidebarOpen ? h(WalletView, { + responsiveDisplayClassname: '.sidebar', + style: {}, + }) : undefined, + + ]), + + // overlay + // TODO: add onClick for overlay to close sidebar + this.props.sidebarOpen ? h('div.sidebar-overlay', { + style: {}, + onClick: () => { + this.props.hideSidebar() + }, + }, []) : undefined, + ]) +} + 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('.full-width', { - height: '38px', + style: {}, }, [ h('.app-header.flex-row.flex-space-between', { - style: { - alignItems: 'center', - visibility: props.isUnlocked ? 'visible' : 'none', - background: props.isUnlocked ? 'white' : 'none', - height: '38px', - position: 'relative', - zIndex: 12, - }, + style: {}, }, [ - - 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', - }, - }, [ - - props.isUnlocked && h(AccountDropdowns, { + h('div.app-header-contents', {}, [ + h('div.left-menu-wrapper', { + style: {}, + }, [ + // mini logo + h('img', { + height: 24, + width: 24, + src: '/images/icon-128.png', + }), + + // metamask name + h('h1', { + style: { + position: 'relative', + paddingLeft: '9px', + color: '#5B5D67', + }, + }, 'MetaMask'), + + ]), + + h('div.network-component-wrapper', { style: {}, - enableAccountsSelector: true, - identities: this.props.identities, - selected: this.props.currentView.context, - network: this.props.network, - keyrings: this.props.keyrings, - }, []), - - // hamburger - props.isUnlocked && h(SandwichExpando, { - className: 'sandwich-expando', - width: 16, - barHeight: 2, - padding: 0, - isOpen: state.isMainMenuOpen, - color: 'rgb(247,146,30)', - onClick: () => { - this.setState({ - isMainMenuOpen: !state.isMainMenuOpen, - }) - }, - }), + }, [ + // Network Indicator + h(NetworkIndicator, { + network: this.props.network, + provider: this.props.provider, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + if (this.props.networkDropdownOpen === false) { + this.props.showNetworkDropdown() + } else { + this.props.hideNetworkDropdown() + } + }, + }), + + ]), ]), ]), + ]) ) } -App.prototype.renderNetworkDropdown = function () { - const props = this.props - const { provider: { type: providerType, rpcTarget: activeNetwork } } = props - const rpcList = props.frequentRpcList - const state = this.state || {} - const isOpen = state.isNetworkMenuOpen - - return h(Dropdown, { - useCssTransition: true, - isOpen, - onClickOutside: (event) => { - const { classList } = event.target - const isNotToggleElement = [ - classList.contains('menu-icon'), - classList.contains('network-name'), - classList.contains('network-indicator'), - ].filter(bool => bool).length === 0 - // classes from three constituent nodes of the toggle element - - if (isNotToggleElement) { - this.setState({ isNetworkMenuOpen: false }) - } - }, - zIndex: 11, - style: { - position: 'absolute', - left: '2px', - top: '36px', - }, - innerStyle: { - padding: '2px 16px 2px 0px', - }, - }, [ - - h( - DropdownMenuItem, - { - key: 'main', - closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), - onClick: () => props.dispatch(actions.setProviderType('mainnet')), - style: { - fontSize: '18px', - }, - }, - [ - h('.menu-icon.diamond'), - 'Main Ethereum Network', - providerType === 'mainnet' ? h('.check', '✓') : null, - ] - ), - - h( - DropdownMenuItem, - { - key: 'ropsten', - closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), - onClick: () => props.dispatch(actions.setProviderType('ropsten')), - style: { - fontSize: '18px', - }, - }, - [ - h('.menu-icon.red-dot'), - 'Ropsten Test Network', - providerType === 'ropsten' ? h('.check', '✓') : null, - ] - ), - - h( - DropdownMenuItem, - { - key: 'kovan', - closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), - onClick: () => props.dispatch(actions.setProviderType('kovan')), - style: { - fontSize: '18px', - }, - }, - [ - h('.menu-icon.hollow-diamond'), - 'Kovan Test Network', - providerType === 'kovan' ? h('.check', '✓') : null, - ] - ), - - h( - DropdownMenuItem, - { - key: 'rinkeby', - closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), - onClick: () => props.dispatch(actions.setProviderType('rinkeby')), - style: { - fontSize: '18px', - }, - }, - [ - h('.menu-icon.golden-square'), - 'Rinkeby Test Network', - providerType === 'rinkeby' ? h('.check', '✓') : null, - ] - ), - - h( - DropdownMenuItem, - { - key: 'default', - closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), - onClick: () => props.dispatch(actions.setDefaultRpcTarget()), - style: { - fontSize: '18px', - }, - }, - [ - h('i.fa.fa-question-circle.fa-lg.menu-icon'), - 'Localhost 8545', - activeNetwork === 'http://localhost:8545' ? h('.check', '✓') : null, - ] - ), - - this.renderCustomOption(props.provider), - this.renderCommonRpc(rpcList, props.provider), - - h( - DropdownMenuItem, - { - closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), - onClick: () => this.props.dispatch(actions.showConfigPage()), - style: { - fontSize: '18px', - }, - }, - [ - h('i.fa.fa-question-circle.fa-lg.menu-icon'), - 'Custom RPC', - activeNetwork === 'custom' ? h('.check', '✓') : null, - ] - ), - - ]) -} - -App.prototype.renderDropdown = function () { - const state = this.state || {} - const isOpen = state.isMainMenuOpen - - return h(Dropdown, { - useCssTransition: true, - isOpen: isOpen, - zIndex: 11, - onClickOutside: (event) => { - const classList = event.target.classList - const parentClassList = event.target.parentElement.classList - - const isToggleElement = classList.contains('sandwich-expando') || - parentClassList.contains('sandwich-expando') - - if (isOpen && !isToggleElement) { - this.setState({ isMainMenuOpen: false }) - } - }, - style: { - position: 'absolute', - right: '2px', - top: '38px', - }, - innerStyle: {}, - }, [ - h(DropdownMenuItem, { - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - onClick: () => { this.props.dispatch(actions.showConfigPage()) }, - }, 'Settings'), - - h(DropdownMenuItem, { - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - onClick: () => { this.props.dispatch(actions.lockMetamask()) }, - }, 'Lock'), - - h(DropdownMenuItem, { - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - onClick: () => { this.props.dispatch(actions.showInfoPage()) }, - }, 'Info/Help'), - ]) -} - App.prototype.renderBackButton = function (style, justArrow = false) { var props = this.props return ( @@ -459,33 +313,27 @@ App.prototype.renderPrimary = function () { // 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'}) - } + return h(MainContainer, { + currentViewName: props.currentView.name, + isUnlocked: props.isUnlocked, + }) } // show current view switch (props.currentView.name) { case 'accountDetail': - log.debug('rendering account detail screen') - return h(AccountDetailScreen, {key: 'account-detail'}) + log.debug('rendering main container') + return h(MainContainer, {key: 'account-detail'}) case 'sendTransaction': log.debug('rendering send tx screen') return h(SendTransactionScreen, {key: 'send-transaction'}) + case 'sendToken': + log.debug('rendering send token screen') + return h(SendTokenScreen, {key: 'sendToken'}) + case 'newKeychain': log.debug('rendering new keychain screen') return h(NewKeyChainScreen, {key: 'new-keychain'}) @@ -548,7 +396,7 @@ App.prototype.renderPrimary = function () { default: log.debug('rendering default, account detail screen') - return h(AccountDetailScreen, {key: 'account-detail'}) + return h(MainContainer, {key: 'account-detail'}) } } @@ -564,40 +412,6 @@ App.prototype.toggleMetamaskActive = function () { } } -App.prototype.renderCustomOption = function (provider) { - const { rpcTarget, type } = provider - const props = this.props - - if (type !== 'rpc') return null - - // Concatenate long URLs - let label = rpcTarget - if (rpcTarget.length > 31) { - label = label.substr(0, 34) + '...' - } - - switch (rpcTarget) { - - case 'http://localhost:8545': - return null - - default: - return h( - DropdownMenuItem, - { - key: rpcTarget, - onClick: () => props.dispatch(actions.setRpcTarget(rpcTarget)), - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - }, - [ - h('i.fa.fa-question-circle.fa-lg.menu-icon'), - label, - h('.check', '✓'), - ] - ) - } -} - App.prototype.getNetworkName = function () { const { provider } = this.props const providerName = provider.type @@ -618,28 +432,3 @@ App.prototype.getNetworkName = function () { return name } - -App.prototype.renderCommonRpc = function (rpcList, provider) { - const props = this.props - const rpcTarget = provider.rpcTarget - - return rpcList.map((rpc) => { - if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { - return null - } else { - return h( - DropdownMenuItem, - { - key: `common${rpc}`, - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - onClick: () => props.dispatch(actions.setRpcTarget(rpc)), - }, - [ - h('i.fa.fa-question-circle.fa-lg.menu-icon'), - rpc, - rpcTarget === rpc ? h('.check', '✓') : null, - ] - ) - } - }) -} diff --git a/ui/app/components/balance-component.js b/ui/app/components/balance-component.js new file mode 100644 index 000000000..d14aa675f --- /dev/null +++ b/ui/app/components/balance-component.js @@ -0,0 +1,120 @@ +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const inherits = require('util').inherits +const TokenBalance = require('./token-balance') +const Identicon = require('./identicon') + +const { formatBalance, generateBalanceObject } = require('../util') + +module.exports = connect(mapStateToProps)(BalanceComponent) + +function mapStateToProps (state) { + const accounts = state.metamask.accounts + const network = state.metamask.network + const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] + const account = accounts[selectedAddress] + + return { + account, + network, + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + } +} + +inherits(BalanceComponent, Component) +function BalanceComponent () { + Component.call(this) +} + +BalanceComponent.prototype.render = function () { + const props = this.props + const { token, network } = props + + return h('div.balance-container', {}, [ + + // TODO: balance icon needs to be passed in + // h('img.balance-icon', { + // src: '../images/eth_logo.svg', + // style: {}, + // }), + h(Identicon, { + diameter: 45, + address: token && token.address, + network, + }), + + token ? this.renderTokenBalance() : this.renderBalance(), + ]) +} + +BalanceComponent.prototype.renderTokenBalance = function () { + const { token } = this.props + + return h('div.flex-column.balance-display', [ + h('div.token-amount', [ h(TokenBalance, { token }) ]), + ]) +} + +BalanceComponent.prototype.renderBalance = function () { + const props = this.props + const { shorten, account } = props + const balanceValue = account && account.balance + const needsParse = 'needsParse' in props ? props.needsParse : true + const formattedBalance = balanceValue ? formatBalance(balanceValue, 6, needsParse) : '...' + const showFiat = 'showFiat' in props ? props.showFiat : true + + if (formattedBalance === 'None' || formattedBalance === '...') { + return h('div.flex-column.balance-display', {}, [ + h('div.token-amount', { + style: {}, + }, formattedBalance), + ]) + } + + return h('div.flex-column.balance-display', {}, [ + h('div.token-amount', { + style: {}, + }, this.getTokenBalance(formattedBalance, shorten)), + + showFiat ? this.renderFiatValue(formattedBalance) : null, + ]) +} + +BalanceComponent.prototype.renderFiatValue = function (formattedBalance) { + + const { conversionRate, currentCurrency } = this.props + + const fiatDisplayNumber = this.getFiatDisplayNumber(formattedBalance, conversionRate) + + const fiatPrefix = currentCurrency === 'USD' ? '$' : '' + + return this.renderFiatAmount(fiatDisplayNumber, currentCurrency, fiatPrefix) +} + +BalanceComponent.prototype.renderFiatAmount = function (fiatDisplayNumber, fiatSuffix, fiatPrefix) { + if (fiatDisplayNumber === 'N/A') return null + + return h('div.fiat-amount', { + style: {}, + }, `${fiatPrefix}${fiatDisplayNumber} ${fiatSuffix}`) +} + +BalanceComponent.prototype.getTokenBalance = function (formattedBalance, shorten) { + const balanceObj = generateBalanceObject(formattedBalance, shorten ? 1 : 3) + + const balanceValue = shorten ? balanceObj.shortBalance : balanceObj.balance + const label = balanceObj.label + + return `${balanceValue} ${label}` +} + +BalanceComponent.prototype.getFiatDisplayNumber = function (formattedBalance, conversionRate) { + if (formattedBalance === 'None') return formattedBalance + if (conversionRate === 0) return 'N/A' + + const splitBalance = formattedBalance.split(' ') + + return (Number(splitBalance[0]) * conversionRate).toFixed(2) +} diff --git a/ui/app/components/buy-button-subview.js b/ui/app/components/buy-button-subview.js index 15281171c..6cf6e9eb9 100644 --- a/ui/app/components/buy-button-subview.js +++ b/ui/app/components/buy-button-subview.js @@ -245,7 +245,7 @@ BuyButtonSubview.prototype.navigateTo = function (url) { BuyButtonSubview.prototype.backButtonContext = function () { if (this.props.context === 'confTx') { - this.props.dispatch(actions.showConfTxPage(false)) + this.props.dispatch(actions.showConfTxPage({transForward: false})) } else { this.props.dispatch(actions.goHome()) } diff --git a/ui/app/components/dropdowns/account-options-dropdown.js b/ui/app/components/dropdowns/account-options-dropdown.js new file mode 100644 index 000000000..50e793d87 --- /dev/null +++ b/ui/app/components/dropdowns/account-options-dropdown.js @@ -0,0 +1,29 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const AccountDropdowns = require('./components/account-dropdowns') + +inherits(AccountOptionsDropdown, Component) +function AccountOptionsDropdown () { + Component.call(this) +} + +module.exports = AccountOptionsDropdown + +// TODO: specify default props and proptypes +// TODO: hook up to state, connect to redux to clean up API +// TODO: selectedAddress is not defined... should we use selected? +AccountOptionsDropdown.prototype.render = function () { + const { selected, network, identities, style, dropdownWrapperStyle, menuItemStyles } = this.props + + return h(AccountDropdowns, { + enableAccountOptions: true, + enableAccountsSelector: false, + selected: selectedAddress, + network, + identities, + style: style || {}, + dropdownWrapperStyle: dropdownWrapperStyle || {}, + menuItemStyles: menuItemStyles || {}, + }, []) +} diff --git a/ui/app/components/dropdowns/account-selection-dropdown.js b/ui/app/components/dropdowns/account-selection-dropdown.js new file mode 100644 index 000000000..7a8502d18 --- /dev/null +++ b/ui/app/components/dropdowns/account-selection-dropdown.js @@ -0,0 +1,29 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const AccountDropdowns = require('./components/account-dropdowns') + +inherits(AccountSelectionDropdown, Component) +function AccountSelectionDropdown () { + Component.call(this) +} + +module.exports = AccountSelectionDropdown + +// TODO: specify default props and proptypes +// TODO: hook up to state, connect to redux to clean up API +// TODO: selectedAddress is not defined... should we use selected? +AccountSelectionDropdown.prototype.render = function () { + const { selected, network, identities, style, dropdownWrapperStyle, menuItemStyles } = this.props + + return h(AccountDropdowns, { + enableAccountOptions: false, + enableAccountsSelector: true, + selected: selectedAddress, + network, + identities, + style: style || {}, + dropdownWrapperStyle: dropdownWrapperStyle || {}, + menuItemStyles: menuItemStyles || {}, + }, []) +} diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/dropdowns/components/account-dropdowns.js index b087a40d4..e2d3d6d64 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/dropdowns/components/account-dropdowns.js @@ -1,14 +1,15 @@ const Component = require('react').Component const PropTypes = require('react').PropTypes const h = require('react-hyperscript') -const actions = require('../actions') -const genAccountLink = require('../../lib/account-link.js') +const actions = require('../../../actions') +const genAccountLink = require('../../../../lib/account-link.js') const connect = require('react-redux').connect const Dropdown = require('./dropdown').Dropdown const DropdownMenuItem = require('./dropdown').DropdownMenuItem -const Identicon = require('./identicon') +const Identicon = require('../../identicon') const ethUtil = require('ethereumjs-util') const copyToClipboard = require('copy-to-clipboard') +const { formatBalance } = require('../../../util') class AccountDropdowns extends Component { constructor (props) { @@ -17,17 +18,21 @@ class AccountDropdowns extends Component { accountSelectorActive: false, optionsMenuActive: false, } - this.accountSelectorToggleClassName = 'accounts-selector' + // Used for orangeaccount selector icon + // this.accountSelectorToggleClassName = 'accounts-selector' + this.accountSelectorToggleClassName = 'fa-angle-down' this.optionsMenuToggleClassName = 'fa-ellipsis-h' } renderAccounts () { - const { identities, selected, keyrings } = this.props + const { identities, accounts, selected, menuItemStyles, actions, keyrings } = this.props return Object.keys(identities).map((key, index) => { const identity = identities[key] const isSelected = identity.address === selected + const balanceValue = accounts[key].balance + const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' const simpleAddress = identity.address.substring(2).toLowerCase() const keyring = keyrings.find((kr) => { @@ -42,34 +47,108 @@ class AccountDropdowns extends Component { onClick: () => { this.props.actions.showAccountDetail(identity.address) }, - style: { - marginTop: index === 0 ? '5px' : '', - fontSize: '24px', - }, + style: Object.assign( + { + marginTop: index === 0 ? '5px' : '', + fontSize: '24px', + }, + menuItemStyles, + ), }, [ - h( - Identicon, - { - address: identity.address, - diameter: 32, + h('div.flex-row.flex-center', {}, [ + + h('span', { + style: { + flex: '1 1 0', + minWidth: '20px', + minHeight: '30px', + }, + }, [ + h('span', { + style: { + flex: '1 1 auto', + fontSize: '14px', + }, + }, isSelected ? h('i.fa.fa-check') : null), + ]), + + h( + Identicon, + { + address: identity.address, + diameter: 24, + style: { + flex: '1 1 auto', + marginLeft: '10px', + }, + }, + ), + + h('span.flex-column', { style: { + flex: '10 10 auto', + width: '175px', + alignItems: 'flex-start', + justifyContent: 'center', marginLeft: '10px', }, - }, - ), - this.indicateIfLoose(keyring), - h('span', { - style: { - marginLeft: '20px', - fontSize: '24px', - maxWidth: '145px', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - }, identity.name || ''), - h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null), + }, [ + this.indicateIfLoose(keyring), + h('span.account-dropdown-name', { + style: { + fontSize: '18px', + maxWidth: '145px', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + }, identity.name || ''), + h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null), + + h('span.account-dropdown-balance', { + style: { + fontSize: '14px', + fontFamily: 'Avenir', + fontWeight: 500, + }, + }, formattedBalance), + ]), + + h('span', { + style: { + flex: '3 3 auto', + }, + }, [ + h('span.account-dropdown-edit-button', { + style: { + fontSize: '16px', + }, + onClick: () => { + actions.showEditAccountModal(identity) + }, + }, [ + 'Edit', + ]), + ]), + + ]), +// ======= +// }, +// ), +// this.indicateIfLoose(keyring), +// h('span', { +// style: { +// marginLeft: '20px', +// fontSize: '24px', +// maxWidth: '145px', +// whiteSpace: 'nowrap', +// overflow: 'hidden', +// textOverflow: 'ellipsis', +// }, +// }, identity.name || ''), +// h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null), +// >>>>>>> master:ui/app/components/account-dropdowns.js ] ) }) @@ -84,24 +163,22 @@ class AccountDropdowns extends Component { } renderAccountSelector () { - const { actions } = this.props - const { accountSelectorActive } = this.state + const { actions, useCssTransition, innerStyle } = this.props + const { accountSelectorActive, menuItemStyles } = this.state return h( Dropdown, { - useCssTransition: true, // Hardcoded because account selector is temporarily in app-header + useCssTransition, style: { - marginLeft: '-238px', - marginTop: '38px', + marginLeft: '-185px', + marginTop: '50px', minWidth: '180px', overflowY: 'auto', maxHeight: '300px', width: '300px', }, - innerStyle: { - padding: '8px 25px', - }, + innerStyle, isOpen: accountSelectorActive, onClickOutside: (event) => { const { classList } = event.target @@ -117,7 +194,11 @@ class AccountDropdowns extends Component { DropdownMenuItem, { closeMenu: () => {}, - onClick: () => actions.addNewAccount(), + style: Object.assign( + {}, + menuItemStyles, + ), + onClick: () => actions.showNewAccountModal(), }, [ h( @@ -129,7 +210,14 @@ class AccountDropdowns extends Component { diameter: 32, }, ), - h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, 'Create Account'), + h('span', { + style: { + marginLeft: '20px', + fontFamily: 'DIN OT', + fontSize: '16px', + lineHeight: '23px', + }, + }, 'Create Account'), ], ), h( @@ -137,6 +225,10 @@ class AccountDropdowns extends Component { { closeMenu: () => {}, onClick: () => actions.showImportPage(), + style: Object.assign( + {}, + menuItemStyles, + ), }, [ h( @@ -151,8 +243,10 @@ class AccountDropdowns extends Component { h('span', { style: { marginLeft: '20px', - fontSize: '24px', marginBottom: '5px', + fontFamily: 'DIN OT', + fontSize: '16px', + lineHeight: '23px', }, }, 'Import Account'), ] @@ -161,19 +255,28 @@ class AccountDropdowns extends Component { ) } - - renderAccountOptions () { - const { actions } = this.props - const { optionsMenuActive } = this.state + const { actions, dropdownWrapperStyle, useCssTransition } = this.props + const { optionsMenuActive, menuItemStyles } = this.state + const dropdownMenuItemStyle = { + fontFamily: 'DIN OT', + fontSize: 16, + lineHeight: '24px', + padding: '8px', + } return h( Dropdown, { - style: { - marginLeft: '-215px', - minWidth: '180px', - }, + useCssTransition, + style: Object.assign( + { + marginLeft: '-10px', + position: 'absolute', + width: '29vh', // affects both mobile and laptop views + }, + dropdownWrapperStyle, + ), isOpen: optionsMenuActive, onClickOutside: () => { const { classList } = event.target @@ -189,24 +292,30 @@ class AccountDropdowns extends Component { { closeMenu: () => {}, onClick: () => { - const { selected, network } = this.props - const url = genAccountLink(selected, network) - global.platform.openWindow({ url }) + this.props.actions.showAccountDetailModal() }, + style: Object.assign( + dropdownMenuItemStyle, + menuItemStyles, + ), }, - 'View account on Etherscan', + 'Account Details', ), h( DropdownMenuItem, { closeMenu: () => {}, onClick: () => { - const { selected, identities } = this.props - var identity = identities[selected] - actions.showQrView(selected, identity ? identity.name : '') + const { selected, network } = this.props + const url = genAccountLink(selected, network) + global.platform.openWindow({ url }) }, + style: Object.assign( + dropdownMenuItemStyle, + menuItemStyles, + ), }, - 'Show QR Code', + 'View account on Etherscan', ), h( DropdownMenuItem, @@ -217,6 +326,10 @@ class AccountDropdowns extends Component { const checkSumAddress = selected && ethUtil.toChecksumAddress(selected) copyToClipboard(checkSumAddress) }, + style: Object.assign( + dropdownMenuItemStyle, + menuItemStyles, + ), }, 'Copy Address to clipboard', ), @@ -227,9 +340,28 @@ class AccountDropdowns extends Component { onClick: () => { actions.requestAccountExport() }, + style: Object.assign( + dropdownMenuItemStyle, + menuItemStyles, + ), }, 'Export Private Key', ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + actions.showAddTokenPage() + }, + style: Object.assign( + dropdownMenuItemStyle, + menuItemStyles, + ), + }, + 'Add Token', + ), + ] ) } @@ -245,16 +377,10 @@ class AccountDropdowns extends Component { }, [ enableAccountsSelector && h( - // 'i.fa.fa-angle-down', - 'div.cursor-pointer.color-orange.accounts-selector', + 'i.fa.fa-angle-down', { style: { - // fontSize: '1.8em', - background: 'url(images/switch_acc.svg) white center center no-repeat', - height: '25px', - width: '25px', - transform: 'scale(0.75)', - marginRight: '3px', + cursor: 'pointer', }, onClick: (event) => { event.stopPropagation() @@ -270,8 +396,8 @@ class AccountDropdowns extends Component { 'i.fa.fa-ellipsis-h', { style: { - marginRight: '0.5em', - fontSize: '1.8em', + fontSize: '135%', + cursor: 'pointer', }, onClick: (event) => { event.stopPropagation() @@ -305,6 +431,21 @@ const mapDispatchToProps = (dispatch) => { showConfigPage: () => dispatch(actions.showConfigPage()), requestAccountExport: () => dispatch(actions.requestExportAccount()), showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)), + showAccountDetailModal: () => { + dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })) + }, + showEditAccountModal: (identity) => { + dispatch(actions.showModal({ + name: 'EDIT_ACCOUNT_NAME', + identity, + })) + }, + showNewAccountModal: () => { + dispatch(actions.showModal({ name: 'NEW_ACCOUNT' })) + }, + showAddTokenPage: () => { + dispatch(actions.showAddTokenPage()) + }, addNewAccount: () => dispatch(actions.addNewAccount()), showImportPage: () => dispatch(actions.showImportPage()), showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)), @@ -312,6 +453,11 @@ const mapDispatchToProps = (dispatch) => { } } -module.exports = { - AccountDropdowns: connect(null, mapDispatchToProps)(AccountDropdowns), +function mapStateToProps (state) { + return { + keyrings: state.metamask.keyrings, + } } + +module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountDropdowns) + diff --git a/ui/app/components/dropdown.js b/ui/app/components/dropdowns/components/dropdown.js index 73710acc2..991c89cb8 100644 --- a/ui/app/components/dropdown.js +++ b/ui/app/components/dropdowns/components/dropdown.js @@ -1,14 +1,22 @@ const Component = require('react').Component const PropTypes = require('react').PropTypes const h = require('react-hyperscript') -const MenuDroppo = require('./menu-droppo') +const MenuDroppo = require('../../menu-droppo') const extend = require('xtend') const noop = () => {} class Dropdown extends Component { render () { - const { isOpen, onClickOutside, style, innerStyle, children, useCssTransition } = this.props + const { + containerClassName, + isOpen, + onClickOutside, + style, + innerStyle, + children, + useCssTransition, + } = this.props const innerStyleDefaults = extend({ borderRadius: '4px', @@ -20,9 +28,10 @@ class Dropdown extends Component { return h( MenuDroppo, { + containerClassName, useCssTransition, isOpen, - zIndex: 11, + zIndex: 30, onClickOutside, style, innerStyle: innerStyleDefaults, @@ -31,8 +40,12 @@ class Dropdown extends Component { h( 'style', ` - li.dropdown-menu-item:hover { color:rgb(225, 225, 225); } - li.dropdown-menu-item { color: rgb(185, 185, 185); position: relative } + li.dropdown-menu-item:hover { + color:rgb(225, 225, 225); + background-color: rgba(255, 255, 255, 0.05); + border-radius: 4px; + } + li.dropdown-menu-item { color: rgb(185, 185, 185); } ` ), ...children, @@ -67,7 +80,7 @@ class DropdownMenuItem extends Component { }, style: Object.assign({ listStyle: 'none', - padding: '8px 0px 8px 0px', + padding: '8px 0px', fontSize: '18px', fontStyle: 'normal', fontFamily: 'Montserrat Regular', @@ -75,6 +88,7 @@ class DropdownMenuItem extends Component { display: 'flex', justifyContent: 'flex-start', alignItems: 'center', + color: 'white', }, style), }, children diff --git a/ui/app/components/dropdowns/components/network-dropdown-icon.js b/ui/app/components/dropdowns/components/network-dropdown-icon.js new file mode 100644 index 000000000..7e94e0af5 --- /dev/null +++ b/ui/app/components/dropdowns/components/network-dropdown-icon.js @@ -0,0 +1,28 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') + + +inherits(NetworkDropdownIcon, Component) +module.exports = NetworkDropdownIcon + +function NetworkDropdownIcon () { + Component.call(this) +} + +NetworkDropdownIcon.prototype.render = function () { + const { + backgroundColor, + isSelected, + innerBorder = 'none', + } = this.props + + return h(`.menu-icon-circle${isSelected ? '--active' : ''}`, {}, + h('div', { + style: { + background: backgroundColor, + border: innerBorder, + }, + }) + ) +} diff --git a/ui/app/components/dropdowns/index.js b/ui/app/components/dropdowns/index.js new file mode 100644 index 000000000..fa66f5000 --- /dev/null +++ b/ui/app/components/dropdowns/index.js @@ -0,0 +1,17 @@ +// Reusable Dropdown Components +// TODO: Refactor into separate components +const Dropdown = require('./components/dropdown').Dropdown +const AccountDropdowns = require('./components/account-dropdowns') + +// App-Specific Instances +const AccountSelectionDropdown = require('./account-selection-dropdown') +const AccountOptionsDropdown = require('./account-options-dropdown') +const NetworkDropdown = require('./network-dropdown').default + +module.exports = { + AccountSelectionDropdown, + AccountOptionsDropdown, + NetworkDropdown, + Dropdown, + AccountDropdowns, +} diff --git a/ui/app/components/dropdowns/network-dropdown.js b/ui/app/components/dropdowns/network-dropdown.js new file mode 100644 index 000000000..4c578fbeb --- /dev/null +++ b/ui/app/components/dropdowns/network-dropdown.js @@ -0,0 +1,315 @@ +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 Dropdown = require('./components/dropdown').Dropdown +const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem +const NetworkDropdownIcon = require('./components/network-dropdown-icon') + +function mapStateToProps (state) { + return { + provider: state.metamask.provider, + frequentRpcList: state.metamask.frequentRpcList || [], + networkDropdownOpen: state.appState.networkDropdownOpen, + } +} + +function mapDispatchToProps (dispatch) { + return { + hideModal: () => { + dispatch(actions.hideModal()) + }, + setProviderType: (type) => { + dispatch(actions.setProviderType(type)) + }, + setDefaultRpcTarget: type => { + dispatch(actions.setDefaultRpcTarget(type)) + }, + setRpcTarget: (target) => { + dispatch(actions.setRpcTarget(target)) + }, + showConfigPage: () => { + dispatch(actions.showConfigPage()) + }, + showNetworkDropdown: () => { dispatch(actions.showNetworkDropdown()) }, + hideNetworkDropdown: () => { dispatch(actions.hideNetworkDropdown()) }, + } +} + + +inherits(NetworkDropdown, Component) +function NetworkDropdown () { + Component.call(this) +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)(NetworkDropdown) + +// TODO: specify default props and proptypes +NetworkDropdown.prototype.render = function () { + const props = this.props + const { provider: { type: providerType, rpcTarget: activeNetwork } } = props + const rpcList = props.frequentRpcList + const isOpen = this.props.networkDropdownOpen + const dropdownMenuItemStyle = { + fontFamily: 'DIN OT', + fontSize: '16px', + lineHeight: '20px', + } + + return h(Dropdown, { + useCssTransition: true, + isOpen, + onClickOutside: (event) => { + const { classList } = event.target + const isNotToggleElement = [ + classList.contains('menu-icon'), + classList.contains('network-name'), + classList.contains('network-indicator'), + ].filter(bool => bool).length === 0 + // classes from three constituent nodes of the toggle element + + if (isNotToggleElement) { + this.props.hideNetworkDropdown() + } + }, + containerClassName: 'network-droppo', + zIndex: 11, + style: { + position: 'absolute', + top: '38px', + minWidth: '309px', + }, + innerStyle: { + padding: '10px 8px', + }, + }, [ + + h('div.network-dropdown-header', {}, [ + h('div.network-dropdown-title', {}, 'Networks'), + + h('div.network-dropdown-divider'), + + h('div.network-dropdown-content', + {}, + 'The default network for Ether transactions is Main Net.' + ), + ]), + + h( + DropdownMenuItem, + { + key: 'main', + closeMenu: () => this.props.hideNetworkDropdown(), + onClick: () => props.setProviderType('mainnet'), + style: dropdownMenuItemStyle, + }, + [ + providerType === 'mainnet' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'), + h(NetworkDropdownIcon, { + backgroundColor: '#038789', // $blue-lagoon + isSelected: providerType === 'mainnet', + }), + h('span.network-name-item', { + style: { + color: providerType === 'mainnet' ? '#ffffff' : '#9b9b9b', + }, + }, 'Main Ethereum Network'), + ] + ), + + h( + DropdownMenuItem, + { + key: 'ropsten', + closeMenu: () => this.props.hideNetworkDropdown(), + onClick: () => props.setProviderType('ropsten'), + style: dropdownMenuItemStyle, + }, + [ + providerType === 'ropsten' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'), + h(NetworkDropdownIcon, { + backgroundColor: '#e91550', // $crimson + isSelected: providerType === 'ropsten', + }), + h('span.network-name-item', { + style: { + color: providerType === 'ropsten' ? '#ffffff' : '#9b9b9b', + }, + }, 'Ropsten Test Network'), + ] + ), + + h( + DropdownMenuItem, + { + key: 'kovan', + closeMenu: () => this.props.hideNetworkDropdown(), + onClick: () => props.setProviderType('kovan'), + style: dropdownMenuItemStyle, + }, + [ + providerType === 'kovan' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'), + h(NetworkDropdownIcon, { + backgroundColor: '#690496', // $purple + isSelected: providerType === 'kovan', + }), + h('span.network-name-item', { + style: { + color: providerType === 'kovan' ? '#ffffff' : '#9b9b9b', + }, + }, 'Kovan Test Network'), + ] + ), + + h( + DropdownMenuItem, + { + key: 'rinkeby', + closeMenu: () => this.props.hideNetworkDropdown(), + onClick: () => props.setProviderType('rinkeby'), + style: dropdownMenuItemStyle, + }, + [ + providerType === 'rinkeby' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'), + h(NetworkDropdownIcon, { + backgroundColor: '#ebb33f', // $tulip-tree + isSelected: providerType === 'rinkeby', + }), + h('span.network-name-item', { + style: { + color: providerType === 'rinkeby' ? '#ffffff' : '#9b9b9b', + }, + }, 'Rinkeby Test Network'), + ] + ), + + h( + DropdownMenuItem, + { + key: 'default', + closeMenu: () => this.props.hideNetworkDropdown(), + onClick: () => props.setDefaultRpcTarget(), + style: dropdownMenuItemStyle, + }, + [ + activeNetwork === 'http://localhost:8545' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'), + h(NetworkDropdownIcon, { + isSelected: activeNetwork === 'http://localhost:8545', + innerBorder: '1px solid #9b9b9b', + }), + h('span.network-name-item', { + style: { + color: activeNetwork === 'http://localhost:8545' ? '#ffffff' : '#9b9b9b', + }, + }, 'Localhost 8545'), + ] + ), + + this.renderCustomOption(props.provider), + this.renderCommonRpc(rpcList, props.provider), + + h( + DropdownMenuItem, + { + closeMenu: () => this.props.hideNetworkDropdown(), + onClick: () => this.props.showConfigPage(), + style: dropdownMenuItemStyle, + }, + [ + activeNetwork === 'custom' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'), + h(NetworkDropdownIcon, { + isSelected: activeNetwork === 'custom', + innerBorder: '1px solid #9b9b9b', + }), + h('span.network-name-item', { + style: { + color: activeNetwork === 'custom' ? '#ffffff' : '#9b9b9b', + }, + }, 'Custom RPC'), + ] + ), + + ]) +} + + +NetworkDropdown.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 +} + +NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) { + const props = this.props + const rpcTarget = provider.rpcTarget + + return rpcList.map((rpc) => { + if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { + return null + } else { + return h( + DropdownMenuItem, + { + key: `common${rpc}`, + closeMenu: () => this.props.hideNetworkDropdown(), + onClick: () => props.setRpcTarget(rpc), + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + rpc, + rpcTarget === rpc ? h('.check', '✓') : null, + ] + ) + } + }) +} + +NetworkDropdown.prototype.renderCustomOption = function (provider) { + const { rpcTarget, type } = provider + const props = this.props + + 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( + DropdownMenuItem, + { + key: rpcTarget, + onClick: () => props.setRpcTarget(rpcTarget), + closeMenu: () => this.props.hideNetworkDropdown(), + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + label, + h('.check', '✓'), + ] + ) + } +} diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index 3a33ebf74..fb8c8e579 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -44,7 +44,7 @@ EnsInput.prototype.render = function () { return h('div', { style: { width: '100%' }, }, [ - h('input.large-input', opts), + h('input.large-input.send-screen-input', opts), // The address book functionality. h('datalist#addresses', [ @@ -125,7 +125,7 @@ EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { EnsInput.prototype.ensIcon = function (recipient) { const { hoverText } = this.state || {} - return h('span', { + return h('span.#ensIcon', { title: hoverText, style: { position: 'absolute', diff --git a/ui/app/components/eth-balance.js b/ui/app/components/eth-balance.js index 4f538fd31..1be8c9731 100644 --- a/ui/app/components/eth-balance.js +++ b/ui/app/components/eth-balance.js @@ -1,8 +1,10 @@ -const Component = require('react').Component +const { Component } = require('react') const h = require('react-hyperscript') -const inherits = require('util').inherits -const formatBalance = require('../util').formatBalance -const generateBalanceObject = require('../util').generateBalanceObject +const { inherits } = require('util') +const { + formatBalance, + generateBalanceObject, +} = require('../util') const Tooltip = require('./tooltip.js') const FiatValue = require('./fiat-value.js') @@ -14,11 +16,10 @@ function EthBalanceComponent () { } 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) : '...' + const props = this.props + const { value, style, width, needsParse = true } = props + + const formattedValue = value ? formatBalance(value, 6, needsParse) : '...' return ( @@ -30,60 +31,66 @@ EthBalanceComponent.prototype.render = function () { display: 'inline', width, }, - }, this.renderBalance(value)), + }, this.renderBalance(formattedValue)), ]) ) } 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 - } + const { + conversionRate, + shorten, + incoming, + currentCurrency, + hideTooltip, + styleOveride, + showFiat = true, + } = this.props + const { fontSize, color, fontFamily, lineHeight } = styleOveride - var label = balanceObj.label + const { shortBalance, balance, label } = generateBalanceObject(value, shorten ? 1 : 3) + const balanceToRender = shorten ? shortBalance : balance + + const [ethNumber, ethSuffix] = value.split(' ') + const containerProps = hideTooltip ? {} : { + position: 'bottom', + title: `${ethNumber} ${ethSuffix}`, + } 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', { + h(hideTooltip ? 'div' : Tooltip, + containerProps, + h('div.flex-column', [ + h('.flex-row', { style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', + alignItems: 'flex-end', + lineHeight: lineHeight || '13px', + fontFamily: fontFamily || 'Montserrat Light', + textRendering: 'geometricPrecision', }, - }, label), - ]), + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + fontSize: fontSize || 'inherit', + color: color || 'inherit', + }, + }, incoming ? `+${balanceToRender}` : balanceToRender), + h('div', { + style: { + color: color || '#AEAEAE', + fontSize: fontSize || '12px', + marginLeft: '5px', + }, + }, label), + ]), - showFiat ? h(FiatValue, { value: props.value, conversionRate, currentCurrency }) : null, - ])) + showFiat ? h(FiatValue, { value: this.props.value, conversionRate, currentCurrency }) : null, + ]) + ) ) } diff --git a/ui/app/components/fiat-value.js b/ui/app/components/fiat-value.js index 8a64a1cfc..665789353 100644 --- a/ui/app/components/fiat-value.js +++ b/ui/app/components/fiat-value.js @@ -12,7 +12,7 @@ function FiatValue () { FiatValue.prototype.render = function () { const props = this.props - const { conversionRate, currentCurrency } = props + const { conversionRate, currentCurrency, style } = props const value = formatBalance(props.value, 6) @@ -28,16 +28,18 @@ FiatValue.prototype.render = function () { fiatTooltipNumber = 'Unknown' } - return fiatDisplay(fiatDisplayNumber, currentCurrency) + return fiatDisplay(fiatDisplayNumber, currentCurrency, style) } -function fiatDisplay (fiatDisplayNumber, fiatSuffix) { +function fiatDisplay (fiatDisplayNumber, fiatSuffix, styleOveride = {}) { + const { fontSize, color, fontFamily, lineHeight } = styleOveride + if (fiatDisplayNumber !== 'N/A') { return h('.flex-row', { style: { alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', + lineHeight: lineHeight || '13px', + fontFamily: fontFamily || 'Montserrat Light', textRendering: 'geometricPrecision', }, }, [ @@ -45,15 +47,15 @@ function fiatDisplay (fiatDisplayNumber, fiatSuffix) { style: { width: '100%', textAlign: 'right', - fontSize: '12px', - color: '#333333', + fontSize: fontSize || '12px', + color: color || '#333333', }, }, fiatDisplayNumber), h('div', { style: { - color: '#AEAEAE', + color: color || '#AEAEAE', marginLeft: '5px', - fontSize: '12px', + fontSize: fontSize || '12px', }, }, fiatSuffix), ]) diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js index c754bc6ba..259fa4d73 100644 --- a/ui/app/components/identicon.js +++ b/ui/app/components/identicon.js @@ -18,21 +18,35 @@ function IdenticonComponent () { IdenticonComponent.prototype.render = function () { var props = this.props + const { className = '', address } = 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', - }, - }) - ) + + return address + ? ( + h('div', { + className: `${className} identicon`, + key: 'identicon-' + address, + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: diameter, + width: diameter, + borderRadius: diameter / 2, + overflow: 'hidden', + }, + }) + ) + : ( + h('img.balance-icon', { + src: '../images/eth_logo.svg', + style: { + height: diameter, + width: diameter, + borderRadius: diameter / 2, + }, + }) + ) } IdenticonComponent.prototype.componentDidMount = function () { diff --git a/ui/app/components/input-number.js b/ui/app/components/input-number.js new file mode 100644 index 000000000..2824d77aa --- /dev/null +++ b/ui/app/components/input-number.js @@ -0,0 +1,57 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = InputNumber + +inherits(InputNumber, Component) +function InputNumber () { + Component.call(this) + + this.state = { + value: 0, + } + + this.setValue = this.setValue.bind(this) +} + +InputNumber.prototype.componentWillMount = function () { + const { initValue = 0 } = this.props + + this.setState({ value: initValue }) +} + +InputNumber.prototype.setValue = function (newValue) { + const { fixed, min = -1, onChange } = this.props + + if (fixed) newValue = Number(newValue.toFixed(4)) + + if (newValue >= min) { + this.setState({ value: newValue }) + onChange(newValue) + } +} + +InputNumber.prototype.render = function () { + const { unitLabel, step = 1, placeholder } = this.props + const { value } = this.state + + return h('div.customize-gas-input-wrapper', {}, [ + h('input.customize-gas-input', { + placeholder, + type: 'number', + value, + onChange: (e) => this.setValue(Number(e.target.value)), + }), + h('span.gas-tooltip-input-detail', {}, [unitLabel]), + h('div.gas-tooltip-input-arrows', {}, [ + h('i.fa.fa-angle-up', { + onClick: () => this.setValue(value + step), + }), + h('i.fa.fa-angle-down', { + style: { cursor: 'pointer' }, + onClick: () => this.setValue(value - step), + }), + ]), + ]) +} diff --git a/ui/app/components/menu-droppo.js b/ui/app/components/menu-droppo.js index 66ab18954..95b75f54c 100644 --- a/ui/app/components/menu-droppo.js +++ b/ui/app/components/menu-droppo.js @@ -13,21 +13,23 @@ function MenuDroppoComponent () { } MenuDroppoComponent.prototype.render = function () { + const { containerClassName = '' } = this.props const speed = this.props.speed || '300ms' const useCssTransition = this.props.useCssTransition const zIndex = ('zIndex' in this.props) ? this.props.zIndex : 0 this.manageListeners() - let style = this.props.style || {} + const style = this.props.style || {} if (!('position' in style)) { style.position = 'fixed' } style.zIndex = zIndex return ( - h('.menu-droppo-container', { + h('div', { style, + className: `.menu-droppo-container ${containerClassName}`, }, [ h('style', ` .menu-droppo-enter { diff --git a/ui/app/components/modals/account-details-modal.js b/ui/app/components/modals/account-details-modal.js new file mode 100644 index 000000000..ec7e4b500 --- /dev/null +++ b/ui/app/components/modals/account-details-modal.js @@ -0,0 +1,82 @@ +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 { getSelectedIdentity, getSelectedAddress } = require('../../selectors') +const genAccountLink = require('../../../lib/account-link.js') +const Identicon = require('../identicon') +const QrView = require('../qr-code') + +function mapStateToProps (state) { + return { + network: state.metamask.network, + address: state.metamask.selectedAddress, + selectedAddress: getSelectedAddress(state), + selectedIdentity: getSelectedIdentity(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + hideModal: () => { + dispatch(actions.hideModal()) + }, + showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)), + } +} + +inherits(AccountDetailsModal, Component) +function AccountDetailsModal () { + Component.call(this) +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountDetailsModal) + +// Not yet pixel perfect todos: + // fonts of qr-header and close button + +AccountDetailsModal.prototype.render = function () { + const { selectedIdentity, network } = this.props + + return h('div', { style: { borderRadius: '4px' }}, [ + h('div.account-details-modal-wrapper', [ + + h('div', [ + + // Needs a border; requires changes to svg + h(Identicon, { + address: selectedIdentity.address, + diameter: 64, + style: {}, + }), + + ]), + + h('div.account-details-modal-close', { + onClick: this.props.hideModal, + }), + + h(QrView, { + Qr: { + message: this.props.selectedIdentity.name, + data: this.props.selectedIdentity.address, + }, + }), + + // divider + h('div.account-details-modal-divider'), + + h('button.btn-clear', { + onClick: () => { + const url = genAccountLink(selectedIdentity.address, network) + global.platform.openWindow({ url }) + }, + }, [ 'View account on Etherscan' ]), + + // Holding on redesign for Export Private Key functionality + h('button.btn-clear', [ 'Export private key' ]), + + ]), + ]) +} diff --git a/ui/app/components/modals/buy-options-modal.js b/ui/app/components/modals/buy-options-modal.js new file mode 100644 index 000000000..79bbc798b --- /dev/null +++ b/ui/app/components/modals/buy-options-modal.js @@ -0,0 +1,77 @@ +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') + +function mapStateToProps (state) { + return { + network: state.metamask.network, + address: state.metamask.selectedAddress, + } +} + +function mapDispatchToProps (dispatch) { + return { + toCoinbase: (address) => { + dispatch(actions.buyEth({ network: '1', address, amount: 0 })) + }, + hideModal: () => { + dispatch(actions.hideModal()) + }, + } +} + +inherits(BuyOptions, Component) +function BuyOptions () { + Component.call(this) +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)(BuyOptions) + +BuyOptions.prototype.render = function () { + return h('div', {}, [ + h('div.buy-modal-content.transfers-subview', { + }, [ + h('div.buy-modal-content-title-wrapper.flex-column.flex-center', { + style: {}, + }, [ + h('div.buy-modal-content-title', { + style: {}, + }, 'Transfers'), + h('div', {}, 'How would you like to buy Ether?'), + ]), + + h('div.buy-modal-content-options.flex-column.flex-center', {}, [ + + h('div.buy-modal-content-option', { + onClick: () => { + const { toCoinbase, address } = this.props + toCoinbase(address) + }, + }, [ + h('div.buy-modal-content-option-title', {}, 'Coinbase'), + h('div.buy-modal-content-option-subtitle', {}, 'Buy with Fiat'), + ]), + + h('div.buy-modal-content-option', {}, [ + h('div.buy-modal-content-option-title', {}, 'Shapeshift'), + h('div.buy-modal-content-option-subtitle', {}, 'Trade any digital asset for any other'), + ]), + + h('div.buy-modal-content-option', {}, [ + h('div.buy-modal-content-option-title', {}, 'Direct Deposit'), + h('div.buy-modal-content-option-subtitle', {}, 'Deposit from another account'), + ]), + + ]), + + h('button', { + style: { + background: 'white', + }, + onClick: () => { this.props.hideModal() }, + }, h('div.buy-modal-content-footer#buy-modal-content-footer-text', {}, 'Cancel')), + ]), + ]) +} diff --git a/ui/app/components/modals/edit-account-name-modal.js b/ui/app/components/modals/edit-account-name-modal.js new file mode 100644 index 000000000..5c25ac245 --- /dev/null +++ b/ui/app/components/modals/edit-account-name-modal.js @@ -0,0 +1,76 @@ +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 { getSelectedAccount } = require('../../selectors') + +function mapStateToProps (state) { + return { + selectedAccount: getSelectedAccount(state), + identity: state.appState.modal.modalState.identity, + } +} + +function mapDispatchToProps (dispatch) { + return { + hideModal: () => { + dispatch(actions.hideModal()) + }, + saveAccountLabel: (account, label) => { + dispatch(actions.saveAccountLabel(account, label)) + }, + } +} + +inherits(EditAccountNameModal, Component) +function EditAccountNameModal () { + Component.call(this) + this.state = { + inputText: '', + } +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)(EditAccountNameModal) + +EditAccountNameModal.prototype.render = function () { + const { hideModal, saveAccountLabel, identity } = this.props + + return h('div', {}, [ + h('div.flex-column.edit-account-name-modal-content', { + }, [ + + h('div.edit-account-name-modal-cancel', { + onClick: () => { + hideModal() + }, + }, [ + h('i.fa.fa-times'), + ]), + + h('div.edit-account-name-modal-title', { + }, ['Edit Account Name']), + + h('input.edit-account-name-modal-input', { + placeholder: identity.name, + onChange: (event) => { + this.setState({ inputText: event.target.value }) + }, + value: this.state.inputText, + }, []), + + h('button.btn-clear.edit-account-name-modal-save-button', { + onClick: () => { + if (this.state.inputText.length !== 0) { + saveAccountLabel(identity.address, this.state.inputText) + hideModal() + } + }, + disabled: this.state.inputText.length === 0, + }, [ + 'SAVE', + ]), + + ]), + ]) +} diff --git a/ui/app/components/modals/index.js b/ui/app/components/modals/index.js new file mode 100644 index 000000000..1db1d33d4 --- /dev/null +++ b/ui/app/components/modals/index.js @@ -0,0 +1,5 @@ +const Modal = require('./modal') + +module.exports = { + Modal, +} diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js new file mode 100644 index 000000000..477dcbe86 --- /dev/null +++ b/ui/app/components/modals/modal.js @@ -0,0 +1,162 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const FadeModal = require('boron').FadeModal +const actions = require('../../actions') +const isMobileView = require('../../../lib/is-mobile-view') +const isPopupOrNotification = require('../../../../app/scripts/lib/is-popup-or-notification') + +// Modal Components +const BuyOptions = require('./buy-options-modal') +const AccountDetailsModal = require('./account-details-modal') +const EditAccountNameModal = require('./edit-account-name-modal') +const NewAccountModal = require('./new-account-modal') + +const MODALS = { + BUY: { + contents: [ + h(BuyOptions, {}, []), + ], + mobileModalStyle: { + width: '95%', + top: isPopupOrNotification() === 'popup' ? '48vh' : '36.5vh', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + }, + laptopModalStyle: { + width: '66%', + top: 'calc(30% + 10px)', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + }, + }, + + EDIT_ACCOUNT_NAME: { + contents: [ + h(EditAccountNameModal, {}, []), + ], + mobileModalStyle: { + width: '95%', + top: isPopupOrNotification() === 'popup' ? '48vh' : '36.5vh', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + }, + laptopModalStyle: { + width: '375px', + top: 'calc(30% + 10px)', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + }, + }, + + ACCOUNT_DETAILS: { + contents: [ + h(AccountDetailsModal, {}, []), + ], + mobileModalStyle: { + width: '95%', + top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + borderRadius: '4px', + }, + laptopModalStyle: { + width: '360px', + top: 'calc(33% + 45px)', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + borderRadius: '4px', + }, + contentStyle: { + borderRadius: '4px', + } + }, + + NEW_ACCOUNT: { + contents: [ + h(NewAccountModal, {}, []), + ], + mobileModalStyle: { + width: '95%', + top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh', + }, + laptopModalStyle: { + width: '449px', + top: 'calc(33% + 45px)', + }, + }, + + DEFAULT: { + contents: [], + mobileModalStyle: {}, + laptopModalStyle: {}, + }, +} + +const BACKDROPSTYLE = { + backgroundColor: 'rgba(245, 245, 245, 0.85)', +} + +function mapStateToProps (state) { + return { + active: state.appState.modal.open, + modalState: state.appState.modal.modalState, + } +} + +function mapDispatchToProps (dispatch) { + return { + hideModal: () => { + dispatch(actions.hideModal()) + }, + } +} + +// Global Modal Component +inherits(Modal, Component) +function Modal () { + Component.call(this) +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)(Modal) + +Modal.prototype.render = function () { + const modal = MODALS[this.props.modalState.name || 'DEFAULT'] + + const children = modal.contents + const modalStyle = modal[isMobileView() ? 'mobileModalStyle' : 'laptopModalStyle'] + const contentStyle = modal.contentStyle || {}; + + return h(FadeModal, + { + className: 'modal', + keyboard: false, + onHide: () => { this.onHide() }, + ref: (ref) => { + this.modalRef = ref + }, + modalStyle, + contentStyle, + backdropStyle: BACKDROPSTYLE, + }, + children, + ) +} + +Modal.prototype.componentWillReceiveProps = function (nextProps) { + if (nextProps.active) { + this.show() + } else if (this.props.active) { + this.hide() + } +} + +Modal.prototype.onHide = function () { + if (this.props.onHideCallback) { + this.props.onHideCallback() + } + this.props.hideModal() +} + +Modal.prototype.hide = function () { + this.modalRef.hide() +} + +Modal.prototype.show = function () { + this.modalRef.show() +} diff --git a/ui/app/components/modals/new-account-modal.js b/ui/app/components/modals/new-account-modal.js new file mode 100644 index 000000000..910f3c0ca --- /dev/null +++ b/ui/app/components/modals/new-account-modal.js @@ -0,0 +1,75 @@ +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') + +function mapStateToProps (state) { + return { + network: state.metamask.network, + address: state.metamask.selectedAddress, + } +} + +function mapDispatchToProps (dispatch) { + return { + toCoinbase: (address) => { + dispatch(actions.buyEth({ network: '1', address, amount: 0 })) + }, + hideModal: () => { + dispatch(actions.hideModal()) + }, + createAccount: () => { + dispatch(actions.addNewAccount()) + dispatch(actions.hideModal()) + }, + } +} + +inherits(NewAccountModal, Component) +function NewAccountModal () { + Component.call(this) +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)(NewAccountModal) + +NewAccountModal.prototype.render = function () { + return h('div', {}, [ + h('div.new-account-modal-wrapper', { + }, [ + h('div.new-account-modal-header', {}, [ + 'New Account', + ]), + + h('div.modal-close-x', { + onClick: this.props.hideModal, + }), + + h('div.new-account-modal-content', {}, [ + 'Account Name', + ]), + + h('div.new-account-input-wrapper', {}, [ + h('input.new-account-input', { + placeholder: 'E.g. My new account', + }, []), + ]), + + h('div.new-account-modal-content.after-input', {}, [ + 'or', + ]), + + h('div.new-account-modal-content.after-input', {}, [ + 'Import an account', + ]), + + h('div.new-account-modal-content.button', {}, [ + h('button.btn-clear', { + onClick: this.props.createAccount + }, [ + 'SAVE', + ]), + ]), + ]), + ]) +} diff --git a/ui/app/components/network.js b/ui/app/components/network.js index 0dbe37cdd..8424a479a 100644 --- a/ui/app/components/network.js +++ b/ui/app/components/network.js @@ -1,6 +1,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits +const NetworkDropdownIcon = require('./dropdowns/components/network-dropdown-icon') module.exports = Network @@ -60,7 +61,7 @@ Network.prototype.render = function () { } return ( - h('#network_component.pointer', { + h('.network-component.pointer', { title: hoverText, onClick: (event) => this.props.onClick(event), }, [ @@ -68,7 +69,10 @@ Network.prototype.render = function () { switch (iconName) { case 'ethereum-network': return h('.network-indicator', [ - h('.menu-icon.diamond'), + h(NetworkDropdownIcon, { + backgroundColor: '#038789', // $blue-lagoon + nonSelectBackgroundColor: '#15afb2' + }), h('.network-name', { style: { color: '#039396', @@ -78,7 +82,10 @@ Network.prototype.render = function () { ]) case 'ropsten-test-network': return h('.network-indicator', [ - h('.menu-icon.red-dot'), + h(NetworkDropdownIcon, { + backgroundColor: '#e91550', // $crimson + nonSelectBackgroundColor: '#ec2c50', + }), h('.network-name', { style: { color: '#ff6666', @@ -88,7 +95,10 @@ Network.prototype.render = function () { ]) case 'kovan-test-network': return h('.network-indicator', [ - h('.menu-icon.hollow-diamond'), + h(NetworkDropdownIcon, { + backgroundColor: '#690496', // $purple + nonSelectBackgroundColor: '#b039f3', + }), h('.network-name', { style: { color: '#690496', @@ -98,7 +108,10 @@ Network.prototype.render = function () { ]) case 'rinkeby-test-network': return h('.network-indicator', [ - h('.menu-icon.golden-square'), + h(NetworkDropdownIcon, { + backgroundColor: '#ebb33f', // $tulip-tree + nonSelectBackgroundColor: '#ecb23e', + }), h('.network-name', { style: { color: '#e7a218', diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index c3350fcc1..18b622925 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -1,26 +1,57 @@ const Component = require('react').Component +const { connect } = require('react-redux') const h = require('react-hyperscript') +const abi = require('human-standard-token-abi') +const abiDecoder = require('abi-decoder') +abiDecoder.addABI(abi) const inherits = require('util').inherits const actions = require('../actions') const clone = require('clone') +const FiatValue = require('./fiat-value') +const Identicon = require('./identicon') 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 { conversionUtil } = require('../conversion-util') const MIN_GAS_PRICE_GWEI_BN = new BN(1) const GWEI_FACTOR = new BN(1e9) const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) -const MIN_GAS_LIMIT_BN = new BN(21000) -module.exports = PendingTx +// Next: create separate react components +// roughly 5 components: +// heroIcon +// numericDisplay (contains symbol + currency) +// divider +// contentBox +// actionButtons + +module.exports = connect(mapStateToProps, mapDispatchToProps)(PendingTx) + +function mapStateToProps (state) { + const { + conversionRate, + identities, + } = state.metamask + const accounts = state.metamask.accounts + const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] + return { + conversionRate, + identities, + selectedAddress, + } +} + +function mapDispatchToProps (dispatch) { + return { + setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('USD')), + backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)), + cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })), + } +} + inherits(PendingTx, Component) function PendingTx () { Component.call(this) @@ -29,412 +60,444 @@ function PendingTx () { txData: null, submitting: false, } + this.onSubmit = this.onSubmit.bind(this) } -PendingTx.prototype.render = function () { - const props = this.props - const { currentCurrency, blockGasLimit } = props +PendingTx.prototype.componentWillMount = function () { + this.props.setCurrentCurrencyToUSD() +} - const conversionRate = props.conversionRate +PendingTx.prototype.getTotal = function () { + const { conversionRate } = this.props const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} + const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data) + const { params = [] } = decodedData || {} + const { name, value } = params[1] || {} + const amountBn = name === '_value' + ? value + : txParams.value + + if (name === '_value') { + const token = util.getContractAtAddress(txParams.to) + token.symbol().then(symbol => console.log({symbol})) + console.log({txParams, txMeta, decodedData, token}) + const USD = conversionUtil(amountBn, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromCurrency: 'ETH', + toCurrency: 'USD', + numberOfDecimals: 2, + fromDenomination: 'WEI', + conversionRate, + }) + const ETH = conversionUtil(amountBn, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromCurrency: 'ETH', + toCurrency: 'ETH', + fromDenomination: 'WEI', + conversionRate, + numberOfDecimals: 6, + }) + return { + USD, + ETH, + } + } else { + const USD = conversionUtil(amountBn, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromCurrency: 'ETH', + toCurrency: 'USD', + numberOfDecimals: 2, + fromDenomination: 'WEI', + conversionRate, + }) + const ETH = conversionUtil(amountBn, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromCurrency: 'ETH', + toCurrency: 'ETH', + fromDenomination: 'WEI', + conversionRate, + numberOfDecimals: 6, + }) - // 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' + return { + USD, + ETH, + } + } - // recipient check - const isValidAddress = !txParams.to || util.isValidAddress(txParams.to) +} + +PendingTx.prototype.getGasFee = function () { + const { conversionRate } = this.props + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} // Gas const gas = txParams.gas const gasBn = hexToBn(gas) - const gasLimit = new BN(parseInt(blockGasLimit)) - const safeGasLimitBN = this.bnMultiplyByFraction(gasLimit, 19, 20) - const saferGasLimitBN = this.bnMultiplyByFraction(gasLimit, 18, 20) - const safeGasLimit = safeGasLimitBN.toString(10) + + // From latest master +// const gasLimit = new BN(parseInt(blockGasLimit)) +// const safeGasLimitBN = this.bnMultiplyByFraction(gasLimit, 19, 20) +// const saferGasLimitBN = this.bnMultiplyByFraction(gasLimit, 18, 20) +// const safeGasLimit = safeGasLimitBN.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 USD = conversionUtil(txFeeBn, { + fromNumericBase: 'BN', + toNumericBase: 'dec', + fromDenomination: 'WEI', + fromCurrency: 'ETH', + toCurrency: 'USD', + numberOfDecimals: 2, + conversionRate, + }) + const ETH = conversionUtil(txFeeBn, { + fromNumericBase: 'BN', + toNumericBase: 'dec', + fromDenomination: 'WEI', + fromCurrency: 'ETH', + toCurrency: 'ETH', + numberOfDecimals: 6, + conversionRate, + }) + + return { + USD, + ETH, + } +} + +PendingTx.prototype.getData = function () { + const { identities } = this.props + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data) + const { name, params = [] } = decodedData || {} + const { type, value } = params[0] || {} + const { USD: gasFeeInUSD, ETH: gasFeeInETH } = this.getGasFee() + const { USD: totalInUSD, ETH: totalInETH } = this.getTotal() + + if (name === 'transfer' && type === 'address') { + return { + from: { + address: txParams.from, + name: identities[txParams.from].name, + }, + to: { + address: value, + name: identities[value] ? identities[value].name : 'New Recipient', + }, + memo: txParams.memo || '', + gasFeeInUSD, + gasFeeInETH, + totalInUSD, + totalInETH, + } + } else { + return { + from: { + address: txParams.from, + name: identities[txParams.from].name, + }, + to: { + address: txParams.to, + name: identities[txParams.to] ? identities[txParams.to].name : 'New Recipient', + }, + memo: txParams.memo || '', + gasFeeInUSD, + gasFeeInETH, + totalInUSD, + totalInETH, + } + } +} + +PendingTx.prototype.render = function () { + const { backToAccountDetail, selectedAddress } = this.props + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} - const balanceBn = hexToBn(balance) - const insufficientBalance = balanceBn.lt(maxCost) - const dangerousGasLimit = gasBn.gte(saferGasLimitBN) - const gasLimitSpecified = txMeta.gasLimitSpecified - const buyDisabled = insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting - const showRejectAll = props.unconfTxListLength > 1 + // recipient check + // const isValidAddress = !txParams.to || util.isValidAddress(txParams.to) + + const { + from: { + address: fromAddress, + name: fromName, + }, + to: { + address: toAddress, + name: toName, + }, + memo, + gasFeeInUSD, + gasFeeInETH, + totalInUSD, + totalInETH, + } = this.getData() + + // This is from the latest master + // It handles some of the errors that we are not currently handling + // Leaving as comments fo reference + + // const balanceBn = hexToBn(balance) + // const insufficientBalance = balanceBn.lt(maxCost) + // const buyDisabled = insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting + // const showRejectAll = props.unconfTxListLength > 1 +// const dangerousGasLimit = gasBn.gte(saferGasLimitBN) +// const gasLimitSpecified = txMeta.gasLimitSpecified this.inputs = [] return ( - - h('div', { - key: txMeta.id, + h('div.flex-column.flex-grow.confirm-screen-container', { + style: { minWidth: '355px' }, }, [ + // Main Send token Card + h('div.confirm-screen-wrapper.flex-column.flex-grow', [ + h('h3.flex-center.confirm-screen-header', [ + h('button.confirm-screen-back-button', { + onClick: () => backToAccountDetail(selectedAddress), + }, 'BACK'), + h('div.confirm-screen-title', 'Confirm Transaction'), + ]), + h('div.flex-row.flex-center.confirm-screen-identicons', [ + h('div.confirm-screen-account-wrapper', [ + h( + Identicon, + { + address: fromAddress, + diameter: 100, + }, + ), + h('span.confirm-screen-account-name', fromName), + h('span.confirm-screen-account-number', fromAddress.slice(fromAddress.length - 4)), + ]), + h('i.fa.fa-arrow-right.fa-lg'), + h('div.confirm-screen-account-wrapper', [ + h( + Identicon, + { + address: txParams.to, + diameter: 100, + }, + ), + h('span.confirm-screen-account-name', toName), + h('span.confirm-screen-account-number', toAddress.slice(toAddress.length - 4)), + ]), + ]), - h('form#pending-tx-form', { - onSubmit: this.onSubmit.bind(this), + h('h3.flex-center.confirm-screen-sending-to-message', { + style: { + textAlign: 'center', + fontSize: '16px', + }, + }, [ + `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`, + ]), - }, [ + h('h3.flex-center.confirm-screen-send-amount', [`$${totalInUSD}`]), + h('h3.flex-center.confirm-screen-send-amount-currency', [ 'USD' ]), + h('div.flex-center.confirm-memo-wrapper', [ + h('h3.confirm-screen-send-memo', [ memo ]), + ]), - // 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', - }), - ]), + h('div.confirm-screen-rows', [ + h('section.flex-row.flex-center.confirm-screen-row', [ + h('span.confirm-screen-label.confirm-screen-section-column', [ 'From' ]), + h('div.confirm-screen-section-column', [ + h('div.confirm-screen-row-info', fromName), + h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`), ]), - - 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 }), + h('section.flex-row.flex-center.confirm-screen-row', [ + h('span.confirm-screen-label.confirm-screen-section-column', [ 'To' ]), + h('div.confirm-screen-section-column', [ + h('div.confirm-screen-row-info', toName), + h('div.confirm-screen-row-detail', `...${toAddress.slice(toAddress.length - 4)}`), ]), + ]), - // 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) }, - }), - ]), - ]), + h('section.flex-row.flex-center.confirm-screen-row', [ + h('span.confirm-screen-label.confirm-screen-section-column', [ 'Gas Fee' ]), + h('div.confirm-screen-section-column', [ + h('div.confirm-screen-row-info', `$${gasFeeInUSD} USD`), - // 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) }, - }), - ]), + h('div.confirm-screen-row-detail', `${gasFeeInETH} ETH`), ]), + ]), - // 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', - }), - ]), + h('section.flex-row.flex-center.confirm-screen-total-box ', [ + h('div.confirm-screen-section-column', [ + h('span.confirm-screen-label', [ 'Total ' ]), + h('div.confirm-screen-total-box__subtitle', [ 'Amount + Gas' ]), ]), - // 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`), + h('div.confirm-screen-section-column', [ + h('div.confirm-screen-row-info', `$${totalInUSD} USD`), + h('div.confirm-screen-row-detail', `${totalInETH} ETH`), ]), - ]), // End of Table - - ]), - - h('style', ` - .conf-buttons button { - margin-left: 10px; - text-transform: uppercase; - } - `), - h('.cell.row', { - style: { - textAlign: 'center', - }, - }, [ - txMeta.simulationFails ? - h('.error', { - style: { - fontSize: '0.9em', - }, - }, 'Transaction Error. Exception thrown in contract code.') - : null, - - !isValidAddress ? - h('.error', { - style: { - fontSize: '0.9em', - }, - }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.') - : null, - - insufficientBalance ? - h('span.error', { - style: { - fontSize: '0.9em', - }, - }, 'Insufficient balance for transaction') - : null, - - (dangerousGasLimit && !gasLimitSpecified) ? - h('span.error', { - style: { - fontSize: '0.9em', - }, - }, 'Gas limit set dangerously high. Approving this transaction is likely to fail.') - : null, + ]), ]), - - // send + cancel - h('.flex-row.flex-space-around.conf-buttons', { - style: { - display: 'flex', - justifyContent: 'flex-end', - margin: '14px 25px', - }, - }, [ - h('button', { - onClick: (event) => { - this.resetGasFields() - event.preventDefault() - }, - }, 'Reset'), - - // Accept Button or Buy Button - insufficientBalance ? h('button.btn-green', { onClick: props.buyEth }, 'Buy Ether') : - h('input.confirm.btn-green', { - type: 'submit', - value: 'SUBMIT', - style: { marginLeft: '10px' }, - disabled: buyDisabled, - }), - - h('button.cancel.btn-red', { - onClick: props.cancelTransaction, - }, 'Reject'), - ]), - showRejectAll ? h('.flex-row.flex-space-around.conf-buttons', { - style: { - display: 'flex', - justifyContent: 'flex-end', - margin: '14px 25px', - }, - }, [ - h('button.cancel.btn-red', { - onClick: props.cancelAllTransactions, - }, 'Reject All'), - ]) : null, +// These are latest errors handling from master +// Leaving as comments as reference when we start implementing error handling +// 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', +// }, +// }, [ +// h('button', { +// onClick: (event) => { +// this.resetGasFields() +// event.preventDefault() +// }, +// }, 'Reset'), + +// // Accept Button or Buy Button +// insufficientBalance ? h('button.btn-green', { onClick: props.buyEth }, 'Buy Ether') : +// h('input.confirm.btn-green', { +// type: 'submit', +// value: 'SUBMIT', +// style: { marginLeft: '10px' }, +// disabled: buyDisabled, +// }), + +// h('button.cancel.btn-red', { +// onClick: props.cancelTransaction, +// }, 'Reject'), +// ]), +// showRejectAll ? h('.flex-row.flex-space-around.conf-buttons', { +// style: { +// display: 'flex', +// justifyContent: 'flex-end', +// margin: '14px 25px', +// }, +// }, [ +// h('button.cancel.btn-red', { +// onClick: props.cancelAllTransactions, +// }, 'Reject All'), +// ]) : null, +// ]), +// ]) +// ) +// } ]), - ]) - ) -} - -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('form#pending-tx-form.flex-column.flex-center', { + onSubmit: this.onSubmit, }, [ - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, addressSummary(txParams.to, 6, 4, false)), + // Reset Button + // h('button', { + // onClick: (event) => { + // this.resetGasFields() + // event.preventDefault() + // }, + // }, 'Reset'), + + // Accept Button + h('button.confirm-screen-confirm-button', ['CONFIRM']), + + // Cancel Button + h('div.cancel.btn-light.confirm-screen-cancel-button', { + onClick: (event) => this.cancel(event, txMeta), + }, 'CANCEL'), ]), - ]) - } 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.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 { @@ -443,6 +506,11 @@ PendingTx.prototype.onSubmit = function (event) { } } +PendingTx.prototype.cancel = function (event, txMeta) { + event.preventDefault() + this.props.cancelTransaction(txMeta) +} + PendingTx.prototype.checkValidity = function () { const form = this.getFormEl() const valid = form.checkValidity() @@ -460,12 +528,11 @@ PendingTx.prototype.getFormEl = function () { // 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)}`) + // log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) return txData } @@ -487,15 +554,3 @@ PendingTx.prototype.bnMultiplyByFraction = function (targetBN, numerator, denomi 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 index 06b9aed9b..4257c1a15 100644 --- a/ui/app/components/qr-code.js +++ b/ui/app/components/qr-code.js @@ -4,13 +4,12 @@ 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, + // Qr code is not fetched from state. 'message' and 'data' props are passed instead. buyView: state.appState.buyView, warning: state.appState.warning, } @@ -29,45 +28,35 @@ QrCodeView.prototype.render = function () { const qrImage = qrCode(4, 'M') qrImage.addData(address) qrImage.make() - return h('.main-container.flex-column', { - key: 'qr', + return h('.div.flex-column.flex-center', { 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', - }, + h('.div.qr-wrapper', { + style: {}, dangerouslySetInnerHTML: { __html: qrImage.createTableTag(4), }, }), - h('.flex-row', [ - h('h3.ellip-address', { + h('.div.ellip-address-wrapper', [ + h('input.qr-ellip-address', { style: { width: '247px', }, - }, Qr.data), - h(CopyButton, { value: Qr.data, + readonly: true, }), + // h(CopyButton, { + // value: Qr.data, + // }), ]), ]) } diff --git a/ui/app/components/send-token/index.js b/ui/app/components/send-token/index.js new file mode 100644 index 000000000..72fb593be --- /dev/null +++ b/ui/app/components/send-token/index.js @@ -0,0 +1,365 @@ +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const { addHexPrefix } = require('ethereumjs-util') +const classnames = require('classnames') +const inherits = require('util').inherits +const actions = require('../../actions') +const selectors = require('../../selectors') + +// const BalanceComponent = require('./balance-component') +const Identicon = require('../identicon') +const TokenBalance = require('../token-balance') +const CurrencyToggle = require('../send/currency-toggle') +const GasTooltip = require('../send/gas-tooltip') +const GasFeeDisplay = require('../send/gas-fee-display') + + +module.exports = connect(mapStateToProps, mapDispatchToProps)(SendTokenScreen) + +function mapStateToProps (state) { + // const sidebarOpen = state.appState.sidebarOpen + + const identities = state.metamask.identities + const addressBook = state.metamask.addressBook + const conversionRate = state.metamask.conversionRate + const currentBlockGasLimit = state.metamask.currentBlockGasLimit + const accounts = state.metamask.accounts + // const network = state.metamask.network + const selectedTokenAddress = state.metamask.selectedTokenAddress + const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] + const selectedToken = selectors.getSelectedToken(state) + const tokenExchangeRates = state.metamask.tokenExchangeRates + const pair = `${selectedToken.symbol.toLowerCase()}_eth` + const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {} + // const checksumAddress = selectedAddress && ethUtil.toChecksumAddress(selectedAddress) + // const identity = identities[selectedAddress] + return { + // sidebarOpen, + selectedAddress, + // checksumAddress, + selectedTokenAddress, + identities, + addressBook, + conversionRate, + tokenExchangeRate, + currentBlockGasLimit, + selectedToken, + // selectedToken: selectors.getSelectedToken(state), + // identity, + // network, + } +} + +function mapDispatchToProps (dispatch) { + return { + backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)), + hideWarning: () => dispatch(actions.hideWarning()), + addToAddressBook: (recipient, nickname) => dispatch( + actions.addToAddressBook(recipient, nickname) + ), + signTx: txParams => dispatch(actions.signTx(txParams)), + signTokenTx: (tokenAddress, toAddress, amount, txData) => ( + dispatch(actions.signTokenTx(tokenAddress, toAddress, amount, txData)) + ), + updateTokenExchangeRate: token => dispatch(actions.updateTokenExchangeRate(token)), + // showSidebar: () => { dispatch(actions.showSidebar()) }, + // hideSidebar: () => { dispatch(actions.hideSidebar()) }, + // showModal: (payload) => { dispatch(actions.showModal(payload)) }, + // showSendPage: () => { dispatch(actions.showSendPage()) }, + // showSendTokenPage: () => { dispatch(actions.showSendTokenPage()) }, + } +} + +inherits(SendTokenScreen, Component) +function SendTokenScreen () { + Component.call(this) + this.state = { + to: '', + amount: '', + selectedCurrency: 'USD', + isGasTooltipOpen: false, + gasPrice: '0x5d21dba00', + gasLimit: '0x7b0d', + errors: {}, + } +} + +SendTokenScreen.prototype.componentWillMount = function () { + const { + updateTokenExchangeRate, + selectedToken: { symbol }, + } = this.props + + updateTokenExchangeRate(symbol) +} + +SendTokenScreen.prototype.validate = function () { + const { + to, + amount: stringAmount, + gasPrice: hexGasPrice, + gasLimit: hexGasLimit, + } = this.state + + const gasPrice = parseInt(hexGasPrice, 16) + const gasLimit = parseInt(hexGasLimit, 16) / 1000000000 + const amount = Number(stringAmount) + + if (to && amount && gasPrice && gasLimit) { + return { + isValid: true, + errors: {}, + } + } + + const errors = { + to: !to ? 'Required' : null, + amount: !amount ? 'Required' : null, + gasPrice: !gasPrice ? 'Gas Price Required' : null, + gasLimit: !gasLimit ? 'Gas Limit Required' : null, + } + + return { + isValid: false, + errors, + } +} + +SendTokenScreen.prototype.submit = function () { + const { + to, + amount, + gasPrice, + gasLimit, + } = this.state + + const { + identities, + selectedAddress, + selectedTokenAddress, + hideWarning, + addToAddressBook, + signTokenTx, + selectedToken, + } = this.props + + const { nickname = ' ' } = identities[to] || {} + + const { isValid, errors } = this.validate() + + if (!isValid) { + return this.setState({ errors }) + } + + hideWarning() + addToAddressBook(to, nickname) + + const txParams = { + from: selectedAddress, + value: '0', + gas: gasLimit, + gasPrice: gasPrice, + } + + const { decimals } = selectedToken || {} + const multiplier = Math.pow(10, Number(decimals || 0)) + const sendAmount = Number(amount * multiplier).toString(16) + + signTokenTx(selectedTokenAddress, to, sendAmount, txParams) +} + +SendTokenScreen.prototype.renderToAddressInput = function () { + const { + identities, + addressBook, + } = this.props + + const { + to, + errors: { to: errorMessage }, + } = this.state + + return h('div', { + className: classnames('send-screen-input-wrapper', { + 'send-screen-input-wrapper--error': errorMessage, + }), + }, [ + h('div', ['To:']), + h('input.large-input.send-screen-input', { + name: 'address', + list: 'addresses', + placeholder: 'Address', + value: to, + onChange: e => this.setState({ + to: e.target.value, + errors: {}, + }), + }), + h('datalist#addresses', [ + // Corresponds to the addresses owned. + Object.entries(identities).map(([key, { address, name }]) => { + return h('option', { + value: address, + label: name, + key: address, + }) + }), + addressBook.map(({ address, name }) => { + return h('option', { + value: address, + label: name, + key: address, + }) + }), + ]), + h('div.send-screen-input-wrapper__error-message', [ errorMessage ]), + ]) +} + +SendTokenScreen.prototype.renderAmountInput = function () { + const { + selectedCurrency, + amount, + errors: { amount: errorMessage }, + } = this.state + + const { + tokenExchangeRate, + selectedToken: {symbol}, + } = this.props + + return h('div.send-screen-input-wrapper', { + className: classnames('send-screen-input-wrapper', { + 'send-screen-input-wrapper--error': errorMessage, + }), + }, [ + h('div.send-screen-amount-labels', [ + h('span', ['Amount']), + h(CurrencyToggle, { + currentCurrency: tokenExchangeRate ? selectedCurrency : 'USD', + currencies: tokenExchangeRate ? [ symbol, 'USD' ] : [], + onClick: currency => this.setState({ selectedCurrency: currency }), + }), + ]), + h('input.large-input.send-screen-input', { + placeholder: `0 ${symbol}`, + type: 'number', + value: amount, + onChange: e => this.setState({ + amount: e.target.value, + errors: {}, + }), + }), + h('div.send-screen-input-wrapper__error-message', [ errorMessage ]), + ]) +} + +SendTokenScreen.prototype.renderGasInput = function () { + const { + isGasTooltipOpen, + gasPrice, + gasLimit, + selectedCurrency, + errors: { + gasPrice: gasPriceErrorMessage, + gasLimit: gasLimitErrorMessage, + }, + } = this.state + + const { + conversionRate, + tokenExchangeRate, + currentBlockGasLimit, + } = this.props + + return h('div.send-screen-input-wrapper', { + className: classnames('send-screen-input-wrapper', { + 'send-screen-input-wrapper--error': gasPriceErrorMessage || gasLimitErrorMessage, + }), + }, [ + isGasTooltipOpen && h(GasTooltip, { + className: 'send-tooltip', + gasPrice, + gasLimit, + onClose: () => this.setState({ isGasTooltipOpen: false }), + onFeeChange: ({ gasLimit, gasPrice }) => { + this.setState({ gasLimit, gasPrice, errors: {} }) + }, + }), + + h('div.send-screen-gas-labels', {}, [ + h('span', [ h('i.fa.fa-bolt'), 'Gas fee:']), + h('span', ['What\'s this?']), + ]), + h('div.large-input.send-screen-gas-input', [ + h(GasFeeDisplay, { + conversionRate, + tokenExchangeRate, + gasPrice, + activeCurrency: selectedCurrency, + gas: gasLimit, + blockGasLimit: currentBlockGasLimit, + }), + h( + 'div.send-screen-gas-input-customize', + { onClick: () => this.setState({ isGasTooltipOpen: !isGasTooltipOpen }) }, + ['Customize'] + ), + ]), + h('div.send-screen-input-wrapper__error-message', [ + gasPriceErrorMessage || gasLimitErrorMessage, + ]), + ]) +} + +SendTokenScreen.prototype.renderMemoInput = function () { + return h('div.send-screen-input-wrapper', [ + h('div', {}, ['Transaction memo (optional)']), + h( + 'input.large-input.send-screen-input', + { onChange: e => this.setState({ memo: e.target.value }) } + ), + ]) +} + +SendTokenScreen.prototype.renderButtons = function () { + const { selectedAddress, backToAccountDetail } = this.props + + return h('div.send-token__button-group', [ + h('button.send-token__button-next.btn-secondary', { + onClick: () => this.submit(), + }, ['Next']), + h('button.send-token__button-cancel.btn-tertiary', { + onClick: () => backToAccountDetail(selectedAddress), + }, ['Cancel']), + ]) +} + +SendTokenScreen.prototype.render = function () { + const { + selectedTokenAddress, + selectedToken, + } = this.props + + return h('div.send-token', [ + h('div.send-token__content', [ + h(Identicon, { + diameter: 75, + address: selectedTokenAddress, + }), + h('div.send-token__title', ['Send Tokens']), + h('div.send-token__description', ['Send Tokens to anyone with an Ethereum account']), + h('div.send-token__balance-text', ['Your Token Balance is:']), + h('div.send-token__token-balance', [ + h(TokenBalance, { token: selectedToken, balanceOnly: true }), + ]), + h('div.send-token__token-symbol', [selectedToken.symbol]), + this.renderToAddressInput(), + this.renderAmountInput(), + this.renderGasInput(), + this.renderMemoInput(), + ]), + this.renderButtons(), + ]) +} diff --git a/ui/app/components/send/currency-toggle.js b/ui/app/components/send/currency-toggle.js new file mode 100644 index 000000000..7aaccd490 --- /dev/null +++ b/ui/app/components/send/currency-toggle.js @@ -0,0 +1,44 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const classnames = require('classnames') + +module.exports = CurrencyToggle + +inherits(CurrencyToggle, Component) +function CurrencyToggle () { + Component.call(this) +} + +const defaultCurrencies = [ 'ETH', 'USD' ] + +CurrencyToggle.prototype.renderToggles = function () { + const { onClick, activeCurrency } = this.props + const [currencyA, currencyB] = this.props.currencies || defaultCurrencies + + return [ + h('span', { + className: classnames('currency-toggle__item', { + 'currency-toggle__item--selected': currencyA === activeCurrency, + }), + onClick: () => onClick(currencyA), + }, [ currencyA ]), + '<>', + h('span', { + className: classnames('currency-toggle__item', { + 'currency-toggle__item--selected': currencyB === activeCurrency, + }), + onClick: () => onClick(currencyB), + }, [ currencyB ]), + ] +} + +CurrencyToggle.prototype.render = function () { + const currencies = this.props.currencies || defaultCurrencies + + return h('span.currency-toggle', currencies.length + ? this.renderToggles() + : [] + ) +} + diff --git a/ui/app/components/send/eth-fee-display.js b/ui/app/components/send/eth-fee-display.js new file mode 100644 index 000000000..8b4cec16c --- /dev/null +++ b/ui/app/components/send/eth-fee-display.js @@ -0,0 +1,37 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const EthBalance = require('../eth-balance') +const { getTxFeeBn } = require('../../util') + +module.exports = EthFeeDisplay + +inherits(EthFeeDisplay, Component) +function EthFeeDisplay () { + Component.call(this) +} + +EthFeeDisplay.prototype.render = function () { + const { + activeCurrency, + conversionRate, + gas, + gasPrice, + blockGasLimit, + } = this.props + + return h(EthBalance, { + value: getTxFeeBn(gas, gasPrice, blockGasLimit), + currentCurrency: activeCurrency, + conversionRate, + showFiat: false, + hideTooltip: true, + styleOveride: { + color: '#5d5d5d', + fontSize: '16px', + fontFamily: 'DIN OT', + lineHeight: '22.4px' + } + }) +} + diff --git a/ui/app/components/send/gas-fee-display.js b/ui/app/components/send/gas-fee-display.js new file mode 100644 index 000000000..a9a3f3f49 --- /dev/null +++ b/ui/app/components/send/gas-fee-display.js @@ -0,0 +1,62 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const USDFeeDisplay = require('./usd-fee-display') +const EthFeeDisplay = require('./eth-fee-display') +const { getTxFeeBn, formatBalance, shortenBalance } = require('../../util') + +module.exports = GasFeeDisplay + +inherits(GasFeeDisplay, Component) +function GasFeeDisplay () { + Component.call(this) +} + +GasFeeDisplay.prototype.getTokenValue = function () { + const { + tokenExchangeRate, + gas, + gasPrice, + blockGasLimit, + } = this.props + + const value = formatBalance(getTxFeeBn(gas, gasPrice, blockGasLimit), 6, true) + const [ethNumber] = value.split(' ') + + return shortenBalance(Number(ethNumber) / tokenExchangeRate, 6) +} + +GasFeeDisplay.prototype.render = function () { + const { + activeCurrency, + conversionRate, + gas, + gasPrice, + blockGasLimit, + } = this.props + + switch (activeCurrency) { + case 'USD': + return h(USDFeeDisplay, { + activeCurrency, + conversionRate, + gas, + gasPrice, + blockGasLimit, + }) + case 'ETH': + return h(EthFeeDisplay, { + activeCurrency, + conversionRate, + gas, + gasPrice, + blockGasLimit, + }) + default: + return h('div.token-gas', [ + h('div.token-gas__amount', this.getTokenValue()), + h('div.token-gas__symbol', activeCurrency), + ]) + } +} + diff --git a/ui/app/components/send/gas-tooltip.js b/ui/app/components/send/gas-tooltip.js new file mode 100644 index 000000000..bef419e48 --- /dev/null +++ b/ui/app/components/send/gas-tooltip.js @@ -0,0 +1,100 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const InputNumber = require('../input-number.js') + +module.exports = GasTooltip + +inherits(GasTooltip, Component) +function GasTooltip () { + Component.call(this) + this.state = { + gasLimit: 0, + gasPrice: 0, + } + + this.updateGasPrice = this.updateGasPrice.bind(this) + this.updateGasLimit = this.updateGasLimit.bind(this) + this.onClose = this.onClose.bind(this) +} + +GasTooltip.prototype.componentWillMount = function () { + const { gasPrice = 0, gasLimit = 0} = this.props + + this.setState({ + gasPrice: parseInt(gasPrice, 16) / 1000000000, + gasLimit: parseInt(gasLimit, 16), + }) +} + +GasTooltip.prototype.updateGasPrice = function (newPrice) { + const { onFeeChange } = this.props + const { gasLimit } = this.state + + this.setState({ gasPrice: newPrice }) + onFeeChange({ + gasLimit: gasLimit.toString(16), + gasPrice: (newPrice * 1000000000).toString(16), + }) +} + +GasTooltip.prototype.updateGasLimit = function (newLimit) { + const { onFeeChange } = this.props + const { gasPrice } = this.state + + this.setState({ gasLimit: newLimit }) + onFeeChange({ + gasLimit: newLimit.toString(16), + gasPrice: (gasPrice * 1000000000).toString(16), + }) +} + +GasTooltip.prototype.onClose = function (e) { + e.stopPropagation() + this.props.onClose() +} + +GasTooltip.prototype.render = function () { + const { gasPrice, gasLimit } = this.state + + return h('div.gas-tooltip', {}, [ + h('div.gas-tooltip-close-area', { + onClick: this.onClose, + }), + h('div.customize-gas-tooltip-container', {}, [ + h('div.customize-gas-tooltip', {}, [ + h('div.gas-tooltip-header.gas-tooltip-label', {}, ['Customize Gas']), + h('div.gas-tooltip-input-label', {}, [ + h('span.gas-tooltip-label', {}, ['Gas Price']), + h('i.fa.fa-info-circle'), + ]), + h(InputNumber, { + unitLabel: 'GWEI', + step: 1, + min: 0, + placeholder: '0', + initValue: gasPrice, + onChange: (newPrice) => this.updateGasPrice(newPrice), + }), + h('div.gas-tooltip-input-label', { + style: { + 'marginTop': '81px', + }, + }, [ + h('span.gas-tooltip-label', {}, ['Gas Limit']), + h('i.fa.fa-info-circle'), + ]), + h(InputNumber, { + unitLabel: 'UNITS', + step: 1, + min: 0, + placeholder: '0', + initValue: gasLimit, + onChange: (newLimit) => this.updateGasLimit(newLimit), + }), + ]), + h('div.gas-tooltip-arrow', {}), + ]), + ]) +} + diff --git a/ui/app/components/send/usd-fee-display.js b/ui/app/components/send/usd-fee-display.js new file mode 100644 index 000000000..6ee38f1b5 --- /dev/null +++ b/ui/app/components/send/usd-fee-display.js @@ -0,0 +1,35 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const FiatValue = require('../fiat-value') +const { getTxFeeBn } = require('../../util') + +module.exports = USDFeeDisplay + +inherits(USDFeeDisplay, Component) +function USDFeeDisplay () { + Component.call(this) +} + +USDFeeDisplay.prototype.render = function () { + const { + activeCurrency, + conversionRate, + gas, + gasPrice, + blockGasLimit, + } = this.props + + return h(FiatValue, { + value: getTxFeeBn(gas, gasPrice, blockGasLimit), + conversionRate, + currentCurrency: activeCurrency, + style: { + color: '#5d5d5d', + fontSize: '16px', + fontFamily: 'DIN OT', + lineHeight: '22.4px' + } + }) +} + diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js index 901a4a956..96a86d3b1 100644 --- a/ui/app/components/shapeshift-form.js +++ b/ui/app/components/shapeshift-form.js @@ -131,8 +131,8 @@ ShapeshiftForm.prototype.renderMain = function () { }, }, [ this.props.warning - ? this.props.warning - && h('span.error.flex-center', { + ? this.props.warning && + h('span.error.flex-center', { style: { textAlign: 'center', width: '229px', diff --git a/ui/app/components/token-balance.js b/ui/app/components/token-balance.js new file mode 100644 index 000000000..3a923eb9d --- /dev/null +++ b/ui/app/components/token-balance.js @@ -0,0 +1,108 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const TokenTracker = require('eth-token-tracker') +const connect = require('react-redux').connect +const selectors = require('../selectors') + +function mapStateToProps (state) { + return { + userAddress: selectors.getSelectedAddress(state), + } +} + +module.exports = connect(mapStateToProps)(TokenBalance) + + +inherits(TokenBalance, Component) +function TokenBalance () { + this.state = { + balance: '', + isLoading: true, + error: null, + } + Component.call(this) +} + +TokenBalance.prototype.render = function () { + const state = this.state + const { balance, isLoading } = state + + return isLoading + ? h('span', '') + : h('span', balance) +} + +TokenBalance.prototype.componentDidMount = function () { + this.createFreshTokenTracker() +} + +TokenBalance.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, token } = this.props + + this.tracker = new TokenTracker({ + userAddress, + provider: global.ethereumProvider, + tokens: [token], + pollingInterval: 8000, + }) + + + // Set up listener instances for cleaning up + this.balanceUpdater = this.updateBalance.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.updateBalance(this.tracker.serialize()) + }) + .catch((reason) => { + log.error(`Problem updating balances`, reason) + this.setState({ isLoading: false }) + }) +} + +TokenBalance.prototype.componentDidUpdate = function (nextProps) { + const { + userAddress: oldAddress, + token: { address: oldTokenAddress }, + } = this.props + const { + userAddress: newAddress, + token: { address: newTokenAddress }, + } = nextProps + + if ((!oldAddress || !newAddress) && (!oldTokenAddress || !newTokenAddress)) return + if ((oldAddress === newAddress) && (oldTokenAddress === newTokenAddress)) return + + this.setState({ isLoading: true }) + this.createFreshTokenTracker() +} + +TokenBalance.prototype.updateBalance = function (tokens = []) { + const [{ string, symbol }] = tokens + const { balanceOnly } = this.props + + this.setState({ + balance: balanceOnly ? string : `${string} ${symbol}`, + isLoading: false, + }) +} + +TokenBalance.prototype.componentWillUnmount = function () { + if (!this.tracker) return + this.tracker.stop() +} + diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index 19d7139bb..dc1c7f46f 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -1,35 +1,101 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits +const connect = require('react-redux').connect const Identicon = require('./identicon') const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') +const selectors = require('../selectors') +const actions = require('../actions') +const { conversionUtil } = require('../conversion-util') -module.exports = TokenCell +function mapStateToProps (state) { + return { + network: state.metamask.network, + selectedTokenAddress: state.metamask.selectedTokenAddress, + userAddress: selectors.getSelectedAddress(state), + tokenExchangeRates: state.metamask.tokenExchangeRates, + ethToUSDRate: state.metamask.conversionRate, + } +} + +function mapDispatchToProps (dispatch) { + return { + setSelectedToken: address => dispatch(actions.setSelectedToken(address)), + updateTokenExchangeRate: token => dispatch(actions.updateTokenExchangeRate(token)), + } +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)(TokenCell) inherits(TokenCell, Component) function TokenCell () { Component.call(this) } +TokenCell.prototype.componentWillMount = function () { + const { + updateTokenExchangeRate, + symbol, + } = this.props + + updateTokenExchangeRate(symbol) +} + TokenCell.prototype.render = function () { const props = this.props - const { address, symbol, string, network, userAddress } = props + const { + address, + symbol, + string, + network, + setSelectedToken, + selectedTokenAddress, + tokenExchangeRates, + ethToUSDRate, + // userAddress, + } = props + + const pair = `${symbol.toLowerCase()}_eth`; + + let currentTokenToEthRate; + let currentTokenInUSD; + let formattedUSD = '' + + if (tokenExchangeRates[pair]) { + currentTokenToEthRate = tokenExchangeRates[pair].rate; + currentTokenInUSD = conversionUtil(string, { + fromNumericBase: 'dec', + fromCurrency: symbol, + toCurrency: 'USD', + numberOfDecimals: 2, + conversionRate: currentTokenToEthRate, + ethToUSDRate, + }) + formattedUSD = `$${currentTokenInUSD} USD`; + } return ( - h('li.token-cell', { - style: { cursor: network === '1' ? 'pointer' : 'default' }, - onClick: this.view.bind(this, address, userAddress, network), + h('div.token-list-item', { + className: `token-list-item ${selectedTokenAddress === address ? 'token-list-item--active' : ''}`, + // style: { cursor: network === '1' ? 'pointer' : 'default' }, + // onClick: this.view.bind(this, address, userAddress, network), + onClick: () => setSelectedToken(address), }, [ h(Identicon, { - diameter: 50, + className: 'token-list-item__identicon', + diameter: 45, address, network, }), - h('h3', `${string || 0} ${symbol}`), + h('h.token-list-item__balance-wrapper', null, [ + h('h3.token-list-item__token-balance', `${string || 0} ${symbol}`), - h('span', { style: { flex: '1 0 auto' } }), + h('div.token-list-item__fiat-amount', { + style: {}, + }, formattedUSD), + ]), /* h('button', { diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index 998ec901d..0efa89c63 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -3,8 +3,30 @@ 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 connect = require('react-redux').connect +const selectors = require('../selectors') + +function mapStateToProps (state) { + return { + network: state.metamask.network, + tokens: state.metamask.tokens, + userAddress: selectors.getSelectedAddress(state), + } +} + +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 = connect(mapStateToProps)(TokenList) -module.exports = TokenList inherits(TokenList, Component) function TokenList () { @@ -19,10 +41,9 @@ function TokenList () { TokenList.prototype.render = function () { const state = this.state const { tokens, isLoading, error } = state - const { userAddress, network } = this.props if (isLoading) { - return this.message('Loading') + return this.message('Loading Tokens...') } if (error) { @@ -47,87 +68,8 @@ TokenList.prototype.render = function () { ]) } - const tokenViews = tokens.map((tokenData) => { - tokenData.network = network - tokenData.userAddress = userAddress - return h(TokenCell, tokenData) - }) - - return h('.full-flex-height', [ - this.renderTokenStatusBar(), + return h('div', tokens.map((tokenData) => h(TokenCell, tokenData))) - h('ol.full-flex-height.flex-column', { - style: { - overflowY: 'auto', - display: 'flex', - flexDirection: 'column', - }, - }, [ - h('style', ` - - li.token-cell { - display: flex; - flex-direction: row; - align-items: center; - padding: 10px; - min-height: 50px; - } - - li.token-cell > h3 { - margin-left: 12px; - } - - li.token-cell:hover { - background: white; - cursor: pointer; - } - - `), - ...tokenViews, - h('.flex-grow'), - ]), - ]) -} - -TokenList.prototype.renderTokenStatusBar = function () { - const { tokens } = this.state - - let msg - if (tokens.length === 1) { - msg = `You own 1 token` - } else if (tokens.length > 1) { - msg = `You own ${tokens.length} tokens` - } else { - msg = `No tokens found` - } - - return h('div', { - style: { - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - minHeight: '70px', - padding: '10px', - }, - }, [ - h('span', msg), - h('button', { - key: 'reveal-account-bar', - onClick: (event) => { - event.preventDefault() - this.props.addToken() - }, - style: { - display: 'flex', - height: '40px', - padding: '10px', - justifyContent: 'center', - alignItems: 'center', - }, - }, [ - 'ADD TOKEN', - ]), - ]) } TokenList.prototype.message = function (body) { @@ -156,6 +98,7 @@ TokenList.prototype.createFreshTokenTracker = function () { if (!global.ethereumProvider) return const { userAddress } = this.props + this.tracker = new TokenTracker({ userAddress, provider: global.ethereumProvider, @@ -182,15 +125,22 @@ TokenList.prototype.createFreshTokenTracker = function () { }) } -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.componentDidUpdate = function (nextProps) { + const { + network: oldNet, + userAddress: oldAddress, + } = this.props + const { + network: newNet, + userAddress: newAddress, + } = nextProps + + if (newNet === 'loading') return + if (!oldNet || !newNet || !oldAddress || !newAddress) return + if (oldAddress === newAddress && oldNet === newNet) return + + this.setState({ isLoading: true }) + this.createFreshTokenTracker() } TokenList.prototype.updateBalances = function (tokens) { @@ -205,3 +155,15 @@ TokenList.prototype.componentWillUnmount = function () { 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/transaction-list-item.js b/ui/app/components/transaction-list-item.js index 5d5d0bcc5..880a288af 100644 --- a/ui/app/components/transaction-list-item.js +++ b/ui/app/components/transaction-list-item.js @@ -147,7 +147,7 @@ function failIfFailed (transaction) { } if (transaction.err || transaction.warning) { const { err, warning = {} } = transaction - const errFirst = !!(( err && warning ) || err) + const errFirst = !!((err && warning) || err) const message = errFirst ? err.message : warning.message errFirst ? err.message : warning.message diff --git a/ui/app/components/tx-list-item.js b/ui/app/components/tx-list-item.js new file mode 100644 index 000000000..8422c02b9 --- /dev/null +++ b/ui/app/components/tx-list-item.js @@ -0,0 +1,187 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const inherits = require('util').inherits +const classnames = require('classnames') +const abi = require('human-standard-token-abi') +const abiDecoder = require('abi-decoder') +abiDecoder.addABI(abi) +const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') +const Identicon = require('./identicon') + +const { conversionUtil } = require('../conversion-util') + +module.exports = connect(mapStateToProps)(TxListItem) + +function mapStateToProps (state) { + return { + tokens: state.metamask.tokens, + } +} + +inherits(TxListItem, Component) +function TxListItem () { + Component.call(this) +} + +TxListItem.prototype.getAddressText = function () { + const { + address, + txParams = {}, + } = this.props + + const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data) + const { name: txDataName, params = [] } = decodedData || {} + const { value } = params[0] || {} + + switch (txDataName) { + case 'transfer': + return `${value.slice(0, 10)}...${value.slice(-4)}` + default: + return address + ? `${address.slice(0, 10)}...${address.slice(-4)}` + : 'Contract Published' + } +} + +TxListItem.prototype.getSendEtherTotal = function () { + const { + transactionAmount, + conversionRate, + address, + } = this.props + + if (!address) { + return {} + } + + const totalInUSD = conversionUtil(transactionAmount, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromCurrency: 'ETH', + toCurrency: 'USD', + fromDenomination: 'WEI', + numberOfDecimals: 2, + conversionRate, + }) + const totalInETH = conversionUtil(transactionAmount, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromCurrency: 'ETH', + toCurrency: 'ETH', + fromDenomination: 'WEI', + conversionRate, + numberOfDecimals: 6, + }) + + return { + total: `${totalInETH} ETH`, + fiatTotal: `$${totalInUSD} USD`, + } +} + +TxListItem.prototype.getSendTokenTotal = function () { + const { + txParams = {}, + tokens, + } = this.props + + const toAddress = txParams.to + const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data) + const { params = [] } = decodedData || {} + const { value } = params[1] || {} + const { decimals, symbol } = tokens.filter(({ address }) => address === toAddress)[0] || {} + + const multiplier = Math.pow(10, Number(decimals || 0)) + const total = Number(value / multiplier) + + return { + total: `${total} ${symbol}`, + } +} + +TxListItem.prototype.render = function () { + const { + transactionStatus, + onClick, + transActionId, + dateString, + address, + className, + txParams = {}, + } = this.props + + const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data) + const { name: txDataName } = decodedData || {} + + const { total, fiatTotal } = txDataName === 'transfer' + ? this.getSendTokenTotal() + : this.getSendEtherTotal() + + return h(`div${className || ''}`, { + key: transActionId, + onClick: () => onClick && onClick(transActionId), + }, [ + h(`div.flex-column.tx-list-item-wrapper`, {}, [ + + h('div.tx-list-date-wrapper', { + style: {}, + }, [ + h('span.tx-list-date', {}, [ + dateString, + ]), + ]), + + h('div.flex-row.tx-list-content-wrapper', { + style: {}, + }, [ + + h('div.tx-list-identicon-wrapper', { + style: {}, + }, [ + h(Identicon, { + address, + diameter: 28, + }), + ]), + + h('div.tx-list-account-and-status-wrapper', {}, [ + h('div.tx-list-account-wrapper', { + style: {}, + }, [ + h('span.tx-list-account', {}, [ + this.getAddressText(address), + ]), + ]), + + h('div.tx-list-status-wrapper', { + style: {}, + }, [ + h('span', { + className: classnames('tx-list-status', { + 'tx-list-status--rejected': transactionStatus === 'rejected', + 'tx-list-status--failed': transactionStatus === 'failed', + }), + }, + transactionStatus, + ), + ]), + ]), + + h('div.flex-column.tx-list-details-wrapper', { + style: {}, + }, [ + + h('span', { + className: classnames('tx-list-value', { + 'tx-list-value--confirmed': transactionStatus === 'confirmed', + }), + }, total), + + h('span.tx-list-fiat-value', fiatTotal), + + ]), + ]), + ]) // holding on icon from design + ]) +} diff --git a/ui/app/components/tx-list.js b/ui/app/components/tx-list.js new file mode 100644 index 000000000..7a147e942 --- /dev/null +++ b/ui/app/components/tx-list.js @@ -0,0 +1,117 @@ +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const inherits = require('util').inherits +const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') +const selectors = require('../selectors') +const TxListItem = require('./tx-list-item') +const { formatBalance, formatDate } = require('../util') +const { showConfTxPage } = require('../actions') + +module.exports = connect(mapStateToProps, mapDispatchToProps)(TxList) + +function mapStateToProps (state) { + return { + txsToRender: selectors.transactionsSelector(state), + conversionRate: selectors.conversionRateSelector(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + showConfTxPage: ({ id }) => dispatch(showConfTxPage({ id })) + } +} + +inherits(TxList, Component) +function TxList () { + Component.call(this) +} + +TxList.prototype.render = function () { + + const { txsToRender, showConfTxPage } = this.props + + return h('div.flex-column.tx-list-container', {}, [ + + h('div.flex-row.tx-list-header-wrapper', [ + h('div.flex-row.tx-list-header', [ + h('div', 'transactions'), + ]), + ]), + + this.renderTransaction(), + + ]) +} + +TxList.prototype.renderTransaction = function () { + const { txsToRender, conversionRate } = this.props + + return txsToRender.length + ? txsToRender.map((transaction, i) => this.renderTransactionListItem(transaction, conversionRate)) + : [h('div.tx-list-item.tx-list-item--empty', [ 'No Transactions' ])] +} + +// TODO: Consider moving TxListItem into a separate component +TxList.prototype.renderTransactionListItem = function (transaction, conversionRate) { + const props = { + dateString: formatDate(transaction.time), + address: transaction.txParams.to, + transactionStatus: transaction.status, + transactionAmount: transaction.txParams.value, + transActionId: transaction.id, + transactionHash: transaction.hash, + transactionNetworkId: transaction.metamaskNetworkId, + } + + const { + address, + transactionStatus, + transactionAmount, + dateString, + transActionId, + transactionHash, + transactionNetworkId, + } = props + const { showConfTxPage } = this.props + + const opts = { + key: transActionId, + txParams: transaction.txParams, + transactionStatus, + transActionId, + dateString, + address, + transactionAmount, + transactionHash, + className: '.tx-list-item.tx-list-clickable', + conversionRate, + } + + if (transactionStatus === 'unapproved') { + opts.onClick = () => showConfTxPage({id: transActionId}) + opts.className += '.tx-list-pending-item-container' + opts.transactionStatus = 'Not Started' + } else if (transactionHash) { + opts.onClick = () => this.view(transactionHash, transactionNetworkId) + } + + return h(TxListItem, opts) +} + +TxList.prototype.view = function (txHash, network) { + const url = etherscanLinkFor(txHash, network) + if (url) { + navigateTo(url) + } +} + +function navigateTo (url) { + global.platform.openWindow({ url }) +} + +function etherscanLinkFor (txHash, network) { + const prefix = prefixForNetwork(network) + return `https://${prefix}etherscan.io/tx/${txHash}` +} diff --git a/ui/app/components/tx-view.js b/ui/app/components/tx-view.js new file mode 100644 index 000000000..59f55d485 --- /dev/null +++ b/ui/app/components/tx-view.js @@ -0,0 +1,148 @@ +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const ethUtil = require('ethereumjs-util') +const inherits = require('util').inherits +const actions = require('../actions') +const selectors = require('../selectors') + +const BalanceComponent = require('./balance-component') +const TxList = require('./tx-list') +const Identicon = require('./identicon') + +module.exports = connect(mapStateToProps, mapDispatchToProps)(TxView) + +function mapStateToProps (state) { + const sidebarOpen = state.appState.sidebarOpen + + const identities = state.metamask.identities + const accounts = state.metamask.accounts + const network = state.metamask.network + const selectedTokenAddress = state.metamask.selectedTokenAddress + const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] + const checksumAddress = selectedAddress && ethUtil.toChecksumAddress(selectedAddress) + const identity = identities[selectedAddress] + + return { + sidebarOpen, + selectedAddress, + checksumAddress, + selectedTokenAddress, + selectedToken: selectors.getSelectedToken(state), + identity, + network, + } +} + +function mapDispatchToProps (dispatch) { + return { + showSidebar: () => { dispatch(actions.showSidebar()) }, + hideSidebar: () => { dispatch(actions.hideSidebar()) }, + showModal: (payload) => { dispatch(actions.showModal(payload)) }, + showSendPage: () => { dispatch(actions.showSendPage()) }, + showSendTokenPage: () => { dispatch(actions.showSendTokenPage()) }, + } +} + +inherits(TxView, Component) +function TxView () { + Component.call(this) +} + +TxView.prototype.renderHeroBalance = function () { + const { selectedToken } = this.props + + return h('div.hero-balance', {}, [ + + h(BalanceComponent, { token: selectedToken }), + + this.renderButtons(), + ]) +} + +TxView.prototype.renderButtons = function () { + const {selectedToken, showModal, showSendPage, showSendTokenPage } = this.props + + return !selectedToken + ? ( + h('div.flex-row.flex-center.hero-balance-buttons', [ + h('button.btn-clear', { + style: { + textAlign: 'center', + }, + onClick: () => showModal({ + name: 'BUY', + }), + }, 'BUY'), + + h('button.btn-clear', { + style: { + textAlign: 'center', + marginLeft: '0.8em', + }, + onClick: showSendPage, + }, 'SEND'), + ]) + ) + : ( + h('div.flex-row.flex-center.hero-balance-buttons', [ + h('button.btn-clear', { + style: { + textAlign: 'center', + marginLeft: '0.8em', + }, + onClick: showSendTokenPage, + }, 'SEND'), + ]) + ) +} + +TxView.prototype.render = function () { + const { selectedAddress, identity, network } = this.props + + return h('div.tx-view.flex-column', { + style: {}, + }, [ + + h('div.flex-row.phone-visible', { + style: { + margin: '1em 0.9em', + alignItems: 'center', + }, + onClick: () => { + this.props.sidebarOpen ? this.props.hideSidebar() : this.props.showSidebar() + }, + }, [ + + h('div.fa.fa-bars', { + style: { + fontSize: '1.3em', + }, + }, []), + + h('.identicon-wrapper.select-none', { + style: { + marginLeft: '0.9em', + }, + }, [ + h(Identicon, { + diameter: 24, + address: selectedAddress, + network, + }), + ]), + + h('span.account-name', { + style: {}, + }, [ + identity.name, + ]), + + ]), + + this.renderHeroBalance(), + + h(TxList), + + ]) +} diff --git a/ui/app/components/wallet-content-display.js b/ui/app/components/wallet-content-display.js new file mode 100644 index 000000000..bfa061be4 --- /dev/null +++ b/ui/app/components/wallet-content-display.js @@ -0,0 +1,56 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = WalletContentDisplay + +inherits(WalletContentDisplay, Component) +function WalletContentDisplay () { + Component.call(this) +} + +WalletContentDisplay.prototype.render = function () { + const { title, amount, fiatValue, active, style } = this.props + + // TODO: Separate component: wallet-content-account + return h('div.flex-column', { + style: { + marginLeft: '1.3em', + alignItems: 'flex-start', + ...style, + }, + }, [ + + h('span', { + style: { + fontSize: '1.1em', + }, + }, title), + + h('span', { + style: { + fontSize: '1.8em', + margin: '0.4em 0em', + }, + }, amount), + + h('span', { + style: { + fontSize: '1.3em', + }, + }, fiatValue), + + active && h('div', { + style: { + position: 'absolute', + marginLeft: '-1.3em', + height: '6em', + width: '0.3em', + background: '#D8D8D8', // $alto + }, + }, [ + ]), + ]) + +} + diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js new file mode 100644 index 000000000..b306fb7d4 --- /dev/null +++ b/ui/app/components/wallet-view.js @@ -0,0 +1,170 @@ +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Identicon = require('./identicon') +const AccountDropdowns = require('./dropdowns/index.js').AccountDropdowns +const actions = require('../actions') +const BalanceComponent = require('./balance-component') +const TokenList = require('./token-list') +const selectors = require('../selectors') + +module.exports = connect(mapStateToProps, mapDispatchToProps)(WalletView) + +function mapStateToProps (state) { + + return { + network: state.metamask.network, + sidebarOpen: state.appState.sidebarOpen, + identities: state.metamask.identities, + accounts: state.metamask.accounts, + tokens: state.metamask.tokens, + selectedAddress: selectors.getSelectedAddress(state), + selectedIdentity: selectors.getSelectedIdentity(state), + selectedAccount: selectors.getSelectedAccount(state), + selectedTokenAddress: state.metamask.selectedTokenAddress, + } +} + +function mapDispatchToProps (dispatch) { + return { + showSendPage: () => { dispatch(actions.showSendPage()) }, + hideSidebar: () => { dispatch(actions.hideSidebar()) }, + unsetSelectedToken: () => dispatch(actions.setSelectedToken()), + } +} + +inherits(WalletView, Component) +function WalletView () { + Component.call(this) +} + +WalletView.prototype.renderWalletBalance = function () { + const { selectedTokenAddress, selectedAccount, unsetSelectedToken } = this.props + const selectedClass = selectedTokenAddress + ? '' + : 'wallet-balance-wrapper--active' + const className = `flex-column wallet-balance-wrapper ${selectedClass}` + + return h('div', { className }, [ + h('div.wallet-balance', + { + onClick: unsetSelectedToken, + }, + [ + h(BalanceComponent, { + balanceValue: selectedAccount.balance, + style: {}, + }), + ] + ), + ]) +} + +WalletView.prototype.render = function () { + const { + network, responsiveDisplayClassname, identities, + selectedAddress, accounts, + selectedIdentity, + } = this.props + // temporary logs + fake extra wallets + // console.log('walletview, selectedAccount:', selectedAccount) + + return h('div.wallet-view.flex-column' + (responsiveDisplayClassname || ''), { + style: {}, + }, [ + + // TODO: Separate component: wallet account details + h('div.flex-column.wallet-view-account-details', { + style: {}, + }, [ + + h('div.flex-row.account-options-menu', { + style: { + position: 'relative', + }, + }, [ + + h(AccountDropdowns, { + selected: selectedAddress, + network, + identities, + useCssTransition: true, + enableAccountOptions: true, + dropdownWrapperStyle: { + padding: '1px 15px', + marginLeft: '-25px', + position: 'absolute', + width: '122%', // TODO, refactor all of this component out into media queries + }, + menuItemStyles: { + padding: '0px 0px', + margin: '22px 0px', + }, + }, []), + + ]), + + h('div.flex-column.flex-center', { + }, [ + h('div', { + style: { + position: 'relative', + }, + }, [ + h(AccountDropdowns, { + accounts, + style: { + position: 'absolute', + left: 'calc(50% + 28px + 5.5px)', + top: '14px', + }, + innerStyle: { + padding: '10px 16px', + }, + useCssTransition: true, + selected: selectedAddress, + network, + identities, + enableAccountsSelector: true, + }, []), + ]), + + h(Identicon, { + diameter: 54, + address: selectedAddress, + }), + + h('span.account-name', { + style: {}, + }, [ + selectedIdentity.name, + ]), + + ]), + ]), + + // 'Wallet' - Title + // Not visible on mobile + h('div.flex-column.wallet-view-title-wrapper', {}, [ + h('span.wallet-view-title', {}, [ + 'Wallet', + ]), + ]), + + this.renderWalletBalance(), + + h(TokenList), + + ]) +} + +// TODO: Extra wallets, for dev testing. Remove when PRing to master. +// const extraWallet = h('div.flex-column.wallet-balance-wrapper', {}, [ +// h('div.wallet-balance', {}, [ +// h(BalanceComponent, { +// balanceValue: selectedAccount.balance, +// style: {}, +// }), +// ]), +// ]) diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 1ee4166f7..7062eee6b 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -3,15 +3,22 @@ const Component = require('react').Component 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') +const contentDivider = h('div', { + style: { + marginLeft: '16px', + marginRight: '16px', + height:'1px', + background:'#E7E7E7', + }, +}) + module.exports = connect(mapStateToProps)(ConfirmTxScreen) function mapStateToProps (state) { @@ -39,83 +46,37 @@ function ConfirmTxScreen () { ConfirmTxScreen.prototype.render = function () { const props = this.props - const { network, provider, unapprovedTxs, currentCurrency, - unapprovedMsgs, unapprovedPersonalMsgs, conversionRate, blockGasLimit } = props + const { network, 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 }) - const unconfTxListLength = unconfTxList.length - - 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), - - currentTxView({ - // Properties - txData: txData, - key: txData.id, - selectedAddress: props.selectedAddress, - accounts: props.accounts, - identities: props.identities, - conversionRate, - currentCurrency, - blockGasLimit, - unconfTxListLength, - // Actions - buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), - sendTransaction: this.sendTransaction.bind(this), - cancelTransaction: this.cancelTransaction.bind(this, txData), - cancelAllTransactions: this.cancelAllTransactions.bind(this, unconfTxList), - signMessage: this.signMessage.bind(this, txData), - signPersonalMessage: this.signPersonalMessage.bind(this, txData), - cancelMessage: this.cancelMessage.bind(this, txData), - cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), - }), - ]) - ) + return 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) { @@ -137,6 +98,7 @@ function currentTxView (opts) { return h(PendingPersonalMsg, opts) } } + return h(Loading, { isLoading: true }) } ConfirmTxScreen.prototype.buyEth = function (address, event) { @@ -200,14 +162,14 @@ ConfirmTxScreen.prototype.goHome = function (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) - } -} +// 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/conversion-util.js b/ui/app/conversion-util.js new file mode 100644 index 000000000..b440aea7f --- /dev/null +++ b/ui/app/conversion-util.js @@ -0,0 +1,119 @@ +/* Currency Conversion Utility +* This utility function can be used for converting currency related values within metamask. +* The caller should be able to pass it a value, along with information about the value's +* numeric base, denomination and currency, and the desired numeric base, denomination and +* currency. It should return a single value. +* +* @param {(number | string | BN)} value The value to convert. +* @param {Object} [options] Options to specify details of the conversion +* @param {string} [options.fromCurrency = 'ETH' | 'USD'] The currency of the passed value +* @param {string} [options.toCurrency = 'ETH' | 'USD'] The desired currency of the result +* @param {string} [options.fromNumericBase = 'hex' | 'dec' | 'BN'] The numeric basic of the passed value. +* @param {string} [options.toNumericBase = 'hex' | 'dec' | 'BN'] The desired numeric basic of the result. +* @param {string} [options.fromDenomination = 'WEI'] The denomination of the passed value +* @param {number} [options.numberOfDecimals] The desired number of in the result +* @param {number} [options.conversionRate] The rate to use to make the fromCurrency -> toCurrency conversion +* @param {number} [options.ethToUSDRate] If present, a second conversion - at ethToUSDRate - happens after conversionRate is applied. +* @returns {(number | string | BN)} +* +* The utility passes value along with the options as a single object to the `converter` function. +* `converter` uses Ramda.js to apply a composition of conditional setters to the `value` property, depending +* on the accompanying options. Some of these conditional setters are selected via key-value maps, where +* the keys are specified in the options parameters and the values are setter functions. +*/ + +const BigNumber = require('bignumber.js') +const R = require('ramda') +const { stripHexPrefix } = require('ethereumjs-util') + +BigNumber.config({ + ROUNDING_MODE: BigNumber.ROUND_HALF_DOWN, +}) + +// Big Number Constants +const BIG_NUMBER_WEI_MULTIPLIER = new BigNumber('1000000000000000000') + +// Individual Setters +const convert = R.invoker(1, 'times') +const round = R.invoker(2, 'toFormat')(R.__, BigNumber.ROUND_DOWN) + +// Setter Maps +const toBigNumber = { + hex: n => new BigNumber(stripHexPrefix(n), 16), + dec: n => new BigNumber(n, 10), + BN: n => new BigNumber(n.toString(16), 16), +} +const toNormalizedDenomination = { + WEI: bigNumber => bigNumber.div(BIG_NUMBER_WEI_MULTIPLIER) +} +const toSpecifiedDenomination = { + WEI: bigNumber => bigNumber.times(BIG_NUMBER_WEI_MULTIPLIER) +} +const baseChange = { + hex: n => n.toString(16), + dec: n => n.toString(10), + BN: n => new BN(n.toString(16)), +} + +// Predicates +const fromAndToCurrencyPropsNotEqual = R.compose( + R.not, + R.eqBy(R.__, 'fromCurrency', 'toCurrency'), + R.flip(R.prop) +) + +// Lens +const valuePropertyLense = R.over(R.lensProp('value')) + +// conditional 'value' setting wrappers +const whenPredSetWithPropAndSetter = (pred, prop, setter) => R.when( + pred, + R.converge( + valuePropertyLense, + [R.pipe(R.prop(prop), setter), R.identity] + ) +) +const whenPropApplySetterMap = (prop, setterMap) => whenPredSetWithPropAndSetter( + R.prop(prop), + prop, + R.prop(R.__, setterMap) +) + +// Conversion utility function +const converter = R.pipe( + whenPropApplySetterMap('fromNumericBase', toBigNumber), + whenPropApplySetterMap('fromDenomination', toNormalizedDenomination), + whenPropApplySetterMap('toDenomination', toSpecifiedDenomination), + whenPredSetWithPropAndSetter(fromAndToCurrencyPropsNotEqual, 'conversionRate', convert), + whenPredSetWithPropAndSetter(R.prop('ethToUSDRate'), 'ethToUSDRate', convert), + whenPredSetWithPropAndSetter(R.prop('numberOfDecimals'), 'numberOfDecimals', round), + whenPropApplySetterMap('toNumericBase', baseChange), + R.view(R.lensProp('value')) +); + +const conversionUtil = (value, { + fromCurrency = null, + toCurrency = fromCurrency, + fromNumericBase, + toNumericBase, + fromDenomination, + toDenomination, + numberOfDecimals, + conversionRate, + ethToUSDRate, +}) => converter({ + fromCurrency, + toCurrency, + fromNumericBase, + toNumericBase, + fromDenomination, + toDenomination, + numberOfDecimals, + conversionRate, + ethToUSDRate, + value, +}); + +module.exports = { + conversionUtil, +}
\ No newline at end of file diff --git a/ui/app/css/debug.css b/ui/app/css/debug.css deleted file mode 100644 index 3e125bcd4..000000000 --- a/ui/app/css/debug.css +++ /dev/null @@ -1,21 +0,0 @@ -/* -debug / dev -*/ - -#app-content { - border: 2px solid green; -} - -#design-container { - position: absolute; - left: 360px; - top: -42px; - width: calc(100vw - 360px); - height: 100vh; - overflow: scroll; -} - -#design-container img { - width: 2000px; - margin-right: 600px; -}
\ No newline at end of file diff --git a/ui/app/css/fonts.css b/ui/app/css/fonts.css deleted file mode 100644 index 3b9f581b9..000000000 --- a/ui/app/css/fonts.css +++ /dev/null @@ -1,36 +0,0 @@ -@import url(https://fonts.googleapis.com/css?family=Roboto:300,500); -@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css); - -@font-face { - font-family: 'Montserrat Regular'; - src: url('/fonts/Montserrat/Montserrat-Regular.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-Regular.ttf') format('truetype'); - font-weight: normal; - font-style: normal; - font-size: 'small'; - -} - -@font-face { - font-family: 'Montserrat Bold'; - src: url('/fonts/Montserrat/Montserrat-Bold.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-Bold.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'Montserrat Light'; - src: url('/fonts/Montserrat/Montserrat-Light.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-Light.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'Montserrat UltraLight'; - src: url('/fonts/Montserrat/Montserrat-UltraLight.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-UltraLight.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} diff --git a/ui/app/css/index.scss b/ui/app/css/index.scss new file mode 100644 index 000000000..01899ccad --- /dev/null +++ b/ui/app/css/index.scss @@ -0,0 +1,13 @@ +/* + ITCSS + + http://www.creativebloq.com/web-design/manage-large-css-projects-itcss-101517528 + https://www.xfive.co/blog/itcss-scalable-maintainable-css-architecture/ + */ +@import './itcss/settings/index.scss'; +@import './itcss/tools/index.scss'; +@import './itcss/generic/index.scss'; +@import './itcss/base/index.scss'; +@import './itcss/objects/index.scss'; +@import './itcss/components/index.scss'; +@import './itcss/trumps/index.scss'; diff --git a/ui/app/css/itcss/base/index.scss b/ui/app/css/itcss/base/index.scss new file mode 100644 index 000000000..baa6ea037 --- /dev/null +++ b/ui/app/css/itcss/base/index.scss @@ -0,0 +1 @@ +// Base diff --git a/ui/app/css/itcss/components/account-dropdown.scss b/ui/app/css/itcss/components/account-dropdown.scss new file mode 100644 index 000000000..1c4620e40 --- /dev/null +++ b/ui/app/css/itcss/components/account-dropdown.scss @@ -0,0 +1,17 @@ +.account-dropdown-name { + font-family: 'DIN OT'; +} + +.account-dropdown-balance { + color: $dusty-gray; + line-height: 19px; +} + +.account-dropdown-edit-button { + color: $dusty-gray; + font-family: "DIN OT"; + + &:hover { + color: $white; + } +} diff --git a/ui/app/css/itcss/components/buttons.scss b/ui/app/css/itcss/components/buttons.scss new file mode 100644 index 000000000..0946cdbbb --- /dev/null +++ b/ui/app/css/itcss/components/buttons.scss @@ -0,0 +1,102 @@ +/* + Buttons + */ + +.btn-green { + background-color: #02c9b1; // TODO: reusable color in colors.css +} + +button.btn-clear { + background: $white; + border: 1px solid; +} + +// No longer used in flat design, remove when modal buttons done +// div.wallet-btn { +// border: 1px solid rgb(91, 93, 103); +// border-radius: 2px; +// height: 30px; +// width: 75px; +// font-size: 0.8em; +// text-align: center; +// line-height: 25px; +// } + +// .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: 0 3px 6px rgba(197, 197, 197, .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); +// } + +button.primary { + padding: 8px 12px; + background: #f7861c; + box-shadow: 0 3px 6px rgba(247, 134, 28, .36); + color: $white; + font-size: 1.1em; + font-family: 'Montserrat Regular'; + text-transform: uppercase; +} + +.btn-light { + padding: 8px 12px; + // background: #FFFFFF; // $bg-white + box-shadow: 0 3px 6px rgba(247, 134, 28, .36); + color: #585d67; // TODO: make reusable light button color + font-size: 1.1em; + font-family: 'Montserrat Regular'; + text-transform: uppercase; + text-align: center; + line-height: 20px; + border-radius: 2px; + border: 1px solid #979797; // #TODO: make reusable light border color + opacity: .5; +} + +// TODO: cleanup: not used anywhere +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; +} + +.btn-secondary { + border: 1px solid #979797; + border-radius: 2px; + background-color: $white; + font-size: 16px; + line-height: 24px; + padding: 16px 42px; +} + +.btn-tertiary { + border: 1px solid transparent; + border-radius: 2px; + background-color: transparent; + font-size: 16px; + line-height: 24px; + padding: 16px 42px; +} diff --git a/ui/app/css/itcss/components/confirm.scss b/ui/app/css/itcss/components/confirm.scss new file mode 100644 index 000000000..e8169ffea --- /dev/null +++ b/ui/app/css/itcss/components/confirm.scss @@ -0,0 +1,257 @@ +.confirm-screen-container { + position: absolute; + align-items: center; + + @media screen and (max-width: 575px) { + margin-top: 35px; + width: 100%; + } + + @media screen and (min-width: 576px) { + margin-top: 6.9vh; + } +} + +.confirm-screen-wrapper { + display: flex; + flex-direction: column; + align-items: center; + z-index: 100; + top: 5%; + font-family: 'DIN NEXT'; + background: $white; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .08); + // padding: 20px 24px 32px; + color: $scorpion; + width: 100%; + + @media screen and (min-width: $break-large) { + width: 498px; + } +} + +.confirm-screen-wrapper > .confirm-screen-total-box { + margin-left: 10px; + margin-right: 10px; +} + +.confirm-screen-wrapper > .confirm-memo-wrapper { + margin: 0; +} + +.confirm-screen-wrapper > .confirm-screen-header { + + @media screen and (max-width: $break-small) { + margin-left: 8px; + } +} + +.confirm-screen-header { + font-size: 26px; + position: relative; + display: flex; + flex-flow: row nowrap; + align-items: center; + width: 100%; + padding: 20px 24px 0; + + @media screen and (max-width: $break-small) { + font-size: 22px; + } +} + +.confirm-screen-title { + line-height: 27px; + + @media screen and (max-width: $break-small) { + margin-left: 22px; + margin-right: 8px; + } +} + +.confirm-screen-back-button { + background: $white; + border: 1px solid $dusty-gray; + left: 24px; + position: absolute; + text-align: center; + color: $black; + padding: 6px 13px 7px 12px; + border-radius: 2px; + height: 30px; + width: 54px; + + @media screen and (max-width: $break-small) { + margin-right: 12px; + } +} + +.confirm-screen-account-wrapper { + display: flex; + flex-direction: column; +} + +.confirm-screen-account-name { + margin-top: 12px; +} + +.confirm-screen-account-name, +.confirm-screen-row-info { + font-size: 16px; + line-height: 24px; + color: $scorpion; + text-align: center; +} + +.confirm-screen-account-number { + font-size: 10px; + line-height: 16px; + color: $dusty-gray; + text-align: center; +} + +.confirm-screen-identicons { + margin-top: 24px; + + i { + align-self: start; + margin: 42px 14px 0; + } +} + +.confirm-screen-sending-to-message { + text-align: center; + font-size: 16px; + margin-top: 30px; + font-family: 'DIN NEXT Light'; +} + +.confirm-screen-send-amount { + font-size: 64px; + color: $scorpion; + margin-top: 12px; + line-height: 60px; + text-align: center; + font-family: 'DIN NEXT Light'; +} + +.confirm-screen-send-amount-currency { + font-size: 20px; + line-height: 20px; + text-align: center; +} + +.confirm-memo-wrapper { + min-height: 24px; + width: 100%; + border-bottom: 1px solid $alto; +} + +.confirm-screen-send-memo { + color: $dusty-gray; + font-size: 16px; + line-height: 24px; + text-align: center; + margin-top: 21px; + margin-bottom: 18px; + font-family: 'DIN NEXT Light'; +} + +.confirm-screen-label { + font-size: 18px; + line-height: 40px; + color: $scorpion; + text-align: left; +} + +section .confirm-screen-account-name, +section .confirm-screen-account-number, +.confirm-screen-row-info, +.confirm-screen-row-detail { + text-align: left; +} + +.confirm-screen-rows { + display: flex; + flex-flow: column nowrap; + width: 100%; + padding: 0 24px 32px; +} + +.confirm-screen-section-column { + flex: .5; +} + +.confirm-screen-row { + display: flex; + flex-flow: row nowrap; + border-bottom: 1px solid $alto; + width: calc(100% - 24px); + align-items: center; + padding: 12px 0; + margin: 0 12px; +} + +.confirm-screen-row-detail { + font-size: 12px; + line-height: 16px; + color: $dusty-gray; + font-family: 'DIN NEXT Light'; +} + +.confirm-screen-total-box { + background-color: $wild-sand; + border-radius: 8px; + padding: 22px 14px; + margin-top: 13px; + + .confirm-screen-label { + line-height: 18px; + } + + .confirm-screen-row-detail { + color: $scorpion; + } + + &__subtitle { + font-size: 14px; + line-height: 20px; + font-family: 'DIN NEXT Light'; + } +} + +.confirm-screen-confirm-button { + height: 62px; + width: 216.88px; + border-radius: 2px; + background-color: #02c9b1; + font-size: 16px; + color: $white; + text-align: center; + font-family: 'DIN NEXT'; + padding-top: 15px; + padding-bottom: 15px; + margin-top: 23px; + border-width: 0; + box-shadow: none; +} + +.btn-light.confirm-screen-cancel-button { + height: 62px; + width: 216.88px; + background: none; + border: none; + opacity: 1; + width: 8em; + font-family: 'DIN NEXT'; + border-width: 0; + padding-top: 15px; + padding-bottom: 15px; + font-size: 16px; + box-shadow: none; + cursor: pointer; +} + +#pending-tx-form { + flex: 1 0 auto; +} diff --git a/ui/app/css/itcss/components/footer.scss b/ui/app/css/itcss/components/footer.scss new file mode 100644 index 000000000..000a53eed --- /dev/null +++ b/ui/app/css/itcss/components/footer.scss @@ -0,0 +1,4 @@ +.app-footer { + padding-bottom: 10px; + align-items: center; +} diff --git a/ui/app/css/itcss/components/header.scss b/ui/app/css/itcss/components/header.scss new file mode 100644 index 000000000..ed569cb08 --- /dev/null +++ b/ui/app/css/itcss/components/header.scss @@ -0,0 +1,71 @@ +.app-header { + align-items: center; + visibility: visible; + background: $gallery; + position: relative; + z-index: $header-z-index; + display: flex; + flex-flow: column nowrap; + + @media screen and (max-width: 575px) { + padding: 0 12px; + width: 100%; + box-shadow: 0 2px 2px 1px rgba(0, 0, 0, .08); + z-index: $mobile-header-z-index; + } + + @media screen and (min-width: 576px) { + height: 14.4vh; + max-height: 97px; + } +} + +.app-header-contents { + display: flex; + justify-content: space-between; + flex-flow: row nowrap; + width: 100%; + height: 6.9vh; + + @media screen and (max-width: 575px) { + height: 100%; + } + + @media screen and (min-width: 576px) { + width: 85vw; + } + + @media screen and (min-width: 769px) { + width: 80vw; + } + + @media screen and (min-width: 1281px) { + width: 65vw; + } +} + +.app-header h1 { + font-family: 'Montserrat Regular'; + text-transform: uppercase; + color: #22232c; // $shark +} + +h2.page-subtitle { + font-family: 'Montserrat Regular'; + text-transform: uppercase; + color: #aeaeae; + font-size: 1em; + margin: 12px; +} + +.network-component-wrapper { + display: flex; + flex-direction: row; + align-items: center; +} + +.left-menu-wrapper { + display: flex; + flex-direction: row; + align-items: center; +} diff --git a/ui/app/css/itcss/components/hero-balance.scss b/ui/app/css/itcss/components/hero-balance.scss new file mode 100644 index 000000000..8f6731358 --- /dev/null +++ b/ui/app/css/itcss/components/hero-balance.scss @@ -0,0 +1,110 @@ +.hero-balance { + + @media screen and (max-width: $break-small) { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + margin: .3em .9em 0; + height: 80vh; + max-height: 225px; + } + + @media screen and (min-width: $break-large) { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + margin: 2.8em 2.37em .8em; + } + + .balance-container { + display: flex; + margin: 0; + justify-content: flex-start; + align-items: center; + + @media screen and (max-width: $break-small) { + flex-direction: column; + } + + @media screen and (min-width: $break-large) { + flex-direction: row; + flex-grow: 3; + } + } + + .balance-display { + + @media screen and (max-width: $break-small) { + text-align: center; + + .token-amount { + font-size: 175%; + margin-top: 12.5%; + } + + .fiat-amount { + font-size: 115%; + margin-top: 8.5%; + color: #a0a0a0; + } + } + + @media screen and (min-width: $break-large) { + margin-left: 3%; + justify-content: flex-start; + align-items: flex-start; + + .token-amount { + font-size: 135%; + } + + .fiat-amount { + margin-top: .25%; + font-size: 105%; + } + } + } + + .balance-icon { + border-radius: 25px; + width: 45px; + height: 45px; + border: 1px solid $alto; + } + + .hero-balance-buttons { + + @media screen and (max-width: $break-small) { + width: 100%; + height: 100px; // needed a round number to set the heights of the buttons inside + } + + @media screen and (min-width: $break-large) { + flex-grow: 2; + justify-content: flex-end; + } + + button.btn-clear { + background: $white; + border: 1px solid; + border-radius: 2px; + font-size: 12px; + + @media screen and (max-width: $break-small) { + width: 23%; + height: 55%; + border-color: $black; + } + + @media screen and (min-width: $break-large) { + border-color: $curious-blue; + color: $curious-blue; + padding: 0; + width: 85px; + height: 34px; + } + } + } +} diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss new file mode 100644 index 000000000..63ac8bd47 --- /dev/null +++ b/ui/app/css/itcss/components/index.scss @@ -0,0 +1,29 @@ +@import './buttons.scss'; + +@import './header.scss'; + +@import './footer.scss'; + +@import './network.scss'; + +@import './modal.scss'; + +@import './newui-sections.scss'; + +@import './account-dropdown.scss'; + +@import './send.scss'; + +@import './confirm.scss'; + +// Balances +@import './hero-balance.scss'; + +@import './wallet-balance.scss'; + +// Tx List and Sections +@import './transaction-list.scss'; + +@import './sections.scss'; + +@import './token-list.scss'; diff --git a/ui/app/css/itcss/components/modal.scss b/ui/app/css/itcss/components/modal.scss new file mode 100644 index 000000000..e9698ce5b --- /dev/null +++ b/ui/app/css/itcss/components/modal.scss @@ -0,0 +1,363 @@ +.modal > div:focus { + outline: none !important; +} + +// Buy Modal +.buy-modal-content { + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + font-family: 'DIN OT'; +} + +@media screen and (max-width: 575px) { + .buy-modal-content-title-wrapper { + justify-content: space-around; + width: 100%; + height: 100px; + } + + .buy-modal-content-title { + font-size: 26px; + margin-top: 15px; + } + + .buy-modal-content-options { + flex-direction: column; + padding: 5% 33%; + } + + .buy-modal-content-footer { + text-transform: uppercase; + width: 100%; + height: 50px; + } + + div.buy-modal-content-option { + display: flex; + flex-direction: column; + width: 80vw; + height: 15vh; + margin: 10px; + text-align: center; + border-radius: 6px; + border: 1px solid $black; + padding: 0% 7%; + justify-content: space-around; + + div.buy-modal-content-option-title { + font-size: 20px; + } + + div.buy-modal-content-option-subtitle { + font-size: 16px; + } + } +} + +@media screen and (min-width: 576px) { + .buy-modal-content-title-wrapper { + justify-content: space-around; + width: 100%; + height: 110px; + } + + .buy-modal-content-title { + font-size: 26px; + margin-top: 15px; + } + + .buy-modal-content-footer { + text-transform: uppercase; + width: 100%; + height: 50px; + } + + .buy-modal-content-options { + flex-direction: row; + margin: 20px 0; + } + + div.buy-modal-content-option { + display: flex; + flex-direction: column; + width: 20vw; + height: 18vw; + text-align: center; + border-radius: 6px; + border: 1px solid $black; + margin: 0 .5vw; + justify-content: space-around; + + div.buy-modal-content-option-title { + font-size: 20px; + + @media screen and (max-width: 679px) { + font-size: 14px; + } + + @media screen and (min-width: 1281px) { + font-size: 26px; + } + } + + div.buy-modal-content-option-subtitle { + font-size: 16px; + padding: 0 10px; + height: 25%; + + @media screen and (max-width: 679px) { + font-size: 10px; + padding: 0 10px; + margin-bottom: 5px; + line-height: 15px; + } + + @media screen and (min-width: 680px) { + font-size: 14px; + padding: 0 4px; + margin-bottom: 2px; + } + + @media screen and (min-width: 1281px) { + font-size: 20px; + padding: 0; + } + } + + div.buy-modal-content-footer { + margin-top: 8vh; + } + } +} + +// Edit Account Name Modal +.edit-account-name-modal-content { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + position: relative; +} + +.edit-account-name-modal-cancel { + position: absolute; + top: 12px; + right: 20px; + font-size: 25px; +} + +.edit-account-name-modal-title { + margin: 15px; +} + +.edit-account-name-modal-save-button { + width: 33%; + height: 45px; + margin: 15px; + font-weight: 700; + margin-top: 25px; +} + +.edit-account-name-modal-input { + width: 90%; + height: 50px; + text-align: left; + margin: 10px; + padding: 10px; + font-size: 18px; +} + +// Account Details Modal +.account-details-modal-wrapper { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + position: relative; + padding: 5px 0 31px 0; + border: 1px solid $silver; + border-radius: 4px; + font-family: 'Montserrat UltraLight'; + + button { + cursor: pointer; + } +} + +.account-details-modal-wrapper .qr-header { + margin-top: 9px; + font-size: 20px; +} + +.account-details-modal-wrapper .qr-wrapper { + margin-top: 5px; +} + +.account-details-modal-wrapper .ellip-address-wrapper { + display: flex; + justify-content: center; + border: 1px solid $alto; + padding: 5px 10px; + font-family: 'Montserrat Light'; + margin-top: 7px; + width: 286px; +} + +.account-details-modal-wrapper .qr-ellip-address { + width: 254px; + border: none; + font-family: 'Montserrat Light'; + font-size: 14px; +} + +.account-details-modal-wrapper .btn-clear { + min-height: 28px; + font-size: 14px; + border-color: $curious-blue; + color: $curious-blue; + border-radius: 2px; + flex-basis: 100%; + width: 75%; + margin-top: 17px; + padding: 10px 22px; + height: 44px; + width: 235px; + font-family: 'Montserrat Light'; +} + +.account-details-modal-divider { + width: 100%; + height: 1px; + margin: 19px 0 8px 0; + background-color: $alto; +} + +.account-details-modal-wrapper .identicon { + position: relative; + left: 0; + right: 0; + margin: 0 auto; + top: -32px; + margin-bottom: -32px; +} + +.account-details-modal-close::after { + content: '\00D7'; + font-size: 40px; + color: $dusty-gray; + position: absolute; + top: 10px; + right: 12px; + cursor: pointer; +} + +// New Account Modal +.new-account-modal-wrapper { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + position: relative; + border: 1px solid $alto; + box-shadow: 0 0 2px 2px $alto; + font-family: 'Montserrat Light'; +} + +.new-account-modal-header { + background: $wild-sand; + width: 100%; + display: flex; + justify-content: center; + padding: 30px; + font-size: 22px; + color: $nile-blue; + height: 79px; +} + +.modal-close-x::after { + content: '\00D7'; + font-size: 2em; + color: $dusty-gray; + position: absolute; + top: 25px; + right: 17.5px; + font-family: sans-serif; + cursor: pointer; +} + +.new-account-modal-content { + width: 100%; + display: flex; + justify-content: center; + margin-top: 15px; + font-size: 17px; + color: $nile-blue; +} + +.new-account-modal-content.after-input { + margin-top: 15px; + line-height: 25px; +} + +.new-account-input-wrapper { + display: flex; + width: 100%; + justify-content: center; + padding-bottom: 2px; + margin-top: 13px; +} + +.new-account-input { + padding: 15px; + padding-bottom: 20px; + border-radius: 8px; + border: 1px solid $alto; + width: 100%; + font-size: 1em; + color: $alto; + font-family: Montserrat Light; + font-size: 17px; + margin: 0 60px; +} + +// For reference on below placeholder selectors: https://stackoverflow.com/questions/2610497/change-an-html5-inputs-placeholder-color-with-css +.new-account-input::-webkit-input-placeholder { + color: $dusty-gray; +} + +.new-account-input:-moz-placeholder { + color: $dusty-gray; + opacity: 1; +} + +.new-account-input::-moz-placeholder { + color: $dusty-gray; + opacity: 1; +} + +.new-account-input:-ms-input-placeholder { + color: $dusty-gray; +} + +.new-account-input::-ms-input-placeholder { + color: $dusty-gray; +} + +.new-account-modal-content.button { + margin-top: 22px; + margin-bottom: 30px; + width: 113px; + height: 44px; +} + +.new-account-modal-wrapper .btn-clear { + font-size: 14px; + font-weight: 700; + background: $white; + border: 1px solid; + border-radius: 2px; + color: $tundora; + flex: 1; +} diff --git a/ui/app/css/itcss/components/network.scss b/ui/app/css/itcss/components/network.scss new file mode 100644 index 000000000..012b1faf6 --- /dev/null +++ b/ui/app/css/itcss/components/network.scss @@ -0,0 +1,110 @@ +.network-indicator { + display: flex; + align-items: center; + font-size: .6em; +} + +.network-name { + line-height: 12px; + padding: 0 4px; + font-family: 'DIN OT'; + font-size: 10px; + flex: 1 0 auto; +} + +.network-droppo { + right: 2px; + + @media screen and (min-width: 576px) { + right: calc(((100% - 85vw) / 2) + 2px); + } + + @media screen and (min-width: 769px) { + right: calc(((100% - 80vw) / 2) + 2px); + } + + @media screen and (min-width: 1281px) { + right: calc(((100% - 65vw) / 2) + 2px); + } +} + +.network-name-item { + font-weight: 100; + flex: 1 0 auto; + color: $dusty-gray; +} + +.network-check, +.network-check__transparent { + color: $white; + margin-left: 7px; +} + +.network-check__transparent { + opacity: 0; + width: 16px; + margin: 0; +} + +.menu-icon-circle, .menu-icon-circle--active { + height: 23px; + width: 23px; + margin: 9px; + background: none; + border-radius: 22px; + display: flex; + justify-content: center; + align-items: center; + border: 1px solid transparent; + background: none; +} + +.menu-icon-circle--active { + border: 1px solid white; + background: rgba(100, 100, 100, 0.4); +} + +.menu-icon-circle div, .menu-icon-circle--active div { + height: 17px; + width: 17px; + border-radius: 17px; + opacity: 0.7; +} + +.menu-icon-circle--active div { + opacity: 1; +} + +.network-dropdown-header { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; +} + +.network-dropdown-divider { + width: 100%; + height: 1px; + margin: 10px 0; + background-color: $scorpion; +} + +.network-dropdown-title { + height: 25px; + width: 75px; + color: $white; + font-family: 'DIN OT'; + font-size: 18px; + line-height: 25px; + text-align: center; +} + +.network-dropdown-content { + height: 36px; + width: 265px; + color: $dusty-gray; + font-family: 'DIN OT'; + font-size: 14px; + line-height: 18px; +} + diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss new file mode 100644 index 000000000..ae6ee6311 --- /dev/null +++ b/ui/app/css/itcss/components/newui-sections.scss @@ -0,0 +1,179 @@ +/* + NewUI Container Elements + */ + +// Component Colors +$tx-view-bg: $white; +$wallet-view-bg: $wild-sand; + +// Main container +.main-container { + position: absolute; + z-index: $main-container-z-index; + font-family: "DIN OT Light"; + display: flex; + flex-wrap: wrap; + align-items: stretch; +} + +.main-container::-webkit-scrollbar { + display: none; +} + +// tx view + +.tx-view { + flex: 63.5 0 66.5%; + background: $tx-view-bg; +} + +// wallet view and sidebar + +.wallet-view { + display: flex; + flex-direction: column; + flex: 33.5 0 33.5%; + background: $wallet-view-bg; + + @media screen and (min-width: 576px) { + overflow-y: scroll; + overflow-x: hidden; + } + + .wallet-view-account-details { + flex: 0 0 150px; + } +} + +@media screen and (min-width: 576px) { + .wallet-view::-webkit-scrollbar { + display: none; + } +} + +.wallet-view-title-wrapper { + flex: 0 0 25px; +} + +.wallet-view-title { + margin-left: 15px; + font-size: 16px; + + // No title on mobile + @media screen and (max-width: 575px) { + display: none; + } +} + +.wallet-view.sidebar { + flex: 1 0 230px; + background: rgb(250, 250, 250); + z-index: $sidebar-z-index; + position: fixed; + top: 41px; + left: 0; + right: 0; + bottom: 0; + opacity: 1; + visibility: visible; + will-change: transform; + overflow-y: auto; + box-shadow: rgba(0, 0, 0, .15) 2px 2px 4px; + width: 85%; + height: 100%; +} + +.sidebar-overlay { + z-index: $sidebar-overlay-z-index; + position: fixed; + top: 41px; + left: 0; + right: 0; + bottom: 0; + opacity: 1; + visibility: visible; + background-color: rgba(0, 0, 0, .3); +} + +// main-container media queries + +@media screen and (min-width: 576px) { + .lap-visible { + display: flex; + } + + .phone-visible { + display: none; + } + + .main-container { + margin-top: 6.9vh; + width: 85%; + height: 90vh; + box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08); + } +} + +@media screen and (min-width: 769px) { + .main-container { + margin-top: 6.9vh; + width: 80%; + height: 82vh; + box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08); + } +} + +@media screen and (min-width: 1281px) { + .main-container { + margin-top: 6.9vh; + width: 65%; + height: 82vh; + box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08); + } +} + +@media screen and (max-width: 575px) { + .lap-visible { + display: none; + } + + .phone-visible { + display: flex; + } + + .main-container { + margin-top: 41px; + height: calc(100% - 41px); + width: 100%; + overflow-y: auto; + background-color: $white; + } + + button.btn-clear { + width: 93px; + height: 50px; + font-size: .7em; + background: $white; + border: 1px solid; + } +} + +// wallet view +.account-name { + + @media screen and (max-width: 575px) { + font-size: 102%; + margin-left: 3%; + } + + @media screen and (max-width: 575px) { + text-align: center; + } +} + +// account options dropdown +.account-options-menu { + align-items: center; + justify-content: flex-start; + margin: 5% 7% 0%; +} diff --git a/ui/app/css/index.css b/ui/app/css/itcss/components/sections.scss index 49b432a1f..44ec3e862 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/itcss/components/sections.scss @@ -1,222 +1,26 @@ -/* -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; - margin: 0; - padding: 0; -} - -html { - min-height: 500px; -} - -.app-root { - overflow: hidden; - position: relative -} - -.app-primary { - display: flex; -} - -input:focus, textarea:focus { - outline: none; -} - -.full-size { - height: 100%; - width: 100%; -} - -.full-width { - width: 100%; -} - -.full-height { - height: 100%; -} - -.full-flex-height { - display: flex; - flex: 1 1 auto; - flex-direction: column; -} - -#app-content { - overflow-x: hidden; - min-width: 357px; - height: 100%; - display: flex; - flex-direction: column; -} - -button, input[type="submit"] { - 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); -} +// Old scss, do not lint - clean up later +/* stylelint-disable */ -.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 +App Sections + TODO: Move into separate files. */ -.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-footer { - padding-bottom: 10px; - align-items: center; -} - -.identicon { - height: 46px; - width: 46px; - background-size: cover; - border-radius: 100%; - border: 3px solid gray; -} - +/* initialize */ textarea.twelve-word-phrase { padding: 12px; width: 300px; height: 140px; font-size: 16px; - background: white; + 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: 12px; - 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-color: #f7861c; border-style: solid; } @@ -235,11 +39,11 @@ app sections /* unlock */ .error { - color: #E20202; + color: #e20202; } .warning { - color: #FFAE00; + color: #ffae00; } .lock { @@ -249,9 +53,10 @@ app sections .lock.locked { transform: scale(1.5); - opacity: 0.0; + opacity: 0; transition: opacity 400ms ease-in, transform 400ms ease-in; } + .lock.unlocked { transform: scale(1); opacity: 1; @@ -262,15 +67,18 @@ app sections 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; } @@ -290,55 +98,46 @@ app sections .unlock-screen input[type=password] { width: 260px; - /*height: 36px; - margin-bottom: 24px; - padding: 8px;*/ } -.sizing-input{ +.sizing-input { font-size: 14px; height: 30px; padding-left: 5px; } -.editable-label{ + +.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 { @@ -346,7 +145,7 @@ input.large-input { } .accounts-section .horizontal-line { - margin: 0px 18px; + margin: 0 18px; } .accounts-list-option { @@ -363,7 +162,7 @@ input.large-input { } .unconftx-link .fa-arrow-right { - margin: 0px -8px 0px 8px; + margin: 0 -8px 0px 8px; } /* identity panel */ @@ -390,7 +189,7 @@ input.large-input { .identity-panel i { margin-top: 32px; margin-right: 6px; - color: #B9B9B9; + color: #b9b9b9; } .identity-panel .arrow-right { @@ -401,34 +200,33 @@ input.large-input { } .identity-copy.flex-column { - flex: 0.25 0 auto; + flex: .25 0 auto; justify-content: center; } /* accounts screen */ .identity-section { - } .identity-section .identity-panel { - background: #E9E9E9; - border-bottom: 1px solid #B1B1B1; + background: #e9e9e9; + border-bottom: 1px solid #b1b1b1; cursor: pointer; } .identity-section .identity-panel.selected { - background: white; - color: #F3C83E; + background: $white; + color: #f3c83e; } .identity-section .identity-panel.selected .identicon { - border-color: orange; + border-color: $orange; } .identity-section .accounts-list-option:hover, .identity-section .accounts-list-option.selected { - background:white; + background: $white; } /* account detail screen */ @@ -444,15 +242,14 @@ input.large-input { flex-grow: 10; } -.name-label{ - +.name-label { } .unapproved-tx-icon { height: 16px; width: 16px; background: rgb(47, 174, 244); - border-color: #AEAEAE; + border-color: $silver-chalice; border-radius: 13px; } @@ -460,6 +257,7 @@ input.large-input { height: 100%; visibility: hidden; } + .editing-label { display: flex; justify-content: flex-start; @@ -467,8 +265,9 @@ input.large-input { margin-bottom: 2px; font-size: 11px; text-rendering: geometricPrecision; - color: #F7861C; + color: #f7861c; } + .name-label:hover .edit-text { visibility: visible; } @@ -480,47 +279,32 @@ input.large-input { 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; + border: 2px solid #f3c83e; + background: #faf6f0; } /* Ether Balance Widget */ .ether-balance-amount { - color: #F7861C; + color: #f7861c; } .ether-balance-label { - color: #ABA9AA; + color: #aba9aa; } /* Info screen */ -.info-gray{ +.info-gray { font-family: 'Montserrat Regular'; text-transform: uppercase; - color: #AEAEAE; + color: $silver-chalice; } -.icon-size{ +.icon-size { width: 20px; } -.info{ +.info { font-family: 'Montserrat Regular', Arial; padding-bottom: 10px; display: inline-block; @@ -533,7 +317,6 @@ input.large-input { align-items: center; } - .custom-radio-selected { width: 17px; height: 17px; @@ -542,7 +325,7 @@ input.large-input { border-radius: 15px; border-width: 5px; background: rgba(247, 134, 28, 1); - border-color: #F7F7F7; + border-color: #f7f7f7; } .custom-radio-inactive { @@ -551,38 +334,26 @@ input.large-input { border: solid; border-width: 1px; border-radius: 24px; - border-color: #AEAEAE; + border-color: $silver-chalice; } .radio-titles { color: rgba(247, 134, 28, 1); } -.radio-titles-subtext { - -} - -.selected-exchange { - -} - -.buy-radio { - -} - -.eth-warning{ +.eth-warning { transition: opacity 400ms ease-in, transform 400ms ease-in; } -.buy-subview{ +.buy-subview { transition: opacity 400ms ease-in, transform 400ms ease-in; } -.input-container:hover .edit-text{ +.input-container:hover .edit-text { visibility: visible; } -.buy-inputs{ +.buy-inputs { font-family: 'Montserrat Light'; font-size: 13px; height: 20px; @@ -590,33 +361,32 @@ input.large-input { box-sizing: border-box; border: solid; border-color: transparent; - border-width: 0.5px; + border-width: .5px; border-radius: 2px; - } -.input-container:hover .buy-inputs{ + +.input-container:hover .buy-inputs { box-sizing: inherit; border: solid; - border-color: #F7861C; - border-width: 0.5px; + border-color: #f7861c; + border-width: .5px; border-radius: 2px; } -.buy-inputs:focus{ +.buy-inputs:focus { border: solid; - border-color: #F7861C; - border-width: 0.5px; + border-color: #f7861c; + border-width: .5px; border-radius: 2px; } .activeForm { - background: #F7F7F7; + background: #f7f7f7; border: none; border-radius: 8px 8px 0px 0px; width: 50%; text-align: center; padding-bottom: 4px; - } .inactiveForm { @@ -635,12 +405,12 @@ input.large-input { width: 118px; height: 42px; padding: 1px; - color: #4D4D4D; + color: #4d4d4d; } -.marketinfo{ +.marketinfo { font-family: 'Montserrat light'; - color: #AEAEAE; + color: $silver-chalice; font-size: 15px; line-height: 17px; } @@ -655,52 +425,50 @@ input.large-input { overflow: scroll; } -.icon-control .fa-refresh{ +.icon-control .fa-refresh { visibility: hidden; } -.icon-control:hover .fa-refresh{ +.icon-control:hover .fa-refresh { visibility: visible; } -.icon-control:hover .fa-chevron-right{ +.icon-control:hover .fa-chevron-right { visibility: hidden; } .inactive { - color: #AEAEAE; + color: $silver-chalice; } -.inactive button{ - background: #AEAEAE; - color: white; +.inactive button { + background: $silver-chalice; + color: $white; } -.ellip-address { +.qr-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; + font-size: 25px; + margin-top: 40px; } .qr-message { font-size: 12px; - color: #F7861C; + color: #f7861c; } div.message-container > div:first-child { margin-top: 18px; font-size: 15px; - color: #4D4D4D; + color: #4d4d4d; } .pop-hover:hover { - transform: scale(1.1); + transform: scale(1.1); } + +/* stylelint-enable */ diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss new file mode 100644 index 000000000..2d6374aa2 --- /dev/null +++ b/ui/app/css/itcss/components/send.scss @@ -0,0 +1,374 @@ +.send-screen-wrapper { + display: flex; + flex-flow: column nowrap; + z-index: 25; + font-family: 'DIN OT'; + + @media screen and (max-width: $break-small) { + width: 100%; + overflow-y: auto; + } + + section { + flex: 0 0 auto; + } +} + +.send-screen-card { + background-color: #fff; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .08); + padding: 46px 40.5px 26px; + position: relative; + top: -26px; + align-items: center; + display: flex; + flex-flow: column nowrap; + width: 498px; + flex: 1 0 auto; + + @media screen and (max-width: $break-small) { + top: 0; + width: 100%; + box-shadow: none; + padding: 12px; + } +} + +/* Send Screen */ + +.send-screen section { + margin: 4px 16px; +} + +.send-screen input { + width: 100%; + font-size: 12px; +} + +.send-eth-icon { + border-radius: 50%; + width: 70px; + height: 70px; + border: 1px solid $alto; + box-shadow: 0 0 4px 0 rgba(0, 0, 0, .2); + position: absolute; + top: -35px; + z-index: 25; + padding: 4px; + background-color: $white; + + @media screen and (max-width: $break-small) { + position: relative; + top: 0; + } +} + +.send-screen-input-wrapper { + width: 95%; + position: relative; + + .fa-bolt { + padding-right: 4px; + } + + .large-input { + border: 1px solid $dusty-gray; + border-radius: 4px; + margin: 4px 0 20px; + font-size: 16px; + line-height: 22.4px; + font-family: "DIN OT"; + } + + .send-screen-gas-input { + border: 1px solid transparent; + } + + &__error-message { + display: none; + } + + &--error { + input, + .send-screen-gas-input { + border-color: $red !important; + } + + .send-screen-input-wrapper__error-message { + display: block; + position: absolute; + bottom: 4px; + font-size: 12px; + line-height: 12px; + left: 8px; + color: $red; + } + } +} + +.send-screen-input { + width: 100%; +} + +.send-screen-gas-input { + width: 100%; + height: 41px; + border-radius: 3px; + background-color: #f3f3f3; + border-width: 0; + border-style: none; + display: flex; + justify-content: space-between; + align-items: center; + padding-left: 10px; + padding-right: 12px; + font-size: 16px; + color: $scorpion; +} + +.send-screen-amount-labels { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.send-screen-gas-labels { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.currency-toggle { + &__item { + color: $curious-blue; + cursor: pointer; + + &--selected { + color: $black; + cursor: default; + } + } +} + +.send-screen-gas-input-customize { + color: $curious-blue; + font-size: 12px; + cursor: pointer; +} + +.gas-tooltip-close-area { + position: fixed; + top: 0; + left: 0; + z-index: 1000; + width: 100%; + height: 100%; +} + +.customize-gas-tooltip-container { + position: absolute; + bottom: 50px; + width: 237px; + height: 307px; + background-color: $white; + opacity: 1; + box-shadow: $alto 0 0 5px; + z-index: 1050; + padding: 13px 19px; + font-size: 16px; + border-radius: 4px; + font-family: "Lato"; + font-weight: 500; +} + +.gas-tooltip-arrow { + height: 25px; + width: 25px; + z-index: 1200; + background: $white; + position: absolute; + transform: rotate(45deg); + left: 107px; + top: 294px; + box-shadow: 2px 2px 2px $alto; +} + +.customize-gas-tooltip-container input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + display: none; +} + +.customize-gas-tooltip-container input[type="number"]:hover::-webkit-inner-spin-button { + -webkit-appearance: none; + display: none; +} + +.customize-gas-tooltip { + position: relative; +} + +.gas-tooltip { + display: flex; + justify-content: center; +} + +.gas-tooltip-label { + font-size: 16px; + color: $tundora; +} + +.gas-tooltip-header { + padding-bottom: 12px; +} + +.gas-tooltip-input-label { + margin-bottom: 5px; +} + +.gas-tooltip-input-label i { + color: $silver-chalice; + margin-left: 6px; +} + +.customize-gas-input { + width: 178px; + height: 28px; + border: 1px solid $alto; + font-size: 16px; + color: $nile-blue; + padding-left: 8px; +} + +.customize-gas-input-wrapper { + position: relative; +} + +.gas-tooltip-input-detail { + position: absolute; + top: 4px; + right: 26px; + font-size: 12px; + color: $silver-chalice; +} + +.gas-tooltip-input-arrows { + position: absolute; + top: 0; + left: 178px; + width: 17px; + height: 28px; + border: 1px solid #dadada; + border-left: 0; + display: flex; + flex-direction: column; + color: #9b9b9b; + font-size: .8em; + padding: 1px 4px; +} + +.token-gas { + &__amount { + display: inline-block; + margin-right: 4px; + } + + &__symbol { + display: inline-block; + } +} + +.send-screen { + &__title { + color: $scorpion; + font-size: 18px; + line-height: 29px; + } + + &__subtitle { + margin: 10px 0 20px; + font-size: 14px; + line-height: 24px; + } + + &__send-button, + &__cancel-button { + width: 163px; + text-align: center; + } +} + +.send-token { + display: flex; + flex-flow: column nowrap; + z-index: 25; + font-family: "Montserrat Light"; + + &__content { + width: 498px; + height: 605px; + background-color: #fff; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .08); + padding: 46px 40.5px 26px; + position: relative; + top: -26px; + align-items: center; + display: flex; + flex-flow: column nowrap; + flex: 1 0 auto; + + @media screen and (max-width: $break-small) { + top: 0; + width: 100%; + box-shadow: none; + padding: 12px; + } + } + + .identicon { + position: absolute; + top: -35px; + z-index: 25; + + @media screen and (max-width: $break-small) { + position: relative; + top: 0; + flex: 0 0 auto; + } + } + + &__title { + color: $scorpion; + font-size: 18px; + line-height: 29px; + } + + &__description, + &__balance-text, + &__token-symbol { + margin-top: 10px; + font-size: 14px; + line-height: 24px; + text-align: center; + } + + &__token-balance { + font-size: 40px; + line-height: 40px; + margin-top: 13px; + } + + &__button-group { + display: flex; + flex-flow: column nowrap; + align-items: center; + flex: 0 0 auto; + + @media screen and (max-width: $break-small) { + margin-top: 24px; + } + + button { + width: 163px; + } + } +} diff --git a/ui/app/css/itcss/components/token-list.scss b/ui/app/css/itcss/components/token-list.scss new file mode 100644 index 000000000..9a772f666 --- /dev/null +++ b/ui/app/css/itcss/components/token-list.scss @@ -0,0 +1,46 @@ +$wallet-balance-breakpoint: 890px; +$wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and (max-width: #{$wallet-balance-breakpoint})"; + +.token-list-item { + display: flex; + flex-flow: row nowrap; + align-items: center; + padding: 20px 24px; + cursor: pointer; + transition: linear 200ms; + background-color: rgba($wallet-balance-bg, 0); + + &__token-balance { + font-size: 130%; + + @media #{$wallet-balance-breakpoint-range} { + font-size: 105%; + } + } + + &__fiat-amount { + margin-top: .25%; + font-size: 105%; + + @media #{$wallet-balance-breakpoint-range} { + font-size: 95%; + } + } + + @media #{$wallet-balance-breakpoint-range} { + padding: 10% 4%; + } + + &--active { + background-color: rgba($wallet-balance-bg, 1); + } + + &__identicon { + margin-right: 15px; + border: '1px solid #dedede'; + + @media #{$wallet-balance-breakpoint-range} { + margin-right: 4%; + } + } +} diff --git a/ui/app/css/itcss/components/transaction-list.scss b/ui/app/css/itcss/components/transaction-list.scss new file mode 100644 index 000000000..e3fe1a8b3 --- /dev/null +++ b/ui/app/css/itcss/components/transaction-list.scss @@ -0,0 +1,257 @@ +.tx-list-container { + height: 87.5%; + + @media screen and (min-width: $break-large) { + overflow-y: scroll; + } +} + +.tx-list-header { + text-transform: capitalize; +} + +@media screen and (max-width: $break-small) { + .tx-list-header-wrapper { + margin-top: .2em; + margin-bottom: .6em; + // TODO: Resolve Layout Conflicst in Wallet View + // - This fixes txlist "transactions" title dispay + // margin-top: 0.2em; + // margin-bottom: 0.6em; + justify-content: center; + } + + .tx-list-header { + align-self: center; + font-size: 12px; + color: $dusty-gray; + } +} + +@media screen and (min-width: $break-large) { + .tx-list-header-wrapper { + flex: 0 0 55px; + } + + .tx-list-header { + font-size: 16px; + margin: 1.5em 2.37em; + } + + .tx-list-container::-webkit-scrollbar { + display: none; + } +} + +.tx-list-content-divider { + height: 1px; + background: rgb(231, 231, 231); + flex: 0 0 1px; + + @media screen and (max-width: $break-small) { + margin: .1em 0; + } + + @media screen and (min-width: $break-large) { + margin: .1em 2.37em; + } +} + +.tx-list-item-wrapper { + flex: 1 1 auto; + width: 0; + align-items: stretch; + justify-content: flex-start; + display: flex; + flex-flow: column nowrap; + + @media screen and (max-width: $break-small) { + padding: 0 1.3em; + } + + @media screen and (min-width: $break-large) { + padding-bottom: 12px; + } +} + +.tx-list-clickable { + cursor: pointer; + + &:hover { + background: rgba($alto, .2); + } +} + +.tx-list-pending-item-container { + cursor: pointer; + opacity: .5; +} + +.tx-list-date-wrapper { + flex: 1 1 auto; + + @media screen and (max-width: $break-small) { + margin-top: 6px; + } + + @media screen and (min-width: $break-large) { + margin-top: 12px; + } +} + +.tx-list-content-wrapper { + align-items: stretch; + margin-bottom: 4px; + margin-top: 2px; + flex: 1 0 auto; + width: 100%; + display: flex; + flex-flow: row nowrap; + + @media screen and (max-width: $break-small) { + font-size: 12px; + + .tx-list-status { + font-size: 12px !important; + } + + .tx-list-account { + font-size: 14px !important; + } + + .tx-list-value { + font-size: 14px; + } + + .tx-list-fiat-value { + font-size: 12px; + } + } +} + +.tx-list-date { + color: $dusty-gray; + font-size: 12px; + font-family: "Montserrat UltraLight"; +} + +.tx-list-identicon-wrapper { + align-self: center; + flex: 0 0 auto; + margin-right: 16px; +} + +.tx-list-account-and-status-wrapper { + display: flex; + flex: 1 1 auto; + flex-flow: row wrap; + width: 0; + + @media screen and (max-width: $break-small) { + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + align-self: center; + } + + @media screen and (min-width: $break-large) { + flex-direction: row; + justify-content: flex-start; + align-items: center; + + .tx-list-account-wrapper { + flex: 1.3 2 auto; + min-width: 153px; + } + + .tx-list-status-wrapper { + flex: 6 6 auto; + } + } + + .tx-list-account { + font-size: 16px; + color: $scorpion; + } + + .tx-list-status { + color: $dusty-gray; + font-size: 16px; + text-transform: capitalize; + } + + .tx-list-status--rejected, + .tx-list-status--failed { + color: $monzo; + } +} + +.tx-list-item { + border-top: 1px solid rgb(231, 231, 231); + flex: 0 0 auto; + display: flex; + flex-flow: row nowrap; + + @media screen and (max-width: $break-small) { + // margin: 0 1.3em .95em; !important + } + + @media screen and (min-width: $break-large) { + margin: 0 2.37em; + } + + &:last-of-type { + border-bottom: 1px solid rgb(231, 231, 231); + margin-bottom: 32px; + } + + &__wrapper { + align-self: center; + flex: 2 2 auto; + color: $dusty-gray; + + .tx-list-value { + font-size: 16px; + text-align: right; + } + + .tx-list-value--confirmed { + color: $caribbean-green; + } + + .tx-list-fiat-value { + font-size: 12px; + text-align: right; + } + } + + &--empty { + text-align: center; + border-bottom: none !important; + padding: 16px; + } +} + +.tx-list-details-wrapper { + overflow: hidden; + flex: 0 0 35%; +} + +.tx-list-value { + font-size: 16px; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.tx-list-fiat-value { + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.tx-list-value--confirmed { + color: $caribbean-green; +} diff --git a/ui/app/css/itcss/components/wallet-balance.scss b/ui/app/css/itcss/components/wallet-balance.scss new file mode 100644 index 000000000..cd44f89bb --- /dev/null +++ b/ui/app/css/itcss/components/wallet-balance.scss @@ -0,0 +1,70 @@ +$wallet-balance-bg: $gallery; +$wallet-balance-breakpoint: 890px; +$wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and (max-width: #{$wallet-balance-breakpoint})"; + +.wallet-balance-wrapper { + flex: 0 0 auto; + transition: linear 200ms; + background: rgba($wallet-balance-bg, 0); + + &--active { + background: rgba($wallet-balance-bg, 1); + } +} + +.wallet-balance { + background: inherit; + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + flex: 0 0 auto; + cursor: pointer; + + .balance-container { + display: flex; + justify-content: flex-start; + align-items: center; + margin: 20px 24px; + flex-direction: row; + flex-grow: 3; + + @media #{$wallet-balance-breakpoint-range} { + margin: 10% 4%; + } + } + + .balance-display { + margin-left: 15px; + justify-content: flex-start; + align-items: flex-start; + + .token-amount { + font-size: 135%; + } + + .fiat-amount { + margin-top: .25%; + font-size: 105%; + } + + @media #{$wallet-balance-breakpoint-range} { + margin-left: 4%; + + .token-amount { + font-size: 105%; + } + + .fiat-amount { + font-size: 95%; + } + } + } + + .balance-icon { + border-radius: 25px; + width: 45px; + height: 45px; + border: 1px solid $alto; + } +} diff --git a/ui/app/css/itcss/generic/index.scss b/ui/app/css/itcss/generic/index.scss new file mode 100644 index 000000000..51b7cf789 --- /dev/null +++ b/ui/app/css/itcss/generic/index.scss @@ -0,0 +1,71 @@ +/* + Generic + */ + +@import './reset.scss'; + +* { + box-sizing: border-box; +} + +html, +body { + font-family: 'Montserrat Regular', Arial; + color: #4d4d4d; + font-weight: 300; + line-height: 1.4em; + background: #f7f7f7; + width: 100%; + height: 100%; + margin: 0; + padding: 0; +} + +html { + min-height: 500px; +} + +.app-root { + overflow: hidden; + position: relative; +} + +.app-primary { + display: flex; +} + +input:focus, +textarea:focus { + outline: none; +} + +/* stylelint-disable */ +#app-content { + overflow-x: hidden; + height: 100%; + display: flex; + flex-direction: column; + + @media screen and (max-width: $break-small) { + background-color: $white; + } +} +/* stylelint-enable */ + +a { + text-decoration: none; + color: inherit; +} + +a:hover { + color: #df6b0e; +} + +input.large-input, +textarea.large-input { + padding: 8px; +} + +input.large-input { + height: 36px; +} diff --git a/ui/app/css/itcss/generic/reset.scss b/ui/app/css/itcss/generic/reset.scss new file mode 100644 index 000000000..e054d533e --- /dev/null +++ b/ui/app/css/itcss/generic/reset.scss @@ -0,0 +1,147 @@ +/* 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%; + /* stylelint-disable */ + font: inherit; + /* stylelint-enable */ + vertical-align: baseline; +} + +/* HTML5 display-role reset for older browsers */ + +/* stylelint-disable */ + +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; +} + +button { + border-style: none; + cursor: pointer; +} + +/* stylelint-enable */ diff --git a/ui/app/css/itcss/objects/index.scss b/ui/app/css/itcss/objects/index.scss new file mode 100644 index 000000000..220775682 --- /dev/null +++ b/ui/app/css/itcss/objects/index.scss @@ -0,0 +1 @@ +// Objects diff --git a/ui/app/css/itcss/settings/index.scss b/ui/app/css/itcss/settings/index.scss new file mode 100644 index 000000000..58a7ca7b7 --- /dev/null +++ b/ui/app/css/itcss/settings/index.scss @@ -0,0 +1,3 @@ +@import './variables.scss'; + +@import './typography.scss'; diff --git a/ui/app/css/itcss/settings/typography.scss b/ui/app/css/itcss/settings/typography.scss new file mode 100644 index 000000000..5b7817651 --- /dev/null +++ b/ui/app/css/itcss/settings/typography.scss @@ -0,0 +1,71 @@ +@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: 400; + 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: 400; + 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: 400; + 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: 400; + font-style: normal; +} + +@font-face { + font-family: 'DIN OT'; + src: url('/fonts/DIN_OT/DINOT-2.otf') format('opentype'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'DIN OT Light'; + src: url('/fonts/DIN_OT/DINOT-2.otf') format('opentype'); + font-weight: 200; + font-style: normal; +} + +@font-face { + font-family: 'DIN NEXT'; + src: url('/fonts/DIN NEXT/DIN NEXT W01 Regular.otf') format('opentype'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'DIN NEXT Light'; + src: url('/fonts/DIN NEXT/DIN NEXT W10 Light.otf') format('opentype'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Lato'; + src: url('/fonts/Lato/Lato-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; +} diff --git a/ui/app/css/itcss/settings/variables.scss b/ui/app/css/itcss/settings/variables.scss new file mode 100644 index 000000000..624b301d1 --- /dev/null +++ b/ui/app/css/itcss/settings/variables.scss @@ -0,0 +1,69 @@ +/* + Variables + */ + +// Base Colors +$white: #fff; +$black: #000; +$orange: #ffa500; +$red: #f00; + +/* + Colors + http://chir.ag/projects/name-that-color + */ +$white-linen: #faf6f0; // formerly 'faint orange (textfield shades)' +$rajah: #f5c26d; // formerly 'light orange (button shades)' +$buttercup: #f5a623; // formerly 'dark orange (text)' +$tundora: #4a4a4a; // formerly 'borders/font/any gray' +$gallery: #efefef; +$alabaster: #f7f7f7; +$shark: #22232c; +$wild-sand: #f6f6f6; +$white: #fff; +$dusty-gray: #9b9b9b; +$alto: #dedede; +$alabaster: #fafafa; +$silver-chalice: #aeaeae; +$curious-blue: #2f9ae0; +$concrete: #f3f3f3; +$tundora: #4d4d4d; +$nile-blue: #1b344d; +$scorpion: #5d5d5d; +$caribbean-green: #02C9B1; +$silver: #cdcdcd; +$caribbean-green: #02c9b1; +$monzo: #d0021b; +$crimson: #e91550; +$blue-lagoon: #038789; +$purple: #690496; +$tulip-tree: #ebb33f; + +/* + Z-Indicies + */ +$dropdown-z-index: 30; +$token-icon-z-index: 15; +$container-z-index: 15; +$header-z-index: 12; +$mobile-header-z-index: 26; +$main-container-z-index: 18; +$send-card-z-index: 20; +$sidebar-z-index: 26; +$sidebar-overlay-z-index: 25; + +/* + Z Indicies - Current + app - 11 + hex/bn as decimal input - 1 - remove? + dropdown - 11 + loading - 10 - higher? + mascot - 0 - remove? + */ + +/* + Responsive Breakpoints + */ +$break-small: 575px; +$break-midpoint: 780px; +$break-large: 576px; diff --git a/ui/app/css/itcss/tools/index.scss b/ui/app/css/itcss/tools/index.scss new file mode 100644 index 000000000..2236729e8 --- /dev/null +++ b/ui/app/css/itcss/tools/index.scss @@ -0,0 +1 @@ +@import './utilities.scss'; diff --git a/ui/app/css/lib.css b/ui/app/css/itcss/tools/utilities.scss index f3acbee76..9f1caa732 100644 --- a/ui/app/css/lib.css +++ b/ui/app/css/itcss/tools/utilities.scss @@ -1,19 +1,34 @@ +/* + Utility Classes + */ + /* color */ .color-orange { - color: #F7861C; + color: #f7861c; // TODO: move to settings/variables } .color-forest { - color: #0A5448; + color: #0a5448; // TODO: move to settings/variables } /* lib */ +.full-size { + height: 100%; + width: 100%; +} + .full-width { width: 100%; } +.full-flex-height { + display: flex; + flex: 1 1 auto; + flex-direction: column; +} + .full-height { height: 100%; } @@ -118,16 +133,19 @@ .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); + transform: scale(.95); } .cursor-disabled { @@ -147,7 +165,7 @@ } .bold { - font-weight: bold; + font-weight: 700; } .text-transform-uppercase { @@ -172,12 +190,12 @@ hr.horizontal-line { } .hover-white:hover { - background: white; + background: $white; } .red-dot { - background: #E91550; - color: white; + background: #e91550; + color: $white; border-radius: 10px; } @@ -192,14 +210,14 @@ hr.horizontal-line { } .golden-square { - background: #EBB33F; + background: #ebb33f; } .pending-dot { - background: red; + background: $red; left: 14px; top: 14px; - color: white; + color: $white; border-radius: 10px; height: 20px; min-width: 20px; @@ -214,10 +232,9 @@ hr.horizontal-line { .keyring-label { z-index: 1; font-size: 11px; - background: rgba(255,0,0,0.8); - color: white; - bottom: 0px; - left: -8px; + background: rgba(255, 0, 0, .8); + bottom: -47px; + color: $white; border-radius: 10px; height: 20px; min-width: 20px; @@ -244,16 +261,13 @@ hr.horizontal-line { margin: 13px; } -i.fa.fa-question-circle.fa-lg.menu-icon { - font-size: 18px; -} - .ether-icon { background: rgb(0, 163, 68); border-radius: 20px; } + .testnet-icon { - background: #2465E1; + background: #2465e1; } .drop-menu-item { @@ -274,33 +288,26 @@ i.fa.fa-question-circle.fa-lg.menu-icon { .critical-error { text-align: center; margin-top: 20px; - color: red; + color: $red; } /* - Hacky breakpoint fix for account + tab sections - Resolves issue from @frankiebee in - https://github.com/MetaMask/metamask-extension/pull/1835 - Please remove this when integrating new designs + Misc */ -@media screen and (min-width: 575px) and (max-width: 800px) { - .account-data-subsection { - flex: 0 0 auto !important; // reset flex - margin-left: 10px !important; // create additional horizontal space - margin-right: 10px !important; - width: 40%; - } - - .tabSection { - flex: 0 0 auto !important; - margin-left: 10px !important; - margin-right: 10px !important; - min-width: 285px; - width: 49%; - } - - .name-label { - width: 80%; - } +// TODO: move into component-level contextual 'active' state +.letter-spacey { + letter-spacing: .1em; +} + +.active { + color: #909090; +} + +.check { + margin-left: 7px; + color: #f7861c; + flex: 1 0 auto; + display: flex; + justify-content: flex-end; } diff --git a/ui/app/css/transitions.css b/ui/app/css/itcss/trumps/index.scss index 393a944f9..d9a4202a4 100644 --- a/ui/app/css/transitions.css +++ b/ui/app/css/itcss/trumps/index.scss @@ -1,3 +1,9 @@ +/* + Trumps + */ + +// Transitions + /* universal */ .app-primary .main-enter { position: absolute; @@ -8,7 +14,7 @@ .app-primary.from-right .main-enter-active, .app-primary.from-left .main-enter-active { overflow-x: hidden; - transform: translateX(0px); + transform: translateX(0); transition: transform 300ms ease-in; } @@ -17,18 +23,27 @@ transform: translateX(360px); transition: transform 300ms ease-in; } + .app-primary.from-right .main-leave-active { transform: translateX(-360px); transition: transform 300ms ease-in; } +.sidebar.from-left { + transform: translateX(-320px); + transition: transform 300ms ease-in; +} + /* loader transitions */ -.loader-enter, .loader-leave-active { - opacity: 0.0; +.loader-enter, +.loader-leave-active { + opacity: 0; transition: opacity 150 ease-in; } -.loader-enter-active, .loader-leave { - opacity: 1.0; + +.loader-enter-active, +.loader-leave { + opacity: 1; transition: opacity 150 ease-in; } @@ -36,7 +51,22 @@ .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); } +i.fa.fa-question-circle.fa-lg.menu-icon { + font-size: 18px; +} + +// This text is contained inside a div. +// ID needed to override user agent stylesheet. +// See components/modal.scss + +/* stylelint-disable */ +#buy-modal-content-footer-text { + font-family: 'DIN OT'; + font-size: 16px; +} +/* stylelint-enable */ diff --git a/ui/app/css/reset.css b/ui/app/css/reset.css deleted file mode 100644 index 9ce89e8bc..000000000 --- a/ui/app/css/reset.css +++ /dev/null @@ -1,48 +0,0 @@ -/* http://meyerweb.com/eric/tools/css/reset/ - v2.0 | 20110126 - License: none (public domain) -*/ - -html, body, div, span, applet, object, iframe, -h1, h2, h3, h4, h5, h6, p, blockquote, pre, -a, abbr, acronym, address, big, cite, code, -del, dfn, em, img, ins, kbd, q, s, samp, -small, strike, strong, sub, sup, tt, var, -b, u, i, center, -dl, dt, dd, ol, ul, li, -fieldset, form, label, legend, -table, caption, tbody, tfoot, thead, tr, th, td, -article, aside, canvas, details, embed, -figure, figcaption, footer, header, hgroup, -menu, nav, output, ruby, section, summary, -time, mark, audio, video { - margin: 0; - padding: 0; - border: 0; - font-size: 100%; - font: inherit; - vertical-align: baseline; -} -/* HTML5 display-role reset for older browsers */ -article, aside, details, figcaption, figure, -footer, header, hgroup, menu, nav, section { - display: block; -} -body { - line-height: 1; -} -ol, ul { - list-style: none; -} -blockquote, q { - quotes: none; -} -blockquote:before, blockquote:after, -q:before, q:after { - content: ''; - content: none; -} -table { - border-collapse: collapse; - border-spacing: 0; -}
\ No newline at end of file diff --git a/ui/app/main-container.js b/ui/app/main-container.js new file mode 100644 index 000000000..eaaff8517 --- /dev/null +++ b/ui/app/main-container.js @@ -0,0 +1,71 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const AccountAndTransactionDetails = require('./account-and-transaction-details') +const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') +const ConfigScreen = require('./config') +const UnlockScreen = require('./unlock') + +module.exports = MainContainer + +inherits(MainContainer, Component) +function MainContainer () { + Component.call(this) +} + +MainContainer.prototype.render = function () { + // 3. summarize: + // switch statement goes inside MainContainer, + // or a method in renderPrimary + // - pass resulting h() to MainContainer + // - error checking in separate func + // - router in separate func + let contents = { + component: AccountAndTransactionDetails, + key: 'account-detail', + style: {}, + } + + if (this.props.isUnlocked === false) { + switch (this.props.currentViewName) { + case 'restoreVault': + log.debug('rendering restore vault screen') + contents = { + component: HDRestoreVaultScreen, + key: 'HDRestoreVaultScreen', + } + break + case 'config': + log.debug('rendering config screen from unlock screen.') + contents = { + component: ConfigScreen, + key: 'config', + } + break + default: + log.debug('rendering locked screen') + contents = { + component: UnlockScreen, + style: { + boxShadow: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + background: '#F7F7F7', + // must force 100%, because lock screen is full-width + width: '100%', + }, + key: 'locked', + } + } + } + + return h('div.main-container', { + style: contents.style, + }, [ + h(contents.component, { + key: contents.key, + }, []), + ]) +} + diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 3a98d53a9..fbabad0ef 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -14,10 +14,6 @@ function reduceApp (state, action) { if (selectedAddress) { name = 'accountDetail' } - if (hasUnconfActions) { - log.debug('pending txs detected, defaulting to conf-tx view.') - name = 'confTx' - } var defaultView = { name, @@ -36,6 +32,14 @@ function reduceApp (state, action) { var appState = extend({ shouldClose: false, menuOpen: false, + modal: { + open: false, + modalState: { + name: null, + }, + }, + sidebarOpen: false, + networkDropdownOpen: false, currentView: seedWords ? seedConfView : defaultView, accountDetail: { subview: 'transactions', @@ -49,9 +53,48 @@ function reduceApp (state, action) { }, state.appState) switch (action.type) { + // dropdown methods + case actions.NETWORK_DROPDOWN_OPEN: + return extend(appState, { + networkDropdownOpen: true, + }) - // transition methods + case actions.NETWORK_DROPDOWN_CLOSE: + return extend(appState, { + networkDropdownOpen: false, + }) + + // sidebar methods + case actions.SIDEBAR_OPEN: + return extend(appState, { + sidebarOpen: true, + }) + + case actions.SIDEBAR_CLOSE: + return extend(appState, { + sidebarOpen: false, + }) + + // modal methods: + case actions.MODAL_OPEN: + return extend(appState, { + modal: Object.assign( + state.appState.modal, + { open: true }, + { modalState: action.payload }, + ), + }) + case actions.MODAL_CLOSE: + return extend(appState, { + modal: Object.assign( + state.appState.modal, + { open: false }, + { modalState: action.payload || state.appState.modal.modalState }, + ), + }) + + // transition methods case actions.TRANSITION_FORWARD: return extend(appState, { transForward: true, @@ -133,7 +176,7 @@ function reduceApp (state, action) { transForward: true, }) - case actions.CREATE_NEW_VAULT_IN_PROGRESS: + case actions.CREATE_NEW_VAULT_IN_PROGRESS: return extend(appState, { currentView: { name: 'createVault', @@ -172,6 +215,16 @@ function reduceApp (state, action) { warning: null, }) + case actions.SHOW_SEND_TOKEN_PAGE: + return extend(appState, { + currentView: { + name: 'sendToken', + context: appState.currentView.context, + }, + transForward: true, + warning: null, + }) + case actions.SHOW_NEW_KEYCHAIN: return extend(appState, { currentView: { @@ -307,7 +360,7 @@ function reduceApp (state, action) { return extend(appState, { currentView: { name: 'confTx', - context: 0, + context: action.id ? indexForPending(state, action.id) : indexForLastPending(state), }, transForward: action.transForward, warning: null, @@ -586,3 +639,7 @@ function indexForPending (state, txId) { const index = unconfTxList.indexOf(match) return index } + +function indexForLastPending (state) { + return getUnconfActionList(state).length +} diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index e0c416c2d..cdc98d05e 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -17,6 +17,8 @@ function reduceMetamask (state, action) { lastUnreadNotice: undefined, frequentRpcList: [], addressBook: [], + selectedTokenAddress: null, + tokenExchangeRates: {}, }, state.metamask) switch (action.type) { @@ -115,6 +117,11 @@ function reduceMetamask (state, action) { delete newState.seedWords return newState + case actions.SET_SELECTED_TOKEN: + return extend(metamaskState, { + selectedTokenAddress: action.value, + }) + case actions.SAVE_ACCOUNT_LABEL: const account = action.value.account const name = action.value.label @@ -130,6 +137,15 @@ function reduceMetamask (state, action) { conversionDate: action.value.conversionDate, }) + case actions.UPDATE_TOKEN_EXCHANGE_RATE: + const { payload: { pair, marketinfo } } = action + return extend(metamaskState, { + tokenExchangeRates: { + ...metamaskState.tokenExchangeRates, + [pair]: marketinfo, + }, + }) + default: return metamaskState diff --git a/ui/app/selectors.js b/ui/app/selectors.js new file mode 100644 index 000000000..9a8bf5c7e --- /dev/null +++ b/ui/app/selectors.js @@ -0,0 +1,61 @@ +const valuesFor = require('./util').valuesFor + +const selectors = { + getSelectedAddress, + getSelectedIdentity, + getSelectedAccount, + getSelectedToken, + conversionRateSelector, + transactionsSelector, +} + +module.exports = selectors + +function getSelectedAddress (state) { + // TODO: accounts is not defined. Is it needed? + const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] + + return selectedAddress +} + +function getSelectedIdentity (state) { + const selectedAddress = getSelectedAddress(state) + const identities = state.metamask.identities + + return identities[selectedAddress] +} + +function getSelectedAccount (state) { + const accounts = state.metamask.accounts + const selectedAddress = getSelectedAddress(state) + + return accounts[selectedAddress] +} + +function getSelectedToken (state) { + const tokens = state.metamask.tokens || [] + const selectedTokenAddress = state.metamask.selectedTokenAddress + const selectedToken = tokens.filter(({ address }) => address === selectedTokenAddress)[0] + + return selectedToken || null +} + +function conversionRateSelector (state) { + return state.metamask.conversionRate +} + +function transactionsSelector (state) { + const { network, selectedTokenAddress } = state.metamask + const unapprovedMsgs = valuesFor(state.metamask.unapprovedMsgs) + const shapeShiftTxList = (network === '1') ? state.metamask.shapeShiftTxList : undefined + const transactions = state.metamask.selectedAddressTxList || [] + const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList) + + console.log({txsToRender, selectedTokenAddress}) + return selectedTokenAddress + ? txsToRender + .filter(({ txParams: { to } }) => to === selectedTokenAddress) + .sort((a, b) => b.time - a.time) + : txsToRender + .sort((a, b) => b.time - a.time) +} diff --git a/ui/app/send.js b/ui/app/send.js index a21a219eb..b14c48e56 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -1,42 +1,89 @@ -const inherits = require('util').inherits +const { inherits } = require('util') 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') +const GasTooltip = require('./components/send/gas-tooltip') +const CurrencyToggle = require('./components/send/currency-toggle') +const GasFeeDisplay = require('./components/send/gas-fee-display') +const { getSelectedIdentity } = require('./selectors') + +const { + showAccountsPage, + backToAccountDetail, + displayWarning, + hideWarning, + addToAddressBook, + signTx, +} = require('./actions') +const { stripHexPrefix, addHexPrefix } = require('ethereumjs-util') +const { isHex, numericBalance } = require('./util') +const { conversionUtil } = require('./conversion-util') +const BigNumber = require('bignumber.js') + +const ARAGON = '960b236A07cf122663c4303350609A66A7B288C0' + 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 + const { + selectedAddress: address, + accounts, + identities, + network, + addressBook, + conversionRate, + currentBlockGasLimit: blockGasLimit, + } = state.metamask + const { warning } = state.appState + const selectedIdentity = getSelectedIdentity(state) + const account = accounts[address] - return result + return { + address, + accounts, + identities, + network, + addressBook, + conversionRate, + blockGasLimit, + warning, + selectedIdentity, + error: warning && warning.split('.')[0], + account, + identity: identities[address], + balance: account ? numericBalance(account.balance) : null, + } } inherits(SendTransactionScreen, PersistentForm) function SendTransactionScreen () { PersistentForm.call(this) + + // [WIP] These are the bare minimum of tx props needed to sign a transaction + // We will need a few more for contract-related interactions + this.state = { + newTx: { + from: '', + to: '', + // these values are hardcoded, so "Next" can be clicked + amount: '0x0', // see L544 + gasPrice: '0x5d21dba00', + gas: '0x7b0d', + txData: null, + memo: '', + }, + activeCurrency: 'USD', + tooltipIsOpen: false, + } + + this.back = this.back.bind(this) + this.closeTooltip = this.closeTooltip.bind(this) + this.onSubmit = this.onSubmit.bind(this) + this.recipientDidChange = this.recipientDidChange.bind(this) + this.setActiveCurrency = this.setActiveCurrency.bind(this) + this.toggleTooltip = this.toggleTooltip.bind(this) } SendTransactionScreen.prototype.render = function () { @@ -44,195 +91,260 @@ SendTransactionScreen.prototype.render = function () { const props = this.props const { - address, - account, - identity, - network, - identities, - addressBook, + // selectedIdentity, + // network, + // identities, + // addressBook, conversionRate, - currentCurrency, } = props + const { blockGasLimit, newTx, activeCurrency } = this.state + const { gas, gasPrice } = newTx + // console.log(`activeCurrency`, activeCurrency) + // console.log({ selectedIdentity, identities }) + // console.log('SendTransactionScreen state:', this.state) + 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), + h('div.send-screen-wrapper', [ + // Main Send token Card + h('div.send-screen-card', [ + + h('img.send-eth-icon', { src: '../images/eth_logo.svg' }), + + h('div.send-screen__title', 'Send'), + + h('div.send-screen__subtitle', 'Send Ethereum to anyone with an Ethereum account'), + + h('div.send-screen-input-wrapper', [ + + h('div', 'From:'), + + h('input.large-input.send-screen-input', { + list: 'accounts', + placeholder: 'Account', + value: this.state.newTx.from, + onChange: (event) => { + console.log('event', event.target.value) + this.setState({ + newTx: { + ...this.state.newTx, + from: event.target.value, + }, + }) + }, }), - // large identicon - h('.identicon-wrapper.flex-column.flex-center.select-none', [ - h(Identicon, { - diameter: 62, - address: address, + h('datalist#accounts', [ + Object.keys(props.identities).map((key) => { + const identity = props.identities[key] + return h('option', { + value: identity.address, + label: identity.name, + key: identity.address, + }) }), ]), - // invisible place holder - h('i.fa.fa-users.fa-lg.invisible', { - style: { - marginTop: '28px', + ]), + + h('div.send-screen-input-wrapper', [ + + h('div', 'To:'), + + h('input.large-input.send-screen-input', { + name: 'address', + list: 'addresses', + placeholder: 'Address', + value: this.state.newTx.to, + onChange: (event) => { + console.log('event', event.target.value) + this.setState({ + newTx: { + ...this.state.newTx, + to: event.target.value, + }, + }) }, }), + h('datalist#addresses', [ + // Corresponds to the addresses owned. + Object.entries(props.identities).map(([key, { address, name }]) => { + return h('option', { + value: address, + label: name, + key: address, + }) + }), + // Corresponds to previously sent-to addresses. + props.addressBook.map(({ address, name }) => { + return h('option', { + value: address, + label: name, + key: address, + }) + }), + ]), + + // h(EnsInput, { + // name: 'address', + // placeholder: 'Recipient Address', + // value: this.state.newTx.to, + // onChange: (event) => { + // this.setState({ + // newTx: Object.assign( + // this.state.newTx, + // { + // to: event.target.value, + // } + // ), + // }) + // }, + // network, + // identities, + // addressBook, + // }), + ]), - // 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), + h('div.send-screen-input-wrapper', [ + + h('div.send-screen-amount-labels', [ + h('span', 'Amount'), + h(CurrencyToggle, { + activeCurrency, + onClick: (newCurrency) => this.setActiveCurrency(newCurrency), + }), // holding on icon from design + ]), - // address and getter actions - h('.flex-row.flex-center', { - style: { - marginBottom: '8px', + h('input.large-input.send-screen-input', { + placeholder: `0 ${activeCurrency}`, + type: 'number', + onChange: (event) => { + this.setState({ + newTx: Object.assign( + this.state.newTx, + { + amount: event.target.value, + } + ), + }) }, - }, [ + }), - h('div', { - style: { - lineHeight: '16px', - }, - }, addressSummary(address)), + ]), - ]), + h('div.send-screen-input-wrapper', [ + this.state.tooltipIsOpen && h(GasTooltip, { + className: 'send-tooltip', + gasPrice, + gasLimit: gas, + onClose: this.closeTooltip, + onFeeChange: ({gasLimit, gasPrice}) => { + this.setState({ + newTx: { + ...this.state.newTx, + gas: gasLimit, + gasPrice, + }, + }) + }, + }), - // balance - h('.flex-row.flex-center', [ + h('div.send-screen-gas-labels', [ + h('span', [ + h('i.fa.fa-bolt'), + 'Gas fee:', + ]), + h('span', 'What\'s this?'), + ]), - h(EthBalance, { - value: account && account.balance, + // TODO: handle loading time when switching to USD + h('div.large-input.send-screen-gas-input', {}, [ + h(GasFeeDisplay, { + activeCurrency, conversionRate, - currentCurrency, + gas, + gasPrice, + blockGasLimit, }), - + h('div.send-screen-gas-input-customize', { + onClick: this.toggleTooltip, + }, [ + 'Customize', + ]), ]), - ]), - ]), - - // - // 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'), + ]), - ]), + h('div.send-screen-input-wrapper', [ + h('div', 'Transaction memo (optional)'), + h('input.large-input.send-screen-input', { + onChange: () => { + this.setState({ + newTx: Object.assign( + this.state.newTx, + { + memo: event.target.value, + } + ), + }) + }, + }), + ]), - // - // Optional Fields - // - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginTop: '16px', - marginBottom: '16px', - }, - }, [ - 'Transaction Data (optional)', + h('div.send-screen-input-wrapper', {}, [ + h('div', {}, ['Data (optional)']), + h('input.large-input.send-screen-input', { + onChange: () => { + this.setState({ + newTx: Object.assign( + this.state.newTx, + { + txData: event.target.value, + } + ), + }) + }, + }), + ]), ]), - // 'data' field + // Buttons underneath card h('section.flex-column.flex-center', [ - h('input.large-input', { - name: 'txData', - placeholder: '0x01234', - style: { - width: '100%', - resize: 'none', - }, - dataset: { - persistentFormId: 'tx-data', - }, - }), + h('button.btn-secondary.send-screen__send-button', { + onClick: (event) => this.onSubmit(event), + }, 'Next'), + h('button.btn-tertiary.send-screen__cancel-button', { + onClick: this.back, + }, 'Cancel'), ]), ]) + ) } +SendTransactionScreen.prototype.toggleTooltip = function () { + this.setState({ tooltipIsOpen: !this.state.tooltipIsOpen }) +} + +SendTransactionScreen.prototype.closeTooltip = function () { + this.setState({ tooltipIsOpen: false }) +} + +SendTransactionScreen.prototype.setActiveCurrency = function (newCurrency) { + this.setState({ activeCurrency: newCurrency }) +} + SendTransactionScreen.prototype.navigateToAccounts = function (event) { event.stopPropagation() - this.props.dispatch(actions.showAccountsPage()) + this.props.dispatch(showAccountsPage()) } SendTransactionScreen.prototype.back = function () { var address = this.props.address - this.props.dispatch(actions.backToAccountDetail(address)) + this.props.dispatch(backToAccountDetail(address)) } SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickname) { @@ -242,47 +354,78 @@ SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickna }) } -SendTransactionScreen.prototype.onSubmit = function () { +SendTransactionScreen.prototype.onSubmit = function (event) { + event.preventDefault() const state = this.state || {} - const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '') + + // const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '') + const recipient = state.newTx.to + 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 + + // const input = document.querySelector('input[name="amount"]').value + // const input = state.newTx.value + // const value = util.normalizeEthStringToWei(input) + + // https://consensys.slack.com/archives/G1L7H42BT/p1503439134000169?thread_ts=1503438076.000411&cid=G1L7H42BT + // From @kumavis: "not needed for MVP but we will end up adding it again so consider just adding it now" + const txData = false + // Must replace with memo data. + // const txData = document.querySelector('input[name="txData"]').value + let message - if (value.gt(balance)) { - message = 'Insufficient funds.' - return this.props.dispatch(actions.displayWarning(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 (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 ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) { + // message = 'Recipient address is invalid.' + // return this.props.dispatch(actions.displayWarning(message)) + // } - if (!isHex(ethUtil.stripHexPrefix(txData)) && txData) { + if (txData && !isHex(stripHexPrefix(txData))) { message = 'Transaction data must be hex string.' - return this.props.dispatch(actions.displayWarning(message)) + return this.props.dispatch(displayWarning(message)) } - this.props.dispatch(actions.hideWarning()) + this.props.dispatch(hideWarning()) + + this.props.dispatch(addToAddressBook(recipient, nickname)) - this.props.dispatch(actions.addToAddressBook(recipient, nickname)) + // TODO: need a clean way to integrate this into conversionUtil + const sendConversionRate = state.activeCurrency === 'ETH' + ? this.props.conversionRate + : new BigNumber(1.0).div(this.props.conversionRate) + const sendAmount = conversionUtil(this.state.newTx.amount, { + fromNumericBase: 'dec', + toNumericBase: 'hex', + fromCurrency: state.activeCurrency, + toCurrency: 'ETH', + toDenomination: 'WEI', + conversionRate: sendConversionRate, + }) + var txParams = { - from: this.props.address, - value: '0x' + value.toString(16), + from: this.state.newTx.from, + to: this.state.newTx.to, + + value: sendAmount, + + // New: gas will now be specified on this step + gas: this.state.newTx.gas, + gasPrice: this.state.newTx.gasPrice, } - if (recipient) txParams.to = ethUtil.addHexPrefix(recipient) + if (recipient) txParams.to = addHexPrefix(recipient) if (txData) txParams.data = txData - this.props.dispatch(actions.signTx(txParams)) + this.props.dispatch(signTx(txParams)) } diff --git a/ui/app/unlock.js b/ui/app/unlock.js index 4180791c4..ec97b03bf 100644 --- a/ui/app/unlock.js +++ b/ui/app/unlock.js @@ -50,7 +50,7 @@ UnlockScreen.prototype.render = function () { id: 'password-box', placeholder: 'enter password', style: { - + background: 'white', }, onKeyPress: this.onKeyPress.bind(this), onInput: this.inputChanged.bind(this), diff --git a/ui/app/util.js b/ui/app/util.js index 1368ebf11..be26e15a5 100644 --- a/ui/app/util.js +++ b/ui/app/util.js @@ -1,4 +1,16 @@ +const abi = require('human-standard-token-abi') const ethUtil = require('ethereumjs-util') +const hexToBn = require('../../app/scripts/lib/hex-to-bn') +const vreme = new (require('vreme'))() + +const MIN_GAS_PRICE_GWEI_BN = new ethUtil.BN(1) +const GWEI_FACTOR = new ethUtil.BN(1e9) +const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) + +// formatData :: ( date: <Unix Timestamp> ) -> String +function formatDate (date) { + return vreme.format(new Date(date), 'March 16 2014 14:30') +} var valueTable = { wei: '1000000000000000000', @@ -36,6 +48,11 @@ module.exports = { valueTable: valueTable, bnTable: bnTable, isHex: isHex, + formatDate, + bnMultiplyByFraction, + getTxFeeBn, + shortenBalance, + getContractAtAddress, exportAsFile: exportAsFile, } @@ -217,6 +234,24 @@ function isHex (str) { return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/)) } +function bnMultiplyByFraction (targetBN, numerator, denominator) { + const numBN = new ethUtil.BN(numerator) + const denomBN = new ethUtil.BN(denominator) + return targetBN.mul(numBN).div(denomBN) +} + +function getTxFeeBn (gas, gasPrice = MIN_GAS_PRICE_BN.toString(16), blockGasLimit) { + const gasBn = hexToBn(gas) + const gasPriceBn = hexToBn(gasPrice) + const txFeeBn = gasBn.mul(gasPriceBn) + + return txFeeBn.toString(16) +} + +function getContractAtAddress (tokenAddress) { + return global.eth.contract(abi).at(tokenAddress) +} + function exportAsFile (filename, data) { // source: https://stackoverflow.com/a/33542499 by Ludovic Feltz const blob = new Blob([data], {type: 'text/csv'}) @@ -4,11 +4,7 @@ const path = require('path') module.exports = bundleCss var cssFiles = { - 'fonts.css': fs.readFileSync(path.join(__dirname, '/app/css/fonts.css'), 'utf8'), - 'reset.css': fs.readFileSync(path.join(__dirname, '/app/css/reset.css'), 'utf8'), - 'lib.css': fs.readFileSync(path.join(__dirname, '/app/css/lib.css'), 'utf8'), - 'index.css': fs.readFileSync(path.join(__dirname, '/app/css/index.css'), 'utf8'), - 'transitions.css': fs.readFileSync(path.join(__dirname, '/app/css/transitions.css'), 'utf8'), + 'index.css': fs.readFileSync(path.join(__dirname, '/app/css/output/index.css'), 'utf8'), 'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'), 'react-css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'), } diff --git a/ui/index.js b/ui/index.js index a729138d3..f748c1ea2 100644 --- a/ui/index.js +++ b/ui/index.js @@ -36,12 +36,6 @@ function startApp (metamaskState, accountManager, opts) { networkVersion: opts.networkVersion, }) - // if unconfirmed txs, start on txConf page - const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.network) - if (unapprovedTxsAll.length > 0) { - store.dispatch(actions.showConfTxPage()) - } - accountManager.on('update', function (metamaskState) { store.dispatch(actions.updateMetamaskState(metamaskState)) }) diff --git a/ui/lib/icon-factory.js b/ui/lib/icon-factory.js index 27a74de66..31498a3a9 100644 --- a/ui/lib/icon-factory.js +++ b/ui/lib/icon-factory.js @@ -53,7 +53,7 @@ function imageElFor (address) { const path = `images/contract/${fileName}` const img = document.createElement('img') img.src = path - img.style.width = '75%' + img.style.width = '100%' return img } diff --git a/ui/lib/is-mobile-view.js b/ui/lib/is-mobile-view.js new file mode 100644 index 000000000..78fd6cb54 --- /dev/null +++ b/ui/lib/is-mobile-view.js @@ -0,0 +1,5 @@ +// Checks if viewport at invoke time fits mobile dimensions +// isMobileView :: () => Bool +const isMobileView = () => window.matchMedia('screen and (max-width: 575px)').matches + +module.exports = isMobileView |