From 648d0d193054d330beff35ce3d071e066d15e262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Thu, 28 Jun 2018 10:12:57 +0200 Subject: [PATCH 1/6] Refactor and Polish Add Accounts --- .../base/AccountsList/AccountRow.js | 140 +++++-------- src/components/base/AccountsList/index.js | 190 ++++++++++-------- src/components/base/Input/index.js | 17 +- src/components/base/Spoiler/index.js | 51 +++-- .../modals/AccountSettingRenderBody.js | 32 ++- src/components/modals/AddAccounts/index.js | 41 +++- .../AddAccounts/steps/03-step-import.js | 143 ++++++++----- .../AddAccounts/steps/04-step-finish.js | 17 +- src/config/constants.js | 2 + src/helpers/accountName.js | 13 +- static/i18n/en/app.yml | 12 +- 11 files changed, 368 insertions(+), 290 deletions(-) diff --git a/src/components/base/AccountsList/AccountRow.js b/src/components/base/AccountsList/AccountRow.js index 580c8355..0b3fce50 100644 --- a/src/components/base/AccountsList/AccountRow.js +++ b/src/components/base/AccountsList/AccountRow.js @@ -11,100 +11,79 @@ import Radio from 'components/base/Radio' import CryptoCurrencyIcon from 'components/CryptoCurrencyIcon' import FormattedVal from 'components/base/FormattedVal' import Input from 'components/base/Input' -import IconEdit from 'icons/Edit' -import IconCheck from 'icons/Check' -import type { T } from 'types/common' +import { MAX_ACCOUNT_NAME_SIZE } from 'config/constants' type Props = { account: Account, isChecked: boolean, isDisabled?: boolean, - onClick: Account => void, - onAccountUpdate: Account => void, - t: T, + autoFocusInput?: boolean, + accountName: string, + onToggleAccount?: (Account, boolean) => void, + onEditName?: (Account, string) => void, } -type State = { - isEditing: boolean, - accountNameCopy: string, -} - -export default class AccountRow extends PureComponent { - state = { - isEditing: false, - accountNameCopy: '', - } - - componentDidUpdate(prevProps: Props, prevState: State) { - const startedEditing = !prevState.isEditing && this.state.isEditing - if (startedEditing) { - this._input && this._input.handleSelectEverything() - } +export default class AccountRow extends PureComponent { + handlePreventSubmit = (e: SyntheticEvent<*>) => { + e.preventDefault() + e.stopPropagation() } - handleEditClick = (e: SyntheticEvent) => { - this.handlePreventSubmit(e) - const { account } = this.props - this.setState({ isEditing: true, accountNameCopy: account.name }) + onToggleAccount = () => { + const { onToggleAccount, account, isChecked } = this.props + if (onToggleAccount) onToggleAccount(account, !isChecked) } - handleSubmitName = (e: SyntheticEvent) => { - this.handlePreventSubmit(e) - const { account, onAccountUpdate, isChecked, onClick } = this.props - const { accountNameCopy } = this.state - const updatedAccount = { ...account, name: accountNameCopy } - this.setState({ isEditing: false, accountNameCopy: '' }) - onAccountUpdate(updatedAccount) - if (!isChecked) { - onClick(updatedAccount) - } + handleChangeName = (name: string) => { + const { onEditName, account } = this.props + if (onEditName) onEditName(account, name) } - handlePreventSubmit = (e: SyntheticEvent) => { - // prevent account row to be submitted + onClickInput = (e: SyntheticEvent<*>) => { e.preventDefault() e.stopPropagation() } - handleChangeName = (accountNameCopy: string) => this.setState({ accountNameCopy }) - - handleReset = () => this.setState({ isEditing: false, accountNameCopy: '' }) + onFocus = (e: *) => { + e.target.select() + } + onBlur = (e: *) => { + const { onEditName, account } = this.props + const { value } = e.target + if (!value && onEditName) { + // don't leave an empty input on blur + onEditName(account, account.name) + } + } _input = null render() { - const { account, isChecked, onClick, isDisabled, t } = this.props - const { isEditing, accountNameCopy } = this.state - + const { account, isChecked, onEditName, accountName, isDisabled, autoFocusInput } = this.props return ( - onClick(account)} isDisabled={isDisabled}> + - {isEditing ? ( + {onEditName ? ( - - - } - ref={input => (this._input = input)} + onClick={this.onClickInput} + onEnter={this.handlePreventSubmit} + onFocus={this.onFocus} + onBlur={this.onBlur} + maxLength={MAX_ACCOUNT_NAME_SIZE} + editInPlace + autoFocus={autoFocusInput} /> ) : ( -
{account.name}
+
{accountName}
)}
- {!isEditing && ( - - - {t('app:addAccounts.editName')} - - )} { fontSize={4} color="grey" /> - + {!isDisabled ? ( + + ) : ( +
+ )} ) } @@ -140,30 +123,3 @@ const AccountRowContainer = styled(Tabbable).attrs({ background-color: ${p => darken(p.theme.colors.lightGrey, 0.03)}; } ` - -const Edit = styled(Box).attrs({ - color: 'wallet', - fontSize: 3, - horizontal: true, - align: 'center', - flow: 1, - py: 1, -})` - display: none; - ${AccountRowContainer}:hover & { - display: flex; - } - &:hover { - text-decoration: underline; - } -` - -const InputRight = styled(Box).attrs({ - bg: 'wallet', - color: 'white', - align: 'center', - justify: 'center', - shrink: 0, -})` - width: 40px; -` diff --git a/src/components/base/AccountsList/index.js b/src/components/base/AccountsList/index.js index bba0c95e..6300a0b4 100644 --- a/src/components/base/AccountsList/index.js +++ b/src/components/base/AccountsList/index.js @@ -1,99 +1,119 @@ // @flow -import React from 'react' -import styled from 'styled-components' +import React, { Component } from 'react' import { translate } from 'react-i18next' import type { Account } from '@ledgerhq/live-common/lib/types' import Box from 'components/base/Box' import FakeLink from 'components/base/FakeLink' -import Spinner from 'components/base/Spinner' import type { T } from 'types/common' +import { SpoilerIcon } from '../Spoiler' import AccountRow from './AccountRow' -const AccountsList = ({ - accounts, - checkedIds, - onToggleAccount, - onUpdateAccount, - onSelectAll, - onUnselectAll, - isLoading, - title, - emptyText, - t, -}: { - accounts: Account[], - checkedIds: string[], - onToggleAccount: Account => void, - onUpdateAccount: Account => void, - onSelectAll: () => void, - onUnselectAll: () => void, - isLoading?: boolean, - title?: string, - emptyText?: string, - t: T, -}) => { - const withToggleAll = !!onSelectAll && !!onUnselectAll && accounts.length > 1 - const isAllSelected = accounts.every(acc => !!checkedIds.find(id => acc.id === id)) - return ( - - {(title || withToggleAll) && ( - - {title && ( - - {title} - - )} - {withToggleAll && ( - - {isAllSelected ? t('app:addAccounts.unselectAll') : t('app:addAccounts.selectAll')} - - )} - - )} - {accounts.length || isLoading ? ( - - {accounts.map(account => ( - id === account.id) !== undefined} - onClick={onToggleAccount} - onAccountUpdate={onUpdateAccount} - t={t} - /> - ))} - {isLoading && ( - - - - )} - - ) : emptyText && !isLoading ? ( - - {emptyText} - - ) : null} - - ) +class AccountsList extends Component< + { + accounts: Account[], + checkedIds?: string[], + editedNames: { [accountId: string]: string }, + setAccountName?: (Account, string) => void, + onToggleAccount?: Account => void, + onSelectAll?: () => void, + onUnselectAll?: () => void, + title?: string, + emptyText?: string, + autoFocusFirstInput?: boolean, + collapsible?: boolean, + t: T, + }, + { + collapsed: boolean, + }, +> { + state = { + collapsed: false, + } + toggleCollapse = () => { + this.setState(({ collapsed }) => ({ collapsed: !collapsed })) + } + render() { + const { + accounts, + checkedIds, + onToggleAccount, + editedNames, + setAccountName, + onSelectAll, + onUnselectAll, + title, + emptyText, + autoFocusFirstInput, + collapsible, + t, + } = this.props + const { collapsed } = this.state + const withToggleAll = !!onSelectAll && !!onUnselectAll && accounts.length > 1 + const isAllSelected = + !checkedIds || accounts.every(acc => !!checkedIds.find(id => acc.id === id)) + return ( + + {(title || withToggleAll) && ( + + {title && ( + + {collapsible ? : null} + {title} + + )} + {withToggleAll && ( + + {isAllSelected + ? t('app:addAccounts.unselectAll', { count: accounts.length }) + : t('app:addAccounts.selectAll', { count: accounts.length })} + + )} + + )} + {collapsed ? null : accounts.length ? ( + + {accounts.map((account, i) => ( + id === account.id) !== undefined} + onToggleAccount={onToggleAccount} + onEditName={setAccountName} + accountName={ + typeof editedNames[account.id] === 'string' + ? editedNames[account.id] + : account.name + } + /> + ))} + + ) : emptyText ? ( + + {emptyText} + + ) : null} + + ) + } } -const LoadingRow = styled(Box).attrs({ - horizontal: true, - borderRadius: 1, - px: 3, - align: 'center', - justify: 'center', -})` - height: 48px; - border: 1px dashed ${p => p.theme.colors.grey}; -` - export default translate()(AccountsList) diff --git a/src/components/base/Input/index.js b/src/components/base/Input/index.js index 4f4388fe..e4affb40 100644 --- a/src/components/base/Input/index.js +++ b/src/components/base/Input/index.js @@ -15,12 +15,18 @@ const Container = styled(Box).attrs({ })` background: ${p => p.theme.colors.white}; border-radius: ${p => p.theme.radii[1]}px; - border: 1px solid - ${p => - p.error ? p.theme.colors.pearl : p.isFocus ? p.theme.colors.wallet : p.theme.colors.fog}; + border-width: 1px; + border-style: solid; + border-color: ${p => + p.error ? p.theme.colors.pearl : p.isFocus ? p.theme.colors.wallet : p.theme.colors.fog}; box-shadow: ${p => (p.isFocus ? `rgba(0, 0, 0, 0.05) 0 2px 2px` : 'none')}; height: ${p => (p.small ? '34' : '40')}px; position: relative; + + &:not(:hover) { + background: ${p => (!p.isFocus && p.editInPlace ? 'transparent' : undefined)}; + border-color: ${p => (!p.isFocus && p.editInPlace ? 'transparent' : undefined)}; + } ` const ErrorDisplay = styled(Box)` @@ -44,6 +50,7 @@ const Base = styled.input.attrs({ outline: none; padding: 0; width: 100%; + background: none; &::placeholder { color: ${p => p.theme.colors.fog}; @@ -82,6 +89,7 @@ type Props = { containerProps?: Object, error?: string | boolean, small?: boolean, + editInPlace?: boolean, } type State = { @@ -152,7 +160,7 @@ class Input extends PureComponent { render() { const { isFocus } = this.state - const { renderLeft, renderRight, containerProps, small, error } = this.props + const { renderLeft, renderRight, containerProps, editInPlace, small, error } = this.props return ( { {...containerProps} small={small} error={error} + editInPlace={editInPlace} > {renderLeft} diff --git a/src/components/base/Spoiler/index.js b/src/components/base/Spoiler/index.js index 1810223f..59302755 100644 --- a/src/components/base/Spoiler/index.js +++ b/src/components/base/Spoiler/index.js @@ -1,6 +1,7 @@ // @flow import React, { PureComponent, Fragment } from 'react' +import uncontrollable from 'uncontrollable' import styled from 'styled-components' import Box from 'components/base/Box' @@ -10,6 +11,8 @@ import IconChevronRight from 'icons/ChevronRight' type Props = { children: any, title: string, + opened: boolean, + onOpen: boolean => void, } type State = { @@ -24,7 +27,6 @@ const Title = styled(Text).attrs({ })` text-transform: ${p => (!p.textTransform ? 'auto' : 'uppercase')}; letter-spacing: 1px; - cursor: pointer; outline: none; ` @@ -33,30 +35,47 @@ const IconContainer = styled(Box)` transition: 150ms linear transform; ` -class Spoiler extends PureComponent { - state = { - isOpened: false, +export class SpoilerIcon extends PureComponent<{ isOpened: boolean }> { + render() { + const { isOpened, ...rest } = this.props + return ( + + + + ) } +} + +/* eslint-disable react/no-multi-comp */ - toggle = () => this.setState({ isOpened: !this.state.isOpened }) +class Spoiler extends PureComponent { + toggle = () => { + const { opened, onOpen } = this.props + onOpen(!opened) + } render() { - const { title, children, ...p } = this.props - const { isOpened } = this.state + const { title, opened, onOpen, children, ...p } = this.props return ( - - - - - - {title} - + + + {title} - {isOpened && children} + {opened && children} ) } } -export default Spoiler +export default uncontrollable(Spoiler, { + opened: 'onOpen', +}) diff --git a/src/components/modals/AccountSettingRenderBody.js b/src/components/modals/AccountSettingRenderBody.js index c4aef19d..373b391c 100644 --- a/src/components/modals/AccountSettingRenderBody.js +++ b/src/components/modals/AccountSettingRenderBody.js @@ -9,7 +9,8 @@ import { translate } from 'react-i18next' import type { Account, Unit, Currency } from '@ledgerhq/live-common/lib/types' import type { T } from 'types/common' -import { MODAL_SETTINGS_ACCOUNT } from 'config/constants' +import { MODAL_SETTINGS_ACCOUNT, MAX_ACCOUNT_NAME_SIZE } from 'config/constants' +import { validateNameEdition } from 'helpers/accountName' import { updateAccount, removeAccount } from 'actions/accounts' import { setDataModal } from 'reducers/modals' @@ -131,23 +132,18 @@ class HelperComp extends PureComponent { const { updateAccount, setDataModal } = this.props const { accountName, accountUnit, endpointConfig, endpointConfigError } = this.state - const sanitizedAccountName = accountName ? accountName.replace(/\s+/g, ' ').trim() : null - - if (account.name || sanitizedAccountName) { - account = { - ...account, - unit: accountUnit || account.unit, - name: sanitizedAccountName || account.name, - } - if (endpointConfig && !endpointConfigError) { - account.endpointConfig = endpointConfig - } - updateAccount(account) - setDataModal(MODAL_SETTINGS_ACCOUNT, { account }) - onClose() - } else { - this.setState({ accountNameError: true }) + const name = validateNameEdition(account, accountName) + account = { + ...account, + unit: accountUnit || account.unit, + name, + } + if (endpointConfig && !endpointConfigError) { + account.endpointConfig = endpointConfig } + updateAccount(account) + setDataModal(MODAL_SETTINGS_ACCOUNT, { account }) + onClose() } handleFocus = (e: any, name: string) => { @@ -211,7 +207,7 @@ class HelperComp extends PureComponent { } onFocus={e => this.handleFocus(e, 'accountName')} diff --git a/src/components/modals/AddAccounts/index.js b/src/components/modals/AddAccounts/index.js index 31aed232..ba0388f8 100644 --- a/src/components/modals/AddAccounts/index.js +++ b/src/components/modals/AddAccounts/index.js @@ -23,6 +23,7 @@ import { closeModal } from 'reducers/modals' import Modal from 'components/base/Modal' import Stepper from 'components/base/Stepper' +import { validateNameEdition } from 'helpers/accountName' import StepChooseCurrency, { StepChooseCurrencyFooter } from './steps/01-step-choose-currency' import StepConnectDevice, { StepConnectDeviceFooter } from './steps/02-step-connect-device' @@ -91,7 +92,9 @@ type State = { currency: ?Currency, scannedAccounts: Account[], checkedAccountsIds: string[], + editedNames: { [_: string]: string }, err: ?Error, + reset: number, } export type StepProps = DefaultStepProps & { @@ -104,12 +107,15 @@ export type StepProps = DefaultStepProps & { checkedAccountsIds: string[], scanStatus: ScanStatus, err: ?Error, - onClickAdd: void => Promise, - onCloseModal: void => void, - resetScanState: void => void, + onClickAdd: () => Promise, + onGoStep1: () => void, + onCloseModal: () => void, + resetScanState: () => void, setCurrency: (?Currency) => void, setAppOpened: boolean => void, setScanStatus: (ScanStatus, ?Error) => string, + setAccountName: (Account, string) => void, + editedNames: { [_: string]: string }, setScannedAccounts: ({ scannedAccounts?: Account[], checkedAccountsIds?: string[] }) => void, } @@ -129,8 +135,10 @@ const INITIAL_STATE = { currency: null, scannedAccounts: [], checkedAccountsIds: [], + editedNames: {}, err: null, scanStatus: 'idle', + reset: 0, } class AddAccounts extends PureComponent { @@ -139,15 +147,16 @@ class AddAccounts extends PureComponent { handleClickAdd = async () => { const { addAccount } = this.props - const { scannedAccounts, checkedAccountsIds } = this.state + const { scannedAccounts, checkedAccountsIds, editedNames } = this.state const accountsIdsMap = checkedAccountsIds.reduce((acc, cur) => { acc[cur] = true return acc }, {}) const accountsToAdd = scannedAccounts.filter(account => accountsIdsMap[account.id] === true) - for (let i = 0; i < accountsToAdd.length; i++) { + for (const account of accountsToAdd) { await idleCallback() - addAccount(accountsToAdd[i]) + const name = validateNameEdition(account, editedNames[account.id]) + addAccount({ ...account, name }) } } @@ -160,6 +169,12 @@ class AddAccounts extends PureComponent { this.setState({ scanStatus, err }) } + handleSetAccountName = (account: Account, name: string) => { + this.setState(({ editedNames }) => ({ + editedNames: { ...editedNames, [account.id]: name }, + })) + } + handleSetScannedAccounts = ({ checkedAccountsIds, scannedAccounts, @@ -184,6 +199,10 @@ class AddAccounts extends PureComponent { handleSetAppOpened = (isAppOpened: boolean) => this.setState({ isAppOpened }) + onGoStep1 = () => { + this.setState(({ reset }) => ({ ...INITIAL_STATE, reset: reset + 1 })) + } + render() { const { t, device, existingAccounts } = this.props const { @@ -194,9 +213,11 @@ class AddAccounts extends PureComponent { checkedAccountsIds, scanStatus, err, + editedNames, + reset, } = this.state - const addtionnalProps = { + const stepperProps = { currency, device, existingAccounts, @@ -212,6 +233,9 @@ class AddAccounts extends PureComponent { setScannedAccounts: this.handleSetScannedAccounts, resetScanState: this.handleResetScanState, setAppOpened: this.handleSetAppOpened, + setAccountName: this.handleSetAccountName, + onGoStep1: this.onGoStep1, + editedNames, } return ( @@ -221,12 +245,13 @@ class AddAccounts extends PureComponent { onHide={() => this.setState({ ...INITIAL_STATE })} render={({ onClose }) => ( diff --git a/src/components/modals/AddAccounts/steps/03-step-import.js b/src/components/modals/AddAccounts/steps/03-step-import.js index 8f960a82..c7372a4d 100644 --- a/src/components/modals/AddAccounts/steps/03-step-import.js +++ b/src/components/modals/AddAccounts/steps/03-step-import.js @@ -1,6 +1,7 @@ // @flow import invariant from 'invariant' +import styled from 'styled-components' import React, { PureComponent, Fragment } from 'react' import type { Account } from '@ledgerhq/live-common/lib/types' import uniq from 'lodash/uniq' @@ -14,9 +15,22 @@ import Button from 'components/base/Button' import AccountsList from 'components/base/AccountsList' import IconExclamationCircleThin from 'icons/ExclamationCircleThin' import TranslatedError from '../../../TranslatedError' +import Spinner from '../../../base/Spinner' import type { StepProps } from '../index' +const LoadingRow = styled(Box).attrs({ + horizontal: true, + borderRadius: 1, + px: 3, + align: 'center', + justify: 'center', + mt: 1, +})` + height: 48px; + border: 1px dashed ${p => p.theme.colors.grey}; +` + class StepImport extends PureComponent { componentDidMount() { this.props.setScanStatus('scanning') @@ -118,18 +132,6 @@ class StepImport extends PureComponent { } } - handleUpdateAccount = (updatedAccount: Account) => { - const { scannedAccounts, setScannedAccounts } = this.props - setScannedAccounts({ - scannedAccounts: scannedAccounts.map(account => { - if (account.id !== updatedAccount.id) { - return account - } - return updatedAccount - }), - }) - } - handleSelectAll = () => { const { scannedAccounts, setScannedAccounts } = this.props setScannedAccounts({ @@ -168,66 +170,98 @@ class StepImport extends PureComponent { scannedAccounts, checkedAccountsIds, existingAccounts, + setAccountName, + editedNames, t, } = this.props if (err) { + // TODO prefer rendering a component return this.renderError() } const currencyName = currency ? currency.name : '' - const importableAccounts = scannedAccounts.filter(acc => { - if (acc.operations.length <= 0) { - return false + const importedAccounts = [] + const importableAccounts = [] + const creatableAccounts = [] + let alreadyEmptyAccount + scannedAccounts.forEach(acc => { + const existingAccount = existingAccounts.find(a => a.id === acc.id) + const empty = acc.operations.length === 0 + if (existingAccount) { + importedAccounts.push(existingAccount) + if (empty) { + alreadyEmptyAccount = existingAccount + } + } else if (empty) { + creatableAccounts.push(acc) + } else { + importableAccounts.push(acc) } - 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 + const importableAccountsListTitle = t('app:addAccounts.accountToImportSubtitle', { + count: importableAccounts.length, }) - const importableAccountsListTitle = t('app:addAccounts.accountToImportSubtitle', { + const importedAccountsListTitle = t('app:addAccounts.accountAlreadyImportedSubtitle', { count: importableAccounts.length, }) const importableAccountsEmpty = t('app:addAccounts.noAccountToImport', { currencyName }) - const alreadyEmptyAccount = scannedAccounts.find(a => a.operations.length === 0) + + const shouldShowNew = scanStatus !== 'scanning' return ( - - - + + {importedAccounts.length === 0 ? null : ( + + )} + {importableAccounts.length === 0 ? null : ( + + )} + {!shouldShowNew ? null : ( + + )} + {scanStatus === 'scanning' ? ( + + + + ) : null} {err && {err.message}} @@ -264,9 +298,7 @@ export const StepImportFooter = ({ const ctaWording = scanStatus === 'scanning' ? t('app:common.sync.syncing') - : willCreateAccount || willAddAccounts - ? t('app:addAccounts.cta.add', { count }) - : t('app:common.close') + : t('app:addAccounts.cta.add', { count }) const willClose = !willCreateAccount && !willAddAccounts const onClick = willClose @@ -291,7 +323,10 @@ export const StepImportFooter = ({ )} + {t('app:addAccounts.success')} + + + + ) } diff --git a/src/config/constants.js b/src/config/constants.js index f873cdcc..4d0c94a7 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -82,6 +82,8 @@ export const EXPERIMENTAL_HTTP_ON_RENDERER = boolFromEnv('EXPERIMENTAL_HTTP_ON_R // Other constants +export const MAX_ACCOUNT_NAME_SIZE = 30 + export const MODAL_ADD_ACCOUNTS = 'MODAL_ADD_ACCOUNTS' export const MODAL_OPERATION_DETAILS = 'MODAL_OPERATION_DETAILS' export const MODAL_RECEIVE = 'MODAL_RECEIVE' diff --git a/src/helpers/accountName.js b/src/helpers/accountName.js index fd230c68..5ca2b8ed 100644 --- a/src/helpers/accountName.js +++ b/src/helpers/accountName.js @@ -1,5 +1,6 @@ // @flow -import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types' +import type { Account, CryptoCurrency } from '@ledgerhq/live-common/lib/types' +import { MAX_ACCOUNT_NAME_SIZE } from 'config/constants' export const getAccountPlaceholderName = ( c: CryptoCurrency, @@ -7,4 +8,12 @@ export const getAccountPlaceholderName = ( isLegacy: boolean = false, ) => `${c.name} ${index}${isLegacy ? ' (legacy)' : ''}` -export const getNewAccountPlaceholderName = (_c: CryptoCurrency, _index: number) => `New Account` +export const getNewAccountPlaceholderName = getAccountPlaceholderName // same naming +// export const getNewAccountPlaceholderName = (_c: CryptoCurrency, _index: number) => `New Account` + +export const validateNameEdition = (account: Account, name: ?string): string => + ( + (name || account.name || '').replace(/\s+/g, ' ').trim() || + account.name || + getAccountPlaceholderName(account.currency, account.index) + ).slice(0, MAX_ACCOUNT_NAME_SIZE) diff --git a/static/i18n/en/app.yml b/static/i18n/en/app.yml index 4ca5e314..2657034d 100644 --- a/static/i18n/en/app.yml +++ b/static/i18n/en/app.yml @@ -146,10 +146,11 @@ addAccounts: connectDevice: Connect device import: Select accounts finish: Confirmation - accountToImportSubtitle: Select existing accounts - accountToImportSubtitle_plural: 'Select ({{count}}) existing accounts' - selectAll: Select all - unselectAll: Deselect all + accountAlreadyImportedSubtitle: Already imported + accountToImportSubtitle: Import existing accounts + accountToImportSubtitle_plural: 'Select existing accounts' + selectAll: Select all ({{count}}) + unselectAll: Deselect all ({{count}}) editName: Edit name newAccount: New account legacyAccount: '{{accountName}} (legacy)' @@ -157,11 +158,12 @@ addAccounts: success: Account added to your portfolio # success_plural: Accounts successfully added to your portfolio. createNewAccount: - title: Create new account + title: Create a new account noOperationOnLastAccount: 'You have to receive crypto assets on {{accountName}} before you can create a new account.' noAccountToCreate: No {{currencyName}} account was found to create somethingWentWrong: Something went wrong during synchronization, please try again. cta: + addMore: 'Add more' add: 'Add account' add_plural: 'Add accounts' operationDetails: From 9a32695441ed3527976e76988e9d913f25ff8df9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Thu, 28 Jun 2018 14:31:57 +0200 Subject: [PATCH 2/6] wording push --- src/helpers/accountName.js | 2 +- static/i18n/en/app.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/helpers/accountName.js b/src/helpers/accountName.js index 5ca2b8ed..dd319a5e 100644 --- a/src/helpers/accountName.js +++ b/src/helpers/accountName.js @@ -6,7 +6,7 @@ export const getAccountPlaceholderName = ( c: CryptoCurrency, index: number, isLegacy: boolean = false, -) => `${c.name} ${index}${isLegacy ? ' (legacy)' : ''}` +) => `${c.name} ${index + 1}${isLegacy ? ' (legacy)' : ''}` export const getNewAccountPlaceholderName = getAccountPlaceholderName // same naming // export const getNewAccountPlaceholderName = (_c: CryptoCurrency, _index: number) => `New Account` diff --git a/static/i18n/en/app.yml b/static/i18n/en/app.yml index 2657034d..acb5c3f1 100644 --- a/static/i18n/en/app.yml +++ b/static/i18n/en/app.yml @@ -146,9 +146,9 @@ addAccounts: connectDevice: Connect device import: Select accounts finish: Confirmation - accountAlreadyImportedSubtitle: Already imported - accountToImportSubtitle: Import existing accounts - accountToImportSubtitle_plural: 'Select existing accounts' + accountAlreadyImportedSubtitle: Already in portfolio + accountToImportSubtitle: 'Select account' + accountToImportSubtitle_plural: 'Select accounts' selectAll: Select all ({{count}}) unselectAll: Deselect all ({{count}}) editName: Edit name From 6979a2abaa9801c279e1e0866cf0f83ffe945f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Thu, 28 Jun 2018 15:02:35 +0200 Subject: [PATCH 3/6] fix weird glitch on Select/Unselect feature when selecting a New --- src/components/base/AccountsList/index.js | 14 +++++++++++--- .../modals/AddAccounts/steps/03-step-import.js | 13 +++++++++---- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/components/base/AccountsList/index.js b/src/components/base/AccountsList/index.js index 6300a0b4..3dc55fdf 100644 --- a/src/components/base/AccountsList/index.js +++ b/src/components/base/AccountsList/index.js @@ -18,8 +18,8 @@ class AccountsList extends Component< editedNames: { [accountId: string]: string }, setAccountName?: (Account, string) => void, onToggleAccount?: Account => void, - onSelectAll?: () => void, - onUnselectAll?: () => void, + onSelectAll?: (Account[]) => void, + onUnselectAll?: (Account[]) => void, title?: string, emptyText?: string, autoFocusFirstInput?: boolean, @@ -36,6 +36,14 @@ class AccountsList extends Component< toggleCollapse = () => { this.setState(({ collapsed }) => ({ collapsed: !collapsed })) } + onSelectAll = () => { + const { accounts, onSelectAll } = this.props + if (onSelectAll) onSelectAll(accounts) + } + onUnselectAll = () => { + const { accounts, onUnselectAll } = this.props + if (onUnselectAll) onUnselectAll(accounts) + } render() { const { accounts, @@ -76,7 +84,7 @@ class AccountsList extends Component< {withToggleAll && ( diff --git a/src/components/modals/AddAccounts/steps/03-step-import.js b/src/components/modals/AddAccounts/steps/03-step-import.js index c7372a4d..1584b9f4 100644 --- a/src/components/modals/AddAccounts/steps/03-step-import.js +++ b/src/components/modals/AddAccounts/steps/03-step-import.js @@ -132,14 +132,19 @@ class StepImport extends PureComponent { } } - handleSelectAll = () => { - const { scannedAccounts, setScannedAccounts } = this.props + handleSelectAll = (accountsToSelect: Account[]) => { + const { setScannedAccounts, checkedAccountsIds } = this.props setScannedAccounts({ - checkedAccountsIds: scannedAccounts.filter(a => a.operations.length > 0).map(a => a.id), + checkedAccountsIds: uniq(checkedAccountsIds.concat(accountsToSelect.map(a => a.id))), }) } - handleUnselectAll = () => this.props.setScannedAccounts({ checkedAccountsIds: [] }) + handleUnselectAll = (accountsToRemove: Account[]) => { + const { setScannedAccounts, checkedAccountsIds } = this.props + setScannedAccounts({ + checkedAccountsIds: checkedAccountsIds.filter(id => !accountsToRemove.some(a => id === a.id)), + }) + } renderError() { const { err, t } = this.props From 4107ab372b70476bec6791cda5460ee13bea371a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Thu, 28 Jun 2018 15:15:37 +0200 Subject: [PATCH 4/6] bold the step 3 --- .../AddAccounts/steps/03-step-import.js | 25 +++++++++++++++---- static/i18n/en/app.yml | 4 +-- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/components/modals/AddAccounts/steps/03-step-import.js b/src/components/modals/AddAccounts/steps/03-step-import.js index 1584b9f4..7c91b17f 100644 --- a/src/components/modals/AddAccounts/steps/03-step-import.js +++ b/src/components/modals/AddAccounts/steps/03-step-import.js @@ -2,6 +2,7 @@ import invariant from 'invariant' import styled from 'styled-components' +import { Trans } from 'react-i18next' import React, { PureComponent, Fragment } from 'react' import type { Account } from '@ledgerhq/live-common/lib/types' import uniq from 'lodash/uniq' @@ -16,6 +17,7 @@ import AccountsList from 'components/base/AccountsList' import IconExclamationCircleThin from 'icons/ExclamationCircleThin' import TranslatedError from '../../../TranslatedError' import Spinner from '../../../base/Spinner' +import Text from '../../../base/Text' import type { StepProps } from '../index' @@ -249,11 +251,24 @@ class StepImport extends PureComponent { autoFocusFirstInput={importableAccounts.length === 0} title={t('app:addAccounts.createNewAccount.title')} emptyText={ - alreadyEmptyAccount - ? t('app:addAccounts.createNewAccount.noOperationOnLastAccount', { - accountName: alreadyEmptyAccount.name, - }) - : t('app:addAccounts.createNewAccount.noAccountToCreate', { currencyName }) + alreadyEmptyAccount ? ( + + {' '} + + {alreadyEmptyAccount.name} + {' '} + + ) : ( + + {' '} + + {currencyName} + {' '} + + ) } accounts={creatableAccounts} checkedIds={checkedAccountsIds} diff --git a/static/i18n/en/app.yml b/static/i18n/en/app.yml index acb5c3f1..760074f8 100644 --- a/static/i18n/en/app.yml +++ b/static/i18n/en/app.yml @@ -159,8 +159,8 @@ addAccounts: # success_plural: Accounts successfully added to your portfolio. createNewAccount: title: Create a new account - noOperationOnLastAccount: 'You have to receive crypto assets on {{accountName}} before you can create a new account.' - noAccountToCreate: No {{currencyName}} account was found to create + noOperationOnLastAccount: 'You have to receive crypto assets on <1><0>{{accountName}} before you can create a new account.' + noAccountToCreate: No <1><0>{{currencyName}} account was found to create somethingWentWrong: Something went wrong during synchronization, please try again. cta: addMore: 'Add more' From 3532d0069f24183ef46d85ea12e7f4af6fa9a012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Thu, 28 Jun 2018 15:59:35 +0200 Subject: [PATCH 5/6] Pixel push confetti --- src/components/ConfettiParty/Confetti.js | 4 ++++ src/components/ConfettiParty/index.js | 20 ++++++++++++++++---- src/reducers/onboarding.js | 2 +- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/components/ConfettiParty/Confetti.js b/src/components/ConfettiParty/Confetti.js index 0d8e8028..2b080cca 100644 --- a/src/components/ConfettiParty/Confetti.js +++ b/src/components/ConfettiParty/Confetti.js @@ -1,6 +1,9 @@ // @flow import React, { PureComponent } from 'react' import Animated from 'animated/lib/targets/react-dom' +import Easing from 'animated/lib/Easing' + +const easing = Easing.bezier(0.0, 0.3, 1, 1) class Confetti extends PureComponent< { @@ -25,6 +28,7 @@ class Confetti extends PureComponent< Animated.timing(this.state.progress, { toValue: 1, duration, + easing, }).start() } render() { diff --git a/src/components/ConfettiParty/index.js b/src/components/ConfettiParty/index.js index 31b46d8b..b6293fc3 100644 --- a/src/components/ConfettiParty/index.js +++ b/src/components/ConfettiParty/index.js @@ -31,24 +31,32 @@ const nextConfetti = (mode: ?string) => id: id++, shape: shapes[Math.floor(shapes.length * Math.random())], initialRotation: 360 * Math.random(), - initialYPercent: -0.15 * Math.random(), + initialYPercent: -0.04 + -0.25 * Math.random(), initialXPercent: 0.2 + 0.6 * Math.random(), initialScale: 1, rotations: 8 * Math.random() - 4, - delta: [(Math.random() - 0.5) * 600, 300 + 300 * Math.random()], - duration: 6000 + 5000 * Math.random(), + delta: [(Math.random() - 0.5) * 1500, 500 + 500 * Math.random()], + duration: 12000 + 8000 * Math.random(), } class ConfettiParty extends PureComponent<{ emit: boolean }, { confettis: Array }> { state = { // $FlowFixMe - confettis: Array(64) + confettis: Array(100) .fill(null) .map(nextConfetti), } componentDidMount() { this.setEmit(this.props.emit) + this.initialTimeout = setTimeout(() => { + clearInterval(this.initialInterval) + }, 10000) + this.initialInterval = setInterval(() => { + this.setState(({ confettis }) => ({ + confettis: confettis.slice(confettis.length > 200 ? 1 : 0).concat(nextConfetti()), + })) + }, 100) } componentDidUpdate(prevProps: *) { @@ -59,6 +67,8 @@ class ConfettiParty extends PureComponent<{ emit: boolean }, { confettis: Array< componentWillUnmount() { this.setEmit(false) + clearInterval(this.initialInterval) + clearTimeout(this.initialTimeout) } setEmit(on: boolean) { @@ -73,6 +83,8 @@ class ConfettiParty extends PureComponent<{ emit: boolean }, { confettis: Array< } } interval: * + initialInterval: * + initialTimeout: * render() { const { confettis } = this.state diff --git a/src/reducers/onboarding.js b/src/reducers/onboarding.js index 1375281e..6484e9a5 100644 --- a/src/reducers/onboarding.js +++ b/src/reducers/onboarding.js @@ -32,7 +32,7 @@ export type OnboardingState = { const state: OnboardingState = { stepIndex: 0, // FIXME is this used at all? dup with stepName? - stepName: SKIP_ONBOARDING ? 'finish' : 'start', + stepName: SKIP_ONBOARDING ? 'analytics' : 'start', genuine: { pinStepPass: false, recoveryStepPass: false, From 2a54980ecb07811496285710cca517c7dc0ff10e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Thu, 28 Jun 2018 16:15:00 +0200 Subject: [PATCH 6/6] Add a cache delay for not blink on genuine - rely on WeakSet --- src/components/GenuineCheck.js | 22 ++++++++++------------ src/config/constants.js | 24 +++++++++++------------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/components/GenuineCheck.js b/src/components/GenuineCheck.js index 50cb9d28..fd333647 100644 --- a/src/components/GenuineCheck.js +++ b/src/components/GenuineCheck.js @@ -5,13 +5,14 @@ import { timeout } from 'rxjs/operators/timeout' import { connect } from 'react-redux' import { compose } from 'redux' import { translate, Trans } from 'react-i18next' +import { delay, createCancelablePolling } from 'helpers/promise' +import logger from 'logger' import type { T, Device } from 'types/common' import type { DeviceInfo } from 'helpers/devices/getDeviceInfo' -import { GENUINE_TIMEOUT, DEVICE_INFOS_TIMEOUT } from 'config/constants' +import { GENUINE_TIMEOUT, DEVICE_INFOS_TIMEOUT, GENUINE_CACHE_DELAY } from 'config/constants' -import { createCancelablePolling } from 'helpers/promise' import { getCurrentDevice } from 'reducers/devices' import { createCustomErrorClass } from 'helpers/errors' @@ -46,12 +47,7 @@ const mapStateToProps = state => ({ const Bold = props => // to speed up genuine check, cache result by device id -const GENUINITY_CACHE = {} -const getDeviceId = (device: Device) => device.path -const setDeviceGenuinity = (device: Device, isGenuine: boolean) => - (GENUINITY_CACHE[getDeviceId(device)] = isGenuine) -const getDeviceGenuinity = (device: Device): ?boolean => - GENUINITY_CACHE[getDeviceId(device)] || null +const genuineDevices = new WeakSet() class GenuineCheck extends PureComponent { connectInteractionHandler = () => @@ -76,7 +72,9 @@ class GenuineCheck extends PureComponent { device: Device, deviceInfo: DeviceInfo, }) => { - if (getDeviceGenuinity(device) === true) { + if (genuineDevices.has(device)) { + logger.log("genuine was already checked. don't check again") + await delay(GENUINE_CACHE_DELAY) return true } const res = await getIsGenuine @@ -85,10 +83,10 @@ class GenuineCheck extends PureComponent { .toPromise() const isGenuine = res === '0000' if (!isGenuine) { - return Promise.reject(new DeviceNotGenuineError()) + throw new DeviceNotGenuineError() } - setDeviceGenuinity(device, true) - return Promise.resolve(true) + genuineDevices.add(device) + return true } handleFail = (err: Error) => { diff --git a/src/config/constants.js b/src/config/constants.js index f873cdcc..ec61c419 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -19,23 +19,21 @@ export const MIN_WIDTH = intFromEnv('LEDGER_MIN_WIDTH', 1024) // time and delays... -export const GET_CALLS_TIMEOUT = intFromEnv('GET_CALLS_TIMEOUT', 30 * 1000) +export const CHECK_APP_INTERVAL_WHEN_INVALID = 600 +export const CHECK_APP_INTERVAL_WHEN_VALID = 1200 +export const CHECK_UPDATE_DELAY = 5000 +export const DEVICE_DISCONNECT_DEBOUNCE = intFromEnv('LEDGER_DEVICE_DISCONNECT_DEBOUNCE', 1000) +export const DEVICE_INFOS_TIMEOUT = intFromEnv('DEVICE_INFOS_TIMEOUT', 5 * 1000) +export const GENUINE_CACHE_DELAY = intFromEnv('GENUINE_CACHE_DELAY', 1000) +export const GENUINE_TIMEOUT = intFromEnv('GENUINE_TIMEOUT', 120 * 1000) export const GET_CALLS_RETRY = intFromEnv('GET_CALLS_RETRY', 2) +export const GET_CALLS_TIMEOUT = intFromEnv('GET_CALLS_TIMEOUT', 30 * 1000) export const LISTEN_DEVICES_POLLING_INTERVAL = intFromEnv('LISTEN_DEVICES_POLLING_INTERVAL', 100) - -export const SYNC_MAX_CONCURRENT = intFromEnv('LEDGER_SYNC_MAX_CONCURRENT', 1) -export const SYNC_BOOT_DELAY = 2 * 1000 +export const OUTDATED_CONSIDERED_DELAY = intFromEnv('OUTDATED_CONSIDERED_DELAY', 5 * 60 * 1000) export const SYNC_ALL_INTERVAL = 120 * 1000 -export const DEVICE_INFOS_TIMEOUT = intFromEnv('DEVICE_INFOS_TIMEOUT', 5 * 1000) -export const GENUINE_TIMEOUT = intFromEnv('GENUINE_TIMEOUT', 120 * 1000) +export const SYNC_BOOT_DELAY = 2 * 1000 +export const SYNC_MAX_CONCURRENT = intFromEnv('LEDGER_SYNC_MAX_CONCURRENT', 1) export const SYNC_TIMEOUT = intFromEnv('SYNC_TIMEOUT', 30 * 1000) -export const OUTDATED_CONSIDERED_DELAY = intFromEnv('OUTDATED_CONSIDERED_DELAY', 5 * 60 * 1000) - -export const CHECK_APP_INTERVAL_WHEN_INVALID = 600 -export const CHECK_APP_INTERVAL_WHEN_VALID = 1200 -export const CHECK_UPDATE_DELAY = 5e3 - -export const DEVICE_DISCONNECT_DEBOUNCE = intFromEnv('LEDGER_DEVICE_DISCONNECT_DEBOUNCE', 1000) // Endpoints...