diff --git a/apps/frontend/src/components/AssetNumericInput/index.tsx b/apps/frontend/src/components/AssetNumericInput/index.tsx index 8a0dd1a4b..deb863609 100644 --- a/apps/frontend/src/components/AssetNumericInput/index.tsx +++ b/apps/frontend/src/components/AssetNumericInput/index.tsx @@ -20,6 +20,7 @@ interface AssetNumericInputProps { disabled?: boolean; readOnly?: boolean; loading?: boolean; + tokenLoading?: boolean; logoURI?: string; fallbackLogoURI?: string; registerInput: UseFormRegisterReturn; @@ -33,6 +34,7 @@ export const AssetNumericInput: FC = ({ onClick, registerInput, loading, + tokenLoading, ...rest }) => (
= ({ { const { openTokenSelectModal } = useTokenSelectionActions(); + const evmTokensLoaded = useSyncExternalStore(subscribeEvmTokensLoaded, getEvmTokensLoadedSnapshot); + const tokenLoading = isNetworkEVM(selectedNetwork as Networks) && !evmTokensLoaded; + const fromToken = getOnChainTokenDetailsOrDefault(selectedNetwork, onChainToken, getEvmTokenConfig()); const toToken = getAnyFiatTokenDetails(fiatToken); @@ -89,6 +99,7 @@ export const Offramp = () => { onChange={handleInputChange} onClick={() => openTokenSelectModal("from")} registerInput={form.register("inputAmount")} + tokenLoading={tokenLoading} tokenSymbol={fromToken.assetSymbol} />
@@ -97,7 +108,7 @@ export const Offramp = () => {
), - [form, fromToken, openTokenSelectModal, handleInputChange, handleBalanceClick, isDisconnected, fromIconInfo] + [form, fromToken, openTokenSelectModal, handleInputChange, handleBalanceClick, isDisconnected, fromIconInfo, tokenLoading] ); const ReceiveNumericInput = useMemo( diff --git a/apps/frontend/src/components/Ramp/Onramp/index.tsx b/apps/frontend/src/components/Ramp/Onramp/index.tsx index 10af35c26..ca051eb13 100644 --- a/apps/frontend/src/components/Ramp/Onramp/index.tsx +++ b/apps/frontend/src/components/Ramp/Onramp/index.tsx @@ -1,6 +1,13 @@ -import { getAnyFiatTokenDetails, getOnChainTokenDetailsOrDefault } from "@vortexfi/shared"; +import { + getAnyFiatTokenDetails, + getEvmTokensLoadedSnapshot, + getOnChainTokenDetailsOrDefault, + isNetworkEVM, + Networks, + subscribeEvmTokensLoaded +} from "@vortexfi/shared"; import { motion } from "motion/react"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from "react"; import { FormProvider } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { useEventsContext } from "../../../contexts/events"; @@ -51,6 +58,9 @@ export const Onramp = () => { const { openTokenSelectModal } = useTokenSelectionActions(); + const evmTokensLoaded = useSyncExternalStore(subscribeEvmTokensLoaded, getEvmTokensLoadedSnapshot); + const tokenLoading = isNetworkEVM(selectedNetwork as Networks) && !evmTokensLoaded; + const fromToken = getAnyFiatTokenDetails(fiatToken); const toToken = getOnChainTokenDetailsOrDefault(selectedNetwork, onChainToken, getEvmTokenConfig()); @@ -99,10 +109,11 @@ export const Onramp = () => { onClick={() => openTokenSelectModal("to")} readOnly={true} registerInput={form.register("outputAmount")} + tokenLoading={tokenLoading} tokenSymbol={toToken.assetSymbol} /> ), - [toToken.assetSymbol, form, quoteLoading, toAmount, openTokenSelectModal, toIconInfo] + [toToken.assetSymbol, form, quoteLoading, toAmount, openTokenSelectModal, toIconInfo, tokenLoading] ); const handleConfirm = useCallback(() => { diff --git a/apps/frontend/src/components/buttons/AssetButton/index.tsx b/apps/frontend/src/components/buttons/AssetButton/index.tsx index bf11dd83e..d89fdfe9a 100644 --- a/apps/frontend/src/components/buttons/AssetButton/index.tsx +++ b/apps/frontend/src/components/buttons/AssetButton/index.tsx @@ -11,6 +11,7 @@ interface AssetButtonProps { fallbackLogoURI?: string; onClick: () => void; disabled?: boolean; + loading?: boolean; network?: Networks; } @@ -19,6 +20,7 @@ export function AssetButton({ tokenSymbol, onClick, disabled, + loading, logoURI, fallbackLogoURI, network @@ -30,22 +32,32 @@ export function AssetButton({ ); } diff --git a/apps/frontend/src/hooks/useRampUrlParams.ts b/apps/frontend/src/hooks/useRampUrlParams.ts index 1a5efb7e5..c6a336df2 100644 --- a/apps/frontend/src/hooks/useRampUrlParams.ts +++ b/apps/frontend/src/hooks/useRampUrlParams.ts @@ -67,7 +67,11 @@ function findFiatToken(fiatToken?: string): FiatToken | undefined { return foundToken; } -function findOnChainToken(tokenStr?: string, networkType?: Networks | string): OnChainTokenSymbol | undefined { +function findOnChainToken( + tokenStr?: string, + networkType?: Networks | string, + evmTokensLoaded?: boolean +): OnChainTokenSymbol | undefined { if (!tokenStr || !networkType) { return undefined; } @@ -86,6 +90,9 @@ function findOnChainToken(tokenStr?: string, networkType?: Networks | string): O return tokenValue as unknown as OnChainToken; } else { if (isNetworkEVM(networkType as Networks)) { + if (!evmTokensLoaded) { + return undefined; // wait — don't fall back to USDC prematurely while tokens load + } const dynamicConfig = getEvmTokenConfig(); const networkTokens = dynamicConfig[networkType as EvmNetworks]; if (networkTokens && tokenStr in networkTokens) { @@ -209,7 +216,7 @@ export const useRampUrlParams = (): RampUrlParams => { const network = getNetworkFromParam(networkParam); const fiat = findFiatToken(fiatParam); - const cryptoLocked = findOnChainToken(cryptoLockedParam, network || selectedNetwork); + const cryptoLocked = findOnChainToken(cryptoLockedParam, network || selectedNetwork, evmTokensLoaded); return { apiKey: apiKeyParam || undefined, diff --git a/apps/frontend/src/hooks/useSyncFormToUrl.ts b/apps/frontend/src/hooks/useSyncFormToUrl.ts index 95417b831..7276af1f9 100644 --- a/apps/frontend/src/hooks/useSyncFormToUrl.ts +++ b/apps/frontend/src/hooks/useSyncFormToUrl.ts @@ -1,5 +1,6 @@ -import { useNavigate, useSearch } from "@tanstack/react-router"; -import { useEffect } from "react"; +import { useNavigate } from "@tanstack/react-router"; +import { getEvmTokensLoadedSnapshot, isNetworkEVM, Networks, subscribeEvmTokensLoaded } from "@vortexfi/shared"; +import { useEffect, useSyncExternalStore } from "react"; import { useNetwork } from "../contexts/network"; import { useFiatToken, useInputAmount, useOnChainToken } from "../stores/quote/useQuoteFormStore"; import { useRampDirection } from "../stores/rampDirectionStore"; @@ -11,32 +12,30 @@ export const useSyncFormToUrl = () => { const rampDirection = useRampDirection(); const { selectedNetwork } = useNetwork(); const navigate = useNavigate(); - const searchParams = useSearch({ from: "/{-$locale}/widget", strict: true }); + const evmTokensLoaded = useSyncExternalStore(subscribeEvmTokensLoaded, getEvmTokensLoadedSnapshot); useEffect(() => { - const newValues: Record = { - cryptoLocked: onChainToken, - fiat: fiatToken, - inputAmount: inputAmount || undefined, - network: selectedNetwork, - rampType: rampDirection - }; + navigate({ + replace: true, + search: prev => { + const isEvmNetwork = isNetworkEVM(selectedNetwork as Networks); + const cryptoLockedValue = isEvmNetwork && !evmTokensLoaded && prev.cryptoLocked ? prev.cryptoLocked : onChainToken; - const alreadyInSync = Object.entries(newValues).every( - ([key, value]) => - (value === undefined && !(key in searchParams)) || - String(searchParams[key as keyof typeof searchParams] ?? "") === value - ); + const newValues = { + cryptoLocked: cryptoLockedValue, + fiat: fiatToken, + inputAmount: inputAmount || undefined, + network: selectedNetwork, + rampType: rampDirection + }; - if (alreadyInSync) return; + const alreadyInSync = Object.entries(newValues).every( + ([key, value]) => (value === undefined && !(key in prev)) || String(prev[key as keyof typeof prev] ?? "") === value + ); - navigate({ - replace: true, - search: { - ...searchParams, - ...newValues + return alreadyInSync ? prev : { ...prev, ...newValues }; }, to: "." }); - }, [inputAmount, onChainToken, fiatToken, rampDirection, selectedNetwork, navigate, searchParams]); + }, [inputAmount, onChainToken, fiatToken, rampDirection, selectedNetwork, navigate, evmTokensLoaded]); };