Browse Source

Merge branch 'develop' into usb-detect

master
Gaëtan Renaudeau 7 years ago
parent
commit
298ce15f33
  1. 1
      README.md
  2. 103
      src/components/DashboardPage/AccountCard.js
  3. 33
      src/components/DashboardPage/AccountCard/Header.js
  4. 94
      src/components/DashboardPage/AccountCard/index.js
  5. 66
      src/components/DashboardPage/AccountCardList.js
  6. 33
      src/components/DashboardPage/AccountCardListHeader.js
  7. 64
      src/components/DashboardPage/AccountCardPlaceholder.js
  8. 31
      src/components/DashboardPage/CurrentGreetings.js
  9. 22
      src/components/DashboardPage/SummaryDesc.js
  10. 186
      src/components/DashboardPage/index.js
  11. 26
      src/components/base/Button/index.js
  12. 3
      src/config/constants.js
  13. 1
      src/config/errors.js
  14. 62
      src/helpers/libcore.js
  15. 9
      src/helpers/linking.js
  16. 18
      src/helpers/promise.js
  17. 6
      src/renderer/init.js
  18. 6
      static/i18n/en/app.json
  19. 4
      static/i18n/en/errors.json

1
README.md

@ -80,7 +80,6 @@ DEBUG_ACTION=1
DEBUG_TAB_KEY=1 DEBUG_TAB_KEY=1
DEBUG_LIBCORE=1 DEBUG_LIBCORE=1
DEBUG_WS=1 DEBUG_WS=1
LEDGER_RESET_ALL=1
LEDGER_DEBUG_ALL_LANGS=1 LEDGER_DEBUG_ALL_LANGS=1
SKIP_GENUINE=1 SKIP_GENUINE=1
SKIP_ONBOARDING=1 SKIP_ONBOARDING=1

103
src/components/DashboardPage/AccountCard.js

@ -1,103 +0,0 @@
// @flow
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import type { Account, Currency } from '@ledgerhq/live-common/lib/types'
import Chart from 'components/base/Chart'
import Bar from 'components/base/Bar'
import Box, { Card } from 'components/base/Box'
import CalculateBalance from 'components/CalculateBalance'
import FormattedVal from 'components/base/FormattedVal'
import Ellipsis from 'components/base/Ellipsis'
import CryptoCurrencyIcon from 'components/CryptoCurrencyIcon'
import DeltaChange from '../DeltaChange'
const Wrapper = styled(Card).attrs({
p: 4,
flex: 1,
})`
cursor: ${p => (p.onClick ? 'pointer' : 'default')};
`
class AccountCard extends PureComponent<{
counterValue: Currency,
account: Account,
onClick?: Account => void,
daysCount: number,
}> {
render() {
const { counterValue, account, onClick, daysCount, ...props } = this.props
return (
<Wrapper onClick={onClick ? () => onClick(account) : null} {...props}>
<Box flow={4}>
<Box horizontal ff="Open Sans|SemiBold" flow={3} alignItems="center">
<Box
alignItems="center"
justifyContent="center"
style={{ color: account.currency.color }}
>
<CryptoCurrencyIcon currency={account.currency} size={20} />
</Box>
<Box grow>
<Box style={{ textTransform: 'uppercase' }} fontSize={0} color="graphite">
{account.currency.name}
</Box>
<Ellipsis fontSize={4} color="dark">
{account.name}
</Ellipsis>
</Box>
</Box>
<Bar size={1} color="fog" />
<Box justifyContent="center">
<FormattedVal
alwaysShowSign={false}
color="dark"
unit={account.unit}
showCode
val={account.balance}
/>
</Box>
</Box>
<CalculateBalance counterValue={counterValue} accounts={[account]} daysCount={daysCount}>
{({ isAvailable, balanceHistory, balanceStart, balanceEnd }) => (
<Box flow={4}>
<Box flow={2} horizontal>
<Box justifyContent="center">
{isAvailable ? (
<FormattedVal
animateTicker
unit={counterValue.units[0]}
val={balanceEnd}
alwaysShowSign={false}
showCode
fontSize={3}
color="graphite"
/>
) : null}
</Box>
<Box grow justifyContent="center">
{isAvailable && !balanceStart.isZero() ? (
<DeltaChange from={balanceStart} to={balanceEnd} alwaysShowSign fontSize={3} />
) : null}
</Box>
</Box>
<Chart
data={balanceHistory}
color={account.currency.color}
height={52}
hideAxis
isInteractive={false}
id={`account-chart-${account.id}`}
unit={account.unit}
/>
</Box>
)}
</CalculateBalance>
</Wrapper>
)
}
}
export default AccountCard

33
src/components/DashboardPage/AccountCard/Header.js

