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 typosfeature/default_network_editable
parent
8bccb88132
commit
badebe017f
After Width: | Height: | Size: 670 B |
@ -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) |
|
||||||
}) |
|
||||||
|
|
||||||
}) |
|
@ -0,0 +1,10 @@ |
|||||||
|
.currency-display-component { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
|
||||||
|
&__text { |
||||||
|
white-space: nowrap; |
||||||
|
overflow: hidden; |
||||||
|
text-overflow: ellipsis; |
||||||
|
} |
||||||
|
} |
@ -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> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -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) |
@ -0,0 +1 @@ |
|||||||
|
export { default } from './currency-input.container' |
@ -0,0 +1,7 @@ |
|||||||
|
.currency-input { |
||||||
|
&__conversion-component { |
||||||
|
font-size: 12px; |
||||||
|
line-height: 12px; |
||||||
|
padding-left: 1px; |
||||||
|
} |
||||||
|
} |
@ -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) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
@ -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', |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
@ -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), |
|
||||||
|
|
||||||
]) |
|
||||||
|
|
||||||
} |
|
||||||
|
|
@ -1 +0,0 @@ |
|||||||
export { default } from './currency-display.js' |
|
@ -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') |
|
||||||
}) |
|
||||||
|
|
||||||
}) |
|
@ -0,0 +1 @@ |
|||||||
|
export { default } from './token-input.container' |
@ -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) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
@ -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', |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
@ -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> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -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) |
@ -0,0 +1 @@ |
|||||||
|
export { default } from './unit-input.component' |
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
@ -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> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
export { default } from './user-preferenced-currency-display.container' |
@ -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) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
@ -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, |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
@ -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()} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -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) |
@ -0,0 +1 @@ |
|||||||
|
export { default } from './user-preferenced-currency-input.container' |
@ -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) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
@ -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, |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
@ -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} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -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) |
@ -0,0 +1 @@ |
|||||||
|
export { default } from './user-preferenced-token-input.container' |
@ -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) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
@ -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, |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
@ -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} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -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) |
@ -1,3 +1,6 @@ |
|||||||
export const ETH = 'ETH' |
export const ETH = 'ETH' |
||||||
export const GWEI = 'GWEI' |
export const GWEI = 'GWEI' |
||||||
export const WEI = 'WEI' |
export const WEI = 'WEI' |
||||||
|
|
||||||
|
export const PRIMARY = 'PRIMARY' |
||||||
|
export const SECONDARY = 'SECONDARY' |
||||||
|
Loading…
Reference in new issue