diff --git a/src/earn/components/DepositBalance.test.tsx b/src/earn/components/DepositBalance.test.tsx new file mode 100644 index 0000000000..ed7892239a --- /dev/null +++ b/src/earn/components/DepositBalance.test.tsx @@ -0,0 +1,77 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import type { Address } from 'viem'; +import { describe, expect, it, vi } from 'vitest'; +import { DepositBalance } from './DepositBalance'; +import { useEarnContext } from './EarnProvider'; + +vi.mock('./EarnProvider', () => ({ + useEarnContext: vi.fn(), +})); + +const baseContext = { + convertedBalance: '1000', + setDepositAmount: vi.fn(), + vaultAddress: '0x123' as Address, + depositAmount: '0', + depositedAmount: '0', + withdrawAmount: '0', + setWithdrawAmount: vi.fn(), +}; + +describe('DepositBalance', () => { + it('renders the converted balance and subtitle correctly', () => { + vi.mocked(useEarnContext).mockReturnValue(baseContext); + + render(); + + expect(screen.getByText('1000 USDC')).toBeInTheDocument(); + expect(screen.getByText('Available to deposit')).toBeInTheDocument(); + }); + + it('calls setDepositAmount with convertedBalance when the action button is clicked', () => { + const mockSetDepositAmount = vi.fn(); + const mockContext = { + ...baseContext, + convertedBalance: '1000', + setDepositAmount: mockSetDepositAmount, + }; + + vi.mocked(useEarnContext).mockReturnValue(mockContext); + + render(); + + const actionButton = screen.getByText('Use max'); + fireEvent.click(actionButton); + + expect(mockSetDepositAmount).toHaveBeenCalledWith('1000'); + }); + + it('does not render the action button when convertedBalance is null', () => { + const mockContext = { + ...baseContext, + convertedBalance: '', + setDepositAmount: vi.fn(), + }; + + vi.mocked(useEarnContext).mockReturnValue(mockContext); + + render(); + + expect(screen.queryByText('Use max')).not.toBeInTheDocument(); + }); + + it('applies custom className', () => { + const mockContext = { + ...baseContext, + convertedBalance: '1000', + setDepositAmount: vi.fn(), + }; + + vi.mocked(useEarnContext).mockReturnValue(mockContext); + + render(); + + const container = screen.getByTestId('ockEarnBalance'); + expect(container).toHaveClass('custom-class'); + }); +}); diff --git a/src/earn/components/DepositBalance.tsx b/src/earn/components/DepositBalance.tsx new file mode 100644 index 0000000000..dc20798618 --- /dev/null +++ b/src/earn/components/DepositBalance.tsx @@ -0,0 +1,24 @@ +import { useCallback } from 'react'; +import type { DepositBalanceReact } from '../types'; +import { EarnBalance } from './EarnBalance'; +import { useEarnContext } from './EarnProvider'; + +export function DepositBalance({ className }: DepositBalanceReact) { + const { convertedBalance, setDepositAmount } = useEarnContext(); + + const handleMaxPress = useCallback(() => { + if (convertedBalance) { + setDepositAmount(convertedBalance); + } + }, [convertedBalance, setDepositAmount]); + + return ( + + ); +} diff --git a/src/earn/components/EarnBalance.test.tsx b/src/earn/components/EarnBalance.test.tsx new file mode 100644 index 0000000000..51cf23614b --- /dev/null +++ b/src/earn/components/EarnBalance.test.tsx @@ -0,0 +1,80 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { EarnBalance } from './EarnBalance'; + +describe('EarnBalance', () => { + it('renders the title and subtitle correctly', () => { + render( + {}} + />, + ); + + expect(screen.getByText('Test Title')).toBeInTheDocument(); + expect(screen.getByText('Test Subtitle')).toBeInTheDocument(); + }); + + it('renders the action button when showAction is true', () => { + render( + {}} + />, + ); + + expect(screen.getByText('Use max')).toBeInTheDocument(); + }); + + it('does not render the action button when showAction is false', () => { + render( + {}} + />, + ); + + expect(screen.queryByText('Use max')).not.toBeInTheDocument(); + }); + + it('calls onActionPress when the action button is clicked', () => { + const mockOnActionPress = vi.fn(); + + render( + , + ); + + const actionButton = screen.getByText('Use max'); + fireEvent.click(actionButton); + + expect(mockOnActionPress).toHaveBeenCalledTimes(1); + }); + + it('applies custom className', () => { + const customClass = 'custom-class'; + + render( + {}} + />, + ); + + const container = screen.getByTestId('ockEarnBalance'); + expect(container).toHaveClass(customClass); + }); +}); diff --git a/src/earn/components/EarnBalance.tsx b/src/earn/components/EarnBalance.tsx new file mode 100644 index 0000000000..9204abd3d6 --- /dev/null +++ b/src/earn/components/EarnBalance.tsx @@ -0,0 +1,37 @@ +import { background, border, cn, color, text } from '@/styles/theme'; +import type { EarnBalanceReact } from '../types'; + +export function EarnBalance({ + className, + onActionPress, + title, + subtitle, + showAction = false, +}: EarnBalanceReact) { + return ( +
+
+
{title}
+
{subtitle}
+
+ {showAction && ( + + )} +
+ ); +} diff --git a/src/earn/components/EarnProvider.test.tsx b/src/earn/components/EarnProvider.test.tsx index 84ccf2febf..fdb582787c 100644 --- a/src/earn/components/EarnProvider.test.tsx +++ b/src/earn/components/EarnProvider.test.tsx @@ -1,8 +1,31 @@ +import { useGetTokenBalance } from '@/wallet/hooks/useGetTokenBalance'; import { renderHook } from '@testing-library/react'; -import { describe, expect, it } from 'vitest'; +import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; +import { useAccount } from 'wagmi'; import { EarnProvider, useEarnContext } from './EarnProvider'; +vi.mock('@/wallet/hooks/useGetTokenBalance', () => ({ + useGetTokenBalance: vi.fn(), +})); + +vi.mock('wagmi', async (importOriginal) => { + return { + ...(await importOriginal()), + useAccount: vi.fn(), + }; +}); + describe('EarnProvider', () => { + beforeEach(() => { + (useAccount as Mock).mockReturnValue({ + address: '0x123', + }); + (useGetTokenBalance as Mock).mockReturnValue({ + convertedBalance: '0.0', + error: null, + }); + }); + const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); diff --git a/src/earn/components/EarnProvider.tsx b/src/earn/components/EarnProvider.tsx index 627dd7b1cd..84e3e2bdb4 100644 --- a/src/earn/components/EarnProvider.tsx +++ b/src/earn/components/EarnProvider.tsx @@ -1,19 +1,29 @@ import { useValue } from '@/core-react/internal/hooks/useValue'; +import { usdcToken } from '@/token/constants'; +import { useGetTokenBalance } from '@/wallet/hooks/useGetTokenBalance'; import { createContext, useContext, useState } from 'react'; +import { useAccount } from 'wagmi'; import type { EarnContextType, EarnProviderReact } from '../types'; const EarnContext = createContext(undefined); export function EarnProvider({ vaultAddress, children }: EarnProviderReact) { + const { address } = useAccount(); + const [depositAmount, setDepositAmount] = useState(''); const [withdrawAmount, setWithdrawAmount] = useState(''); + const { convertedBalance } = useGetTokenBalance(address, usdcToken); + const value = useValue({ + convertedBalance, vaultAddress, depositAmount, setDepositAmount, withdrawAmount, setWithdrawAmount, + // TODO: update when we have logic to fetch deposited amount + depositedAmount: '', }); return {children}; diff --git a/src/earn/components/WithdrawBalance.test.tsx b/src/earn/components/WithdrawBalance.test.tsx new file mode 100644 index 0000000000..a0ff4e6f98 --- /dev/null +++ b/src/earn/components/WithdrawBalance.test.tsx @@ -0,0 +1,77 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import type { Address } from 'viem'; +import { describe, expect, it, vi } from 'vitest'; +import { useEarnContext } from './EarnProvider'; +import { WithdrawBalance } from './WithdrawBalance'; + +vi.mock('./EarnProvider', () => ({ + useEarnContext: vi.fn(), +})); + +const baseContext = { + convertedBalance: '0', + setDepositAmount: vi.fn(), + vaultAddress: '0x123' as Address, + depositAmount: '0', + depositedAmount: '1000', + withdrawAmount: '0', + setWithdrawAmount: vi.fn(), +}; + +describe('WithdrawBalance', () => { + it('renders the converted balance and subtitle correctly', () => { + vi.mocked(useEarnContext).mockReturnValue(baseContext); + + render(); + + expect(screen.getByText('1000 USDC')).toBeInTheDocument(); + expect(screen.getByText('Available to withdraw')).toBeInTheDocument(); + }); + + it('calls setWithdrawAmount with convertedBalance when the action button is clicked', () => { + const mocksetWithdrawAmount = vi.fn(); + const mockContext = { + ...baseContext, + depositedAmount: '1000', + setWithdrawAmount: mocksetWithdrawAmount, + }; + + vi.mocked(useEarnContext).mockReturnValue(mockContext); + + render(); + + const actionButton = screen.getByText('Use max'); + fireEvent.click(actionButton); + + expect(mocksetWithdrawAmount).toHaveBeenCalledWith('1000'); + }); + + it('does not render the action button when convertedBalance is null', () => { + const mockContext = { + ...baseContext, + depositedAmount: '', + setWithdrawAmount: vi.fn(), + }; + + vi.mocked(useEarnContext).mockReturnValue(mockContext); + + render(); + + expect(screen.queryByText('Use max')).not.toBeInTheDocument(); + }); + + it('applies custom className', () => { + const mockContext = { + ...baseContext, + depositedAmount: '1000', + setWithdrawAmount: vi.fn(), + }; + + vi.mocked(useEarnContext).mockReturnValue(mockContext); + + render(); + + const container = screen.getByTestId('ockEarnBalance'); + expect(container).toHaveClass('custom-class'); + }); +}); diff --git a/src/earn/components/WithdrawBalance.tsx b/src/earn/components/WithdrawBalance.tsx new file mode 100644 index 0000000000..c90b305f7a --- /dev/null +++ b/src/earn/components/WithdrawBalance.tsx @@ -0,0 +1,24 @@ +import { useCallback } from 'react'; +import type { WithdrawBalanceReact } from '../types'; +import { EarnBalance } from './EarnBalance'; +import { useEarnContext } from './EarnProvider'; + +export function WithdrawBalance({ className }: WithdrawBalanceReact) { + const { depositedAmount, setWithdrawAmount } = useEarnContext(); + + const handleMaxPress = useCallback(() => { + if (depositedAmount) { + setWithdrawAmount(depositedAmount); + } + }, [depositedAmount, setWithdrawAmount]); + + return ( + + ); +} diff --git a/src/earn/types.ts b/src/earn/types.ts index 38eeaad7dd..a325be83e8 100644 --- a/src/earn/types.ts +++ b/src/earn/types.ts @@ -6,8 +6,10 @@ export type EarnProviderReact = { }; export type EarnContextType = { + convertedBalance?: string; vaultAddress: Address; depositAmount: string; + depositedAmount: string; setDepositAmount: (amount: string) => void; withdrawAmount: string; setWithdrawAmount: (amount: string) => void; @@ -28,3 +30,18 @@ export type WithdrawAmountInputReact = { export type DepositAmountInputReact = { className?: string; }; +export type EarnBalanceReact = { + className?: string; + onActionPress: () => void; + title: string; + subtitle: string; + showAction?: boolean; +}; + +export type DepositBalanceReact = { + className?: string; +}; + +export type WithdrawBalanceReact = { + className?: string; +};