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 { ThemeProvider } from 'styled-components'
import { I18nextProvider } from 'react-i18next'
import { Provider } from 'react-redux'
import 'globals'
import 'styles/global'
import theme from 'styles/theme'
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$/)
function loadStories() {
req.keys().forEach(filename => req(filename))
}
const store = createStore({ state })
addDecorator(story => (
<I18nextProvider i18n={i18n} initialLanguage="en">
<ThemeProvider theme={theme}>
<div style={{ padding: 20 }}>{story()}</div>
<Provider store={store}>
<div style={{ padding: 20 }}>{story()}</div>
</Provider>
</ThemeProvider>
</I18nextProvider>
))

2
flow-defs/globals.js

@ -11,5 +11,7 @@ declare var __APP_VERSION__: string
declare var __static: string
declare var describe: Function
declare var test: Function
declare var it: Function
declare var expect: Function
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 { connect } from 'react-redux'
import moment from 'moment'
import type { Unit, Currency } from '@ledgerhq/currencies'
import { getFiatUnit } from '@ledgerhq/currencies'
import { getCounterValueCode } from 'reducers/settings'
import { calculateCounterValueSelector } from 'reducers/counterValues'
import FormattedVal from 'components/base/FormattedVal'
const mapStateToProps = state => ({
counterValueCode: getCounterValueCode(state),
getCounterValue: calculateCounterValueSelector(state),
})
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,
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 = {
formatValue: true,
value: 0,
time: undefined,
date: undefined,
}
render() {
const {
formatValue,
value,
currency,
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
const { getCounterValue, counterValueCode, date, value, ...props } = this.props
const counterValue = getCounterValue(value, date)
return (
<FormattedVal val={counterValue} fiat={counterValueCode} showCode alwaysShowSign {...props} />
)
}
}
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)

32
src/components/CounterValue/stories.js

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

5
src/components/OperationsList/index.js

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

34
src/components/OperationsList/stories.js

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

69
src/components/RequestAmount/index.js

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

46
src/components/RequestAmount/stories.js

@ -1,33 +1,39 @@
// @flow
import React from 'react'
import React, { PureComponent } from 'react'
import { storiesOf } from '@storybook/react'
import { action } from '@storybook/addon-actions'
import { number } from '@storybook/addon-knobs'
import { translate } from 'react-i18next'
import { accounts } from 'components/SelectAccount/stories'
import { RequestAmount } from 'components/RequestAmount'
import RequestAmount from 'components/RequestAmount'
const stories = storiesOf('Components', module)
const props = {
counterValue: 'USD',
lastCounterValue: 9177.69,
account: accounts[0],
type State = {
value: number,
}
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', () => (
<RequestAmountComp
{...props}
t={k => k}
onChange={action('onChange')}
value={{
left: number('left value', 0),
right: number('right value', 0),
}}
/>
))
stories.add('RequestAmount', () => <Wrapper />)

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

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

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

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

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

