diff options
Diffstat (limited to 'ui')
72 files changed, 2643 insertions, 783 deletions
diff --git a/ui/app/accounts/import/json.js b/ui/app/accounts/import/json.js index 158a3c923..486ed8886 100644 --- a/ui/app/accounts/import/json.js +++ b/ui/app/accounts/import/json.js @@ -5,7 +5,7 @@ const connect = require('react-redux').connect const actions = require('../../actions') const FileInput = require('react-simple-file-input').default -const HELP_LINK = 'https://github.com/MetaMask/faq/blob/master/README.md#q-i-cant-use-the-import-feature-for-uploading-a-json-file-the-window-keeps-closing-when-i-try-to-select-a-file' +const HELP_LINK = 'https://support.metamask.io/kb/article/7-importing-accounts' module.exports = connect(mapStateToProps)(JsonImportSubview) diff --git a/ui/app/actions.js b/ui/app/actions.js index 4c83f95b4..2e9b34c58 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -1,5 +1,6 @@ const abi = require('human-standard-token-abi') const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url') +const ethUtil = require('ethereumjs-util') var actions = { _setBackgroundConnection: _setBackgroundConnection, @@ -143,6 +144,7 @@ var actions = { UPDATE_SEND_AMOUNT: 'UPDATE_SEND_AMOUNT', UPDATE_SEND_MEMO: 'UPDATE_SEND_MEMO', UPDATE_SEND_ERRORS: 'UPDATE_SEND_ERRORS', + CLEAR_SEND: 'CLEAR_SEND', updateGasLimit, updateGasPrice, updateGasTotal, @@ -151,6 +153,7 @@ var actions = { updateSendAmount, updateSendMemo, updateSendErrors, + clearSend, setSelectedAddress, // app messages confirmSeedWords: confirmSeedWords, @@ -181,9 +184,12 @@ var actions = { showLoadingIndication: showLoadingIndication, hideLoadingIndication: hideLoadingIndication, // buy Eth with coinbase + onboardingBuyEthView, + ONBOARDING_BUY_ETH_VIEW: 'ONBOARDING_BUY_ETH_VIEW', BUY_ETH: 'BUY_ETH', buyEth: buyEth, buyEthView: buyEthView, + buyWithShapeShift, BUY_ETH_VIEW: 'BUY_ETH_VIEW', COINBASE_SUBVIEW: 'COINBASE_SUBVIEW', coinBaseSubview: coinBaseSubview, @@ -268,14 +274,18 @@ function confirmSeedWords () { return (dispatch) => { dispatch(actions.showLoadingIndication()) log.debug(`background.clearSeedWordCache`) - background.clearSeedWordCache((err, account) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } + return new Promise((resolve, reject) => { + background.clearSeedWordCache((err, account) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.displayWarning(err.message)) + reject(err) + } - log.info('Seed word cache cleared. ' + account) - dispatch(actions.showAccountDetail(account)) + log.info('Seed word cache cleared. ' + account) + dispatch(actions.showAccountsPage()) + resolve(account) + }) }) } } @@ -284,10 +294,20 @@ function createNewVaultAndRestore (password, seed) { return (dispatch) => { dispatch(actions.showLoadingIndication()) log.debug(`background.createNewVaultAndRestore`) - background.createNewVaultAndRestore(password, seed, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.showAccountsPage()) + + return new Promise((resolve, reject) => { + background.createNewVaultAndRestore(password, seed, (err) => { + + dispatch(actions.hideLoadingIndication()) + + if (err) { + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + + dispatch(actions.showAccountsPage()) + resolve() + }) }) } } @@ -296,19 +316,26 @@ function createNewVaultAndKeychain (password) { return (dispatch) => { dispatch(actions.showLoadingIndication()) log.debug(`background.createNewVaultAndKeychain`) - background.createNewVaultAndKeychain(password, (err) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - log.debug(`background.placeSeedWords`) - background.placeSeedWords((err) => { + + return new Promise((resolve, reject) => { + background.createNewVaultAndKeychain(password, (err) => { if (err) { - return dispatch(actions.displayWarning(err.message)) + dispatch(actions.displayWarning(err.message)) + return reject(err) } - dispatch(actions.hideLoadingIndication()) - forceUpdateMetamaskState(dispatch) + log.debug(`background.placeSeedWords`) + background.placeSeedWords((err) => { + if (err) { + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + dispatch(actions.hideLoadingIndication()) + forceUpdateMetamaskState(dispatch) + resolve() + }) }) }) + } } @@ -352,18 +379,25 @@ function importNewAccount (strategy, args) { return (dispatch) => { dispatch(actions.showLoadingIndication('This may take a while, be patient.')) log.debug(`background.importAccountWithStrategy`) - background.importAccountWithStrategy(strategy, args, (err) => { - if (err) return dispatch(actions.displayWarning(err.message)) - log.debug(`background.getState`) - background.getState((err, newState) => { - dispatch(actions.hideLoadingIndication()) + return new Promise((resolve, reject) => { + background.importAccountWithStrategy(strategy, args, (err) => { if (err) { - return dispatch(actions.displayWarning(err.message)) + dispatch(actions.displayWarning(err.message)) + return reject(err) } - dispatch(actions.updateMetamaskState(newState)) - dispatch({ - type: actions.SHOW_ACCOUNT_DETAIL, - value: newState.selectedAddress, + log.debug(`background.getState`) + background.getState((err, newState) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + dispatch(actions.updateMetamaskState(newState)) + dispatch({ + type: actions.SHOW_ACCOUNT_DETAIL, + value: newState.selectedAddress, + }) + resolve(newState) }) }) }) @@ -577,13 +611,18 @@ function updateSendMemo (memo) { } function updateSendErrors (error) { - console.log(`updateSendErrors error`, error); return { type: actions.UPDATE_SEND_ERRORS, value: error, } } +function clearSend () { + return { + type: actions.CLEAR_SEND + } +} + function sendTx (txData) { log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`) @@ -603,7 +642,7 @@ function signTokenTx (tokenAddress, toAddress, amount, txData) { return dispatch => { dispatch(actions.showLoadingIndication()) const token = global.eth.contract(abi).at(tokenAddress) - token.transfer(toAddress, amount, txData) + token.transfer(toAddress, ethUtil.addHexPrefix(amount), txData) .catch(err => { dispatch(actions.hideLoadingIndication()) dispatch(actions.displayWarning(err.message)) @@ -941,21 +980,23 @@ function goBackToInitView () { function markNoticeRead (notice) { return (dispatch) => { - dispatch(this.showLoadingIndication()) + dispatch(actions.showLoadingIndication()) log.debug(`background.markNoticeRead`) - background.markNoticeRead(notice, (err, notice) => { - dispatch(this.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err)) - } - if (notice) { - return dispatch(actions.showNotice(notice)) - } else { - dispatch(this.clearNotices()) - return { - type: actions.SHOW_ACCOUNTS_PAGE, + return new Promise((resolve, reject) => { + background.markNoticeRead(notice, (err, notice) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.displayWarning(err)) + return reject(err) } - } + if (notice) { + dispatch(actions.showNotice(notice)) + resolve() + } else { + dispatch(actions.clearNotices()) + resolve() + } + }) }) } } @@ -993,7 +1034,7 @@ function setProviderType (type) { dispatch(actions.updateProviderType(type)) dispatch(actions.setSelectedToken()) }) - + } } @@ -1172,14 +1213,22 @@ function saveAccountLabel (account, label) { return (dispatch) => { dispatch(actions.showLoadingIndication()) log.debug(`background.saveAccountLabel`) - background.saveAccountLabel(account, label, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch({ - type: actions.SAVE_ACCOUNT_LABEL, - value: { account, label }, + + return new Promise((resolve, reject) => { + background.saveAccountLabel(account, label, (err) => { + dispatch(actions.hideLoadingIndication()) + + if (err) { + dispatch(actions.displayWarning(err.message)) + reject(err) + } + + dispatch({ + type: actions.SAVE_ACCOUNT_LABEL, + value: { account, label }, + }) + + resolve(account) }) }) } @@ -1207,6 +1256,13 @@ function buyEth (opts) { } } +function onboardingBuyEthView (address) { + return { + type: actions.ONBOARDING_BUY_ETH_VIEW, + value: address, + } +} + function buyEthView (address) { return { type: actions.BUY_ETH_VIEW, @@ -1272,6 +1328,18 @@ function coinShiftRquest (data, marketData) { } } +function buyWithShapeShift (data) { + return dispatch => new Promise((resolve, reject) => { + shapeShiftRequest('shift', { method: 'POST', data}, (response) => { + if (response.error) { + return reject(response.error) + } + background.createShapeShiftTx(response.deposit, response.depositType) + return resolve(response) + }) + }) +} + function showQrView (data, message) { return { type: actions.SHOW_QR_VIEW, @@ -1308,9 +1376,14 @@ function shapeShiftRequest (query, options, cb) { options.method ? method = options.method : method = 'GET' var requestListner = function (request) { - queryResponse = JSON.parse(this.responseText) - cb ? cb(queryResponse) : null - return queryResponse + try { + queryResponse = JSON.parse(this.responseText) + cb ? cb(queryResponse) : null + return queryResponse + } catch (e) { + cb ? cb({error: e}) : null + return e + } } var shapShiftReq = new XMLHttpRequest() diff --git a/ui/app/add-token.js b/ui/app/add-token.js index e313babf3..518701a1d 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -255,9 +255,9 @@ AddTokenScreen.prototype.renderTokenList = function () { h('div.add-token__token-symbol', symbol), h('div.add-token__token-name', name), ]), - tokenAlreadyAdded && ( - h('div.add-token__token-message', 'Already added') - ), + // tokenAlreadyAdded && ( + // h('div.add-token__token-message', 'Already added') + // ), ]) ) }) @@ -350,7 +350,10 @@ AddTokenScreen.prototype.render = function () { h('div.add-token__footers', [ h('div.add-token__add-custom', { onClick: () => this.setState({ isCollapsed: !isCollapsed }), - }, 'Add custom token'), + }, [ + 'Add custom token', + h(`i.fa.fa-angle-${isCollapsed ? 'down' : 'up'}`), + ]), this.renderCustomForm(), ]), ]), diff --git a/ui/app/app.js b/ui/app/app.js index ae38fad7f..7264c79c7 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -4,6 +4,9 @@ const connect = require('react-redux').connect const h = require('react-hyperscript') const { checkFeatureToggle } = require('../lib/feature-toggle-utils') const actions = require('./actions') +// mascara +const MascaraFirstTime = require('../../mascara/src/app/first-time').default +const MascaraBuyEtherScreen = require('../../mascara/src/app/first-time/buy-ether-screen').default // init const InitializeMenuScreen = require('./first-time/init-menu') const NewKeyChainScreen = require('./new-keychain') @@ -21,7 +24,7 @@ const generateLostAccountsNotice = require('../lib/lost-accounts-notice') const WalletView = require('./components/wallet-view') // other views -const ConfigScreen = require('./config') +const Settings = require('./settings') const AddTokenScreen = require('./add-token') const Import = require('./accounts/import') const InfoScreen = require('./info') @@ -50,6 +53,9 @@ function mapStateToProps (state) { accounts, address, keyrings, + isInitialized, + noActiveNotices, + seedWords, } = state.metamask const selected = address || Object.keys(accounts)[0] @@ -66,6 +72,8 @@ function mapStateToProps (state) { currentView: state.appState.currentView, activeAddress: state.appState.activeAddress, transForward: state.appState.transForward, + isMascara: state.metamask.isMascara, + isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized), seedWords: state.metamask.seedWords, unapprovedTxs: state.metamask.unapprovedTxs, unapprovedMsgs: state.metamask.unapprovedMsgs, @@ -140,6 +148,8 @@ App.prototype.render = function () { (isLoading || isLoadingNetwork) && h(Loading, { loadingMessage: loadMessage, }), + + // this.renderLoadingIndicator({ isLoading, isLoadingNetwork, loadMessage }), // content this.renderPrimary(), @@ -203,9 +213,35 @@ App.prototype.renderSidebar = function () { } App.prototype.renderAppBar = function () { + const { + isUnlocked, + network, + provider, + networkDropdownOpen, + showNetworkDropdown, + hideNetworkDropdown, + currentView, + } = this.props + if (window.METAMASK_UI_TYPE === 'notification') { return null } + + const props = this.props + const state = this.state || {} + const isNetworkMenuOpen = state.isNetworkMenuOpen || false + const {isMascara, isOnboarding} = props + + // Do not render header if user is in mascara onboarding + if (isMascara && isOnboarding) { + return null + } + + // Do not render header if user is in mascara buy ether + if (isMascara && props.currentView.name === 'buyEth') { + return null + } + return ( h('.full-width', { @@ -217,12 +253,14 @@ App.prototype.renderAppBar = function () { }, [ h('div.app-header-contents', {}, [ h('div.left-menu-wrapper', { - style: {}, + onClick: () => { + props.dispatch(actions.backToAccountDetail(props.activeAddress)) + }, }, [ // mini logo - h('img', { - height: 24, - width: 24, + h('img.metafox-icon', { + height: 29, + width: 29, src: '/images/icon-128.png', }), @@ -243,22 +281,21 @@ App.prototype.renderAppBar = function () { }, [ // Network Indicator h(NetworkIndicator, { - network: this.props.network, - provider: this.props.provider, + network, + provider, + disabled: currentView.name === 'confTx', onClick: (event) => { event.preventDefault() event.stopPropagation() - if (this.props.networkDropdownOpen === false) { - this.props.showNetworkDropdown() - } else { - this.props.hideNetworkDropdown() - } + return networkDropdownOpen === false + ? showNetworkDropdown() + : hideNetworkDropdown() }, }), ]), - h('div.account-menu__icon', { onClick: this.props.toggleAccountMenu }, [ + isUnlocked && h('div.account-menu__icon', { onClick: this.props.toggleAccountMenu }, [ h(Identicon, { address: this.props.selectedAddress, diameter: 32, @@ -273,6 +310,17 @@ App.prototype.renderAppBar = function () { } +App.prototype.renderLoadingIndicator = function ({ isLoading, isLoadingNetwork, loadMessage }) { + const { isMascara } = this.props + + return isMascara + ? null + : h(Loading, { + isLoading: isLoading || isLoadingNetwork, + loadingMessage: loadMessage, + }) +} + App.prototype.renderBackButton = function (style, justArrow = false) { var props = this.props return ( @@ -295,6 +343,11 @@ App.prototype.renderBackButton = function (style, justArrow = false) { App.prototype.renderPrimary = function () { log.debug('rendering primary') var props = this.props + const {isMascara, isOnboarding} = props + + if (isMascara && isOnboarding) { + return h(MascaraFirstTime) + } // notices if (!props.noActiveNotices) { @@ -383,7 +436,7 @@ App.prototype.renderPrimary = function () { case 'config': log.debug('rendering config screen') - return h(ConfigScreen, {key: 'config'}) + return h(Settings, {key: 'config'}) case 'import-menu': log.debug('rendering import screen') @@ -395,12 +448,44 @@ App.prototype.renderPrimary = function () { case 'info': log.debug('rendering info screen') - return h(InfoScreen, {key: 'info'}) + return h(Settings, {key: 'info', tab: 'info'}) case 'buyEth': log.debug('rendering buy ether screen') return h(BuyView, {key: 'buyEthView'}) + case 'onboardingBuyEth': + log.debug('rendering onboarding buy ether screen') + return h(MascaraBuyEtherScreen, {key: 'buyEthView'}) + + case 'qr': + log.debug('rendering show qr screen') + return h('div', { + style: { + position: 'absolute', + height: '100%', + top: '0px', + left: '0px', + }, + }, [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: () => props.dispatch(actions.backToAccountDetail(props.activeAddress)), + style: { + marginLeft: '10px', + marginTop: '50px', + }, + }), + h('div', { + style: { + position: 'absolute', + left: '44px', + width: '285px', + }, + }, [ + h(QrView, {key: 'qr'}), + ]), + ]) + default: log.debug('rendering default, account detail screen') return h(MainContainer, {key: 'account-detail'}) diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index 1b46e532a..0c34a5154 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -161,8 +161,6 @@ class AccountDropdowns extends Component { ) } - - renderAccountOptions () { const { actions } = this.props const { optionsMenuActive } = this.state @@ -297,6 +295,11 @@ AccountDropdowns.propTypes = { identities: PropTypes.objectOf(PropTypes.object), selected: PropTypes.string, keyrings: PropTypes.array, + actions: PropTypes.objectOf(PropTypes.func), + network: PropTypes.string, + style: PropTypes.object, + enableAccountOptions: PropTypes.bool, + enableAccountsSelector: PropTypes.bool, } const mapDispatchToProps = (dispatch) => { diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index 85bd21076..38c7bcb2d 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -46,6 +46,10 @@ function mapDispatchToProps (dispatch) { dispatch(actions.showImportPage()) dispatch(actions.toggleAccountMenu()) }, + showInfoPage: () => { + dispatch(actions.showInfoPage()) + dispatch(actions.toggleAccountMenu()) + }, } } @@ -57,16 +61,18 @@ AccountMenu.prototype.render = function () { showImportPage, lockMetamask, showConfigPage, + showInfoPage, } = this.props return h(Menu, { className: 'account-menu', isShowing: isAccountMenuOpen }, [ h(CloseArea, { onClick: toggleAccountMenu }), h(Item, { className: 'account-menu__header', - onClick: lockMetamask, }, [ 'My Accounts', - h('button.account-menu__logout-button', 'Log out'), + h('button.account-menu__logout-button', { + onClick: lockMetamask, + }, 'Log out'), ]), h(Divider), h('div.account-menu__accounts', this.renderAccounts()), @@ -83,6 +89,7 @@ AccountMenu.prototype.render = function () { }), h(Divider), h(Item, { + onClick: showInfoPage, icon: h('img', { src: 'images/mm-info-icon.svg' }), text: 'Info & Help', }), @@ -98,15 +105,14 @@ AccountMenu.prototype.renderAccounts = function () { const { identities, accounts, - selected, + selectedAddress, keyrings, showAccountDetail, } = this.props - console.log({ accounts }) return Object.keys(identities).map((key, index) => { const identity = identities[key] - const isSelected = identity.address === selected + const isSelected = identity.address === selectedAddress const balanceValue = accounts[key] ? accounts[key].balance : '' const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' @@ -122,7 +128,7 @@ AccountMenu.prototype.renderAccounts = function () { { onClick: () => showAccountDetail(identity.address) }, [ h('div.account-menu__check-mark', [ - isSelected ? h('i.fa.fa-check') : null, + isSelected ? h('div.account-menu__check-mark-icon') : null, ]), h( @@ -148,6 +154,6 @@ AccountMenu.prototype.indicateIfLoose = function (keyring) { try { // Sometimes keyrings aren't loaded yet: const type = keyring.type const isLoose = type !== 'HD Key Tree' - return isLoose ? h('.keyring-label', 'LOOSE') : null + return isLoose ? h('.keyring-label', 'IMPORTED') : null } catch (e) { return } } diff --git a/ui/app/components/bn-as-decimal-input.js b/ui/app/components/bn-as-decimal-input.js index d84834d06..22e37602e 100644 --- a/ui/app/components/bn-as-decimal-input.js +++ b/ui/app/components/bn-as-decimal-input.js @@ -31,6 +31,8 @@ BnAsDecimalInput.prototype.render = function () { const suffix = props.suffix const style = props.style const valueString = value.toString(10) + const newMin = min && this.downsize(min.toString(10), scale) + const newMax = max && this.downsize(max.toString(10), scale) const newValue = this.downsize(valueString, scale) return ( @@ -47,8 +49,8 @@ BnAsDecimalInput.prototype.render = function () { type: 'number', step: 'any', required: true, - min, - max, + min: newMin, + max: newMax, style: extend({ display: 'block', textAlign: 'right', @@ -128,15 +130,17 @@ BnAsDecimalInput.prototype.updateValidity = function (event) { } BnAsDecimalInput.prototype.constructWarning = function () { - const { name, min, max } = this.props + const { name, min, max, scale, suffix } = this.props + const newMin = min && this.downsize(min.toString(10), scale) + const newMax = max && this.downsize(max.toString(10), scale) let message = name ? name + ' ' : '' if (min && max) { - message += `must be greater than or equal to ${min} and less than or equal to ${max}.` + message += `must be greater than or equal to ${newMin} ${suffix} and less than or equal to ${newMax} ${suffix}.` } else if (min) { - message += `must be greater than or equal to ${min}.` + message += `must be greater than or equal to ${newMin} ${suffix}.` } else if (max) { - message += `must be less than or equal to ${max}.` + message += `must be less than or equal to ${newMax} ${suffix}.` } else { message += 'Invalid input.' } diff --git a/ui/app/components/buy-button-subview.js b/ui/app/components/buy-button-subview.js index a36f41df5..d5958787b 100644 --- a/ui/app/components/buy-button-subview.js +++ b/ui/app/components/buy-button-subview.js @@ -76,7 +76,7 @@ BuyButtonSubview.prototype.headerSubview = function () { paddingTop: '4px', paddingBottom: '4px', }, - }, 'Buy Eth'), + }, 'Deposit Eth'), ]), // loading indication diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js index 744891c47..722ed2b23 100644 --- a/ui/app/components/customize-gas-modal/index.js +++ b/ui/app/components/customize-gas-modal/index.js @@ -6,23 +6,46 @@ const actions = require('../../actions') const GasModalCard = require('./gas-modal-card') const { - MIN_GAS_PRICE, - MIN_GAS_LIMIT, + MIN_GAS_PRICE_DEC, + MIN_GAS_LIMIT_DEC, + MIN_GAS_PRICE_GWEI, } = require('../send/send-constants') -const { conversionUtil, multiplyCurrencies } = require('../../conversion-util') +const { + isBalanceSufficient, +} = require('../send/send-utils') + +const { + conversionUtil, + multiplyCurrencies, + conversionGreaterThan, +} = require('../../conversion-util') const { getGasPrice, getGasLimit, conversionRateSelector, + getSendAmount, + getSelectedToken, + getSendFrom, + getCurrentAccountWithSendEtherInfo, + getSelectedTokenToFiatRate, } = require('../../selectors') function mapStateToProps (state) { + const selectedToken = getSelectedToken(state) + const currentAccount = getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state) + const conversionRate = conversionRateSelector(state) + return { gasPrice: getGasPrice(state), gasLimit: getGasLimit(state), - conversionRate: conversionRateSelector(state), + conversionRate, + amount: getSendAmount(state), + balance: currentAccount.balance, + primaryCurrency: selectedToken && selectedToken.symbol, + selectedToken, + amountConversionRate: selectedToken ? getSelectedTokenToFiatRate(state) : conversionRate, } } @@ -35,19 +58,34 @@ function mapDispatchToProps (dispatch) { } } +function getOriginalState(props) { + const gasPrice = props.gasPrice || MIN_GAS_PRICE_DEC + const gasLimit = props.gasLimit || MIN_GAS_LIMIT_DEC + + const gasTotal = multiplyCurrencies(gasLimit, gasPrice, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 16, + }) + + return { + gasPrice, + gasLimit, + gasTotal, + error: null, + } +} + inherits(CustomizeGasModal, Component) function CustomizeGasModal (props) { Component.call(this) - this.state = { - gasPrice: props.gasPrice || MIN_GAS_PRICE, - gasLimit: props.gasLimit || MIN_GAS_LIMIT, - } + this.state = getOriginalState(props) } module.exports = connect(mapStateToProps, mapDispatchToProps)(CustomizeGasModal) -CustomizeGasModal.prototype.save = function (gasPrice, gasLimit) { +CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) { const { updateGasPrice, updateGasLimit, @@ -55,41 +93,105 @@ CustomizeGasModal.prototype.save = function (gasPrice, gasLimit) { updateGasTotal } = this.props - const newGasTotal = multiplyCurrencies(gasLimit, gasPrice, { - toNumericBase: 'hex', - multiplicandBase: 16, - multiplierBase: 16, - }) - updateGasPrice(gasPrice) updateGasLimit(gasLimit) - updateGasTotal(newGasTotal) + updateGasTotal(gasTotal) hideModal() } +CustomizeGasModal.prototype.revert = function () { + this.setState(getOriginalState(this.props)) +} + +CustomizeGasModal.prototype.validate = function ({ gasTotal, gasLimit }) { + const { + amount, + balance, + primaryCurrency, + selectedToken, + amountConversionRate, + conversionRate, + } = this.props + + let error = null + + const balanceIsSufficient = isBalanceSufficient({ + amount, + gasTotal, + balance, + primaryCurrency, + selectedToken, + amountConversionRate, + conversionRate, + }) + + if (!balanceIsSufficient) { + error = 'Insufficient balance for current gas total' + } + + const gasLimitTooLow = gasLimit && conversionGreaterThan( + { + value: MIN_GAS_LIMIT_DEC, + fromNumericBase: 'dec', + conversionRate, + }, + { + value: gasLimit, + fromNumericBase: 'hex', + }, + ) + + if (gasLimitTooLow) { + error = 'Gas limit must be at least 21000' + } + + this.setState({ error }) + return error +} + CustomizeGasModal.prototype.convertAndSetGasLimit = function (newGasLimit) { - const convertedGasLimit = conversionUtil(newGasLimit, { + const { gasPrice } = this.state + + const gasLimit = conversionUtil(newGasLimit, { fromNumericBase: 'dec', toNumericBase: 'hex', }) - this.setState({ gasLimit: convertedGasLimit }) + const gasTotal = multiplyCurrencies(gasLimit, gasPrice, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 16, + }) + + this.validate({ gasTotal, gasLimit }) + + this.setState({ gasTotal, gasLimit }) } CustomizeGasModal.prototype.convertAndSetGasPrice = function (newGasPrice) { - const convertedGasPrice = conversionUtil(newGasPrice, { + const { gasLimit } = this.state + + const gasPrice = conversionUtil(newGasPrice, { fromNumericBase: 'dec', toNumericBase: 'hex', fromDenomination: 'GWEI', toDenomination: 'WEI', }) - this.setState({ gasPrice: convertedGasPrice }) + const gasTotal = multiplyCurrencies(gasLimit, gasPrice, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 16, + }) + + this.validate({ gasTotal }) + + this.setState({ gasTotal, gasPrice }) } CustomizeGasModal.prototype.render = function () { const { hideModal, conversionRate } = this.props - const { gasPrice, gasLimit } = this.state + const { gasPrice, gasLimit, gasTotal, error } = this.state const convertedGasPrice = conversionUtil(gasPrice, { fromNumericBase: 'hex', @@ -104,7 +206,7 @@ CustomizeGasModal.prototype.render = function () { }) return h('div.send-v2__customize-gas', {}, [ - h('div', { + h('div.send-v2__customize-gas__content', { }, [ h('div.send-v2__customize-gas__header', {}, [ @@ -120,17 +222,17 @@ CustomizeGasModal.prototype.render = function () { h(GasModalCard, { value: convertedGasPrice, - min: MIN_GAS_PRICE, + min: MIN_GAS_PRICE_GWEI, // max: 1000, step: 1, onChange: value => this.convertAndSetGasPrice(value), - title: 'Gas Price', + title: 'Gas Price (GWEI)', copy: 'We calculate the suggested gas prices based on network success rates.', }), h(GasModalCard, { value: convertedGasLimit, - min: MIN_GAS_LIMIT, + min: 1, // max: 100000, step: 1, onChange: value => this.convertAndSetGasLimit(value), @@ -141,9 +243,13 @@ CustomizeGasModal.prototype.render = function () { ]), h('div.send-v2__customize-gas__footer', {}, [ + + error && h('div.send-v2__customize-gas__error-message', [ + error, + ]), h('div.send-v2__customize-gas__revert', { - onClick: () => console.log('Revert'), + onClick: () => this.revert(), }, ['Revert']), h('div.send-v2__customize-gas__buttons', [ @@ -151,8 +257,8 @@ CustomizeGasModal.prototype.render = function () { onClick: this.props.hideModal, }, ['CANCEL']), - h('div.send-v2__customize-gas__save', { - onClick: () => this.save(gasPrice, gasLimit), + h(`div.send-v2__customize-gas__save${error ? '__error' : ''}`, { + onClick: () => !error && this.save(gasPrice, gasLimit, gasTotal), }, ['SAVE']), ]) diff --git a/ui/app/components/dropdowns/account-dropdown-mini.js b/ui/app/components/dropdowns/account-dropdown-mini.js new file mode 100644 index 000000000..96057d2b4 --- /dev/null +++ b/ui/app/components/dropdowns/account-dropdown-mini.js @@ -0,0 +1,78 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Identicon = require('../identicon') +const AccountListItem = require('../send/account-list-item') + +module.exports = AccountDropdownMini + +inherits(AccountDropdownMini, Component) +function AccountDropdownMini () { + Component.call(this) +} + +AccountDropdownMini.prototype.getListItemIcon = function (currentAccount, selectedAccount) { + const listItemIcon = h(`i.fa.fa-check.fa-lg`, { style: { color: '#02c9b1' } }) + + return currentAccount.address === selectedAccount.address + ? listItemIcon + : null +} + +AccountDropdownMini.prototype.renderDropdown = function () { + const { + accounts, + selectedAccount, + closeDropdown, + onSelect, + } = this.props + + return h('div', {}, [ + + h('div.account-dropdown-mini__close-area', { + onClick: closeDropdown, + }), + + h('div.account-dropdown-mini__list', {}, [ + + ...accounts.map(account => h(AccountListItem, { + account, + displayBalance: false, + displayAddress: false, + handleClick: () => { + onSelect(account) + closeDropdown() + }, + icon: this.getListItemIcon(account, selectedAccount), + })) + + ]), + + ]) +} + +AccountDropdownMini.prototype.render = function () { + const { + accounts, + selectedAccount, + openDropdown, + closeDropdown, + dropdownOpen, + } = this.props + + return h('div.account-dropdown-mini', {}, [ + + h(AccountListItem, { + account: selectedAccount, + handleClick: openDropdown, + displayBalance: false, + displayAddress: false, + icon: h(`i.fa.fa-caret-down.fa-lg`, { style: { color: '#dedede' } }) + }), + + dropdownOpen && this.renderDropdown(), + + ]) + +} + diff --git a/ui/app/components/dropdowns/components/dropdown.js b/ui/app/components/dropdowns/components/dropdown.js index 991c89cb8..ca68e55f7 100644 --- a/ui/app/components/dropdowns/components/dropdown.js +++ b/ui/app/components/dropdowns/components/dropdown.js @@ -65,6 +65,9 @@ Dropdown.propTypes = { onClick: PropTypes.func.isRequired, children: PropTypes.node, style: PropTypes.object.isRequired, + onClickOutside: PropTypes.func, + innerStyle: PropTypes.object, + useCssTransition: PropTypes.bool, } class DropdownMenuItem extends Component { @@ -100,6 +103,7 @@ DropdownMenuItem.propTypes = { closeMenu: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired, children: PropTypes.node, + style: PropTypes.object, } module.exports = { diff --git a/ui/app/components/dropdowns/simple-dropdown.js b/ui/app/components/dropdowns/simple-dropdown.js new file mode 100644 index 000000000..8cea78518 --- /dev/null +++ b/ui/app/components/dropdowns/simple-dropdown.js @@ -0,0 +1,91 @@ +const { Component, PropTypes } = require('react') +const h = require('react-hyperscript') +const classnames = require('classnames') +const R = require('ramda') + +class SimpleDropdown extends Component { + constructor (props) { + super(props) + this.state = { + isOpen: false, + } + } + + getDisplayValue () { + const { selectedOption, options } = this.props + const matchesOption = option => option.value === selectedOption + const matchingOption = R.find(matchesOption)(options) + return matchingOption + ? matchingOption.displayValue || matchingOption.value + : selectedOption + } + + handleClose () { + this.setState({ isOpen: false }) + } + + toggleOpen () { + const { isOpen } = this.state + this.setState({ isOpen: !isOpen }) + } + + renderOptions () { + const { options, onSelect, selectedOption } = this.props + + return h('div', [ + h('div.simple-dropdown__close-area', { + onClick: event => { + event.stopPropagation() + this.handleClose() + }, + }), + h('div.simple-dropdown__options', [ + ...options.map(option => { + return h( + 'div.simple-dropdown__option', + { + className: classnames({ + 'simple-dropdown__option--selected': option.value === selectedOption, + }), + key: option.value, + onClick: () => { + if (option.value !== selectedOption) { + onSelect(option.value) + } + + this.handleClose() + }, + }, + option.displayValue || option.value, + ) + }), + ]), + ]) + } + + render () { + const { placeholder } = this.props + const { isOpen } = this.state + + return h( + 'div.simple-dropdown', + { + onClick: () => this.toggleOpen(), + }, + [ + h('div.simple-dropdown__selected', this.getDisplayValue() || placeholder || 'Select'), + h('i.fa.fa-caret-down.fa-lg.simple-dropdown__caret'), + isOpen && this.renderOptions(), + ] + ) + } +} + +SimpleDropdown.propTypes = { + options: PropTypes.array.isRequired, + placeholder: PropTypes.string, + onSelect: PropTypes.func, + selectedOption: PropTypes.string, +} + +module.exports = SimpleDropdown diff --git a/ui/app/components/editable-label.js b/ui/app/components/editable-label.js index 167be7eaf..eb41ec50c 100644 --- a/ui/app/components/editable-label.js +++ b/ui/app/components/editable-label.js @@ -1,56 +1,88 @@ -const Component = require('react').Component +const { Component } = require('react') +const PropTypes = require('prop-types') const h = require('react-hyperscript') -const inherits = require('util').inherits -const findDOMNode = require('react-dom').findDOMNode +const classnames = require('classnames') -module.exports = EditableLabel +class EditableLabel extends Component { + constructor (props) { + super(props) -inherits(EditableLabel, Component) -function EditableLabel () { - Component.call(this) -} + this.state = { + isEditing: false, + value: props.defaultValue || '', + } + } + + handleSubmit () { + const { value } = this.state + + if (value === '') { + return + } + + Promise.resolve(this.props.onSubmit(value)) + .then(() => this.setState({ isEditing: false })) + } + + saveIfEnter (event) { + if (event.key === 'Enter') { + this.handleSubmit() + } + } -EditableLabel.prototype.render = function () { - const props = this.props - const state = this.state + renderEditing () { + const { value } = this.state - if (state && state.isEditingLabel) { - return h('div.editable-label', [ - h('input.sizing-input', { - defaultValue: props.textValue, - maxLength: '20', + return ([ + h('input.large-input.editable-label__input', { + type: 'text', + required: true, + value: this.state.value, onKeyPress: (event) => { - this.saveIfEnter(event) + if (event.key === 'Enter') { + this.handleSubmit() + } }, + onChange: event => this.setState({ value: event.target.value }), + className: classnames({ 'editable-label__input--error': value === '' }), }), - h('button.editable-button', { - onClick: () => this.saveText(), - }, 'Save'), + h('div.editable-label__icon-wrapper', [ + h('i.fa.fa-check.editable-label__icon', { + onClick: () => this.handleSubmit(), + }), + ]), ]) - } else { - return h('div.name-label', { - onClick: (event) => { - const nameAttribute = event.target.getAttribute('name') - // checks for class to handle smaller CTA above the account name - const classAttribute = event.target.getAttribute('class') - if (nameAttribute === 'edit' || classAttribute === 'edit-text') { - this.setState({ isEditingLabel: true }) - } - }, - }, this.props.children) } -} -EditableLabel.prototype.saveIfEnter = function (event) { - if (event.key === 'Enter') { - this.saveText() + renderReadonly () { + return ([ + h('div.editable-label__value', this.state.value), + h('div.editable-label__icon-wrapper', [ + h('i.fa.fa-pencil.editable-label__icon', { + onClick: () => this.setState({ isEditing: true }), + }), + ]), + ]) + } + + render () { + const { isEditing } = this.state + const { className } = this.props + + return ( + h('div.editable-label', { className: classnames(className) }, + isEditing + ? this.renderEditing() + : this.renderReadonly() + ) + ) } } -EditableLabel.prototype.saveText = function () { - var container = findDOMNode(this) - var text = container.querySelector('.editable-label input').value - var truncatedText = text.substring(0, 20) - this.props.saveText(truncatedText) - this.setState({ isEditingLabel: false, textLabel: truncatedText }) +EditableLabel.propTypes = { + onSubmit: PropTypes.func.isRequired, + defaultValue: PropTypes.string, + className: PropTypes.string, } + +module.exports = EditableLabel diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index fb8c8e579..6553053f7 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -6,7 +6,7 @@ const debounce = require('debounce') const copyToClipboard = require('copy-to-clipboard') const ENS = require('ethjs-ens') const networkMap = require('ethjs-ens/lib/network-map.json') -const ensRE = /.+\.eth$/ +const ensRE = /.+\..+$/ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js index 259fa4d73..d30b7cd56 100644 --- a/ui/app/components/identicon.js +++ b/ui/app/components/identicon.js @@ -55,6 +55,7 @@ IdenticonComponent.prototype.componentDidMount = function () { if (!address) return + // eslint-disable-next-line react/no-find-dom-node var container = findDOMNode(this) var diameter = props.diameter || this.defaultDiameter @@ -70,6 +71,7 @@ IdenticonComponent.prototype.componentDidUpdate = function () { if (!address) return + // eslint-disable-next-line react/no-find-dom-node var container = findDOMNode(this) var children = container.children diff --git a/ui/app/components/menu-droppo.js b/ui/app/components/menu-droppo.js index 95b75f54c..c80bee2be 100644 --- a/ui/app/components/menu-droppo.js +++ b/ui/app/components/menu-droppo.js @@ -97,6 +97,7 @@ MenuDroppoComponent.prototype.componentDidMount = function () { if (this && document.body) { this.globalClickHandler = this.globalClickOccurred.bind(this) document.body.addEventListener('click', this.globalClickHandler) + // eslint-disable-next-line react/no-find-dom-node var container = findDOMNode(this) this.container = container } @@ -110,6 +111,7 @@ MenuDroppoComponent.prototype.componentWillUnmount = function () { MenuDroppoComponent.prototype.globalClickOccurred = function (event) { const target = event.target + // eslint-disable-next-line react/no-find-dom-node const container = findDOMNode(this) if (target !== container && diff --git a/ui/app/components/modals/account-details-modal.js b/ui/app/components/modals/account-details-modal.js index 37a62e1c0..e3c936702 100644 --- a/ui/app/components/modals/account-details-modal.js +++ b/ui/app/components/modals/account-details-modal.js @@ -7,6 +7,7 @@ const AccountModalContainer = require('./account-modal-container') const { getSelectedIdentity, getSelectedAddress } = require('../../selectors') const genAccountLink = require('../../../lib/account-link.js') const QrView = require('../qr-code') +const EditableLabel = require('../editable-label') function mapStateToProps (state) { return { @@ -23,6 +24,7 @@ function mapDispatchToProps (dispatch) { dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' })) }, hideModal: () => dispatch(actions.hideModal()), + saveAccountLabel: (address, label) => dispatch(actions.saveAccountLabel(address, label)), } } @@ -41,14 +43,19 @@ AccountDetailsModal.prototype.render = function () { selectedIdentity, network, showExportPrivateKeyModal, - hideModal, + saveAccountLabel, } = this.props const { name, address } = selectedIdentity return h(AccountModalContainer, {}, [ + h(EditableLabel, { + className: 'account-modal__name', + defaultValue: name, + onSubmit: label => saveAccountLabel(address, label), + }), + h(QrView, { Qr: { - message: name, data: address, }, }), @@ -57,14 +64,12 @@ AccountDetailsModal.prototype.render = function () { h('button.btn-clear', { onClick: () => global.platform.openWindow({ url: genAccountLink(address, network) }), - }, [ 'View account on Etherscan' ]), + }, 'View account on Etherscan'), // Holding on redesign for Export Private Key functionality h('button.btn-clear', { - onClick: () => { - showExportPrivateKeyModal() - }, - }, [ 'Export private key' ]), - + onClick: () => showExportPrivateKeyModal(), + }, 'Export private key'), + ]) } diff --git a/ui/app/components/modals/buy-options-modal.js b/ui/app/components/modals/buy-options-modal.js index f1a5aa9fd..33615c483 100644 --- a/ui/app/components/modals/buy-options-modal.js +++ b/ui/app/components/modals/buy-options-modal.js @@ -42,7 +42,7 @@ BuyOptions.prototype.render = function () { h('div.buy-modal-content-title', { style: {}, }, 'Transfers'), - h('div', {}, 'How would you like to buy Ether?'), + h('div', {}, 'How would you like to deposit Ether?'), ]), h('div.buy-modal-content-options.flex-column.flex-center', {}, [ @@ -54,7 +54,7 @@ BuyOptions.prototype.render = function () { }, }, [ h('div.buy-modal-content-option-title', {}, 'Coinbase'), - h('div.buy-modal-content-option-subtitle', {}, 'Buy with Fiat'), + h('div.buy-modal-content-option-subtitle', {}, 'Deposit with Fiat'), ]), // h('div.buy-modal-content-option', {}, [ diff --git a/ui/app/components/modals/export-private-key-modal.js b/ui/app/components/modals/export-private-key-modal.js index 302596eda..2d8470634 100644 --- a/ui/app/components/modals/export-private-key-modal.js +++ b/ui/app/components/modals/export-private-key-modal.js @@ -66,7 +66,6 @@ ExportPrivateKeyModal.prototype.renderPasswordInput = function (privateKey) { }) : h('input.private-key-password-input', { type: 'password', - placeholder: 'Type password', onChange: event => this.setState({ password: event.target.value }), }) } @@ -84,7 +83,7 @@ ExportPrivateKeyModal.prototype.renderButtons = function (privateKey, password, (privateKey ? this.renderButton('btn-clear', () => hideModal(), 'Done') - : this.renderButton('btn-clear', () => this.exportAccountAndGetPrivateKey(this.state.password, address), 'Download') + : this.renderButton('btn-clear', () => this.exportAccountAndGetPrivateKey(this.state.password, address), 'Show') ), ]) @@ -118,7 +117,7 @@ ExportPrivateKeyModal.prototype.render = function () { h('div.account-modal-divider'), - h('span.modal-body-title', 'Download Private Keys'), + h('span.modal-body-title', 'Show Private Keys'), h('div.private-key-password', {}, [ this.renderPasswordLabel(privateKey), diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 88deb2bb0..e15dd6c1b 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -162,10 +162,9 @@ const MODALS = { h(CustomizeGasModal, {}, []), ], mobileModalStyle: { - width: '355px', - height: '598px', - // top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh', - top: '5%', + width: '100vw', + height: '100vh', + top: '0', transform: 'none', left: '0', right: '0', diff --git a/ui/app/components/modals/new-account-modal.js b/ui/app/components/modals/new-account-modal.js index 25beb6745..b78de1d8d 100644 --- a/ui/app/components/modals/new-account-modal.js +++ b/ui/app/components/modals/new-account-modal.js @@ -28,6 +28,7 @@ function mapDispatchToProps (dispatch) { dispatch(actions.hideModal()) }) }, + showImportPage: () => dispatch(actions.showImportPage()), } } @@ -36,7 +37,7 @@ function NewAccountModal () { Component.call(this) this.state = { - newAccountName: '' + newAccountName: '', } } @@ -63,7 +64,7 @@ NewAccountModal.prototype.render = function () { h('div.new-account-input-wrapper', {}, [ h('input.new-account-input', { placeholder: 'E.g. My new account', - onChange: (event) => this.setState({ newAccountName: event.target.value }) + onChange: event => this.setState({ newAccountName: event.target.value }), }, []), ]), @@ -71,13 +72,16 @@ NewAccountModal.prototype.render = function () { 'or', ]), - h('div.new-account-modal-content.after-input', {}, [ - 'Import an account', - ]), + h('div.new-account-modal-content.after-input.pointer', { + onClick: () => { + this.props.hideModal() + this.props.showImportPage() + }, + }, 'Import an account'), h('div.new-account-modal-content.button', {}, [ h('button.btn-clear', { - onClick: () => this.props.createAccount(newAccountName) + onClick: () => this.props.createAccount(newAccountName), }, [ 'SAVE', ]), diff --git a/ui/app/components/network.js b/ui/app/components/network.js index b24505750..229d02e36 100644 --- a/ui/app/components/network.js +++ b/ui/app/components/network.js @@ -64,13 +64,18 @@ Network.prototype.render = function () { return ( h('div.network-component.pointer', { className: classnames('network-component pointer', { + 'network-component--disabled': this.props.disabled, 'ethereum-network': providerName === 'mainnet', 'ropsten-test-network': providerName === 'ropsten' || parseInt(networkNumber) === 3, 'kovan-test-network': providerName === 'kovan', 'rinkeby-test-network': providerName === 'rinkeby', }), title: hoverText, - onClick: (event) => this.props.onClick(event), + onClick: (event) => { + if (!this.props.disabled) { + this.props.onClick(event) + } + }, }, [ (function () { switch (iconName) { diff --git a/ui/app/components/notice.js b/ui/app/components/notice.js index abfff1f5c..941ac33e6 100644 --- a/ui/app/components/notice.js +++ b/ui/app/components/notice.js @@ -117,6 +117,7 @@ Notice.prototype.render = function () { } Notice.prototype.componentDidMount = function () { + // eslint-disable-next-line react/no-find-dom-node var node = findDOMNode(this) linker.setupListener(node) if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) { @@ -125,6 +126,7 @@ Notice.prototype.componentDidMount = function () { } Notice.prototype.componentWillUnmount = function () { + // eslint-disable-next-line react/no-find-dom-node var node = findDOMNode(this) linker.teardownListener(node) } diff --git a/ui/app/components/pending-personal-msg.js b/ui/app/components/pending-personal-msg.js deleted file mode 100644 index 4542adb28..000000000 --- a/ui/app/components/pending-personal-msg.js +++ /dev/null @@ -1,47 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const PendingTxDetails = require('./pending-personal-msg-details') - -module.exports = PendingMsg - -inherits(PendingMsg, Component) -function PendingMsg () { - Component.call(this) -} - -PendingMsg.prototype.render = function () { - var state = this.props - var msgData = state.txData - - return ( - - h('div', { - key: msgData.id, - }, [ - - // header - h('h3', { - style: { - fontWeight: 'bold', - textAlign: 'center', - }, - }, 'Sign Message'), - - // message details - h(PendingTxDetails, state), - - // sign + cancel - h('.flex-row.flex-space-around', [ - h('button', { - onClick: state.cancelPersonalMessage, - }, 'Cancel'), - h('button', { - onClick: state.signPersonalMessage, - }, 'Sign'), - ]), - ]) - - ) -} - diff --git a/ui/app/components/pending-tx/confirm-send-ether.js b/ui/app/components/pending-tx/confirm-send-ether.js index 7162c7122..2f178f179 100644 --- a/ui/app/components/pending-tx/confirm-send-ether.js +++ b/ui/app/components/pending-tx/confirm-send-ether.js @@ -50,7 +50,7 @@ ConfirmSendEther.prototype.getAmount = function () { const { conversionRate, currentCurrency } = this.props const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} - console.log(`conversionRate, currentCurrency`, conversionRate, currentCurrency); + const FIAT = conversionUtil(txParams.value, { fromNumericBase: 'hex', toNumericBase: 'dec', @@ -194,7 +194,7 @@ ConfirmSendEther.prototype.render = function () { this.inputs = [] return ( - h('div.confirm-screen-container', { + h('div.confirm-screen-container.confirm-send-ether', { style: { minWidth: '355px' }, }, [ // Main Send token Card diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js index a4c3d16e3..abb7a0770 100644 --- a/ui/app/components/pending-tx/confirm-send-token.js +++ b/ui/app/components/pending-tx/confirm-send-token.js @@ -224,7 +224,7 @@ ConfirmSendToken.prototype.renderTotalPlusGas = function () { ]), h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', `${fiatAmount + fiatGas} ${currentCurrency}`), + h('div.confirm-screen-row-info', `${addCurrencies(fiatAmount, fiatGas)} ${currentCurrency}`), h('div.confirm-screen-row-detail', `${addCurrencies(tokenAmount, tokenGas || '0')} ${symbol}`), ]), ]) @@ -263,7 +263,7 @@ ConfirmSendToken.prototype.render = function () { this.inputs = [] return ( - h('div.confirm-screen-container', { + h('div.confirm-screen-container.confirm-send-token', { style: { minWidth: '355px' }, }, [ // Main Send token Card diff --git a/ui/app/components/qr-code.js b/ui/app/components/qr-code.js index cc723df14..83885539c 100644 --- a/ui/app/components/qr-code.js +++ b/ui/app/components/qr-code.js @@ -29,11 +29,11 @@ QrCodeView.prototype.render = function () { const qrImage = qrCode(4, 'M') qrImage.addData(address) qrImage.make() - return h('.div.flex-column.flex-center', { - style: { - }, - }, [ - Array.isArray(Qr.message) ? h('.message-container', this.renderMultiMessage()) : h('.qr-header', Qr.message), + + return h('.div.flex-column.flex-center', [ + Array.isArray(Qr.message) + ? h('.message-container', this.renderMultiMessage()) + : Qr.message && h('.qr-header', Qr.message), this.props.warning ? this.props.warning && h('span.error.flex-center', { style: { diff --git a/ui/app/components/send/account-list-item.js b/ui/app/components/send/account-list-item.js index ba7eec940..cc514cbd4 100644 --- a/ui/app/components/send/account-list-item.js +++ b/ui/app/components/send/account-list-item.js @@ -27,6 +27,8 @@ AccountListItem.prototype.render = function () { icon = null, conversionRate, currentCurrency, + displayBalance = true, + displayAddress = false, } = this.props const { name, address, balance } = account || {} @@ -46,13 +48,15 @@ AccountListItem.prototype.render = function () { }, ), - h('div.account-list-item__account-name', {}, name), + h('div.account-list-item__account-name', {}, name || address), icon && h('div.account-list-item__icon', [icon]), ]), - h(CurrencyDisplay, { + displayAddress && name && h('div.account-list-item__account-address', address), + + displayBalance && h(CurrencyDisplay, { primaryCurrency: 'ETH', convertedCurrency: currentCurrency, value: balance, diff --git a/ui/app/components/send/currency-display.js b/ui/app/components/send/currency-display.js index 7180b94d3..5dba6a8dd 100644 --- a/ui/app/components/send/currency-display.js +++ b/ui/app/components/send/currency-display.js @@ -2,7 +2,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits const Identicon = require('../identicon') -const { conversionUtil } = require('../../conversion-util') +const { conversionUtil, multiplyCurrencies } = require('../../conversion-util') module.exports = CurrencyDisplay @@ -20,14 +20,6 @@ function isValidInput (text) { return re.test(text) } -function resetCaretIfPastEnd (value, event) { - const caretPosition = event.target.selectionStart - - if (caretPosition > value.length) { - event.target.setSelectionRange(value.length, value.length) - } -} - function toHexWei (value) { return conversionUtil(value, { fromNumericBase: 'dec', @@ -40,7 +32,9 @@ CurrencyDisplay.prototype.getAmount = function (value) { const { selectedToken } = this.props const { decimals } = selectedToken || {} const multiplier = Math.pow(10, Number(decimals || 0)) - const sendAmount = '0x' + Number(value * multiplier).toString(16) + + const sendAmount = multiplyCurrencies(value, multiplier, {toNumericBase: 'hex'}) + return selectedToken ? sendAmount : toHexWei(value) @@ -80,6 +74,8 @@ CurrencyDisplay.prototype.render = function () { conversionRate, }) + const inputSizeMultiplier = readOnly ? 1 : 1.2; + return h('div', { className, style: { @@ -93,35 +89,33 @@ CurrencyDisplay.prototype.render = function () { h('input', { className: primaryBalanceClassName, - value: `${value || initValueToRender} ${primaryCurrency}`, - placeholder: `${0} ${primaryCurrency}`, + value: `${value || initValueToRender}`, + placeholder: '0', + size: (value || initValueToRender).length * inputSizeMultiplier, readOnly, onChange: (event) => { - let newValue = event.target.value.split(' ')[0] + let newValue = event.target.value if (newValue === '') { - this.setState({ value: '0' }) + newValue = '0' } else if (newValue.match(/^0[1-9]$/)) { - this.setState({ value: newValue.match(/[1-9]/)[0] }) + newValue = newValue.match(/[1-9]/)[0] } - else if (newValue && !isValidInput(newValue)) { + + if (newValue && !isValidInput(newValue)) { event.preventDefault() } else { + validate(this.getAmount(newValue)) this.setState({ value: newValue }) } }, - onBlur: event => !readOnly && handleChange(this.getAmount(event.target.value.split(' ')[0])), - onKeyUp: event => { - if (!readOnly) { - validate(toHexWei(value || initValueToRender)) - resetCaretIfPastEnd(value || initValueToRender, event) - } - }, - onClick: event => !readOnly && resetCaretIfPastEnd(value || initValueToRender, event), + onBlur: event => !readOnly && handleChange(this.getAmount(event.target.value)), }), + h('span.currency-display__currency-symbol', primaryCurrency), + ]), ]), diff --git a/ui/app/components/send/send-constants.js b/ui/app/components/send/send-constants.js index a819a8c28..8b56607cc 100644 --- a/ui/app/components/send/send-constants.js +++ b/ui/app/components/send/send-constants.js @@ -3,12 +3,19 @@ const { multiplyCurrencies } = require('../../conversion-util') const MIN_GAS_PRICE_GWEI = '1' const GWEI_FACTOR = '1e9' -const MIN_GAS_PRICE = multiplyCurrencies(GWEI_FACTOR, MIN_GAS_PRICE_GWEI, { +const MIN_GAS_PRICE_HEX = multiplyCurrencies(GWEI_FACTOR, MIN_GAS_PRICE_GWEI, { multiplicandBase: 16, multiplierBase: 16, + toNumericBase: 'hex', +}) +const MIN_GAS_PRICE_DEC = multiplyCurrencies(GWEI_FACTOR, MIN_GAS_PRICE_GWEI, { + multiplicandBase: 16, + multiplierBase: 16, + toNumericBase: 'dec', }) -const MIN_GAS_LIMIT = (21000).toString(16) -const MIN_GAS_TOTAL = multiplyCurrencies(MIN_GAS_LIMIT, MIN_GAS_PRICE, { +const MIN_GAS_LIMIT_HEX = (21000).toString(16) +const MIN_GAS_LIMIT_DEC = 21000 +const MIN_GAS_TOTAL = multiplyCurrencies(MIN_GAS_LIMIT_HEX, MIN_GAS_PRICE_HEX, { toNumericBase: 'hex', multiplicandBase: 16, multiplierBase: 16, @@ -16,8 +23,9 @@ const MIN_GAS_TOTAL = multiplyCurrencies(MIN_GAS_LIMIT, MIN_GAS_PRICE, { module.exports = { MIN_GAS_PRICE_GWEI, - GWEI_FACTOR, - MIN_GAS_PRICE, - MIN_GAS_LIMIT, + MIN_GAS_PRICE_HEX, + MIN_GAS_PRICE_DEC, + MIN_GAS_LIMIT_HEX, + MIN_GAS_LIMIT_DEC, MIN_GAS_TOTAL, } diff --git a/ui/app/components/send/send-utils.js b/ui/app/components/send/send-utils.js new file mode 100644 index 000000000..bf096d610 --- /dev/null +++ b/ui/app/components/send/send-utils.js @@ -0,0 +1,39 @@ +const { addCurrencies, conversionGreaterThan } = require('../../conversion-util') + +function isBalanceSufficient({ + amount, + gasTotal, + balance, + primaryCurrency, + selectedToken, + amountConversionRate, + conversionRate, +}) { + const totalAmount = addCurrencies(amount, gasTotal, { + aBase: 16, + bBase: 16, + toNumericBase: 'hex', + }) + + const balanceIsSufficient = conversionGreaterThan( + { + value: balance, + fromNumericBase: 'hex', + fromCurrency: primaryCurrency, + conversionRate, + }, + { + value: totalAmount, + fromNumericBase: 'hex', + conversionRate: amountConversionRate, + fromCurrency: selectedToken || primaryCurrency, + conversionRate: amountConversionRate, + }, + ) + + return balanceIsSufficient +} + +module.exports = { + isBalanceSufficient, +}
\ No newline at end of file diff --git a/ui/app/components/send/send-v2-container.js b/ui/app/components/send/send-v2-container.js index c14865e9f..fb2634de2 100644 --- a/ui/app/components/send/send-v2-container.js +++ b/ui/app/components/send/send-v2-container.js @@ -17,6 +17,7 @@ const { getAddressBook, getSendFrom, getCurrentCurrency, + getSelectedTokenToFiatRate, } = require('../../selectors') module.exports = connect(mapStateToProps, mapDispatchToProps)(SendEther) @@ -26,7 +27,6 @@ function mapStateToProps (state) { const selectedAddress = getSelectedAddress(state) const selectedToken = getSelectedToken(state) const tokenExchangeRates = state.metamask.tokenExchangeRates - const selectedTokenExchangeRate = getSelectedTokenExchangeRate(state) const conversionRate = conversionRateSelector(state) let data; @@ -40,11 +40,7 @@ function mapStateToProps (state) { primaryCurrency = selectedToken.symbol - tokenToFiatRate = multiplyCurrencies( - conversionRate, - selectedTokenExchangeRate, - { toNumericBase: 'dec' } - ) + tokenToFiatRate = getSelectedTokenToFiatRate(state) } return { @@ -80,5 +76,6 @@ function mapDispatchToProps (dispatch) { updateSendMemo: newMemo => dispatch(actions.updateSendMemo(newMemo)), updateSendErrors: newError => dispatch(actions.updateSendErrors(newError)), goHome: () => dispatch(actions.goHome()), + clearSend: () => dispatch(actions.clearSend()) } } diff --git a/ui/app/components/send/to-autocomplete.js b/ui/app/components/send/to-autocomplete.js index 686a7a23e..ab490155b 100644 --- a/ui/app/components/send/to-autocomplete.js +++ b/ui/app/components/send/to-autocomplete.js @@ -2,54 +2,118 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits const Identicon = require('../identicon') +const AccountListItem = require('./account-list-item') module.exports = ToAutoComplete inherits(ToAutoComplete, Component) function ToAutoComplete () { Component.call(this) + + this.state = { accountsToRender: [] } +} + +ToAutoComplete.prototype.getListItemIcon = function (listItemAddress, toAddress) { + const listItemIcon = h(`i.fa.fa-check.fa-lg`, { style: { color: '#02c9b1' } }) + + return toAddress && listItemAddress === toAddress + ? listItemIcon + : null +} + +ToAutoComplete.prototype.renderDropdown = function () { + const { + accounts, + closeDropdown, + onChange, + to, + } = this.props + const { accountsToRender } = this.state + + return accountsToRender.length && h('div', {}, [ + + h('div.send-v2__from-dropdown__close-area', { + onClick: closeDropdown, + }), + + h('div.send-v2__from-dropdown__list', {}, [ + + ...accountsToRender.map(account => h(AccountListItem, { + account, + handleClick: () => { + onChange(account.address) + closeDropdown() + }, + icon: this.getListItemIcon(account.address, to), + displayBalance: false, + displayAddress: true, + })) + + ]), + + ]) +} + +ToAutoComplete.prototype.handleInputEvent = function (event = {}, cb) { + const { + to, + accounts, + closeDropdown, + openDropdown, + } = this.props + + const matchingAccounts = accounts.filter(({ address }) => address.match(to || '')) + const matches = matchingAccounts.length + + if (!matches || matchingAccounts[0].address === to) { + this.setState({ accountsToRender: [] }) + event.target && event.target.select() + closeDropdown() + } + else { + this.setState({ accountsToRender: matchingAccounts }) + openDropdown() + } + cb && cb(event.target.value) +} + +ToAutoComplete.prototype.componentDidUpdate = function (nextProps, nextState) { + if (this.props.to !== nextProps.to) { + this.handleInputEvent() + } } ToAutoComplete.prototype.render = function () { - const { to, accounts, onChange, inError } = this.props + const { + to, + accounts, + openDropdown, + closeDropdown, + dropdownOpen, + onChange, + inError, + } = this.props - return h('div.send-v2__to-autocomplete', [ + return h('div.to-autocomplete', {}, [ h('input.send-v2__to-autocomplete__input', { - name: 'address', - list: 'addresses', placeholder: 'Recipient Address', className: inError ? `send-v2__error-border` : '', value: to, - onChange, - onFocus: event => { - to && event.target.select() - }, + onChange: event => onChange(event.target.value), + onFocus: event => this.handleInputEvent(event), style: { borderColor: inError ? 'red' : null, } }), - h('datalist#addresses', [ - // Corresponds to the addresses owned. - ...Object.entries(accounts).map(([key, { address, name }]) => { - return h('option', { - value: address, - label: name, - key: address, - }) - }), - // Corresponds to previously sent-to addresses. - // ...addressBook.map(({ address, name }) => { - // return h('option', { - // value: address, - // label: name, - // key: address, - // }) - // }), - ]), + !to && h(`i.fa.fa-caret-down.fa-lg.send-v2__to-autocomplete__down-caret`, { + style: { color: '#dedede' }, + onClick: () => this.handleInputEvent(), + }), + + dropdownOpen && this.renderDropdown(), ]) - } diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js index 96a86d3b1..c5993e3d3 100644 --- a/ui/app/components/shapeshift-form.js +++ b/ui/app/components/shapeshift-form.js @@ -130,8 +130,8 @@ ShapeshiftForm.prototype.renderMain = function () { alignItems: 'flex-start', }, }, [ - this.props.warning - ? this.props.warning && + this.props.warning ? + this.props.warning && h('span.error.flex-center', { style: { textAlign: 'center', diff --git a/ui/app/components/signature-request.js b/ui/app/components/signature-request.js new file mode 100644 index 000000000..a0ecbe8ec --- /dev/null +++ b/ui/app/components/signature-request.js @@ -0,0 +1,255 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Identicon = require('./identicon') +const connect = require('react-redux').connect +const ethUtil = require('ethereumjs-util') +const PendingTxDetails = require('./pending-personal-msg-details') +const AccountDropdownMini = require('./dropdowns/account-dropdown-mini') +const BinaryRenderer = require('./binary-renderer') + +const actions = require('../actions') +const { conversionUtil } = require('../conversion-util') + +const { + getSelectedAccount, + getCurrentAccountWithSendEtherInfo, + getSelectedAddress, + accountsWithSendEtherInfoSelector, + conversionRateSelector, +} = require('../selectors.js') + +function mapStateToProps (state) { + return { + balance: getSelectedAccount(state).balance, + selectedAccount: getCurrentAccountWithSendEtherInfo(state), + selectedAddress: getSelectedAddress(state), + requester: null, + requesterAddress: null, + accounts: accountsWithSendEtherInfoSelector(state), + conversionRate: conversionRateSelector(state) + } +} + +function mapDispatchToProps (dispatch) { + return { + goHome: () => dispatch(actions.goHome()) + } +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)(SignatureRequest) + +inherits(SignatureRequest, Component) +function SignatureRequest (props) { + Component.call(this) + + this.state = { + selectedAccount: props.selectedAccount, + accountDropdownOpen: false, + } +} + +SignatureRequest.prototype.renderHeader = function () { + return h('div.request-signature__header', [ + + h('div.request-signature__header-background'), + + h('div.request-signature__header__text', 'Signature Request'), + + h('div.request-signature__header__tip-container', [ + h('div.request-signature__header__tip'), + ]), + + ]) +} + +SignatureRequest.prototype.renderAccountDropdown = function () { + const { + selectedAccount, + accountDropdownOpen, + } = this.state + + const { + accounts, + } = this.props + + return h('div.request-signature__account', [ + + h('div.request-signature__account-text', ['Account:']), + + h(AccountDropdownMini, { + selectedAccount, + accounts, + onSelect: selectedAccount => this.setState({ selectedAccount }), + dropdownOpen: accountDropdownOpen, + openDropdown: () => this.setState({ accountDropdownOpen: true }), + closeDropdown: () => this.setState({ accountDropdownOpen: false }), + }) + + ]) +} + +SignatureRequest.prototype.renderBalance = function () { + const { balance, conversionRate } = this.props + + const balanceInEther = conversionUtil(balance, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromDenomination: 'WEI', + numberOfDecimals: 6, + conversionRate, + }) + + return h('div.request-signature__balance', [ + + h('div.request-signature__balance-text', ['Balance:']), + + h('div.request-signature__balance-value', `${balanceInEther} ETH`), + + ]) +} + +SignatureRequest.prototype.renderAccountInfo = function () { + return h('div.request-signature__account-info', [ + + this.renderAccountDropdown(), + + this.renderRequestIcon(), + + this.renderBalance(), + + ]) +} + +SignatureRequest.prototype.renderRequestIcon = function () { + const { requesterAddress } = this.props + + return h('div.request-signature__request-icon', [ + h(Identicon, { + diameter: 40, + address: requesterAddress, + }) + ]) +} + +SignatureRequest.prototype.renderRequestInfo = function () { + const { requester } = this.props + + return h('div.request-signature__request-info', [ + + h('div.request-signature__headline', [ + `Your signature is being requested`, + ]) + + ]) +} + +SignatureRequest.prototype.msgHexToText = function (hex) { + try { + const stripped = ethUtil.stripHexPrefix(hex) + const buff = Buffer.from(stripped, 'hex') + return buff.toString('utf8') + } catch (e) { + return hex + } +} + +SignatureRequest.prototype.renderBody = function () { + let rows + let notice = 'You are signing:' + + const { txData } = this.props + const { type, msgParams: { data } } = txData + + if (type === 'personal_sign') { + rows = [{ name: 'Message', value: this.msgHexToText(data) }] + } + else if (type === 'eth_signTypedData') { + rows = data + } + else if (type === 'eth_sign') { + rows = [{ name: 'Message', value: data }] + notice = `Signing this message can have + dangerous side effects. Only sign messages from + sites you fully trust with your entire account. + This dangerous method will be removed in a future version. ` + } + + return h('div.request-signature__body', {}, [ + + this.renderAccountInfo(), + + this.renderRequestInfo(), + + h('div.request-signature__notice', [notice]), + + h('div.request-signature__rows', [ + + ...rows.map(({ name, value }) => { + return h('div.request-signature__row', [ + h('div.request-signature__row-title', [`${name}:`]), + h('div.request-signature__row-value', value), + ]) + }), + + ]), + + ]) +} + +SignatureRequest.prototype.renderFooter = function () { + const { + goHome, + signPersonalMessage, + signTypedMessage, + cancelPersonalMessage, + cancelTypedMessage, + signMessage, + cancelMessage, + } = this.props + + const { txData } = this.props + const { type } = txData + + let cancel + let sign + if (type === 'personal_sign') { + cancel = cancelPersonalMessage + sign = signPersonalMessage + } + else if (type === 'eth_signTypedData') { + cancel = cancelTypedMessage + sign = signTypedMessage + } + else if (type === 'eth_sign') { + cancel = cancelMessage + sign = signMessage + } + + return h('div.request-signature__footer', [ + h('button.request-signature__footer__cancel-button', { + onClick: cancel, + }, 'CANCEL'), + h('button.request-signature__footer__sign-button', { + onClick: sign, + }, 'SIGN'), + ]) +} + +SignatureRequest.prototype.render = function () { + return ( + + h('div.request-signature__container', [ + + this.renderHeader(), + + this.renderBody(), + + this.renderFooter(), + + ]) + + ) + +} + diff --git a/ui/app/components/tab-bar.js b/ui/app/components/tab-bar.js index bef444a48..fe4076ed0 100644 --- a/ui/app/components/tab-bar.js +++ b/ui/app/components/tab-bar.js @@ -1,37 +1,40 @@ -const Component = require('react').Component +const { Component } = require('react') const h = require('react-hyperscript') -const inherits = require('util').inherits +const classnames = require('classnames') -module.exports = TabBar +class TabBar extends Component { + constructor (props) { + super(props) + const { defaultTab, tabs } = props -inherits(TabBar, Component) -function TabBar () { - Component.call(this) -} + this.state = { + subview: defaultTab || tabs[0].key, + } + } -TabBar.prototype.render = function () { - const props = this.props - const state = this.state || {} - const { tabs = [], defaultTab, tabSelected } = props - const { subview = defaultTab } = state + render () { + const { tabs = [], tabSelected } = this.props + const { subview } = this.state - return ( - h('.flex-row.space-around.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - paddingTop: '4px', - minHeight: '30px', - }, - }, tabs.map((tab) => { - const { key, content } = tab - return h(subview === key ? '.activeForm' : '.inactiveForm.pointer', { - onClick: () => { - this.setState({ subview: key }) - tabSelected(key) - }, - }, content) - })) - ) + return ( + h('.tab-bar', {}, [ + tabs.map((tab) => { + const { key, content } = tab + return h('div', { + className: classnames('tab-bar__tab pointer', { + 'tab-bar__tab--active': subview === key, + }), + onClick: () => { + this.setState({ subview: key }) + tabSelected(key) + }, + key, + }, content) + }), + h('div.tab-bar__tab.tab-bar__grow-tab'), + ]) + ) + } } +module.exports = TabBar diff --git a/ui/app/components/tx-view.js b/ui/app/components/tx-view.js index 59f55d485..ebef22680 100644 --- a/ui/app/components/tx-view.js +++ b/ui/app/components/tx-view.js @@ -73,7 +73,7 @@ TxView.prototype.renderButtons = function () { onClick: () => showModal({ name: 'BUY', }), - }, 'BUY'), + }, 'DEPOSIT'), h('button.btn-clear', { style: { @@ -109,14 +109,15 @@ TxView.prototype.render = function () { 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', + cursor: 'pointer', + }, + onClick: () => { + this.props.sidebarOpen ? this.props.hideSidebar() : this.props.showSidebar() }, }, []), diff --git a/ui/app/components/typed-message-renderer.js b/ui/app/components/typed-message-renderer.js index a042b57be..d170d63b7 100644 --- a/ui/app/components/typed-message-renderer.js +++ b/ui/app/components/typed-message-renderer.js @@ -32,11 +32,11 @@ TypedMessageRenderer.prototype.render = function () { ) } -function renderTypedData(values) { +function renderTypedData (values) { return values.map(function (value) { return h('div', {}, [ h('strong', {style: {display: 'block', fontWeight: 'bold'}}, String(value.name) + ':'), h('div', {}, value.value), ]) }) -}
\ No newline at end of file +} diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index a870a24e3..3cb7a8b76 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -3,7 +3,8 @@ 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 AccountDropdowns = require('./dropdowns/index.js').AccountDropdowns +const copyToClipboard = require('copy-to-clipboard') const actions = require('../actions') const BalanceComponent = require('./balance-component') const TokenList = require('./token-list') @@ -19,6 +20,7 @@ function mapStateToProps (state) { identities: state.metamask.identities, accounts: state.metamask.accounts, tokens: state.metamask.tokens, + keyrings: state.metamask.keyrings, selectedAddress: selectors.getSelectedAddress(state), selectedIdentity: selectors.getSelectedIdentity(state), selectedAccount: selectors.getSelectedAccount(state), @@ -28,15 +30,22 @@ function mapStateToProps (state) { function mapDispatchToProps (dispatch) { return { - showSendPage: () => { dispatch(actions.showSendPage()) }, - hideSidebar: () => { dispatch(actions.hideSidebar()) }, + showSendPage: () => dispatch(actions.showSendPage()), + hideSidebar: () => dispatch(actions.hideSidebar()), unsetSelectedToken: () => dispatch(actions.setSelectedToken()), + showAccountDetailModal: () => { + dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })) + }, + showAddTokenPage: () => dispatch(actions.showAddTokenPage()), } } inherits(WalletView, Component) function WalletView () { Component.call(this) + this.state = { + hasCopied: false, + } } WalletView.prototype.renderWalletBalance = function () { @@ -47,7 +56,7 @@ WalletView.prototype.renderWalletBalance = function () { hideSidebar, sidebarOpen, } = this.props - console.log({ selectedAccount }) + const selectedClass = selectedTokenAddress ? '' : 'wallet-balance-wrapper--active' @@ -73,13 +82,25 @@ WalletView.prototype.renderWalletBalance = function () { WalletView.prototype.render = function () { const { - network, responsiveDisplayClassname, identities, - selectedAddress, accounts, + responsiveDisplayClassname, + selectedAddress, selectedIdentity, + keyrings, + showAccountDetailModal, + hideSidebar, + showAddTokenPage, } = this.props // temporary logs + fake extra wallets // console.log('walletview, selectedAccount:', selectedAccount) + const keyring = keyrings.find((kr) => { + return kr.accounts.includes(selectedAddress) || + kr.accounts.includes(selectedIdentity.address) + }) + + const type = keyring.type + const isLoose = type !== 'HD Key Tree' + return h('div.wallet-view.flex-column' + (responsiveDisplayClassname || ''), { style: {}, }, [ @@ -88,57 +109,16 @@ WalletView.prototype.render = function () { h('div.flex-column.wallet-view-account-details', { style: {}, }, [ + h('div.wallet-view__sidebar-close', { + onClick: hideSidebar, + }), - 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.wallet-view__keyring-label', isLoose ? 'IMPORTED' : ''), - h('div.flex-column.flex-center', { + h('div.flex-column.flex-center.wallet-view__name-container', { + style: { margin: '0 auto' }, + onClick: showAccountDetailModal, }, [ - 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, - }, []), - ]), - h(Identicon, { diameter: 54, address: selectedAddress, @@ -150,21 +130,33 @@ WalletView.prototype.render = function () { selectedIdentity.name, ]), + h('button.wallet-view__details-button', 'DETAILS'), ]), ]), - // 'Wallet' - Title - // Not visible on mobile - h('div.flex-column.wallet-view-title-wrapper', {}, [ - h('span.wallet-view-title', {}, [ - 'Wallet', - ]), + + h('div.wallet-view__address', { + onClick: () => { + copyToClipboard(selectedAddress) + this.setState({ hasCopied: true }) + setTimeout(() => this.setState({ hasCopied: false }), 3000) + }, + }, [ + this.state.hasCopied && 'Copied to Clipboard', + !this.state.hasCopied && `${selectedAddress.slice(0, 4)}...${selectedAddress.slice(-4)}`, + h('i.fa.fa-clipboard', { style: { marginLeft: '8px' } }), ]), this.renderWalletBalance(), h(TokenList), + h('button.wallet-view__add-token-button', { + onClick: () => { + showAddTokenPage() + hideSidebar() + }, + }, 'Add Token'), ]) } diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index dfa6f88c4..97e0646e8 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -6,9 +6,10 @@ const actions = require('./actions') const txHelper = require('../lib/tx-helper') const PendingTx = require('./components/pending-tx') -const PendingMsg = require('./components/pending-msg') -const PendingPersonalMsg = require('./components/pending-personal-msg') -const PendingTypedMsg = require('./components/pending-typed-msg') +const SignatureRequest = require('./components/signature-request') +// const PendingMsg = require('./components/pending-msg') +// const PendingPersonalMsg = require('./components/pending-personal-msg') +// const PendingTypedMsg = require('./components/pending-typed-msg') const Loading = require('./components/loading') // const contentDivider = h('div', { @@ -102,8 +103,10 @@ ConfirmTxScreen.prototype.render = function () { cancelTransaction: this.cancelTransaction.bind(this, txData), signMessage: this.signMessage.bind(this, txData), signPersonalMessage: this.signPersonalMessage.bind(this, txData), + signTypedMessage: this.signTypedMessage.bind(this, txData), cancelMessage: this.cancelMessage.bind(this, txData), cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), + cancelTypedMessage: this.cancelTypedMessage.bind(this, txData), }) } @@ -118,17 +121,19 @@ function currentTxView (opts) { return h(PendingTx, opts) } else if (msgParams) { log.debug('msgParams detected, rendering pending msg') - - if (type === 'eth_sign') { - log.debug('rendering eth_sign message') - return h(PendingMsg, opts) - } else if (type === 'personal_sign') { - log.debug('rendering personal_sign message') - return h(PendingPersonalMsg, opts) - } else if (type === 'eth_signTypedData') { - log.debug('rendering eth_signTypedData message') - return h(PendingTypedMsg, opts) - } + + return h(SignatureRequest, opts) + + // if (type === 'eth_sign') { + // log.debug('rendering eth_sign message') + // return h(PendingMsg, opts) + // } else if (type === 'personal_sign') { + // log.debug('rendering personal_sign message') + // return h(PendingPersonalMsg, opts) + // } else if (type === 'eth_signTypedData') { + // log.debug('rendering eth_signTypedData message') + // return h(PendingTypedMsg, opts) + // } } return h(Loading) } diff --git a/ui/app/config.js b/ui/app/config.js deleted file mode 100644 index 8b4044882..000000000 --- a/ui/app/config.js +++ /dev/null @@ -1,215 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') -const infuraCurrencies = require('./infura-conversion.json').objects.sort((a, b) => { - return a.quote.name.toLocaleLowerCase().localeCompare(b.quote.name.toLocaleLowerCase()) - }) -const validUrl = require('valid-url') -const exportAsFile = require('./util').exportAsFile - - -module.exports = connect(mapStateToProps)(ConfigScreen) - -function mapStateToProps (state) { - return { - metamask: state.metamask, - warning: state.appState.warning, - } -} - -inherits(ConfigScreen, Component) -function ConfigScreen () { - Component.call(this) -} - -ConfigScreen.prototype.render = function () { - var state = this.props - var metamaskState = state.metamask - var warning = state.warning - - return ( - h('.flex-column.flex-grow', { style: { marginTop: '32px' } }, [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: (event) => { - state.dispatch(actions.goHome()) - }, - }), - h('h2.page-subtitle', 'Settings'), - ]), - - h('.error', { - style: { - display: warning ? 'block' : 'none', - padding: '0 20px', - textAlign: 'center', - }, - }, warning), - - // conf view - h('.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-space-around', { - style: { - padding: '20px', - }, - }, [ - - currentProviderDisplay(metamaskState), - - h('div', { style: {display: 'flex'} }, [ - h('input#new_rpc', { - placeholder: 'New RPC URL', - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - onKeyPress (event) { - if (event.key === 'Enter') { - var element = event.target - var newRpc = element.value - rpcValidation(newRpc, state) - } - }, - }), - h('button', { - style: { - alignSelf: 'center', - }, - onClick (event) { - event.preventDefault() - var element = document.querySelector('input#new_rpc') - var newRpc = element.value - rpcValidation(newRpc, state) - }, - }, 'Save'), - ]), - - h('hr.horizontal-line'), - - currentConversionInformation(metamaskState, state), - - h('hr.horizontal-line'), - - h('div', { - style: { - marginTop: '20px', - }, - }, [ - h('p', { - style: { - fontFamily: 'Montserrat Light', - fontSize: '13px', - }, - }, `State logs contain your public account addresses and sent transactions.`), - h('br'), - h('button', { - style: { - alignSelf: 'center', - }, - onClick (event) { - exportAsFile('MetaMask State Logs', window.logState()) - }, - }, 'Download State Logs'), - ]), - - h('hr.horizontal-line'), - - h('div', { - style: { - marginTop: '20px', - }, - }, [ - h('button', { - style: { - alignSelf: 'center', - }, - onClick (event) { - event.preventDefault() - state.dispatch(actions.revealSeedConfirmation()) - }, - }, 'Reveal Seed Words'), - ]), - - ]), - ]), - ]) - ) -} - -function rpcValidation (newRpc, state) { - if (validUrl.isWebUri(newRpc)) { - state.dispatch(actions.setRpcTarget(newRpc)) - } else { - var appendedRpc = `http://${newRpc}` - if (validUrl.isWebUri(appendedRpc)) { - state.dispatch(actions.displayWarning('URIs require the appropriate HTTP/HTTPS prefix.')) - } else { - state.dispatch(actions.displayWarning('Invalid RPC URI')) - } - } -} - -function currentConversionInformation (metamaskState, state) { - var currentCurrency = metamaskState.currentCurrency - var conversionDate = metamaskState.conversionDate - return h('div', [ - h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, 'Current Conversion'), - h('span', {style: { fontWeight: 'bold', paddingRight: '10px', fontSize: '13px'}}, `Updated ${Date(conversionDate)}`), - h('select#currentCurrency', { - onChange (event) { - event.preventDefault() - var element = document.getElementById('currentCurrency') - var newCurrency = element.value - state.dispatch(actions.setCurrentCurrency(newCurrency)) - }, - defaultValue: currentCurrency, - }, infuraCurrencies.map((currency) => { - console.log(`currency`, currency); - return h('option', {key: currency.quote.code, value: currency.quote.code}, `${currency.quote.code.toUpperCase()} - ${currency.quote.name}`) - }) - ), - ]) -} - -function currentProviderDisplay (metamaskState) { - var provider = metamaskState.provider - var title, value - - switch (provider.type) { - - case 'mainnet': - title = 'Current Network' - value = 'Main Ethereum Network' - break - - case 'ropsten': - title = 'Current Network' - value = 'Ropsten Test Network' - break - - case 'kovan': - title = 'Current Network' - value = 'Kovan Test Network' - break - - case 'rinkeby': - title = 'Current Network' - value = 'Rinkeby Test Network' - break - - default: - title = 'Current RPC' - value = metamaskState.provider.rpcTarget - } - - return h('div', [ - h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, title), - h('span', value), - ]) -} diff --git a/ui/app/css/itcss/components/account-dropdown-mini.scss b/ui/app/css/itcss/components/account-dropdown-mini.scss new file mode 100644 index 000000000..996993db7 --- /dev/null +++ b/ui/app/css/itcss/components/account-dropdown-mini.scss @@ -0,0 +1,48 @@ +.account-dropdown-mini { + height: 22px; + background-color: $white; + font-family: Roboto; + line-height: 16px; + font-size: 12px; + width: 124px; + + &__close-area { + position: fixed; + top: 0; + left: 0; + z-index: 1000; + width: 100%; + height: 100%; + } + + &__list { + z-index: 1050; + position: absolute; + height: 180px; + width: 96pxpx; + border: 1px solid $geyser; + border-radius: 4px; + background-color: $white; + box-shadow: 0 3px 6px 0 rgba(0 ,0 ,0 ,.11); + overflow-y: scroll; + } + + .account-list-item { + margin-top: 6px; + } + + .account-list-item__account-name { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 80px; + } + + .account-list-item__top-row { + margin: 0; + } + + .account-list-item__icon { + position: initial; + } +}
\ No newline at end of file diff --git a/ui/app/css/itcss/components/account-dropdown.scss b/ui/app/css/itcss/components/account-dropdown.scss index 4fc7c705a..c298c4019 100644 --- a/ui/app/css/itcss/components/account-dropdown.scss +++ b/ui/app/css/itcss/components/account-dropdown.scss @@ -62,4 +62,11 @@ &__account-secondary-balance { color: $dusty-gray; } + + &__account-address { + margin-left: 35px; + width: 80%; + overflow: hidden; + text-overflow: ellipsis; + } } diff --git a/ui/app/css/itcss/components/account-menu.scss b/ui/app/css/itcss/components/account-menu.scss index 090710f7b..e40e5a8c0 100644 --- a/ui/app/css/itcss/components/account-menu.scss +++ b/ui/app/css/itcss/components/account-menu.scss @@ -21,6 +21,7 @@ } &__icon { + margin-left: 20px; cursor: pointer; } @@ -65,6 +66,8 @@ .keyring-label { margin-top: 5px; + background-color: $black; + color: $dusty-gray; } } @@ -88,9 +91,20 @@ &__check-mark { width: 14px; + margin-right: 12px; flex: 0 0 auto; } + &__check-mark-icon { + background-image: url("images/check-white.svg"); + height: 18px; + width: 18px; + background-repeat: no-repeat; + background-position: center; + background-size: contain; + margin: 3px 0; + } + .identicon { margin: 0 12px 0 0; flex: 0 0 auto; diff --git a/ui/app/css/itcss/components/add-token.scss b/ui/app/css/itcss/components/add-token.scss index aa8221c9a..5f6d0fcff 100644 --- a/ui/app/css/itcss/components/add-token.scss +++ b/ui/app/css/itcss/components/add-token.scss @@ -7,19 +7,6 @@ z-index: 12; font-family: 'DIN Next Light'; - @media screen and (max-width: $break-small) { - top: 0; - width: 100%; - - &__wrapper { - box-shadow: none !important; - } - - &__footers { - border-bottom: 1px solid $gallery; - } - } - &__wrapper { background-color: $white; box-shadow: 0 2px 4px 0 rgba($black, .08); @@ -109,7 +96,18 @@ cursor: pointer; &:hover { - background-color: $gallery; + background-color: rgba(0, 0, 0, .05); + } + + &:active { + background-color: rgba(0, 0, 0, .1); + } + + .fa { + position: absolute; + right: 24px; + font-size: 24px; + line-height: 24px; } } @@ -180,7 +178,7 @@ transition: 200ms ease-in-out; display: flex; flex-flow: row nowrap; - flex: 0 0 45%; + flex: 0 0 42.5%; align-items: center; padding: 12px; margin: 2.5%; @@ -204,6 +202,10 @@ } } + &__token-data { + align-self: flex-start; + } + &__token-name { font-size: 14px; line-height: 19px; @@ -263,6 +265,11 @@ &__confirmation-title { padding: 30px 120px 12px; + + @media screen and (max-width: $break-small) { + padding: 20px 0; + width: 100%; + } } &__confirmation-content { @@ -272,7 +279,7 @@ &__confirmation-token-list-item { display: flex; flex-flow: row nowrap; - padding: 0 120px; + margin: 0 auto; align-items: center; } @@ -283,4 +290,52 @@ &__confirmation-token-icon { margin-right: 18px; } + + @media screen and (max-width: $break-small) { + top: 0; + width: 100%; + overflow: hidden; + height: 100%; + + &__wrapper { + box-shadow: none !important; + flex: 1 1 auto; + width: 100%; + overflow-y: auto; + } + + &__footers { + border-bottom: 1px solid $gallery; + } + + &__token-icon { + width: 50px; + height: 50px; + } + + &__token-symbol { + font-size: 18px; + line-height: 24px; + } + + &__token-name { + font-size: 12px; + line-height: 16px; + } + + &__buttons { + flex-flow: row nowrap; + width: 100%; + align-items: center; + justify-content: center; + padding: 12px 0; + margin: 0; + border-top: 1px solid $gallery; + + button { + flex: 1 0 auto; + margin: 0 12px; + } + } + } } diff --git a/ui/app/css/itcss/components/confirm.scss b/ui/app/css/itcss/components/confirm.scss index d4f0fe5ac..4a8232e39 100644 --- a/ui/app/css/itcss/components/confirm.scss +++ b/ui/app/css/itcss/components/confirm.scss @@ -16,6 +16,15 @@ } } +.notification { + .confirm-screen-wrapper { + + @media screen and (max-width: $break-small) { + height: calc(100vh - 85px); + } + } +} + .confirm-screen-wrapper { height: 100%; width: 380px; @@ -37,7 +46,7 @@ overflow-y: auto; top: 0; box-shadow: none; - height: calc(100vh - 58px - 100px); + height: calc(100vh - 58px - 85px); border-top-left-radius: 0; border-top-right-radius: 0; } @@ -66,7 +75,7 @@ flex: 0 0 auto; @media screen and (max-width: $break-small) { - font-size: 22px; + font-size: 20px; } } @@ -76,8 +85,10 @@ background: $athens-grey; position: absolute; transform: rotate(45deg); - left: 178px; top: 71px; + left: 0; + right: 0; + margin: 0 auto; } .confirm-screen-title { @@ -133,6 +144,14 @@ height: 16px; } +.confirm-send-ether, +.confirm-send-token { + i.fa-arrow-right { + align-self: start; + margin: 24px 14px 0 !important; + } +} + .confirm-screen-identicons { margin-top: 24px; flex: 0 0 auto; @@ -271,6 +290,7 @@ section .confirm-screen-account-number, box-shadow: none; flex: 1 0 auto; font-weight: 300; + margin: 0 8px; } .btn-light.confirm-screen-cancel-button { @@ -288,6 +308,7 @@ section .confirm-screen-account-number, cursor: pointer; flex: 1 0 auto; font-weight: 300; + margin: 0 8px; } #pending-tx-form { @@ -296,7 +317,7 @@ section .confirm-screen-account-number, display: flex; flex-flow: row nowrap; background-color: $white; - padding: 19px 18px; + padding: 12px 18px; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; width: 100%; diff --git a/ui/app/css/itcss/components/currency-display.scss b/ui/app/css/itcss/components/currency-display.scss index eb1776c58..9459629b6 100644 --- a/ui/app/css/itcss/components/currency-display.scss +++ b/ui/app/css/itcss/components/currency-display.scss @@ -22,6 +22,7 @@ line-height: 22px; border: none; outline: 0 !important; + max-width: 100%; } &__primary-currency { @@ -43,4 +44,13 @@ font-size: 12px; line-height: 12px; } + + &__input-wrapper { + position: relative; + display: flex; + } + + &__currency-symbol { + margin-top: 1px; + } }
\ No newline at end of file diff --git a/ui/app/css/itcss/components/editable-label.scss b/ui/app/css/itcss/components/editable-label.scss new file mode 100644 index 000000000..13570610c --- /dev/null +++ b/ui/app/css/itcss/components/editable-label.scss @@ -0,0 +1,34 @@ +.editable-label { + display: flex; + align-items: center; + justify-content: center; + position: relative; + + &__value { + max-width: 250px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + &__input { + width: 250px; + font-size: 14px; + text-align: center; + + &--error { + border: 1px solid $monzo; + } + } + + &__icon-wrapper { + position: absolute; + margin-left: 10px; + left: 100%; + } + + &__icon { + cursor: pointer; + color: $dusty-gray; + } +} diff --git a/ui/app/css/itcss/components/header.scss b/ui/app/css/itcss/components/header.scss index ef84dc3f4..a6332f819 100644 --- a/ui/app/css/itcss/components/header.scss +++ b/ui/app/css/itcss/components/header.scss @@ -27,6 +27,10 @@ bottom: -32px; } } + + .metafox-icon { + cursor: pointer; + } } .app-header-contents { @@ -58,6 +62,7 @@ text-transform: uppercase; font-weight: 400; color: #22232c; // $shark + line-height: 29px; @media screen and (max-width: 575px) { display: none; @@ -75,13 +80,13 @@ h2.page-subtitle { display: flex; flex-direction: row; align-items: center; - margin-right: 20px; } .left-menu-wrapper { display: flex; flex-direction: row; align-items: center; + cursor: pointer; } .header__right-actions { diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss index fda002785..4ba02be67 100644 --- a/ui/app/css/itcss/components/index.scss +++ b/ui/app/css/itcss/components/index.scss @@ -38,3 +38,14 @@ @import './gas-slider.scss'; +@import './settings.scss'; + +@import './tab-bar.scss'; + +@import './simple-dropdown.scss'; + +@import './request-signature.scss'; + +@import './account-dropdown-mini.scss'; + +@import './editable-label.scss'; diff --git a/ui/app/css/itcss/components/modal.scss b/ui/app/css/itcss/components/modal.scss index 1ffea58a9..b69bd5c7e 100644 --- a/ui/app/css/itcss/components/modal.scss +++ b/ui/app/css/itcss/components/modal.scss @@ -57,6 +57,7 @@ border-radius: 6px; border: 1px solid $black; padding: 0% 7%; + justify-content: center; div.buy-modal-content-option-title { font-size: 20px; @@ -293,6 +294,11 @@ font-size: 18px; } +.account-modal__name { + margin-top: 9px; + font-size: 20px; +} + .private-key-password { display: flex; flex-direction: column; @@ -372,6 +378,7 @@ resize: none; padding: 9px 13px 8px; text-transform: uppercase; + font-weight: 300; } diff --git a/ui/app/css/itcss/components/network.scss b/ui/app/css/itcss/components/network.scss index bb8c4eea8..98dbdffb2 100644 --- a/ui/app/css/itcss/components/network.scss +++ b/ui/app/css/itcss/components/network.scss @@ -1,3 +1,12 @@ +.network-component--disabled { + // border-color: transparent !important; + cursor: default; + + .fa-caret-down { + opacity: 0; + } +} + .network-component.pointer { border: 1px solid $shark; border-radius: 82px; @@ -40,7 +49,7 @@ .dropdown-menu-item { .menu-icon-circle, .menu-icon-circle--active { - margin: 0 16px; + margin: 0 14px; } } @@ -116,8 +125,8 @@ .menu-icon-circle div, .menu-icon-circle--active div { - height: 17px; - width: 17px; + height: 12px; + width: 12px; border-radius: 17px; } diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss index 1ee8283ef..244de2ba0 100644 --- a/ui/app/css/itcss/components/newui-sections.scss +++ b/ui/app/css/itcss/components/newui-sections.scss @@ -43,8 +43,11 @@ $wallet-view-bg: $wild-sand; .wallet-view { display: flex; flex-direction: column; - flex: 33.5 0 33.5%; + flex: 33.5 1 33.5%; + width: 0; background: $wallet-view-bg; + z-index: 200; + position: relative; @media screen and (min-width: 576px) { overflow-y: scroll; @@ -52,7 +55,78 @@ $wallet-view-bg: $wild-sand; } .wallet-view-account-details { - flex: 0 0 150px; + flex: 0 0 auto; + } + + &__name-container { + flex: 0 0 auto; + cursor: pointer; + width: 100%; + } + + &__keyring-label { + height: 40px; + color: $dusty-gray; + font-family: Roboto; + font-size: 10px; + line-height: 40px; + text-align: right; + padding: 0 20px; + } + + &__details-button { + color: $curious-blue; + font-size: 10px; + line-height: 13px; + text-align: center; + border: 1px solid $curious-blue; + border-radius: 10.5px; + background-color: transparent; + margin: 0 auto; + padding: 4px 12px; + flex: 0 0 auto; + } + + &__address { + border-radius: 3px; + background-color: $alto; + color: $scorpion; + font-size: 14px; + line-height: 12px; + padding: 4px 12px; + margin: 24px auto; + font-weight: 300; + cursor: pointer; + flex: 0 0 auto; + } + + &__sidebar-close { + + @media screen and (max-width: 575px) { + &::after { + content: '\00D7'; + font-size: 40px; + color: $tundora; + position: absolute; + top: 12px; + left: 12px; + cursor: pointer; + } + } + } + + &__add-token-button { + flex: 0 0 auto; + color: $dusty-gray; + font-size: 14px; + line-height: 19px; + text-align: center; + margin: 36px auto; + border: 1px solid $dusty-gray; + border-radius: 2px; + font-weight: 300; + background: none; + padding: 9px 30px; } } @@ -81,7 +155,7 @@ $wallet-view-bg: $wild-sand; background: rgb(250, 250, 250); z-index: $sidebar-z-index; position: fixed; - top: 57px; + top: 56px; left: 0; right: 0; bottom: 0; @@ -91,7 +165,7 @@ $wallet-view-bg: $wild-sand; overflow-y: auto; box-shadow: rgba(0, 0, 0, .15) 2px 2px 4px; width: 85%; - height: calc(100% - 57px); + height: calc(100% - 56px); } .sidebar-overlay { @@ -173,15 +247,18 @@ $wallet-view-bg: $wild-sand; // 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; - } + font-size: 24px; + font-weight: 200; + line-height: 20px; + color: $scorpion; + margin-top: 8px; + margin-bottom: 24px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + width: 100%; + padding: 0 8px; + text-align: center; } // account options dropdown diff --git a/ui/app/css/itcss/components/request-signature.scss b/ui/app/css/itcss/components/request-signature.scss new file mode 100644 index 000000000..ee54235d0 --- /dev/null +++ b/ui/app/css/itcss/components/request-signature.scss @@ -0,0 +1,222 @@ +.request-signature { + &__container { + width: 380px; + border-radius: 8px; + background-color: $white; + box-shadow: 0 2px 4px 0 rgba(0,0,0,0.08); + display: flex; + flex-flow: column nowrap; + z-index: 25; + align-items: center; + font-family: Roboto; + position: relative; + height: 100%; + + @media screen and (max-width: $break-small) { + width: 100%; + top: 0; + box-shadow: none; + } + + @media screen and (min-width: $break-large) { + max-height: 620px; + } + } + + &__header { + height: 64px; + width: 100%; + position: relative; + display: flex; + flex-flow: column; + justify-content: center; + align-items: center; + flex: 0 0 auto; + } + + &__header-background { + position: absolute; + background-color: $athens-grey; + z-index: 2; + width: 100%; + height: 100%; + } + + &__header__text { + height: 29px; + width: 179px; + color: #5B5D67; + font-family: Roboto; + font-size: 22px; + font-weight: 300; + line-height: 29px; + z-index: 3; + } + + &__header__tip-container { + width: 100%; + display: flex; + justify-content: center; + } + + &__header__tip { + height: 25px; + width: 25px; + background: $athens-grey; + transform: rotate(45deg); + position: absolute; + bottom: -8px; + z-index: 1; + } + + &__account-info { + display: flex; + justify-content: space-between; + margin-top: 18px; + margin-bottom: 20px; + } + + &__account { + color: $dusty-gray; + margin-left: 17px; + } + + &__account-text { + font-size: 14px; + } + + &__balance { + color: $dusty-gray; + margin-right: 17px; + width: 124px; + } + + &__balance-text { + text-align: right; + font-size: 14px; + } + + &__balance-value { + text-align: right; + margin-top: 2.5px; + } + + &__request-icon { + margin-top: 25px; + } + + &__body { + width: 100%; + height: 100%; + display: flex; + flex-flow: column; + flex: 1 1 auto; + height: 0; + } + + &__request-info { + display: flex; + justify-content: center; + } + + &__headline { + height: 48px; + width: 240px; + color: $tundora; + font-family: Roboto; + font-size: 18px; + font-weight: 300; + line-height: 24px; + text-align: center; + margin-top: 20px; + } + + &__notice { + color: #9B9B9B; + font-family: "Avenir Next"; + font-size: 14px; + line-height: 19px; + text-align: center; + margin-top: 41px; + margin-bottom: 11px; + width: 100%; + } + + &__rows { + height: 100%; + overflow-y: scroll; + overflow-x: hidden; + border-top: 1px solid $geyser; + display: flex; + flex-flow: column; + } + + &__row { + display: flex; + flex-flow: column; + } + + &__row-title { + width: 80px; + color: $dusty-gray; + font-family: Roboto; + font-size: 16px; + line-height: 22px; + margin-top: 12px; + margin-left: 18px; + width: 100%; + } + + &__row-value { + color: $scorpion; + font-family: Roboto; + font-size: 14px; + line-height: 19px; + width: 100%; + overflow-wrap: break-word; + border-bottom: 1px solid #d2d8dd; + padding: 6px 18px 15px; + } + + &__footer { + width: 100%; + display: flex; + align-items: center; + justify-content: space-evenly; + font-size: 22px; + position: relative; + flex: 0 0 auto; + border-top: 1px solid $geyser; + + &__cancel-button, + &__sign-button { + display: flex; + align-items: center; + justify-content: center; + flex: 1 0 auto; + font-family: Roboto; + font-size: 16px; + font-weight: 300; + height: 55px; + line-height: 32px; + cursor: pointer; + border-radius: 2px; + box-shadow: none; + max-width: 162px; + margin: 12px; + } + + &__cancel-button { + background: none; + border: 1px solid $dusty-gray; + margin-right: 6px; + } + + &__sign-button { + background-color: $caribbean-green; + border-width: 0; + color: $white; + margin-left: 6px; + } + } +}
\ No newline at end of file diff --git a/ui/app/css/itcss/components/sections.scss b/ui/app/css/itcss/components/sections.scss index bc89fdccc..388aea175 100644 --- a/ui/app/css/itcss/components/sections.scss +++ b/ui/app/css/itcss/components/sections.scss @@ -39,7 +39,9 @@ textarea.twelve-word-phrase { /* unlock */ .error { - color: #e20202; + // color: #e20202; + color: #f7861c; + margin-bottom: 9px; } .warning { diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index 9a076551e..282eef030 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -274,6 +274,7 @@ color: #9b9b9b; font-size: .8em; padding: 1px 4px; + cursor: pointer; } .token-gas { @@ -400,7 +401,7 @@ .send-v2 { &__container { - height: 701px; + // height: 701px; width: 380px; border-radius: 8px; background-color: $white; @@ -416,6 +417,7 @@ width: 100%; top: 0; box-shadow: none; + flex: 1 1 auto; } } @@ -473,6 +475,7 @@ @media screen and (max-width: $break-small) { height: 59px; + width: 100vw; } } @@ -483,10 +486,13 @@ position: absolute; transform: rotate(45deg); left: 178px; - top: 65px; + top: 75px; @media screen and (max-width: $break-small) { top: 46px; + left: 0; + right: 0; + margin: 0 auto; } } @@ -520,18 +526,20 @@ } &__form { - margin-top: 13px; + margin: 13px 0; width: 100%; @media screen and (max-width: $break-small) { - margin-top: 0px; + padding: 13px 0; + margin: 0; height: 0; overflow-y: auto; flex: 1 1 auto; } } - &__form-header, &__form-header-copy { + &__form-header, + &__form-header-copy { width: 100%; display: flex; flex-flow: column; @@ -594,6 +602,16 @@ } } + &__to-autocomplete { + position: relative; + + &__down-caret { + position: absolute; + top: 18px; + right: 12px; + } + } + &__to-autocomplete, &__memo-text-area { &__input { height: 54px; @@ -652,31 +670,32 @@ } &__next-btn, - &__cancel-btn { + &__cancel-btn, + &__next-btn__disabled { width: 163px; text-align: center; height: 55px; - width: 163px; border-radius: 2px; background-color: $white; font-family: Roboto; font-size: 16px; font-weight: 300; line-height: 21px; - text-align: center; border: 1px solid; + margin: 0 4px; } + &__next-btn, &__next-btn__disabled { - opacity: .5; - cursor: auto; - } - - &__next-btn { color: $curious-blue; border-color: $curious-blue; } + &__next-btn__disabled { + opacity: .5; + cursor: auto; + } + &__cancel-btn { color: $dusty-gray; border-color: $dusty-gray; @@ -692,8 +711,8 @@ flex-flow: column; @media screen and (max-width: $break-small) { - width: 355px; - height: 598px; + width: 100vw; + height: 100vh; } &__header { @@ -703,6 +722,10 @@ align-items: center; justify-content: space-between; font-size: 22px; + + @media screen and (max-width: $break-small) { + flex: 0 0 auto; + } } &__title { @@ -718,14 +741,19 @@ margin-right: 19.25px; } + &__content { + display: flex; + flex-flow: column nowrap; + height: 100%; + } + &__body { - height: 248px; display: flex; + margin-bottom: 24px; @media screen and (max-width: $break-small) { - width: 355px; - height: 470px; flex-flow: column; + flex: 1 1 auto; } } @@ -736,6 +764,11 @@ align-items: center; justify-content: space-between; font-size: 22px; + position: relative; + + @media screen and (max-width: $break-small) { + flex: 0 0 auto; + } } &__buttons { @@ -745,7 +778,7 @@ margin-right: 21.25px; } - &__revert, &__cancel, &__save { + &__revert, &__cancel, &__save, &__save__error { display: flex; justify-content: center; align-items: center; @@ -758,7 +791,7 @@ margin-left: 21.25px; } - &__cancel, &__save { + &__cancel, &__save, &__save__error { height: 34.64px; width: 85.74px; border: 1px solid $dusty-gray; @@ -767,6 +800,21 @@ font-size: 12px; color: $dusty-gray; } + + &__save__error { + opacity: 0.5; + cursor: auto; + } + + &__error-message { + display: block; + position: absolute; + top: 4px; + right: 4px; + font-size: 12px; + line-height: 12px; + color: $red; + } } &__gas-modal-card { @@ -778,7 +826,6 @@ &__title { height: 26px; - width: 84px; color: $tundora; font-family: Roboto; font-size: 20px; diff --git a/ui/app/css/itcss/components/settings.scss b/ui/app/css/itcss/components/settings.scss new file mode 100644 index 000000000..2f29d8017 --- /dev/null +++ b/ui/app/css/itcss/components/settings.scss @@ -0,0 +1,201 @@ +.settings { + position: relative; + background: $white; + display: flex; + flex-flow: column nowrap; + height: auto; + overflow: auto; +} + +.settings__header { + padding: 25px; +} + +.settings__close-button::after { + content: '\00D7'; + font-size: 40px; + color: $dusty-gray; + position: absolute; + top: 25px; + right: 30px; + cursor: pointer; +} + +.settings__error { + padding-bottom: 20px; + text-align: center; + color: $crimson; +} + +.settings__content { + padding: 0 25px; +} + +.settings__content-row { + display: flex; + flex-direction: row; + padding: 10px 0 20px; + + @media screen and (max-width: 575px) { + flex-direction: column; + padding: 10px 0; + } +} + +.settings__content-item { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + padding: 0 5px; + height: 71px; + + @media screen and (max-width: 575px) { + height: initial; + padding: 5px 0; + } + + &--without-height { + height: initial; + } +} + +.settings__content-item-col { + max-width: 300px; + display: flex; + flex-direction: column; + + @media screen and (max-width: 575px) { + max-width: 100%; + width: 100%; + } +} + +.settings__content-description { + font-size: 14px; + color: $dusty-gray; + padding-top: 5px; +} + +.settings__input { + padding-left: 10px; + font-size: 14px; + height: 40px; + border: 1px solid $alto; +} + +.settings__input::-webkit-input-placeholder { + font-weight: 100; + color: $dusty-gray; +} + +.settings__input::-moz-placeholder { + font-weight: 100; + color: $dusty-gray; +} + +.settings__input:-ms-input-placeholder { + font-weight: 100; + color: $dusty-gray; +} + +.settings__input:-moz-placeholder { + font-weight: 100; + color: $dusty-gray; +} + +.settings__provider-wrapper { + font-size: 16px; + border: 1px solid $alto; + border-radius: 2px; + padding: 15px; + background-color: $white; + display: flex; + align-items: center; + justify-content: flex-start; +} + +.settings__provider-icon { + height: 10px; + width: 10px; + margin-right: 10px; + border-radius: 10px; +} + +.settings__rpc-save-button { + align-self: flex-end; + padding: 5px; + text-transform: uppercase; + color: $dusty-gray; + cursor: pointer; +} + +.settings__clear-button { + font-size: 16px; + border: 1px solid $curious-blue; + color: $curious-blue; + border-radius: 2px; + padding: 18px; + background-color: $white; + text-transform: uppercase; +} + +.settings__clear-button--red { + border: 1px solid $monzo; + color: $monzo; +} + +.settings__info-logo-wrapper { + height: 80px; + margin-bottom: 20px; +} + +.settings__info-logo { + max-height: 100%; + max-width: 100%; +} + +.settings__info-item { + padding: 10px 0; +} + +.settings__info-link-header { + padding-bottom: 15px; + + @media screen and (max-width: 575px) { + padding-bottom: 5px; + } +} + +.settings__info-link-item { + padding: 15px 0; + + @media screen and (max-width: 575px) { + padding: 5px 0; + } +} + +.settings__info-version-number { + padding-top: 5px; + font-size: 13px; + color: $dusty-gray; +} + +.settings__info-about { + color: $dusty-gray; + margin-bottom: 15px; +} + +.settings__info-link { + color: $curious-blue; +} + +.settings__info-separator { + margin: 15px 0; + width: 80px; + border-color: $alto; + border: none; + height: 1px; + background-color: $alto; + color: $alto; +} diff --git a/ui/app/css/itcss/components/simple-dropdown.scss b/ui/app/css/itcss/components/simple-dropdown.scss new file mode 100644 index 000000000..a21095a3e --- /dev/null +++ b/ui/app/css/itcss/components/simple-dropdown.scss @@ -0,0 +1,65 @@ +.simple-dropdown { + height: 56px; + display: flex; + justify-content: flex-start; + align-items: center; + border: 1px solid $alto; + border-radius: 4px; + background-color: $white; + font-size: 16px; + color: #4d4d4d; + cursor: pointer; + position: relative; +} + +.simple-dropdown__caret { + color: $silver; + padding: 0 10px; +} + +.simple-dropdown__selected { + flex-grow: 1; + padding: 0 15px; +} + +.simple-dropdown__options { + z-index: 1050; + position: absolute; + height: 220px; + width: 100%; + border: 1px solid #d2d8dd; + border-radius: 4px; + background-color: #fff; + -webkit-box-shadow: 0 3px 6px 0 rgba(0, 0, 0, .11); + box-shadow: 0 3px 6px 0 rgba(0, 0, 0, .11); + margin-top: 10px; + overflow-y: scroll; + left: 0; + top: 100%; +} + +.simple-dropdown__option { + padding: 10px; + + &:hover { + background-color: $gallery; + } +} + +.simple-dropdown__option--selected { + background-color: $alto; + + &:hover { + background-color: $alto; + cursor: default; + } +} + +.simple-dropdown__close-area { + position: fixed; + top: 0; + left: 0; + z-index: 1000; + width: 100%; + height: 100%; +} diff --git a/ui/app/css/itcss/components/tab-bar.scss b/ui/app/css/itcss/components/tab-bar.scss new file mode 100644 index 000000000..4f3077974 --- /dev/null +++ b/ui/app/css/itcss/components/tab-bar.scss @@ -0,0 +1,23 @@ +.tab-bar { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: flex-end; +} + +.tab-bar__tab { + min-width: 0; + flex: 0 0 auto; + padding: 15px 25px; + border-bottom: 1px solid $alto; + box-sizing: border-box; + font-size: 18px; +} + +.tab-bar__tab--active { + border-color: $black; +} + +.tab-bar__grow-tab { + flex-grow: 1; +} diff --git a/ui/app/css/itcss/components/token-list.scss b/ui/app/css/itcss/components/token-list.scss index bbc64c324..d4add71b1 100644 --- a/ui/app/css/itcss/components/token-list.scss +++ b/ui/app/css/itcss/components/token-list.scss @@ -67,19 +67,21 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and ( position: fixed; margin-top: 20px; margin-left: 105px; + z-index: 2000; &__close-area { position: fixed; top: 0; left: 0; - z-index: 1000; + z-index: 2100; width: 100%; height: 100%; + cursor: default; } &__container { padding: 16px 34px 32px; - z-index: 1050; + z-index: 2200; position: relative; } diff --git a/ui/app/css/itcss/components/wallet-balance.scss b/ui/app/css/itcss/components/wallet-balance.scss index cd44f89bb..64b291b89 100644 --- a/ui/app/css/itcss/components/wallet-balance.scss +++ b/ui/app/css/itcss/components/wallet-balance.scss @@ -1,4 +1,4 @@ -$wallet-balance-bg: $gallery; +$wallet-balance-bg: #e7e7e7; $wallet-balance-breakpoint: 890px; $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and (max-width: #{$wallet-balance-breakpoint})"; @@ -20,6 +20,7 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and ( align-items: center; flex: 0 0 auto; cursor: pointer; + border-top: 1px solid $wallet-balance-bg; .balance-container { display: flex; diff --git a/ui/app/css/itcss/tools/utilities.scss b/ui/app/css/itcss/tools/utilities.scss index ca9fd0d9c..ee867640d 100644 --- a/ui/app/css/itcss/tools/utilities.scss +++ b/ui/app/css/itcss/tools/utilities.scss @@ -237,7 +237,6 @@ hr.horizontal-line { color: #fff; border-radius: 10px; padding: 4px; - width: 41px; text-align: center; height: 15px; } diff --git a/ui/app/keychains/hd/create-vault-complete.js b/ui/app/keychains/hd/create-vault-complete.js index 745990351..5ab5d4c33 100644 --- a/ui/app/keychains/hd/create-vault-complete.js +++ b/ui/app/keychains/hd/create-vault-complete.js @@ -62,7 +62,8 @@ CreateVaultCompleteScreen.prototype.render = function () { }), h('button.primary', { - onClick: () => this.confirmSeedWords(), + onClick: () => this.confirmSeedWords() + .then(account => this.showAccountDetail(account)), style: { margin: '24px', fontSize: '0.9em', @@ -82,5 +83,9 @@ CreateVaultCompleteScreen.prototype.render = function () { } CreateVaultCompleteScreen.prototype.confirmSeedWords = function () { - this.props.dispatch(actions.confirmSeedWords()) + return this.props.dispatch(actions.confirmSeedWords()) +} + +CreateVaultCompleteScreen.prototype.showAccountDetail = function (account) { + return this.props.dispatch(actions.showAccountDetail(account)) } diff --git a/ui/app/main-container.js b/ui/app/main-container.js index eaaff8517..6e2342c2b 100644 --- a/ui/app/main-container.js +++ b/ui/app/main-container.js @@ -3,7 +3,7 @@ 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 Settings = require('./settings') const UnlockScreen = require('./unlock') module.exports = MainContainer @@ -38,7 +38,7 @@ MainContainer.prototype.render = function () { case 'config': log.debug('rendering config screen from unlock screen.') contents = { - component: ConfigScreen, + component: Settings, key: 'config', } break diff --git a/ui/app/reducers.js b/ui/app/reducers.js index 1cded7ca7..e1a890535 100644 --- a/ui/app/reducers.js +++ b/ui/app/reducers.js @@ -42,7 +42,7 @@ function rootReducer (state, action) { } window.logState = function () { - let state = window.METAMASK_CACHED_LOG_STATE + const state = window.METAMASK_CACHED_LOG_STATE let version try { version = global.platform.getVersion() @@ -50,7 +50,7 @@ window.logState = function () { version = 'unable to load version.' } state.version = version - let stateString = JSON.stringify(state, removeSeedWords, 2) + const stateString = JSON.stringify(state, removeSeedWords, 2) return stateString } diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index f10bf9fb7..6fb7f8cca 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -557,6 +557,16 @@ function reduceApp (state, action) { }, }) + case actions.ONBOARDING_BUY_ETH_VIEW: + return extend(appState, { + transForward: true, + currentView: { + name: 'onboardingBuyEth', + context: appState.currentView.name, + }, + identity: state.metamask.identities[action.value], + }) + case actions.COINBASE_SUBVIEW: return extend(appState, { buyView: { diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index fb2b2e674..7408f827a 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -1,5 +1,6 @@ const extend = require('xtend') const actions = require('../actions') +const MetamascaraPlatform = require('../../../app/scripts/platforms/window') module.exports = reduceMetamask @@ -11,6 +12,7 @@ function reduceMetamask (state, action) { isInitialized: false, isUnlocked: false, isAccountMenuOpen: false, + isMascara: window.platform instanceof MetamascaraPlatform, rpcTarget: 'https://rawtestrpc.metamask.io/', identities: {}, unapprovedTxs: {}, @@ -31,6 +33,7 @@ function reduceMetamask (state, action) { memo: '', errors: {}, }, + coinOptions: {}, }, state.metamask) switch (action.type) { @@ -150,7 +153,7 @@ function reduceMetamask (state, action) { }) case actions.UPDATE_TOKEN_EXCHANGE_RATE: - const { payload: { pair, marketinfo } } = action + const { payload: { pair, marketinfo } } = action return extend(metamaskState, { tokenExchangeRates: { ...metamaskState.tokenExchangeRates, @@ -226,10 +229,6 @@ function reduceMetamask (state, action) { }) case actions.UPDATE_SEND_ERRORS: - console.log(123, { - ...metamaskState.send.errors, - ...action.value, - }) return extend(metamaskState, { send: { ...metamaskState.send, @@ -240,6 +239,39 @@ function reduceMetamask (state, action) { }, }) + case actions.CLEAR_SEND: + return extend(metamaskState, { + send: { + gasLimit: null, + gasPrice: null, + gasTotal: null, + from: '', + to: '', + amount: '0x0', + memo: '', + errors: {}, + }, + }) + + case actions.PAIR_UPDATE: + const { value: { marketinfo: pairMarketInfo } } = action + return extend(metamaskState, { + tokenExchangeRates: { + ...metamaskState.tokenExchangeRates, + [pairMarketInfo.pair]: pairMarketInfo, + }, + }) + + case actions.SHAPESHIFT_SUBVIEW: + const { value: { marketinfo: ssMarketInfo, coinOptions } } = action + return extend(metamaskState, { + tokenExchangeRates: { + ...metamaskState.tokenExchangeRates, + [marketinfo.pair]: ssMarketInfo, + }, + coinOptions, + }) + default: return metamaskState diff --git a/ui/app/selectors.js b/ui/app/selectors.js index 4c3d21d33..3a15cef4c 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -1,5 +1,9 @@ const valuesFor = require('./util').valuesFor +const { + multiplyCurrencies, +} = require('./conversion-util') + const selectors = { getSelectedAddress, getSelectedIdentity, @@ -16,6 +20,8 @@ const selectors = { getAddressBook, getSendFrom, getCurrentCurrency, + getSendAmount, + getSelectedTokenToFiatRate, } module.exports = selectors @@ -123,6 +129,23 @@ function getSendFrom (state) { return state.metamask.send.from } +function getSendAmount (state) { + return state.metamask.send.amount +} + function getCurrentCurrency (state) { return state.metamask.currentCurrency } + +function getSelectedTokenToFiatRate (state) { + const selectedTokenExchangeRate = getSelectedTokenExchangeRate(state) + const conversionRate = conversionRateSelector(state) + + const tokenToFiatRate = multiplyCurrencies( + conversionRate, + selectedTokenExchangeRate, + { toNumericBase: 'dec' } + ) + + return tokenToFiatRate +} diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index 5e64daceb..e772477ae 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -2,6 +2,7 @@ const { inherits } = require('util') const PersistentForm = require('../lib/persistent-form') const h = require('react-hyperscript') const connect = require('react-redux').connect +const classnames = require('classnames') const Identicon = require('./components/identicon') const FromDropdown = require('./components/send/from-dropdown') @@ -19,6 +20,9 @@ const { conversionGreaterThan, addCurrencies, } = require('./conversion-util') +const { + isBalanceSufficient, +} = require('./components/send/send-utils.js') const { isValidAddress } = require('./util') module.exports = SendTransactionScreen @@ -28,7 +32,8 @@ function SendTransactionScreen () { PersistentForm.call(this) this.state = { - dropdownOpen: false, + fromDropdownOpen: false, + toDropdownOpen: false, errors: { to: null, amount: null, @@ -153,7 +158,7 @@ SendTransactionScreen.prototype.renderFromRow = function () { updateSendFrom, } = this.props - const { dropdownOpen } = this.state + const { fromDropdownOpen } = this.state return h('div.send-v2__form-row', [ @@ -161,12 +166,12 @@ SendTransactionScreen.prototype.renderFromRow = function () { h('div.send-v2__form-field', [ h(FromDropdown, { - dropdownOpen, + dropdownOpen: fromDropdownOpen, accounts: fromAccounts, selectedAccount: from, onSelect: updateSendFrom, - openDropdown: () => this.setState({ dropdownOpen: true }), - closeDropdown: () => this.setState({ dropdownOpen: false }), + openDropdown: () => this.setState({ fromDropdownOpen: true }), + closeDropdown: () => this.setState({ fromDropdownOpen: false }), conversionRate, }), ]), @@ -174,15 +179,20 @@ SendTransactionScreen.prototype.renderFromRow = function () { ]) } -SendTransactionScreen.prototype.handleToChange = function (event) { - const { updateSendTo, updateSendErrors } = this.props - const to = event.target.value +SendTransactionScreen.prototype.handleToChange = function (to) { + const { + updateSendTo, + updateSendErrors, + from: {address: from}, + } = this.props let toError = null if (!to) { toError = 'Required' } else if (!isValidAddress(to)) { - toError = 'Recipient address is invalid.' + toError = 'Recipient address is invalid' + } else if (to === from) { + toError = 'From and To address cannot be the same' } updateSendTo(to) @@ -190,8 +200,9 @@ SendTransactionScreen.prototype.handleToChange = function (event) { } SendTransactionScreen.prototype.renderToRow = function () { - const { toAccounts, errors } = this.props - const { to } = this.state + const { toAccounts, errors, to } = this.props + + const { toDropdownOpen } = this.state return h('div.send-v2__form-row', [ @@ -206,7 +217,10 @@ SendTransactionScreen.prototype.renderToRow = function () { h('div.send-v2__form-field', [ h(ToAutoComplete, { to, - accounts: toAccounts, + accounts: Object.entries(toAccounts).map(([key, account]) => account), + dropdownOpen: toDropdownOpen, + openDropdown: () => this.setState({ toDropdownOpen: true }), + closeDropdown: () => this.setState({ toDropdownOpen: false }), onChange: this.handleToChange, inError: Boolean(errors.to), }), @@ -237,28 +251,16 @@ SendTransactionScreen.prototype.validateAmount = function (value) { let amountError = null - const totalAmount = addCurrencies(amount, gasTotal, { - aBase: 16, - bBase: 16, - toNumericBase: 'hex', + const sufficientBalance = isBalanceSufficient({ + amount, + gasTotal, + balance, + primaryCurrency, + selectedToken, + amountConversionRate, + conversionRate, }) - const sufficientBalance = conversionGreaterThan( - { - value: balance, - fromNumericBase: 'hex', - fromCurrency: primaryCurrency, - conversionRate, - }, - { - value: totalAmount, - fromNumericBase: 'hex', - conversionRate: amountConversionRate, - fromCurrency: selectedToken || primaryCurrency, - conversionRate: amountConversionRate, - }, - ) - const amountLessThanZero = conversionGreaterThan( { value: 0, fromNumericBase: 'dec' }, { value: amount, fromNumericBase: 'hex' }, @@ -376,19 +378,29 @@ SendTransactionScreen.prototype.renderForm = function () { this.renderGasRow(), - this.renderMemoRow(), + // this.renderMemoRow(), ]) } SendTransactionScreen.prototype.renderFooter = function () { - const { goHome } = this.props + const { + goHome, + clearSend, + errors: { amount: amountError, to: toError }, + } = this.props + + const noErrors = amountError === null && toError === null + const errorClass = noErrors ? '' : '__disabled' return h('div.send-v2__footer', [ h('button.send-v2__cancel-btn', { - onClick: goHome, + onClick: () => { + clearSend() + goHome() + }, }, 'Cancel'), - h('button.send-v2__next-btn', { + h(`button.send-v2__next-btn${errorClass}`, { onClick: event => this.onSubmit(event), }, 'Next'), ]) @@ -429,8 +441,16 @@ SendTransactionScreen.prototype.onSubmit = function (event) { signTx, selectedToken, toAccounts, + clearSend, + errors: { amount: amountError, to: toError }, } = this.props + const noErrors = amountError === null && toError === null + + if (!noErrors) { + return + } + this.addToAddressBookIfNew(to) const txParams = { @@ -445,6 +465,8 @@ SendTransactionScreen.prototype.onSubmit = function (event) { txParams.to = to } + clearSend() + selectedToken ? signTokenTx(selectedToken.address, to, amount, txParams) : signTx(txParams) diff --git a/ui/app/settings.js b/ui/app/settings.js index 454cc95e0..786a70e7e 100644 --- a/ui/app/settings.js +++ b/ui/app/settings.js @@ -1,59 +1,363 @@ -const inherits = require('util').inherits -const Component = require('react').Component +const { Component } = require('react') +const PropTypes = require('prop-types') const h = require('react-hyperscript') -const connect = require('react-redux').connect +const { connect } = require('react-redux') const actions = require('./actions') +const infuraCurrencies = require('./infura-conversion.json') +const validUrl = require('valid-url') +const { exportAsFile } = require('./util') +const TabBar = require('./components/tab-bar') +const SimpleDropdown = require('./components/dropdowns/simple-dropdown') -module.exports = connect(mapStateToProps)(AppSettingsPage) +const getInfuraCurrencyOptions = () => { + const sortedCurrencies = infuraCurrencies.objects.sort((a, b) => { + return a.quote.name.toLocaleLowerCase().localeCompare(b.quote.name.toLocaleLowerCase()) + }) -function mapStateToProps (state) { - return {} + return sortedCurrencies.map(({ quote: { code, name } }) => { + return { + displayValue: `${code.toUpperCase()} - ${name}`, + key: code, + value: code, + } + }) } -inherits(AppSettingsPage, Component) -function AppSettingsPage () { - Component.call(this) -} +class Settings extends Component { + constructor (props) { + super(props) -AppSettingsPage.prototype.render = function () { - return ( + const { tab } = props + const activeTab = tab === 'info' ? 'info' : 'settings' - h('.account-detail-section.flex-column.flex-grow', [ + this.state = { + activeTab, + newRpc: '', + } + } - // subtitle and nav - h('.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.navigateToAccounts.bind(this), - }), - h('h2.page-subtitle', 'Settings'), - ]), + renderTabs () { + const { activeTab } = this.state - h('label', { - htmlFor: 'settings-rpc-endpoint', - }, 'RPC Endpoint:'), - h('input', { - type: 'url', - id: 'settings-rpc-endpoint', - onKeyPress: this.onKeyPress.bind(this), + return h('div.settings__tabs', [ + h(TabBar, { + tabs: [ + { content: 'Settings', key: 'settings' }, + { content: 'Info', key: 'info' }, + ], + defaultTab: activeTab, + tabSelected: key => this.setState({ activeTab: key }), }), + ]) + } + + renderCurrentConversion () { + const { metamask: { currentCurrency, conversionDate }, setCurrentCurrency } = this.props + return h('div.settings__content-row', [ + h('div.settings__content-item', [ + h('span', 'Current Conversion'), + h('span.settings__content-description', `Updated ${Date(conversionDate)}`), + ]), + h('div.settings__content-item', [ + h('div.settings__content-item-col', [ + h(SimpleDropdown, { + placeholder: 'Select Currency', + options: getInfuraCurrencyOptions(), + selectedOption: currentCurrency, + onSelect: newCurrency => setCurrentCurrency(newCurrency), + }), + ]), + ]), ]) + } + + renderCurrentProvider () { + const { metamask: { provider = {} } } = this.props + let title, value, color + + switch (provider.type) { + + case 'mainnet': + title = 'Current Network' + value = 'Main Ethereum Network' + color = '#038789' + break + + case 'ropsten': + title = 'Current Network' + value = 'Ropsten Test Network' + color = '#e91550' + break + + case 'kovan': + title = 'Current Network' + value = 'Kovan Test Network' + color = '#690496' + break + + case 'rinkeby': + title = 'Current Network' + value = 'Rinkeby Test Network' + color = '#ebb33f' + break + + default: + title = 'Current RPC' + value = provider.rpcTarget + } + + return h('div.settings__content-row', [ + h('div.settings__content-item', title), + h('div.settings__content-item', [ + h('div.settings__content-item-col', [ + h('div.settings__provider-wrapper', [ + h('div.settings__provider-icon', { style: { background: color } }), + h('div', value), + ]), + ]), + ]), + ]) + } + + renderNewRpcUrl () { + return ( + h('div.settings__content-row', [ + h('div.settings__content-item', [ + h('span', 'New RPC URL'), + ]), + h('div.settings__content-item', [ + h('div.settings__content-item-col', [ + h('input.settings__input', { + placeholder: 'New RPC URL', + onChange: event => this.setState({ newRpc: event.target.value }), + onKeyPress: event => { + if (event.key === 'Enter') { + this.validateRpc(this.state.newRpc) + } + }, + }), + h('div.settings__rpc-save-button', { + onClick: event => { + event.preventDefault() + this.validateRpc(this.state.newRpc) + }, + }, 'Save'), + ]), + ]), + ]) + ) + } + + validateRpc (newRpc) { + const { setRpcTarget, displayWarning } = this.props - ) + if (validUrl.isWebUri(newRpc)) { + setRpcTarget(newRpc) + } else { + const appendedRpc = `http://${newRpc}` + + if (validUrl.isWebUri(appendedRpc)) { + displayWarning('URIs require the appropriate HTTP/HTTPS prefix.') + } else { + displayWarning('Invalid RPC URI') + } + } + } + + renderStateLogs () { + return ( + h('div.settings__content-row', [ + h('div.settings__content-item', [ + h('div', 'State Logs'), + h( + 'div.settings__content-description', + 'State logs contain your public account addresses and sent transactions.' + ), + ]), + h('div.settings__content-item', [ + h('div.settings__content-item-col', [ + h('button.settings__clear-button', { + onClick (event) { + exportAsFile('MetaMask State Logs', window.logState()) + }, + }, 'Download State Logs'), + ]), + ]), + ]) + ) + } + + renderSeedWords () { + const { revealSeedConfirmation } = this.props + + return ( + h('div.settings__content-row', [ + h('div.settings__content-item', 'Reveal Seed Words'), + h('div.settings__content-item', [ + h('div.settings__content-item-col', [ + h('button.settings__clear-button.settings__clear-button--red', { + onClick (event) { + event.preventDefault() + revealSeedConfirmation() + }, + }, 'Reveal Seed Words'), + ]), + ]), + ]) + ) + } + + renderSettingsContent () { + const { warning } = this.props + + return ( + h('div.settings__content', [ + warning && h('div.settings__error', warning), + this.renderCurrentConversion(), + // this.renderCurrentProvider(), + this.renderNewRpcUrl(), + this.renderStateLogs(), + this.renderSeedWords(), + ]) + ) + } + + renderLogo () { + return ( + h('div.settings__info-logo-wrapper', [ + h('img.settings__info-logo', { src: 'images/info-logo.png' }), + ]) + ) + } + + renderInfoLinks () { + return ( + h('div.settings__content-item.settings__content-item--without-height', [ + h('div.settings__info-link-header', 'Links'), + h('div.settings__info-link-item', [ + h('a', { + href: 'https://metamask.io/privacy.html', + target: '_blank', + }, [ + h('span.settings__info-link', 'Privacy Policy'), + ]), + ]), + h('div.settings__info-link-item', [ + h('a', { + href: 'https://metamask.io/terms.html', + target: '_blank', + }, [ + h('span.settings__info-link', 'Terms of Use'), + ]), + ]), + h('div.settings__info-link-item', [ + h('a', { + href: 'https://metamask.io/attributions.html', + target: '_blank', + }, [ + h('span.settings__info-link', 'Attributions'), + ]), + ]), + h('hr.settings__info-separator'), + h('div.settings__info-link-item', [ + h('a', { + href: 'https://support.metamask.io', + target: '_blank', + }, [ + h('span.settings__info-link', 'Visit our Support Center'), + ]), + ]), + h('div.settings__info-link-item', [ + h('a', { + href: 'https://metamask.io/', + target: '_blank', + }, [ + h('span.settings__info-link', 'Visit our web site'), + ]), + ]), + h('div.settings__info-link-item', [ + h('a', { + target: '_blank', + href: 'mailto:help@metamask.io?subject=Feedback', + }, [ + h('span.settings__info-link', 'Email us!'), + ]), + ]), + ]) + ) + } + + renderInfoContent () { + return ( + h('div.settings__content', [ + h('div.settings__content-row', [ + h('div.settings__content-item.settings__content-item--without-height', [ + this.renderLogo(), + h('div.settings__info-item', [ + h('div.settings__info-version-header', 'MetaMask Version'), + h('div.settings__info-version-number', '4.0.0'), + ]), + h('div.settings__info-item', [ + h( + 'div.settings__info-about', + 'MetaMask is designed and built in California.' + ), + ]), + ]), + this.renderInfoLinks(), + ]), + ]) + ) + } + + render () { + const { goHome } = this.props + const { activeTab } = this.state + + return ( + h('.main-container.settings', {}, [ + h('.settings__header', [ + h('div.settings__close-button', { + onClick: goHome, + }), + this.renderTabs(), + ]), + + activeTab === 'settings' + ? this.renderSettingsContent() + : this.renderInfoContent(), + ]) + ) + } } -AppSettingsPage.prototype.componentDidMount = function () { - document.querySelector('input').focus() +Settings.propTypes = { + tab: PropTypes.string, + metamask: PropTypes.object, + setCurrentCurrency: PropTypes.func, + setRpcTarget: PropTypes.func, + displayWarning: PropTypes.func, + revealSeedConfirmation: PropTypes.func, + warning: PropTypes.string, + goHome: PropTypes.func, } -AppSettingsPage.prototype.onKeyPress = function (event) { - // get submit event - if (event.key === 'Enter') { - // this.submitPassword(event) +const mapStateToProps = state => { + return { + metamask: state.metamask, + warning: state.appState.warning, } } -AppSettingsPage.prototype.navigateToAccounts = function (event) { - event.stopPropagation() - this.props.dispatch(actions.showAccountsPage()) +const mapDispatchToProps = dispatch => { + return { + goHome: () => dispatch(actions.goHome()), + setCurrentCurrency: currency => dispatch(actions.setCurrentCurrency(currency)), + setRpcTarget: newRpc => dispatch(actions.setRpcTarget(newRpc)), + displayWarning: warning => dispatch(actions.displayWarning(warning)), + revealSeedConfirmation: () => dispatch(actions.revealSeedConfirmation()), + } } + +module.exports = connect(mapStateToProps, mapDispatchToProps)(Settings) @@ -5,6 +5,7 @@ module.exports = bundleCss var cssFiles = { 'index.css': fs.readFileSync(path.join(__dirname, '/app/css/output/index.css'), 'utf8'), + 'first-time.css': fs.readFileSync(path.join(__dirname, '../mascara/src/app/first-time/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/lib/tx-helper.js b/ui/lib/tx-helper.js index 341567e2f..de3f00d2d 100644 --- a/ui/lib/tx-helper.js +++ b/ui/lib/tx-helper.js @@ -24,4 +24,4 @@ module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, typedMes }) return allValues -}
\ No newline at end of file +} |