generated from Dhaiwat10/react-library-starter
-
Notifications
You must be signed in to change notification settings - Fork 151
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add
AddressInput
component (#41)
* Add AddressInput component * Add MockProvider * Add debounce * Add FormControl, Error handling and chakra props to AddressInput * Use useWalletProvider in storybook * refactor AddressInput * Fix debouncedValue typo * Pass AddressInput test * Add test to check if value changes correctly * Update test name * Add label prop, remove hard defaults Co-authored-by: Dhaiwat Pandya <dhaiwatpandya@gmail.com>
- Loading branch information
Showing
6 changed files
with
234 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
54 changes: 54 additions & 0 deletions
54
packages/components/src/components/AddressInput/AddressInput.stories.tsx
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,54 @@ | ||
import React, { useEffect } from 'react'; | ||
import { storiesOf } from '@storybook/react'; | ||
import { AddressInput } from '.'; | ||
import { ethers } from 'ethers'; | ||
import { Provider, useWallet } from '@web3-ui/hooks'; | ||
import { Text } from '@chakra-ui/layout'; | ||
|
||
const WithUseWallet = () => { | ||
const { connectWallet, connection } = useWallet(); | ||
const [value, setValue] = React.useState(''); | ||
|
||
useEffect(() => { | ||
connectWallet!(); | ||
}, []); | ||
|
||
return ( | ||
<> | ||
<AddressInput value={value} onChange={(e) => setValue(e)} provider={connection.signer!} /> | ||
<Text>value: {value}</Text> | ||
</> | ||
); | ||
}; | ||
|
||
const Component = ({ ...props }) => { | ||
const provider = new ethers.providers.Web3Provider(window.ethereum); | ||
const [value, setValue] = React.useState(''); | ||
return ( | ||
<> | ||
<AddressInput | ||
value={value} | ||
onChange={(e) => setValue(e)} | ||
provider={provider} | ||
placeholder='Enter input address' | ||
{...props} | ||
/> | ||
<Text>value: {value}</Text> | ||
</> | ||
); | ||
}; | ||
|
||
storiesOf('AddressInput', module) | ||
.add('Default', () => { | ||
return <Component />; | ||
}) | ||
.add('Using @web3-hook', () => { | ||
return ( | ||
<Provider network='mainnet'> | ||
<WithUseWallet /> | ||
</Provider> | ||
); | ||
}) | ||
.add('With label', () => { | ||
return <Component label='Address' />; | ||
}); |
61 changes: 61 additions & 0 deletions
61
packages/components/src/components/AddressInput/AddressInput.test.tsx
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,61 @@ | ||
import React from 'react'; | ||
import { render, fireEvent } from '@testing-library/react'; | ||
import { AddressInput } from '.'; | ||
import { ethers } from 'ethers'; | ||
import 'regenerator-runtime/runtime'; | ||
|
||
const WALLET_ADDRESS = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'; | ||
const SIGNED_MESSAGE = | ||
'0xa2162955fbfbac44ad895441a3501465861435d6615053a64fc9622d98061f1556e47c6655d0ea02df00ed6f6050298eea381b4c46f8148ecb617b32695bdc451c'; | ||
|
||
const WINDOW_ETHEREUM = { | ||
isMetaMask: true, | ||
request: async (request: { method: string; params?: Array<unknown> }) => { | ||
if (['eth_accounts', 'eth_requestAccounts'].includes(request.method)) { | ||
return [WALLET_ADDRESS]; | ||
} else if (['personal_sign'].includes(request.method)) { | ||
return SIGNED_MESSAGE; | ||
} | ||
|
||
throw Error(`Unknown request: ${request.method}`); | ||
}, | ||
}; | ||
|
||
jest.mock('ethers', () => { | ||
const original = jest.requireActual('ethers'); | ||
return { | ||
...original, | ||
ethers: { | ||
...original.ethers, | ||
}, | ||
}; | ||
}); | ||
|
||
const Component = () => { | ||
const provider = new ethers.providers.Web3Provider(WINDOW_ETHEREUM); | ||
const [value, setValue] = React.useState(''); | ||
|
||
return ( | ||
<AddressInput | ||
value={value} | ||
onChange={(e) => setValue(e)} | ||
provider={provider} | ||
placeholder='Input address' | ||
/> | ||
); | ||
}; | ||
|
||
describe('AddressInput', () => { | ||
it('renders AddressInput correctly', () => { | ||
const { container } = render(<Component />); | ||
expect(container); | ||
}); | ||
|
||
it('changes Input value correctly', () => { | ||
const { getByPlaceholderText } = render(<Component />); | ||
const input = getByPlaceholderText('Input address') as HTMLInputElement; | ||
|
||
fireEvent.change(input, { target: { value: WALLET_ADDRESS } }); | ||
expect(input.value).toBe(WALLET_ADDRESS); | ||
}); | ||
}); |
95 changes: 95 additions & 0 deletions
95
packages/components/src/components/AddressInput/AddressInput.tsx
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,95 @@ | ||
import { FormControl, FormLabel, Input, FormErrorMessage, InputProps } from '@chakra-ui/react'; | ||
import React, { useEffect, useState } from 'react'; | ||
import { ethers } from 'ethers'; | ||
import { useDebounce } from './useDebounce'; | ||
import { JsonRpcSigner } from '@ethersproject/providers/src.ts/json-rpc-provider'; | ||
|
||
export interface AddressInputProps { | ||
/** | ||
* @dev The provider or signer to fetch the address from the ens | ||
* @type JsonRpcSigner or ethers.providers.Web3Provider | ||
*/ | ||
provider: ethers.providers.Web3Provider | JsonRpcSigner; | ||
/** | ||
* @dev The value for the input | ||
* @type string | ||
* @default '' | ||
*/ | ||
value: string; | ||
/** | ||
* @dev The label for the input | ||
* @type string | ||
* @default null | ||
*/ | ||
label?: string; | ||
/** | ||
* @dev Change handler for the text input | ||
* @type (value: string) => void | ||
*/ | ||
onChange: (value: string) => void; | ||
} | ||
|
||
/** | ||
* @dev A text input component that is used to get the address of the user from the ens. You can also pass all the styling props of the Chakra UI Input component. | ||
* @param provider The provider or signer to fetch the address from the ens | ||
* @param value The value for the input | ||
* @param onChange Change hanlder for the text input | ||
* @returns JSX.Element | ||
*/ | ||
export const AddressInput: React.FC<AddressInputProps & InputProps> = ({ | ||
provider, | ||
value, | ||
onChange, | ||
label, | ||
...props | ||
}) => { | ||
const [inputValue, setInputValue] = useState(''); | ||
const debouncedValue = useDebounce(inputValue, 700); | ||
const [error, setError] = useState<null | string>(null); | ||
const regex = /^0x[a-fA-F0-9]{40}$/; | ||
|
||
const getAddressFromEns = async () => { | ||
try { | ||
let address = await provider.resolveName(debouncedValue); | ||
if (!address) { | ||
setError('Invalid Input'); | ||
} | ||
return address; | ||
} catch (error) { | ||
setError(error as string); | ||
return; | ||
} | ||
}; | ||
|
||
useEffect(() => { | ||
if (debouncedValue) { | ||
onChange(''); | ||
setError(null); | ||
if (regex.test(debouncedValue)) { | ||
onChange(debouncedValue); | ||
} else if (debouncedValue.endsWith('.eth') || debouncedValue.endsWith('.xyz')) { | ||
getAddressFromEns().then((address) => onChange(address ? address : '')); | ||
} | ||
} | ||
}, [debouncedValue]); | ||
|
||
useEffect(() => { | ||
if (inputValue === '') { | ||
onChange(''); | ||
setError(null); | ||
} | ||
}, [inputValue]); | ||
|
||
return ( | ||
<FormControl isInvalid={!!error}> | ||
{label && <FormLabel>Input address</FormLabel>} | ||
<Input | ||
isInvalid={!!error} | ||
value={inputValue} | ||
onChange={(e) => setInputValue(e.target.value)} | ||
{...props} | ||
/> | ||
<FormErrorMessage>{error ? ' ' + error : ''}</FormErrorMessage> | ||
</FormControl> | ||
); | ||
}; |
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 @@ | ||
export * from './AddressInput'; |
22 changes: 22 additions & 0 deletions
22
packages/components/src/components/AddressInput/useDebounce.tsx
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,22 @@ | ||
import { useEffect, useState } from 'react'; | ||
|
||
export const useDebounce = (value: string, delay: number) => { | ||
// State and setters for debounced value | ||
const [debouncedValue, setDebouncedValue] = useState(value); | ||
useEffect( | ||
() => { | ||
// Update debounced value after delay | ||
const handler = setTimeout(() => { | ||
setDebouncedValue(value); | ||
}, delay); | ||
// Cancel the timeout if value changes (also on delay change or unmount) | ||
// This is how we prevent debounced value from updating if value is changed ... | ||
// .. within the delay period. Timeout gets cleared and restarted. | ||
return () => { | ||
clearTimeout(handler); | ||
}; | ||
}, | ||
[value, delay] // Only re-call effect if value or delay changes | ||
); | ||
return debouncedValue; | ||
}; |