diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 94c411e4918..044c7b0d22e 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -52,6 +52,7 @@ "withdrawal": "Withdrawal", "claim": "Claim", "claiming": "Claiming...", + "confirming": "Confirming...", "withdrawAndClaim": "Withdraw & Claim", "overview": "Overview", "connectWallet": "Connect Wallet", diff --git a/src/lib/yieldxyz/utils.ts b/src/lib/yieldxyz/utils.ts index 3fe6b72688f..f71a9fb4993 100644 --- a/src/lib/yieldxyz/utils.ts +++ b/src/lib/yieldxyz/utils.ts @@ -19,10 +19,60 @@ const TX_TITLE_PATTERNS: [RegExp, string][] = [ [/supply|deposit|enter/i, 'Deposit'], [/withdraw|exit/i, 'Withdraw'], [/claim/i, 'Claim'], - [/unstake/i, 'Unstake'], - [/stake/i, 'Stake'], + [/unstake|undelegate/i, 'Unstake'], + [/stake|delegate/i, 'Stake'], + [/bridge/i, 'Bridge'], + [/swap/i, 'Swap'], ] +// Map of transaction types to user-friendly button labels +// These should match the action verbs shown in the step row (without the asset symbol) +const TX_TYPE_TO_LABEL: Record = { + APPROVE: 'Approve', + APPROVAL: 'Approve', + DELEGATE: 'Stake', // Monad uses DELEGATE for staking + UNDELEGATE: 'Unstake', // Monad uses UNDELEGATE for unstaking + STAKE: 'Stake', + UNSTAKE: 'Unstake', + DEPOSIT: 'Deposit', + WITHDRAW: 'Withdraw', + SUPPLY: 'Deposit', + EXIT: 'Withdraw', + ENTER: 'Deposit', + BRIDGE: 'Bridge', + SWAP: 'Swap', + CLAIM: 'Claim', + CLAIM_REWARDS: 'Claim', + TRANSFER: 'Transfer', +} + +/** + * Gets a clean button label from a transaction type or title. + * Used for the main CTA button in the yield action modal. + */ +export const getTransactionButtonText = ( + type: string | undefined, + title: string | undefined, +): string => { + // First try to use the transaction type directly + if (type) { + const normalized = type.toUpperCase().replace(/[_-]/g, '_') + if (TX_TYPE_TO_LABEL[normalized]) { + return TX_TYPE_TO_LABEL[normalized] + } + // Fallback: capitalize the type + return type.charAt(0).toUpperCase() + type.slice(1).toLowerCase() + } + + // Fall back to parsing the title + if (title) { + const match = TX_TITLE_PATTERNS.find(([pattern]) => pattern.test(title)) + if (match) return match[1] + } + + return 'Confirm' +} + export const formatYieldTxTitle = (title: string, assetSymbol: string): string => { const normalized = title.replace(/ transaction$/i, '').toLowerCase() const match = TX_TITLE_PATTERNS.find(([pattern]) => pattern.test(normalized)) diff --git a/src/pages/Yields/YieldAssetDetails.tsx b/src/pages/Yields/YieldAssetDetails.tsx index 644a8fae98a..0aeea1a69ac 100644 --- a/src/pages/Yields/YieldAssetDetails.tsx +++ b/src/pages/Yields/YieldAssetDetails.tsx @@ -51,9 +51,10 @@ export const YieldAssetDetails = memo(() => { const setViewMode = useCallback( (mode: 'grid' | 'list') => { setSearchParams(prev => { - if (mode === 'grid') prev.delete('view') - else prev.set('view', mode) - return prev + const next = new URLSearchParams(prev) + if (mode === 'grid') next.delete('view') + else next.set('view', mode) + return next }) }, [setSearchParams], @@ -85,6 +86,8 @@ export const YieldAssetDetails = memo(() => { [yields, decodedSymbol], ) + // Networks available for THIS asset - since we're on an asset-specific page, + // we show only networks that have yields for this particular asset (not all global networks) const networks = useMemo( () => Array.from(new Set(assetYields.map(y => y.network))).map(net => ({ @@ -95,6 +98,7 @@ export const YieldAssetDetails = memo(() => { [assetYields], ) + // Providers available for THIS asset - shows only providers that offer yields for this asset const providers = useMemo( () => Array.from(new Set(assetYields.map(y => y.providerId))).map(pId => ({ @@ -287,6 +291,8 @@ export const YieldAssetDetails = memo(() => { onSortingChange: setSorting, }) + const sortedRows = table.getSortedRowModel().rows + const handleYieldClick = useCallback( (yieldId: string) => { const balances = allBalances?.[yieldId] @@ -353,7 +359,7 @@ export const YieldAssetDetails = memo(() => { const gridViewElement = useMemo( () => ( - {table.getSortedRowModel().rows.map(row => ( + {sortedRows.map(row => ( { ))} ), - [allBalances, getProviderLogo, handleYieldClick, table], + [allBalances, getProviderLogo, handleYieldClick, sortedRows], ) const listViewElement = useMemo( @@ -384,7 +390,9 @@ export const YieldAssetDetails = memo(() => { ), - [handleRowClick, table], + // sortedRows needed to trigger re-memoization when filtered data changes (table ref is stable) + // eslint-disable-next-line react-hooks/exhaustive-deps + [handleRowClick, sortedRows, table], ) const contentElement = useMemo(() => { diff --git a/src/pages/Yields/YieldDetail.tsx b/src/pages/Yields/YieldDetail.tsx index 43c2e759690..3587d39746f 100644 --- a/src/pages/Yields/YieldDetail.tsx +++ b/src/pages/Yields/YieldDetail.tsx @@ -98,6 +98,8 @@ export const YieldDetail = memo(() => { [error, heroBg, navigate, translate], ) + const iconBg = useColorModeValue('white', 'gray.800') + const heroIcon = useMemo(() => { if (!yieldItem) return null const iconSource = resolveYieldInputAssetIcon(yieldItem) @@ -111,6 +113,7 @@ export const YieldDetail = memo(() => { border='4px solid' borderColor={heroIconBorderColor} borderRadius='full' + bg={iconBg} /> ) return ( @@ -121,9 +124,10 @@ export const YieldDetail = memo(() => { border='4px solid' borderColor={heroIconBorderColor} borderRadius='full' + bg={iconBg} /> ) - }, [heroIconBorderColor, yieldItem]) + }, [heroIconBorderColor, yieldItem, iconBg]) const providerOrValidatorsElement = useMemo(() => { if (!yieldItem) return null diff --git a/src/pages/Yields/components/ValidatorBreakdown.tsx b/src/pages/Yields/components/ValidatorBreakdown.tsx index 067df5509a1..dd6b0efd6ba 100644 --- a/src/pages/Yields/components/ValidatorBreakdown.tsx +++ b/src/pages/Yields/components/ValidatorBreakdown.tsx @@ -342,8 +342,9 @@ export const ValidatorBreakdown = memo( (validatorAddress: string) => (e: React.MouseEvent) => { e.stopPropagation() setSearchParams(prev => { - prev.set('validator', validatorAddress) - return prev + const next = new URLSearchParams(prev) + next.set('validator', validatorAddress) + return next }) }, [setSearchParams], diff --git a/src/pages/Yields/components/YieldActionModal.tsx b/src/pages/Yields/components/YieldActionModal.tsx index 45924cb3f17..1b0632d3501 100644 --- a/src/pages/Yields/components/YieldActionModal.tsx +++ b/src/pages/Yields/components/YieldActionModal.tsx @@ -28,7 +28,9 @@ import { Amount } from '@/components/Amount/Amount' import { MiddleEllipsis } from '@/components/MiddleEllipsis/MiddleEllipsis' import { bnOrZero } from '@/lib/bignumber/bignumber' import type { AugmentedYieldDto } from '@/lib/yieldxyz/types' +import { formatYieldTxTitle, getTransactionButtonText } from '@/lib/yieldxyz/utils' import { GradientApy } from '@/pages/Yields/components/GradientApy' +import type { TransactionStep } from '@/pages/Yields/hooks/useYieldTransactionFlow' import { ModalStep, useYieldTransactionFlow } from '@/pages/Yields/hooks/useYieldTransactionFlow' import { useYieldProviders } from '@/react-queries/queries/yieldxyz/useYieldProviders' import { useYieldValidators } from '@/react-queries/queries/yieldxyz/useYieldValidators' @@ -86,10 +88,12 @@ export const YieldActionModal = memo(function YieldActionModal({ step, transactionSteps, isSubmitting, + activeStepIndex, canSubmit, handleConfirm, handleClose, isQuoteLoading, + quoteData, } = useYieldTransactionFlow({ yieldItem, action, @@ -197,16 +201,31 @@ export const YieldActionModal = memo(function YieldActionModal({ const loadingText = useMemo(() => { if (isQuoteLoading) return translate('yieldXYZ.loadingQuote') + // Use the current step's loading message if available + if (activeStepIndex >= 0 && transactionSteps[activeStepIndex]?.loadingMessage) { + return transactionSteps[activeStepIndex].loadingMessage + } if (action === 'enter') return translate('yieldXYZ.depositing') if (action === 'exit') return translate('yieldXYZ.withdrawing') return translate('common.claiming') - }, [isQuoteLoading, action, translate]) + }, [isQuoteLoading, action, translate, activeStepIndex, transactionSteps]) const buttonText = useMemo(() => { + // Use the current step's type/title for a clean button label (e.g., "Delegate", "Undelegate", "Approve") + if (activeStepIndex >= 0 && transactionSteps[activeStepIndex]) { + const step = transactionSteps[activeStepIndex] + return getTransactionButtonText(step.type, step.originalTitle) + } + // Before execution starts, use the first CREATED transaction from quoteData + const firstCreatedTx = quoteData?.transactions?.find(tx => tx.status === 'CREATED') + if (firstCreatedTx) { + return getTransactionButtonText(firstCreatedTx.type, firstCreatedTx.title) + } + // Fallback to action-based text if (action === 'enter') return translate('yieldXYZ.deposit') if (action === 'exit') return translate('yieldXYZ.withdraw') return translate('common.claim') - }, [action, translate]) + }, [action, translate, activeStepIndex, transactionSteps, quoteData]) const modalHeading = useMemo(() => { if (action === 'enter') return translate('yieldXYZ.supplySymbol', { symbol: assetSymbol }) @@ -227,6 +246,26 @@ export const YieldActionModal = memo(function YieldActionModal({ [feeAsset?.networkIcon, feeAsset?.icon], ) + // Show steps from quoteData before execution starts, then switch to actual transactionSteps + const displaySteps = useMemo((): TransactionStep[] => { + // If we have transactionSteps (execution has started or completed), use those + if (transactionSteps.length > 0) { + return transactionSteps + } + // Before execution, create preview steps from quoteData (filter out SKIPPED transactions) + if (quoteData?.transactions?.length) { + return quoteData.transactions + .filter(tx => tx.status === 'CREATED') + .map((tx, i) => ({ + title: formatYieldTxTitle(tx.title || `Transaction ${i + 1}`, assetSymbol), + originalTitle: tx.title || '', + type: tx.type, + status: 'pending' as const, + })) + } + return [] + }, [transactionSteps, quoteData, assetSymbol]) + const statusCard = useMemo( () => ( - {transactionSteps.map((s, idx) => ( + {displaySteps.map((s, idx) => ( (value === null ? selectedBg : undefined), [value, selectedBg]) const allItemColor = useMemo( () => (value === null ? selectedColor : undefined), [value, selectedColor], @@ -89,7 +87,6 @@ const FilterMenu = memo(({ label, value, options, onSelect, renderIcon }: Filter onSelect(opt.id)} - bg={isSelected ? selectedBg : undefined} color={isSelected ? selectedColor : undefined} fontWeight={isSelected ? 'semibold' : undefined} > @@ -100,7 +97,7 @@ const FilterMenu = memo(({ label, value, options, onSelect, renderIcon }: Filter ) }), - [options, value, selectedBg, selectedColor, renderIcon, onSelect], + [options, value, selectedColor, renderIcon, onSelect], ) return ( @@ -126,12 +123,7 @@ const FilterMenu = memo(({ label, value, options, onSelect, renderIcon }: Filter - + {label} {menuItems} diff --git a/src/pages/Yields/components/YieldItem.tsx b/src/pages/Yields/components/YieldItem.tsx index cbf6e465903..72da2e25d23 100644 --- a/src/pages/Yields/components/YieldItem.tsx +++ b/src/pages/Yields/components/YieldItem.tsx @@ -49,93 +49,119 @@ type YieldItemProps = { variant: 'card' | 'row' userBalanceUsd?: BigNumber onEnter?: (yieldItem: AugmentedYieldDto) => void + searchString?: string } -export const YieldItem = memo(({ data, variant, userBalanceUsd, onEnter }: YieldItemProps) => { - const navigate = useNavigate() - const translate = useTranslate() - const userCurrencyToUsdRate = useAppSelector(selectUserCurrencyToUsdRate) - const { data: yieldProviders } = useYieldProviders() +export const YieldItem = memo( + ({ data, variant, userBalanceUsd, onEnter, searchString }: YieldItemProps) => { + const navigate = useNavigate() + const translate = useTranslate() + const userCurrencyToUsdRate = useAppSelector(selectUserCurrencyToUsdRate) + const { data: yieldProviders } = useYieldProviders() - const borderColor = useColorModeValue('gray.100', 'gray.750') - const cardBg = useColorModeValue('white', 'gray.800') - const hoverBorderColor = useColorModeValue('blue.500', 'blue.400') - const hoverBg = useColorModeValue('gray.50', 'whiteAlpha.50') - const cardShadow = useColorModeValue('sm', 'none') - const cardHoverShadow = useColorModeValue('lg', 'lg') + const borderColor = useColorModeValue('gray.100', 'gray.750') + const cardBg = useColorModeValue('white', 'gray.800') + const hoverBorderColor = useColorModeValue('blue.500', 'blue.400') + const hoverBg = useColorModeValue('gray.50', 'whiteAlpha.50') + const cardShadow = useColorModeValue('sm', 'none') + const cardHoverShadow = useColorModeValue('lg', 'lg') - const isSingle = data.type === 'single' - const isGroup = data.type === 'group' + const isSingle = data.type === 'single' + const isGroup = data.type === 'group' - const stats = useMemo(() => { - if (isSingle) { - const y = data.yieldItem - return { - apy: y.rewardRate.total, - apyLabel: y.rewardRate.rateType, - tvlUsd: y.statistics?.tvlUsd ?? '0', - providers: [{ id: y.providerId, logo: data.providerIcon }], - chainIds: y.chainId ? [y.chainId] : [], - count: 1, - name: y.metadata.name, - canEnter: y.status.enter, + const stats = useMemo(() => { + if (isSingle) { + const y = data.yieldItem + return { + apy: y.rewardRate.total, + apyLabel: y.rewardRate.rateType, + tvlUsd: y.statistics?.tvlUsd ?? '0', + providers: [{ id: y.providerId, logo: data.providerIcon }], + chainIds: y.chainId ? [y.chainId] : [], + count: 1, + name: y.metadata.name, + canEnter: y.status.enter, + } } - } - const yields = data.yields - const maxApy = Math.max(0, ...yields.map(y => y.rewardRate.total)) - const totalTvlUsd = yields - .reduce((acc, y) => acc.plus(bnOrZero(y.statistics?.tvlUsd)), bnOrZero(0)) - .toFixed() - const providerIds = [...new Set(yields.map(y => y.providerId))] - const chainIds = [...new Set(yields.map(y => y.chainId).filter(Boolean))] as string[] + const yields = data.yields + const maxApy = Math.max(0, ...yields.map(y => y.rewardRate.total)) + const totalTvlUsd = yields + .reduce((acc, y) => acc.plus(bnOrZero(y.statistics?.tvlUsd)), bnOrZero(0)) + .toFixed() + const providerIds = [...new Set(yields.map(y => y.providerId))] + const chainIds = [...new Set(yields.map(y => y.chainId).filter(Boolean))] as string[] - return { - apy: maxApy, - apyLabel: 'APY', - tvlUsd: totalTvlUsd, - providers: providerIds.map(id => ({ id, logo: yieldProviders?.[id]?.logoURI })), - chainIds, - count: yields.length, - name: data.assetName, - canEnter: true, - } - }, [data, isSingle, yieldProviders]) + return { + apy: maxApy, + apyLabel: 'APY', + tvlUsd: totalTvlUsd, + providers: providerIds.map(id => ({ id, logo: yieldProviders?.[id]?.logoURI })), + chainIds, + count: yields.length, + name: data.assetName, + canEnter: true, + } + }, [data, isSingle, yieldProviders]) - const apyFormatted = useMemo(() => `${(stats.apy * 100).toFixed(2)}%`, [stats.apy]) + const apyFormatted = useMemo(() => `${(stats.apy * 100).toFixed(2)}%`, [stats.apy]) - const tvlUserCurrency = useMemo( - () => bnOrZero(stats.tvlUsd).times(userCurrencyToUsdRate).toFixed(), - [stats.tvlUsd, userCurrencyToUsdRate], - ) + const tvlUserCurrency = useMemo( + () => bnOrZero(stats.tvlUsd).times(userCurrencyToUsdRate).toFixed(), + [stats.tvlUsd, userCurrencyToUsdRate], + ) - const userBalanceUserCurrency = useMemo( - () => (userBalanceUsd ? userBalanceUsd.times(userCurrencyToUsdRate).toFixed() : undefined), - [userBalanceUsd, userCurrencyToUsdRate], - ) + const userBalanceUserCurrency = useMemo( + () => (userBalanceUsd ? userBalanceUsd.times(userCurrencyToUsdRate).toFixed() : undefined), + [userBalanceUsd, userCurrencyToUsdRate], + ) - const hasBalance = userBalanceUsd && userBalanceUsd.gt(0) + const hasBalance = userBalanceUsd && userBalanceUsd.gt(0) - const handleClick = useCallback(() => { - if (isSingle) { - if (stats.canEnter && onEnter) { - onEnter(data.yieldItem) + const handleClick = useCallback(() => { + if (isSingle) { + if (stats.canEnter && onEnter) { + onEnter(data.yieldItem) + } else { + navigate(`/yields/${data.yieldItem.id}`) + } } else { - navigate(`/yields/${data.yieldItem.id}`) + const suffix = searchString ? `?${searchString}` : '' + navigate(`/yields/asset/${encodeURIComponent(data.assetSymbol)}${suffix}`) } - } else { - navigate(`/yields/asset/${encodeURIComponent(data.assetSymbol)}`) - } - }, [data, isSingle, navigate, onEnter, stats.canEnter]) + }, [data, isSingle, navigate, onEnter, searchString, stats.canEnter]) - const iconElement = useMemo(() => { - if (isSingle) { - const iconSource = resolveYieldInputAssetIcon(data.yieldItem) + const iconElement = useMemo(() => { + if (isSingle) { + const iconSource = resolveYieldInputAssetIcon(data.yieldItem) + const size = variant === 'card' ? 'md' : 'sm' + if (iconSource.assetId) { + return ( + + ) + } + return ( + + ) + } const size = variant === 'card' ? 'md' : 'sm' - if (iconSource.assetId) { + if (data.assetId) { return ( ) - } - const size = variant === 'card' ? 'md' : 'sm' - if (data.assetId) { + }, [data, isSingle, variant, borderColor]) + + const subtitle = useMemo(() => { + if (isSingle) { + return data.yieldItem.providerId + } + return `${stats.count} ${ + stats.count === 1 ? translate('yieldXYZ.market') : translate('yieldXYZ.markets') + }` + }, [data, isSingle, stats.count, translate]) + + const title = useMemo(() => { + if (isSingle) return data.yieldItem.metadata.name + return data.assetSymbol + }, [data, isSingle]) + + if (variant === 'row') { return ( - + + + + {iconElement} + + + {title} + + + {subtitle} + + + + + + + {isGroup ? translate('yieldXYZ.maxApy') : translate('yieldXYZ.apy')} + + + {apyFormatted} + + + + + {translate('yieldXYZ.tvl')} + + + + + + + {isGroup ? ( + + {stats.providers.map(p => ( + + ))} + + ) : ( + + {stats.providers.slice(0, 1).map(p => ( + + ))} + + )} + + + {hasBalance ? ( + + + + ) : ( + + — + + )} + + + + ) } - return ( - - ) - }, [data, isSingle, variant, borderColor]) - - const subtitle = useMemo(() => { - if (isSingle) { - return data.yieldItem.providerId - } - return `${stats.count} ${ - stats.count === 1 ? translate('yieldXYZ.market') : translate('yieldXYZ.markets') - }` - }, [data, isSingle, stats.count, translate]) - - const title = useMemo(() => { - if (isSingle) return data.yieldItem.metadata.name - return data.assetSymbol - }, [data, isSingle]) - if (variant === 'row') { return ( - - - - {iconElement} - - - {title} - - - {subtitle} - - + + + + {iconElement} + + + {title} + + + {isSingle && data.providerIcon && ( + + )} + + {subtitle} + + + + - - - - {isGroup ? translate('yieldXYZ.maxApy') : translate('yieldXYZ.apy')} - - + + + + + {isGroup + ? translate('yieldXYZ.maxApy') + : `${translate('yieldXYZ.apy')} (${stats.apyLabel})`} + + {apyFormatted} - - - - - {translate('yieldXYZ.tvl')} - - - - - - - {isGroup ? ( - - {stats.providers.map(p => ( - - ))} - - ) : ( - - {stats.providers.slice(0, 1).map(p => ( - - ))} - - )} - - + + + {hasBalance ? ( - + - + ) : ( - - — - + <> + + {translate('yieldXYZ.tvl')} + + + + + )} - - - - - ) - } + + - return ( - - - - - {iconElement} - - - {title} - - - {isSingle && data.providerIcon && ( - - )} - - {subtitle} - + {isGroup && ( + + + + + {stats.providers.length}{' '} + {stats.providers.length === 1 + ? translate('yieldXYZ.protocol') + : translate('yieldXYZ.protocols')} + + + {stats.providers.map(p => ( + + ))} + + + + + {stats.chainIds.length}{' '} + {stats.chainIds.length === 1 + ? translate('yieldXYZ.chain') + : translate('yieldXYZ.chains')} + + + {stats.chainIds.slice(0, 5).map(chainId => ( + + ))} + + - - - - - - - {isGroup - ? translate('yieldXYZ.maxApy') - : `${translate('yieldXYZ.apy')} (${stats.apyLabel})`} - - - {apyFormatted} - - - - {hasBalance ? ( - - - - ) : ( - <> - - {translate('yieldXYZ.tvl')} - - - - - - )} - - - - {isGroup && ( - - - - - {stats.providers.length}{' '} - {stats.providers.length === 1 - ? translate('yieldXYZ.protocol') - : translate('yieldXYZ.protocols')} - - - {stats.providers.map(p => ( - - ))} - - - - - {stats.chainIds.length}{' '} - {stats.chainIds.length === 1 - ? translate('yieldXYZ.chain') - : translate('yieldXYZ.chains')} - - - {stats.chainIds.slice(0, 5).map(chainId => ( - - ))} - - - - - )} - - - ) -}) + )} + + + ) + }, +) export const YieldItemSkeleton = memo(({ variant }: { variant: 'card' | 'row' }) => { const borderColor = useColorModeValue('gray.100', 'gray.750') diff --git a/src/pages/Yields/components/YieldsList.tsx b/src/pages/Yields/components/YieldsList.tsx index 9df01b97dba..62d90cfb6eb 100644 --- a/src/pages/Yields/components/YieldsList.tsx +++ b/src/pages/Yields/components/YieldsList.tsx @@ -70,14 +70,16 @@ export const YieldsList = memo(() => { const setViewMode = useCallback( (mode: 'grid' | 'list') => { setSearchParams(prev => { - if (mode === 'grid') prev.delete('view') - else prev.set('view', mode) - return prev + const next = new URLSearchParams(prev) + if (mode === 'grid') next.delete('view') + else next.set('view', mode) + return next }) }, [setSearchParams], ) const [searchQuery, setSearchQuery] = useState('') + const filterSearchString = useMemo(() => searchParams.toString(), [searchParams]) const { selectedNetwork, @@ -110,9 +112,10 @@ export const YieldsList = memo(() => { const handleTabChange = useCallback( (index: number) => { setSearchParams(prev => { - if (index === 0) prev.delete('tab') - else prev.set('tab', 'my-positions') - return prev + const next = new URLSearchParams(prev) + if (index === 0) next.delete('tab') + else next.set('tab', 'my-positions') + return next }) }, [setSearchParams], @@ -120,9 +123,10 @@ export const YieldsList = memo(() => { const handleToggleMyOpportunities = useCallback(() => { setSearchParams(prev => { - if (isMyOpportunities) prev.delete('filter') - else prev.set('filter', 'my-assets') - return prev + const next = new URLSearchParams(prev) + if (isMyOpportunities) next.delete('filter') + else next.set('filter', 'my-assets') + return next }) }, [isMyOpportunities, setSearchParams]) @@ -498,11 +502,12 @@ export const YieldsList = memo(() => { }} variant='card' userBalanceUsd={group.userGroupBalanceUsd} + searchString={filterSearchString} /> ))} ), - [yieldsByAsset], + [filterSearchString, yieldsByAsset], ) const allYieldsListElement = useMemo( @@ -557,11 +562,12 @@ export const YieldsList = memo(() => { }} variant='row' userBalanceUsd={group.userGroupBalanceUsd} + searchString={filterSearchString} /> ))} ), - [headerBg, translate, yieldsByAsset], + [filterSearchString, headerBg, translate, yieldsByAsset], ) const allYieldsContentElement = useMemo(() => { diff --git a/src/pages/Yields/hooks/useYieldFilters.ts b/src/pages/Yields/hooks/useYieldFilters.ts index e9bb091ba37..6b2d61e13e0 100644 --- a/src/pages/Yields/hooks/useYieldFilters.ts +++ b/src/pages/Yields/hooks/useYieldFilters.ts @@ -18,9 +18,10 @@ export const useYieldFilters = () => { const handleNetworkChange = useCallback( (network: string | null) => { setSearchParams(prev => { - if (!network) prev.delete('network') - else prev.set('network', network) - return prev + const next = new URLSearchParams(prev) + if (!network) next.delete('network') + else next.set('network', network) + return next }) }, [setSearchParams], @@ -29,9 +30,10 @@ export const useYieldFilters = () => { const handleProviderChange = useCallback( (provider: string | null) => { setSearchParams(prev => { - if (!provider) prev.delete('provider') - else prev.set('provider', provider) - return prev + const next = new URLSearchParams(prev) + if (!provider) next.delete('provider') + else next.set('provider', provider) + return next }) }, [setSearchParams], @@ -40,8 +42,9 @@ export const useYieldFilters = () => { const handleSortChange = useCallback( (option: SortOption) => { setSearchParams(prev => { - prev.set('sort', option) - return prev + const next = new URLSearchParams(prev) + next.set('sort', option) + return next }) }, [setSearchParams], diff --git a/src/pages/Yields/hooks/useYieldTransactionFlow.ts b/src/pages/Yields/hooks/useYieldTransactionFlow.ts index 8349bc90348..200350d7a6a 100644 --- a/src/pages/Yields/hooks/useYieldTransactionFlow.ts +++ b/src/pages/Yields/hooks/useYieldTransactionFlow.ts @@ -1,11 +1,13 @@ import { useToast } from '@chakra-ui/react' import type { AssetId } from '@shapeshiftoss/caip' import { cosmosChainId, fromAccountId } from '@shapeshiftoss/caip' +import type { KnownChainIds } from '@shapeshiftoss/types' import { useQuery, useQueryClient } from '@tanstack/react-query' import { uuidv4 } from '@walletconnect/utils' import { useCallback, useMemo, useState } from 'react' import { useTranslate } from 'react-polyglot' +import { SECOND_CLASS_CHAINS } from '@/constants/chains' import { useWallet } from '@/hooks/useWallet/useWallet' import { bnOrZero } from '@/lib/bignumber/bignumber' import { enterYield, exitYield, fetchAction, manageYield } from '@/lib/yieldxyz/api' @@ -27,6 +29,7 @@ import { ActionType, GenericTransactionDisplayType, } from '@/state/slices/actionSlice/types' +import { portfolioApi } from '@/state/slices/portfolioSlice/portfolioSlice' import { selectPortfolioAccountMetadataByAccountId } from '@/state/slices/portfolioSlice/selectors' import { selectAccountIdByAccountNumberAndChainId, @@ -43,6 +46,7 @@ export type TransactionStep = { title: string status: 'pending' | 'success' | 'loading' originalTitle: string + type?: string txHash?: string txUrl?: string loadingMessage?: string @@ -211,7 +215,8 @@ export const useYieldTransactionFlow = ({ return fn({ yieldId: yieldItem.id, address: userAddress, arguments: txArguments }) }, enabled: !!txArguments && !!wallet && !!accountId && canSubmit && isOpen, - staleTime: 60_000, + staleTime: Infinity, + gcTime: 0, retry: false, }) @@ -259,11 +264,17 @@ export const useYieldTransactionFlow = ({ // For now, KISS and simply don't handle claims in action center. if (action === 'manage') return + const typeMessagesMap: Partial> = { + [ActionType.Deposit]: 'actionCenter.deposit.complete', + [ActionType.Withdraw]: 'actionCenter.withdrawal.complete', + [ActionType.Approve]: 'actionCenter.approve.approvalTxComplete', + } + dispatch( actionSlice.actions.upsertAction({ id: uuidv4(), type: actionType, - status: ActionStatus.Pending, + status: ActionStatus.Complete, createdAt: Date.now(), updatedAt: Date.now(), transactionMetadata: { @@ -272,13 +283,27 @@ export const useYieldTransactionFlow = ({ chainId: yieldChainId, assetId: yieldItem.token.assetId as AssetId, accountId, - message: formatYieldTxTitle(tx.title || 'Transaction', assetSymbol), + message: + typeMessagesMap[actionType] ?? + formatYieldTxTitle(tx.title || 'Transaction', assetSymbol), amountCryptoPrecision: amount, + contractName: yieldItem.metadata.name, + chainName: yieldItem.network, }, }), ) }, - [dispatch, yieldChainId, accountId, action, yieldItem.token.assetId, assetSymbol, amount], + [ + dispatch, + yieldChainId, + accountId, + action, + yieldItem.token.assetId, + yieldItem.metadata.name, + yieldItem.network, + assetSymbol, + amount, + ], ) const buildCosmosStakeArgs = useCallback((): CosmosStakeArgs | undefined => { @@ -348,8 +373,20 @@ export const useYieldTransactionFlow = ({ if (isLastTransaction) { await waitForActionCompletion(actionId) - queryClient.invalidateQueries({ queryKey: ['yieldxyz', 'allBalances'] }) - queryClient.invalidateQueries({ queryKey: ['yieldxyz', 'yields'] }) + await queryClient.refetchQueries({ queryKey: ['yieldxyz', 'allBalances'] }) + await queryClient.refetchQueries({ queryKey: ['yieldxyz', 'yields'] }) + if ( + yieldChainId && + accountId && + SECOND_CLASS_CHAINS.includes(yieldChainId as KnownChainIds) + ) { + dispatch( + portfolioApi.endpoints.getAccount.initiate( + { accountId, upsertOnFetch: true }, + { forceRefetch: true }, + ), + ) + } dispatchNotification(tx, txHash) updateStepStatus(index, { status: 'success', loadingMessage: undefined }) setStep(ModalStep.Success) @@ -365,8 +402,20 @@ export const useYieldTransactionFlow = ({ setActiveStepIndex(index + 1) } else { await waitForActionCompletion(actionId) - queryClient.invalidateQueries({ queryKey: ['yieldxyz', 'allBalances'] }) - queryClient.invalidateQueries({ queryKey: ['yieldxyz', 'yields'] }) + await queryClient.refetchQueries({ queryKey: ['yieldxyz', 'allBalances'] }) + await queryClient.refetchQueries({ queryKey: ['yieldxyz', 'yields'] }) + if ( + yieldChainId && + accountId && + SECOND_CLASS_CHAINS.includes(yieldChainId as KnownChainIds) + ) { + dispatch( + portfolioApi.endpoints.getAccount.initiate( + { accountId, upsertOnFetch: true }, + { forceRefetch: true }, + ), + ) + } dispatchNotification(tx, txHash) updateStepStatus(index, { status: 'success', loadingMessage: undefined }) setStep(ModalStep.Success) @@ -398,18 +447,20 @@ export const useYieldTransactionFlow = ({ queryClient, dispatchNotification, showErrorToast, + dispatch, ], ) const handleClose = useCallback(() => { if (isSubmitting) return + queryClient.removeQueries({ queryKey: ['yieldxyz', 'quote'] }) setStep(ModalStep.InProgress) setTransactionSteps([]) setRawTransactions([]) setActiveStepIndex(-1) setCurrentActionId(null) onClose() - }, [isSubmitting, onClose]) + }, [isSubmitting, onClose, queryClient]) const handleConfirm = useCallback(async () => { if (activeStepIndex >= 0 && rawTransactions[activeStepIndex] && currentActionId) { @@ -474,6 +525,7 @@ export const useYieldTransactionFlow = ({ transactions.map((tx, i) => ({ title: formatYieldTxTitle(tx.title || `Transaction ${i + 1}`, assetSymbol), originalTitle: tx.title || '', + type: tx.type, status: 'pending' as const, })), ) @@ -516,6 +568,7 @@ export const useYieldTransactionFlow = ({ handleConfirm, handleClose, isQuoteLoading, + quoteData, }), [ step, @@ -526,6 +579,7 @@ export const useYieldTransactionFlow = ({ handleConfirm, handleClose, isQuoteLoading, + quoteData, ], ) } diff --git a/src/state/slices/actionSlice/types.ts b/src/state/slices/actionSlice/types.ts index 08fb078224b..e63a15d63d0 100644 --- a/src/state/slices/actionSlice/types.ts +++ b/src/state/slices/actionSlice/types.ts @@ -114,6 +114,7 @@ type ActionGenericTransactionMetadata = { amountCryptoPrecision: string | undefined newAddress?: string contractName?: string + chainName?: string cooldownPeriod?: string cooldownPeriodSeconds?: number thorMemo?: string | null