From d0254d5cc335a9a2a324c6135fa5776874e1da04 Mon Sep 17 00:00:00 2001 From: gomes-bot Date: Tue, 17 Mar 2026 23:57:46 +0100 Subject: [PATCH 01/18] feat: chainflip lending dashboard revamp - Replace MyBalances with new Dashboard component with tabs (My Dashboard / Markets) - Add per-section cards: Free Balance, Supplied, Collateral, Borrowed with contextual CTAs - Add DashboardSidebar with Borrowing Power gauge and Next Steps card with decorative art - Add InitView for first-time users with Get Started hero, Lending Markets table, info cards - Add LoanHealth card with horizontal multi-segment LTV bar (Safe/Risky/Liquidation zones) - Revamp all confirm screens (Deposit, Supply, Withdraw, Borrow, Repay, Collateral, Egress) to use centered asset icon + amount, info rows, side-by-side Back/Confirm buttons - Add Pool APY and Current Position stats to Supply input screen - Add Borrow APR column to Markets table, reorder columns to match Figma - Add Lending Markets section title with description - Fix double button issue in empty state sections - Use outline button style with + prefix for CTAs per Figma - Add Asset/Balance column headers to Free Balance section - Update header to show user-specific summary cards when wallet connected - Add ~50 new translation keys Co-Authored-By: Claude Opus 4.6 (1M context) --- src/assets/translations/en/main.json | 74 +++ .../Pool/components/Borrow/BorrowConfirm.tsx | 108 +++- .../components/Borrow/CollateralConfirm.tsx | 124 ++-- .../Pool/components/Borrow/LtvGauge.tsx | 243 ++++---- .../Pool/components/Borrow/RepayConfirm.tsx | 130 +++- .../components/Deposit/DepositConfirm.tsx | 136 ++-- .../Pool/components/Egress/EgressConfirm.tsx | 114 ++-- .../Pool/components/Supply/SupplyConfirm.tsx | 128 +++- .../Pool/components/Supply/SupplyInput.tsx | 78 ++- .../components/Withdraw/WithdrawConfirm.tsx | 99 ++- .../components/ChainflipLendingHeader.tsx | 159 +++-- .../ChainflipLending/components/Dashboard.tsx | 61 ++ .../components/DashboardSections.tsx | 581 ++++++++++++++++++ .../components/DashboardSidebar.tsx | 264 ++++++++ .../ChainflipLending/components/InitView.tsx | 323 ++++++++++ .../components/LoanHealth.tsx | 102 +++ .../ChainflipLending/components/Markets.tsx | 151 +++-- .../components/MyBalances.tsx | 262 -------- 18 files changed, 2415 insertions(+), 722 deletions(-) create mode 100644 src/pages/ChainflipLending/components/Dashboard.tsx create mode 100644 src/pages/ChainflipLending/components/DashboardSections.tsx create mode 100644 src/pages/ChainflipLending/components/DashboardSidebar.tsx create mode 100644 src/pages/ChainflipLending/components/InitView.tsx create mode 100644 src/pages/ChainflipLending/components/LoanHealth.tsx delete mode 100644 src/pages/ChainflipLending/components/MyBalances.tsx diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 4aab16c207f..4190d11d01f 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -2451,6 +2451,7 @@ "manageLoan": "Manage Loan", "supply": { "title": "Supply", + "subtitle": "Allocate your free balance into lending pools to earn yield", "amount": "Amount", "available": "Available to supply", "availableTooltip": "Your free balance on Chainflip State Chain available for lending", @@ -2458,6 +2459,12 @@ "confirmTitle": "Confirm Supply", "confirmDescription": "Supply %{amount} %{asset} to the lending pool", "confirmAndSupply": "Confirm & Supply", + "poolApy": "Pool APY", + "currentPosition": "Current Position", + "autoCompounding": "Auto-compounding", + "enabled": "Enabled", + "destination": "Destination", + "lendingPool": "Lending Pool", "executingTitle": "Supplying...", "executingDescription": "Your supply is being processed", "successTitle": "Supply Successful", @@ -2474,6 +2481,7 @@ }, "borrow": { "title": "Borrow", + "subtitle": "Borrow assets against your collateral", "amount": "Amount", "available": "Available to borrow", "maxLtv": "Max LTV (80%)", @@ -2482,6 +2490,7 @@ "confirmTitle": "Confirm Borrow", "confirmDescription": "Borrow %{amount} %{asset} against your collateral", "confirmAndBorrow": "Confirm & Borrow", + "asset": "Asset", "executingTitle": "Borrowing...", "executingDescription": "Your loan is being processed", "successTitle": "Borrow Successful", @@ -2524,6 +2533,8 @@ }, "supplyApy": "Supply APY", "supplyApyTooltip": "Annual percentage yield earned by supplying assets to this pool.", + "borrowApr": "Borrow APR", + "borrowAprTooltip": "Annual percentage rate charged for borrowing from this pool.", "borrowRate": "Borrow Rate", "utilisation": "Utilisation", "utilisationTooltip": "Percentage of supplied assets currently being borrowed. Higher utilisation means higher yields but less available liquidity.", @@ -2538,6 +2549,8 @@ "add": "Add Collateral", "remove": "Remove Collateral", "amount": "Amount", + "asset": "Asset", + "action": "Action", "availableToAdd": "Available to add", "availableToRemove": "Available to remove", "confirmAddTitle": "Confirm Add Collateral", @@ -2577,9 +2590,12 @@ "connectWalletDescription": "Connect your wallet to view your lending positions.", "protocolStats": "Protocol Stats", "deposit": { + "subtitle": "Move assets from your wallet into your free balance", "amount": "Amount", "available": "Available", "explainer": "Assets will be deposited to your Chainflip State Chain account. Once deposited, you can supply to lending pools to earn yield.", + "asset": "Asset", + "recoveryAddress": "Recovery Address", "openChannel": "Open Deposit Channel", "confirmTitle": "Confirm Deposit", "confirmDescription": "Deposit %{amount} %{asset} to your Chainflip State Chain account. You will be guided through each required step and prompted to sign transactions as needed.", @@ -2627,8 +2643,10 @@ "myBalancesTitle": "Chainflip Lending - My Balances", "withdraw": { "title": "Withdraw", + "subtitle": "Withdraw assets from the lending pool back to your free balance", "amount": "Amount", "available": "Available to withdraw", + "destination": "Destination", "availableTooltip": "Your supply position in the lending pool", "confirmTitle": "Confirm Withdrawal", "confirmDescription": "Withdraw %{amount} %{asset} from the lending pool", @@ -2675,8 +2693,11 @@ }, "repay": { "title": "Repay", + "subtitle": "Repay outstanding loan balance", "amount": "Amount", + "asset": "Asset", "outstanding": "Outstanding debt", + "repaymentType": "Repayment Type", "fullRepayment": "Full repayment", "partialRepayment": "Partial repayment", "confirmTitle": "Confirm Repayment", @@ -2707,12 +2728,16 @@ "softLiquidation": "Soft Liquidation", "hardLiquidation": "Hard Liquidation", "safe": "Safe", + "risky": "Risky", + "liquidation": "Liquidation", "warning": "Warning", "danger": "Danger" }, "egress": { "title": "Withdraw from Chainflip", + "subtitle": "Withdraw from your free balance to your wallet", "amount": "Amount", + "asset": "Asset", "destination": "Destination Address", "destinationPlaceholder": "Enter destination address", "available": "Available Free Balance", @@ -2761,6 +2786,55 @@ "signing": "Sign transaction", "confirming": "Confirming on-chain" } + }, + "dashboard": { + "freeBalance": "Free Balance", + "supplied": "Supplied", + "collateral": "Collateral", + "borrowed": "Borrowed", + "loanHealth": "Loan Health", + "currentLtv": "Current LTV %{ltv}", + "liquidationDistance": "Liquidation Distance", + "borrowingPower": "Borrowing Power", + "available": "Available", + "yourNextSteps": "Your Next Steps", + "nextStepsSupplyOrCollateral": "Choose whether you want to start earning yield or provide collateral for borrowing.", + "nextStepsCollateral": "Provide collateral to start borrowing.", + "nextStepsBorrow": "You can now borrow against your collateral.", + "noEarningPositions": "No earning positions yet", + "noEarningPositionsDescription": "Supply from your free balance to start earning yield from borrower demand.", + "provideCollateral": "Provide collateral to start borrowing", + "provideCollateralDescription": "Move assets from your free balance into collateral. Your collateral determines how much you can borrow.", + "noActiveLoans": "No Active Loans", + "noActiveLoansDescription": "Post collateral first, then open borrow against it.", + "cantRepay": "Can't Repay?", + "startLiquidation": "Start Liquidation", + "getStarted": "Get Started", + "depositFirstAsset": "Deposit your first asset to get started", + "depositFirstAssetDescription": "Your first deposit handles everything - account creation, FLIP funding for State Chain gas, and a recovery address for the chain you deposit from. Future deposits skip all of this.", + "requiresFlip": "Requires 2 FLIP for one-time account funding", + "earnYield": "Earn Yield", + "earnYieldDescription": "Supply assets into a lending pool and earn variable APY. Yields auto-compound, no action needed after supplying.", + "borrowAgainstCollateral": "Borrow Against Collateral", + "borrowAgainstCollateralDescription": "Post BTC, ETH, or SOL as collateral and borrow stablecoins up to the target LTV.", + "estimatedYearlyEarnings": "Est. Yearly Earnings", + "freeBalanceTooltip": "Your available balance on Chainflip State Chain. Deposit from your wallet, or withdraw back.", + "suppliedTooltip": "Assets supplied to lending pools earning yield.", + "collateralTooltip": "Assets posted as collateral for borrowing.", + "borrowedTooltip": "Outstanding loan balances.", + "loanHealthTooltip": "Your loan-to-value ratio. Keep it below the target LTV to avoid liquidation.", + "borrowingPowerTooltip": "How much more you can borrow based on your collateral.", + "deposit": "Deposit", + "withdraw": "Withdraw", + "supply": "Supply", + "addCollateral": "Add Collateral", + "borrow": "Borrow", + "repay": "Repay", + "apy": "APY", + "borrowRate": "Borrow Rate", + "viewDashboard": "View Dashboard", + "lendingMarkets": "Lending Markets", + "lendingMarketsDescription": "Each pool is isolated per asset. Pick a market to see what you could earn or borrow before you deposit." } }, "chart": { diff --git a/src/pages/ChainflipLending/Pool/components/Borrow/BorrowConfirm.tsx b/src/pages/ChainflipLending/Pool/components/Borrow/BorrowConfirm.tsx index 974b7a5f61c..e139fa8aecd 100644 --- a/src/pages/ChainflipLending/Pool/components/Borrow/BorrowConfirm.tsx +++ b/src/pages/ChainflipLending/Pool/components/Borrow/BorrowConfirm.tsx @@ -1,10 +1,11 @@ import { ArrowForwardIcon, CheckCircleIcon } from '@chakra-ui/icons' -import { Button, CardBody, CardFooter, Flex, HStack, VStack } from '@chakra-ui/react' +import { Button, CardBody, CardFooter, Divider, Flex, HStack, VStack } from '@chakra-ui/react' import type { AssetId } from '@shapeshiftoss/caip' import { flipAssetId } from '@shapeshiftoss/caip' import { useQueryClient } from '@tanstack/react-query' import { memo, useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' +import { useNavigate } from 'react-router-dom' import { BorrowMachineCtx } from './BorrowMachineContext' import { BorrowStepper } from './BorrowStepper' @@ -30,6 +31,7 @@ type BorrowConfirmProps = { export const BorrowConfirm = memo(({ assetId }: BorrowConfirmProps) => { const translate = useTranslate() + const navigate = useNavigate() const queryClient = useQueryClient() const { scAccount } = useChainflipLendingAccount() @@ -89,6 +91,11 @@ export const BorrowConfirm = memo(({ assetId }: BorrowConfirmProps) => { closeModal() }, [scAccount, queryClient, closeModal]) + const handleViewDashboard = useCallback(() => { + closeModal() + navigate('/chainflip-lending') + }, [closeModal, navigate]) + const handleBack = useCallback(() => { actorRef.send({ type: 'BACK' }) }, [actorRef]) @@ -135,17 +142,29 @@ export const BorrowConfirm = memo(({ assetId }: BorrowConfirmProps) => { px={6} py={4} > - + + + + ) @@ -258,24 +277,42 @@ export const BorrowConfirm = memo(({ assetId }: BorrowConfirmProps) => { return ( - - - - - - - - {translate('chainflipLending.borrow.confirmTitle')} - - + + + + + + + + + {translate('chainflipLending.borrow.asset')} + + + {asset.name} + + + + + {translate('chainflipLending.borrow.amount')} + - - {translate('chainflipLending.borrow.projectedLtv')}: {projectedLtvPercent}% + + + + {translate('chainflipLending.borrow.projectedLtv')} + + + {projectedLtvPercent}% @@ -283,25 +320,32 @@ export const BorrowConfirm = memo(({ assetId }: BorrowConfirmProps) => { + - ) diff --git a/src/pages/ChainflipLending/Pool/components/Borrow/CollateralConfirm.tsx b/src/pages/ChainflipLending/Pool/components/Borrow/CollateralConfirm.tsx index 3723cbdd72d..fa25b638de6 100644 --- a/src/pages/ChainflipLending/Pool/components/Borrow/CollateralConfirm.tsx +++ b/src/pages/ChainflipLending/Pool/components/Borrow/CollateralConfirm.tsx @@ -1,10 +1,11 @@ import { ArrowForwardIcon, CheckCircleIcon } from '@chakra-ui/icons' -import { Button, CardBody, CardFooter, Flex, HStack, VStack } from '@chakra-ui/react' +import { Button, CardBody, CardFooter, Divider, Flex, HStack, VStack } from '@chakra-ui/react' import type { AssetId } from '@shapeshiftoss/caip' import { flipAssetId } from '@shapeshiftoss/caip' import { useQueryClient } from '@tanstack/react-query' import { memo, useCallback } from 'react' import { useTranslate } from 'react-polyglot' +import { useNavigate } from 'react-router-dom' import { CollateralMachineCtx } from './CollateralMachineContext' import { CollateralStepper } from './CollateralStepper' @@ -29,6 +30,7 @@ type CollateralConfirmProps = { export const CollateralConfirm = memo(({ assetId }: CollateralConfirmProps) => { const translate = useTranslate() + const navigate = useNavigate() const queryClient = useQueryClient() const { scAccount } = useChainflipLendingAccount() const { close: closeModal } = useModal('chainflipLending') @@ -73,6 +75,11 @@ export const CollateralConfirm = memo(({ assetId }: CollateralConfirmProps) => { closeModal() }, [scAccount, queryClient, closeModal]) + const handleViewDashboard = useCallback(() => { + closeModal() + navigate('/chainflip-lending') + }, [closeModal, navigate]) + const handleBack = useCallback(() => { actorRef.send({ type: 'BACK' }) }, [actorRef]) @@ -129,17 +136,29 @@ export const CollateralConfirm = memo(({ assetId }: CollateralConfirmProps) => { px={6} py={4} > - + + + + ) @@ -252,56 +271,74 @@ export const CollateralConfirm = memo(({ assetId }: CollateralConfirmProps) => { return ( - - - - - - - - - {translate( - isAddMode - ? 'chainflipLending.collateral.confirmAddTitle' - : 'chainflipLending.collateral.confirmRemoveTitle', - )} + + + + + + + + + {translate('chainflipLending.collateral.asset')} - - {translate( - isAddMode - ? 'chainflipLending.collateral.confirmAddDescription' - : 'chainflipLending.collateral.confirmRemoveDescription', - { - amount: collateralAmountCryptoPrecision, - asset: asset.symbol, - }, - )} + + {asset.name} + + + + + {translate('chainflipLending.collateral.amount')} - - + + + {translate('chainflipLending.collateral.action')} + + + {translate( + isAddMode + ? 'chainflipLending.collateral.add' + : 'chainflipLending.collateral.remove', + )} + + + - ) diff --git a/src/pages/ChainflipLending/Pool/components/Borrow/LtvGauge.tsx b/src/pages/ChainflipLending/Pool/components/Borrow/LtvGauge.tsx index 518e3e7e9e9..d5ce7f20eaf 100644 --- a/src/pages/ChainflipLending/Pool/components/Borrow/LtvGauge.tsx +++ b/src/pages/ChainflipLending/Pool/components/Borrow/LtvGauge.tsx @@ -1,5 +1,6 @@ -import { Box, Flex, Text, VStack } from '@chakra-ui/react' +import { Box, Circle, Flex, Icon, Text } from '@chakra-ui/react' import { memo, useMemo } from 'react' +import { TbSkull } from 'react-icons/tb' import { useTranslate } from 'react-polyglot' import { useChainflipLtvThresholds } from '@/pages/ChainflipLending/hooks/useChainflipLtvThresholds' @@ -9,161 +10,179 @@ type LtvGaugeProps = { projectedLtv?: number } -const GAUGE_HEIGHT = '12px' -const MARKER_HEIGHT = '24px' -const ltvToPercent = (ltv: number): string => `${Math.min(Math.max(ltv * 100, 0), 100)}%` +const GAUGE_HEIGHT = '10px' +const THUMB_SIZE = '20px' -const ltvToDisplayPercent = (ltv: number): string => - `${(Math.min(Math.max(ltv, 0), 1) * 100).toFixed(1)}%` +const DEFAULT_HARD_LIQUIDATION_LTV = 0.93 -const DEFAULT_SOFT_LIQUIDATION_LTV = 0.8 -const DEFAULT_HARD_LIQUIDATION_LTV = 0.9 +// Risky threshold sits between soft and hard liquidation - use topup or midpoint +const DEFAULT_RISKY_LTV = 0.8 -const getStatusColor = ( - ltv: number, - softLiquidationLtv: number, - hardLiquidationLtv: number, -): string => { +const ltvToPercent = (ltv: number): number => Math.min(Math.max(ltv * 100, 0), 100) + +const getStatusColor = (ltv: number, riskyLtv: number, hardLiquidationLtv: number): string => { if (ltv >= hardLiquidationLtv) return 'red.500' - if (ltv >= softLiquidationLtv) return 'yellow.500' + if (ltv >= riskyLtv) return 'yellow.500' return 'green.500' } -const getStatusKey = ( - ltv: number, - softLiquidationLtv: number, - hardLiquidationLtv: number, -): string => { - if (ltv >= hardLiquidationLtv) return 'chainflipLending.ltv.danger' - if (ltv >= softLiquidationLtv) return 'chainflipLending.ltv.warning' - return 'chainflipLending.ltv.safe' +type LegendItem = { + labelKey: string + color: string } +const legendItems: LegendItem[] = [ + { labelKey: 'chainflipLending.ltv.safe', color: 'green.500' }, + { labelKey: 'chainflipLending.ltv.risky', color: 'yellow.500' }, + { labelKey: 'chainflipLending.ltv.liquidation', color: 'red.500' }, +] + export const LtvGauge = memo(({ currentLtv, projectedLtv }: LtvGaugeProps) => { const translate = useTranslate() const { thresholds } = useChainflipLtvThresholds() - const softLiquidationLtv = thresholds?.softLiquidation ?? DEFAULT_SOFT_LIQUIDATION_LTV + // Zone boundaries: safe (0 -> risky), risky (risky -> hard liq), liquidation (hard liq -> 100%) + const riskyLtv = thresholds?.target ?? DEFAULT_RISKY_LTV const hardLiquidationLtv = thresholds?.hardLiquidation ?? DEFAULT_HARD_LIQUIDATION_LTV const statusColor = useMemo( - () => getStatusColor(currentLtv, softLiquidationLtv, hardLiquidationLtv), - [currentLtv, softLiquidationLtv, hardLiquidationLtv], + () => getStatusColor(currentLtv, riskyLtv, hardLiquidationLtv), + [currentLtv, riskyLtv, hardLiquidationLtv], ) - const statusKey = useMemo( - () => getStatusKey(currentLtv, softLiquidationLtv, hardLiquidationLtv), - [currentLtv, softLiquidationLtv, hardLiquidationLtv], + + const thumbPosition = useMemo(() => ltvToPercent(currentLtv), [currentLtv]) + + const projectedThumbPosition = useMemo( + () => (projectedLtv !== undefined ? ltvToPercent(projectedLtv) : undefined), + [projectedLtv], ) - const gaugeGradient = useMemo( - () => - `linear(to-r, green.500 0%, green.500 ${softLiquidationLtv * 100}%, yellow.500 ${ - softLiquidationLtv * 100 - }%, yellow.500 ${hardLiquidationLtv * 100}%, red.500 ${ - hardLiquidationLtv * 100 - }%, red.800 100%)`, - [softLiquidationLtv, hardLiquidationLtv], + // Segment widths as percentages + const safeWidth = useMemo(() => riskyLtv * 100, [riskyLtv]) + const riskyWidth = useMemo( + () => (hardLiquidationLtv - riskyLtv) * 100, + [hardLiquidationLtv, riskyLtv], ) + const liquidationWidth = useMemo(() => (1 - hardLiquidationLtv) * 100, [hardLiquidationLtv]) - const thresholdMarkers = useMemo(() => { - if (!thresholds) return [] - return [ - { - value: thresholds.target, - labelKey: 'chainflipLending.ltv.target', - color: 'green.500', - }, - { - value: thresholds.softLiquidation, - labelKey: 'chainflipLending.ltv.softLiquidation', - color: 'yellow.500', - }, - { - value: thresholds.hardLiquidation, - labelKey: 'chainflipLending.ltv.hardLiquidation', - color: 'red.500', - }, - ] - }, [thresholds]) + // Position of skull icon at the hard liquidation boundary + const skullPosition = useMemo(() => hardLiquidationLtv * 100, [hardLiquidationLtv]) return ( - - + {/* Bar track */} + - + {/* Safe segment (green) */} + + {/* Risky segment (yellow) */} + + {/* Liquidation segment (red) */} + + + + {/* Filled portion up to current LTV */} + = 100 ? 'full' : '0'} + overflow='hidden' + transition='width 0.3s ease' + > + + + + + + {/* Skull icon at hard liquidation boundary */} + > + + + + - {projectedLtv !== undefined && ( + {/* Projected LTV thumb (dashed circle) */} + {projectedThumbPosition !== undefined && ( + top='50%' + left={`${projectedThumbPosition}%`} + transform='translate(-50%, -50%)' + zIndex={3} + transition='left 0.3s ease' + > + + )} - {thresholdMarkers.map(marker => ( - + - ))} + - - - - {ltvToDisplayPercent(currentLtv)} - - - {translate(statusKey)} - - - - {thresholdMarkers.map(marker => ( - - - - {translate(marker.labelKey)} - - - ))} - - + {/* Percentage labels under the bar */} + + + 0% + + + 100% + + + + {/* Legend */} + + {legendItems.map(item => ( + + + + {translate(item.labelKey)} + + + ))} + ) }) diff --git a/src/pages/ChainflipLending/Pool/components/Borrow/RepayConfirm.tsx b/src/pages/ChainflipLending/Pool/components/Borrow/RepayConfirm.tsx index 59762893b31..6577f9bd2bc 100644 --- a/src/pages/ChainflipLending/Pool/components/Borrow/RepayConfirm.tsx +++ b/src/pages/ChainflipLending/Pool/components/Borrow/RepayConfirm.tsx @@ -1,10 +1,20 @@ import { ArrowForwardIcon, CheckCircleIcon } from '@chakra-ui/icons' -import { Badge, Button, CardBody, CardFooter, Flex, HStack, VStack } from '@chakra-ui/react' +import { + Badge, + Button, + CardBody, + CardFooter, + Divider, + Flex, + HStack, + VStack, +} from '@chakra-ui/react' import type { AssetId } from '@shapeshiftoss/caip' import { flipAssetId } from '@shapeshiftoss/caip' import { useQueryClient } from '@tanstack/react-query' import { memo, useCallback } from 'react' import { useTranslate } from 'react-polyglot' +import { useNavigate } from 'react-router-dom' import { useRepayActionCenter } from './hooks/useRepayActionCenter' import { useRepayConfirmation } from './hooks/useRepayConfirmation' @@ -29,6 +39,7 @@ type RepayConfirmProps = { export const RepayConfirm = memo(({ assetId }: RepayConfirmProps) => { const translate = useTranslate() + const navigate = useNavigate() const queryClient = useQueryClient() const { scAccount } = useChainflipLendingAccount() @@ -73,6 +84,11 @@ export const RepayConfirm = memo(({ assetId }: RepayConfirmProps) => { closeModal() }, [scAccount, queryClient, closeModal]) + const handleViewDashboard = useCallback(() => { + closeModal() + navigate('/chainflip-lending') + }, [closeModal, navigate]) + const handleBack = useCallback(() => { actorRef.send({ type: 'BACK' }) }, [actorRef]) @@ -126,17 +142,29 @@ export const RepayConfirm = memo(({ assetId }: RepayConfirmProps) => { px={6} py={4} > - + + + + ) @@ -249,50 +277,86 @@ export const RepayConfirm = memo(({ assetId }: RepayConfirmProps) => { return ( - - - - - - - - {translate('chainflipLending.repay.confirmTitle')} - - + + + + + + + + + {translate('chainflipLending.repay.asset')} + + + {asset.name} + + + + + {translate('chainflipLending.repay.amount')} + - {isFullRepayment && ( - {translate('chainflipLending.repay.fullRepayment')} - )} + + + + {translate('chainflipLending.repay.repaymentType')} + + + + {translate( + isFullRepayment + ? 'chainflipLending.repay.fullRepayment' + : 'chainflipLending.repay.partialRepayment', + )} + + {isFullRepayment && ( + + Full + + )} + + - ) diff --git a/src/pages/ChainflipLending/Pool/components/Deposit/DepositConfirm.tsx b/src/pages/ChainflipLending/Pool/components/Deposit/DepositConfirm.tsx index 00e9f2828c5..c83c2db8b3e 100644 --- a/src/pages/ChainflipLending/Pool/components/Deposit/DepositConfirm.tsx +++ b/src/pages/ChainflipLending/Pool/components/Deposit/DepositConfirm.tsx @@ -6,6 +6,7 @@ import { BigAmount } from '@shapeshiftoss/utils' import { skipToken, useQuery, useQueryClient } from '@tanstack/react-query' import { memo, useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' +import { useNavigate } from 'react-router-dom' import { DepositMachineCtx } from './DepositMachineContext' import { DepositStepper } from './DepositStepper' @@ -47,6 +48,7 @@ type DepositConfirmProps = { export const DepositConfirm = memo(({ assetId }: DepositConfirmProps) => { const translate = useTranslate() + const navigate = useNavigate() const queryClient = useQueryClient() const { accountNumber, scAccount } = useChainflipLendingAccount() const { close: closeModal } = useModal('chainflipLending') @@ -167,6 +169,11 @@ export const DepositConfirm = memo(({ assetId }: DepositConfirmProps) => { closeModal() }, [scAccount, queryClient, actorRef, closeModal]) + const handleViewDashboard = useCallback(() => { + closeModal() + navigate('/chainflip-lending') + }, [closeModal, navigate]) + const handleBack = useCallback(() => { actorRef.send({ type: 'BACK' }) }, [actorRef]) @@ -231,17 +238,29 @@ export const DepositConfirm = memo(({ assetId }: DepositConfirmProps) => { px={6} py={4} > - + + + + ) @@ -354,54 +373,92 @@ export const DepositConfirm = memo(({ assetId }: DepositConfirmProps) => { return ( - - - - - - - - {translate('chainflipLending.deposit.confirmTitle')} - - + + + + + + + + + {translate('chainflipLending.deposit.asset')} + + + {asset.name} + + + + + {translate('chainflipLending.deposit.amount')} + + + + + {translate('chainflipLending.deposit.freeBalance')} + + {effectiveRefundAddress && ( - <> - - - - {translate('chainflipLending.deposit.refundAddress.label')} + + + {translate('chainflipLending.deposit.recoveryAddress')} + + + + - - - - - - - + + )} + - ) diff --git a/src/pages/ChainflipLending/Pool/components/Egress/EgressConfirm.tsx b/src/pages/ChainflipLending/Pool/components/Egress/EgressConfirm.tsx index b3cd5205d8f..d2bf9fe4812 100644 --- a/src/pages/ChainflipLending/Pool/components/Egress/EgressConfirm.tsx +++ b/src/pages/ChainflipLending/Pool/components/Egress/EgressConfirm.tsx @@ -5,6 +5,7 @@ import { flipAssetId } from '@shapeshiftoss/caip' import { useQueryClient } from '@tanstack/react-query' import { memo, useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' +import { useNavigate } from 'react-router-dom' import { EgressMachineCtx } from './EgressMachineContext' import { EgressStepper } from './EgressStepper' @@ -31,6 +32,7 @@ type EgressConfirmProps = { export const EgressConfirm = memo(({ assetId }: EgressConfirmProps) => { const translate = useTranslate() + const navigate = useNavigate() const queryClient = useQueryClient() const { scAccount } = useChainflipLendingAccount() const { close: closeModal } = useModal('chainflipLending') @@ -80,6 +82,11 @@ export const EgressConfirm = memo(({ assetId }: EgressConfirmProps) => { closeModal() }, [scAccount, queryClient, closeModal]) + const handleViewDashboard = useCallback(() => { + closeModal() + navigate('/chainflip-lending') + }, [closeModal, navigate]) + const handleBack = useCallback(() => { actorRef.send({ type: 'BACK' }) }, [actorRef]) @@ -144,17 +151,29 @@ export const EgressConfirm = memo(({ assetId }: EgressConfirmProps) => { px={6} py={4} > - + + + + ) @@ -267,60 +286,79 @@ export const EgressConfirm = memo(({ assetId }: EgressConfirmProps) => { return ( - - - - - - - - {translate('chainflipLending.egress.confirmTitle')} - + + + + + + + + {translate('chainflipLending.egress.asset')} + + + {asset.name} + + + + + {translate('chainflipLending.egress.amount')} + + + {destinationAddress && ( - <> - - - - {translate('chainflipLending.egress.receiveAddress')} + + + {translate('chainflipLending.egress.receiveAddress')} + + + + - - - - - - - + + )} + - ) diff --git a/src/pages/ChainflipLending/Pool/components/Supply/SupplyConfirm.tsx b/src/pages/ChainflipLending/Pool/components/Supply/SupplyConfirm.tsx index 18a18f7d673..d9037bcd0c3 100644 --- a/src/pages/ChainflipLending/Pool/components/Supply/SupplyConfirm.tsx +++ b/src/pages/ChainflipLending/Pool/components/Supply/SupplyConfirm.tsx @@ -1,10 +1,11 @@ import { ArrowForwardIcon, CheckCircleIcon } from '@chakra-ui/icons' -import { Button, CardBody, CardFooter, Flex, HStack, VStack } from '@chakra-ui/react' +import { Button, CardBody, CardFooter, Divider, Flex, HStack, VStack } from '@chakra-ui/react' import type { AssetId } from '@shapeshiftoss/caip' import { flipAssetId } from '@shapeshiftoss/caip' import { useQueryClient } from '@tanstack/react-query' -import { memo, useCallback } from 'react' +import { memo, useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' +import { useNavigate } from 'react-router-dom' import { useSupplyActionCenter } from './hooks/useSupplyActionCenter' import { useSupplyConfirmation } from './hooks/useSupplyConfirmation' @@ -18,7 +19,9 @@ import { CircularProgress } from '@/components/CircularProgress/CircularProgress import { SlideTransition } from '@/components/SlideTransition' import { RawText } from '@/components/Text' import { useModal } from '@/hooks/useModal/useModal' +import { bnOrZero } from '@/lib/bignumber/bignumber' import { useChainflipLendingAccount } from '@/pages/ChainflipLending/ChainflipLendingAccountContext' +import { useChainflipLendingPools } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools' import { reactQueries } from '@/react-queries' import { selectAssetById } from '@/state/slices/selectors' import { useAppSelector } from '@/state/store' @@ -29,6 +32,7 @@ type SupplyConfirmProps = { export const SupplyConfirm = memo(({ assetId }: SupplyConfirmProps) => { const translate = useTranslate() + const navigate = useNavigate() const queryClient = useQueryClient() const { scAccount } = useChainflipLendingAccount() const { close: closeModal } = useModal('chainflipLending') @@ -47,6 +51,13 @@ export const SupplyConfirm = memo(({ assetId }: SupplyConfirmProps) => { const stepConfirmed = SupplyMachineCtx.useSelector(s => s.context.stepConfirmed) const isConfirming = SupplyMachineCtx.useSelector(s => s.matches('confirming')) + const { pools } = useChainflipLendingPools() + + const supplyApyPercent = useMemo(() => { + const pool = pools.find(p => p.assetId === assetId) + return pool ? bnOrZero(pool.supplyApy).times(100).toFixed(2) : null + }, [pools, assetId]) + useSupplySign() useSupplyConfirmation() useSupplyActionCenter() @@ -72,6 +83,11 @@ export const SupplyConfirm = memo(({ assetId }: SupplyConfirmProps) => { closeModal() }, [scAccount, queryClient, actorRef, closeModal]) + const handleViewDashboard = useCallback(() => { + closeModal() + navigate('/chainflip-lending') + }, [closeModal, navigate]) + const handleBack = useCallback(() => { actorRef.send({ type: 'BACK' }) }, [actorRef]) @@ -117,17 +133,29 @@ export const SupplyConfirm = memo(({ assetId }: SupplyConfirmProps) => { px={6} py={4} > - + + + + ) @@ -240,47 +268,85 @@ export const SupplyConfirm = memo(({ assetId }: SupplyConfirmProps) => { return ( - - - - - - - - {translate('chainflipLending.supply.confirmTitle')} - - + + + + + + + + + {translate('chainflipLending.supply.amount')} + + {supplyApyPercent !== null && ( + + + {translate('chainflipLending.supply.poolApy')} + + + {supplyApyPercent}% + + + )} + + + {translate('chainflipLending.supply.destination')} + + + {translate('chainflipLending.supply.lendingPool')} + + + + + {translate('chainflipLending.supply.autoCompounding')} + + + {translate('chainflipLending.supply.enabled')} + + + - ) diff --git a/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx b/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx index 2d5ca4de85d..b8cc875f525 100644 --- a/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx +++ b/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx @@ -1,4 +1,4 @@ -import { Button, CardBody, CardFooter, Flex, Stack, VStack } from '@chakra-ui/react' +import { Button, CardBody, CardFooter, Divider, Flex, Stack, VStack } from '@chakra-ui/react' import type { AssetId } from '@shapeshiftoss/caip' import type { Asset } from '@shapeshiftoss/types' import { BigAmount } from '@shapeshiftoss/utils' @@ -19,7 +19,9 @@ import { useModal } from '@/hooks/useModal/useModal' import { useToggle } from '@/hooks/useToggle/useToggle' import { bnOrZero } from '@/lib/bignumber/bignumber' import { CHAINFLIP_LENDING_ASSET_BY_ASSET_ID } from '@/lib/chainflip/constants' +import { useChainflipLendingPools } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools' import { useChainflipMinimumSupply } from '@/pages/ChainflipLending/hooks/useChainflipMinimumSupply' +import { useChainflipSupplyPositions } from '@/pages/ChainflipLending/hooks/useChainflipSupplyPositions' import { selectMarketDataByAssetIdUserCurrency } from '@/state/slices/marketDataSlice/selectors' import { allowedDecimalSeparators } from '@/state/slices/preferencesSlice/preferencesSlice' import { selectAssetById, selectAssets } from '@/state/slices/selectors' @@ -79,6 +81,28 @@ export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => { const cryptoValue = isFiat ? cryptoFromFiat : inputValue const fiatValue = isFiat ? inputValue : fiatFromCrypto + const { pools } = useChainflipLendingPools() + const { supplyPositions } = useChainflipSupplyPositions() + + const poolForAsset = useMemo(() => pools.find(p => p.assetId === assetId), [pools, assetId]) + + const currentPositionCrypto = useMemo(() => { + const position = supplyPositions.find(p => p.assetId === assetId) + return position?.totalAmountCryptoPrecision ?? '0' + }, [supplyPositions, assetId]) + + const supplyApyPercent = useMemo( + () => (poolForAsset ? bnOrZero(poolForAsset.supplyApy).times(100).toFixed(2) : null), + [poolForAsset], + ) + + const estYearlyEarningsFiat = useMemo(() => { + if (!poolForAsset || !marketData?.price) return null + const apyDecimal = bnOrZero(poolForAsset.supplyApy) + if (apyDecimal.isZero()) return null + return bnOrZero(cryptoValue).times(marketData.price).times(apyDecimal).toFixed(2) + }, [poolForAsset, cryptoValue, marketData?.price]) + const { minSupply, isLoading: isMinSupplyLoading } = useChainflipMinimumSupply(assetId) const isBelowMinimum = useMemo(() => { @@ -207,6 +231,34 @@ export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => { mb={0} /> + {(supplyApyPercent !== null || bnOrZero(currentPositionCrypto).gt(0)) && ( + + {supplyApyPercent !== null && ( + + + {translate('chainflipLending.supply.poolApy')} + + + {supplyApyPercent}% + + + )} + {bnOrZero(currentPositionCrypto).gt(0) && ( + + + {translate('chainflipLending.supply.currentPosition')} + + + + )} + + )} + {translate('chainflipLending.supply.amount')} @@ -293,6 +345,30 @@ export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => { + {supplyApyPercent !== null && ( + <> + + + + + {translate('chainflipLending.dashboard.apy')} + + + {supplyApyPercent}% + + + {estYearlyEarningsFiat !== null && !bnOrZero(cryptoValue).isZero() && ( + + + {translate('chainflipLending.dashboard.estimatedYearlyEarnings')} + + + + )} + + + )} + {!hasFreeBalance && ( {translate('chainflipLending.supply.noFreeBalance')} diff --git a/src/pages/ChainflipLending/Pool/components/Withdraw/WithdrawConfirm.tsx b/src/pages/ChainflipLending/Pool/components/Withdraw/WithdrawConfirm.tsx index 1034d8f6fa6..ecef81d3a30 100644 --- a/src/pages/ChainflipLending/Pool/components/Withdraw/WithdrawConfirm.tsx +++ b/src/pages/ChainflipLending/Pool/components/Withdraw/WithdrawConfirm.tsx @@ -1,10 +1,11 @@ import { ArrowForwardIcon, CheckCircleIcon } from '@chakra-ui/icons' -import { Button, CardBody, CardFooter, Flex, HStack, VStack } from '@chakra-ui/react' +import { Button, CardBody, CardFooter, Divider, Flex, HStack, VStack } from '@chakra-ui/react' import type { AssetId } from '@shapeshiftoss/caip' import { flipAssetId } from '@shapeshiftoss/caip' import { useQueryClient } from '@tanstack/react-query' import { memo, useCallback } from 'react' import { useTranslate } from 'react-polyglot' +import { useNavigate } from 'react-router-dom' import { useWithdrawActionCenter } from './hooks/useWithdrawActionCenter' import { useWithdrawConfirmation } from './hooks/useWithdrawConfirmation' @@ -29,6 +30,7 @@ type WithdrawConfirmProps = { export const WithdrawConfirm = memo(({ assetId }: WithdrawConfirmProps) => { const translate = useTranslate() + const navigate = useNavigate() const queryClient = useQueryClient() const { scAccount } = useChainflipLendingAccount() const { close: closeModal } = useModal('chainflipLending') @@ -71,6 +73,11 @@ export const WithdrawConfirm = memo(({ assetId }: WithdrawConfirmProps) => { closeModal() }, [scAccount, queryClient, closeModal]) + const handleViewDashboard = useCallback(() => { + closeModal() + navigate('/chainflip-lending') + }, [closeModal, navigate]) + const handleBack = useCallback(() => { actorRef.send({ type: 'BACK' }) }, [actorRef]) @@ -116,17 +123,29 @@ export const WithdrawConfirm = memo(({ assetId }: WithdrawConfirmProps) => { px={6} py={4} > - + + + + ) @@ -239,47 +258,67 @@ export const WithdrawConfirm = memo(({ assetId }: WithdrawConfirmProps) => { return ( - - - - - - - - {translate('chainflipLending.withdraw.confirmTitle')} - - + + + + + + + + + {translate('chainflipLending.withdraw.amount')} + + + + {translate('chainflipLending.withdraw.destination')} + + + {translate('chainflipLending.freeBalance')} + + + - ) diff --git a/src/pages/ChainflipLending/components/ChainflipLendingHeader.tsx b/src/pages/ChainflipLending/components/ChainflipLendingHeader.tsx index d1d16a66570..00dc8169479 100644 --- a/src/pages/ChainflipLending/components/ChainflipLendingHeader.tsx +++ b/src/pages/ChainflipLending/components/ChainflipLendingHeader.tsx @@ -1,6 +1,6 @@ import { Button, Card, CardBody, Container, Flex, Heading, Skeleton, Stack } from '@chakra-ui/react' import { ethAssetId } from '@shapeshiftoss/caip' -import { useCallback } from 'react' +import { useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' import { useNavigate } from 'react-router-dom' @@ -12,12 +12,46 @@ import { PageBackButton, PageHeader } from '@/components/Layout/Header/PageHeade import { Text } from '@/components/Text' import { WalletActions } from '@/context/WalletProvider/actions' import { useWallet } from '@/hooks/useWallet/useWallet' +import { bnOrZero } from '@/lib/bignumber/bignumber' import { useChainflipLendingAccount } from '@/pages/ChainflipLending/ChainflipLendingAccountContext' +import { useChainflipFreeBalances } from '@/pages/ChainflipLending/hooks/useChainflipFreeBalances' import { useChainflipLendingPools } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools' +import { useChainflipLoanAccount } from '@/pages/ChainflipLending/hooks/useChainflipLoanAccount' +import { useChainflipSupplyPositions } from '@/pages/ChainflipLending/hooks/useChainflipSupplyPositions' const responsiveFlex = { base: 'auto', lg: 1 } const containerPaddingTop = { base: 0, md: 8 } +type SummaryCardProps = { + value: string + labelKey: string + tooltipKey: string + isLoading: boolean + labelColor?: string +} + +const SummaryCard = ({ value, labelKey, tooltipKey, isLoading, labelColor }: SummaryCardProps) => { + const translate = useTranslate() + + return ( + + + + + + + + + + + ) +} + export const ChainflipLendingHeader = () => { const translate = useTranslate() const navigate = useNavigate() @@ -33,8 +67,30 @@ export const ChainflipLendingHeader = () => { [walletDispatch], ) - const { totalSuppliedFiat, availableLiquidityFiat, totalBorrowedFiat, isLoading } = - useChainflipLendingPools() + const { + totalSuppliedFiat, + availableLiquidityFiat, + totalBorrowedFiat, + isLoading: isPoolsLoading, + } = useChainflipLendingPools() + + const { totalFiat: freeBalanceTotalFiat, isLoading: isFreeBalancesLoading } = + useChainflipFreeBalances() + + const { supplyPositions, isLoading: isPositionsLoading } = useChainflipSupplyPositions() + + const suppliedTotalFiat = useMemo( + () => supplyPositions.reduce((sum, p) => sum.plus(p.totalAmountFiat), bnOrZero(0)).toFixed(2), + [supplyPositions], + ) + + const { + totalCollateralFiat, + totalBorrowedFiat: userBorrowedFiat, + isLoading: isLoanLoading, + } = useChainflipLoanAccount() + + const isUserDataLoading = isFreeBalancesLoading || isPositionsLoading || isLoanLoading return ( <> @@ -65,48 +121,61 @@ export const ChainflipLendingHeader = () => { - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {accountId ? ( + <> + + + + + + ) : ( + <> + + + + + )} {!accountId && ( diff --git a/src/pages/ChainflipLending/components/Dashboard.tsx b/src/pages/ChainflipLending/components/Dashboard.tsx new file mode 100644 index 00000000000..985483ef2fe --- /dev/null +++ b/src/pages/ChainflipLending/components/Dashboard.tsx @@ -0,0 +1,61 @@ +import { Flex, Stack } from '@chakra-ui/react' +import { memo, useMemo } from 'react' + +import { bnOrZero } from '@/lib/bignumber/bignumber' +import { + BorrowedSection, + CollateralSection, + FreeBalanceSection, + SuppliedSection, +} from '@/pages/ChainflipLending/components/DashboardSections' +import { DashboardSidebar } from '@/pages/ChainflipLending/components/DashboardSidebar' +import { InitView } from '@/pages/ChainflipLending/components/InitView' +import { LoanHealth } from '@/pages/ChainflipLending/components/LoanHealth' +import { useChainflipAccount } from '@/pages/ChainflipLending/hooks/useChainflipAccount' +import { useChainflipFreeBalances } from '@/pages/ChainflipLending/hooks/useChainflipFreeBalances' +import { useChainflipLoanAccount } from '@/pages/ChainflipLending/hooks/useChainflipLoanAccount' +import { useChainflipSupplyPositions } from '@/pages/ChainflipLending/hooks/useChainflipSupplyPositions' + +export const Dashboard = memo(() => { + const { isFunded, isLpRegistered } = useChainflipAccount() + const { freeBalances } = useChainflipFreeBalances() + const { supplyPositions } = useChainflipSupplyPositions() + const { collateralWithFiat, loansWithFiat } = useChainflipLoanAccount() + + const hasFreeBalance = useMemo( + () => freeBalances.some(b => b.assetId && bnOrZero(b.balanceCryptoPrecision).gt(0)), + [freeBalances], + ) + + const hasAnyPosition = useMemo( + () => + hasFreeBalance || + supplyPositions.length > 0 || + collateralWithFiat.length > 0 || + loansWithFiat.length > 0, + [hasFreeBalance, supplyPositions, collateralWithFiat, loansWithFiat], + ) + + // Show init view for users with no positions (wallet is connected since Markets.tsx handles no-wallet) + const showInitView = useMemo(() => { + if (!isFunded && !isLpRegistered) return true + return !hasAnyPosition + }, [isFunded, isLpRegistered, hasAnyPosition]) + + if (showInitView) { + return + } + + return ( + + + + + + + + + + + ) +}) diff --git a/src/pages/ChainflipLending/components/DashboardSections.tsx b/src/pages/ChainflipLending/components/DashboardSections.tsx new file mode 100644 index 00000000000..c619d7aa526 --- /dev/null +++ b/src/pages/ChainflipLending/components/DashboardSections.tsx @@ -0,0 +1,581 @@ +import { Button, Card, CardBody, Flex, HStack, Skeleton, Stack } from '@chakra-ui/react' +import type { AssetId } from '@shapeshiftoss/caip' +import { memo, useCallback, useMemo } from 'react' +import { useTranslate } from 'react-polyglot' + +import { Amount } from '@/components/Amount/Amount' +import { AssetIcon } from '@/components/AssetIcon' +import { HelperTooltip } from '@/components/HelperTooltip/HelperTooltip' +import { RawText, Text } from '@/components/Text' +import { useModal } from '@/hooks/useModal/useModal' +import { bnOrZero } from '@/lib/bignumber/bignumber' +import { CHAINFLIP_LENDING_ASSET_BY_ASSET_ID } from '@/lib/chainflip/constants' +import type { ChainflipFreeBalanceWithFiat } from '@/pages/ChainflipLending/hooks/useChainflipFreeBalances' +import { useChainflipFreeBalances } from '@/pages/ChainflipLending/hooks/useChainflipFreeBalances' +import type { ChainflipLendingPoolWithFiat } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools' +import { useChainflipLendingPools } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools' +import type { + CollateralWithFiat, + LoanWithFiat, +} from '@/pages/ChainflipLending/hooks/useChainflipLoanAccount' +import { useChainflipLoanAccount } from '@/pages/ChainflipLending/hooks/useChainflipLoanAccount' +import type { ChainflipSupplyPositionWithFiat } from '@/pages/ChainflipLending/hooks/useChainflipSupplyPositions' +import { useChainflipSupplyPositions } from '@/pages/ChainflipLending/hooks/useChainflipSupplyPositions' +import { selectAssetById } from '@/state/slices/assetsSlice/selectors' +import { useAppSelector } from '@/state/store' + +const LENDING_ASSET_IDS = Object.keys(CHAINFLIP_LENDING_ASSET_BY_ASSET_ID) as AssetId[] + +type AssetRowProps = { + assetId: AssetId + children: React.ReactNode +} + +const AssetRow = ({ assetId, children }: AssetRowProps) => { + const asset = useAppSelector(state => selectAssetById(state, assetId)) + if (!asset) return null + + return ( + + + + {asset.name} + + {children} + + ) +} + +type SectionHeaderProps = { + titleKey: string + tooltipKey: string + totalFiat: string + isLoading: boolean + primaryAction?: { labelKey: string; handleClick: () => void } + secondaryAction?: { labelKey: string; handleClick: () => void } +} + +const SectionHeader = ({ + titleKey, + tooltipKey, + totalFiat, + isLoading, + primaryAction, + secondaryAction, +}: SectionHeaderProps) => { + const translate = useTranslate() + + return ( + + + + + + + + + + + {primaryAction && ( + + )} + {secondaryAction && ( + + )} + + + ) +} + +type EmptyStateProps = { + titleKey: string + descriptionKey: string + actionLabelKey: string + onAction: () => void +} + +const EmptyState = ({ titleKey, descriptionKey, actionLabelKey, onAction }: EmptyStateProps) => { + const translate = useTranslate() + + return ( + + + + + + ) +} + +// Free Balance Section +const FreeBalanceRow = ({ balance }: { balance: ChainflipFreeBalanceWithFiat }) => { + const assetId = balance.assetId + const asset = useAppSelector(state => (assetId ? selectAssetById(state, assetId) : undefined)) + if (!assetId || bnOrZero(balance.balanceCryptoPrecision).isZero()) return null + + return ( + + + + + + + ) +} + +export const FreeBalanceSection = memo(() => { + const chainflipLendingModal = useModal('chainflipLending') + const { freeBalances, isLoading, totalFiat } = useChainflipFreeBalances() + + const nonZeroBalances = useMemo( + () => freeBalances.filter(b => b.assetId && bnOrZero(b.balanceCryptoPrecision).gt(0)), + [freeBalances], + ) + + const handleDeposit = useCallback(() => { + const firstAssetId = LENDING_ASSET_IDS[0] + if (firstAssetId) chainflipLendingModal.open({ mode: 'deposit', assetId: firstAssetId }) + }, [chainflipLendingModal]) + + const handleWithdraw = useCallback(() => { + const firstWithBalance = nonZeroBalances[0]?.assetId ?? LENDING_ASSET_IDS[0] + if (firstWithBalance) { + chainflipLendingModal.open({ mode: 'withdrawFromChainflip', assetId: firstWithBalance }) + } + }, [chainflipLendingModal, nonZeroBalances]) + + return ( + + + + 0 + ? { + labelKey: 'chainflipLending.dashboard.withdraw', + handleClick: handleWithdraw, + } + : undefined + } + /> + {isLoading ? ( + + {Array.from({ length: 3 }).map((_, i) => ( + + ))} + + ) : nonZeroBalances.length > 0 ? ( + + + Asset + Balance + + }> + {nonZeroBalances.map(balance => + balance.assetId ? ( + + ) : null, + )} + + + ) : null} + + + + ) +}) + +// Supplied Section +const SuppliedRow = ({ + position, + apy, +}: { + position: ChainflipSupplyPositionWithFiat + apy: string +}) => { + const asset = useAppSelector(state => selectAssetById(state, position.assetId)) + + return ( + + + + + + + + + + ) +} + +export const SuppliedSection = memo(() => { + const chainflipLendingModal = useModal('chainflipLending') + const { supplyPositions, isLoading } = useChainflipSupplyPositions() + const { pools } = useChainflipLendingPools() + + const totalSuppliedFiat = useMemo( + () => supplyPositions.reduce((sum, p) => sum.plus(p.totalAmountFiat), bnOrZero(0)).toFixed(2), + [supplyPositions], + ) + + const poolsByAssetId = useMemo( + () => + pools.reduce>>((acc, pool) => { + if (pool.assetId) acc[pool.assetId] = pool + return acc + }, {}), + [pools], + ) + + const handleSupply = useCallback(() => { + const firstAssetId = LENDING_ASSET_IDS[0] + if (firstAssetId) chainflipLendingModal.open({ mode: 'supply', assetId: firstAssetId }) + }, [chainflipLendingModal]) + + const handleWithdraw = useCallback(() => { + const firstPosition = supplyPositions[0] + if (firstPosition) { + chainflipLendingModal.open({ mode: 'withdrawSupply', assetId: firstPosition.assetId }) + } + }, [chainflipLendingModal, supplyPositions]) + + return ( + + + + 0 + ? { + labelKey: 'chainflipLending.dashboard.deposit', + handleClick: handleSupply, + } + : undefined + } + secondaryAction={ + supplyPositions.length > 0 + ? { + labelKey: 'chainflipLending.dashboard.withdraw', + handleClick: handleWithdraw, + } + : undefined + } + /> + {isLoading ? ( + + {Array.from({ length: 3 }).map((_, i) => ( + + ))} + + ) : supplyPositions.length > 0 ? ( + + + Asset + + + Amount + + + }> + {supplyPositions.map(position => ( + + ))} + + + ) : ( + + )} + + + + ) +}) + +// Collateral Section +const CollateralRow = ({ collateral }: { collateral: CollateralWithFiat }) => { + const asset = useAppSelector(state => selectAssetById(state, collateral.assetId)) + + return ( + + + + + + + ) +} + +export const CollateralSection = memo(() => { + const chainflipLendingModal = useModal('chainflipLending') + const { collateralWithFiat, totalCollateralFiat, isLoading } = useChainflipLoanAccount() + + const handleAddCollateral = useCallback(() => { + const firstAssetId = LENDING_ASSET_IDS[0] + if (firstAssetId) chainflipLendingModal.open({ mode: 'addCollateral', assetId: firstAssetId }) + }, [chainflipLendingModal]) + + const handleRemoveCollateral = useCallback(() => { + const firstCollateral = collateralWithFiat[0] + if (firstCollateral) { + chainflipLendingModal.open({ mode: 'removeCollateral', assetId: firstCollateral.assetId }) + } + }, [chainflipLendingModal, collateralWithFiat]) + + return ( + + + + 0 + ? { + labelKey: 'chainflipLending.dashboard.deposit', + handleClick: handleAddCollateral, + } + : undefined + } + secondaryAction={ + collateralWithFiat.length > 0 + ? { + labelKey: 'chainflipLending.dashboard.withdraw', + handleClick: handleRemoveCollateral, + } + : undefined + } + /> + {isLoading ? ( + + {Array.from({ length: 3 }).map((_, i) => ( + + ))} + + ) : collateralWithFiat.length > 0 ? ( + }> + {collateralWithFiat.map(collateral => ( + + ))} + + ) : ( + + )} + + + + ) +}) + +// Borrowed Section +const BorrowedRow = ({ loan, borrowRate }: { loan: LoanWithFiat; borrowRate: string }) => { + const asset = useAppSelector(state => selectAssetById(state, loan.assetId)) + + return ( + + + + + + + + + + ) +} + +export const BorrowedSection = memo(() => { + const translate = useTranslate() + const chainflipLendingModal = useModal('chainflipLending') + const { loansWithFiat, totalBorrowedFiat, collateralWithFiat, isLoading } = + useChainflipLoanAccount() + const { pools } = useChainflipLendingPools() + + const poolsByAssetId = useMemo( + () => + pools.reduce>>((acc, pool) => { + if (pool.assetId) acc[pool.assetId] = pool + return acc + }, {}), + [pools], + ) + + const handleBorrow = useCallback(() => { + const firstAssetId = LENDING_ASSET_IDS[0] + if (firstAssetId) chainflipLendingModal.open({ mode: 'borrow', assetId: firstAssetId }) + }, [chainflipLendingModal]) + + const handleRepay = useCallback(() => { + const firstLoan = loansWithFiat[0] + if (firstLoan) { + chainflipLendingModal.open({ + mode: 'repay', + assetId: firstLoan.assetId, + loanId: firstLoan.loanId, + }) + } + }, [chainflipLendingModal, loansWithFiat]) + + const handleAddCollateral = useCallback(() => { + const firstAssetId = LENDING_ASSET_IDS[0] + if (firstAssetId) chainflipLendingModal.open({ mode: 'addCollateral', assetId: firstAssetId }) + }, [chainflipLendingModal]) + + const handleVoluntaryLiquidation = useCallback(() => { + const firstLoan = loansWithFiat[0] + if (firstLoan) { + chainflipLendingModal.open({ + mode: 'voluntaryLiquidation', + assetId: firstLoan.assetId, + liquidationAction: 'initiate', + }) + } + }, [chainflipLendingModal, loansWithFiat]) + + return ( + + + + 0 + ? { + labelKey: 'chainflipLending.dashboard.borrow', + handleClick: handleBorrow, + } + : undefined + } + secondaryAction={ + loansWithFiat.length > 0 + ? { + labelKey: 'chainflipLending.dashboard.repay', + handleClick: handleRepay, + } + : undefined + } + /> + {isLoading ? ( + + {Array.from({ length: 3 }).map((_, i) => ( + + ))} + + ) : loansWithFiat.length > 0 ? ( + <> + + + Asset + + + Amount + + + } + > + {loansWithFiat.map(loan => ( + + ))} + + + + + ) : ( + + )} + + + + ) +}) diff --git a/src/pages/ChainflipLending/components/DashboardSidebar.tsx b/src/pages/ChainflipLending/components/DashboardSidebar.tsx new file mode 100644 index 00000000000..1affb6a434a --- /dev/null +++ b/src/pages/ChainflipLending/components/DashboardSidebar.tsx @@ -0,0 +1,264 @@ +import { + Box, + Button, + Card, + CardBody, + Center, + CircularProgress, + CircularProgressLabel, + Flex, + Icon, + Stack, +} from '@chakra-ui/react' +import type { AssetId } from '@shapeshiftoss/caip' +import { memo, useCallback, useMemo } from 'react' +import { TbRefresh, TbSparkles } from 'react-icons/tb' +import { useTranslate } from 'react-polyglot' + +import { Amount } from '@/components/Amount/Amount' +import { HelperTooltip } from '@/components/HelperTooltip/HelperTooltip' +import { Text } from '@/components/Text' +import { useModal } from '@/hooks/useModal/useModal' +import { bnOrZero } from '@/lib/bignumber/bignumber' +import { CHAINFLIP_LENDING_ASSET_BY_ASSET_ID } from '@/lib/chainflip/constants' +import { useChainflipFreeBalances } from '@/pages/ChainflipLending/hooks/useChainflipFreeBalances' +import { useChainflipLoanAccount } from '@/pages/ChainflipLending/hooks/useChainflipLoanAccount' +import { useChainflipLtvThresholds } from '@/pages/ChainflipLending/hooks/useChainflipLtvThresholds' +import { useChainflipSupplyPositions } from '@/pages/ChainflipLending/hooks/useChainflipSupplyPositions' + +const LENDING_ASSET_IDS = Object.keys(CHAINFLIP_LENDING_ASSET_BY_ASSET_ID) as AssetId[] + +const sidebarPosition = { base: 'relative' as const, lg: 'sticky' as const } +const sidebarTop = { base: 0, lg: 4 } + +export const BorrowingPowerCard = memo(() => { + const translate = useTranslate() + const { totalCollateralFiat, totalBorrowedFiat } = useChainflipLoanAccount() + const { thresholds } = useChainflipLtvThresholds() + + const hasCollateral = useMemo(() => bnOrZero(totalCollateralFiat).gt(0), [totalCollateralFiat]) + + const targetLtv = thresholds?.target ?? 0.5 + + const maxBorrow = useMemo( + () => bnOrZero(totalCollateralFiat).times(targetLtv).toFixed(2), + [totalCollateralFiat, targetLtv], + ) + + const available = useMemo( + () => bnOrZero(maxBorrow).minus(totalBorrowedFiat).toFixed(2), + [maxBorrow, totalBorrowedFiat], + ) + + const percentUsed = useMemo(() => { + if (bnOrZero(maxBorrow).isZero()) return 0 + return bnOrZero(totalBorrowedFiat).div(maxBorrow).times(100).toNumber() + }, [totalBorrowedFiat, maxBorrow]) + + if (!hasCollateral) return null + + return ( + + + + + + + 80 ? 'red.500' : percentUsed > 50 ? 'yellow.500' : 'green.500'} + trackColor='whiteAlpha.100' + > + + {Math.round(percentUsed)}% + + + + + + + + + + ) +}) + +const NextStepsArt = memo(({ colorScheme }: { colorScheme: 'green' | 'purple' }) => { + const isGreen = colorScheme === 'green' + const ringColor = isGreen ? 'green.500' : 'purple.500' + const bgColor = isGreen ? 'rgba(0, 205, 152, 0.05)' : 'rgba(128, 90, 213, 0.05)' + const iconColor = isGreen ? 'green.500' : 'purple.500' + + return ( +
+ {/* Outer decorative rings */} + + + + {/* Center circle with icon */} +
+ +
+
+ ) +}) + +export const NextStepsCard = memo(() => { + const translate = useTranslate() + const chainflipLendingModal = useModal('chainflipLending') + const { freeBalances } = useChainflipFreeBalances() + const { supplyPositions } = useChainflipSupplyPositions() + const { collateralWithFiat, loansWithFiat } = useChainflipLoanAccount() + + const hasFreeBalance = useMemo( + () => freeBalances.some(b => b.assetId && bnOrZero(b.balanceCryptoPrecision).gt(0)), + [freeBalances], + ) + + const hasSupply = supplyPositions.length > 0 + const hasCollateral = collateralWithFiat.length > 0 + const hasLoans = loansWithFiat.length > 0 + + const handleSupply = useCallback(() => { + const firstAssetId = LENDING_ASSET_IDS[0] + if (firstAssetId) chainflipLendingModal.open({ mode: 'supply', assetId: firstAssetId }) + }, [chainflipLendingModal]) + + const handleAddCollateral = useCallback(() => { + const firstAssetId = LENDING_ASSET_IDS[0] + if (firstAssetId) chainflipLendingModal.open({ mode: 'addCollateral', assetId: firstAssetId }) + }, [chainflipLendingModal]) + + const handleBorrow = useCallback(() => { + const firstAssetId = LENDING_ASSET_IDS[0] + if (firstAssetId) chainflipLendingModal.open({ mode: 'borrow', assetId: firstAssetId }) + }, [chainflipLendingModal]) + + // Hide when user has everything or no free balance + if (!hasFreeBalance || (hasSupply && hasCollateral && hasLoans)) return null + + const getContent = () => { + if (hasFreeBalance && !hasSupply && !hasCollateral) { + return { + colorScheme: 'green' as const, + descriptionKey: 'chainflipLending.dashboard.nextStepsSupplyOrCollateral', + actions: ( + + + + + ), + } + } + + if (hasSupply && !hasCollateral) { + return { + colorScheme: 'green' as const, + descriptionKey: 'chainflipLending.dashboard.nextStepsCollateral', + actions: ( + + ), + } + } + + if (hasCollateral && !hasLoans) { + return { + colorScheme: 'purple' as const, + descriptionKey: 'chainflipLending.dashboard.nextStepsBorrow', + actions: ( + + ), + } + } + + return null + } + + const content = getContent() + if (!content) return null + + return ( + + + + + + + {content.actions} + + + + ) +}) + +export const DashboardSidebar = memo(() => { + return ( + + + + + + + ) +}) diff --git a/src/pages/ChainflipLending/components/InitView.tsx b/src/pages/ChainflipLending/components/InitView.tsx new file mode 100644 index 00000000000..31d795c871f --- /dev/null +++ b/src/pages/ChainflipLending/components/InitView.tsx @@ -0,0 +1,323 @@ +import { + Badge, + Box, + Button, + Card, + CardBody, + CircularProgress, + Flex, + Heading, + HStack, + Icon, + SimpleGrid, + Skeleton, + Stack, +} from '@chakra-ui/react' +import type { AssetId } from '@shapeshiftoss/caip' +import { btcAssetId, ethAssetId, solAssetId, usdcAssetId, usdtAssetId } from '@shapeshiftoss/caip' +import { memo, useCallback, useMemo } from 'react' +import { TbRefresh, TbSparkles } from 'react-icons/tb' +import { useTranslate } from 'react-polyglot' +import { useNavigate } from 'react-router-dom' + +import { Amount } from '@/components/Amount/Amount' +import { AssetIcon } from '@/components/AssetIcon' +import { HelperTooltip } from '@/components/HelperTooltip/HelperTooltip' +import { AssetCell } from '@/components/StakingVaults/Cells' +import { Text } from '@/components/Text' +import { useModal } from '@/hooks/useModal/useModal' +import { bnOrZero } from '@/lib/bignumber/bignumber' +import { CHAINFLIP_LENDING_ASSET_BY_ASSET_ID } from '@/lib/chainflip/constants' +import { permillToDecimal } from '@/lib/chainflip/utils' +import type { ChainflipLendingPoolWithFiat } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools' +import { useChainflipLendingPools } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools' + +const LENDING_ASSET_IDS = Object.keys(CHAINFLIP_LENDING_ASSET_BY_ASSET_ID) as AssetId[] + +const marketRowGrid = { + base: 'minmax(150px, 1fr) repeat(1, minmax(40px, max-content))', + md: '200px repeat(4, 1fr)', +} + +const mobileDisplay = { base: 'none', md: 'flex' } + +type MarketRowProps = { + pool: ChainflipLendingPoolWithFiat + onViewMarket: (assetId: AssetId) => void +} + +const MarketRow = memo(({ pool, onViewMarket }: MarketRowProps) => { + const handleClick = useCallback(() => { + if (pool.assetId) onViewMarket(pool.assetId) + }, [pool.assetId, onViewMarket]) + + const utilisationPercent = useMemo( + () => permillToDecimal(pool.pool.utilisation_rate), + [pool.pool.utilisation_rate], + ) + + const utilisationNumber = useMemo( + () => bnOrZero(utilisationPercent).times(100).toNumber(), + [utilisationPercent], + ) + + const utilisationColor = useMemo(() => { + if (utilisationNumber >= 90) return 'red.400' + if (utilisationNumber >= 70) return 'orange.400' + return 'blue.400' + }, [utilisationNumber]) + + if (!pool.assetId) return null + + return ( + + ) +}) + +const AssetConstellation = memo(() => { + // Positions for the orbital arrangement of asset icons + // Center is at roughly (120, 100) within a 240x200 box + const positions = [ + { assetId: btcAssetId, top: '10%', left: '50%', size: 'md' }, + { assetId: ethAssetId, top: '30%', left: '15%', size: 'sm' }, + { assetId: usdcAssetId, top: '25%', left: '80%', size: 'md' }, + { assetId: usdtAssetId, top: '65%', left: '25%', size: 'sm' }, + { assetId: solAssetId, top: '70%', left: '72%', size: 'xs' }, + ] as const + + return ( + + {/* Subtle orbital ring */} + + + {positions.map(({ assetId, top, left, size }) => ( + + + + ))} + + ) +}) + +const MarketsTable = memo(() => { + const translate = useTranslate() + const navigate = useNavigate() + const { pools, isLoading } = useChainflipLendingPools() + + const handleViewMarket = useCallback( + (assetId: AssetId) => { + navigate(`/chainflip-lending/pool/${assetId}`) + }, + [navigate], + ) + + const sortedPools = useMemo( + () => [...pools].sort((a, b) => bnOrZero(b.supplyApy).minus(a.supplyApy).toNumber()), + [pools], + ) + + const marketRows = useMemo(() => { + if (isLoading) { + return Array.from({ length: 5 }).map((_, i) => ) + } + + return sortedPools.map(pool => + pool.assetId ? ( + + ) : null, + ) + }, [isLoading, sortedPools, handleViewMarket]) + + return ( + + {translate('chainflipLending.dashboard.lendingMarkets')} + + + + + + + + + + + + + + + + + + + + + + + {marketRows} + + + ) +}) + +type InfoCardProps = { + titleKey: string + descriptionKey: string + icon: React.ElementType + accentColor: string +} + +const InfoCard = memo(({ titleKey, descriptionKey, icon, accentColor }: InfoCardProps) => { + return ( + + + + + + + + + + + + + + ) +}) + +export const InitView = memo(() => { + const translate = useTranslate() + const chainflipLendingModal = useModal('chainflipLending') + + const handleDeposit = useCallback(() => { + const firstAssetId = LENDING_ASSET_IDS[0] + if (firstAssetId) chainflipLendingModal.open({ mode: 'deposit', assetId: firstAssetId }) + }, [chainflipLendingModal]) + + return ( + + {/* Hero Card */} + + + + + + {translate('chainflipLending.dashboard.getStarted')} + + + {translate('chainflipLending.dashboard.depositFirstAsset')} + + + + + + + + + + + {/* Lending Markets Table */} + + + {/* Info Cards */} + + + + + + ) +}) diff --git a/src/pages/ChainflipLending/components/LoanHealth.tsx b/src/pages/ChainflipLending/components/LoanHealth.tsx new file mode 100644 index 00000000000..879fd29c28a --- /dev/null +++ b/src/pages/ChainflipLending/components/LoanHealth.tsx @@ -0,0 +1,102 @@ +import { Card, CardBody, Flex, Icon, Stack, Text as CText } from '@chakra-ui/react' +import { memo, useMemo } from 'react' +import { TbHeartRateMonitor } from 'react-icons/tb' +import { useTranslate } from 'react-polyglot' + +import { Amount } from '@/components/Amount/Amount' +import { HelperTooltip } from '@/components/HelperTooltip/HelperTooltip' +import { Text } from '@/components/Text' +import { bnOrZero } from '@/lib/bignumber/bignumber' +import { useChainflipLoanAccount } from '@/pages/ChainflipLending/hooks/useChainflipLoanAccount' +import { useChainflipLtvThresholds } from '@/pages/ChainflipLending/hooks/useChainflipLtvThresholds' +import { LtvGauge } from '@/pages/ChainflipLending/Pool/components/Borrow/LtvGauge' + +const DEFAULT_HARD_LIQUIDATION_LTV = 0.93 + +const getLtvColor = (ltv: number, riskyThreshold: number, hardLiqThreshold: number): string => { + if (ltv >= hardLiqThreshold) return 'red.500' + if (ltv >= riskyThreshold) return 'yellow.500' + return 'green.500' +} + +export const LoanHealth = memo(() => { + const translate = useTranslate() + const { totalCollateralFiat, totalBorrowedFiat } = useChainflipLoanAccount() + const { thresholds } = useChainflipLtvThresholds() + + const hasLoans = useMemo(() => bnOrZero(totalBorrowedFiat).gt(0), [totalBorrowedFiat]) + + const currentLtv = useMemo(() => { + if (bnOrZero(totalCollateralFiat).isZero()) return 0 + return bnOrZero(totalBorrowedFiat).div(totalCollateralFiat).toNumber() + }, [totalBorrowedFiat, totalCollateralFiat]) + + const hardLiquidationLtv = thresholds?.hardLiquidation ?? DEFAULT_HARD_LIQUIDATION_LTV + const riskyLtv = thresholds?.target ?? 0.8 + + const liquidationDistance = useMemo(() => { + if (hardLiquidationLtv === 0) return '0' + const liquidationCollateral = bnOrZero(totalBorrowedFiat).div(hardLiquidationLtv) + return bnOrZero(totalCollateralFiat).minus(liquidationCollateral).toFixed(2) + }, [totalCollateralFiat, totalBorrowedFiat, hardLiquidationLtv]) + + const ltvColor = useMemo( + () => getLtvColor(currentLtv, riskyLtv, hardLiquidationLtv), + [currentLtv, riskyLtv, hardLiquidationLtv], + ) + + const ltvDisplayPercent = useMemo( + () => (Math.min(Math.max(currentLtv, 0), 1) * 100).toFixed(1), + [currentLtv], + ) + + if (!hasLoans) return null + + return ( + + + + {/* Header row: left = icon + label + current LTV, right = liquidation distance */} + + + + + + + + {translate('chainflipLending.dashboard.currentLtv', { + ltv: `${ltvDisplayPercent}%`, + })} + + + + + + + + + {/* Multi-segment LTV gauge bar */} + + + + + ) +}) diff --git a/src/pages/ChainflipLending/components/Markets.tsx b/src/pages/ChainflipLending/components/Markets.tsx index 7e559556420..d4ad559bea3 100644 --- a/src/pages/ChainflipLending/components/Markets.tsx +++ b/src/pages/ChainflipLending/components/Markets.tsx @@ -7,9 +7,14 @@ import { SimpleGrid, Skeleton, Stack, + Tab, + TabList, + TabPanel, + TabPanels, + Tabs, } from '@chakra-ui/react' import type { AssetId } from '@shapeshiftoss/caip' -import { useCallback, useMemo } from 'react' +import { useCallback, useMemo, useState } from 'react' import { useTranslate } from 'react-polyglot' import { useNavigate } from 'react-router-dom' @@ -21,14 +26,15 @@ import { AssetCell } from '@/components/StakingVaults/Cells' import { Text } from '@/components/Text' import { bnOrZero } from '@/lib/bignumber/bignumber' import { permillToDecimal } from '@/lib/chainflip/utils' +import { useChainflipLendingAccount } from '@/pages/ChainflipLending/ChainflipLendingAccountContext' import { ChainflipLendingHeader } from '@/pages/ChainflipLending/components/ChainflipLendingHeader' -import { MyBalancesList } from '@/pages/ChainflipLending/components/MyBalances' +import { Dashboard } from '@/pages/ChainflipLending/components/Dashboard' import type { ChainflipLendingPoolWithFiat } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools' import { useChainflipLendingPools } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools' const marketRowGrid: GridProps['gridTemplateColumns'] = { base: 'minmax(150px, 1fr) repeat(1, minmax(40px, max-content))', - md: '200px repeat(4, 1fr)', + md: '200px repeat(5, 1fr)', } const mobileDisplay = { base: 'none', md: 'flex' } @@ -78,11 +84,14 @@ const MarketRow = ({ pool, onViewMarket }: MarketRowProps) => { onClick={handleClick} > + + + - + { ) } -export const Markets = () => { +const MarketsTable = () => { const translate = useTranslate() const navigate = useNavigate() const { pools, isLoading } = useChainflipLendingPools() @@ -115,8 +124,6 @@ export const Markets = () => { [navigate], ) - const headerComponent = useMemo(() => , []) - const sortedPools = useMemo( () => [...pools].sort((a, b) => bnOrZero(b.supplyApy).minus(a.supplyApy).toNumber()), [pools], @@ -135,58 +142,98 @@ export const Markets = () => { }, [isLoading, sortedPools, handleViewMarket]) return ( -
- - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {marketRows} + + + ) +} - - - - { + const translate = useTranslate() + const { accountId } = useChainflipLendingAccount() + const [tabIndex, setTabIndex] = useState(0) + + const headerComponent = useMemo(() => , []) + + return ( +
+ + + {accountId ? ( + + + {translate('chainflipLending.myDashboard')} + {translate('chainflipLending.markets')} + + + + + + + + + + + ) : ( + + - - - - - - - - - - - - - - - - - - - - - {marketRows} + /> + - + )}
) diff --git a/src/pages/ChainflipLending/components/MyBalances.tsx b/src/pages/ChainflipLending/components/MyBalances.tsx deleted file mode 100644 index c5238a2eaf8..00000000000 --- a/src/pages/ChainflipLending/components/MyBalances.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import type { GridProps } from '@chakra-ui/react' -import { Button, Flex, SimpleGrid, Skeleton, Stack } from '@chakra-ui/react' -import type { AssetId } from '@shapeshiftoss/caip' -import { fromAssetId } from '@shapeshiftoss/caip' -import { useCallback, useMemo } from 'react' -import { useNavigate } from 'react-router-dom' - -import { Amount } from '@/components/Amount/Amount' -import { AssetCell } from '@/components/StakingVaults/Cells' -import { Text } from '@/components/Text' -import { CHAINFLIP_LENDING_ASSET_BY_ASSET_ID } from '@/lib/chainflip/constants' -import { useChainflipLendingAccount } from '@/pages/ChainflipLending/ChainflipLendingAccountContext' -import type { ChainflipFreeBalanceWithFiat } from '@/pages/ChainflipLending/hooks/useChainflipFreeBalances' -import { useChainflipFreeBalances } from '@/pages/ChainflipLending/hooks/useChainflipFreeBalances' -import type { - CollateralWithFiat, - LoanWithFiat, -} from '@/pages/ChainflipLending/hooks/useChainflipLoanAccount' -import { useChainflipLoanAccount } from '@/pages/ChainflipLending/hooks/useChainflipLoanAccount' -import type { ChainflipSupplyPositionWithFiat } from '@/pages/ChainflipLending/hooks/useChainflipSupplyPositions' -import { useChainflipSupplyPositions } from '@/pages/ChainflipLending/hooks/useChainflipSupplyPositions' -import { selectAssetById } from '@/state/slices/assetsSlice/selectors' -import { selectPortfolioCryptoBalanceByFilter } from '@/state/slices/common-selectors' -import { selectAccountIdsByAccountNumberAndChainId } from '@/state/slices/portfolioSlice/selectors' -import { useAppSelector } from '@/state/store' - -const balanceRowGrid: GridProps['gridTemplateColumns'] = { - base: '1fr', - md: '200px 1fr 1fr 1fr 1fr 1fr', -} - -const mobileDisplay = { base: 'none', md: 'flex' } -const mobilePadding = { base: 4, lg: 4, xl: 0 } -const listMargin = { base: 0, lg: 0, xl: -4 } - -const LENDING_ASSET_IDS = Object.keys(CHAINFLIP_LENDING_ASSET_BY_ASSET_ID) as AssetId[] - -type BalanceRowProps = { - assetId: AssetId - accountNumber: number - freeBalance: ChainflipFreeBalanceWithFiat | undefined - supplyPosition: ChainflipSupplyPositionWithFiat | undefined - collateral: CollateralWithFiat | undefined - loan: LoanWithFiat | undefined - onDeposit: (assetId: AssetId) => void -} - -const BalanceRow = ({ - assetId, - accountNumber, - freeBalance, - supplyPosition, - collateral, - loan, - onDeposit, -}: BalanceRowProps) => { - const asset = useAppSelector(state => selectAssetById(state, assetId)) - const accountIdsByAccountNumberAndChainId = useAppSelector( - selectAccountIdsByAccountNumberAndChainId, - ) - - const chainId = useMemo(() => fromAssetId(assetId).chainId, [assetId]) - - const poolChainAccountId = useMemo(() => { - const byChainId = accountIdsByAccountNumberAndChainId[accountNumber] - return byChainId?.[chainId]?.[0] - }, [accountIdsByAccountNumberAndChainId, accountNumber, chainId]) - - const balanceFilter = useMemo( - () => ({ assetId, accountId: poolChainAccountId ?? '' }), - [assetId, poolChainAccountId], - ) - - const walletBalance = useAppSelector(state => - selectPortfolioCryptoBalanceByFilter(state, balanceFilter), - ) - - const walletBalancePrecision = useMemo( - () => (poolChainAccountId ? walletBalance.toPrecision() : '0'), - [walletBalance, poolChainAccountId], - ) - - const scBalancePrecision = useMemo( - () => freeBalance?.balanceCryptoPrecision ?? '0', - [freeBalance?.balanceCryptoPrecision], - ) - - const suppliedPrecision = useMemo( - () => supplyPosition?.totalAmountCryptoPrecision ?? '0', - [supplyPosition?.totalAmountCryptoPrecision], - ) - - const collateralPrecision = useMemo( - () => collateral?.amountCryptoPrecision ?? '0', - [collateral?.amountCryptoPrecision], - ) - - const borrowedPrecision = useMemo( - () => loan?.principalAmountCryptoPrecision ?? '0', - [loan?.principalAmountCryptoPrecision], - ) - - const handleClick = useCallback(() => { - onDeposit(assetId) - }, [assetId, onDeposit]) - - const symbol = useMemo(() => asset?.symbol ?? '', [asset?.symbol]) - - if (!asset) return null - - return ( - - ) -} - -export const MyBalancesList = () => { - const navigate = useNavigate() - const { accountId, accountNumber } = useChainflipLendingAccount() - const { freeBalances, isLoading } = useChainflipFreeBalances() - const { supplyPositions, isLoading: isPositionsLoading } = useChainflipSupplyPositions() - const { collateralWithFiat, loansWithFiat, isLoading: isLoanLoading } = useChainflipLoanAccount() - - const handleDeposit = useCallback( - (assetId: AssetId) => { - navigate(`/chainflip-lending/pool/${assetId}`) - }, - [navigate], - ) - - const freeBalancesByAssetId = useMemo( - () => - freeBalances.reduce>>( - (acc, balance) => { - if (balance.assetId) acc[balance.assetId] = balance - return acc - }, - {}, - ), - [freeBalances], - ) - - const supplyPositionsByAssetId = useMemo( - () => - supplyPositions.reduce>>( - (acc, position) => { - acc[position.assetId] = position - return acc - }, - {}, - ), - [supplyPositions], - ) - - const collateralByAssetId = useMemo( - () => - collateralWithFiat.reduce>>((acc, c) => { - acc[c.assetId] = c - return acc - }, {}), - [collateralWithFiat], - ) - - const loansByAssetId = useMemo( - () => - loansWithFiat.reduce>>((acc, l) => { - acc[l.assetId] = l - return acc - }, {}), - [loansWithFiat], - ) - - const balanceRows = useMemo(() => { - if (isLoading || isPositionsLoading || isLoanLoading) { - return Array.from({ length: 5 }).map((_, i) => ) - } - - return LENDING_ASSET_IDS.map(assetId => ( - - )) - }, [ - accountNumber, - isLoading, - isPositionsLoading, - isLoanLoading, - freeBalancesByAssetId, - supplyPositionsByAssetId, - collateralByAssetId, - loansByAssetId, - handleDeposit, - ]) - - if (!accountId) return null - - return ( - - - - - - - - - - - - - - - - - - - - {balanceRows} - - ) -} From 6a34d6755c235a3c57ad28abaed55cba6bc1f5d5 Mon Sep 17 00:00:00 2001 From: gomes-bot Date: Wed, 18 Mar 2026 00:13:18 +0100 Subject: [PATCH 02/18] fix: visual polish - init view art, info cards, markets table - Rewrite AssetConstellation with larger icons (52-88px), organic positions, no network badges (showNetworkIcon=false), subtle orbital arcs - Rewrite InfoCard with concentric ring art on right side, proper accent colors, radial lines for borrow card - Add Borrow APR column to Markets table (6 columns matching Figma) - Reorder Markets columns: Asset, Supply APY, Total Supplied, Borrow APR, Total Borrowed, Utilisation - Add Lending Markets section title with description - Fix hero card: inline Deposit button + FLIP note, more padding, borderRadius 2xl - Fix Supplied empty state CTA from "+ Supply" to "+ Deposit" per Figma Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/ChainflipLendingHeader.tsx | 19 +- .../ChainflipLending/components/Dashboard.tsx | 9 +- .../components/DashboardSections.tsx | 35 ++- .../components/DashboardSidebar.tsx | 14 +- .../ChainflipLending/components/InitView.tsx | 274 +++++++++++++----- .../components/LoanHealth.tsx | 2 +- .../ChainflipLending/components/Markets.tsx | 10 +- 7 files changed, 264 insertions(+), 99 deletions(-) diff --git a/src/pages/ChainflipLending/components/ChainflipLendingHeader.tsx b/src/pages/ChainflipLending/components/ChainflipLendingHeader.tsx index 00dc8169479..64209343b75 100644 --- a/src/pages/ChainflipLending/components/ChainflipLendingHeader.tsx +++ b/src/pages/ChainflipLending/components/ChainflipLendingHeader.tsx @@ -28,13 +28,21 @@ type SummaryCardProps = { tooltipKey: string isLoading: boolean labelColor?: string + 'data-testid'?: string } -const SummaryCard = ({ value, labelKey, tooltipKey, isLoading, labelColor }: SummaryCardProps) => { +const SummaryCard = ({ + value, + labelKey, + tooltipKey, + isLoading, + labelColor, + 'data-testid': testId, +}: SummaryCardProps) => { const translate = useTranslate() return ( - + { labelKey='chainflipLending.dashboard.freeBalance' tooltipKey='chainflipLending.dashboard.freeBalanceTooltip' isLoading={isUserDataLoading} + data-testid='chainflip-lending-summary-free-balance' /> { tooltipKey='chainflipLending.dashboard.suppliedTooltip' isLoading={isUserDataLoading} labelColor='green.400' + data-testid='chainflip-lending-summary-supplied' /> { tooltipKey='chainflipLending.dashboard.collateralTooltip' isLoading={isUserDataLoading} labelColor='blue.300' + data-testid='chainflip-lending-summary-collateral' /> { tooltipKey='chainflipLending.dashboard.borrowedTooltip' isLoading={isUserDataLoading} labelColor='purple.300' + data-testid='chainflip-lending-summary-borrowed' /> ) : ( @@ -159,6 +171,7 @@ export const ChainflipLendingHeader = () => { tooltipKey='chainflipLending.totalSuppliedTooltip' isLoading={isPoolsLoading} labelColor='green.400' + data-testid='chainflip-lending-summary-total-supplied' /> { tooltipKey='chainflipLending.availableLiquidityTooltip' isLoading={isPoolsLoading} labelColor='blue.300' + data-testid='chainflip-lending-summary-available-liquidity' /> { tooltipKey='chainflipLending.totalBorrowedTooltip' isLoading={isPoolsLoading} labelColor='purple.300' + data-testid='chainflip-lending-summary-total-borrowed' /> )} diff --git a/src/pages/ChainflipLending/components/Dashboard.tsx b/src/pages/ChainflipLending/components/Dashboard.tsx index 985483ef2fe..4a30ca5daf5 100644 --- a/src/pages/ChainflipLending/components/Dashboard.tsx +++ b/src/pages/ChainflipLending/components/Dashboard.tsx @@ -43,11 +43,16 @@ export const Dashboard = memo(() => { }, [isFunded, isLpRegistered, hasAnyPosition]) if (showInitView) { - return + return } return ( - + diff --git a/src/pages/ChainflipLending/components/DashboardSections.tsx b/src/pages/ChainflipLending/components/DashboardSections.tsx index c619d7aa526..d1d9c1c5c39 100644 --- a/src/pages/ChainflipLending/components/DashboardSections.tsx +++ b/src/pages/ChainflipLending/components/DashboardSections.tsx @@ -51,7 +51,7 @@ type SectionHeaderProps = { tooltipKey: string totalFiat: string isLoading: boolean - primaryAction?: { labelKey: string; handleClick: () => void } + primaryAction?: { labelKey: string; handleClick: () => void; testId?: string } secondaryAction?: { labelKey: string; handleClick: () => void } } @@ -77,7 +77,12 @@ const SectionHeader = ({ {primaryAction && ( - )} @@ -96,16 +101,23 @@ type EmptyStateProps = { descriptionKey: string actionLabelKey: string onAction: () => void + actionTestId?: string } -const EmptyState = ({ titleKey, descriptionKey, actionLabelKey, onAction }: EmptyStateProps) => { +const EmptyState = ({ + titleKey, + descriptionKey, + actionLabelKey, + onAction, + actionTestId, +}: EmptyStateProps) => { const translate = useTranslate() return ( - @@ -154,7 +166,7 @@ export const FreeBalanceSection = memo(() => { }, [chainflipLendingModal, nonZeroBalances]) return ( - + { primaryAction={{ labelKey: 'chainflipLending.dashboard.deposit', handleClick: handleDeposit, + testId: 'chainflip-lending-deposit-action', }} secondaryAction={ nonZeroBalances.length > 0 @@ -268,7 +281,7 @@ export const SuppliedSection = memo(() => { }, [chainflipLendingModal, supplyPositions]) return ( - + { ? { labelKey: 'chainflipLending.dashboard.deposit', handleClick: handleSupply, + testId: 'chainflip-lending-supply-action', } : undefined } @@ -331,6 +345,7 @@ export const SuppliedSection = memo(() => { descriptionKey='chainflipLending.dashboard.noEarningPositionsDescription' actionLabelKey='chainflipLending.dashboard.deposit' onAction={handleSupply} + actionTestId='chainflip-lending-supplied-empty-cta' /> )} @@ -374,7 +389,7 @@ export const CollateralSection = memo(() => { }, [chainflipLendingModal, collateralWithFiat]) return ( - + { ? { labelKey: 'chainflipLending.dashboard.deposit', handleClick: handleAddCollateral, + testId: 'chainflip-lending-collateral-action', } : undefined } @@ -417,6 +433,7 @@ export const CollateralSection = memo(() => { descriptionKey='chainflipLending.dashboard.provideCollateralDescription' actionLabelKey='chainflipLending.dashboard.addCollateral' onAction={handleAddCollateral} + actionTestId='chainflip-lending-collateral-empty-cta' /> )} @@ -495,7 +512,7 @@ export const BorrowedSection = memo(() => { }, [chainflipLendingModal, loansWithFiat]) return ( - + { ? { labelKey: 'chainflipLending.dashboard.borrow', handleClick: handleBorrow, + testId: 'chainflip-lending-borrow-action', } : undefined } @@ -572,6 +590,7 @@ export const BorrowedSection = memo(() => { descriptionKey='chainflipLending.dashboard.noActiveLoansDescription' actionLabelKey='chainflipLending.dashboard.addCollateral' onAction={handleAddCollateral} + actionTestId='chainflip-lending-borrowed-empty-cta' /> )} diff --git a/src/pages/ChainflipLending/components/DashboardSidebar.tsx b/src/pages/ChainflipLending/components/DashboardSidebar.tsx index 1affb6a434a..37f91fab0ed 100644 --- a/src/pages/ChainflipLending/components/DashboardSidebar.tsx +++ b/src/pages/ChainflipLending/components/DashboardSidebar.tsx @@ -58,7 +58,7 @@ export const BorrowingPowerCard = memo(() => { if (!hasCollateral) return null return ( - + @@ -135,13 +135,11 @@ const NextStepsArt = memo(({ colorScheme }: { colorScheme: 'green' | 'purple' }) height='64px' borderRadius='full' bg={bgColor} - boxShadow={`inset 0 1px 0 0 ${isGreen ? 'rgba(0, 205, 152, 0.25)' : 'rgba(128, 90, 213, 0.25)'}`} + boxShadow={`inset 0 1px 0 0 ${ + isGreen ? 'rgba(0, 205, 152, 0.25)' : 'rgba(128, 90, 213, 0.25)' + }`} > - + ) @@ -230,7 +228,7 @@ export const NextStepsCard = memo(() => { if (!content) return null return ( - + diff --git a/src/pages/ChainflipLending/components/InitView.tsx b/src/pages/ChainflipLending/components/InitView.tsx index 31d795c871f..601fe0a4723 100644 --- a/src/pages/ChainflipLending/components/InitView.tsx +++ b/src/pages/ChainflipLending/components/InitView.tsx @@ -4,6 +4,7 @@ import { Button, Card, CardBody, + Center, CircularProgress, Flex, Heading, @@ -31,6 +32,8 @@ import { CHAINFLIP_LENDING_ASSET_BY_ASSET_ID } from '@/lib/chainflip/constants' import { permillToDecimal } from '@/lib/chainflip/utils' import type { ChainflipLendingPoolWithFiat } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools' import { useChainflipLendingPools } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools' +import { selectAssetById } from '@/state/slices/assetsSlice/selectors' +import { useAppSelector } from '@/state/store' const LENDING_ASSET_IDS = Object.keys(CHAINFLIP_LENDING_ASSET_BY_ASSET_ID) as AssetId[] @@ -47,6 +50,9 @@ type MarketRowProps = { } const MarketRow = memo(({ pool, onViewMarket }: MarketRowProps) => { + const asset = useAppSelector(state => + pool.assetId ? selectAssetById(state, pool.assetId) : undefined, + ) const handleClick = useCallback(() => { if (pool.assetId) onViewMarket(pool.assetId) }, [pool.assetId, onViewMarket]) @@ -82,6 +88,7 @@ const MarketRow = memo(({ pool, onViewMarket }: MarketRowProps) => { height='auto' color='text.base' onClick={handleClick} + data-testid={`chainflip-lending-market-row-${asset?.symbol?.toLowerCase() ?? 'unknown'}`} > @@ -110,52 +117,57 @@ const MarketRow = memo(({ pool, onViewMarket }: MarketRowProps) => { }) const AssetConstellation = memo(() => { - // Positions for the orbital arrangement of asset icons - // Center is at roughly (120, 100) within a 240x200 box - const positions = [ - { assetId: btcAssetId, top: '10%', left: '50%', size: 'md' }, - { assetId: ethAssetId, top: '30%', left: '15%', size: 'sm' }, - { assetId: usdcAssetId, top: '25%', left: '80%', size: 'md' }, - { assetId: usdtAssetId, top: '65%', left: '25%', size: 'sm' }, - { assetId: solAssetId, top: '70%', left: '72%', size: 'xs' }, - ] as const - return ( - - {/* Subtle orbital ring */} + + {/* Orbital arc - sweeping from top-left to bottom-right (dark blue) */} + {/* Orbital arc - bottom arc (green/orange tinted) */} - {positions.map(({ assetId, top, left, size }) => ( - - - - ))} + {/* USDC - top left, medium */} + + + + {/* ETH - center, largest */} + + + + {/* USDT - top right, medium-large */} + + + + {/* BTC - bottom center-left, large */} + + + + {/* SOL - bottom right, medium */} + + + ) }) @@ -232,34 +244,122 @@ type InfoCardProps = { descriptionKey: string icon: React.ElementType accentColor: string + 'data-testid'?: string } -const InfoCard = memo(({ titleKey, descriptionKey, icon, accentColor }: InfoCardProps) => { - return ( - - - - - +const InfoCard = memo( + ({ titleKey, descriptionKey, icon, accentColor, 'data-testid': testId }: InfoCardProps) => { + const iconColor = `${accentColor}.500` + const bgGlow = `${accentColor}.900` + const ringColor = + accentColor === 'green' ? 'rgba(0, 205, 152, 0.2)' : 'rgba(128, 90, 213, 0.2)' + const ringColorStrong = + accentColor === 'green' ? 'rgba(0, 205, 152, 0.35)' : 'rgba(128, 90, 213, 0.35)' + + return ( + + + + + + + + {/* Decorative art - concentric arcs with centered icon */} + + {/* Outer arc */} + + {/* Middle arc */} + + {/* Inner circle with icon */} +
+ +
+ {/* Radial lines for borrow card */} + {accentColor === 'purple' && ( + <> + + + + + + )} +
- - - - -
-
-
- ) -}) +
+
+ ) + }, +) -export const InitView = memo(() => { +export const InitView = memo(({ 'data-testid': testId }: { 'data-testid'?: string }) => { const translate = useTranslate() const chainflipLendingModal = useModal('chainflipLending') @@ -269,31 +369,51 @@ export const InitView = memo(() => { }, [chainflipLendingModal]) return ( - + {/* Hero Card */} - - - - - + + + + + {translate('chainflipLending.dashboard.getStarted')} - + {translate('chainflipLending.dashboard.depositFirstAsset')} - - + + + + @@ -310,12 +430,14 @@ export const InitView = memo(() => { descriptionKey='chainflipLending.dashboard.earnYieldDescription' icon={TbSparkles} accentColor='green' + data-testid='chainflip-lending-earn-yield-card' />
diff --git a/src/pages/ChainflipLending/components/LoanHealth.tsx b/src/pages/ChainflipLending/components/LoanHealth.tsx index 879fd29c28a..60d88dd5009 100644 --- a/src/pages/ChainflipLending/components/LoanHealth.tsx +++ b/src/pages/ChainflipLending/components/LoanHealth.tsx @@ -53,7 +53,7 @@ export const LoanHealth = memo(() => { if (!hasLoans) return null return ( - + {/* Header row: left = icon + label + current LTV, right = liquidation distance */} diff --git a/src/pages/ChainflipLending/components/Markets.tsx b/src/pages/ChainflipLending/components/Markets.tsx index d4ad559bea3..83ac8578738 100644 --- a/src/pages/ChainflipLending/components/Markets.tsx +++ b/src/pages/ChainflipLending/components/Markets.tsx @@ -31,6 +31,8 @@ import { ChainflipLendingHeader } from '@/pages/ChainflipLending/components/Chai import { Dashboard } from '@/pages/ChainflipLending/components/Dashboard' import type { ChainflipLendingPoolWithFiat } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools' import { useChainflipLendingPools } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools' +import { selectAssetById } from '@/state/slices/assetsSlice/selectors' +import { useAppSelector } from '@/state/store' const marketRowGrid: GridProps['gridTemplateColumns'] = { base: 'minmax(150px, 1fr) repeat(1, minmax(40px, max-content))', @@ -47,6 +49,9 @@ type MarketRowProps = { } const MarketRow = ({ pool, onViewMarket }: MarketRowProps) => { + const asset = useAppSelector(state => + pool.assetId ? selectAssetById(state, pool.assetId) : undefined, + ) const handleClick = useCallback(() => { if (pool.assetId) onViewMarket(pool.assetId) }, [pool.assetId, onViewMarket]) @@ -82,6 +87,7 @@ const MarketRow = ({ pool, onViewMarket }: MarketRowProps) => { height='auto' color='text.base' onClick={handleClick} + data-testid={`chainflip-lending-market-row-${asset?.symbol?.toLowerCase() ?? 'unknown'}`} > @@ -210,7 +216,7 @@ export const Markets = () => { {accountId ? ( - + {translate('chainflipLending.myDashboard')} {translate('chainflipLending.markets')} @@ -218,7 +224,7 @@ export const Markets = () => { - + From ad4ebd8b0492f742f2f605ae8d2c05790f7982f6 Mon Sep 17 00:00:00 2001 From: gomes-bot Date: Wed, 18 Mar 2026 00:22:29 +0100 Subject: [PATCH 03/18] fix: use exact figma svgs for init view art - Download and embed orbital ring SVGs from Figma MCP for hero constellation - Download and embed concentric ring SVGs for Earn Yield and Borrow info cards - Use sparkles-icon.svg and refresh-icon.svg from Figma for info card centers - Remove showNetworkIcon from hero asset icons (plain icons like Figma) - Match Figma card styling: rgba bg/border, 2xl border radius, 32px/64px padding - Inline "Requires 2 FLIP" note next to Deposit button per Figma layout Co-Authored-By: Claude Opus 4.6 (1M context) --- src/assets/chainflip-lending/borrow-glow.svg | 12 + .../chainflip-lending/borrow-ring-1.svg | 13 + .../chainflip-lending/borrow-ring-2.svg | 13 + .../chainflip-lending/borrow-ring-3.svg | 13 + .../chainflip-lending/borrow-ring-inner.svg | 13 + src/assets/chainflip-lending/earn-glow.svg | 12 + .../chainflip-lending/earn-ring-inner.svg | 13 + .../chainflip-lending/earn-ring-middle.svg | 13 + .../chainflip-lending/earn-ring-outer.svg | 13 + src/assets/chainflip-lending/glow-btc.svg | Bin 0 -> 29750 bytes src/assets/chainflip-lending/glow-eth.svg | Bin 0 -> 9669 bytes src/assets/chainflip-lending/orbital-btc.svg | 13 + src/assets/chainflip-lending/orbital-eth.svg | 13 + src/assets/chainflip-lending/orbital-sol.svg | 13 + .../chainflip-lending/orbital-tether.svg | 13 + src/assets/chainflip-lending/orbital-usdc.svg | 13 + src/assets/chainflip-lending/refresh-icon.svg | 5 + .../chainflip-lending/sparkles-icon.svg | 5 + .../ChainflipLending/components/InitView.tsx | 237 ++++++++---------- 19 files changed, 294 insertions(+), 133 deletions(-) create mode 100644 src/assets/chainflip-lending/borrow-glow.svg create mode 100644 src/assets/chainflip-lending/borrow-ring-1.svg create mode 100644 src/assets/chainflip-lending/borrow-ring-2.svg create mode 100644 src/assets/chainflip-lending/borrow-ring-3.svg create mode 100644 src/assets/chainflip-lending/borrow-ring-inner.svg create mode 100644 src/assets/chainflip-lending/earn-glow.svg create mode 100644 src/assets/chainflip-lending/earn-ring-inner.svg create mode 100644 src/assets/chainflip-lending/earn-ring-middle.svg create mode 100644 src/assets/chainflip-lending/earn-ring-outer.svg create mode 100644 src/assets/chainflip-lending/glow-btc.svg create mode 100644 src/assets/chainflip-lending/glow-eth.svg create mode 100644 src/assets/chainflip-lending/orbital-btc.svg create mode 100644 src/assets/chainflip-lending/orbital-eth.svg create mode 100644 src/assets/chainflip-lending/orbital-sol.svg create mode 100644 src/assets/chainflip-lending/orbital-tether.svg create mode 100644 src/assets/chainflip-lending/orbital-usdc.svg create mode 100644 src/assets/chainflip-lending/refresh-icon.svg create mode 100644 src/assets/chainflip-lending/sparkles-icon.svg diff --git a/src/assets/chainflip-lending/borrow-glow.svg b/src/assets/chainflip-lending/borrow-glow.svg new file mode 100644 index 00000000000..6c76440904b --- /dev/null +++ b/src/assets/chainflip-lending/borrow-glow.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/chainflip-lending/borrow-ring-1.svg b/src/assets/chainflip-lending/borrow-ring-1.svg new file mode 100644 index 00000000000..a92b59e8483 --- /dev/null +++ b/src/assets/chainflip-lending/borrow-ring-1.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/chainflip-lending/borrow-ring-2.svg b/src/assets/chainflip-lending/borrow-ring-2.svg new file mode 100644 index 00000000000..d1d3ce292b5 --- /dev/null +++ b/src/assets/chainflip-lending/borrow-ring-2.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/chainflip-lending/borrow-ring-3.svg b/src/assets/chainflip-lending/borrow-ring-3.svg new file mode 100644 index 00000000000..9548e3cdd19 --- /dev/null +++ b/src/assets/chainflip-lending/borrow-ring-3.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/chainflip-lending/borrow-ring-inner.svg b/src/assets/chainflip-lending/borrow-ring-inner.svg new file mode 100644 index 00000000000..4fcf8325f28 --- /dev/null +++ b/src/assets/chainflip-lending/borrow-ring-inner.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/chainflip-lending/earn-glow.svg b/src/assets/chainflip-lending/earn-glow.svg new file mode 100644 index 00000000000..dd85a9c9cb6 --- /dev/null +++ b/src/assets/chainflip-lending/earn-glow.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/chainflip-lending/earn-ring-inner.svg b/src/assets/chainflip-lending/earn-ring-inner.svg new file mode 100644 index 00000000000..522748941c5 --- /dev/null +++ b/src/assets/chainflip-lending/earn-ring-inner.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/chainflip-lending/earn-ring-middle.svg b/src/assets/chainflip-lending/earn-ring-middle.svg new file mode 100644 index 00000000000..13ca13d1cde --- /dev/null +++ b/src/assets/chainflip-lending/earn-ring-middle.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/chainflip-lending/earn-ring-outer.svg b/src/assets/chainflip-lending/earn-ring-outer.svg new file mode 100644 index 00000000000..e7615115bd3 --- /dev/null +++ b/src/assets/chainflip-lending/earn-ring-outer.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/chainflip-lending/glow-btc.svg b/src/assets/chainflip-lending/glow-btc.svg new file mode 100644 index 0000000000000000000000000000000000000000..9aa0db74b36e5661b9462ecf091a1352d962bb4f GIT binary patch literal 29750 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 E09!kzU;qFB literal 0 HcmV?d00001 diff --git a/src/assets/chainflip-lending/glow-eth.svg b/src/assets/chainflip-lending/glow-eth.svg new file mode 100644 index 0000000000000000000000000000000000000000..2a33fe95f74e052d1cf4279579d292855a5903e8 GIT binary patch literal 9669 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 UG1sj)?ym(}8sH}-YPN6w4}gW!8~^|S literal 0 HcmV?d00001 diff --git a/src/assets/chainflip-lending/orbital-btc.svg b/src/assets/chainflip-lending/orbital-btc.svg new file mode 100644 index 00000000000..d9e469a2789 --- /dev/null +++ b/src/assets/chainflip-lending/orbital-btc.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/chainflip-lending/orbital-eth.svg b/src/assets/chainflip-lending/orbital-eth.svg new file mode 100644 index 00000000000..73ae0542394 --- /dev/null +++ b/src/assets/chainflip-lending/orbital-eth.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/chainflip-lending/orbital-sol.svg b/src/assets/chainflip-lending/orbital-sol.svg new file mode 100644 index 00000000000..1ad6262f3ba --- /dev/null +++ b/src/assets/chainflip-lending/orbital-sol.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/chainflip-lending/orbital-tether.svg b/src/assets/chainflip-lending/orbital-tether.svg new file mode 100644 index 00000000000..528ecd9a152 --- /dev/null +++ b/src/assets/chainflip-lending/orbital-tether.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/chainflip-lending/orbital-usdc.svg b/src/assets/chainflip-lending/orbital-usdc.svg new file mode 100644 index 00000000000..db36016ed6c --- /dev/null +++ b/src/assets/chainflip-lending/orbital-usdc.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/chainflip-lending/refresh-icon.svg b/src/assets/chainflip-lending/refresh-icon.svg new file mode 100644 index 00000000000..0679a629a48 --- /dev/null +++ b/src/assets/chainflip-lending/refresh-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/chainflip-lending/sparkles-icon.svg b/src/assets/chainflip-lending/sparkles-icon.svg new file mode 100644 index 00000000000..8d12cd182b5 --- /dev/null +++ b/src/assets/chainflip-lending/sparkles-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pages/ChainflipLending/components/InitView.tsx b/src/pages/ChainflipLending/components/InitView.tsx index 601fe0a4723..cb3302af417 100644 --- a/src/pages/ChainflipLending/components/InitView.tsx +++ b/src/pages/ChainflipLending/components/InitView.tsx @@ -9,7 +9,6 @@ import { Flex, Heading, HStack, - Icon, SimpleGrid, Skeleton, Stack, @@ -17,10 +16,25 @@ import { import type { AssetId } from '@shapeshiftoss/caip' import { btcAssetId, ethAssetId, solAssetId, usdcAssetId, usdtAssetId } from '@shapeshiftoss/caip' import { memo, useCallback, useMemo } from 'react' -import { TbRefresh, TbSparkles } from 'react-icons/tb' import { useTranslate } from 'react-polyglot' import { useNavigate } from 'react-router-dom' +import borrowGlow from '@/assets/chainflip-lending/borrow-glow.svg' +import borrowRing1 from '@/assets/chainflip-lending/borrow-ring-1.svg' +import borrowRing2 from '@/assets/chainflip-lending/borrow-ring-2.svg' +import borrowRing3 from '@/assets/chainflip-lending/borrow-ring-3.svg' +import borrowRingInner from '@/assets/chainflip-lending/borrow-ring-inner.svg' +import earnGlow from '@/assets/chainflip-lending/earn-glow.svg' +import earnRingInner from '@/assets/chainflip-lending/earn-ring-inner.svg' +import earnRingMiddle from '@/assets/chainflip-lending/earn-ring-middle.svg' +import earnRingOuter from '@/assets/chainflip-lending/earn-ring-outer.svg' +import orbitalBtc from '@/assets/chainflip-lending/orbital-btc.svg' +import orbitalEth from '@/assets/chainflip-lending/orbital-eth.svg' +import orbitalSol from '@/assets/chainflip-lending/orbital-sol.svg' +import orbitalTether from '@/assets/chainflip-lending/orbital-tether.svg' +import orbitalUsdc from '@/assets/chainflip-lending/orbital-usdc.svg' +import refreshIcon from '@/assets/chainflip-lending/refresh-icon.svg' +import sparklesIcon from '@/assets/chainflip-lending/sparkles-icon.svg' import { Amount } from '@/components/Amount/Amount' import { AssetIcon } from '@/components/AssetIcon' import { HelperTooltip } from '@/components/HelperTooltip/HelperTooltip' @@ -117,56 +131,48 @@ const MarketRow = memo(({ pool, onViewMarket }: MarketRowProps) => { }) const AssetConstellation = memo(() => { + // Exact positions from Figma design context (2918:4222) + // Container is positioned absolutely from left=590px in the original ~1200px wide card + // We scale proportionally to fit our responsive layout return ( - {/* Orbital arc - sweeping from top-left to bottom-right (dark blue) */} - - {/* Orbital arc - bottom arc (green/orange tinted) */} - - {/* USDC - top left, medium */} - - + {/* Orbital ring SVGs from Figma - ETH blue arc */} + + {/* BTC orange arc */} + + {/* USDC arc */} + + {/* Tether arc */} + + {/* SOL arc */} + + {/* Asset icons - positioned per Figma */} + {/* USDC - top left */} + + {/* ETH - center, largest */} - - + + - {/* USDT - top right, medium-large */} - - + {/* USDT - top right */} + + - {/* BTC - bottom center-left, large */} - - + {/* BTC - bottom center-left */} + + - {/* SOL - bottom right, medium */} - - + {/* SOL - bottom right */} + + ) @@ -242,113 +248,80 @@ const MarketsTable = memo(() => { type InfoCardProps = { titleKey: string descriptionKey: string - icon: React.ElementType accentColor: string 'data-testid'?: string } const InfoCard = memo( - ({ titleKey, descriptionKey, icon, accentColor, 'data-testid': testId }: InfoCardProps) => { - const iconColor = `${accentColor}.500` - const bgGlow = `${accentColor}.900` - const ringColor = - accentColor === 'green' ? 'rgba(0, 205, 152, 0.2)' : 'rgba(128, 90, 213, 0.2)' - const ringColorStrong = - accentColor === 'green' ? 'rgba(0, 205, 152, 0.35)' : 'rgba(128, 90, 213, 0.35)' + ({ titleKey, descriptionKey, accentColor, 'data-testid': testId }: InfoCardProps) => { + const isGreen = accentColor === 'green' return ( - - - - - - - {/* Decorative art - concentric arcs with centered icon */} - - {/* Outer arc */} - - {/* Middle arc */} - + + + + - {/* Inner circle with icon */} -
- -
- {/* Radial lines for borrow card */} - {accentColor === 'purple' && ( +
+ {/* Art from Figma SVGs */} + + {isGreen ? ( <> - + + + +
- - - + +
+ + ) : ( + <> + {/* Borrow art - purple rings with radiating lines */} + + + + + +
+ top='24px' + right='30px' + width='64px' + height='64px' + borderRadius='full' + bg='rgba(128, 90, 213, 0.1)' + borderWidth='1px' + borderColor='blue.500' + backdropFilter='blur(25px)' + > + +
)}
@@ -428,14 +401,12 @@ export const InitView = memo(({ 'data-testid': testId }: { 'data-testid'?: strin From 7c98de821c3de691fda0bea97f630ee0ba995297 Mon Sep 17 00:00:00 2001 From: gomes-bot Date: Wed, 18 Mar 2026 00:28:27 +0100 Subject: [PATCH 04/18] fix: remove duplicate apy in supply input modal The Pool APY stat row at top was showing alongside a duplicate APY info row below the divider. Kept the top stat row (matches Figma) and removed the bottom duplicate. Estimated yearly earnings still shows when user enters an amount. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Pool/components/Supply/SupplyInput.tsx | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx b/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx index b8cc875f525..080c8c9db73 100644 --- a/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx +++ b/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx @@ -345,27 +345,15 @@ export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => {
- {supplyApyPercent !== null && ( + {(estYearlyEarningsFiat !== null && !bnOrZero(cryptoValue).isZero()) && ( <> - - - - {translate('chainflipLending.dashboard.apy')} - - - {supplyApyPercent}% - - - {estYearlyEarningsFiat !== null && !bnOrZero(cryptoValue).isZero() && ( - - - {translate('chainflipLending.dashboard.estimatedYearlyEarnings')} - - - - )} - + + + {translate('chainflipLending.dashboard.estimatedYearlyEarnings')} + + + )} From d298039ac8541c076769b8b538312726acc8a7a7 Mon Sep 17 00:00:00 2001 From: gomes-bot Date: Wed, 18 Mar 2026 00:30:15 +0100 Subject: [PATCH 05/18] fix: remove unused collateralWithFiat in borrowed section Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Pool/components/Supply/SupplyInput.tsx | 2 +- .../components/DashboardSections.tsx | 3 +- .../ChainflipLending/components/InitView.tsx | 178 ++++++++++++++++-- 3 files changed, 162 insertions(+), 21 deletions(-) diff --git a/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx b/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx index 080c8c9db73..e886f17bff1 100644 --- a/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx +++ b/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx @@ -345,7 +345,7 @@ export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => {
- {(estYearlyEarningsFiat !== null && !bnOrZero(cryptoValue).isZero()) && ( + {estYearlyEarningsFiat !== null && !bnOrZero(cryptoValue).isZero() && ( <> diff --git a/src/pages/ChainflipLending/components/DashboardSections.tsx b/src/pages/ChainflipLending/components/DashboardSections.tsx index d1d9c1c5c39..a0df7c35501 100644 --- a/src/pages/ChainflipLending/components/DashboardSections.tsx +++ b/src/pages/ChainflipLending/components/DashboardSections.tsx @@ -466,8 +466,7 @@ const BorrowedRow = ({ loan, borrowRate }: { loan: LoanWithFiat; borrowRate: str export const BorrowedSection = memo(() => { const translate = useTranslate() const chainflipLendingModal = useModal('chainflipLending') - const { loansWithFiat, totalBorrowedFiat, collateralWithFiat, isLoading } = - useChainflipLoanAccount() + const { loansWithFiat, totalBorrowedFiat, isLoading } = useChainflipLoanAccount() const { pools } = useChainflipLendingPools() const poolsByAssetId = useMemo( diff --git a/src/pages/ChainflipLending/components/InitView.tsx b/src/pages/ChainflipLending/components/InitView.tsx index cb3302af417..d95605e70ca 100644 --- a/src/pages/ChainflipLending/components/InitView.tsx +++ b/src/pages/ChainflipLending/components/InitView.tsx @@ -144,15 +144,61 @@ const AssetConstellation = memo(() => { overflow='visible' > {/* Orbital ring SVGs from Figma - ETH blue arc */} - + {/* BTC orange arc */} - + {/* USDC arc */} - + {/* Tether arc */} - + {/* SOL arc */} - + {/* Asset icons - positioned per Figma */} {/* USDC - top left */} @@ -160,19 +206,39 @@ const AssetConstellation = memo(() => { {/* ETH - center, largest */} - + {/* USDT - top right */} - + {/* BTC - bottom center-left */} - + {/* SOL - bottom right */} - + ) @@ -283,10 +349,42 @@ const InfoCard = memo( {isGreen ? ( <> {/* Earn Yield art - concentric green arcs */} - - - - + + + +
{/* Borrow art - purple rings with radiating lines */} - - - - - + + + + +
Date: Wed, 18 Mar 2026 00:36:07 +0100 Subject: [PATCH 06/18] fix: use figma svgs for next steps card art in sidebar Replace CSS-based concentric rings with exact Figma SVG assets for both green (supply) and purple (borrow) variants. Remove unused Icon, TbRefresh, TbSparkles imports. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/assets/translations/en/main.json | 7 +- .../Pool/components/Supply/SupplyInput.tsx | 265 ++++++++++++------ .../components/DashboardSidebar.tsx | 92 +++--- 3 files changed, 235 insertions(+), 129 deletions(-) diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 4190d11d01f..4b8532af237 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -2477,7 +2477,12 @@ "signing": "Signing supply transaction", "confirming": "Confirming supply" }, - "noFreeBalance": "No free balance. Deposit to Chainflip first." + "noFreeBalance": "No free balance. Deposit to Chainflip first.", + "assetToSupply": "Asset to supply", + "poolShare": "Pool Share", + "riskBand": "Risk band", + "conservativeStablecoin": "Conservative stablecoin pool", + "volatileAsset": "Volatile asset pool" }, "borrow": { "title": "Borrow", diff --git a/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx b/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx index e886f17bff1..e0c481fcf05 100644 --- a/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx +++ b/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx @@ -1,5 +1,6 @@ -import { Button, CardBody, CardFooter, Divider, Flex, Stack, VStack } from '@chakra-ui/react' +import { Box, Button, CardBody, CardFooter, Divider, Flex, Stack, VStack } from '@chakra-ui/react' import type { AssetId } from '@shapeshiftoss/caip' +import { usdcAssetId, usdtAssetId } from '@shapeshiftoss/caip' import type { Asset } from '@shapeshiftoss/types' import { BigAmount } from '@shapeshiftoss/utils' import { useCallback, useMemo, useState } from 'react' @@ -10,8 +11,8 @@ import { useTranslate } from 'react-polyglot' import { SupplyMachineCtx } from './SupplyMachineContext' import { Amount } from '@/components/Amount/Amount' +import { AssetIcon } from '@/components/AssetIcon' import { TradeAssetSelect } from '@/components/AssetSelection/AssetSelection' -import { HelperTooltip } from '@/components/HelperTooltip/HelperTooltip' import { SlideTransition } from '@/components/SlideTransition' import { RawText } from '@/components/Text' import { useLocaleFormatter } from '@/hooks/useLocaleFormatter/useLocaleFormatter' @@ -27,6 +28,8 @@ import { allowedDecimalSeparators } from '@/state/slices/preferencesSlice/prefer import { selectAssetById, selectAssets } from '@/state/slices/selectors' import { useAppSelector } from '@/state/store' +const STABLECOIN_ASSET_IDS: AssetId[] = [usdcAssetId, usdtAssetId] + type SupplyInputProps = { assetId: AssetId onAssetChange: (assetId: AssetId) => void @@ -91,11 +94,29 @@ export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => { return position?.totalAmountCryptoPrecision ?? '0' }, [supplyPositions, assetId]) + const currentPositionFiat = useMemo(() => { + const position = supplyPositions.find(p => p.assetId === assetId) + return position?.totalAmountFiat ?? '0' + }, [supplyPositions, assetId]) + const supplyApyPercent = useMemo( () => (poolForAsset ? bnOrZero(poolForAsset.supplyApy).times(100).toFixed(2) : null), [poolForAsset], ) + const poolSharePercent = useMemo(() => { + if (!poolForAsset) return '0.00' + const totalPoolCrypto = bnOrZero(poolForAsset.totalAmountCryptoPrecision) + if (totalPoolCrypto.isZero()) return '0.00' + const userCrypto = bnOrZero(currentPositionCrypto).plus(bnOrZero(cryptoValue)) + return userCrypto + .div(totalPoolCrypto.plus(bnOrZero(cryptoValue))) + .times(100) + .toFixed(2) + }, [poolForAsset, currentPositionCrypto, cryptoValue]) + + const isStablecoin = useMemo(() => STABLECOIN_ASSET_IDS.includes(assetId), [assetId]) + const estYearlyEarningsFiat = useMemo(() => { if (!poolForAsset || !marketData?.price) return null const apyDecimal = bnOrZero(poolForAsset.supplyApy) @@ -221,19 +242,67 @@ export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => { - - - {(supplyApyPercent !== null || bnOrZero(currentPositionCrypto).gt(0)) && ( - - {supplyApyPercent !== null && ( + {/* Asset to supply label */} + + {translate('chainflipLending.supply.assetToSupply')} + + + {/* Asset card */} + + + + + + + {asset.name} + + + + + {availableFiat !== undefined && ( + + )} + + + + {/* Hidden TradeAssetSelect for asset change handling */} + + + + + {/* Stats row - Pool APY and Current Position in bordered boxes */} + + {supplyApyPercent !== null && ( + {translate('chainflipLending.supply.poolApy')} @@ -242,23 +311,23 @@ export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => { {supplyApyPercent}% - )} - {bnOrZero(currentPositionCrypto).gt(0) && ( - - - {translate('chainflipLending.supply.currentPosition')} - - - - )} - - )} + + )} + + + + {translate('chainflipLending.supply.currentPosition')} + + {bnOrZero(currentPositionCrypto).gt(0) ? ( + + ) : ( + + )} + + + + {/* Amount label */} {translate('chainflipLending.supply.amount')} @@ -281,7 +350,7 @@ export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => { onValueChange={handleInputChange} style={{ flex: 1, - fontSize: '1.25rem', + fontSize: '1.5rem', fontWeight: 'bold', background: 'transparent', border: 'none', @@ -290,72 +359,104 @@ export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => { }} /> {!isFiat && ( - - {asset.symbol} - + + + + {asset.symbol} + + )} - {marketData?.price && ( - - )} - - - - - {translate('chainflipLending.supply.available')} - - - - - {availableFiat !== undefined && ( - - )} + {/* Fiat estimate + Balance/Max row */} + + {marketData?.price ? ( + + ) : ( + + )} + + + {translate('common.balance')}: + - - + + - + - {estYearlyEarningsFiat !== null && !bnOrZero(cryptoValue).isZero() && ( - <> - - + + + {/* Info rows */} + + {estYearlyEarningsFiat !== null && !bnOrZero(cryptoValue).isZero() && ( + {translate('chainflipLending.dashboard.estimatedYearlyEarnings')} - - )} + )} + + + + {translate('chainflipLending.supply.poolShare')} + + + {poolSharePercent}% + + + + + + {translate('chainflipLending.supply.autoCompounding')} + + + {translate('chainflipLending.supply.enabled')} + + + + + + {translate('chainflipLending.supply.riskBand')} + + + {isStablecoin + ? translate('chainflipLending.supply.conservativeStablecoin') + : translate('chainflipLending.supply.volatileAsset')} + + + {!hasFreeBalance && ( diff --git a/src/pages/ChainflipLending/components/DashboardSidebar.tsx b/src/pages/ChainflipLending/components/DashboardSidebar.tsx index 37f91fab0ed..0d66cd34055 100644 --- a/src/pages/ChainflipLending/components/DashboardSidebar.tsx +++ b/src/pages/ChainflipLending/components/DashboardSidebar.tsx @@ -7,14 +7,22 @@ import { CircularProgress, CircularProgressLabel, Flex, - Icon, Stack, } from '@chakra-ui/react' import type { AssetId } from '@shapeshiftoss/caip' import { memo, useCallback, useMemo } from 'react' -import { TbRefresh, TbSparkles } from 'react-icons/tb' import { useTranslate } from 'react-polyglot' +import borrowGlow from '@/assets/chainflip-lending/borrow-glow.svg' +import borrowRing1 from '@/assets/chainflip-lending/borrow-ring-1.svg' +import borrowRingInner from '@/assets/chainflip-lending/borrow-ring-inner.svg' +import earnGlow from '@/assets/chainflip-lending/earn-glow.svg' +import earnRingInner from '@/assets/chainflip-lending/earn-ring-inner.svg' +import earnRingMiddle from '@/assets/chainflip-lending/earn-ring-middle.svg' +import earnRingOuter from '@/assets/chainflip-lending/earn-ring-outer.svg' +import refreshIcon from '@/assets/chainflip-lending/refresh-icon.svg' +import sparklesIcon from '@/assets/chainflip-lending/sparkles-icon.svg' + import { Amount } from '@/components/Amount/Amount' import { HelperTooltip } from '@/components/HelperTooltip/HelperTooltip' import { Text } from '@/components/Text' @@ -95,52 +103,44 @@ export const BorrowingPowerCard = memo(() => { const NextStepsArt = memo(({ colorScheme }: { colorScheme: 'green' | 'purple' }) => { const isGreen = colorScheme === 'green' - const ringColor = isGreen ? 'green.500' : 'purple.500' - const bgColor = isGreen ? 'rgba(0, 205, 152, 0.05)' : 'rgba(128, 90, 213, 0.05)' - const iconColor = isGreen ? 'green.500' : 'purple.500' return ( -
- {/* Outer decorative rings */} - - - - {/* Center circle with icon */} -
- -
+
+ {isGreen ? ( + <> + + + + +
+ +
+ + ) : ( + <> + + + +
+ +
+ + )}
) }) From 81fa802174dd8b2ecb6d696e9efbf7721ab84aeb Mon Sep 17 00:00:00 2001 From: gomes-bot Date: Wed, 18 Mar 2026 00:38:51 +0100 Subject: [PATCH 07/18] fix: match next steps button sizes to figma (40px height, xl radius) All Next Steps card buttons now use height=40px, borderRadius=xl, fontWeight=semibold matching the Figma design. The Add Collateral button uses subtle whiteAlpha.50 bg for the outline variant. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Pool/components/Supply/SupplyInput.tsx | 19 ---------- .../components/DashboardSidebar.tsx | 37 +++++++++++++++++-- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx b/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx index e0c481fcf05..74bdf8b5bac 100644 --- a/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx +++ b/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx @@ -12,7 +12,6 @@ import { SupplyMachineCtx } from './SupplyMachineContext' import { Amount } from '@/components/Amount/Amount' import { AssetIcon } from '@/components/AssetIcon' -import { TradeAssetSelect } from '@/components/AssetSelection/AssetSelection' import { SlideTransition } from '@/components/SlideTransition' import { RawText } from '@/components/Text' import { useLocaleFormatter } from '@/hooks/useLocaleFormatter/useLocaleFormatter' @@ -164,11 +163,6 @@ export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => { }) }, [buyAssetSearch, onAssetChange, lendingAssets]) - const handleAssetChange = useCallback( - (asset: Asset) => onAssetChange(asset.assetId), - [onAssetChange], - ) - const handleInputChange = useCallback((values: NumberFormatValues) => { setInputValue(values.value) }, []) @@ -279,19 +273,6 @@ export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => {
- {/* Hidden TradeAssetSelect for asset change handling */} - - - - {/* Stats row - Pool APY and Current Position in bordered boxes */} {supplyApyPercent !== null && ( diff --git a/src/pages/ChainflipLending/components/DashboardSidebar.tsx b/src/pages/ChainflipLending/components/DashboardSidebar.tsx index 0d66cd34055..d16c3eb14f5 100644 --- a/src/pages/ChainflipLending/components/DashboardSidebar.tsx +++ b/src/pages/ChainflipLending/components/DashboardSidebar.tsx @@ -186,10 +186,25 @@ export const NextStepsCard = memo(() => { descriptionKey: 'chainflipLending.dashboard.nextStepsSupplyOrCollateral', actions: ( - - @@ -202,7 +217,14 @@ export const NextStepsCard = memo(() => { colorScheme: 'green' as const, descriptionKey: 'chainflipLending.dashboard.nextStepsCollateral', actions: ( - ), @@ -214,7 +236,14 @@ export const NextStepsCard = memo(() => { colorScheme: 'purple' as const, descriptionKey: 'chainflipLending.dashboard.nextStepsBorrow', actions: ( - ), From 704caf7b4a934cf599f8bbc74f3165adfec80582 Mon Sep 17 00:00:00 2001 From: gomes-bot Date: Wed, 18 Mar 2026 01:13:00 +0100 Subject: [PATCH 08/18] fix: visual polish pass 2 - filled buttons, column headers, supply confirm, init markets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SectionHeader primary action buttons now filled blue (matching Figma) - Supplied section column header "Amount" → "Supplied" - Collateral section now has Asset/Amount column headers when populated - Supply confirm wraps amount in bordered card per Figma - Supply confirm adds Asset info row + parameterized destination label - InitView MarketsTable now shows 6 columns including Borrow APR - InitView MarketsTable adds description text matching Markets.tsx Co-Authored-By: Claude Opus 4.6 (1M context) --- src/assets/translations/en/main.json | 4 +- .../Pool/components/Supply/SupplyConfirm.tsx | 50 ++++++++++++------- .../Pool/components/Supply/SupplyInput.tsx | 9 +++- .../components/DashboardSections.tsx | 25 +++++++--- .../ChainflipLending/components/InitView.tsx | 21 +++++++- 5 files changed, 81 insertions(+), 28 deletions(-) diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 4b8532af237..f166f0bb28e 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -2452,6 +2452,7 @@ "supply": { "title": "Supply", "subtitle": "Allocate your free balance into lending pools to earn yield", + "asset": "Asset", "amount": "Amount", "available": "Available to supply", "availableTooltip": "Your free balance on Chainflip State Chain available for lending", @@ -2464,7 +2465,8 @@ "autoCompounding": "Auto-compounding", "enabled": "Enabled", "destination": "Destination", - "lendingPool": "Lending Pool", + "lendingPool": "%{asset} Lending Pool", + "yearlyEarningsSuffix": "%{amount} / year", "executingTitle": "Supplying...", "executingDescription": "Your supply is being processed", "successTitle": "Supply Successful", diff --git a/src/pages/ChainflipLending/Pool/components/Supply/SupplyConfirm.tsx b/src/pages/ChainflipLending/Pool/components/Supply/SupplyConfirm.tsx index d9037bcd0c3..4b1335b9c3e 100644 --- a/src/pages/ChainflipLending/Pool/components/Supply/SupplyConfirm.tsx +++ b/src/pages/ChainflipLending/Pool/components/Supply/SupplyConfirm.tsx @@ -1,5 +1,5 @@ import { ArrowForwardIcon, CheckCircleIcon } from '@chakra-ui/icons' -import { Button, CardBody, CardFooter, Divider, Flex, HStack, VStack } from '@chakra-ui/react' +import { Box, Button, CardBody, CardFooter, Divider, Flex, HStack, VStack } from '@chakra-ui/react' import type { AssetId } from '@shapeshiftoss/caip' import { flipAssetId } from '@shapeshiftoss/caip' import { useQueryClient } from '@tanstack/react-query' @@ -269,16 +269,40 @@ export const SupplyConfirm = memo(({ assetId }: SupplyConfirmProps) => { - - + + + + + + + {translate('chainflipLending.supply.asset')} + + + + + {asset.symbol} + + + {translate('chainflipLending.supply.amount')} @@ -305,15 +329,7 @@ export const SupplyConfirm = memo(({ assetId }: SupplyConfirmProps) => { {translate('chainflipLending.supply.destination')} - {translate('chainflipLending.supply.lendingPool')} - - - - - {translate('chainflipLending.supply.autoCompounding')} - - - {translate('chainflipLending.supply.enabled')} + {translate('chainflipLending.supply.lendingPool', { asset: asset.symbol })} diff --git a/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx b/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx index 74bdf8b5bac..d32993eee0b 100644 --- a/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx +++ b/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx @@ -227,7 +227,7 @@ export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => { return translate('chainflipLending.supply.minimumSupply', { amount: `${bnOrZero(minSupply).decimalPlaces(2).toFixed()} ${asset?.symbol}`, }) - return translate('chainflipLending.supply.title') + return translate('common.next') }, [exceedsBalance, isBelowMinimum, minSupply, asset?.symbol, translate]) if (!asset) return null @@ -405,7 +405,12 @@ export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => { {translate('chainflipLending.dashboard.estimatedYearlyEarnings')} - + )} diff --git a/src/pages/ChainflipLending/components/DashboardSections.tsx b/src/pages/ChainflipLending/components/DashboardSections.tsx index a0df7c35501..69f881fa933 100644 --- a/src/pages/ChainflipLending/components/DashboardSections.tsx +++ b/src/pages/ChainflipLending/components/DashboardSections.tsx @@ -79,7 +79,7 @@ const SectionHeader = ({ {primaryAction && ( )} @@ -117,7 +117,7 @@ const EmptyState = ({ - diff --git a/src/pages/ChainflipLending/components/DashboardSidebar.tsx b/src/pages/ChainflipLending/components/DashboardSidebar.tsx index 7061d94ebc9..da5fd1c7182 100644 --- a/src/pages/ChainflipLending/components/DashboardSidebar.tsx +++ b/src/pages/ChainflipLending/components/DashboardSidebar.tsx @@ -286,7 +286,7 @@ export const NextStepsCard = memo(() => { return ( - + Date: Wed, 18 Mar 2026 10:21:04 +0100 Subject: [PATCH 14/18] fix: sidebar button font size consistency Co-Authored-By: Claude Opus 4.6 (1M context) --- src/pages/ChainflipLending/components/DashboardSidebar.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/ChainflipLending/components/DashboardSidebar.tsx b/src/pages/ChainflipLending/components/DashboardSidebar.tsx index da5fd1c7182..c1b92a125c6 100644 --- a/src/pages/ChainflipLending/components/DashboardSidebar.tsx +++ b/src/pages/ChainflipLending/components/DashboardSidebar.tsx @@ -219,6 +219,7 @@ export const NextStepsCard = memo(() => { height='40px' borderRadius='xl' fontWeight='semibold' + fontSize='sm' onClick={handleSupply} > {translate('chainflipLending.dashboard.supply')} @@ -229,6 +230,7 @@ export const NextStepsCard = memo(() => { height='40px' borderRadius='xl' fontWeight='semibold' + fontSize='sm' bg='whiteAlpha.50' onClick={handleAddCollateral} > From 6bc7cb3c46181a488096b7402fa83e864c28eec3 Mon Sep 17 00:00:00 2001 From: gomes-bot Date: Wed, 18 Mar 2026 10:36:27 +0100 Subject: [PATCH 15/18] fix: visual polish pass 4 - dashed empty borders, outline withdraw, white totals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Empty section cards (Supplied, Collateral, Borrowed) now have dashed borders when no data, matching Figma empty state styling - Withdraw button changed from ghost to outline variant with ↑ prefix - Section total fiat values now white (removed text.subtle) for prominence - Added Loan Health bar and dashed border steps to revamp-ui fixture Co-Authored-By: Claude Opus 4.6 (1M context) --- e2e/fixtures/chainflip-lending-revamp-ui.yaml | 26 +++++++++++++++++++ .../components/DashboardSections.tsx | 10 +++---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/e2e/fixtures/chainflip-lending-revamp-ui.yaml b/e2e/fixtures/chainflip-lending-revamp-ui.yaml index 1312644805d..e8ea628dec7 100644 --- a/e2e/fixtures/chainflip-lending-revamp-ui.yaml +++ b/e2e/fixtures/chainflip-lending-revamp-ui.yaml @@ -65,6 +65,32 @@ steps: Add Collateral modal opens with asset selection, amount input, and LTV gauge preview. screenshot: true + # === Loan Health / LTV bar === + - name: Loan Health bar (requires active loan) + instruction: > + If the wallet has an active loan (collateral posted + borrowed amount > 0), + verify the Loan Health card appears above the section cards on My Dashboard. + It should show: heart rate monitor icon, "Loan Health" label, Current LTV percentage + (color-coded green/yellow/red), Liquidation Distance fiat value on the right, + and a horizontal multi-segment bar (Safe green / Risky yellow / Liquidation red) + with a skull icon at the hard liquidation boundary. + If no active loan exists, the Loan Health card should NOT render (this is correct). + expected: > + Loan Health card renders when loans exist with LTV gauge bar, + or correctly hidden when no loans. + data-testid: chainflip-lending-loan-health + screenshot: true + + # === Visual polish checks === + - name: Empty section dashed borders + instruction: > + On the funded dashboard with no supply/collateral/borrowed positions, + verify that the Supplied, Collateral, and Borrowed section cards have + dashed borders (not solid) to visually indicate empty state. + expected: > + Empty section cards have dashed border style. Free Balance card (with data) has solid border. + screenshot: true + # === Markets tab === - name: Markets tab instruction: Close any open modal. Switch to the "Markets" tab. diff --git a/src/pages/ChainflipLending/components/DashboardSections.tsx b/src/pages/ChainflipLending/components/DashboardSections.tsx index 617b1899bf0..86ec22d182f 100644 --- a/src/pages/ChainflipLending/components/DashboardSections.tsx +++ b/src/pages/ChainflipLending/components/DashboardSections.tsx @@ -72,7 +72,7 @@ const SectionHeader = ({ - + @@ -87,7 +87,7 @@ const SectionHeader = ({ )} {secondaryAction && ( - )} @@ -281,7 +281,7 @@ export const SuppliedSection = memo(() => { }, [chainflipLendingModal, supplyPositions]) return ( - + { }, [chainflipLendingModal, collateralWithFiat]) return ( - + { }, [chainflipLendingModal, loansWithFiat]) return ( - + Date: Wed, 18 Mar 2026 11:17:50 +0100 Subject: [PATCH 16/18] fix: visual polish pass 5 - repay prefix, liquidation arrow, section details MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Repay button uses ↻ prefix instead of ↑ (not a withdraw action) - "Start Liquidation" link now has → arrow suffix matching Figma - Secondary action prefix is now configurable per section Co-Authored-By: Claude Opus 4.6 (1M context) --- .gemini/settings.json | 8 ++++++++ .../ChainflipLending/components/DashboardSections.tsx | 7 ++++--- 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 .gemini/settings.json diff --git a/.gemini/settings.json b/.gemini/settings.json new file mode 100644 index 00000000000..6935a3297af --- /dev/null +++ b/.gemini/settings.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "figma": { + "url": "https://mcp.figma.com/mcp", + "type": "sse" + } + } +} \ No newline at end of file diff --git a/src/pages/ChainflipLending/components/DashboardSections.tsx b/src/pages/ChainflipLending/components/DashboardSections.tsx index 86ec22d182f..2f68494bdeb 100644 --- a/src/pages/ChainflipLending/components/DashboardSections.tsx +++ b/src/pages/ChainflipLending/components/DashboardSections.tsx @@ -52,7 +52,7 @@ type SectionHeaderProps = { totalFiat: string isLoading: boolean primaryAction?: { labelKey: string; handleClick: () => void; testId?: string } - secondaryAction?: { labelKey: string; handleClick: () => void } + secondaryAction?: { labelKey: string; handleClick: () => void; prefix?: string } } const SectionHeader = ({ @@ -88,7 +88,7 @@ const SectionHeader = ({ )} {secondaryAction && ( )} @@ -546,6 +546,7 @@ export const BorrowedSection = memo(() => { ? { labelKey: 'chainflipLending.dashboard.repay', handleClick: handleRepay, + prefix: '↻', } : undefined } @@ -593,7 +594,7 @@ export const BorrowedSection = memo(() => { onClick={handleVoluntaryLiquidation} > {translate('chainflipLending.dashboard.cantRepay')}{' '} - {translate('chainflipLending.dashboard.startLiquidation')} + {translate('chainflipLending.dashboard.startLiquidation')} → ) : ( From 9f7d329833b37ed9bf4b69a4316431a72186148f Mon Sep 17 00:00:00 2001 From: gomes-bot Date: Wed, 18 Mar 2026 13:45:11 +0100 Subject: [PATCH 17/18] feat: visual polish and UX improvements for Chainflip Lending dashboard --- .gemini/settings.json | 8 - .gitignore | 1 + src/assets/translations/en/main.json | 1 + .../Pool/components/Borrow/BorrowInput.tsx | 15 +- .../Pool/components/Borrow/LtvGauge.tsx | 12 +- .../Pool/components/Borrow/RepayInput.tsx | 47 ++-- .../components/ChainflipLendingHeader.tsx | 44 ++-- .../components/DashboardSections.tsx | 222 +++++++++++++----- .../components/DashboardSidebar.tsx | 10 +- .../components/LoanHealth.tsx | 13 +- .../ChainflipLending/components/Markets.tsx | 34 ++- 11 files changed, 285 insertions(+), 122 deletions(-) delete mode 100644 .gemini/settings.json diff --git a/.gemini/settings.json b/.gemini/settings.json deleted file mode 100644 index 6935a3297af..00000000000 --- a/.gemini/settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "mcpServers": { - "figma": { - "url": "https://mcp.figma.com/mcp", - "type": "sse" - } - } -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 23407396ef9..b4d14549b04 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ yarn-error.log* # translation benchmark data scripts/translations/benchmark/ +.gemini/ diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 6988df8a64c..b1073873a04 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -2818,6 +2818,7 @@ "noActiveLoansDescription": "Post collateral first, then open borrow against it.", "cantRepay": "Can't Repay?", "startLiquidation": "Start Liquidation", + "topupAsset": "Top-up Asset", "getStarted": "Get Started", "depositFirstAsset": "Deposit your first asset to get started", "depositFirstAssetDescription": "Your first deposit handles everything - account creation, FLIP funding for State Chain gas, and a recovery address for the chain you deposit from. Future deposits skip all of this.", diff --git a/src/pages/ChainflipLending/Pool/components/Borrow/BorrowInput.tsx b/src/pages/ChainflipLending/Pool/components/Borrow/BorrowInput.tsx index 9fe127d95d1..61080ec5d75 100644 --- a/src/pages/ChainflipLending/Pool/components/Borrow/BorrowInput.tsx +++ b/src/pages/ChainflipLending/Pool/components/Borrow/BorrowInput.tsx @@ -33,6 +33,8 @@ type BorrowInputProps = { onAssetChange: (assetId: AssetId) => void } +const DEFAULT_RISKY_LTV = 0.8 + export const BorrowInput = ({ assetId, onAssetChange }: BorrowInputProps) => { const translate = useTranslate() const { @@ -100,6 +102,12 @@ export const BorrowInput = ({ assetId, onAssetChange }: BorrowInputProps) => { const projectedLtvDecimal = useMemo(() => projectedLtvBps / 10000, [projectedLtvBps]) + const riskyLtv = thresholds?.target ?? DEFAULT_RISKY_LTV + const projectedLtvColor = useMemo( + () => (projectedLtvDecimal > riskyLtv ? 'red.500' : 'text.base'), + [projectedLtvDecimal, riskyLtv], + ) + const assetIds = useMemo(() => Object.keys(CHAINFLIP_LENDING_ASSET_BY_ASSET_ID) as AssetId[], []) const assets = useAppSelector(selectAssets) @@ -256,7 +264,12 @@ export const BorrowInput = ({ assetId, onAssetChange }: BorrowInputProps) => { fontWeight='medium' /> - + )} diff --git a/src/pages/ChainflipLending/Pool/components/Borrow/LtvGauge.tsx b/src/pages/ChainflipLending/Pool/components/Borrow/LtvGauge.tsx index 47d9bb914e4..1ab09a02ebc 100644 --- a/src/pages/ChainflipLending/Pool/components/Borrow/LtvGauge.tsx +++ b/src/pages/ChainflipLending/Pool/components/Borrow/LtvGauge.tsx @@ -45,11 +45,6 @@ export const LtvGauge = memo(({ currentLtv, projectedLtv }: LtvGaugeProps) => { const riskyLtv = thresholds?.target ?? DEFAULT_RISKY_LTV const hardLiquidationLtv = thresholds?.hardLiquidation ?? DEFAULT_HARD_LIQUIDATION_LTV - const statusColor = useMemo( - () => getStatusColor(currentLtv, riskyLtv, hardLiquidationLtv), - [currentLtv, riskyLtv, hardLiquidationLtv], - ) - const thumbPosition = useMemo(() => ltvToPercent(currentLtv), [currentLtv]) const projectedThumbPosition = useMemo( @@ -153,12 +148,7 @@ export const LtvGauge = memo(({ currentLtv, projectedLtv }: LtvGaugeProps) => { zIndex={4} transition='left 0.3s ease' > - +
diff --git a/src/pages/ChainflipLending/Pool/components/Borrow/RepayInput.tsx b/src/pages/ChainflipLending/Pool/components/Borrow/RepayInput.tsx index 600103d31b8..4f681128d72 100644 --- a/src/pages/ChainflipLending/Pool/components/Borrow/RepayInput.tsx +++ b/src/pages/ChainflipLending/Pool/components/Borrow/RepayInput.tsx @@ -1,4 +1,4 @@ -import { Button, CardBody, CardFooter, Flex, Stack, Switch, VStack } from '@chakra-ui/react' +import { Box, Button, CardBody, CardFooter, Flex, Stack, Switch, VStack } from '@chakra-ui/react' import type { AssetId } from '@shapeshiftoss/caip' import { BigAmount } from '@shapeshiftoss/utils' import { useCallback, useMemo, useState } from 'react' @@ -189,12 +189,19 @@ export const RepayInput = ({ assetId }: RepayInputProps) => { {translate('chainflipLending.repay.outstanding')} - + + + + @@ -271,18 +278,20 @@ export const RepayInput = ({ assetId }: RepayInputProps) => { color='text.subtle' /> - {!isFullRepayment && ( - - )} + + {!isFullRepayment && ( + + )} + diff --git a/src/pages/ChainflipLending/components/ChainflipLendingHeader.tsx b/src/pages/ChainflipLending/components/ChainflipLendingHeader.tsx index 64209343b75..37bbe96a308 100644 --- a/src/pages/ChainflipLending/components/ChainflipLendingHeader.tsx +++ b/src/pages/ChainflipLending/components/ChainflipLendingHeader.tsx @@ -1,6 +1,6 @@ import { Button, Card, CardBody, Container, Flex, Heading, Skeleton, Stack } from '@chakra-ui/react' import { ethAssetId } from '@shapeshiftoss/caip' -import { useCallback, useMemo } from 'react' +import { memo, useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' import { useNavigate } from 'react-router-dom' @@ -60,7 +60,11 @@ const SummaryCard = ({ ) } -export const ChainflipLendingHeader = () => { +type ChainflipLendingHeaderProps = { + tabIndex?: number +} + +export const ChainflipLendingHeader = memo(({ tabIndex }: ChainflipLendingHeaderProps) => { const translate = useTranslate() const navigate = useNavigate() const { dispatch: walletDispatch } = useWallet() @@ -113,7 +117,7 @@ export const ChainflipLendingHeader = () => { - + @@ -129,7 +133,7 @@ export const ChainflipLendingHeader = () => { - {accountId ? ( + {accountId && tabIndex === 0 ? ( <> { labelKey='chainflipLending.dashboard.supplied' tooltipKey='chainflipLending.dashboard.suppliedTooltip' isLoading={isUserDataLoading} - labelColor='green.400' + labelColor='blue.200' data-testid='chainflip-lending-summary-supplied' /> { labelKey='chainflipLending.dashboard.collateral' tooltipKey='chainflipLending.dashboard.collateralTooltip' isLoading={isUserDataLoading} - labelColor='blue.300' + labelColor='purple.200' data-testid='chainflip-lending-summary-collateral' /> { labelKey='chainflipLending.dashboard.borrowed' tooltipKey='chainflipLending.dashboard.borrowedTooltip' isLoading={isUserDataLoading} - labelColor='purple.300' + labelColor='red.200' data-testid='chainflip-lending-summary-borrowed' /> @@ -170,7 +174,7 @@ export const ChainflipLendingHeader = () => { labelKey='chainflipLending.totalSupplied' tooltipKey='chainflipLending.totalSuppliedTooltip' isLoading={isPoolsLoading} - labelColor='green.400' + labelColor='blue.200' data-testid='chainflip-lending-summary-total-supplied' /> { labelKey='chainflipLending.availableLiquidity' tooltipKey='chainflipLending.availableLiquidityTooltip' isLoading={isPoolsLoading} - labelColor='blue.300' + labelColor='purple.200' data-testid='chainflip-lending-summary-available-liquidity' /> { labelKey='chainflipLending.totalBorrowed' tooltipKey='chainflipLending.totalBorrowedTooltip' isLoading={isPoolsLoading} - labelColor='purple.300' + labelColor='red.200' data-testid='chainflip-lending-summary-total-borrowed' /> + {!accountId && ( + + + + )} )} - {!accountId && ( - - - - )} ) -} +}) diff --git a/src/pages/ChainflipLending/components/DashboardSections.tsx b/src/pages/ChainflipLending/components/DashboardSections.tsx index 2f68494bdeb..17b37e6feec 100644 --- a/src/pages/ChainflipLending/components/DashboardSections.tsx +++ b/src/pages/ChainflipLending/components/DashboardSections.tsx @@ -1,7 +1,8 @@ -import { Button, Card, CardBody, Flex, HStack, Skeleton, Stack } from '@chakra-ui/react' +import { Badge, Button, Card, CardBody, Flex, HStack, Skeleton, Stack } from '@chakra-ui/react' import type { AssetId } from '@shapeshiftoss/caip' import { memo, useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' +import { useNavigate } from 'react-router-dom' import { Amount } from '@/components/Amount/Amount' import { AssetIcon } from '@/components/AssetIcon' @@ -9,7 +10,10 @@ import { HelperTooltip } from '@/components/HelperTooltip/HelperTooltip' import { RawText, Text } from '@/components/Text' import { useModal } from '@/hooks/useModal/useModal' import { bnOrZero } from '@/lib/bignumber/bignumber' -import { CHAINFLIP_LENDING_ASSET_BY_ASSET_ID } from '@/lib/chainflip/constants' +import { + CHAINFLIP_LENDING_ASSET_BY_ASSET_ID, + CHAINFLIP_LENDING_ASSET_IDS_BY_ASSET, +} from '@/lib/chainflip/constants' import type { ChainflipFreeBalanceWithFiat } from '@/pages/ChainflipLending/hooks/useChainflipFreeBalances' import { useChainflipFreeBalances } from '@/pages/ChainflipLending/hooks/useChainflipFreeBalances' import type { ChainflipLendingPoolWithFiat } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools' @@ -26,23 +30,44 @@ import { useAppSelector } from '@/state/store' const LENDING_ASSET_IDS = Object.keys(CHAINFLIP_LENDING_ASSET_BY_ASSET_ID) as AssetId[] +// Shared grid for consistent alignment across dashboard sections +const dashboardRowGrid = '1fr 100px 120px' + type AssetRowProps = { assetId: AssetId children: React.ReactNode + badge?: React.ReactNode + onClick?: () => void } -const AssetRow = ({ assetId, children }: AssetRowProps) => { +const AssetRow = ({ assetId, children, badge, onClick }: AssetRowProps) => { const asset = useAppSelector(state => selectAssetById(state, assetId)) if (!asset) return null return ( - + ) } @@ -79,7 +104,8 @@ const SectionHeader = ({ {primaryAction && ( )} {secondaryAction && ( - )} @@ -117,7 +148,7 @@ const EmptyState = ({ - @@ -128,10 +159,18 @@ const EmptyState = ({ const FreeBalanceRow = ({ balance }: { balance: ChainflipFreeBalanceWithFiat }) => { const assetId = balance.assetId const asset = useAppSelector(state => (assetId ? selectAssetById(state, assetId) : undefined)) + const navigate = useNavigate() + + const handleRowClick = useCallback(() => { + if (!assetId) return + navigate(`/chainflip-lending/pool/${assetId}`) + }, [navigate, assetId]) + if (!assetId || bnOrZero(balance.balanceCryptoPrecision).isZero()) return null return ( - + + { Asset - Balance + + Balance }> {nonZeroBalances.map(balance => @@ -231,20 +274,29 @@ const SuppliedRow = ({ apy: string }) => { const asset = useAppSelector(state => selectAssetById(state, position.assetId)) + const navigate = useNavigate() + + const handleRowClick = useCallback(() => { + navigate(`/chainflip-lending/pool/${position.assetId}`) + }, [navigate, position.assetId]) return ( - - - - - - - - + + + + + + ) } @@ -281,7 +333,11 @@ export const SuppliedSection = memo(() => { }, [chainflipLendingModal, supplyPositions]) return ( - + { Asset - - - Supplied - + + Supplied }> {supplyPositions.map(position => ( @@ -355,11 +412,34 @@ export const SuppliedSection = memo(() => { }) // Collateral Section -const CollateralRow = ({ collateral }: { collateral: CollateralWithFiat }) => { +const CollateralRow = ({ + collateral, + isTopupAsset, +}: { + collateral: CollateralWithFiat + isTopupAsset: boolean +}) => { + const translate = useTranslate() const asset = useAppSelector(state => selectAssetById(state, collateral.assetId)) + const navigate = useNavigate() + + const handleRowClick = useCallback(() => { + navigate(`/chainflip-lending/pool/${collateral.assetId}`) + }, [navigate, collateral.assetId]) return ( - + + {translate('chainflipLending.dashboard.topupAsset')} + + ) + } + onClick={handleRowClick} + > + { export const CollateralSection = memo(() => { const chainflipLendingModal = useModal('chainflipLending') - const { collateralWithFiat, totalCollateralFiat, isLoading } = useChainflipLoanAccount() + const { collateralWithFiat, totalCollateralFiat, loanAccount, isLoading } = + useChainflipLoanAccount() + + const topupAssetId = useMemo(() => { + if (!loanAccount?.collateral_topup_asset) return undefined + return CHAINFLIP_LENDING_ASSET_IDS_BY_ASSET[loanAccount.collateral_topup_asset.asset] + }, [loanAccount?.collateral_topup_asset]) const handleAddCollateral = useCallback(() => { const firstAssetId = LENDING_ASSET_IDS[0] @@ -389,7 +475,11 @@ export const CollateralSection = memo(() => { }, [chainflipLendingModal, collateralWithFiat]) return ( - + { Asset - Amount + + Amount }> {collateralWithFiat.map(collateral => ( - + ))} @@ -458,20 +556,29 @@ export const CollateralSection = memo(() => { // Borrowed Section const BorrowedRow = ({ loan, borrowRate }: { loan: LoanWithFiat; borrowRate: string }) => { const asset = useAppSelector(state => selectAssetById(state, loan.assetId)) + const navigate = useNavigate() + + const handleRowClick = useCallback(() => { + navigate(`/chainflip-lending/pool/${loan.assetId}`) + }, [navigate, loan.assetId]) return ( - - - - - - - - + + + + + + ) } @@ -524,7 +631,11 @@ export const BorrowedSection = memo(() => { }, [chainflipLendingModal, loansWithFiat]) return ( - + { ? { labelKey: 'chainflipLending.dashboard.repay', handleClick: handleRepay, - prefix: '↻', + prefix: '⟲', } : undefined } @@ -562,17 +673,18 @@ export const BorrowedSection = memo(() => { Asset - - - Amount - + + Amount { return bnOrZero(totalBorrowedFiat).div(maxBorrow).times(100).toNumber() }, [totalBorrowedFiat, maxBorrow]) + const gaugeColor = useMemo(() => (percentUsed > 80 ? 'red.500' : 'blue.500'), [percentUsed]) + if (!hasCollateral) return null return ( @@ -79,10 +81,10 @@ export const BorrowingPowerCard = memo(() => { value={percentUsed} size='120px' thickness='8px' - color={percentUsed > 80 ? 'red.500' : percentUsed > 50 ? 'yellow.500' : 'green.500'} + color={gaugeColor} trackColor='whiteAlpha.100' > - + {Math.round(percentUsed)}% @@ -90,9 +92,9 @@ export const BorrowingPowerCard = memo(() => { - + diff --git a/src/pages/ChainflipLending/components/LoanHealth.tsx b/src/pages/ChainflipLending/components/LoanHealth.tsx index 60d88dd5009..a6a0f31a62b 100644 --- a/src/pages/ChainflipLending/components/LoanHealth.tsx +++ b/src/pages/ChainflipLending/components/LoanHealth.tsx @@ -40,6 +40,17 @@ export const LoanHealth = memo(() => { return bnOrZero(totalCollateralFiat).minus(liquidationCollateral).toFixed(2) }, [totalCollateralFiat, totalBorrowedFiat, hardLiquidationLtv]) + const liquidationDistancePercent = useMemo(() => { + if (bnOrZero(totalCollateralFiat).isZero()) return 0 + return bnOrZero(liquidationDistance).div(totalCollateralFiat).times(100).toNumber() + }, [liquidationDistance, totalCollateralFiat]) + + const liquidationDistanceColor = useMemo(() => { + if (liquidationDistancePercent > 10) return 'green.500' + if (liquidationDistancePercent >= 5) return 'yellow.500' + return 'red.500' + }, [liquidationDistancePercent]) + const ltvColor = useMemo( () => getLtvColor(currentLtv, riskyLtv, hardLiquidationLtv), [currentLtv, riskyLtv, hardLiquidationLtv], @@ -88,7 +99,7 @@ export const LoanHealth = memo(() => { value={liquidationDistance} fontSize='sm' fontWeight='bold' - color={bnOrZero(liquidationDistance).gt(0) ? 'green.500' : 'red.500'} + color={liquidationDistanceColor} /> diff --git a/src/pages/ChainflipLending/components/Markets.tsx b/src/pages/ChainflipLending/components/Markets.tsx index 871775d5f15..7aca2a3effd 100644 --- a/src/pages/ChainflipLending/components/Markets.tsx +++ b/src/pages/ChainflipLending/components/Markets.tsx @@ -206,17 +206,41 @@ export const Markets = () => { const { accountId } = useChainflipLendingAccount() const [tabIndex, setTabIndex] = useState(0) - const headerComponent = useMemo(() => , []) + const headerComponent = useMemo(() => , [tabIndex]) return (
{accountId ? ( - - - {translate('chainflipLending.myDashboard')} - {translate('chainflipLending.markets')} + + + + {translate('chainflipLending.myDashboard')} + + + {translate('chainflipLending.markets')} + From 76f9f62969be9bbecb623ac5b0c4b81e9040f4d9 Mon Sep 17 00:00:00 2001 From: gomes-bot Date: Wed, 18 Mar 2026 14:19:46 +0100 Subject: [PATCH 18/18] fix: tackle coderabbit review comments - Fix Next Steps card hiding when no free balance but collateral exists - Localize hardcoded column headers in dashboard sections - Fix e2e fixture button style descriptions Co-Authored-By: Claude Opus 4.6 (1M context) --- e2e/fixtures/chainflip-lending-revamp-ui.yaml | 6 +++--- src/assets/translations/en/main.json | 1 + .../components/DashboardSections.tsx | 21 ++++++++++++------- .../components/DashboardSidebar.tsx | 4 ++-- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/e2e/fixtures/chainflip-lending-revamp-ui.yaml b/e2e/fixtures/chainflip-lending-revamp-ui.yaml index e8ea628dec7..0da1ee74806 100644 --- a/e2e/fixtures/chainflip-lending-revamp-ui.yaml +++ b/e2e/fixtures/chainflip-lending-revamp-ui.yaml @@ -36,9 +36,9 @@ steps: - name: Empty sections display instruction: Scroll down on the funded dashboard to see Supplied, Collateral, and Borrowed sections. expected: > - Supplied section: "$0.00", "No earning positions yet" empty state with "+ Deposit" outline button. - Collateral section: "$0.00", "Provide collateral to start borrowing" with "+ Add Collateral" button. - Borrowed section: "$0.00", "No Active Loans" with "+ Add Collateral" button. + Supplied section: "$0.00", "No earning positions yet" empty state with blue "+ Deposit" button. + Collateral section: "$0.00", "Provide collateral to start borrowing" with blue "+ Add Collateral" button. + Borrowed section: "$0.00", "No Active Loans" with blue "+ Add Collateral" button. screenshot: true # === Action modals from dashboard === diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index b1073873a04..857525fad9a 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -2844,6 +2844,7 @@ "borrowRate": "Borrow Rate", "viewDashboard": "View Dashboard", "lendingMarkets": "Lending Markets", + "asset": "Asset", "lendingMarketsDescription": "Each pool is isolated per asset. Pick a market to see what you could earn or borrow before you deposit." } }, diff --git a/src/pages/ChainflipLending/components/DashboardSections.tsx b/src/pages/ChainflipLending/components/DashboardSections.tsx index 17b37e6feec..8cfe95e3cbc 100644 --- a/src/pages/ChainflipLending/components/DashboardSections.tsx +++ b/src/pages/ChainflipLending/components/DashboardSections.tsx @@ -184,6 +184,7 @@ const FreeBalanceRow = ({ balance }: { balance: ChainflipFreeBalanceWithFiat }) } export const FreeBalanceSection = memo(() => { + const translate = useTranslate() const chainflipLendingModal = useModal('chainflipLending') const { freeBalances, isLoading, totalFiat } = useChainflipFreeBalances() @@ -246,9 +247,9 @@ export const FreeBalanceSection = memo(() => { gridTemplateColumns={dashboardRowGrid} columnGap={4} > - Asset + {translate('chainflipLending.dashboard.asset')} - Balance + {translate('common.balance')} }> {nonZeroBalances.map(balance => @@ -302,6 +303,7 @@ const SuppliedRow = ({ } export const SuppliedSection = memo(() => { + const translate = useTranslate() const chainflipLendingModal = useModal('chainflipLending') const { supplyPositions, isLoading } = useChainflipSupplyPositions() const { pools } = useChainflipLendingPools() @@ -382,9 +384,11 @@ export const SuppliedSection = memo(() => { gridTemplateColumns={dashboardRowGrid} columnGap={4} > - Asset + {translate('chainflipLending.dashboard.asset')} - Supplied + + {translate('chainflipLending.dashboard.supplied')} + }> {supplyPositions.map(position => ( @@ -453,6 +457,7 @@ const CollateralRow = ({ } export const CollateralSection = memo(() => { + const translate = useTranslate() const chainflipLendingModal = useModal('chainflipLending') const { collateralWithFiat, totalCollateralFiat, loanAccount, isLoading } = useChainflipLoanAccount() @@ -524,9 +529,9 @@ export const CollateralSection = memo(() => { gridTemplateColumns={dashboardRowGrid} columnGap={4} > - Asset + {translate('chainflipLending.dashboard.asset')} - Amount + {translate('common.amount')} }> {collateralWithFiat.map(collateral => ( @@ -682,9 +687,9 @@ export const BorrowedSection = memo(() => { gridTemplateColumns={dashboardRowGrid} columnGap={4} > - Asset + {translate('chainflipLending.dashboard.asset')} - Amount + {translate('common.amount')} { if (firstAssetId) chainflipLendingModal.open({ mode: 'borrow', assetId: firstAssetId }) }, [chainflipLendingModal]) - // Hide when user has everything or no free balance - if (!hasFreeBalance || (hasSupply && hasCollateral && hasLoans)) return null + // Hide when user has completed all steps + if (hasSupply && hasCollateral && hasLoans) return null const getContent = () => { if (hasFreeBalance && !hasSupply && !hasCollateral) {