Adds toggle for primary currency (#5421)

* Add UnitInput component

* Add CurrencyInput component

* Add UserPreferencedCurrencyInput component

* Add UserPreferencedCurrencyDisplay component

* Add updatePreferences action

* Add styles for CurrencyInput, CurrencyDisplay, and UnitInput

* Update SettingsTab page with Primary Currency toggle

* Refactor currency displays and inputs to use UserPreferenced displays and inputs

* Add TokenInput component

* Add UserPreferencedTokenInput component

* Use TokenInput in the send screen

* Fix unit tests

* Fix e2e and integration tests

* Remove send/CurrencyDisplay component

* Replace diamond unicode character with Eth logo. Fix typos
This commit is contained in:
Alexander Tseung 2018-10-17 07:03:29 +08:00 committed by GitHub
parent 8bccb88132
commit badebe017f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
85 changed files with 2466 additions and 653 deletions

View File

@ -361,6 +361,9 @@
"enterPasswordContinue": {
"message": "Enter password to continue"
},
"eth": {
"message": "ETH"
},
"etherscanView": {
"message": "View account on Etherscan"
},
@ -380,7 +383,7 @@
"message": "Failed"
},
"fiat": {
"message": "FIAT",
"message": "Fiat",
"description": "Exchange type"
},
"fileImportFail": {
@ -790,6 +793,12 @@
"prev": {
"message": "Prev"
},
"primaryCurrencySetting": {
"message": "Primary Currency"
},
"primaryCurrencySettingDescription": {
"message": "Select ETH to prioritize displaying values in ETH. Select Fiat to prioritize displaying values in your selected currency."
},
"privacyMsg": {
"message": "Privacy Policy"
},

14
app/images/eth.svg Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve">
<style type="text/css">
.st0{fill:#38393A;}
</style>
<title>deposit-eth</title>
<desc>Created with Sketch.</desc>
<g id="deposit-eth" transform="translate(0.000000, 14.000000)">
<path id="Shape" class="st0" d="M19.9,16L7.5,8.7L19.9,26L32.3,8.7L19.9,16L19.9,16z M20.1-14L7.7,6.4l12.4,7.3l12.4-7.2L20.1-14z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 670 B

View File

@ -38,6 +38,9 @@ class PreferencesController {
lostIdentities: {},
seedWords: null,
forgottenPassword: false,
preferences: {
useETHAsPrimaryCurrency: true,
},
}, opts.initState)
this.diagnostics = opts.diagnostics
@ -463,6 +466,33 @@ class PreferencesController {
getFeatureFlags () {
return this.store.getState().featureFlags
}
/**
* Updates the `preferences` property, which is an object. These are user-controlled features
* found in the settings page.
* @param {string} preference The preference to enable or disable.
* @param {boolean} value Indicates whether or not the preference should be enabled or disabled.
* @returns {Promise<object>} Promises a new object; the updated preferences object.
*/
setPreference (preference, value) {
const currentPreferences = this.getPreferences()
const updatedPreferences = {
...currentPreferences,
[preference]: value,
}
this.store.updateState({ preferences: updatedPreferences })
return Promise.resolve(updatedPreferences)
}
/**
* A getter for the `preferences` property
* @returns {object} A key-boolean map of user-selected preferences.
*/
getPreferences () {
return this.store.getState().preferences
}
//
// PRIVATE METHODS
//

View File

@ -387,6 +387,7 @@ module.exports = class MetamaskController extends EventEmitter {
setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController),
setAccountLabel: nodeify(preferencesController.setAccountLabel, preferencesController),
setFeatureFlag: nodeify(preferencesController.setFeatureFlag, preferencesController),
setPreference: nodeify(preferencesController.setPreference, preferencesController),
// BlacklistController
whitelistPhishingDomain: this.whitelistPhishingDomain.bind(this),

View File

@ -107,7 +107,10 @@
"maxModeOn": false,
"editingTransactionId": null
},
"currentLocale": "en"
"currentLocale": "en",
"preferences": {
"useETHAsPrimaryCurrency": true
}
},
"appState": {
"menuOpen": false,

View File

@ -150,7 +150,10 @@
"maxModeOn": false,
"editingTransactionId": null
},
"currentLocale": "en"
"currentLocale": "en",
"preferences": {
"useETHAsPrimaryCurrency": true
}
},
"appState": {
"menuOpen": false,

View File

@ -108,7 +108,10 @@
"maxModeOn": false,
"editingTransactionId": null
},
"currentLocale": "en"
"currentLocale": "en",
"preferences": {
"useETHAsPrimaryCurrency": true
}
},
"appState": {
"menuOpen": false,

View File

@ -37,7 +37,10 @@
"shapeShiftTxList": [],
"lostAccounts": [],
"tokens": [],
"currentLocale": "en"
"currentLocale": "en",
"preferences": {
"useETHAsPrimaryCurrency": true
}
},
"appState": {
"menuOpen": false,

View File

@ -109,7 +109,10 @@
"maxModeOn": false,
"editingTransactionId": null
},
"currentLocale": "en"
"currentLocale": "en",
"preferences": {
"useETHAsPrimaryCurrency": true
}
},
"appState": {
"menuOpen": false,

View File

@ -102,7 +102,10 @@
"shapeShiftTxList": [{"depositAddress":"34vJ3AfmNcLiziA4VFgEVcQTwxVLD1qkke","depositType":"BTC","key":"shapeshift","response":{"status":"no_deposits","address":"34vJ3AfmNcLiziA4VFgEVcQTwxVLD1qkke"},"time":1522377459106}],
"lostAccounts": [],
"send": {},
"currentLocale": "en"
"currentLocale": "en",
"preferences": {
"useETHAsPrimaryCurrency": true
}
},
"appState": {
"menuOpen": false,

View File

@ -286,7 +286,7 @@ describe('Using MetaMask with an existing account', function () {
await delay(regularDelayMs)
const inputAddress = await findElement(driver, By.css('input[placeholder="Recipient Address"]'))
const inputAmount = await findElement(driver, By.css('.currency-display__input'))
const inputAmount = await findElement(driver, By.css('.unit-input__input'))
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
await inputAmount.sendKeys('1')

View File

@ -383,7 +383,7 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
const inputAddress = await findElement(driver, By.css('input[placeholder="Recipient Address"]'))
const inputAmount = await findElement(driver, By.css('.currency-display__input'))
const inputAmount = await findElement(driver, By.css('.unit-input__input'))
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
await inputAmount.sendKeys('1')
@ -702,7 +702,7 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
const inputAddress = await findElement(driver, By.css('input[placeholder="Recipient Address"]'))
const inputAmount = await findElement(driver, By.css('.currency-display__input'))
const inputAmount = await findElement(driver, By.css('.unit-input__input'))
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
await inputAmount.sendKeys('50')
@ -834,8 +834,8 @@ describe('MetaMask', function () {
await save.click()
await driver.wait(until.stalenessOf(gasModal))
const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__eth'))
assert.equal(await gasFeeInputs[0].getText(), '0.0006')
const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__primary'))
assert.equal(await gasFeeInputs[0].getText(), '0.0006')
})
it('submits the transaction', async function () {
@ -957,8 +957,8 @@ describe('MetaMask', function () {
await save.click()
await driver.wait(until.stalenessOf(gasModal))
const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__eth'))
assert.equal(await gasFeeInputs[0].getText(), '0.0006')
const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__primary'))
assert.equal(await gasFeeInputs[0].getText(), '0.0006')
})
it('submits the transaction', async function () {

View File

@ -40,7 +40,7 @@ async function customizeGas (assert, price, limit, ethFee, usdFee) {
const sendGasField = await queryAsync($, '.send-v2__gas-fee-display')
assert.equal(
(await findAsync(sendGasField, '.currency-display__input-wrapper > input')).val(),
(await findAsync(sendGasField, '.currency-display-component'))[0].textContent,
ethFee,
'send gas field should show customized gas total'
)
@ -97,9 +97,9 @@ async function runSendFlowTest (assert, done) {
assert.equal(sendToAccountAddress, '0x2f8D4a878cFA04A6E60D46362f5644DeAb66572D', 'send to dropdown selects the correct address')
const sendAmountField = await queryAsync($, '.send-v2__form-row:eq(2)')
sendAmountField.find('.currency-display')[0].click()
sendAmountField.find('.unit-input')[0].click()
const sendAmountFieldInput = await findAsync(sendAmountField, '.currency-display__input')
const sendAmountFieldInput = await findAsync(sendAmountField, '.unit-input__input')
sendAmountFieldInput.val('5.1')
reactTriggerChange(sendAmountField.find('input')[0])
@ -112,9 +112,9 @@ async function runSendFlowTest (assert, done) {
errorMessage = $('.send-v2__error')
assert.equal(errorMessage.length, 0, 'send should stop rendering amount error message after amount is corrected')
await customizeGas(assert, 0, 21000, '0', '$0.00 USD')
await customizeGas(assert, 1, 21000, '0.000021', '$0.03 USD')
await customizeGas(assert, 500, 60000, '0.03', '$36.03 USD')
await customizeGas(assert, 0, 21000, '0 ETH', '$0.00 USD')
await customizeGas(assert, 1, 21000, '0.000021 ETH', '$0.03 USD')
await customizeGas(assert, 500, 60000, '0.03 ETH', '$36.03 USD')
const sendButton = await queryAsync($, 'button.btn-primary.btn--large.page-container__footer-button')
assert.equal(sendButton[0].textContent, 'Next', 'next button rendered')
@ -130,11 +130,11 @@ async function runSendFlowTest (assert, done) {
const confirmToName = (await queryAsync($, '.sender-to-recipient__name')).last()
assert.equal(confirmToName[0].textContent, 'Send Account 3', 'confirm screen should show correct to name')
const confirmScreenRowFiats = await queryAsync($, '.confirm-detail-row__fiat')
const confirmScreenRowFiats = await queryAsync($, '.confirm-detail-row__secondary')
const confirmScreenGas = confirmScreenRowFiats[0]
assert.equal(confirmScreenGas.textContent, '$3.60', 'confirm screen should show correct gas')
const confirmScreenTotal = confirmScreenRowFiats[1]
assert.equal(confirmScreenTotal.textContent, '$2,405.36', 'confirm screen should show correct total')
assert.equal(confirmScreenTotal.textContent, '$2,405.37', 'confirm screen should show correct total')
const confirmScreenBackButton = await queryAsync($, '.confirm-page-container-header__back-button')
confirmScreenBackButton[0].click()
@ -150,9 +150,9 @@ async function runSendFlowTest (assert, done) {
sendToFieldInputInEdit.val('0xd85a4b6a394794842887b8284293d69163007bbb')
const sendAmountFieldInEdit = await queryAsync($, '.send-v2__form-row:eq(2)')
sendAmountFieldInEdit.find('.currency-display')[0].click()
sendAmountFieldInEdit.find('.unit-input')[0].click()
const sendAmountFieldInputInEdit = sendAmountFieldInEdit.find('.currency-display__input')
const sendAmountFieldInputInEdit = sendAmountFieldInEdit.find('.unit-input__input')
sendAmountFieldInputInEdit.val('1.0')
reactTriggerChange(sendAmountFieldInputInEdit[0])

View File

@ -1,44 +0,0 @@
const assert = require('assert')
const h = require('react-hyperscript')
const { createMockStore } = require('redux-test-utils')
const { shallowWithStore } = require('../../lib/render-helpers')
const BalanceComponent = require('../../../ui/app/components/balance-component')
const mockState = {
metamask: {
accounts: { abc: {} },
network: 1,
selectedAddress: 'abc',
},
}
describe('BalanceComponent', function () {
let balanceComponent
let store
let component
beforeEach(function () {
store = createMockStore(mockState)
component = shallowWithStore(h(BalanceComponent), store)
balanceComponent = component.dive()
})
it('shows token balance and convert to fiat value based on conversion rate', function () {
const formattedBalance = '1.23 ETH'
const tokenBalance = balanceComponent.instance().getTokenBalance(formattedBalance, false)
const fiatDisplayNumber = balanceComponent.instance().getFiatDisplayNumber(formattedBalance, 2)
assert.equal('1.23 ETH', tokenBalance)
assert.equal(2.46, fiatDisplayNumber)
})
it('shows only the token balance when conversion rate is not available', function () {
const formattedBalance = '1.23 ETH'
const tokenBalance = balanceComponent.instance().getTokenBalance(formattedBalance, false)
const fiatDisplayNumber = balanceComponent.instance().getFiatDisplayNumber(formattedBalance, 0)
assert.equal('1.23 ETH', tokenBalance)
assert.equal('N/A', fiatDisplayNumber)
})
})

View File

@ -305,6 +305,12 @@ var actions = {
updateFeatureFlags,
UPDATE_FEATURE_FLAGS: 'UPDATE_FEATURE_FLAGS',
// Preferences
setPreference,
updatePreferences,
UPDATE_PREFERENCES: 'UPDATE_PREFERENCES',
setUseETHAsPrimaryCurrencyPreference,
setMouseUserState,
SET_MOUSE_USER_STATE: 'SET_MOUSE_USER_STATE',
@ -2298,6 +2304,36 @@ function updateFeatureFlags (updatedFeatureFlags) {
}
}
function setPreference (preference, value) {
return dispatch => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
background.setPreference(preference, value, (err, updatedPreferences) => {
dispatch(actions.hideLoadingIndication())
if (err) {
dispatch(actions.displayWarning(err.message))
return reject(err)
}
dispatch(actions.updatePreferences(updatedPreferences))
resolve(updatedPreferences)
})
})
}
}
function updatePreferences (value) {
return {
type: actions.UPDATE_PREFERENCES,
value,
}
}
function setUseETHAsPrimaryCurrencyPreference (value) {
return setPreference('useETHAsPrimaryCurrency', value)
}
function setNetworkNonce (networkNonce) {
return {
type: actions.SET_NETWORK_NONCE,

View File

@ -8,11 +8,11 @@ const h = require('react-hyperscript')
const actions = require('../../actions')
const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu')
const Identicon = require('../identicon')
const { formatBalance } = require('../../util')
const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums')
const { getEnvironmentType } = require('../../../../app/scripts/lib/util')
const Tooltip = require('../tooltip')
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
import { PRIMARY } from '../../constants/common'
const {
SETTINGS_ROUTE,
@ -163,7 +163,6 @@ AccountMenu.prototype.renderAccounts = function () {
const isSelected = identity.address === selectedAddress
const balanceValue = accounts[address] ? accounts[address].balance : ''
const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...'
const simpleAddress = identity.address.substring(2).toLowerCase()
const keyring = keyrings.find((kr) => {
@ -189,7 +188,11 @@ AccountMenu.prototype.renderAccounts = function () {
h('div.account-menu__account-info', [
h('div.account-menu__name', identity.name || ''),
h('div.account-menu__balance', formattedBalance),
h(UserPreferencedCurrencyDisplay, {
className: 'account-menu__balance',
value: balanceValue,
type: PRIMARY,
}),
]),
this.renderKeyringType(keyring),

View File

@ -4,10 +4,11 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits
const TokenBalance = require('./token-balance')
const Identicon = require('./identicon')
import CurrencyDisplay from './currency-display'
import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display'
import { PRIMARY, SECONDARY } from '../constants/common'
const { getAssetImages, conversionRateSelector, getCurrentCurrency} = require('../selectors')
const { formatBalance, generateBalanceObject } = require('../util')
const { formatBalance } = require('../util')
module.exports = connect(mapStateToProps)(BalanceComponent)
@ -65,7 +66,7 @@ BalanceComponent.prototype.renderTokenBalance = function () {
BalanceComponent.prototype.renderBalance = function () {
const props = this.props
const { shorten, account } = props
const { account } = props
const balanceValue = account && account.balance
const needsParse = 'needsParse' in props ? props.needsParse : true
const formattedBalance = balanceValue ? formatBalance(balanceValue, 6, needsParse) : '...'
@ -80,25 +81,20 @@ BalanceComponent.prototype.renderBalance = function () {
}
return h('div.flex-column.balance-display', {}, [
h('div.token-amount', {
style: {},
}, this.getTokenBalance(formattedBalance, shorten)),
showFiat && h(CurrencyDisplay, {
h('div.token-amount', {}, h(UserPreferencedCurrencyDisplay, {
value: balanceValue,
type: PRIMARY,
ethNumberOfDecimals: 3,
})),
showFiat && h(UserPreferencedCurrencyDisplay, {
value: balanceValue,
type: SECONDARY,
ethNumberOfDecimals: 3,
}),
])
}
BalanceComponent.prototype.getTokenBalance = function (formattedBalance, shorten) {
const balanceObj = generateBalanceObject(formattedBalance, shorten ? 1 : 3)
const balanceValue = shorten ? balanceObj.shortBalance : balanceObj.balance
const label = balanceObj.label
return `${balanceValue} ${label}`
}
BalanceComponent.prototype.getFiatDisplayNumber = function (formattedBalance, conversionRate) {
if (formattedBalance === 'None') return formattedBalance
if (conversionRate === 0) return 'N/A'

View File

@ -1,16 +1,19 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display'
import { PRIMARY, SECONDARY } from '../../../constants/common'
const ConfirmDetailRow = props => {
const {
label,
fiatText,
ethText,
primaryText,
secondaryText,
onHeaderClick,
fiatTextColor,
primaryValueTextColor,
headerText,
headerTextClassName,
value,
} = props
return (
@ -25,28 +28,57 @@ const ConfirmDetailRow = props => {
>
{ headerText }
</div>
<div
className="confirm-detail-row__fiat"
style={{ color: fiatTextColor }}
>
{ fiatText }
</div>
<div className="confirm-detail-row__eth">
{ ethText }
</div>
{
primaryText
? (
<div
className="confirm-detail-row__primary"
style={{ color: primaryValueTextColor }}
>
{ primaryText }
</div>
) : (
<UserPreferencedCurrencyDisplay
className="confirm-detail-row__primary"
type={PRIMARY}
value={value}
showEthLogo
ethLogoHeight="18"
style={{ color: primaryValueTextColor }}
hideLabel
/>
)
}
{
secondaryText
? (
<div className="confirm-detail-row__secondary">
{ secondaryText }
</div>
) : (
<UserPreferencedCurrencyDisplay
className="confirm-detail-row__secondary"
type={SECONDARY}
value={value}
showEthLogo
hideLabel
/>
)
}
</div>
</div>
)
}
ConfirmDetailRow.propTypes = {
label: PropTypes.string,
fiatText: PropTypes.string,
ethText: PropTypes.string,
fiatTextColor: PropTypes.string,
onHeaderClick: PropTypes.func,
headerText: PropTypes.string,
headerTextClassName: PropTypes.string,
label: PropTypes.string,
onHeaderClick: PropTypes.func,
primaryValueTextColor: PropTypes.string,
primaryText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
secondaryText: PropTypes.string,
value: PropTypes.string,
}
export default ConfirmDetailRow

View File

@ -18,18 +18,14 @@
min-width: 0;
}
&__fiat {
&__primary {
font-size: 1.5rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
justify-content: flex-end;
}
&__eth {
&__secondary {
color: $oslo-gray;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
justify-content: flex-end;
}
&__header-text {

View File

@ -12,17 +12,19 @@ describe('Confirm Detail Row Component', function () {
let wrapper
beforeEach(() => {
wrapper = shallow(<ConfirmDetailRow
errorType={'mockErrorType'}
label={'mockLabel'}
showError={false}
fiatText = {'mockFiatText'}
ethText = {'mockEthText'}
fiatTextColor= {'mockColor'}
onHeaderClick= {propsMethodSpies.onHeaderClick}
headerText = {'mockHeaderText'}
headerTextClassName = {'mockHeaderClass'}
/>)
wrapper = shallow(
<ConfirmDetailRow
errorType={'mockErrorType'}
label={'mockLabel'}
showError={false}
primaryText = {'mockFiatText'}
secondaryText = {'mockEthText'}
primaryValueTextColor= {'mockColor'}
onHeaderClick= {propsMethodSpies.onHeaderClick}
headerText = {'mockHeaderText'}
headerTextClassName = {'mockHeaderClass'}
/>
)
})
describe('render', () => {
@ -38,16 +40,16 @@ describe('Confirm Detail Row Component', function () {
assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__header-text').childAt(0).text(), 'mockHeaderText')
})
it('should render the fiatText as a child of the confirm-detail-row__fiat', () => {
assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__fiat').childAt(0).text(), 'mockFiatText')
it('should render the primaryText as a child of the confirm-detail-row__primary', () => {
assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__primary').childAt(0).text(), 'mockFiatText')
})
it('should render the ethText as a child of the confirm-detail-row__eth', () => {
assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__eth').childAt(0).text(), 'mockEthText')
it('should render the ethText as a child of the confirm-detail-row__secondary', () => {
assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__secondary').childAt(0).text(), 'mockEthText')
})
it('should set the fiatTextColor on confirm-detail-row__fiat', () => {
assert.equal(wrapper.find('.confirm-detail-row__fiat').props().style.color, 'mockColor')
it('should set the fiatTextColor on confirm-detail-row__primary', () => {
assert.equal(wrapper.find('.confirm-detail-row__primary').props().style.color, 'mockColor')
})
it('should assure the confirm-detail-row__header-text classname is correct', () => {
@ -58,7 +60,5 @@ describe('Confirm Detail Row Component', function () {
wrapper.find('.confirm-detail-row__header-text').props().onClick()
assert.equal(assert.equal(propsMethodSpies.onHeaderClick.callCount, 1))
})
})
})

View File

@ -17,9 +17,10 @@ export default class ConfirmPageContainerContent extends Component {
nonce: PropTypes.string,
assetImage: PropTypes.string,
subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
subtitleComponent: PropTypes.node,
summaryComponent: PropTypes.node,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
titleComponent: PropTypes.func,
titleComponent: PropTypes.node,
warning: PropTypes.string,
}
@ -54,7 +55,9 @@ export default class ConfirmPageContainerContent extends Component {
errorKey,
errorMessage,
title,
titleComponent,
subtitle,
subtitleComponent,
hideSubtitle,
identiconAddress,
nonce,
@ -80,7 +83,9 @@ export default class ConfirmPageContainerContent extends Component {
})}
action={action}
title={title}
titleComponent={titleComponent}
subtitle={subtitle}
subtitleComponent={subtitleComponent}
hideSubtitle={hideSubtitle}
identiconAddress={identiconAddress}
nonce={nonce}

View File

@ -4,7 +4,18 @@ import classnames from 'classnames'
import Identicon from '../../../identicon'
const ConfirmPageContainerSummary = props => {
const { action, title, subtitle, hideSubtitle, className, identiconAddress, nonce, assetImage } = props
const {
action,
title,
titleComponent,
subtitle,
subtitleComponent,
hideSubtitle,
className,
identiconAddress,
nonce,
assetImage,
} = props
return (
<div className={classnames('confirm-page-container-summary', className)}>
@ -32,12 +43,12 @@ const ConfirmPageContainerSummary = props => {
)
}
<div className="confirm-page-container-summary__title-text">
{ title }
{ titleComponent || title }
</div>
</div>
{
hideSubtitle || <div className="confirm-page-container-summary__subtitle">
{ subtitle }
{ subtitleComponent || subtitle }
</div>
}
</div>
@ -47,7 +58,9 @@ const ConfirmPageContainerSummary = props => {
ConfirmPageContainerSummary.propTypes = {
action: PropTypes.string,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
titleComponent: PropTypes.node,
subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
subtitleComponent: PropTypes.node,
hideSubtitle: PropTypes.bool,
className: PropTypes.string,
identiconAddress: PropTypes.string,

View File

@ -16,8 +16,9 @@ export default class ConfirmPageContainer extends Component {
onEdit: PropTypes.func,
showEdit: PropTypes.bool,
subtitle: PropTypes.string,
subtitleComponent: PropTypes.node,
title: PropTypes.string,
titleComponent: PropTypes.func,
titleComponent: PropTypes.node,
// Sender to Recipient
fromAddress: PropTypes.string,
fromName: PropTypes.string,
@ -65,6 +66,7 @@ export default class ConfirmPageContainer extends Component {
title,
titleComponent,
subtitle,
subtitleComponent,
hideSubtitle,
summaryComponent,
detailsComponent,
@ -101,6 +103,7 @@ export default class ConfirmPageContainer extends Component {
title={title}
titleComponent={titleComponent}
subtitle={subtitle}
subtitleComponent={subtitleComponent}
hideSubtitle={hideSubtitle}
summaryComponent={summaryComponent}
detailsComponent={detailsComponent}

View File

@ -1,5 +1,6 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { ETH, GWEI } from '../../constants/common'
export default class CurrencyDisplay extends PureComponent {
@ -7,6 +8,8 @@ export default class CurrencyDisplay extends PureComponent {
className: PropTypes.string,
displayValue: PropTypes.string,
prefix: PropTypes.string,
prefixComponent: PropTypes.node,
style: PropTypes.object,
// Used in container
currency: PropTypes.oneOf([ETH]),
denomination: PropTypes.oneOf([GWEI]),
@ -16,15 +19,17 @@ export default class CurrencyDisplay extends PureComponent {
}
render () {
const { className, displayValue, prefix } = this.props
const { className, displayValue, prefix, prefixComponent, style } = this.props
const text = `${prefix || ''}${displayValue}`
return (
<div
className={className}
className={classnames('currency-display-component', className)}
style={style}
title={text}
>
{ text }
{ prefixComponent}
<span className="currency-display-component__text">{ text }</span>
</div>
)
}

View File

@ -2,10 +2,26 @@ import { connect } from 'react-redux'
import CurrencyDisplay from './currency-display.component'
import { getValueFromWeiHex, formatCurrency } from '../../helpers/confirm-transaction/util'
const mapStateToProps = (state, ownProps) => {
const { value, numberOfDecimals = 2, currency, denomination, hideLabel } = ownProps
const mapStateToProps = state => {
const { metamask: { currentCurrency, conversionRate } } = state
return {
currentCurrency,
conversionRate,
}
}
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { currentCurrency, conversionRate, ...restStateProps } = stateProps
const {
value,
numberOfDecimals = 2,
currency,
denomination,
hideLabel,
...restOwnProps
} = ownProps
const toCurrency = currency || currentCurrency
const convertedValue = getValueFromWeiHex({
value, toCurrency, conversionRate, numberOfDecimals, toDenomination: denomination,
@ -14,8 +30,11 @@ const mapStateToProps = (state, ownProps) => {
const displayValue = hideLabel ? formattedValue : `${formattedValue} ${toCurrency.toUpperCase()}`
return {
...restStateProps,
...dispatchProps,
...restOwnProps,
displayValue,
}
}
export default connect(mapStateToProps)(CurrencyDisplay)
export default connect(mapStateToProps, null, mergeProps)(CurrencyDisplay)

View File

@ -0,0 +1,10 @@
.currency-display-component {
display: flex;
align-items: center;
&__text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}

View File

@ -1,12 +1,13 @@
import assert from 'assert'
import proxyquire from 'proxyquire'
let mapStateToProps
let mapStateToProps, mergeProps
proxyquire('../currency-display.container.js', {
'react-redux': {
connect: ms => {
connect: (ms, md, mp) => {
mapStateToProps = ms
mergeProps = mp
return () => ({})
},
},
@ -22,6 +23,20 @@ describe('CurrencyDisplay container', () => {
},
}
assert.deepEqual(mapStateToProps(mockState), {
conversionRate: 280.45,
currentCurrency: 'usd',
})
})
})
describe('mergeProps()', () => {
it('should return the correct props', () => {
const mockStateProps = {
conversionRate: 280.45,
currentCurrency: 'usd',
}
const tests = [
{
props: {
@ -98,7 +113,7 @@ describe('CurrencyDisplay container', () => {
]
tests.forEach(({ props, result }) => {
assert.deepEqual(mapStateToProps(mockState, props), result)
assert.deepEqual(mergeProps(mockStateProps, {}, { ...props }), result)
})
})
})

View File

@ -0,0 +1,120 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import UnitInput from '../unit-input'
import CurrencyDisplay from '../currency-display'
import { getValueFromWeiHex, getWeiHexFromDecimalValue } from '../../helpers/conversions.util'
import { ETH } from '../../constants/common'
/**
* Component that allows user to enter currency values as a number, and props receive a converted
* hex value in WEI. props.value, used as a default or forced value, should be a hex value, which
* gets converted into a decimal value depending on the currency (ETH or Fiat).
*/
export default class CurrencyInput extends PureComponent {
static propTypes = {
conversionRate: PropTypes.number,
currentCurrency: PropTypes.string,
onChange: PropTypes.func,
onBlur: PropTypes.func,
suffix: PropTypes.string,
useFiat: PropTypes.bool,
value: PropTypes.string,
}
constructor (props) {
super(props)
const { value: hexValue } = props
const decimalValue = hexValue ? this.getDecimalValue(props) : 0
this.state = {
decimalValue,
hexValue,
}
}
componentDidUpdate (prevProps) {
const { value: prevPropsHexValue } = prevProps
const { value: propsHexValue } = this.props
const { hexValue: stateHexValue } = this.state
if (prevPropsHexValue !== propsHexValue && propsHexValue !== stateHexValue) {
const decimalValue = this.getDecimalValue(this.props)
this.setState({ hexValue: propsHexValue, decimalValue })
}
}
getDecimalValue (props) {
const { value: hexValue, useFiat, currentCurrency, conversionRate } = props
const decimalValueString = useFiat
? getValueFromWeiHex({
value: hexValue, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2,
})
: getValueFromWeiHex({
value: hexValue, toCurrency: ETH, numberOfDecimals: 6,
})
return Number(decimalValueString) || 0
}
handleChange = decimalValue => {
const { useFiat, currentCurrency: fromCurrency, conversionRate, onChange } = this.props
const hexValue = useFiat
? getWeiHexFromDecimalValue({
value: decimalValue, fromCurrency, conversionRate, invertConversionRate: true,
})
: getWeiHexFromDecimalValue({
value: decimalValue, fromCurrency: ETH, fromDenomination: ETH, conversionRate,
})
this.setState({ hexValue, decimalValue })
onChange(hexValue)
}
handleBlur = () => {
this.props.onBlur(this.state.hexValue)
}
renderConversionComponent () {
const { useFiat, currentCurrency } = this.props
const { hexValue } = this.state
let currency, numberOfDecimals
if (useFiat) {
// Display ETH
currency = ETH
numberOfDecimals = 6
} else {
// Display Fiat
currency = currentCurrency
numberOfDecimals = 2
}
return (
<CurrencyDisplay
className="currency-input__conversion-component"
currency={currency}
value={hexValue}
numberOfDecimals={numberOfDecimals}
/>
)
}
render () {
const { suffix, ...restProps } = this.props
const { decimalValue } = this.state
return (
<UnitInput
{...restProps}
suffix={suffix}
onChange={this.handleChange}
onBlur={this.handleBlur}
value={decimalValue}
>
{ this.renderConversionComponent() }
</UnitInput>
)
}
}

View File

@ -0,0 +1,27 @@
import { connect } from 'react-redux'
import CurrencyInput from './currency-input.component'
import { ETH } from '../../constants/common'
const mapStateToProps = state => {
const { metamask: { currentCurrency, conversionRate } } = state
return {
currentCurrency,
conversionRate,
}
}
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { currentCurrency } = stateProps
const { useFiat } = ownProps
const suffix = useFiat ? currentCurrency.toUpperCase() : ETH
return {
...stateProps,
...dispatchProps,
...ownProps,
suffix,
}
}
export default connect(mapStateToProps, null, mergeProps)(CurrencyInput)

View File

@ -0,0 +1 @@
export { default } from './currency-input.container'

View File

@ -0,0 +1,7 @@
.currency-input {
&__conversion-component {
font-size: 12px;
line-height: 12px;
padding-left: 1px;
}
}

View File

@ -0,0 +1,239 @@
import React from 'react'
import assert from 'assert'
import { shallow, mount } from 'enzyme'
import sinon from 'sinon'
import { Provider } from 'react-redux'
import configureMockStore from 'redux-mock-store'
import CurrencyInput from '../currency-input.component'
import UnitInput from '../../unit-input'
import CurrencyDisplay from '../../currency-display'
describe('CurrencyInput Component', () => {
describe('rendering', () => {
it('should render properly without a suffix', () => {
const wrapper = shallow(
<CurrencyInput />
)
assert.ok(wrapper)
assert.equal(wrapper.find(UnitInput).length, 1)
})
it('should render properly with a suffix', () => {
const mockStore = {
metamask: {
currentCurrency: 'usd',
conversionRate: 231.06,
},
}
const store = configureMockStore()(mockStore)
const wrapper = mount(
<Provider store={store}>
<CurrencyInput
suffix="ETH"
/>
</Provider>
)
assert.ok(wrapper)
assert.equal(wrapper.find('.unit-input__suffix').length, 1)
assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH')
assert.equal(wrapper.find(CurrencyDisplay).length, 1)
})
it('should render properly with an ETH value', () => {
const mockStore = {
metamask: {
currentCurrency: 'usd',
conversionRate: 231.06,
},
}
const store = configureMockStore()(mockStore)
const wrapper = mount(
<Provider store={store}>
<CurrencyInput
value="de0b6b3a7640000"
suffix="ETH"
currentCurrency="usd"
conversionRate={231.06}
/>
</Provider>
)
assert.ok(wrapper)
const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance()
assert.equal(currencyInputInstance.state.decimalValue, 1)
assert.equal(currencyInputInstance.state.hexValue, 'de0b6b3a7640000')
assert.equal(wrapper.find('.unit-input__suffix').length, 1)
assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH')
assert.equal(wrapper.find('.unit-input__input').props().value, '1')
assert.equal(wrapper.find('.currency-display-component').text(), '$231.06 USD')
})
it('should render properly with a fiat value', () => {
const mockStore = {
metamask: {
currentCurrency: 'usd',
conversionRate: 231.06,
},
}
const store = configureMockStore()(mockStore)
const wrapper = mount(
<Provider store={store}>
<CurrencyInput
value="f602f2234d0ea"
suffix="USD"
useFiat
currentCurrency="usd"
conversionRate={231.06}
/>
</Provider>
)
assert.ok(wrapper)
const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance()
assert.equal(currencyInputInstance.state.decimalValue, 1)
assert.equal(currencyInputInstance.state.hexValue, 'f602f2234d0ea')
assert.equal(wrapper.find('.unit-input__suffix').length, 1)
assert.equal(wrapper.find('.unit-input__suffix').text(), 'USD')
assert.equal(wrapper.find('.unit-input__input').props().value, '1')
assert.equal(wrapper.find('.currency-display-component').text(), '0.004328 ETH')
})
})
describe('handling actions', () => {
const handleChangeSpy = sinon.spy()
const handleBlurSpy = sinon.spy()
afterEach(() => {
handleChangeSpy.resetHistory()
handleBlurSpy.resetHistory()
})
it('should call onChange and onBlur on input changes with the hex value for ETH', () => {
const mockStore = {
metamask: {
currentCurrency: 'usd',
conversionRate: 231.06,
},
}
const store = configureMockStore()(mockStore)
const wrapper = mount(
<Provider store={store}>
<CurrencyInput
onChange={handleChangeSpy}
onBlur={handleBlurSpy}
suffix="ETH"
currentCurrency="usd"
conversionRate={231.06}
/>
</Provider>
)
assert.ok(wrapper)
assert.equal(handleChangeSpy.callCount, 0)
assert.equal(handleBlurSpy.callCount, 0)
const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance()
assert.equal(currencyInputInstance.state.decimalValue, 0)
assert.equal(currencyInputInstance.state.hexValue, undefined)
assert.equal(wrapper.find('.currency-display-component').text(), '$0.00 USD')
const input = wrapper.find('input')
assert.equal(input.props().value, 0)
input.simulate('change', { target: { value: 1 } })
assert.equal(handleChangeSpy.callCount, 1)
assert.ok(handleChangeSpy.calledWith('de0b6b3a7640000'))
assert.equal(wrapper.find('.currency-display-component').text(), '$231.06 USD')
assert.equal(currencyInputInstance.state.decimalValue, 1)
assert.equal(currencyInputInstance.state.hexValue, 'de0b6b3a7640000')
assert.equal(handleBlurSpy.callCount, 0)
input.simulate('blur')
assert.equal(handleBlurSpy.callCount, 1)
assert.ok(handleBlurSpy.calledWith('de0b6b3a7640000'))
})
it('should call onChange and onBlur on input changes with the hex value for fiat', () => {
const mockStore = {
metamask: {
currentCurrency: 'usd',
conversionRate: 231.06,
},
}
const store = configureMockStore()(mockStore)
const wrapper = mount(
<Provider store={store}>
<CurrencyInput
onChange={handleChangeSpy}
onBlur={handleBlurSpy}
suffix="USD"
currentCurrency="usd"
conversionRate={231.06}
useFiat
/>
</Provider>
)
assert.ok(wrapper)
assert.equal(handleChangeSpy.callCount, 0)
assert.equal(handleBlurSpy.callCount, 0)
const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance()
assert.equal(currencyInputInstance.state.decimalValue, 0)
assert.equal(currencyInputInstance.state.hexValue, undefined)
assert.equal(wrapper.find('.currency-display-component').text(), '0 ETH')
const input = wrapper.find('input')
assert.equal(input.props().value, 0)
input.simulate('change', { target: { value: 1 } })
assert.equal(handleChangeSpy.callCount, 1)
assert.ok(handleChangeSpy.calledWith('f602f2234d0ea'))
assert.equal(wrapper.find('.currency-display-component').text(), '0.004328 ETH')
assert.equal(currencyInputInstance.state.decimalValue, 1)
assert.equal(currencyInputInstance.state.hexValue, 'f602f2234d0ea')
assert.equal(handleBlurSpy.callCount, 0)
input.simulate('blur')
assert.equal(handleBlurSpy.callCount, 1)
assert.ok(handleBlurSpy.calledWith('f602f2234d0ea'))
})
it('should change the state and pass in a new decimalValue when props.value changes', () => {
const mockStore = {
metamask: {
currentCurrency: 'usd',
conversionRate: 231.06,
},
}
const store = configureMockStore()(mockStore)
const wrapper = shallow(
<Provider store={store}>
<CurrencyInput
onChange={handleChangeSpy}
onBlur={handleBlurSpy}
suffix="USD"
currentCurrency="usd"
conversionRate={231.06}
useFiat
/>
</Provider>
)
assert.ok(wrapper)
const currencyInputInstance = wrapper.find(CurrencyInput).dive()
assert.equal(currencyInputInstance.state('decimalValue'), 0)
assert.equal(currencyInputInstance.state('hexValue'), undefined)
assert.equal(currencyInputInstance.find(UnitInput).props().value, 0)
currencyInputInstance.setProps({ value: '1ec05e43e72400' })
currencyInputInstance.update()
assert.equal(currencyInputInstance.state('decimalValue'), 2)
assert.equal(currencyInputInstance.state('hexValue'), '1ec05e43e72400')
assert.equal(currencyInputInstance.find(UnitInput).props().value, 2)
})
})
})

View File

@ -0,0 +1,55 @@
import assert from 'assert'
import proxyquire from 'proxyquire'
let mapStateToProps, mergeProps
proxyquire('../currency-input.container.js', {
'react-redux': {
connect: (ms, md, mp) => {
mapStateToProps = ms
mergeProps = mp
return () => ({})
},
},
})
describe('CurrencyInput container', () => {
describe('mapStateToProps()', () => {
it('should return the correct props', () => {
const mockState = {
metamask: {
conversionRate: 280.45,
currentCurrency: 'usd',
},
}
assert.deepEqual(mapStateToProps(mockState), {
conversionRate: 280.45,
currentCurrency: 'usd',
})
})
})
describe('mergeProps()', () => {
it('should return the correct props', () => {
const mockStateProps = {
conversionRate: 280.45,
currentCurrency: 'usd',
}
const mockDispatchProps = {}
assert.deepEqual(mergeProps(mockStateProps, mockDispatchProps, { useFiat: true }), {
conversionRate: 280.45,
currentCurrency: 'usd',
useFiat: true,
suffix: 'USD',
})
assert.deepEqual(mergeProps(mockStateProps, mockDispatchProps, {}), {
conversionRate: 280.45,
currentCurrency: 'usd',
suffix: 'ETH',
})
})
})
})

View File

@ -6,6 +6,10 @@
@import './confirm-page-container/index';
@import './currency-input/index';
@import './currency-display/index';
@import './error-message/index';
@import './export-text-container/index';
@ -49,3 +53,5 @@
@import './app-header/index';
@import './sidebars/index';
@import './unit-input/index';

View File

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import CurrencyDisplay from '../../../currency-display'
import { ETH } from '../../../../constants/common'
import UserPreferencedCurrencyDisplay from '../../../user-preferenced-currency-display'
import { PRIMARY, SECONDARY } from '../../../../constants/common'
export default class CancelTransaction extends PureComponent {
static propTypes = {
@ -13,15 +13,15 @@ export default class CancelTransaction extends PureComponent {
return (
<div className="cancel-transaction-gas-fee">
<CurrencyDisplay
<UserPreferencedCurrencyDisplay
className="cancel-transaction-gas-fee__eth"
currency={ETH}
value={value}
numberOfDecimals={6}
type={PRIMARY}
/>
<CurrencyDisplay
<UserPreferencedCurrencyDisplay
className="cancel-transaction-gas-fee__fiat"
value={value}
type={SECONDARY}
/>
</div>
)

View File

@ -2,7 +2,7 @@ import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import CancelTransactionGasFee from '../cancel-transaction-gas-fee.component'
import CurrencyDisplay from '../../../../currency-display'
import UserPreferencedCurrencyDisplay from '../../../../user-preferenced-currency-display'
describe('CancelTransactionGasFee Component', () => {
it('should render', () => {
@ -13,12 +13,11 @@ describe('CancelTransactionGasFee Component', () => {
)
assert.ok(wrapper)
assert.equal(wrapper.find(CurrencyDisplay).length, 2)
const ethDisplay = wrapper.find(CurrencyDisplay).at(0)
const fiatDisplay = wrapper.find(CurrencyDisplay).at(1)
assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2)
const ethDisplay = wrapper.find(UserPreferencedCurrencyDisplay).at(0)
const fiatDisplay = wrapper.find(UserPreferencedCurrencyDisplay).at(1)
assert.equal(ethDisplay.props().value, '0x3b9aca00')
assert.equal(ethDisplay.props().currency, 'ETH')
assert.equal(ethDisplay.props().className, 'cancel-transaction-gas-fee__eth')
assert.equal(fiatDisplay.props().value, '0x3b9aca00')

View File

@ -1,12 +1,15 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ConfirmTransactionBase from '../confirm-transaction-base'
import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display'
import {
formatCurrency,
convertTokenToFiat,
addFiat,
roundExponential,
} from '../../../helpers/confirm-transaction/util'
import { getWeiHexFromDecimalValue } from '../../../helpers/conversions.util'
import { ETH, PRIMARY } from '../../../constants/common'
export default class ConfirmTokenTransactionBase extends Component {
static contextTypes = {
@ -36,19 +39,48 @@ export default class ConfirmTokenTransactionBase extends Component {
})
}
getSubtitle () {
const { currentCurrency, contractExchangeRate } = this.props
renderSubtitleComponent () {
const { contractExchangeRate, tokenAmount } = this.props
if (typeof contractExchangeRate === 'undefined') {
return this.context.t('noConversionRateAvailable')
} else {
const fiatTransactionAmount = this.getFiatTransactionAmount()
const roundedFiatTransactionAmount = roundExponential(fiatTransactionAmount)
return formatCurrency(roundedFiatTransactionAmount, currentCurrency)
}
const decimalEthValue = (tokenAmount * contractExchangeRate) || 0
const hexWeiValue = getWeiHexFromDecimalValue({
value: decimalEthValue,
fromCurrency: ETH,
fromDenomination: ETH,
})
return typeof contractExchangeRate === 'undefined'
? (
<span>
{ this.context.t('noConversionRateAvailable') }
</span>
) : (
<UserPreferencedCurrencyDisplay
value={hexWeiValue}
type={PRIMARY}
showEthLogo
hideLabel
/>
)
}
getFiatTotalTextOverride () {
renderPrimaryTotalTextOverride () {
const { tokenAmount, tokenSymbol, ethTransactionTotal } = this.props
const tokensText = `${tokenAmount} ${tokenSymbol}`
return (
<div>
<span>{ `${tokensText} + ` }</span>
<img
src="/images/eth.svg"
height="18"
/>
<span>{ ethTransactionTotal }</span>
</div>
)
}
getSecondaryTotalTextOverride () {
const { fiatTransactionTotal, currentCurrency, contractExchangeRate } = this.props
if (typeof contractExchangeRate === 'undefined') {
@ -67,7 +99,6 @@ export default class ConfirmTokenTransactionBase extends Component {
tokenAddress,
tokenSymbol,
tokenAmount,
ethTransactionTotal,
...restProps
} = this.props
@ -78,9 +109,9 @@ export default class ConfirmTokenTransactionBase extends Component {
toAddress={toAddress}
identiconAddress={tokenAddress}
title={tokensText}
subtitle={this.getSubtitle()}
ethTotalTextOverride={`${tokensText} + \u2666 ${ethTransactionTotal}`}
fiatTotalTextOverride={this.getFiatTotalTextOverride()}
subtitleComponent={this.renderSubtitleComponent()}
primaryTotalTextOverride={this.renderPrimaryTotalTextOverride()}
secondaryTotalTextOverride={this.getSecondaryTotalTextOverride()}
{...restProps}
/>
)

View File

@ -1,7 +1,6 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ConfirmPageContainer, { ConfirmDetailRow } from '../../confirm-page-container'
import { formatCurrency } from '../../../helpers/confirm-transaction/util'
import { isBalanceSufficient } from '../../send/send.utils'
import { DEFAULT_ROUTE } from '../../../routes'
import {
@ -9,6 +8,8 @@ import {
TRANSACTION_ERROR_KEY,
} from '../../../constants/error-keys'
import { CONFIRMED_STATUS, DROPPED_STATUS } from '../../../constants/transactions'
import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display'
import { PRIMARY, SECONDARY } from '../../../constants/common'
export default class ConfirmTransactionBase extends Component {
static contextTypes = {
@ -36,7 +37,9 @@ export default class ConfirmTransactionBase extends Component {
fiatTransactionTotal: PropTypes.string,
fromAddress: PropTypes.string,
fromName: PropTypes.string,
hexGasTotal: PropTypes.string,
hexTransactionAmount: PropTypes.string,
hexTransactionFee: PropTypes.string,
hexTransactionTotal: PropTypes.string,
isTxReprice: PropTypes.bool,
methodData: PropTypes.object,
nonce: PropTypes.string,
@ -59,8 +62,8 @@ export default class ConfirmTransactionBase extends Component {
detailsComponent: PropTypes.node,
errorKey: PropTypes.string,
errorMessage: PropTypes.string,
ethTotalTextOverride: PropTypes.string,
fiatTotalTextOverride: PropTypes.string,
primaryTotalTextOverride: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
secondaryTotalTextOverride: PropTypes.string,
hideData: PropTypes.bool,
hideDetails: PropTypes.bool,
hideSubtitle: PropTypes.bool,
@ -70,8 +73,10 @@ export default class ConfirmTransactionBase extends Component {
onEditGas: PropTypes.func,
onSubmit: PropTypes.func,
subtitle: PropTypes.string,
subtitleComponent: PropTypes.node,
summaryComponent: PropTypes.node,
title: PropTypes.string,
titleComponent: PropTypes.node,
valid: PropTypes.bool,
warning: PropTypes.string,
}
@ -105,7 +110,7 @@ export default class ConfirmTransactionBase extends Component {
const {
balance,
conversionRate,
hexGasTotal,
hexTransactionFee,
txData: {
simulationFails,
txParams: {
@ -116,7 +121,7 @@ export default class ConfirmTransactionBase extends Component {
const insufficientBalance = balance && !isBalanceSufficient({
amount,
gasTotal: hexGasTotal || '0x0',
gasTotal: hexTransactionFee || '0x0',
balance,
conversionRate,
})
@ -153,13 +158,10 @@ export default class ConfirmTransactionBase extends Component {
renderDetails () {
const {
detailsComponent,
fiatTransactionFee,
ethTransactionFee,
currentCurrency,
fiatTransactionTotal,
ethTransactionTotal,
fiatTotalTextOverride,
ethTotalTextOverride,
primaryTotalTextOverride,
secondaryTotalTextOverride,
hexTransactionFee,
hexTransactionTotal,
hideDetails,
} = this.props
@ -167,16 +169,13 @@ export default class ConfirmTransactionBase extends Component {
return null
}
const formattedCurrency = formatCurrency(fiatTransactionTotal, currentCurrency)
return (
detailsComponent || (
<div className="confirm-page-container-content__details">
<div className="confirm-page-container-content__gas-fee">
<ConfirmDetailRow
label="Gas Fee"
fiatText={formatCurrency(fiatTransactionFee, currentCurrency)}
ethText={`\u2666 ${ethTransactionFee}`}
value={hexTransactionFee}
headerText="Edit"
headerTextClassName="confirm-detail-row__header-text--edit"
onHeaderClick={() => this.handleEditGas()}
@ -185,11 +184,12 @@ export default class ConfirmTransactionBase extends Component {
<div>
<ConfirmDetailRow
label="Total"
fiatText={fiatTotalTextOverride || formattedCurrency}
ethText={ethTotalTextOverride || `\u2666 ${ethTransactionTotal}`}
value={hexTransactionTotal}
primaryText={primaryTotalTextOverride}
secondaryText={secondaryTotalTextOverride}
headerText="Amount + Gas Fee"
headerTextClassName="confirm-detail-row__header-text--total"
fiatTextColor="#2f9ae0"
primaryValueTextColor="#2f9ae0"
/>
</div>
</div>
@ -311,6 +311,43 @@ export default class ConfirmTransactionBase extends Component {
}
}
renderTitleComponent () {
const { title, titleComponent, hexTransactionAmount } = this.props
// Title string passed in by props takes priority
if (title) {
return null
}
return titleComponent || (
<UserPreferencedCurrencyDisplay
value={hexTransactionAmount}
type={PRIMARY}
showEthLogo
ethLogoHeight="26"
hideLabel
/>
)
}
renderSubtitleComponent () {
const { subtitle, subtitleComponent, hexTransactionAmount } = this.props
// Subtitle string passed in by props takes priority
if (subtitle) {
return null
}
return subtitleComponent || (
<UserPreferencedCurrencyDisplay
value={hexTransactionAmount}
type={SECONDARY}
showEthLogo
hideLabel
/>
)
}
render () {
const {
isTxReprice,
@ -319,12 +356,9 @@ export default class ConfirmTransactionBase extends Component {
toName,
toAddress,
methodData,
ethTransactionAmount,
fiatTransactionAmount,
valid: propsValid = true,
errorMessage,
errorKey: propsErrorKey,
currentCurrency,
action,
title,
subtitle,
@ -341,7 +375,6 @@ export default class ConfirmTransactionBase extends Component {
const { submitting, submitError } = this.state
const { name } = methodData
const fiatConvertedAmount = formatCurrency(fiatTransactionAmount, currentCurrency)
const { valid, errorKey } = this.getErrorKey()
return (
@ -352,8 +385,10 @@ export default class ConfirmTransactionBase extends Component {
toAddress={toAddress}
showEdit={onEdit && !isTxReprice}
action={action || name || this.context.t('unknownFunction')}
title={title || `${fiatConvertedAmount} ${currentCurrency.toUpperCase()}`}
subtitle={subtitle || `\u2666 ${ethTransactionAmount}`}
title={title}
titleComponent={this.renderTitleComponent()}
subtitle={subtitle}
subtitleComponent={this.renderSubtitleComponent()}
hideSubtitle={hideSubtitle}
summaryComponent={summaryComponent}
detailsComponent={this.renderDetails()}

View File

@ -36,7 +36,9 @@ const mapStateToProps = (state, props) => {
fiatTransactionAmount,
fiatTransactionFee,
fiatTransactionTotal,
hexGasTotal,
hexTransactionAmount,
hexTransactionFee,
hexTransactionTotal,
tokenData,
methodData,
txData,
@ -87,7 +89,9 @@ const mapStateToProps = (state, props) => {
fiatTransactionAmount,
fiatTransactionFee,
fiatTransactionTotal,
hexGasTotal,
hexTransactionAmount,
hexTransactionFee,
hexTransactionTotal,
txData,
tokenData,
methodData,

View File

@ -48,4 +48,22 @@
border-color: $ecstasy;
}
}
&__radio-buttons {
display: flex;
align-items: center;
}
&__radio-button {
display: flex;
align-items: center;
&:not(:last-child) {
margin-right: 16px;
}
}
&__radio-label {
padding-left: 4px;
}
}

View File

@ -55,6 +55,8 @@ export default class SettingsTab extends PureComponent {
sendHexData: PropTypes.bool,
currentCurrency: PropTypes.string,
conversionDate: PropTypes.number,
useETHAsPrimaryCurrency: PropTypes.bool,
setUseETHAsPrimaryCurrencyPreference: PropTypes.func,
}
state = {
@ -339,6 +341,56 @@ export default class SettingsTab extends PureComponent {
)
}
renderUseEthAsPrimaryCurrency () {
const { t } = this.context
const { useETHAsPrimaryCurrency, setUseETHAsPrimaryCurrencyPreference } = this.props
return (
<div className="settings-page__content-row">
<div className="settings-page__content-item">
<span>{ t('primaryCurrencySetting') }</span>
<div className="settings-page__content-description">
{ t('primaryCurrencySettingDescription') }
</div>
</div>
<div className="settings-page__content-item">
<div className="settings-page__content-item-col">
<div className="settings-tab__radio-buttons">
<div className="settings-tab__radio-button">
<input
type="radio"
id="eth-primary-currency"
onChange={() => setUseETHAsPrimaryCurrencyPreference(true)}
checked={Boolean(useETHAsPrimaryCurrency)}
/>
<label
htmlFor="eth-primary-currency"
className="settings-tab__radio-label"
>
{ t('eth') }
</label>
</div>
<div className="settings-tab__radio-button">
<input
type="radio"
id="fiat-primary-currency"
onChange={() => setUseETHAsPrimaryCurrencyPreference(false)}
checked={!useETHAsPrimaryCurrency}
/>
<label
htmlFor="fiat-primary-currency"
className="settings-tab__radio-label"
>
{ t('fiat') }
</label>
</div>
</div>
</div>
</div>
</div>
)
}
render () {
const { warning, isMascara } = this.props
@ -346,6 +398,7 @@ export default class SettingsTab extends PureComponent {
<div className="settings-page__content">
{ warning && <div className="settings-tab__error">{ warning }</div> }
{ this.renderCurrentConversion() }
{ this.renderUseEthAsPrimaryCurrency() }
{ this.renderCurrentLocale() }
{ this.renderNewRpcUrl() }
{ this.renderStateLogs() }

View File

@ -11,7 +11,9 @@ import {
updateCurrentLocale,
setFeatureFlag,
showModal,
setUseETHAsPrimaryCurrencyPreference,
} from '../../../../actions'
import { preferencesSelector } from '../../../../selectors'
const mapStateToProps = state => {
const { appState: { warning }, metamask } = state
@ -24,6 +26,7 @@ const mapStateToProps = state => {
isMascara,
currentLocale,
} = metamask
const { useETHAsPrimaryCurrency } = preferencesSelector(state)
return {
warning,
@ -34,6 +37,7 @@ const mapStateToProps = state => {
useBlockie,
sendHexData,
provider,
useETHAsPrimaryCurrency,
}
}
@ -50,6 +54,9 @@ const mapDispatchToProps = dispatch => {
},
setHexDataFeatureFlag: shouldShow => dispatch(setFeatureFlag('sendHexData', shouldShow)),
showResetAccountConfirmationModal: () => dispatch(showModal({ name: 'CONFIRM_RESET_ACCOUNT' })),
setUseETHAsPrimaryCurrencyPreference: value => {
return dispatch(setUseETHAsPrimaryCurrencyPreference(value))
},
}
}

View File

@ -2,7 +2,8 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { checksumAddress } from '../../../util'
import Identicon from '../../identicon'
import CurrencyDisplay from '../currency-display'
import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display'
import { PRIMARY, SECONDARY } from '../../../constants/common'
export default class AccountListItem extends Component {
@ -25,8 +26,6 @@ export default class AccountListItem extends Component {
const {
account,
className,
conversionRate,
currentCurrency,
displayAddress = false,
displayBalance = true,
handleClick,
@ -57,16 +56,20 @@ export default class AccountListItem extends Component {
{ checksumAddress(address) }
</div>}
{displayBalance && <CurrencyDisplay
className="account-list-item__account-balances"
conversionRate={conversionRate}
convertedBalanceClassName="account-list-item__account-secondary-balance"
convertedCurrency={currentCurrency}
primaryBalanceClassName="account-list-item__account-primary-balance"
primaryCurrency="ETH"
readOnly={true}
value={balance}
/>}
{
displayBalance && (
<div className="account-list-item__account-balances">
<UserPreferencedCurrencyDisplay
type={PRIMARY}
value={balance}
/>
<UserPreferencedCurrencyDisplay
type={SECONDARY}
value={balance}
/>
</div>
)
}
</div>)
}

View File

@ -4,7 +4,7 @@ import { shallow } from 'enzyme'
import sinon from 'sinon'
import proxyquire from 'proxyquire'
import Identicon from '../../../identicon'
import CurrencyDisplay from '../../currency-display'
import UserPreferencedCurrencyDisplay from '../../../user-preferenced-currency-display'
const utilsMethodStubs = {
checksumAddress: sinon.stub().returns('mockCheckSumAddress'),
@ -114,17 +114,11 @@ describe('AccountListItem Component', function () {
it('should render a CurrencyDisplay with the correct props if displayBalance is true', () => {
wrapper.setProps({ displayBalance: true })
assert.equal(wrapper.find(CurrencyDisplay).length, 1)
assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2)
assert.deepEqual(
wrapper.find(CurrencyDisplay).props(),
wrapper.find(UserPreferencedCurrencyDisplay).at(0).props(),
{
className: 'account-list-item__account-balances',
conversionRate: 4,
convertedBalanceClassName: 'account-list-item__account-secondary-balance',
convertedCurrency: 'mockCurrentyCurrency',
primaryBalanceClassName: 'account-list-item__account-primary-balance',
primaryCurrency: 'ETH',
readOnly: true,
type: 'PRIMARY',
value: 'mockBalance',
}
)
@ -132,7 +126,7 @@ describe('AccountListItem Component', function () {
it('should not render a CurrencyDisplay if displayBalance is false', () => {
wrapper.setProps({ displayBalance: false })
assert.equal(wrapper.find(CurrencyDisplay).length, 0)
assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 0)
})
})
})

View File

@ -1,186 +0,0 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const { conversionUtil, multiplyCurrencies } = require('../../../conversion-util')
const { removeLeadingZeroes } = require('../send.utils')
const currencyFormatter = require('currency-formatter')
const currencies = require('currency-formatter/currencies')
const ethUtil = require('ethereumjs-util')
const PropTypes = require('prop-types')
CurrencyDisplay.contextTypes = {
t: PropTypes.func,
}
module.exports = CurrencyDisplay
inherits(CurrencyDisplay, Component)
function CurrencyDisplay () {
Component.call(this)
}
function toHexWei (value) {
return conversionUtil(value, {
fromNumericBase: 'dec',
toNumericBase: 'hex',
toDenomination: 'WEI',
})
}
CurrencyDisplay.prototype.componentWillMount = function () {
this.setState({
valueToRender: this.getValueToRender(this.props),
})
}
CurrencyDisplay.prototype.componentWillReceiveProps = function (nextProps) {
const currentValueToRender = this.getValueToRender(this.props)
const newValueToRender = this.getValueToRender(nextProps)
if (currentValueToRender !== newValueToRender) {
this.setState({
valueToRender: newValueToRender,
})
}
}
CurrencyDisplay.prototype.getAmount = function (value) {
const { selectedToken } = this.props
const { decimals } = selectedToken || {}
const multiplier = Math.pow(10, Number(decimals || 0))
const sendAmount = multiplyCurrencies(value || '0', multiplier, {toNumericBase: 'hex'})
return selectedToken
? sendAmount
: toHexWei(value)
}
CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversionRate, value, readOnly }) {
if (value === '0x0') return readOnly ? '0' : ''
const { decimals, symbol } = selectedToken || {}
const multiplier = Math.pow(10, Number(decimals || 0))
return selectedToken
? conversionUtil(ethUtil.addHexPrefix(value), {
fromNumericBase: 'hex',
toNumericBase: 'dec',
toCurrency: symbol,
conversionRate: multiplier,
invertConversionRate: true,
})
: conversionUtil(ethUtil.addHexPrefix(value), {
fromNumericBase: 'hex',
toNumericBase: 'dec',
fromDenomination: 'WEI',
numberOfDecimals: 9,
conversionRate,
})
}
CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValue) {
const { primaryCurrency, convertedCurrency, conversionRate } = this.props
if (conversionRate === 0 || conversionRate === null || conversionRate === undefined) {
if (nonFormattedValue !== 0) {
return null
}
}
let convertedValue = conversionUtil(nonFormattedValue, {
fromNumericBase: 'dec',
fromCurrency: primaryCurrency,
toCurrency: convertedCurrency,
numberOfDecimals: 2,
conversionRate,
})
convertedValue = Number(convertedValue).toFixed(2)
const upperCaseCurrencyCode = convertedCurrency.toUpperCase()
return currencies.find(currency => currency.code === upperCaseCurrencyCode)
? currencyFormatter.format(Number(convertedValue), {
code: upperCaseCurrencyCode,
})
: convertedValue
}
CurrencyDisplay.prototype.handleChange = function (newVal) {
this.setState({ valueToRender: removeLeadingZeroes(newVal) })
this.props.onChange(this.getAmount(newVal))
}
CurrencyDisplay.prototype.getInputWidth = function (valueToRender, readOnly) {
const valueString = String(valueToRender)
const valueLength = valueString.length || 1
const decimalPointDeficit = valueString.match(/\./) ? -0.5 : 0
return (valueLength + decimalPointDeficit + 0.75) + 'ch'
}
CurrencyDisplay.prototype.onlyRenderConversions = function (convertedValueToRender) {
const {
convertedBalanceClassName = 'currency-display__converted-value',
convertedCurrency,
} = this.props
return h('div', {
className: convertedBalanceClassName,
}, convertedValueToRender == null
? this.context.t('noConversionRateAvailable')
: `${convertedValueToRender} ${convertedCurrency.toUpperCase()}`
)
}
CurrencyDisplay.prototype.render = function () {
const {
className = 'currency-display',
primaryBalanceClassName = 'currency-display__input',
primaryCurrency,
readOnly = false,
inError = false,
onBlur,
step,
} = this.props
const { valueToRender } = this.state
const convertedValueToRender = this.getConvertedValueToRender(valueToRender)
return h('div', {
className,
style: {
borderColor: inError ? 'red' : null,
},
onClick: () => {
this.currencyInput && this.currencyInput.focus()
},
}, [
h('div.currency-display__primary-row', [
h('div.currency-display__input-wrapper', [
h('input', {
className: primaryBalanceClassName,
value: `${valueToRender}`,
placeholder: '0',
type: 'number',
readOnly,
...(!readOnly ? {
onChange: e => this.handleChange(e.target.value),
onBlur: () => onBlur(this.getAmount(valueToRender)),
} : {}),
ref: input => { this.currencyInput = input },
style: {
width: this.getInputWidth(valueToRender, readOnly),
},
min: 0,
step,
}),
h('span.currency-display__currency-symbol', primaryCurrency),
]),
]), this.onlyRenderConversions(convertedValueToRender),
])
}

View File

@ -1 +0,0 @@
export { default } from './currency-display.js'

View File

@ -1,91 +0,0 @@
import React from 'react'
import assert from 'assert'
import sinon from 'sinon'
import { shallow, mount } from 'enzyme'
import CurrencyDisplay from '../currency-display'
describe('', () => {
const token = {
address: '0xTest',
symbol: 'TST',
decimals: '13',
}
it('retuns ETH value for wei value', () => {
const wrapper = mount(<CurrencyDisplay />, {context: {t: str => str + '_t'}})
const value = wrapper.instance().getValueToRender({
// 1000000000000000000
value: 'DE0B6B3A7640000',
})
assert.equal(value, 1)
})
it('returns value of token based on token decimals', () => {
const wrapper = mount(<CurrencyDisplay />, {context: {t: str => str + '_t'}})
const value = wrapper.instance().getValueToRender({
selectedToken: token,
// 1000000000000000000
value: 'DE0B6B3A7640000',
})
assert.equal(value, 100000)
})
it('returns hex value with decimal adjustment', () => {
const wrapper = mount(
<CurrencyDisplay
selectedToken={token}
/>, {context: {t: str => str + '_t'}})
const value = wrapper.instance().getAmount(1)
// 10000000000000
assert.equal(value, '9184e72a000')
})
it('#getConvertedValueToRender converts input value based on conversionRate', () => {
const wrapper = mount(
<CurrencyDisplay
primaryCurrency={'usd'}
convertedCurrency={'ja'}
conversionRate={2}
/>, {context: {t: str => str + '_t'}})
const value = wrapper.instance().getConvertedValueToRender(32)
assert.equal(value, 64)
})
it('#onlyRenderConversions renders single element for converted currency and value', () => {
const wrapper = mount(
<CurrencyDisplay
convertedCurrency={'test'}
/>, {context: {t: str => str + '_t'}})
const value = wrapper.instance().onlyRenderConversions(10)
assert.equal(value.props.className, 'currency-display__converted-value')
assert.equal(value.props.children, '10 TEST')
})
it('simulates change value in input', () => {
const handleChangeSpy = sinon.spy()
const wrapper = shallow(
<CurrencyDisplay
onChange={handleChangeSpy}
/>, {context: {t: str => str + '_t'}})
const input = wrapper.find('input')
input.simulate('focus')
input.simulate('change', { target: { value: '100' } })
assert.equal(wrapper.state().valueToRender, '100')
assert.equal(wrapper.find('input').prop('value'), '100')
})
})

View File

@ -2,7 +2,8 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import SendRowWrapper from '../send-row-wrapper/'
import AmountMaxButton from './amount-max-button/'
import CurrencyDisplay from '../../currency-display'
import UserPreferencedCurrencyInput from '../../../user-preferenced-currency-input'
import UserPreferencedTokenInput from '../../../user-preferenced-token-input'
export default class SendAmountRow extends Component {
@ -84,16 +85,25 @@ export default class SendAmountRow extends Component {
}
}
renderInput () {
const { amount, inError, selectedToken } = this.props
const Component = selectedToken ? UserPreferencedTokenInput : UserPreferencedCurrencyInput
return (
<Component
onChange={newAmount => this.validateAmount(newAmount)}
onBlur={newAmount => {
this.updateGas(newAmount)
this.updateAmount(newAmount)
}}
error={inError}
value={amount}
/>
)
}
render () {
const {
amount,
amountConversionRate,
convertedCurrency,
gasTotal,
inError,
primaryCurrency,
selectedToken,
} = this.props
const { gasTotal, inError } = this.props
return (
<SendRowWrapper
@ -102,20 +112,7 @@ export default class SendAmountRow extends Component {
errorType={'amount'}
>
{!inError && gasTotal && <AmountMaxButton />}
<CurrencyDisplay
conversionRate={amountConversionRate}
convertedCurrency={convertedCurrency}
onBlur={newAmount => {
this.updateGas(newAmount)
this.updateAmount(newAmount)
}}
onChange={newAmount => this.validateAmount(newAmount)}
inError={inError}
primaryCurrency={primaryCurrency || 'ETH'}
selectedToken={selectedToken}
value={amount}
step="any"
/>
{ this.renderInput() }
</SendRowWrapper>
)
}

View File

@ -6,7 +6,7 @@ import SendAmountRow from '../send-amount-row.component.js'
import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component'
import AmountMaxButton from '../amount-max-button/amount-max-button.container'
import CurrencyDisplay from '../../../currency-display'
import UserPreferencedTokenInput from '../../../../user-preferenced-token-input'
const propsMethodSpies = {
setMaxModeTo: sinon.spy(),
@ -150,26 +150,19 @@ describe('SendAmountRow Component', function () {
assert(wrapper.find(SendRowWrapper).childAt(0).is(AmountMaxButton))
})
it('should render a CurrencyDisplay as the second child of the SendRowWrapper', () => {
assert(wrapper.find(SendRowWrapper).childAt(1).is(CurrencyDisplay))
it('should render a UserPreferencedTokenInput as the second child of the SendRowWrapper', () => {
console.log('HI', wrapper.find(SendRowWrapper).childAt(1))
assert(wrapper.find(SendRowWrapper).childAt(1).is(UserPreferencedTokenInput))
})
it('should render the CurrencyDisplay with the correct props', () => {
it('should render the UserPreferencedTokenInput with the correct props', () => {
const {
conversionRate,
convertedCurrency,
onBlur,
onChange,
inError,
primaryCurrency,
selectedToken,
error,
value,
} = wrapper.find(SendRowWrapper).childAt(1).props()
assert.equal(conversionRate, 'mockAmountConversionRate')
assert.equal(convertedCurrency, 'mockConvertedCurrency')
assert.equal(inError, false)
assert.equal(primaryCurrency, 'mockPrimaryCurrency')
assert.deepEqual(selectedToken, { address: 'mockTokenAddress' })
assert.equal(error, false)
assert.equal(value, 'mockAmount')
assert.equal(SendAmountRow.prototype.updateGas.callCount, 0)
assert.equal(SendAmountRow.prototype.updateAmount.callCount, 0)
@ -192,11 +185,5 @@ describe('SendAmountRow Component', function () {
['mockNewAmount']
)
})
it('should pass the default primaryCurrency to the CurrencyDisplay if primaryCurrency is falsy', () => {
wrapper.setProps({ primaryCurrency: null })
const { primaryCurrency } = wrapper.find(SendRowWrapper).childAt(1).props()
assert.equal(primaryCurrency, 'ETH')
})
})
})

View File

@ -1,7 +1,7 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import CurrencyDisplay from '../../../../send/currency-display'
import UserPreferencedCurrencyDisplay from '../../../../user-preferenced-currency-display'
import { PRIMARY, SECONDARY } from '../../../../../constants/common'
export default class GasFeeDisplay extends Component {
@ -19,27 +19,24 @@ export default class GasFeeDisplay extends Component {
};
render () {
const {
conversionRate,
gasTotal,
onClick,
primaryCurrency = 'ETH',
convertedCurrency,
gasLoadingError,
} = this.props
const { gasTotal, onClick, gasLoadingError } = this.props
return (
<div className="send-v2__gas-fee-display">
{gasTotal
? <CurrencyDisplay
primaryCurrency={primaryCurrency}
convertedCurrency={convertedCurrency}
value={gasTotal}
conversionRate={conversionRate}
gasLoadingError={gasLoadingError}
convertedPrefix={'$'}
readOnly
/>
? (
<div className="currency-display">
<UserPreferencedCurrencyDisplay
value={gasTotal}
type={PRIMARY}
/>
<UserPreferencedCurrencyDisplay
className="currency-display__converted-value"
value={gasTotal}
type={SECONDARY}
/>
</div>
)
: gasLoadingError
? <div className="currency-display.currency-display--message">
{this.context.t('setGasPrice')}

View File

@ -2,7 +2,7 @@ import React from 'react'
import assert from 'assert'
import {shallow} from 'enzyme'
import GasFeeDisplay from '../gas-fee-display.component'
import CurrencyDisplay from '../../../../../send/currency-display'
import UserPreferencedCurrencyDisplay from '../../../../../user-preferenced-currency-display'
import sinon from 'sinon'
@ -29,17 +29,15 @@ describe('SendGasRow Component', function () {
describe('render', () => {
it('should render a CurrencyDisplay component', () => {
assert.equal(wrapper.find(CurrencyDisplay).length, 1)
assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2)
})
it('should render the CurrencyDisplay with the correct props', () => {
const {
conversionRate,
convertedCurrency,
type,
value,
} = wrapper.find(CurrencyDisplay).props()
assert.equal(conversionRate, 20)
assert.equal(convertedCurrency, 'mockConvertedCurrency')
} = wrapper.find(UserPreferencedCurrencyDisplay).at(0).props()
assert.equal(type, 'PRIMARY')
assert.equal(value, 'mockGasTotal')
})

View File

@ -0,0 +1 @@
export { default } from './token-input.container'

View File

@ -0,0 +1,308 @@
import React from 'react'
import PropTypes from 'prop-types'
import assert from 'assert'
import { shallow, mount } from 'enzyme'
import sinon from 'sinon'
import { Provider } from 'react-redux'
import configureMockStore from 'redux-mock-store'
import TokenInput from '../token-input.component'
import UnitInput from '../../unit-input'
import CurrencyDisplay from '../../currency-display'
describe('TokenInput Component', () => {
const t = key => `translate ${key}`
describe('rendering', () => {
it('should render properly without a token', () => {
const wrapper = shallow(
<TokenInput />,
{ context: { t } }
)
assert.ok(wrapper)
assert.equal(wrapper.find(UnitInput).length, 1)
})
it('should render properly with a token', () => {
const mockStore = {
metamask: {
currentCurrency: 'usd',
conversionRate: 231.06,
},
}
const store = configureMockStore()(mockStore)
const wrapper = mount(
<Provider store={store}>
<TokenInput
selectedToken={{
address: '0x1',
decimals: '4',
symbol: 'ABC',
}}
suffix="ABC"
/>
</Provider>,
{ context: { t },
childContextTypes: {
t: PropTypes.func,
},
},
)
assert.ok(wrapper)
assert.equal(wrapper.find('.unit-input__suffix').length, 1)
assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC')
assert.equal(wrapper.find('.currency-input__conversion-component').length, 1)
assert.equal(wrapper.find('.currency-input__conversion-component').text(), 'translate noConversionRateAvailable')
})
it('should render properly with a token and selectedTokenExchangeRate', () => {
const mockStore = {
metamask: {
currentCurrency: 'usd',
conversionRate: 231.06,
},
}
const store = configureMockStore()(mockStore)
const wrapper = mount(
<Provider store={store}>
<TokenInput
selectedToken={{
address: '0x1',
decimals: '4',
symbol: 'ABC',
}}
suffix="ABC"
selectedTokenExchangeRate={2}
/>
</Provider>,
{ context: { t },
childContextTypes: {
t: PropTypes.func,
},
},
)
assert.ok(wrapper)
assert.equal(wrapper.find('.unit-input__suffix').length, 1)
assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC')
assert.equal(wrapper.find(CurrencyDisplay).length, 1)
})
it('should render properly with a token value for ETH', () => {
const mockStore = {
metamask: {
currentCurrency: 'usd',
conversionRate: 231.06,
},
}
const store = configureMockStore()(mockStore)
const wrapper = mount(
<Provider store={store}>
<TokenInput
value="2710"
selectedToken={{
address: '0x1',
decimals: '4',
symbol: 'ABC',
}}
suffix="ABC"
selectedTokenExchangeRate={2}
/>
</Provider>
)
assert.ok(wrapper)
const tokenInputInstance = wrapper.find(TokenInput).at(0).instance()
assert.equal(tokenInputInstance.state.decimalValue, 1)
assert.equal(tokenInputInstance.state.hexValue, '2710')
assert.equal(wrapper.find('.unit-input__suffix').length, 1)
assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC')
assert.equal(wrapper.find('.unit-input__input').props().value, '1')
assert.equal(wrapper.find('.currency-display-component').text(), '2 ETH')
})
it('should render properly with a token value for fiat', () => {
const mockStore = {
metamask: {
currentCurrency: 'usd',
conversionRate: 231.06,
},
}
const store = configureMockStore()(mockStore)
const wrapper = mount(
<Provider store={store}>
<TokenInput
value="2710"
selectedToken={{
address: '0x1',
decimals: '4',
symbol: 'ABC',
}}
suffix="ABC"
selectedTokenExchangeRate={2}
showFiat
/>
</Provider>
)
assert.ok(wrapper)
const tokenInputInstance = wrapper.find(TokenInput).at(0).instance()
assert.equal(tokenInputInstance.state.decimalValue, 1)
assert.equal(tokenInputInstance.state.hexValue, '2710')
assert.equal(wrapper.find('.unit-input__suffix').length, 1)
assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC')
assert.equal(wrapper.find('.unit-input__input').props().value, '1')
assert.equal(wrapper.find('.currency-display-component').text(), '$462.12 USD')
})
})
describe('handling actions', () => {
const handleChangeSpy = sinon.spy()
const handleBlurSpy = sinon.spy()
afterEach(() => {
handleChangeSpy.resetHistory()
handleBlurSpy.resetHistory()
})
it('should call onChange and onBlur on input changes with the hex value for ETH', () => {
const mockStore = {
metamask: {
currentCurrency: 'usd',
conversionRate: 231.06,
},
}
const store = configureMockStore()(mockStore)
const wrapper = mount(
<Provider store={store}>
<TokenInput
onChange={handleChangeSpy}
onBlur={handleBlurSpy}
selectedToken={{
address: '0x1',
decimals: '4',
symbol: 'ABC',
}}
suffix="ABC"
selectedTokenExchangeRate={2}
/>
</Provider>
)
assert.ok(wrapper)
assert.equal(handleChangeSpy.callCount, 0)
assert.equal(handleBlurSpy.callCount, 0)
const tokenInputInstance = wrapper.find(TokenInput).at(0).instance()
assert.equal(tokenInputInstance.state.decimalValue, 0)
assert.equal(tokenInputInstance.state.hexValue, undefined)
assert.equal(wrapper.find('.currency-display-component').text(), '0 ETH')
const input = wrapper.find('input')
assert.equal(input.props().value, 0)
input.simulate('change', { target: { value: 1 } })
assert.equal(handleChangeSpy.callCount, 1)
assert.ok(handleChangeSpy.calledWith('2710'))
assert.equal(wrapper.find('.currency-display-component').text(), '2 ETH')
assert.equal(tokenInputInstance.state.decimalValue, 1)
assert.equal(tokenInputInstance.state.hexValue, '2710')
assert.equal(handleBlurSpy.callCount, 0)
input.simulate('blur')
assert.equal(handleBlurSpy.callCount, 1)
assert.ok(handleBlurSpy.calledWith('2710'))
})
it('should call onChange and onBlur on input changes with the hex value for fiat', () => {
const mockStore = {
metamask: {
currentCurrency: 'usd',
conversionRate: 231.06,
},
}
const store = configureMockStore()(mockStore)
const wrapper = mount(
<Provider store={store}>
<TokenInput
onChange={handleChangeSpy}
onBlur={handleBlurSpy}
selectedToken={{
address: '0x1',
decimals: '4',
symbol: 'ABC',
}}
suffix="ABC"
selectedTokenExchangeRate={2}
showFiat
/>
</Provider>
)
assert.ok(wrapper)
assert.equal(handleChangeSpy.callCount, 0)
assert.equal(handleBlurSpy.callCount, 0)
const tokenInputInstance = wrapper.find(TokenInput).at(0).instance()
assert.equal(tokenInputInstance.state.decimalValue, 0)
assert.equal(tokenInputInstance.state.hexValue, undefined)
assert.equal(wrapper.find('.currency-display-component').text(), '$0.00 USD')
const input = wrapper.find('input')
assert.equal(input.props().value, 0)
input.simulate('change', { target: { value: 1 } })
assert.equal(handleChangeSpy.callCount, 1)
assert.ok(handleChangeSpy.calledWith('2710'))
assert.equal(wrapper.find('.currency-display-component').text(), '$462.12 USD')
assert.equal(tokenInputInstance.state.decimalValue, 1)
assert.equal(tokenInputInstance.state.hexValue, '2710')
assert.equal(handleBlurSpy.callCount, 0)
input.simulate('blur')
assert.equal(handleBlurSpy.callCount, 1)
assert.ok(handleBlurSpy.calledWith('2710'))
})
it('should change the state and pass in a new decimalValue when props.value changes', () => {
const mockStore = {
metamask: {
currentCurrency: 'usd',
conversionRate: 231.06,
},
}
const store = configureMockStore()(mockStore)
const wrapper = shallow(
<Provider store={store}>
<TokenInput
onChange={handleChangeSpy}
onBlur={handleBlurSpy}
selectedToken={{
address: '0x1',
decimals: '4',
symbol: 'ABC',
}}
suffix="ABC"
selectedTokenExchangeRate={2}
showFiat
/>
</Provider>
)
assert.ok(wrapper)
const tokenInputInstance = wrapper.find(TokenInput).dive()
assert.equal(tokenInputInstance.state('decimalValue'), 0)
assert.equal(tokenInputInstance.state('hexValue'), undefined)
assert.equal(tokenInputInstance.find(UnitInput).props().value, 0)
tokenInputInstance.setProps({ value: '2710' })
tokenInputInstance.update()
assert.equal(tokenInputInstance.state('decimalValue'), 1)
assert.equal(tokenInputInstance.state('hexValue'), '2710')
assert.equal(tokenInputInstance.find(UnitInput).props().value, 1)
})
})
})

View File

@ -0,0 +1,129 @@
import assert from 'assert'
import proxyquire from 'proxyquire'
let mapStateToProps, mergeProps
proxyquire('../token-input.container.js', {
'react-redux': {
connect: (ms, md, mp) => {
mapStateToProps = ms
mergeProps = mp
return () => ({})
},
},
})
describe('TokenInput container', () => {
describe('mapStateToProps()', () => {
it('should return the correct props when send is empty', () => {
const mockState = {
metamask: {
currentCurrency: 'usd',
tokens: [
{
address: '0x1',
decimals: '4',
symbol: 'ABC',
},
],
selectedTokenAddress: '0x1',
contractExchangeRates: {},
send: {},
},
}
assert.deepEqual(mapStateToProps(mockState), {
currentCurrency: 'usd',
selectedToken: {
address: '0x1',
decimals: '4',
symbol: 'ABC',
},
selectedTokenExchangeRate: 0,
})
})
it('should return the correct props when selectedTokenAddress is not found and send is populated', () => {
const mockState = {
metamask: {
currentCurrency: 'usd',
tokens: [
{
address: '0x1',
decimals: '4',
symbol: 'ABC',
},
],
selectedTokenAddress: '0x2',
contractExchangeRates: {},
send: {
token: { address: 'test' },
},
},
}
assert.deepEqual(mapStateToProps(mockState), {
currentCurrency: 'usd',
selectedToken: {
address: 'test',
},
selectedTokenExchangeRate: 0,
})
})
it('should return the correct props when contractExchangeRates is populated', () => {
const mockState = {
metamask: {
currentCurrency: 'usd',
tokens: [
{
address: '0x1',
decimals: '4',
symbol: 'ABC',
},
],
selectedTokenAddress: '0x1',
contractExchangeRates: {
'0x1': 5,
},
send: {},
},
}
assert.deepEqual(mapStateToProps(mockState), {
currentCurrency: 'usd',
selectedToken: {
address: '0x1',
decimals: '4',
symbol: 'ABC',
},
selectedTokenExchangeRate: 5,
})
})
})
describe('mergeProps()', () => {
it('should return the correct props', () => {
const mockStateProps = {
currentCurrency: 'usd',
selectedToken: {
address: '0x1',
decimals: '4',
symbol: 'ABC',
},
selectedTokenExchangeRate: 5,
}
assert.deepEqual(mergeProps(mockStateProps, {}, {}), {
currentCurrency: 'usd',
selectedToken: {
address: '0x1',
decimals: '4',
symbol: 'ABC',
},
selectedTokenExchangeRate: 5,
suffix: 'ABC',
})
})
})
})

View File

@ -0,0 +1,136 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import UnitInput from '../unit-input'
import CurrencyDisplay from '../currency-display'
import { getWeiHexFromDecimalValue } from '../../helpers/conversions.util'
import ethUtil from 'ethereumjs-util'
import { conversionUtil, multiplyCurrencies } from '../../conversion-util'
import { ETH } from '../../constants/common'
/**
* Component that allows user to enter token values as a number, and props receive a converted
* hex value. props.value, used as a default or forced value, should be a hex value, which
* gets converted into a decimal value.
*/
export default class TokenInput extends PureComponent {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
currentCurrency: PropTypes.string,
onChange: PropTypes.func,
onBlur: PropTypes.func,
value: PropTypes.string,
suffix: PropTypes.string,
showFiat: PropTypes.bool,
selectedToken: PropTypes.object,
selectedTokenExchangeRate: PropTypes.number,
}
constructor (props) {
super(props)
const { value: hexValue } = props
const decimalValue = hexValue ? this.getDecimalValue(props) : 0
this.state = {
decimalValue,
hexValue,
}
}
componentDidUpdate (prevProps) {
const { value: prevPropsHexValue } = prevProps
const { value: propsHexValue } = this.props
const { hexValue: stateHexValue } = this.state
if (prevPropsHexValue !== propsHexValue && propsHexValue !== stateHexValue) {
const decimalValue = this.getDecimalValue(this.props)
this.setState({ hexValue: propsHexValue, decimalValue })
}
}
getDecimalValue (props) {
const { value: hexValue, selectedToken: { decimals, symbol } = {} } = props
const multiplier = Math.pow(10, Number(decimals || 0))
const decimalValueString = conversionUtil(ethUtil.addHexPrefix(hexValue), {
fromNumericBase: 'hex',
toNumericBase: 'dec',
toCurrency: symbol,
conversionRate: multiplier,
invertConversionRate: true,
})
return Number(decimalValueString) || 0
}
handleChange = decimalValue => {
const { selectedToken: { decimals } = {}, onChange } = this.props
const multiplier = Math.pow(10, Number(decimals || 0))
const hexValue = multiplyCurrencies(decimalValue || 0, multiplier, { toNumericBase: 'hex' })
this.setState({ hexValue, decimalValue })
onChange(hexValue)
}
handleBlur = () => {
this.props.onBlur(this.state.hexValue)
}
renderConversionComponent () {
const { selectedTokenExchangeRate, showFiat, currentCurrency } = this.props
const { decimalValue } = this.state
let currency, numberOfDecimals
if (showFiat) {
// Display Fiat
currency = currentCurrency
numberOfDecimals = 2
} else {
// Display ETH
currency = ETH
numberOfDecimals = 6
}
const decimalEthValue = (decimalValue * selectedTokenExchangeRate) || 0
const hexWeiValue = getWeiHexFromDecimalValue({
value: decimalEthValue,
fromCurrency: ETH,
fromDenomination: ETH,
})
return selectedTokenExchangeRate
? (
<CurrencyDisplay
className="currency-input__conversion-component"
currency={currency}
value={hexWeiValue}
numberOfDecimals={numberOfDecimals}
/>
) : (
<div className="currency-input__conversion-component">
{ this.context.t('noConversionRateAvailable') }
</div>
)
}
render () {
const { suffix, ...restProps } = this.props
const { decimalValue } = this.state
return (
<UnitInput
{...restProps}
suffix={suffix}
onChange={this.handleChange}
onBlur={this.handleBlur}
value={decimalValue}
>
{ this.renderConversionComponent() }
</UnitInput>
)
}
}

View File

@ -0,0 +1,27 @@
import { connect } from 'react-redux'
import TokenInput from './token-input.component'
import { getSelectedToken, getSelectedTokenExchangeRate } from '../../selectors'
const mapStateToProps = state => {
const { metamask: { currentCurrency } } = state
return {
currentCurrency,
selectedToken: getSelectedToken(state),
selectedTokenExchangeRate: getSelectedTokenExchangeRate(state),
}
}
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { selectedToken } = stateProps
const suffix = selectedToken && selectedToken.symbol
return {
...stateProps,
...dispatchProps,
...ownProps,
suffix,
}
}
export default connect(mapStateToProps, null, mergeProps)(TokenInput)

View File

@ -4,8 +4,9 @@ import classnames from 'classnames'
import TransactionBreakdownRow from './transaction-breakdown-row'
import Card from '../card'
import CurrencyDisplay from '../currency-display'
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
import HexToDecimal from '../hex-to-decimal'
import { ETH, GWEI } from '../../constants/common'
import { ETH, GWEI, PRIMARY, SECONDARY } from '../../constants/common'
import { getHexGasTotal } from '../../helpers/confirm-transaction/util'
import { sumHexes } from '../../helpers/transactions.util'
@ -40,9 +41,9 @@ export default class TransactionBreakdown extends PureComponent {
className="transaction-breakdown__card"
>
<TransactionBreakdownRow title={t('amount')}>
<CurrencyDisplay
<UserPreferencedCurrencyDisplay
className="transaction-breakdown__value"
currency={ETH}
type={PRIMARY}
value={value}
/>
</TransactionBreakdownRow>
@ -79,14 +80,14 @@ export default class TransactionBreakdown extends PureComponent {
</TransactionBreakdownRow>
<TransactionBreakdownRow title={t('total')}>
<div>
<CurrencyDisplay
<UserPreferencedCurrencyDisplay
className="transaction-breakdown__value transaction-breakdown__value--eth-total"
currency={ETH}
type={PRIMARY}
value={totalInHex}
numberOfDecimals={6}
/>
<CurrencyDisplay
<UserPreferencedCurrencyDisplay
className="transaction-breakdown__value"
type={SECONDARY}
value={totalInHex}
/>
</div>

View File

@ -4,12 +4,12 @@ import classnames from 'classnames'
import Identicon from '../identicon'
import TransactionStatus from '../transaction-status'
import TransactionAction from '../transaction-action'
import CurrencyDisplay from '../currency-display'
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
import TokenCurrencyDisplay from '../token-currency-display'
import TransactionListItemDetails from '../transaction-list-item-details'
import { CONFIRM_TRANSACTION_ROUTE } from '../../routes'
import { UNAPPROVED_STATUS, TOKEN_METHOD_TRANSFER } from '../../constants/transactions'
import { ETH } from '../../constants/common'
import { PRIMARY, SECONDARY } from '../../constants/common'
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../app/scripts/lib/enums'
import { getStatusKey } from '../../helpers/transactions.util'
@ -103,12 +103,11 @@ export default class TransactionListItem extends PureComponent {
prefix="-"
/>
) : (
<CurrencyDisplay
<UserPreferencedCurrencyDisplay
className="transaction-list-item__amount transaction-list-item__amount--primary"
value={value}
type={PRIMARY}
prefix="-"
numberOfDecimals={2}
currency={ETH}
/>
)
}
@ -119,10 +118,11 @@ export default class TransactionListItem extends PureComponent {
return token
? null
: (
<CurrencyDisplay
<UserPreferencedCurrencyDisplay
className="transaction-list-item__amount transaction-list-item__amount--secondary"
prefix="-"
value={value}
prefix="-"
type={SECONDARY}
/>
)
}

View File

@ -3,7 +3,7 @@ import assert from 'assert'
import { shallow } from 'enzyme'
import sinon from 'sinon'
import TokenBalance from '../../token-balance'
import CurrencyDisplay from '../../currency-display'
import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display'
import { SEND_ROUTE } from '../../../routes'
import TransactionViewBalance from '../transaction-view-balance.component'
@ -35,7 +35,7 @@ describe('TransactionViewBalance Component', () => {
assert.equal(wrapper.find('.transaction-view-balance').length, 1)
assert.equal(wrapper.find('.transaction-view-balance__button').length, 2)
assert.equal(wrapper.find(CurrencyDisplay).length, 2)
assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2)
const buttons = wrapper.find('.transaction-view-balance__buttons')
assert.equal(propsMethodSpies.showDepositModal.callCount, 0)

View File

@ -3,9 +3,9 @@ import PropTypes from 'prop-types'
import Button from '../button'
import Identicon from '../identicon'
import TokenBalance from '../token-balance'
import CurrencyDisplay from '../currency-display'
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
import { SEND_ROUTE } from '../../routes'
import { ETH } from '../../constants/common'
import { PRIMARY, SECONDARY } from '../../constants/common'
export default class TransactionViewBalance extends PureComponent {
static contextTypes = {
@ -33,15 +33,17 @@ export default class TransactionViewBalance extends PureComponent {
/>
) : (
<div className="transaction-view-balance__balance">
<CurrencyDisplay
<UserPreferencedCurrencyDisplay
className="transaction-view-balance__primary-balance"
value={balance}
currency={ETH}
numberOfDecimals={3}
type={PRIMARY}
ethNumberOfDecimals={3}
/>
<CurrencyDisplay
<UserPreferencedCurrencyDisplay
className="transaction-view-balance__secondary-balance"
value={balance}
type={SECONDARY}
ethNumberOfDecimals={3}
/>
</div>
)

View File

@ -0,0 +1 @@
export { default } from './unit-input.component'

View File

@ -0,0 +1,44 @@
.unit-input {
min-height: 54px;
border: 1px solid #dedede;
border-radius: 4px;
background-color: #fff;
color: #4d4d4d;
font-size: 1rem;
padding: 8px 10px;
position: relative;
input[type="number"] {
-moz-appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
-moz-appearance: none;
display: none;
}
input[type="number"]:hover::-webkit-inner-spin-button {
-webkit-appearance: none;
-moz-appearance: none;
display: none;
}
&__input {
color: #4d4d4d;
font-size: 1rem;
font-family: Roboto;
border: none;
outline: 0 !important;
max-width: 22ch;
}
&__input-container {
display: flex;
align-items: center;
}
&--error {
border-color: $red;
}
}

View File

@ -0,0 +1,146 @@
import React from 'react'
import assert from 'assert'
import { shallow, mount } from 'enzyme'
import sinon from 'sinon'
import UnitInput from '../unit-input.component'
describe('UnitInput Component', () => {
describe('rendering', () => {
it('should render properly without a suffix', () => {
const wrapper = shallow(
<UnitInput />
)
assert.ok(wrapper)
assert.equal(wrapper.find('.unit-input__suffix').length, 0)
})
it('should render properly with a suffix', () => {
const wrapper = shallow(
<UnitInput
suffix="ETH"
/>
)
assert.ok(wrapper)
assert.equal(wrapper.find('.unit-input__suffix').length, 1)
assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH')
})
it('should render properly with a child omponent', () => {
const wrapper = shallow(
<UnitInput>
<div className="testing">
TESTCOMPONENT
</div>
</UnitInput>
)
assert.ok(wrapper)
assert.equal(wrapper.find('.testing').length, 1)
assert.equal(wrapper.find('.testing').text(), 'TESTCOMPONENT')
})
it('should render with an error class when props.error === true', () => {
const wrapper = shallow(
<UnitInput
error
/>
)
assert.ok(wrapper)
assert.equal(wrapper.find('.unit-input--error').length, 1)
})
})
describe('handling actions', () => {
const handleChangeSpy = sinon.spy()
const handleBlurSpy = sinon.spy()
afterEach(() => {
handleChangeSpy.resetHistory()
handleBlurSpy.resetHistory()
})
it('should focus the input on component click', () => {
const wrapper = mount(
<UnitInput />
)
assert.ok(wrapper)
const handleFocusSpy = sinon.spy(wrapper.instance(), 'handleFocus')
wrapper.instance().forceUpdate()
wrapper.update()
assert.equal(handleFocusSpy.callCount, 0)
wrapper.find('.unit-input').simulate('click')
assert.equal(handleFocusSpy.callCount, 1)
})
it('should call onChange on input changes with the value', () => {
const wrapper = mount(
<UnitInput
onChange={handleChangeSpy}
/>
)
assert.ok(wrapper)
assert.equal(handleChangeSpy.callCount, 0)
const input = wrapper.find('input')
input.simulate('change', { target: { value: 123 } })
assert.equal(handleChangeSpy.callCount, 1)
assert.ok(handleChangeSpy.calledWith(123))
assert.equal(wrapper.state('value'), 123)
})
it('should call onBlur on blur with the value', () => {
const wrapper = mount(
<UnitInput
onChange={handleChangeSpy}
onBlur={handleBlurSpy}
/>
)
assert.ok(wrapper)
assert.equal(handleChangeSpy.callCount, 0)
assert.equal(handleBlurSpy.callCount, 0)
const input = wrapper.find('input')
input.simulate('change', { target: { value: 123 } })
assert.equal(handleChangeSpy.callCount, 1)
assert.ok(handleChangeSpy.calledWith(123))
assert.equal(wrapper.state('value'), 123)
input.simulate('blur')
assert.equal(handleBlurSpy.callCount, 1)
assert.ok(handleBlurSpy.calledWith(123))
})
it('should set the component state value with props.value', () => {
const wrapper = mount(
<UnitInput
value={123}
/>
)
assert.ok(wrapper)
assert.equal(wrapper.state('value'), 123)
})
it('should update the component state value with props.value', () => {
const wrapper = mount(
<UnitInput
onChange={handleChangeSpy}
/>
)
assert.ok(wrapper)
assert.equal(handleChangeSpy.callCount, 0)
const input = wrapper.find('input')
input.simulate('change', { target: { value: 123 } })
assert.equal(wrapper.state('value'), 123)
assert.equal(handleChangeSpy.callCount, 1)
assert.ok(handleChangeSpy.calledWith(123))
wrapper.setProps({ value: 456 })
assert.equal(wrapper.state('value'), 456)
assert.equal(handleChangeSpy.callCount, 1)
})
})
})

View File

@ -0,0 +1,104 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { removeLeadingZeroes } from '../send/send.utils'
/**
* Component that attaches a suffix or unit of measurement trailing user input, ex. 'ETH'. Also
* allows rendering a child component underneath the input to, for example, display conversions of
* the shown suffix.
*/
export default class UnitInput extends PureComponent {
static propTypes = {
children: PropTypes.node,
error: PropTypes.bool,
onBlur: PropTypes.func,
onChange: PropTypes.func,
placeholder: PropTypes.string,
suffix: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
}
static defaultProps = {
placeholder: '0',
}
constructor (props) {
super(props)
this.state = {
value: props.value || '',
}
}
componentDidUpdate (prevProps) {
const { value: prevPropsValue } = prevProps
const { value: propsValue } = this.props
const { value: stateValue } = this.state
if (prevPropsValue !== propsValue && propsValue !== stateValue) {
this.setState({ value: propsValue })
}
}
handleFocus = () => {
this.unitInput.focus()
}
handleChange = event => {
const { value: userInput } = event.target
let value = userInput
if (userInput.length && userInput.length > 1) {
value = removeLeadingZeroes(userInput)
}
this.setState({ value })
this.props.onChange(value)
}
handleBlur = event => {
const { onBlur } = this.props
typeof onBlur === 'function' && onBlur(this.state.value)
}
getInputWidth (value) {
const valueString = String(value)
const valueLength = valueString.length || 1
const decimalPointDeficit = valueString.match(/\./) ? -0.5 : 0
return (valueLength + decimalPointDeficit + 0.75) + 'ch'
}
render () {
const { error, placeholder, suffix, children } = this.props
const { value } = this.state
return (
<div
className={classnames('unit-input', { 'unit-input--error': error })}
onClick={this.handleFocus}
>
<div className="unit-input__input-container">
<input
type="number"
className="unit-input__input"
value={value}
placeholder={placeholder}
onChange={this.handleChange}
onBlur={this.handleBlur}
style={{ width: this.getInputWidth(value) }}
ref={ref => { this.unitInput = ref }}
/>
{
suffix && (
<div className="unit-input__suffix">
{ suffix }
</div>
)
}
</div>
{ children }
</div>
)
}
}

View File

@ -0,0 +1 @@
export { default } from './user-preferenced-currency-display.container'

View File

@ -0,0 +1,34 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display.component'
import CurrencyDisplay from '../../currency-display'
describe('UserPreferencedCurrencyDisplay Component', () => {
describe('rendering', () => {
it('should render properly', () => {
const wrapper = shallow(
<UserPreferencedCurrencyDisplay />
)
assert.ok(wrapper)
assert.equal(wrapper.find(CurrencyDisplay).length, 1)
})
it('should pass all props to the CurrencyDisplay child component', () => {
const wrapper = shallow(
<UserPreferencedCurrencyDisplay
prop1={true}
prop2="test"
prop3={1}
/>
)
assert.ok(wrapper)
assert.equal(wrapper.find(CurrencyDisplay).length, 1)
assert.equal(wrapper.find(CurrencyDisplay).props().prop1, true)
assert.equal(wrapper.find(CurrencyDisplay).props().prop2, 'test')
assert.equal(wrapper.find(CurrencyDisplay).props().prop3, 1)
})
})
})

View File

@ -0,0 +1,105 @@
import assert from 'assert'
import proxyquire from 'proxyquire'
let mapStateToProps, mergeProps
proxyquire('../user-preferenced-currency-display.container.js', {
'react-redux': {
connect: (ms, md, mp) => {
mapStateToProps = ms
mergeProps = mp
return () => ({})
},
},
})
describe('UserPreferencedCurrencyDisplay container', () => {
describe('mapStateToProps()', () => {
it('should return the correct props', () => {
const mockState = {
metamask: {
preferences: {
useETHAsPrimaryCurrency: true,
},
},
}
assert.deepEqual(mapStateToProps(mockState), {
useETHAsPrimaryCurrency: true,
})
})
})
describe('mergeProps()', () => {
it('should return the correct props', () => {
const mockDispatchProps = {}
const tests = [
{
stateProps: {
useETHAsPrimaryCurrency: true,
},
ownProps: {
type: 'PRIMARY',
},
result: {
currency: 'ETH',
numberOfDecimals: 6,
prefix: undefined,
},
},
{
stateProps: {
useETHAsPrimaryCurrency: false,
},
ownProps: {
type: 'PRIMARY',
},
result: {
currency: undefined,
numberOfDecimals: 2,
prefix: undefined,
},
},
{
stateProps: {
useETHAsPrimaryCurrency: true,
},
ownProps: {
type: 'SECONDARY',
fiatNumberOfDecimals: 4,
fiatPrefix: '-',
},
result: {
currency: undefined,
numberOfDecimals: 4,
prefix: '-',
},
},
{
stateProps: {
useETHAsPrimaryCurrency: false,
},
ownProps: {
type: 'SECONDARY',
fiatNumberOfDecimals: 4,
numberOfDecimals: 3,
fiatPrefix: 'a',
prefix: 'b',
},
result: {
currency: 'ETH',
numberOfDecimals: 3,
prefix: 'b',
},
},
]
tests.forEach(({ stateProps, ownProps, result }) => {
assert.deepEqual(mergeProps({ ...stateProps }, mockDispatchProps, { ...ownProps }), {
...result,
})
})
})
})
})

View File

@ -0,0 +1,45 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { PRIMARY, SECONDARY, ETH } from '../../constants/common'
import CurrencyDisplay from '../currency-display'
export default class UserPreferencedCurrencyDisplay extends PureComponent {
static propTypes = {
className: PropTypes.string,
prefix: PropTypes.string,
value: PropTypes.string,
numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
hideLabel: PropTypes.bool,
style: PropTypes.object,
showEthLogo: PropTypes.bool,
ethLogoHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
// Used in container
type: PropTypes.oneOf([PRIMARY, SECONDARY]),
ethNumberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
fiatNumberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
ethPrefix: PropTypes.string,
fiatPrefix: PropTypes.string,
// From container
currency: PropTypes.string,
}
renderEthLogo () {
const { currency, showEthLogo, ethLogoHeight = 12 } = this.props
return currency === ETH && showEthLogo && (
<img
src="/images/eth.svg"
height={ethLogoHeight}
/>
)
}
render () {
return (
<CurrencyDisplay
{...this.props}
prefixComponent={this.renderEthLogo()}
/>
)
}
}

View File

@ -0,0 +1,52 @@
import { connect } from 'react-redux'
import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display.component'
import { preferencesSelector } from '../../selectors'
import { ETH, PRIMARY, SECONDARY } from '../../constants/common'
const mapStateToProps = (state, ownProps) => {
const { useETHAsPrimaryCurrency } = preferencesSelector(state)
return {
useETHAsPrimaryCurrency,
}
}
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { useETHAsPrimaryCurrency, ...restStateProps } = stateProps
const {
type,
numberOfDecimals: propsNumberOfDecimals,
ethNumberOfDecimals,
fiatNumberOfDecimals,
ethPrefix,
fiatPrefix,
prefix: propsPrefix,
...restOwnProps
} = ownProps
let currency, numberOfDecimals, prefix
if (type === PRIMARY && useETHAsPrimaryCurrency ||
type === SECONDARY && !useETHAsPrimaryCurrency) {
// Display ETH
currency = ETH
numberOfDecimals = propsNumberOfDecimals || ethNumberOfDecimals || 6
prefix = propsPrefix || ethPrefix
} else if (type === SECONDARY && useETHAsPrimaryCurrency ||
type === PRIMARY && !useETHAsPrimaryCurrency) {
// Display Fiat
numberOfDecimals = propsNumberOfDecimals || fiatNumberOfDecimals || 2
prefix = propsPrefix || fiatPrefix
}
return {
...restStateProps,
...dispatchProps,
...restOwnProps,
currency,
numberOfDecimals,
prefix,
}
}
export default connect(mapStateToProps, null, mergeProps)(UserPreferencedCurrencyDisplay)

View File

@ -0,0 +1 @@
export { default } from './user-preferenced-currency-input.container'

View File

@ -0,0 +1,32 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import UserPreferencedCurrencyInput from '../user-preferenced-currency-input.component'
import CurrencyInput from '../../currency-input'
describe('UserPreferencedCurrencyInput Component', () => {
describe('rendering', () => {
it('should render properly', () => {
const wrapper = shallow(
<UserPreferencedCurrencyInput />
)
assert.ok(wrapper)
assert.equal(wrapper.find(CurrencyInput).length, 1)
})
it('should render useFiat for CurrencyInput based on preferences.useETHAsPrimaryCurrency', () => {
const wrapper = shallow(
<UserPreferencedCurrencyInput
useETHAsPrimaryCurrency
/>
)
assert.ok(wrapper)
assert.equal(wrapper.find(CurrencyInput).length, 1)
assert.equal(wrapper.find(CurrencyInput).props().useFiat, false)
wrapper.setProps({ useETHAsPrimaryCurrency: false })
assert.equal(wrapper.find(CurrencyInput).props().useFiat, true)
})
})
})

View File

@ -0,0 +1,31 @@
import assert from 'assert'
import proxyquire from 'proxyquire'
let mapStateToProps
proxyquire('../user-preferenced-currency-input.container.js', {
'react-redux': {
connect: ms => {
mapStateToProps = ms
return () => ({})
},
},
})
describe('UserPreferencedCurrencyInput container', () => {
describe('mapStateToProps()', () => {
it('should return the correct props', () => {
const mockState = {
metamask: {
preferences: {
useETHAsPrimaryCurrency: true,
},
},
}
assert.deepEqual(mapStateToProps(mockState), {
useETHAsPrimaryCurrency: true,
})
})
})
})

View File

@ -0,0 +1,20 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import CurrencyInput from '../currency-input'
export default class UserPreferencedCurrencyInput extends PureComponent {
static propTypes = {
useETHAsPrimaryCurrency: PropTypes.bool,
}
render () {
const { useETHAsPrimaryCurrency, ...restProps } = this.props
return (
<CurrencyInput
{...restProps}
useFiat={!useETHAsPrimaryCurrency}
/>
)
}
}

View File

@ -0,0 +1,13 @@
import { connect } from 'react-redux'
import UserPreferencedCurrencyInput from './user-preferenced-currency-input.component'
import { preferencesSelector } from '../../selectors'
const mapStateToProps = state => {
const { useETHAsPrimaryCurrency } = preferencesSelector(state)
return {
useETHAsPrimaryCurrency,
}
}
export default connect(mapStateToProps)(UserPreferencedCurrencyInput)

View File

@ -0,0 +1 @@
export { default } from './user-preferenced-token-input.container'

View File

@ -0,0 +1,32 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import UserPreferencedTokenInput from '../user-preferenced-token-input.component'
import TokenInput from '../../token-input'
describe('UserPreferencedCurrencyInput Component', () => {
describe('rendering', () => {
it('should render properly', () => {
const wrapper = shallow(
<UserPreferencedTokenInput />
)
assert.ok(wrapper)
assert.equal(wrapper.find(TokenInput).length, 1)
})
it('should render showFiat for TokenInput based on preferences.useETHAsPrimaryCurrency', () => {
const wrapper = shallow(
<UserPreferencedTokenInput
useETHAsPrimaryCurrency
/>
)
assert.ok(wrapper)
assert.equal(wrapper.find(TokenInput).length, 1)
assert.equal(wrapper.find(TokenInput).props().showFiat, false)
wrapper.setProps({ useETHAsPrimaryCurrency: false })
assert.equal(wrapper.find(TokenInput).props().showFiat, true)
})
})
})

View File

@ -0,0 +1,31 @@
import assert from 'assert'
import proxyquire from 'proxyquire'
let mapStateToProps
proxyquire('../user-preferenced-token-input.container.js', {
'react-redux': {
connect: ms => {
mapStateToProps = ms
return () => ({})
},
},
})
describe('UserPreferencedTokenInput container', () => {
describe('mapStateToProps()', () => {
it('should return the correct props', () => {
const mockState = {
metamask: {
preferences: {
useETHAsPrimaryCurrency: true,
},
},
}
assert.deepEqual(mapStateToProps(mockState), {
useETHAsPrimaryCurrency: true,
})
})
})
})

View File

@ -0,0 +1,20 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import TokenInput from '../token-input'
export default class UserPreferencedTokenInput extends PureComponent {
static propTypes = {
useETHAsPrimaryCurrency: PropTypes.bool,
}
render () {
const { useETHAsPrimaryCurrency, ...restProps } = this.props
return (
<TokenInput
{...restProps}
showFiat={!useETHAsPrimaryCurrency}
/>
)
}
}

View File

@ -0,0 +1,13 @@
import { connect } from 'react-redux'
import UserPreferencedTokenInput from './user-preferenced-token-input.component'
import { preferencesSelector } from '../../selectors'
const mapStateToProps = state => {
const { useETHAsPrimaryCurrency } = preferencesSelector(state)
return {
useETHAsPrimaryCurrency,
}
}
export default connect(mapStateToProps)(UserPreferencedTokenInput)

View File

@ -1,3 +1,6 @@
export const ETH = 'ETH'
export const GWEI = 'GWEI'
export const WEI = 'WEI'
export const PRIMARY = 'PRIMARY'
export const SECONDARY = 'SECONDARY'

View File

@ -14,7 +14,13 @@ import {
hexGreaterThan,
} from '../helpers/confirm-transaction/util'
import { getTokenData, getMethodData, isSmartContractAddress } from '../helpers/transactions.util'
import {
getTokenData,
getMethodData,
isSmartContractAddress,
sumHexes,
} from '../helpers/transactions.util'
import { getSymbolAndDecimals } from '../token-util'
import { conversionUtil } from '../conversion-util'
@ -31,7 +37,6 @@ const CLEAR_CONFIRM_TRANSACTION = createActionType('CLEAR_CONFIRM_TRANSACTION')
const UPDATE_TRANSACTION_AMOUNTS = createActionType('UPDATE_TRANSACTION_AMOUNTS')
const UPDATE_TRANSACTION_FEES = createActionType('UPDATE_TRANSACTION_FEES')
const UPDATE_TRANSACTION_TOTALS = createActionType('UPDATE_TRANSACTION_TOTALS')
const UPDATE_HEX_GAS_TOTAL = createActionType('UPDATE_HEX_GAS_TOTAL')
const UPDATE_TOKEN_PROPS = createActionType('UPDATE_TOKEN_PROPS')
const UPDATE_NONCE = createActionType('UPDATE_NONCE')
const UPDATE_TO_SMART_CONTRACT = createActionType('UPDATE_TO_SMART_CONTRACT')
@ -53,7 +58,9 @@ const initState = {
ethTransactionAmount: '',
ethTransactionFee: '',
ethTransactionTotal: '',
hexGasTotal: '',
hexTransactionAmount: '',
hexTransactionFee: '',
hexTransactionTotal: '',
nonce: '',
toSmartContract: false,
fetchingData: false,
@ -99,30 +106,28 @@ export default function reducer ({ confirmTransaction: confirmState = initState
methodData: {},
}
case UPDATE_TRANSACTION_AMOUNTS:
const { fiatTransactionAmount, ethTransactionAmount } = action.payload
const { fiatTransactionAmount, ethTransactionAmount, hexTransactionAmount } = action.payload
return {
...confirmState,
fiatTransactionAmount: fiatTransactionAmount || confirmState.fiatTransactionAmount,
ethTransactionAmount: ethTransactionAmount || confirmState.ethTransactionAmount,
hexTransactionAmount: hexTransactionAmount || confirmState.hexTransactionAmount,
}
case UPDATE_TRANSACTION_FEES:
const { fiatTransactionFee, ethTransactionFee } = action.payload
const { fiatTransactionFee, ethTransactionFee, hexTransactionFee } = action.payload
return {
...confirmState,
fiatTransactionFee: fiatTransactionFee || confirmState.fiatTransactionFee,
ethTransactionFee: ethTransactionFee || confirmState.ethTransactionFee,
hexTransactionFee: hexTransactionFee || confirmState.hexTransactionFee,
}
case UPDATE_TRANSACTION_TOTALS:
const { fiatTransactionTotal, ethTransactionTotal } = action.payload
const { fiatTransactionTotal, ethTransactionTotal, hexTransactionTotal } = action.payload
return {
...confirmState,
fiatTransactionTotal: fiatTransactionTotal || confirmState.fiatTransactionTotal,
ethTransactionTotal: ethTransactionTotal || confirmState.ethTransactionTotal,
}
case UPDATE_HEX_GAS_TOTAL:
return {
...confirmState,
hexGasTotal: action.payload,
hexTransactionTotal: hexTransactionTotal || confirmState.hexTransactionTotal,
}
case UPDATE_TOKEN_PROPS:
const { tokenSymbol = '', tokenDecimals = '' } = action.payload
@ -222,13 +227,6 @@ export function updateTransactionTotals (totals) {
}
}
export function updateHexGasTotal (hexGasTotal) {
return {
type: UPDATE_HEX_GAS_TOTAL,
payload: hexGasTotal,
}
}
export function updateTokenProps (tokenProps) {
return {
type: UPDATE_TOKEN_PROPS,
@ -297,7 +295,7 @@ export function updateTxDataAndCalculate (txData) {
dispatch(updateTxData(txData))
const { txParams: { value, gas: gasLimit = '0x0', gasPrice = '0x0' } = {} } = txData
const { txParams: { value = '0x0', gas: gasLimit = '0x0', gasPrice = '0x0' } = {} } = txData
const fiatTransactionAmount = getValueFromWeiHex({
value, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2,
@ -306,31 +304,39 @@ export function updateTxDataAndCalculate (txData) {
value, toCurrency: 'ETH', conversionRate, numberOfDecimals: 6,
})
dispatch(updateTransactionAmounts({ fiatTransactionAmount, ethTransactionAmount }))
dispatch(updateTransactionAmounts({
fiatTransactionAmount,
ethTransactionAmount,
hexTransactionAmount: value,
}))
const hexGasTotal = getHexGasTotal({ gasLimit, gasPrice })
dispatch(updateHexGasTotal(hexGasTotal))
const hexTransactionFee = getHexGasTotal({ gasLimit, gasPrice })
const fiatTransactionFee = getTransactionFee({
value: hexGasTotal,
value: hexTransactionFee,
toCurrency: currentCurrency,
numberOfDecimals: 2,
conversionRate,
})
const ethTransactionFee = getTransactionFee({
value: hexGasTotal,
value: hexTransactionFee,
toCurrency: 'ETH',
numberOfDecimals: 6,
conversionRate,
})
dispatch(updateTransactionFees({ fiatTransactionFee, ethTransactionFee }))
dispatch(updateTransactionFees({ fiatTransactionFee, ethTransactionFee, hexTransactionFee }))
const fiatTransactionTotal = addFiat(fiatTransactionFee, fiatTransactionAmount)
const ethTransactionTotal = addEth(ethTransactionFee, ethTransactionAmount)
console.log('HIHIH', value, hexTransactionFee)
const hexTransactionTotal = sumHexes(value, hexTransactionFee)
dispatch(updateTransactionTotals({ fiatTransactionTotal, ethTransactionTotal }))
dispatch(updateTransactionTotals({
fiatTransactionTotal,
ethTransactionTotal,
hexTransactionTotal,
}))
}
}

View File

@ -19,7 +19,9 @@ const initialState = {
ethTransactionAmount: '',
ethTransactionFee: '',
ethTransactionTotal: '',
hexGasTotal: '',
hexTransactionAmount: '',
hexTransactionFee: '',
hexTransactionTotal: '',
nonce: '',
toSmartContract: false,
fetchingData: false,
@ -34,7 +36,6 @@ const CLEAR_METHOD_DATA = 'metamask/confirm-transaction/CLEAR_METHOD_DATA'
const UPDATE_TRANSACTION_AMOUNTS = 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS'
const UPDATE_TRANSACTION_FEES = 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES'
const UPDATE_TRANSACTION_TOTALS = 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS'
const UPDATE_HEX_GAS_TOTAL = 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL'
const UPDATE_TOKEN_PROPS = 'metamask/confirm-transaction/UPDATE_TOKEN_PROPS'
const UPDATE_NONCE = 'metamask/confirm-transaction/UPDATE_NONCE'
const UPDATE_TO_SMART_CONTRACT = 'metamask/confirm-transaction/UPDATE_TO_SMART_CONTRACT'
@ -65,7 +66,9 @@ describe('Confirm Transaction Duck', () => {
ethTransactionAmount: '1',
ethTransactionFee: '0.000021',
ethTransactionTotal: '469.27',
hexGasTotal: '0x1319718a5000',
hexTransactionAmount: '',
hexTransactionFee: '0x1319718a5000',
hexTransactionTotal: '',
nonce: '0x0',
toSmartContract: false,
fetchingData: false,
@ -186,12 +189,14 @@ describe('Confirm Transaction Duck', () => {
payload: {
fiatTransactionAmount: '123.45',
ethTransactionAmount: '.5',
hexTransactionAmount: '0x1',
},
}),
{
...mockState.confirmTransaction,
fiatTransactionAmount: '123.45',
ethTransactionAmount: '.5',
hexTransactionAmount: '0x1',
}
)
})
@ -203,12 +208,14 @@ describe('Confirm Transaction Duck', () => {
payload: {
fiatTransactionFee: '123.45',
ethTransactionFee: '.5',
hexTransactionFee: '0x1',
},
}),
{
...mockState.confirmTransaction,
fiatTransactionFee: '123.45',
ethTransactionFee: '.5',
hexTransactionFee: '0x1',
}
)
})
@ -220,25 +227,14 @@ describe('Confirm Transaction Duck', () => {
payload: {
fiatTransactionTotal: '123.45',
ethTransactionTotal: '.5',
hexTransactionTotal: '0x1',
},
}),
{
...mockState.confirmTransaction,
fiatTransactionTotal: '123.45',
ethTransactionTotal: '.5',
}
)
})
it('should update hexGasTotal when receiving an UPDATE_HEX_GAS_TOTAL action', () => {
assert.deepEqual(
ConfirmTransactionReducer(mockState, {
type: UPDATE_HEX_GAS_TOTAL,
payload: '0x0',
}),
{
...mockState.confirmTransaction,
hexGasTotal: '0x0',
hexTransactionTotal: '0x1',
}
)
})
@ -435,19 +431,6 @@ describe('Confirm Transaction Duck', () => {
)
})
it('should create an action to update hexGasTotal', () => {
const hexGasTotal = '0x0'
const expectedAction = {
type: UPDATE_HEX_GAS_TOTAL,
payload: hexGasTotal,
}
assert.deepEqual(
actions.updateHexGasTotal(hexGasTotal),
expectedAction
)
})
it('should create an action to update tokenProps', () => {
const tokenProps = {
tokenDecimals: '1',
@ -568,7 +551,6 @@ describe('Confirm Transaction Duck', () => {
const expectedActions = [
'metamask/confirm-transaction/UPDATE_TX_DATA',
'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS',
'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL',
'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES',
'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS',
]
@ -637,7 +619,6 @@ describe('Confirm Transaction Duck', () => {
const expectedActions = [
'metamask/confirm-transaction/UPDATE_TX_DATA',
'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS',
'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL',
'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES',
'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS',
]
@ -687,7 +668,6 @@ describe('Confirm Transaction Duck', () => {
const expectedActions = [
'metamask/confirm-transaction/UPDATE_TX_DATA',
'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS',
'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL',
'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES',
'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS',
]

View File

@ -61,3 +61,22 @@ export function getValueFromWeiHex ({
conversionRate,
})
}
export function getWeiHexFromDecimalValue ({
value,
fromCurrency,
conversionRate,
fromDenomination,
invertConversionRate,
}) {
return conversionUtil(value, {
fromNumericBase: 'dec',
toNumericBase: 'hex',
toCurrency: ETH,
fromCurrency,
conversionRate,
invertConversionRate,
fromDenomination,
toDenomination: WEI,
})
}

View File

@ -51,6 +51,9 @@ function reduceMetamask (state, action) {
isRevealingSeedWords: false,
welcomeScreenSeen: false,
currentLocale: '',
preferences: {
useETHAsPrimaryCurrency: true,
},
}, state.metamask)
switch (action.type) {
@ -365,6 +368,12 @@ function reduceMetamask (state, action) {
})
}
case actions.UPDATE_PREFERENCES: {
return extend(metamaskState, {
preferences: { ...action.payload },
})
}
default:
return metamaskState

View File

@ -33,6 +33,7 @@ const selectors = {
getSendMaxModeState,
getCurrentViewContext,
getTotalUnapprovedCount,
preferencesSelector,
}
module.exports = selectors
@ -195,3 +196,7 @@ function getTotalUnapprovedCount ({ metamask }) {
return Object.keys(unapprovedTxs).length + unapprovedMsgCount + unapprovedPersonalMsgCount +
unapprovedTypedMessagesCount
}
function preferencesSelector ({ metamask }) {
return metamask.preferences
}