Skip to content

Commit

Permalink
feat: account management wiring progression (#6798)
Browse files Browse the repository at this point in the history
* feat: initial utxo multi account rows for import accounts

* fix: load native asset balances in import accounts

* styles

* fix: use updated query key for loading state

* chore: actioned apojuice code review feedback

* chore: actioned gome code review feedback

---------

Co-authored-by: reallybeard <[email protected]>
  • Loading branch information
woodenfurniture and reallybeard committed May 7, 2024
1 parent 149ac8c commit 20f57fc
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 143 deletions.
172 changes: 107 additions & 65 deletions src/components/ManageAccountsDrawer/components/ImportAccounts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ import { MiddleEllipsis } from 'components/MiddleEllipsis/MiddleEllipsis'
import { RawText } from 'components/Text'
import { useToggle } from 'hooks/useToggle/useToggle'
import { useWallet } from 'hooks/useWallet/useWallet'
import { fromBaseUnit } from 'lib/math'
import { isUtxoAccountId } from 'lib/utils/utxo'
import { portfolio, portfolioApi } from 'state/slices/portfolioSlice/portfolioSlice'
import { accountIdToLabel } from 'state/slices/portfolioSlice/utils'
import {
selectFeeAssetByChainId,
selectHighestAccountNumberForChainId,
selectIsAccountIdEnabled,
selectPortfolioCryptoPrecisionBalanceByFilter,
selectIsAnyAccountIdEnabled,
} from 'state/slices/selectors'
import { useAppDispatch, useAppSelector } from 'state/store'

Expand All @@ -46,61 +46,88 @@ export type ImportAccountsProps = {
}

type TableRowProps = {
accountId: AccountId
accountIds: AccountId[]
accountNumber: number
asset: Asset
onAccountIdActiveChange: (accountId: AccountId, isActive: boolean) => void
onActiveAccountIdsChange: (accountIds: AccountId[], isActive: boolean) => void
}

type TableRowAccountProps = {
accountId: AccountId
asset: Asset
}

const disabledProps = { opacity: 0.5, cursor: 'not-allowed', userSelect: 'none' }

const TableRowAccount = forwardRef<TableRowAccountProps, 'div'>(({ asset, accountId }, ref) => {
const accountLabel = useMemo(() => accountIdToLabel(accountId), [accountId])
const pubkey = useMemo(() => fromAccountId(accountId).account, [accountId])
const isUtxoAccount = useMemo(() => isUtxoAccountId(accountId), [accountId])

const { data: account, isLoading } = useQuery(accountManagement.getAccount(accountId))

const assetBalanceCryptoPrecision = useMemo(() => {
if (!account) return '0'
return fromBaseUnit(account.balance, asset.precision)
}, [account, asset.precision])

return (
<>
<Td fontWeight='bold'>
<Tooltip label={pubkey} isDisabled={isUtxoAccount}>
<div ref={ref}>
<MiddleEllipsis value={accountLabel} />
</div>
</Tooltip>
</Td>
<Td textAlign='right'>
{isLoading ? (
<Skeleton height='24px' width='100%' />
) : (
<Amount.Crypto value={assetBalanceCryptoPrecision} symbol={asset.symbol} />
)}
</Td>
</>
)
})

const TableRow = forwardRef<TableRowProps, 'div'>(
({ asset, accountId, accountNumber, onAccountIdActiveChange }, ref) => {
const translate = useTranslate()
const accountLabel = useMemo(() => accountIdToLabel(accountId), [accountId])
const balanceFilter = useMemo(() => ({ assetId: asset.assetId, accountId }), [asset, accountId])
const isAccountEnabledFilter = useMemo(() => ({ accountId }), [accountId])
({ asset, accountNumber, accountIds, onActiveAccountIdsChange }, ref) => {
const isAccountEnabledInRedux = useAppSelector(state =>
selectIsAccountIdEnabled(state, isAccountEnabledFilter),
selectIsAnyAccountIdEnabled(state, accountIds),
)

const [isAccountActive, toggleIsAccountActive] = useToggle(isAccountEnabledInRedux)

useEffect(() => {
onAccountIdActiveChange(accountId, isAccountActive)
}, [accountId, isAccountActive, isAccountEnabledInRedux, onAccountIdActiveChange])

// TODO: Redux wont have this for new accounts and will be 0, so we'll need to fetch it
const assetBalancePrecision = useAppSelector(s =>
selectPortfolioCryptoPrecisionBalanceByFilter(s, balanceFilter),
)
const pubkey = useMemo(() => fromAccountId(accountId).account, [accountId])
onActiveAccountIdsChange(accountIds, isAccountActive)
}, [accountIds, isAccountActive, isAccountEnabledInRedux, onActiveAccountIdsChange])

const isUtxoAccount = useMemo(() => isUtxoAccountId(accountId), [accountId])
const firstAccount = useMemo(() => accountIds[0], [accountIds])
const otherAccountIds = useMemo(() => accountIds.slice(1), [accountIds])
const otherAccounts = useMemo(() => {
return otherAccountIds.map(accountId => (
<Tr opacity={isAccountActive ? '1' : '0.5'}>
<Td colSpan={2} bg='background.surface.raised.base' />
<TableRowAccount key={accountId} ref={ref} asset={asset} accountId={accountId} />
</Tr>
))
}, [asset, isAccountActive, otherAccountIds, ref])

return (
<Tr>
<Td>
<RawText>{accountNumber}</RawText>
</Td>
<Td>
<Switch isChecked={isAccountActive} onChange={toggleIsAccountActive} />
</Td>
<Td>
<Tooltip label={pubkey} isDisabled={isUtxoAccount}>
<div ref={ref}>
{isUtxoAccount ? (
<RawText>{`${accountLabel} ${translate('common.account')}`}</RawText>
) : (
<MiddleEllipsis value={accountLabel} />
)}
</div>
</Tooltip>
</Td>
<Td>
<Amount.Crypto value={assetBalancePrecision} symbol={asset?.symbol ?? ''} />
</Td>
</Tr>
<>
<Tr opacity={isAccountActive ? '1' : '0.5'}>
<Td>
<Switch size='lg' isChecked={isAccountActive} onChange={toggleIsAccountActive} />
</Td>
<Td>
<RawText color='text.subtle'>{accountNumber}</RawText>
</Td>

<TableRowAccount ref={ref} asset={asset} accountId={firstAccount} />
</Tr>
{otherAccounts}
</>
)
},
)
Expand Down Expand Up @@ -135,10 +162,20 @@ export const ImportAccounts = ({ chainId, onClose }: ImportAccountsProps) => {
)
const chainNamespaceDisplayName = asset?.networkName ?? ''
const [accounts, setAccounts] = useState<
{ accountId: AccountId; accountMetadata: AccountMetadata; hasActivity: boolean }[]
{ accountId: AccountId; accountMetadata: AccountMetadata; hasActivity: boolean }[][]
>([])
const queryClient = useQueryClient()
const isLoading = useIsFetching({ queryKey: ['accountManagement'] }) > 0
const isLoading =
useIsFetching({
predicate: query => {
return (
query.queryKey[0] === 'accountManagement' &&
['accountIdWithActivityAndMetadata', 'firstAccountIdsWithActivityAndMetadata'].some(
str => str === query.queryKey[1],
)
)
},
}) > 0
const [accountIdActiveStateUpdate, setAccountIdActiveStateUpdate] = useState<
Record<string, boolean>
>({})
Expand All @@ -162,24 +199,26 @@ export const ImportAccounts = ({ chainId, onClose }: ImportAccountsProps) => {
const handleLoadMore = useCallback(async () => {
if (!wallet) return
const accountNumber = accounts.length
const accountResult = await queryClient.fetchQuery(
const accountResults = await queryClient.fetchQuery(
reactQueries.accountManagement.accountIdWithActivityAndMetadata(
accountNumber,
chainId,
wallet,
walletDeviceId,
),
)
if (!accountResult) return
if (!accountResults.length) return
setAccounts(previousAccounts => {
const { accountId, accountMetadata, hasActivity } = accountResult
return [...previousAccounts, { accountId, accountMetadata, hasActivity }]
return [...previousAccounts, accountResults]
})
}, [accounts, chainId, queryClient, wallet, walletDeviceId])

const handleAccountIdActiveChange = useCallback((accountId: AccountId, isActive: boolean) => {
const handleAccountIdsActiveChange = useCallback((accountIds: AccountId[], isActive: boolean) => {
setAccountIdActiveStateUpdate(previousState => {
return { ...previousState, [accountId]: isActive }
const stateUpdate = accountIds.reduce((accumulator, accountId) => {
return { ...accumulator, [accountId]: isActive }
}, {})
return { ...previousState, ...stateUpdate }
})
}, [])

Expand All @@ -191,12 +230,12 @@ export const ImportAccounts = ({ chainId, onClose }: ImportAccountsProps) => {
await dispatch(portfolioApi.endpoints.getAccount.initiate({ accountId, upsertOnFetch: true }))
}

const accountMetadataByAccountId = accounts.reduce(
(accumulator, { accountId, accountMetadata }) => {
return { ...accumulator, [accountId]: accountMetadata }
},
{},
)
const accountMetadataByAccountId = accounts.reduce((accumulator, accounts) => {
const obj = accounts.reduce((innerAccumulator, { accountId, accountMetadata }) => {
return { ...innerAccumulator, [accountId]: accountMetadata }
}, {})
return { ...accumulator, ...obj }
}, {})

dispatch(
portfolio.actions.upsertAccountMetadata({
Expand All @@ -216,16 +255,19 @@ export const ImportAccounts = ({ chainId, onClose }: ImportAccountsProps) => {

const accountRows = useMemo(() => {
if (!asset) return null
return accounts.map(({ accountId }, accountNumber) => (
<TableRow
key={accountId}
accountId={accountId}
accountNumber={accountNumber}
asset={asset}
onAccountIdActiveChange={handleAccountIdActiveChange}
/>
))
}, [accounts, asset, handleAccountIdActiveChange])
return accounts.map((accountsForAccountNumber, accountNumber) => {
const accountIds = accountsForAccountNumber.map(({ accountId }) => accountId)
return (
<TableRow
key={accountNumber}
accountNumber={accountNumber}
accountIds={accountIds}
asset={asset}
onActiveAccountIdsChange={handleAccountIdsActiveChange}
/>
)
})
}, [accounts, asset, handleAccountIdsActiveChange])

if (!asset) {
console.error(`No fee asset found for chainId: ${chainId}`)
Expand Down
71 changes: 21 additions & 50 deletions src/components/ManageAccountsDrawer/components/SelectChain.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
import { SearchIcon } from '@chakra-ui/icons'
import {
Box,
Button,
Input,
InputGroup,
InputLeftElement,
SimpleGrid,
VStack,
} from '@chakra-ui/react'
import { Button, SimpleGrid, Stack, VStack } from '@chakra-ui/react'
import type { ChainId } from '@shapeshiftoss/caip'
import type { FormEvent } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useForm } from 'react-hook-form'
import { useTranslate } from 'react-polyglot'
import { LazyLoadAvatar } from 'components/LazyLoadAvatar'
import { GlobalFilter } from 'components/StakingVaults/GlobalFilter'
import { RawText } from 'components/Text'
import { assertGetChainAdapter, chainIdToFeeAssetId } from 'lib/utils'
import { selectAssetById, selectWalletSupportedChainIds } from 'state/slices/selectors'
Expand All @@ -22,6 +12,8 @@ import { useAppSelector } from 'state/store'
import { filterChainIdsBySearchTerm } from '../helpers'
import { DrawerContentWrapper } from './DrawerContent'

const inputGroupProps = { size: 'lg' }

export type SelectChainProps = {
onSelectChainId: (chainId: ChainId) => void
onClose: () => void
Expand Down Expand Up @@ -57,33 +49,24 @@ const ChainButton = ({
export const SelectChain = ({ onSelectChainId, onClose }: SelectChainProps) => {
const translate = useTranslate()
const [searchTermChainIds, setSearchTermChainIds] = useState<ChainId[]>([])
const [searchQuery, setSearchQuery] = useState('')

const walletSupportedChainIds = useAppSelector(selectWalletSupportedChainIds)

const handleSubmit = useCallback((e: FormEvent<unknown>) => e.preventDefault(), [])

const { register, watch } = useForm<{ search: string }>({
mode: 'onChange',
defaultValues: {
search: '',
},
})
const searchString = watch('search')

const searching = useMemo(() => searchString.length > 0, [searchString])
const isSearching = useMemo(() => searchQuery.length > 0, [searchQuery])

useEffect(() => {
if (!searching) return
if (!isSearching) return

setSearchTermChainIds(filterChainIdsBySearchTerm(searchString, walletSupportedChainIds))
}, [searchString, searching, walletSupportedChainIds])
setSearchTermChainIds(filterChainIdsBySearchTerm(searchQuery, walletSupportedChainIds))
}, [searchQuery, isSearching, walletSupportedChainIds])

const chainButtons = useMemo(() => {
const listChainIds = searching ? searchTermChainIds : walletSupportedChainIds
const listChainIds = isSearching ? searchTermChainIds : walletSupportedChainIds
return listChainIds.map(chainId => {
return <ChainButton key={chainId} chainId={chainId} onClick={onSelectChainId} />
})
}, [onSelectChainId, searchTermChainIds, searching, walletSupportedChainIds])
}, [onSelectChainId, searchTermChainIds, isSearching, walletSupportedChainIds])

const footer = useMemo(() => {
return (
Expand All @@ -97,31 +80,19 @@ export const SelectChain = ({ onSelectChainId, onClose }: SelectChainProps) => {

const body = useMemo(() => {
return (
<>
<Box as='form' mb={3} px={4} visibility='visible' onSubmit={handleSubmit}>
<InputGroup size='lg'>
{/* Override zIndex to prevent element displaying on overlay components */}
<InputLeftElement pointerEvents='none' zIndex={1}>
<SearchIcon color='gray.300' />
</InputLeftElement>
<Input
{...register('search')}
type={'text'}
placeholder={translate('accountManagement.selectChain.searchChains')}
pl={10}
variant={'filled'}
autoComplete={'off'}
autoFocus={false}
transitionProperty={'none'}
/>
</InputGroup>
</Box>
<SimpleGrid columns={3} spacing={6}>
<Stack spacing={4}>
<GlobalFilter
setSearchQuery={setSearchQuery}
searchQuery={searchQuery}
placeholder={translate('accountManagement.selectChain.searchChains')}
inputGroupProps={inputGroupProps}
/>
<SimpleGrid columns={3} spacing={4}>
{chainButtons}
</SimpleGrid>
</>
</Stack>
)
}, [chainButtons, handleSubmit, register, translate])
}, [chainButtons, searchQuery, translate])

return (
<DrawerContentWrapper
Expand Down
Loading

0 comments on commit 20f57fc

Please sign in to comment.