-
Notifications
You must be signed in to change notification settings - Fork 192
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4e37fc7
commit 3d3ebc7
Showing
9 changed files
with
395 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.