aboutsummaryrefslogtreecommitdiffstats
path: root/ui
diff options
context:
space:
mode:
authorDan J Miller <danjm.com@gmail.com>2017-10-14 04:19:22 +0800
committerDaniel Tsui <szehungdanieltsui@gmail.com>2017-10-14 04:19:22 +0800
commit803eaaf968161f16aaf72d59b979dfbb7fb9b352 (patch)
treeadf8cbf5240e592ae0ede85be1181132612b2d8a /ui
parent81f62a7443d47461b5f9b20f442392562458c79a (diff)
downloadtangerine-wallet-browser-803eaaf968161f16aaf72d59b979dfbb7fb9b352.tar
tangerine-wallet-browser-803eaaf968161f16aaf72d59b979dfbb7fb9b352.tar.gz
tangerine-wallet-browser-803eaaf968161f16aaf72d59b979dfbb7fb9b352.tar.bz2
tangerine-wallet-browser-803eaaf968161f16aaf72d59b979dfbb7fb9b352.tar.lz
tangerine-wallet-browser-803eaaf968161f16aaf72d59b979dfbb7fb9b352.tar.xz
tangerine-wallet-browser-803eaaf968161f16aaf72d59b979dfbb7fb9b352.tar.zst
tangerine-wallet-browser-803eaaf968161f16aaf72d59b979dfbb7fb9b352.zip
[NewUI] SendV2-#8: Send container handles tokens; gas info dynamic from state (#2364)
* Adds memo field to send-v2. * Vertical align transaction with flexbox. * Customize Gas UI * Remove internal state from InputNumber and fix use in gastooltip. * Move customize-gas-modal to its own folder and minor cleanup * Create send container, get account info from state, and make currency display more reusable * Adjusts send-v2 and container for send-token. Dynamically getting suggested gas prices.
Diffstat (limited to 'ui')
-rw-r--r--ui/app/app.js9
-rw-r--r--ui/app/components/customize-gas-modal/gas-modal-card.js55
-rw-r--r--ui/app/components/customize-gas-modal/gas-slider.js50
-rw-r--r--ui/app/components/customize-gas-modal/index.js91
-rw-r--r--ui/app/components/input-number.js29
-rw-r--r--ui/app/components/modals/modal.js26
-rw-r--r--ui/app/components/send/account-list-item.js33
-rw-r--r--ui/app/components/send/currency-display.js61
-rw-r--r--ui/app/components/send/from-dropdown.js2
-rw-r--r--ui/app/components/send/gas-fee-display-v2.js47
-rw-r--r--ui/app/components/send/gas-tooltip.js4
-rw-r--r--ui/app/components/send/memo-textarea.js33
-rw-r--r--ui/app/components/send/send-v2-container.js62
-rw-r--r--ui/app/components/send/to-autocomplete.js4
-rw-r--r--ui/app/conversion-util.js14
-rw-r--r--ui/app/css/itcss/components/account-dropdown.scss25
-rw-r--r--ui/app/css/itcss/components/currency-display.scss4
-rw-r--r--ui/app/css/itcss/components/gas-slider.scss51
-rw-r--r--ui/app/css/itcss/components/index.scss3
-rw-r--r--ui/app/css/itcss/components/send.scss206
-rw-r--r--ui/app/selectors.js35
-rw-r--r--ui/app/send-v2.js203
22 files changed, 915 insertions, 132 deletions
diff --git a/ui/app/app.js b/ui/app/app.js
index 92fc5e697..08d24d86c 100644
--- a/ui/app/app.js
+++ b/ui/app/app.js
@@ -10,7 +10,7 @@ const NewKeyChainScreen = require('./new-keychain')
// accounts
const MainContainer = require('./main-container')
const SendTransactionScreen = require('./send')
-const SendTransactionScreen2 = require('./send-v2.js')
+const SendTransactionScreen2 = require('./components/send/send-v2-container')
const SendTokenScreen = require('./components/send-token')
const ConfirmTxScreen = require('./conf-tx')
// notice
@@ -356,7 +356,12 @@ App.prototype.renderPrimary = function () {
case 'sendToken':
log.debug('rendering send token screen')
- return h(SendTokenScreen, {key: 'sendToken'})
+
+ const SendTokenComponentToRender = checkFeatureToggle('send-v2')
+ ? SendTransactionScreen2
+ : SendTokenScreen
+
+ return h(SendTokenComponentToRender, {key: 'sendToken'})
case 'newKeychain':
log.debug('rendering new keychain screen')
diff --git a/ui/app/components/customize-gas-modal/gas-modal-card.js b/ui/app/components/customize-gas-modal/gas-modal-card.js
new file mode 100644
index 000000000..8e739ee40
--- /dev/null
+++ b/ui/app/components/customize-gas-modal/gas-modal-card.js
@@ -0,0 +1,55 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const InputNumber = require('../input-number.js')
+const GasSlider = require('./gas-slider.js')
+
+module.exports = GasModalCard
+
+inherits(GasModalCard, Component)
+function GasModalCard () {
+ Component.call(this)
+}
+
+GasModalCard.prototype.render = function () {
+ const {
+ memo,
+ identities,
+ onChange,
+ unitLabel,
+ value,
+ min,
+ max,
+ step,
+ title,
+ copy
+ } = this.props
+
+ return h('div.send-v2__gas-modal-card', [
+
+ h('div.send-v2__gas-modal-card__title', {}, title),
+
+ h('div.send-v2__gas-modal-card__copy', {}, copy),
+
+ h(InputNumber, {
+ unitLabel,
+ step,
+ max,
+ min,
+ placeholder: '0',
+ value,
+ onChange,
+ }),
+
+ h(GasSlider, {
+ value,
+ step,
+ max,
+ min,
+ onChange,
+ }),
+
+ ])
+
+}
+
diff --git a/ui/app/components/customize-gas-modal/gas-slider.js b/ui/app/components/customize-gas-modal/gas-slider.js
new file mode 100644
index 000000000..e76e96545
--- /dev/null
+++ b/ui/app/components/customize-gas-modal/gas-slider.js
@@ -0,0 +1,50 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+
+module.exports = GasSlider
+
+inherits(GasSlider, Component)
+function GasSlider () {
+ Component.call(this)
+}
+
+GasSlider.prototype.render = function () {
+ const {
+ memo,
+ identities,
+ onChange,
+ unitLabel,
+ value,
+ id,
+ step,
+ max,
+ min,
+ } = this.props
+
+ return h('div.gas-slider', [
+
+ h('input.gas-slider__input', {
+ type: 'range',
+ step,
+ max,
+ min,
+ value,
+ id: 'gasSlider',
+ onChange: event => onChange(event.target.value),
+ }, []),
+
+ h('div.gas-slider__bar', [
+
+ h('div.gas-slider__low'),
+
+ h('div.gas-slider__mid'),
+
+ h('div.gas-slider__high'),
+
+ ]),
+
+ ])
+
+}
+
diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js
new file mode 100644
index 000000000..91e2626b4
--- /dev/null
+++ b/ui/app/components/customize-gas-modal/index.js
@@ -0,0 +1,91 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const connect = require('react-redux').connect
+const actions = require('../../actions')
+const GasModalCard = require('./gas-modal-card')
+
+function mapStateToProps (state) {
+ return {}
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ hideModal: () => dispatch(actions.hideModal()),
+ }
+}
+
+inherits(CustomizeGasModal, Component)
+function CustomizeGasModal () {
+ Component.call(this)
+
+ this.state = {
+ gasPrice: '0.23',
+ gasLimit: '25000',
+ }
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(CustomizeGasModal)
+
+CustomizeGasModal.prototype.render = function () {
+ const { hideModal } = this.props
+ const { gasPrice, gasLimit } = this.state
+
+ return h('div.send-v2__customize-gas', {}, [
+ h('div', {
+ }, [
+ h('div.send-v2__customize-gas__header', {}, [
+
+ h('div.send-v2__customize-gas__title', 'Customize Gas'),
+
+ h('div.send-v2__customize-gas__close', {
+ onClick: hideModal,
+ }),
+
+ ]),
+
+ h('div.send-v2__customize-gas__body', {}, [
+
+ h(GasModalCard, {
+ value: gasPrice,
+ min: 0.0,
+ max: 5.0,
+ step: 0.01,
+ onChange: gasPrice => this.setState({ gasPrice }),
+ title: 'Gas Price',
+ copy: 'We calculate the suggested gas prices based on network success rates.',
+ }),
+
+ h(GasModalCard, {
+ value: gasLimit,
+ min: 20000,
+ max: 100000,
+ step: 1,
+ onChange: gasLimit => this.setState({ gasLimit }),
+ title: 'Gas Limit',
+ copy: 'We calculate the suggested gas limit based on network success rates.',
+ }),
+
+ ]),
+
+ h('div.send-v2__customize-gas__footer', {}, [
+
+ h('div.send-v2__customize-gas__revert', {
+ onClick: () => console.log('Revert'),
+ }, ['Revert']),
+
+ h('div.send-v2__customize-gas__buttons', [
+ h('div.send-v2__customize-gas__cancel', {
+ onClick: this.props.hideModal,
+ }, ['CANCEL']),
+
+ h('div.send-v2__customize-gas__save', {
+ onClick: () => console.log('Save'),
+ }, ['SAVE']),
+ ])
+
+ ]),
+
+ ]),
+ ])
+}
diff --git a/ui/app/components/input-number.js b/ui/app/components/input-number.js
index 2824d77aa..16347fd5e 100644
--- a/ui/app/components/input-number.js
+++ b/ui/app/components/input-number.js
@@ -1,6 +1,7 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
+const { addCurrencies } = require('../conversion-util')
module.exports = InputNumber
@@ -8,49 +9,37 @@ inherits(InputNumber, Component)
function InputNumber () {
Component.call(this)
- this.state = {
- value: 0,
- }
-
this.setValue = this.setValue.bind(this)
}
-InputNumber.prototype.componentWillMount = function () {
- const { initValue = 0 } = this.props
-
- this.setState({ value: initValue })
-}
-
InputNumber.prototype.setValue = function (newValue) {
- const { fixed, min = -1, onChange } = this.props
+ const { fixed, min = -1, max = Infinity, onChange } = this.props
- if (fixed) newValue = Number(newValue.toFixed(4))
+ newValue = Number(fixed ? newValue.toFixed(4) : newValue)
- if (newValue >= min) {
- this.setState({ value: newValue })
+ if (newValue >= min && newValue <= max) {
onChange(newValue)
}
}
InputNumber.prototype.render = function () {
- const { unitLabel, step = 1, placeholder } = this.props
- const { value } = this.state
+ const { unitLabel, step = 1, placeholder, value = 0 } = this.props
return h('div.customize-gas-input-wrapper', {}, [
h('input.customize-gas-input', {
placeholder,
type: 'number',
- value,
- onChange: (e) => this.setValue(Number(e.target.value)),
+ value: value,
+ onChange: (e) => this.setValue(e.target.value),
}),
h('span.gas-tooltip-input-detail', {}, [unitLabel]),
h('div.gas-tooltip-input-arrows', {}, [
h('i.fa.fa-angle-up', {
- onClick: () => this.setValue(value + step),
+ onClick: () => this.setValue(addCurrencies(value, step)),
}),
h('i.fa.fa-angle-down', {
style: { cursor: 'pointer' },
- onClick: () => this.setValue(value - step),
+ onClick: () => this.setValue(addCurrencies(value, step * -1)),
}),
]),
])
diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js
index 7247d840e..88deb2bb0 100644
--- a/ui/app/components/modals/modal.js
+++ b/ui/app/components/modals/modal.js
@@ -15,6 +15,7 @@ const ExportPrivateKeyModal = require('./export-private-key-modal')
const NewAccountModal = require('./new-account-modal')
const ShapeshiftDepositTxModal = require('./shapeshift-deposit-tx-modal.js')
const HideTokenConfirmationModal = require('./hide-token-confirmation-modal')
+const CustomizeGasModal = require('../customize-gas-modal')
const accountModalStyle = {
mobileModalStyle: {
@@ -156,6 +157,31 @@ const MODALS = {
},
},
+ CUSTOMIZE_GAS: {
+ contents: [
+ h(CustomizeGasModal, {}, []),
+ ],
+ mobileModalStyle: {
+ width: '355px',
+ height: '598px',
+ // top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh',
+ top: '5%',
+ transform: 'none',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ },
+ laptopModalStyle: {
+ width: '720px',
+ height: '377px',
+ top: '80px',
+ transform: 'none',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ },
+ },
+
DEFAULT: {
contents: [],
mobileModalStyle: {},
diff --git a/ui/app/components/send/account-list-item.js b/ui/app/components/send/account-list-item.js
index b11527d95..64acde767 100644
--- a/ui/app/components/send/account-list-item.js
+++ b/ui/app/components/send/account-list-item.js
@@ -3,27 +3,34 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const Identicon = require('../identicon')
+const CurrencyDisplay = require('./currency-display')
+const { conversionRateSelector } = require('../../selectors')
inherits(AccountListItem, Component)
function AccountListItem () {
Component.call(this)
}
-module.exports = AccountListItem
+function mapStateToProps(state) {
+ return {
+ conversionRate: conversionRateSelector(state)
+ }
+}
+
+module.exports = connect(mapStateToProps)(AccountListItem)
AccountListItem.prototype.render = function () {
const {
account,
handleClick,
icon = null,
+ conversionRate,
} = this.props
- const { identity, balancesToRender } = account
- const { name, address } = identity
- const { primary, secondary } = balancesToRender
+ const { name, address, balance } = account
return h('div.account-list-item', {
- onClick: () => handleClick(identity),
+ onClick: () => handleClick({ name, address, balance }),
}, [
h('div.account-list-item__top-row', {}, [
@@ -35,7 +42,7 @@ AccountListItem.prototype.render = function () {
diameter: 18,
className: 'account-list-item__identicon',
},
- ),
+ ),
h('div.account-list-item__account-name', {}, name),
@@ -43,9 +50,17 @@ AccountListItem.prototype.render = function () {
]),
- h('div.account-list-item__account-primary-balance', {}, primary),
-
- h('div.account-list-item__account-secondary-balance', {}, secondary),
+ h(CurrencyDisplay, {
+ primaryCurrency: 'ETH',
+ convertedCurrency: 'USD',
+ value: balance,
+ conversionRate,
+ convertedPrefix: '$',
+ readOnly: true,
+ className: 'account-list-item__account-balances',
+ primaryBalanceClassName: 'account-list-item__account-primary-balance',
+ convertedBalanceClassName: 'account-list-item__account-secondary-balance',
+ }, name),
])
} \ No newline at end of file
diff --git a/ui/app/components/send/currency-display.js b/ui/app/components/send/currency-display.js
index 332d722ec..ed9847fdb 100644
--- a/ui/app/components/send/currency-display.js
+++ b/ui/app/components/send/currency-display.js
@@ -11,8 +11,7 @@ function CurrencyDisplay () {
Component.call(this)
this.state = {
- minWidth: null,
- currentScrollWidth: null,
+ value: null,
}
}
@@ -29,28 +28,50 @@ function resetCaretIfPastEnd (value, event) {
}
}
+CurrencyDisplay.prototype.handleChangeInHexWei = function (value) {
+ const { handleChange } = this.props
+
+ const valueInHexWei = conversionUtil(value, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'hex',
+ toDenomination: 'WEI',
+ })
+
+ handleChange(valueInHexWei)
+}
+
CurrencyDisplay.prototype.render = function () {
const {
- className,
+ className = 'currency-display',
+ primaryBalanceClassName = 'currency-display__input',
+ convertedBalanceClassName = 'currency-display__converted-value',
+ conversionRate,
primaryCurrency,
convertedCurrency,
- value = '',
- placeholder = '0',
- conversionRate,
convertedPrefix = '',
+ placeholder = '0',
readOnly = false,
- handleChange,
+ value: initValue,
} = this.props
- const { minWidth } = this.state
+ const { value } = this.state
+
+ const initValueToRender = conversionUtil(initValue, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromDenomination: 'WEI',
+ numberOfDecimals: 6,
+ conversionRate,
+ })
- const convertedValue = conversionUtil(value, {
+ const convertedValue = conversionUtil(value || initValueToRender, {
fromNumericBase: 'dec',
fromCurrency: primaryCurrency,
toCurrency: convertedCurrency,
+ numberOfDecimals: 2,
conversionRate,
})
- return h('div.currency-display', {
+ return h('div', {
className,
}, [
@@ -58,35 +79,39 @@ CurrencyDisplay.prototype.render = function () {
h('div.currency-display__input-wrapper', [
- h('input.currency-display__input', {
- value: `${value} ${primaryCurrency}`,
+ h('input', {
+ className: primaryBalanceClassName,
+ value: `${value || initValueToRender} ${primaryCurrency}`,
placeholder: `${0} ${primaryCurrency}`,
readOnly,
onChange: (event) => {
let newValue = event.target.value.split(' ')[0]
if (newValue === '') {
- handleChange('0')
+ this.setState({ value: '0' })
}
else if (newValue.match(/^0[1-9]$/)) {
- handleChange(newValue.match(/[1-9]/)[0])
+ this.setState({ value: newValue.match(/[1-9]/)[0] })
}
else if (newValue && !isValidInput(newValue)) {
event.preventDefault()
}
else {
- handleChange(newValue)
+ this.setState({ value: newValue })
}
},
- onKeyUp: event => resetCaretIfPastEnd(value, event),
- onClick: event => resetCaretIfPastEnd(value, event),
+ onBlur: event => this.handleChangeInHexWei(event.target.value.split(' ')[0]),
+ onKeyUp: event => resetCaretIfPastEnd(value || initValueToRender, event),
+ onClick: event => resetCaretIfPastEnd(value || initValueToRender, event),
}),
]),
]),
- h('div.currency-display__converted-value', {}, `${convertedPrefix}${convertedValue} ${convertedCurrency}`),
+ h('div', {
+ className: convertedBalanceClassName,
+ }, `${convertedPrefix}${convertedValue} ${convertedCurrency}`),
])
diff --git a/ui/app/components/send/from-dropdown.js b/ui/app/components/send/from-dropdown.js
index fb0a00cc2..e8e1d43f0 100644
--- a/ui/app/components/send/from-dropdown.js
+++ b/ui/app/components/send/from-dropdown.js
@@ -14,7 +14,7 @@ function FromDropdown () {
FromDropdown.prototype.getListItemIcon = function (currentAccount, selectedAccount) {
const listItemIcon = h(`i.fa.fa-check.fa-lg`, { style: { color: '#02c9b1' } })
- return currentAccount.identity.address === selectedAccount.identity.address
+ return currentAccount.address === selectedAccount.address
? listItemIcon
: null
}
diff --git a/ui/app/components/send/gas-fee-display-v2.js b/ui/app/components/send/gas-fee-display-v2.js
new file mode 100644
index 000000000..226ae93f8
--- /dev/null
+++ b/ui/app/components/send/gas-fee-display-v2.js
@@ -0,0 +1,47 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const CurrencyDisplay = require('./currency-display');
+
+const { multiplyCurrencies } = require('../../conversion-util')
+
+module.exports = GasFeeDisplay
+
+inherits(GasFeeDisplay, Component)
+function GasFeeDisplay () {
+ Component.call(this)
+}
+
+GasFeeDisplay.prototype.render = function () {
+ const {
+ conversionRate,
+ gasLimit,
+ gasPrice,
+ onClick,
+ } = this.props
+
+ const readyToRender = Boolean(gasLimit && gasPrice)
+
+ return h('div', [
+
+ readyToRender
+ ? h(CurrencyDisplay, {
+ primaryCurrency: 'ETH',
+ convertedCurrency: 'USD',
+ value: multiplyCurrencies(gasLimit, gasPrice, { toNumericBase: 'hex' }),
+ conversionRate,
+ convertedPrefix: '$',
+ readOnly: true,
+ })
+ : h('div.currency-display', 'Loading...')
+ ,
+
+ h('div.send-v2__sliders-icon-container', {
+ onClick,
+ }, [
+ h('i.fa.fa-sliders.send-v2__sliders-icon'),
+ ])
+
+ ])
+}
+
diff --git a/ui/app/components/send/gas-tooltip.js b/ui/app/components/send/gas-tooltip.js
index bef419e48..46aff3499 100644
--- a/ui/app/components/send/gas-tooltip.js
+++ b/ui/app/components/send/gas-tooltip.js
@@ -73,7 +73,7 @@ GasTooltip.prototype.render = function () {
step: 1,
min: 0,
placeholder: '0',
- initValue: gasPrice,
+ value: gasPrice,
onChange: (newPrice) => this.updateGasPrice(newPrice),
}),
h('div.gas-tooltip-input-label', {
@@ -89,7 +89,7 @@ GasTooltip.prototype.render = function () {
step: 1,
min: 0,
placeholder: '0',
- initValue: gasLimit,
+ value: gasLimit,
onChange: (newLimit) => this.updateGasLimit(newLimit),
}),
]),
diff --git a/ui/app/components/send/memo-textarea.js b/ui/app/components/send/memo-textarea.js
new file mode 100644
index 000000000..4005b9493
--- /dev/null
+++ b/ui/app/components/send/memo-textarea.js
@@ -0,0 +1,33 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const Identicon = require('../identicon')
+
+module.exports = MemoTextArea
+
+inherits(MemoTextArea, Component)
+function MemoTextArea () {
+ Component.call(this)
+}
+
+MemoTextArea.prototype.render = function () {
+ const { memo, identities, onChange } = this.props
+
+ return h('div.send-v2__memo-text-area', [
+
+ h('textarea.send-v2__memo-text-area__input', {
+ placeholder: 'Optional',
+ value: memo,
+ onChange,
+ // onBlur: () => {
+ // this.setErrorsFor('memo')
+ // },
+ onFocus: event => {
+ // this.clearErrorsFor('memo')
+ },
+ }),
+
+ ])
+
+}
+
diff --git a/ui/app/components/send/send-v2-container.js b/ui/app/components/send/send-v2-container.js
new file mode 100644
index 000000000..0c8dd5335
--- /dev/null
+++ b/ui/app/components/send/send-v2-container.js
@@ -0,0 +1,62 @@
+const connect = require('react-redux').connect
+const actions = require('../../actions')
+const abi = require('ethereumjs-abi')
+const SendEther = require('../../send-v2')
+
+const { multiplyCurrencies } = require('../../conversion-util')
+
+const {
+ accountsWithSendEtherInfoSelector,
+ getCurrentAccountWithSendEtherInfo,
+ conversionRateSelector,
+ getSelectedToken,
+ getSelectedTokenExchangeRate,
+ getSelectedAddress,
+} = require('../../selectors')
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(SendEther)
+
+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;
+ let primaryCurrency;
+ let tokenToUSDRate;
+ if (selectedToken) {
+ data = Array.prototype.map.call(
+ abi.rawEncode(['address', 'uint256'], [selectedAddress, '0x0']),
+ x => ('00' + x.toString(16)).slice(-2)
+ ).join('')
+
+ primaryCurrency = selectedToken.symbol
+
+ tokenToUSDRate = multiplyCurrencies(
+ conversionRate,
+ selectedTokenExchangeRate,
+ { toNumericBase: 'dec' }
+ )
+ }
+
+ return {
+ selectedAccount: getCurrentAccountWithSendEtherInfo(state),
+ accounts: accountsWithSendEtherInfoSelector(state),
+ conversionRate,
+ selectedToken,
+ primaryCurrency,
+ data,
+ tokenToUSDRate,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ showCustomizeGasModal: () => dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' })),
+ estimateGas: params => dispatch(actions.estimateGas(params)),
+ getGasPrice: () => dispatch(actions.getGasPrice()),
+ updateTokenExchangeRate: token => dispatch(actions.updateTokenExchangeRate(token)),
+ }
+}
diff --git a/ui/app/components/send/to-autocomplete.js b/ui/app/components/send/to-autocomplete.js
index 3808bf496..1bf1e1907 100644
--- a/ui/app/components/send/to-autocomplete.js
+++ b/ui/app/components/send/to-autocomplete.js
@@ -11,7 +11,7 @@ function ToAutoComplete () {
}
ToAutoComplete.prototype.render = function () {
- const { to, identities, onChange } = this.props
+ const { to, accounts, onChange } = this.props
return h('div.send-v2__to-autocomplete', [
@@ -32,7 +32,7 @@ ToAutoComplete.prototype.render = function () {
h('datalist#addresses', [
// Corresponds to the addresses owned.
- ...Object.entries(identities).map(([key, { address, name }]) => {
+ ...Object.entries(accounts).map(([key, { address, name }]) => {
return h('option', {
value: address,
label: name,
diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js
index 70c3c2622..3a702bcdd 100644
--- a/ui/app/conversion-util.js
+++ b/ui/app/conversion-util.js
@@ -128,7 +128,8 @@ const conversionUtil = (value, {
value: value || '0',
});
-const addCurrencies = (a, b, { toNumericBase, numberOfDecimals }) => {
+const addCurrencies = (a, b, options = {}) => {
+ const { toNumericBase, numberOfDecimals } = options
const value = (new BigNumber(a)).add(b);
return converter({
value,
@@ -137,6 +138,16 @@ const addCurrencies = (a, b, { toNumericBase, numberOfDecimals }) => {
})
}
+const multiplyCurrencies = (a, b, options = {}) => {
+ const { toNumericBase, numberOfDecimals } = options
+ const value = (new BigNumber(a)).times(b);
+ return converter({
+ value,
+ toNumericBase,
+ numberOfDecimals,
+ })
+}
+
const conversionGreaterThan = (
{ value, fromNumericBase },
{ value: compareToValue, fromNumericBase: compareToBase },
@@ -152,5 +163,6 @@ const conversionGreaterThan = (
module.exports = {
conversionUtil,
addCurrencies,
+ multiplyCurrencies,
conversionGreaterThan,
} \ 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 9966c7f3f..4fc7c705a 100644
--- a/ui/app/css/itcss/components/account-dropdown.scss
+++ b/ui/app/css/itcss/components/account-dropdown.scss
@@ -23,6 +23,16 @@
margin-left: 8px;
position: relative;
}
+
+ &__account-balances {
+ height: auto;
+ border: none;
+ background-color: transparent;
+ color: #9b9b9b;
+ margin-left: 34px;
+ margin-top: 4px;
+ position: relative;
+ }
&__account-name {
font-size: 16px;
@@ -34,13 +44,22 @@
right: 12px;
top: 1px;
}
+
+ &__account-primary-balance,
+ &__account-secondary-balance {
+ font-family: Roboto;
+ line-height: 16px;
+ font-size: 12px;
+ font-weight: 300;
+ }
+
&__account-primary-balance {
- margin-left: 34px;
- margin-top: 4px;
+ color: $scorpion;
+ border: none;
+ outline: 0 !important;
}
&__account-secondary-balance {
- margin-left: 34px;
color: $dusty-gray;
}
}
diff --git a/ui/app/css/itcss/components/currency-display.scss b/ui/app/css/itcss/components/currency-display.scss
index b2776bb47..f2cc6e700 100644
--- a/ui/app/css/itcss/components/currency-display.scss
+++ b/ui/app/css/itcss/components/currency-display.scss
@@ -15,10 +15,6 @@
display: flex;
}
- &__input-wrapper {
- margin-top: -1px;
- }
-
&__input {
color: $scorpion;
font-family: Roboto;
diff --git a/ui/app/css/itcss/components/gas-slider.scss b/ui/app/css/itcss/components/gas-slider.scss
new file mode 100644
index 000000000..c27a560bd
--- /dev/null
+++ b/ui/app/css/itcss/components/gas-slider.scss
@@ -0,0 +1,51 @@
+.gas-slider {
+ position: relative;
+ width: 313px;
+
+ &__input {
+ width: 317px;
+ margin-left: -2px;
+ z-index: 2;
+ }
+
+ input[type=range] {
+ -webkit-appearance: none !important;
+ }
+
+ input[type=range]::-webkit-slider-thumb {
+ -webkit-appearance: none !important;
+ height: 26px;
+ width: 26px;
+ border: 2px solid #B8B8B8;
+ background-color: #FFFFFF;
+ box-shadow: 0 2px 4px 0 rgba(0,0,0,0.08);
+ border-radius: 50%;
+ position: relative;
+ z-index: 10;
+ }
+
+ &__bar {
+ height: 6px;
+ width: 313px;
+ background: $alto;
+ display: flex;
+ justify-content: space-between;
+ position: absolute;
+ top: 11px;
+ z-index: 0;
+ }
+
+ &__low, &__high {
+ height: 6px;
+ width: 49px;
+ z-index: 1;
+ }
+
+ &__low {
+ background-color: $crimson;
+ }
+
+ &__high {
+ background-color: $caribbean-green;
+ }
+} \ No newline at end of file
diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss
index dee0959b7..fda002785 100644
--- a/ui/app/css/itcss/components/index.scss
+++ b/ui/app/css/itcss/components/index.scss
@@ -35,3 +35,6 @@
@import './account-menu.scss';
@import './menu.scss';
+
+@import './gas-slider.scss';
+
diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss
index 80aacf1ab..ddabdee2e 100644
--- a/ui/app/css/itcss/components/send.scss
+++ b/ui/app/css/itcss/components/send.scss
@@ -264,7 +264,7 @@
.gas-tooltip-input-arrows {
position: absolute;
top: 0;
- left: 178px;
+ right: 4px;
width: 17px;
height: 28px;
border: 1px solid #dadada;
@@ -420,7 +420,16 @@
}
}
- &__send-eth-icon {
+ &__send-header-icon-container {
+ z-index: 25;
+
+ @media screen and (max-width: $break-small) {
+ position: relative;
+ top: 0;
+ }
+ }
+
+ &__send-header-icon {
border-radius: 50%;
width: 48px;
height: 48px;
@@ -428,11 +437,6 @@
z-index: 25;
padding: 4px;
background-color: $white;
-
- @media screen and (max-width: $break-small) {
- position: relative;
- top: 0;
- }
}
&__send-arrow-icon {
@@ -472,7 +476,7 @@
position: absolute;
transform: rotate(45deg);
left: 178px;
- top: 71px;
+ top: 65px;
}
&__title {
@@ -512,7 +516,9 @@
font-family: Roboto;
font-size: 16px;
line-height: 22px;
- margin-top: 16px;
+ display: flex;
+ flex-flow: column;
+ justify-content: center;
}
&__from-dropdown {
@@ -550,7 +556,7 @@
}
}
- &__to-autocomplete {
+ &__to-autocomplete, &__memo-text-area {
&__input {
height: 54px;
width: 240px;
@@ -566,6 +572,32 @@
}
}
+ &__sliders-icon-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 24px;
+ width: 24px;
+ border: 1px solid $curious-blue;
+ border-radius: 4px;
+ background-color: $white;
+ padding: 5px;
+ position: absolute;
+ right: 15px;
+ top: 14px;
+ cursor: pointer;
+ }
+
+ &__sliders-icon {
+ color: $curious-blue;
+ }
+
+ &__memo-text-area {
+ &__input {
+ padding: 6px 10px;
+ }
+ }
+
&__footer {
height: 92px;
width: 100%;
@@ -573,8 +605,7 @@
justify-content: space-evenly;
align-items: center;
border-top: 1px solid $alto;
- position: absolute;
- bottom: 0;
+ margin-top: 29px;
}
&__next-btn,
@@ -607,4 +638,155 @@
color: $dusty-gray;
border-color: $dusty-gray;
}
+
+ &__customize-gas {
+ border: 1px solid #D8D8D8;
+ border-radius: 4px;
+ background-color: #FFFFFF;
+ box-shadow: 0 2px 4px 0 rgba(0,0,0,0.14);
+ font-family: Roboto;
+ display: flex;
+ flex-flow: column;
+
+ @media screen and (max-width: $break-small) {
+ width: 355px;
+ height: 598px;
+ }
+
+ &__header {
+ height: 52px;
+ border-bottom: 1px solid $alto;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 22px;
+ }
+
+ &__title {
+ margin-left: 19.25px;
+ }
+
+ &__close::after {
+ content: '\00D7';
+ font-size: 1.8em;
+ color: $dusty-gray;
+ font-family: sans-serif;
+ cursor: pointer;
+ margin-right: 19.25px;
+ }
+
+ &__body {
+ height: 248px;
+ display: flex;
+
+ @media screen and (max-width: $break-small) {
+ width: 355px;
+ height: 470px;
+ flex-flow: column;
+ }
+ }
+
+ &__footer {
+ height: 75px;
+ border-top: 1px solid $alto;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 22px;
+ }
+
+ &__buttons {
+ display: flex;
+ justify-content: space-between;
+ width: 181.75px;
+ margin-right: 21.25px;
+ }
+
+ &__revert, &__cancel, &__save {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+ }
+
+ &__revert {
+ color: $silver-chalice;
+ font-size: 16px;
+ margin-left: 21.25px;
+ }
+
+ &__cancel, &__save {
+ height: 34.64px;
+ width: 85.74px;
+ border: 1px solid $dusty-gray;
+ border-radius: 2px;
+ font-family: 'DIN OT';
+ font-size: 12px;
+ color: $dusty-gray;
+ }
+ }
+
+ &__gas-modal-card {
+ width: 360px;
+ display: flex;
+ flex-flow: column;
+ align-items: flex-start;
+ padding-left: 20px;
+
+ &__title {
+ height: 26px;
+ width: 84px;
+ color: $tundora;
+ font-family: Roboto;
+ font-size: 20px;
+ font-weight: 300;
+ line-height: 26px;
+ margin-top: 17px;
+ }
+
+ &__copy {
+ height: 38px;
+ width: 314px;
+ color: $tundora;
+ font-family: Roboto;
+ font-size: 14px;
+ line-height: 19px;
+ margin-top: 17px;
+ }
+
+ .customize-gas-input-wrapper {
+ margin-top: 17px;
+ }
+
+ .customize-gas-input {
+ height: 54px;
+ width: 315px;
+ border: 1px solid $geyser;
+ background-color: $white;
+ padding-left: 15px;
+ }
+
+ .gas-tooltip-input-arrows {
+ width: 32px;
+ height: 54px;
+ border-left: 1px solid #dadada;
+ font-size: 18px;
+ color: $tundora;
+ right: 0px;
+ padding: 1px 4px;
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ }
+
+ input[type="number"]::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ display: none;
+ }
+
+ input[type="number"]:hover::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ display: none;
+ }
+ }
}
diff --git a/ui/app/selectors.js b/ui/app/selectors.js
index fdbc5fcde..951161510 100644
--- a/ui/app/selectors.js
+++ b/ui/app/selectors.js
@@ -5,14 +5,16 @@ const selectors = {
getSelectedIdentity,
getSelectedAccount,
getSelectedToken,
+ getSelectedTokenExchangeRate,
conversionRateSelector,
transactionsSelector,
+ accountsWithSendEtherInfoSelector,
+ getCurrentAccountWithSendEtherInfo,
}
module.exports = selectors
function getSelectedAddress (state) {
- // TODO: accounts is not defined. Is it needed?
const selectedAddress = state.metamask.selectedAddress || Object.keys(state.metamask.accounts)[0]
return selectedAddress
@@ -40,10 +42,41 @@ function getSelectedToken (state) {
return selectedToken || null
}
+function getSelectedTokenExchangeRate (state) {
+ const tokenExchangeRates = state.metamask.tokenExchangeRates
+ const selectedToken = getSelectedToken(state) || {}
+ const { symbol = '' } = selectedToken
+
+ const pair = `${symbol.toLowerCase()}_eth`
+ const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {}
+
+ return tokenExchangeRate
+}
+
function conversionRateSelector (state) {
return state.metamask.conversionRate
}
+function accountsWithSendEtherInfoSelector (state) {
+ const {
+ accounts,
+ identities,
+ } = state.metamask
+
+ const accountsWithSendEtherInfo = Object.entries(accounts).map(([key, account]) => {
+ return Object.assign({}, account, identities[key])
+ })
+
+ return accountsWithSendEtherInfo
+}
+
+function getCurrentAccountWithSendEtherInfo (state) {
+ const currentAddress = getSelectedAddress(state)
+ const accounts = accountsWithSendEtherInfoSelector(state)
+
+ return accounts.find(({ address }) => address === currentAddress)
+}
+
function transactionsSelector (state) {
const { network, selectedTokenAddress } = state.metamask
const unapprovedMsgs = valuesFor(state.metamask.unapprovedMsgs)
diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js
index 47f8b18bd..af7586859 100644
--- a/ui/app/send-v2.js
+++ b/ui/app/send-v2.js
@@ -2,61 +2,135 @@ const { inherits } = require('util')
const PersistentForm = require('../lib/persistent-form')
const h = require('react-hyperscript')
const connect = require('react-redux').connect
+
+const Identicon = require('./components/identicon')
const FromDropdown = require('./components/send/from-dropdown')
const ToAutoComplete = require('./components/send/to-autocomplete')
const CurrencyDisplay = require('./components/send/currency-display')
+const MemoTextArea = require('./components/send/memo-textarea')
+const GasFeeDisplay = require('./components/send/gas-fee-display-v2')
-module.exports = connect(mapStateToProps)(SendTransactionScreen)
-
-function mapStateToProps (state) {
- const mockAccounts = Array.from(new Array(5))
- .map((v, i) => ({
- identity: {
- name: `Test Account Name ${i}`,
- address: `0x02f567704cc6569127e18e3d00d2c85bcbfa6f0${i}`,
- },
- balancesToRender: {
- primary: `100${i}.000001 ETH`,
- secondary: `$30${i},000.00 USD`,
- }
- }))
- const conversionRate = 301.0005
-
- return {
- accounts: mockAccounts,
- conversionRate
- }
-}
+const { showModal } = require('./actions')
+
+module.exports = SendTransactionScreen
inherits(SendTransactionScreen, PersistentForm)
function SendTransactionScreen () {
PersistentForm.call(this)
this.state = {
- newTx: {
- from: '',
- to: '',
- gasPrice: null,
- gas: '0.001',
- amount: '10',
- txData: null,
- memo: '',
- },
+ from: '',
+ to: '',
+ gasPrice: null,
+ gasLimit: null,
+ amount: '0x0',
+ txData: null,
+ memo: '',
dropdownOpen: false,
}
}
+SendTransactionScreen.prototype.componentWillMount = function () {
+ const {
+ updateTokenExchangeRate,
+ selectedToken = {},
+ getGasPrice,
+ estimateGas,
+ selectedAddress,
+ data,
+ } = this.props
+ const { symbol } = selectedToken || {}
+
+ const estimateGasParams = {
+ from: selectedAddress,
+ gas: '746a528800',
+ }
+
+ if (symbol) {
+ updateTokenExchangeRate(symbol)
+ Object.assign(estimateGasParams, { value: '0x0' })
+ }
+
+ if (data) {
+ Object.assign(estimateGasParams, { data })
+ }
+
+ Promise.all([
+ getGasPrice(),
+ estimateGas({
+ from: selectedAddress,
+ gas: '746a528800',
+ }),
+ ])
+ .then(([blockGasPrice, estimatedGas]) => {
+ this.setState({
+ gasPrice: blockGasPrice,
+ gasLimit: estimatedGas,
+ })
+ })
+}
+
+SendTransactionScreen.prototype.renderHeaderIcon = function () {
+ const { selectedToken } = this.props
+
+ return h('div.send-v2__send-header-icon-container', [
+ selectedToken
+ ? h(Identicon, {
+ diameter: 40,
+ address: selectedToken.address,
+ })
+ : h('img.send-v2__send-header-icon', { src: '../images/eth_logo.svg' })
+ ])
+}
+
+SendTransactionScreen.prototype.renderTitle = function () {
+ const { selectedToken } = this.props
+
+ return h('div.send-v2__title', [selectedToken ? 'Send Tokens' : 'Send Funds'])
+}
+
+SendTransactionScreen.prototype.renderCopy = function () {
+ const { selectedToken } = this.props
+
+ const tokenText = selectedToken ? 'tokens' : 'ETH'
+
+ return h('div', [
+
+ h('div.send-v2__copy', `Only send ${tokenText} to an Ethereum address.`),
+
+ h('div.send-v2__copy', 'Sending to a different crytpocurrency that is not Ethereum may result in permanent loss.'),
+
+ ])
+}
+
SendTransactionScreen.prototype.render = function () {
- const { accounts, conversionRate } = this.props
- const { dropdownOpen, newTx } = this.state
- const { to, amount, gas } = newTx
+ const {
+ accounts,
+ conversionRate,
+ tokenToUSDRate,
+ selectedToken,
+ showCustomizeGasModal,
+ selectedAccount,
+ primaryCurrency = 'ETH',
+ } = this.props
+
+ const {
+ dropdownOpen,
+ to,
+ amount,
+ gasLimit,
+ gasPrice,
+ memo,
+ } = this.state
+
+ const amountConversionRate = selectedToken ? tokenToUSDRate : conversionRate
return (
h('div.send-v2__container', [
h('div.send-v2__header', {}, [
- h('img.send-v2__send-eth-icon', { src: '../images/eth_logo.svg' }),
+ this.renderHeaderIcon(),
h('div.send-v2__arrow-background', [
h('i.fa.fa-lg.fa-arrow-circle-right.send-v2__send-arrow-icon'),
@@ -66,11 +140,9 @@ SendTransactionScreen.prototype.render = function () {
]),
- h('div.send-v2__title', 'Send Funds'),
-
- h('div.send-v2__copy', 'Only send ETH to an Ethereum address.'),
+ this.renderTitle(),
- h('div.send-v2__copy', 'Sending to a different crytpocurrency that is not Ethereum may result in permanent loss.'),
+ this.renderCopy(),
h('div.send-v2__form', {}, [
@@ -81,10 +153,11 @@ SendTransactionScreen.prototype.render = function () {
h(FromDropdown, {
dropdownOpen,
accounts,
- selectedAccount: accounts[0],
+ selectedAccount,
setFromField: () => console.log('Set From Field'),
openDropdown: () => this.setState({ dropdownOpen: true }),
closeDropdown: () => this.setState({ dropdownOpen: false }),
+ conversionRate,
}),
]),
@@ -95,13 +168,11 @@ SendTransactionScreen.prototype.render = function () {
h(ToAutoComplete, {
to,
- identities: accounts.map(({ identity }) => identity),
+ accounts,
onChange: (event) => {
this.setState({
- newTx: {
- ...this.state.newTx,
- to: event.target.value,
- },
+ ...this.state,
+ to: event.target.value,
})
},
}),
@@ -113,17 +184,15 @@ SendTransactionScreen.prototype.render = function () {
h('div.send-v2__form-label', 'Amount:'),
h(CurrencyDisplay, {
- primaryCurrency: 'ETH',
+ primaryCurrency,
convertedCurrency: 'USD',
value: amount,
- conversionRate,
+ conversionRate: amountConversionRate,
convertedPrefix: '$',
handleChange: (value) => {
this.setState({
- newTx: {
- ...this.state.newTx,
- amount: value,
- },
+ ...this.state,
+ amount: value,
})
}
}),
@@ -134,14 +203,34 @@ SendTransactionScreen.prototype.render = function () {
h('div.send-v2__form-label', 'Gas fee:'),
- h(CurrencyDisplay, {
- primaryCurrency: 'ETH',
- convertedCurrency: 'USD',
- value: gas,
+ h(GasFeeDisplay, {
+ gasLimit,
+ gasPrice,
conversionRate,
- convertedPrefix: '$',
- readOnly: true,
- }),
+ onClick: showCustomizeGasModal,
+ }),
+
+ h('div.send-v2__sliders-icon-container', {
+ onClick: showCustomizeGasModal,
+ }, [
+ h('i.fa.fa-sliders.send-v2__sliders-icon'),
+ ])
+
+ ]),
+
+ h('div.send-v2__form-row', [
+
+ h('div.send-v2__form-label', 'Transaction Memo:'),
+
+ h(MemoTextArea, {
+ memo,
+ onChange: (event) => {
+ this.setState({
+ ...this.state,
+ memo: event.target.value,
+ })
+ },
+ }),
]),