Skip to content

Commit ae04e6c

Browse files
Merge pull request #475 from BootNodeDev/feat/466
feat(token-select): sort by positive balance by default when wallet connected
2 parents 3465e8e + 0897b15 commit ae04e6c

14 files changed

Lines changed: 406 additions & 56 deletions

File tree

‎src/components/pageComponents/home/Examples/demos/TransactionButton/NativeToken.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Dialog } from '@chakra-ui/react'
22
import { type ReactElement, useState } from 'react'
33
import { type Hash, parseEther, type TransactionReceipt } from 'viem'
4+
import { sepolia } from 'viem/chains'
45
import { useSendTransaction } from 'wagmi'
56
import Wrapper from '@/src/components/pageComponents/home/Examples/demos/TransactionButton/Wrapper'
67
import TransactionButton from '@/src/components/sharedComponents/TransactionButton'
@@ -46,7 +47,10 @@ const NativeToken = () => {
4647
text="Demo transaction that sends 0.1 Sepolia ETH from / to your wallet."
4748
title="Native token demo"
4849
>
50+
{/* chainId must be explicit: the parent WalletStatusVerifier already verified Sepolia,
51+
but TransactionButton checks against appChainId without it. */}
4952
<TransactionButton
53+
chainId={sepolia.id}
5054
labelSending="Sending 0.1 ETH..."
5155
onMined={handleOnMined}
5256
transaction={handleSendTransaction}

‎src/components/sharedComponents/SignButton.test.tsx‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { render, screen } from '@testing-library/react'
33
import type { ReactNode } from 'react'
44
import { createElement } from 'react'
55
import { beforeEach, describe, expect, it, vi } from 'vitest'
6+
import type { Web3Status } from '@/src/hooks/useWeb3Status'
67
import SignButton from './SignButton'
78

89
const mockSwitchChain = vi.fn()
@@ -45,6 +46,21 @@ vi.mock('wagmi', () => ({
4546
const { useWalletStatus } = await import('@/src/hooks/useWalletStatus')
4647
const mockedUseWalletStatus = vi.mocked(useWalletStatus)
4748

49+
const mockWeb3Status = {
50+
readOnlyClient: undefined,
51+
appChainId: 1,
52+
address: '0xdeadbeef',
53+
balance: undefined,
54+
connectingWallet: false,
55+
switchingChain: false,
56+
isWalletConnected: true,
57+
walletClient: undefined,
58+
isWalletSynced: true,
59+
walletChainId: 1,
60+
switchChain: vi.fn(),
61+
disconnect: vi.fn(),
62+
} as unknown as Web3Status
63+
4864
const system = createSystem(defaultConfig)
4965

5066
const renderWithChakra = (ui: ReactNode) =>
@@ -63,6 +79,7 @@ describe('SignButton', () => {
6379
targetChain: { id: 1, name: 'Ethereum' } as ReturnType<typeof useWalletStatus>['targetChain'],
6480
targetChainId: 1,
6581
switchChain: mockSwitchChain,
82+
web3Status: mockWeb3Status,
6683
})
6784

6885
renderWithChakra(<SignButton message="Hello" />)
@@ -79,6 +96,7 @@ describe('SignButton', () => {
7996
targetChain: { id: 1, name: 'Ethereum' } as ReturnType<typeof useWalletStatus>['targetChain'],
8097
targetChainId: 1,
8198
switchChain: mockSwitchChain,
99+
web3Status: mockWeb3Status,
82100
})
83101

84102
renderWithChakra(
@@ -102,6 +120,7 @@ describe('SignButton', () => {
102120
>['targetChain'],
103121
targetChainId: 10,
104122
switchChain: mockSwitchChain,
123+
web3Status: mockWeb3Status,
105124
})
106125

107126
renderWithChakra(<SignButton message="Hello" />)
@@ -119,6 +138,7 @@ describe('SignButton', () => {
119138
targetChain: { id: 1, name: 'Ethereum' } as ReturnType<typeof useWalletStatus>['targetChain'],
120139
targetChainId: 1,
121140
switchChain: mockSwitchChain,
141+
web3Status: mockWeb3Status,
122142
})
123143

124144
renderWithChakra(<SignButton message="Hello" />)

‎src/components/sharedComponents/TokenDropdown.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ type Props = ComponentPropsWithoutRef<'span'> & TokenDropdownProps
2828
* @param {string} [props.placeholder] - Placeholder text for the search input.
2929
* @param {number} [props.containerHeight] - Height of the virtualized tokens list.
3030
* @param {number} [props.itemHeight] - Height of each item in the tokens list.
31-
* @param {boolean} [props.showBalance] - Whether to show the token balance in the list.
31+
* @param {boolean} [props.showBalance] - Whether to show the token balance column in each row.
3232
* @param {boolean} [props.showTopTokens] - Whether to show the top tokens section in the list.
33+
* @param {boolean} [props.sortByBalance] - Sort tokens with a positive balance to the top, ordered by USD value descending. Defaults to true when a wallet is connected.
3334
* @param {ComponentPropsWithoutRef<'span'>} props.restProps - Additional props for the span element.
3435
*
3536
* @example

‎src/components/sharedComponents/TokenInput/index.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,9 @@ type Props = FlexProps & TokenInputProps
5858
* @param {number} [props.iconSize=32] - Optional size of the token icon in the list. Default is 32.
5959
* @param {number} [props.itemHeight=64] - Optional height of each item in the list. Default is 64.
6060
* @param {boolean} [props.showAddTokenButton=false] - Optional flag to allow adding a token. Default is false.
61-
* @param {boolean} [props.showBalance=false] - Optional flag to show the token balance in the list. Default is false.
61+
* @param {boolean} [props.showBalance=false] - Optional flag to show the token balance column in each row. Default is false.
6262
* @param {boolean} [props.showTopTokens=false] - Optional flag to show the top tokens in the list. Default is false.
63+
* @param {boolean} [props.sortByBalance] - Sort tokens with a positive balance to the top, ordered by USD value descending. Defaults to true when a wallet is connected.
6364
*/
6465
const TokenInput: FC<Props> = ({
6566
containerHeight,
@@ -73,6 +74,7 @@ const TokenInput: FC<Props> = ({
7374
showBalance,
7475
showTopTokens,
7576
singleToken,
77+
sortByBalance,
7678
thousandSeparator = true,
7779
title,
7880
tokenInput,
@@ -230,6 +232,7 @@ const TokenInput: FC<Props> = ({
230232
showAddTokenButton={showAddTokenButton}
231233
showBalance={showBalance}
232234
showTopTokens={showTopTokens}
235+
sortByBalance={sortByBalance}
233236
>
234237
<CloseButton
235238
aria-label="Close"

‎src/components/sharedComponents/TokenSelect/List/TokenBalance.tsx‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,16 @@ const TokenBalance = withSuspenseAndRetry<TokenBalanceProps>(({ isLoading, token
7373

7474
if (hasExtensions) {
7575
const balance = formatUnits((token.extensions?.balance ?? 0n) as bigint, token.decimals)
76-
const value = (
77-
Number.parseFloat((token.extensions?.priceUSD ?? '0') as string) * Number.parseFloat(balance)
78-
).toFixed(2)
76+
const priceUSD = token.extensions?.priceUSD as string | undefined
77+
const usdLabel =
78+
priceUSD !== undefined
79+
? `$ ${(Number.parseFloat(priceUSD) * Number.parseFloat(balance)).toFixed(2)}`
80+
: NO_PRICE_DATA_LABEL
7981

8082
return (
8183
<Flex {...flexProps}>
8284
<Box {...balanceBoxProps}>{balance}</Box>
83-
<Box {...valueBoxProps}>$ {value}</Box>
85+
<Box {...valueBoxProps}>{usdLabel}</Box>
8486
</Flex>
8587
)
8688
}

‎src/components/sharedComponents/TokenSelect/index.tsx‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface TokenSelectProps {
2525
showAddTokenButton?: boolean
2626
showTopTokens?: boolean
2727
showBalance?: boolean
28+
sortByBalance?: boolean
2829
}
2930

3031
/** @ignore */
@@ -42,8 +43,9 @@ type Props = FlexProps & TokenSelectProps
4243
* @param {number} [props.iconSize=32] - Optional size of the token icon in the list. Default is 32.
4344
* @param {number} [props.itemHeight=64] - Optional height of each item in the list. Default is 64.
4445
* @param {boolean} [props.showAddTokenButton=false] - Optional flag to allow adding a token. Default is false.
45-
* @param {boolean} [props.showBalance=false] - Optional flag to show the token balance in the list. Default is false.
46+
* @param {boolean} [props.showBalance=false] - Optional flag to show the token balance column in each row. Default is false.
4647
* @param {boolean} [props.showTopTokens=false] - Optional flag to show the top tokens in the list. Default is false.
48+
* @param {boolean} [props.sortByBalance] - Sort tokens with a positive balance to the top, ordered by USD value descending. Defaults to true when a wallet is connected.
4749
*/
4850
const TokenSelect = withSuspenseAndRetry<Props>(
4951
({
@@ -59,9 +61,10 @@ const TokenSelect = withSuspenseAndRetry<Props>(
5961
showAddTokenButton = false,
6062
showBalance = false,
6163
showTopTokens = false,
64+
sortByBalance,
6265
...restProps
6366
}) => {
64-
const { appChainId, walletChainId } = useWeb3Status()
67+
const { appChainId, isWalletConnected, walletChainId } = useWeb3Status()
6568

6669
const [chainId, setChainId] = useState<Chain['id']>(() =>
6770
getValidChainId({
@@ -122,9 +125,12 @@ const TokenSelect = withSuspenseAndRetry<Props>(
122125
previousDepsRef.current = [appChainId, currentNetworkId, walletChainId]
123126
}, [appChainId, currentNetworkId, networks, walletChainId])
124127

128+
const resolvedSortByBalance = sortByBalance ?? isWalletConnected
129+
125130
const { isLoadingBalances, tokensByChainId } = useTokens({
126131
chainId,
127-
withBalance: showBalance,
132+
withBalance: showBalance || resolvedSortByBalance,
133+
sortByBalance: resolvedSortByBalance,
128134
})
129135

130136
const { searchResult, searchTerm, setSearchTerm } = useTokenSearch(

‎src/components/sharedComponents/TransactionButton.test.tsx‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ChakraProvider, createSystem, defaultConfig } from '@chakra-ui/react'
22
import { render, screen } from '@testing-library/react'
33
import { createElement, type ReactNode } from 'react'
44
import { beforeEach, describe, expect, it, vi } from 'vitest'
5+
import type { Web3Status } from '@/src/hooks/useWeb3Status'
56
import TransactionButton from './TransactionButton'
67

78
const mockSwitchChain = vi.fn()
@@ -43,6 +44,21 @@ vi.mock('wagmi', () => ({
4344
const { useWalletStatus } = await import('@/src/hooks/useWalletStatus')
4445
const mockedUseWalletStatus = vi.mocked(useWalletStatus)
4546

47+
const mockWeb3Status = {
48+
readOnlyClient: undefined,
49+
appChainId: 1,
50+
address: '0xdeadbeef',
51+
balance: undefined,
52+
connectingWallet: false,
53+
switchingChain: false,
54+
isWalletConnected: true,
55+
walletClient: undefined,
56+
isWalletSynced: true,
57+
walletChainId: 1,
58+
switchChain: vi.fn(),
59+
disconnect: vi.fn(),
60+
} as unknown as Web3Status
61+
4662
const system = createSystem(defaultConfig)
4763

4864
const renderWithChakra = (ui: ReactNode) =>
@@ -61,6 +77,7 @@ describe('TransactionButton', () => {
6177
targetChain: { id: 1, name: 'Ethereum' } as ReturnType<typeof useWalletStatus>['targetChain'],
6278
targetChainId: 1,
6379
switchChain: mockSwitchChain,
80+
web3Status: mockWeb3Status,
6481
})
6582

6683
renderWithChakra(<TransactionButton transaction={mockTransaction}>Send</TransactionButton>)
@@ -77,6 +94,7 @@ describe('TransactionButton', () => {
7794
targetChain: { id: 1, name: 'Ethereum' } as ReturnType<typeof useWalletStatus>['targetChain'],
7895
targetChainId: 1,
7996
switchChain: mockSwitchChain,
97+
web3Status: mockWeb3Status,
8098
})
8199

82100
renderWithChakra(
@@ -102,6 +120,7 @@ describe('TransactionButton', () => {
102120
>['targetChain'],
103121
targetChainId: 10,
104122
switchChain: mockSwitchChain,
123+
web3Status: mockWeb3Status,
105124
})
106125

107126
renderWithChakra(<TransactionButton transaction={mockTransaction}>Send</TransactionButton>)
@@ -121,6 +140,7 @@ describe('TransactionButton', () => {
121140
>['targetChain'],
122141
targetChainId: 10,
123142
switchChain: mockSwitchChain,
143+
web3Status: mockWeb3Status,
124144
})
125145

126146
renderWithChakra(
@@ -144,6 +164,7 @@ describe('TransactionButton', () => {
144164
targetChain: { id: 1, name: 'Ethereum' } as ReturnType<typeof useWalletStatus>['targetChain'],
145165
targetChainId: 1,
146166
switchChain: mockSwitchChain,
167+
web3Status: mockWeb3Status,
147168
})
148169

149170
renderWithChakra(<TransactionButton transaction={mockTransaction}>Send ETH</TransactionButton>)

‎src/components/sharedComponents/WalletStatusVerifier.test.tsx‎

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,26 @@ import { render, screen } from '@testing-library/react'
33
import userEvent from '@testing-library/user-event'
44
import { createElement, type ReactNode } from 'react'
55
import { beforeEach, describe, expect, it, vi } from 'vitest'
6+
import type { Web3Status } from '@/src/hooks/useWeb3Status'
67
import { useWeb3StatusConnected, WalletStatusVerifier } from './WalletStatusVerifier'
78

89
const mockSwitchChain = vi.fn()
910

11+
const mockWeb3Status = {
12+
readOnlyClient: undefined,
13+
appChainId: 1,
14+
address: '0xdeadbeef',
15+
balance: undefined,
16+
connectingWallet: false,
17+
switchingChain: false,
18+
isWalletConnected: true,
19+
walletClient: undefined,
20+
isWalletSynced: true,
21+
walletChainId: 1,
22+
switchChain: vi.fn(),
23+
disconnect: vi.fn(),
24+
} as unknown as Web3Status
25+
1026
vi.mock('@/src/hooks/useWalletStatus', () => ({
1127
useWalletStatus: vi.fn(() => ({
1228
isReady: false,
@@ -15,23 +31,7 @@ vi.mock('@/src/hooks/useWalletStatus', () => ({
1531
targetChain: { id: 1, name: 'Ethereum' },
1632
targetChainId: 1,
1733
switchChain: mockSwitchChain,
18-
})),
19-
}))
20-
21-
vi.mock('@/src/hooks/useWeb3Status', () => ({
22-
useWeb3Status: vi.fn(() => ({
23-
readOnlyClient: {},
24-
appChainId: 1,
25-
address: '0xdeadbeef',
26-
balance: undefined,
27-
connectingWallet: false,
28-
switchingChain: false,
29-
isWalletConnected: true,
30-
walletClient: undefined,
31-
isWalletSynced: true,
32-
walletChainId: 1,
33-
switchChain: vi.fn(),
34-
disconnect: vi.fn(),
34+
web3Status: mockWeb3Status,
3535
})),
3636
}))
3737

@@ -65,6 +65,7 @@ describe('WalletStatusVerifier', () => {
6565
targetChain: { id: 1, name: 'Ethereum' } as ReturnType<typeof useWalletStatus>['targetChain'],
6666
targetChainId: 1,
6767
switchChain: mockSwitchChain,
68+
web3Status: mockWeb3Status,
6869
})
6970

7071
renderWithChakra(
@@ -87,6 +88,7 @@ describe('WalletStatusVerifier', () => {
8788
targetChain: { id: 1, name: 'Ethereum' } as ReturnType<typeof useWalletStatus>['targetChain'],
8889
targetChainId: 1,
8990
switchChain: mockSwitchChain,
91+
web3Status: mockWeb3Status,
9092
})
9193

9294
renderWithChakra(
@@ -111,6 +113,7 @@ describe('WalletStatusVerifier', () => {
111113
>['targetChain'],
112114
targetChainId: 10,
113115
switchChain: mockSwitchChain,
116+
web3Status: mockWeb3Status,
114117
})
115118

116119
renderWithChakra(
@@ -134,6 +137,7 @@ describe('WalletStatusVerifier', () => {
134137
targetChain: { id: 1, name: 'Ethereum' } as ReturnType<typeof useWalletStatus>['targetChain'],
135138
targetChainId: 1,
136139
switchChain: mockSwitchChain,
140+
web3Status: mockWeb3Status,
137141
})
138142

139143
renderWithChakra(
@@ -159,6 +163,7 @@ describe('WalletStatusVerifier', () => {
159163
>['targetChain'],
160164
targetChainId: 10,
161165
switchChain: mockSwitchChain,
166+
web3Status: mockWeb3Status,
162167
})
163168

164169
renderWithChakra(
@@ -179,6 +184,7 @@ describe('WalletStatusVerifier', () => {
179184
targetChain: { id: 1, name: 'Ethereum' } as ReturnType<typeof useWalletStatus>['targetChain'],
180185
targetChainId: 1,
181186
switchChain: mockSwitchChain,
187+
web3Status: mockWeb3Status,
182188
})
183189

184190
const ChildComponent = () => {

‎src/components/sharedComponents/WalletStatusVerifier.tsx‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createContext, type FC, type ReactElement, type ReactNode, useContext } from 'react'
22
import SwitchChainButton from '@/src/components/sharedComponents/ui/SwitchChainButton'
33
import { useWalletStatus } from '@/src/hooks/useWalletStatus'
4-
import { useWeb3Status, type Web3Status } from '@/src/hooks/useWeb3Status'
4+
import type { Web3Status } from '@/src/hooks/useWeb3Status'
55
import type { ChainsIds } from '@/src/lib/networks.config'
66
import { ConnectWalletButton } from '@/src/providers/Web3Provider'
77
import type { RequiredNonNull } from '@/src/types/utils'
@@ -51,9 +51,8 @@ const WalletStatusVerifier: FC<WalletStatusVerifierProps> = ({
5151
fallback = <ConnectWalletButton />,
5252
switchChainLabel = 'Switch to',
5353
}: WalletStatusVerifierProps) => {
54-
const { needsConnect, needsChainSwitch, targetChain, targetChainId, switchChain } =
54+
const { needsConnect, needsChainSwitch, targetChain, targetChainId, switchChain, web3Status } =
5555
useWalletStatus({ chainId })
56-
const web3Status = useWeb3Status()
5756

5857
if (needsConnect) {
5958
return fallback

0 commit comments

Comments
 (0)