Meriadec Pillet
7 years ago
committed by
GitHub
18 changed files with 722 additions and 243 deletions
@ -0,0 +1,275 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React, { PureComponent } from 'react' |
||||
|
import { compose } from 'redux' |
||||
|
import { translate } from 'react-i18next' |
||||
|
import styled from 'styled-components' |
||||
|
import { connect } from 'react-redux' |
||||
|
import { getDefaultUnitByCoinType, getFiatUnit } from '@ledgerhq/currencies' |
||||
|
|
||||
|
import isNaN from 'lodash/isNaN' |
||||
|
import noop from 'lodash/noop' |
||||
|
|
||||
|
import type { T, Account } from 'types/common' |
||||
|
|
||||
|
import { getCounterValue } from 'reducers/settings' |
||||
|
import { getLastCounterValueBySymbol } from 'reducers/counterValues' |
||||
|
|
||||
|
import InputCurrency from 'components/base/InputCurrency' |
||||
|
import Button from 'components/base/Button' |
||||
|
import Box from 'components/base/Box' |
||||
|
|
||||
|
const InputRight = styled(Box).attrs({ |
||||
|
ff: 'Rubik', |
||||
|
color: 'graphite', |
||||
|
fontSize: 4, |
||||
|
justifyContent: 'center', |
||||
|
pr: 3, |
||||
|
})`` |
||||
|
const InputCenter = styled(Box).attrs({ |
||||
|
ff: 'Rubik', |
||||
|
color: 'graphite', |
||||
|
fontSize: 4, |
||||
|
justifyContent: 'center', |
||||
|
})`` |
||||
|
|
||||
|
const mapStateToProps = (state, { account }) => { |
||||
|
const counterValue = getCounterValue(state) |
||||
|
const unit = getDefaultUnitByCoinType(account.coinType) |
||||
|
const symbol = `${unit.code}-${counterValue}` |
||||
|
return { |
||||
|
counterValue, |
||||
|
lastCounterValue: getLastCounterValueBySymbol(symbol, state), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function maxUnitDigits(unit, value) { |
||||
|
const [leftDigits, rightDigits] = value.toString().split('.') |
||||
|
|
||||
|
return Number(`${leftDigits}${rightDigits ? `.${rightDigits.slice(0, unit.magnitude)}` : ''}`) |
||||
|
} |
||||
|
|
||||
|
function calculateMax(props) { |
||||
|
const { account, counterValue, lastCounterValue } = props |
||||
|
|
||||
|
const unit = getUnit({ account, counterValue }) |
||||
|
const leftMax = account.balance / 10 ** unit.left.magnitude |
||||
|
|
||||
|
return { |
||||
|
left: account.balance / 10 ** unit.left.magnitude, |
||||
|
right: maxUnitDigits(unit.right, leftMax * lastCounterValue), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function getUnit({ account, counterValue }) { |
||||
|
return { |
||||
|
left: getDefaultUnitByCoinType(account.coinType), |
||||
|
right: getFiatUnit(counterValue), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function calculateValues({ |
||||
|
dir, |
||||
|
value, |
||||
|
max, |
||||
|
lastCounterValue, |
||||
|
}: { |
||||
|
dir: string, |
||||
|
value: Object, |
||||
|
max: Object, |
||||
|
lastCounterValue: number, |
||||
|
}) { |
||||
|
const v = value[dir] |
||||
|
|
||||
|
const getMax = (d, v) => { |
||||
|
const result = v > max[d] ? max[d] : v |
||||
|
return isNaN(result) ? '0' : result.toString() |
||||
|
} |
||||
|
|
||||
|
const newValue = {} |
||||
|
|
||||
|
if (dir === 'left') { |
||||
|
newValue.left = v === '' ? v : getMax('left', v) |
||||
|
newValue.right = getMax('right', Number(v) * lastCounterValue) |
||||
|
} |
||||
|
|
||||
|
if (dir === 'right') { |
||||
|
newValue.left = getMax('left', Number(v) / lastCounterValue) |
||||
|
newValue.right = v === '' ? v : getMax('right', v) |
||||
|
} |
||||
|
|
||||
|
return newValue |
||||
|
} |
||||
|
|
||||
|
type Direction = 'left' | 'right' |
||||
|
|
||||
|
type Props = { |
||||
|
account: Account, |
||||
|
counterValue: string, |
||||
|
lastCounterValue: number, // eslint-disable-line react/no-unused-prop-types
|
||||
|
onChange: Function, |
||||
|
t: T, |
||||
|
value: Object, |
||||
|
} |
||||
|
|
||||
|
type State = { |
||||
|
max: { |
||||
|
left: number, |
||||
|
right: number, |
||||
|
}, |
||||
|
value: { |
||||
|
left: string | number, |
||||
|
right: string | number, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
export class RequestAmount extends PureComponent<Props, State> { |
||||
|
static defaultProps = { |
||||
|
onChange: noop, |
||||
|
value: {}, |
||||
|
} |
||||
|
|
||||
|
constructor(props: Props) { |
||||
|
super() |
||||
|
|
||||
|
this.props = props |
||||
|
|
||||
|
const max = calculateMax(props) |
||||
|
|
||||
|
let v = { |
||||
|
left: 0, |
||||
|
right: 0, |
||||
|
} |
||||
|
|
||||
|
if (props.value.left) { |
||||
|
v = calculateValues({ |
||||
|
...props, |
||||
|
dir: 'left', |
||||
|
max, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
if (props.value.right) { |
||||
|
v = calculateValues({ |
||||
|
...props, |
||||
|
dir: 'right', |
||||
|
max, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
this.state = { |
||||
|
max, |
||||
|
value: v, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
componentWillReceiveProps(nextProps: Props) { |
||||
|
if (this.props.account !== nextProps.account) { |
||||
|
const max = calculateMax(nextProps) |
||||
|
this.setState({ |
||||
|
max, |
||||
|
value: calculateValues({ |
||||
|
...nextProps, |
||||
|
dir: 'left', |
||||
|
max, |
||||
|
}), |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
componentDidUpdate(prevProps: Props) { |
||||
|
this.updateValueWithProps(prevProps, this.props) |
||||
|
} |
||||
|
|
||||
|
handleChangeAmount = (dir: Direction) => (v: number | string) => { |
||||
|
const { onChange, value, account, counterValue, ...otherProps } = this.props |
||||
|
const { max } = this.state |
||||
|
|
||||
|
const otherDir = dir === 'left' ? 'right' : 'left' |
||||
|
const unit = getUnit({ |
||||
|
account, |
||||
|
counterValue, |
||||
|
}) |
||||
|
|
||||
|
const newValue = calculateValues({ |
||||
|
...otherProps, |
||||
|
dir, |
||||
|
value: { |
||||
|
[dir]: v.toString(), |
||||
|
}, |
||||
|
max, |
||||
|
}) |
||||
|
newValue[otherDir] = maxUnitDigits(unit[otherDir], newValue[otherDir]).toString() |
||||
|
|
||||
|
this.setState({ |
||||
|
value: newValue, |
||||
|
}) |
||||
|
onChange(newValue) |
||||
|
} |
||||
|
|
||||
|
handleClickMax = () => { |
||||
|
const { account } = this.props |
||||
|
this.handleChangeAmount('left')(account.balance) |
||||
|
} |
||||
|
|
||||
|
updateValueWithProps = (props: Props, nextProps: Props) => { |
||||
|
if ( |
||||
|
props.value.left !== nextProps.value.left && |
||||
|
nextProps.value.left !== this.state.value.left |
||||
|
) { |
||||
|
this.setState({ |
||||
|
value: calculateValues({ |
||||
|
...nextProps, |
||||
|
dir: 'left', |
||||
|
max: this.state.max, |
||||
|
}), |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
if ( |
||||
|
props.value.right !== nextProps.value.right && |
||||
|
nextProps.value.right !== this.state.value.right |
||||
|
) { |
||||
|
this.setState({ |
||||
|
value: calculateValues({ |
||||
|
...nextProps, |
||||
|
dir: 'right', |
||||
|
max: this.state.max, |
||||
|
}), |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { account, counterValue, t } = this.props |
||||
|
const { value } = this.state |
||||
|
|
||||
|
const unit = getUnit({ |
||||
|
account, |
||||
|
counterValue, |
||||
|
}) |
||||
|
|
||||
|
return ( |
||||
|
<Box horizontal flow={2}> |
||||
|
<InputCurrency |
||||
|
unit={unit.left} |
||||
|
value={value.left} |
||||
|
onChange={this.handleChangeAmount('left')} |
||||
|
renderRight={<InputRight>{unit.left.code}</InputRight>} |
||||
|
/> |
||||
|
<InputCenter>=</InputCenter> |
||||
|
<InputCurrency |
||||
|
unit={unit.right} |
||||
|
value={value.right} |
||||
|
onChange={this.handleChangeAmount('right')} |
||||
|
renderRight={<InputRight>{unit.right.code}</InputRight>} |
||||
|
/> |
||||
|
<Button ml={5} primary onClick={this.handleClickMax}> |
||||
|
{t('common:max')} |
||||
|
</Button> |
||||
|
</Box> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default compose(connect(mapStateToProps), translate())(RequestAmount) |
@ -0,0 +1,30 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React from 'react' |
||||
|
import { storiesOf } from '@storybook/react' |
||||
|
import { action } from '@storybook/addon-actions' |
||||
|
import { number } from '@storybook/addon-knobs' |
||||
|
|
||||
|
import { accounts } from 'components/SelectAccount/stories' |
||||
|
|
||||
|
import { RequestAmount } from 'components/RequestAmount' |
||||
|
|
||||
|
const stories = storiesOf('Components/RequestAmount', module) |
||||
|
|
||||
|
const props = { |
||||
|
counterValue: 'USD', |
||||
|
lastCounterValue: 9177.69, |
||||
|
account: accounts[0], |
||||
|
} |
||||
|
|
||||
|
stories.add('basic', () => ( |
||||
|
<RequestAmount |
||||
|
{...props} |
||||
|
t={k => k} |
||||
|
onChange={action('onChange')} |
||||
|
value={{ |
||||
|
left: number('left value', 0), |
||||
|
right: number('right value', 0), |
||||
|
}} |
||||
|
/> |
||||
|
)) |
@ -0,0 +1,132 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React, { PureComponent } from 'react' |
||||
|
|
||||
|
import { parseCurrencyUnit, formatCurrencyUnit } from '@ledgerhq/currencies' |
||||
|
|
||||
|
import noop from 'lodash/noop' |
||||
|
import isNaN from 'lodash/isNaN' |
||||
|
|
||||
|
import Input from 'components/base/Input' |
||||
|
|
||||
|
import type { Unit } from '@ledgerhq/currencies' |
||||
|
|
||||
|
function parseValue(value) { |
||||
|
return value.toString().replace(/,/, '.') |
||||
|
} |
||||
|
|
||||
|
function format(unit: Unit, value: Value) { |
||||
|
let v = value === '' ? 0 : Number(value) |
||||
|
v *= 10 ** unit.magnitude |
||||
|
return formatCurrencyUnit(unit, v, { |
||||
|
disableRounding: true, |
||||
|
showAllDigits: false, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
function unformat(unit, value) { |
||||
|
if (value === 0 || value === '') { |
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
let v = parseCurrencyUnit(unit, value.toString()) |
||||
|
v /= 10 ** unit.magnitude |
||||
|
|
||||
|
return v |
||||
|
} |
||||
|
|
||||
|
type Value = string | number |
||||
|
|
||||
|
type Props = { |
||||
|
onChange: Function, |
||||
|
value: Value, |
||||
|
unit: Unit, |
||||
|
} |
||||
|
|
||||
|
type State = { |
||||
|
isFocus: boolean, |
||||
|
value: Value, |
||||
|
} |
||||
|
|
||||
|
class InputCurrency extends PureComponent<Props, State> { |
||||
|
static defaultProps = { |
||||
|
onChange: noop, |
||||
|
value: 0, |
||||
|
} |
||||
|
|
||||
|
state = { |
||||
|
isFocus: false, |
||||
|
value: this.props.value, |
||||
|
} |
||||
|
|
||||
|
componentWillReceiveProps(nextProps: Props) { |
||||
|
if (this.props.value !== nextProps.value) { |
||||
|
const { isFocus } = this.state |
||||
|
const value = isFocus ? nextProps.value : format(nextProps.unit, nextProps.value) |
||||
|
this.setState({ |
||||
|
value, |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
handleChange = (v: Value) => { |
||||
|
v = parseValue(v) |
||||
|
|
||||
|
// Check if value is valid Number
|
||||
|
if (isNaN(Number(v))) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
this.emitOnChange(v) |
||||
|
this.setState({ |
||||
|
value: v, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
handleBlur = () => { |
||||
|
const { unit } = this.props |
||||
|
const { value } = this.state |
||||
|
|
||||
|
const v = format(unit, value) |
||||
|
|
||||
|
this.setState({ |
||||
|
isFocus: false, |
||||
|
value: v, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
handleFocus = () => { |
||||
|
const { unit } = this.props |
||||
|
|
||||
|
this.setState(prev => ({ |
||||
|
isFocus: true, |
||||
|
value: unformat(unit, prev.value), |
||||
|
})) |
||||
|
} |
||||
|
|
||||
|
emitOnChange = (v: Value) => { |
||||
|
const { onChange } = this.props |
||||
|
const { value } = this.state |
||||
|
|
||||
|
if (value.toString() !== v.toString()) { |
||||
|
onChange(v.toString()) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { value } = this.state |
||||
|
|
||||
|
return ( |
||||
|
<Input |
||||
|
{...this.props} |
||||
|
ff="Rubik" |
||||
|
value={value} |
||||
|
onChange={this.handleChange} |
||||
|
onFocus={this.handleFocus} |
||||
|
onBlur={this.handleBlur} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default InputCurrency |
@ -0,0 +1,15 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React from 'react' |
||||
|
import { storiesOf } from '@storybook/react' |
||||
|
import { action } from '@storybook/addon-actions' |
||||
|
|
||||
|
import { getDefaultUnitByCoinType } from '@ledgerhq/currencies' |
||||
|
|
||||
|
import InputCurrency from 'components/base/InputCurrency' |
||||
|
|
||||
|
const stories = storiesOf('Components/InputCurrency', module) |
||||
|
|
||||
|
const unit = getDefaultUnitByCoinType(1) |
||||
|
|
||||
|
stories.add('basic', () => <InputCurrency unit={unit} onChange={action('onChange')} />) |
Loading…
Reference in new issue