├── .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 | ![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g16f1pyv3a14tvuziq9z.png) 18 | 19 | ![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tafzgakh8z2883lo393r.png) 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 | ![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h4n8s9h9g81lx8byew0d.png) 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 | ![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0wzkybxu3nvphc0njprq.png) 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 | ![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ud4gwrvca0sdidtic6jn.png) 234 | 235 | Click "Connect" and all of a sudden you'll see that the Connect Button has been replaced by some text: 236 | 237 | ![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mlb16wgul3qneygruaxu.png) 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 | ![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/88y1h25j2toatbvf44sv.png) 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 | ![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/clkj5sok6vx8yb64a5j9.png) 335 | 336 | ![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k6s4wxfw6w2c7szaw36l.png) 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 | ![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/386eb74lrefdltu2cq4q.png) 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 | ![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q1j1zqr2nhww5hwwx18h.png) 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 | ![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ztluk95drttpttp6tbq9.png) 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 | 724 | 725 | ``` 726 | 727 | Now we should be able to trigger our `AccountModal` when we click on the logged-in `ConnectButton`, which will open up the modal and allow us to deactivate the account by clicking "Change". 728 | 729 | ![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/el02jknrq8beijxlsz5w.png) 730 | 731 | You'll notice that if you click "Change" that we will see the logged out `ConnectButton`. If you click it you'll also see that we're immediately logged in - our MetaMask was never disconnected from the dapp. 732 | 733 | And that's it for this tutorial! Hopefully it will help you feel comfortable getting started with connecting an Ethereum account to a Web3 dapp, and show you the power of combining the useDApp framework with React, alongside the power of the Chakra UI component library. 734 | 735 | The next steps for us will be to learn how to import contracts and make transactions - let me know in the comments if that's something you'd like to learn! 736 | 737 | Thanks for playing ;) 738 | 739 | [Full Repo](https://github.com/jacobedawson/connect-metamask-react-dapp) 740 | 741 | Follow me on Twitter: https://twitter.com/jacobedawson 742 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect-metamask-react-dapp", 3 | "version": "0.1.0", 4 | "author": "Jacob E. Dawson", 5 | "private": true, 6 | "dependencies": { 7 | "@chakra-ui/icons": "^1.0.14", 8 | "@chakra-ui/react": "^1.6.5", 9 | "@emotion/react": "^11.4.0", 10 | "@emotion/styled": "^11.3.0", 11 | "@fontsource/inter": "^4.5.0", 12 | "@metamask/jazzicon": "^2.0.0", 13 | "@testing-library/jest-dom": "^5.11.4", 14 | "@testing-library/react": "^11.1.0", 15 | "@testing-library/user-event": "^12.1.10", 16 | "@types/jest": "^26.0.15", 17 | "@types/node": "^12.0.0", 18 | "@types/react": "^17.0.0", 19 | "@types/react-dom": "^17.0.0", 20 | "@usedapp/core": "^0.4.1", 21 | "ethers": "^5.4.0", 22 | "framer-motion": "^4.1.17", 23 | "react": "^17.0.2", 24 | "react-dom": "^17.0.2", 25 | "react-scripts": "4.0.3", 26 | "typescript": "^4.1.2" 27 | }, 28 | "resolutions": { 29 | "@ethersproject/abi": "5.2.0", 30 | "@ethersproject/contracts": "5.2.0" 31 | }, 32 | "scripts": { 33 | "start": "react-scripts start", 34 | "build": "react-scripts build", 35 | "test": "react-scripts test", 36 | "eject": "react-scripts eject" 37 | }, 38 | "eslintConfig": { 39 | "extends": [ 40 | "react-app", 41 | "react-app/jest" 42 | ] 43 | }, 44 | "browserslist": { 45 | "production": [ 46 | ">0.2%", 47 | "not dead", 48 | "not op_mini all" 49 | ], 50 | "development": [ 51 | "last 1 chrome version", 52 | "last 1 firefox version", 53 | "last 1 safari version" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacobedawson/connect-metamask-react-dapp/5bcc4f4916b4d0ae2068829c80b390cdb94f0552/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacobedawson/connect-metamask-react-dapp/5bcc4f4916b4d0ae2068829c80b390cdb94f0552/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacobedawson/connect-metamask-react-dapp/5bcc4f4916b4d0ae2068829c80b390cdb94f0552/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { ChakraProvider, useDisclosure } from "@chakra-ui/react"; 2 | import theme from "./theme"; 3 | import Layout from "./components/Layout"; 4 | import ConnectButton from "./components/ConnectButton"; 5 | import AccountModal from "./components/AccountModal"; 6 | import "@fontsource/inter"; 7 | 8 | function App() { 9 | const { isOpen, onOpen, onClose } = useDisclosure(); 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /src/components/AccountModal.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Button, 4 | Flex, 5 | Link, 6 | Modal, 7 | ModalOverlay, 8 | ModalContent, 9 | ModalHeader, 10 | ModalFooter, 11 | ModalBody, 12 | ModalCloseButton, 13 | Text, 14 | } from "@chakra-ui/react"; 15 | import { ExternalLinkIcon, CopyIcon } from "@chakra-ui/icons"; 16 | import { useEthers } from "@usedapp/core"; 17 | import Identicon from "./Identicon"; 18 | 19 | type Props = { 20 | isOpen: any; 21 | onClose: any; 22 | }; 23 | 24 | export default function AccountModal({ isOpen, onClose }: Props) { 25 | const { account, deactivate } = useEthers(); 26 | 27 | function handleDeactivateAccount() { 28 | deactivate(); 29 | onClose(); 30 | } 31 | 32 | return ( 33 | 34 | 35 | 42 | 43 | Account 44 | 45 | 52 | 53 | 63 | 64 | 65 | Connected with MetaMask 66 | 67 | 86 | 87 | 88 | 89 | 96 | {account && 97 | `${account.slice(0, 6)}...${account.slice( 98 | account.length - 4, 99 | account.length 100 | )}`} 101 | 102 | 103 | 104 | 117 | 130 | 131 | View on Explorer 132 | 133 | 134 | 135 | 136 | 137 | 144 | 150 | Your transactions willl appear here... 151 | 152 | 153 | 154 | 155 | ); 156 | } 157 | -------------------------------------------------------------------------------- /src/components/ConnectButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Box, Text } from "@chakra-ui/react"; 2 | import { useEthers, useEtherBalance } from "@usedapp/core"; 3 | import { formatEther } from "@ethersproject/units"; 4 | import Identicon from "./Identicon"; 5 | 6 | type Props = { 7 | handleOpenModal: any; 8 | }; 9 | 10 | export default function ConnectButton({ handleOpenModal }: Props) { 11 | const { activateBrowserWallet, account } = useEthers(); 12 | const etherBalance = useEtherBalance(account); 13 | 14 | function handleConnectWallet() { 15 | activateBrowserWallet(); 16 | } 17 | 18 | return account ? ( 19 | 26 | 27 | 28 | {etherBalance && parseFloat(formatEther(etherBalance)).toFixed(3)} ETH 29 | 30 | 31 | 55 | 56 | ) : ( 57 | 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/components/Identicon.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import { useEthers } from "@usedapp/core"; 3 | import Jazzicon from "@metamask/jazzicon"; 4 | import styled from "@emotion/styled"; 5 | 6 | const StyledIdenticon = styled.div` 7 | height: 1rem; 8 | width: 1rem; 9 | border-radius: 1.125rem; 10 | background-color: black; 11 | `; 12 | 13 | export default function Identicon() { 14 | const ref = useRef(); 15 | const { account } = useEthers(); 16 | 17 | useEffect(() => { 18 | if (account && ref.current) { 19 | ref.current.innerHTML = ""; 20 | ref.current.appendChild(Jazzicon(16, parseInt(account.slice(2, 10), 16))); 21 | } 22 | }, [account]); 23 | 24 | return ; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { Flex } from "@chakra-ui/react"; 3 | 4 | type Props = { 5 | children?: ReactNode; 6 | }; 7 | 8 | export default function Layout({ children }: Props) { 9 | return ( 10 | 17 | {children} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import { DAppProvider } from "@usedapp/core"; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | 10 | 11 | , 12 | document.getElementById("root") 13 | ); 14 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "@metamask/jazzicon" { 4 | export default function (diameter: number, seed: number): HTMLElement; 5 | } 6 | -------------------------------------------------------------------------------- /src/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { extendTheme } from "@chakra-ui/react"; 2 | 3 | export default extendTheme({ 4 | fonts: { 5 | heading: "Inter", 6 | body: "Inter", 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------