Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: top 5 assets on the dashboard #3796

Merged
merged 8 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cypress/e2e/pages/dashboard.pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const txBuilder = 'a[href*="tx-builder"]'
const safeSpecificLink = 'a[href*="&appUrl=http"]'
const copyShareBtn = '[data-testid="copy-btn-icon"]'
const exploreAppsBtn = '[data-testid="explore-apps-btn"]'
const viewAllLink = '[data-testid="view-all-link"]'
const viewAllLink = '[data-testid="view-all-link"][href^="/transactions/queue"]'
const noTxIcon = '[data-testid="no-tx-icon"]'
const noTxText = '[data-testid="no-tx-text"]'
const pendingTxWidget = '[data-testid="pending-tx-widget"]'
Expand Down
41 changes: 41 additions & 0 deletions src/components/balances/AssetsTable/SendButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useContext } from 'react'
import type { TokenInfo } from '@safe-global/safe-gateway-typescript-sdk'
import { Button } from '@mui/material'
import ArrowIconNW from '@/public/images/common/arrow-top-right.svg'
import CheckWallet from '@/components/common/CheckWallet'
import useSpendingLimit from '@/hooks/useSpendingLimit'
import Track from '@/components/common/Track'
import { ASSETS_EVENTS } from '@/services/analytics/events/assets'
import { TokenTransferFlow } from '@/components/tx-flow/flows'
import { TxModalContext } from '@/components/tx-flow'

const SendButton = ({ tokenInfo, isOutlined }: { tokenInfo: TokenInfo; isOutlined?: boolean }) => {
const spendingLimit = useSpendingLimit(tokenInfo)
const { setTxFlow } = useContext(TxModalContext)

const onSendClick = () => {
setTxFlow(<TokenTransferFlow tokenAddress={tokenInfo.address} />)
}

return (
<CheckWallet allowSpendingLimit={!!spendingLimit}>
{(isOk) => (
<Track {...ASSETS_EVENTS.SEND}>
<Button
variant={isOutlined ? 'outlined' : 'contained'}
color="primary"
size="small"
startIcon={<ArrowIconNW />}
onClick={onSendClick}
disabled={!isOk}
sx={{ height: '37.5px' }}
>
Send
</Button>
</Track>
)}
</CheckWallet>
)
}

export default SendButton
65 changes: 11 additions & 54 deletions src/components/balances/AssetsTable/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import CheckBalance from '@/features/counterfactual/CheckBalance'
import { useHasFeature } from '@/hooks/useChains'
import ArrowIconNW from '@/public/images/common/arrow-top-right.svg'
import { FEATURES } from '@/utils/chains'
import { type ReactElement, useMemo, useContext } from 'react'
import { Button, Tooltip, Typography, SvgIcon, IconButton, Box, Checkbox, Skeleton } from '@mui/material'
import { type ReactElement } from 'react'
import { Tooltip, Typography, SvgIcon, IconButton, Box, Checkbox, Skeleton } from '@mui/material'
import type { TokenInfo } from '@safe-global/safe-gateway-typescript-sdk'
import { TokenType } from '@safe-global/safe-gateway-typescript-sdk'
import css from './styles.module.css'
Expand All @@ -18,15 +17,12 @@ import InfoIcon from '@/public/images/notifications/info.svg'
import { VisibilityOutlined } from '@mui/icons-material'
import TokenMenu from '../TokenMenu'
import useBalances from '@/hooks/useBalances'
import useHiddenTokens from '@/hooks/useHiddenTokens'
import { useHideAssets } from './useHideAssets'
import CheckWallet from '@/components/common/CheckWallet'
import useSpendingLimit from '@/hooks/useSpendingLimit'
import { TxModalContext } from '@/components/tx-flow'
import { TokenTransferFlow } from '@/components/tx-flow/flows'
import { useHideAssets, useVisibleAssets } from './useHideAssets'
import AddFundsCTA from '@/components/common/AddFunds'
import SwapButton from '@/features/swap/components/SwapButton'
import useIsCounterfactualSafe from '@/features/counterfactual/hooks/useIsCounterfactualSafe'
import { SWAP_LABELS } from '@/services/analytics/events/swaps'
import SendButton from './SendButton'

