Skip to content

Commit 9e1f80a

Browse files
authored
Improved TokenWidget (#854)
* fix misaligned quote breakdown * fix wrapped address truncation * improved slippage config trigger * consistent bitcoin handling * fix multi wallet balance filter * fix stale payment method selection * address unverified token actions loop * clean up * move token resolution to consumer layer * feat: changeset * revert tx modal renderer * clean up multi wallet balance retrieval * support programmatic tab switch * clean up * unverified token decline callback * cleaner conditions
1 parent 78d6bdb commit 9e1f80a

File tree

11 files changed

+516
-365
lines changed

11 files changed

+516
-365
lines changed

.changeset/shaggy-cups-sleep.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@relayprotocol/relay-kit-ui': patch
3+
---
4+
5+
Improve token widget

demo/pages/ui/token/[[...params]].tsx

Lines changed: 206 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { isSolanaWallet } from '@dynamic-labs/solana'
2525
import { adaptSolanaWallet } from '@relayprotocol/relay-svm-wallet-adapter'
2626
import {
2727
adaptViemWallet,
28+
ASSETS_RELAY_API,
2829
type AdaptedWallet,
2930
type ChainVM
3031
} from '@relayprotocol/relay-sdk'
@@ -37,6 +38,7 @@ import { isEclipseWallet } from '@dynamic-labs/eclipse'
3738
import type { LinkedWallet, Token } from '@relayprotocol/relay-kit-ui'
3839
import { isSuiWallet, type SuiWallet } from '@dynamic-labs/sui'
3940
import { adaptSuiWallet } from '@relayprotocol/relay-sui-wallet-adapter'
41+
import { useTokenList } from '@relayprotocol/relay-kit-hooks'
4042
import Head from 'next/head'
4143

4244
const WALLET_VM_TYPES: Exclude<ChainVM, 'hypevm'>[] = [
@@ -57,17 +59,10 @@ const TokenWidgetPage: NextPage = () => {
5759
}
5860
})
5961

60-
// Default tokens
6162
const [fromToken, setFromToken] = useState<Token | undefined>()
6263
const [toToken, setToToken] = useState<Token | undefined>()
6364

64-
const getTokenKey = useCallback((token?: Token) => {
65-
if (!token) {
66-
return undefined
67-
}
68-
69-
return `${token.chainId}:${token.address.toLowerCase()}`
70-
}, [])
65+
const [hasInitialized, setHasInitialized] = useState(false)
7166

