Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add earn balance cards #1873

Merged
merged 9 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions src/earn/components/DepositBalance.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<DepositBalance className="test-class" />);

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(<DepositBalance className="test-class" />);

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(<DepositBalance className="test-class" />);

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(<DepositBalance className="custom-class" />);

const container = screen.getByTestId('ockEarnBalance');
expect(container).toHaveClass('custom-class');
});
});
24 changes: 24 additions & 0 deletions src/earn/components/DepositBalance.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<EarnBalance
className={className}
title={`${convertedBalance} USDC`}
subtitle="Available to deposit"
onActionPress={handleMaxPress}
showAction={!!convertedBalance}
/>
);
}
80 changes: 80 additions & 0 deletions src/earn/components/EarnBalance.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<EarnBalance
title="Test Title"
subtitle="Test Subtitle"
showAction={false}
onActionPress={() => {}}
/>,
);

expect(screen.getByText('Test Title')).toBeInTheDocument();
expect(screen.getByText('Test Subtitle')).toBeInTheDocument();
});

it('renders the action button when showAction is true', () => {
render(
<EarnBalance
title="Test Title"
subtitle="Test Subtitle"
showAction={true}
onActionPress={() => {}}
/>,
);

expect(screen.getByText('Use max')).toBeInTheDocument();
});

it('does not render the action button when showAction is false', () => {
render(
<EarnBalance
title="Test Title"
subtitle="Test Subtitle"
showAction={false}
onActionPress={() => {}}
/>,
);

expect(screen.queryByText('Use max')).not.toBeInTheDocument();
});

it('calls onActionPress when the action button is clicked', () => {
const mockOnActionPress = vi.fn();

render(
<EarnBalance
title="Test Title"
subtitle="Test Subtitle"
showAction={true}
onActionPress={mockOnActionPress}
/>,
);

const actionButton = screen.getByText('Use max');
fireEvent.click(actionButton);

expect(mockOnActionPress).toHaveBeenCalledTimes(1);
});

it('applies custom className', () => {
const customClass = 'custom-class';

render(
<EarnBalance
title="Test Title"
subtitle="Test Subtitle"
className={customClass}
showAction={false}
onActionPress={() => {}}
/>,
);

const container = screen.getByTestId('ockEarnBalance');
expect(container).toHaveClass(customClass);
});
});
37 changes: 37 additions & 0 deletions src/earn/components/EarnBalance.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className={cn(
background.alternate,
border.radius,
'flex items-center justify-between gap-4 p-3 px-4',
className,
)}
data-testid="ockEarnBalance"
>
<div className={cn('flex flex-col', color.foreground)}>
<div className={text.headline}>{title}</div>
<div className={cn(text.label2, color.foregroundMuted)}>{subtitle}</div>
</div>
{showAction && (
<button
onClick={onActionPress}
className={cn(text.label2, color.primary)}
type="button"
aria-label="Use max"
>
Use max
</button>
)}
</div>
);
}
25 changes: 24 additions & 1 deletion src/earn/components/EarnProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof import('wagmi')>()),
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 }) => (
<EarnProvider vaultAddress="0x123">{children}</EarnProvider>
);
Expand Down
10 changes: 10 additions & 0 deletions src/earn/components/EarnProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<EarnContextType | undefined>(undefined);

export function EarnProvider({ vaultAddress, children }: EarnProviderReact) {
const { address } = useAccount();

const [depositAmount, setDepositAmount] = useState('');
const [withdrawAmount, setWithdrawAmount] = useState('');

const { convertedBalance } = useGetTokenBalance(address, usdcToken);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be fine for testing but will probably have to make this more generic given we will try and support all vaults not just USDC

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah lets leave as is and revisit when we're hooking up the actions


const value = useValue({
convertedBalance,
vaultAddress,
depositAmount,
setDepositAmount,
withdrawAmount,
setWithdrawAmount,
// TODO: update when we have logic to fetch deposited amount
depositedAmount: '',
});

return <EarnContext.Provider value={value}>{children}</EarnContext.Provider>;
Expand Down
77 changes: 77 additions & 0 deletions src/earn/components/WithdrawBalance.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<WithdrawBalance className="test-class" />);

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(<WithdrawBalance className="test-class" />);

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(<WithdrawBalance className="test-class" />);

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(<WithdrawBalance className="custom-class" />);

const container = screen.getByTestId('ockEarnBalance');
expect(container).toHaveClass('custom-class');
});
});
24 changes: 24 additions & 0 deletions src/earn/components/WithdrawBalance.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<EarnBalance
className={className}
title={`${depositedAmount} USDC`}
subtitle="Available to withdraw"
onActionPress={handleMaxPress}
showAction={!!depositedAmount}
/>
);
}
Loading
Loading