diff --git a/package.json b/package.json index 7d0272e8..40a7ca87 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "bitcoinjs-lib": "^3.3.2", "bs58check": "^2.1.1", "color": "^3.0.0", - "cross-env": "^5.1.3", + "cross-env": "^5.1.4", "d3": "^4.13.0", "debug": "^3.1.0", "downshift": "^1.30.0", @@ -91,7 +91,7 @@ "source-map-support": "^0.5.3", "styled-components": "^3.2.1", "styled-system": "^2.2.1", - "tippy.js": "^2.2.3", + "tippy.js": "^2.3.0", "victory": "^0.25.6" }, "devDependencies": { @@ -114,7 +114,7 @@ "concurrently": "^3.5.1", "dotenv": "^5.0.1", "electron": "1.8.3", - "electron-builder": "^20.4.0", + "electron-builder": "^20.4.1", "electron-devtools-installer": "^2.2.3", "electron-rebuild": "^1.7.3", "electron-webpack": "1.13.0", diff --git a/src/actions/counterValues.js b/src/actions/counterValues.js index 8bb055f2..22d4c005 100644 --- a/src/actions/counterValues.js +++ b/src/actions/counterValues.js @@ -6,6 +6,8 @@ import { getDefaultUnitByCoinType } from '@ledgerhq/currencies' import type { Dispatch } from 'redux' +import { serializeCounterValues } from 'reducers/counterValues' + import get from 'lodash/get' import db from 'helpers/db' @@ -51,21 +53,19 @@ export const fetchCounterValues: FetchCounterValues = coinType => (dispatch, get ) .then(({ data }) => ({ symbol: `${code}-${counterValue}`, - values: data.Data.reduce((result, d) => { - const date = moment(d.time * 1000).format('YYYY-MM-DD') - result[date] = d.close - return result - }, {}), + values: data.Data.map(d => [moment(d.time * 1000).format('YYYY-MM-DD'), d.close]), })) } - return Promise.all(coinTypes.map(fetchCounterValuesByCoinType)).then(result => { - const newCounterValues = result.reduce((r, v) => { - if (v !== null) { - r[v.symbol] = v.values - } - return r - }, {}) + return Promise.all(coinTypes.map(fetchCounterValuesByCoinType)).then(results => { + const newCounterValues = serializeCounterValues( + results.reduce((r, v) => { + if (v !== null) { + r[v.symbol] = v.values + } + return r + }, {}), + ) if (Object.keys(newCounterValues).length !== 0) { dispatch(updateCounterValues(newCounterValues)) diff --git a/src/components/RequestAmount/index.js b/src/components/RequestAmount/index.js new file mode 100644 index 00000000..5d66c704 --- /dev/null +++ b/src/components/RequestAmount/index.js @@ -0,0 +1,287 @@ +// @flow + +import React, { PureComponent } from 'react' +import { connect } from 'react-redux' +import { getDefaultUnitByCoinType, getFiatUnit } from '@ledgerhq/currencies' + +import isNaN from 'lodash/isNaN' + +import type { Account } from 'types/common' + +import { getCounterValue } from 'reducers/settings' + +import Input from 'components/base/Input' +import Box from 'components/base/Box' + +const mapStateToProps = state => ({ + counterValue: getCounterValue(state), + counterValues: state.counterValues, +}) + +function calculateMax(props) { + const { account, counterValue, lastCounterValue } = props + + const unit = { + currency: getDefaultUnitByCoinType(account.coinType), + fiat: getFiatUnit(counterValue), + } + + const leftMax = account.balance / 10 ** unit.currency.magnitude + + return { + left: account.balance / 10 ** unit.currency.magnitude, + right: leftMax * lastCounterValue, + } +} + +function formatCur(unit, val) { + if (val === '') { + return '' + } + if (val === '0' || val <= 0) { + return 0 + } + const factor = 10 ** unit.magnitude + return (Math.round(val * factor) / factor).toFixed(unit.magnitude) +} + +function cleanValue(value) { + return { + left: value.left || 0, + right: value.right || 0, + } +} + +function parseValue(value) { + return value.toString().replace(/,/, '.') +} + +function getUnit({ account, counterValue }) { + return { + currency: getDefaultUnitByCoinType(account.coinType), + fiat: getFiatUnit(counterValue), + } +} + +function calculateValues({ + dir, + value, + max, + unit, + lastCounterValue, +}: { + dir: string, + value: string | number, + max: Object, + unit: Object, + lastCounterValue: number, +}) { + value = parseValue(value) + + const getMax = (d, v) => { + const result = v > max[d] ? max[d] : v + return isNaN(result) ? 0 : result + } + + const newValue = {} + + if (dir === 'left') { + newValue.left = value === '' ? value : getMax('left', value) + newValue.right = formatCur(unit.fiat, getMax('right', Number(value) * lastCounterValue)) + } + + if (dir === 'right') { + newValue.left = formatCur(unit.currency, getMax('left', Number(value) / lastCounterValue)) + newValue.right = value === '' ? value : getMax('right', value) + } + + return newValue +} + +type Direction = 'left' | 'right' + +type Props = { + account: Account, + lastCounterValue: number, + counterValue: string, + onChange: Function, + value: Object, +} + +type State = { + max: { + left: number, + right: number, + }, + value: { + left: string | number, + right: string | number, + }, +} + +export class RequestAmount extends PureComponent { + static defaultProps = { + value: {}, + } + + constructor(props: Props) { + super() + + this.props = props + + const max = calculateMax(props) + + let value = {} + + if (props.value.left) { + value = { + ...calculateValues({ + dir: 'left', + value: props.value.left, + max, + lastCounterValue: props.lastCounterValue, + unit: getUnit({ + account: props.account, + counterValue: props.counterValue, + }), + }), + } + } + + if (props.value.right) { + value = { + ...calculateValues({ + dir: 'right', + value: props.value.right, + max, + lastCounterValue: props.lastCounterValue, + unit: getUnit({ + account: props.account, + counterValue: props.counterValue, + }), + }), + } + } + + value = cleanValue(value) + + this.state = { + max, + value, + } + } + + componentWillReceiveProps(nextProps: Props) { + if (this.props.account !== nextProps.account) { + this.setState({ + max: calculateMax(nextProps), + }) + } + + if (this.props.value.left !== nextProps.value.left) { + this.setState({ + value: cleanValue({ + ...calculateValues({ + dir: 'left', + value: nextProps.value.left, + max: this.state.max, + lastCounterValue: nextProps.lastCounterValue, + unit: getUnit({ + account: nextProps.account, + counterValue: nextProps.counterValue, + }), + }), + }), + }) + } + + if (this.props.value.right !== nextProps.value.right) { + this.setState({ + value: cleanValue({ + ...calculateValues({ + dir: 'right', + value: nextProps.value.right, + max: this.state.max, + lastCounterValue: nextProps.lastCounterValue, + unit: getUnit({ + account: nextProps.account, + counterValue: nextProps.counterValue, + }), + }), + }), + }) + } + } + + handleChangeAmount = (dir: Direction) => (v: string) => { + const { onChange, lastCounterValue, account, counterValue } = this.props + const { max } = this.state + + v = parseValue(v) + + // Check if value is valid Number + if (isNaN(Number(v))) { + return + } + + const newValue = calculateValues({ + dir, + value: v, + max, + lastCounterValue, + unit: getUnit({ + account, + counterValue, + }), + }) + + this.setState({ + value: newValue, + }) + + onChange(cleanValue(newValue)) + } + + handleBlur = () => + this.setState(prev => ({ + value: cleanValue(prev.value), + })) + + render() { + const { value } = this.state + const { account, counterValue } = this.props + + const unit = getUnit({ + account, + counterValue, + }) + + return ( + + + {unit.currency.code} + + + + + = + + {unit.fiat.code} + + + + + + ) + } +} + +export default connect(mapStateToProps)(RequestAmount) diff --git a/src/components/RequestAmount/stories.js b/src/components/RequestAmount/stories.js new file mode 100644 index 00000000..edb96339 --- /dev/null +++ b/src/components/RequestAmount/stories.js @@ -0,0 +1,29 @@ +// @flow + +import React from 'react' +import { storiesOf } from '@storybook/react' +import { action } from '@storybook/addon-actions' +import { text } 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', () => ( + +)) diff --git a/src/components/SelectAccount/stories.js b/src/components/SelectAccount/stories.js index ac23ba4c..a46156d3 100644 --- a/src/components/SelectAccount/stories.js +++ b/src/components/SelectAccount/stories.js @@ -10,20 +10,20 @@ import { SelectAccount } from 'components/SelectAccount' const chance = new Chance() const stories = storiesOf('Components', module) -const accounts = [...Array(20)].map(() => ({ +export const accounts = [...Array(20)].map(() => ({ id: chance.string(), address: chance.string(), addresses: [], balance: chance.integer({ min: 10000000, max: 2000000000 }), balanceByDay: {}, - coinType: 0, - currency: getCurrencyByCoinType(0), + coinType: 1, + currency: getCurrencyByCoinType(1), index: chance.integer({ min: 0, max: 20 }), name: chance.name(), path: '', rootPath: '', transactions: [], - unit: getDefaultUnitByCoinType(0), + unit: getDefaultUnitByCoinType(1), settings: { minConfirmations: 2, }, diff --git a/src/components/modals/Receive.js b/src/components/modals/Receive/index.js similarity index 85% rename from src/components/modals/Receive.js rename to src/components/modals/Receive/index.js index 4fe836da..4db3f029 100644 --- a/src/components/modals/Receive.js +++ b/src/components/modals/Receive/index.js @@ -2,13 +2,16 @@ import React, { PureComponent, Fragment } from 'react' import { translate } from 'react-i18next' + import get from 'lodash/get' import { MODAL_RECEIVE } from 'constants' +// import { getCounterValue } from 'reducers/settings' + import Box from 'components/base/Box' -import Button from 'components/base/Button' -import Input from 'components/base/Input' +// import Button from 'components/base/Button' +// import Input from 'components/base/Input' import Label from 'components/base/Label' import Modal, { ModalBody, ModalTitle, ModalFooter, ModalContent } from 'components/base/Modal' import ReceiveBox from 'components/ReceiveBox' @@ -51,15 +54,21 @@ class ReceiveModal extends PureComponent { ...defaultState, }) + _steps = [ + 'receiveModal:Infos', + 'receiveModal:ConnectDevice', + 'receiveModal:SecureValidation', + 'receiveModal:Confirmation', + ].map(v => ({ label: this.props.t(v) })) + render() { - const { amount } = this.state - const { t } = this.props + // const { amount } = this.state return ( { + render={({ data }) => { const account = this.getAccount(data) return ( diff --git a/src/components/modals/Send.js b/src/components/modals/Send.js index a4a4fdf7..7ed12705 100644 --- a/src/components/modals/Send.js +++ b/src/components/modals/Send.js @@ -87,16 +87,6 @@ class Send extends PureComponent { ...defaultState, } - componentWillMount() { - const { t } = this.props - this._items = [ - { label: t('sendModal:Amount') }, - { label: t('sendModal:Summary') }, - { label: t('sendModal:SecureValidation') }, - { label: t('sendModal:Confirmation') }, - ] - } - getStepProps(data: any) { const { inputValue, step } = this.state const { t } = this.props @@ -125,8 +115,6 @@ class Send extends PureComponent { } } - _items = [] - handleChangeInput = (key: $Keys) => (value: $Values) => this.setState(prev => ({ inputValue: { @@ -145,6 +133,13 @@ class Send extends PureComponent { ...defaultState, }) + _steps = [ + 'sendModal:Amount', + 'sendModal:Summary', + 'sendModal:SecureValidation', + 'sendModal:Confirmation', + ].map(v => ({ label: this.props.t(v) })) + render() { const { step } = this.state const { t } = this.props @@ -158,7 +153,7 @@ class Send extends PureComponent { {t('send:title')} - + diff --git a/src/helpers/__tests__/balance.test.js b/src/helpers/__tests__/balance.test.js index 50db37bc..b265162c 100644 --- a/src/helpers/__tests__/balance.test.js +++ b/src/helpers/__tests__/balance.test.js @@ -2,11 +2,13 @@ import { getBalanceHistoryForAccount, getBalanceHistoryForAccounts } from 'helpe const counterValues = { 'BTC-USD': { - '2018-01-01': 1000, - '2018-01-02': 2000, - '2018-01-03': 3000, - '2018-01-04': 4000, - '2018-01-05': 5000, + byDate: { + '2018-01-01': 1000, + '2018-01-02': 2000, + '2018-01-03': 3000, + '2018-01-04': 4000, + '2018-01-05': 5000, + }, }, } diff --git a/src/helpers/balance.js b/src/helpers/balance.js index c51279b6..e4d25aa1 100644 --- a/src/helpers/balance.js +++ b/src/helpers/balance.js @@ -70,7 +70,7 @@ export function getBalanceHistoryForAccount({ interval: DateInterval, }): Array { const unit = getDefaultUnitByCoinType(account.coinType) - const counterVals = counterValues[`${unit.code}-${counterValue}`] + const counterVals = counterValues[`${unit.code}-${counterValue}`].byDate let lastBalance = getBalanceAtIntervalStart(account, interval) return mapInterval(interval, date => { let balance = 0 diff --git a/src/helpers/db.js b/src/helpers/db.js index 6110a036..7808cbba 100644 --- a/src/helpers/db.js +++ b/src/helpers/db.js @@ -5,6 +5,7 @@ import set from 'lodash/set' import get from 'lodash/get' import { serializeAccounts, deserializeAccounts } from 'reducers/accounts' +import { serializeCounterValues, deserializeCounterValues } from 'reducers/counterValues' type DBKey = 'settings' | 'accounts' | 'counterValues' @@ -34,6 +35,16 @@ function middleware(type, key, data: any) { } } + if (key === 'counterValues') { + if (type === 'get') { + data = serializeCounterValues(data) + } + + if (type === 'set') { + data = deserializeCounterValues(data) + } + } + return data } diff --git a/src/reducers/counterValues.js b/src/reducers/counterValues.js index 5ac78eb6..f7bfe042 100644 --- a/src/reducers/counterValues.js +++ b/src/reducers/counterValues.js @@ -16,4 +16,28 @@ const handlers = { }), } +export function serializeCounterValues(counterValues: Object) { + return Object.keys(counterValues).reduce((result, key) => { + const counterValue = counterValues[key].sort(([dateA], [dateB]) => (dateA < dateB ? 1 : -1)) + + result[key] = { + byDate: counterValue.reduce((r, [date, value]) => { + r[date] = value + return r + }, {}), + list: counterValue, + } + + return result + }, {}) +} + +export function deserializeCounterValues(counterValues: Object) { + return Object.keys(counterValues).reduce((result, key) => { + const counterValue = counterValues[key] + result[key] = counterValue.list + return result + }, {}) +} + export default handleActions(handlers, state) diff --git a/yarn.lock b/yarn.lock index 487d9750..ea6e37ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2902,9 +2902,9 @@ crocket@^0.9.11: dependencies: xpipe "*" -cross-env@^5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.1.3.tgz#f8ae18faac87692b0a8b4d2f7000d4ec3a85dfd7" +cross-env@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.1.4.tgz#f61c14291f7cc653bb86457002ea80a04699d022" dependencies: cross-spawn "^5.1.0" is-windows "^1.0.0" @@ -3687,6 +3687,35 @@ electron-builder-lib@20.4.0: semver "^5.5.0" temp-file "^3.1.1" +electron-builder-lib@20.4.1: + version "20.4.1" + resolved "https://registry.yarnpkg.com/electron-builder-lib/-/electron-builder-lib-20.4.1.tgz#8560563e21ca0596046eac398cad204154665a5e" + dependencies: + "7zip-bin" "~3.1.0" + app-builder-bin "1.7.2" + async-exit-hook "^2.0.1" + bluebird-lst "^1.0.5" + builder-util "5.6.4" + builder-util-runtime "4.0.5" + chromium-pickle-js "^0.2.0" + debug "^3.1.0" + ejs "^2.5.7" + electron-osx-sign "0.4.10" + electron-publish "20.2.0" + fs-extra-p "^4.5.2" + hosted-git-info "^2.6.0" + is-ci "^1.1.0" + isbinaryfile "^3.0.2" + js-yaml "^3.11.0" + lazy-val "^1.0.3" + minimatch "^3.0.4" + normalize-package-data "^2.4.0" + plist "^2.1.0" + read-config-file "3.0.0" + sanitize-filename "^1.6.1" + semver "^5.5.0" + temp-file "^3.1.1" + electron-builder-lib@~20.2.0: version "20.2.1" resolved "https://registry.yarnpkg.com/electron-builder-lib/-/electron-builder-lib-20.2.1.tgz#ff8dc6ac7f6f3c676fc370ddafb2aba464a17672" @@ -3716,7 +3745,11 @@ electron-builder-lib@~20.2.0: semver "^5.5.0" temp-file "^3.1.1" +<<<<<<< HEAD electron-builder@^20.4.0: +======= +electron-builder@^20.0.4: +>>>>>>> Clean RequestAmount version "20.4.0" resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-20.4.0.tgz#d1393719339c17dd7c2dd16d58b4e138ca6646ce" dependencies: @@ -3735,6 +3768,25 @@ electron-builder@^20.4.0: update-notifier "^2.3.0" yargs "^11.0.0" +electron-builder@^20.4.1: + version "20.4.1" + resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-20.4.1.tgz#ec8b5ada929df8d7a4ab4b1685568be4e12bf8f3" + dependencies: + bluebird-lst "^1.0.5" + builder-util "5.6.4" + builder-util-runtime "4.0.5" + chalk "^2.3.2" + dmg-builder "4.1.1" + electron-builder-lib "20.4.1" + electron-download-tf "4.3.4" + fs-extra-p "^4.5.2" + is-ci "^1.1.0" + lazy-val "^1.0.3" + read-config-file "3.0.0" + sanitize-filename "^1.6.1" + update-notifier "^2.3.0" + yargs "^11.0.0" + electron-devtools-installer@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/electron-devtools-installer/-/electron-devtools-installer-2.2.3.tgz#58b9a4ec507377bc46e091cd43714188e0c369be" @@ -3776,6 +3828,17 @@ electron-is-dev@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-0.3.0.tgz#14e6fda5c68e9e4ecbeff9ccf037cbd7c05c5afe" +electron-osx-sign@0.4.10: + version "0.4.10" + resolved "https://registry.yarnpkg.com/electron-osx-sign/-/electron-osx-sign-0.4.10.tgz#be4f3b89b2a75a1dc5f1e7249081ab2929ca3a26" + dependencies: + bluebird "^3.5.0" + compare-version "^0.1.2" + debug "^2.6.8" + isbinaryfile "^3.0.2" + minimist "^1.2.0" + plist "^2.1.0" + electron-osx-sign@0.4.8: version "0.4.8" resolved "https://registry.yarnpkg.com/electron-osx-sign/-/electron-osx-sign-0.4.8.tgz#f0b9fadded9e1e54ec35fa89877b5c6c34c7bc40" @@ -5257,6 +5320,10 @@ hosted-git-info@^2.1.4, hosted-git-info@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" +hosted-git-info@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222" + hpack.js@^2.1.6: version "2.1.6" resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" @@ -7788,9 +7855,9 @@ pngjs@^3.3.0: version "3.3.2" resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.2.tgz#097c3c2a75feb223eadddea6bc9f0050cf830bc3" -popper.js@^1.12.9: - version "1.12.9" - resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.12.9.tgz#0dfbc2dff96c451bb332edcfcfaaf566d331d5b3" +popper.js@^1.13.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.13.0.tgz#e1e7ff65cc43f7cf9cf16f1510a75e81f84f4565" portfinder@^1.0.9: version "1.0.13" @@ -10036,11 +10103,11 @@ tinycolor2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8" -tippy.js@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-2.2.3.tgz#ae2aa54763a86d38682de199ef7f6442c2926413" +tippy.js@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-2.3.0.tgz#22505ce181f5049063c0201d310f4f0bfa794ce7" dependencies: - popper.js "^1.12.9" + popper.js "^1.13.0" tmp@^0.0.33: version "0.0.33"