Browse Source

Merge pull request #259 from meriadec/master

Various fixes on counter values & pal.
master
Loëck Vézien 7 years ago
committed by GitHub
parent
commit
76b2988617
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      .storybook/config.js
  2. 2
      flow-defs/globals.js
  3. 19
      src/__mocks__/render.js
  4. 15
      src/__mocks__/storybook-state.js
  5. 8
      src/__mocks__/withStore.js
  6. 63
      src/components/CounterValue/__tests__/CounterValue.test.js
  7. 55
      src/components/CounterValue/__tests__/__snapshots__/CounterValue.test.js.snap
  8. 92
      src/components/CounterValue/index.js
  9. 32
      src/components/CounterValue/stories.js
  10. 5
      src/components/OperationsList/index.js
  11. 34
      src/components/OperationsList/stories.js
  12. 69
      src/components/RequestAmount/index.js
  13. 46
      src/components/RequestAmount/stories.js
  14. 2
      src/components/base/FormattedVal/__tests__/FormattedVal.test.js
  15. 86
      src/components/base/InputCurrency/index.js
  16. 18
      src/components/base/InputCurrency/stories.js
  17. 5
      src/components/modals/OperationDetails.js
  18. 26
      src/components/modals/Send/01-step-amount.js
  19. 15
      src/components/modals/Send/Footer.js
  20. 63
      src/components/modals/Send/index.js
  21. 10
      src/helpers/balance.js
  22. 2
      src/reducers/counterValues.js
  23. 25
      src/renderer/createStore.js
  24. 3
      src/renderer/init.js
  25. 9
      src/test-utils.js

10
.storybook/config.js

@ -4,22 +4,30 @@ import { withKnobs } from '@storybook/addon-knobs'
import { setOptions } from '@storybook/addon-options' import { setOptions } from '@storybook/addon-options'
import { ThemeProvider } from 'styled-components' import { ThemeProvider } from 'styled-components'
import { I18nextProvider } from 'react-i18next' import { I18nextProvider } from 'react-i18next'
import { Provider } from 'react-redux'
import 'globals' import 'globals'
import 'styles/global' import 'styles/global'
import theme from 'styles/theme' import theme from 'styles/theme'
import i18n from 'renderer/i18n/storybook' import i18n from 'renderer/i18n/storybook'
import createStore from 'renderer/createStore'
import state from '__mocks__/storybook-state'
const req = require.context('../src', true, /.stories.js$/) const req = require.context('../src', true, /.stories.js$/)
function loadStories() { function loadStories() {
req.keys().forEach(filename => req(filename)) req.keys().forEach(filename => req(filename))
} }
const store = createStore({ state })
addDecorator(story => ( addDecorator(story => (
<I18nextProvider i18n={i18n} initialLanguage="en"> <I18nextProvider i18n={i18n} initialLanguage="en">
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<div style={{ padding: 20 }}>{story()}</div> <Provider store={store}>
<div style={{ padding: 20 }}>{story()}</div>
</Provider>
</ThemeProvider> </ThemeProvider>
</I18nextProvider> </I18nextProvider>
)) ))

2
flow-defs/globals.js

@ -11,5 +11,7 @@ declare var __APP_VERSION__: string
declare var __static: string declare var __static: string
declare var describe: Function declare var describe: Function
declare var test: Function declare var test: Function
declare var it: Function
declare var expect: Function
declare var ResizeObserver: Class<any> declare var ResizeObserver: Class<any>

19
src/__mocks__/render.js

@ -0,0 +1,19 @@
import React from 'react'
import { Provider } from 'react-redux'
import renderer from 'react-test-renderer'
import { ThemeProvider } from 'styled-components'
import createStore from 'renderer/createStore'
import theme from 'styles/theme'
export default function render(component, state) {
const store = createStore({ state })
return renderer
.create(
<Provider store={store}>
<ThemeProvider theme={theme}>{component}</ThemeProvider>
</Provider>,
)
.toJSON()
}

15
src/__mocks__/storybook-state.js

@ -0,0 +1,15 @@
export default {
counterValues: {
BTC: {
USD: {
'2018-01-09': 0.00795978,
'2018-03-29': 0.007106619999999999,
'2018-03-30': 0.0068537599999999995,
'2018-03-31': 0.00694377,
'2018-04-01': 0.00683584,
'2018-04-02': 0.007061689999999999,
latest: 0.00706156,
},
},
},
}

8
src/__mocks__/withStore.js

@ -0,0 +1,8 @@
import React from 'react'
import { Provider } from 'react-redux'
import createStore from 'renderer/createStore'
export default function withStore(state, component) {
const store = createStore({ state })
return <Provider store={store}>{component}</Provider>
}

63
src/components/CounterValue/__tests__/CounterValue.test.js

@ -0,0 +1,63 @@
// @flow
import React from 'react'
import render from '__mocks__/render'
import CounterValue from '..'
describe('components', () => {
describe('CounterValue', () => {
it('basic', () => {
const state = { counterValues: { BTC: { USD: { latest: 10e2 } } } }
const component = <CounterValue ticker="BTC" value={1} />
const tree = render(component, state)
expect(tree).toMatchSnapshot()
})
it('specifying ticker different from default', () => {
const state = { counterValues: { LOL: { USD: { latest: 5e2 } } } }
const component = <CounterValue ticker="LOL" value={1} />
const tree = render(component, state)
expect(tree).toMatchSnapshot()
})
it('using countervalue different from default', () => {
const state = {
counterValues: { BTC: { EUR: { latest: 42 } } },
settings: {
counterValue: 'EUR',
},
}
const component = <CounterValue ticker="BTC" value={1} />
const tree = render(component, state)
expect(tree).toMatchSnapshot()
})
it('without countervalues populated', () => {
const state = { counterValues: {} }
const component = <CounterValue ticker="BTC" value={1} />
const tree = render(component, state)
expect(tree).toMatchSnapshot()
})
it('with time travel whith date in countervalues', () => {
const state = { counterValues: { BTC: { USD: { '2018-01-01': 20e2 } } } }
const date = new Date('2018-01-01')
const component = <CounterValue ticker="BTC" value={1} date={date} />
const tree = render(component, state)
expect(tree).toMatchSnapshot()
})
it('with time travel whith date not in countervalues', () => {
const state = { counterValues: { BTC: { USD: { '2018-01-01': 20e2 } } } }
const date = new Date('2018-01-02')
const component = <CounterValue ticker="BTC" value={1} date={date} />
const tree = render(component, state)
// TODO: actually it returns 0 when date is not in countervalues
// do we want to use closest past value instead?
expect(tree).toMatchSnapshot()
})
})
})

