Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8f7e626
feat: prevent re-access to /earn/confirm after yield transaction comp…
gomesalexandre Jan 26, 2026
c27a523
fix: correct yield routing from /yields/ to /yield/
gomesalexandre Jan 26, 2026
530d6b5
fix: eliminate dead space in yield success step footer
gomesalexandre Jan 26, 2026
e2e5cc4
fix: style fiat zero amount as placeholder in yield modals
gomesalexandre Jan 26, 2026
01fe19b
fix: filter non-default validator positions at data layer
gomesalexandre Jan 26, 2026
36a34a3
fix: show provider row for liquid staking yields
gomesalexandre Jan 26, 2026
bb00234
chore: add PR template rule to CLAUDE.md
gomesalexandre Jan 26, 2026
1df6077
fix: add spinner loading state for legacy positions in DeFi earn
gomesalexandre Jan 27, 2026
5284013
fix: select best actionable yield for asset page CTA
gomesalexandre Jan 27, 2026
c4bb8b8
fix: aggregate yield balances across all accounts in drawer
gomesalexandre Jan 27, 2026
0be3258
refactor: yield domain code improvements
gomesalexandre Jan 27, 2026
99c4280
fix: use inputToken symbol for deposit-related displays
gomesalexandre Jan 27, 2026
808a613
fix: add Claim button for withdrawable balances with CLAIM_UNSTAKED a…
gomesalexandre Jan 27, 2026
c066157
fix: show action-relevant info in exit/withdraw modal
gomesalexandre Jan 27, 2026
45836a2
fix: disable unstake button when no active balance to unstake
gomesalexandre Jan 27, 2026
98da651
merge: resolve conflict with origin/develop in yieldxyz utils
gomesalexandre Jan 27, 2026
defb87a
Merge branch 'develop' into feat_yield_full_toggle_3
gomesalexandre Jan 28, 2026
281082d
fix: use bnOrZero to select effective claim balance and add input tok…
gomesalexandre Jan 28, 2026
149e530
Merge remote-tracking branch 'origin/develop' into feat_yield_full_to…
gomesalexandre Jan 28, 2026
e99bd15
Merge branch 'develop' into feat_yield_full_toggle_3
gomesalexandre Jan 28, 2026
4c16ec0
Merge branch 'feat_yield_full_toggle_3' into feat_yield_unstaking_polish
gomesalexandre Jan 28, 2026
b2344fb
Merge remote-tracking branch 'origin/develop' into feat_yield_unstaki…
NeOMakinG Feb 10, 2026
15b4393
Merge branch 'develop' into feat_yield_unstaking_polish
gomesalexandre Feb 10, 2026
8527a2e
chore: trigger CI
gomesalexandre Feb 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/assets/translations/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -2802,6 +2802,7 @@
"depositsDisabledDescription": "Deposits are temporarily unavailable for this yield opportunity.",
"withdrawalsDisabled": "Withdrawals Disabled",
"withdrawalsDisabledDescription": "Withdrawals are temporarily unavailable for this yield opportunity.",
"noActiveBalanceToExit": "No active balance available to unstake.",
"noAvailableYields": "No yield opportunities available for your assets",
"connectWalletAvailable": "Connect a wallet to see yields available for your assets",
"aboutProvider": "About %{provider}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,11 @@ export const EarnConfirm = memo(() => {

{selectedYield && (
<Box mt={4}>
<YieldExplainers selectedYield={selectedYield} sellAssetSymbol={sellAsset?.symbol} />
<YieldExplainers
selectedYield={selectedYield}
sellAssetSymbol={sellAsset?.symbol}
action='enter'
/>
</Box>
)}

