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: SwapAmountInput #504

Merged
merged 35 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
45d1b07
add token amount input component
abcrane123 Jun 10, 2024
2c87bde
adjust style
abcrane123 Jun 10, 2024
a7b29b3
add component to docs
abcrane123 Jun 10, 2024
aa8eb46
update token amount input
abcrane123 Jun 10, 2024
546c3da
move component to swap directory
abcrane123 Jun 10, 2024
ecfb419
remove token amount input
abcrane123 Jun 10, 2024
5d99397
update types and remove export
abcrane123 Jun 10, 2024
28dd4d1
add amount input container
abcrane123 Jun 10, 2024
d1cfea8
update docs
abcrane123 Jun 10, 2024
4c82674
remove unused prop
abcrane123 Jun 10, 2024
f2aed07
make dropdown onToggle prop optional
abcrane123 Jun 10, 2024
9fb661b
update test id for swap input
abcrane123 Jun 10, 2024
1bc7182
add html for swap input
abcrane123 Jun 10, 2024
2e55bba
address optional param issue
abcrane123 Jun 10, 2024
771b405
add SwapAmountInput tests
abcrane123 Jun 10, 2024
4eefeb9
comment out imports
abcrane123 Jun 10, 2024
63fd99f
add description to type
abcrane123 Jun 10, 2024
9ce1605
format code
abcrane123 Jun 10, 2024
2e3b96d
add utils test
abcrane123 Jun 10, 2024
09862b2
remove unused prop
abcrane123 Jun 10, 2024
3ce8811
remove unused imports
abcrane123 Jun 10, 2024
1612e68
remove unnecessary code in test file
abcrane123 Jun 10, 2024
45df7e3
remove unused file
abcrane123 Jun 10, 2024
55e3c9e
remove unused type
abcrane123 Jun 10, 2024
2bd86a7
remove unused code
abcrane123 Jun 10, 2024
af68798
move max button functionality inside SwapAmountInput component
abcrane123 Jun 10, 2024
79f7aef
rename props
abcrane123 Jun 10, 2024
37cafa2
update token selector test
abcrane123 Jun 10, 2024
9fd7391
address pr comments - alphabetize and reorder imports
abcrane123 Jun 10, 2024
b9fe0ac
alphabetize
abcrane123 Jun 10, 2024
64fb0d8
remove import from docs
abcrane123 Jun 10, 2024
508b979
remove export
abcrane123 Jun 10, 2024
4047ea3
remove css classnames
abcrane123 Jun 11, 2024
fb559fd
remove token changes
abcrane123 Jun 11, 2024
743398f
comment out token selector
abcrane123 Jun 11, 2024
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
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"
Copy link
Contributor

Choose a reason for hiding this comment

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

in general, always keep everything Props and Types alphabetical so it's easiser to code review and mantian.

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
Loading