1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 01:39:44 +01:00

[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.
This commit is contained in:
Dan J Miller 2017-10-13 16:19:22 -04:00 committed by Daniel Tsui
parent 81f62a7443
commit 803eaaf968
22 changed files with 917 additions and 134 deletions

View File

@ -10,7 +10,7 @@ const NewKeyChainScreen = require('./new-keychain')
// accounts // accounts
const MainContainer = require('./main-container') const MainContainer = require('./main-container')
const SendTransactionScreen = require('./send') 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 SendTokenScreen = require('./components/send-token')
const ConfirmTxScreen = require('./conf-tx') const ConfirmTxScreen = require('./conf-tx')
// notice // notice
@ -356,7 +356,12 @@ App.prototype.renderPrimary = function () {
case 'sendToken': case 'sendToken':
log.debug('rendering send token screen') 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': case 'newKeychain':
log.debug('rendering new keychain screen') log.debug('rendering new keychain screen')

View File

@ -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,
}),
])
}

View File

@ -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'),
]),
])
}

View File

@ -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']),
])
]),
]),
])
}

View File

@ -1,6 +1,7 @@
const Component = require('react').Component const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const { addCurrencies } = require('../conversion-util')
module.exports = InputNumber module.exports = InputNumber
@ -8,49 +9,37 @@ inherits(InputNumber, Component)
function InputNumber () { function InputNumber () {
Component.call(this) Component.call(this)
this.state = {
value: 0,
}
this.setValue = this.setValue.bind(this) this.setValue = this.setValue.bind(this)
} }
InputNumber.prototype.componentWillMount = function () {
const { initValue = 0 } = this.props
this.setState({ value: initValue })
}
InputNumber.prototype.setValue = function (newValue) { 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) { if (newValue >= min && newValue <= max) {
this.setState({ value: newValue })
onChange(newValue) onChange(newValue)
} }
} }
InputNumber.prototype.render = function () { InputNumber.prototype.render = function () {
const { unitLabel, step = 1, placeholder } = this.props const { unitLabel, step = 1, placeholder, value = 0 } = this.props
const { value } = this.state
return h('div.customize-gas-input-wrapper', {}, [ return h('div.customize-gas-input-wrapper', {}, [
h('input.customize-gas-input', { h('input.customize-gas-input', {
placeholder, placeholder,
type: 'number', type: 'number',
value, value: value,
onChange: (e) => this.setValue(Number(e.target.value)), onChange: (e) => this.setValue(e.target.value),
}), }),
h('span.gas-tooltip-input-detail', {}, [unitLabel]), h('span.gas-tooltip-input-detail', {}, [unitLabel]),
h('div.gas-tooltip-input-arrows', {}, [ h('div.gas-tooltip-input-arrows', {}, [
h('i.fa.fa-angle-up', { h('i.fa.fa-angle-up', {
onClick: () => this.setValue(value + step), onClick: () => this.setValue(addCurrencies(value, step)),
}), }),
h('i.fa.fa-angle-down', { h('i.fa.fa-angle-down', {
style: { cursor: 'pointer' }, style: { cursor: 'pointer' },
onClick: () => this.setValue(value - step), onClick: () => this.setValue(addCurrencies(value, step * -1)),
}), }),
]), ]),
]) ])

View File

@ -15,6 +15,7 @@ const ExportPrivateKeyModal = require('./export-private-key-modal')
const NewAccountModal = require('./new-account-modal') const NewAccountModal = require('./new-account-modal')
const ShapeshiftDepositTxModal = require('./shapeshift-deposit-tx-modal.js') const ShapeshiftDepositTxModal = require('./shapeshift-deposit-tx-modal.js')
const HideTokenConfirmationModal = require('./hide-token-confirmation-modal') const HideTokenConfirmationModal = require('./hide-token-confirmation-modal')
const CustomizeGasModal = require('../customize-gas-modal')
const accountModalStyle = { const accountModalStyle = {
mobileModalStyle: { 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: { DEFAULT: {
contents: [], contents: [],
mobileModalStyle: {}, mobileModalStyle: {},

View File

@ -3,27 +3,34 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const connect = require('react-redux').connect const connect = require('react-redux').connect
const Identicon = require('../identicon') const Identicon = require('../identicon')
const CurrencyDisplay = require('./currency-display')
const { conversionRateSelector } = require('../../selectors')
inherits(AccountListItem, Component) inherits(AccountListItem, Component)
function AccountListItem () { function AccountListItem () {
Component.call(this) Component.call(this)
} }
module.exports = AccountListItem function mapStateToProps(state) {
return {
conversionRate: conversionRateSelector(state)
}
}
module.exports = connect(mapStateToProps)(AccountListItem)
AccountListItem.prototype.render = function () { AccountListItem.prototype.render = function () {
const { const {
account, account,
handleClick, handleClick,
icon = null, icon = null,
conversionRate,
} = this.props } = this.props
const { identity, balancesToRender } = account const { name, address, balance } = account
const { name, address } = identity
const { primary, secondary } = balancesToRender
return h('div.account-list-item', { return h('div.account-list-item', {
onClick: () => handleClick(identity), onClick: () => handleClick({ name, address, balance }),
}, [ }, [
h('div.account-list-item__top-row', {}, [ h('div.account-list-item__top-row', {}, [
@ -43,9 +50,17 @@ AccountListItem.prototype.render = function () {
]), ]),
h('div.account-list-item__account-primary-balance', {}, primary), h(CurrencyDisplay, {
primaryCurrency: 'ETH',
h('div.account-list-item__account-secondary-balance', {}, secondary), 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),
]) ])
} }

View File

@ -11,8 +11,7 @@ function CurrencyDisplay () {
Component.call(this) Component.call(this)
this.state = { this.state = {
minWidth: null, value: null,
currentScrollWidth: 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 () { CurrencyDisplay.prototype.render = function () {
const { const {
className, className = 'currency-display',
primaryBalanceClassName = 'currency-display__input',
convertedBalanceClassName = 'currency-display__converted-value',
conversionRate,
primaryCurrency, primaryCurrency,
convertedCurrency, convertedCurrency,
value = '',
placeholder = '0',
conversionRate,
convertedPrefix = '', convertedPrefix = '',
placeholder = '0',
readOnly = false, readOnly = false,
handleChange, value: initValue,
} = this.props } = this.props
const { minWidth } = this.state const { value } = this.state
const convertedValue = conversionUtil(value, { const initValueToRender = conversionUtil(initValue, {
fromNumericBase: 'dec', fromNumericBase: 'hex',
fromCurrency: primaryCurrency, toNumericBase: 'dec',
toCurrency: convertedCurrency, fromDenomination: 'WEI',
numberOfDecimals: 6,
conversionRate, conversionRate,
}) })
return h('div.currency-display', { const convertedValue = conversionUtil(value || initValueToRender, {
fromNumericBase: 'dec',
fromCurrency: primaryCurrency,
toCurrency: convertedCurrency,
numberOfDecimals: 2,
conversionRate,
})
return h('div', {
className, className,
}, [ }, [
@ -58,35 +79,39 @@ CurrencyDisplay.prototype.render = function () {
h('div.currency-display__input-wrapper', [ h('div.currency-display__input-wrapper', [
h('input.currency-display__input', { h('input', {
value: `${value} ${primaryCurrency}`, className: primaryBalanceClassName,
value: `${value || initValueToRender} ${primaryCurrency}`,
placeholder: `${0} ${primaryCurrency}`, placeholder: `${0} ${primaryCurrency}`,
readOnly, readOnly,
onChange: (event) => { onChange: (event) => {
let newValue = event.target.value.split(' ')[0] let newValue = event.target.value.split(' ')[0]
if (newValue === '') { if (newValue === '') {
handleChange('0') this.setState({ value: '0' })
} }
else if (newValue.match(/^0[1-9]$/)) { 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)) { else if (newValue && !isValidInput(newValue)) {
event.preventDefault() event.preventDefault()
} }
else { else {
handleChange(newValue) this.setState({ value: newValue })
} }
}, },
onKeyUp: event => resetCaretIfPastEnd(value, event), onBlur: event => this.handleChangeInHexWei(event.target.value.split(' ')[0]),
onClick: event => resetCaretIfPastEnd(value, event), 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}`),
]) ])

View File

@ -14,7 +14,7 @@ function FromDropdown () {
FromDropdown.prototype.getListItemIcon = function (currentAccount, selectedAccount) { FromDropdown.prototype.getListItemIcon = function (currentAccount, selectedAccount) {
const listItemIcon = h(`i.fa.fa-check.fa-lg`, { style: { color: '#02c9b1' } }) 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 ? listItemIcon
: null : null
} }

View File

@ -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'),
])
])
}

View File

@ -73,7 +73,7 @@ GasTooltip.prototype.render = function () {
step: 1, step: 1,
min: 0, min: 0,
placeholder: '0', placeholder: '0',
initValue: gasPrice, value: gasPrice,
onChange: (newPrice) => this.updateGasPrice(newPrice), onChange: (newPrice) => this.updateGasPrice(newPrice),
}), }),
h('div.gas-tooltip-input-label', { h('div.gas-tooltip-input-label', {
@ -89,7 +89,7 @@ GasTooltip.prototype.render = function () {
step: 1, step: 1,
min: 0, min: 0,
placeholder: '0', placeholder: '0',
initValue: gasLimit, value: gasLimit,
onChange: (newLimit) => this.updateGasLimit(newLimit), onChange: (newLimit) => this.updateGasLimit(newLimit),
}), }),
]), ]),

View File

@ -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')
},
}),
])
}

View File

@ -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)),
}
}

View File

@ -11,7 +11,7 @@ function ToAutoComplete () {
} }
ToAutoComplete.prototype.render = function () { ToAutoComplete.prototype.render = function () {
const { to, identities, onChange } = this.props const { to, accounts, onChange } = this.props
return h('div.send-v2__to-autocomplete', [ return h('div.send-v2__to-autocomplete', [
@ -32,7 +32,7 @@ ToAutoComplete.prototype.render = function () {
h('datalist#addresses', [ h('datalist#addresses', [
// Corresponds to the addresses owned. // Corresponds to the addresses owned.
...Object.entries(identities).map(([key, { address, name }]) => { ...Object.entries(accounts).map(([key, { address, name }]) => {
return h('option', { return h('option', {
value: address, value: address,
label: name, label: name,

View File

@ -128,7 +128,8 @@ const conversionUtil = (value, {
value: value || '0', 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); const value = (new BigNumber(a)).add(b);
return converter({ return converter({
value, 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 = ( const conversionGreaterThan = (
{ value, fromNumericBase }, { value, fromNumericBase },
{ value: compareToValue, fromNumericBase: compareToBase }, { value: compareToValue, fromNumericBase: compareToBase },
@ -152,5 +163,6 @@ const conversionGreaterThan = (
module.exports = { module.exports = {
conversionUtil, conversionUtil,
addCurrencies, addCurrencies,
multiplyCurrencies,
conversionGreaterThan, conversionGreaterThan,
} }

View File

@ -24,6 +24,16 @@
position: relative; position: relative;
} }
&__account-balances {
height: auto;
border: none;
background-color: transparent;
color: #9b9b9b;
margin-left: 34px;
margin-top: 4px;
position: relative;
}
&__account-name { &__account-name {
font-size: 16px; font-size: 16px;
margin-left: 8px; margin-left: 8px;
@ -34,13 +44,22 @@
right: 12px; right: 12px;
top: 1px; top: 1px;
} }
&__account-primary-balance,
&__account-secondary-balance {
font-family: Roboto;
line-height: 16px;
font-size: 12px;
font-weight: 300;
}
&__account-primary-balance { &__account-primary-balance {
margin-left: 34px; color: $scorpion;
margin-top: 4px; border: none;
outline: 0 !important;
} }
&__account-secondary-balance { &__account-secondary-balance {
margin-left: 34px;
color: $dusty-gray; color: $dusty-gray;
} }
} }

View File

@ -15,10 +15,6 @@
display: flex; display: flex;
} }
&__input-wrapper {
margin-top: -1px;
}
&__input { &__input {
color: $scorpion; color: $scorpion;
font-family: Roboto; font-family: Roboto;

View File

@ -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;
}
}

View File

@ -35,3 +35,6 @@
@import './account-menu.scss'; @import './account-menu.scss';
@import './menu.scss'; @import './menu.scss';
@import './gas-slider.scss';

View File

@ -264,7 +264,7 @@
.gas-tooltip-input-arrows { .gas-tooltip-input-arrows {
position: absolute; position: absolute;
top: 0; top: 0;
left: 178px; right: 4px;
width: 17px; width: 17px;
height: 28px; height: 28px;
border: 1px solid #dadada; 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%; border-radius: 50%;
width: 48px; width: 48px;
height: 48px; height: 48px;
@ -428,11 +437,6 @@
z-index: 25; z-index: 25;
padding: 4px; padding: 4px;
background-color: $white; background-color: $white;
@media screen and (max-width: $break-small) {
position: relative;
top: 0;
}
} }
&__send-arrow-icon { &__send-arrow-icon {
@ -472,7 +476,7 @@
position: absolute; position: absolute;
transform: rotate(45deg); transform: rotate(45deg);
left: 178px; left: 178px;
top: 71px; top: 65px;
} }
&__title { &__title {
@ -512,7 +516,9 @@
font-family: Roboto; font-family: Roboto;
font-size: 16px; font-size: 16px;
line-height: 22px; line-height: 22px;
margin-top: 16px; display: flex;
flex-flow: column;
justify-content: center;
} }
&__from-dropdown { &__from-dropdown {
@ -550,7 +556,7 @@
} }
} }
&__to-autocomplete { &__to-autocomplete, &__memo-text-area {
&__input { &__input {
height: 54px; height: 54px;
width: 240px; 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 { &__footer {
height: 92px; height: 92px;
width: 100%; width: 100%;
@ -573,8 +605,7 @@
justify-content: space-evenly; justify-content: space-evenly;
align-items: center; align-items: center;
border-top: 1px solid $alto; border-top: 1px solid $alto;
position: absolute; margin-top: 29px;
bottom: 0;
} }
&__next-btn, &__next-btn,
@ -607,4 +638,155 @@
color: $dusty-gray; color: $dusty-gray;
border-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;
}
}
} }

View File

@ -5,14 +5,16 @@ const selectors = {
getSelectedIdentity, getSelectedIdentity,
getSelectedAccount, getSelectedAccount,
getSelectedToken, getSelectedToken,
getSelectedTokenExchangeRate,
conversionRateSelector, conversionRateSelector,
transactionsSelector, transactionsSelector,
accountsWithSendEtherInfoSelector,
getCurrentAccountWithSendEtherInfo,
} }
module.exports = selectors module.exports = selectors
function getSelectedAddress (state) { function getSelectedAddress (state) {
// TODO: accounts is not defined. Is it needed?
const selectedAddress = state.metamask.selectedAddress || Object.keys(state.metamask.accounts)[0] const selectedAddress = state.metamask.selectedAddress || Object.keys(state.metamask.accounts)[0]
return selectedAddress return selectedAddress
@ -40,10 +42,41 @@ function getSelectedToken (state) {
return selectedToken || null 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) { function conversionRateSelector (state) {
return state.metamask.conversionRate 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) { function transactionsSelector (state) {
const { network, selectedTokenAddress } = state.metamask const { network, selectedTokenAddress } = state.metamask
const unapprovedMsgs = valuesFor(state.metamask.unapprovedMsgs) const unapprovedMsgs = valuesFor(state.metamask.unapprovedMsgs)

View File

@ -2,61 +2,135 @@ const { inherits } = require('util')
const PersistentForm = require('../lib/persistent-form') const PersistentForm = require('../lib/persistent-form')
const h = require('react-hyperscript') const h = require('react-hyperscript')
const connect = require('react-redux').connect const connect = require('react-redux').connect
const Identicon = require('./components/identicon')
const FromDropdown = require('./components/send/from-dropdown') const FromDropdown = require('./components/send/from-dropdown')
const ToAutoComplete = require('./components/send/to-autocomplete') const ToAutoComplete = require('./components/send/to-autocomplete')
const CurrencyDisplay = require('./components/send/currency-display') 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) const { showModal } = require('./actions')
function mapStateToProps (state) { module.exports = SendTransactionScreen
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
}
}
inherits(SendTransactionScreen, PersistentForm) inherits(SendTransactionScreen, PersistentForm)
function SendTransactionScreen () { function SendTransactionScreen () {
PersistentForm.call(this) PersistentForm.call(this)
this.state = { this.state = {
newTx: {
from: '', from: '',
to: '', to: '',
gasPrice: null, gasPrice: null,
gas: '0.001', gasLimit: null,
amount: '10', amount: '0x0',
txData: null, txData: null,
memo: '', memo: '',
},
dropdownOpen: false, 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 () { SendTransactionScreen.prototype.render = function () {
const { accounts, conversionRate } = this.props const {
const { dropdownOpen, newTx } = this.state accounts,
const { to, amount, gas } = newTx conversionRate,
tokenToUSDRate,
selectedToken,
showCustomizeGasModal,
selectedAccount,
primaryCurrency = 'ETH',
} = this.props
const {
dropdownOpen,
to,
amount,
gasLimit,
gasPrice,
memo,
} = this.state
const amountConversionRate = selectedToken ? tokenToUSDRate : conversionRate
return ( return (
h('div.send-v2__container', [ h('div.send-v2__container', [
h('div.send-v2__header', {}, [ 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('div.send-v2__arrow-background', [
h('i.fa.fa-lg.fa-arrow-circle-right.send-v2__send-arrow-icon'), 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'), this.renderTitle(),
h('div.send-v2__copy', 'Only send ETH to an Ethereum address.'), this.renderCopy(),
h('div.send-v2__copy', 'Sending to a different crytpocurrency that is not Ethereum may result in permanent loss.'),
h('div.send-v2__form', {}, [ h('div.send-v2__form', {}, [
@ -81,10 +153,11 @@ SendTransactionScreen.prototype.render = function () {
h(FromDropdown, { h(FromDropdown, {
dropdownOpen, dropdownOpen,
accounts, accounts,
selectedAccount: accounts[0], selectedAccount,
setFromField: () => console.log('Set From Field'), setFromField: () => console.log('Set From Field'),
openDropdown: () => this.setState({ dropdownOpen: true }), openDropdown: () => this.setState({ dropdownOpen: true }),
closeDropdown: () => this.setState({ dropdownOpen: false }), closeDropdown: () => this.setState({ dropdownOpen: false }),
conversionRate,
}), }),
]), ]),
@ -95,13 +168,11 @@ SendTransactionScreen.prototype.render = function () {
h(ToAutoComplete, { h(ToAutoComplete, {
to, to,
identities: accounts.map(({ identity }) => identity), accounts,
onChange: (event) => { onChange: (event) => {
this.setState({ this.setState({
newTx: { ...this.state,
...this.state.newTx,
to: event.target.value, to: event.target.value,
},
}) })
}, },
}), }),
@ -113,17 +184,15 @@ SendTransactionScreen.prototype.render = function () {
h('div.send-v2__form-label', 'Amount:'), h('div.send-v2__form-label', 'Amount:'),
h(CurrencyDisplay, { h(CurrencyDisplay, {
primaryCurrency: 'ETH', primaryCurrency,
convertedCurrency: 'USD', convertedCurrency: 'USD',
value: amount, value: amount,
conversionRate, conversionRate: amountConversionRate,
convertedPrefix: '$', convertedPrefix: '$',
handleChange: (value) => { handleChange: (value) => {
this.setState({ this.setState({
newTx: { ...this.state,
...this.state.newTx,
amount: value, amount: value,
},
}) })
} }
}), }),
@ -134,13 +203,33 @@ SendTransactionScreen.prototype.render = function () {
h('div.send-v2__form-label', 'Gas fee:'), h('div.send-v2__form-label', 'Gas fee:'),
h(CurrencyDisplay, { h(GasFeeDisplay, {
primaryCurrency: 'ETH', gasLimit,
convertedCurrency: 'USD', gasPrice,
value: gas,
conversionRate, conversionRate,
convertedPrefix: '$', onClick: showCustomizeGasModal,
readOnly: true, }),
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,
})
},
}), }),
]), ]),