├── .eslintrc.json ├── public ├── favicon.ico ├── vercel.svg └── truffle.svg ├── README.md ├── postcss.config.js ├── next-env.d.ts ├── next.config.js ├── types.d.ts ├── pages ├── api │ └── hello.ts ├── _app.tsx └── index.tsx ├── tailwind.config.js ├── components ├── Loading.tsx └── Wallet.tsx ├── tsconfig.json ├── styles └── globals.css ├── package.json ├── LICENSE ├── hooks ├── useListen.tsx └── useMetamask.tsx ├── .gitignore └── instructions.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarforever/wall/main/public/favicon.ico -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Ethers.js如何调用WTF学院合约获取NFT证书 2 | 3 | 通过解析WTF学院NFT证书智能合约的信息,利用Ethers.js实现与合约的交互,实现在页面展示账户被授予的NFT毕业证书。 -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | images: { 6 | domains: ['gateway.ipfs.io'], 7 | }, 8 | } 9 | 10 | module.exports = nextConfig 11 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | type InjectedProviders = { 2 | isMetaMask?: true; 3 | }; 4 | 5 | interface Window { 6 | ethereum: InjectedProviders & { 7 | on: (...args: any[]) => void; 8 | removeListener: (...args: any[]) => void; 9 | removeAllListeners: (...args: any[]) => void; 10 | request(args: any): Promise; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | 3 | import type { AppProps } from "next/app"; 4 | import { MetamaskProvider } from "../hooks/useMetamask"; 5 | 6 | function MyApp({ Component, pageProps }: AppProps) { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | export default MyApp; 15 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./pages/**/*.{js,ts,jsx,tsx}", 5 | "./components/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: { 9 | colors: { 10 | 'ganache': '#3fe0c5', 11 | 'truffle': '#ff6b4a' 12 | } 13 | }, 14 | }, 15 | plugins: [ 16 | require('@tailwindcss/aspect-ratio'),], 17 | } 18 | -------------------------------------------------------------------------------- /components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { type FC } from "react"; 2 | 3 | const dot = `rounded-full h-2 w-2 mx-0.5 bg-current animate-[blink_1s_ease_0s_infinite_normal_both]"`; 4 | 5 | export const Loading: FC = () => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body { 7 | padding: 0; 8 | margin: 0; 9 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 10 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 11 | } 12 | 13 | a { 14 | color: inherit; 15 | text-decoration: none; 16 | } 17 | 18 | * { 19 | box-sizing: border-box; 20 | } 21 | 22 | @media (prefers-color-scheme: dark) { 23 | html { 24 | color-scheme: dark; 25 | } 26 | body { 27 | color: white; 28 | background: black; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web3-unleashed-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "ethers": "^5.7.1", 13 | "next": "12.3.1", 14 | "react": "18.2.0", 15 | "react-dom": "18.2.0" 16 | }, 17 | "devDependencies": { 18 | "@tailwindcss/aspect-ratio": "^0.4.2", 19 | "@types/node": "18.7.23", 20 | "@types/react": "18.0.21", 21 | "@types/react-dom": "18.0.6", 22 | "autoprefixer": "^10.4.12", 23 | "eslint": "8.24.0", 24 | "eslint-config-next": "12.3.1", 25 | "postcss": "^8.4.17", 26 | "tailwindcss": "^3.1.8", 27 | "typescript": "4.8.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 sugarforever 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import { useEffect } from "react"; 3 | import Wallet from "../components/Wallet"; 4 | import { useListen } from "../hooks/useListen"; 5 | import { useMetamask } from "../hooks/useMetamask"; 6 | 7 | const Home: NextPage = () => { 8 | const { dispatch } = useMetamask(); 9 | const listen = useListen(); 10 | 11 | useEffect(() => { 12 | if (typeof window !== undefined) { 13 | // start by checking if window.ethereum is present, indicating a wallet extension 14 | const ethereumProviderInjected = typeof window.ethereum !== "undefined"; 15 | // this could be other wallets so we can verify if we are dealing with metamask 16 | // using the boolean constructor to be explecit and not let this be used as a falsy value (optional) 17 | const isMetamaskInstalled = 18 | ethereumProviderInjected && Boolean(window.ethereum.isMetaMask); 19 | 20 | const local = window.localStorage.getItem("metamaskState"); 21 | 22 | // user was previously connected, start listening to MM 23 | if (local) { 24 | listen(); 25 | } 26 | 27 | // local could be null if not present in LocalStorage 28 | const { wallet, balance } = local 29 | ? JSON.parse(local) 30 | : // backup if local storage is empty 31 | { wallet: null, balance: null }; 32 | 33 | dispatch({ type: "pageLoaded", isMetamaskInstalled, wallet, balance }); 34 | } 35 | }, []); 36 | 37 | return ( 38 | <> 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default Home; 45 | -------------------------------------------------------------------------------- /hooks/useListen.tsx: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { useMetamask } from "./useMetamask"; 3 | 4 | export const useListen = () => { 5 | const { dispatch } = useMetamask(); 6 | 7 | return () => { 8 | window.ethereum.on("accountsChanged", async (newAccounts: string[]) => { 9 | if (newAccounts.length > 0) { 10 | // uppon receiving a new wallet, we'll request again the balance to synchronize the UI. 11 | const newBalance = await window.ethereum!.request({ 12 | method: "eth_getBalance", 13 | params: [newAccounts[0], "latest"], 14 | }); 15 | 16 | const [firstAccount] = newAccounts; 17 | const provider = new ethers.providers.JsonRpcProvider('https://goerli.infura.io/v3/51f732adc1b443ad9b1f34fb65b9aaad') 18 | const abi = [ 19 | "function balanceOf(address,uint256) view returns (uint256)", 20 | ] 21 | const contractAddress = '0xDF9C19ceAdf7e4A9db07A57Fc0bFA246938e3BCA' 22 | const contract = new ethers.Contract(contractAddress, abi, provider) 23 | const wtfIntroTransaction = await contract.balanceOf(firstAccount, 0) // WTF Solidity Intro Pass 24 | const wtfAdvancedTransaction = await contract.balanceOf(firstAccount, 1) // WTF Solidity Advanced Pass 25 | const { _introHex } = wtfIntroTransaction 26 | const { _advancedHex } = wtfAdvancedTransaction 27 | 28 | dispatch({ 29 | type: "connect", 30 | wallet: newAccounts[0], 31 | balance: newBalance, 32 | introPassed: _introHex > 0, 33 | advancedPassed: _advancedHex > 0 34 | }); 35 | } else { 36 | // if the length is 0, then the user has disconnected from the wallet UI 37 | dispatch({ type: "disconnect" }); 38 | } 39 | }); 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | .vercel 106 | -------------------------------------------------------------------------------- /hooks/useMetamask.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, type PropsWithChildren } from "react"; 2 | 3 | type ConnectAction = { type: "connect"; wallet: string; balance: string, introPassed: boolean; advancedPassed: boolean }; 4 | type DisconnectAction = { type: "disconnect" }; 5 | type PageLoadedAction = { type: "pageLoaded"; isMetamaskInstalled: boolean, wallet: string, balance: string }; 6 | type LoadingAction = { type: "loading" }; 7 | 8 | type Action = 9 | | ConnectAction 10 | | DisconnectAction 11 | | PageLoadedAction 12 | | LoadingAction; 13 | 14 | type Dispatch = (action: Action) => void; 15 | 16 | type Status = "loading" | "idle" | "pageNotLoaded"; 17 | 18 | type State = { 19 | wallet: string | null; 20 | isMetamaskInstalled: boolean; 21 | status: Status; 22 | balance: string | null; 23 | introPassed: boolean | false; 24 | advancedPassed: boolean | false; 25 | }; 26 | 27 | const initialState: State = { 28 | wallet: null, 29 | isMetamaskInstalled: false, 30 | status: "loading", 31 | balance: null, 32 | introPassed: false, 33 | advancedPassed: false 34 | } as const; 35 | 36 | function metamaskReducer(state: State, action: Action): State { 37 | switch (action.type) { 38 | case "connect": { 39 | const { wallet, balance, introPassed, advancedPassed } = action; 40 | return { ...state, wallet, balance, introPassed, advancedPassed, status: "idle" }; 41 | } 42 | case "disconnect": { 43 | window.localStorage.removeItem("metamaskState"); 44 | if (typeof window.ethereum !== undefined) { 45 | window.ethereum.removeAllListeners(["accountsChanged"]); 46 | } 47 | return { ...state, wallet: null, balance: null }; 48 | } 49 | case "pageLoaded": { 50 | const { isMetamaskInstalled } = action; 51 | return { ...state, isMetamaskInstalled, status: "idle" }; 52 | } 53 | case "loading": { 54 | return { ...state, status: "loading" }; 55 | } 56 | default: { 57 | throw new Error("Unhandled action type"); 58 | } 59 | } 60 | } 61 | 62 | const MetamaskContext = React.createContext< 63 | { state: State; dispatch: Dispatch } | undefined 64 | >(undefined); 65 | 66 | function MetamaskProvider({ children }: PropsWithChildren) { 67 | const [state, dispatch] = React.useReducer(metamaskReducer, initialState); 68 | const value = { state, dispatch }; 69 | 70 | useEffect(() => { 71 | if (typeof window !== undefined) { 72 | // start by checking if window.ethereum is present, indicating a wallet extension 73 | const ethereumProviderInjected = typeof window.ethereum !== "undefined"; 74 | // this could be other wallets so we can verify if we are dealing with metamask 75 | // using the boolean constructor to be explecit and not let this be used as a falsy value (optional) 76 | const isMetamaskInstalled = 77 | ethereumProviderInjected && Boolean(window.ethereum.isMetaMask); 78 | 79 | dispatch({ type: "pageLoaded", isMetamaskInstalled, wallet: '', balance: '' }); 80 | } 81 | }, []); 82 | 83 | return ( 84 | 85 | {children} 86 | 87 | ); 88 | } 89 | 90 | function useMetamask() { 91 | const context = React.useContext(MetamaskContext); 92 | if (context === undefined) { 93 | throw new Error("useMetamask must be used within a MetamaskProvider"); 94 | } 95 | return context; 96 | } 97 | 98 | export { MetamaskProvider, useMetamask }; -------------------------------------------------------------------------------- /public/truffle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Truffle 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /components/Wallet.tsx: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | import { useMetamask } from "../hooks/useMetamask"; 5 | import { Loading } from "./Loading"; 6 | 7 | export default function Wallet() { 8 | const { 9 | dispatch, 10 | state: { status, isMetamaskInstalled, wallet, balance, introPassed, advancedPassed }, 11 | } = useMetamask(); 12 | 13 | const showInstallMetamask = status !== "pageNotLoaded" && !isMetamaskInstalled; 14 | const showConnectButton = status !== "pageNotLoaded" && isMetamaskInstalled && !wallet; 15 | 16 | const handleConnect = async () => { 17 | dispatch({ type: "loading" }); 18 | const accounts = await window.ethereum.request({ 19 | method: "eth_requestAccounts", 20 | }); 21 | 22 | if (accounts.length > 0) { 23 | const balance = await window.ethereum!.request({ 24 | method: "eth_getBalance", 25 | params: [accounts[0], "latest"], 26 | }); 27 | const [firstAccount] = accounts; 28 | const provider = new ethers.providers.JsonRpcProvider('https://goerli.infura.io/v3/51f732adc1b443ad9b1f34fb65b9aaad') 29 | const abi = [ 30 | "function balanceOf(address,uint256) view returns (uint256)", 31 | ] 32 | const contractAddress = '0xDF9C19ceAdf7e4A9db07A57Fc0bFA246938e3BCA' 33 | const contract = new ethers.Contract(contractAddress, abi, provider) 34 | const wtfIntroTransaction = await contract.balanceOf(firstAccount, 0) // WTF Solidity Intro Pass 35 | const wtfAdvancedTransaction = await contract.balanceOf(firstAccount, 1) // WTF Solidity Advanced Pass 36 | const { _hex: introHex } = wtfIntroTransaction 37 | const { _hex: advancedHex } = wtfAdvancedTransaction 38 | 39 | dispatch({ 40 | type: "connect", 41 | wallet: firstAccount, 42 | balance, 43 | introPassed: parseInt(introHex, 16) > 0, 44 | advancedPassed: parseInt(advancedHex, 16) > 0 45 | }); 46 | } 47 | }; 48 | 49 | return ( 50 |
51 |
52 |

53 | This is Your Wall 54 |

55 |

56 | Connect with MetaMask and Stick Your NFT on the Wall 57 |

58 | 59 | {wallet && ( 60 |
61 |
62 |
63 |
64 |
65 |

66 | Address: {wallet} 67 |

68 |

69 | Balance: {balance} 70 |

71 |
72 |
73 |
74 |
75 |
76 | )} 77 | 78 | {wallet && introPassed && ( 79 |
80 |
81 |
82 |
83 |
84 |

85 | WTF Solidity Intro Pass (Test) 86 |

87 |
88 | WTF Solidity Intro Pass (Test) 92 |
93 |
94 |
95 |
96 |
97 |
98 | )} 99 | 100 | {wallet && advancedPassed && ( 101 |
102 |
103 |
104 |
105 |
106 |

107 | WTF Solidity Advanced Pass (Test) 108 |

109 |
110 | WTF Solidity Advanced Pass (Test) 114 |
115 |
116 |
117 |
118 |
119 |
120 | )} 121 | 122 | {showConnectButton && ( 123 | 129 | )} 130 | 131 | {showInstallMetamask && ( 132 | 133 | 134 | Connect Wallet 135 | 136 | 137 | )} 138 |
139 |
140 | ); 141 | } -------------------------------------------------------------------------------- /instructions.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | In this edition of [Web3 Unleashed](https://trufflesuite.com/unleashed/), we're interviewing the MetaMask DevRel team [Gui Bibeau](https://twitter.com/guibibeau) and [Eric Bishard](https://twitter.com/httpjunkie) about the MetaMask API and other tools and features on the horizon like their MetaMask SDK. We'll will be building a dapp live with them to custom integrate MetaMask into a NextJS application! 4 | 5 | [![Web3 Unleashed Ep 7](https://i.ytimg.com/vi/vQVletnhLVk/maxresdefault.jpg)](http://www.youtube.com/watch?v=vQVletnhLVk "Web3 Unleashed Ep 7: Build a dapp with Next.js and the MetaMaskAPI") 6 | 7 | If you would like to follow along with the Web3 Unleashed Episode #7 demo during or after the stream, below are the steps we walked through to build our dapp integration with MetaMask API. 8 | 9 | ## Prerequisites 10 | 11 | - [MetaMask Extension](https://metamask.io/download/) 12 | - NodeJS 13 | - NPM 14 | - Git 15 | 16 | ## Clone and Install Dependencies 17 | 18 | Clone the [MetaMask API Web3 Unleashed repo](https://github.com/GuiBibeau/web3-unleashed-demo) and once in the directory we want to ensure we are on the main branch and run: 19 | 20 | ```bash 21 | npm i && npm run dev 22 | ``` 23 | 24 | This will give us a starting point in a NextJS application to build our demo. 25 | 26 | ### Connecting the User 27 | 28 | We will start by updating the `hooks/useMetamask.tsx` file. This is our global app context that utilizes "out of the box" Context API in React. 29 | 30 | Update `hooks/useMetamask.tsx` file: 31 | 32 | ```typescript 33 | import React, { useEffect, type PropsWithChildren } from "react"; 34 | 35 | type ConnectAction = { type: "connect"; wallet: string }; 36 | type DisconnectAction = { type: "disconnect" }; 37 | type PageLoadedAction = { type: "pageLoaded"; isMetamaskInstalled: boolean }; 38 | type LoadingAction = { type: "loading" }; 39 | 40 | type Action = 41 | | ConnectAction 42 | | DisconnectAction 43 | | PageLoadedAction 44 | | LoadingAction; 45 | 46 | type Dispatch = (action: Action) => void; 47 | 48 | type Status = "loading" | "idle" | "pageNotLoaded"; 49 | 50 | type State = { 51 | wallet: string | null; 52 | isMetamaskInstalled: boolean; 53 | status: Status; 54 | }; 55 | 56 | const MetamaskContext = React.createContext< 57 | { state: State; dispatch: Dispatch } | undefined 58 | >(undefined); 59 | 60 | const initialState: State = { 61 | wallet: null, 62 | isMetamaskInstalled: false, 63 | status: "loading", 64 | } as const; 65 | 66 | function metamaskReducer(state: State, action: Action): State { 67 | switch (action.type) { 68 | case "connect": { 69 | const { wallet } = action; 70 | return { ...state, wallet, status: "idle" }; 71 | } 72 | case "disconnect": { 73 | return { ...state, wallet: null }; 74 | } 75 | case "pageLoaded": { 76 | const { isMetamaskInstalled } = action; 77 | return { ...state, isMetamaskInstalled, status: "idle" }; 78 | } 79 | case "loading": { 80 | return { ...state, status: "loading" }; 81 | } 82 | default: { 83 | throw new Error("Unhandled action type"); 84 | } 85 | } 86 | } 87 | 88 | function MetamaskProvider({ children }: PropsWithChildren) { 89 | const [state, dispatch] = React.useReducer(metamaskReducer, initialState); 90 | const value = { state, dispatch }; 91 | 92 | useEffect(() => { 93 | if (typeof window !== undefined) { 94 | // start by checking if window.ethereum is present, indicating a wallet extension 95 | const ethereumProviderInjected = typeof window.ethereum !== "undefined"; 96 | // this could be other wallets so we can verify if we are dealing with metamask 97 | // using the boolean constructor to be explicit and not let this be used as a falsy value (optional) 98 | const isMetamaskInstalled = 99 | ethereumProviderInjected && Boolean(window.ethereum.isMetaMask); 100 | 101 | dispatch({ type: "pageLoaded", isMetamaskInstalled }); 102 | } 103 | }, []); 104 | 105 | return ( 106 | 107 | {children} 108 | 109 | ); 110 | } 111 | 112 | function useMetamask() { 113 | const context = React.useContext(MetamaskContext); 114 | if (context === undefined) { 115 | throw new Error("useMetamask must be used within a MetamaskProvider"); 116 | } 117 | return context; 118 | } 119 | 120 | export { MetamaskProvider, useMetamask }; 121 | ``` 122 | 123 | The above change is by far one of our largest changes that we will do at one time but this file is in charge of helping us keep our application state in sync with the wallet state and is crucial so that we can build the components and features that we want. 124 | 125 | After this change you will might notice red squiggly lines under the `window.ethereum` object, this is only because if we want TypeScript to stop yelling at us in our code editor, we need to tell it what `window.ethereum` is type-wise. 126 | 127 | Add the file `types.d.ts` to the app root: 128 | 129 | ```typescript 130 | type InjectedProviders = { 131 | isMetaMask?: true; 132 | }; 133 | 134 | interface Window { 135 | ethereum: InjectedProviders & { 136 | on: (...args: any[]) => void; 137 | removeListener?: (...args: any[]) => void; 138 | request(args: any): Promise; 139 | }; 140 | } 141 | ``` 142 | 143 | You should no longer see those warnings in your `hooks/useMetamask.tsx` file. 144 | 145 | Create a `components/Loading.tsx` file: 146 | 147 | ```typescript 148 | import { type FC } from "react"; 149 | 150 | const dot = `rounded-full h-2 w-2 mx-0.5 bg-current animate-[blink_1s_ease_0s_infinite_normal_both]"`; 151 | 152 | export const Loading: FC = () => { 153 | return ( 154 | 155 | 156 | 157 | 158 | 159 | ); 160 | }; 161 | ``` 162 | 163 | With our Type Definitions added, our Context Provider updated, and our `Loading.tsx` in place, we can now make changes to our `components/Wallet.tsx` file and add a loading state for our app. 164 | 165 | Update the `components/Wallet.tsx` file to: 166 | 167 | ```typescript 168 | import Image from "next/future/image"; 169 | import Link from "next/link"; 170 | import { useMetamask } from "../hooks/useMetamask"; 171 | import { Loading } from "./Loading"; 172 | 173 | export default function Wallet() { 174 | const { 175 | dispatch, 176 | state: { status, isMetamaskInstalled }, 177 | } = useMetamask(); 178 | 179 | const showInstallMetamask = status !== "pageNotLoaded" && !isMetamaskInstalled; 180 | const showConnectButton = status !== "pageNotLoaded" && isMetamaskInstalled; 181 | 182 | const handleConnect = async () => { 183 | dispatch({ type: "loading" }); 184 | const accounts = await window.ethereum.request({ 185 | method: "eth_requestAccounts", 186 | }); 187 | 188 | if (accounts.length > 0) { 189 | dispatch({ type: "connect", wallet: accounts[0] }); 190 | } 191 | }; 192 | 193 | return ( 194 |
195 |
196 |

197 | Metamask API intro 198 |

199 |

200 | Follow along with the{" "} 201 | 205 | Repo 206 | {" "} 207 | in order to learn how to use the Metamask API. 208 |

209 | {showConnectButton && ( 210 | 216 | )} 217 | 218 | {showInstallMetamask && ( 219 | 220 | 221 | Connect Wallet 222 | 223 | 224 | )} 225 |
226 |
227 | ); 228 | } 229 | ``` 230 | 231 | This imports the loading component, further destructures the return value of our `useMetaMask()` custom hook, sets up variables to track if MetaMask is installed or connected for conditional rendering and gives us a `handleConnect()` function for dispatching changes to our state reducer. 232 | 233 | If we are tracking our changes we can see that we have touched 4 files by creating or updating/refactoring. At this point we should be able to connect a user to our dapp with MetaMask. 234 | 235 | Run the project and attempt to connect to your MetaMask wallet. 236 | 237 | ```bash 238 | npm run dev 239 | ``` 240 | 241 | Two things are happening now: 242 | 243 | 1. If a user does not have MetaMask installed they will get a "Connect Wallet" button that simply takes you to download MetaMask. 244 | 2. If MetaMask is installed they will see a "Connect Wallet" button that actually connects their wallet to the dapp. 245 | 246 | > We are not yet hiding the button once connected or displaying any wallet information. As well, you will notice in MetaMask that you are connected to the dapp. To test the Install link you can go into your extension manager and disable MetaMask temporarily. 247 | 248 | [Checkout the Diff to see what changed](https://github.com/GuiBibeau/web3-unleashed-demo/pull/1/files) 249 | 250 | ### Use the MetaMask API to get User Info 251 | 252 | We want to display the a balance from our wallet, and the public address of the wallet account that is connected to our dapp. For this we need to make a few changes again to the `hooks/useMetamask.tsx` and add the logic and JSX/HTML in our `components/Wallet.tsx`. 253 | 254 | Update `hooks/useMetamask.tsx` to: 255 | 256 | ```typescript 257 | import React, { useEffect, type PropsWithChildren } from "react"; 258 | 259 | type ConnectAction = { type: "connect"; wallet: string; balance: string }; 260 | type DisconnectAction = { type: "disconnect" }; 261 | type PageLoadedAction = { type: "pageLoaded"; isMetamaskInstalled: boolean }; 262 | type LoadingAction = { type: "loading" }; 263 | 264 | type Action = 265 | | ConnectAction 266 | | DisconnectAction 267 | | PageLoadedAction 268 | | LoadingAction; 269 | 270 | type Dispatch = (action: Action) => void; 271 | 272 | type Status = "loading" | "idle" | "pageNotLoaded"; 273 | 274 | type State = { 275 | wallet: string | null; 276 | isMetamaskInstalled: boolean; 277 | status: Status; 278 | balance: string | null; 279 | }; 280 | 281 | const initialState: State = { 282 | wallet: null, 283 | isMetamaskInstalled: false, 284 | status: "loading", 285 | balance: null, 286 | } as const; 287 | 288 | function metamaskReducer(state: State, action: Action): State { 289 | switch (action.type) { 290 | case "connect": { 291 | const { wallet, balance } = action; 292 | return { ...state, wallet, balance, status: "idle" }; 293 | } 294 | case "disconnect": { 295 | return { ...state, wallet: null }; 296 | } 297 | case "pageLoaded": { 298 | const { isMetamaskInstalled } = action; 299 | return { ...state, isMetamaskInstalled, status: "idle" }; 300 | } 301 | case "loading": { 302 | return { ...state, status: "loading" }; 303 | } 304 | default: { 305 | throw new Error("Unhandled action type"); 306 | } 307 | } 308 | } 309 | 310 | const MetamaskContext = React.createContext< 311 | { state: State; dispatch: Dispatch } | undefined 312 | >(undefined); 313 | 314 | function MetamaskProvider({ children }: PropsWithChildren) { 315 | const [state, dispatch] = React.useReducer(metamaskReducer, initialState); 316 | const value = { state, dispatch }; 317 | 318 | useEffect(() => { 319 | if (typeof window !== undefined) { 320 | // start by checking if window.ethereum is present, indicating a wallet extension 321 | const ethereumProviderInjected = typeof window.ethereum !== "undefined"; 322 | // this could be other wallets so we can verify if we are dealing with metamask 323 | // using the boolean constructor to be explecit and not let this be used as a falsy value (optional) 324 | const isMetamaskInstalled = 325 | ethereumProviderInjected && Boolean(window.ethereum.isMetaMask); 326 | 327 | dispatch({ type: "pageLoaded", isMetamaskInstalled }); 328 | } 329 | }, []); 330 | 331 | return ( 332 | 333 | {children} 334 | 335 | ); 336 | } 337 | 338 | function useMetamask() { 339 | const context = React.useContext(MetamaskContext); 340 | if (context === undefined) { 341 | throw new Error("useMetamask must be used within a MetamaskProvider"); 342 | } 343 | return context; 344 | } 345 | 346 | export { MetamaskProvider, useMetamask }; 347 | ``` 348 | 349 | We have done some slight refactoring to account for the ability to track the state of the wallet balance, added **balance** to our **initialState**, and updated our **connect** action in our reducer 350 | 351 | Update `components/Wallet.tsx` to: 352 | 353 | ```typescript 354 | import Image from "next/future/image"; 355 | import Link from "next/link"; 356 | import { useMetamask } from "../hooks/useMetamask"; 357 | import { Loading } from "./Loading"; 358 | 359 | export default function Wallet() { 360 | const { 361 | dispatch, 362 | state: { status, isMetamaskInstalled, wallet, balance }, 363 | } = useMetamask(); 364 | 365 | const showInstallMetamask = status !== "pageNotLoaded" && !isMetamaskInstalled; 366 | const showConnectButton = status !== "pageNotLoaded" && isMetamaskInstalled && !wallet; 367 | 368 | const handleConnect = async () => { 369 | dispatch({ type: "loading" }); 370 | const accounts = await window.ethereum.request({ 371 | method: "eth_requestAccounts", 372 | }); 373 | 374 | if (accounts.length > 0) { 375 | const balance = await window.ethereum!.request({ 376 | method: "eth_getBalance", 377 | params: [accounts[0], "latest"], 378 | }); 379 | 380 | dispatch({ type: "connect", wallet: accounts[0], balance }); 381 | } 382 | }; 383 | 384 | return ( 385 |
386 |
387 |

388 | Metamask API intro 389 |

390 |

391 | Follow along with the{" "} 392 | 396 | Repo 397 | {" "} 398 | in order to learn how to use the Metamask API. 399 |

400 | 401 | {wallet && ( 402 |
403 |
404 |
405 |
406 |
407 |

408 | Address: {wallet} 409 |

410 |

411 | Balance: {balance} 412 |

413 |
414 |
415 |
416 |
417 |
418 | )} 419 | 420 | {showConnectButton && ( 421 | 427 | )} 428 | 429 | {showInstallMetamask && ( 430 | 431 | 432 | Connect Wallet 433 | 434 | 435 | )} 436 |
437 |
438 | ); 439 | } 440 | ``` 441 | 442 | As well we have added balance to our destructured object so that we have access to it in our component, updated the **showConnectButton** logic and requested the balance using the `eth_getBalance` MetaMask (RPC API) method. 443 | 444 | We have also updated our JSX/HTML to include an **address** and **balance**. 445 | 446 | This is a great start, but our UI is still lacking and there is more logic we need to properly track our wallet state and update the page because if we connect to our wallet we get a funny display for our balance and if we refresh our page, we don't see our address and balance. But we will now fix those issues. 447 | 448 | [Checkout the Diff to see what changed](https://github.com/GuiBibeau/web3-unleashed-demo/pull/2/files) 449 | 450 | ### Two Way Communication with MetaMask 451 | 452 | Again we will be updating the `hooks/useMetamask.tsx` and `components/Wallet.tsx` files. The idea will be to add a few more reducer actions including **Loading** and **Idle** states for the page, we will fix our button to say **"Install MetaMask"** instead of **"Connect MetaMask"** and we will parse the **balance** to display a readable number. 453 | 454 | Finally, we will add some code that uses the `wallet_watchAsset` MetaMask (RPC API) method to add **$USDC** token to our MetaMask wallet. This will enable our users to see those tokens in their wallet if they have them. If a dApp uses a particular token, we can programmatically do this for them rather than expecting to do it themselves manually through the MetMask UI. 455 | 456 | Update `hooks/useMetamask.tsx` to: 457 | 458 | ```typescript 459 | import React, { useEffect, type PropsWithChildren } from "react"; 460 | 461 | type ConnectAction = { type: "connect"; wallet: string; balance: string }; 462 | type DisconnectAction = { type: "disconnect" }; 463 | type PageLoadedAction = { type: "pageLoaded"; isMetamaskInstalled: boolean }; 464 | type LoadingAction = { type: "loading" }; 465 | type IdleAction = { type: "idle" }; 466 | 467 | type Action = 468 | | ConnectAction 469 | | DisconnectAction 470 | | PageLoadedAction 471 | | LoadingAction 472 | | IdleAction; 473 | 474 | type Dispatch = (action: Action) => void; 475 | 476 | type Status = "loading" | "idle" | "pageNotLoaded"; 477 | 478 | type State = { 479 | wallet: string | null; 480 | isMetamaskInstalled: boolean; 481 | status: Status; 482 | balance: string | null; 483 | }; 484 | 485 | const initialState: State = { 486 | wallet: null, 487 | isMetamaskInstalled: false, 488 | status: "loading", 489 | balance: null, 490 | } as const; 491 | 492 | function metamaskReducer(state: State, action: Action): State { 493 | switch (action.type) { 494 | case "connect": { 495 | const { wallet, balance } = action; 496 | return { ...state, wallet, balance, status: "idle" }; 497 | } 498 | case "disconnect": { 499 | return { ...state, wallet: null, balance: null }; 500 | } 501 | case "pageLoaded": { 502 | const { isMetamaskInstalled } = action; 503 | return { ...state, isMetamaskInstalled, status: "idle" }; 504 | } 505 | case "loading": { 506 | return { ...state, status: "loading" }; 507 | } 508 | case "idle": { 509 | return { ...state, status: "idle" }; 510 | } 511 | default: { 512 | throw new Error("Unhandled action type"); 513 | } 514 | } 515 | } 516 | 517 | const MetamaskContext = React.createContext< 518 | { state: State; dispatch: Dispatch } | undefined 519 | >(undefined); 520 | 521 | function MetamaskProvider({ children }: PropsWithChildren) { 522 | const [state, dispatch] = React.useReducer(metamaskReducer, initialState); 523 | const value = { state, dispatch }; 524 | 525 | useEffect(() => { 526 | if (typeof window !== undefined) { 527 | // start by checking if window.ethereum is present, indicating a wallet extension 528 | const ethereumProviderInjected = typeof window.ethereum !== "undefined"; 529 | // this could be other wallets so we can verify if we are dealing with metamask 530 | // using the boolean constructor to be explecit and not let this be used as a falsy value (optional) 531 | const isMetamaskInstalled = 532 | ethereumProviderInjected && Boolean(window.ethereum.isMetaMask); 533 | 534 | dispatch({ type: "pageLoaded", isMetamaskInstalled }); 535 | } 536 | }, []); 537 | 538 | return ( 539 | 540 | {children} 541 | 542 | ); 543 | } 544 | 545 | function useMetamask() { 546 | const context = React.useContext(MetamaskContext); 547 | if (context === undefined) { 548 | throw new Error("useMetamask must be used within a MetamaskProvider"); 549 | } 550 | return context; 551 | } 552 | 553 | export { MetamaskProvider, useMetamask }; 554 | ``` 555 | 556 | Update `components/Wallet.tsx` to: 557 | 558 | ```typescript 559 | import Link from "next/link"; 560 | import { useMetamask } from "../hooks/useMetamask"; 561 | import { Loading } from "./Loading"; 562 | 563 | export default function Wallet() { 564 | const { 565 | dispatch, 566 | state: { status, isMetamaskInstalled, wallet, balance }, 567 | } = useMetamask(); 568 | 569 | const showInstallMetamask = status !== "pageNotLoaded" && !isMetamaskInstalled; 570 | const showConnectButton = status !== "pageNotLoaded" && isMetamaskInstalled && !wallet; 571 | 572 | const showAddToken = status !== "pageNotLoaded" && typeof wallet === "string"; 573 | 574 | const handleConnect = async () => { 575 | dispatch({ type: "loading" }); 576 | const accounts = await window.ethereum.request({ 577 | method: "eth_requestAccounts", 578 | }); 579 | 580 | if (accounts.length > 0) { 581 | const balance = await window.ethereum!.request({ 582 | method: "eth_getBalance", 583 | params: [accounts[0], "latest"], 584 | }); 585 | 586 | dispatch({ type: "connect", wallet: accounts[0], balance }); 587 | 588 | // we can register an event listener for changes to the users wallet 589 | window.ethereum.on("accountsChanged", async (newAccounts: string[]) => { 590 | if (newAccounts.length > 0) { 591 | // uppon receiving a new wallet, we'll request again the balance to synchronize the UI. 592 | const newBalance = await window.ethereum!.request({ 593 | method: "eth_getBalance", 594 | params: [newAccounts[0], "latest"], 595 | }); 596 | 597 | dispatch({ 598 | type: "connect", 599 | wallet: newAccounts[0], 600 | balance: newBalance, 601 | }); 602 | } else { 603 | // if the length is 0, then the user has disconnected from the wallet UI 604 | dispatch({ type: "disconnect" }); 605 | } 606 | }); 607 | } 608 | }; 609 | 610 | const handleAddUsdc = async () => { 611 | dispatch({ type: "loading" }); 612 | 613 | await window.ethereum.request({ 614 | method: "wallet_watchAsset", 615 | params: { 616 | type: "ERC20", 617 | options: { 618 | address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 619 | symbol: "USDC", 620 | decimals: 18, 621 | image: "https://cryptologos.cc/logos/usd-coin-usdc-logo.svg?v=023", 622 | }, 623 | }, 624 | }); 625 | dispatch({ type: "idle" }); 626 | }; 627 | 628 | return ( 629 |
630 |
631 |

632 | Metamask API intro 633 |

634 |

635 | Follow along with the{" "} 636 | 640 | Repo 641 | {" "} 642 | in order to learn how to use the Metamask API. 643 |

644 | 645 | {wallet && balance && ( 646 |
647 |
648 |
649 |
650 |
651 |

652 | Address: {wallet} 653 |

654 |

655 | Balance:{" "} 656 | 657 | {(parseInt(balance) / 1000000000000000000).toFixed(4)}{" "} ETH 658 | 659 |

660 |
661 |
662 |
663 |
664 |
665 | )} 666 | 667 | {showConnectButton && ( 668 | 674 | )} 675 | 676 | {showInstallMetamask && ( 677 | 678 | 679 | Install Metamask 680 | 681 | 682 | )} 683 | 684 | {showAddToken && ( 685 | 691 | )} 692 |
693 |
694 | ); 695 | } 696 | ``` 697 | 698 | With those changes in place we can now install, connect to and view information from our MetaMask wallet. We can also see a nicely formatted version of our ETH balance and we can see **$USDC** tokens in our wallet. 699 | 700 | [Checkout the Diff to see what changed](https://github.com/GuiBibeau/web3-unleashed-demo/pull/3/files) 701 | 702 | We have one more UX improvement to push our dapp. 703 | 704 | ### More UX Goodies 705 | 706 | We'd like to store some MetaMask state in the browser's local storage to help us create a Disconnect button, something that we feel makes the UX better in a dapp. We will register an event listener for changes to the users wallet, so that when connecting and disconnecting the UX is just a little bit better. We will add a custom React Hook called `useListen` to help us achieve this and to co-locate some code that would otherwise be added in two different components so that our final code is a bit cleaner. We do a small refactor to get rid of a `useEffect` and we will display our buttons side by side when we have more than one showing on the page (Disconnect & Add Tokens) and we will use Tailwind's flex-box options to make this easy. 707 | 708 | 709 | Update `hooks/useMetamask.tsx` 710 | 711 | ```typescript 712 | import React, { useEffect, type PropsWithChildren } from "react"; 713 | 714 | type ConnectAction = { type: "connect"; wallet: string; balance: string }; 715 | type DisconnectAction = { type: "disconnect" }; 716 | type PageLoadedAction = { 717 | type: "pageLoaded"; 718 | isMetamaskInstalled: boolean; 719 | wallet: string | null; 720 | balance: string | null; 721 | }; 722 | type LoadingAction = { type: "loading" }; 723 | type IdleAction = { type: "idle" }; 724 | 725 | type Action = 726 | | ConnectAction 727 | | DisconnectAction 728 | | PageLoadedAction 729 | | LoadingAction 730 | | IdleAction; 731 | 732 | type Dispatch = (action: Action) => void; 733 | 734 | type Status = "loading" | "idle" | "pageNotLoaded"; 735 | 736 | type State = { 737 | wallet: string | null; 738 | isMetamaskInstalled: boolean; 739 | status: Status; 740 | balance: string | null; 741 | }; 742 | 743 | const initialState: State = { 744 | wallet: null, 745 | isMetamaskInstalled: false, 746 | status: "loading", 747 | balance: null, 748 | } as const; 749 | 750 | function metamaskReducer(state: State, action: Action): State { 751 | switch (action.type) { 752 | case "connect": { 753 | const { wallet, balance } = action; 754 | const newState = { ...state, wallet, balance, status: "idle" } as State; 755 | const info = JSON.stringify(newState); 756 | window.localStorage.setItem("metamaskState", info); 757 | 758 | return newState; 759 | } 760 | case "disconnect": { 761 | window.localStorage.removeItem("metamaskState"); 762 | return { ...state, wallet: null, balance: null }; 763 | } 764 | case "pageLoaded": { 765 | const { isMetamaskInstalled, balance, wallet } = action; 766 | return { ...state, isMetamaskInstalled, status: "idle", wallet, balance }; 767 | } 768 | case "loading": { 769 | return { ...state, status: "loading" }; 770 | } 771 | case "idle": { 772 | return { ...state, status: "idle" }; 773 | } 774 | default: { 775 | throw new Error("Unhandled action type"); 776 | } 777 | } 778 | } 779 | 780 | const MetamaskContext = React.createContext< 781 | { state: State; dispatch: Dispatch } | undefined 782 | >(undefined); 783 | 784 | function MetamaskProvider({ children }: PropsWithChildren) { 785 | const [state, dispatch] = React.useReducer(metamaskReducer, initialState); 786 | const value = { state, dispatch }; 787 | 788 | return ( 789 | 790 | {children} 791 | 792 | ); 793 | } 794 | 795 | function useMetamask() { 796 | const context = React.useContext(MetamaskContext); 797 | if (context === undefined) { 798 | throw new Error("useMetamask must be used within a MetamaskProvider"); 799 | } 800 | return context; 801 | } 802 | 803 | export { MetamaskProvider, useMetamask }; 804 | ``` 805 | 806 | Create `hooks/useListen.tsx` 807 | 808 | ```typescript 809 | import { useMetamask } from "./useMetamask"; 810 | 811 | export const useListen = () => { 812 | const { dispatch } = useMetamask(); 813 | 814 | return () => { 815 | window.ethereum.on("accountsChanged", async (newAccounts: string[]) => { 816 | if (newAccounts.length > 0) { 817 | // uppon receiving a new wallet, we'll request again the balance to synchronize the UI. 818 | const newBalance = await window.ethereum!.request({ 819 | method: "eth_getBalance", 820 | params: [newAccounts[0], "latest"], 821 | }); 822 | 823 | dispatch({ 824 | type: "connect", 825 | wallet: newAccounts[0], 826 | balance: newBalance, 827 | }); 828 | } else { 829 | // if the length is 0, then the user has disconnected from the wallet UI 830 | dispatch({ type: "disconnect" }); 831 | } 832 | }); 833 | }; 834 | }; 835 | ``` 836 | 837 | Update `components/Wallet.tsx` 838 | 839 | ```typescript 840 | import Link from "next/link"; 841 | import { useListen } from "../hooks/useListen"; 842 | import { useMetamask } from "../hooks/useMetamask"; 843 | import { Loading } from "./Loading"; 844 | 845 | export default function Wallet() { 846 | const { 847 | dispatch, 848 | state: { status, isMetamaskInstalled, wallet, balance }, 849 | } = useMetamask(); 850 | const listen = useListen(); 851 | 852 | const showInstallMetamask = status !== "pageNotLoaded" && !isMetamaskInstalled; 853 | const showConnectButton = status !== "pageNotLoaded" && isMetamaskInstalled && !wallet; 854 | 855 | const isConnected = status !== "pageNotLoaded" && typeof wallet === "string"; 856 | 857 | const handleConnect = async () => { 858 | dispatch({ type: "loading" }); 859 | const accounts = await window.ethereum.request({ 860 | method: "eth_requestAccounts", 861 | }); 862 | 863 | if (accounts.length > 0) { 864 | const balance = await window.ethereum!.request({ 865 | method: "eth_getBalance", 866 | params: [accounts[0], "latest"], 867 | }); 868 | 869 | dispatch({ type: "connect", wallet: accounts[0], balance }); 870 | 871 | // we can register an event listener for changes to the users wallet 872 | listen(); 873 | } 874 | }; 875 | 876 | const handleDisconnect = () => { 877 | dispatch({ type: "disconnect" }); 878 | }; 879 | 880 | const handleAddUsdc = async () => { 881 | dispatch({ type: "loading" }); 882 | 883 | await window.ethereum.request({ 884 | method: "wallet_watchAsset", 885 | params: { 886 | type: "ERC20", 887 | options: { 888 | address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 889 | symbol: "USDC", 890 | decimals: 18, 891 | image: "https://cryptologos.cc/logos/usd-coin-usdc-logo.svg?v=023", 892 | }, 893 | }, 894 | }); 895 | dispatch({ type: "idle" }); 896 | }; 897 | 898 | 899 | return ( 900 |
901 |
902 |

903 | Metamask API intro 904 |

905 |

906 | Follow along with the{" "} 907 | 911 | Repo 912 | {" "} 913 | in order to learn how to use the Metamask API. 914 |

915 | 916 | {wallet && balance && ( 917 |
918 |
919 |
920 |
921 |
922 |

923 | Address: {wallet} 924 |

925 |

926 | Balance:{" "} 927 | 928 | {(parseInt(balance) / 1000000000000000000).toFixed(4)}{" "} ETH 929 | 930 |

931 |
932 |
933 |
934 |
935 |
936 | )} 937 | 938 | {showConnectButton && ( 939 | 945 | )} 946 | 947 | {showInstallMetamask && ( 948 | 949 | 950 | Install Metamask 951 | 952 | 953 | )} 954 | 955 | {isConnected && ( 956 |
957 | 963 | 969 |
970 | )} 971 | 972 |
973 |
974 | ); 975 | } 976 | ``` 977 | 978 | Finally, we will update our `pages/index.tsx` file with a `useEffect` to wrap all of these final changes up. 979 | 980 | Update `pages/index/tsx` 981 | 982 | ```typescript 983 | import type { NextPage } from "next"; 984 | import { useEffect } from "react"; 985 | import Wallet from "../components/Wallet"; 986 | import { useListen } from "../hooks/useListen"; 987 | import { useMetamask } from "../hooks/useMetamask"; 988 | 989 | const Home: NextPage = () => { 990 | const { dispatch } = useMetamask(); 991 | const listen = useListen(); 992 | 993 | useEffect(() => { 994 | if (typeof window !== undefined) { 995 | // start by checking if window.ethereum is present, indicating a wallet extension 996 | const ethereumProviderInjected = typeof window.ethereum !== "undefined"; 997 | // this could be other wallets so we can verify if we are dealing with metamask 998 | // using the boolean constructor to be explecit and not let this be used as a falsy value (optional) 999 | const isMetamaskInstalled = 1000 | ethereumProviderInjected && Boolean(window.ethereum.isMetaMask); 1001 | 1002 | const local = window.localStorage.getItem("metamaskState"); 1003 | 1004 | // user was previously connected, start listening to MM 1005 | if (local) { 1006 | listen(); 1007 | } 1008 | 1009 | // local could be null if not present in LocalStorage 1010 | const { wallet, balance } = local 1011 | ? JSON.parse(local) 1012 | : // backup if local storage is empty 1013 | { wallet: null, balance: null }; 1014 | 1015 | dispatch({ type: "pageLoaded", isMetamaskInstalled, wallet, balance }); 1016 | } 1017 | }, []); 1018 | 1019 | return ( 1020 | <> 1021 | 1022 | 1023 | ); 1024 | }; 1025 | 1026 | export default Home; 1027 | ``` 1028 | 1029 | In this last page update to `pages/index.tsx` we have relocated the `useEffect` from the `hooks/useMetaMask.tsx` page for separation of concerns since the hook is consuming **dispatch** as a proper next step would to be creating a layout page with NextJS, but since we only have one page we simply added this code here. 1030 | 1031 | We have updated our `hooks/useMetamask.tsx` page's **PageLoadAction** to include **wallet** and **balance** as well as the code required to access our local storage and rehydrate our app. 1032 | 1033 | With those changes in place we have also updated our `components/Wallet.tsx` page to use our `useListen` hook since we are using that code in multiple places now, updated the `showAddToken` variable to a more descriptive name of `isConnected`, and added a `handleDisconnect()` function to dispatch an action clearing local storage in our browser. 1034 | 1035 | This also required a slight update to our JSX/HTML to display our buttons more neatly. 1036 | 1037 | [Checkout the Diff to see what changed](https://github.com/GuiBibeau/web3-unleashed-demo/pull/4/files) 1038 | 1039 | ## Remove Listeners after Disconnect 1040 | 1041 | We have one final change we want to make to ensure that we stop listening to changes once the user has disconnected their wallet. 1042 | 1043 | We will update the `` and `` files. This will make TypeScript definitions file aware of the `removeAllListeners()` method we will be using as well add necessary code to the `disconnect` case inside the `metamaskReducer`. 1044 | 1045 | Update `types.d.ts` file: 1046 | 1047 | ```typescript 1048 | type InjectedProviders = { 1049 | isMetaMask?: true; 1050 | }; 1051 | 1052 | interface Window { 1053 | ethereum: InjectedProviders & { 1054 | on: (...args: any[]) => void; 1055 | removeListener: (...args: any[]) => void; 1056 | removeAllListeners: (...args: any[]) => void; 1057 | request(args: any): Promise; 1058 | }; 1059 | } 1060 | ``` 1061 | 1062 | Update the case statement in the `useMetamask.tsx` file to: 1063 | 1064 | ```typescript 1065 | case "disconnect": { 1066 | window.localStorage.removeItem("metamaskState"); 1067 | if (typeof window.ethereum !== undefined) { 1068 | window.ethereum.removeAllListeners(["accountsChanged"]); 1069 | } 1070 | return { ...state, wallet: null, balance: null }; 1071 | } 1072 | ``` 1073 | 1074 | Again, here we have simply ensured that all listeners added after connecting the wallet stop listening once the user is disconnected. 1075 | 1076 | This concludes the demo! But you're just getting started, for a challenge, try updating the UI, try to add functionality to switch chains and overall have fun, if you have any questions or need help with MetaMask, reach out to our DevRel team on Twitter. You can contact [Gui Bibeau](https://twitter.com/guibibeau) and [Eric Bishard](https://twitter.com/httpjunkie) with any questions or feedback. 1077 | 1078 | > One final note, Gui has a great resource and blog called [web3-fullstack](https://www.web3-fullstack.com/) where he waxes poetically about Web3, full stack development and UX which is a great resource for Web2 developers getting into Web3 as well as seasoned veterans of the space! --------------------------------------------------------------------------------