Expand Down
1 change: 1 addition & 0 deletions src/pages/Yields/components/YieldEnterModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,7 @@ export const YieldEnterModal = memo(
<YieldExplainers
selectedYield={yieldItem}
sellAssetSymbol={inputTokenAsset?.symbol}
action='enter'
/>
{stepsToShow.length > 0 && <TransactionStepsList steps={stepsToShow} />}
</Flex>
Expand Down
112 changes: 66 additions & 46 deletions src/pages/Yields/components/YieldExplainers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const infoIcon = <InfoIcon color='text.subtle' />
type ExplainerItem = {
icon: ReactNode
textKey: string
relevance: 'enter' | 'exit' | 'both'
}

const getYieldExplainers = (selectedYield: AugmentedYieldDto): ExplainerItem[] => {
Expand All @@ -29,31 +30,40 @@ const getYieldExplainers = (selectedYield: AugmentedYieldDto): ExplainerItem[] =
textKey: outputTokenSymbol
? 'earn.explainers.liquidStakingReceive'
: 'earn.explainers.liquidStakingTrade',
relevance: 'enter' as const,
},
{ icon: giftIcon, textKey: 'earn.explainers.rewardsSchedule', relevance: 'enter' as const },
{
icon: infoIcon,
textKey: 'earn.explainers.liquidStakingWithdraw',
relevance: 'both' as const,
},
{ icon: giftIcon, textKey: 'earn.explainers.rewardsSchedule' },
{ icon: infoIcon, textKey: 'earn.explainers.liquidStakingWithdraw' },
]
case 'native-staking':
case 'pooled-staking':
case 'staking':
return [
{ icon: giftIcon, textKey: 'earn.explainers.rewardsSchedule' },
{ icon: infoIcon, textKey: 'earn.explainers.stakingUnbonding' },
{ icon: giftIcon, textKey: 'earn.explainers.rewardsSchedule', relevance: 'enter' as const },
{ icon: infoIcon, textKey: 'earn.explainers.stakingUnbonding', relevance: 'both' as const },
]
case 'restaking':
return [
{ icon: giftIcon, textKey: 'earn.explainers.restakingYield' },
{ icon: infoIcon, textKey: 'earn.explainers.restakingWithdraw' },
{ icon: giftIcon, textKey: 'earn.explainers.restakingYield', relevance: 'enter' as const },
{
icon: infoIcon,
textKey: 'earn.explainers.restakingWithdraw',
relevance: 'both' as const,
},
]
case 'vault':
return [
{ icon: giftIcon, textKey: 'earn.explainers.vaultYield' },
{ icon: infoIcon, textKey: 'earn.explainers.vaultWithdraw' },
{ icon: giftIcon, textKey: 'earn.explainers.vaultYield', relevance: 'enter' as const },
{ icon: infoIcon, textKey: 'earn.explainers.vaultWithdraw', relevance: 'both' as const },
]
case 'lending':
return [
{ icon: giftIcon, textKey: 'earn.explainers.lendingYield' },
{ icon: infoIcon, textKey: 'earn.explainers.lendingWithdraw' },
{ icon: giftIcon, textKey: 'earn.explainers.lendingYield', relevance: 'enter' as const },
{ icon: infoIcon, textKey: 'earn.explainers.lendingWithdraw', relevance: 'both' as const },
]
default:
return []
Expand All @@ -63,48 +73,58 @@ const getYieldExplainers = (selectedYield: AugmentedYieldDto): ExplainerItem[] =
type YieldExplainersProps = {
selectedYield: AugmentedYieldDto
sellAssetSymbol?: string
action: 'enter' | 'exit' | 'claim'
}