@ -0,0 +1,33 @@
// @flow
import React, { PureComponent } from 'react'
import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import Box from 'components/base/Box'
import Ellipsis from 'components/base/Ellipsis'
import CryptoCurrencyIcon from 'components/CryptoCurrencyIcon'
class AccountCardHeader extends PureComponent<{
currency: CryptoCurrency,
accountName: string,
}> {
render() {
const { currency, accountName } = this.props
return (
<Box horizontal ff="Open Sans|SemiBold" flow={3} alignItems="center">
<Box alignItems="center" justifyContent="center" style={{ color: currency.color }}>
<CryptoCurrencyIcon currency={currency} size={20} />
</Box>
<Box grow>
<Box style={{ textTransform: 'uppercase' }} fontSize={0} color="graphite">
{currency.name}
</Box>
<Ellipsis fontSize={4} color="dark">
{accountName}
</Ellipsis>
</Box>
</Box>
)
}
}
export default AccountCardHeader

94
src/components/DashboardPage/AccountCard/index.js

@ -0,0 +1,94 @@
// @flow
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import type { Account, Currency } from '@ledgerhq/live-common/lib/types'
import Chart from 'components/base/Chart'
import Bar from 'components/base/Bar'
import Box, { Card } from 'components/base/Box'
import CalculateBalance from 'components/CalculateBalance'
import FormattedVal from 'components/base/FormattedVal'
import DeltaChange from 'components/DeltaChange'
import AccountCardHeader from './Header'
const Wrapper = styled(Card).attrs({
p: 4,
flex: 1,
})`
cursor: ${p => (p.onClick ? 'pointer' : 'default')};
`
class AccountCard extends PureComponent<{
counterValue: Currency,
account: Account,
onClick: Account => void,
daysCount: number,
}> {
renderBody = ({ isAvailable, balanceHistory, balanceStart, balanceEnd }: *) => {
const { counterValue, account } = this.props
return (
<Box flow={4}>
<Box flow={2} horizontal>
<Box justifyContent="center">
{isAvailable ? (
<FormattedVal
animateTicker
unit={counterValue.units[0]}
val={balanceEnd}
alwaysShowSign={false}
showCode
fontSize={3}
color="graphite"
/>
) : null}
</Box>
<Box grow justifyContent="center">
{isAvailable && !balanceStart.isZero() ? (
<DeltaChange from={balanceStart} to={balanceEnd} alwaysShowSign fontSize={3} />
) : null}
</Box>
</Box>
<Chart
data={balanceHistory}
color={account.currency.color}
height={52}
hideAxis
isInteractive={false}
id={`account-chart-${account.id}`}
unit={account.unit}
/>
</Box>
)
}
onClick = () => {
const { account, onClick } = this.props
onClick(account)
}
render() {
const { counterValue, account, onClick, daysCount, ...props } = this.props
return (
<Wrapper onClick={this.onClick} {...props}>
<Box flow={4}>
<AccountCardHeader accountName={account.name} currency={account.currency} />
<Bar size={1} color="fog" />
<Box justifyContent="center">
<FormattedVal
alwaysShowSign={false}
color="dark"
unit={account.unit}
showCode
val={account.balance}
/>
</Box>
</Box>
<CalculateBalance counterValue={counterValue} accounts={[account]} daysCount={daysCount}>
{this.renderBody}
</CalculateBalance>
</Wrapper>
)
}
}
export default AccountCard

66
src/components/DashboardPage/AccountCardList.js

@ -0,0 +1,66 @@
// @flow
import React, { Component } from 'react'
import type { Account, Currency } from '@ledgerhq/live-common/lib/types'
import Box from 'components/base/Box'
import AccountCard from './AccountCard'
import AccountCardListHeader from './AccountCardListHeader'
import AccountCardPlaceholder from './AccountCardPlaceholder'
type Props = {
accounts: Account[],
onAccountClick: Account => void,
counterValue: Currency,
daysCount: number,
}
class AccountCardList extends Component<Props> {
render() {
const { accounts, counterValue, daysCount, onAccountClick } = this.props
return (
<Box flow={4}>
<AccountCardListHeader accountsLength={accounts.length} />
<Box
horizontal
flexWrap="wrap"
justifyContent="flex-start"
alignItems="center"
style={{ margin: '0 -16px' }}
>
{accounts
.map(account => ({
key: account.id,
account,
}))
.concat(
Array(3 - (accounts.length % 3))
.fill(null)
.map((_, i) => ({
key: `placeholder_${i}`,
withPlaceholder: i === 0,
})),
)
.map(item => (
<Box key={item.key} flex="33%" p={16}>
{item.account ? (
<AccountCard
key={item.account.id}
counterValue={counterValue}
account={item.account}
daysCount={daysCount}
onClick={onAccountClick}
/>
) : item.withPlaceholder ? (
<AccountCardPlaceholder />
) : null}
</Box>
))}
</Box>
</Box>
)
}
}
export default AccountCardList

33
src/components/DashboardPage/AccountCardListHeader.js

@ -0,0 +1,33 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import Box from 'components/base/Box'
import Text from 'components/base/Text'
import AccountsOrder from './AccountsOrder'
type Props = {
t: T,
accountsLength: number,
}
class AccountCardListHeader extends PureComponent<Props> {
render() {
const { accountsLength, t } = this.props
return (
<Box horizontal alignItems="flex-end">
<Text color="dark" ff="Museo Sans" fontSize={6}>
{t('app:dashboard.accounts.title', { count: accountsLength })}
</Text>
<Box ml="auto" horizontal flow={1}>
<AccountsOrder />
</Box>
</Box>
)
}
}
export default translate()(AccountCardListHeader)

