diff --git a/docs/whats-new.md b/docs/whats-new.md
index 65f24c692c5..1c8919ce993 100644
--- a/docs/whats-new.md
+++ b/docs/whats-new.md
@@ -11,6 +11,9 @@ of the [MetaMask developer page](https://metamask.io/developer/).
## June 2024
+- Updated [React dapp with global state tutorial](/wallet/tutorials/react-dapp-global-state) with
+ instructions for EIP-6963.
+ ([#1330](https://github.com/MetaMask/metamask-docs/pull/1330))
- Documented that the Gas API can be [called without an API key secret](/services/gas-api/api-reference). ([#1346](https://github.com/MetaMask/metamask-docs/pull/1346))
- Updated [Snaps resources](/snaps/learn/resources) and added a new section "Snaps for developers." ([#1329](https://github.com/MetaMask/metamask-docs/pull/1329))
- Documented [how to allow automatic connections to a Snap](/snaps/how-to/allow-automatic-connections).
@@ -22,6 +25,9 @@ of the [MetaMask developer page](https://metamask.io/developer/).
([#1276](https://github.com/MetaMask/metamask-docs/pull/1276))
- Discontinued support for [`eth_sign`](/wallet/concepts/signing-methods/#eth_sign).
([#1319](https://github.com/MetaMask/metamask-docs/pull/1319/))
+- Updated [React dapp with local state tutorial](/wallet/tutorials/react-dapp-local-state) with
+ instructions for EIP-6963.
+ ([#1299](https://github.com/MetaMask/metamask-docs/pull/1299))
- Documented [Snaps initial connections](/snaps/reference/permissions/#initial-connections).
([#1318](https://github.com/MetaMask/metamask-docs/pull/1318/))
- Updated [Snaps allowlisting guide](/snaps/how-to/get-allowlisted) with open permissions.
diff --git a/wallet-sidebar.js b/wallet-sidebar.js
index 4d750504443..d43e6a215bf 100644
--- a/wallet-sidebar.js
+++ b/wallet-sidebar.js
@@ -21,16 +21,7 @@ const sidebar = {
type: "category",
label: "Tutorials",
link: { type: "generated-index", slug: "/tutorials" },
- items: [
- {
- type: "doc",
- id: "tutorials/react-dapp-local-state",
- },
- {
- type: "doc",
- id: "tutorials/javascript-dapp-simple",
- },
- ],
+ items: [{ type: "autogenerated", dirName: "tutorials" }],
},
{
type: "category",
diff --git a/wallet/assets/tutorials/react-dapp/react-tutorial-02-final-preview.png b/wallet/assets/tutorials/react-dapp/react-tutorial-02-final-preview.png
new file mode 100644
index 00000000000..ed0f5c2c343
Binary files /dev/null and b/wallet/assets/tutorials/react-dapp/react-tutorial-02-final-preview.png differ
diff --git a/wallet/assets/tutorials/react-dapp/react-tutorial-02-selected-wallet.png b/wallet/assets/tutorials/react-dapp/react-tutorial-02-selected-wallet.png
new file mode 100644
index 00000000000..c505559bb8b
Binary files /dev/null and b/wallet/assets/tutorials/react-dapp/react-tutorial-02-selected-wallet.png differ
diff --git a/wallet/assets/tutorials/react-dapp/react-tutorial-02-wallet-error.png b/wallet/assets/tutorials/react-dapp/react-tutorial-02-wallet-error.png
new file mode 100644
index 00000000000..faef95049c4
Binary files /dev/null and b/wallet/assets/tutorials/react-dapp/react-tutorial-02-wallet-error.png differ
diff --git a/wallet/assets/tutorials/react-dapp/react-tutorial-02-wallet-list.png b/wallet/assets/tutorials/react-dapp/react-tutorial-02-wallet-list.png
new file mode 100644
index 00000000000..f88e4b88bfa
Binary files /dev/null and b/wallet/assets/tutorials/react-dapp/react-tutorial-02-wallet-list.png differ
diff --git a/wallet/tutorials/javascript-dapp-simple.md b/wallet/tutorials/javascript-dapp-simple.md
index 9c6bc7881fa..550fe9edbdf 100644
--- a/wallet/tutorials/javascript-dapp-simple.md
+++ b/wallet/tutorials/javascript-dapp-simple.md
@@ -86,10 +86,8 @@ Update `index.html` to include the script:
### 3. Detect MetaMask
:::caution
-
The `@metamask/detect-provider` module is deprecated, and is only used here for educational purposes.
In production environments, we recommend [connecting to MetaMask using EIP-6963](../how-to/connect/index.md).
-
:::
Install the `@metamask/detect-provider` module in your project directory:
diff --git a/wallet/tutorials/react-dapp-global-state.md b/wallet/tutorials/react-dapp-global-state.md
index 5fbb4d8930e..f4f3d2f0933 100644
--- a/wallet/tutorials/react-dapp-global-state.md
+++ b/wallet/tutorials/react-dapp-global-state.md
@@ -1,37 +1,38 @@
---
-description: Create a multi-component React dapp with global state.
+description: Create a multi-component React dapp with global state using EIP-6963.
toc_max_heading_level: 4
sidebar_position: 2
---
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
# Create a React dapp with global state
This tutorial walks you through integrating a React dapp with MetaMask.
-The dapp has multiple components, so requires managing global state.
+The dapp has multiple components and requires managing the state globally, which can be helpful for
+real-world use cases.
You'll use the [Vite](https://v3.vitejs.dev/guide) build tool with React and TypeScript to create
the dapp.
-:::tip
-We recommend first [creating a React dapp with local state](react-dapp-local-state.md).
-This tutorial is a follow-up to that tutorial.
-:::
+The final state of the dapp will look like the following:
-The [previous tutorial](react-dapp-local-state.md) walks you through creating a dapp that connects
-to MetaMask and handles account, balance, and network changes with a single component.
-In real world use cases, a dapp might need to respond to state changes in different components.
+![React dapp with global state](../assets/tutorials/react-dapp/react-tutorial-02-final-preview.png)
-In this tutorial, you'll move that state and its relevant functions into
-[React context](https://react.dev/reference/react/useContext), creating a
-[global state](https://react.dev/learn/reusing-logic-with-custom-hooks#custom-hooks-sharing-logic-between-components)
-so other components and UI can affect it and get MetaMask wallet updates.
+In this tutorial, you'll put the state into a [React
+Context](https://react.dev/reference/react/useContext) component, creating a [global
+state](https://react.dev/learn/reusing-logic-with-custom-hooks#custom-hooks-sharing-logic-between-components)
+that allows other components and UI elements to benefit from its data and functions.
+You'll use `localStorage` to persist the selected wallet, ensuring the last connected wallet state
+remains intact even after a page refresh.
-This tutorial also provides a few best practices for a clean code base, since you'll have multiple
-components and a slightly more complex file structure.
+This tutorial addresses the edge case where a browser wallet might be disabled or uninstalled
+between refreshes or visits to the dapp.
+You'll add a disconnect function to reset the state, and use
+[`wallet_revokePermissions`](/wallet/reference/wallet_revokePermissions) to properly disconnect from MetaMask.
:::info Project source code
-You can see the source code for the
-[starting point](https://github.com/MetaMask/react-dapp-tutorial/tree/global-state-start) and
-[final state](https://github.com/MetaMask/react-dapp-tutorial/tree/global-state-final) of this dapp.
+You can view the [dapp source code on GitHub](https://github.com/MetaMask/vite-react-global-tutorial).
:::
## Prerequisites
@@ -40,556 +41,736 @@ You can see the source code for the
- [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) version 9+
- A text editor (for example, [VS Code](https://code.visualstudio.com/))
- The [MetaMask extension](https://metamask.io/download) installed
-- Basic knowledge of TypeScript, React and React Hooks
+- Basic knowledge of TypeScript, React, React Context, and React Hooks
+
+:::tip
+We recommend following the [Create a React dapp with local state](react-dapp-local-state.md)
+tutorial first, which introduces [EIP-6963](https://eips.ethereum.org/EIPS/eip-6963).
+The tutorial demonstrates how to iterate over all discovered providers, connect to the selected
+wallet, and remember the selection within a single component.
+
+If you skip the tutorial, consider reviewing [wallet
+interoperability](../concepts/wallet-interoperability.md) to understand how multiple injected wallet
+providers work.
+:::
## Steps
### 1. Set up the project
-Clone the [`react-dapp-tutorial`](https://github.com/MetaMask/react-dapp-tutorial) GitHub repository
-on GitHub by running the following command:
+This project introduces a new structure, independent of previous tutorials.
+Instead of reusing code or states, this tutorial guides you through breaking down the
+single-component structure into multiple components.
+
+Set up a new project using Vite, React, and TypeScript by running the following command:
```bash
-git clone https://github.com/MetaMask/react-dapp-tutorial.git
+npm create vite@latest vite-react-global-state -- --template react-ts
```
-Checkout the `global-state-start` branch:
+Install the node module dependencies:
```bash
-cd react-dapp-tutorial && git checkout global-state-start
+cd vite-react-global-state && npm install
```
-Install the node module dependencies:
+Launch the development server:
```bash
-npm install
+npm run dev
```
-Open the project in a text editor.
+This displays a `localhost` URL in your terminal, where you can view the dapp in your browser.
-:::note tip
+:::note
If you use VS Code, you can run the command `code .` to open the project.
+If the development server has stopped, you can run the command `npx vite` or `npm run dev` to
+restart your project.
:::
-This is a working React dapp, but it's wiped out the code from the previous tutorial's
-[`App.tsx`](https://github.com/MetaMask/react-dapp-tutorial/blob/local-state-final/src/App.tsx) file.
+Open the project in your editor.
+Create three directories, `src/components`, `src/hooks`, and `src/utils`, in the root of the project
+using the following commands:
+
+```bash
+mkdir src/components && mkdir src/hooks && mkdir src/utils
+```
+
+Create the following files in `src/components`, which will be used to create components for listing
+installed wallets, displaying connected wallet information, and handling errors:
+
+- `WalletList.tsx`
+- `WalletList.module.css`
+- `SelectedWallet.tsx`
+- `SelectedWallet.module.css`
+- `WalletError.tsx`
+- `WalletError.module.css`
+
+Create the following files in `src/hooks`:
+
+- `Eip6963Provider.tsx`
+- `useEip6963Provider.tsx`
-Run the dapp using the command `npx vite`.
-The starting point looks like the following:
+Create the following file in `src/utils`:
-![](../assets/tutorials/react-dapp/pt2-01.png)
+- `index.ts`
-There are three components, each with static text: navigation (with a logo area and connect button),
-display (main content area), and footer.
-You'll use the footer to show any MetaMask errors.
+#### 1.1. Style the components
-Before you start, comment out or remove the `border` CSS selector, as it's only used as a visual aid.
-Remove the following line from each component style sheet:
+Add the following CSS code to `SelectedWallet.module.css`:
-```css title="Display.module.css | MetaMaskError.module.css | Navigation.module.css"
-// border: 1px solid rgb(...);
+```css title="SelectedWallet.module.css"
+.selectedWallet {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: flex-start;
+
+ padding: 0.6em 1.2em;
+ margin-bottom: 0.5em;
+
+ font-family: inherit;
+ font-size: 1em;
+ font-weight: 500;
+}
+.selectedWallet > img {
+ width: 2em;
+ height: 1.5em;
+ margin-right: 1em;
+}
+
+.providers {
+ display: flex;
+ flex-flow: column wrap;
+ justify-content: center;
+ align-items: center;
+ align-content: center;
+
+ padding: 0.6em 1.2em;
+}
```
-#### Styling
-
-This dapp has Vite's typical `App.css` and `index.css` files removed, and uses a modular approach to CSS.
-
-In the `/src` directory, `App.global.css` contains styles for the entire dapp (not specific to a
-single component), and styles you might want to reuse (such as buttons).
-
-In the `/src` directory, `App.module.css` contains styles specific to `App.tsx`, your dapp's
-container component.
-It uses the `appContainer` class, which sets up a
-[Flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox) to define the `display` type
-(`flex`) and the `flex-direction` (`column`).
-
-Using Flexbox here ensures that any child `div`s are laid out in a single-column layout (vertically).
-
-Finally, the `/src/components` directory has subdirectories for `Display`, `Navigation`, and `MetaMaskError`.
-Each subdirectory contains a corresponding component file and CSS file.
-Each component is a
-[flex-items](https://css-tricks.com/snippets/css/a-guide-to-flexbox/#aa-basics-and-terminology)
-within a
-[flex-container](https://css-tricks.com/snippets/css/a-guide-to-flexbox/#aa-flexbox-properties),
-stacked in a vertical column with the navigation and footer (`MetaMaskError`) being of fixed height
-and the middle component (`Display`) taking up the remaining vertical space.
-
-#### Optional: Linting with ESLint
-
-This dapp uses a standard ESLint configuration to keep the code consistent.
-There are two ways to use ESLint:
-
-1. Run `npm run lint` or `npm run lint:fix` from the command line.
- The former displays all the linting errors, and the latter updates your code to fix linting
- errors where possible.
-2. Set up your IDE to show linting errors and automatically fix them on save.
- For example, in VS Code, you can create or update the file at `.vscode/settings.json` in the
- root of the project with the following settings:
-
- ```json title="settings.json"
- {
- "eslint.format.enable": true,
- "eslint.packageManager": "npm",
- "editor.codeActionsOnSave": {
- "source.fixAll.eslint": true
- },
- "eslint.codeActionsOnSave.mode": "all"
- }
- ```
+Add the following CSS code to `WalletError.module.css`:
+
+```css title="WalletError.module.css"
+.walletError {
+ margin-top: 1em;
+ border-radius: 0.5em;
+ height: 36px;
+ padding: 16px;
+ color: #EFEFEF;
+ background-color: transparent;
+ user-select: none;
+}
+```
+
+Add the following CSS code to `WalletList.module.css`:
+
+```css title="WalletList.module.css"
+.walletList {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+```
+
+Append the following code to the end of `src/index.css`:
+
+```css title="index.css"
+/* Added CSS */
+:root {
+ text-align: left;
+}
+
+hr {
+ margin-top: 2em;
+ height: 1px;
+}
+
+button {
+ min-width: 12em;
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: flex-start;
+
+ align-items: center;
+ border-radius: 0.5em;
+ margin-bottom: 0.5em;
+ border: 1px solid transparent;
+}
+
+button > img {
+ width: 1.5em;
+ height: 1.5em;
+ margin-right: 1em;
+}
-#### Project structure
+button:hover {
+ border-color: #75079d;
+}
+
+button:first-child {
+ margin-top: 0.5em;
+}
+button:last-child {
+ margin-bottom: 0;
+}
+```
+
+#### 1.2. Project structure
-The following is a tree representation of the dapp's `/src` directory:
+You now have some basic global and component-level styling for your dapp.
+The directory structure in the dapp's `/src` directory should look like the following:
```text
├── src
│ ├── assets
│ ├── components
-│ │ └── Display
-│ │ | └── index.tsx
-│ │ | └── Display.module.css
-│ │ | └── Display.tsx
-│ │ ├── MetaMaskError
-│ │ | └── index.tsx
-│ │ | └── MetaMaskError.module.css
-│ │ | └── MetaMaskError.tsx
-│ │ ├─── Navigation
-│ │ | └── index.tsx
-│ │ | └── Navigation.module.css
-│ │ | └── Navigation.tsx
+│ │ ├── SelectedWallet.module.css
+│ │ ├── SelectedWallet.tsx
+│ │ ├── WalletError.module.css
+│ │ ├── WalletError.tsx
+│ │ ├── WalletList.module.css
+│ │ └── WalletList.tsx
│ ├── hooks
-│ │ ├── useMetaMask.tsx
+│ │ ├── WalletProvider.tsx
+│ │ └── useWalletProvider.tsx
│ ├── utils
│ │ └── index.tsx
-├── App.global.css
-├── App.module.css
+├── App.css
├── App.tsx
+├── index.css
├── main.tsx
├── vite-env.d.ts
```
-Instead of a single component, there's a `src/components` directory with UI and functionality
-distributed into multiple components.
-You'll modify the dapp's state in this directory and make it available to the rest of the dapp using
-a [context provider](https://react.dev/reference/react/useContext).
-This provider will sit in the `src/App.tsx` file and wrap the three child components.
+### 2. Import EIP-6963 interfaces
+
+The dapp will connect to MetaMask using the mechanism introduced by
+[EIP-6963](https://eips.ethereum.org/EIPS/eip-6963).
+
+:::info Why EIP-6963?
+[EIP-6963](https://eips.ethereum.org/EIPS/eip-6963) introduces an alternative wallet detection
+mechanism to the `window.ethereum` injected provider.
+This alternative mechanism enables dapps to support
+[wallet interoperability](../concepts/wallet-interoperability.md) by discovering multiple injected
+wallet providers in a user's browser.
+:::
-The child components will have access to the global state and the functions that modify the global state.
-This ensures that any change to the `wallet` (`address`, `balance`, and `chainId`), or the global
-state's properties and functions (`hasProvider`, `error`, `errorMessage`, and `isConnecting`) will
-be accessible by re-rendering those child components.
+Update the Vite environment variable file, `src/vite-env.d.ts`, with the types and interfaces
+needed for [EIP-6963](https://eips.ethereum.org/EIPS/eip-6963) and
+[EIP-1193](https://eips.ethereum.org/EIPS/eip-1193):
-The following graphic shows how the context provider wraps its child components, providing access to
-the state modifier functions and the actual state itself.
-Since React uses a one-way data flow, any change to the data gets re-rendered in those components automatically.
+```tsx title="vite-env.d.ts"
+///
-![](../assets/tutorials/react-dapp/pt2-02.png)
+// Describes metadata related to a provider based on EIP-6963.
+interface EIP6963ProviderInfo {
+ rdns: string
+ uuid: string
+ name: string
+ icon: string
+}
-### 2. Build the context provider
+// Represents the structure of a provider based on EIP-1193.
+interface EIP1193Provider {
+ isStatus?: boolean
+ host?: string
+ path?: string
+ sendAsync?: (request: { method: string, params?: Array }, callback: (error: Error | null, response: unknown) => void) => void
+ send?: (request: { method: string, params?: Array }, callback: (error: Error | null, response: unknown) => void) => void
+ request: (request: { method: string, params?: Array }) => Promise
+}
-In this step, you'll create a context called `MetaMaskContext` and a provider component called
-`MetaMaskContextProvider` in the `/src/hooks/useMetaMask.tsx` file.
+// Combines the provider's metadata with an actual provider object, creating a complete picture of a
+// wallet provider at a glance.
+interface EIP6963ProviderDetail {
+ info: EIP6963ProviderInfo
+ provider: EIP1193Provider
+}
-This provider component will use similar `useState` and `useEffect` hooks with some changes from
-the previous tutorial's local state component to make it more DRY (don't repeat yourself).
+// Represents the structure of an event dispatched by a wallet to announce its presence based on EIP-6963.
+type EIP6963AnnounceProviderEvent = {
+ detail:{
+ info: EIP6963ProviderInfo,
+ provider: Readonly
+ }
+}
-It will also have similar `updateWallet`, `connectMetaMask`, and `clearError` functions, all of
-which do their part to connect to MetaMask or update the MetaMask state.
+// An error object with optional properties, commonly encountered when handling eth_requestAccounts errors.
+interface WalletError {
+ code?: string
+ message?: string
+}
+```
-`MetaMaskContext` will return a `MetaMaskContext.Provider`, which takes a value of type
-`MetaMaskContextData`, and supplies that to its children.
+### 3. Build the context provider
-You'll export a React hook called `useMetaMask`, which uses your `MetaMaskContext`.
+In this step, you'll create the React Context component, which wraps the dapp and provides all
+components access to the state and functions required to modify the state and manage connections to
+discovered wallets.
-Update `/src/hooks/useMetaMask.tsx` with the following:
+Add the following code to `src/hooks/WalletProvider.tsx` to import the context, define the
+type alias, and define the context interface for the EIP-6963 provider:
-:::caution Read the comments
-The following code contains comments describing advanced React patterns and how MetaMask state is managed.
-:::
+```tsx title="WalletProvider.tsx"
+import { PropsWithChildren, createContext, useCallback, useEffect, useState } from "react"
-```tsx title="useMetaMask.tsx"
-/* eslint-disable @typescript-eslint/no-explicit-any */
-import {
- useState,
- useEffect,
- createContext,
- PropsWithChildren,
- useContext,
- useCallback,
-} from "react";
-
-import detectEthereumProvider from "@metamask/detect-provider";
-import { formatBalance } from "~/utils";
-
-interface WalletState {
- accounts: any[];
- balance: string;
- chainId: string;
-}
+// Type alias for a record where the keys are wallet identifiers and the values are account
+// addresses or null.
+type SelectedAccountByWallet = Record
-interface MetaMaskContextData {
- wallet: WalletState;
- hasProvider: boolean | null;
- error: boolean;
- errorMessage: string;
- isConnecting: boolean;
- connectMetaMask: () => void;
- clearError: () => void;
+// Context interface for the EIP-6963 provider.
+interface WalletProviderContext {
+ wallets: Record // A list of wallets.
+ selectedWallet: EIP6963ProviderDetail | null // The selected wallet.
+ selectedAccount: string | null // The selected account address.
+ errorMessage: string | null // An error message.
+ connectWallet: (walletUuid: string) => Promise // Function to connect wallets.
+ disconnectWallet: () => void // Function to disconnect wallets.
+ clearError: () => void
}
+```
-const disconnectedState: WalletState = {
- accounts: [],
- balance: "",
- chainId: "",
-};
+Add the following code to `src/hooks/WalletProvider.tsx` to extend the global `WindowEventMap`
+interface with the custom `eip6963:announceProvider` event:
-const MetaMaskContext = createContext(
- {} as MetaMaskContextData
-);
+```tsx title="WalletProvider.tsx"
+declare global{
+ interface WindowEventMap {
+ "eip6963:announceProvider": CustomEvent
+ }
+}
+```
-export const MetaMaskContextProvider = ({ children }: PropsWithChildren) => {
- const [hasProvider, setHasProvider] = useState(null);
+Explicitly declaring the custom `eip6963:announceProvider` event prevents type errors, enables
+proper type checking, and supports autocompletion in TypeScript.
- const [isConnecting, setIsConnecting] = useState(false);
+Add the following code to `src/hooks/WalletProvider.tsx` to create the React Context for the
+EIP-6963 provider with the defined interface `WalletProviderContext`, and define the
+`WalletProvider` component:
- const [errorMessage, setErrorMessage] = useState("");
- const clearError = () => setErrorMessage("");
+```tsx title="WalletProvider.tsx" showLineNumbers {6-12,14}
+export const WalletProviderContext = createContext(null)
- const [wallet, setWallet] = useState(disconnectedState);
- // useCallback ensures that you don't uselessly recreate the _updateWallet function on every render.
- const _updateWallet = useCallback(async (providedAccounts?: any) => {
- const accounts =
- providedAccounts ||
- (await window.ethereum.request({ method: "eth_accounts" }));
+// The WalletProvider component wraps all other components in the dapp, providing them with the
+// necessary data and functions related to wallets.
+export const WalletProvider: React.FC = ({ children }) => {
+ const [wallets, setWallets] = useState>({})
+ const [selectedWalletRdns, setSelectedWalletRdns] = useState(null)
+ const [selectedAccountByWalletRdns, setSelectedAccountByWalletRdns] = useState({})
- if (accounts.length === 0) {
- // If there are no accounts, then the user is disconnected.
- setWallet(disconnectedState);
- return;
- }
+ const [errorMessage, setErrorMessage] = useState("")
+ const clearError = () => setErrorMessage("")
+ const setError = (error: string) => setErrorMessage(error)
- const balance = formatBalance(
- await window.ethereum.request({
- method: "eth_getBalance",
- params: [accounts[0], "latest"],
- })
- );
- const chainId = await window.ethereum.request({
- method: "eth_chainId",
- });
-
- setWallet({ accounts, balance, chainId });
- }, []);
-
- const updateWalletAndAccounts = useCallback(
- () => _updateWallet(),
- [_updateWallet]
- );
- const updateWallet = useCallback(
- (accounts: any) => _updateWallet(accounts),
- [_updateWallet]
- );
-
- /**
- * This logic checks if MetaMask is installed. If it is, some event handlers are set up to update
- * the wallet state when MetaMask changes. The function returned by useEffect is used as a
- * "cleanup"; it removes the event handlers whenever the MetaMaskProvider is unmounted.
- */
useEffect(() => {
- const getProvider = async () => {
- const provider = await detectEthereumProvider({ silent: true });
- setHasProvider(Boolean(provider));
-
- if (provider) {
- updateWalletAndAccounts();
- window.ethereum.on("accountsChanged", updateWallet);
- window.ethereum.on("chainChanged", updateWalletAndAccounts);
- }
- };
-
- getProvider();
+ const savedSelectedWalletRdns = localStorage.getItem("selectedWalletRdns")
+ const savedSelectedAccountByWalletRdns = localStorage.getItem("selectedAccountByWalletRdns")
- return () => {
- window.ethereum?.removeListener("accountsChanged", updateWallet);
- window.ethereum?.removeListener(
- "chainChanged",
- updateWalletAndAccounts
- );
- };
- }, [updateWallet, updateWalletAndAccounts]);
+ if (savedSelectedAccountByWalletRdns) {
+ setSelectedAccountByWalletRdns(JSON.parse(savedSelectedAccountByWalletRdns))
+ }
- const connectMetaMask = async () => {
- setIsConnecting(true);
+ function onAnnouncement(event: EIP6963AnnounceProviderEvent){
+ setWallets(currentWallets => ({
+ ...currentWallets,
+ [event.detail.info.rdns]: event.detail
+ }))
- try {
- const accounts = await window.ethereum.request({
- method: "eth_requestAccounts",
- });
- clearError();
- updateWallet(accounts);
- } catch (err: any) {
- setErrorMessage(err.message);
+ if (savedSelectedWalletRdns && event.detail.info.rdns === savedSelectedWalletRdns) {
+ setSelectedWalletRdns(savedSelectedWalletRdns)
+ }
}
- setIsConnecting(false);
- };
- return (
-
- {children}
-
- );
-};
-
-export const useMetaMask = () => {
- const context = useContext(MetaMaskContext);
- if (context === undefined) {
- throw new Error(
- "useMetaMask must be used within a MetaMaskContextProvider"
- );
+ window.addEventListener("eip6963:announceProvider", onAnnouncement)
+ window.dispatchEvent(new Event("eip6963:requestProvider"))
+
+ return () => window.removeEventListener("eip6963:announceProvider", onAnnouncement)
+ }, [])
+```
+
+In this code sample, lines 6–12 are state definitions:
+
+- `wallets` - State to hold detected wallets.
+- `selectedWalletRdns` - State to hold the Reverse Domain Name System (RDNS) of the selected wallet.
+- `selectedAccountByWalletRdns` - State to hold accounts associated with each wallet.
+- `errorMessage` - State to hold the error message when a wallet throws an error on connection.
+- `clearError` - Function to clear the state in `errorMessage`.
+- `setError` - Function to set the state in `errorMessage`.
+
+Line 14 is the `useEffect` hook and it handles the following:
+
+- Local storage retrieval - On mount, it retrieves the saved selected wallet and accounts from local storage.
+- Event listener - It adds an event listener for the custom `eip6963:announceProvider` event.
+- State update - When the provider announces itself, it updates the state.
+- Provider request - It dispatches an event to request existing providers.
+- Cleanup - It removes the event listener on unmount.
+
+Add the following code to `src/hooks/WalletProvider.tsx` to connect a wallet and update the component's state:
+
+```tsx title="WalletProvider.tsx"
+const connectWallet = useCallback(async (walletRdns: string) => {
+ try {
+ const wallet = wallets[walletRdns]
+ const accounts = await wallet.provider.request({method:"eth_requestAccounts"}) as string[]
+
+ if(accounts?.[0]) {
+ setSelectedWalletRdns(wallet.info.rdns)
+ setSelectedAccountByWalletRdns((currentAccounts) => ({
+ ...currentAccounts,
+ [wallet.info.rdns]: accounts[0],
+ }))
+
+ localStorage.setItem("selectedWalletRdns", wallet.info.rdns)
+ localStorage.setItem("selectedAccountByWalletRdns", JSON.stringify({
+ ...selectedAccountByWalletRdns,
+ [wallet.info.rdns]: accounts[0],
+ }))
+ }
+ } catch (error) {
+ console.error("Failed to connect to provider:", error)
+ const walletError: WalletError = error as WalletError
+ setError(`Code: ${walletError.code} \nError Message: ${walletError.message}`)
}
- return context;
-};
+}, [wallets, selectedAccountByWalletRdns])
```
-With this context provider in place, you can update `/src/App.tsx` to include the provider and wrap
-it around the three components.
+This code uses the `walletRdns` parameter to identify the wallet's RDNS for connecting.
+It performs an asynchronous operation to request accounts from the wallet provider using the
+[`eth_requestAccounts`](/wallet/reference/eth_requestaccounts) RPC method.
-Notice the use of `~/utils` to import the utility functions.
+Add the following code to `src/hooks/WalletProvider.tsx` to disconnect from a wallet:
-:::note vite-tsconfig-paths
-This dapp is configured to use `vite-tsconfig-paths`, allowing it to load modules with locations
-specified by the `compilerOptions.paths` object in `tsconfig.json`.
-The path corresponding to the `./src/*` directory is represented by the `~/*` symbol.
-There's also a reference to `./tsconfig.node.json` in the `reference`'s array objects that correspond
-to `path`.
+```tsx title="WalletProvider.tsx"
+const disconnectWallet = useCallback(async () => {
+ if (selectedWalletRdns) {
+ setSelectedAccountByWalletRdns((currentAccounts) => ({
+ ...currentAccounts,
+ [selectedWalletRdns]: null,
+ }))
-`vite.config.ts` imports `tsconfigPaths` from `vite-tsconfig-paths` and adds it to the `plugins` array.
+ const wallet = wallets[selectedWalletRdns];
+ setSelectedWalletRdns(null)
+ localStorage.removeItem("selectedWalletRdns")
-See more information about [`vite-tsconfig-paths`](https://github.com/aleclarson/vite-tsconfig-paths).
+ try {
+ await wallet.provider.request({
+ method: "wallet_revokePermissions",
+ params: [{ "eth_accounts": {} }]
+ });
+ } catch (error) {
+ console.error("Failed to revoke permissions:", error);
+ }
+ }
+}, [selectedWalletRdns, wallets])
+```
+
+:::caution important
+[`wallet_revokePermission`](/wallet/reference/wallet_revokePermissions) is an experimental RPC
+method that might only work with MetaMask.
+Configuring the revocation in a try/catch block and separating it from the rest of the cleanup
+ensures that if a wallet does not support this feature, the rest of the disconnect functionality
+will still execute.
:::
-### 3. Wrap components with the context provider
+
+Use of `useCallback`
+