From 1a7a96ca8b0c6a27aa67c3dcc4224de9dbd38c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Mon, 11 Jun 2018 21:21:58 +0200 Subject: [PATCH 1/9] save about 10ms at each re-render of Account Page --- src/components/PillsDaysCount.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/components/PillsDaysCount.js b/src/components/PillsDaysCount.js index 53fe1fdc..c2dbea0d 100644 --- a/src/components/PillsDaysCount.js +++ b/src/components/PillsDaysCount.js @@ -1,6 +1,6 @@ // @flow -import React from 'react' +import React, { PureComponent } from 'react' import { translate } from 'react-i18next' import type { T } from 'types/common' @@ -9,7 +9,7 @@ import Pills from 'components/base/Pills' type Props = { selectedTime: string, - onChange: Function, + onChange: ({ key: string, value: *, label: string }) => void, t: T, } @@ -19,18 +19,20 @@ const itemsTimes = [ { key: 'year', value: 365 }, ] -function PillsDaysCount(props: Props) { - const { selectedTime, onChange, t } = props - return ( - ({ - ...item, - label: t(`time:${item.key}`), - }))} - activeKey={selectedTime} - onChange={onChange} - /> - ) +class PillsDaysCount extends PureComponent { + render() { + const { selectedTime, onChange, t } = this.props + return ( + ({ + ...item, + label: t(`time:${item.key}`), + }))} + activeKey={selectedTime} + onChange={onChange} + /> + ) + } } export default translate()(PillsDaysCount) From baaa6bdb5c65ebd66eaf29bc92be1699402c72b5 Mon Sep 17 00:00:00 2001 From: meriadec Date: Mon, 11 Jun 2018 17:47:56 +0200 Subject: [PATCH 2/9] Fix when removing selected currency in select --- .../ImportAccounts/steps/01-step-choose-currency.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/modals/ImportAccounts/steps/01-step-choose-currency.js b/src/components/modals/ImportAccounts/steps/01-step-choose-currency.js index 27f6136e..8e8f1bdd 100644 --- a/src/components/modals/ImportAccounts/steps/01-step-choose-currency.js +++ b/src/components/modals/ImportAccounts/steps/01-step-choose-currency.js @@ -1,6 +1,7 @@ // @flow import React, { Fragment } from 'react' +import isArray from 'lodash/isArray' import SelectCurrency from 'components/SelectCurrency' import Button from 'components/base/Button' @@ -9,7 +10,16 @@ import CurrencyBadge from 'components/base/CurrencyBadge' import type { StepProps } from '../index' function StepChooseCurrency({ currency, setState }: StepProps) { - return setState({ currency })} value={currency} /> + return ( + { + setState({ + currency: isArray(currency) && currency.length === 0 ? null : currency, + }) + }} + value={currency} + /> + ) } export function StepChooseCurrencyFooter({ transitionTo, currency, t }: StepProps) { From cbaf653d2e43214ff2cc43017810e8551701e2c8 Mon Sep 17 00:00:00 2001 From: meriadec Date: Mon, 11 Jun 2018 18:29:57 +0200 Subject: [PATCH 3/9] Separate importable/creatable accounts --- .../ImportAccounts/steps/03-step-import.js | 130 +++++++++++++----- static/i18n/en/importAccounts.yml | 2 +- 2 files changed, 97 insertions(+), 35 deletions(-) diff --git a/src/components/modals/ImportAccounts/steps/03-step-import.js b/src/components/modals/ImportAccounts/steps/03-step-import.js index 0841d37c..960304b3 100644 --- a/src/components/modals/ImportAccounts/steps/03-step-import.js +++ b/src/components/modals/ImportAccounts/steps/03-step-import.js @@ -1,7 +1,9 @@ // @flow import React, { PureComponent } from 'react' +import styled from 'styled-components' import type { Account } from '@ledgerhq/live-common/lib/types' +import uniq from 'lodash/uniq' import { getBridgeForCurrency } from 'bridge' @@ -48,10 +50,16 @@ class StepImport extends PureComponent { this.scanSubscription = bridge.scanAccountsOnDevice(currency, devicePath, { next: account => { - const { scannedAccounts } = this.props + const { scannedAccounts, checkedAccountsIds } = this.props const hasAlreadyBeenScanned = !!scannedAccounts.find(a => account.id === a.id) if (!hasAlreadyBeenScanned) { - setState({ scannedAccounts: [...scannedAccounts, account] }) + setState({ + scannedAccounts: [...scannedAccounts, account], + checkedAccountsIds: + account.operations.length > 0 + ? uniq([...checkedAccountsIds, account.id]) + : checkedAccountsIds, + }) } }, complete: () => setState({ scanStatus: 'finished' }), @@ -105,57 +113,100 @@ class StepImport extends PureComponent { handleToggleSelectAll = () => { const { scannedAccounts, setState } = this.props - setState({ checkedAccountsIds: scannedAccounts.map(a => a.id) }) + setState({ + checkedAccountsIds: scannedAccounts.filter(a => a.operations.length > 0).map(a => a.id), + }) } render() { const { scanStatus, err, scannedAccounts, checkedAccountsIds, existingAccounts } = this.props + const importableAccounts = scannedAccounts.filter(acc => { + if (acc.operations.length <= 0) { + return false + } + return existingAccounts.find(a => a.id === acc.id) === undefined + }) + + const creatableAccounts = scannedAccounts.filter(acc => { + if (acc.operations.length > 0) { + return false + } + return existingAccounts.find(a => a.id === acc.id) === undefined + }) + return ( {err && {err.message}} - {!!scannedAccounts.length && ( - - - {'Select all'} - + + + {!!importableAccounts.length && ( + + + {`Account(s) to import (${importableAccounts.length})`} + + + {'Select all'} + + + )} + + + {importableAccounts.map(account => { + const isChecked = checkedAccountsIds.find(id => id === account.id) !== undefined + const existingAccount = existingAccounts.find(a => a.id === account.id) + const isDisabled = existingAccount !== undefined + return ( + + ) + })} + + {scanStatus === 'scanning' && ( + + + + )} + - )} - - - {scannedAccounts.map(account => { - const isChecked = checkedAccountsIds.find(id => id === account.id) !== undefined - const existingAccount = existingAccounts.find(a => a.id === account.id) - const isDisabled = existingAccount !== undefined - return ( + {creatableAccounts.length > 0 && ( + + + + {'Create account'} + + id === creatableAccounts[0].id) !== undefined + } onClick={this.handleToggleAccount} onAccountUpdate={this.handleAccountUpdate} /> - ) - })} - {scanStatus === 'scanning' && ( - - )} - {['error', 'finished'].includes(scanStatus) && ( + {['error'].includes(scanStatus) && ( + this.handleRemoveAccount(account)} + title={t('settings:removeAccountModal.title')} + subTitle={t('settings:removeAccountModal.subTitle')} + desc={t('settings:removeAccountModal.desc')} + /> ) } diff --git a/static/i18n/en/settings.yml b/static/i18n/en/settings.yml index d28f2a94..b6a77760 100644 --- a/static/i18n/en/settings.yml +++ b/static/i18n/en/settings.yml @@ -57,6 +57,10 @@ softResetModal: title: Clean application cache subTitle: Are you sure houston? desc: Lorem ipsum dolor sit amet +removeAccountModal: + title: Delete this account + subTitle: Are you sure houston? + desc: Lorem ipsum dolor sit amet exportLogs: title: Export Logs desc: Export Logs From 564f57ec4d61d7b71d66097322dbb041cd495200 Mon Sep 17 00:00:00 2001 From: meriadec Date: Tue, 12 Jun 2018 10:26:22 +0200 Subject: [PATCH 8/9] Add accounts: handle cta wording and unselect all --- src/components/MainSideBar.js | 19 ++- .../modals/ImportAccounts/AccountRow.js | 4 +- .../ImportAccounts/steps/03-step-import.js | 159 ++++++++++++------ static/i18n/en/importAccounts.yml | 10 ++ 4 files changed, 130 insertions(+), 62 deletions(-) diff --git a/src/components/MainSideBar.js b/src/components/MainSideBar.js index 06bcafe6..3dcfb4f8 100644 --- a/src/components/MainSideBar.js +++ b/src/components/MainSideBar.js @@ -17,10 +17,13 @@ import type { UpdateStatus } from 'reducers/update' import { MODAL_RECEIVE, MODAL_SEND } from 'config/constants' +import { rgba } from 'styles/helpers' + import { accountsSelector } from 'reducers/accounts' import { openModal } from 'reducers/modals' import { getUpdateStatus } from 'reducers/update' +import Tooltip from 'components/base/Tooltip' import { SideBarList } from 'components/base/SideBar' import Box, { Tabbable } from 'components/base/Box' import Space from 'components/base/Space' @@ -140,9 +143,11 @@ class MainSideBar extends PureComponent { scroll title={t('sidebar:accounts')} titleRight={ - openModal('importAccounts')}> - - + t('importAccounts:title')}> + openModal('importAccounts')}> + + + } items={accountsItems} emptyText={t('emptyState:sidebar.text')} @@ -157,15 +162,15 @@ const PlusWrapper = styled(Tabbable).attrs({ cursor: 'pointer', borderRadius: 1, })` - opacity: 0.4; + color: ${p => p.theme.colors.smoke}; &:hover { - opacity: 1; + color: ${p => p.theme.colors.dark}; } - border: 1px dashed rgba(0, 0, 0, 0); + border: 1px solid transparent; &:focus { - border: 1px dashed rgba(0, 0, 0, 0.2); outline: none; + border-color: ${p => rgba(p.theme.colors.wallet, 0.3)}; } ` diff --git a/src/components/modals/ImportAccounts/AccountRow.js b/src/components/modals/ImportAccounts/AccountRow.js index 99251f28..e571e5bb 100644 --- a/src/components/modals/ImportAccounts/AccountRow.js +++ b/src/components/modals/ImportAccounts/AccountRow.js @@ -17,7 +17,7 @@ import IconCheck from 'icons/Check' type Props = { account: Account, isChecked: boolean, - isDisabled: boolean, + isDisabled?: boolean, onClick: Account => void, onAccountUpdate: Account => void, } @@ -110,7 +110,7 @@ export default class AccountRow extends PureComponent { fontSize={4} color="grey" /> - + ) } diff --git a/src/components/modals/ImportAccounts/steps/03-step-import.js b/src/components/modals/ImportAccounts/steps/03-step-import.js index 960304b3..76cc7987 100644 --- a/src/components/modals/ImportAccounts/steps/03-step-import.js +++ b/src/components/modals/ImportAccounts/steps/03-step-import.js @@ -50,13 +50,15 @@ class StepImport extends PureComponent { this.scanSubscription = bridge.scanAccountsOnDevice(currency, devicePath, { next: account => { - const { scannedAccounts, checkedAccountsIds } = this.props + const { scannedAccounts, checkedAccountsIds, existingAccounts } = this.props const hasAlreadyBeenScanned = !!scannedAccounts.find(a => account.id === a.id) + const hasAlreadyBeenImported = !!existingAccounts.find(a => account.id === a.id) + const isNewAccount = account.operations.length === 0 if (!hasAlreadyBeenScanned) { setState({ scannedAccounts: [...scannedAccounts, account], checkedAccountsIds: - account.operations.length > 0 + !hasAlreadyBeenImported && !isNewAccount ? uniq([...checkedAccountsIds, account.id]) : checkedAccountsIds, }) @@ -111,15 +113,17 @@ class StepImport extends PureComponent { }) } - handleToggleSelectAll = () => { + handleSelectAll = () => { const { scannedAccounts, setState } = this.props setState({ checkedAccountsIds: scannedAccounts.filter(a => a.operations.length > 0).map(a => a.id), }) } + handleUnselectAll = () => this.props.setState({ checkedAccountsIds: [] }) + render() { - const { scanStatus, err, scannedAccounts, checkedAccountsIds, existingAccounts } = this.props + const { scanStatus, err, scannedAccounts, checkedAccountsIds, existingAccounts, t } = this.props const importableAccounts = scannedAccounts.filter(acc => { if (acc.operations.length <= 0) { @@ -135,52 +139,69 @@ class StepImport extends PureComponent { return existingAccounts.find(a => a.id === acc.id) === undefined }) + const isAllSelected = scannedAccounts.filter(acc => acc.operations.length > 0).every(acc => { + const isChecked = !!checkedAccountsIds.find(id => acc.id === id) + const isImported = !!existingAccounts.find(a => acc.id === a.id) + return isChecked || isImported + }) + return ( {err && {err.message}} - - {!!importableAccounts.length && ( - - - {`Account(s) to import (${importableAccounts.length})`} + {(!!importableAccounts.length || scanStatus === 'scanning') && ( + + {!!importableAccounts.length && ( + + + {t('importAccounts:accountToImportSubtitle', { + count: importableAccounts.length, + })} + + + {isAllSelected + ? t('importAccounts:unselectAll') + : t('importAccounts:selectAll')} + - - {'Select all'} - - - )} - - - {importableAccounts.map(account => { - const isChecked = checkedAccountsIds.find(id => id === account.id) !== undefined - const existingAccount = existingAccounts.find(a => a.id === account.id) - const isDisabled = existingAccount !== undefined - return ( - - ) - })} - - {scanStatus === 'scanning' && ( - - - )} + + + {importableAccounts.map(account => { + const isChecked = checkedAccountsIds.find(id => id === account.id) !== undefined + const existingAccount = existingAccounts.find(a => a.id === account.id) + const isDisabled = existingAccount !== undefined + return ( + + ) + })} + + {scanStatus === 'scanning' && ( + + + + )} + - + )} + {creatableAccounts.length > 0 && ( @@ -190,7 +211,7 @@ class StepImport extends PureComponent { fontSize={2} style={{ textTransform: 'uppercase' }} > - {'Create account'} + {t('importAccounts:createNewAccount')} { )} @@ -233,12 +254,44 @@ export const LoadingRow = styled(Box).attrs({ border: 1px dashed ${p => p.theme.colors.fog}; ` -export const StepImportFooter = ({ scanStatus, onClickImport, checkedAccountsIds }: StepProps) => ( - -) +export const StepImportFooter = ({ + scanStatus, + onClickImport, + checkedAccountsIds, + scannedAccounts, + t, +}: StepProps) => { + const willCreateAccount = checkedAccountsIds.some(id => { + const account = scannedAccounts.find(a => a.id === id) + return account && account.operations.length === 0 + }) + + const willImportAccounts = checkedAccountsIds.some(id => { + const account = scannedAccounts.find(a => a.id === id) + return account && account.operations.length > 0 + }) + + const importedAccountsCount = checkedAccountsIds.filter(id => { + const account = scannedAccounts.find(acc => acc.id === id) + return account && account.operations.length > 0 + }).length + + const ctaWording = + willCreateAccount && willImportAccounts + ? `${t('importAccounts:cta.create')} / ${t('importAccounts:cta.import', { + count: importedAccountsCount, + })}` + : willCreateAccount + ? t('importAccounts:cta.create') + : t('importAccounts:cta.import', { count: importedAccountsCount }) + + return ( + + ) +} diff --git a/static/i18n/en/importAccounts.yml b/static/i18n/en/importAccounts.yml index e5d05c98..3a78ea12 100644 --- a/static/i18n/en/importAccounts.yml +++ b/static/i18n/en/importAccounts.yml @@ -4,3 +4,13 @@ breadcrumb: connectDevice: Connect device import: Import finish: End +accountToImportSubtitle: Account to import +accountToImportSubtitle_plural: 'Accounts to import ({{count}})' +selectAll: Select all +unselectAll: Unselect all +createNewAccount: Create new account +retrySync: Retry sync +cta: + create: 'Create account' + import: 'Import account' + import_plural: 'Import accounts' From 8f2f0f9fe6cd1d98eb3bb0e324d6cde480caca9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Tue, 12 Jun 2018 14:19:27 +0200 Subject: [PATCH 9/9] add a legacy derivation for ripple --- src/helpers/derivations.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/helpers/derivations.js b/src/helpers/derivations.js index a5832b83..0ff30c3c 100644 --- a/src/helpers/derivations.js +++ b/src/helpers/derivations.js @@ -11,9 +11,12 @@ const ethLegacyMEW: Derivation = ({ x }) => `44'/60'/0'/${x}` const etcLegacyMEW: Derivation = ({ x }) => `44'/60'/160720'/${x}` +const rippleLegacy: Derivation = ({ x }) => `44'/144'/0'/${x}'` + const legacyDerivations = { ethereum: [ethLegacyMEW], ethereum_classic: [etcLegacyMEW], + ripple: [rippleLegacy], } export const standardDerivation: Derivation = ({ currency, segwit, x }) => {