55
src/components/CounterValue/__tests__/__snapshots__/CounterValue.test.js.snap

@ -0,0 +1,55 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components CounterValue basic 1`] = `
<span
className="s1c17x4y-0 fJKscS s8vzclq-0 hjqlvj"
color="#66be54"
>
+ USD 10.00
</span>
`;
exports[`components CounterValue specifying ticker different from default 1`] = `
<span
className="s1c17x4y-0 fJKscS s8vzclq-0 hjqlvj"
color="#66be54"
>
+ USD 5.00
</span>
`;
exports[`components CounterValue using countervalue different from default 1`] = `
<span
className="s1c17x4y-0 fJKscS s8vzclq-0 hjqlvj"
color="#66be54"
>
+ EUR 0.42
</span>
`;
exports[`components CounterValue with time travel whith date in countervalues 1`] = `
<span
className="s1c17x4y-0 fJKscS s8vzclq-0 hjqlvj"
color="#66be54"
>
+ USD 20.00
</span>
`;
exports[`components CounterValue with time travel whith date not in countervalues 1`] = `
<span
className="s1c17x4y-0 fJKscS s8vzclq-0 hjqlvj"
color="#66be54"
>
+ USD 0.00
</span>
`;
exports[`components CounterValue without countervalues populated 1`] = `
<span
className="s1c17x4y-0 fJKscS s8vzclq-0 hjqlvj"
color="#66be54"
>
+ USD 0.00
</span>
`;

92
src/components/CounterValue/index.js

@ -2,57 +2,83 @@
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import moment from 'moment' import { getFiatUnit } from '@ledgerhq/currencies'
import type { Unit, Currency } from '@ledgerhq/currencies'
import { getCounterValueCode } from 'reducers/settings' import { getCounterValueCode } from 'reducers/settings'
import { calculateCounterValueSelector } from 'reducers/counterValues' import { calculateCounterValueSelector } from 'reducers/counterValues'
import FormattedVal from 'components/base/FormattedVal' import FormattedVal from 'components/base/FormattedVal'
const mapStateToProps = state => ({
counterValueCode: getCounterValueCode(state),
getCounterValue: calculateCounterValueSelector(state),
})
type Props = { type Props = {
formatValue: boolean, // wich market to query
ticker: string,
// the value :)
value: number,
// when? if not given: take latest
date?: Date,
// from reducers
counterValueCode: string, counterValueCode: string,
getCounterValue: Function, getCounterValue: Function,
time?: Date | string | number,
unit: Unit,
currency: Currency,
value: number,
} }
export class CounterValue extends PureComponent<Props> { const mapStateToProps = (state, props) => {
const { ticker } = props
// TODO: in wallet-common, stop using currency.
// always use ticker and remove that hack
let { currency } = props
if (!currency && ticker) {
currency = generateFakeCurrency(ticker)
} else if (currency) {
console.warn('`currency` is deprecated in CounterValue. use `ticker` instead.') // eslint-disable-line no-console
}
const counterValueCode = getCounterValueCode(state)
const counterValueUnit = getFiatUnit(counterValueCode)
const getCounterValue = calculateCounterValueSelector(state)(currency, counterValueUnit)
return {
counterValueCode,
getCounterValue,
}
}
class CounterValue extends PureComponent<Props> {
static defaultProps = { static defaultProps = {
formatValue: true,
value: 0, value: 0,
time: undefined, date: undefined,
} }
render() { render() {
const { const { getCounterValue, counterValueCode, date, value, ...props } = this.props
formatValue, const counterValue = getCounterValue(value, date)
value, return (
currency, <FormattedVal val={counterValue} fiat={counterValueCode} showCode alwaysShowSign {...props} />
unit,
counterValueCode,
time,
getCounterValue,
...props
} = this.props
const date = moment(time).format('YYYY-MM-DD')
const v = getCounterValue(currency, counterValueCode)(value, date)
return formatValue ? (
<FormattedVal val={v} fiat={counterValueCode} showCode alwaysShowSign {...props} />
) : (
v
) )
} }
} }
function generateFakeCurrency(ticker) {
return {
units: [
{
code: ticker,
// unused
name: 'fake-unit',
magnitude: 0,
},
],
// unused
coinType: 0,
color: '#000',
name: 'fake-coin',
scheme: 'bitcoin',
}
}
export default connect(mapStateToProps)(CounterValue) export default connect(mapStateToProps)(CounterValue)

32
src/components/CounterValue/stories.js

@ -1,40 +1,16 @@
// @flow // @flow
import React from 'react' import React from 'react'
import { getCurrencyByCoinType, getDefaultUnitByCoinType } from '@ledgerhq/currencies' import { getCurrencyByCoinType } from '@ledgerhq/currencies'
import { storiesOf } from '@storybook/react' import { storiesOf } from '@storybook/react'
import { boolean, text } from '@storybook/addon-knobs' import { number } from '@storybook/addon-knobs'
import createHistory from 'history/createHashHistory'
import { CounterValue } from 'components/CounterValue' import CounterValue from 'components/CounterValue'
import { calculateCounterValueSelector } from 'reducers/counterValues'
import createStore from 'renderer/createStore'
const stories = storiesOf('Components', module) const stories = storiesOf('Components', module)
const currency = getCurrencyByCoinType(0) const currency = getCurrencyByCoinType(0)
const unit = getDefaultUnitByCoinType(0)
const counterValue = 'USD'
const counterValues = {
BTC: {
USD: {
'2018-01-09': 10000,
},
},
}
const store = createStore(createHistory(), { counterValues })
const getCounterValue = calculateCounterValueSelector(store.getState())
stories.add('CounterValue', () => ( stories.add('CounterValue', () => (
<CounterValue <CounterValue ticker={currency.units[0].code} value={Number(number('value', 100000000) || 0)} />
getCounterValue={getCounterValue}
counterValueCode={counterValue}
counterValues={counterValues}
currency={currency}
unit={unit}
formatValue={boolean('formatValue', true)}
value={Number(text('value', '100000000'))}
/>
)) ))

5
src/components/OperationsList/index.js

@ -192,9 +192,8 @@ const Operation = ({
<CounterValue <CounterValue
color="grey" color="grey"
fontSize={3} fontSize={3}
time={time} date={time.toDate()}
currency={currency} ticker={currency.units[0].code}
unit={unit}
value={op.amount} value={op.amount}
/> />
</Box> </Box>

34
src/components/OperationsList/stories.js

@ -4,11 +4,10 @@ import React from 'react'
import { getCurrencyByCoinType, getDefaultUnitByCoinType } from '@ledgerhq/currencies' import { getCurrencyByCoinType, getDefaultUnitByCoinType } from '@ledgerhq/currencies'
import { storiesOf } from '@storybook/react' import { storiesOf } from '@storybook/react'
import { boolean } from '@storybook/addon-knobs' import { boolean } from '@storybook/addon-knobs'
import { translate } from 'react-i18next'
import { accounts } from 'components/SelectAccount/stories' import { accounts } from 'components/SelectAccount/stories'
import { OperationsList } from 'components/OperationsList' import OperationsList from 'components/OperationsList'
const stories = storiesOf('Components', module) const stories = storiesOf('Components', module)
@ -23,20 +22,12 @@ const account = ({ name }) => ({
unit, unit,
}) })
const counterValue = 'USD'
const counterValues = {
'BTC-USD': {
byDate: {
'2018-01-09': 10000,
},
},
}
const operations = [ const operations = [
{ {
address: '5c6ea1716520c7d6e038d36a3223faced3c', address: '5c6ea1716520c7d6e038d36a3223faced3c',
hash: '5c6ea1716520c7d6e038d36a3223faced3c4b8f7ffb69d9fb5bd527d562fdb62', hash: '5c6ea1716520c7d6e038d36a3223faced3c4b8f7ffb69d9fb5bd527d562fdb62',
amount: 130000000, id: '5c6ea1716520c7d6e038d36a3223faced3c4b8f7ffb69d9fb5bd527d562fdb62',
amount: 1.3e8,
date: new Date('2018-01-09T16:03:52Z'), date: new Date('2018-01-09T16:03:52Z'),
confirmations: 1, confirmations: 1,
account: account({ account: account({
@ -45,8 +36,9 @@ const operations = [
}, },
{ {
address: '5c6ea1716520c7d6e038d36a3223faced3c', address: '5c6ea1716520c7d6e038d36a3223faced3c',
hash: '5c6ea1716520c7d6e038d36a3223faced3c4b8f7ffb69d9fb5bd527d562fdb62', hash: '26bdf265d725db5bf9d96bff7f8b4c3decaf3223a63d830e6d7c0256171ae6c5',
amount: 130000000, id: '26bdf265d725db5bf9d96bff7f8b4c3decaf3223a63d830e6d7c0256171ae6c5',
amount: 1.6e8,
date: new Date('2018-01-09T16:03:52Z'), date: new Date('2018-01-09T16:03:52Z'),
confirmations: 11, confirmations: 11,
account: account({ account: account({
@ -56,7 +48,8 @@ const operations = [
{ {
address: '27416a48caab90fab053b507b8b6b9d4', address: '27416a48caab90fab053b507b8b6b9d4',
hash: '27416a48caab90fab053b507b8b6b9d48fba75421d3bfdbae4b85f64024bc9c4', hash: '27416a48caab90fab053b507b8b6b9d48fba75421d3bfdbae4b85f64024bc9c4',
amount: -65000000, id: '27416a48caab90fab053b507b8b6b9d48fba75421d3bfdbae4b85f64024bc9c4',
amount: -6.5e8,
date: new Date('2018-01-09T16:02:40Z'), date: new Date('2018-01-09T16:02:40Z'),
confirmations: 11, confirmations: 11,
account: account({ account: account({
@ -65,8 +58,9 @@ const operations = [
}, },
{ {
address: '27416a48caab90fab053b507b8b6b9d4', address: '27416a48caab90fab053b507b8b6b9d4',
hash: '27416a48caab90fab053b507b8b6b9d48fba75421d3bfdbae4b85f64024bc9c4', hash: '4c9cb42046f58b4eabdfb3d12457abf84d9b6b8b705b350baf09baac84a61472',
amount: -65000000, id: '4c9cb42046f58b4eabdfb3d12457abf84d9b6b8b705b350baf09baac84a61472',
amount: -4.2e8,
date: new Date('2018-01-09T16:02:40Z'), date: new Date('2018-01-09T16:02:40Z'),
confirmations: 1, confirmations: 1,
account: account({ account: account({
@ -75,12 +69,8 @@ const operations = [
}, },
] ]
const OperationsListComp = translate()(OperationsList)
stories.add('OperationsList', () => ( stories.add('OperationsList', () => (
<OperationsListComp <OperationsList
counterValue={counterValue}
counterValues={counterValues}
operations={operations} operations={operations}
canShowMore={boolean('canShowMore')} canShowMore={boolean('canShowMore')}
withAccount={boolean('withAccount')} withAccount={boolean('withAccount')}

69
src/components/RequestAmount/index.js

@ -52,7 +52,7 @@ type Props = {
max: number, max: number,
// change handler // change handler
onChange: ({ left: number, right: number }) => void, onChange: number => void,
// used to determine the left input unit // used to determine the left input unit
account: Account, account: Account,
@ -66,83 +66,40 @@ type Props = {
getReverseCounterValue: CalculateCounterValue, getReverseCounterValue: CalculateCounterValue,
} }
type State = { export class RequestAmount extends PureComponent<Props> {
leftUnit: Unit,
rightUnit: Unit,
leftValue: number,
rightValue: number,
}
export class RequestAmount extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
const { account, rightUnit, value, getCounterValue } = this.props
const rawLeftValue = value * 10 ** account.unit.magnitude
const rawRightValue = getCounterValue(account.currency, rightUnit)(rawLeftValue)
const rightValue = rawRightValue / 10 ** rightUnit.magnitude
this.state = {
leftUnit: account.unit,
rightUnit,
leftValue: value,
rightValue,
}
}
handleClickMax = () => { handleClickMax = () => {
const leftValue = this.props.max / 10 ** this.props.account.unit.magnitude this.props.onChange(this.props.max)
this.handleChangeAmount('left')(leftValue)
this.setState({ leftValue })
} }
handleChangeAmount = (changedField: string) => (val: number) => { handleChangeAmount = (changedField: string) => (val: number) => {
const { getCounterValue, getReverseCounterValue, account, max, onChange } = this.props const { rightUnit, getReverseCounterValue, account, max, onChange } = this.props
const { rightUnit } = this.state
if (changedField === 'left') { if (changedField === 'left') {
let rawLeftValue = val * 10 ** account.unit.magnitude onChange(val > max ? max : val)
if (rawLeftValue > max) {
rawLeftValue = max
}
const leftValue = rawLeftValue / 10 ** account.unit.magnitude
const rawRightValue = getCounterValue(account.currency, rightUnit)(rawLeftValue)
const rightValue = rawRightValue / 10 ** rightUnit.magnitude
this.setState({ rightValue, leftValue })
onChange({ left: rawLeftValue, right: rawRightValue })
} else if (changedField === 'right') { } else if (changedField === 'right') {
let rawRightValue = val * 10 ** rightUnit.magnitude const leftVal = getReverseCounterValue(account.currency, rightUnit)(val)
let rawLeftValue = getReverseCounterValue(account.currency, rightUnit)(rawRightValue) onChange(leftVal > max ? max : leftVal)
if (rawLeftValue > max) {
rawLeftValue = max
rawRightValue = getCounterValue(account.currency, rightUnit)(rawLeftValue)
}
const rightValue = rawRightValue / 10 ** rightUnit.magnitude
const leftValue = rawLeftValue / 10 ** account.unit.magnitude
this.setState({ rightValue, leftValue })
onChange({ left: rawLeftValue, right: rawRightValue })
} }
} }
render() { render() {
const { t } = this.props const { t, value, account, rightUnit, getCounterValue } = this.props
const { leftUnit, rightUnit, leftValue, rightValue } = this.state const right = getCounterValue(account.currency, rightUnit)(value)
return ( return (
<Box horizontal flow="5"> <Box horizontal flow="5">
<Box horizontal align="center"> <Box horizontal align="center">
<InputCurrency <InputCurrency
containerProps={{ style: { width: 156 } }} containerProps={{ style: { width: 156 } }}
unit={leftUnit} unit={account.unit}
value={leftValue} value={value}
onChange={this.handleChangeAmount('left')} onChange={this.handleChangeAmount('left')}
renderRight={<InputRight>{leftUnit.code}</InputRight>} renderRight={<InputRight>{account.unit.code}</InputRight>}
/> />
<InputCenter>=</InputCenter> <InputCenter>=</InputCenter>
<InputCurrency <InputCurrency
containerProps={{ style: { width: 156 } }} containerProps={{ style: { width: 156 } }}
unit={rightUnit} unit={rightUnit}
value={rightValue} value={right}
onChange={this.handleChangeAmount('right')} onChange={this.handleChangeAmount('right')}
renderRight={<InputRight>{rightUnit.code}</InputRight>} renderRight={<InputRight>{rightUnit.code}</InputRight>}
/> />

46
src/components/RequestAmount/stories.js

@ -1,33 +1,39 @@
// @flow // @flow
import React from 'react' import React, { PureComponent } from 'react'
import { storiesOf } from '@storybook/react' import { storiesOf } from '@storybook/react'
import { action } from '@storybook/addon-actions' import { action } from '@storybook/addon-actions'
import { number } from '@storybook/addon-knobs'
import { translate } from 'react-i18next'
import { accounts } from 'components/SelectAccount/stories' import { accounts } from 'components/SelectAccount/stories'
import { RequestAmount } from 'components/RequestAmount' import RequestAmount from 'components/RequestAmount'
const stories = storiesOf('Components', module) const stories = storiesOf('Components', module)
const props = { type State = {
counterValue: 'USD', value: number,
lastCounterValue: 9177.69,
account: accounts[0],
} }
const RequestAmountComp = translate()(RequestAmount) class Wrapper extends PureComponent<any, State> {
state = {
value: 3e8,
}
handleChange = value => {
action('onChange')(value)
this.setState({ value })
}
render() {
const { value } = this.state
return (
<RequestAmount
counterValue="USD"
account={accounts[0]}
onChange={this.handleChange}
value={value}
max={4e8}
/>
)
}
}
stories.add('RequestAmount', () => ( stories.add('RequestAmount', () => <Wrapper />)
<RequestAmountComp
{...props}
t={k => k}
onChange={action('onChange')}
value={{
left: number('left value', 0),
right: number('right value', 0),
}}
/>
))

2
src/components/base/FormattedVal/__tests__/FormattedVal.test.js

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import { getDefaultUnitByCoinType } from '@ledgerhq/currencies' import { getDefaultUnitByCoinType } from '@ledgerhq/currencies'
import { render } from 'test-utils' import render from '__mocks__/render'
import FormattedVal from '..' import FormattedVal from '..'
const bitcoinUnit = getDefaultUnitByCoinType(0) const bitcoinUnit = getDefaultUnitByCoinType(0)

86
src/components/base/InputCurrency/index.js

@ -17,10 +17,8 @@ function parseValue(value) {
return value.toString().replace(/,/g, '.') return value.toString().replace(/,/g, '.')
} }
function format(unit: Unit, value: Value) { function format(unit: Unit, value: number) {
let v = value === '' ? 0 : Number(value) return formatCurrencyUnit(unit, value, {
v *= 10 ** unit.magnitude
return formatCurrencyUnit(unit, v, {
disableRounding: true, disableRounding: true,
showAllDigits: false, showAllDigits: false,
}) })
@ -28,13 +26,13 @@ function format(unit: Unit, value: Value) {
function unformat(unit, value) { function unformat(unit, value) {
if (value === 0 || value === '') { if (value === 0 || value === '') {
return 0 return '0'
} }
let v = parseCurrencyUnit(unit, value.toString()) let v = parseCurrencyUnit(unit, value.toString())
v /= 10 ** unit.magnitude v /= 10 ** unit.magnitude
return v return v.toString()
} }
const Currencies = styled(Box)` const Currencies = styled(Box)`
@ -50,19 +48,18 @@ const Currency = styled(Box).attrs({
pr: 1, pr: 1,
})`` })``
type Value = string | number
type Props = { type Props = {
onChange: Function, onChange: Function,
renderRight: any, renderRight: any,
unit: Unit, unit: Unit,
units: Array<Unit>, units: Array<Unit>,
value: Value, value: number,
} }
type State = { type State = {
unit: Unit,
isFocus: boolean, isFocus: boolean,
value: Value, displayValue: string,
} }
class InputCurrency extends PureComponent<Props, State> { class InputCurrency extends PureComponent<Props, State> {
@ -75,66 +72,74 @@ class InputCurrency extends PureComponent<Props, State> {
state = { state = {
isFocus: false, isFocus: false,
value: this.props.value, displayValue: '0',
unit: this.props.unit,
}
componentWillMount() {
const { value } = this.props
const { unit } = this.state
const displayValue = format(unit, value)
this.setState({ displayValue })
} }
componentWillReceiveProps(nextProps: Props) { componentWillReceiveProps(nextProps: Props) {
const { unit } = this.state
if (this.props.value !== nextProps.value) { if (this.props.value !== nextProps.value) {
const { isFocus } = this.state const { isFocus } = this.state
const value = isFocus ? nextProps.value : format(nextProps.unit, nextProps.value) const displayValue = isFocus
this.setState({ ? (nextProps.value / 10 ** unit.magnitude).toString()
value, : format(unit, nextProps.value)
}) this.setState({ displayValue })
} }
} }
handleChange = (v: Value) => { handleChange = (v: string) => {
// const { displayValue } = this.state
v = parseValue(v) v = parseValue(v)
if (v.startsWith('00')) {
return
}
// Check if value is valid Number // Check if value is valid Number
if (isNaN(Number(v))) { if (isNaN(Number(v))) {
return return
} }
this.emitOnChange(v) this.emitOnChange(v)
this.setState({ this.setState({ displayValue: v || '0' })
value: v,
})
} }
handleBlur = () => { handleBlur = () => {
const { unit } = this.props const { value } = this.props
const { value } = this.state const { unit } = this.state
const v = format(unit, value) const v = format(unit, value)
this.setState({ isFocus: false, displayValue: v })
this.setState({
isFocus: false,
value: v,
})
} }
handleFocus = () => { handleFocus = () => {
const { unit } = this.props const { unit } = this.state
this.setState(prev => ({ this.setState(prev => ({
isFocus: true, isFocus: true,
value: unformat(unit, prev.value), displayValue: unformat(unit, prev.displayValue),
})) }))
} }
emitOnChange = (v: Value) => { emitOnChange = (v: string) => {
const { onChange, unit } = this.props const { onChange } = this.props
const { value } = this.state const { displayValue, unit } = this.state
if (value.toString() !== v.toString()) { if (displayValue.toString() !== v.toString()) {
onChange(v.toString(), unit) const satoshiValue = Number(v) * 10 ** unit.magnitude
onChange(satoshiValue, unit)
} }
} }
renderListUnits = () => { renderListUnits = () => {
const { unit, units, onChange } = this.props const { units, value } = this.props
const { value } = this.state const { unit } = this.state
if (units.length <= 1) { if (units.length <= 1) {
return null return null
@ -146,7 +151,10 @@ class InputCurrency extends PureComponent<Props, State> {
bg="lightGraphite" bg="lightGraphite"
keyProp="code" keyProp="code"
flatLeft flatLeft
onChange={item => onChange(unformat(item, value), item)} onChange={item => {
this.setState({ unit: item, displayValue: format(item, value) })
// onChange(unformat(item, value), item)
}}
items={units} items={units}
value={unit} value={unit}
renderItem={item => item.code} renderItem={item => item.code}
@ -158,13 +166,13 @@ class InputCurrency extends PureComponent<Props, State> {
render() { render() {
const { renderRight } = this.props const { renderRight } = this.props
const { value } = this.state const { displayValue } = this.state
return ( return (
<Input <Input
{...this.props} {...this.props}
ff="Rubik" ff="Rubik"
value={value} value={displayValue}
onChange={this.handleChange} onChange={this.handleChange}
onFocus={this.handleFocus} onFocus={this.handleFocus}
onBlur={this.handleBlur} onBlur={this.handleBlur}

18
src/components/base/InputCurrency/stories.js

@ -2,28 +2,26 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { storiesOf } from '@storybook/react' import { storiesOf } from '@storybook/react'
import { action } from '@storybook/addon-actions'
import { getDefaultUnitByCoinType, getFiatUnit } from '@ledgerhq/currencies' import { getCurrencyByCoinType } from '@ledgerhq/currencies'
import InputCurrency from 'components/base/InputCurrency' import InputCurrency from 'components/base/InputCurrency'
const stories = storiesOf('Components', module) const stories = storiesOf('Components', module)
const units = [ const { units } = getCurrencyByCoinType(1)
getDefaultUnitByCoinType(1),
getDefaultUnitByCoinType(2),
getDefaultUnitByCoinType(3),
getDefaultUnitByCoinType(6),
getFiatUnit('USD'),
]
class Wrapper extends Component<any, any> { class Wrapper extends Component<any, any> {
state = { state = {
value: 0, value: 1e8,
unit: units[0], unit: units[0],
} }
handleChange = (value, unit) => this.setState({ value, unit }) handleChange = (value, unit) => {
action('onChange')(value, unit)
this.setState({ value, unit })
}
render() { render() {
const { render } = this.props const { render } = this.props

5
src/components/modals/OperationDetails.js

@ -90,9 +90,8 @@ const OperationDetails = ({ t }: { t: T }) => (
color="grey" color="grey"
fontSize={5} fontSize={5}
style={{ lineHeight: 1 }} style={{ lineHeight: 1 }}
time={date} date={date}
unit={unit} ticker={currency.units[0].code}
currency={currency}
value={amount} value={amount}
/> />
</Box> </Box>

26
src/components/modals/Send/01-step-amount.js

@ -3,7 +3,6 @@
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
import type { Account } from '@ledgerhq/wallet-common/lib/types' import type { Account } from '@ledgerhq/wallet-common/lib/types'
import type { Unit } from '@ledgerhq/currencies'
import type { T } from 'types/common' import type { T } from 'types/common'
import Box from 'components/base/Box' import Box from 'components/base/Box'
@ -22,11 +21,8 @@ type PropsStepAmount = {
account: Account | null, account: Account | null,
onChange: Function, onChange: Function,
recipientAddress: string, recipientAddress: string,
amount: { left: number, right: number }, amount: number,
fees: { fees: number,
value: number,
unit: Unit | null,
},
isRBF: boolean, isRBF: boolean,
t: T, t: T,
} }
@ -61,10 +57,10 @@ function StepAmount(props: PropsStepAmount) {
<Box flow={1}> <Box flow={1}>
<Label>{t('send:steps.amount.amount')}</Label> <Label>{t('send:steps.amount.amount')}</Label>
<RequestAmount <RequestAmount
max={account.balance - 0} max={account.balance - fees}
account={account} account={account}
onChange={onChange('amount')} onChange={onChange('amount')}
value={amount.left} value={amount}
/> />
</Box> </Box>
@ -75,12 +71,7 @@ function StepAmount(props: PropsStepAmount) {
<LabelInfoTooltip ml={1} text={t('send:steps.amount.fees')} /> <LabelInfoTooltip ml={1} text={t('send:steps.amount.fees')} />
</Label> </Label>
<Box horizontal flow={5}> <Box horizontal flow={5}>
<Fees <Fees amount={fees} account={account} onChange={value => onChange('fees')(value)} />
amount={fees.value}
unit={fees.unit}
account={account}
onChange={(value, unit) => onChange('fees')({ value, unit })}
/>
</Box> </Box>
</Box> </Box>
@ -121,11 +112,10 @@ type PropsFees = {
account: Account, account: Account,
amount: number, amount: number,
onChange: Function, onChange: Function,
unit: Unit | null,
} }
function Fees(props: PropsFees) { function Fees(props: PropsFees) {
const { onChange, account, unit, amount } = props const { onChange, account, amount } = props
const { units } = account.currency const { units } = account.currency
return ( return (
@ -135,11 +125,11 @@ function Fees(props: PropsFees) {
items={[{ key: 'custom', name: 'Custom' }]} items={[{ key: 'custom', name: 'Custom' }]}
value={{ key: 'custom', name: 'Custom' }} value={{ key: 'custom', name: 'Custom' }}
renderSelected={item => item.name} renderSelected={item => item.name}
onChange={() => onChange(amount, unit)} onChange={() => onChange(amount)}
/> />
<InputCurrency <InputCurrency
unit={units[0]}
units={units} units={units}
unit={unit || units[0]}
containerProps={{ grow: true }} containerProps={{ grow: true }}
value={amount} value={amount}
onChange={onChange} onChange={onChange}

15
src/components/modals/Send/Footer.js

@ -8,6 +8,7 @@ import type { T } from 'types/common'
import { ModalFooter } from 'components/base/Modal' import { ModalFooter } from 'components/base/Modal'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Label from 'components/base/Label' import Label from 'components/base/Label'
import CounterValue from 'components/CounterValue'
import FormattedVal from 'components/base/FormattedVal' import FormattedVal from 'components/base/FormattedVal'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import Text from 'components/base/Text' import Text from 'components/base/Text'
@ -15,13 +16,13 @@ import Text from 'components/base/Text'
type Props = { type Props = {
t: T, t: T,
account: Account, account: Account,
amount: { left: number, right: number }, amount: number,
fees: number,
onNext: Function, onNext: Function,
canNext: boolean, canNext: boolean,
counterValue: string,
} }
function Footer({ account, amount, t, onNext, canNext, counterValue }: Props) { function Footer({ account, amount, fees, t, onNext, canNext }: Props) {
return ( return (
<ModalFooter horizontal alignItems="center"> <ModalFooter horizontal alignItems="center">
<Box grow> <Box grow>
@ -30,7 +31,7 @@ function Footer({ account, amount, t, onNext, canNext, counterValue }: Props) {
<FormattedVal <FormattedVal
disableRounding disableRounding
color="dark" color="dark"
val={amount.left} val={amount + fees}
unit={account.unit} unit={account.unit}
showCode showCode
/> />
@ -38,12 +39,12 @@ function Footer({ account, amount, t, onNext, canNext, counterValue }: Props) {
<Text ff="Rubik" fontSize={3}> <Text ff="Rubik" fontSize={3}>
{'('} {'('}
</Text> </Text>
<FormattedVal <CounterValue
ticker={account.currency.units[0].code}
value={amount + fees}
disableRounding disableRounding
color="grey" color="grey"
fontSize={3} fontSize={3}
val={amount.right}
fiat={counterValue}
showCode showCode
/> />
<Text ff="Rubik" fontSize={3}> <Text ff="Rubik" fontSize={3}>

63
src/components/modals/Send/index.js

@ -1,19 +1,14 @@
// @flow // @flow
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import { compose } from 'redux'
import { connect } from 'react-redux'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import get from 'lodash/get' import get from 'lodash/get'
import type { Account } from '@ledgerhq/wallet-common/lib/types' import type { Account } from '@ledgerhq/wallet-common/lib/types'
import type { Unit } from '@ledgerhq/currencies'
import type { T } from 'types/common' import type { T } from 'types/common'
import { MODAL_SEND } from 'config/constants' import { MODAL_SEND } from 'config/constants'
import { getCounterValueCode } from 'reducers/settings'
import Breadcrumb from 'components/Breadcrumb' import Breadcrumb from 'components/Breadcrumb'
import Modal, { ModalBody, ModalTitle, ModalContent } from 'components/base/Modal' import Modal, { ModalBody, ModalTitle, ModalContent } from 'components/base/Modal'
@ -24,25 +19,17 @@ import StepConnectDevice from './02-step-connect-device'
import StepVerification from './03-step-verification' import StepVerification from './03-step-verification'
import StepConfirmation from './04-step-confirmation' import StepConfirmation from './04-step-confirmation'
const mapStateToProps = state => ({
counterValue: getCounterValueCode(state),
})
type Props = { type Props = {
t: T, t: T,
counterValue: string,
} }
type State = { type State = {
stepIndex: number, stepIndex: number,
isDeviceReady: boolean, isDeviceReady: boolean,
amount: { left: number, right: number }, amount: number,
fees: number,
account: Account | null, account: Account | null,
recipientAddress: string, recipientAddress: string,
fees: {
value: number,
unit: Unit | null,
},
isRBF: boolean, isRBF: boolean,
} }
@ -58,14 +45,8 @@ const INITIAL_STATE = {
isDeviceReady: false, isDeviceReady: false,
account: null, account: null,
recipientAddress: '', recipientAddress: '',
amount: { amount: 0,
left: 0, fees: 0,
right: 0,
},
fees: {
value: 0,
unit: null,
},
isRBF: false, isRBF: false,
} }
@ -73,6 +54,7 @@ class SendModal extends PureComponent<Props, State> {
state = INITIAL_STATE state = INITIAL_STATE
_steps = GET_STEPS(this.props.t) _steps = GET_STEPS(this.props.t)
_account: Account | null = null
canNext = account => { canNext = account => {
const { stepIndex } = this.state const { stepIndex } = this.state
@ -80,7 +62,7 @@ class SendModal extends PureComponent<Props, State> {
// informations // informations
if (stepIndex === 0) { if (stepIndex === 0) {
const { amount, recipientAddress } = this.state const { amount, recipientAddress } = this.state
return !!amount.left && !!recipientAddress && !!account return !!amount && !!recipientAddress && !!account
} }
// connect device // connect device
@ -102,7 +84,25 @@ class SendModal extends PureComponent<Props, State> {
this.setState({ stepIndex: stepIndex + 1 }) this.setState({ stepIndex: stepIndex + 1 })
} }
createChangeHandler = key => value => this.setState({ [key]: value }) createChangeHandler = key => value => {
const patch = { [key]: value }
// ensure max is always restecped when changing fees
if (key === 'fees') {
const { amount } = this.state
// if changing fees goes further than max, change amount
if (this._account && amount + value > this._account.balance) {
const diff = amount + value - this._account.balance
patch.amount = amount - diff
// if the user is a little joker, and try to put fees superior
// to the max, let's reset amount to 0 and put fees to max.
if (patch.amount < 0) {
patch.amount = 0
patch.fees = this._account.balance
}
}
}
this.setState(patch)
}
renderStep = acc => { renderStep = acc => {
const { stepIndex, account, amount, ...othersState } = this.state const { stepIndex, account, amount, ...othersState } = this.state
@ -121,8 +121,8 @@ class SendModal extends PureComponent<Props, State> {
} }
render() { render() {
const { t, counterValue } = this.props const { t } = this.props
const { stepIndex, amount, account } = this.state const { stepIndex, amount, account, fees } = this.state
return ( return (
<Modal <Modal
@ -131,6 +131,11 @@ class SendModal extends PureComponent<Props, State> {
render={({ data, onClose }) => { render={({ data, onClose }) => {
const acc = account || get(data, 'account', null) const acc = account || get(data, 'account', null)
const canNext = this.canNext(acc) const canNext = this.canNext(acc)
// hack: access the selected account, living in modal data, outside
// of the modal render function
this._account = acc
return ( return (
<ModalBody onClose={onClose} deferHeight={acc ? 630 : 355}> <ModalBody onClose={onClose} deferHeight={acc ? 630 : 355}>
<ModalTitle>{t('send:title')}</ModalTitle> <ModalTitle>{t('send:title')}</ModalTitle>
@ -140,11 +145,11 @@ class SendModal extends PureComponent<Props, State> {
</ModalContent> </ModalContent>
{acc && ( {acc && (
<Footer <Footer
counterValue={counterValue}
canNext={canNext} canNext={canNext}
onNext={this.handleNextStep} onNext={this.handleNextStep}
account={acc} account={acc}
amount={amount} amount={amount}
fees={fees}
t={t} t={t}
/> />
)} )}
@ -156,4 +161,4 @@ class SendModal extends PureComponent<Props, State> {
} }
} }
export default compose(connect(mapStateToProps), translate())(SendModal) export default translate()(SendModal)

10
src/helpers/balance.js

@ -69,6 +69,7 @@ export function getBalanceHistoryForAccount({
counterValues: Object, counterValues: Object,
interval: DateInterval, interval: DateInterval,
}): Array<BalanceHistoryDay> { }): Array<BalanceHistoryDay> {
const todayDate = moment().format('YYYY-MM-DD')
const unit = getDefaultUnitByCoinType(account.coinType) const unit = getDefaultUnitByCoinType(account.coinType)
const counterVals = get(counterValues, `${unit.code}.${counterValue}`) const counterVals = get(counterValues, `${unit.code}.${counterValue}`)
let lastBalance = getBalanceAtIntervalStart(account, interval) let lastBalance = getBalanceAtIntervalStart(account, interval)
@ -79,14 +80,19 @@ export function getBalanceHistoryForAccount({
return { balance, date } return { balance, date }
} }
const isToday = date === todayDate
const counterVal = isToday
? counterVals.latest || counterVals[date] || 0
: counterVals[date] || 0
// if we don't have data on account balance for that day, // if we don't have data on account balance for that day,
// we take the prev day // we take the prev day
if (isUndefined(account.balanceByDay[date])) { if (isUndefined(account.balanceByDay[date])) {
balance = lastBalance === null ? 0 : lastBalance * counterVals[date] balance = lastBalance === null ? 0 : lastBalance * counterVal
} else { } else {
const b = account.balanceByDay[date] const b = account.balanceByDay[date]
lastBalance = b lastBalance = b
balance = b * counterVals[date] balance = b * counterVal
} }
if (isNaN(balance)) { if (isNaN(balance)) {

2
src/reducers/counterValues.js

@ -16,7 +16,7 @@ export type CounterValuesState = {}
const state: CounterValuesState = {} const state: CounterValuesState = {}
const handlers = { const handlers = {
UPDATE_COUNTER_VALUES: (state, { payload: counterValues }) => merge(state, counterValues), UPDATE_COUNTER_VALUES: (state, { payload: counterValues }) => merge({ ...state }, counterValues),
} }
const getPairHistory = state => (coinTicker, fiat) => { const getPairHistory = state => (coinTicker, fiat) => {

25
src/renderer/createStore.js

@ -1,20 +1,31 @@
// @flow // @flow
import type { HashHistory } from 'history'
import { createStore, applyMiddleware, compose } from 'redux' import { createStore, applyMiddleware, compose } from 'redux'
import { routerMiddleware } from 'react-router-redux' import { routerMiddleware } from 'react-router-redux'
import thunk from 'redux-thunk' import thunk from 'redux-thunk'
import createHistory from 'history/createHashHistory'
import db from 'middlewares/db' import type { HashHistory } from 'history'
import reducers from 'reducers' import reducers from 'reducers'
export default (history: HashHistory, initialState: any) => { type Props = {
const middlewares = [routerMiddleware(history), thunk, db] history: HashHistory,
state?: Object,
history?: any,
dbMiddleware?: Function,
}
export default ({ state, history, dbMiddleware }: Props) => {
if (!history) {
history = createHistory()
}
const middlewares = [routerMiddleware(history), thunk]
if (dbMiddleware) {
middlewares.push(dbMiddleware)
}
const enhancers = compose( const enhancers = compose(
applyMiddleware(...middlewares), applyMiddleware(...middlewares),
window.devToolsExtension ? window.devToolsExtension() : f => f, // eslint-disable-line window.devToolsExtension ? window.devToolsExtension() : f => f, // eslint-disable-line
) )
return createStore(reducers, initialState, enhancers) return createStore(reducers, state, enhancers)
} }

3
src/renderer/init.js

@ -16,6 +16,7 @@ import { isLocked } from 'reducers/application'
import { getLanguage } from 'reducers/settings' import { getLanguage } from 'reducers/settings'
import db from 'helpers/db' import db from 'helpers/db'
import dbMiddleware from 'middlewares/db'
import App from 'components/App' import App from 'components/App'
@ -26,7 +27,7 @@ db.init('settings', {})
db.init('counterValues', {}) db.init('counterValues', {})
const history = createHistory() const history = createHistory()
const store = createStore(history) const store = createStore({ history, dbMiddleware })
const rootNode = document.getElementById('app') const rootNode = document.getElementById('app')
store.dispatch(fetchSettings()) store.dispatch(fetchSettings())

9
src/test-utils.js

@ -1,9 +0,0 @@
import React from 'react'
import renderer from 'react-test-renderer'
import { ThemeProvider } from 'styled-components'
import theme from 'styles/theme'
export function render(component) {
return renderer.create(<ThemeProvider theme={theme}>{component}</ThemeProvider>).toJSON()
}
Loading…
Cancel
Save