From 575311b17aa152262c4cb68841c8160fb92467a0 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:22:23 +0100 Subject: [PATCH 1/9] chore: yield polish - Remove unnecessary filteredYields dependency in YieldAssetDetails useMemo hooks - Add validator breakdown utility functions - Enhance yield action modal with additional transaction handling - Refactor yield filters component for better code organization - Update yields list component layout - Improve yield filters hook logic - Add yield transaction flow hooks - Add missing translation key Co-Authored-By: Claude Sonnet 4.5 --- YIELD_POLISH.md | 272 ++++++++++++++++++ src/assets/translations/en/main.json | 1 + src/lib/yieldxyz/utils.ts | 53 +++- src/pages/Yields/YieldAssetDetails.tsx | 10 +- .../Yields/components/ValidatorBreakdown.tsx | 5 +- .../Yields/components/YieldActionModal.tsx | 47 ++- src/pages/Yields/components/YieldFilters.tsx | 14 +- src/pages/Yields/components/YieldsList.tsx | 21 +- src/pages/Yields/hooks/useYieldFilters.ts | 19 +- .../Yields/hooks/useYieldTransactionFlow.ts | 4 + 10 files changed, 406 insertions(+), 40 deletions(-) create mode 100644 YIELD_POLISH.md diff --git a/YIELD_POLISH.md b/YIELD_POLISH.md new file mode 100644 index 00000000000..aab03b0ce20 --- /dev/null +++ b/YIELD_POLISH.md @@ -0,0 +1,272 @@ +# Yield.xyz Integration Polish - Execution Plan + +> **Context**: Bugfixes and polishes identified during testing of PR #11578. Feedback from @NeOMakinG. +> **Related Issue**: [#11611](https://github.com/shapeshift/web/issues/11611) +> **Related PR**: [#11578](https://github.com/shapeshift/web/pull/11578) + +--- + +## Execution Instructions + +Each section below is a **standalone TODO** that can be executed independently. Before proceeding with each item: + +1. **ASK** the user: "Should I proceed with TODO X: [Title]?" +2. **WAIT** for explicit confirmation before implementing +3. **AFTER** completion, ask: "TODO X complete. Should I continue to TODO Y?" + +--- + +## Bugs + +### TODO 1: Fix Network Filtering - Hiding Other Networks + +> [!IMPORTANT] +> **Issue**: Selecting a network hides other networks. Users should be able to click again to see all networks and instantly select another one. + +**Current Behavior**: When a network is selected in the filter dropdown, clicking the dropdown again only shows the selected network (or a subset). + +**Expected Behavior**: The dropdown should always display ALL available networks, with the currently selected one highlighted. Clicking "All Networks" should clear the filter. + +**Investigation Areas**: +- [YieldFilters.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldFilters.tsx) - `FilterMenu` component logic +- [useYieldFilters.ts](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/hooks/useYieldFilters.ts) - Filter state management + +**Proposed Fix**: +1. In `YieldFilters.tsx`, ensure the `networks` prop passed to `FilterMenu` is always the full list of networks (not filtered) +2. The filtering should only affect the yields list, not the filter options themselves +3. Verify `YieldsList.tsx` passes the complete `networks` array from `yields.meta.networks` + +--- + +### TODO 2: Debug Tron Deposit Not Working + +> [!CAUTION] +> **Issue**: Deposits on Tron chain don't work. Requires debugging. + +**Investigation Areas**: +- [executeTransaction.ts](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/lib/yieldxyz/executeTransaction.ts#L432-495) - `executeTronTransaction` function +- [constants.ts](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/lib/yieldxyz/constants.ts#L38) - Tron chain ID mapping + +**Debugging Steps**: +1. Verify `tronChainId` mapping to `YieldNetwork.Tron` +2. Check if `executeTronTransaction` handles all required fields +3. Verify Tron chain adapter is correctly initialized +4. Check for errors in transaction parsing/signing + +**Proposed Fix**: TBD after debugging - needs reproduction and console log analysis. + +--- + +### TODO 3: Fix Wrong Translation Chain on Approved Deposit Toast + +> [!NOTE] +> **Issue**: The toast notification for approved deposits shows wrong chain translation. + +**Investigation Areas**: +- [useYieldTransactionFlow.ts](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/hooks/useYieldTransactionFlow.ts#L235-282) - `dispatchNotification` function +- Translation keys in `main.json` for yield notifications + +**Current Code Analysis**: +```typescript +// Line 275 in useYieldTransactionFlow.ts +message: formatYieldTxTitle(tx.title || 'Transaction', assetSymbol), +``` + +**Proposed Fix**: +1. Review the notification message construction in `dispatchNotification` +2. Ensure the correct chain name is passed to the translation +3. Check if `yieldChainId` is being properly resolved to chain name +4. Add chain name to the toast message if missing + +--- + +### TODO 4: Fix Transparent Icon on Yield Page + +> [!NOTE] +> **Issue**: Some icons appear transparent/invisible on yield pages. + +**Investigation Areas**: +- [YieldDetail.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/YieldDetail.tsx#L100-127) - `heroIcon` element +- [YieldItem.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldItem.tsx#L130-177) - `iconElement` rendering +- CSS/styling for icon backgrounds + +**Proposed Fix**: +1. Add background color to icon containers for better visibility +2. Check if icons with transparency need a white/dark background based on color mode +3. Review `AssetIcon` component for proper fallback handling + +--- + +### TODO 5: Show Pending Deposits After Deposit (Solana) + +> [!IMPORTANT] +> **Issue**: Balance isn't updated after a deposit. For Solana, deposits are pending by nature - should show pending deposit status. + +**Investigation Areas**: +- [useAllYieldBalances.ts](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/react-queries/queries/yieldxyz/useAllYieldBalances.ts) - Balance fetching and normalization +- [YieldPositionCard.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldPositionCard.tsx) - Position display +- `YieldBalanceType` enum - includes `Entering` type for pending deposits + +**Current Code Analysis**: +The balance types already include: +- `Active` - Active staked balance +- `Entering` - Pending/entering deposits +- `Exiting` - Pending withdrawals +- `Claimable` - Rewards ready to claim + +**Proposed Fix**: +1. Ensure `Entering` balance type is properly displayed in UI +2. Add visual indicator for pending deposits in `YieldPositionCard` +3. Possibly add polling or refetch after deposit completion +4. Show "Pending" badge for `hasEntering` positions (already tracked in `ValidatorSummary`) + +--- + +### TODO 6: Fix Network Filter + Asset Selection Opening Wrong Yield + +> [!IMPORTANT] +> **Issue**: Selecting Ethereum as network, clicking on USDC, selecting the AAVE yield opens the BASE yield instead. + +**Investigation Areas**: +- [YieldItem.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldItem.tsx#L118-128) - `handleClick` navigation +- [YieldAssetDetails.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/YieldAssetDetails.tsx) - Asset yields filtering +- [useYieldFilters.ts](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/hooks/useYieldFilters.ts) - Filter state via URL params + +**Root Cause Hypothesis**: +When navigating from YieldsList → YieldAssetDetails → YieldDetail, the network filter state may not be properly passed through, or the filtered yields in YieldAssetDetails may not respect the network filter. + +**Proposed Fix**: +1. Pass network filter through URL params when navigating to asset details +2. In `YieldAssetDetails`, respect the `network` search param when filtering yields +3. When clicking a specific yield, ensure the correct yield ID is used based on filtered context +4. Consider storing selected yield context in navigation state + +--- + +## UX Improvements + +### TODO 7: Improve "You Could Earn More" Messaging + +> [!NOTE] +> **Issue**: "You could earn up to X% on your balance" messaging seems weird. Need to emphasize actual annual earnings. + +**Investigation Areas**: +- [YieldOpportunityCard.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldOpportunityCard.tsx#L25) - `earnUpToText` usage +- Translation key: `yieldXYZ.earnUpTo` = "You could earn up to %{apy}% on your balance" + +**Current Code**: +```typescript +const earnUpToText = useMemo(() => translate('yieldXYZ.earnUpTo', { apy }), [translate, apy]) +``` + +**Proposed Changes**: +1. Change messaging to show estimated annual earnings in $ amount +2. Calculate: `userBalance * APY = estimatedYearlyEarnings` +3. New message format: "Earn ~$X per year" or "Your $X balance could earn ~$Y/year at X% APY" +4. Update translation key and component to accept balance amount + +--- + +### TODO 8: Review Yield Opportunities in Account Page + +> [!NOTE] +> **Issue**: Review design and layout of yield opportunities section in account page. + +**Investigation Areas**: +- [AccountToken.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Accounts/AccountToken/AccountToken.tsx#L71) - Integration of `YieldAssetSection` +- [YieldAssetSection.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldAssetSection.tsx) - Section component +- [YieldOpportunityCard.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldOpportunityCard.tsx) - Card layout + +**Current Layout**: +- Section appears after `EarnOpportunities` in account page +- Shows `YieldActivePositions` if positions exist +- Shows `YieldOpportunityCard` for best yield if no positions + +**Design Review Points**: +1. Consistency with other account page sections +2. Card styling matches design system +3. Loading states are smooth +4. Empty states are handled gracefully + +--- + +## Design/Polish + +### TODO 9: Align Design with ShapeShift Design System + +> [!WARNING] +> **Issue**: Current design feels "very AI" - needs to follow ShapeShift's established design patterns. + +**Investigation Areas**: +- All yield component files in `src/pages/Yields/components/` +- Compare with established patterns in `src/components/` +- Color usage, spacing, typography + +**Design Audit Checklist**: +1. [ ] Color palette matches design system (avoid arbitrary colors) +2. [ ] Consistent use of Chakra UI theme tokens +3. [ ] Typography follows established patterns +4. [ ] Spacing/padding consistent with other pages +5. [ ] Dark/light mode support complete +6. [ ] Loading states match app patterns +7. [ ] Error states match app patterns +8. [ ] Empty states match app patterns +9. [ ] Animations/transitions smooth and purposeful + +**Files to Review**: +- [YieldsList.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldsList.tsx) +- [YieldItem.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldItem.tsx) +- [YieldDetail.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/YieldDetail.tsx) +- [YieldEnterExit.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldEnterExit.tsx) +- [YieldActionModal.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldActionModal.tsx) + +--- + +### TODO 10: Improve Marketing Content/Messaging + +> [!NOTE] +> **Issue**: Improve messaging and user-facing content throughout yield features. + +**Investigation Areas**: +- Translation keys in `main.json` under `yieldXYZ` namespace +- Component text that may be hardcoded +- Call-to-action buttons +- Descriptions and help text + +**Content Review Points**: +1. Clear value proposition for users +2. Simple, non-technical language where possible +3. Consistent terminology (yield vs staking vs earning) +4. Helpful tooltips for complex concepts +5. Error messages are actionable + +--- + +## Summary Table + +| # | Type | Issue | Priority | Complexity | +|---|------|-------|----------|------------| +| 1 | Bug | Network filtering hides networks | High | Low | +| 2 | Bug | Tron deposit not working | High | Medium | +| 3 | Bug | Wrong toast translation | Medium | Low | +| 4 | Bug | Transparent icon | Low | Low | +| 5 | Bug | Balance not updated (pending) | Medium | Medium | +| 6 | Bug | Wrong yield opens | High | Medium | +| 7 | UX | Earn messaging improvement | Low | Low | +| 8 | UX | Account page layout review | Low | Medium | +| 9 | Design | Design system alignment | Medium | High | +| 10 | Design | Marketing content | Low | Medium | + +--- + +## Execution Order Recommendation + +1. **High priority bugs first**: TODOs 1, 2, 6 +2. **Medium priority bugs**: TODOs 3, 5 +3. **Low priority bugs**: TODO 4 +4. **UX improvements**: TODOs 7, 8 +5. **Design/polish**: TODOs 9, 10 + +--- + +*Generated from issue analysis on 2026-01-11* 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..baee6072ddf 100644 --- a/src/lib/yieldxyz/utils.ts +++ b/src/lib/yieldxyz/utils.ts @@ -19,10 +19,59 @@ 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', + DELEGATE: 'Stake', // Monad uses DELEGATE for staking + UNDELEGATE: 'Unstake', // Monad uses UNDELEGATE for unstaking + STAKE: 'Stake', + UNSTAKE: 'Unstake', + DEPOSIT: 'Deposit', + WITHDRAW: 'Withdraw', + SUPPLY: 'Supply', + EXIT: 'Exit', + ENTER: 'Enter', + 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..fc44ff95bff 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 => ({ 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..38131dfa8de 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 transaction from quoteData + if (quoteData?.transactions?.[0]) { + const firstTx = quoteData.transactions[0] + return getTransactionButtonText(firstTx.type, firstTx.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,24 @@ 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 + if (quoteData?.transactions?.length) { + return quoteData.transactions.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/YieldsList.tsx b/src/pages/Yields/components/YieldsList.tsx index 9df01b97dba..5479f0bad0a 100644 --- a/src/pages/Yields/components/YieldsList.tsx +++ b/src/pages/Yields/components/YieldsList.tsx @@ -70,9 +70,10 @@ 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], @@ -110,9 +111,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 +122,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]) 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..b620bfc69d5 100644 --- a/src/pages/Yields/hooks/useYieldTransactionFlow.ts +++ b/src/pages/Yields/hooks/useYieldTransactionFlow.ts @@ -43,6 +43,7 @@ export type TransactionStep = { title: string status: 'pending' | 'success' | 'loading' originalTitle: string + type?: string txHash?: string txUrl?: string loadingMessage?: string @@ -474,6 +475,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 +518,7 @@ export const useYieldTransactionFlow = ({ handleConfirm, handleClose, isQuoteLoading, + quoteData, }), [ step, @@ -526,6 +529,7 @@ export const useYieldTransactionFlow = ({ handleConfirm, handleClose, isQuoteLoading, + quoteData, ], ) } From 5d6c60cea9147d165325d3cbeb1454e0774119ae Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:32:50 +0100 Subject: [PATCH 2/9] fix: yield modal SKIPPED transaction handling - Add APPROVAL type to TX_TYPE_TO_LABEL (button now shows "Approve" not "Approval") - Filter SKIPPED transactions in displaySteps preview - Use first CREATED transaction for button text - Harmonize button/row labels: SUPPLY/ENTER -> "Deposit", EXIT -> "Withdraw" - Fix useCallback missing dependencies in useYieldTransactionFlow - Add icon background color for yield detail hero - Add chainName to action transaction metadata Co-Authored-By: Claude Opus 4.5 --- src/lib/yieldxyz/utils.ts | 7 +++--- src/pages/Yields/YieldDetail.tsx | 6 ++++- .../Yields/components/YieldActionModal.tsx | 24 ++++++++++--------- .../Yields/hooks/useYieldTransactionFlow.ts | 14 ++++++++++- src/state/slices/actionSlice/types.ts | 1 + 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/lib/yieldxyz/utils.ts b/src/lib/yieldxyz/utils.ts index baee6072ddf..f71a9fb4993 100644 --- a/src/lib/yieldxyz/utils.ts +++ b/src/lib/yieldxyz/utils.ts @@ -29,15 +29,16 @@ const TX_TITLE_PATTERNS: [RegExp, string][] = [ // 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: 'Supply', - EXIT: 'Exit', - ENTER: 'Enter', + SUPPLY: 'Deposit', + EXIT: 'Withdraw', + ENTER: 'Deposit', BRIDGE: 'Bridge', SWAP: 'Swap', CLAIM: 'Claim', 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/YieldActionModal.tsx b/src/pages/Yields/components/YieldActionModal.tsx index 38131dfa8de..1b0632d3501 100644 --- a/src/pages/Yields/components/YieldActionModal.tsx +++ b/src/pages/Yields/components/YieldActionModal.tsx @@ -216,10 +216,10 @@ export const YieldActionModal = memo(function YieldActionModal({ const step = transactionSteps[activeStepIndex] return getTransactionButtonText(step.type, step.originalTitle) } - // Before execution starts, use the first transaction from quoteData - if (quoteData?.transactions?.[0]) { - const firstTx = quoteData.transactions[0] - return getTransactionButtonText(firstTx.type, firstTx.title) + // 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') @@ -252,14 +252,16 @@ export const YieldActionModal = memo(function YieldActionModal({ if (transactionSteps.length > 0) { return transactionSteps } - // Before execution, create preview steps from quoteData + // Before execution, create preview steps from quoteData (filter out SKIPPED transactions) if (quoteData?.transactions?.length) { - return quoteData.transactions.map((tx, i) => ({ - title: formatYieldTxTitle(tx.title || `Transaction ${i + 1}`, assetSymbol), - originalTitle: tx.title || '', - type: tx.type, - status: 'pending' as const, - })) + 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]) diff --git a/src/pages/Yields/hooks/useYieldTransactionFlow.ts b/src/pages/Yields/hooks/useYieldTransactionFlow.ts index b620bfc69d5..e80dc99fe59 100644 --- a/src/pages/Yields/hooks/useYieldTransactionFlow.ts +++ b/src/pages/Yields/hooks/useYieldTransactionFlow.ts @@ -275,11 +275,23 @@ export const useYieldTransactionFlow = ({ accountId, message: 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 => { 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 From d1927d4ccf84e7588fd361d2f084f80be8e066f3 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:40:42 +0100 Subject: [PATCH 3/9] fix: await query refetch after yield transactions - Change invalidateQueries to refetchQueries with await - Ensures position data is fresh before modal closes - Fixes "My Positions" showing empty after deposit/withdraw Co-Authored-By: Claude Opus 4.5 --- src/pages/Yields/hooks/useYieldTransactionFlow.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/Yields/hooks/useYieldTransactionFlow.ts b/src/pages/Yields/hooks/useYieldTransactionFlow.ts index e80dc99fe59..f48f4f13705 100644 --- a/src/pages/Yields/hooks/useYieldTransactionFlow.ts +++ b/src/pages/Yields/hooks/useYieldTransactionFlow.ts @@ -361,8 +361,8 @@ 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'] }) dispatchNotification(tx, txHash) updateStepStatus(index, { status: 'success', loadingMessage: undefined }) setStep(ModalStep.Success) @@ -378,8 +378,8 @@ 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'] }) dispatchNotification(tx, txHash) updateStepStatus(index, { status: 'success', loadingMessage: undefined }) setStep(ModalStep.Success) From f2c504f8295e860ccaf0a4aeb3c3dc2e95327dfa Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:51:51 +0100 Subject: [PATCH 4/9] fix: dispatch yield actions as Complete instead of Pending Since dispatchNotification is called after waitForActionCompletion, the transaction is already confirmed. Dispatch with Complete status directly instead of relying on Unchained subscriber (which doesn't work for chains without Unchained support like Monad). Co-Authored-By: Claude Opus 4.5 --- src/pages/Yields/hooks/useYieldTransactionFlow.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/pages/Yields/hooks/useYieldTransactionFlow.ts b/src/pages/Yields/hooks/useYieldTransactionFlow.ts index f48f4f13705..2d9fb9bd09e 100644 --- a/src/pages/Yields/hooks/useYieldTransactionFlow.ts +++ b/src/pages/Yields/hooks/useYieldTransactionFlow.ts @@ -260,11 +260,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: { @@ -273,7 +279,9 @@ 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, From 14d003e70bb92ba73fb05ed57c07e70e1eeea861 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Sun, 11 Jan 2026 17:01:31 +0100 Subject: [PATCH 5/9] fix: refetch portfolio balance for second class chains after yield tx After yield transaction completion, trigger portfolioApi.getAccount refetch for second class chains (Monad, Tron, etc.) that don't have Unchained support. This ensures wallet balances update immediately. Co-Authored-By: Claude Opus 4.5 --- .../Yields/hooks/useYieldTransactionFlow.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/pages/Yields/hooks/useYieldTransactionFlow.ts b/src/pages/Yields/hooks/useYieldTransactionFlow.ts index 2d9fb9bd09e..146cac79ec2 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, @@ -371,6 +374,18 @@ export const useYieldTransactionFlow = ({ await waitForActionCompletion(actionId) 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) @@ -388,6 +403,18 @@ export const useYieldTransactionFlow = ({ await waitForActionCompletion(actionId) 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) @@ -419,6 +446,7 @@ export const useYieldTransactionFlow = ({ queryClient, dispatchNotification, showErrorToast, + dispatch, ], ) From bf3bb789226d22035e46b28157ebd7af31072213 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Sun, 11 Jan 2026 17:12:39 +0100 Subject: [PATCH 6/9] fix: symbol page filters not working - Extract sortedRows from table to use as dependency - TanStack Table's table object reference is stable, so useMemo was not re-running when filtered data changed - Using sortedRows as dependency triggers proper re-memoization Co-Authored-By: Claude Opus 4.5 --- src/pages/Yields/YieldAssetDetails.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pages/Yields/YieldAssetDetails.tsx b/src/pages/Yields/YieldAssetDetails.tsx index fc44ff95bff..0aeea1a69ac 100644 --- a/src/pages/Yields/YieldAssetDetails.tsx +++ b/src/pages/Yields/YieldAssetDetails.tsx @@ -291,6 +291,8 @@ export const YieldAssetDetails = memo(() => { onSortingChange: setSorting, }) + const sortedRows = table.getSortedRowModel().rows + const handleYieldClick = useCallback( (yieldId: string) => { const balances = allBalances?.[yieldId] @@ -357,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( @@ -388,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(() => { From 4f632c55e7bf296374f519150a80f9c59924fb5b Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Sun, 11 Jan 2026 17:44:49 +0100 Subject: [PATCH 7/9] feat: preserve filters when navigating to asset page - Add searchString prop to YieldItem - Pass current filter params when navigating to asset detail page - Filters persist across navigation (e.g., network=ethereum) Co-Authored-By: Claude Opus 4.5 --- src/pages/Yields/components/YieldItem.tsx | 580 +++++++++++---------- src/pages/Yields/components/YieldsList.tsx | 7 +- 2 files changed, 297 insertions(+), 290 deletions(-) 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 5479f0bad0a..62d90cfb6eb 100644 --- a/src/pages/Yields/components/YieldsList.tsx +++ b/src/pages/Yields/components/YieldsList.tsx @@ -79,6 +79,7 @@ export const YieldsList = memo(() => { [setSearchParams], ) const [searchQuery, setSearchQuery] = useState('') + const filterSearchString = useMemo(() => searchParams.toString(), [searchParams]) const { selectedNetwork, @@ -501,11 +502,12 @@ export const YieldsList = memo(() => { }} variant='card' userBalanceUsd={group.userGroupBalanceUsd} + searchString={filterSearchString} /> ))} ), - [yieldsByAsset], + [filterSearchString, yieldsByAsset], ) const allYieldsListElement = useMemo( @@ -560,11 +562,12 @@ export const YieldsList = memo(() => { }} variant='row' userBalanceUsd={group.userGroupBalanceUsd} + searchString={filterSearchString} /> ))} ), - [headerBg, translate, yieldsByAsset], + [filterSearchString, headerBg, translate, yieldsByAsset], ) const allYieldsContentElement = useMemo(() => { From f063d64f603d0d6d1d707998f035c8d6787e90b4 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Sun, 11 Jan 2026 17:50:54 +0100 Subject: [PATCH 8/9] fix: always fetch fresh quote when modal opens - Set staleTime: Infinity to prevent refetching during modal lifecycle - Set gcTime: 0 to garbage collect cached quote when modal closes - Ensures fresh quote on re-entry, no stale data from previous tx Co-Authored-By: Claude Opus 4.5 --- src/pages/Yields/hooks/useYieldTransactionFlow.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/Yields/hooks/useYieldTransactionFlow.ts b/src/pages/Yields/hooks/useYieldTransactionFlow.ts index 146cac79ec2..1207315817c 100644 --- a/src/pages/Yields/hooks/useYieldTransactionFlow.ts +++ b/src/pages/Yields/hooks/useYieldTransactionFlow.ts @@ -215,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, }) From 1a4a1477837cd5cb67600ec4aed6bbe766d77144 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Sun, 11 Jan 2026 17:54:30 +0100 Subject: [PATCH 9/9] fix: remove cached quote on modal close - Remove quote queries from cache when modal closes - Ensures fresh quote fetch on subsequent modal opens - Remove YIELD_POLISH.md Co-Authored-By: Claude Opus 4.5 --- YIELD_POLISH.md | 272 ------------------ .../Yields/hooks/useYieldTransactionFlow.ts | 3 +- 2 files changed, 2 insertions(+), 273 deletions(-) delete mode 100644 YIELD_POLISH.md diff --git a/YIELD_POLISH.md b/YIELD_POLISH.md deleted file mode 100644 index aab03b0ce20..00000000000 --- a/YIELD_POLISH.md +++ /dev/null @@ -1,272 +0,0 @@ -# Yield.xyz Integration Polish - Execution Plan - -> **Context**: Bugfixes and polishes identified during testing of PR #11578. Feedback from @NeOMakinG. -> **Related Issue**: [#11611](https://github.com/shapeshift/web/issues/11611) -> **Related PR**: [#11578](https://github.com/shapeshift/web/pull/11578) - ---- - -## Execution Instructions - -Each section below is a **standalone TODO** that can be executed independently. Before proceeding with each item: - -1. **ASK** the user: "Should I proceed with TODO X: [Title]?" -2. **WAIT** for explicit confirmation before implementing -3. **AFTER** completion, ask: "TODO X complete. Should I continue to TODO Y?" - ---- - -## Bugs - -### TODO 1: Fix Network Filtering - Hiding Other Networks - -> [!IMPORTANT] -> **Issue**: Selecting a network hides other networks. Users should be able to click again to see all networks and instantly select another one. - -**Current Behavior**: When a network is selected in the filter dropdown, clicking the dropdown again only shows the selected network (or a subset). - -**Expected Behavior**: The dropdown should always display ALL available networks, with the currently selected one highlighted. Clicking "All Networks" should clear the filter. - -**Investigation Areas**: -- [YieldFilters.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldFilters.tsx) - `FilterMenu` component logic -- [useYieldFilters.ts](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/hooks/useYieldFilters.ts) - Filter state management - -**Proposed Fix**: -1. In `YieldFilters.tsx`, ensure the `networks` prop passed to `FilterMenu` is always the full list of networks (not filtered) -2. The filtering should only affect the yields list, not the filter options themselves -3. Verify `YieldsList.tsx` passes the complete `networks` array from `yields.meta.networks` - ---- - -### TODO 2: Debug Tron Deposit Not Working - -> [!CAUTION] -> **Issue**: Deposits on Tron chain don't work. Requires debugging. - -**Investigation Areas**: -- [executeTransaction.ts](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/lib/yieldxyz/executeTransaction.ts#L432-495) - `executeTronTransaction` function -- [constants.ts](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/lib/yieldxyz/constants.ts#L38) - Tron chain ID mapping - -**Debugging Steps**: -1. Verify `tronChainId` mapping to `YieldNetwork.Tron` -2. Check if `executeTronTransaction` handles all required fields -3. Verify Tron chain adapter is correctly initialized -4. Check for errors in transaction parsing/signing - -**Proposed Fix**: TBD after debugging - needs reproduction and console log analysis. - ---- - -### TODO 3: Fix Wrong Translation Chain on Approved Deposit Toast - -> [!NOTE] -> **Issue**: The toast notification for approved deposits shows wrong chain translation. - -**Investigation Areas**: -- [useYieldTransactionFlow.ts](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/hooks/useYieldTransactionFlow.ts#L235-282) - `dispatchNotification` function -- Translation keys in `main.json` for yield notifications - -**Current Code Analysis**: -```typescript -// Line 275 in useYieldTransactionFlow.ts -message: formatYieldTxTitle(tx.title || 'Transaction', assetSymbol), -``` - -**Proposed Fix**: -1. Review the notification message construction in `dispatchNotification` -2. Ensure the correct chain name is passed to the translation -3. Check if `yieldChainId` is being properly resolved to chain name -4. Add chain name to the toast message if missing - ---- - -### TODO 4: Fix Transparent Icon on Yield Page - -> [!NOTE] -> **Issue**: Some icons appear transparent/invisible on yield pages. - -**Investigation Areas**: -- [YieldDetail.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/YieldDetail.tsx#L100-127) - `heroIcon` element -- [YieldItem.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldItem.tsx#L130-177) - `iconElement` rendering -- CSS/styling for icon backgrounds - -**Proposed Fix**: -1. Add background color to icon containers for better visibility -2. Check if icons with transparency need a white/dark background based on color mode -3. Review `AssetIcon` component for proper fallback handling - ---- - -### TODO 5: Show Pending Deposits After Deposit (Solana) - -> [!IMPORTANT] -> **Issue**: Balance isn't updated after a deposit. For Solana, deposits are pending by nature - should show pending deposit status. - -**Investigation Areas**: -- [useAllYieldBalances.ts](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/react-queries/queries/yieldxyz/useAllYieldBalances.ts) - Balance fetching and normalization -- [YieldPositionCard.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldPositionCard.tsx) - Position display -- `YieldBalanceType` enum - includes `Entering` type for pending deposits - -**Current Code Analysis**: -The balance types already include: -- `Active` - Active staked balance -- `Entering` - Pending/entering deposits -- `Exiting` - Pending withdrawals -- `Claimable` - Rewards ready to claim - -**Proposed Fix**: -1. Ensure `Entering` balance type is properly displayed in UI -2. Add visual indicator for pending deposits in `YieldPositionCard` -3. Possibly add polling or refetch after deposit completion -4. Show "Pending" badge for `hasEntering` positions (already tracked in `ValidatorSummary`) - ---- - -### TODO 6: Fix Network Filter + Asset Selection Opening Wrong Yield - -> [!IMPORTANT] -> **Issue**: Selecting Ethereum as network, clicking on USDC, selecting the AAVE yield opens the BASE yield instead. - -**Investigation Areas**: -- [YieldItem.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldItem.tsx#L118-128) - `handleClick` navigation -- [YieldAssetDetails.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/YieldAssetDetails.tsx) - Asset yields filtering -- [useYieldFilters.ts](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/hooks/useYieldFilters.ts) - Filter state via URL params - -**Root Cause Hypothesis**: -When navigating from YieldsList → YieldAssetDetails → YieldDetail, the network filter state may not be properly passed through, or the filtered yields in YieldAssetDetails may not respect the network filter. - -**Proposed Fix**: -1. Pass network filter through URL params when navigating to asset details -2. In `YieldAssetDetails`, respect the `network` search param when filtering yields -3. When clicking a specific yield, ensure the correct yield ID is used based on filtered context -4. Consider storing selected yield context in navigation state - ---- - -## UX Improvements - -### TODO 7: Improve "You Could Earn More" Messaging - -> [!NOTE] -> **Issue**: "You could earn up to X% on your balance" messaging seems weird. Need to emphasize actual annual earnings. - -**Investigation Areas**: -- [YieldOpportunityCard.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldOpportunityCard.tsx#L25) - `earnUpToText` usage -- Translation key: `yieldXYZ.earnUpTo` = "You could earn up to %{apy}% on your balance" - -**Current Code**: -```typescript -const earnUpToText = useMemo(() => translate('yieldXYZ.earnUpTo', { apy }), [translate, apy]) -``` - -**Proposed Changes**: -1. Change messaging to show estimated annual earnings in $ amount -2. Calculate: `userBalance * APY = estimatedYearlyEarnings` -3. New message format: "Earn ~$X per year" or "Your $X balance could earn ~$Y/year at X% APY" -4. Update translation key and component to accept balance amount - ---- - -### TODO 8: Review Yield Opportunities in Account Page - -> [!NOTE] -> **Issue**: Review design and layout of yield opportunities section in account page. - -**Investigation Areas**: -- [AccountToken.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Accounts/AccountToken/AccountToken.tsx#L71) - Integration of `YieldAssetSection` -- [YieldAssetSection.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldAssetSection.tsx) - Section component -- [YieldOpportunityCard.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldOpportunityCard.tsx) - Card layout - -**Current Layout**: -- Section appears after `EarnOpportunities` in account page -- Shows `YieldActivePositions` if positions exist -- Shows `YieldOpportunityCard` for best yield if no positions - -**Design Review Points**: -1. Consistency with other account page sections -2. Card styling matches design system -3. Loading states are smooth -4. Empty states are handled gracefully - ---- - -## Design/Polish - -### TODO 9: Align Design with ShapeShift Design System - -> [!WARNING] -> **Issue**: Current design feels "very AI" - needs to follow ShapeShift's established design patterns. - -**Investigation Areas**: -- All yield component files in `src/pages/Yields/components/` -- Compare with established patterns in `src/components/` -- Color usage, spacing, typography - -**Design Audit Checklist**: -1. [ ] Color palette matches design system (avoid arbitrary colors) -2. [ ] Consistent use of Chakra UI theme tokens -3. [ ] Typography follows established patterns -4. [ ] Spacing/padding consistent with other pages -5. [ ] Dark/light mode support complete -6. [ ] Loading states match app patterns -7. [ ] Error states match app patterns -8. [ ] Empty states match app patterns -9. [ ] Animations/transitions smooth and purposeful - -**Files to Review**: -- [YieldsList.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldsList.tsx) -- [YieldItem.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldItem.tsx) -- [YieldDetail.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/YieldDetail.tsx) -- [YieldEnterExit.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldEnterExit.tsx) -- [YieldActionModal.tsx](file:///Users/alexandre.gomes/Sites/shapeshiftWebClone/src/pages/Yields/components/YieldActionModal.tsx) - ---- - -### TODO 10: Improve Marketing Content/Messaging - -> [!NOTE] -> **Issue**: Improve messaging and user-facing content throughout yield features. - -**Investigation Areas**: -- Translation keys in `main.json` under `yieldXYZ` namespace -- Component text that may be hardcoded -- Call-to-action buttons -- Descriptions and help text - -**Content Review Points**: -1. Clear value proposition for users -2. Simple, non-technical language where possible -3. Consistent terminology (yield vs staking vs earning) -4. Helpful tooltips for complex concepts -5. Error messages are actionable - ---- - -## Summary Table - -| # | Type | Issue | Priority | Complexity | -|---|------|-------|----------|------------| -| 1 | Bug | Network filtering hides networks | High | Low | -| 2 | Bug | Tron deposit not working | High | Medium | -| 3 | Bug | Wrong toast translation | Medium | Low | -| 4 | Bug | Transparent icon | Low | Low | -| 5 | Bug | Balance not updated (pending) | Medium | Medium | -| 6 | Bug | Wrong yield opens | High | Medium | -| 7 | UX | Earn messaging improvement | Low | Low | -| 8 | UX | Account page layout review | Low | Medium | -| 9 | Design | Design system alignment | Medium | High | -| 10 | Design | Marketing content | Low | Medium | - ---- - -## Execution Order Recommendation - -1. **High priority bugs first**: TODOs 1, 2, 6 -2. **Medium priority bugs**: TODOs 3, 5 -3. **Low priority bugs**: TODO 4 -4. **UX improvements**: TODOs 7, 8 -5. **Design/polish**: TODOs 9, 10 - ---- - -*Generated from issue analysis on 2026-01-11* diff --git a/src/pages/Yields/hooks/useYieldTransactionFlow.ts b/src/pages/Yields/hooks/useYieldTransactionFlow.ts index 1207315817c..200350d7a6a 100644 --- a/src/pages/Yields/hooks/useYieldTransactionFlow.ts +++ b/src/pages/Yields/hooks/useYieldTransactionFlow.ts @@ -453,13 +453,14 @@ export const useYieldTransactionFlow = ({ 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) {