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 (
+
+
+ {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;
+};