aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app/components/token-input
diff options
context:
space:
mode:
Diffstat (limited to 'ui/app/components/token-input')
-rw-r--r--ui/app/components/token-input/index.js1
-rw-r--r--ui/app/components/token-input/tests/token-input.component.test.js308
-rw-r--r--ui/app/components/token-input/tests/token-input.container.test.js129
-rw-r--r--ui/app/components/token-input/token-input.component.js136
-rw-r--r--ui/app/components/token-input/token-input.container.js27
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..2131e7705
--- /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(), '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)
+ })
+ })
+})
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)