diff --git a/public/images/transactions/transactions.svg b/public/images/transactions/transactions.svg new file mode 100644 index 0000000000..0d2d4f1d7a --- /dev/null +++ b/public/images/transactions/transactions.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/common/EthHashInfo/index.tsx b/src/components/common/EthHashInfo/index.tsx index ecf40cb6f3..69d9fe0dad 100644 --- a/src/components/common/EthHashInfo/index.tsx +++ b/src/components/common/EthHashInfo/index.tsx @@ -17,7 +17,8 @@ const EthHashInfo = ({ const chain = useChain(props.chainId || currentChainId) const addressBooks = useAllAddressBooks() const link = chain && props.hasExplorer ? getBlockExplorerLink(chain, props.address) : undefined - const name = showName && chain ? addressBooks?.[chain.chainId]?.[props.address] || props.name : undefined + const addressBookName = chain ? addressBooks?.[chain.chainId]?.[props.address] : undefined + const name = showName ? addressBookName || props.name : undefined return ( @@ -35,16 +33,10 @@ const SkeletonOverview = ( ) const Overview = (): ReactElement => { - const currency = useAppSelector(selectCurrency) const { safe, safeLoading, safeLoaded } = useSafeInfo() const { balances, loading: balancesLoading } = useVisibleBalances() const { setTxFlow } = useContext(TxModalContext) - const fiatTotal = useMemo( - () => (balances.fiatTotal ? formatCurrency(balances.fiatTotal, currency) : ''), - [currency, balances.fiatTotal], - ) - const isInitialState = !safeLoaded && !safeLoading const isLoading = safeLoading || balancesLoading || isInitialState @@ -67,7 +59,7 @@ const Overview = (): ReactElement => { {safe.deployed ? ( - fiatTotal + ) : ( { - if (ens) { - setValue(`${fieldName}.ens`, ens) - } - if (name && !getValues(`${fieldName}.name`)) { setValue(`${fieldName}.name`, name) } - }, [ens, setValue, getValues, name, fieldName]) + }, [setValue, getValues, name, fieldName]) + + useEffect(() => { + if (ens) { + setValue(`${fieldName}.ens`, ens) + } + }, [ens, setValue, fieldName]) const walletIsOwner = owner.address === wallet?.address diff --git a/src/components/new-safe/load/steps/SetAddressStep/index.tsx b/src/components/new-safe/load/steps/SetAddressStep/index.tsx index 510678934e..9b85dfe8f9 100644 --- a/src/components/new-safe/load/steps/SetAddressStep/index.tsx +++ b/src/components/new-safe/load/steps/SetAddressStep/index.tsx @@ -60,7 +60,6 @@ const SetAddressStep = ({ data, onSubmit, onBack }: StepRenderProps void - shortName: string - safeAddress: string -}) => { - const wallet = useWallet() - - const queueLink: UrlObject = useMemo( - () => ({ - pathname: AppRoutes.transactions.queue, - query: { safe: `${shortName}:${safeAddress}` }, - }), - [safeAddress, shortName], - ) - - const shortAddress = shortenAddress(wallet?.address || '') - - return ( - - {wallet && totalToSign && ( - - - - shape.borderRadius, - borderBottomRightRadius: ({ shape }) => shape.borderRadius, - }} - > - - {totalToSign} - - - - - )} - - {totalQueued && ( - - - - shape.borderRadius, - borderBottomRightRadius: ({ shape }) => shape.borderRadius, - }} - > - {/* TODO: replace for Icon library */} - - {totalQueued} - - - - - )} - - ) -} - -export default PendingActionButtons diff --git a/src/components/sidebar/PendingActions/styles.module.css b/src/components/sidebar/PendingActions/styles.module.css deleted file mode 100644 index 6f73f43bc3..0000000000 --- a/src/components/sidebar/PendingActions/styles.module.css +++ /dev/null @@ -1,38 +0,0 @@ -.pendingButtons { - display: flex; - flex-direction: column; - justify-content: center; - gap: 4px; - padding-left: 0px; - margin-right: var(--space-2); -} - -.pendingButton { - width: 52px; - height: 22px; - display: flex; - justify-content: space-between; - padding: 4px 8px; -} - -.pendingButton img { - height: 24px; - width: 20px; -} - -.missingSignatures { - height: 32px; - background-color: var(--color-info-light); -} - -[data-theme='dark'] .missingSignatures { - background-color: var(--color-info-dark); -} - -.queued { - background-color: var(--color-background-main); -} - -[data-theme='dark'] .queued { - background-color: var(--color-secondary-dark); -} diff --git a/src/components/sidebar/SidebarHeader/index.tsx b/src/components/sidebar/SidebarHeader/index.tsx index 1efb962fd6..869733670e 100644 --- a/src/components/sidebar/SidebarHeader/index.tsx +++ b/src/components/sidebar/SidebarHeader/index.tsx @@ -1,17 +1,15 @@ import TokenAmount from '@/components/common/TokenAmount' import CounterfactualStatusButton from '@/features/counterfactual/CounterfactualStatusButton' -import { type ReactElement, useMemo } from 'react' +import { type ReactElement } from 'react' import Typography from '@mui/material/Typography' import IconButton from '@mui/material/IconButton' import Skeleton from '@mui/material/Skeleton' import Tooltip from '@mui/material/Tooltip' -import { formatCurrency } from '@/utils/formatNumber' import useSafeInfo from '@/hooks/useSafeInfo' import SafeIcon from '@/components/common/SafeIcon' import NewTxButton from '@/components/sidebar/NewTxButton' import { useAppSelector } from '@/store' -import { selectCurrency } from '@/store/settingsSlice' import css from './styles.module.css' import QrIconBold from '@/public/images/sidebar/qr-bold.svg' @@ -31,10 +29,10 @@ import EnvHintButton from '@/components/settings/EnvironmentVariables/EnvHintBut import useSafeAddress from '@/hooks/useSafeAddress' import ExplorerButton from '@/components/common/ExplorerButton' import CopyTooltip from '@/components/common/CopyTooltip' +import FiatValue from '@/components/common/FiatValue' import { useAddressResolver } from '@/hooks/useAddressResolver' const SafeHeader = (): ReactElement => { - const currency = useAppSelector(selectCurrency) const { balances } = useVisibleBalances() const safeAddress = useSafeAddress() const { safe } = useSafeInfo() @@ -43,11 +41,6 @@ const SafeHeader = (): ReactElement => { const settings = useAppSelector(selectSettings) const { ens } = useAddressResolver(safeAddress) - const fiatTotal = useMemo( - () => (balances.fiatTotal ? formatCurrency(balances.fiatTotal, currency) : ''), - [currency, balances.fiatTotal], - ) - const addressCopyText = settings.shortName.copy && chain ? `${chain.shortName}:${safeAddress}` : safeAddress const blockExplorerLink = chain ? getBlockExplorerLink(chain, safeAddress) : undefined @@ -76,7 +69,11 @@ const SafeHeader = (): ReactElement => { {safe.deployed ? ( - fiatTotal || + balances.fiatTotal ? ( + + ) : ( + + ) ) : ( void } -const AccountItem = ({ onLinkClick, chainId, address, ...rest }: AccountItemProps) => { +const AccountItem = ({ onLinkClick, safeItem, safeOverview }: AccountItemProps) => { + const { chainId, address } = safeItem const chain = useAppSelector((state) => selectChainById(state, chainId)) const undeployedSafe = useAppSelector((state) => selectUndeployedSafe(state, chainId, address)) const safeAddress = useSafeAddress() @@ -73,7 +75,9 @@ const AccountItem = ({ onLinkClick, chainId, address, ...rest }: AccountItemProp > - + + + {name && ( @@ -105,13 +109,22 @@ const AccountItem = ({ onLinkClick, chainId, address, ...rest }: AccountItemProp )} - + + {safeOverview?.fiatTotal && } + + + ) } diff --git a/src/components/welcome/MyAccounts/PaginatedSafeList.tsx b/src/components/welcome/MyAccounts/PaginatedSafeList.tsx index d27816611f..a022bae193 100644 --- a/src/components/welcome/MyAccounts/PaginatedSafeList.tsx +++ b/src/components/welcome/MyAccounts/PaginatedSafeList.tsx @@ -1,23 +1,82 @@ -import type { ReactElement, ReactNode } from 'react' +import { type ReactElement, type ReactNode, useState, useCallback, useEffect } from 'react' import { Paper, Typography } from '@mui/material' import AccountItem from './AccountItem' -import { type SafeItems } from './useAllSafes' +import { type SafeItem } from './useAllSafes' import css from './styles.module.css' +import useSafeOverviews from './useSafeOverviews' +import { sameAddress } from '@/utils/addresses' +import InfiniteScroll from '@/components/common/InfiniteScroll' type PaginatedSafeListProps = { - safes: SafeItems + safes: SafeItem[] title: ReactNode noSafesMessage?: ReactNode action?: ReactElement onLinkClick?: () => void } +type SafeListPageProps = { + safes: SafeItem[] + onLinkClick: PaginatedSafeListProps['onLinkClick'] +} + +const PAGE_SIZE = 10 + +const SafeListPage = ({ safes, onLinkClick }: SafeListPageProps) => { + const [overviews] = useSafeOverviews(safes) + + const findOverview = (item: SafeItem) => { + return overviews?.find((overview) => sameAddress(overview.address.value, item.address)) + } + + return ( + <> + {safes.map((item) => ( + + ))} + + ) +} + +const AllSafeListPages = ({ safes, onLinkClick }: SafeListPageProps) => { + const totalPages = Math.ceil(safes.length / PAGE_SIZE) + const [pages, setPages] = useState([]) + + const onNextPage = useCallback(() => { + setPages((prev) => { + const pageIndex = prev.length + const nextPage = safes.slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE) + return prev.concat([nextPage]) + }) + }, [safes]) + + useEffect(() => { + setPages([safes.slice(0, PAGE_SIZE)]) + }, [safes]) + + return ( + <> + {pages.map((pageSafes, index) => ( + + ))} + + {totalPages > pages.length && } + + ) +} + const PaginatedSafeList = ({ safes, title, action, noSafesMessage, onLinkClick }: PaginatedSafeListProps) => { return (
{title} + {safes.length > 0 && ( {' '} @@ -25,10 +84,12 @@ const PaginatedSafeList = ({ safes, title, action, noSafesMessage, onLinkClick } )} + {action}
- {safes.length ? ( - safes.map((item) => ) + + {safes.length > 0 ? ( + ) : ( {noSafesMessage} diff --git a/src/components/welcome/MyAccounts/QueueActions.tsx b/src/components/welcome/MyAccounts/QueueActions.tsx new file mode 100644 index 0000000000..f7a7169546 --- /dev/null +++ b/src/components/welcome/MyAccounts/QueueActions.tsx @@ -0,0 +1,71 @@ +import { useMemo, type ReactNode } from 'react' +import type { UrlObject } from 'url' +import NextLink from 'next/link' +import { Box, Chip, Typography, SvgIcon } from '@mui/material' +import CheckIcon from '@mui/icons-material/Check' +import TransactionsIcon from '@/public/images/transactions/transactions.svg' +import Track from '@/components/common/Track' +import { OVERVIEW_EVENTS } from '@/services/analytics/events/overview' +import { AppRoutes } from '@/config/routes' + +const ChipLink = ({ children, color }: { children: ReactNode; color?: string }) => ( + + {children} + + } + /> +) + +const QueueActions = ({ + safeAddress, + chainShortName, + queued, + awaitingConfirmation, +}: { + safeAddress: string + chainShortName: string + queued: number + awaitingConfirmation: number +}) => { + const queueLink = useMemo( + () => ({ + pathname: AppRoutes.transactions.queue, + query: { safe: `${chainShortName}:${safeAddress}` }, + }), + [chainShortName, safeAddress], + ) + + if (!queued && !awaitingConfirmation) { + return null + } + + return ( + + + + + {queued > 0 && ( + + + {queued} pending transaction{queued > 1 ? 's' : ''} + + )} + + {awaitingConfirmation > 0 && ( + + + {awaitingConfirmation} to confirm + + )} + + + + + ) +} + +export default QueueActions diff --git a/src/components/welcome/MyAccounts/styles.module.css b/src/components/welcome/MyAccounts/styles.module.css index 80c5311361..5aa0edf2b1 100644 --- a/src/components/welcome/MyAccounts/styles.module.css +++ b/src/components/welcome/MyAccounts/styles.module.css @@ -35,6 +35,7 @@ padding-top: 0; padding-bottom: 0; padding-left: 0; + flex-wrap: wrap; } .currentListItem { @@ -49,16 +50,29 @@ } .safeLink { - display: flex; - align-items: center; + display: grid; padding: var(--space-2) var(--space-1) var(--space-2) var(--space-2); + grid-template-columns: auto 3fr 2fr auto; + align-items: center; +} + +@media (max-width: 599.95px) { + .safeLink { + grid-template-columns: auto 1fr auto; + grid-template-areas: + 'a b d' + 'a c d'; + } + + .safeLink :nth-child(1) { grid-area: a; } + .safeLink :nth-child(2) { grid-area: b; } + .safeLink :nth-child(3) { grid-area: c; } + .safeLink :nth-child(4) { grid-area: d; } } .safeAddress { white-space: nowrap; - padding-left: var(--space-2); overflow: hidden; - margin-right: 10px; text-overflow: ellipsis; } diff --git a/src/components/welcome/MyAccounts/useAllOwnedSafes.ts b/src/components/welcome/MyAccounts/useAllOwnedSafes.ts index 023bc00b91..a14c362eff 100644 --- a/src/components/welcome/MyAccounts/useAllOwnedSafes.ts +++ b/src/components/welcome/MyAccounts/useAllOwnedSafes.ts @@ -1,11 +1,25 @@ +import type { AllOwnedSafes } from '@safe-global/safe-gateway-typescript-sdk' import { getAllOwnedSafes } from '@safe-global/safe-gateway-typescript-sdk' +import type { AsyncResult } from '@/hooks/useAsync' import useAsync from '@/hooks/useAsync' +import useLocalStorage from '@/services/local-storage/useLocalStorage' +import { useEffect } from 'react' -const useAllOwnedSafes = (address: string) => { - return useAsync(() => { - if (!address) return +const CACHE_KEY = 'ownedSafesCache_' + +const useAllOwnedSafes = (address: string): AsyncResult => { + const [cache, setCache] = useLocalStorage(CACHE_KEY + address) + + const [data, error, isLoading] = useAsync(async () => { + if (!address) return {} return getAllOwnedSafes(address) }, [address]) + + useEffect(() => { + if (data != undefined) setCache(data) + }, [data, setCache]) + + return [cache, error, isLoading] } export default useAllOwnedSafes diff --git a/src/components/welcome/MyAccounts/useAllSafes.ts b/src/components/welcome/MyAccounts/useAllSafes.ts index 14206126ce..c5dc62f6a5 100644 --- a/src/components/welcome/MyAccounts/useAllSafes.ts +++ b/src/components/welcome/MyAccounts/useAllSafes.ts @@ -9,13 +9,13 @@ import useWallet from '@/hooks/wallets/useWallet' import { selectUndeployedSafes } from '@/store/slices' import { sameAddress } from '@/utils/addresses' -export type SafeItems = Array<{ +export type SafeItem = { chainId: string address: string isWatchlist: boolean - threshold?: number - owners?: number -}> +} + +export type SafeItems = SafeItem[] const useAddedSafes = () => { const allAdded = useAppSelector(selectAllAddedSafes) @@ -61,8 +61,6 @@ const useAllSafes = (): SafeItems | undefined => { address, chainId, isWatchlist: !isOwned && !isUndeployed, - threshold: allAdded[chainId]?.[address]?.threshold, - owners: allAdded[chainId]?.[address]?.owners.length, } }) }) diff --git a/src/components/welcome/MyAccounts/useSafeOverviews.ts b/src/components/welcome/MyAccounts/useSafeOverviews.ts new file mode 100644 index 0000000000..bfe33c3eb4 --- /dev/null +++ b/src/components/welcome/MyAccounts/useSafeOverviews.ts @@ -0,0 +1,26 @@ +import { useTokenListSetting } from '@/hooks/loadables/useLoadBalances' +import useAsync from '@/hooks/useAsync' +import useWallet from '@/hooks/wallets/useWallet' +import { useAppSelector } from '@/store' +import { selectCurrency } from '@/store/settingsSlice' +import { getSafeOverviews } from '@safe-global/safe-gateway-typescript-sdk' + +function useSafeOverviews(safes: Array<{ address: string; chainId: string }>) { + const excludeSpam = useTokenListSetting() || false + const currency = useAppSelector(selectCurrency) + const wallet = useWallet() + const walletAddress = wallet?.address + + return useAsync(async () => { + const safesStrings = safes.map((safe) => `${safe.chainId}:${safe.address}` as `${number}:0x${string}`) + + return await getSafeOverviews(safesStrings, { + trusted: true, + exclude_spam: excludeSpam, + currency, + wallet_address: walletAddress, + }) + }, [safes, excludeSpam, currency, walletAddress]) +} + +export default useSafeOverviews diff --git a/src/hooks/loadables/useLoadBalances.ts b/src/hooks/loadables/useLoadBalances.ts index 3d52ffe067..40324c8b11 100644 --- a/src/hooks/loadables/useLoadBalances.ts +++ b/src/hooks/loadables/useLoadBalances.ts @@ -12,7 +12,7 @@ import { POLLING_INTERVAL } from '@/config/constants' import useIntervalCounter from '../useIntervalCounter' import useSafeInfo from '../useSafeInfo' -const useTokenListSetting = (): boolean | undefined => { +export const useTokenListSetting = (): boolean | undefined => { const chain = useCurrentChain() const settings = useAppSelector(selectSettings) diff --git a/src/utils/__tests__/formatNumber.test.ts b/src/utils/__tests__/formatNumber.test.ts index c0d1e5eb05..84f4c000f5 100644 --- a/src/utils/__tests__/formatNumber.test.ts +++ b/src/utils/__tests__/formatNumber.test.ts @@ -181,6 +181,15 @@ describe('formatNumber', () => { expect(formatCurrency(amount3, 'BHD')).toBe('1.778 BHD') }) + it('should drop decimals for values above 1k', () => { + // It should stop + expect(formatCurrency(999.99, 'USD')).toBe('999.99 USD') + expect(formatCurrency(1000.1, 'USD')).toBe('1,000 USD') + expect(formatCurrency(1000.99, 'USD')).toBe('1,001 USD') + expect(formatCurrency(32500.5, 'EUR')).toBe('32,501 EUR') + expect(formatCurrency(314285500.1, 'JPY')).toBe('314.286M JPY') + }) + it('should use M symbol for numbers between 100,000,000 and 999,999,500', () => { const amount1 = 100_000_100 diff --git a/src/utils/formatNumber.ts b/src/utils/formatNumber.ts index 6623327d39..b0cb100983 100644 --- a/src/utils/formatNumber.ts +++ b/src/utils/formatNumber.ts @@ -6,6 +6,7 @@ import memoize from 'lodash/memoize' const LOWER_LIMIT = 0.00001 const COMPACT_LIMIT = 99_999_999.5 const UPPER_LIMIT = 999 * 10 ** 12 +const NO_DECIMALS_LIMIT = 1000 /** * Formatter that restricts the upper and lower limit of numbers that can be formatted @@ -140,13 +141,12 @@ const getCurrencyFormatterMaxFractionDigits = ( ): Intl.NumberFormatOptions['maximumFractionDigits'] => { const float = Number(number) - if (float < 1_000_000) { + if (float < NO_DECIMALS_LIMIT) { const [, decimals] = getMinimumCurrencyDenominator(currency).toString().split('.') return decimals?.length ?? 0 } - // Represents numbers like 767.343M - if (float < UPPER_LIMIT) { + if (float >= COMPACT_LIMIT) { return 3 }