Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/frontend/src/components/AssetNumericInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface AssetNumericInputProps {
disabled?: boolean;
readOnly?: boolean;
loading?: boolean;
tokenLoading?: boolean;
logoURI?: string;
fallbackLogoURI?: string;
registerInput: UseFormRegisterReturn<keyof QuoteFormValues>;
Expand All @@ -33,6 +34,7 @@ export const AssetNumericInput: FC<AssetNumericInputProps> = ({
onClick,
registerInput,
loading,
tokenLoading,
...rest
}) => (
<div
Expand All @@ -45,6 +47,7 @@ export const AssetNumericInput: FC<AssetNumericInputProps> = ({
<AssetButton
assetIcon={assetIcon}
fallbackLogoURI={rest.fallbackLogoURI}
loading={tokenLoading}
logoURI={rest.logoURI}
network={rest.network}
onClick={onClick}
Expand Down
17 changes: 14 additions & 3 deletions apps/frontend/src/components/Ramp/Offramp/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -56,6 +63,9 @@ export const Offramp = () => {

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);

Expand Down Expand Up @@ -89,6 +99,7 @@ export const Offramp = () => {
onChange={handleInputChange}
onClick={() => openTokenSelectModal("from")}
registerInput={form.register("inputAmount")}
tokenLoading={tokenLoading}
tokenSymbol={fromToken.assetSymbol}
/>
<div className="flex grow-1 flex-row justify-between">
Expand All @@ -97,7 +108,7 @@ export const Offramp = () => {
</div>
</>
),
[form, fromToken, openTokenSelectModal, handleInputChange, handleBalanceClick, isDisconnected, fromIconInfo]
[form, fromToken, openTokenSelectModal, handleInputChange, handleBalanceClick, isDisconnected, fromIconInfo, tokenLoading]
);

const ReceiveNumericInput = useMemo(
Expand Down
17 changes: 14 additions & 3 deletions apps/frontend/src/components/Ramp/Onramp/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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());

Expand Down Expand Up @@ -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(() => {
Expand Down
36 changes: 24 additions & 12 deletions apps/frontend/src/components/buttons/AssetButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface AssetButtonProps {
fallbackLogoURI?: string;
onClick: () => void;
disabled?: boolean;
loading?: boolean;
network?: Networks;
}

Expand All @@ -19,6 +20,7 @@ export function AssetButton({
tokenSymbol,
onClick,
disabled,
loading,
logoURI,
fallbackLogoURI,
network
Expand All @@ -30,22 +32,32 @@ export function AssetButton({
<button
className={cn(
" mt-0.5 flex h-8 cursor-pointer items-center rounded-full border border-blue-700 px-2 py-1 pr-3 text-base",
disabled ? "cursor-not-allowed" : "hover:bg-blue-200"
disabled || loading ? "cursor-not-allowed" : "hover:bg-blue-200"
)}
disabled={disabled}
disabled={disabled || loading}
onClick={onClick}
type="button"
>
<TokenIconWithNetwork
className="mr-1 h-5 w-5"
fallbackIconSrc={fallbackLogoURI ?? fallbackIcon.fallbackIconSrc}
iconSrc={primaryIcon}
network={network}
showNetworkOverlay={!!network}
tokenSymbol={assetIcon}
/>
<strong className="font-bold text-black">{tokenSymbol}</strong>
<ChevronDownIcon className="w-6" />
{loading ? (
<div className="flex animate-pulse items-center gap-1">
<div className="mr-1 h-5 w-5 rounded-full bg-neutral-300" />
<div className="h-3 w-12 rounded bg-neutral-300" />
<ChevronDownIcon className="w-6 opacity-30" />
</div>
) : (
<>
<TokenIconWithNetwork
className="mr-1 h-5 w-5"
fallbackIconSrc={fallbackLogoURI ?? fallbackIcon.fallbackIconSrc}
iconSrc={primaryIcon}
network={network}
showNetworkOverlay={!!network}
tokenSymbol={assetIcon}
/>
<strong className="font-bold text-black">{tokenSymbol}</strong>
<ChevronDownIcon className="w-6" />
</>
)}
</button>
);
}
11 changes: 9 additions & 2 deletions apps/frontend/src/hooks/useRampUrlParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
43 changes: 21 additions & 22 deletions apps/frontend/src/hooks/useSyncFormToUrl.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<string, string | undefined> = {
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]);
};