const skeletonCells: EnhancedTableProps['rows'][0]['cells'] = {
asset: {
Expand Down Expand Up @@ -93,69 +89,28 @@ const headCells = [
},
]

const SendButton = ({
tokenInfo,
onClick,
}: {
tokenInfo: TokenInfo
onClick: (tokenAddress: string) => void
}): ReactElement => {
const spendingLimit = useSpendingLimit(tokenInfo)

return (
<CheckWallet allowSpendingLimit={!!spendingLimit}>
{(isOk) => (
<Track {...ASSETS_EVENTS.SEND}>
<Button
variant="contained"
color="primary"
size="small"
startIcon={<ArrowIconNW />}
onClick={() => onClick(tokenInfo.address)}
disabled={!isOk}
sx={{ height: '37.5px' }}
>
Send
</Button>
</Track>
)}
</CheckWallet>
)
}

const AssetsTable = ({
showHiddenAssets,
setShowHiddenAssets,
}: {
showHiddenAssets: boolean
setShowHiddenAssets: (hidden: boolean) => void
}): ReactElement => {
const hiddenAssets = useHiddenTokens()
const { balances, loading } = useBalances()
const { setTxFlow } = useContext(TxModalContext)
const isCounterfactualSafe = useIsCounterfactualSafe()
const isSwapFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS) && !isCounterfactualSafe

const { isAssetSelected, toggleAsset, hidingAsset, hideAsset, cancel, deselectAll, saveChanges } = useHideAssets(() =>
setShowHiddenAssets(false),
)

const visibleAssets = useMemo(
() =>
showHiddenAssets
? balances.items
: balances.items?.filter((item) => !hiddenAssets.includes(item.tokenInfo.address)),
[hiddenAssets, balances.items, showHiddenAssets],
)
const visible = useVisibleAssets()
const visibleAssets = showHiddenAssets ? balances.items : visible

const hasNoAssets = !loading && balances.items.length === 1 && balances.items[0].balance === '0'

const selectedAssetCount = visibleAssets?.filter((item) => isAssetSelected(item.tokenInfo.address)).length || 0

const onSendClick = (tokenAddress: string) => {
setTxFlow(<TokenTransferFlow tokenAddress={tokenAddress} />)
}

const rows = loading
? skeletonRows
: (visibleAssets || []).map((item) => {
Expand Down Expand Up @@ -225,9 +180,11 @@ const AssetsTable = ({
content: (
<Box display="flex" flexDirection="row" gap={1} alignItems="center">
<>
<SendButton tokenInfo={item.tokenInfo} onClick={() => onSendClick(item.tokenInfo.address)} />
<SendButton tokenInfo={item.tokenInfo} />

{isSwapFeatureEnabled && <SwapButton tokenInfo={item.tokenInfo} amount="0" />}
{isSwapFeatureEnabled && (
<SwapButton tokenInfo={item.tokenInfo} amount="0" trackingLabel={SWAP_LABELS.asset} />
)}

{showHiddenAssets ? (
<Checkbox size="small" checked={isSelected} onClick={() => toggleAsset(item.tokenInfo.address)} />
Expand Down
11 changes: 10 additions & 1 deletion src/components/balances/AssetsTable/useHideAssets.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useCallback, useMemo, useState } from 'react'
import useBalances from '@/hooks/useBalances'
import useChainId from '@/hooks/useChainId'
import useHiddenTokens from '@/hooks/useHiddenTokens'
import { useAppDispatch } from '@/store'
import { setHiddenTokensForChain } from '@/store/settingsSlice'
import { useCallback, useState } from 'react'

// This is the default for MUI Collapse
export const COLLAPSE_TIMEOUT_MS = 300
Expand Down Expand Up @@ -91,3 +91,12 @@ export const useHideAssets = (closeDialog: () => void) => {
hidingAsset,
}
}

export const useVisibleAssets = () => {
const hiddenAssets = useHiddenTokens()
const { balances } = useBalances()
return useMemo(
() => balances.items?.filter((item) => !hiddenAssets.includes(item.tokenInfo.address)),
[hiddenAssets, balances.items],
)
}
121 changes: 121 additions & 0 deletions src/components/dashboard/Assets/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { useMemo } from 'react'
import { Box, Skeleton, Typography, Paper } from '@mui/material'
import type { SafeBalanceResponse } from '@safe-global/safe-gateway-typescript-sdk'
import useBalances from '@/hooks/useBalances'
import FiatValue from '@/components/common/FiatValue'
import TokenAmount from '@/components/common/TokenAmount'
import SwapButton from '@/features/swap/components/SwapButton'
import { AppRoutes } from '@/config/routes'
import { WidgetContainer, WidgetBody, ViewAllLink } from '../styled'
import css from '../PendingTxs/styles.module.css'
import { useRouter } from 'next/router'
import { useHasFeature } from '@/hooks/useChains'
import { FEATURES } from '@/utils/chains'
import { SWAP_LABELS } from '@/services/analytics/events/swaps'
import { useVisibleAssets } from '@/components/balances/AssetsTable/useHideAssets'
import BuyCryptoButton from '@/components/common/BuyCryptoButton'
import SendButton from '@/components/balances/AssetsTable/SendButton'

const MAX_ASSETS = 5

const AssetsDummy = () => (
<Box className={css.container}>
<Skeleton variant="circular" width={26} height={26} />
{Array.from({ length: 2 }).map((_, index) => (
<Skeleton variant="text" sx={{ flex: 1 }} key={index} />
))}
<Skeleton variant="text" width={88} />
</Box>
)

const NoAssets = () => (
<Paper elevation={0} sx={{ p: 5 }}>
<Typography variant="h3" fontWeight="bold" mb={1}>
Add funds to get started
</Typography>

<Typography>
Add funds directly from your bank account or copy your address to send tokens from a different account.
</Typography>

<Box display="flex" mt={2}>
<BuyCryptoButton />
</Box>
</Paper>
)

const AssetRow = ({ item, showSwap }: { item: SafeBalanceResponse['items'][number]; showSwap: boolean }) => (
<Box className={css.container} key={item.tokenInfo.address}>
<Box flex={1}>
<TokenAmount
value={item.balance}
decimals={item.tokenInfo.decimals}
tokenSymbol={item.tokenInfo.symbol}
logoUri={item.tokenInfo.logoUri}
/>
</Box>

<Box flex={1} display={['none', 'block']}>
<FiatValue value={item.fiatBalance} />
</Box>

<Box my={-0.7}>
{showSwap ? (
<SwapButton tokenInfo={item.tokenInfo} amount="0" trackingLabel={SWAP_LABELS.dashboard_assets} />
) : (
<SendButton tokenInfo={item.tokenInfo} isOutlined />
)}
</Box>
</Box>
)

const AssetList = ({ items }: { items: SafeBalanceResponse['items'] }) => {
const isSwapFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS)

return (
<Box display="flex" flexDirection="column" gap={1}>
{items.map((item) => (
<AssetRow item={item} key={item.tokenInfo.address} showSwap={isSwapFeatureEnabled} />
))}
</Box>
)
}

const isNonZeroBalance = (item: SafeBalanceResponse['items'][number]) => item.balance !== '0'

const AssetsWidget = () => {
const router = useRouter()
const { safe } = router.query
const { loading } = useBalances()
const visibleAssets = useVisibleAssets()

const items = useMemo(() => {
return visibleAssets.filter(isNonZeroBalance).slice(0, MAX_ASSETS)
}, [visibleAssets])

const viewAllUrl = useMemo(
() => ({
pathname: AppRoutes.balances.index,
query: { safe },
}),
[safe],
)

return (
<WidgetContainer data-testid="assets-widget">
<div className={css.title}>
<Typography component="h2" variant="subtitle1" fontWeight={700} mb={2}>
Top assets
</Typography>

{items.length > 0 && <ViewAllLink url={viewAllUrl} text={`View all (${visibleAssets.length})`} />}
</div>

<WidgetBody>
{loading ? <AssetsDummy /> : items.length > 0 ? <AssetList items={items} /> : <NoAssets />}
</WidgetBody>
</WidgetContainer>
)
}

export default AssetsWidget
1 change: 1 addition & 0 deletions src/components/dashboard/PendingTxs/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
display: flex;
align-items: center;
gap: var(--space-2);
min-height: 50px;
}

.container:hover {
Expand Down
12 changes: 5 additions & 7 deletions src/components/dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ReactElement } from 'react'
import dynamic from 'next/dynamic'
import { Grid } from '@mui/material'
import PendingTxsList from '@/components/dashboard/PendingTxs/PendingTxsList'
import AssetsWidget from '@/components/dashboard/Assets'
import Overview from '@/components/dashboard/Overview/Overview'
import { FeaturedApps } from '@/components/dashboard/FeaturedApps/FeaturedApps'
import SafeAppsDashboardSection from '@/components/dashboard/SafeAppsDashboardSection/SafeAppsDashboardSection'
Expand All @@ -17,7 +18,6 @@ import ActivityRewardsSection from '@/components/dashboard/ActivityRewardsSectio
import { useHasFeature } from '@/hooks/useChains'
import { FEATURES } from '@/utils/chains'
const RecoveryHeader = dynamic(() => import('@/features/recovery/components/RecoveryHeader'))
const RecoveryWidget = dynamic(() => import('@/features/recovery/components/RecoveryWidget'))

const Dashboard = (): ReactElement => {
const router = useRouter()
Expand Down Expand Up @@ -46,14 +46,12 @@ const Dashboard = (): ReactElement => {
<ActivityRewardsSection />

<Grid item xs={12} lg={6}>
<PendingTxsList />
<AssetsWidget />
</Grid>

{showRecoveryWidget ? (
<Grid item xs={12} lg={6}>
<RecoveryWidget />
</Grid>
) : null}
<Grid item xs={12} lg={6}>
<PendingTxsList />
</Grid>

{showSafeApps && (
<Grid item xs={12} lg={showRecoveryWidget ? 12 : 6}>
Expand Down
Loading
Loading