Skip to content

Commit ed78267

Browse files
naz3ehDhaiwat10
andauthored
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>
1 parent 3e62159 commit ed78267

File tree

6 files changed

+234
-0
lines changed

6 files changed

+234
-0
lines changed

packages/components/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"@web3-ui/hooks": "^0.1.0",
5151
"babel-loader": "^8.2.1",
5252
"classnames": "^2.2.6",
53+
"ethers": "^5.5.1",
5354
"husky": "^7.0.0",
5455
"identity-obj-proxy": "^3.0.0",
5556
"lint-staged": "^12.1.2",
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React, { useEffect } from 'react';
2+
import { storiesOf } from '@storybook/react';
3+
import { AddressInput } from '.';
4+
import { ethers } from 'ethers';
5+
import { Provider, useWallet } from '@web3-ui/hooks';
6+
import { Text } from '@chakra-ui/layout';
7+
8+
const WithUseWallet = () => {
9+
const { connectWallet, connection } = useWallet();
10+
const [value, setValue] = React.useState('');
11+
12+
useEffect(() => {
13+
connectWallet!();
14+
}, []);
15+
16+
return (
17+
<>
18+
<AddressInput value={value} onChange={(e) => setValue(e)} provider={connection.signer!} />
19+
<Text>value: {value}</Text>
20+
</>
21+
);
22+
};
23+
24+
const Component = ({ ...props }) => {
25+
const provider = new ethers.providers.Web3Provider(window.ethereum);
26+
const [value, setValue] = React.useState('');
27+
return (
28+
<>
29+
<AddressInput
30+
value={value}
31+
onChange={(e) => setValue(e)}
32+
provider={provider}
33+
placeholder='Enter input address'
34+
{...props}
35+
/>
36+
<Text>value: {value}</Text>
37+
</>
38+
);
39+
};
40+
41+
storiesOf('AddressInput', module)
42+
.add('Default', () => {
43+
return <Component />;
44+
})
45+
.add('Using @web3-hook', () => {
46+
return (
47+
<Provider network='mainnet'>
48+
<WithUseWallet />
49+
</Provider>
50+
);
51+
})
52+
.add('With label', () => {
53+
return <Component label='Address' />;
54+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React from 'react';
2+
import { render, fireEvent } from '@testing-library/react';
3+
import { AddressInput } from '.';
4+
import { ethers } from 'ethers';
5+
import 'regenerator-runtime/runtime';
6+
7+
const WALLET_ADDRESS = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
8+
const SIGNED_MESSAGE =
9+
'0xa2162955fbfbac44ad895441a3501465861435d6615053a64fc9622d98061f1556e47c6655d0ea02df00ed6f6050298eea381b4c46f8148ecb617b32695bdc451c';
10+
11+
const WINDOW_ETHEREUM = {
12+
isMetaMask: true,
13+
request: async (request: { method: string; params?: Array<unknown> }) => {
14+
if (['eth_accounts', 'eth_requestAccounts'].includes(request.method)) {
15+
return [WALLET_ADDRESS];
16+
} else if (['personal_sign'].includes(request.method)) {
17+
return SIGNED_MESSAGE;
18+
}
19+
20+
throw Error(`Unknown request: ${request.method}`);
21+
},
22+
};
23+
24+
jest.mock('ethers', () => {
25+
const original = jest.requireActual('ethers');
26+
return {
27+
...original,
28+
ethers: {
29+
...original.ethers,
30+
},
31+
};
32+
});
33+
34+
const Component = () => {
35+
const provider = new ethers.providers.Web3Provider(WINDOW_ETHEREUM);
36+
const [value, setValue] = React.useState('');
37+
38+
return (
39+
<AddressInput
40+
value={value}
41+
onChange={(e) => setValue(e)}
42+
provider={provider}
43+
placeholder='Input address'
44+
/>
45+
);
46+
};
47+
48+
describe('AddressInput', () => {
49+
it('renders AddressInput correctly', () => {
50+
const { container } = render(<Component />);
51+
expect(container);
52+
});
53+
54+
it('changes Input value correctly', () => {
55+
const { getByPlaceholderText } = render(<Component />);
56+
const input = getByPlaceholderText('Input address') as HTMLInputElement;
57+
58+
fireEvent.change(input, { target: { value: WALLET_ADDRESS } });
59+
expect(input.value).toBe(WALLET_ADDRESS);
60+
});
61+
});
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { FormControl, FormLabel, Input, FormErrorMessage, InputProps } from '@chakra-ui/react';
2+
import React, { useEffect, useState } from 'react';
3+
import { ethers } from 'ethers';
4+
import { useDebounce } from './useDebounce';
5+
import { JsonRpcSigner } from '@ethersproject/providers/src.ts/json-rpc-provider';
6+
7+
export interface AddressInputProps {
8+
/**
9+
* @dev The provider or signer to fetch the address from the ens
10+
* @type JsonRpcSigner or ethers.providers.Web3Provider
11+
*/
12+
provider: ethers.providers.Web3Provider | JsonRpcSigner;
13+
/**
14+
* @dev The value for the input
15+
* @type string
16+
* @default ''
17+
*/
18+
value: string;
19+
/**
20+
* @dev The label for the input
21+
* @type string
22+
* @default null
23+
*/
24+
label?: string;
25+
/**
26+
* @dev Change handler for the text input
27+
* @type (value: string) => void
28+
*/
29+
onChange: (value: string) => void;
30+
}
31+
32+
/**
33+
* @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.
34+
* @param provider The provider or signer to fetch the address from the ens
35+
* @param value The value for the input
36+
* @param onChange Change hanlder for the text input
37+
* @returns JSX.Element
38+
*/
39+
export const AddressInput: React.FC<AddressInputProps & InputProps> = ({
40+
provider,
41+
value,
42+
onChange,
43+
label,
44+
...props
45+
}) => {
46+
const [inputValue, setInputValue] = useState('');
47+
const debouncedValue = useDebounce(inputValue, 700);
48+
const [error, setError] = useState<null | string>(null);
49+
const regex = /^0x[a-fA-F0-9]{40}$/;
50+
51+
const getAddressFromEns = async () => {
52+
try {
53+
let address = await provider.resolveName(debouncedValue);
54+
if (!address) {
55+
setError('Invalid Input');
56+
}
57+
return address;
58+
} catch (error) {
59+
setError(error as string);
60+
return;
61+
}
62+
};
63+
64+
useEffect(() => {
65+
if (debouncedValue) {
66+
onChange('');
67+
setError(null);
68+
if (regex.test(debouncedValue)) {
69+
onChange(debouncedValue);
70+
} else if (debouncedValue.endsWith('.eth') || debouncedValue.endsWith('.xyz')) {
71+
getAddressFromEns().then((address) => onChange(address ? address : ''));
72+
}
73+
}
74+
}, [debouncedValue]);
75+
76+
useEffect(() => {
77+
if (inputValue === '') {
78+
onChange('');
79+
setError(null);
80+
}
81+
}, [inputValue]);
82+
83+
return (
84+
<FormControl isInvalid={!!error}>
85+
{label && <FormLabel>Input address</FormLabel>}
86+
<Input
87+
isInvalid={!!error}
88+
value={inputValue}
89+
onChange={(e) => setInputValue(e.target.value)}
90+
{...props}
91+
/>
92+
<FormErrorMessage>{error ? ' ' + error : ''}</FormErrorMessage>
93+
</FormControl>
94+
);
95+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './AddressInput';
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { useEffect, useState } from 'react';
2+
3+
export const useDebounce = (value: string, delay: number) => {
4+
// State and setters for debounced value
5+
const [debouncedValue, setDebouncedValue] = useState(value);
6+
useEffect(
7+
() => {
8+
// Update debounced value after delay
9+
const handler = setTimeout(() => {
10+
setDebouncedValue(value);
11+
}, delay);
12+
// Cancel the timeout if value changes (also on delay change or unmount)
13+
// This is how we prevent debounced value from updating if value is changed ...
14+
// .. within the delay period. Timeout gets cleared and restarted.
15+
return () => {
16+
clearTimeout(handler);
17+
};
18+
},
19+
[value, delay] // Only re-call effect if value or delay changes
20+
);
21+
return debouncedValue;
22+
};

0 commit comments

Comments
 (0)