64
src/components/DashboardPage/AccountCardPlaceholder.js

@ -0,0 +1,64 @@
// @flow
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { translate } from 'react-i18next'
import styled from 'styled-components'
import { openModal } from 'reducers/modals'
import { MODAL_ADD_ACCOUNTS } from 'config/constants'
import type { T } from 'types/common'
import { i } from 'helpers/staticPath'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
const Wrapper = styled(Box).attrs({
p: 4,
flex: 1,
alignItems: 'center',
})`
border: 1px dashed ${p => p.theme.colors.fog};
border-radius: 4px;
height: 215px;
`
class AccountCardPlaceholder extends PureComponent<{
t: T,
openModal: string => void,
}> {
onAddAccounts = () => this.props.openModal(MODAL_ADD_ACCOUNTS)
render() {
const { t } = this.props
return (
<Wrapper>
<Box mt={2}>
<img alt="" src={i('empty-account-tile.svg')} />
</Box>
<Box
ff="Open Sans"
fontSize={3}
color="grey"
pb={2}
mt={3}
textAlign="center"
style={{ maxWidth: 150 }}
>
{t('app:dashboard.emptyAccountTile.desc')}
</Box>
<Button primary onClick={this.onAddAccounts}>
{t('app:dashboard.emptyAccountTile.createAccount')}
</Button>
</Wrapper>
)
}
}
export default translate()(
connect(
null,
{
openModal,
},
)(AccountCardPlaceholder),
)

31
src/components/DashboardPage/CurrentGreetings.js

@ -0,0 +1,31 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import Text from 'components/base/Text'
const getCurrentGreetings = () => {
const localTimeHour = new Date().getHours()
const afternoon_breakpoint = 12
const evening_breakpoint = 17
if (localTimeHour >= afternoon_breakpoint && localTimeHour < evening_breakpoint) {
return 'app:dashboard.greeting.afternoon'
} else if (localTimeHour >= evening_breakpoint) {
return 'app:dashboard.greeting.evening'
}
return 'app:dashboard.greeting.morning'
}
class CurrentGettings extends PureComponent<{ t: T }> {
render() {
const { t } = this.props
return (
<Text color="dark" ff="Museo Sans" fontSize={7}>
{t(getCurrentGreetings())}
</Text>
)
}
}
export default translate()(CurrentGettings)

22
src/components/DashboardPage/SummaryDesc.js

@ -0,0 +1,22 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import Text from 'components/base/Text'
class SummaryDesc extends PureComponent<{
t: T,
totalAccounts: number,
}> {
render() {
const { totalAccounts, t } = this.props
return (
<Text color="grey" fontSize={5} ff="Museo Sans|Light">
{t('app:dashboard.summary', { count: totalAccounts })}
</Text>
)
}
}
export default translate()(SummaryDesc)

186
src/components/DashboardPage/index.js

