diff options
author | Thomas <thomas.b.huang@gmail.com> | 2018-10-31 20:55:25 +0800 |
---|---|---|
committer | Thomas <thomas.b.huang@gmail.com> | 2018-10-31 20:55:25 +0800 |
commit | 7531de14f9575e59b9d0a1b30cf0df46dbc4af67 (patch) | |
tree | d9268e89a9fd7e5c6bf3c7c82c195422d7ffa117 /ui/app/components/token-input | |
parent | 611a61b7bc576bb83a6b7851f26d00488fe060f2 (diff) | |
parent | ac079365e6b4cf8b19db127e6971fa694fa54fff (diff) | |
download | tangerine-wallet-browser-7531de14f9575e59b9d0a1b30cf0df46dbc4af67.tar tangerine-wallet-browser-7531de14f9575e59b9d0a1b30cf0df46dbc4af67.tar.gz tangerine-wallet-browser-7531de14f9575e59b9d0a1b30cf0df46dbc4af67.tar.bz2 tangerine-wallet-browser-7531de14f9575e59b9d0a1b30cf0df46dbc4af67.tar.lz tangerine-wallet-browser-7531de14f9575e59b9d0a1b30cf0df46dbc4af67.tar.xz tangerine-wallet-browser-7531de14f9575e59b9d0a1b30cf0df46dbc4af67.tar.zst tangerine-wallet-browser-7531de14f9575e59b9d0a1b30cf0df46dbc4af67.zip |
Merge branch 'develop' into reducers
Diffstat (limited to 'ui/app/components/token-input')
5 files changed, 601 insertions, 0 deletions
diff --git a/ui/app/components/token-input/index.js b/ui/app/components/token-input/index.js new file mode 100644 index 000000000..22c06111e --- /dev/null +++ b/ui/app/components/token-input/index.js @@ -0,0 +1 @@ +export { default } from './token-input.container' diff --git a/ui/app/components/token-input/tests/token-input.component.test.js b/ui/app/components/token-input/tests/token-input.component.test.js new file mode 100644 index 000000000..2dacb9bc4 --- /dev/null +++ b/ui/app/components/token-input/tests/token-input.component.test.js @@ -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(), '2ETH') + }) + + 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.12USD') + }) + }) + + 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(), '0ETH') + 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(), '2ETH') + 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.00USD') + 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.12USD') + 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) + }) + }) +}) diff --git a/ui/app/components/token-input/tests/token-input.container.test.js b/ui/app/components/token-input/tests/token-input.container.test.js new file mode 100644 index 000000000..d73bc9a94 --- /dev/null +++ b/ui/app/components/token-input/tests/token-input.container.test.js @@ -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', + }) + }) + }) +}) diff --git a/ui/app/components/token-input/token-input.component.js b/ui/app/components/token-input/token-input.component.js new file mode 100644 index 000000000..d1388945b --- /dev/null +++ b/ui/app/components/token-input/token-input.component.js @@ -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> + ) + } +} diff --git a/ui/app/components/token-input/token-input.container.js b/ui/app/components/token-input/token-input.container.js new file mode 100644 index 000000000..ec233b1b8 --- /dev/null +++ b/ui/app/components/token-input/token-input.container.js @@ -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) |