From b6c77a88ae48f9f3477a6ed60eef4949b14ab9ba Mon Sep 17 00:00:00 2001 From: meriadec Date: Thu, 19 Apr 2018 11:19:07 +0200 Subject: [PATCH] Handle recent types & settings changes on Operation and Account - make sure no extra field is used in the app - use currency settings to display confirmed/not-confirmed operation - add blockHeight fetch when syncing account --- src/components/OperationsList/Operation.js | 209 +++++++++++++++ src/components/OperationsList/index.js | 199 +------------- .../SettingsPage/sections/Currencies.js | 29 +- src/components/modals/OperationDetails.js | 249 ++++++++++-------- src/helpers/btc.js | 10 +- src/reducers/accounts.js | 11 +- src/reducers/settings.js | 25 +- src/types/common.js | 9 +- yarn.lock | 3 - 9 files changed, 422 insertions(+), 322 deletions(-) create mode 100644 src/components/OperationsList/Operation.js diff --git a/src/components/OperationsList/Operation.js b/src/components/OperationsList/Operation.js new file mode 100644 index 00000000..f417188e --- /dev/null +++ b/src/components/OperationsList/Operation.js @@ -0,0 +1,209 @@ +// @flow + +import React, { PureComponent } from 'react' +import { connect } from 'react-redux' +import styled from 'styled-components' +import moment from 'moment' +import noop from 'lodash/noop' +import { getIconByCoinType } from '@ledgerhq/currencies/react' + +import type { Account, Operation as OperationType } from '@ledgerhq/wallet-common/lib/types' + +import type { T } from 'types/common' + +import { currencySettingsSelector, marketIndicatorSelector } from 'reducers/settings' +import { rgba, getMarketColor } from 'styles/helpers' + +import Box from 'components/base/Box' +import Text from 'components/base/Text' +import CounterValue from 'components/CounterValue' +import FormattedVal from 'components/base/FormattedVal' + +import ConfirmationCheck from './ConfirmationCheck' + +const mapStateToProps = (state, props) => ({ + minConfirmations: currencySettingsSelector(state, props.account.currency).confirmationsNb, + marketIndicator: marketIndicatorSelector(state), +}) + +const DATE_COL_SIZE = 100 +const ACCOUNT_COL_SIZE = 150 +const AMOUNT_COL_SIZE = 150 +const CONFIRMATION_COL_SIZE = 44 + +const OperationRaw = styled(Box).attrs({ + horizontal: true, + alignItems: 'center', +})` + cursor: pointer; + border-bottom: 1px solid ${p => p.theme.colors.lightGrey}; + height: 68px; + + &:last-child { + border-bottom: 0; + } + + &:hover { + background: ${p => rgba(p.theme.colors.wallet, 0.04)}; + } +` + +const Address = ({ value }: { value: string }) => { + const addrSize = value.length / 2 + + const left = value.slice(0, 10) + const right = value.slice(-addrSize) + const middle = value.slice(10, -addrSize) + + return ( + +
{left}
+ {middle} +
{right}
+
+ ) +} + +const AddressEllipsis = styled.div` + display: block; + flex-shrink: 1; + min-width: 20px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +` + +const Day = styled(Text).attrs({ + color: 'dark', + fontSize: 3, + ff: 'Open Sans', +})` + letter-spacing: 0.3px; + text-transform: uppercase; +` + +const Hour = styled(Day).attrs({ + color: 'grey', +})`` + +const Cell = styled(Box).attrs({ + px: 4, + horizontal: true, + alignItems: 'center', +})` + width: ${p => (p.size ? `${p.size}px` : '')}; + overflow: ${p => (p.noOverflow ? 'hidden' : '')}; +` + +type Props = { + account: Account, + minConfirmations: number, + onAccountClick: Function, + onOperationClick: Function, + marketIndicator: string, + t: T, + op: OperationType, + withAccount?: boolean, +} + +class Operation extends PureComponent { + static defaultProps = { + onAccountClick: noop, + onOperationClick: noop, + withAccount: false, + } + + render() { + const { + account, + minConfirmations, + onAccountClick, + onOperationClick, + t, + op, + withAccount, + marketIndicator, + } = this.props + const { unit, currency } = account + const time = moment(op.date) + const Icon = getIconByCoinType(account.currency.coinType) + const isNegative = op.amount < 0 + const type = !isNegative ? 'from' : 'to' + + const marketColor = getMarketColor({ + marketIndicator, + isNegative, + }) + + return ( + onOperationClick({ operation: op, account, type, marketColor })}> + + + + + + + {t(`operationsList:${type}`)} + + {time.format('HH:mm')} + + + {withAccount && + account && ( + { + e.stopPropagation() + onAccountClick(account) + }} + > + + {Icon && } + + + {account.name} + + + )} + +
+ + + + + + + + + ) + } +} + +export default connect(mapStateToProps)(Operation) diff --git a/src/components/OperationsList/index.js b/src/components/OperationsList/index.js index c36ae15b..4d31a567 100644 --- a/src/components/OperationsList/index.js +++ b/src/components/OperationsList/index.js @@ -6,19 +6,17 @@ import moment from 'moment' import { connect } from 'react-redux' import { compose } from 'redux' import { translate } from 'react-i18next' -import { getIconByCoinType } from '@ledgerhq/currencies/react' import { groupAccountOperationsByDay, groupAccountsOperationsByDay, } from '@ledgerhq/wallet-common/lib/helpers/account' -import type { Account, Operation as OperationType } from '@ledgerhq/wallet-common/lib/types' + +import type { Account } from '@ledgerhq/wallet-common/lib/types' import noop from 'lodash/noop' import keyBy from 'lodash/keyBy' -import { getMarketColor, rgba } from 'styles/helpers' - -import type { Settings, T } from 'types/common' +import type { T } from 'types/common' import { MODAL_OPERATION_DETAILS } from 'config/constants' @@ -27,17 +25,10 @@ import { openModal } from 'reducers/modals' import IconAngleDown from 'icons/AngleDown' import Box, { Card } from 'components/base/Box' -import CounterValue from 'components/CounterValue' -import FormattedVal from 'components/base/FormattedVal' import Text from 'components/base/Text' import Defer from 'components/base/Defer' -import ConfirmationCheck from './ConfirmationCheck' - -const DATE_COL_SIZE = 100 -const ACCOUNT_COL_SIZE = 150 -const AMOUNT_COL_SIZE = 150 -const CONFIRMATION_COL_SIZE = 44 +import Operation from './Operation' const calendarOpts = { sameDay: 'LL – [Today]', @@ -47,45 +38,6 @@ const calendarOpts = { sameElse: 'LL', } -const Day = styled(Text).attrs({ - color: 'dark', - fontSize: 3, - ff: 'Open Sans', -})` - letter-spacing: 0.3px; - text-transform: uppercase; -` - -const Hour = styled(Day).attrs({ - color: 'grey', -})`` - -const OperationRaw = styled(Box).attrs({ - horizontal: true, - alignItems: 'center', -})` - cursor: pointer; - border-bottom: 1px solid ${p => p.theme.colors.lightGrey}; - height: 68px; - - &:last-child { - border-bottom: 0; - } - - &:hover { - background: ${p => rgba(p.theme.colors.wallet, 0.04)}; - } -` - -const Cell = styled(Box).attrs({ - px: 4, - horizontal: true, - alignItems: 'center', -})` - width: ${p => (p.size ? `${p.size}px` : '')}; - overflow: ${p => (p.noOverflow ? 'hidden' : '')}; -` - const ShowMore = styled(Box).attrs({ horizontal: true, flow: 1, @@ -102,141 +54,6 @@ const ShowMore = styled(Box).attrs({ } ` -const AddressEllipsis = styled.div` - display: block; - flex-shrink: 1; - min-width: 20px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -` - -const Address = ({ value }: { value: string }) => { - const addrSize = value.length / 2 - - const left = value.slice(0, 10) - const right = value.slice(-addrSize) - const middle = value.slice(10, -addrSize) - - return ( - -
{left}
- {middle} -
{right}
-
- ) -} - -const Operation = ({ - account, - minConfirmations, - onAccountClick, - onOperationClick, - t, - op, - withAccount, - marketIndicator, -}: { - account: Account, - minConfirmations: number, - onAccountClick: Function, - onOperationClick: Function, - t: T, - op: OperationType, - withAccount?: boolean, - marketIndicator: string, -}) => { - const { unit, currency } = account - const time = moment(op.date) - const Icon = getIconByCoinType(account.currency.coinType) - const isNegative = op.amount < 0 - const type = !isNegative ? 'from' : 'to' - - const marketColor = getMarketColor({ - marketIndicator, - isNegative, - }) - - return ( - onOperationClick({ operation: op, account, type, marketColor })}> - - - - - - - {t(`operationsList:${type}`)} - - {time.format('HH:mm')} - - - {withAccount && - account && ( - { - e.stopPropagation() - onAccountClick(account) - }} - > - - {Icon && } - - - {account.name} - - - )} - -
- - - - - - - - - ) -} - -Operation.defaultProps = { - onAccountClick: noop, - onOperationClick: noop, - withAccount: false, -} - -const mapStateToProps = state => ({ - settings: state.settings, -}) - const mapDispatchToProps = { openModal, } @@ -251,7 +68,6 @@ type Props = { withAccount?: boolean, nbToShow: number, title?: string, - settings: Settings, } export class OperationsList extends PureComponent { @@ -271,7 +87,6 @@ export class OperationsList extends PureComponent { canShowMore, nbToShow, onAccountClick, - settings, t, title, withAccount, @@ -295,7 +110,7 @@ export class OperationsList extends PureComponent { {title} )} - {groupedOperations.map(group => { + {groupedOperations.sections.map(group => { const d = moment(group.day) return ( @@ -312,8 +127,6 @@ export class OperationsList extends PureComponent { { } } -export default compose(translate(), connect(mapStateToProps, mapDispatchToProps))(OperationsList) +export default compose(translate(), connect(null, mapDispatchToProps))(OperationsList) diff --git a/src/components/SettingsPage/sections/Currencies.js b/src/components/SettingsPage/sections/Currencies.js index 22e6f72d..1f71fbdf 100644 --- a/src/components/SettingsPage/sections/Currencies.js +++ b/src/components/SettingsPage/sections/Currencies.js @@ -25,9 +25,6 @@ import { // instead of using same default for all. // const CURRENCY_DEFAULTS_SETTINGS: CurrencySettings = { - // will be overwritten - coinType: 0, - confirmationsToSpend: 10, minConfirmationsToSpend: 10, maxConfirmationsToSpend: 50, @@ -57,7 +54,7 @@ class TabCurrencies extends PureComponent { getCurrencySettings() { const { settings } = this.props const { currency } = this.state - return settings.currencies.find(c => c.coinType === currency.coinType) + return settings.currenciesSettings[currency.coinType] } handleChangeCurrency = (currency: Currency) => this.setState({ currency }) @@ -73,23 +70,23 @@ class TabCurrencies extends PureComponent { const currencySettings = this.getCurrencySettings() let newCurrenciesSettings = [] if (!currencySettings) { - newCurrenciesSettings = [ - ...settings.currencies, - { + newCurrenciesSettings = { + ...settings.currenciesSettings, + [currency.coinType]: { ...CURRENCY_DEFAULTS_SETTINGS, - coinType: currency.coinType, [key]: val, }, - ] + } } else { - newCurrenciesSettings = settings.currencies.map(c => { - if (c.coinType !== currency.coinType) { - return c - } - return { ...c, [key]: val } - }) + newCurrenciesSettings = { + ...settings.currenciesSettings, + [currency.coinType]: { + ...currencySettings, + [key]: val, + }, + } } - saveSettings({ currencies: newCurrenciesSettings }) + saveSettings({ currenciesSettings: newCurrenciesSettings }) } render() { diff --git a/src/components/modals/OperationDetails.js b/src/components/modals/OperationDetails.js index 89da1583..ccbbc8db 100644 --- a/src/components/modals/OperationDetails.js +++ b/src/components/modals/OperationDetails.js @@ -1,11 +1,13 @@ // @flow import React from 'react' +import { connect } from 'react-redux' import { shell } from 'electron' import { translate } from 'react-i18next' import styled from 'styled-components' import moment from 'moment' +import type { Account, Operation } from '@ledgerhq/wallet-common/lib/types' import type { T } from 'types/common' import { MODAL_OPERATION_DETAILS } from 'config/constants' @@ -16,6 +18,8 @@ import Bar from 'components/base/Bar' import FormattedVal from 'components/base/FormattedVal' import Modal, { ModalBody, ModalTitle, ModalFooter, ModalContent } from 'components/base/Modal' +import { currencySettingsSelector } from 'reducers/settings' + import CounterValue from 'components/CounterValue' import ConfirmationCheck from 'components/OperationsList/ConfirmationCheck' @@ -48,116 +52,155 @@ const B = styled(Bar).attrs({ size: 1, })`` -const OperationDetails = ({ t }: { t: T }) => ( - { - const { marketColor, operation, account, type } = data +const mapStateToProps = (state, props) => ({ + minConfirmations: currencySettingsSelector(state, props.account.currency).confirmationsNb, +}) - const { name, unit, currency, minConfirmations } = account - const { id, amount, confirmations, date, from, to } = operation +type Props = { + t: T, + operation: Operation, + account: Account, + type: 'from' | 'to', + onClose: Function, + minConfirmations: number, + marketColor: string, +} - const isConfirmed = confirmations >= minConfirmations +const OperationDetails = connect(mapStateToProps)((props: Props) => { + const { t, type, onClose, minConfirmations, operation, account, marketColor } = props + const { id, amount, date } = operation - return ( - - Operation details - - - = minConfirmations + return ( + + Operation details + + + + + + + + + - - - - - - - - - - Acccount - {name} - - - - Date - {moment(date).format('LLL')} - - - - Status - - - {isConfirmed - ? t('operationDetails:confirmed') - : t('operationDetails:notConfirmed')} - - ({confirmations}) - - - - - From - - {from.map((v, i) => ( - - {v} - - ))} - - - - - To - - {to.map((v, i) => ( - - {v} - - ))} - - - - - Identifier - - {id} - - - - - - - - + + + + Acccount + {name} + + + + Date + {moment(date).format('LLL')} + + + + Status + + + {isConfirmed ? t('operationDetails:confirmed') : t('operationDetails:notConfirmed')} + + {`(${confirmations})`} + + + + + From + + {from.map((v, i) => ( + + {v} + + ))} + + + + + To + + {to.map((v, i) => ( + + {v} + + ))} + + + + + Identifier + + {id} + + + + + + + + + ) +}) + +type ModalRenderProps = { + data: { + account: Account, + operation: Operation, + type: 'from' | 'to', + marketColor: string, + }, + onClose: Function, +} + +const OperationDetailsWrapper = ({ t }: { t: T }) => ( + { + const { data, onClose } = props + const { operation, account, type, marketColor } = data + return ( + ) }} /> ) -export default translate()(OperationDetails) +export default translate()(OperationDetailsWrapper) diff --git a/src/helpers/btc.js b/src/helpers/btc.js index 4ec15342..343de057 100644 --- a/src/helpers/btc.js +++ b/src/helpers/btc.js @@ -2,6 +2,7 @@ import ledger from 'ledger-test-library' import bitcoin from 'bitcoinjs-lib' +import axios from 'axios' import type { OperationRaw } from '@ledgerhq/wallet-common/lib/types' import groupBy from 'lodash/groupBy' @@ -40,7 +41,7 @@ export function computeOperation(addresses: Array, accountId: string) { confirmations: t.confirmations, date: t.received_at, accountId, - blockHeight: 0, + blockHeight: t.block.height, } } } @@ -196,6 +197,11 @@ export async function getAccount({ }) : getAddress({ type: 'external', index: 0 }) + // TODO: in the future, it should be done with the libc call + const { + data: { height: blockHeight }, + } = await axios.get('https://api.ledgerwallet.com/blockchain/v2/btc_testnet/blocks/current') + const account = { ...nextAddress, coinType, @@ -204,6 +210,8 @@ export async function getAccount({ balanceByDay: getBalanceByDay(operations), rootPath, operations, + blockTime: new Date(), + blockHeight, } onProgress({ diff --git a/src/reducers/accounts.js b/src/reducers/accounts.js index 49c9633a..6806f79e 100644 --- a/src/reducers/accounts.js +++ b/src/reducers/accounts.js @@ -101,7 +101,16 @@ export function serializeAccounts(accounts: any): Account[] { } export function deserializeAccounts(accounts: Account[]) { - return accounts.map(accountModel.encode) + return accounts.map(account => { + // as account can be passed by main process, the Date types + // can be converted to string. we ensure here that we have real + // date + if (typeof account.blockTime === 'string') { + account.blockTime = new Date(account.blockTime) + } + + return accountModel.encode(account) + }) } export default handleActions(handlers, state) diff --git a/src/reducers/settings.js b/src/reducers/settings.js index 03d049e3..27e2953f 100644 --- a/src/reducers/settings.js +++ b/src/reducers/settings.js @@ -2,10 +2,12 @@ import { handleActions } from 'redux-actions' import { getFiatUnit } from '@ledgerhq/currencies' +import type { Currency } from '@ledgerhq/currencies' import get from 'lodash/get' -import type { Settings } from 'types/common' +import type { Settings, CurrencySettings } from 'types/common' +import type { State } from 'reducers' export type SettingsState = Object @@ -18,8 +20,20 @@ const defaultState: SettingsState = { isEnabled: false, value: '', }, - currencies: [], marketIndicator: 'eastern', + currenciesSettings: {}, +} + +const CURRENCY_DEFAULTS_SETTINGS: CurrencySettings = { + confirmationsToSpend: 10, + minConfirmationsToSpend: 10, + maxConfirmationsToSpend: 50, + + confirmationsNb: 10, + minConfirmationsNb: 10, + maxConfirmationsNb: 50, + + transactionFees: 10, } const state: SettingsState = { @@ -50,4 +64,11 @@ export const getLanguage = (state: Object) => get(state.settings, 'language', de export const getOrderAccounts = (state: Object) => get(state.settings, 'orderAccounts', defaultState.orderAccounts) +export const currencySettingsSelector = (state: State, currency: Currency): CurrencySettings => { + const currencySettings = state.settings.currenciesSettings[currency.coinType] + return currencySettings || CURRENCY_DEFAULTS_SETTINGS +} + +export const marketIndicatorSelector = (state: State) => state.settings.marketIndicator + export default handleActions(handlers, state) diff --git a/src/types/common.js b/src/types/common.js index a6944484..5435fd60 100644 --- a/src/types/common.js +++ b/src/types/common.js @@ -13,8 +13,6 @@ export type Devices = Array // -------------------- Settings export type CurrencySettings = { - coinType: number, - confirmationsToSpend: number, minConfirmationsToSpend: number, maxConfirmationsToSpend: number, @@ -26,16 +24,21 @@ export type CurrencySettings = { transactionFees: number, } +export type CurrenciesSettings = { + [coinType: number]: CurrencySettings, +} + export type Settings = { language: string, + orderAccounts: string, username: string, counterValue: string, password: { isEnabled: boolean, value: string, }, - currencies: CurrencySettings[], marketIndicator: 'eastern' | 'western', + currenciesSettings: CurrenciesSettings, } export type T = (?string, ?Object) => string diff --git a/yarn.lock b/yarn.lock index 8396a3d4..c53e19ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8050,9 +8050,6 @@ ledger-test-library@KhalilBellakrid/ledger-test-library-nodejs#7d37482: dependencies: axios "^0.17.1" bindings "^1.3.0" - electron "^1.8.2" - electron-builder "^20.0.4" - electron-rebuild "^1.7.3" nan "^2.6.2" prebuild-install "^2.2.2"