diff --git a/src/earn/components/DepositBalance.test.tsx b/src/earn/components/DepositBalance.test.tsx index ed7892239a..b306f59caa 100644 --- a/src/earn/components/DepositBalance.test.tsx +++ b/src/earn/components/DepositBalance.test.tsx @@ -16,6 +16,8 @@ const baseContext = { depositedAmount: '0', withdrawAmount: '0', setWithdrawAmount: vi.fn(), + depositCalls: [], + withdrawCalls: [], }; describe('DepositBalance', () => { diff --git a/src/earn/components/DepositButton.test.tsx b/src/earn/components/DepositButton.test.tsx new file mode 100644 index 0000000000..88e94f823f --- /dev/null +++ b/src/earn/components/DepositButton.test.tsx @@ -0,0 +1,123 @@ +import type { Call } from '@/transaction/types'; +import { render, screen } from '@testing-library/react'; +import type { Address } from 'viem'; +import { describe, expect, it, vi } from 'vitest'; +import { DepositButton } from './DepositButton'; +import { useEarnContext } from './EarnProvider'; + +vi.mock('./EarnProvider', () => ({ + useEarnContext: vi.fn(), +})); + +vi.mock('wagmi', () => ({ + useAccount: vi.fn(), + useConfig: vi.fn(), + useCapabilities: vi.fn(), +})); + +const baseContext = { + convertedBalance: '1000', + setDepositAmount: vi.fn(), + vaultAddress: '0x123' as Address, + depositAmount: '0', + depositedAmount: '0', + withdrawAmount: '0', + setWithdrawAmount: vi.fn(), + depositCalls: [], + withdrawCalls: [], +}; + +vi.mock('@/transaction', async (importOriginal) => { + return { + ...(await importOriginal()), + TransactionLifecycleStatus: vi.fn(), + TransactionButton: ({ + text, + disabled, + }: { text: string; disabled: boolean }) => ( + + ), + Transaction: ({ + onStatus, + children, + capabilities, + }: { + onStatus: (status: { statusName: string }) => void; + children: React.ReactNode; + capabilities: { paymasterService: { url: string } }; + }) => ( + <> +
+ + + + +
{capabilities?.paymasterService?.url}
+
+ {children} + + ), + TransactionSponsor: vi.fn(), + TransactionStatus: vi.fn(), + TransactionStatusAction: vi.fn(), + TransactionStatusLabel: vi.fn(), + }; +}); + +vi.mock('@/internal/hooks/useTheme', () => ({ + useTheme: vi.fn(), +})); + +describe('DepositButton Component', () => { + it('renders Transaction with depositCalls from EarnProvider', () => { + const mockDepositCalls = [{ to: '0x123', data: '0x456' }] as Call[]; + vi.mocked(useEarnContext).mockReturnValue({ + ...baseContext, + depositCalls: mockDepositCalls, + }); + + render(); + + const transactionElement = screen.getByTestId('transaction'); + expect(transactionElement).toBeInTheDocument(); + }); + + it('renders TransactionButton with the correct text', () => { + vi.mocked(useEarnContext).mockReturnValue({ + ...baseContext, + depositCalls: [], + }); + + const { container } = render(); + + expect(container).toHaveTextContent('Deposit'); + }); +}); diff --git a/src/earn/components/DepositButton.tsx b/src/earn/components/DepositButton.tsx new file mode 100644 index 0000000000..138197ef1b --- /dev/null +++ b/src/earn/components/DepositButton.tsx @@ -0,0 +1,13 @@ +import { Transaction, TransactionButton } from '@/transaction'; +import type { DepositButtonReact } from '../types'; +import { useEarnContext } from './EarnProvider'; + +export function DepositButton({ className }: DepositButtonReact) { + const { depositCalls } = useEarnContext(); + + return ( + + + + ); +} diff --git a/src/earn/components/DepositDetails.test.tsx b/src/earn/components/DepositDetails.test.tsx new file mode 100644 index 0000000000..3159ee2653 --- /dev/null +++ b/src/earn/components/DepositDetails.test.tsx @@ -0,0 +1,60 @@ +import { usdcToken } from '@/token/constants'; +import { render, screen } from '@testing-library/react'; +import type { Address } from 'viem'; +import { type Mock, describe, expect, it, vi } from 'vitest'; +import { DepositDetails } from './DepositDetails'; +import { useEarnContext } from './EarnProvider'; + +vi.mock('./EarnProvider', () => ({ + useEarnContext: vi.fn(), +})); + +vi.mock('@/internal/hooks/useTheme', () => ({ + useTheme: vi.fn(), +})); + +const baseContext = { + convertedBalance: '1000', + setDepositAmount: vi.fn(), + vaultAddress: '0x123' as Address, + depositAmount: '0', + depositedAmount: '0', + withdrawAmount: '0', + setWithdrawAmount: vi.fn(), + depositCalls: [], + withdrawCalls: [], +}; + +describe('DepositDetails Component', () => { + it('renders EarnDetails with default APY tag when APY is provided', () => { + const mockApy = '5.00%'; + vi.mocked(useEarnContext).mockReturnValue({ ...baseContext, apy: mockApy }); + + const { container } = render(); + + const tokenElement = screen.getByTestId('ockTokenChip_Button'); + expect(tokenElement).toHaveTextContent(usdcToken.name); + + // const tagElement = screen.getByTestId('tag'); + expect(container).toHaveTextContent(`APY ${mockApy}`); + }); + + it('renders EarnDetails with an empty tag when APY is not provided', () => { + vi.mocked(useEarnContext).mockReturnValue({ ...baseContext, apy: '' }); + + render(); + + const tokenElement = screen.getByTestId('ockTokenChip_Button'); + expect(tokenElement).toHaveTextContent(usdcToken.name); + }); + + it('applies custom className to the EarnDetails container', () => { + const customClass = 'custom-class'; + (useEarnContext as Mock).mockReturnValue({ apy: null }); + + render(); + + const earnDetails = screen.getByTestId('ockEarnDetails'); + expect(earnDetails).toHaveClass(customClass); + }); +}); diff --git a/src/earn/components/DepositDetails.tsx b/src/earn/components/DepositDetails.tsx new file mode 100644 index 0000000000..bf04be62d0 --- /dev/null +++ b/src/earn/components/DepositDetails.tsx @@ -0,0 +1,26 @@ +import { usdcToken } from '@/token/constants'; +import { useMemo } from 'react'; +import type { DepositDetailsReact } from '../types'; +import { EarnDetails } from './EarnDetails'; +import { useEarnContext } from './EarnProvider'; + +export function DepositDetails({ className }: DepositDetailsReact) { + const { apy } = useEarnContext(); + + const tag = useMemo(() => { + if (apy) { + return `APY ${apy}`; + } + return ''; + }, [apy]); + + // TODO: update token when we have logic to fetch vault info + return ( + + ); +} diff --git a/src/earn/components/Earn.test.tsx b/src/earn/components/Earn.test.tsx new file mode 100644 index 0000000000..9f55d11bfe --- /dev/null +++ b/src/earn/components/Earn.test.tsx @@ -0,0 +1,72 @@ +import type { Call } from '@/transaction/types'; +import { useGetTokenBalance } from '@/wallet/hooks/useGetTokenBalance'; +import { render, screen } from '@testing-library/react'; +import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; +import { useAccount, useConfig } from 'wagmi'; +import { Earn } from './Earn'; + +vi.mock('wagmi', () => ({ + useAccount: vi.fn(), + useConfig: vi.fn(), + useCapabilities: vi.fn(), +})); + +vi.mock('@/transaction', () => ({ + Transaction: ({ + className, + calls, + children, + }: { className: string; calls: Call[]; children: React.ReactNode }) => ( +
+ {children} +
+ ), + TransactionButton: ({ text }: { text: string }) => ( + + ), +})); + +vi.mock('@/wallet/hooks/useGetTokenBalance', () => ({ + useGetTokenBalance: vi.fn(), +})); + +vi.mock('@/internal/hooks/useTheme', () => ({ + useTheme: vi.fn(), +})); + +describe('Earn Component', () => { + beforeEach(() => { + (useAccount as Mock).mockReturnValue({ + address: '0x123', + }); + (useGetTokenBalance as Mock).mockReturnValue({ + convertedBalance: '0.0', + error: null, + }); + (useConfig as Mock).mockReturnValue({}); + }); + + it('renders custom children when provided', () => { + const customChildren =

Custom Children

; + render({customChildren}); + + expect(screen.getByText('Custom Children')).toBeInTheDocument(); + expect(screen.queryByTestId('tabs')).not.toBeInTheDocument(); + }); + + it('renders default tabs and their contents when children are not provided', () => { + const { container } = render(); + + const tabs = screen.getByTestId('ockTabs'); + expect(tabs).toBeInTheDocument(); + + expect(container).toHaveTextContent('Deposit'); + expect(container).toHaveTextContent('Withdraw'); + }); +}); diff --git a/src/earn/components/Earn.tsx b/src/earn/components/Earn.tsx new file mode 100644 index 0000000000..f73a24aba2 --- /dev/null +++ b/src/earn/components/Earn.tsx @@ -0,0 +1,56 @@ +import { Tab, TabContent, Tabs, TabsList } from '@/internal'; +import { border, cn } from '@/styles/theme'; +import type { EarnReact } from '../types'; +import { EarnDeposit } from './EarnDeposit'; +import { EarnProvider } from './EarnProvider'; +import { EarnWithdraw } from './EarnWithdraw'; + +export function Earn({ + children = , + className, + vaultAddress, +}: EarnReact) { + return ( + +
+ {children} +
+
+ ); +} + +function EarnDefaultContent() { + return ( + + + Deposit + Withdraw + + + + + + + + + ); +} diff --git a/src/earn/components/EarnAmountInput.tsx b/src/earn/components/EarnAmountInput.tsx index 25852b8162..993e51e55d 100644 --- a/src/earn/components/EarnAmountInput.tsx +++ b/src/earn/components/EarnAmountInput.tsx @@ -12,7 +12,10 @@ export function EarnAmountInput({ 'aria-label': ariaLabel, }: EarnAmountInputReact) { return ( -
+
{ + it('renders children correctly', () => { + render( + +

Test Child

+
, + ); + + expect(screen.getByText('Test Child')).toBeInTheDocument(); + }); + + it('applies custom className', () => { + const customClass = 'custom-class'; + render( + +
earn
+
, + ); + + const cardElement = screen.getByTestId('ockEarnCard'); + expect(cardElement).toHaveClass(customClass); + }); + + it('has default styles applied', () => { + render( + +
earn
+
, + ); + + const cardElement = screen.getByTestId('ockEarnCard'); + expect(cardElement).toHaveClass('border-t'); + expect(cardElement).toHaveClass('flex'); + expect(cardElement).toHaveClass('flex-col'); + expect(cardElement).toHaveClass('p-4'); + expect(cardElement).toHaveClass('gap-4'); + }); +}); diff --git a/src/earn/components/EarnCard.tsx b/src/earn/components/EarnCard.tsx new file mode 100644 index 0000000000..b330b3a849 --- /dev/null +++ b/src/earn/components/EarnCard.tsx @@ -0,0 +1,18 @@ +import { background, border, cn } from '@/styles/theme'; +import type { EarnCardReact } from '../types'; + +export function EarnCard({ children, className }: EarnCardReact) { + return ( +
+ {children} +
+ ); +} diff --git a/src/earn/components/EarnDeposit.test.tsx b/src/earn/components/EarnDeposit.test.tsx new file mode 100644 index 0000000000..b822ac11fa --- /dev/null +++ b/src/earn/components/EarnDeposit.test.tsx @@ -0,0 +1,83 @@ +import type { Call } from '@/transaction/types'; +import { render, screen } from '@testing-library/react'; +import type { Address } from 'viem'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { EarnDeposit } from './EarnDeposit'; +import { useEarnContext } from './EarnProvider'; + +vi.mock('@/internal/hooks/useTheme', () => ({ + useTheme: vi.fn(), +})); + +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(), + interest: '1.2k', + apy: '5%', + depositCalls: [], + withdrawCalls: [], +}; + +vi.mock('@/transaction', () => ({ + Transaction: ({ + className, + calls, + children, + }: { className: string; calls: Call[]; children: React.ReactNode }) => ( +
+ {children} +
+ ), + TransactionButton: ({ text }: { text: string }) => ( + + ), +})); + +describe('EarnDeposit Component', () => { + beforeEach(() => { + vi.mocked(useEarnContext).mockReturnValue(baseContext); + }); + + it('renders children when provided', () => { + const mockChildren =

Custom Children

; + render({mockChildren}); + + const earnCard = screen.getByTestId('ockEarnCard'); + expect(earnCard).toBeInTheDocument(); + expect(earnCard).toHaveTextContent('Custom Children'); + }); + + it('renders default components when children are not provided', () => { + render(); + + const earnCard = screen.getByTestId('ockEarnCard'); + expect(earnCard).toBeInTheDocument(); + + expect(screen.getByTestId('ockEarnDetails')).toBeInTheDocument(); + expect(screen.getByTestId('ockEarnAmountInput')).toBeInTheDocument(); + expect(screen.getByTestId('ockEarnBalance')).toBeInTheDocument(); + }); + + it('applies custom className to the EarnCard', () => { + const customClass = 'custom-class'; + render(); + + const earnCard = screen.getByTestId('ockEarnCard'); + expect(earnCard).toHaveClass(customClass); + }); +}); diff --git a/src/earn/components/EarnDeposit.tsx b/src/earn/components/EarnDeposit.tsx new file mode 100644 index 0000000000..c54ddae8d9 --- /dev/null +++ b/src/earn/components/EarnDeposit.tsx @@ -0,0 +1,24 @@ +import type { EarnDepositReact } from '../types'; +import { DepositAmountInput } from './DepositAmountInput'; +import { DepositBalance } from './DepositBalance'; +import { DepositButton } from './DepositButton'; +import { DepositDetails } from './DepositDetails'; +import { EarnCard } from './EarnCard'; + +export function EarnDeposit({ + children = , + className, +}: EarnDepositReact) { + return {children}; +} + +function EarnDepositDefaultContent() { + return ( + <> + + + + + + ); +} diff --git a/src/earn/components/EarnDetails.test.tsx b/src/earn/components/EarnDetails.test.tsx new file mode 100644 index 0000000000..53c19bf210 --- /dev/null +++ b/src/earn/components/EarnDetails.test.tsx @@ -0,0 +1,48 @@ +import { usdcToken } from '@/token/constants'; +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { EarnDetails } from './EarnDetails'; + +vi.mock('@/internal/hooks/useTheme', () => ({ + useTheme: vi.fn(), +})); + +describe('EarnDetails Component', () => { + it('renders nothing when token is not provided', () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it('renders TokenChip with the correct props', () => { + render(); + + const tokenChip = screen.getByTestId('ockTokenChip_Button'); + expect(tokenChip).toBeInTheDocument(); + expect(tokenChip).toHaveTextContent('USDC'); + expect(tokenChip).toHaveClass('!bg-transparent'); + }); + + it('renders tag with default styles when tagVariant is "default"', () => { + const mockTag = 'Default Tag'; + render( + , + ); + + const tagElement = screen.getByText(mockTag); + expect(tagElement).toBeInTheDocument(); + expect(tagElement).toHaveClass('flex'); + expect(tagElement).toHaveClass('items-center'); + expect(tagElement).toHaveClass('justify-center'); + expect(tagElement).toHaveClass('rounded-full'); + expect(tagElement).toHaveClass('p-1'); + expect(tagElement).toHaveClass('px-3'); + }); + + it('applies custom className to the container', () => { + const customClass = 'custom-class'; + render(); + + const container = screen.getByTestId('ockTokenChip_Button').parentElement; + expect(container).toHaveClass(customClass); + }); +}); diff --git a/src/earn/components/EarnDetails.tsx b/src/earn/components/EarnDetails.tsx new file mode 100644 index 0000000000..dcc3cebd55 --- /dev/null +++ b/src/earn/components/EarnDetails.tsx @@ -0,0 +1,42 @@ +import { background, border, cn, color, text } from '@/styles/theme'; +import { TokenChip } from '@/token'; +import type { EarnDetailsReact } from '../types'; + +export function EarnDetails({ + className, + token, + tag, + tagVariant = 'default', +}: EarnDetailsReact) { + if (!token) { + return null; + } + return ( +
+ + {tag && ( +
+ {tag} +
+ )} +
+ ); +} diff --git a/src/earn/components/EarnProvider.tsx b/src/earn/components/EarnProvider.tsx index fc579ff730..044b9a9378 100644 --- a/src/earn/components/EarnProvider.tsx +++ b/src/earn/components/EarnProvider.tsx @@ -24,6 +24,12 @@ export function EarnProvider({ vaultAddress, children }: EarnProviderReact) { setWithdrawAmount, // TODO: update when we have logic to fetch deposited amount depositedAmount: '', + // TODO: update when we have logic to fetch apy + apy: '', + // TODO: update when we have logic to fetch interest + interest: '', + withdrawCalls: [], + depositCalls: [], }); return {children}; diff --git a/src/earn/components/EarnWithdraw.test.tsx b/src/earn/components/EarnWithdraw.test.tsx new file mode 100644 index 0000000000..f23d7ff944 --- /dev/null +++ b/src/earn/components/EarnWithdraw.test.tsx @@ -0,0 +1,82 @@ +import type { Call } from '@/transaction/types'; +import { render, screen } from '@testing-library/react'; +import type { Address } from 'viem'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useEarnContext } from './EarnProvider'; +import { EarnWithdraw } from './EarnWithdraw'; + +vi.mock('@/internal/hooks/useTheme', () => ({ + useTheme: vi.fn(), +})); + +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(), + interest: '1.2k', + depositCalls: [], + withdrawCalls: [], +}; + +vi.mock('@/transaction', () => ({ + Transaction: ({ + className, + calls, + children, + }: { className: string; calls: Call[]; children: React.ReactNode }) => ( +
+ {children} +
+ ), + TransactionButton: ({ text }: { text: string }) => ( + + ), +})); + +describe('EarnWithdraw Component', () => { + beforeEach(() => { + vi.mocked(useEarnContext).mockReturnValue(baseContext); + }); + + it('renders children when provided', () => { + const mockChildren =

Custom Children

; + render({mockChildren}); + + const earnCard = screen.getByTestId('ockEarnCard'); + expect(earnCard).toBeInTheDocument(); + expect(earnCard).toHaveTextContent('Custom Children'); + }); + + it('renders default components when children are not provided', () => { + render(); + + const earnCard = screen.getByTestId('ockEarnCard'); + expect(earnCard).toBeInTheDocument(); + + expect(screen.getByTestId('ockEarnDetails')).toBeInTheDocument(); + expect(screen.getByTestId('ockEarnAmountInput')).toBeInTheDocument(); + expect(screen.getByTestId('ockEarnBalance')).toBeInTheDocument(); + }); + + it('applies custom className to the EarnCard', () => { + const customClass = 'custom-class'; + render(); + + const earnCard = screen.getByTestId('ockEarnCard'); + expect(earnCard).toHaveClass(customClass); + }); +}); diff --git a/src/earn/components/EarnWithdraw.tsx b/src/earn/components/EarnWithdraw.tsx new file mode 100644 index 0000000000..8b5ccfc43e --- /dev/null +++ b/src/earn/components/EarnWithdraw.tsx @@ -0,0 +1,24 @@ +import type { EarnWithdrawReact } from '../types'; +import { EarnCard } from './EarnCard'; +import { WithdrawAmountInput } from './WithdrawAmountInput'; +import { WithdrawBalance } from './WithdrawBalance'; +import { WithdrawButton } from './WithdrawButton'; +import { WithdrawDetails } from './WithdrawDetails'; + +export function EarnWithdraw({ + children = , + className, +}: EarnWithdrawReact) { + return {children}; +} + +function EarnWithdrawDefaultContent() { + return ( + <> + + + + + + ); +} diff --git a/src/earn/components/WithdrawBalance.test.tsx b/src/earn/components/WithdrawBalance.test.tsx index a0ff4e6f98..84fe206edb 100644 --- a/src/earn/components/WithdrawBalance.test.tsx +++ b/src/earn/components/WithdrawBalance.test.tsx @@ -16,6 +16,8 @@ const baseContext = { depositedAmount: '1000', withdrawAmount: '0', setWithdrawAmount: vi.fn(), + depositCalls: [], + withdrawCalls: [], }; describe('WithdrawBalance', () => { diff --git a/src/earn/components/WithdrawButton.test.tsx b/src/earn/components/WithdrawButton.test.tsx new file mode 100644 index 0000000000..349b8053d5 --- /dev/null +++ b/src/earn/components/WithdrawButton.test.tsx @@ -0,0 +1,123 @@ +import type { Call } from '@/transaction/types'; +import { render, screen } from '@testing-library/react'; +import type { Address } from 'viem'; +import { describe, expect, it, vi } from 'vitest'; +import { useEarnContext } from './EarnProvider'; +import { WithdrawButton } from './WithdrawButton'; + +vi.mock('./EarnProvider', () => ({ + useEarnContext: vi.fn(), +})); + +vi.mock('wagmi', () => ({ + useAccount: vi.fn(), + useConfig: vi.fn(), + useCapabilities: vi.fn(), +})); + +const baseContext = { + convertedBalance: '1000', + setDepositAmount: vi.fn(), + vaultAddress: '0x123' as Address, + depositAmount: '0', + depositedAmount: '0', + withdrawAmount: '0', + setWithdrawAmount: vi.fn(), + depositCalls: [], + withdrawCalls: [], +}; + +vi.mock('@/transaction', async (importOriginal) => { + return { + ...(await importOriginal()), + TransactionLifecycleStatus: vi.fn(), + TransactionButton: ({ + text, + disabled, + }: { text: string; disabled: boolean }) => ( + + ), + Transaction: ({ + onStatus, + children, + capabilities, + }: { + onStatus: (status: { statusName: string }) => void; + children: React.ReactNode; + capabilities: { paymasterService: { url: string } }; + }) => ( + <> +
+ + + + +
{capabilities?.paymasterService?.url}
+
+ {children} + + ), + TransactionSponsor: vi.fn(), + TransactionStatus: vi.fn(), + TransactionStatusAction: vi.fn(), + TransactionStatusLabel: vi.fn(), + }; +}); + +vi.mock('@/internal/hooks/useTheme', () => ({ + useTheme: vi.fn(), +})); + +describe('WithdrawButton Component', () => { + it('renders Transaction with depositCalls from EarnProvider', () => { + const mockDepositCalls = [{ to: '0x123', data: '0x456' }] as Call[]; + vi.mocked(useEarnContext).mockReturnValue({ + ...baseContext, + depositCalls: mockDepositCalls, + }); + + render(); + + const transactionElement = screen.getByTestId('transaction'); + expect(transactionElement).toBeInTheDocument(); + }); + + it('renders TransactionButton with the correct text', () => { + vi.mocked(useEarnContext).mockReturnValue({ + ...baseContext, + depositCalls: [], + }); + + const { container } = render(); + + expect(container).toHaveTextContent('Withdraw'); + }); +}); diff --git a/src/earn/components/WithdrawButton.tsx b/src/earn/components/WithdrawButton.tsx new file mode 100644 index 0000000000..614508e5f9 --- /dev/null +++ b/src/earn/components/WithdrawButton.tsx @@ -0,0 +1,13 @@ +import { Transaction, TransactionButton } from '@/transaction'; +import type { WithdrawButtonReact } from '../types'; +import { useEarnContext } from './EarnProvider'; + +export function WithdrawButton({ className }: WithdrawButtonReact) { + const { withdrawCalls } = useEarnContext(); + + return ( + + + + ); +} diff --git a/src/earn/components/WithdrawDetails.test.tsx b/src/earn/components/WithdrawDetails.test.tsx new file mode 100644 index 0000000000..25e03769cf --- /dev/null +++ b/src/earn/components/WithdrawDetails.test.tsx @@ -0,0 +1,62 @@ +import { usdcToken } from '@/token/constants'; +import { render, screen } from '@testing-library/react'; +import type { Address } from 'viem'; +import { type Mock, describe, expect, it, vi } from 'vitest'; +import { useEarnContext } from './EarnProvider'; +import { WithdrawDetails } from './WithdrawDetails'; + +vi.mock('./EarnProvider', () => ({ + useEarnContext: vi.fn(), +})); + +vi.mock('@/internal/hooks/useTheme', () => ({ + useTheme: vi.fn(), +})); + +const baseContext = { + convertedBalance: '1000', + setDepositAmount: vi.fn(), + vaultAddress: '0x123' as Address, + depositAmount: '0', + depositedAmount: '0', + withdrawAmount: '0', + setWithdrawAmount: vi.fn(), + depositCalls: [], + withdrawCalls: [], +}; + +describe('WithdrawDetails Component', () => { + it('renders EarnDetails with interest earned when interest is provided', () => { + const mockInterest = '1.2k'; + vi.mocked(useEarnContext).mockReturnValue({ + ...baseContext, + interest: mockInterest, + }); + + const { container } = render(); + + const tokenElement = screen.getByTestId('ockTokenChip_Button'); + expect(tokenElement).toHaveTextContent(usdcToken.name); + + expect(container).toHaveTextContent(`${mockInterest} interest earned`); + }); + + it('renders EarnDetails with an empty tag when interest is not provided', () => { + vi.mocked(useEarnContext).mockReturnValue({ ...baseContext, apy: '' }); + + render(); + + const tokenElement = screen.getByTestId('ockTokenChip_Button'); + expect(tokenElement).toHaveTextContent(usdcToken.name); + }); + + it('applies custom className to the EarnDetails container', () => { + const customClass = 'custom-class'; + (useEarnContext as Mock).mockReturnValue({ apy: null }); + + render(); + + const earnDetails = screen.getByTestId('ockEarnDetails'); + expect(earnDetails).toHaveClass(customClass); + }); +}); diff --git a/src/earn/components/WithdrawDetails.tsx b/src/earn/components/WithdrawDetails.tsx new file mode 100644 index 0000000000..407cb036f9 --- /dev/null +++ b/src/earn/components/WithdrawDetails.tsx @@ -0,0 +1,26 @@ +import { usdcToken } from '@/token/constants'; +import { useMemo } from 'react'; +import type { WithdrawDetailsReact } from '../types'; +import { EarnDetails } from './EarnDetails'; +import { useEarnContext } from './EarnProvider'; + +export function WithdrawDetails({ className }: WithdrawDetailsReact) { + const { interest } = useEarnContext(); + + const tag = useMemo(() => { + if (interest) { + return `${interest} interest earned`; + } + return ''; + }, [interest]); + + // TODO: update token when we have logic to fetch vault info + return ( + + ); +} diff --git a/src/earn/types.ts b/src/earn/types.ts index a325be83e8..4376150f4a 100644 --- a/src/earn/types.ts +++ b/src/earn/types.ts @@ -1,18 +1,30 @@ +import type { Token } from '@/token'; +import type { Call } from '@/transaction/types'; import type { Address } from 'viem'; +export type EarnReact = { + children?: React.ReactNode; + className?: string; + vaultAddress: Address; +}; + export type EarnProviderReact = { children: React.ReactNode; vaultAddress: Address; }; export type EarnContextType = { + apy?: string; convertedBalance?: string; vaultAddress: Address; depositAmount: string; depositedAmount: string; + interest?: string; setDepositAmount: (amount: string) => void; - withdrawAmount: string; setWithdrawAmount: (amount: string) => void; + withdrawAmount: string; + withdrawCalls: Call[]; + depositCalls: Call[]; }; export type EarnAmountInputReact = { @@ -45,3 +57,41 @@ export type DepositBalanceReact = { export type WithdrawBalanceReact = { className?: string; }; + +export type EarnCardReact = { + children: React.ReactNode; + className?: string; +}; + +export type EarnDepositReact = { + children?: React.ReactNode; + className?: string; +}; + +export type EarnWithdrawReact = { + children?: React.ReactNode; + className?: string; +}; + +export type EarnDetailsReact = { + className?: string; + token?: Token; + tag?: string; + tagVariant?: 'default' | 'primary'; +}; + +export type DepositDetailsReact = { + className?: string; +}; + +export type WithdrawDetailsReact = { + className?: string; +}; + +export type DepositButtonReact = { + className?: string; +}; + +export type WithdrawButtonReact = { + className?: string; +}; diff --git a/src/internal/components/tabs/Tabs.tsx b/src/internal/components/tabs/Tabs.tsx index 90c31a54ef..a012d0daf8 100644 --- a/src/internal/components/tabs/Tabs.tsx +++ b/src/internal/components/tabs/Tabs.tsx @@ -40,7 +40,10 @@ export function Tabs({ children, defaultValue, className }: TabsReact) { const componentTheme = useTheme(); return ( -
+
{children}
diff --git a/src/internal/index.ts b/src/internal/index.ts new file mode 100644 index 0000000000..b675eddbb4 --- /dev/null +++ b/src/internal/index.ts @@ -0,0 +1,4 @@ +export { Tabs } from './components/tabs/Tabs'; +export { Tab } from './components/tabs/Tab'; +export { TabsList } from './components/tabs/TabsList'; +export { TabContent } from './components/tabs/TabContent';