Browse Source

Merge pull request #740 from gre/polish/add-account

Refactor and Polish Add Accounts
master
Meriadec Pillet 7 years ago
committed by GitHub
parent
commit
a11115a28c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 140
      src/components/base/AccountsList/AccountRow.js
  2. 198
      src/components/base/AccountsList/index.js
  3. 17
      src/components/base/Input/index.js
  4. 51
      src/components/base/Spoiler/index.js
  5. 32
      src/components/modals/AccountSettingRenderBody.js
  6. 41
      src/components/modals/AddAccounts/index.js
  7. 165
      src/components/modals/AddAccounts/steps/03-step-import.js
  8. 17
      src/components/modals/AddAccounts/steps/04-step-finish.js
  9. 2
      src/config/constants.js
  10. 15
      src/helpers/accountName.js
  11. 16
      static/i18n/en/app.yml

140
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<Props, State> {
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<Props> {
handlePreventSubmit = (e: SyntheticEvent<*>) => {
e.preventDefault()
e.stopPropagation()
}
handleEditClick = (e: SyntheticEvent<any>) => {
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<any>) => {
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<any>) => {
// 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 (
<AccountRowContainer onClick={() => onClick(account)} isDisabled={isDisabled}>
<AccountRowContainer
isDisabled={isDisabled}
onClick={isDisabled ? null : this.onToggleAccount}
>
<CryptoCurrencyIcon currency={account.currency} size={16} color={account.currency.color} />
<Box shrink grow ff="Open Sans|SemiBold" color="dark" fontSize={4}>
{isEditing ? (
{onEditName ? (
<Input
containerProps={{ style: { width: 260 } }}
value={accountNameCopy}
containerProps={{ style: { width: 200 } }}
value={accountName}
onChange={this.handleChangeName}
onClick={this.handlePreventSubmit}
onEnter={this.handleSubmitName}
onEsc={this.handleReset}
renderRight={
<InputRight onClick={this.handleSubmitName}>
<IconCheck size={16} />
</InputRight>
}
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}
/>
) : (
<div style={{ textOverflow: 'ellipsis', overflow: 'hidden' }}>{account.name}</div>
<div style={{ textOverflow: 'ellipsis', overflow: 'hidden' }}>{accountName}</div>
)}
</Box>
{!isEditing && (
<Edit onClick={this.handleEditClick}>
<IconEdit size={13} />
<span>{t('app:addAccounts.editName')}</span>
</Edit>
)}
<FormattedVal
val={account.balance}
unit={account.unit}
@ -112,7 +91,11 @@ export default class AccountRow extends PureComponent<Props, State> {
fontSize={4}
color="grey"
/>
<Radio disabled isChecked={isChecked || !!isDisabled} />
{!isDisabled ? (
<Radio disabled isChecked={isChecked || !!isDisabled} />
) : (
<div style={{ width: 20 }} />
)}
</AccountRowContainer>
)
}
@ -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;
`

198
src/components/base/AccountsList/index.js

@ -1,99 +1,127 @@
// @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 (
<Box flow={3}>
{(title || withToggleAll) && (
<Box horizontal align="center">
{title && (
<Box ff="Open Sans|Bold" color="dark" fontSize={2} textTransform="uppercase">
{title}
</Box>
)}
{withToggleAll && (
<FakeLink
ml="auto"
onClick={isAllSelected ? onUnselectAll : onSelectAll}
fontSize={3}
style={{ lineHeight: '10px' }}
>
{isAllSelected ? t('app:addAccounts.unselectAll') : t('app:addAccounts.selectAll')}
</FakeLink>
)}
</Box>
)}
{accounts.length || isLoading ? (
<Box flow={2}>
{accounts.map(account => (
<AccountRow
key={account.id}
account={account}
isChecked={checkedIds.find(id => id === account.id) !== undefined}
onClick={onToggleAccount}
onAccountUpdate={onUpdateAccount}
t={t}
/>
))}
{isLoading && (
<LoadingRow>
<Spinner color="grey" size={16} />
</LoadingRow>
)}
</Box>
) : emptyText && !isLoading ? (
<Box ff="Open Sans|Regular" fontSize={3}>
{emptyText}
</Box>
) : null}
</Box>
)
class AccountsList extends Component<
{
accounts: Account[],
checkedIds?: string[],
editedNames: { [accountId: string]: string },
setAccountName?: (Account, string) => void,
onToggleAccount?: Account => void,
onSelectAll?: (Account[]) => void,
onUnselectAll?: (Account[]) => void,
title?: string,
emptyText?: string,
autoFocusFirstInput?: boolean,
collapsible?: boolean,
t: T,
},
{
collapsed: boolean,
},
> {
state = {
collapsed: false,
}
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,
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 (
<Box flow={3} mt={4}>
{(title || withToggleAll) && (
<Box horizontal align="center">
{title && (
<Box
horizontal
ff="Open Sans|Bold"
color="dark"
fontSize={2}
textTransform="uppercase"
cursor={collapsible ? 'pointer' : undefined}
onClick={collapsible ? this.toggleCollapse : undefined}
>
{collapsible ? <SpoilerIcon isOpened={!collapsed} mr={1} /> : null}
{title}
</Box>
)}
{withToggleAll && (
<FakeLink
ml="auto"
onClick={isAllSelected ? this.onUnselectAll : this.onSelectAll}
fontSize={3}
style={{ lineHeight: '10px' }}
>
{isAllSelected
? t('app:addAccounts.unselectAll', { count: accounts.length })
: t('app:addAccounts.selectAll', { count: accounts.length })}
</FakeLink>
)}
</Box>
)}
{collapsed ? null : accounts.length ? (
<Box flow={2}>
{accounts.map((account, i) => (
<AccountRow
key={account.id}
account={account}
autoFocusInput={i === 0 && autoFocusFirstInput}
isDisabled={!onToggleAccount || !checkedIds}
isChecked={!checkedIds || checkedIds.find(id => id === account.id) !== undefined}
onToggleAccount={onToggleAccount}
onEditName={setAccountName}
accountName={
typeof editedNames[account.id] === 'string'
? editedNames[account.id]
: account.name
}
/>
))}
</Box>
) : emptyText ? (
<Box ff="Open Sans|Regular" fontSize={3}>
{emptyText}
</Box>
) : null}
</Box>
)
}
}
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)

17
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<Props, State> {
render() {
const { isFocus } = this.state
const { renderLeft, renderRight, containerProps, small, error } = this.props
const { renderLeft, renderRight, containerProps, editInPlace, small, error } = this.props
return (
<Container
@ -162,6 +170,7 @@ class Input extends PureComponent<Props, State> {
{...containerProps}
small={small}
error={error}
editInPlace={editInPlace}
>
{renderLeft}
<Box px={3} grow shrink>

51
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<Props, State> {
state = {
isOpened: false,
export class SpoilerIcon extends PureComponent<{ isOpened: boolean }> {
render() {
const { isOpened, ...rest } = this.props
return (
<IconContainer isOpened={isOpened} {...rest}>
<IconChevronRight size={12} />
</IconContainer>
)
}
}
/* eslint-disable react/no-multi-comp */
toggle = () => this.setState({ isOpened: !this.state.isOpened })
class Spoiler extends PureComponent<Props, State> {
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 (
<Fragment>
<Box horizontal flow={1} color="dark" align="center" {...p}>
<IconContainer isOpened={isOpened}>
<IconChevronRight size={12} />
</IconContainer>
<Title {...p} onClick={this.toggle}>
{title}
</Title>
<Box
onClick={this.toggle}
horizontal
flow={1}
color="dark"
cursor="pointer"
align="center"
{...p}
>
<SpoilerIcon isOpened={opened} />
<Title {...p}>{title}</Title>
</Box>
{isOpened && children}
{opened && children}
</Fragment>
)
}
}
export default Spoiler
export default uncontrollable(Spoiler, {
opened: 'onOpen',
})

32
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<Props, State> {
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<Props, State> {
<Box>
<Input
value={account.name}
maxLength={30}
maxLength={MAX_ACCOUNT_NAME_SIZE}
onChange={this.handleChangeName}
renderLeft={<InputLeft currency={account.currency} />}
onFocus={e => this.handleFocus(e, 'accountName')}

41
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<void>,
onCloseModal: void => void,
resetScanState: void => void,
onClickAdd: () => Promise<void>,
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<Props, State> {
@ -139,15 +147,16 @@ class AddAccounts extends PureComponent<Props, State> {
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<Props, State> {
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<Props, State> {
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<Props, State> {
checkedAccountsIds,
scanStatus,
err,
editedNames,
reset,
} = this.state
const addtionnalProps = {
const stepperProps = {
currency,
device,
existingAccounts,
@ -212,6 +233,9 @@ class AddAccounts extends PureComponent<Props, State> {
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<Props, State> {
onHide={() => this.setState({ ...INITIAL_STATE })}
render={({ onClose }) => (
<Stepper
key={reset} // THIS IS A HACK because stepper is not controllable. FIXME
title={t('app:addAccounts.title')}
initialStepId="chooseCurrency"
onStepChange={this.handleStepChange}
onClose={onClose}
steps={this.STEPS}
{...addtionnalProps}
{...stepperProps}
>
<Track onUnmount event="CloseModalAddAccounts" />
<SyncSkipUnderPriority priority={100} />

165
src/components/modals/AddAccounts/steps/03-step-import.js

@ -1,6 +1,8 @@
// @flow
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'
@ -14,9 +16,23 @@ 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 Text from '../../../base/Text'
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<StepProps> {
componentDidMount() {
this.props.setScanStatus('scanning')
@ -118,27 +134,20 @@ class StepImport extends PureComponent<StepProps> {
}
}
handleUpdateAccount = (updatedAccount: Account) => {
const { scannedAccounts, setScannedAccounts } = this.props
handleSelectAll = (accountsToSelect: Account[]) => {
const { setScannedAccounts, checkedAccountsIds } = this.props
setScannedAccounts({
scannedAccounts: scannedAccounts.map(account => {
if (account.id !== updatedAccount.id) {
return account
}
return updatedAccount
}),
checkedAccountsIds: uniq(checkedAccountsIds.concat(accountsToSelect.map(a => a.id))),
})
}
handleSelectAll = () => {
const { scannedAccounts, setScannedAccounts } = this.props
handleUnselectAll = (accountsToRemove: Account[]) => {
const { setScannedAccounts, checkedAccountsIds } = this.props
setScannedAccounts({
checkedAccountsIds: scannedAccounts.filter(a => a.operations.length > 0).map(a => a.id),
checkedAccountsIds: checkedAccountsIds.filter(id => !accountsToRemove.some(a => id === a.id)),
})
}
handleUnselectAll = () => this.props.setScannedAccounts({ checkedAccountsIds: [] })
renderError() {
const { err, t } = this.props
invariant(err, 'Trying to render inexisting error')
@ -168,66 +177,111 @@ class StepImport extends PureComponent<StepProps> {
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 (
<Fragment>
<TrackPage category="AddAccounts" name="Step3" />
<Box flow={5}>
<AccountsList
title={importableAccountsListTitle}
emptyText={importableAccountsEmpty}
accounts={importableAccounts}
checkedIds={checkedAccountsIds}
onToggleAccount={this.handleToggleAccount}
onUpdateAccount={this.handleUpdateAccount}
onSelectAll={this.handleSelectAll}
onUnselectAll={this.handleUnselectAll}
isLoading={scanStatus === 'scanning'}
/>
<AccountsList
title={t('app:addAccounts.createNewAccount.title')}
emptyText={
alreadyEmptyAccount
? t('app:addAccounts.createNewAccount.noOperationOnLastAccount', {
accountName: alreadyEmptyAccount.name,
})
: t('app:addAccounts.createNewAccount.noAccountToCreate', { currencyName })
}
accounts={creatableAccounts}
checkedIds={checkedAccountsIds}
onToggleAccount={this.handleToggleAccount}
onUpdateAccount={this.handleUpdateAccount}
isLoading={scanStatus === 'scanning'}
/>
<Box mt={-4}>
{importedAccounts.length === 0 ? null : (
<AccountsList
title={importedAccountsListTitle}
accounts={importedAccounts}
editedNames={editedNames}
collapsible
/>
)}
{importableAccounts.length === 0 ? null : (
<AccountsList
title={importableAccountsListTitle}
emptyText={importableAccountsEmpty}
accounts={importableAccounts}
checkedIds={checkedAccountsIds}
onToggleAccount={this.handleToggleAccount}
setAccountName={setAccountName}
editedNames={editedNames}
onSelectAll={this.handleSelectAll}
onUnselectAll={this.handleUnselectAll}
autoFocusFirstInput
/>
)}
{!shouldShowNew ? null : (
<AccountsList
autoFocusFirstInput={importableAccounts.length === 0}
title={t('app:addAccounts.createNewAccount.title')}
emptyText={
alreadyEmptyAccount ? (
<Trans
i18nKey="app:addAccounts.createNewAccount.noOperationOnLastAccount"
parent="div"
>
{' '}
<Text ff="Open Sans|SemiBold" color="dark">
{alreadyEmptyAccount.name}
</Text>{' '}
</Trans>
) : (
<Trans i18nKey="app:addAccounts.createNewAccount.noAccountToCreate" parent="div">
{' '}
<Text ff="Open Sans|SemiBold" color="dark">
{currencyName}
</Text>{' '}
</Trans>
)
}
accounts={creatableAccounts}
checkedIds={checkedAccountsIds}
onToggleAccount={this.handleToggleAccount}
setAccountName={setAccountName}
editedNames={editedNames}
/>
)}
{scanStatus === 'scanning' ? (
<LoadingRow>
<Spinner color="grey" size={16} />
</LoadingRow>
) : null}
</Box>
{err && <Box shrink>{err.message}</Box>}
@ -264,9 +318,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 +343,10 @@ export const StepImportFooter = ({
)}
<Button
primary
disabled={scanStatus !== 'finished' && scanStatus !== 'error'}
disabled={
(scanStatus !== 'finished' && scanStatus !== 'error') ||
!(willCreateAccount || willAddAccounts)
}
onClick={onClick}
>
{ctaWording}

17
src/components/modals/AddAccounts/steps/04-step-finish.js

@ -9,17 +9,22 @@ import IconCheckCircle from 'icons/CheckCircle'
import type { StepProps } from '../index'
function StepFinish({ onCloseModal, t }: StepProps) {
function StepFinish({ onCloseModal, onGoStep1, t }: StepProps) {
return (
<Box align="center" py={6}>
<TrackPage category="AddAccounts" name="Step4" />
<Box color="positiveGreen" mb={4}>
<Box color="positiveGreen">
<IconCheckCircle size={40} />
</Box>
<Box mb={4}>{t('app:addAccounts.success')}</Box>
<Button primary onClick={onCloseModal}>
{t('app:common.close')}
</Button>
<Box p={4}>{t('app:addAccounts.success')}</Box>
<Box horizontal>
<Button mr={2} outline onClick={onGoStep1}>
{t('app:addAccounts.cta.addMore')}
</Button>
<Button primary onClick={onCloseModal}>
{t('app:common.close')}
</Button>
</Box>
</Box>
)
}

2
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'

15
src/helpers/accountName.js

@ -1,10 +1,19 @@
// @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,
index: number,
isLegacy: boolean = false,
) => `${c.name} ${index}${isLegacy ? ' (legacy)' : ''}`
) => `${c.name} ${index + 1}${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)

16
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 in portfolio
accountToImportSubtitle: 'Select account'
accountToImportSubtitle_plural: 'Select 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
noOperationOnLastAccount: 'You have to receive crypto assets on {{accountName}} before you can create a new account.'
noAccountToCreate: No {{currencyName}} account was found to create
title: Create a new account
noOperationOnLastAccount: 'You have to receive crypto assets on <1><0>{{accountName}}</0></1> before you can create a new account.'
noAccountToCreate: No <1><0>{{currencyName}}</0></1> 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:

Loading…
Cancel
Save