@ -2,28 +2,26 @@
import React, { Component } from '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'
const stories = storiesOf('Components', module)
const units = [
getDefaultUnitByCoinType(1),
getDefaultUnitByCoinType(2),
getDefaultUnitByCoinType(3),
getDefaultUnitByCoinType(6),
getFiatUnit('USD'),
]
const { units } = getCurrencyByCoinType(1)
class Wrapper extends Component<any, any> {
state = {
value: 0,
value: 1e8,
unit: units[0],
}
handleChange = (value, unit) => this.setState({ value, unit })
handleChange = (value, unit) => {
action('onChange')(value, unit)
this.setState({ value, unit })
}
render() {
const { render } = this.props

5
src/components/modals/OperationDetails.js

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

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

@ -3,7 +3,6 @@
import React, { Fragment } from 'react'
import type { Account } from '@ledgerhq/wallet-common/lib/types'
import type { Unit } from '@ledgerhq/currencies'
import type { T } from 'types/common'
import Box from 'components/base/Box'
@ -22,11 +21,8 @@ type PropsStepAmount = {
account: Account | null,
onChange: Function,
recipientAddress: string,
amount: { left: number, right: number },
fees: {
value: number,
unit: Unit | null,
},
amount: number,
fees: number,
isRBF: boolean,
t: T,
}
@ -61,10 +57,10 @@ function StepAmount(props: PropsStepAmount) {
<Box flow={1}>
<Label>{t('send:steps.amount.amount')}</Label>
<RequestAmount
max={account.balance - 0}
max={account.balance - fees}
account={account}
onChange={onChange('amount')}
value={amount.left}
value={amount}
/>
</Box>
@ -75,12 +71,7 @@ function StepAmount(props: PropsStepAmount) {
<LabelInfoTooltip ml={1} text={t('send:steps.amount.fees')} />
</Label>
<Box horizontal flow={5}>
<Fees
amount={fees.value}
unit={fees.unit}
account={account}
onChange={(value, unit) => onChange('fees')({ value, unit })}
/>
<Fees amount={fees} account={account} onChange={value => onChange('fees')(value)} />
</Box>
</Box>
@ -121,11 +112,10 @@ type PropsFees = {
account: Account,
amount: number,
onChange: Function,
unit: Unit | null,
}
function Fees(props: PropsFees) {
const { onChange, account, unit, amount } = props
const { onChange, account, amount } = props
const { units } = account.currency
return (
@ -135,11 +125,11 @@ function Fees(props: PropsFees) {
items={[{ key: 'custom', name: 'Custom' }]}
value={{ key: 'custom', name: 'Custom' }}
renderSelected={item => item.name}
onChange={() => onChange(amount, unit)}
onChange={() => onChange(amount)}
/>
<InputCurrency
unit={units[0]}
units={units}
unit={unit || units[0]}
containerProps={{ grow: true }}
value={amount}
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 Box from 'components/base/Box'
import Label from 'components/base/Label'
import CounterValue from 'components/CounterValue'
import FormattedVal from 'components/base/FormattedVal'
import Button from 'components/base/Button'
import Text from 'components/base/Text'
@ -15,13 +16,13 @@ import Text from 'components/base/Text'
type Props = {
t: T,
account: Account,
amount: { left: number, right: number },
amount: number,
fees: number,
onNext: Function,
canNext: boolean,
counterValue: string,
}
function Footer({ account, amount, t, onNext, canNext, counterValue }: Props) {
function Footer({ account, amount, fees, t, onNext, canNext }: Props) {
return (
<ModalFooter horizontal alignItems="center">
<Box grow>
@ -30,7 +31,7 @@ function Footer({ account, amount, t, onNext, canNext, counterValue }: Props) {
<FormattedVal
disableRounding
color="dark"
val={amount.left}
val={amount + fees}
unit={account.unit}
showCode
/>
@ -38,12 +39,12 @@ function Footer({ account, amount, t, onNext, canNext, counterValue }: Props) {
<Text ff="Rubik" fontSize={3}>
{'('}
</Text>
<FormattedVal
<CounterValue
ticker={account.currency.units[0].code}
value={amount + fees}
disableRounding
color="grey"
fontSize={3}
val={amount.right}
fiat={counterValue}
showCode
/>
<Text ff="Rubik" fontSize={3}>

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

@ -1,19 +1,14 @@
// @flow
import React, { PureComponent } from 'react'
import { compose } from 'redux'
import { connect } from 'react-redux'
import { translate } from 'react-i18next'
import get from 'lodash/get'
import type { Account } from '@ledgerhq/wallet-common/lib/types'
import type { Unit } from '@ledgerhq/currencies'
import type { T } from 'types/common'
import { MODAL_SEND } from 'config/constants'
import { getCounterValueCode } from 'reducers/settings'
import Breadcrumb from 'components/Breadcrumb'
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 StepConfirmation from './04-step-confirmation'
const mapStateToProps = state => ({
counterValue: getCounterValueCode(state),
})
type Props = {
t: T,
counterValue: string,
}
type State = {
stepIndex: number,
isDeviceReady: boolean,
amount: { left: number, right: number },
amount: number,
fees: number,
account: Account | null,
recipientAddress: string,
fees: {
value: number,
unit: Unit | null,
},
isRBF: boolean,
}
@ -58,14 +45,8 @@ const INITIAL_STATE = {
isDeviceReady: false,
account: null,
recipientAddress: '',
amount: {
left: 0,
right: 0,
},
fees: {
value: 0,
unit: null,
},
amount: 0,
fees: 0,
isRBF: false,
}
@ -73,6 +54,7 @@ class SendModal extends PureComponent<Props, State> {
state = INITIAL_STATE
_steps = GET_STEPS(this.props.t)
_account: Account | null = null
canNext = account => {
const { stepIndex } = this.state
@ -80,7 +62,7 @@ class SendModal extends PureComponent<Props, State> {
// informations
if (stepIndex === 0) {
const { amount, recipientAddress } = this.state
return !!amount.left && !!recipientAddress && !!account
return !!amount && !!recipientAddress && !!account
}
// connect device
@ -102,7 +84,25 @@ class SendModal extends PureComponent<Props, State> {
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 => {
const { stepIndex, account, amount, ...othersState } = this.state
@ -121,8 +121,8 @@ class SendModal extends PureComponent<Props, State> {
}
render() {
const { t, counterValue } = this.props
const { stepIndex, amount, account } = this.state
const { t } = this.props
const { stepIndex, amount, account, fees } = this.state
return (
<Modal
@ -131,6 +131,11 @@ class SendModal extends PureComponent<Props, State> {
render={({ data, onClose }) => {
const acc = account || get(data, 'account', null)
const canNext = this.canNext(acc)
// hack: access the selected account, living in modal data, outside
// of the modal render function
this._account = acc
return (
<ModalBody onClose={onClose} deferHeight={acc ? 630 : 355}>
<ModalTitle>{t('send:title')}</ModalTitle>
@ -140,11 +145,11 @@ class SendModal extends PureComponent<Props, State> {
</ModalContent>
{acc && (
<Footer
counterValue={counterValue}
canNext={canNext}
onNext={this.handleNextStep}
account={acc}
amount={amount}
fees={fees}
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,
interval: DateInterval,
}): Array<BalanceHistoryDay> {
const todayDate = moment().format('YYYY-MM-DD')
const unit = getDefaultUnitByCoinType(account.coinType)
const counterVals = get(counterValues, `${unit.code}.${counterValue}`)
let lastBalance = getBalanceAtIntervalStart(account, interval)
@ -79,14 +80,19 @@ export function getBalanceHistoryForAccount({
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,
// we take the prev day
if (isUndefined(account.balanceByDay[date])) {
balance = lastBalance === null ? 0 : lastBalance * counterVals[date]
balance = lastBalance === null ? 0 : lastBalance * counterVal
} else {
const b = account.balanceByDay[date]
lastBalance = b
balance = b * counterVals[date]
balance = b * counterVal
}
if (isNaN(balance)) {

2
src/reducers/counterValues.js

@ -16,7 +16,7 @@ export type CounterValuesState = {}
const state: CounterValuesState = {}
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) => {

25
src/renderer/createStore.js

@ -1,20 +1,31 @@
// @flow
import type { HashHistory } from 'history'
import { createStore, applyMiddleware, compose } from 'redux'
import { routerMiddleware } from 'react-router-redux'
import thunk from 'redux-thunk'
import db from 'middlewares/db'
import createHistory from 'history/createHashHistory'
import type { HashHistory } from 'history'
import reducers from 'reducers'
export default (history: HashHistory, initialState: any) => {
const middlewares = [routerMiddleware(history), thunk, db]
type Props = {
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(
applyMiddleware(...middlewares),
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 db from 'helpers/db'
import dbMiddleware from 'middlewares/db'
import App from 'components/App'
@ -26,7 +27,7 @@ db.init('settings', {})
db.init('counterValues', {})
const history = createHistory()
const store = createStore(history)
const store = createStore({ history, dbMiddleware })
const rootNode = document.getElementById('app')
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