7267
const { setWalletFilter } = useWalletFilter()
7368
const { setShowAuthFlow, primaryWallet } = useDynamicContext()
@@ -96,11 +91,99 @@ const TokenWidgetPage: NextPage = () => {
9691
const [urlTokenAddress, setUrlTokenAddress] = useState<string | undefined>()
9792
const [urlTokenChainId, setUrlTokenChainId] = useState<number | undefined>()
9893

99-
// State for manual token input
10094
const [addressInput, setAddressInput] = useState('')
10195
const [chainInput, setChainInput] = useState('')
10296
const [inputError, setInputError] = useState<string | null>(null)
10397
const [tokenNotFound, setTokenNotFound] = useState(false)
98+
const [activeTab, setActiveTab] = useState<'buy' | 'sell'>('buy')
99+
100+
const queryEnabled = !!(urlTokenAddress && urlTokenChainId && relayClient)
101+
102+
const isChainSupported = relayClient?.chains?.some(
103+
(chain) => chain.id === urlTokenChainId
104+
)
105+
106+
const { data: tokenListFromUrl } = useTokenList(
107+
relayClient?.baseApiUrl,
108+
urlTokenAddress && urlTokenChainId && isChainSupported
109+
? {
110+
chainIds: [urlTokenChainId],
111+
address: urlTokenAddress,
112+
limit: 1,
113+
referrer: relayClient?.source
114+
}
115+
: undefined,
116+
{
117+
enabled: queryEnabled && isChainSupported,
118+
retry: 1,
119+
retryDelay: 1000,
120+
staleTime: 0
121+
}
122+
)
123+
124+
const { data: externalTokenListFromUrl } = useTokenList(
125+
relayClient?.baseApiUrl,
126+
urlTokenAddress && urlTokenChainId && isChainSupported
127+
? {
128+
chainIds: [urlTokenChainId],
129+
address: urlTokenAddress,
130+
limit: 1,
131+
useExternalSearch: true,
132+
referrer: relayClient?.source
133+
}
134+
: undefined,
135+
{
136+
enabled: queryEnabled && isChainSupported,
137+
retry: 1,
138+
retryDelay: 1000,
139+
staleTime: 0
140+
}
141+
)
142+
143+
// Resolve URL params to Token object
144+
const urlToken = useMemo(() => {
145+
const apiToken = tokenListFromUrl?.[0] || externalTokenListFromUrl?.[0]
146+
147+
if (!apiToken && urlTokenAddress && urlTokenChainId) {
148+
const isTargetUSDC =
149+
urlTokenAddress.toLowerCase() ===
150+
'0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'.toLowerCase() &&
151+
urlTokenChainId === 8453
152+
153+
if (isTargetUSDC) {
154+
return {
155+
chainId: 8453,
156+
address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
157+
name: 'USD Coin',
158+
symbol: 'USDC',
159+
decimals: 6,
160+
logoURI: `${ASSETS_RELAY_API}/icons/currencies/usdc.png`,
161+
verified: true
162+
} as Token
163+
}
164+
165+
return undefined
166+
}
167+
168+
if (!apiToken) {
169+
return undefined
170+
}
171+
172+
return {
173+
chainId: apiToken.chainId!,
174+
address: apiToken.address!,
175+
name: apiToken.name!,
176+
symbol: apiToken.symbol!,
177+
decimals: apiToken.decimals!,
178+
logoURI: apiToken.metadata?.logoURI ?? '',
179+
verified: apiToken.metadata?.verified ?? false
180+
} as Token
181+
}, [
182+
tokenListFromUrl,
183+
externalTokenListFromUrl,
184+
urlTokenAddress,
185+
urlTokenChainId
186+
])
104187

105188
const linkedWallets = useMemo(() => {
106189
const _wallets = userWallets.reduce((linkedWallets, wallet) => {
@@ -111,7 +194,6 @@ const TokenWidgetPage: NextPage = () => {
111194
return _wallets
112195
}, [userWallets])
113196

114-
// Parse URL params
115197
useEffect(() => {
116198
if (!router.isReady) {
117199
return
@@ -139,42 +221,12 @@ const TokenWidgetPage: NextPage = () => {
139221
if (!Number.isNaN(chainId)) {
140222
setUrlTokenAddress(decodedAddress)
141223
setUrlTokenChainId(chainId)
142-
// Auto-populate form inputs with URL params
143224
setAddressInput(decodedAddress)
144225
setChainInput(chainId.toString())
145226
setTokenNotFound(false)
146227
}
147228
}, [router.isReady, router.query.params])
148229

149-
const updateDemoUrl = useCallback(
150-
(token?: Token) => {
151-
if (!router.isReady) {
152-
return
153-
}
154-
155-
const basePath = '/ui/token'
156-
157-
if (!token) {
158-
router.replace(basePath, undefined, { shallow: true })
159-
return
160-
}
161-
162-
const encodedAddress = encodeURIComponent(token.address)
163-
const chainParam = token.chainId.toString()
164-
const nextPath = `${basePath}/${encodedAddress}/${chainParam}`
165-
166-
router.replace(
167-
{
168-
pathname: '/ui/token/[[...params]]',
169-
query: { params: [token.address, chainParam] }
170-
},
171-
nextPath,
172-
{ shallow: true }
173-
)
174-
},
175-
[router]
176-
)
177-
178230
const updateDemoUrlWithRawParams = useCallback(
179231
(address: string, chainId: number) => {
180232
if (!router.isReady) {
@@ -224,7 +276,6 @@ const TokenWidgetPage: NextPage = () => {
224276
setToToken(undefined)
225277
setTokenNotFound(false)
226278

227-
// Update the URL with the new token params
228279
setUrlTokenAddress(normalizedAddress)
229280
setUrlTokenChainId(parsedChainId)
230281
updateDemoUrlWithRawParams(normalizedAddress, parsedChainId)
@@ -236,19 +287,45 @@ const TokenWidgetPage: NextPage = () => {
236287
switchWallet.current = _switchWallet
237288
}, [_switchWallet])
238289

239-
// Check if token should have loaded but didn't (token not found)
240290
useEffect(() => {
241-
if (urlTokenAddress && urlTokenChainId && !fromToken && relayClient) {
242-
// Wait a bit for the query to complete, then check if token was not found
291+
if (!hasInitialized && router.isReady && relayClient) {
292+
const params = router.query.params
293+
const hasParams = Array.isArray(params) && params.length >= 2
294+
295+
if (!hasParams) {
296+
const targetAddress = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'
297+
const targetChainId = 8453
298+
299+
setUrlTokenAddress(targetAddress)
300+
setUrlTokenChainId(targetChainId)
301+
setAddressInput(targetAddress)
302+
setChainInput(targetChainId.toString())
303+
updateDemoUrlWithRawParams(targetAddress, targetChainId)
304+
}
305+
306+
setHasInitialized(true)
307+
}
308+
}, [
309+
hasInitialized,
310+
router.isReady,
311+
relayClient,
312+
router.query.params,
313+
updateDemoUrlWithRawParams
314+
])
315+
316+
useEffect(() => {
317+
if (urlTokenAddress && urlTokenChainId && !urlToken && relayClient) {
243318
const timer = setTimeout(() => {
244-
if (!fromToken) {
319+
if (!urlToken) {
245320
setTokenNotFound(true)
246321
}
247-
}, 2000) // Wait 2 seconds for token query to complete
322+
}, 2000)
248323

249324
return () => clearTimeout(timer)
325+
} else if (urlToken) {
326+
setTokenNotFound(false)
250327
}
251-
}, [urlTokenAddress, urlTokenChainId, fromToken, relayClient])
328+
}, [urlTokenAddress, urlTokenChainId, urlToken, relayClient])
252329

253330
useEffect(() => {
254331
const adaptWallet = async () => {
@@ -363,17 +440,80 @@ const TokenWidgetPage: NextPage = () => {
363440
gap: 20
364441
}}
365442
>
443+
<div
444+
style={{
445+
display: 'flex',
446+
gap: 12,
447+
padding: '12px 16px',
448+
background:
449+
theme === 'light'
450+
? 'rgba(255, 255, 255, 0.8)'
451+
: 'rgba(28, 23, 43, 0.8)',
452+
borderRadius: 16,
453+
border: `1px solid ${
454+
theme === 'light'
455+
? 'rgba(148, 163, 184, 0.2)'
456+
: 'rgba(148, 163, 184, 0.1)'
457+
}`
458+
}}
459+
>
460+
<button
461+
onClick={() => setActiveTab('buy')}
462+
style={{
463+
padding: '10px 20px',
464+
borderRadius: 12,
465+
border: 'none',
466+
background: activeTab === 'buy' ? '#4f46e5' : '#94a3b8',
467+
color: 'white',
468+
fontWeight: 600,
469+
cursor: 'pointer',
470+
transition: 'all 0.2s',
471+
opacity: activeTab === 'buy' ? 1 : 0.6
472+
}}
473+
>
474+
Open Buy Tab
475+
</button>
476+
<button
477+
onClick={() => setActiveTab('sell')}
478+
style={{
479+
padding: '10px 20px',
480+
borderRadius: 12,
481+
border: 'none',
482+
background: activeTab === 'sell' ? '#4f46e5' : '#94a3b8',
483+
color: 'white',
484+
fontWeight: 600,
485+
cursor: 'pointer',
486+
transition: 'all 0.2s',
487+
opacity: activeTab === 'sell' ? 1 : 0.6
488+
}}
489+
>
490+
Open Sell Tab
491+
</button>
492+
<div
493+
style={{
494+
display: 'flex',
495+
alignItems: 'center',
496+
padding: '0 12px',
497+
color: theme === 'light' ? '#475569' : '#94a3b8',
498+
fontSize: 14,
499+
fontWeight: 500
500+
}}
501+
>
502+
Current: {activeTab === 'buy' ? 'Buy' : 'Sell'}
503+
</div>
504+
</div>
366505
<TokenWidget
367506
key={`swap-widget-${singleChainMode ? 'single' : 'multi'}-chain`}
368507
lockChainId={singleChainMode ? 8453 : undefined}
369508
singleChainMode={singleChainMode}
370509
supportedWalletVMs={supportedWalletVMs}
371-
toToken={toToken}
510+
toToken={urlToken || toToken}
372511
setToToken={setToToken}
373512
fromToken={fromToken}
374513
setFromToken={setFromToken}
375-
defaultFromTokenAddress={urlTokenAddress}
376-
defaultFromTokenChainId={urlTokenChainId}
514+
lockToToken={!!urlToken}
515+
activeTab={activeTab}
516+
setActiveTab={setActiveTab}
377517
wallet={wallet}
378518
multiWalletSupportEnabled={true}
379519
linkedWallets={linkedWallets}
@@ -444,7 +584,6 @@ const TokenWidgetPage: NextPage = () => {
444584
setFromToken(token)
445585
if (token) {
446586
setTokenNotFound(false)
447-
updateDemoUrl(token)
448587
}
449588
}}
450589
onToTokenChange={(token) => {
@@ -459,6 +598,19 @@ const TokenWidgetPage: NextPage = () => {
459598
onSwapSuccess={(data) => {
460599
console.log('onSwapSuccess Triggered', data)
461600
}}
601+
onUnverifiedTokenDecline={(
602+
token: Token,
603+
context: 'from' | 'to'
604+
) => {
605+
console.log('User declined unverified token:', {
606+
symbol: token.symbol,
607+
address: token.address,
608+
chainId: token.chainId,
609+
context: context
610+
})
611+
// Redirect to swap page
612+
router.push('/ui/swap')
613+
}}
462614
slippageTolerance={undefined}
463615
onOpenSlippageConfig={() => {
464616
// setSlippageToleranceConfigOpen(true)
@@ -548,8 +700,9 @@ const TokenWidgetPage: NextPage = () => {
548700
textAlign: 'center'
549701
}}
550702
>
551-
Token from URL was not found on the specified chain. Please
552-
select a token manually to continue.
703+
{!isChainSupported && urlTokenChainId
704+
? `Chain ${urlTokenChainId} is not supported by Relay. Please select a token from a supported chain.`
705+
: 'Token from URL was not found on the specified chain. Please select a token manually to continue.'}
553706
</p>
554707
) : null}
555708
</div>

0 commit comments

Comments
 (0)