diff --git a/package.json b/package.json index 14c84564..ce767421 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "productName": "Ledger Live", "description": "Ledger Live - Desktop", "repository": "https://github.com/LedgerHQ/ledger-live-desktop", - "version": "0.1.0-alpha.7", + "version": "0.1.0-alpha.8", "author": "Ledger", "license": "MIT", "scripts": { @@ -41,7 +41,7 @@ "@ledgerhq/hw-app-xrp": "^4.13.0", "@ledgerhq/hw-transport": "^4.13.0", "@ledgerhq/hw-transport-node-hid": "^4.13.0", - "@ledgerhq/ledger-core": "1.4.5", + "@ledgerhq/ledger-core": "1.6.0", "@ledgerhq/live-common": "2.29.0", "async": "^2.6.1", "axios": "^0.18.0", diff --git a/src/api/Ethereum.js b/src/api/Ethereum.js index 5abb3139..6d0260b0 100644 --- a/src/api/Ethereum.js +++ b/src/api/Ethereum.js @@ -1,8 +1,7 @@ // @flow -import axios from 'axios' -import { retry } from 'helpers/promise' import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types' -import { blockchainBaseURL, userFriendlyError } from './Ledger' +import network from './network' +import { blockchainBaseURL } from './Ledger' export type Block = { height: number } // TODO more fields actually export type Tx = { @@ -47,37 +46,40 @@ export const apiForCurrency = (currency: CryptoCurrency): API => { } return { async getTransactions(address, blockHash) { - const { data } = await userFriendlyError( - retry( - () => - axios.get(`${baseURL}/addresses/${address}/transactions`, { - params: { blockHash, noToken: 1 }, - }), - { maxRetry: 3 }, - ), - ) + const { data } = await network({ + method: 'GET', + url: `${baseURL}/addresses/${address}/transactions`, + params: { blockHash, noToken: 1 }, + }) return data }, async getCurrentBlock() { - const { data } = await userFriendlyError( - retry(() => axios.get(`${baseURL}/blocks/current`), { maxRetry: 3 }), - ) + const { data } = await network({ + method: 'GET', + url: `${baseURL}/blocks/current`, + }) return data }, async getAccountNonce(address) { - const { data } = await userFriendlyError( - retry(() => axios.get(`${baseURL}/addresses/${address}/nonce`), { maxRetry: 3 }), - ) + const { data } = await network({ + method: 'GET', + url: `${baseURL}/addresses/${address}/nonce`, + }) return data[0].nonce }, async broadcastTransaction(tx) { - const { data } = await userFriendlyError(axios.post(`${baseURL}/transactions/send`, { tx })) + const { data } = await network({ + method: 'POST', + url: `${baseURL}/transactions/send`, + data: { tx }, + }) return data.result }, async getAccountBalance(address) { - const { data } = await userFriendlyError( - retry(() => axios.get(`${baseURL}/addresses/${address}/balance`), { maxRetry: 3 }), - ) + const { data } = await network({ + method: 'GET', + url: `${baseURL}/addresses/${address}/balance`, + }) return data[0].balance }, } diff --git a/src/api/Fees.js b/src/api/Fees.js index 6f933309..6796f66f 100644 --- a/src/api/Fees.js +++ b/src/api/Fees.js @@ -1,8 +1,8 @@ // @flow import invariant from 'invariant' -import axios from 'axios' import type { Currency } from '@ledgerhq/live-common/lib/types' -import { blockchainBaseURL, userFriendlyError } from './Ledger' +import { blockchainBaseURL } from './Ledger' +import network from './network' export type Fees = { [_: string]: number, @@ -11,7 +11,7 @@ export type Fees = { export const getEstimatedFees = async (currency: Currency): Promise => { const baseURL = blockchainBaseURL(currency) invariant(baseURL, `Fees for ${currency.id} are not supported`) - const { data, status } = await userFriendlyError(axios.get(`${baseURL}/fees`)) + const { data, status } = await network({ method: 'GET', url: `${baseURL}/fees` }) if (data) { return data } diff --git a/src/api/network.js b/src/api/network.js new file mode 100644 index 00000000..ce619668 --- /dev/null +++ b/src/api/network.js @@ -0,0 +1,22 @@ +// @flow +import axios from 'axios' +import { GET_CALLS_RETRY, GET_CALLS_TIMEOUT } from 'config/constants' +import { userFriendlyError } from 'api/Ledger' +import { retry } from 'helpers/promise' + +const doRequest = axios // TODO later introduce a way to run it in renderer based on a env, we will diverge this implementation + +export default (arg: Object) => { + let promise + if (arg.method === 'GET') { + if (!('timeout' in arg)) { + arg.timeout = GET_CALLS_TIMEOUT + } + promise = retry(() => doRequest(arg), { + maxRetry: GET_CALLS_RETRY, + }) + } else { + promise = doRequest(arg) + } + return userFriendlyError(promise) +} diff --git a/src/components/AccountPage/index.js b/src/components/AccountPage/index.js index 95080626..7b0f5101 100644 --- a/src/components/AccountPage/index.js +++ b/src/components/AccountPage/index.js @@ -74,8 +74,8 @@ type State = { class AccountPage extends PureComponent { state = { - selectedTime: 'week', - daysCount: 7, + selectedTime: 'month', + daysCount: 30, } handleChangeSelectedTime = item => @@ -180,7 +180,7 @@ class AccountPage extends PureComponent { )} /> - + ) : ( diff --git a/src/components/BalanceSummary/index.js b/src/components/BalanceSummary/index.js index 80080c44..78d47a7c 100644 --- a/src/components/BalanceSummary/index.js +++ b/src/components/BalanceSummary/index.js @@ -35,7 +35,7 @@ const BalanceSummary = ({ }: Props) => { const account = accounts.length === 1 ? accounts[0] : undefined return ( - + {({ isAvailable, balanceHistory, balanceStart, balanceEnd }) => !isAvailable ? null : ( @@ -57,7 +57,7 @@ const BalanceSummary = ({ unit={account ? account.unit : null} color={chartColor} data={balanceHistory} - height={250} + height={200} currency={counterValue} tickXScale={selectedTime} renderTickY={val => formatShort(counterValue.units[0], val)} diff --git a/src/components/DashboardPage/AccountCard.js b/src/components/DashboardPage/AccountCard.js index 1aa61b95..1e392ff2 100644 --- a/src/components/DashboardPage/AccountCard.js +++ b/src/components/DashboardPage/AccountCard.js @@ -41,7 +41,7 @@ class AccountCard extends PureComponent<{ - {account.unit.code} + {account.currency.name} {account.name} diff --git a/src/components/DashboardPage/index.js b/src/components/DashboardPage/index.js index 4e43c8f1..8ec63080 100644 --- a/src/components/DashboardPage/index.js +++ b/src/components/DashboardPage/index.js @@ -56,8 +56,9 @@ type State = { class DashboardPage extends PureComponent { state = { - selectedTime: 'week', - daysCount: 7, + // save to user preference? + selectedTime: 'month', + daysCount: 30, } onAccountClick = account => this.props.push(`/account/${account.id}`) @@ -167,7 +168,6 @@ class DashboardPage extends PureComponent { {displayOperations && ( ({ - accounts: accountsSelector(state), - updateStatus: getUpdateStatus(state), -}) - -const mapDispatchToProps = { - push, - openModal, -} - -type Props = { - t: T, - accounts: Account[], - location: Location, - push: string => void, - openModal: string => void, - updateStatus: UpdateStatus, -} - -class MainSideBar extends PureComponent { - push(to: string) { - const { push } = this.props - const { - location: { pathname }, - } = this.props - if (pathname === to) { - return - } - push(to) - } - - render() { - const { t, accounts, openModal, location, updateStatus } = this.props - const { pathname } = location - - const navigationItems = [ - { - value: 'dashboard', - label: t('dashboard:title'), - icon: IconPieChart, - iconActiveColor: 'wallet', - onClick: () => this.push('/'), - isActive: pathname === '/', - hasNotif: updateStatus === 'downloaded', - }, - { - value: 'send', - label: t('send:title'), - icon: IconSend, - iconActiveColor: 'wallet', - onClick: () => openModal(MODAL_SEND), - }, - { - value: 'receive', - label: t('receive:title'), - icon: IconReceive, - iconActiveColor: 'wallet', - onClick: () => openModal(MODAL_RECEIVE), - }, - { - value: 'manager', - label: t('sidebar:manager'), - icon: IconManager, - iconActiveColor: 'wallet', - onClick: () => this.push('/manager'), - isActive: pathname === '/manager', - }, - { - value: 'exchange', - label: t('sidebar:exchange'), - icon: IconExchange, - iconActiveColor: 'wallet', - onClick: () => this.push('/exchange'), - isActive: pathname === '/exchange', - }, - ] - - const accountsItems = accounts.map(account => { - const accountURL = `/account/${account.id}` - return { - value: account.id, - label: account.name, - desc: () => ( - - ), - iconActiveColor: account.currency.color, - icon: getCryptoCurrencyIcon(account.currency), - onClick: () => this.push(accountURL), - isActive: pathname === accountURL, - } - }) - - return ( - - - - - t('importAccounts:title')}> - openModal('importAccounts')}> - - - - } - items={accountsItems} - emptyText={t('emptyState:sidebar.text')} - /> - - ) - } -} - -const PlusWrapper = styled(Tabbable).attrs({ - p: 1, - cursor: 'pointer', - borderRadius: 1, -})` - color: ${p => p.theme.colors.smoke}; - &:hover { - color: ${p => p.theme.colors.dark}; - } - - border: 1px solid transparent; - &:focus { - outline: none; - border-color: ${p => rgba(p.theme.colors.wallet, 0.3)}; - } -` - -const decorate = compose( - withRouter, - translate(), - connect( - mapStateToProps, - mapDispatchToProps, - ), -) -export default decorate(MainSideBar) diff --git a/src/components/MainSideBar/AccountListItem.js b/src/components/MainSideBar/AccountListItem.js new file mode 100644 index 00000000..b7c36d09 --- /dev/null +++ b/src/components/MainSideBar/AccountListItem.js @@ -0,0 +1,37 @@ +// @flow + +import React, { PureComponent } from 'react' +import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react' + +import type { Account } from '@ledgerhq/live-common/lib/types' + +import FormattedVal from 'components/base/FormattedVal' +import { SideBarListItem } from 'components/base/SideBar' + +export default class AccountListItem extends PureComponent<{ + account: Account, + push: string => void, + isActive: boolean, +}> { + render() { + const { account, push, isActive } = this.props + const accountURL = `/account/${account.id}` + const item = { + label: account.name, + desc: () => ( + + ), + iconActiveColor: account.currency.color, + icon: getCryptoCurrencyIcon(account.currency), + onClick: () => push(accountURL), + isActive, + } + return + } +} diff --git a/src/components/MainSideBar/AddAccountButton.js b/src/components/MainSideBar/AddAccountButton.js new file mode 100644 index 00000000..4e7054a1 --- /dev/null +++ b/src/components/MainSideBar/AddAccountButton.js @@ -0,0 +1,43 @@ +// @flow + +import React, { PureComponent } from 'react' +import styled from 'styled-components' + +import { Tabbable } from 'components/base/Box' +import Tooltip from 'components/base/Tooltip' +import IconCirclePlus from 'icons/CirclePlus' + +import { rgba } from 'styles/helpers' + +const PlusWrapper = styled(Tabbable).attrs({ + p: 1, + cursor: 'pointer', + borderRadius: 1, +})` + color: ${p => p.theme.colors.smoke}; + &:hover { + color: ${p => p.theme.colors.dark}; + } + + border: 1px solid transparent; + &:focus { + outline: none; + border-color: ${p => rgba(p.theme.colors.wallet, 0.3)}; + } +` + +export default class AddAccountButton extends PureComponent<{ + onClick: () => void, + tooltipText: string, +}> { + render() { + const { onClick, tooltipText } = this.props + return ( + tooltipText}> + + + + + ) + } +} diff --git a/src/components/MainSideBar/index.js b/src/components/MainSideBar/index.js new file mode 100644 index 00000000..b425f7e8 --- /dev/null +++ b/src/components/MainSideBar/index.js @@ -0,0 +1,155 @@ +// @flow + +import React, { PureComponent } from 'react' +import { translate } from 'react-i18next' +import { connect } from 'react-redux' +import { compose } from 'redux' +import { withRouter } from 'react-router' +import { push } from 'react-router-redux' + +import type { Location } from 'react-router' +import type { Account } from '@ledgerhq/live-common/lib/types' + +import type { T } from 'types/common' +import type { UpdateStatus } from 'reducers/update' + +import { MODAL_RECEIVE, MODAL_SEND } from 'config/constants' + +import { accountsSelector } from 'reducers/accounts' +import { openModal } from 'reducers/modals' +import { getUpdateStatus } from 'reducers/update' + +import { SideBarList, SideBarListItem } from 'components/base/SideBar' +import Box from 'components/base/Box' +import GrowScroll from 'components/base/GrowScroll' +import Space from 'components/base/Space' + +import IconManager from 'icons/Manager' +import IconPieChart from 'icons/PieChart' +import IconReceive from 'icons/Receive' +import IconSend from 'icons/Send' +import IconExchange from 'icons/Exchange' + +import AccountListItem from './AccountListItem' +import AddAccountButton from './AddAccountButton' + +const mapStateToProps = state => ({ + accounts: accountsSelector(state), + updateStatus: getUpdateStatus(state), +}) + +const mapDispatchToProps = { + push, + openModal, +} + +type Props = { + t: T, + accounts: Account[], + location: Location, + push: string => void, + openModal: string => void, + updateStatus: UpdateStatus, +} + +class MainSideBar extends PureComponent { + push = (to: string) => { + const { push } = this.props + const { + location: { pathname }, + } = this.props + if (pathname === to) { + return + } + push(to) + } + + handleClickDashboard = () => this.push('/') + handleOpenSendModal = () => this.props.openModal(MODAL_SEND) + handleOpenReceiveModal = () => this.props.openModal(MODAL_RECEIVE) + handleClickManager = () => this.push('/manager') + handleClickExchange = () => this.push('/exchange') + handleOpenImportModal = () => this.props.openModal('importAccounts') + + render() { + const { t, accounts, location, updateStatus } = this.props + const { pathname } = location + + const addAccountButton = ( + + ) + + return ( + + + + + + + + + + + + + {accounts.map(account => ( + + ))} + + + + + ) + } +} + +const decorate = compose( + withRouter, + translate(), + connect( + mapStateToProps, + mapDispatchToProps, + ), +) +export default decorate(MainSideBar) diff --git a/src/components/OperationsList/index.js b/src/components/OperationsList/index.js index 37588a87..80497d5c 100644 --- a/src/components/OperationsList/index.js +++ b/src/components/OperationsList/index.js @@ -52,8 +52,7 @@ const mapDispatchToProps = { type Props = { account: Account, accounts: Account[], - canShowMore: boolean, - openModal: Function, + openModal: (string, Object) => *, t: T, withAccount?: boolean, title?: string, @@ -66,10 +65,18 @@ type State = { const initialState = { nbToShow: 20, } + +const footerPlaceholder = ( + + + No more operations + + +) + export class OperationsList extends PureComponent { static defaultProps = { withAccount: false, - canShowMore: false, } state = initialState @@ -86,7 +93,7 @@ export class OperationsList extends PureComponent { } render() { - const { account, accounts, canShowMore, t, title, withAccount } = this.props + const { account, accounts, t, title, withAccount } = this.props const { nbToShow } = this.state if (!account && !accounts) { @@ -130,13 +137,14 @@ export class OperationsList extends PureComponent { ))} - {canShowMore && - !groupedOperations.completed && ( - - {t('operationsList:showMore')} - - - )} + {!groupedOperations.completed ? ( + + {t('operationsList:showMore')} + + + ) : ( + footerPlaceholder + )} ) diff --git a/src/components/OperationsList/stories.js b/src/components/OperationsList/stories.js index b8237619..5324fb8a 100644 --- a/src/components/OperationsList/stories.js +++ b/src/components/OperationsList/stories.js @@ -15,10 +15,6 @@ const account2 = genAccount('account2') stories.add('OperationsList', () => ( - + )) diff --git a/src/components/SelectCurrency/index.js b/src/components/SelectCurrency/index.js index 6635f5b6..e6ae33a6 100644 --- a/src/components/SelectCurrency/index.js +++ b/src/components/SelectCurrency/index.js @@ -14,7 +14,7 @@ import Select from 'components/base/Select' import Box from 'components/base/Box' type OwnProps = { - onChange: Option => void, + onChange: (?Option) => void, currencies?: CryptoCurrency[], value?: CryptoCurrency, placeholder: string, @@ -30,7 +30,9 @@ const mapStateToProps = (state, props: OwnProps) => ({ }) const SelectCurrency = ({ onChange, value, t, placeholder, currencies, ...props }: Props) => { - const options = currencies ? currencies.map(c => ({ ...c, value: c.id, label: c.name })) : [] + const options = currencies + ? currencies.map(c => ({ ...c, value: c.id, label: c.name, currency: c })) + : [] return (