Skip to content

Commit

Permalink
feat: added SwapAmountInput (#504)
Browse files Browse the repository at this point in the history
  • Loading branch information
abcrane123 authored Jun 11, 2024
1 parent 4e37fc7 commit 3d3ebc7
Show file tree
Hide file tree
Showing 9 changed files with 395 additions and 0 deletions.
27 changes: 27 additions & 0 deletions site/docs/components/SwapAmountInputContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ReactElement, useState } from 'react';
import type { Token } from '@coinbase/onchainkit/token';

type SwapAmountInputContainer = {
children: (
amount: string,
setAmount: (a: string) => void,
setToken: (t: Token) => void,
token: Token,
tokenBalance: string,
) => ReactElement;
};

const TOKEN_BALANCE_MAP: Record<string, string> = {
ETH: '3.5',
USDC: '2.77',
DAI: '4.9',
};

export default function SwapAmountInputContainer({ children }: SwapAmountInputContainer) {
const [token, setToken] = useState<Token>();
const [amount, setAmount] = useState('');

const tokenBalance = TOKEN_BALANCE_MAP[token?.symbol];

return children(amount, setAmount, setToken, token, tokenBalance);
}
75 changes: 75 additions & 0 deletions site/docs/pages/swap/swap-amount-input.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{/* import { SwapAmountInput } from '../../../../src/swap'; */}
import App from '../App';
import SwapAmountInputContainer from '../../components/SwapAmountInputContainer.tsx';

# `<SwapAmountInput />`

The `SwapAmountInput` component is a stylized input field designed for users to specify the amount of a particular token they wish to swap. This component integrates the `TokenSelector` component to allow users to select different tokens.

## Usage

:::code-group

```tsx [code]
<SwapAmountInput
amount={amount}
label="Sell"
setAmount={setAmount}
setToken={setToken}
swappableTokens={[
{
address: '0x1234',
chainId: 1,
decimals: 18,
image:
'https://dynamic-assets.coinbase.com/dbb4b4983bde81309ddab83eb598358eb44375b930b94687ebe38bc22e52c3b2125258ffb8477a5ef22e33d6bd72e32a506c391caa13af64c00e46613c3e5806/asset_icons/4113b082d21cc5fab17fc8f2d19fb996165bcce635e6900f7fc2d57c4ef33ae9.png',
name: 'Ethereum',
symbol: 'ETH',
},
...
]}
token={token}
tokenBalance={tokenBalance}
/>
```

```html [return html]
<div data-testid="ockSwapAmountInput_Container" class="ock-swapamountinput-container">
<div class="ock-swapamountinput-row">
<label class="ock-swapamountinput-label">Sell</label>
</div>
<div class="ock-swapamountinput-row">
<div class="ock-tokenselector-container">
<button data-testid="ockTokenSelector_Button" class="ock-tokenselector-button">
<span class="ock-tokenselector-label">Select</span>
<svg
data-testid="ockTokenSelector_CaretDown"
width="16"
height="17"
viewBox="0 0 16 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.95 4.85999L8.00001 9.80999L3.05001 4.85999L1.64001 6.27999L8.00001 12.64L14.36 6.27999L12.95 4.85999Z"
fill="#0A0B0D"
></path>
</svg>
</button>
</div>
<button class="ock-swapamountinput-maxbutton">Max</button>
</div>
<input
class="ock-swapamountinput-input"
placeholder="0"
data-testid="ockSwapAmountInput_Input"
value=""
/>
</div>
```

:::

## Props

