From badebe017fe28b58ac742082368484c3a4b1c1bc Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Wed, 17 Oct 2018 07:03:29 +0800 Subject: Adds toggle for primary currency (#5421) * Add UnitInput component * Add CurrencyInput component * Add UserPreferencedCurrencyInput component * Add UserPreferencedCurrencyDisplay component * Add updatePreferences action * Add styles for CurrencyInput, CurrencyDisplay, and UnitInput * Update SettingsTab page with Primary Currency toggle * Refactor currency displays and inputs to use UserPreferenced displays and inputs * Add TokenInput component * Add UserPreferencedTokenInput component * Use TokenInput in the send screen * Fix unit tests * Fix e2e and integration tests * Remove send/CurrencyDisplay component * Replace diamond unicode character with Eth logo. Fix typos --- .../currency-input/currency-input.component.js | 120 +++++++++++ .../currency-input/currency-input.container.js | 27 +++ ui/app/components/currency-input/index.js | 1 + ui/app/components/currency-input/index.scss | 7 + .../tests/currency-input.component.test.js | 239 +++++++++++++++++++++ .../tests/currency-input.container.test.js | 55 +++++ 6 files changed, 449 insertions(+) create mode 100644 ui/app/components/currency-input/currency-input.component.js create mode 100644 ui/app/components/currency-input/currency-input.container.js create mode 100644 ui/app/components/currency-input/index.js create mode 100644 ui/app/components/currency-input/index.scss create mode 100644 ui/app/components/currency-input/tests/currency-input.component.test.js create mode 100644 ui/app/components/currency-input/tests/currency-input.container.test.js (limited to 'ui/app/components/currency-input') diff --git a/ui/app/components/currency-input/currency-input.component.js b/ui/app/components/currency-input/currency-input.component.js new file mode 100644 index 000000000..54cd0e1ac --- /dev/null +++ b/ui/app/components/currency-input/currency-input.component.js @@ -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 ( + + ) + } + + render () { + const { suffix, ...restProps } = this.props + const { decimalValue } = this.state + + return ( + + { this.renderConversionComponent() } + + ) + } +} diff --git a/ui/app/components/currency-input/currency-input.container.js b/ui/app/components/currency-input/currency-input.container.js new file mode 100644 index 000000000..18e5533de --- /dev/null +++ b/ui/app/components/currency-input/currency-input.container.js @@ -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) diff --git a/ui/app/components/currency-input/index.js b/ui/app/components/currency-input/index.js new file mode 100644 index 000000000..d8069fb67 --- /dev/null +++ b/ui/app/components/currency-input/index.js @@ -0,0 +1 @@ +export { default } from './currency-input.container' diff --git a/ui/app/components/currency-input/index.scss b/ui/app/components/currency-input/index.scss new file mode 100644 index 000000000..fcb2db461 --- /dev/null +++ b/ui/app/components/currency-input/index.scss @@ -0,0 +1,7 @@ +.currency-input { + &__conversion-component { + font-size: 12px; + line-height: 12px; + padding-left: 1px; + } +} diff --git a/ui/app/components/currency-input/tests/currency-input.component.test.js b/ui/app/components/currency-input/tests/currency-input.component.test.js new file mode 100644 index 000000000..8de0ef863 --- /dev/null +++ b/ui/app/components/currency-input/tests/currency-input.component.test.js @@ -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( + + ) + + 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( + + + + ) + + 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( + + + + ) + + 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( + + + + ) + + 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( + + + + ) + + 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( + + + + ) + + 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( + + + + ) + + 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) + }) + }) +}) diff --git a/ui/app/components/currency-input/tests/currency-input.container.test.js b/ui/app/components/currency-input/tests/currency-input.container.test.js new file mode 100644 index 000000000..e77945e4d --- /dev/null +++ b/ui/app/components/currency-input/tests/currency-input.container.test.js @@ -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', + }) + }) + }) +}) -- cgit