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/13] 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/13] 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 8768bc8dc1f27774b2655521bc8fc3e1c1858b7f Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 19 Jan 2026 19:09:40 +0100 Subject: [PATCH 03/13] feat(yields): add SwapperModal for in-context asset acquisition - Create SwapperModal component wrapping StandaloneMultiHopTrade in a Dialog - Use MemoryRouter for isolated routing within modal - Clear Redux trade state on modal open/close - Show modal when user clicks "Get {TOKEN}" button - Supports desktop modal and mobile drawer via existing Dialog component Co-Authored-By: Claude Opus 4.5 --- src/components/SwapperModal/SwapperModal.tsx | 71 ++++++++++++ .../SwapperModal/SwapperModalContent.tsx | 64 +++++++++++ src/components/SwapperModal/index.ts | 1 + .../components/YieldAvailableToDeposit.tsx | 104 +++++++++--------- 4 files changed, 191 insertions(+), 49 deletions(-) create mode 100644 src/components/SwapperModal/SwapperModal.tsx create mode 100644 src/components/SwapperModal/SwapperModalContent.tsx create mode 100644 src/components/SwapperModal/index.ts diff --git a/src/components/SwapperModal/SwapperModal.tsx b/src/components/SwapperModal/SwapperModal.tsx new file mode 100644 index 00000000000..00735dbe0f2 --- /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 00000000000..3368f8ce539 --- /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 00000000000..672e8d29e77 --- /dev/null +++ b/src/components/SwapperModal/index.ts @@ -0,0 +1 @@ +export { SwapperModal } from './SwapperModal' diff --git a/src/pages/Yields/components/YieldAvailableToDeposit.tsx b/src/pages/Yields/components/YieldAvailableToDeposit.tsx index da1f6af67eb..8ee4221f2f4 100644 --- a/src/pages/Yields/components/YieldAvailableToDeposit.tsx +++ b/src/pages/Yields/components/YieldAvailableToDeposit.tsx @@ -11,11 +11,11 @@ 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' @@ -30,7 +30,7 @@ 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]) @@ -62,9 +62,8 @@ 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 || !isConnected) return null @@ -74,49 +73,56 @@ export const YieldAvailableToDeposit = memo( if (!hasAvailableBalance) { return ( - - - - - - - {translate('yieldXYZ.availableToDeposit')} - - - - - - - - - - - - - - - - - - - - + <> + + + + + + + {translate('yieldXYZ.availableToDeposit')} + + + + + + + + + + + + + + + + + + + + + + ) } 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 04/13] 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 05/13] 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 06/13] 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 07/13] 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 08/13] 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 bd8e0d906c9b56aa453183043f4c5b74ffe2489f Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 19 Jan 2026 19:51:47 +0100 Subject: [PATCH 09/13] fix: simplify SwapperModal - remove header on desktop, keep on mobile Co-Authored-By: Claude Opus 4.5 --- src/components/SwapperModal/SwapperModal.tsx | 23 +++++++++++--------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/SwapperModal/SwapperModal.tsx b/src/components/SwapperModal/SwapperModal.tsx index 00735dbe0f2..44b33914c13 100644 --- a/src/components/SwapperModal/SwapperModal.tsx +++ b/src/components/SwapperModal/SwapperModal.tsx @@ -4,6 +4,7 @@ import { MemoryRouter } from 'react-router-dom' import { SwapperModalContent } from './SwapperModalContent' +import { Display } from '@/components/Display' import { Dialog } from '@/components/Modal/components/Dialog' import { DialogBody } from '@/components/Modal/components/DialogBody' import { DialogCloseButton } from '@/components/Modal/components/DialogCloseButton' @@ -46,16 +47,18 @@ export const SwapperModal = memo( size: { base: 'full', md: 'md' }, }} > - - {null} - - Trade - - - - - - + + + {null} + + Trade + + + + + + + {isOpen && ( Date: Mon, 19 Jan 2026 19:56:27 +0100 Subject: [PATCH 10/13] fix: use isCompact mode for SwapperModal to show single-column view Co-Authored-By: Claude Opus 4.5 --- src/components/SwapperModal/SwapperModalContent.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/SwapperModal/SwapperModalContent.tsx b/src/components/SwapperModal/SwapperModalContent.tsx index 3368f8ce539..2011684b5e1 100644 --- a/src/components/SwapperModal/SwapperModalContent.tsx +++ b/src/components/SwapperModal/SwapperModalContent.tsx @@ -56,6 +56,7 @@ export const SwapperModalContent = memo(function SwapperModalContent({ defaultBuyAssetId={defaultBuyAssetId} defaultSellAssetId={defaultSellAssetId} onChangeTab={handleChangeTab} + isCompact isStandalone /> From 3523ced3281e8c30f6df43e50c69079c85a791f8 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 19 Jan 2026 20:07:10 +0100 Subject: [PATCH 11/13] chore: remove unnecessary barrel file Co-Authored-By: Claude Opus 4.5 --- src/components/SwapperModal/index.ts | 1 - src/pages/Yields/components/YieldAvailableToDeposit.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 src/components/SwapperModal/index.ts diff --git a/src/components/SwapperModal/index.ts b/src/components/SwapperModal/index.ts deleted file mode 100644 index 672e8d29e77..00000000000 --- a/src/components/SwapperModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { SwapperModal } from './SwapperModal' diff --git a/src/pages/Yields/components/YieldAvailableToDeposit.tsx b/src/pages/Yields/components/YieldAvailableToDeposit.tsx index 35fdd2a2db9..283fd8b00c1 100644 --- a/src/pages/Yields/components/YieldAvailableToDeposit.tsx +++ b/src/pages/Yields/components/YieldAvailableToDeposit.tsx @@ -15,7 +15,7 @@ import { memo, useCallback, useMemo, useState } from 'react' import { useTranslate } from 'react-polyglot' import { Amount } from '@/components/Amount/Amount' -import { SwapperModal } from '@/components/SwapperModal' +import { SwapperModal } from '@/components/SwapperModal/SwapperModal' import { KeyManager } from '@/context/WalletProvider/KeyManager' import { useFeatureFlag } from '@/hooks/useFeatureFlag/useFeatureFlag' import { useWallet } from '@/hooks/useWallet/useWallet' From 177584366c7bcba6530b0a5f476053bf747d3c2a Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 20 Jan 2026 14:04:35 +0100 Subject: [PATCH 12/13] feat(swapper-modal): add onSuccess callback for auto-close on trade completion Thread an optional onSuccess callback through SwapperModal -> SwapperModalContent -> StandaloneMultiHopTrade -> TradeConfirm. When trade completes, the callback fires exactly once, allowing consumers like YieldAvailableToDeposit to auto-close the modal instead of requiring manual dismissal. Co-Authored-By: Claude Opus 4.5 --- .../MultiHopTrade/StandaloneMultiHopTrade.tsx | 14 ++++++++--- .../components/TradeConfirm/TradeConfirm.tsx | 24 +++++++++++++++++-- src/components/SwapperModal/SwapperModal.tsx | 4 +++- .../SwapperModal/SwapperModalContent.tsx | 3 +++ .../components/YieldAvailableToDeposit.tsx | 1 + 5 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/components/MultiHopTrade/StandaloneMultiHopTrade.tsx b/src/components/MultiHopTrade/StandaloneMultiHopTrade.tsx index ef9664699e7..45f0733f063 100644 --- a/src/components/MultiHopTrade/StandaloneMultiHopTrade.tsx +++ b/src/components/MultiHopTrade/StandaloneMultiHopTrade.tsx @@ -25,7 +25,9 @@ import { import { tradeInput } from '@/state/slices/tradeInputSlice/tradeInputSlice' import { useAppDispatch, useAppSelector } from '@/state/store' -export type StandaloneTradeCardProps = TradeCardProps +export type StandaloneTradeCardProps = TradeCardProps & { + onSuccess?: () => void +} const GetTradeRates = () => { useGetTradeRates() @@ -38,6 +40,7 @@ export const StandaloneMultiHopTrade = memo( defaultSellAssetId, isCompact, onChangeTab, + onSuccess, isStandalone, }: StandaloneTradeCardProps) => { const dispatch = useAppDispatch() @@ -129,6 +132,7 @@ export const StandaloneMultiHopTrade = memo( ) @@ -139,10 +143,11 @@ type StandaloneTradeRoutesProps = { isCompact?: boolean isStandalone?: boolean onChangeTab: (newTab: TradeInputTab) => void + onSuccess?: () => void } const StandaloneTradeRoutes = memo( - ({ isCompact, isStandalone, onChangeTab }: StandaloneTradeRoutesProps) => { + ({ isCompact, isStandalone, onChangeTab, onSuccess }: StandaloneTradeRoutesProps) => { const location = useLocation() const tradeInputRef = useRef(null) @@ -165,7 +170,10 @@ const StandaloneTradeRoutes = memo( }, [location.pathname]) // Create memoized elements for each route - const tradeConfirmElement = useMemo(() => , [isCompact]) + const tradeConfirmElement = useMemo( + () => , + [isCompact, onSuccess], + ) const verifyAddressesElement = useMemo(() => , []) diff --git a/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirm.tsx b/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirm.tsx index ac62dfc4b02..8066f2bc7a1 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirm.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirm.tsx @@ -1,6 +1,6 @@ import { Stepper, usePrevious } from '@chakra-ui/react' import { isArbitrumBridgeTradeQuoteOrRate } from '@shapeshiftoss/swapper' -import { useCallback, useEffect, useMemo } from 'react' +import { useCallback, useEffect, useMemo, useRef } from 'react' import { Navigate, useNavigate } from 'react-router-dom' import { useTrackTradeQuotes } from '../../hooks/useGetTradeQuotes/hooks/useTrackTradeQuotes' @@ -26,7 +26,12 @@ import { tradeQuoteSlice } from '@/state/slices/tradeQuoteSlice/tradeQuoteSlice' import { TradeExecutionState } from '@/state/slices/tradeQuoteSlice/types' import { useAppDispatch, useAppSelector } from '@/state/store' -export const TradeConfirm = ({ isCompact }: { isCompact: boolean | undefined }) => { +type TradeConfirmProps = { + isCompact?: boolean + onSuccess?: () => void +} + +export const TradeConfirm = ({ isCompact, onSuccess }: TradeConfirmProps) => { const navigate = useNavigate() const { isLoading } = useIsApprovalInitiallyNeeded() const dispatch = useAppDispatch() @@ -51,6 +56,21 @@ export const TradeConfirm = ({ isCompact }: { isCompact: boolean | undefined }) [confirmedTradeExecutionState], ) + const hasCalledOnSuccess = useRef(false) + + useEffect(() => { + if (isTradeComplete && onSuccess && !hasCalledOnSuccess.current) { + hasCalledOnSuccess.current = true + onSuccess() + } + }, [isTradeComplete, onSuccess]) + + useEffect(() => { + if (confirmedTradeExecutionState !== TradeExecutionState.TradeComplete) { + hasCalledOnSuccess.current = false + } + }, [confirmedTradeExecutionState]) + const handleBack = useCallback(() => { if (isTradeComplete) { dispatch(tradeQuoteSlice.actions.clear()) diff --git a/src/components/SwapperModal/SwapperModal.tsx b/src/components/SwapperModal/SwapperModal.tsx index 44b33914c13..6f6bacd0f82 100644 --- a/src/components/SwapperModal/SwapperModal.tsx +++ b/src/components/SwapperModal/SwapperModal.tsx @@ -17,6 +17,7 @@ import { useAppDispatch } from '@/state/store' type SwapperModalProps = { isOpen: boolean onClose: () => void + onSuccess?: () => void defaultBuyAssetId?: AssetId defaultSellAssetId?: AssetId } @@ -29,7 +30,7 @@ const initialEntries = [ ] export const SwapperModal = memo( - ({ isOpen, onClose, defaultBuyAssetId, defaultSellAssetId }: SwapperModalProps) => { + ({ isOpen, onClose, onSuccess, defaultBuyAssetId, defaultSellAssetId }: SwapperModalProps) => { const dispatch = useAppDispatch() const handleClose = useCallback(() => { @@ -64,6 +65,7 @@ export const SwapperModal = memo( )} diff --git a/src/components/SwapperModal/SwapperModalContent.tsx b/src/components/SwapperModal/SwapperModalContent.tsx index 2011684b5e1..bf25f7a8255 100644 --- a/src/components/SwapperModal/SwapperModalContent.tsx +++ b/src/components/SwapperModal/SwapperModalContent.tsx @@ -13,11 +13,13 @@ import { useAppDispatch, useAppSelector } from '@/state/store' type SwapperModalContentProps = { defaultBuyAssetId?: AssetId defaultSellAssetId?: AssetId + onSuccess?: () => void } export const SwapperModalContent = memo(function SwapperModalContent({ defaultBuyAssetId, defaultSellAssetId, + onSuccess, }: SwapperModalContentProps) { const methods = useForm({ mode: 'onChange' }) const navigate = useNavigate() @@ -56,6 +58,7 @@ export const SwapperModalContent = memo(function SwapperModalContent({ defaultBuyAssetId={defaultBuyAssetId} defaultSellAssetId={defaultSellAssetId} onChangeTab={handleChangeTab} + onSuccess={onSuccess} isCompact isStandalone /> diff --git a/src/pages/Yields/components/YieldAvailableToDeposit.tsx b/src/pages/Yields/components/YieldAvailableToDeposit.tsx index 283fd8b00c1..8f0b5b60882 100644 --- a/src/pages/Yields/components/YieldAvailableToDeposit.tsx +++ b/src/pages/Yields/components/YieldAvailableToDeposit.tsx @@ -133,6 +133,7 @@ export const YieldAvailableToDeposit = memo( From 64431504e3aeb5efcd067df2012ddbf7991c7502 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:09:02 +0100 Subject: [PATCH 13/13] feat(swapper-modal): add modal styling and fire callback on swap broadcast - Add modalCardStyles for seamless modal appearance (no nested borders) - Thread isModal prop through component chain to apply transparent styling - Change onSuccess callback to fire on swap TX broadcast (not trade completion) - Callback fires before navigation for cleaner modal close timing Co-Authored-By: Claude Opus 4.5 --- .../MultiHopTrade/StandaloneMultiHopTrade.tsx | 13 +++++--- .../SharedConfirm/SharedConfirm.tsx | 12 +++++-- .../SharedTradeInput/SharedTradeInput.tsx | 6 ++-- .../components/TradeConfirm/TradeConfirm.tsx | 32 ++++++++----------- .../TradeConfirm/TradeConfirmFooter.tsx | 4 +++ .../TradeConfirm/TradeFooterButton.tsx | 3 ++ .../hooks/useTradeButtonProps.tsx | 4 ++- .../TradeConfirm/hooks/useTradeExecution.tsx | 4 +++ .../components/TradeInput/TradeInput.tsx | 3 ++ src/components/MultiHopTrade/const.ts | 13 ++++++++ .../SwapperModal/SwapperModalContent.tsx | 1 + 11 files changed, 68 insertions(+), 27 deletions(-) diff --git a/src/components/MultiHopTrade/StandaloneMultiHopTrade.tsx b/src/components/MultiHopTrade/StandaloneMultiHopTrade.tsx index 45f0733f063..8af0115d6ba 100644 --- a/src/components/MultiHopTrade/StandaloneMultiHopTrade.tsx +++ b/src/components/MultiHopTrade/StandaloneMultiHopTrade.tsx @@ -27,6 +27,7 @@ import { useAppDispatch, useAppSelector } from '@/state/store' export type StandaloneTradeCardProps = TradeCardProps & { onSuccess?: () => void + isModal?: boolean } const GetTradeRates = () => { @@ -42,6 +43,7 @@ export const StandaloneMultiHopTrade = memo( onChangeTab, onSuccess, isStandalone, + isModal, }: StandaloneTradeCardProps) => { const dispatch = useAppDispatch() const location = useLocation() @@ -134,6 +136,7 @@ export const StandaloneMultiHopTrade = memo( onChangeTab={onChangeTab} onSuccess={onSuccess} isStandalone={isStandalone} + isModal={isModal} /> ) }, @@ -142,12 +145,13 @@ export const StandaloneMultiHopTrade = memo( type StandaloneTradeRoutesProps = { isCompact?: boolean isStandalone?: boolean + isModal?: boolean onChangeTab: (newTab: TradeInputTab) => void onSuccess?: () => void } const StandaloneTradeRoutes = memo( - ({ isCompact, isStandalone, onChangeTab, onSuccess }: StandaloneTradeRoutesProps) => { + ({ isCompact, isStandalone, isModal, onChangeTab, onSuccess }: StandaloneTradeRoutesProps) => { const location = useLocation() const tradeInputRef = useRef(null) @@ -171,8 +175,8 @@ const StandaloneTradeRoutes = memo( // Create memoized elements for each route const tradeConfirmElement = useMemo( - () => , - [isCompact, onSuccess], + () => , + [isCompact, isModal, onSuccess], ) const verifyAddressesElement = useMemo(() => , []) @@ -200,12 +204,13 @@ const StandaloneTradeRoutes = memo( () => ( ), - [isCompact, onChangeTab, isStandalone], + [isCompact, isModal, onChangeTab, isStandalone], ) return ( diff --git a/src/components/MultiHopTrade/components/SharedConfirm/SharedConfirm.tsx b/src/components/MultiHopTrade/components/SharedConfirm/SharedConfirm.tsx index 01d678d0397..7813f225b7c 100644 --- a/src/components/MultiHopTrade/components/SharedConfirm/SharedConfirm.tsx +++ b/src/components/MultiHopTrade/components/SharedConfirm/SharedConfirm.tsx @@ -2,7 +2,7 @@ import type { CardFooterProps } from '@chakra-ui/react' import { Card, CardBody, CardFooter, CardHeader, Heading } from '@chakra-ui/react' import type { JSX } from 'react' -import { cardstyles } from '../../const' +import { cardstyles, modalCardStyles } from '../../const' import { WithBackButton } from '../WithBackButton' import { TradeSlideTransition } from '@/components/MultiHopTrade/TradeSlideTransition' @@ -15,6 +15,7 @@ type SharedConfirmProps = { onBack: () => void headerTranslation: TextPropTypes['translation'] isLoading?: boolean + isModal?: boolean } const cardMinHeight = { base: 'calc(100vh - var(--mobile-nav-offset))', md: 'initial' } @@ -25,10 +26,17 @@ export const SharedConfirm = ({ footerContent, onBack, headerTranslation, + isModal, }: SharedConfirmProps) => { return ( - + diff --git a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx index 352a1eb3302..de9057c3d55 100644 --- a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx +++ b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx @@ -2,7 +2,7 @@ import type { CardProps } from '@chakra-ui/react' import { Box, Card, Center, Flex, useMediaQuery } from '@chakra-ui/react' import type { FormEvent, JSX } from 'react' -import { cardstyles } from '../../const' +import { cardstyles, modalCardStyles } from '../../const' import { SharedTradeInputHeader } from '../SharedTradeInput/SharedTradeInputHeader' import { useSharedWidth } from '../TradeInput/hooks/useSharedWidth' @@ -32,6 +32,7 @@ type SharedTradeInputProps = { onChangeTab: (newTab: TradeInputTab) => void onSubmit: (e: FormEvent) => void isStandalone?: boolean + isModal?: boolean } const cardBorderRadius = { base: '0', md: '2xl' } @@ -53,6 +54,7 @@ export const SharedTradeInput: React.FC = ({ onChangeTab, onSubmit, isStandalone, + isModal, }) => { const [isSmallerThanMd] = useMediaQuery(`(max-width: ${breakpoints.md})`, { ssr: false }) const [isSmallerThanXl] = useMediaQuery(`(max-width: ${breakpoints.xl})`, { ssr: false }) @@ -79,7 +81,7 @@ export const SharedTradeInput: React.FC = ({ borderRadius={cardBorderRadius} minHeight={cardMinHeight} height={!hasUserEnteredAmount && isSmallerThanMd ? cardMinHeight.base : 'initial'} - {...cardstyles} + {...(isModal ? modalCardStyles : cardstyles)} > void } -export const TradeConfirm = ({ isCompact, onSuccess }: TradeConfirmProps) => { +export const TradeConfirm = ({ isCompact, isModal, onSuccess }: TradeConfirmProps) => { const navigate = useNavigate() const { isLoading } = useIsApprovalInitiallyNeeded() const dispatch = useAppDispatch() @@ -56,21 +57,6 @@ export const TradeConfirm = ({ isCompact, onSuccess }: TradeConfirmProps) => { [confirmedTradeExecutionState], ) - const hasCalledOnSuccess = useRef(false) - - useEffect(() => { - if (isTradeComplete && onSuccess && !hasCalledOnSuccess.current) { - hasCalledOnSuccess.current = true - onSuccess() - } - }, [isTradeComplete, onSuccess]) - - useEffect(() => { - if (confirmedTradeExecutionState !== TradeExecutionState.TradeComplete) { - hasCalledOnSuccess.current = false - } - }, [confirmedTradeExecutionState]) - const handleBack = useCallback(() => { if (isTradeComplete) { dispatch(tradeQuoteSlice.actions.clear()) @@ -110,9 +96,18 @@ export const TradeConfirm = ({ isCompact, onSuccess }: TradeConfirmProps) => { isCompact={isCompact} tradeQuoteStep={tradeQuoteStep} activeTradeId={activeTradeId} + onSwapTxBroadcast={onSuccess} /> ) - }, [isTradeComplete, activeQuote, tradeQuoteLastHop, tradeQuoteStep, activeTradeId, isCompact]) + }, [ + isTradeComplete, + activeQuote, + tradeQuoteLastHop, + tradeQuoteStep, + activeTradeId, + isCompact, + onSuccess, + ]) const isArbitrumBridgeWithdraw = useMemo(() => { return isArbitrumBridgeTradeQuoteOrRate(activeQuote) && activeQuote.direction === 'withdrawal' @@ -161,6 +156,7 @@ export const TradeConfirm = ({ isCompact, onSuccess }: TradeConfirmProps) => { isLoading={isLoading} onBack={handleBack} headerTranslation={headerTranslation} + isModal={isModal} /> ) } diff --git a/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirmFooter.tsx b/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirmFooter.tsx index 3ac576301e0..b8717cab179 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirmFooter.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirmFooter.tsx @@ -51,11 +51,13 @@ type TradeConfirmFooterProps = { tradeQuoteStep: TradeQuoteStep activeTradeId: string isCompact: boolean | undefined + onSwapTxBroadcast?: () => void } export const TradeConfirmFooter: FC = ({ tradeQuoteStep, activeTradeId, + onSwapTxBroadcast, }) => { const [isExactAllowance, toggleIsExactAllowance] = useToggle(true) const translate = useTranslate() @@ -412,6 +414,7 @@ export const TradeConfirmFooter: FC = ({ activeTradeId={activeTradeId} isExactAllowance={isExactAllowance} isLoading={isNetworkFeeCryptoBaseUnitLoading || isNetworkFeeCryptoBaseUnitRefetching} + onSwapTxBroadcast={onSwapTxBroadcast} /> ) }, [ @@ -421,6 +424,7 @@ export const TradeConfirmFooter: FC = ({ isExactAllowance, isNetworkFeeCryptoBaseUnitLoading, isNetworkFeeCryptoBaseUnitRefetching, + onSwapTxBroadcast, ]) return ( diff --git a/src/components/MultiHopTrade/components/TradeConfirm/TradeFooterButton.tsx b/src/components/MultiHopTrade/components/TradeConfirm/TradeFooterButton.tsx index aeeed78ef01..44f854e94c7 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/TradeFooterButton.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/TradeFooterButton.tsx @@ -56,6 +56,7 @@ type TradeFooterButtonProps = { activeTradeId: string isExactAllowance: boolean isLoading?: boolean + onSwapTxBroadcast?: () => void } export const TradeFooterButton: FC = ({ @@ -64,6 +65,7 @@ export const TradeFooterButton: FC = ({ activeTradeId, isExactAllowance, isLoading = false, + onSwapTxBroadcast, }) => { const [isSubmitting, setIsSubmitting] = useState(false) const [shouldShowWarningAcknowledgement, setShouldShowWarningAcknowledgement] = useState(false) @@ -76,6 +78,7 @@ export const TradeFooterButton: FC = ({ currentHopIndex, activeTradeId, isExactAllowance, + onSwapTxBroadcast, }) const translate = useTranslate() const swapperName = useAppSelector(selectActiveSwapperName) diff --git a/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeButtonProps.tsx b/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeButtonProps.tsx index 28071152f94..67177f64ffb 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeButtonProps.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeButtonProps.tsx @@ -31,6 +31,7 @@ type UseTradeButtonPropsProps = { currentHopIndex: SupportedTradeQuoteStepIndex activeTradeId: string isExactAllowance: boolean + onSwapTxBroadcast?: () => void } type TradeButtonProps = { @@ -45,6 +46,7 @@ export const useTradeButtonProps = ({ currentHopIndex, activeTradeId, isExactAllowance, + onSwapTxBroadcast, }: UseTradeButtonPropsProps): TradeButtonProps | undefined => { const dispatch = useAppDispatch() const navigate = useNavigate() @@ -143,7 +145,7 @@ export const useTradeButtonProps = ({ relayerTxHash, ]) - const executeTrade = useTradeExecution(currentHopIndex, activeTradeId) + const executeTrade = useTradeExecution(currentHopIndex, activeTradeId, onSwapTxBroadcast) const handleSignTx = useCallback(() => { if ( diff --git a/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeExecution.tsx b/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeExecution.tsx index 6d6bda0d11e..1d13e537b61 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeExecution.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeExecution.tsx @@ -67,6 +67,7 @@ import { store, useAppDispatch, useAppSelector } from '@/state/store' export const useTradeExecution = ( hopIndex: SupportedTradeQuoteStepIndex, confirmedTradeId: TradeQuote['id'], + onSwapTxBroadcast?: () => void, ) => { const translate = useTranslate() const dispatch = useAppDispatch() @@ -238,6 +239,8 @@ export const useTradeExecution = ( }) } + onSwapTxBroadcast?.() + // Don't navigate away during QuickBuy - let the QuickBuy component handle the success state if (!isQuickBuy) { navigate(TradeRoutePaths.Input) @@ -772,6 +775,7 @@ export const useTradeExecution = ( swapsById, toast, isQuickBuy, + onSwapTxBroadcast, ]) return executeTrade diff --git a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx index 939f09e303d..b6bcb71f232 100644 --- a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx @@ -86,12 +86,14 @@ type TradeInputProps = { tradeInputRef: React.MutableRefObject isCompact?: boolean isStandalone?: boolean + isModal?: boolean onChangeTab: (newTab: TradeInputTab) => void } export const TradeInput = ({ isCompact, isStandalone, + isModal, tradeInputRef, onChangeTab, }: TradeInputProps) => { @@ -609,6 +611,7 @@ export const TradeInput = ({ onSubmit={handleTradeQuoteConfirm} onChangeTab={onChangeTab} isStandalone={isStandalone} + isModal={isModal} /> ) diff --git a/src/components/MultiHopTrade/const.ts b/src/components/MultiHopTrade/const.ts index ed1689dd332..85aaa6d714a 100644 --- a/src/components/MultiHopTrade/const.ts +++ b/src/components/MultiHopTrade/const.ts @@ -27,3 +27,16 @@ export const cardstyles: CardProps = { boxShadow: '0 1px 0 rgba(255,255,255,0.05) inset, 0 2px 5px rgba(0,0,0,.2)', }, } + +export const modalCardStyles: CardProps = { + bg: 'transparent', + borderColor: 'transparent', + boxShadow: 'none', + borderWidth: 0, + borderRadius: 0, + _dark: { + bg: 'transparent', + borderColor: 'transparent', + boxShadow: 'none', + }, +} diff --git a/src/components/SwapperModal/SwapperModalContent.tsx b/src/components/SwapperModal/SwapperModalContent.tsx index bf25f7a8255..d307456906c 100644 --- a/src/components/SwapperModal/SwapperModalContent.tsx +++ b/src/components/SwapperModal/SwapperModalContent.tsx @@ -61,6 +61,7 @@ export const SwapperModalContent = memo(function SwapperModalContent({ onSuccess={onSuccess} isCompact isStandalone + isModal />