[`SwapAmountInputReact`](/swap/types#SwapAmountInputReact)
15 changes: 15 additions & 0 deletions site/docs/pages/swap/types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,18 @@ description: Glossary of Types in Swap Kit.
---

# Types [Glossary of Types in Swap Kit.]

## `SwapAmountInputReact`

```ts
type SwapAmountInputReact = {
amount?: string; // Token amount
disabled?: boolean; // Whether the input is disabled
label: string; // Descriptive label for the input field
setAmount: (amount: string) => void; // Callback function when the amount changes
setToken: () => void; // Callback function when the token selector is clicked
swappableTokens: Token[]; // Tokens available for swap
token?: Token; // Selected token
tokenBalance?: string; // Amount of selected token user owns
};
```
9 changes: 9 additions & 0 deletions site/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,15 @@ export const sidebar = [
text: 'Swap',
items: [
{ text: 'Introduction', link: '/swap/introduction' },
{
text: 'Components',
items: [
{
text: 'SwapAmountInput',
link: '/swap/swap-amount-input',
},
],
},
{
text: 'Types',
link: '/swap/types',
Expand Down
145 changes: 145 additions & 0 deletions src/swap/components/SwapAmountInput.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* @jest-environment jsdom
*/
import React from 'react';
import '@testing-library/jest-dom';
import { fireEvent, render, screen, within } from '@testing-library/react';
import { Address } from 'viem';
import { SwapAmountInput } from './SwapAmountInput';
import { Token } from '../../token';

const setAmountMock = jest.fn();
const selectTokenClickMock = jest.fn();

const token = {
address: '0x123' as Address,
chainId: 1,
decimals: 2,
image: 'imageURL',
name: 'Ether',
symbol: 'ETH',
};

const swappableTokens: Token[] = [
{
name: 'Ethereum',
address: '',
symbol: 'ETH',
decimals: 18,
image: 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png',
chainId: 8453,
},
{
name: 'USDC',
address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
symbol: 'USDC',
decimals: 6,
image:
'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2',
chainId: 8453,
},
{
name: 'Dai',
address: '0x50c5725949a6f0c72e6c4a641f24049a917db0cb',
symbol: 'DAI',
decimals: 18,
image:
'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/d0/d7/d0d7784975771dbbac9a22c8c0c12928cc6f658cbcf2bbbf7c909f0fa2426dec-NmU4ZWViMDItOTQyYy00Yjk5LTkzODUtNGJlZmJiMTUxOTgy',
chainId: 8453,
},
];

describe('SwapAmountInput Component', () => {
it('should render', async () => {
render(
<SwapAmountInput
token={token}
swappableTokens={swappableTokens}
label="Sell"
setAmount={setAmountMock}
setToken={selectTokenClickMock}
amount="1"
tokenBalance="100"
/>,
);

const amountInput = screen.getByTestId('ockSwapAmountInput_Container');
expect(amountInput).toBeInTheDocument();

const labelElement = within(amountInput).getByText('Sell');
expect(labelElement).toBeInTheDocument();

const balanceElement = within(amountInput).getByText('Balance: 100');
expect(balanceElement).toBeInTheDocument();
});

it('should update the amount if a user enters a number', async () => {
render(
<SwapAmountInput
token={token}
swappableTokens={swappableTokens}
label="Sell"
setAmount={setAmountMock}
setToken={selectTokenClickMock}
/>,
);

const input = screen.getByTestId('ockSwapAmountInput_Input') as HTMLInputElement;
fireEvent.change(input, { target: { value: '2' } });
expect(setAmountMock).toHaveBeenCalledWith('2');
expect(input.value).toBe('2');
});

it('should not update the amount if a user enters a non-number', async () => {
render(
<SwapAmountInput
token={token}
swappableTokens={swappableTokens}
label="Sell"
setAmount={setAmountMock}
setToken={selectTokenClickMock}
amount="1"
/>,
);

const input = screen.getByTestId('ockSwapAmountInput_Input') as HTMLInputElement;
fireEvent.change(input, { target: { value: 'a' } });
expect(setAmountMock).not.toHaveBeenCalledWith('a');
expect(input.value).toBe('1');
});

it('should call setAmount with tokenBalance when the max button is clicked', async () => {
render(
<SwapAmountInput
token={token}
swappableTokens={swappableTokens}
label="Sell"
amount="1"
setAmount={setAmountMock}
setToken={selectTokenClickMock}
tokenBalance="100"
/>,
);

const maxButton = screen.getByTestId('ockSwapAmountInput_MaxButton');
fireEvent.click(maxButton);
expect(setAmountMock).toHaveBeenCalledWith('100');
});

it('should disable the input when disabled prop is true', async () => {
render(
<SwapAmountInput
token={token}
swappableTokens={swappableTokens}
label="Sell"
amount="1"
setAmount={setAmountMock}
setToken={selectTokenClickMock}
disabled
/>,
);

const input = screen.getByTestId('ockSwapAmountInput_Input') as HTMLInputElement;
expect(input).toBeDisabled();
});
});
63 changes: 63 additions & 0 deletions src/swap/components/SwapAmountInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useCallback } from 'react';

import { isValidAmount } from '../utils';
import type { SwapAmountInputReact } from '../types';

export function SwapAmountInput({
amount,
disabled = false,
label,
setAmount,
tokenBalance,
}: SwapAmountInputReact) {
const handleAmountChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
if (isValidAmount(event.target.value)) {
setAmount(event.target.value);
}
},
[setAmount],
);

const handleMaxButtonClick = useCallback(() => {
if (tokenBalance && isValidAmount(tokenBalance)) {
setAmount(tokenBalance);
}
}, [tokenBalance, setAmount]);

return (
<div
className="box-border flex w-fit flex-col items-start gap-[11px] bg-[#FFF] p-4"
data-testid="ockSwapAmountInput_Container"
>
<div className="flex w-full items-center justify-between">
<label className="text-sm font-semibold text-[#030712]">{label}</label>
{tokenBalance && (
<label className="text-sm font-normal text-gray-400">{`Balance: ${tokenBalance}`}</label>
)}
</div>
<div className="flex w-full items-center justify-between">
{/* TODO: add back in when TokenSelector is complete */}
{/* <TokenSelector setToken={setToken} token={token}>
<TokenSelectorDropdown options={swappableTokens} setToken={setToken} />
</TokenSelector> */}
<button
className="flex h-8 w-[58px] max-w-[200px] items-center rounded-[40px] bg-gray-100 px-3 py-2 text-base font-medium not-italic leading-6 text-gray-500"
data-testid="ockSwapAmountInput_MaxButton"
disabled={tokenBalance === undefined}
onClick={handleMaxButtonClick}
>
Max
</button>
</div>
<input
className="border-[none] bg-transparent text-5xl text-[black]"
data-testid="ockSwapAmountInput_Input"
disabled={disabled}
onChange={handleAmountChange}
placeholder="0"
value={amount}
/>
</div>
);
}
11 changes: 11 additions & 0 deletions src/swap/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,14 @@ export type Transaction = {
to: string; // The recipient address
value: string; // The value of the transaction
};

export type SwapAmountInputReact = {
amount?: string; // Token amount
disabled?: boolean; // Whether the input is disabled
label: string; // Descriptive label for the input field
setAmount: (amount: string) => void; // Callback function when the amount changes
setToken: () => void; // Callback function when the token selector is clicked
swappableTokens: Token[]; // Tokens available for swap
token?: Token; // Selected token
tokenBalance?: string; // Amount of selected token user owns
};
Loading

0 comments on commit 3d3ebc7

Please sign in to comment.