export const YieldExplainers = memo(({ selectedYield, sellAssetSymbol }: YieldExplainersProps) => {
const translate = useTranslate()
export const YieldExplainers = memo(
({ selectedYield, sellAssetSymbol, action }: YieldExplainersProps) => {
const translate = useTranslate()

const explainers = useMemo(() => getYieldExplainers(selectedYield), [selectedYield])
const actionRelevance = action === 'enter' ? 'enter' : 'exit'
const explainers = useMemo(
() =>
getYieldExplainers(selectedYield).filter(
e => e.relevance === actionRelevance || e.relevance === 'both',
),
[selectedYield, actionRelevance],
)

const rewardSchedule = selectedYield.mechanics.rewardSchedule
const outputSymbol = selectedYield.outputToken?.symbol
const rewardSchedule = selectedYield.mechanics.rewardSchedule
const outputSymbol = selectedYield.outputToken?.symbol

const cooldownDays = useMemo(() => {
const seconds = selectedYield.mechanics.cooldownPeriod?.seconds
if (!seconds) return undefined
return Math.ceil(seconds / 86400)
}, [selectedYield.mechanics.cooldownPeriod?.seconds])
const cooldownDays = useMemo(() => {
const seconds = selectedYield.mechanics.cooldownPeriod?.seconds
if (!seconds) return undefined
return Math.ceil(seconds / 86400)
}, [selectedYield.mechanics.cooldownPeriod?.seconds])

const symbol = outputSymbol ?? sellAssetSymbol ?? ''
const symbol = outputSymbol ?? sellAssetSymbol ?? ''

const translatedExplainers = useMemo(() => {
if (explainers.length === 0) return []
return explainers.map(explainer => ({
icon: explainer.icon,
text: translate(explainer.textKey, {
symbol,
schedule: rewardSchedule ?? '',
days: cooldownDays ?? '',
}),
}))
}, [explainers, translate, symbol, rewardSchedule, cooldownDays])
const translatedExplainers = useMemo(() => {
if (explainers.length === 0) return []
return explainers.map(explainer => ({
icon: explainer.icon,
text: translate(explainer.textKey, {
symbol,
schedule: rewardSchedule ?? '',
days: cooldownDays ?? '',
}),
}))
}, [explainers, translate, symbol, rewardSchedule, cooldownDays])

if (translatedExplainers.length === 0) return null
if (translatedExplainers.length === 0) return null

return (
<VStack spacing={3} align='stretch'>
{translatedExplainers.map((explainer, index) => (
<HStack key={index} spacing={3} align='flex-start'>
<Box mt={0.5}>{explainer.icon}</Box>
<Text fontSize='sm' color='text.subtle'>
{explainer.text}
</Text>
</HStack>
))}
</VStack>
)
})
return (
<VStack spacing={3} align='stretch'>
{translatedExplainers.map((explainer, index) => (
<HStack key={index} spacing={3} align='flex-start'>
<Box mt={0.5}>{explainer.icon}</Box>
<Text fontSize='sm' color='text.subtle'>
{explainer.text}
</Text>
</HStack>
))}
</VStack>
)
},
)
41 changes: 25 additions & 16 deletions src/pages/Yields/components/YieldForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Icon,
Skeleton,
Text,
VStack,
} from '@chakra-ui/react'
import type { AccountId } from '@shapeshiftoss/caip'
import { useQueryClient } from '@tanstack/react-query'
Expand Down Expand Up @@ -515,23 +516,27 @@ export const YieldForm = memo(

const statsContent = useMemo(
() => (
<Box
<VStack
spacing={3}
align='stretch'
bg='background.surface.raised.base'
borderRadius='xl'
p={4}
borderWidth='1px'
borderColor='border.base'
>
<Flex justify='space-between' align='center'>
<Text fontSize='sm' color='text.subtle'>
{translate('yieldXYZ.currentApy')}
</Text>
<GradientApy fontSize='sm' fontWeight='bold'>
{apyDisplay}
</GradientApy>
</Flex>
{hasAmount && (
<Flex justify='space-between' align='center' mt={3}>
{action === 'enter' && (
<Flex justify='space-between' align='center'>
<Text fontSize='sm' color='text.subtle'>
{translate('yieldXYZ.currentApy')}
</Text>
<GradientApy fontSize='sm' fontWeight='bold'>
{apyDisplay}
</GradientApy>
</Flex>
)}
{action === 'enter' && hasAmount && (
<Flex justify='space-between' align='center'>
<Text fontSize='sm' color='text.subtle'>
{translate('yieldXYZ.estYearlyEarnings')}
</Text>
Expand All @@ -546,7 +551,7 @@ export const YieldForm = memo(
</Flex>
)}
{isStaking && maybeSelectedValidatorMetadata && (
<Flex justify='space-between' align='center' mt={3}>
<Flex justify='space-between' align='center'>
<Text fontSize='sm' color='text.subtle'>
{translate('yieldXYZ.validator')}
</Text>
Expand All @@ -563,7 +568,7 @@ export const YieldForm = memo(
</Flex>
)}
{(!isStaking || !maybeSelectedValidatorMetadata) && maybeProviderMetadata && (
<Flex justify='space-between' align='center' mt={3}>
<Flex justify='space-between' align='center'>
<Text fontSize='sm' color='text.subtle'>
{translate('yieldXYZ.provider')}
</Text>
Expand All @@ -580,7 +585,7 @@ export const YieldForm = memo(
</Flex>
)}
{minDeposit && bnOrZero(minDeposit).gt(0) && action === 'enter' && (
<Flex justify='space-between' align='center' mt={3}>
<Flex justify='space-between' align='center'>
<Text fontSize='sm' color='text.subtle'>
{translate(getYieldMinAmountKey(yieldItem.mechanics.type))}
</Text>
Expand All @@ -593,7 +598,7 @@ export const YieldForm = memo(
</Text>
</Flex>
)}
</Box>
</VStack>
),
[
translate,
Expand Down Expand Up @@ -757,7 +762,11 @@ export const YieldForm = memo(
)}
{!isClaimAction && statsContent}
{!isClaimAction && (
<YieldExplainers selectedYield={yieldItem} sellAssetSymbol={inputTokenAsset?.symbol} />
<YieldExplainers
selectedYield={yieldItem}
sellAssetSymbol={inputTokenAsset?.symbol}
action={action}
/>
)}
{stepsToShow.length > 0 && <TransactionStepsList steps={stepsToShow} />}
</Flex>
Expand Down
45 changes: 27 additions & 18 deletions src/pages/Yields/components/YieldPositionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
HStack,
Skeleton,
Text,
Tooltip,
VStack,
} from '@chakra-ui/react'
import { fromAccountId } from '@shapeshiftoss/caip'
Expand Down Expand Up @@ -105,6 +106,17 @@ export const YieldPositionCard = memo(
const withdrawableBalance = balancesByType?.[YieldBalanceType.Withdrawable]
const claimableBalance = balancesByType?.[YieldBalanceType.Claimable]

const hasActiveBalance = Boolean(
activeBalance && bnOrZero(activeBalance.aggregatedAmount).gt(0),
)

const isExitDisabled = !yieldItem.status.exit || !hasActiveBalance

const exitDisabledTitle = useMemo(() => {
if (!yieldItem.status.exit) return translate('yieldXYZ.withdrawalsDisabledDescription')
if (!hasActiveBalance) return translate('yieldXYZ.noActiveBalanceToExit')
}, [hasActiveBalance, translate, yieldItem.status.exit])

const claimAction = useMemo(
() =>
claimableBalance?.pendingActions?.find(action =>
Expand Down Expand Up @@ -505,24 +517,21 @@ export const YieldPositionCard = memo(
{enterLabel}
</Button>
{hasAnyPosition && (
<Button
leftIcon={exitIcon}
variant='outline'
size='lg'
height={12}
borderRadius='xl'
onClick={handleExit}
flex={1}
fontWeight='bold'
isDisabled={!yieldItem.status.exit}
title={
!yieldItem.status.exit
? translate('yieldXYZ.withdrawalsDisabledDescription')
: undefined
}
>
{exitLabel}
</Button>
<Tooltip label={exitDisabledTitle} isDisabled={!isExitDisabled} hasArrow>
<Button
leftIcon={exitIcon}
variant='outline'
size='lg'
height={12}
borderRadius='xl'
onClick={handleExit}
flex={1}
fontWeight='bold'
isDisabled={isExitDisabled}
>
{exitLabel}
</Button>
</Tooltip>
)}
</HStack>
</Display.Desktop>
Expand Down