Repository: Developer-DAO/web3-ui (ts)

Issue:
ETH wallet address input with support for ENS domains
Please take inspiration from [scaffold-eth's `AddressInput` component](https://github.com/scaffold-eth/scaffold-eth/blob/master/packages/react-app/src/components/AddressInput.jsx).

Pull request:
Add AddressInput component
This PR fixes #9
I've added the AddressInput component that returns the address for the entered ENS. This PR is still in development. 
Everyone's thoughts are welcome. 


Patch:
diff --git a/packages/components/package.json b/packages/components/package.json
index f174723..7a57d6c 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -50,6 +50,7 @@
     "@web3-ui/hooks": "^0.1.0",
     "babel-loader": "^8.2.1",
     "classnames": "^2.2.6",
+    "ethers": "^5.5.1",
     "husky": "^7.0.0",
     "identity-obj-proxy": "^3.0.0",
     "lint-staged": "^12.1.2",
diff --git a/packages/components/src/components/AddressInput/AddressInput.stories.tsx b/packages/components/src/components/AddressInput/AddressInput.stories.tsx
new file mode 100644
index 0000000..485df62
--- /dev/null
+++ b/packages/components/src/components/AddressInput/AddressInput.stories.tsx
@@ -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' />;
+  });
diff --git a/packages/components/src/components/AddressInput/AddressInput.tsx b/packages/components/src/components/AddressInput/AddressInput.tsx
new file mode 100644
index 0000000..2af3110
--- /dev/null
+++ b/packages/components/src/components/AddressInput/AddressInput.tsx
@@ -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>
+  );
+};
diff --git a/packages/components/src/components/AddressInput/index.ts b/packages/components/src/components/AddressInput/index.ts
new file mode 100644
index 0000000..1845571
--- /dev/null
+++ b/packages/components/src/components/AddressInput/index.ts
@@ -0,0 +1,1 @@
+export * from './AddressInput';
diff --git a/packages/components/src/components/AddressInput/useDebounce.tsx b/packages/components/src/components/AddressInput/useDebounce.tsx
new file mode 100644
index 0000000..b721a75
--- /dev/null
+++ b/packages/components/src/components/AddressInput/useDebounce.tsx
@@ -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;
+};


Test patch:
diff --git a/packages/components/src/components/AddressInput/AddressInput.test.tsx b/packages/components/src/components/AddressInput/AddressInput.test.tsx
new file mode 100644
index 0000000..25945bb
--- /dev/null
+++ b/packages/components/src/components/AddressInput/AddressInput.test.tsx
@@ -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);
+  });
+});