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 connect wallet functionality to Swap component #1173

Merged
merged 22 commits into from
Aug 28, 2024
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
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