Skip to content

Commit

Permalink
feat: add connect wallet functionality to Swap component (#1173)
Browse files Browse the repository at this point in the history
Co-authored-by: Alissa Crane <[email protected]>
  • Loading branch information
abcrane123 and alissacrane-cb authored Aug 28, 2024
1 parent 8372d07 commit 1a25d7d
Show file tree
Hide file tree
Showing 15 changed files with 301 additions and 111 deletions.
5 changes: 5 additions & 0 deletions .changeset/olive-monkeys-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@coinbase/onchainkit": patch
---

- **patch**: Add connect wallet functionality to Swap component for disconnected user. By @abcrane123 #1173
84 changes: 39 additions & 45 deletions playground/nextjs-app-router/components/demo/Swap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ import {
} from '@coinbase/onchainkit/swap';
import type { Token } from '@coinbase/onchainkit/token';
import { useCallback, useContext } from 'react';
import { useAccount } from 'wagmi';
import { base } from 'viem/chains';
import { AppContext } from '../AppProvider';

function SwapComponent() {
const { address } = useAccount();
const { chainId } = useContext(AppContext);

const degenToken: Token = {
Expand All @@ -23,7 +22,7 @@ function SwapComponent() {
decimals: 18,
image:
'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/3b/bf/3bbf118b5e6dc2f9e7fc607a6e7526647b4ba8f0bea87125f971446d57b296d2-MDNmNjY0MmEtNGFiZi00N2I0LWIwMTItMDUyMzg2ZDZhMWNm',
chainId: 8453,
chainId: base.id,
};

const ethToken: Token = {
Expand All @@ -33,7 +32,7 @@ function SwapComponent() {
decimals: 18,
image:
'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png',
chainId: 8453,
chainId: base.id,
};

const usdcToken: Token = {
Expand All @@ -43,7 +42,7 @@ function SwapComponent() {
decimals: 6,
image:
'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2',
chainId: 8453,
chainId: base.id,
};

const wethToken: Token = {
Expand All @@ -53,7 +52,7 @@ function SwapComponent() {
decimals: 6,
image:
'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/47/bc/47bc3593c2dec7c846b66b7ba5f6fa6bd69ec34f8ebb931f2a43072e5aaac7a8-YmUwNmRjZDUtMjczYy00NDFiLWJhZDUtMzgwNjFmYWM0Njkx',
chainId: 8453,
chainId: base.id,
};

const swappableTokens = [degenToken, ethToken, usdcToken, wethToken];
Expand All @@ -63,53 +62,48 @@ function SwapComponent() {
}, []);

return (
<div className="relative h-full w-full">
{address ? (
chainId !== 8453 ? (
<div className="absolute top-0 left-0 z-10 flex h-full w-full flex-col justify-center rounded-xl bg-[#000000] bg-opacity-50 text-center">
<div className="mx-auto w-2/3 rounded-md bg-muted p-6 text-sm">
Swap Demo is only available on Base.
<br />
Please change your chain in settings.
</div>
</div>
) : (
<></>
)
) : (
<div className="relative flex h-full w-full flex-col items-center">
{chainId !== base.id ? (
<div className="absolute top-0 left-0 z-10 flex h-full w-full flex-col justify-center rounded-xl bg-[#000000] bg-opacity-50 text-center">
<div className="mx-auto w-2/3 rounded-md bg-muted p-6 text-sm">
Swap Demo requires wallet.
Swap Demo is only available on Base.
<br />
Please connect in settings.
You're connected to a different network. Switch to Base to continue
using the app.
</div>
</div>
)}
{address ? (
<Swap className="border bg-[#ffffff]" onStatus={handleOnStatus}>
<SwapAmountInput
label="Sell"
swappableTokens={swappableTokens}
token={ethToken}
type="from"
/>
<SwapToggleButton />
<SwapAmountInput
label="Buy"
swappableTokens={swappableTokens}
token={usdcToken}
type="to"
/>
<SwapButton
disabled={
ENVIRONMENT_VARIABLES[ENVIRONMENT.ENVIRONMENT] === 'production'
}
/>
<SwapMessage />
</Swap>
) : (
<></>
)}

{ENVIRONMENT_VARIABLES[ENVIRONMENT.ENVIRONMENT] === 'production' &&
chainId === base.id ? (
<div className="mb-5 italic">
Note: Swap is disabled on production. To test, run the app locally.
</div>
) : null}

<Swap className="border bg-[#ffffff]" onStatus={handleOnStatus}>
<SwapAmountInput
label="Sell"
swappableTokens={swappableTokens}
token={ethToken}
type="from"
/>
<SwapToggleButton />
<SwapAmountInput
label="Buy"
swappableTokens={swappableTokens}
token={usdcToken}
type="to"
/>
<SwapButton
disabled={
ENVIRONMENT_VARIABLES[ENVIRONMENT.ENVIRONMENT] === 'production'
}
/>
<SwapMessage />
</Swap>
</div>
);
}
Expand Down
9 changes: 9 additions & 0 deletions src/swap/components/SwapAmountInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ vi.mock('./SwapProvider', () => ({
const useSwapContextMock = useSwapContext as Mock;

const mockContextValue = {
address: '0x123',
from: {
amount: '10',
balance: '0.0002851826238227',
Expand Down Expand Up @@ -87,6 +88,14 @@ describe('SwapAmountInput', () => {
).not.toBeInTheDocument();
});

it('should not render max button if wallet not connected', () => {
useSwapContextMock.mockReturnValue({ ...mockContextValue, address: '' });
render(<SwapAmountInput label="From" token={ETH_TOKEN} type="from" />);
expect(
screen.queryByTestId('ockSwapAmountInput_MaxButton'),
).not.toBeInTheDocument();
});

it('should update input value with balance amount on max button click', () => {
useSwapContextMock.mockReturnValue(mockContextValue);
render(<SwapAmountInput label="From" token={ETH_TOKEN} type="from" />);
Expand Down
6 changes: 3 additions & 3 deletions src/swap/components/SwapAmountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function SwapAmountInput({
type,
swappableTokens,
}: SwapAmountInputReact) {
const { to, from, handleAmountChange } = useSwapContext();
const { address, to, from, handleAmountChange } = useSwapContext();

const source = useValue(type === 'from' ? from : to);
const destination = useValue(type === 'from' ? to : from);
Expand Down Expand Up @@ -82,7 +82,7 @@ export function SwapAmountInput({
className={cn(
'w-full border-[none] bg-transparent font-display text-[2.5rem]',
'leading-none outline-none',
hasInsufficientBalance ? color.error : color.foreground,
hasInsufficientBalance && address ? color.error : color.foreground,
)}
placeholder="0.0"
delayMs={delayMs}
Expand Down Expand Up @@ -112,7 +112,7 @@ export function SwapAmountInput({
className={cn(text.label2, color.foregroundMuted)}
>{`Balance: ${getRoundedAmount(source.balance, 8)}`}</span>
)}
{type === 'from' && (
{type === 'from' && address && (
<button
type="button"
className="flex cursor-pointer items-center justify-center px-2 py-1"
Expand Down
51 changes: 35 additions & 16 deletions src/swap/components/SwapButton.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest';
import { useAccount, useConnect } from 'wagmi';
import { SwapButton } from './SwapButton';
import { useSwapContext } from './SwapProvider';

vi.mock('wagmi', () => ({
useAccount: vi.fn(),
useConnect: vi.fn(),
}));

vi.mock('./SwapProvider', () => ({
useSwapContext: vi.fn(),
}));
Expand All @@ -20,78 +26,91 @@ describe('SwapButton', () => {
mockHandleSubmit.mockClear();
});

it('renders button with text "Swap" when not loading', () => {
it('should render button with text "Swap" when not loading', () => {
useSwapContextMock.mockReturnValue({
address: '0x123',
to: { loading: false, amount: 1, token: 'ETH' },
from: { loading: false, amount: 1, token: 'BTC' },
loading: false,
handleSubmit: mockHandleSubmit,
});

render(<SwapButton />);

const button = screen.getByTestId('ockSwapButton_Button');
expect(button).toHaveTextContent('Swap');
expect(button).not.toBeDisabled();
});

it('renders Spinner when loading', () => {
it('should render Spinner when loading', () => {
useSwapContextMock.mockReturnValue({
to: { loading: true, amount: 1, token: 'ETH' },
from: { loading: false, amount: 1, token: 'BTC' },
loading: false,
handleSubmit: mockHandleSubmit,
});

render(<SwapButton />);

const button = screen.getByTestId('ockSwapButton_Button');
expect(screen.getByTestId('spinner')).toBeInTheDocument();
expect(button).toBeDisabled();
});

it('button is disabled when required fields are missing', () => {
it('should disable button when required fields are missing', () => {
useSwapContextMock.mockReturnValue({
to: { loading: false, amount: 1, token: 'ETH' },
from: { loading: false, amount: null, token: 'BTC' },
loading: false,
handleSubmit: mockHandleSubmit,
});

render(<SwapButton />);

const button = screen.getByTestId('ockSwapButton_Button');
expect(button).toBeDisabled();
});

it('calls handleSubmit with mockHandleSubmit when clicked', () => {
it('should call handleSubmit with mockHandleSubmit when clicked', () => {
useSwapContextMock.mockReturnValue({
address: '0x123',
to: { loading: false, amount: 1, token: 'ETH' },
from: { loading: false, amount: 1, token: 'BTC' },
loading: false,
handleSubmit: mockHandleSubmit,
});

render(<SwapButton />);

const button = screen.getByTestId('ockSwapButton_Button');
fireEvent.click(button);

expect(mockHandleSubmit).toHaveBeenCalled();
});

it('applies additional className correctly', () => {
it('should apply additional className correctly', () => {
useSwapContextMock.mockReturnValue({
address: '0x123',
to: { loading: false, amount: 1, token: 'ETH' },
from: { loading: false, amount: 1, token: 'BTC' },
loading: false,
handleSubmit: mockHandleSubmit,
});

const customClass = 'custom-class';
render(<SwapButton className={customClass} />);

const button = screen.getByTestId('ockSwapButton_Button');
expect(button).toHaveClass(customClass);
});

it('should render ConnectWallet if disconnected and no missing fields', () => {
useSwapContextMock.mockReturnValue({
to: { loading: false, amount: 1, token: 'ETH' },
from: { loading: false, amount: 1, token: 'BTC' },
loading: false,
handleSubmit: mockHandleSubmit,
});
vi.mocked(useAccount).mockReturnValue({
address: '',
status: 'disconnected',
});
vi.mocked(useConnect).mockReturnValue({
connectors: [{ id: 'mockConnector' }],
connect: vi.fn(),
status: 'idle',
});
render(<SwapButton />);
const button = screen.getByTestId('ockConnectWallet_Container');
expect(button).toBeDefined();
});
});
8 changes: 7 additions & 1 deletion src/swap/components/SwapButton.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Spinner } from '../../internal/components/Spinner';
import { background, cn, color, pressable, text } from '../../styles/theme';
import { ConnectWallet } from '../../wallet';
import type { SwapButtonReact } from '../types';
import { useSwapContext } from './SwapProvider';

export function SwapButton({ className, disabled = false }: SwapButtonReact) {
const { to, from, loading, isTransactionPending, handleSubmit } =
const { address, to, from, loading, isTransactionPending, handleSubmit } =
useSwapContext();

const isLoading =
Expand All @@ -18,6 +19,11 @@ export function SwapButton({ className, disabled = false }: SwapButtonReact) {
disabled ||
isLoading;

// prompt user to connect wallet
if (!isDisabled && !address) {
return <ConnectWallet className="mt-4 w-full" />;
}

return (
<button
type="button"
Expand Down
Loading

0 comments on commit 1a25d7d

Please sign in to comment.