@ -5,7 +5,6 @@ import uniq from 'lodash/uniq'
import { compose } from 'redux' import { compose } from 'redux'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import styled from 'styled-components'
import { push } from 'react-router-redux' import { push } from 'react-router-redux'
import { createStructuredSelector } from 'reselect' import { createStructuredSelector } from 'reselect'
import type { Account, Currency } from '@ledgerhq/live-common/lib/types' import type { Account, Currency } from '@ledgerhq/live-common/lib/types'
@ -14,11 +13,8 @@ import type { T } from 'types/common'
import { colors } from 'styles/theme' import { colors } from 'styles/theme'
import { accountsSelector } from 'reducers/accounts' import { accountsSelector } from 'reducers/accounts'
import { openModal } from 'reducers/modals'
import { MODAL_ADD_ACCOUNTS } from 'config/constants'
import { import {
counterValueCurrencySelector, counterValueCurrencySelector,
localeSelector,
selectedTimeRangeSelector, selectedTimeRangeSelector,
timeRangeDaysByKey, timeRangeDaysByKey,
} from 'reducers/settings' } from 'reducers/settings'
@ -32,27 +28,23 @@ import UpdateNotifier from 'components/UpdateNotifier'
import BalanceInfos from 'components/BalanceSummary/BalanceInfos' import BalanceInfos from 'components/BalanceSummary/BalanceInfos'
import BalanceSummary from 'components/BalanceSummary' import BalanceSummary from 'components/BalanceSummary'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import { i } from 'helpers/staticPath'
import PillsDaysCount from 'components/PillsDaysCount' import PillsDaysCount from 'components/PillsDaysCount'
import Text from 'components/base/Text'
import OperationsList from 'components/OperationsList' import OperationsList from 'components/OperationsList'
import StickyBackToTop from 'components/StickyBackToTop' import StickyBackToTop from 'components/StickyBackToTop'
import Button from 'components/base/Button'
import AccountCard from './AccountCard'
import AccountsOrder from './AccountsOrder'
import EmptyState from './EmptyState' import EmptyState from './EmptyState'
import CurrentGreetings from './CurrentGreetings'
import SummaryDesc from './SummaryDesc'
import AccountCardList from './AccountCardList'
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
accounts: accountsSelector, accounts: accountsSelector,
counterValue: counterValueCurrencySelector, counterValue: counterValueCurrencySelector,
locale: localeSelector,
selectedTimeRange: selectedTimeRangeSelector, selectedTimeRange: selectedTimeRangeSelector,
}) })
const mapDispatchToProps = { const mapDispatchToProps = {
push, push,
saveSettings, saveSettings,
openModal,
} }
type Props = { type Props = {
@ -62,41 +54,33 @@ type Props = {
counterValue: Currency, counterValue: Currency,
selectedTimeRange: TimeRange, selectedTimeRange: TimeRange,
saveSettings: ({ selectedTimeRange: TimeRange }) => *, saveSettings: ({ selectedTimeRange: TimeRange }) => *,
openModal: string => void,
} }
class DashboardPage extends PureComponent<Props> { class DashboardPage extends PureComponent<Props> {
onAccountClick = account => this.props.push(`/account/${account.id}`) onAccountClick = account => this.props.push(`/account/${account.id}`)
handleGreeting = () => {
const localTimeHour = new Date().getHours()
const afternoon_breakpoint = 12
const evening_breakpoint = 17
if (localTimeHour >= afternoon_breakpoint && localTimeHour < evening_breakpoint) {
return 'app:dashboard.greeting.afternoon'
} else if (localTimeHour >= evening_breakpoint) {
return 'app:dashboard.greeting.evening'
}
return 'app:dashboard.greeting.morning'
}
handleChangeSelectedTime = item => { handleChangeSelectedTime = item => {
this.props.saveSettings({ selectedTimeRange: item.key }) this.props.saveSettings({ selectedTimeRange: item.key })
} }
_cacheBalance = null renderHeader = ({ isAvailable, totalBalance, selectedTimeRange, sinceBalance, refBalance }) => (
<BalanceInfos
t={this.props.t}
counterValue={this.props.counterValue}
isAvailable={isAvailable}
totalBalance={totalBalance}
since={selectedTimeRange}
sinceBalance={sinceBalance}
refBalance={refBalance}
/>
)
render() { render() {
const { accounts, t, counterValue, selectedTimeRange, openModal } = this.props const { accounts, t, counterValue, selectedTimeRange } = this.props
const daysCount = timeRangeDaysByKey[selectedTimeRange] const daysCount = timeRangeDaysByKey[selectedTimeRange]
const timeFrame = this.handleGreeting()
const imagePath = i('empty-account-tile.svg')
const totalAccounts = accounts.length const totalAccounts = accounts.length
const totalCurrencies = uniq(accounts.map(a => a.currency.id)).length const totalCurrencies = uniq(accounts.map(a => a.currency.id)).length
const totalOperations = accounts.reduce((sum, a) => sum + a.operations.length, 0) const totalOperations = accounts.reduce((sum, a) => sum + a.operations.length, 0)
const displayOperationsHelper = (account: Account) => account.operations.length > 0
const displayOperations = accounts.some(displayOperationsHelper)
return ( return (
<Fragment> <Fragment>
@ -113,12 +97,8 @@ class DashboardPage extends PureComponent<Props> {
<Fragment> <Fragment>
<Box horizontal alignItems="flex-end"> <Box horizontal alignItems="flex-end">
<Box grow> <Box grow>
<Text color="dark" ff="Museo Sans" fontSize={7}> <CurrentGreetings />
{t(timeFrame)} <SummaryDesc totalAccounts={totalAccounts} />
</Text>
<Text color="grey" fontSize={5} ff="Museo Sans|Light">
{t('app:dashboard.summary', { count: totalAccounts })}
</Text>
</Box> </Box>
<Box> <Box>
<PillsDaysCount <PillsDaysCount
@ -127,105 +107,33 @@ class DashboardPage extends PureComponent<Props> {
/> />
</Box> </Box>
</Box> </Box>
<Fragment>
<BalanceSummary <BalanceSummary
counterValue={counterValue} counterValue={counterValue}
chartId="dashboard-chart" chartId="dashboard-chart"
chartColor={colors.wallet} chartColor={colors.wallet}
accounts={accounts}
selectedTimeRange={selectedTimeRange}
daysCount={daysCount}
renderHeader={this.renderHeader}
/>
<AccountCardList
onAccountClick={this.onAccountClick}
accounts={accounts}
daysCount={daysCount}
counterValue={counterValue}
/>
{totalOperations > 0 && (
<OperationsList
onAccountClick={this.onAccountClick}
accounts={accounts} accounts={accounts}
selectedTimeRange={selectedTimeRange} title={t('app:dashboard.recentActivity')}
daysCount={daysCount} withAccount
renderHeader={({
isAvailable,
totalBalance,
selectedTimeRange,
sinceBalance,
refBalance,
}) => (
<BalanceInfos
t={t}
counterValue={counterValue}
isAvailable={isAvailable}
totalBalance={totalBalance}
since={selectedTimeRange}
sinceBalance={sinceBalance}
refBalance={refBalance}
/>
)}
/> />
<Box flow={4}> )}
<Box horizontal alignItems="flex-end"> <StickyBackToTop />
<Text color="dark" ff="Museo Sans" fontSize={6}>
{t('app:dashboard.accounts.title', { count: accounts.length })}
</Text>
<Box ml="auto" horizontal flow={1}>
<AccountsOrder />
</Box>
</Box>
<Box
horizontal
flexWrap="wrap"
justifyContent="flex-start"
alignItems="center"
style={{ margin: '0 -16px' }}
>
{accounts
.concat(
Array(3 - (accounts.length % 3))
.fill(null)
.map((_, i) => i === 0),
)
.map((account, i) => (
<Box
key={typeof account === 'object' ? account.id : `placeholder_${i}`}
flex="33%"
p={16}
>
{account ? (
typeof account === 'object' ? (
<AccountCard
key={account.id}
counterValue={counterValue}
account={account}
daysCount={daysCount}
onClick={this.onAccountClick}
/>
) : (
<Wrapper>
<Box mt={2}>
<img alt="" src={imagePath} />
</Box>
<Box
ff="Open Sans"
fontSize={3}
color="grey"
pb={2}
mt={3}
textAlign="center"
style={{ maxWidth: 150 }}
>
{t('app:dashboard.emptyAccountTile.desc')}
</Box>
<Button primary onClick={() => openModal(MODAL_ADD_ACCOUNTS)}>
{t('app:dashboard.emptyAccountTile.createAccount')}
</Button>
</Wrapper>
)
) : null}
</Box>
))}
</Box>
</Box>
{displayOperations && (
<OperationsList
onAccountClick={this.onAccountClick}
accounts={accounts}
title={t('app:dashboard.recentActivity')}
withAccount
/>
)}
<StickyBackToTop />
</Fragment>
</Fragment> </Fragment>
) : ( ) : (
<EmptyState /> <EmptyState />
@ -243,13 +151,3 @@ export default compose(
), ),
translate(), translate(),
)(DashboardPage) )(DashboardPage)
const Wrapper = styled(Box).attrs({
p: 4,
flex: 1,
alignItems: 'center',
})`
border: 1px dashed ${p => p.theme.colors.fog};
border-radius: 4px;
height: 215px;
`

26
src/components/base/Button/index.js

@ -93,16 +93,22 @@ const buttonStyles: { [_: string]: Style } = {
background: ${rgba(c, 0.1)}; background: ${rgba(c, 0.1)};
` `
}, },
active: p => ` active: p => {
color: ${darken( const c = p.outlineColor
p.outlineColor ? p.theme.colors[p.outlineColor] || p.outlineColor : p.theme.colors.wallet, ? p.theme.colors[p.outlineColor] || p.outlineColor
0.1, : p.theme.colors.wallet
)}; return `
border-color: ${darken( background: ${rgba(c, 0.15)};
p.outlineColor ? p.theme.colors[p.outlineColor] || p.outlineColor : p.theme.colors.wallet, color: ${darken(
0.1, p.outlineColor ? p.theme.colors[p.outlineColor] || p.outlineColor : p.theme.colors.wallet,
)}; 0.1,
`, )};
border-color: ${darken(
p.outlineColor ? p.theme.colors[p.outlineColor] || p.outlineColor : p.theme.colors.wallet,
0.1,
)};
`
},
}, },
outlineGrey: { outlineGrey: {
default: p => ` default: p => `

3
src/config/constants.js

@ -45,7 +45,7 @@ export const SYNC_ALL_INTERVAL = 120 * 1000
export const SYNC_BOOT_DELAY = 2 * 1000 export const SYNC_BOOT_DELAY = 2 * 1000
export const SYNC_PENDING_INTERVAL = 10 * 1000 export const SYNC_PENDING_INTERVAL = 10 * 1000
export const SYNC_MAX_CONCURRENT = intFromEnv('LEDGER_SYNC_MAX_CONCURRENT', 1) export const SYNC_MAX_CONCURRENT = intFromEnv('LEDGER_SYNC_MAX_CONCURRENT', 1)
export const SYNC_TIMEOUT = intFromEnv('SYNC_TIMEOUT', 30 * 1000) export const SYNC_TIMEOUT = intFromEnv('SYNC_TIMEOUT', 60 * 1000)
// Endpoints... // Endpoints...
@ -79,7 +79,6 @@ export const DEBUG_TAB_KEY = boolFromEnv('DEBUG_TAB_KEY')
export const DEBUG_LIBCORE = boolFromEnv('DEBUG_LIBCORE') export const DEBUG_LIBCORE = boolFromEnv('DEBUG_LIBCORE')
export const DEBUG_WS = boolFromEnv('DEBUG_WS') export const DEBUG_WS = boolFromEnv('DEBUG_WS')
export const DEBUG_SYNC = boolFromEnv('DEBUG_SYNC') export const DEBUG_SYNC = boolFromEnv('DEBUG_SYNC')
export const LEDGER_RESET_ALL = boolFromEnv('LEDGER_RESET_ALL')
export const LEDGER_DEBUG_ALL_LANGS = boolFromEnv('LEDGER_DEBUG_ALL_LANGS') export const LEDGER_DEBUG_ALL_LANGS = boolFromEnv('LEDGER_DEBUG_ALL_LANGS')
export const SKIP_GENUINE = boolFromEnv('SKIP_GENUINE') export const SKIP_GENUINE = boolFromEnv('SKIP_GENUINE')
export const SKIP_ONBOARDING = boolFromEnv('SKIP_ONBOARDING') export const SKIP_ONBOARDING = boolFromEnv('SKIP_ONBOARDING')

1
src/config/errors.js

@ -12,6 +12,7 @@ export const UserRefusedAddress = createCustomErrorClass('UserRefusedAddress')
export const WrongDeviceForAccount = createCustomErrorClass('WrongDeviceForAccount') export const WrongDeviceForAccount = createCustomErrorClass('WrongDeviceForAccount')
export const DeviceNotGenuineError = createCustomErrorClass('DeviceNotGenuine') export const DeviceNotGenuineError = createCustomErrorClass('DeviceNotGenuine')
export const DeviceGenuineSocketEarlyClose = createCustomErrorClass('DeviceGenuineSocketEarlyClose') export const DeviceGenuineSocketEarlyClose = createCustomErrorClass('DeviceGenuineSocketEarlyClose')
export const TimeoutTagged = createCustomErrorClass('TimeoutTagged')
// db stuff, no need to translate // db stuff, no need to translate
export const NoDBPathGiven = createCustomErrorClass('NoDBPathGiven') export const NoDBPathGiven = createCustomErrorClass('NoDBPathGiven')

62
src/helpers/libcore.js

@ -16,6 +16,7 @@ import { isSegwitPath, isUnsplitPath } from 'helpers/bip32'
import * as accountIdHelper from 'helpers/accountId' import * as accountIdHelper from 'helpers/accountId'
import { createCustomErrorClass, deserializeError } from './errors' import { createCustomErrorClass, deserializeError } from './errors'
import { getAccountPlaceholderName, getNewAccountPlaceholderName } from './accountName' import { getAccountPlaceholderName, getNewAccountPlaceholderName } from './accountName'
import { timeoutTagged } from './promise'
const NoAddressesFound = createCustomErrorClass('NoAddressesFound') const NoAddressesFound = createCustomErrorClass('NoAddressesFound')
@ -202,6 +203,7 @@ const coreSyncAccount = (core, account) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const eventReceiver = createEventReceiver(core, e => { const eventReceiver = createEventReceiver(core, e => {
const code = e.getCode() const code = e.getCode()
logger.debug(`syncAccountEvent ${code}`, { type: 'libcore-sync' })
if (code === core.EVENT_CODE.UNDEFINED || code === core.EVENT_CODE.SYNCHRONIZATION_FAILED) { if (code === core.EVENT_CODE.UNDEFINED || code === core.EVENT_CODE.SYNCHRONIZATION_FAILED) {
const payload = e.getPayload() const payload = e.getPayload()
const message = ( const message = (
@ -270,7 +272,7 @@ async function scanNextAccount(props: {
const shouldSyncAccount = true // TODO: let's sync everytime. maybe in the future we can optimize. const shouldSyncAccount = true // TODO: let's sync everytime. maybe in the future we can optimize.
if (shouldSyncAccount) { if (shouldSyncAccount) {
await coreSyncAccount(core, njsAccount) await timeoutTagged('coreSyncAccount', 30000, coreSyncAccount(core, njsAccount))
} }
if (isUnsubscribed()) return [] if (isUnsubscribed()) return []
@ -325,10 +327,10 @@ async function getOrCreateWallet(
): NJSWallet { ): NJSWallet {
const pool = core.getPoolInstance() const pool = core.getPoolInstance()
try { try {
const wallet = await pool.getWallet(WALLET_IDENTIFIER) const wallet = await timeoutTagged('getWallet', 5000, pool.getWallet(WALLET_IDENTIFIER))
return wallet return wallet
} catch (err) { } catch (err) {
const currency = await pool.getCurrency(currencyId) const currency = await timeoutTagged('getCurrency', 5000, pool.getCurrency(currencyId))
const splitConfig = isUnsplit ? SPLITTED_CURRENCIES[currencyId] || null : null const splitConfig = isUnsplit ? SPLITTED_CURRENCIES[currencyId] || null : null
const coinType = splitConfig ? splitConfig.coinType : '<coin_type>' const coinType = splitConfig ? splitConfig.coinType : '<coin_type>'
const walletConfig = isSegwit const walletConfig = isSegwit
@ -342,9 +344,11 @@ async function getOrCreateWallet(
} }
: undefined : undefined
const njsWalletConfig = createWalletConfig(core, walletConfig) const njsWalletConfig = createWalletConfig(core, walletConfig)
const wallet = await core const wallet = await timeoutTagged(
.getPoolInstance() 'createWallet',
.createWallet(WALLET_IDENTIFIER, currency, njsWalletConfig) 10000,
core.getPoolInstance().createWallet(WALLET_IDENTIFIER, currency, njsWalletConfig),
)
return wallet return wallet
} }
} }
@ -368,21 +372,33 @@ async function buildAccountRaw({
core: *, core: *,
ops: NJSOperation[], ops: NJSOperation[],
}): Promise<AccountRaw> { }): Promise<AccountRaw> {
const njsBalance = await njsAccount.getBalance() const njsBalance = await timeoutTagged('getBalance', 10000, njsAccount.getBalance())
const balance = njsBalance.toLong() const balance = njsBalance.toLong()
const jsCurrency = getCryptoCurrencyById(currencyId) const jsCurrency = getCryptoCurrencyById(currencyId)
const { derivations } = await wallet.getAccountCreationInfo(accountIndex) const { derivations } = await timeoutTagged(
'getAccountCreationInfo',
10000,
wallet.getAccountCreationInfo(accountIndex),
)
const [walletPath, accountPath] = derivations const [walletPath, accountPath] = derivations
// retrieve xpub // retrieve xpub
const xpub = njsAccount.getRestoreKey() const xpub = njsAccount.getRestoreKey()
// blockHeight // blockHeight
const { height: blockHeight } = await njsAccount.getLastBlock() const { height: blockHeight } = await timeoutTagged(
'getLastBlock',
30000,
njsAccount.getLastBlock(),
)
// get a bunch of fresh addresses // get a bunch of fresh addresses
const rawAddresses = await njsAccount.getFreshPublicAddresses() const rawAddresses = await timeoutTagged(
'getFreshPublicAddresses',
10000,
njsAccount.getFreshPublicAddresses(),
)
const addresses = rawAddresses.map(njsAddress => ({ const addresses = rawAddresses.map(njsAddress => ({
str: njsAddress.toString(), str: njsAddress.toString(),
@ -500,7 +516,11 @@ export async function syncAccount({
const isUnsplit = isUnsplitPath(freshAddressPath, SPLITTED_CURRENCIES[currencyId]) const isUnsplit = isUnsplitPath(freshAddressPath, SPLITTED_CURRENCIES[currencyId])
let njsWallet let njsWallet
try { try {
njsWallet = await core.getPoolInstance().getWallet(decodedAccountId.walletName) njsWallet = await timeoutTagged(
'getWallet',
10000,
core.getPoolInstance().getWallet(decodedAccountId.walletName),
)
} catch (e) { } catch (e) {
logger.warn(`Have to reimport the account... (${e})`) logger.warn(`Have to reimport the account... (${e})`)
njsWallet = await getOrCreateWallet( njsWallet = await getOrCreateWallet(
@ -514,20 +534,28 @@ export async function syncAccount({
let njsAccount let njsAccount
try { try {
njsAccount = await njsWallet.getAccount(index) njsAccount = await timeoutTagged('getAccount', 10000, njsWallet.getAccount(index))
} catch (e) { } catch (e) {
logger.warn(`Have to recreate the account... (${e.message})`) logger.warn(`Have to recreate the account... (${e.message})`)
const extendedInfos = await njsWallet.getExtendedKeyAccountCreationInfo(index) const extendedInfos = await timeoutTagged(
'getEKACI',
10000,
njsWallet.getExtendedKeyAccountCreationInfo(index),
)
extendedInfos.extendedKeys.push(decodedAccountId.xpub) extendedInfos.extendedKeys.push(decodedAccountId.xpub)
njsAccount = await njsWallet.newAccountWithExtendedKeyInfo(extendedInfos) njsAccount = await timeoutTagged(
'newAWEKI',
10000,
njsWallet.newAccountWithExtendedKeyInfo(extendedInfos),
)
} }
const unsub = await coreSyncAccount(core, njsAccount) const unsub = await timeoutTagged('coreSyncAccount', 30000, coreSyncAccount(core, njsAccount))
unsub() unsub()
const query = njsAccount.queryOperations() const query = njsAccount.queryOperations()
const ops = await query.complete().execute() const ops = await timeoutTagged('ops', 30000, query.complete().execute())
const njsBalance = await njsAccount.getBalance() const njsBalance = await timeoutTagged('getBalance', 10000, njsAccount.getBalance())
const syncedRawAccount = await buildAccountRaw({ const syncedRawAccount = await buildAccountRaw({
njsAccount, njsAccount,

9
src/helpers/linking.js

@ -1,12 +1,17 @@
// @flow // @flow
import { shell } from 'electron'
import { track } from 'analytics/segment' import { track } from 'analytics/segment'
let shell
if (!process.env.STORYBOOK_ENV) {
const electron = require('electron')
shell = electron.shell
}
export const openURL = ( export const openURL = (
url: string, url: string,
customEventName: string = 'OpenURL', customEventName: string = 'OpenURL',
extraParams: Object = {}, extraParams: Object = {},
) => { ) => {
track(customEventName, { ...extraParams, url }) track(customEventName, { ...extraParams, url })
shell.openExternal(url) shell && shell.openExternal(url)
} }

18
src/helpers/promise.js

@ -2,6 +2,7 @@
// small utilities for Promises // small utilities for Promises
import logger from 'logger' import logger from 'logger'
import { TimeoutTagged } from 'config/errors'
export const delay = (ms: number): Promise<void> => new Promise(f => setTimeout(f, ms)) export const delay = (ms: number): Promise<void> => new Promise(f => setTimeout(f, ms))
@ -65,6 +66,23 @@ export function createCancelablePolling(
return { unsubscribe, promise } return { unsubscribe, promise }
} }
export const timeoutTagged = <T>(tag: string, delay: number, promise: Promise<T>): Promise<T> =>
new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new TimeoutTagged('timeout', { tag }))
}, delay)
promise.then(
r => {
clearTimeout(timeout)
resolve(r)
},
e => {
clearTimeout(timeout)
reject(e)
},
)
})
export const promisify = (fn: any) => (...args: any) => export const promisify = (fn: any) => (...args: any) =>
new Promise((resolve, reject) => new Promise((resolve, reject) =>
fn(...args, (err: Error, res: any) => { fn(...args, (err: Error, res: any) => {

6
src/renderer/init.js

@ -12,7 +12,6 @@ import { runMigrations } from 'migrations'
import createStore from 'renderer/createStore' import createStore from 'renderer/createStore'
import events from 'renderer/events' import events from 'renderer/events'
import { LEDGER_RESET_ALL } from 'config/constants'
import { enableGlobalTab, disableGlobalTab, isGlobalTabEnabled } from 'config/global-tab' import { enableGlobalTab, disableGlobalTab, isGlobalTabEnabled } from 'config/global-tab'
import { fetchAccounts } from 'actions/accounts' import { fetchAccounts } from 'actions/accounts'
@ -25,7 +24,6 @@ import resolveUserDataDirectory from 'helpers/resolveUserDataDirectory'
import db from 'helpers/db' import db from 'helpers/db'
import dbMiddleware from 'middlewares/db' import dbMiddleware from 'middlewares/db'
import CounterValues from 'helpers/countervalues' import CounterValues from 'helpers/countervalues'
import hardReset from 'helpers/hardReset'
import { decodeAccountsModel, encodeAccountsModel } from 'reducers/accounts' import { decodeAccountsModel, encodeAccountsModel } from 'reducers/accounts'
@ -43,10 +41,6 @@ const TAB_KEY = 9
db.init(userDataDirectory) db.init(userDataDirectory)
async function init() { async function init() {
if (LEDGER_RESET_ALL) {
await hardReset()
}
await runMigrations() await runMigrations()
db.init(userDataDirectory) db.init(userDataDirectory)
db.registerTransform('app', 'accounts', { get: decodeAccountsModel, set: encodeAccountsModel }) db.registerTransform('app', 'accounts', { get: decodeAccountsModel, set: encodeAccountsModel })

6
static/i18n/en/app.json

@ -196,7 +196,7 @@
"successDescription_plural": "Your accounts have been created.", "successDescription_plural": "Your accounts have been created.",
"createNewAccount": { "createNewAccount": {
"title": "Add a new account", "title": "Add a new account",
"noOperationOnLastAccount": "No transactions found on your last new account <1><0>{{accountName}}</0></1>. You can add a new account after you've started transacting on that account.", "noOperationOnLastAccount": "There are no transactions on your last created <1><0>{{accountName}}</0></1> account. You must first receive crypto assets on that account before you can add a new one.",
"noAccountToCreate": "No <1><0>{{currencyName}}</0></1> account was found to create" "noAccountToCreate": "No <1><0>{{currencyName}}</0></1> account was found to create"
}, },
"cta": { "cta": {
@ -399,7 +399,7 @@
}, },
"softResetModal": { "softResetModal": {
"title": "Clear cache", "title": "Clear cache",
"desc": "Clearing the Ledger Live cache forces network resynchronization" "desc": "Clearing the Ledger Live cache forces network resynchronization. Your settings and accounts are not affected. The private keys to access your crypto assets in the blockchain remain secure on your Ledger device and on your Recovery sheet."
}, },
"removeAccountModal": { "removeAccountModal": {
"title": "Remove account", "title": "Remove account",
@ -459,4 +459,4 @@
"desc_2": "Please beware that Ledger does not provide financial, tax, or legal advice. You should take such decisions on your own or consult with reliable experts.", "desc_2": "Please beware that Ledger does not provide financial, tax, or legal advice. You should take such decisions on your own or consult with reliable experts.",
"cta": "Got it" "cta": "Got it"
} }
} }

4
static/i18n/en/errors.json

@ -115,6 +115,10 @@
"title": "Oops, a time out occurred", "title": "Oops, a time out occurred",
"description": "It took too long for the server to respond." "description": "It took too long for the server to respond."
}, },
"TimeoutTagged": {
"title": "Oops, a time out occurred ({{tag}})",
"description": "It took too long for the server to respond."
},
"TransportError": { "TransportError": {
"title": "Something went wrong. Please reconnect your device.", "title": "Something went wrong. Please reconnect your device.",
"description": "{{message}}" "description": "{{message}}"

Loading…
Cancel
Save