From d265d3b19a6c489b29ac63b0e3c8ad5b3430e859 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:47:51 +0100 Subject: [PATCH 01/11] fix: improve yield UI terminology, button states, and add Get Asset feature - Replace generic "Exit" terminology with type-specific labels: - "Unstake" for staking yields (native-staking, pooled-staking, etc.) - "Withdraw" for vault/lending yields - Add proper disabled state to exit button with tooltip explaining why (no active position or no withdrawable amount) - Show position card even without connected wallet with "Connect Wallet" button - Display zeroed values maintaining consistent layout - Add "Get {TOKEN}" button in Available to Deposit section when user has no balance - Navigates to swapper with the token pre-selected as buy asset - Add new i18n keys for type-specific success messages and loading states Co-Authored-By: Claude Opus 4.5 --- src/assets/translations/en/main.json | 43 +++- .../Yields/components/YieldActionModal.tsx | 81 +++++--- .../components/YieldAvailableToDeposit.tsx | 172 ++++++++++++++++ .../Yields/components/YieldPositionCard.tsx | 186 ++++++++++++------ src/pages/Yields/components/YieldSuccess.tsx | 17 +- 5 files changed, 410 insertions(+), 89 deletions(-) create mode 100644 src/pages/Yields/components/YieldAvailableToDeposit.tsx diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index a926e3a4742..466091b9712 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -180,6 +180,7 @@ "featureDisabled": "This feature is temporarily disabled.", "yes": "Yes", "activeAccount": "Active Account", + "selectAccount": "Select Account", "update": "Update", "apy": "APY", "installed": "Installed", @@ -2674,6 +2675,9 @@ "enter": "Enter", "exit": "Exit", "enterAsset": "Enter %{asset}", + "actions": { + "restake": "Restake" + }, "yield": "Yield", "apy": "APY", "apr": "APR", @@ -2690,6 +2694,8 @@ "gasToken": "Gas Token", "entering": "Entering...", "exiting": "Exiting...", + "unstakingLoading": "Unstaking...", + "withdrawing": "Withdrawing...", "unstaking": "Unstaking", "availableDate": "available %{date}", "withdrawable": "Withdrawable", @@ -2713,6 +2719,7 @@ "preferred": "Preferred", "pending": "Pending", "ready": "Ready", + "bestReturn": "Best Return", "highestApy": "Highest APY", "lowestApy": "Lowest APY", "highestTvl": "Highest TVL", @@ -2769,10 +2776,13 @@ "recommendedForYou": "Recommended for you", "earn": "Earn", "myBalance": "My Balance", + "balanceByAccount": "Balance by Account", "providers": "Providers", "successEnter": "You successfully entered %{amount} %{symbol}", "successExit": "You successfully exited %{amount} %{symbol}", "successClaim": "You successfully claimed %{amount} %{symbol}", + "successUnstaked": "You successfully unstaked %{amount} %{symbol}", + "successWithdrawn": "You successfully withdrew %{amount} %{symbol}", "viewPosition": "View Position", "via": "via", "resetAllowance": "Reset Allowance", @@ -2799,7 +2809,38 @@ "initiateFailedDescription": "Failed to initiate transaction sequence.", "quoteFailedTitle": "Quote failed", "quoteFailedDescription": "Unable to get a quote for this transaction. Please try again." - } + }, + "underMaintenance": "Under Maintenance", + "underMaintenanceDescription": "This yield opportunity is currently under maintenance. Deposits may be unavailable.", + "deprecated": "Deprecated", + "deprecatedDescription": "This yield opportunity has been deprecated and may be discontinued soon.", + "learnMore": "Learn more", + "noAvailableYields": "No yield opportunities available for your assets", + "connectWalletAvailable": "Connect a wallet to see yields available for your assets", + "aboutProvider": "About %{provider}", + "visitWebsite": "Visit Website", + "providerDescriptions": { + "morpho": "Morpho is a money market and vault infrastructure protocol, multiply audited by top-tier security firms, live since 2022, with $2.5M in bug bounty incentives.", + "morpho-aave": "Morpho is a money market and vault infrastructure protocol, multiply audited by top-tier security firms, live since 2022, with $2.5M in bug bounty incentives.", + "morpho-compound": "Morpho is a money market and vault infrastructure protocol, multiply audited by top-tier security firms, live since 2022, with $2.5M in bug bounty incentives.", + "lido": "Lido is a liquid staking protocol that lets users stake ETH while keeping liquidity via stETH. Multiply audited by top-tier security firms, live since 2020, with $2M in bug bounty incentives.", + "aave": "Aave is a multi-chain lending marketplace enabling users to lend, borrow, and build advanced strategies. Multiply audited by top-tier security firms, live since 2017, with $1M in bug bounty incentives.", + "compound": "Compound is a foundational DeFi money market with algorithmic interest rates. Multiply audited by top-tier security firms, live since 2018, with $1M in bug bounty incentives.", + "kamino": "Kamino is a Solana DeFi suite unifying lending, liquidity, and leverage into one platform. Runs an Immunefi program with up to $1.5M maximum bounty.", + "fluid": "Fluid is a liquidity layer built by the Instadapp team, connecting lending, DEX, borrowing, and stablecoin markets into one efficient system. Multiply audited by top-tier security firms, live since 2024, with $0.5M in bug bounty incentives.", + "venus": "Venus is a lending and borrowing protocol focused on BNB Chain. Emphasizes security through third-party audits and an ongoing bug bounty program.", + "gearbox": "Gearbox is a composable leverage protocol enabling credit accounts that plug into DeFi strategies. Multiply audited by top-tier security firms, live since 2021, with $0.2M in bug bounty incentives." + }, + "otherYields": "Other %{symbol} Yields", + "availableToDeposit": "Available to Deposit", + "availableToDepositTooltip": "This is the amount of %{symbol} in your wallet that you can deposit into this yield opportunity.", + "potentialEarningsAmount": "%{amount}/yr at %{apy}% APY", + "depositNow": "Deposit Now", + "getAsset": "Get %{symbol}", + "strategyInfo": "Strategy Info", + "overview": "Overview", + "noActivePosition": "You don't have any active position", + "noWithdrawableAmount": "No withdrawable amount" }, "earn": { "enterFrom": "Enter from", diff --git a/src/pages/Yields/components/YieldActionModal.tsx b/src/pages/Yields/components/YieldActionModal.tsx index 719933082fe..698bdcfe1da 100644 --- a/src/pages/Yields/components/YieldActionModal.tsx +++ b/src/pages/Yields/components/YieldActionModal.tsx @@ -16,7 +16,11 @@ import { SHAPESHIFT_VALIDATOR_NAME, } from '@/lib/yieldxyz/constants' import type { AugmentedYieldDto } from '@/lib/yieldxyz/types' -import { getTransactionButtonText } from '@/lib/yieldxyz/utils' +import { + getTransactionButtonText, + getYieldActionLabelKeys, + isStakingYieldType, +} from '@/lib/yieldxyz/utils' import { GradientApy } from '@/pages/Yields/components/GradientApy' import { TransactionStepsList } from '@/pages/Yields/components/TransactionStepsList' import { YieldAssetFlow } from '@/pages/Yields/components/YieldAssetFlow' @@ -58,6 +62,7 @@ export const YieldActionModal = memo(function YieldActionModal({ validatorName, validatorLogoURI, passthrough, + accountId, ...props }: YieldActionModalProps) { const translate = useTranslate() @@ -85,12 +90,22 @@ export const YieldActionModal = memo(function YieldActionModal({ validatorAddress, passthrough, manageActionType: props.manageActionType, - accountId: props.accountId, + accountId, }) + const isStaking = useMemo( + () => isStakingYieldType(yieldItem.mechanics.type), + [yieldItem.mechanics.type], + ) + + const actionLabelKeys = useMemo( + () => getYieldActionLabelKeys(yieldItem.mechanics.type), + [yieldItem.mechanics.type], + ) + const shouldFetchValidators = useMemo( - () => yieldItem.mechanics.type === 'staking' && yieldItem.mechanics.requiresValidatorSelection, - [yieldItem.mechanics.type, yieldItem.mechanics.requiresValidatorSelection], + () => isStaking && yieldItem.mechanics.requiresValidatorSelection, + [isStaking, yieldItem.mechanics.requiresValidatorSelection], ) const { data: validators } = useYieldValidators(yieldItem.id, shouldFetchValidators) @@ -105,7 +120,7 @@ export const YieldActionModal = memo(function YieldActionModal({ ) const vaultMetadata = useMemo(() => { - if (yieldItem.mechanics.type === 'staking' && validatorAddress) { + if (isStaking && validatorAddress) { const validator = validators?.find(v => v.address === validatorAddress) if (validator) return { name: validator.name, logoURI: validator.logoURI } if (validatorAddress === SHAPESHIFT_COSMOS_VALIDATOR_ADDRESS) { @@ -116,7 +131,15 @@ export const YieldActionModal = memo(function YieldActionModal({ const provider = providers?.[yieldItem.providerId] if (provider) return { name: provider.name, logoURI: provider.logoURI } return { name: 'Vault', logoURI: yieldItem.metadata.logoURI } - }, [yieldItem, validatorAddress, validatorName, validatorLogoURI, validators, providers]) + }, [ + isStaking, + yieldItem, + validatorAddress, + validatorName, + validatorLogoURI, + validators, + providers, + ]) const chainId = useMemo(() => yieldItem.chainId ?? '', [yieldItem.chainId]) const feeAsset = useAppSelector(state => selectFeeAssetByChainId(state, chainId)) @@ -151,14 +174,9 @@ export const YieldActionModal = memo(function YieldActionModal({ [amount, yieldItem.rewardRate.total, marketData?.price], ) - const isStaking = useMemo( - () => yieldItem.mechanics.type === 'staking', - [yieldItem.mechanics.type], - ) - const showValidatorRow = useMemo( - () => isStaking && vaultMetadata.name !== 'Vault', - [isStaking, vaultMetadata.name], + () => isStaking && Boolean(validatorAddress), + [isStaking, validatorAddress], ) const isButtonDisabled = useMemo( @@ -177,9 +195,11 @@ export const YieldActionModal = memo(function YieldActionModal({ return transactionSteps[activeStepIndex].loadingMessage } if (action === 'enter') return translate('yieldXYZ.entering') - if (action === 'exit') return translate('yieldXYZ.exiting') + if (action === 'exit') { + return translate(isStaking ? 'yieldXYZ.unstakingLoading' : 'yieldXYZ.withdrawing') + } return translate('common.claiming') - }, [isQuoteLoading, action, translate, activeStepIndex, transactionSteps]) + }, [isQuoteLoading, action, translate, activeStepIndex, transactionSteps, isStaking]) const buttonText = useMemo(() => { // Use the current step's type/title for a clean button label (e.g., "Enter", "Exit", "Approve") @@ -197,16 +217,27 @@ export const YieldActionModal = memo(function YieldActionModal({ return getTransactionButtonText(firstCreatedTx.type, firstCreatedTx.title) } // Fallback to action-based text - if (action === 'enter') return translate('yieldXYZ.enter') - if (action === 'exit') return translate('yieldXYZ.exit') + if (action === 'enter') return translate(actionLabelKeys.enter) + if (action === 'exit') return translate(actionLabelKeys.exit) return translate('common.claim') - }, [action, translate, activeStepIndex, transactionSteps, quoteData, isUsdtResetRequired]) + }, [ + action, + translate, + activeStepIndex, + transactionSteps, + quoteData, + isUsdtResetRequired, + actionLabelKeys, + ]) const modalHeading = useMemo(() => { if (action === 'enter') return translate('yieldXYZ.enterSymbol', { symbol: assetSymbol }) - if (action === 'exit') return translate('yieldXYZ.exitSymbol', { symbol: assetSymbol }) + if (action === 'exit') { + const exitKey = isStaking ? 'yieldXYZ.unstakeSymbol' : 'yieldXYZ.withdrawSymbol' + return translate(exitKey, { symbol: assetSymbol }) + } return translate('yieldXYZ.claimSymbol', { symbol: assetSymbol }) - }, [action, assetSymbol, translate]) + }, [action, assetSymbol, translate, isStaking]) const networkAvatarSrc = useMemo( () => feeAsset?.networkIcon ?? feeAsset?.icon, @@ -283,7 +314,7 @@ export const YieldActionModal = memo(function YieldActionModal({ )} - {!isStaking && ( + {!showValidatorRow && ( {translate('yieldXYZ.provider')} @@ -319,7 +350,6 @@ export const YieldActionModal = memo(function YieldActionModal({ estimatedEarningsAmount, estimatedEarningsFiat, showValidatorRow, - isStaking, vaultMetadata.logoURI, vaultMetadata.name, feeAsset, @@ -341,9 +371,10 @@ export const YieldActionModal = memo(function YieldActionModal({ const successMessageKey = useMemo(() => { if (action === 'enter') return 'successEnter' as const - if (action === 'exit') return 'successExit' as const + if (action === 'exit') + return isStaking ? ('successUnstaked' as const) : ('successWithdrawn' as const) return 'successClaim' as const - }, [action]) + }, [action, isStaking]) const successProviderInfo = useMemo( () => (vaultMetadata ? { name: vaultMetadata.name, logoURI: vaultMetadata.logoURI } : null), @@ -358,6 +389,7 @@ export const YieldActionModal = memo(function YieldActionModal({ providerInfo={successProviderInfo} transactionSteps={transactionSteps} yieldId={yieldItem.id} + accountId={accountId} onDone={handleClose} successMessageKey={successMessageKey} /> @@ -368,6 +400,7 @@ export const YieldActionModal = memo(function YieldActionModal({ successProviderInfo, transactionSteps, yieldItem.id, + accountId, handleClose, successMessageKey, ], diff --git a/src/pages/Yields/components/YieldAvailableToDeposit.tsx b/src/pages/Yields/components/YieldAvailableToDeposit.tsx new file mode 100644 index 00000000000..80e3ca3a70f --- /dev/null +++ b/src/pages/Yields/components/YieldAvailableToDeposit.tsx @@ -0,0 +1,172 @@ +import { InfoOutlineIcon } from '@chakra-ui/icons' +import { + Box, + Button, + Card, + CardBody, + Flex, + Heading, + HStack, + Text, + Tooltip, + VStack, +} from '@chakra-ui/react' +import { memo, useCallback, useMemo } from 'react' +import { useTranslate } from 'react-polyglot' +import { useNavigate } from 'react-router-dom' + +import { Amount } from '@/components/Amount/Amount' +import { bnOrZero } from '@/lib/bignumber/bignumber' +import type { AugmentedYieldDto } from '@/lib/yieldxyz/types' +import { selectPortfolioCryptoBalanceBaseUnitByFilter } from '@/state/slices/selectors' +import { useAppSelector } from '@/state/store' + +type YieldAvailableToDepositProps = { + yieldItem: AugmentedYieldDto + inputTokenMarketData: { price?: string } | undefined +} + +export const YieldAvailableToDeposit = memo( + ({ yieldItem, inputTokenMarketData }: YieldAvailableToDepositProps) => { + const translate = useTranslate() + const navigate = useNavigate() + + const inputToken = yieldItem.inputTokens[0] + const inputTokenAssetId = inputToken?.assetId ?? '' + const inputTokenPrecision = inputToken?.decimals + + const availableBalanceBaseUnit = useAppSelector(state => + selectPortfolioCryptoBalanceBaseUnitByFilter(state, { assetId: inputTokenAssetId }), + ) + + const availableBalance = useMemo( + () => + inputTokenPrecision + ? bnOrZero(availableBalanceBaseUnit).shiftedBy(-inputTokenPrecision) + : bnOrZero(0), + [availableBalanceBaseUnit, inputTokenPrecision], + ) + + const availableBalanceFiat = useMemo( + () => availableBalance.times(bnOrZero(inputTokenMarketData?.price)), + [availableBalance, inputTokenMarketData?.price], + ) + + const potentialYearlyEarningsFiat = useMemo( + () => availableBalanceFiat.times(yieldItem.rewardRate.total), + [availableBalanceFiat, yieldItem.rewardRate.total], + ) + + const hasAvailableBalance = availableBalance.gt(0) + + const handleGetAsset = useCallback(() => { + navigate(`/trade/${inputTokenAssetId}`) + }, [navigate, inputTokenAssetId]) + + if (!inputTokenPrecision) return null + + const tooltipLabel = translate('yieldXYZ.availableToDepositTooltip', { + symbol: yieldItem.token.symbol, + }) + + if (!hasAvailableBalance) { + return ( + + + + + + + {translate('yieldXYZ.availableToDeposit')} + + + + + + + + + + + + + + + + + + + + + ) + } + + return ( + + + + + + + {translate('yieldXYZ.availableToDeposit')} + + + + + + + + + + + + + + + + + {potentialYearlyEarningsFiat.gt(0) && ( + + + {translate('yieldXYZ.potentialEarnings')} + + + + )} + + + + ) + }, +) diff --git a/src/pages/Yields/components/YieldPositionCard.tsx b/src/pages/Yields/components/YieldPositionCard.tsx index 3595f47aea2..cc405580c2d 100644 --- a/src/pages/Yields/components/YieldPositionCard.tsx +++ b/src/pages/Yields/components/YieldPositionCard.tsx @@ -1,3 +1,4 @@ +import { ArrowDownIcon, ArrowUpIcon } from '@chakra-ui/icons' import { Alert, Badge, @@ -8,8 +9,10 @@ import { Divider, Flex, Heading, + HStack, Skeleton, Text, + Tooltip, VStack, } from '@chakra-ui/react' import { fromAccountId } from '@shapeshiftoss/caip' @@ -17,14 +20,17 @@ import dayjs from 'dayjs' import qs from 'qs' import { memo, useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' -import { useNavigate, useSearchParams } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { Amount } from '@/components/Amount/Amount' +import { Display } from '@/components/Display' +import { WalletActions } from '@/context/WalletProvider/actions' import { useBrowserRouter } from '@/hooks/useBrowserRouter/useBrowserRouter' +import { useWallet } from '@/hooks/useWallet/useWallet' import { bnOrZero } from '@/lib/bignumber/bignumber' -import { DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID } from '@/lib/yieldxyz/constants' import type { AugmentedYieldDto } from '@/lib/yieldxyz/types' import { YieldBalanceType } from '@/lib/yieldxyz/types' +import { getYieldActionLabelKeys } from '@/lib/yieldxyz/utils' import { useYieldAccount } from '@/pages/Yields/YieldAccountContext' import type { AggregatedBalance, @@ -37,27 +43,40 @@ import { } from '@/state/slices/selectors' import { useAppSelector } from '@/state/store' +const enterIcon = +const exitIcon = + +const loadingState = ( + + + + +) + type YieldPositionCardProps = { yieldItem: AugmentedYieldDto balances: NormalizedYieldBalances | undefined isBalancesLoading: boolean + selectedValidatorAddress: string | undefined } export const YieldPositionCard = memo( - ({ yieldItem, balances, isBalancesLoading }: YieldPositionCardProps) => { + ({ + yieldItem, + balances, + isBalancesLoading, + selectedValidatorAddress, + }: YieldPositionCardProps) => { const translate = useTranslate() const navigate = useNavigate() const { location } = useBrowserRouter() - const [searchParams] = useSearchParams() - const validatorParam = searchParams.get('validator') + const { dispatch: walletDispatch } = useWallet() const { chainId } = yieldItem - const { accountNumber } = useYieldAccount() - - const defaultValidator = chainId ? DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID[chainId] : undefined - const selectedValidatorAddress = validatorParam || defaultValidator + const { accountId: contextAccountId, accountNumber } = useYieldAccount() const accountId = useAppSelector(state => { + if (contextAccountId) return contextAccountId if (!chainId) return undefined const accountIdsByNumberAndChain = selectAccountIdByAccountNumberAndChainId(state) return accountIdsByNumberAndChain[accountNumber]?.[chainId] @@ -102,10 +121,7 @@ export const YieldPositionCard = memo( ) }, []) - const hasEntering = useMemo( - () => enteringBalance && bnOrZero(enteringBalance.aggregatedAmount).gt(0), - [enteringBalance], - ) + const hasEntering = Boolean(enteringBalance && bnOrZero(enteringBalance.aggregatedAmount).gt(0)) const exitingEntries = useMemo(() => { if (!balances?.raw) return [] @@ -117,12 +133,17 @@ export const YieldPositionCard = memo( }) }, [balances?.raw, selectedValidatorAddress]) - const hasExiting = useMemo(() => exitingEntries.length > 0, [exitingEntries]) - const hasWithdrawable = useMemo( - () => withdrawableBalance && bnOrZero(withdrawableBalance.aggregatedAmount).gt(0), - [withdrawableBalance], + const hasExiting = exitingEntries.length > 0 + const hasWithdrawable = Boolean( + withdrawableBalance && bnOrZero(withdrawableBalance.aggregatedAmount).gt(0), + ) + const hasClaimable = Boolean( + claimableBalance && bnOrZero(claimableBalance.aggregatedAmount).gt(0), ) - const hasClaimable = useMemo(() => Boolean(claimableBalance), [claimableBalance]) + + const hasActive = Boolean(activeBalance && bnOrZero(activeBalance.aggregatedAmount).gt(0)) + + const canExit = hasActive || hasWithdrawable const totalValueUsd = useMemo( () => @@ -147,7 +168,7 @@ export const YieldPositionCard = memo( [activeBalance, enteringBalance, exitingBalance, withdrawableBalance], ) - const hasAnyPosition = useMemo(() => totalAmount.gt(0), [totalAmount]) + const hasAnyPosition = totalAmount.gt(0) const { data: validators } = useYieldValidators(yieldItem.id) @@ -161,47 +182,48 @@ export const YieldPositionCard = memo( return foundInBalances?.validator?.name }, [validators, selectedValidatorAddress, balances]) - const headingText = useMemo( - () => - selectedValidatorName - ? translate('yieldXYZ.myValidatorPosition', { validator: selectedValidatorName }) - : translate('yieldXYZ.myPosition'), - [selectedValidatorName, translate], - ) + const headingText = selectedValidatorName + ? translate('yieldXYZ.myValidatorPosition', { validator: selectedValidatorName }) + : translate('yieldXYZ.myPosition') - const addressBadgeText = useMemo( - () => (address ? `${address.slice(0, 4)}...${address.slice(-4)}` : ''), - [address], - ) + const addressBadgeText = address ? `${address.slice(0, 4)}...${address.slice(-4)}` : '' - const totalAmountFixed = useMemo(() => totalAmount.toFixed(), [totalAmount]) + const totalAmountFixed = totalAmount.toFixed() - const handleClaimClick = useCallback(() => { - navigate({ - pathname: location.pathname, - search: qs.stringify({ - action: 'claim', - modal: 'yield', - ...(selectedValidatorAddress ? { validator: selectedValidatorAddress } : {}), - }), - }) - }, [navigate, location.pathname, selectedValidatorAddress]) - - const showPendingActions = useMemo( - () => hasEntering || hasExiting || hasWithdrawable || hasClaimable, - [hasEntering, hasExiting, hasWithdrawable, hasClaimable], + const navigateToAction = useCallback( + (action: 'claim' | 'enter' | 'exit') => { + navigate({ + pathname: location.pathname, + search: qs.stringify({ + action, + modal: 'yield', + ...(selectedValidatorAddress ? { validator: selectedValidatorAddress } : {}), + }), + }) + }, + [navigate, location.pathname, selectedValidatorAddress], ) - const loadingState = useMemo( - () => ( - - - - - ), - [], + const handleClaimClick = useCallback(() => navigateToAction('claim'), [navigateToAction]) + const handleEnter = useCallback(() => navigateToAction('enter'), [navigateToAction]) + const handleExit = useCallback(() => navigateToAction('exit'), [navigateToAction]) + const handleConnectWallet = useCallback( + () => walletDispatch({ type: WalletActions.SET_WALLET_MODAL, payload: true }), + [walletDispatch], ) + const actionLabelKeys = getYieldActionLabelKeys(yieldItem.mechanics.type) + const enterLabel = translate(actionLabelKeys.enter) + const exitLabel = translate(actionLabelKeys.exit) + + const exitDisabledReason = useMemo(() => { + if (!hasAnyPosition) return translate('yieldXYZ.noActivePosition') + if (!canExit) return translate('yieldXYZ.noWithdrawableAmount') + return undefined + }, [hasAnyPosition, canExit, translate]) + + const showPendingActions = hasEntering || hasExiting || hasWithdrawable || hasClaimable + const enteringSection = useMemo(() => { if (!hasEntering) return null return ( @@ -356,9 +378,7 @@ export const YieldPositionCard = memo( claimableSection, ]) - if (!accountId) return null - - if (isBalancesLoading) { + if (accountId && isBalancesLoading) { return ( @@ -379,8 +399,6 @@ export const YieldPositionCard = memo( ) } - if (!hasAnyPosition && !showPendingActions) return null - return ( @@ -402,17 +420,63 @@ export const YieldPositionCard = memo( {translate('yieldXYZ.totalValue')} - + - {pendingActionsSection} + {accountId && pendingActionsSection} + + {accountId ? ( + + + + + + + ) : ( + + )} + diff --git a/src/pages/Yields/components/YieldSuccess.tsx b/src/pages/Yields/components/YieldSuccess.tsx index 588d1469551..7812afc8a67 100644 --- a/src/pages/Yields/components/YieldSuccess.tsx +++ b/src/pages/Yields/components/YieldSuccess.tsx @@ -1,4 +1,5 @@ import { Avatar, Box, Button, Flex, Heading, Icon, Text, VStack } from '@chakra-ui/react' +import type { AccountId } from '@shapeshiftoss/caip' import { memo, useCallback, useEffect, useMemo } from 'react' import ReactCanvasConfetti from 'react-canvas-confetti' import { FaCheck } from 'react-icons/fa' @@ -20,9 +21,15 @@ type YieldSuccessProps = { providerInfo: ProviderInfo | null transactionSteps: TransactionStep[] yieldId?: string + accountId?: AccountId onDone: () => void showConfetti?: boolean - successMessageKey?: 'successEnter' | 'successExit' | 'successClaim' + successMessageKey?: + | 'successEnter' + | 'successExit' + | 'successClaim' + | 'successUnstaked' + | 'successWithdrawn' } export const YieldSuccess = memo( @@ -32,6 +39,7 @@ export const YieldSuccess = memo( providerInfo, transactionSteps, yieldId, + accountId, onDone, showConfetti = true, successMessageKey = 'successEnter', @@ -46,8 +54,11 @@ export const YieldSuccess = memo( const handleViewPosition = useCallback(() => { if (!yieldId) return - navigate(`/yields/${yieldId}`) - }, [yieldId, navigate]) + const params = new URLSearchParams() + if (accountId) params.set('accountId', accountId) + const queryString = params.toString() + navigate(queryString ? `/yields/${yieldId}?${queryString}` : `/yields/${yieldId}`) + }, [yieldId, accountId, navigate]) const providerPillProps = useMemo( () => From 8a5511691a85ac9265b990206dbe5d97363cc1a7 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 19 Jan 2026 19:05:19 +0100 Subject: [PATCH 02/11] feat(yields): add "Get Asset" button to redirect users to trade page - Show "Get {TOKEN}" button when user has no balance for yield input token - Only display when wallet is connected - Routes to /trade/{assetId} with buy asset pre-selected - Store swapper modal implementation as .diffs for future use Co-Authored-By: Claude Opus 4.5 --- .diffs/swapper-modal-component.diff | 154 ++++++++++++++++++ .diffs/swapper-modal-yield.diff | 145 +++++++++++++++++ .../components/YieldAvailableToDeposit.tsx | 74 ++++++++- 3 files changed, 369 insertions(+), 4 deletions(-) create mode 100644 .diffs/swapper-modal-component.diff create mode 100644 .diffs/swapper-modal-yield.diff diff --git a/.diffs/swapper-modal-component.diff b/.diffs/swapper-modal-component.diff new file mode 100644 index 00000000000..5f52e72da3e --- /dev/null +++ b/.diffs/swapper-modal-component.diff @@ -0,0 +1,154 @@ +diff --git a/src/components/SwapperModal/SwapperModal.tsx b/src/components/SwapperModal/SwapperModal.tsx +new file mode 100644 +index 0000000000..00735dbe0f +--- /dev/null ++++ b/src/components/SwapperModal/SwapperModal.tsx +@@ -0,0 +1,71 @@ ++import type { AssetId } from '@shapeshiftoss/caip' ++import { memo, useCallback } from 'react' ++import { MemoryRouter } from 'react-router-dom' ++ ++import { SwapperModalContent } from './SwapperModalContent' ++ ++import { Dialog } from '@/components/Modal/components/Dialog' ++import { DialogBody } from '@/components/Modal/components/DialogBody' ++import { DialogCloseButton } from '@/components/Modal/components/DialogCloseButton' ++import { DialogHeader } from '@/components/Modal/components/DialogHeader' ++import { DialogTitle } from '@/components/Modal/components/DialogTitle' ++import { TradeRoutePaths } from '@/components/MultiHopTrade/types' ++import { tradeInput } from '@/state/slices/tradeInputSlice/tradeInputSlice' ++import { useAppDispatch } from '@/state/store' ++ ++type SwapperModalProps = { ++ isOpen: boolean ++ onClose: () => void ++ defaultBuyAssetId?: AssetId ++ defaultSellAssetId?: AssetId ++} ++ ++const initialEntries = [ ++ { pathname: TradeRoutePaths.Input }, ++ { pathname: TradeRoutePaths.Confirm }, ++ { pathname: TradeRoutePaths.VerifyAddresses }, ++ { pathname: TradeRoutePaths.QuoteList }, ++] ++ ++export const SwapperModal = memo( ++ ({ isOpen, onClose, defaultBuyAssetId, defaultSellAssetId }: SwapperModalProps) => { ++ const dispatch = useAppDispatch() ++ ++ const handleClose = useCallback(() => { ++ dispatch(tradeInput.actions.clear()) ++ onClose() ++ }, [dispatch, onClose]) ++ ++ return ( ++ ++ ++ {null} ++ ++ Trade ++ ++ ++ ++ ++ ++ ++ {isOpen && ( ++ ++ ++ ++ )} ++ ++ ++ ) ++ }, ++) +diff --git a/src/components/SwapperModal/SwapperModalContent.tsx b/src/components/SwapperModal/SwapperModalContent.tsx +new file mode 100644 +index 0000000000..3368f8ce53 +--- /dev/null ++++ b/src/components/SwapperModal/SwapperModalContent.tsx +@@ -0,0 +1,64 @@ ++import type { AssetId } from '@shapeshiftoss/caip' ++import { memo, useCallback, useLayoutEffect, useRef } from 'react' ++import { FormProvider, useForm } from 'react-hook-form' ++import { useNavigate } from 'react-router-dom' ++ ++import { TradingErrorBoundary } from '@/components/ErrorBoundary' ++import { StandaloneMultiHopTrade } from '@/components/MultiHopTrade/StandaloneMultiHopTrade' ++import { TradeInputTab, TradeRoutePaths } from '@/components/MultiHopTrade/types' ++import { selectAssetById } from '@/state/slices/assetsSlice/selectors' ++import { tradeInput } from '@/state/slices/tradeInputSlice/tradeInputSlice' ++import { useAppDispatch, useAppSelector } from '@/state/store' ++ ++type SwapperModalContentProps = { ++ defaultBuyAssetId?: AssetId ++ defaultSellAssetId?: AssetId ++} ++ ++export const SwapperModalContent = memo(function SwapperModalContent({ ++ defaultBuyAssetId, ++ defaultSellAssetId, ++}: SwapperModalContentProps) { ++ const methods = useForm({ mode: 'onChange' }) ++ const navigate = useNavigate() ++ const dispatch = useAppDispatch() ++ const hasInitialized = useRef(false) ++ ++ const defaultBuyAsset = useAppSelector(state => selectAssetById(state, defaultBuyAssetId ?? '')) ++ const defaultSellAsset = useAppSelector(state => selectAssetById(state, defaultSellAssetId ?? '')) ++ ++ useLayoutEffect(() => { ++ if (hasInitialized.current) return ++ hasInitialized.current = true ++ ++ dispatch(tradeInput.actions.clear()) ++ if (defaultBuyAsset) { ++ dispatch(tradeInput.actions.setBuyAsset(defaultBuyAsset)) ++ } ++ if (defaultSellAsset) { ++ dispatch(tradeInput.actions.setSellAsset(defaultSellAsset)) ++ } ++ }, [dispatch, defaultBuyAsset, defaultSellAsset]) ++ ++ const handleChangeTab = useCallback( ++ (newTab: TradeInputTab) => { ++ if (newTab === TradeInputTab.Trade) { ++ navigate(TradeRoutePaths.Input) ++ } ++ }, ++ [navigate], ++ ) ++ ++ return ( ++ ++ ++ ++ ++ ++ ) ++}) +diff --git a/src/components/SwapperModal/index.ts b/src/components/SwapperModal/index.ts +new file mode 100644 +index 0000000000..672e8d29e7 +--- /dev/null ++++ b/src/components/SwapperModal/index.ts +@@ -0,0 +1 @@ ++export { SwapperModal } from './SwapperModal' diff --git a/.diffs/swapper-modal-yield.diff b/.diffs/swapper-modal-yield.diff new file mode 100644 index 00000000000..aa508f0ac03 --- /dev/null +++ b/.diffs/swapper-modal-yield.diff @@ -0,0 +1,145 @@ +diff --git a/src/pages/Yields/components/YieldAvailableToDeposit.tsx b/src/pages/Yields/components/YieldAvailableToDeposit.tsx +index 80e3ca3a70..8ee4221f2f 100644 +--- a/src/pages/Yields/components/YieldAvailableToDeposit.tsx ++++ b/src/pages/Yields/components/YieldAvailableToDeposit.tsx +@@ -11,11 +11,12 @@ import { + Tooltip, + VStack, + } from '@chakra-ui/react' +-import { memo, useCallback, useMemo } from 'react' ++import { memo, useCallback, useMemo, useState } from 'react' + import { useTranslate } from 'react-polyglot' +-import { useNavigate } from 'react-router-dom' + + import { Amount } from '@/components/Amount/Amount' ++import { SwapperModal } from '@/components/SwapperModal' ++import { useWallet } from '@/hooks/useWallet/useWallet' + import { bnOrZero } from '@/lib/bignumber/bignumber' + import type { AugmentedYieldDto } from '@/lib/yieldxyz/types' + import { selectPortfolioCryptoBalanceBaseUnitByFilter } from '@/state/slices/selectors' +@@ -29,7 +30,9 @@ type YieldAvailableToDepositProps = { + export const YieldAvailableToDeposit = memo( + ({ yieldItem, inputTokenMarketData }: YieldAvailableToDepositProps) => { + const translate = useTranslate() +- const navigate = useNavigate() ++ const [isSwapperModalOpen, setIsSwapperModalOpen] = useState(false) ++ const { state: walletState } = useWallet() ++ const isConnected = useMemo(() => Boolean(walletState.walletInfo), [walletState.walletInfo]) + + const inputToken = yieldItem.inputTokens[0] + const inputTokenAssetId = inputToken?.assetId ?? '' +@@ -59,11 +62,10 @@ export const YieldAvailableToDeposit = memo( + + const hasAvailableBalance = availableBalance.gt(0) + +- const handleGetAsset = useCallback(() => { +- navigate(`/trade/${inputTokenAssetId}`) +- }, [navigate, inputTokenAssetId]) ++ const handleOpenSwapperModal = useCallback(() => setIsSwapperModalOpen(true), []) ++ const handleCloseSwapperModal = useCallback(() => setIsSwapperModalOpen(false), []) + +- if (!inputTokenPrecision) return null ++ if (!inputTokenPrecision || !isConnected) return null + + const tooltipLabel = translate('yieldXYZ.availableToDepositTooltip', { + symbol: yieldItem.token.symbol, +@@ -71,49 +73,56 @@ export const YieldAvailableToDeposit = memo( + + if (!hasAvailableBalance) { + return ( +- +- +- +- +- +- +- {translate('yieldXYZ.availableToDeposit')} +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- ++ <> ++ ++ ++ ++ ++ ++ ++ {translate('yieldXYZ.availableToDeposit')} ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ + ) + } + diff --git a/src/pages/Yields/components/YieldAvailableToDeposit.tsx b/src/pages/Yields/components/YieldAvailableToDeposit.tsx index cf998872508..da1f6af67eb 100644 --- a/src/pages/Yields/components/YieldAvailableToDeposit.tsx +++ b/src/pages/Yields/components/YieldAvailableToDeposit.tsx @@ -1,9 +1,22 @@ import { InfoOutlineIcon } from '@chakra-ui/icons' -import { Box, Card, CardBody, Flex, Heading, HStack, Text, Tooltip, VStack } from '@chakra-ui/react' -import { memo, useMemo } from 'react' +import { + Box, + Button, + Card, + CardBody, + Flex, + Heading, + HStack, + Text, + Tooltip, + VStack, +} from '@chakra-ui/react' +import { memo, useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' +import { useNavigate } from 'react-router-dom' import { Amount } from '@/components/Amount/Amount' +import { useWallet } from '@/hooks/useWallet/useWallet' import { bnOrZero } from '@/lib/bignumber/bignumber' import type { AugmentedYieldDto } from '@/lib/yieldxyz/types' import { selectPortfolioCryptoBalanceBaseUnitByFilter } from '@/state/slices/selectors' @@ -17,6 +30,9 @@ type YieldAvailableToDepositProps = { export const YieldAvailableToDeposit = memo( ({ yieldItem, inputTokenMarketData }: YieldAvailableToDepositProps) => { const translate = useTranslate() + const navigate = useNavigate() + const { state: walletState } = useWallet() + const isConnected = useMemo(() => Boolean(walletState.walletInfo), [walletState.walletInfo]) const inputToken = yieldItem.inputTokens[0] const inputTokenAssetId = inputToken?.assetId ?? '' @@ -46,13 +62,63 @@ export const YieldAvailableToDeposit = memo( const hasAvailableBalance = availableBalance.gt(0) - if (!inputTokenPrecision) return null + const handleGetAsset = useCallback(() => { + navigate(`/trade/${inputTokenAssetId}`) + }, [navigate, inputTokenAssetId]) + + if (!inputTokenPrecision || !isConnected) return null const tooltipLabel = translate('yieldXYZ.availableToDepositTooltip', { symbol: yieldItem.token.symbol, }) - if (!hasAvailableBalance) return null + if (!hasAvailableBalance) { + return ( + + + + + + + {translate('yieldXYZ.availableToDeposit')} + + + + + + + + + + + + + + + + + + + + + ) + } return ( From 578950d4051c6afdf74f478e0092b23873091bf9 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 19 Jan 2026 19:25:12 +0100 Subject: [PATCH 03/11] chore: remove diff files Co-Authored-By: Claude Opus 4.5 --- .diffs/swapper-modal-component.diff | 154 ---------------------------- .diffs/swapper-modal-yield.diff | 145 -------------------------- 2 files changed, 299 deletions(-) delete mode 100644 .diffs/swapper-modal-component.diff delete mode 100644 .diffs/swapper-modal-yield.diff diff --git a/.diffs/swapper-modal-component.diff b/.diffs/swapper-modal-component.diff deleted file mode 100644 index 5f52e72da3e..00000000000 --- a/.diffs/swapper-modal-component.diff +++ /dev/null @@ -1,154 +0,0 @@ -diff --git a/src/components/SwapperModal/SwapperModal.tsx b/src/components/SwapperModal/SwapperModal.tsx -new file mode 100644 -index 0000000000..00735dbe0f ---- /dev/null -+++ b/src/components/SwapperModal/SwapperModal.tsx -@@ -0,0 +1,71 @@ -+import type { AssetId } from '@shapeshiftoss/caip' -+import { memo, useCallback } from 'react' -+import { MemoryRouter } from 'react-router-dom' -+ -+import { SwapperModalContent } from './SwapperModalContent' -+ -+import { Dialog } from '@/components/Modal/components/Dialog' -+import { DialogBody } from '@/components/Modal/components/DialogBody' -+import { DialogCloseButton } from '@/components/Modal/components/DialogCloseButton' -+import { DialogHeader } from '@/components/Modal/components/DialogHeader' -+import { DialogTitle } from '@/components/Modal/components/DialogTitle' -+import { TradeRoutePaths } from '@/components/MultiHopTrade/types' -+import { tradeInput } from '@/state/slices/tradeInputSlice/tradeInputSlice' -+import { useAppDispatch } from '@/state/store' -+ -+type SwapperModalProps = { -+ isOpen: boolean -+ onClose: () => void -+ defaultBuyAssetId?: AssetId -+ defaultSellAssetId?: AssetId -+} -+ -+const initialEntries = [ -+ { pathname: TradeRoutePaths.Input }, -+ { pathname: TradeRoutePaths.Confirm }, -+ { pathname: TradeRoutePaths.VerifyAddresses }, -+ { pathname: TradeRoutePaths.QuoteList }, -+] -+ -+export const SwapperModal = memo( -+ ({ isOpen, onClose, defaultBuyAssetId, defaultSellAssetId }: SwapperModalProps) => { -+ const dispatch = useAppDispatch() -+ -+ const handleClose = useCallback(() => { -+ dispatch(tradeInput.actions.clear()) -+ onClose() -+ }, [dispatch, onClose]) -+ -+ return ( -+ -+ -+ {null} -+ -+ Trade -+ -+ -+ -+ -+ -+ -+ {isOpen && ( -+ -+ -+ -+ )} -+ -+ -+ ) -+ }, -+) -diff --git a/src/components/SwapperModal/SwapperModalContent.tsx b/src/components/SwapperModal/SwapperModalContent.tsx -new file mode 100644 -index 0000000000..3368f8ce53 ---- /dev/null -+++ b/src/components/SwapperModal/SwapperModalContent.tsx -@@ -0,0 +1,64 @@ -+import type { AssetId } from '@shapeshiftoss/caip' -+import { memo, useCallback, useLayoutEffect, useRef } from 'react' -+import { FormProvider, useForm } from 'react-hook-form' -+import { useNavigate } from 'react-router-dom' -+ -+import { TradingErrorBoundary } from '@/components/ErrorBoundary' -+import { StandaloneMultiHopTrade } from '@/components/MultiHopTrade/StandaloneMultiHopTrade' -+import { TradeInputTab, TradeRoutePaths } from '@/components/MultiHopTrade/types' -+import { selectAssetById } from '@/state/slices/assetsSlice/selectors' -+import { tradeInput } from '@/state/slices/tradeInputSlice/tradeInputSlice' -+import { useAppDispatch, useAppSelector } from '@/state/store' -+ -+type SwapperModalContentProps = { -+ defaultBuyAssetId?: AssetId -+ defaultSellAssetId?: AssetId -+} -+ -+export const SwapperModalContent = memo(function SwapperModalContent({ -+ defaultBuyAssetId, -+ defaultSellAssetId, -+}: SwapperModalContentProps) { -+ const methods = useForm({ mode: 'onChange' }) -+ const navigate = useNavigate() -+ const dispatch = useAppDispatch() -+ const hasInitialized = useRef(false) -+ -+ const defaultBuyAsset = useAppSelector(state => selectAssetById(state, defaultBuyAssetId ?? '')) -+ const defaultSellAsset = useAppSelector(state => selectAssetById(state, defaultSellAssetId ?? '')) -+ -+ useLayoutEffect(() => { -+ if (hasInitialized.current) return -+ hasInitialized.current = true -+ -+ dispatch(tradeInput.actions.clear()) -+ if (defaultBuyAsset) { -+ dispatch(tradeInput.actions.setBuyAsset(defaultBuyAsset)) -+ } -+ if (defaultSellAsset) { -+ dispatch(tradeInput.actions.setSellAsset(defaultSellAsset)) -+ } -+ }, [dispatch, defaultBuyAsset, defaultSellAsset]) -+ -+ const handleChangeTab = useCallback( -+ (newTab: TradeInputTab) => { -+ if (newTab === TradeInputTab.Trade) { -+ navigate(TradeRoutePaths.Input) -+ } -+ }, -+ [navigate], -+ ) -+ -+ return ( -+ -+ -+ -+ -+ -+ ) -+}) -diff --git a/src/components/SwapperModal/index.ts b/src/components/SwapperModal/index.ts -new file mode 100644 -index 0000000000..672e8d29e7 ---- /dev/null -+++ b/src/components/SwapperModal/index.ts -@@ -0,0 +1 @@ -+export { SwapperModal } from './SwapperModal' diff --git a/.diffs/swapper-modal-yield.diff b/.diffs/swapper-modal-yield.diff deleted file mode 100644 index aa508f0ac03..00000000000 --- a/.diffs/swapper-modal-yield.diff +++ /dev/null @@ -1,145 +0,0 @@ -diff --git a/src/pages/Yields/components/YieldAvailableToDeposit.tsx b/src/pages/Yields/components/YieldAvailableToDeposit.tsx -index 80e3ca3a70..8ee4221f2f 100644 ---- a/src/pages/Yields/components/YieldAvailableToDeposit.tsx -+++ b/src/pages/Yields/components/YieldAvailableToDeposit.tsx -@@ -11,11 +11,12 @@ import { - Tooltip, - VStack, - } from '@chakra-ui/react' --import { memo, useCallback, useMemo } from 'react' -+import { memo, useCallback, useMemo, useState } from 'react' - import { useTranslate } from 'react-polyglot' --import { useNavigate } from 'react-router-dom' - - import { Amount } from '@/components/Amount/Amount' -+import { SwapperModal } from '@/components/SwapperModal' -+import { useWallet } from '@/hooks/useWallet/useWallet' - import { bnOrZero } from '@/lib/bignumber/bignumber' - import type { AugmentedYieldDto } from '@/lib/yieldxyz/types' - import { selectPortfolioCryptoBalanceBaseUnitByFilter } from '@/state/slices/selectors' -@@ -29,7 +30,9 @@ type YieldAvailableToDepositProps = { - export const YieldAvailableToDeposit = memo( - ({ yieldItem, inputTokenMarketData }: YieldAvailableToDepositProps) => { - const translate = useTranslate() -- const navigate = useNavigate() -+ const [isSwapperModalOpen, setIsSwapperModalOpen] = useState(false) -+ const { state: walletState } = useWallet() -+ const isConnected = useMemo(() => Boolean(walletState.walletInfo), [walletState.walletInfo]) - - const inputToken = yieldItem.inputTokens[0] - const inputTokenAssetId = inputToken?.assetId ?? '' -@@ -59,11 +62,10 @@ export const YieldAvailableToDeposit = memo( - - const hasAvailableBalance = availableBalance.gt(0) - -- const handleGetAsset = useCallback(() => { -- navigate(`/trade/${inputTokenAssetId}`) -- }, [navigate, inputTokenAssetId]) -+ const handleOpenSwapperModal = useCallback(() => setIsSwapperModalOpen(true), []) -+ const handleCloseSwapperModal = useCallback(() => setIsSwapperModalOpen(false), []) - -- if (!inputTokenPrecision) return null -+ if (!inputTokenPrecision || !isConnected) return null - - const tooltipLabel = translate('yieldXYZ.availableToDepositTooltip', { - symbol: yieldItem.token.symbol, -@@ -71,49 +73,56 @@ export const YieldAvailableToDeposit = memo( - - if (!hasAvailableBalance) { - return ( -- -- -- -- -- -- -- {translate('yieldXYZ.availableToDeposit')} -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -+ <> -+ -+ -+ -+ -+ -+ -+ {translate('yieldXYZ.availableToDeposit')} -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ - ) - } - From dcceb873126a767f0aae9081f5675be6b7edf0a3 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 19 Jan 2026 19:39:30 +0100 Subject: [PATCH 04/11] fix: show My Position card with Connect Wallet when no wallet connected - YieldPositionCard: show zeroed position with Connect Wallet button instead of hiding - YieldAvailableToDeposit: properly handle Ledger read-only mode using established pattern Co-Authored-By: Claude Opus 4.5 --- .../components/YieldAvailableToDeposit.tsx | 19 +++++-- .../Yields/components/YieldPositionCard.tsx | 52 ++++++++++++++++++- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/src/pages/Yields/components/YieldAvailableToDeposit.tsx b/src/pages/Yields/components/YieldAvailableToDeposit.tsx index da1f6af67eb..63feca7b403 100644 --- a/src/pages/Yields/components/YieldAvailableToDeposit.tsx +++ b/src/pages/Yields/components/YieldAvailableToDeposit.tsx @@ -16,9 +16,12 @@ import { useTranslate } from 'react-polyglot' import { useNavigate } from 'react-router-dom' import { Amount } from '@/components/Amount/Amount' +import { KeyManager } from '@/context/WalletProvider/KeyManager' +import { useFeatureFlag } from '@/hooks/useFeatureFlag/useFeatureFlag' import { useWallet } from '@/hooks/useWallet/useWallet' import { bnOrZero } from '@/lib/bignumber/bignumber' import type { AugmentedYieldDto } from '@/lib/yieldxyz/types' +import { selectWalletType } from '@/state/slices/localWalletSlice/selectors' import { selectPortfolioCryptoBalanceBaseUnitByFilter } from '@/state/slices/selectors' import { useAppSelector } from '@/state/store' @@ -31,8 +34,18 @@ export const YieldAvailableToDeposit = memo( ({ yieldItem, inputTokenMarketData }: YieldAvailableToDepositProps) => { const translate = useTranslate() const navigate = useNavigate() - const { state: walletState } = useWallet() - const isConnected = useMemo(() => Boolean(walletState.walletInfo), [walletState.walletInfo]) + const { + state: { isConnected }, + } = useWallet() + const isLedgerReadOnlyEnabled = useFeatureFlag('LedgerReadOnly') + const walletType = useAppSelector(selectWalletType) + const isLedgerReadOnly = isLedgerReadOnlyEnabled && walletType === KeyManager.Ledger + + // Either wallet is physically connected, or it's a Ledger in read-only mode + const hasWallet = useMemo( + () => isConnected || isLedgerReadOnly, + [isConnected, isLedgerReadOnly], + ) const inputToken = yieldItem.inputTokens[0] const inputTokenAssetId = inputToken?.assetId ?? '' @@ -66,7 +79,7 @@ export const YieldAvailableToDeposit = memo( navigate(`/trade/${inputTokenAssetId}`) }, [navigate, inputTokenAssetId]) - if (!inputTokenPrecision || !isConnected) return null + if (!inputTokenPrecision || !hasWallet) return null const tooltipLabel = translate('yieldXYZ.availableToDepositTooltip', { symbol: yieldItem.token.symbol, diff --git a/src/pages/Yields/components/YieldPositionCard.tsx b/src/pages/Yields/components/YieldPositionCard.tsx index e468aae7977..e321fa421fa 100644 --- a/src/pages/Yields/components/YieldPositionCard.tsx +++ b/src/pages/Yields/components/YieldPositionCard.tsx @@ -23,7 +23,9 @@ import { useNavigate } from 'react-router-dom' import { Amount } from '@/components/Amount/Amount' import { Display } from '@/components/Display' +import { WalletActions } from '@/context/WalletProvider/actions' import { useBrowserRouter } from '@/hooks/useBrowserRouter/useBrowserRouter' +import { useWallet } from '@/hooks/useWallet/useWallet' import { bnOrZero } from '@/lib/bignumber/bignumber' import type { AugmentedYieldDto } from '@/lib/yieldxyz/types' import { YieldBalanceType } from '@/lib/yieldxyz/types' @@ -67,6 +69,12 @@ export const YieldPositionCard = memo( const translate = useTranslate() const navigate = useNavigate() const { location } = useBrowserRouter() + const { dispatch: walletDispatch } = useWallet() + + const handleConnectWallet = useCallback( + () => walletDispatch({ type: WalletActions.SET_WALLET_MODAL, payload: true }), + [walletDispatch], + ) const { chainId } = yieldItem const { accountId: contextAccountId, accountNumber } = useYieldAccount() @@ -360,7 +368,49 @@ export const YieldPositionCard = memo( claimableSection, ]) - if (!accountId) return null + if (!accountId) { + return ( + + + + + {translate('yieldXYZ.myPosition')} + + + + + + {translate('yieldXYZ.totalValue')} + + + + + + + + + + + + + ) + } if (isBalancesLoading) { return ( From cd8661d981d162b9dd8b1043c753f2540f5264ac Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 19 Jan 2026 19:40:31 +0100 Subject: [PATCH 05/11] fix: add missing yieldXYZ.getAsset translation Co-Authored-By: Claude Opus 4.5 --- src/assets/translations/en/main.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 1763f62f254..6e592a12bda 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -2830,6 +2830,7 @@ "otherYields": "Other %{symbol} Yields", "availableToDeposit": "Available to Deposit", "availableToDepositTooltip": "This is the amount of %{symbol} in your wallet that you can deposit into this yield opportunity.", + "getAsset": "Get %{symbol}", "potentialEarningsAmount": "%{amount}/yr at %{apy}% APY", "depositNow": "Deposit Now", "strategyInfo": "Strategy Info", From b1c7bf2dcad4f7d2b11e27970d2003633f6e6769 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 19 Jan 2026 19:41:27 +0100 Subject: [PATCH 06/11] docs: add i18n workflow note to CLAUDE.md Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CLAUDE.md b/CLAUDE.md index 1c415341010..f649e39d0d0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -96,6 +96,7 @@ - Add English copy to `src/assets/translations/en/main.json` (find appropriate section) - Ignore other language translation files - only update English - Use the translation hook: `useTranslate()` from `react-polyglot` +- **Both steps required**: Translations must be (1) added to `en/main.json` AND (2) consumed via `translate('key')` - missing either step results in untranslated strings showing raw keys ### Feature Flags - Feature flags are stored in Redux state under `preferences.featureFlags` From 9d13a593eeba62b4907e1e425604016ec0db5454 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 19 Jan 2026 19:43:07 +0100 Subject: [PATCH 07/11] fix: use useTradeNavigation for Get Asset button to properly set buy asset Co-Authored-By: Claude Opus 4.5 --- src/pages/Yields/components/YieldAvailableToDeposit.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/Yields/components/YieldAvailableToDeposit.tsx b/src/pages/Yields/components/YieldAvailableToDeposit.tsx index 63feca7b403..fe0b5675e47 100644 --- a/src/pages/Yields/components/YieldAvailableToDeposit.tsx +++ b/src/pages/Yields/components/YieldAvailableToDeposit.tsx @@ -13,9 +13,9 @@ import { } from '@chakra-ui/react' import { memo, useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' -import { useNavigate } from 'react-router-dom' import { Amount } from '@/components/Amount/Amount' +import { useTradeNavigation } from '@/components/MultiHopTrade/hooks/useTradeNavigation' import { KeyManager } from '@/context/WalletProvider/KeyManager' import { useFeatureFlag } from '@/hooks/useFeatureFlag/useFeatureFlag' import { useWallet } from '@/hooks/useWallet/useWallet' @@ -33,7 +33,7 @@ type YieldAvailableToDepositProps = { export const YieldAvailableToDeposit = memo( ({ yieldItem, inputTokenMarketData }: YieldAvailableToDepositProps) => { const translate = useTranslate() - const navigate = useNavigate() + const { navigateToTrade } = useTradeNavigation() const { state: { isConnected }, } = useWallet() @@ -76,8 +76,8 @@ export const YieldAvailableToDeposit = memo( const hasAvailableBalance = availableBalance.gt(0) const handleGetAsset = useCallback(() => { - navigate(`/trade/${inputTokenAssetId}`) - }, [navigate, inputTokenAssetId]) + navigateToTrade(inputTokenAssetId) + }, [navigateToTrade, inputTokenAssetId]) if (!inputTokenPrecision || !hasWallet) return null From a786901578b408938266696a788d5ce804f211b5 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 20 Jan 2026 11:11:47 +0100 Subject: [PATCH 08/11] feat(i18n): add missing yield success translations Add successUnstaked and successWithdrawn translation entries for the YieldSuccess component. Co-Authored-By: Claude Opus 4.5 --- src/assets/translations/en/main.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 0c447cf0c50..ed816d7fc62 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -2779,6 +2779,8 @@ "successEnter": "You successfully entered %{amount} %{symbol}", "successExit": "You successfully exited %{amount} %{symbol}", "successClaim": "You successfully claimed %{amount} %{symbol}", + "successUnstaked": "You successfully unstaked %{amount} %{symbol}", + "successWithdrawn": "You successfully withdrew %{amount} %{symbol}", "viewPosition": "View Position", "via": "via", "resetAllowance": "Reset Allowance", From becee13a91e65a0fae0f46ead254458d883b20b2 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:33:00 +0100 Subject: [PATCH 09/11] feat(yields): replace enter/exit terminology with yield-type-aware labels - Add YieldType union type with exhaustiveness checks for all yield type handling - Replace generic "enter/exit" UI labels with contextual terminology: - Staking yields: stake/unstake - Vault/lending yields: deposit/withdraw - Restaking: restake/unstake - Add utility functions: getYieldActionLabelKeys, getYieldLoadingStateKeys, getYieldHeadingKeys, getYieldPendingStatusKeys, getYieldMinAmountKey, getYieldSuccessMessageKey with exhaustiveness checks - Update YieldSuccess component with typed successMessageKey prop - Clean up unused translations and remove old enter/exit keys - Add comprehensive tests for new utility functions Co-Authored-By: Claude Opus 4.5 --- src/assets/translations/en/main.json | 46 ++-- .../components/Earn/EarnConfirm.tsx | 23 +- src/lib/yieldxyz/types.ts | 11 +- src/lib/yieldxyz/utils.test.ts | 78 ++++-- src/lib/yieldxyz/utils.ts | 249 ++++++++++++++---- .../Yields/components/YieldActionModal.tsx | 108 ++++---- .../Yields/components/YieldEnterModal.tsx | 48 +++- src/pages/Yields/components/YieldForm.tsx | 34 ++- .../Yields/components/YieldPositionCard.tsx | 11 +- src/pages/Yields/components/YieldSuccess.tsx | 11 +- .../Yields/hooks/useYieldTransactionFlow.ts | 66 +++-- 11 files changed, 468 insertions(+), 217 deletions(-) diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index ed816d7fc62..028252fcade 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -2672,9 +2672,6 @@ "yieldXYZ": { "pageTitle": "Yields", "pageSubtitle": "Discover and manage yield opportunities across multiple chains", - "enter": "Enter", - "exit": "Exit", - "enterAsset": "Enter %{asset}", "actions": { "restake": "Restake" }, @@ -2689,12 +2686,17 @@ "noYields": "No yield opportunities available", "connectWallet": "Connect a wallet to view yields", "stats": "Stats", - "minEnter": "Min Enter", + "minStake": "Min Stake", + "minDeposit": "Min Deposit", "rewardSchedule": "Reward Schedule", - "gasToken": "Gas Token", - "entering": "Entering...", - "exiting": "Exiting...", - "unstaking": "Unstaking", + "staking": "Staking...", + "unstaking": "Unstaking...", + "depositing": "Depositing...", + "withdrawing": "Withdrawing...", + "stakingPending": "Staking", + "unstakingPending": "Unstaking", + "depositingPending": "Depositing", + "withdrawingPending": "Withdrawing", "availableDate": "available %{date}", "withdrawable": "Withdrawable", "type": "Type", @@ -2707,13 +2709,9 @@ "maxApy": "Max APY", "nativeStaking": "Native Staking", "validator": "Validator", - "entered": "Entered", + "staked": "Staked", "claimable": "Claimable", "loadingQuote": "Loading Quote...", - "selectValidator": "Select Validator", - "allValidators": "All Validators", - "myValidators": "My Validators", - "noValidatorsFound": "No validators found", "preferred": "Preferred", "pending": "Pending", "ready": "Ready", @@ -2729,16 +2727,11 @@ "allTypes": "All Types", "showAll": "Show All", "searchValidator": "Search for validator", - "enterYourToken": "Enter your %{symbol} to start earning yield securely.", - "noActiveValidators": "You don't have any active validators yet.", "success": "Success!", "transactions": "Transactions", "currentApy": "Current APY", "estYearlyEarnings": "Est. Yearly Earnings", - "allPositions": "All Positions", "switch": "Switch", - "enterSymbol": "Enter %{symbol}", - "exitSymbol": "Exit %{symbol}", "claimSymbol": "Claim %{symbol}", "stakeSymbol": "Stake %{symbol}", "unstakeSymbol": "Unstake %{symbol}", @@ -2754,12 +2747,10 @@ "market": "market", "markets": "markets", "protocol": "protocol", - "protocols": "protocols", "chain": "chain", "chains": "chains", "reward": "Reward", "assetYields": "%{asset} Yields", - "opportunitiesAvailable": "%{count} opportunities available", "noYieldsMatchingFilters": "No yields found matching filters.", "activePositions": "Active Positions", "acrossPositions": "Across %{count} positions", @@ -2774,17 +2765,16 @@ "recommendedForYou": "Recommended for you", "earn": "Earn", "myBalance": "My Balance", - "balanceByAccount": "Balance by Account", "providers": "Providers", - "successEnter": "You successfully entered %{amount} %{symbol}", - "successExit": "You successfully exited %{amount} %{symbol}", - "successClaim": "You successfully claimed %{amount} %{symbol}", + "successStaked": "You successfully staked %{amount} %{symbol}", "successUnstaked": "You successfully unstaked %{amount} %{symbol}", + "successDeposited": "You successfully deposited %{amount} %{symbol}", "successWithdrawn": "You successfully withdrew %{amount} %{symbol}", + "successRestaked": "You successfully restaked %{amount} %{symbol}", + "successClaim": "You successfully claimed %{amount} %{symbol}", "viewPosition": "View Position", "via": "via", "resetAllowance": "Reset Allowance", - "transactionNumber": "Transaction %{number}", "loading": { "signInWallet": "Sign in Wallet", "waiting": "Waiting", @@ -2832,11 +2822,7 @@ "otherYields": "Other %{symbol} Yields", "availableToDeposit": "Available to Deposit", "availableToDepositTooltip": "This is the amount of %{symbol} in your wallet that you can deposit into this yield opportunity.", - "getAsset": "Get %{symbol}", - "potentialEarningsAmount": "%{amount}/yr at %{apy}% APY", - "depositNow": "Deposit Now", - "strategyInfo": "Strategy Info", - "overview": "Overview" + "getAsset": "Get %{symbol}" }, "earn": { "enterFrom": "Enter from", diff --git a/src/components/MultiHopTrade/components/Earn/EarnConfirm.tsx b/src/components/MultiHopTrade/components/Earn/EarnConfirm.tsx index e9cfbc29688..e82fb192ffa 100644 --- a/src/components/MultiHopTrade/components/Earn/EarnConfirm.tsx +++ b/src/components/MultiHopTrade/components/Earn/EarnConfirm.tsx @@ -9,7 +9,7 @@ import { EarnRoutePaths } from './types' import { Amount } from '@/components/Amount/Amount' import { bnOrZero } from '@/lib/bignumber/bignumber' import { DEFAULT_NATIVE_VALIDATOR_BY_CHAIN_ID } from '@/lib/yieldxyz/constants' -import { getTransactionButtonText } from '@/lib/yieldxyz/utils' +import { getTransactionButtonText, getYieldActionLabelKeys } from '@/lib/yieldxyz/utils' import { GradientApy } from '@/pages/Yields/components/GradientApy' import { TransactionStepsList } from '@/pages/Yields/components/TransactionStepsList' import { YieldAssetFlow } from '@/pages/Yields/components/YieldAssetFlow' @@ -157,14 +157,25 @@ export const EarnConfirm = memo(() => { return translate('yieldXYZ.resetAllowance') } // Before execution starts, use the first CREATED transaction from quoteData + const yieldType = selectedYield?.mechanics.type const firstCreatedTx = quoteData?.transactions?.find(tx => tx.status === 'CREATED') if (firstCreatedTx) { - return getTransactionButtonText(firstCreatedTx.type, firstCreatedTx.title) + return getTransactionButtonText(firstCreatedTx.type, firstCreatedTx.title, yieldType) } // Fallback states if (isLoading) return translate('common.loadingText') - return translate('yieldXYZ.enter') - }, [activeStepIndex, transactionSteps, isUsdtResetRequired, quoteData, isLoading, translate]) + if (!yieldType) return translate('common.deposit') + const actionLabelKeys = getYieldActionLabelKeys(yieldType) + return translate(actionLabelKeys.enter) + }, [ + activeStepIndex, + transactionSteps, + isUsdtResetRequired, + quoteData, + isLoading, + translate, + selectedYield?.mechanics.type, + ]) const providerInfo = useMemo(() => { if (selectedValidator) { @@ -297,9 +308,7 @@ export const EarnConfirm = memo(() => { {providerInfo && ( - {selectedValidator - ? translate('yieldXYZ.validator') - : translate('yieldXYZ.provider')} + {translate(selectedValidator ? 'yieldXYZ.validator' : 'yieldXYZ.provider')} diff --git a/src/lib/yieldxyz/types.ts b/src/lib/yieldxyz/types.ts index d4197dc0627..3f00a467e2c 100644 --- a/src/lib/yieldxyz/types.ts +++ b/src/lib/yieldxyz/types.ts @@ -30,6 +30,15 @@ export enum ActionIntent { Manage = 'manage', } +export type YieldType = + | 'staking' + | 'native-staking' + | 'pooled-staking' + | 'liquid-staking' + | 'restaking' + | 'vault' + | 'lending' + export enum ActionStatus { Canceled = 'CANCELED', Created = 'CREATED', @@ -230,7 +239,7 @@ export type YieldEntryLimits = { } export type YieldMechanics = { - type: string + type: YieldType requiresValidatorSelection: boolean rewardSchedule: string rewardClaiming: string diff --git a/src/lib/yieldxyz/utils.test.ts b/src/lib/yieldxyz/utils.test.ts index 1935ac4258c..228f6c341b4 100644 --- a/src/lib/yieldxyz/utils.test.ts +++ b/src/lib/yieldxyz/utils.test.ts @@ -4,8 +4,10 @@ import { SHAPESHIFT_COSMOS_VALIDATOR_ADDRESS } from './constants' import type { AugmentedYieldDto, ValidatorDto } from './types' import { ensureValidatorApr, + formatYieldTxTitle, getTransactionButtonText, getYieldActionLabelKeys, + getYieldSuccessMessageKey, isStakingYieldType, resolveYieldInputAssetIcon, searchValidators, @@ -19,10 +21,20 @@ describe('getTransactionButtonText', () => { expect(getTransactionButtonText('claim-rewards', undefined)).toBe('Claim') }) - it('should fallback to parsing title when type is unknown', () => { + it('should use vault terminology (deposit/withdraw) by default', () => { expect(getTransactionButtonText(undefined, 'Approve token')).toBe('Approve') - expect(getTransactionButtonText(undefined, 'Deposit ETH transaction')).toBe('Enter') + expect(getTransactionButtonText(undefined, 'Deposit ETH transaction')).toBe('Deposit') expect(getTransactionButtonText(undefined, 'Claim rewards')).toBe('Claim') + expect(getTransactionButtonText('DEPOSIT', undefined)).toBe('Deposit') + expect(getTransactionButtonText('WITHDRAW', undefined)).toBe('Withdraw') + }) + + it('should use staking terminology when yieldType is staking', () => { + expect(getTransactionButtonText('STAKE', undefined, 'staking')).toBe('Stake') + expect(getTransactionButtonText('UNSTAKE', undefined, 'staking')).toBe('Unstake') + expect(getTransactionButtonText('DEPOSIT', undefined, 'liquid-staking')).toBe('Stake') + expect(getTransactionButtonText('WITHDRAW', undefined, 'liquid-staking')).toBe('Unstake') + expect(getTransactionButtonText(undefined, 'Deposit ETH', 'native-staking')).toBe('Stake') }) it('should return Confirm as final fallback', () => { @@ -286,17 +298,6 @@ describe('getYieldActionLabelKeys', () => { exit: 'common.withdraw', }) }) - - it('should return deposit/withdraw for unknown yield types', () => { - expect(getYieldActionLabelKeys('unknown')).toEqual({ - enter: 'common.deposit', - exit: 'common.withdraw', - }) - expect(getYieldActionLabelKeys('')).toEqual({ - enter: 'common.deposit', - exit: 'common.withdraw', - }) - }) }) describe('isStakingYieldType', () => { @@ -311,7 +312,54 @@ describe('isStakingYieldType', () => { it('should return false for non-staking yield types', () => { expect(isStakingYieldType('vault')).toBe(false) expect(isStakingYieldType('lending')).toBe(false) - expect(isStakingYieldType('unknown')).toBe(false) - expect(isStakingYieldType('')).toBe(false) + }) +}) + +describe('formatYieldTxTitle', () => { + it('should use vault terminology by default', () => { + expect(formatYieldTxTitle('Deposit ETH', 'ETH')).toBe('Deposit ETH') + expect(formatYieldTxTitle('Withdraw ETH transaction', 'ETH')).toBe('Withdraw ETH') + expect(formatYieldTxTitle('Approve ETH', 'ETH')).toBe('Approve ETH') + }) + + it('should use staking terminology when yieldType is staking', () => { + expect(formatYieldTxTitle('Deposit ETH', 'ETH', 'staking')).toBe('Stake ETH') + expect(formatYieldTxTitle('Withdraw ETH', 'ETH', 'liquid-staking')).toBe('Unstake ETH') + expect(formatYieldTxTitle('Exit ETH', 'ETH', 'native-staking')).toBe('Unstake ETH') + expect(formatYieldTxTitle('Unstake ETH', 'ETH', 'pooled-staking')).toBe('Unstake ETH') + }) + + it('should preserve unknown titles', () => { + expect(formatYieldTxTitle('Custom action', 'ETH')).toBe('Custom action') + expect(formatYieldTxTitle('Custom action', 'ETH', 'staking')).toBe('Custom action') + }) +}) + +describe('getYieldSuccessMessageKey', () => { + it('should return staking success keys for staking yield types', () => { + expect(getYieldSuccessMessageKey('staking', 'enter')).toBe('successStaked') + expect(getYieldSuccessMessageKey('staking', 'exit')).toBe('successUnstaked') + expect(getYieldSuccessMessageKey('native-staking', 'enter')).toBe('successStaked') + expect(getYieldSuccessMessageKey('liquid-staking', 'exit')).toBe('successUnstaked') + expect(getYieldSuccessMessageKey('pooled-staking', 'enter')).toBe('successStaked') + }) + + it('should return restake success key for restaking enter', () => { + expect(getYieldSuccessMessageKey('restaking', 'enter')).toBe('successRestaked') + expect(getYieldSuccessMessageKey('restaking', 'exit')).toBe('successUnstaked') + }) + + it('should return vault success keys for vault/lending yield types', () => { + expect(getYieldSuccessMessageKey('vault', 'enter')).toBe('successDeposited') + expect(getYieldSuccessMessageKey('vault', 'exit')).toBe('successWithdrawn') + expect(getYieldSuccessMessageKey('lending', 'enter')).toBe('successDeposited') + expect(getYieldSuccessMessageKey('lending', 'exit')).toBe('successWithdrawn') + }) + + it('should return successClaim for claim and manage actions', () => { + expect(getYieldSuccessMessageKey('staking', 'claim')).toBe('successClaim') + expect(getYieldSuccessMessageKey('vault', 'claim')).toBe('successClaim') + expect(getYieldSuccessMessageKey('staking', 'manage')).toBe('successClaim') + expect(getYieldSuccessMessageKey('vault', 'manage')).toBe('successClaim') }) }) diff --git a/src/lib/yieldxyz/utils.ts b/src/lib/yieldxyz/utils.ts index 4e02ee855a6..73a5bc51791 100644 --- a/src/lib/yieldxyz/utils.ts +++ b/src/lib/yieldxyz/utils.ts @@ -6,76 +6,110 @@ import { SHAPESHIFT_COSMOS_VALIDATOR_ADDRESS, YIELD_NETWORK_TO_CHAIN_ID, } from './constants' -import type { AugmentedYieldDto, ValidatorDto, YieldIconSource } from './types' +import type { AugmentedYieldDto, ValidatorDto, YieldIconSource, YieldType } from './types' export const yieldNetworkToChainId = (network: string): ChainId | undefined => { if (!isSupportedYieldNetwork(network)) return undefined return YIELD_NETWORK_TO_CHAIN_ID[network] } -const TX_TITLE_PATTERNS: [RegExp, string][] = [ - [/approv/i, 'Approve'], - [/supply|deposit|enter/i, 'Enter'], - [/withdraw|exit|unstake|undelegate/i, 'Exit'], - [/claim/i, 'Claim'], - [/stake|delegate/i, 'Enter'], - [/bridge/i, 'Bridge'], - [/swap/i, 'Swap'], +type TxTitlePattern = { + pattern: RegExp + staking: string + vault: string +} + +const TX_TITLE_PATTERNS: TxTitlePattern[] = [ + { pattern: /approv/i, staking: 'Approve', vault: 'Approve' }, + { pattern: /supply|deposit|enter/i, staking: 'Stake', vault: 'Deposit' }, + { pattern: /withdraw|exit/i, staking: 'Unstake', vault: 'Withdraw' }, + { pattern: /unstake|undelegate/i, staking: 'Unstake', vault: 'Withdraw' }, + { pattern: /claim/i, staking: 'Claim', vault: 'Claim' }, + { pattern: /stake|delegate/i, staking: 'Stake', vault: 'Deposit' }, + { pattern: /bridge/i, staking: 'Bridge', vault: 'Bridge' }, + { pattern: /swap/i, staking: 'Swap', vault: '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) -// Yield.xyz uses Enter/Exit terminology consistently -const TX_TYPE_TO_LABEL: Record = { - APPROVE: 'Approve', - APPROVAL: 'Approve', - DELEGATE: 'Enter', // Monad uses DELEGATE for staking - UNDELEGATE: 'Exit', // Monad uses UNDELEGATE for unstaking - STAKE: 'Enter', - UNSTAKE: 'Exit', - DEPOSIT: 'Enter', - WITHDRAW: 'Exit', - SUPPLY: 'Enter', - EXIT: 'Exit', - ENTER: 'Enter', - BRIDGE: 'Bridge', - SWAP: 'Swap', - CLAIM: 'Claim', - CLAIM_REWARDS: 'Claim', - TRANSFER: 'Transfer', +type TxTypeLabels = { + staking: string + vault: string +} + +const TX_TYPE_TO_LABELS: Record = { + APPROVE: { staking: 'Approve', vault: 'Approve' }, + APPROVAL: { staking: 'Approve', vault: 'Approve' }, + DELEGATE: { staking: 'Stake', vault: 'Deposit' }, + UNDELEGATE: { staking: 'Unstake', vault: 'Withdraw' }, + STAKE: { staking: 'Stake', vault: 'Deposit' }, + UNSTAKE: { staking: 'Unstake', vault: 'Withdraw' }, + DEPOSIT: { staking: 'Stake', vault: 'Deposit' }, + WITHDRAW: { staking: 'Unstake', vault: 'Withdraw' }, + SUPPLY: { staking: 'Stake', vault: 'Deposit' }, + EXIT: { staking: 'Unstake', vault: 'Withdraw' }, + ENTER: { staking: 'Stake', vault: 'Deposit' }, + BRIDGE: { staking: 'Bridge', vault: 'Bridge' }, + SWAP: { staking: 'Swap', vault: 'Swap' }, + CLAIM: { staking: 'Claim', vault: 'Claim' }, + CLAIM_REWARDS: { staking: 'Claim', vault: 'Claim' }, + TRANSFER: { staking: 'Transfer', vault: 'Transfer' }, +} + +type TerminologyKey = 'staking' | 'vault' + +const isStakingType = (yieldType: YieldType): boolean => { + switch (yieldType) { + case 'staking': + case 'native-staking': + case 'pooled-staking': + case 'liquid-staking': + case 'restaking': + return true + case 'vault': + case 'lending': + return false + default: + // This shouldn't happen but satisfies exhaustiveness check + assertNever(yieldType) + return false + } } /** * Gets a clean button label from a transaction type or title. - * Used for the main CTA button in the yield action modal. + * Uses yield-type-aware terminology (stake/unstake vs deposit/withdraw). */ export const getTransactionButtonText = ( type: string | undefined, title: string | undefined, + yieldType?: YieldType, ): string => { - // First try to use the transaction type directly + const labelKey: TerminologyKey = yieldType && isStakingType(yieldType) ? 'staking' : 'vault' + if (type) { const normalized = type.toUpperCase().replace(/[_-]/g, '_') - if (TX_TYPE_TO_LABEL[normalized]) { - return TX_TYPE_TO_LABEL[normalized] - } - // Fallback: capitalize the type + const labels = TX_TYPE_TO_LABELS[normalized] + if (labels) return labels[labelKey] 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] + const match = TX_TITLE_PATTERNS.find(p => p.pattern.test(title)) + if (match) return match[labelKey] } return 'Confirm' } -export const formatYieldTxTitle = (title: string, assetSymbol: string): string => { +export const formatYieldTxTitle = ( + title: string, + assetSymbol: string, + yieldType?: YieldType, +): string => { + const labelKey: TerminologyKey = yieldType && isStakingType(yieldType) ? 'staking' : 'vault' + const normalized = title.replace(/ transaction$/i, '').toLowerCase() - const match = TX_TITLE_PATTERNS.find(([pattern]) => pattern.test(normalized)) - if (match) return `${match[1]} ${assetSymbol}` + const match = TX_TITLE_PATTERNS.find(p => p.pattern.test(normalized)) + if (match) return `${match[labelKey]} ${assetSymbol}` return normalized.charAt(0).toUpperCase() + normalized.slice(1) } @@ -166,15 +200,19 @@ export type YieldActionLabelKeys = { exit: string } +const assertNever = (value: never): never => { + throw new Error(`Unhandled yield type: ${value}`) +} + /** * Gets the appropriate translation keys for yield actions based on yield type. * * Yield types and their terminology: * - staking, native-staking, pooled-staking, liquid-staking → Stake/Unstake * - restaking → Restake/Unstake - * - vault, lending, and others → Deposit/Withdraw + * - vault, lending → Deposit/Withdraw */ -export const getYieldActionLabelKeys = (yieldType: string): YieldActionLabelKeys => { +export const getYieldActionLabelKeys = (yieldType: YieldType): YieldActionLabelKeys => { switch (yieldType) { case 'staking': case 'native-staking': @@ -185,22 +223,121 @@ export const getYieldActionLabelKeys = (yieldType: string): YieldActionLabelKeys return { enter: 'yieldXYZ.actions.restake', exit: 'defi.unstake' } case 'vault': case 'lending': - default: return { enter: 'common.deposit', exit: 'common.withdraw' } + default: + return assertNever(yieldType) } } -const STAKING_YIELD_TYPES = new Set([ - 'staking', - 'native-staking', - 'pooled-staking', - 'liquid-staking', - 'restaking', -]) +export type YieldLoadingStateKeys = { + enter: string + exit: string +} -/** - * Checks if a yield type uses staking terminology (stake/unstake). - */ -export const isStakingYieldType = (yieldType: string): boolean => { - return STAKING_YIELD_TYPES.has(yieldType) +export const getYieldLoadingStateKeys = (yieldType: YieldType): YieldLoadingStateKeys => { + switch (yieldType) { + case 'staking': + case 'native-staking': + case 'pooled-staking': + case 'liquid-staking': + case 'restaking': + return { enter: 'yieldXYZ.staking', exit: 'yieldXYZ.unstaking' } + case 'vault': + case 'lending': + return { enter: 'yieldXYZ.depositing', exit: 'yieldXYZ.withdrawing' } + default: + return assertNever(yieldType) + } +} + +export type YieldHeadingKeys = { + enter: string + exit: string +} + +export const getYieldHeadingKeys = (yieldType: YieldType): YieldHeadingKeys => { + switch (yieldType) { + case 'staking': + case 'native-staking': + case 'pooled-staking': + case 'liquid-staking': + case 'restaking': + return { enter: 'yieldXYZ.stakeSymbol', exit: 'yieldXYZ.unstakeSymbol' } + case 'vault': + case 'lending': + return { enter: 'yieldXYZ.depositSymbol', exit: 'yieldXYZ.withdrawSymbol' } + default: + return assertNever(yieldType) + } +} + +export type YieldPendingStatusKeys = { + enter: string + exit: string +} + +export const getYieldPendingStatusKeys = (yieldType: YieldType): YieldPendingStatusKeys => { + switch (yieldType) { + case 'staking': + case 'native-staking': + case 'pooled-staking': + case 'liquid-staking': + case 'restaking': + return { enter: 'yieldXYZ.stakingPending', exit: 'yieldXYZ.unstakingPending' } + case 'vault': + case 'lending': + return { enter: 'yieldXYZ.depositingPending', exit: 'yieldXYZ.withdrawingPending' } + default: + return assertNever(yieldType) + } +} + +export const getYieldMinAmountKey = (yieldType: YieldType): string => { + switch (yieldType) { + case 'staking': + case 'native-staking': + case 'pooled-staking': + case 'liquid-staking': + case 'restaking': + return 'yieldXYZ.minStake' + case 'vault': + case 'lending': + return 'yieldXYZ.minDeposit' + default: + return assertNever(yieldType) + } +} + +export const isStakingYieldType = (yieldType: YieldType): boolean => { + return isStakingType(yieldType) +} + +export type YieldSuccessMessageKey = + | 'successStaked' + | 'successUnstaked' + | 'successDeposited' + | 'successWithdrawn' + | 'successRestaked' + | 'successClaim' + +export const getYieldSuccessMessageKey = ( + yieldType: YieldType, + action: 'enter' | 'exit' | 'claim' | 'manage', +): YieldSuccessMessageKey => { + if (action === 'claim' || action === 'manage') return 'successClaim' + + switch (yieldType) { + case 'staking': + case 'native-staking': + case 'pooled-staking': + case 'liquid-staking': + return action === 'enter' ? 'successStaked' : 'successUnstaked' + case 'restaking': + return action === 'enter' ? 'successRestaked' : 'successUnstaked' + case 'vault': + case 'lending': + return action === 'enter' ? 'successDeposited' : 'successWithdrawn' + default: + return assertNever(yieldType) + } } diff --git a/src/pages/Yields/components/YieldActionModal.tsx b/src/pages/Yields/components/YieldActionModal.tsx index e29bfc1d100..219afc0aff5 100644 --- a/src/pages/Yields/components/YieldActionModal.tsx +++ b/src/pages/Yields/components/YieldActionModal.tsx @@ -16,7 +16,14 @@ import { SHAPESHIFT_VALIDATOR_NAME, } from '@/lib/yieldxyz/constants' import type { AugmentedYieldDto } from '@/lib/yieldxyz/types' -import { getTransactionButtonText, isStakingYieldType } from '@/lib/yieldxyz/utils' +import { + getTransactionButtonText, + getYieldActionLabelKeys, + getYieldHeadingKeys, + getYieldLoadingStateKeys, + getYieldSuccessMessageKey, + isStakingYieldType, +} from '@/lib/yieldxyz/utils' import { GradientApy } from '@/pages/Yields/components/GradientApy' import { TransactionStepsList } from '@/pages/Yields/components/TransactionStepsList' import { YieldAssetFlow } from '@/pages/Yields/components/YieldAssetFlow' @@ -132,13 +139,10 @@ export const YieldActionModal = memo(function YieldActionModal({ providers, ]) - const chainId = useMemo(() => yieldItem.chainId ?? '', [yieldItem.chainId]) + const chainId = yieldItem.chainId ?? '' const feeAsset = useAppSelector(state => selectFeeAssetByChainId(state, chainId)) - const assetAvatarSrc = useMemo( - () => assetLogoURI ?? yieldItem.token.logoURI, - [assetLogoURI, yieldItem.token.logoURI], - ) + const assetAvatarSrc = assetLogoURI ?? yieldItem.token.logoURI const aprFormatted = useMemo( () => `${bnOrZero(yieldItem.rewardRate.total).times(100).toFixed(2)}%`, @@ -185,16 +189,25 @@ export const YieldActionModal = memo(function YieldActionModal({ if (activeStepIndex >= 0 && transactionSteps[activeStepIndex]?.loadingMessage) { return transactionSteps[activeStepIndex].loadingMessage } - if (action === 'enter') return translate('yieldXYZ.entering') - if (action === 'exit') return translate('yieldXYZ.exiting') + const loadingKeys = getYieldLoadingStateKeys(yieldItem.mechanics.type) + if (action === 'enter') return translate(loadingKeys.enter) + if (action === 'exit') return translate(loadingKeys.exit) return translate('common.claiming') - }, [isQuoteLoading, action, translate, activeStepIndex, transactionSteps]) + }, [ + isQuoteLoading, + action, + translate, + activeStepIndex, + transactionSteps, + yieldItem.mechanics.type, + ]) const buttonText = useMemo(() => { - // Use the current step's type/title for a clean button label (e.g., "Enter", "Exit", "Approve") + const yieldType = yieldItem.mechanics.type + // Use the current step's type/title for a clean button label (e.g., "Stake", "Unstake", "Approve") if (activeStepIndex >= 0 && transactionSteps[activeStepIndex]) { const step = transactionSteps[activeStepIndex] - return getTransactionButtonText(step.type, step.originalTitle) + return getTransactionButtonText(step.type, step.originalTitle, yieldType) } // USDT reset required before other transactions if (isUsdtResetRequired) { @@ -203,24 +216,31 @@ export const YieldActionModal = memo(function YieldActionModal({ // 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) + return getTransactionButtonText(firstCreatedTx.type, firstCreatedTx.title, yieldType) } - // Fallback to action-based text - if (action === 'enter') return translate('yieldXYZ.enter') - if (action === 'exit') return translate('yieldXYZ.exit') + // Fallback to action-based text using yield-type-aware labels + const actionLabelKeys = getYieldActionLabelKeys(yieldType) + if (action === 'enter') return translate(actionLabelKeys.enter) + if (action === 'exit') return translate(actionLabelKeys.exit) return translate('common.claim') - }, [action, translate, activeStepIndex, transactionSteps, quoteData, isUsdtResetRequired]) + }, [ + action, + translate, + activeStepIndex, + transactionSteps, + quoteData, + isUsdtResetRequired, + yieldItem.mechanics.type, + ]) const modalHeading = useMemo(() => { - if (action === 'enter') return translate('yieldXYZ.enterSymbol', { symbol: assetSymbol }) - if (action === 'exit') return translate('yieldXYZ.exitSymbol', { symbol: assetSymbol }) + const headingKeys = getYieldHeadingKeys(yieldItem.mechanics.type) + if (action === 'enter') return translate(headingKeys.enter, { symbol: assetSymbol }) + if (action === 'exit') return translate(headingKeys.exit, { symbol: assetSymbol }) return translate('yieldXYZ.claimSymbol', { symbol: assetSymbol }) - }, [action, assetSymbol, translate]) + }, [action, assetSymbol, translate, yieldItem.mechanics.type]) - const networkAvatarSrc = useMemo( - () => feeAsset?.networkIcon ?? feeAsset?.icon, - [feeAsset?.networkIcon, feeAsset?.icon], - ) + const networkAvatarSrc = feeAsset?.networkIcon ?? feeAsset?.icon const assetFlowDirection = action === 'exit' ? 'exit' : 'enter' @@ -279,32 +299,17 @@ export const YieldActionModal = memo(function YieldActionModal({ )} )} - {showValidatorRow && ( - - - {translate('yieldXYZ.validator')} - - - - - {vaultMetadata.name} - - - - )} - {!showValidatorRow && ( - - - {translate('yieldXYZ.provider')} + + + {translate(showValidatorRow ? 'yieldXYZ.validator' : 'yieldXYZ.provider')} + + + + + {vaultMetadata.name} - - - - {vaultMetadata.name} - - - )} + {translate('yieldXYZ.network')} @@ -347,11 +352,10 @@ export const YieldActionModal = memo(function YieldActionModal({ [animatedAvatarRow, statsContent, displaySteps], ) - const successMessageKey = useMemo(() => { - if (action === 'enter') return 'successEnter' as const - if (action === 'exit') return 'successExit' as const - return 'successClaim' as const - }, [action]) + const successMessageKey = useMemo( + () => getYieldSuccessMessageKey(yieldItem.mechanics.type, action), + [yieldItem.mechanics.type, action], + ) const successProviderInfo = useMemo( () => (vaultMetadata ? { name: vaultMetadata.name, logoURI: vaultMetadata.logoURI } : null), diff --git a/src/pages/Yields/components/YieldEnterModal.tsx b/src/pages/Yields/components/YieldEnterModal.tsx index 3ea4266d513..5ac534b1401 100644 --- a/src/pages/Yields/components/YieldEnterModal.tsx +++ b/src/pages/Yields/components/YieldEnterModal.tsx @@ -29,7 +29,14 @@ import { SHAPESHIFT_VALIDATOR_NAME, } from '@/lib/yieldxyz/constants' import type { AugmentedYieldDto } from '@/lib/yieldxyz/types' -import { getTransactionButtonText, isStakingYieldType } from '@/lib/yieldxyz/utils' +import { + getTransactionButtonText, + getYieldActionLabelKeys, + getYieldHeadingKeys, + getYieldMinAmountKey, + getYieldSuccessMessageKey, + isStakingYieldType, +} from '@/lib/yieldxyz/utils' import { GradientApy } from '@/pages/Yields/components/GradientApy' import { TransactionStepsList } from '@/pages/Yields/components/TransactionStepsList' import { YieldExplainers } from '@/pages/Yields/components/YieldExplainers' @@ -357,12 +364,21 @@ export const YieldEnterModal = memo( if (isSubmitting && transactionSteps.length > 0) { const activeStep = transactionSteps.find(s => s.status !== 'success') - if (activeStep) return getTransactionButtonText(activeStep.type, activeStep.originalTitle) + if (activeStep) + return getTransactionButtonText( + activeStep.type, + activeStep.originalTitle, + yieldItem.mechanics.type, + ) } if (activeStepIndex >= 0 && transactionSteps[activeStepIndex]) { const currentStep = transactionSteps[activeStepIndex] - return getTransactionButtonText(currentStep.type, currentStep.originalTitle) + return getTransactionButtonText( + currentStep.type, + currentStep.originalTitle, + yieldItem.mechanics.type, + ) } if (isUsdtResetRequired) { @@ -370,9 +386,15 @@ export const YieldEnterModal = memo( } const firstCreatedTx = quoteData?.transactions?.find(tx => tx.status === 'CREATED') - if (firstCreatedTx) return getTransactionButtonText(firstCreatedTx.type, firstCreatedTx.title) - - return translate('yieldXYZ.enterAsset', { asset: inputTokenAsset?.symbol }) + if (firstCreatedTx) + return getTransactionButtonText( + firstCreatedTx.type, + firstCreatedTx.title, + yieldItem.mechanics.type, + ) + + const actionLabelKeys = getYieldActionLabelKeys(yieldItem.mechanics.type) + return `${translate(actionLabelKeys.enter)} ${inputTokenAsset?.symbol ?? ''}` }, [ isConnected, isQuoteActive, @@ -383,12 +405,14 @@ export const YieldEnterModal = memo( quoteData, translate, inputTokenAsset?.symbol, + yieldItem.mechanics.type, ]) + const headingKeys = getYieldHeadingKeys(yieldItem.mechanics.type) const modalTitle = useMemo(() => { if (step === ModalStep.Success) return translate('common.success') - return translate('yieldXYZ.enterAsset', { asset: inputTokenAsset?.symbol }) - }, [translate, inputTokenAsset?.symbol, step]) + return translate(headingKeys.enter, { symbol: inputTokenAsset?.symbol }) + }, [translate, inputTokenAsset?.symbol, step, headingKeys.enter]) const percentButtons = useMemo( () => ( @@ -482,7 +506,7 @@ export const YieldEnterModal = memo( {minDeposit && bnOrZero(minDeposit).gt(0) && ( - {translate('yieldXYZ.minEnter')} + {translate(getYieldMinAmountKey(yieldItem.mechanics.type))} ( ), [ @@ -598,6 +625,7 @@ export const YieldEnterModal = memo( yieldItem.id, accountId, hookHandleClose, + successMessageKey, ], ) diff --git a/src/pages/Yields/components/YieldForm.tsx b/src/pages/Yields/components/YieldForm.tsx index ead0f0557cf..33019235efc 100644 --- a/src/pages/Yields/components/YieldForm.tsx +++ b/src/pages/Yields/components/YieldForm.tsx @@ -27,6 +27,8 @@ import { YieldBalanceType } from '@/lib/yieldxyz/types' import { getTransactionButtonText, getYieldActionLabelKeys, + getYieldMinAmountKey, + getYieldSuccessMessageKey, isStakingYieldType, } from '@/lib/yieldxyz/utils' import { GradientApy } from '@/pages/Yields/components/GradientApy' @@ -433,12 +435,21 @@ export const YieldForm = memo( if (isSubmitting && transactionSteps.length > 0) { const activeStep = transactionSteps.find(s => s.status !== 'success') - if (activeStep) return getTransactionButtonText(activeStep.type, activeStep.originalTitle) + if (activeStep) + return getTransactionButtonText( + activeStep.type, + activeStep.originalTitle, + yieldItem.mechanics.type, + ) } if (activeStepIndex >= 0 && transactionSteps[activeStepIndex]) { const currentStep = transactionSteps[activeStepIndex] - return getTransactionButtonText(currentStep.type, currentStep.originalTitle) + return getTransactionButtonText( + currentStep.type, + currentStep.originalTitle, + yieldItem.mechanics.type, + ) } if (isUsdtResetRequired) { @@ -446,7 +457,12 @@ export const YieldForm = memo( } const firstCreatedTx = quoteData?.transactions?.find(tx => tx.status === 'CREATED') - if (firstCreatedTx) return getTransactionButtonText(firstCreatedTx.type, firstCreatedTx.title) + if (firstCreatedTx) + return getTransactionButtonText( + firstCreatedTx.type, + firstCreatedTx.title, + yieldItem.mechanics.type, + ) const actionLabelKeys = getYieldActionLabelKeys(yieldItem.mechanics.type) if (action === 'enter') { @@ -566,7 +582,7 @@ export const YieldForm = memo( {minDeposit && bnOrZero(minDeposit).gt(0) && action === 'enter' && ( - {translate('yieldXYZ.minEnter')} + {translate(getYieldMinAmountKey(yieldItem.mechanics.type))} navigateToAction('exit'), [navigateToAction]) const actionLabelKeys = getYieldActionLabelKeys(yieldItem.mechanics.type) + const pendingStatusKeys = getYieldPendingStatusKeys(yieldItem.mechanics.type) const enterLabel = translate(actionLabelKeys.enter) const exitLabel = translate(actionLabelKeys.exit) @@ -221,7 +222,7 @@ export const YieldPositionCard = memo( - {translate('yieldXYZ.entering')} + {translate(pendingStatusKeys.enter)} {formatBalance(enteringBalance)} @@ -233,7 +234,7 @@ export const YieldPositionCard = memo( ) - }, [hasEntering, translate, formatBalance, enteringBalance]) + }, [hasEntering, translate, formatBalance, enteringBalance, pendingStatusKeys.enter]) const unstakingSection = useMemo(() => { if (!hasExiting) return null @@ -253,7 +254,7 @@ export const YieldPositionCard = memo( - {translate('yieldXYZ.unstaking')} + {translate(pendingStatusKeys.exit)} @@ -273,7 +274,7 @@ export const YieldPositionCard = memo( ) }) - }, [hasExiting, exitingEntries, translate]) + }, [hasExiting, exitingEntries, translate, pendingStatusKeys.exit]) const withdrawableSection = useMemo(() => { if (!hasWithdrawable) return null diff --git a/src/pages/Yields/components/YieldSuccess.tsx b/src/pages/Yields/components/YieldSuccess.tsx index 7812afc8a67..3a1cd81fe92 100644 --- a/src/pages/Yields/components/YieldSuccess.tsx +++ b/src/pages/Yields/components/YieldSuccess.tsx @@ -10,6 +10,8 @@ import { useConfetti } from '../hooks/useConfetti' import type { TransactionStep } from '../hooks/useYieldTransactionFlow' import { TransactionStepsList } from './TransactionStepsList' +import type { YieldSuccessMessageKey } from '@/lib/yieldxyz/utils' + type ProviderInfo = { name: string logoURI: string | undefined @@ -24,12 +26,7 @@ type YieldSuccessProps = { accountId?: AccountId onDone: () => void showConfetti?: boolean - successMessageKey?: - | 'successEnter' - | 'successExit' - | 'successClaim' - | 'successUnstaked' - | 'successWithdrawn' + successMessageKey?: YieldSuccessMessageKey } export const YieldSuccess = memo( @@ -42,7 +39,7 @@ export const YieldSuccess = memo( accountId, onDone, showConfetti = true, - successMessageKey = 'successEnter', + successMessageKey = 'successStaked', }: YieldSuccessProps) => { const translate = useTranslate() const navigate = useNavigate() diff --git a/src/pages/Yields/hooks/useYieldTransactionFlow.ts b/src/pages/Yields/hooks/useYieldTransactionFlow.ts index ac0c2266c4c..624c02092fc 100644 --- a/src/pages/Yields/hooks/useYieldTransactionFlow.ts +++ b/src/pages/Yields/hooks/useYieldTransactionFlow.ts @@ -186,7 +186,7 @@ export const useYieldTransactionFlow = ({ const yieldChainId = yieldItem?.chainId const { accountId: contextAccountId, accountNumber: contextAccountNumber } = useYieldAccount() - const derivedAccountId = useAppSelector(state => { + const accountId = useAppSelector(state => { if (accountIdProp) return accountIdProp if (contextAccountId) return contextAccountId if (!yieldChainId) return undefined @@ -194,8 +194,6 @@ export const useYieldTransactionFlow = ({ return selectAccountIdByAccountNumberAndChainId(state)[contextAccountNumber]?.[yieldChainId] }) - const accountId = derivedAccountId - const feeAsset = useAppSelector(state => yieldChainId ? selectFeeAssetByChainId(state, yieldChainId) : undefined, ) @@ -221,12 +219,12 @@ export const useYieldTransactionFlow = ({ if (!yieldItem || !userAddress || !yieldChainId) return null if (action !== 'manage' && !amount) return null - const fields = - action === 'enter' - ? yieldItem.mechanics.arguments.enter.fields - : action === 'exit' - ? yieldItem.mechanics.arguments.exit.fields - : [] + const getFields = () => { + if (action === 'enter') return yieldItem.mechanics.arguments.enter.fields + if (action === 'exit') return yieldItem.mechanics.arguments.exit.fields + return [] + } + const fields = getFields() const fieldNames = new Set(fields.map(field => field.name)) const args: Record = {} @@ -356,7 +354,11 @@ export const useYieldTransactionFlow = ({ ...quoteData.transactions .filter(tx => tx.status === TransactionStatus.Created) .map((tx, i) => ({ - title: formatYieldTxTitle(tx.title || `Transaction ${i + 1}`, assetSymbol), + title: formatYieldTxTitle( + tx.title || `Transaction ${i + 1}`, + assetSymbol, + yieldItem?.mechanics.type, + ), originalTitle: tx.title || '', type: tx.type, status: 'pending' as const, @@ -369,6 +371,7 @@ export const useYieldTransactionFlow = ({ transactionSteps, quoteData, assetSymbol, + yieldItem?.mechanics.type, isAllowanceCheckPending, isUsdtResetRequired, translate, @@ -400,19 +403,27 @@ export const useYieldTransactionFlow = ({ } const isApproval = tx.title?.toLowerCase().includes('approv') - const actionType = isApproval - ? ActionType.Approve - : action === 'enter' - ? ActionType.Deposit - : action === 'exit' - ? ActionType.Withdraw - : ActionType.Claim - - const displayType = isApproval - ? GenericTransactionDisplayType.Approve - : action === 'manage' - ? GenericTransactionDisplayType.Claim - : GenericTransactionDisplayType.Yield + + type GenericActionType = + | typeof ActionType.Approve + | typeof ActionType.Deposit + | typeof ActionType.Withdraw + | typeof ActionType.Claim + + const getActionType = (): GenericActionType => { + if (isApproval) return ActionType.Approve + if (action === 'enter') return ActionType.Deposit + if (action === 'exit') return ActionType.Withdraw + return ActionType.Claim + } + const actionType = getActionType() + + const getDisplayType = (): GenericTransactionDisplayType => { + if (isApproval) return GenericTransactionDisplayType.Approve + if (action === 'manage') return GenericTransactionDisplayType.Claim + return GenericTransactionDisplayType.Yield + } + const displayType = getDisplayType() // TODO(gomes): handle claim notifications - there's more logic TBD here (e.g. unbonding periods). // For now, KISS and simply don't handle claims in action center. @@ -439,7 +450,7 @@ export const useYieldTransactionFlow = ({ accountId, message: typeMessagesMap[actionType] ?? - formatYieldTxTitle(tx.title || 'Transaction', assetSymbol), + formatYieldTxTitle(tx.title || 'Transaction', assetSymbol, yieldItem.mechanics.type), amountCryptoPrecision: amount, contractName: yieldItem.metadata.name, chainName: yieldItem.network, @@ -759,7 +770,11 @@ export const useYieldTransactionFlow = ({ } steps.push( ...transactions.map((tx, i) => ({ - title: formatYieldTxTitle(tx.title || `Transaction ${i + 1}`, assetSymbol), + title: formatYieldTxTitle( + tx.title || `Transaction ${i + 1}`, + assetSymbol, + yieldItem?.mechanics.type, + ), originalTitle: tx.title || '', type: tx.type, status: 'pending' as const, @@ -802,6 +817,7 @@ export const useYieldTransactionFlow = ({ assetSymbol, translate, showErrorToast, + yieldItem?.mechanics.type, ]) return useMemo( From 9bf2b5237b389f95548b5c1cb29d2f5ab1dcf617 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:41:30 +0100 Subject: [PATCH 10/11] docs(CLAUDE.md): add CLI tool preferences for rg and jq Prefer ripgrep over grep and use jq for JSON file querying to avoid context bloat and improve performance when working with large files. Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index f649e39d0d0..4beb4e217f3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,6 +79,13 @@ - Avoid `let` variable assignments - prefer `const` with inline IIFE switch statements or extract to functions for conditional logic - For static JSX icon elements (e.g., ``) that don't depend on state/props, define them as constants outside the component to avoid re-renders instead of using useMemo +### CLI Tool Preferences +- Prefer `rg` (ripgrep) over `grep` for searching - it's faster and respects .gitignore +- Use `jq` for querying and manipulating JSON files instead of reading entire files into context + - Example: `jq '.yieldXYZ | keys' src/assets/translations/en/main.json` to list keys + - Example: `jq '.someKey.nested' file.json` to extract specific values +- This is especially important for large JSON files (like generated data or translation files) to avoid context bloat and improve performance + ### Git & Version Control - Never commit changes unless explicitly requested - When creating commits, follow the Git Safety Protocol (see session notes) From db0d13cd7046b3f49664c6c5eba9e0040a8dd493 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 20 Jan 2026 13:07:51 +0100 Subject: [PATCH 11/11] fix(yields): use consistent stake/unstake terminology for restaking yields Restaking yields now use "Stake/Unstake" terminology consistently instead of "Restake/Unstake" to match the other staking yield types. - Update getYieldActionLabelKeys to return 'defi.stake' for restaking - Update getYieldSuccessMessageKey to return 'successStaked' for restaking - Remove unused successRestaked and actions.restake translations - Update tests to reflect consistent terminology @coderabbitai resolve Co-Authored-By: Claude Opus 4.5 --- src/assets/translations/en/main.json | 5 +---- src/lib/yieldxyz/utils.test.ts | 8 ++++---- src/lib/yieldxyz/utils.ts | 10 +++------- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 028252fcade..c324d311341 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -2672,9 +2672,7 @@ "yieldXYZ": { "pageTitle": "Yields", "pageSubtitle": "Discover and manage yield opportunities across multiple chains", - "actions": { - "restake": "Restake" - }, + "actions": {}, "yield": "Yield", "apy": "APY", "apr": "APR", @@ -2770,7 +2768,6 @@ "successUnstaked": "You successfully unstaked %{amount} %{symbol}", "successDeposited": "You successfully deposited %{amount} %{symbol}", "successWithdrawn": "You successfully withdrew %{amount} %{symbol}", - "successRestaked": "You successfully restaked %{amount} %{symbol}", "successClaim": "You successfully claimed %{amount} %{symbol}", "viewPosition": "View Position", "via": "via", diff --git a/src/lib/yieldxyz/utils.test.ts b/src/lib/yieldxyz/utils.test.ts index 228f6c341b4..037b94343e6 100644 --- a/src/lib/yieldxyz/utils.test.ts +++ b/src/lib/yieldxyz/utils.test.ts @@ -278,9 +278,9 @@ describe('getYieldActionLabelKeys', () => { }) }) - it('should return restake/unstake for restaking yield types', () => { + it('should return stake/unstake for restaking yield types', () => { expect(getYieldActionLabelKeys('restaking')).toEqual({ - enter: 'yieldXYZ.actions.restake', + enter: 'defi.stake', exit: 'defi.unstake', }) }) @@ -344,8 +344,8 @@ describe('getYieldSuccessMessageKey', () => { expect(getYieldSuccessMessageKey('pooled-staking', 'enter')).toBe('successStaked') }) - it('should return restake success key for restaking enter', () => { - expect(getYieldSuccessMessageKey('restaking', 'enter')).toBe('successRestaked') + it('should return staking success key for restaking yield types', () => { + expect(getYieldSuccessMessageKey('restaking', 'enter')).toBe('successStaked') expect(getYieldSuccessMessageKey('restaking', 'exit')).toBe('successUnstaked') }) diff --git a/src/lib/yieldxyz/utils.ts b/src/lib/yieldxyz/utils.ts index 73a5bc51791..ec55769eae3 100644 --- a/src/lib/yieldxyz/utils.ts +++ b/src/lib/yieldxyz/utils.ts @@ -208,8 +208,7 @@ const assertNever = (value: never): never => { * Gets the appropriate translation keys for yield actions based on yield type. * * Yield types and their terminology: - * - staking, native-staking, pooled-staking, liquid-staking → Stake/Unstake - * - restaking → Restake/Unstake + * - staking, native-staking, pooled-staking, liquid-staking, restaking → Stake/Unstake * - vault, lending → Deposit/Withdraw */ export const getYieldActionLabelKeys = (yieldType: YieldType): YieldActionLabelKeys => { @@ -218,9 +217,8 @@ export const getYieldActionLabelKeys = (yieldType: YieldType): YieldActionLabelK case 'native-staking': case 'pooled-staking': case 'liquid-staking': - return { enter: 'defi.stake', exit: 'defi.unstake' } case 'restaking': - return { enter: 'yieldXYZ.actions.restake', exit: 'defi.unstake' } + return { enter: 'defi.stake', exit: 'defi.unstake' } case 'vault': case 'lending': return { enter: 'common.deposit', exit: 'common.withdraw' } @@ -317,7 +315,6 @@ export type YieldSuccessMessageKey = | 'successUnstaked' | 'successDeposited' | 'successWithdrawn' - | 'successRestaked' | 'successClaim' export const getYieldSuccessMessageKey = ( @@ -331,9 +328,8 @@ export const getYieldSuccessMessageKey = ( case 'native-staking': case 'pooled-staking': case 'liquid-staking': - return action === 'enter' ? 'successStaked' : 'successUnstaked' case 'restaking': - return action === 'enter' ? 'successRestaked' : 'successUnstaked' + return action === 'enter' ? 'successStaked' : 'successUnstaked' case 'vault': case 'lending': return action === 'enter' ? 'successDeposited' : 'successWithdrawn'