├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.tsx
├── components
│ ├── AccountModal.tsx
│ ├── ConnectButton.tsx
│ ├── Identicon.tsx
│ └── Layout.tsx
├── index.tsx
├── react-app-env.d.ts
└── theme
│ └── index.ts
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Today we're going to build a simple React / Web3 Dapp that replicates a small portion of the Uniswap v2 interface - specifically, we are building the "account login" button that allows users to connect to a Dapp using their MetaMask extension.
2 |
3 | By the end of the tutorial you will have a working React app that will be able to connect to your MetaMask account, and read your address & ETH balance. If you connect with multiple accounts the interface will change to reflect the active account.
4 |
5 | A lot of tutorials skip this basic login strategy, or use outdated libraries (which you don't find out until you're halfway through!). To avoid confusion, as of July, 2021 this tutorial & the accompanying repo uses the following tech:
6 |
7 | - react ^17.0.2
8 | - typescript ^4.2.1
9 | - ethers.js ^5.4.0
10 | - @usedapp/core ^0.4.1
11 | - @chakra-ui/react ^1.6.5
12 |
13 | The full repository can be found [HERE](https://github.com/jacobedawson/connect-metamask-react-dapp).
14 |
15 | We will be replicating (fairly closely) the look, feel, and functionality of the following "Connect to a wallet" section of the [Uniswap v2 interface](https://app.uniswap.org/#/swap):
16 |
17 | 
18 |
19 | 
20 |
21 | ### Before we get started:
22 |
23 | You'll need MetaMask installed to get this working. If you don't already have it, start by downloading & installing the MetaMask extension for Chrome, Firefox, Brave, or Edge: https://metamask.io/download.html (be careful to triple check the URL and ensure you are downloading from a trusted website). If you haven't set up MetaMask before, follow the instructions to set up an Ethereum account.
24 |
25 | Once you have MetaMask installed, we are ready to start coding...
26 |
27 | ## Step 1: Install Our Libraries
28 |
29 | We'll be using Create React App with a TypeScript template to build our app. We don't use a lot of TypeScript in the tutorial but it's a good way to dip your toes in if you haven't used it before.
30 |
31 | To create the app, open up a console and execute the following instructions:
32 |
33 | ```
34 | npx create-react-app YOUR_APP_NAME --template typescript
35 | ```
36 | This will make a new Create React App project called simple-web3-dapp, with TypeScript pre-configured.
37 |
38 | If you open up a copy of VSCode (or the editor of your choice) and navigate to your app folder, you'll see a React project ready to go, including index.tsx, App.tsx and a tsconfig.json file.
39 |
40 | We won't need a lot of the template files & code, so delete all of the code in the `index.tsx` file and add the following code:
41 |
42 | ```javascript
43 | // index.tsx
44 | import React from "react";
45 | import ReactDOM from "react-dom";
46 | import App from "./App"
47 |
48 | ReactDOM.render(
49 |
50 |
51 | ,
52 | document.getElementById("root")
53 | );
54 | ```
55 | This gives us the most basic `index.tsx` file we need to begin. Next, we're going to install a few more libraries that we'll be using to create our app:
56 |
57 | ```
58 | npm i @chakra-ui/react @emotion/react @emotion/styled @framer-motion @usedapp/core
59 | ```
60 |
61 |
62 | ## Step 2: Set up useDApp
63 |
64 | Apart from their decision to add a capital A to the name of their library, useDApp is an incredibly useful framework for "rapid DApp development", and includes some helpful hooks and seamless integration into a modern React project. To dive into everything you can do with the framework, check out their website at https://usedapp.io/. We'll only be using some basic elements of useDApp to get our web3 dapp working, but there's much more you can do with it.
65 |
66 | In our `index.tsx` file, we're going to import the DAppProvider from useDApp to set up an app-wide provider that will allow us to access Ethereum accounts and prompt MetaMask to ask for permission to read addresses:
67 |
68 | ```javascript
69 | // index.tsx
70 | import React from "react";
71 | import ReactDOM from "react-dom";
72 | import App from "./App"
73 | // Import DAppProvider
74 | import { DAppProvider } from "@usedapp/core";
75 |
76 | ReactDOM.render(
77 |
78 | {/*
79 | Wrap our app in the provider, config is required,
80 | but can be left as an empty object:
81 | */}
82 |
83 |
84 |
85 | ,
86 | document.getElementById("root")
87 | );
88 | ```
89 |
90 | ## Step 3: Set Up App.tsx and a Layout component
91 |
92 | Next let's move to our `App.tsx` file, where we'll add Chakra UI to handle styling & components within our app. Chakra UI has become my favourite React component library, I find it extremely intuitive with sensible defaults and an API that makes it super-easy to override when necessary. I recommend Chakra over Tailwind because it's less verbose and easier to get started with:
93 |
94 | ```javascript
95 | // App.tsx
96 | import { ChakraProvider } from "@chakra-ui/react";
97 |
98 | export default function App() {
99 | return (
100 | // lets us use Chakra UI syntax across our app:
101 |
102 | // we'll add content to our app shortly
103 |
104 | )
105 | }
106 | ```
107 |
108 | To keep this tutorial focused, we won't be replicating the entire Uniswap navbar, so we'll just center the elements we're focused on by wrapping them in a Layout component. Inside the `src` directory of your project, create a `components` directory and inside that create a `Layout.tsx` file:
109 |
110 | ```javascript
111 | // Layout.tsx
112 | import { ReactNode } from "react";
113 | import { Flex } from "@chakra-ui/react";
114 |
115 | type Props = {
116 | children?: ReactNode;
117 | };
118 |
119 | export default function Layout({ children }: Props) {
120 | return (
121 |
128 | {children}
129 |
130 | )
131 | }
132 | ```
133 |
134 | The code we've added should be pretty easy to follow - we're using a Chakra Flex component, setting the height to the full page height and centering the child elements. We've also added a TypeScript type to define the child elements as a ReactNode, which lets us add individual elements, and arrays of elements, while keeping TypeScript happy and providing us with type hints elsewhere in the project.
135 |
136 | Let's now import that Layout component into our `App.tsx`:
137 |
138 | ```javascript
139 | // App.tsx
140 | import { ChakraProvider } from "@chakra-ui/react";
141 | import Layout from "./components/Layout";
142 |
143 | export default function App() {
144 | return (
145 |
146 |
147 |
Hello, world!
148 |
149 |
150 | )
151 | }
152 | ```
153 | If you run `npm start` you should now see a page with "Hello, world!" vertically centered. We're getting to the good stuff soon, I promise :)
154 |
155 |
156 | ## Step 4: Creating our "Connect to a wallet" button
157 |
158 | We're going to create our ConnectButton now, which is where the bulk of the magic happens. Start by creating a file called `ConnectButton.tsx` inside the `components` folder:
159 |
160 | ```javascript
161 | // ConnectButton.tsx
162 | import { Button, Box, Text } from "@chakra-ui/react";
163 | import { useEthers, useEtherBalance } from "@usedapp/core";
164 |
165 | export default function ConnectButton() {
166 | const {activateBrowserWallet, account } = useEthers();
167 | const etherBalance = useEtherBalance(account);
168 |
169 | return account ? (
170 |
171 |
172 | {etherBalance && etherBalance} ETH
173 |
174 |
175 | ) : (
176 |
177 | );
178 | }
179 | ```
180 |
181 | Here we've imported the useEthers and useEtherBalance hooks from useDApp, which will enable us to connect to our MetaMask wallet. Import `ConnectButton.tsx` into `App.tsx` and place the component in between the Layout component in `App.tsx`:
182 |
183 | ```javascript
184 | // App.tsx
185 | import ConnectButton from "./components/ConnectButton";
186 | // other code
187 |
188 |
189 |
190 |
191 | ```
192 |
193 | If you still have React running your page should have hot reloaded and you'll see this:
194 |
195 | 
196 |
197 | Great - we've got a button, but it doesn't do anything - let's add a function to handle the button click. In `ConnectButton.tsx`, we'll add a click handler:
198 |
199 | ```javascript
200 | // ConnectButton.tsx
201 | export default function ConnectButton() {
202 | // other code
203 |
204 | function handleConnectWallet() {
205 | activateBrowserWallet();
206 | }
207 |
208 | return account ? (
209 |
210 |
211 | // etherBalance will be an object, so we stringify it
212 | {etherBalance && JSON.stringify(etherBalance)} ETH
213 |
214 |
215 | ) : (
216 |
219 | );
220 | }
221 | ```
222 |
223 | I personally like to define named functions within my components, so we're creating the handleConnectWallet function which simply invokes the activateBrowserWallet function provided by useDApp. It might seem unnecessary at the moment, but I find that getting into the practice of defining function handlers keeps my code cleaner and easier to manage than mixing inline event handlers.
224 |
225 | Now for the moment of truth: let's click the "Connect to a wallet button"...
226 |
227 | If everything has gone to plan then clicking the button should have prompted MetaMask to open and give us a "Connect With MetaMask" view:
228 |
229 | 
230 |
231 | Select the account that you'd like to log in with and click "Next" in the MetaMask UI. You should then see a section asking if you will let the dapp view the addresses of your permitted accounts:
232 |
233 | 
234 |
235 | Click "Connect" and all of a sudden you'll see that the Connect Button has been replaced by some text:
236 |
237 | 
238 |
239 | This means we're connected! If you have React Dev Tools installed you can also navigate to the Components tab and look for `Web3ReactContext - primary.Provider` - you'll see that the context now holds a `value` object with an `account` property that matches the Ethereum account you connected with:
240 |
241 | 
242 |
243 | Ok, so we've made some progress - we're connecting to a React dapp with MetaMask, and we can see account info within our dapp's state. But, this isn't very pretty or useful. We have to do a little bit of work to display something nicer.
244 |
245 | ## Step 4: Formatting & Styling our Connect Button
246 |
247 | The etherBalance value returned by the useEtherBalance hook is giving us a `BigNumber` object that we need to format. Let's quickly install and then import a utility from ethers.js:
248 |
249 | ```
250 | npm i @ethersproject/units
251 | ```
252 |
253 | ```javascript
254 | // ConnectButton.tsx
255 | import { formatEther } from "@ethersproject/units";
256 | ```
257 |
258 | Once we've done that, we can use formatEther which will convert ETH denominated in Wei into a floating point number, which we will then pass through the JavaScript method `parseFloat` set to 3 fixed decimal places:
259 |
260 | ```javascript
261 | // ConnectButton.tsx
262 |
263 |
264 | {etherBalance && parseFloat(formatEther(etherBalance)).toFixed(3)} ETH
265 |
266 |
295 |
296 |
297 | {etherBalance && parseFloat(formatEther(etherBalance)).toFixed(3)} ETH
298 |
299 |
300 |
323 |
324 | ) : (
325 |
326 | );
327 | }
328 | ```
329 |
330 | Here we've added some styles to replicate the Uniswap button, using some neat Chakra properties on the components. You'll notice that we also use the `.slice` string method to shorten the Ethereum account address - an Ethereum address is 42 characters long, which is a bit unwieldy for the UI, so the standard practice is to trim some of the middle characters for display. Here we show the first 6 and last 4 characters, the same as the Uniswap UI.
331 |
332 | Let's compare our Connect Button with the Uniswap version:
333 |
334 | 
335 |
336 | 
337 |
338 | We're looking pretty close, but we're missing the little avatar. Let's make another component, called `Identicon.tsx`, which we'll create in our `components` folder:
339 |
340 | ```javascript
341 | // Identicon.tsx
342 | import { useEffect, useRef } from "react";
343 | import { useEthers } from "@usedapp/core";
344 | import styled from "@emotion/styled";
345 |
346 | const StyledIdenticon = styled.div`
347 | height: 1rem;
348 | width: 1rem;
349 | border-radius: 1.125rem;
350 | background-color: black;
351 | `;
352 |
353 | export default function Identicon() {
354 | const ref = useRef();
355 | const { account } = useEthers();
356 |
357 | useEffect(() => {
358 | if (account && ref.current) {
359 | ref.current.innerHTML = "";
360 | }
361 | }, [account]);
362 |
363 | return
364 | }
365 | ```
366 |
367 | At the moment this won't show us anything different - we're going to install a library called Jazzicon made by MetaMask themselves:
368 |
369 | ```
370 | npm i @metamask/jazzicon
371 | ```
372 |
373 | The Jazzicon library takes a diameter in pixels, and a JavaScript integer and returns a colorful, Cubist avatar - this is actually the exact same library and technique that the Uniswap interface uses:
374 |
375 | ```javascript
376 | // Identicon.tsx
377 | import Jazzicon from "@metamask/jazzicon";
378 | // ...othercode
379 |
380 | useEffect(() => {
381 | if (account && ref.current) {
382 | ref.current.innerHTML = "";
383 | ref.current.appendChild(Jazzicon(16, parseInt(account.slice(2, 10), 16)));
384 | }
385 | }, [account]);
386 |
387 | return
388 | ```
389 |
390 | NOTE: You might run into a TypeScript error here, so we'll quickly declare a module to get rid of the error. Go to the `react-app-env.d.ts` file (preinstalled by Create React App), and add the following module declaration:
391 |
392 | ```javascript
393 | declare module "@metamask/jazzicon" {
394 | export default function (diameter: number, seed: number): HTMLElement;
395 | }
396 | ```
397 |
398 | Now let's import `Identicon.tsx` into `ConnectButton.tsx` and add the Identicon component to our account button:
399 |
400 | ```javascript
401 | // ConnectButton.tsx
402 | import Identicon from "./Identicon";
403 |
404 | // ...other code
405 |
429 | ```
430 |
431 | Lovely! Now we should have an element that displays our Ethereum account and ETH balance along with a nice little avatar:
432 |
433 | 
434 |
435 |
436 | So let's take stock of where we are now:
437 |
438 | - We have added the useDApp provider to our React dapp
439 | - We can connect an Ethereum account and retrieve the address & ETH balance
440 | - We've used Chakra to mimic the style of the Uniswap connect element
441 |
442 | A reasonable question that might come up now is: how do we "logout" from the dapp? Notice that the `useEthers` hook comes with the `activateBrowserWallet` function. It also comes with a `deactivate` function that we can use to "log out" from the dapp - however, that needs to come with a bit of extra info: using the `deactivate` function *does not* actually disconnect the user from the dapp, it merely clears the state in the provider. If we "deactivate" and refresh the page, then click "Connect to a wallet" again, you'll see that the user address and balance is instantly shown, without logging back in via MetaMask.
443 |
444 | The reason for this is that once MetaMask is connected via the permissions, it will remain connected until we explicitly disconnect via the MetaMask interface:
445 |
446 | 
447 |
448 | If you've used a lot of DeFi products you'll notice that this is the standard Web3 practice, even though it is unintuitive compared to traditional Web2-style auth. This is an ongoing issue related to MetaMask: [https://github.com/MetaMask/metamask-extension/issues/8990](https://github.com/MetaMask/metamask-extension/issues/8990), and while several solutions have been suggested, I personally haven't found one that works as expected. You might notice that in the Uniswap interface itself, they don't provide a "logout" button, just a way to swap wallets:
449 |
450 | 
451 |
452 | For a bit of extra fun, let's emulate that modal, which will give us a chance to see what else Chakra UI can do in terms of building interfaces.
453 |
454 | ### Step 5: Add an Account Modal
455 |
456 | Let's start by creating `AccountModal.tsx` inside our `components` folder:
457 |
458 | ```javascript
459 | // AccountModal.tsx
460 | import {
461 | Box,
462 | Button,
463 | Flex,
464 | Link,
465 | Modal,
466 | ModalOverlay,
467 | ModalContent,
468 | ModalHeader,
469 | ModalFooter,
470 | ModalBody,
471 | ModalCloseButton,
472 | Text,
473 | } from "@chakra-ui/react";
474 | import { ExternalLinkIcon, CopyIcon } from "@chakra-ui/icons";
475 | import { useEthers } from "@usedapp/core";
476 | import Identicon from "./Identicon";
477 |
478 | export default function AccountModal() {}
479 | ```
480 |
481 | We're importing a lot of components from Chakra UI here, including 6 modal component elements, and also a couple of icons.
482 |
483 | ```
484 | npm i @chakra-ui/icons
485 | ```
486 |
487 | Now let's flesh out the modal:
488 |
489 | ```javascript
490 | // AccountModal.tsx
491 | export default function AccountModal() {
492 | const { account, deactivate } = useEthers();
493 |
494 |
495 |
496 |
503 |
504 | Account
505 |
506 |
513 |
514 |
524 |
525 |
526 | Connected with MetaMask
527 |
528 |
547 |
548 |
549 |
550 |
557 | {account &&
558 | `${account.slice(0, 6)}...${account.slice(
559 | account.length - 4,
560 | account.length
561 | )}`}
562 |
563 |
564 |
565 |
578 |
591 |
592 | View on Explorer
593 |
594 |
595 |
596 |
597 |
598 |
605 |
611 | Your transactions willl appear here...
612 |
613 |
614 |
615 |
616 | }
617 | ```
618 |
619 | Now we've got the layout for the modal, but no way to trigger it. Normally in a React UI I would create a UI context and wrap my app in the provider, so that I can trigger modals and sidebars via a single control point. However, we're keeping our dapp simple, so instead we're going to pass callbacks as props down from our `App.tsx` component.
620 |
621 | Inside `App.tsx`, we'll import a handy Chakra hook called useDisclosure that abstracts away the standard (and often repeated) logic for opening and closing modals. We've already imported ChakraProvider, so we'll just add the `useDisclosure` hook and destructure the variables:
622 |
623 | ```javascript
624 | // App.tsx
625 | import { ChakraProvider, useDisclosure } from "@chakra-ui/react";
626 | import theme from "./theme";
627 | import Layout from "./components/Layout";
628 | import ConnectButton from "./components/ConnectButton";
629 | import AccountModal from "./components/AccountModal";
630 |
631 | function App() {
632 | // Pull the disclosure methods
633 | const { isOpen, onOpen, onClose } = useDisclosure();
634 | return (
635 |
636 |
637 | // Our connect button will only handle opening
638 |
639 | // Our Account modal will handle open state & closing
640 |
641 |
642 |
643 | );
644 | }
645 |
646 | export default App;
647 | ```
648 |
649 | Since we're passing a prop to our ConnectButton, we'll have to make a slight change there to handle it:
650 |
651 | ```javascript
652 | // ConnectButton.tsx
653 | // ...other code
654 | type Props = {
655 | handleOpenModal: any;
656 | }
657 |
658 | // ...other code
659 |