@@ -25,6 +25,7 @@ import { isSolanaWallet } from '@dynamic-labs/solana'
2525import { adaptSolanaWallet } from '@relayprotocol/relay-svm-wallet-adapter'
2626import {
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'
3738import type { LinkedWallet , Token } from '@relayprotocol/relay-kit-ui'
3839import { isSuiWallet , type SuiWallet } from '@dynamic-labs/sui'
3940import { adaptSuiWallet } from '@relayprotocol/relay-sui-wallet-adapter'
41+ import { useTokenList } from '@relayprotocol/relay-kit-hooks'
4042import Head from 'next/head'
4143
4244const 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