DEV Community

Jamiebones
Jamiebones

Posted on

How to Connect to Metamask Wallet in React

To integrate a smart contract with a frontend, the first task is usually how to connect a wallet to your frontend application. This blog post will show you how to connect a wallet from a React application.

Packages to install

The following packages will be needed to connect the wallet:

  • WalletConnectProvider
  • WalletLink
  • Web3Modal
  • Ether (this is used to connect to the smart contract on the blockchain)

The following packages above can be installed in a React application by running the following commands on terminal:

  npm i --save walletlink @walletconnect/web3-provider ethers 
  web3modal
Enter fullscreen mode Exit fullscreen mode

After installing the following packages, open your React App.js file or the top most component of your application. We will firstly import the packages we just installed.

  import { useEffect, useState, useCallback } from "react";
  import { ethers, providers } from "ethers";
  import Web3Modal from "web3modal";
  import WalletConnectProvider from '@walletconnect/web3-provider'
  import WalletLink from 'walletlink';

  const App = () => {
     return (
       <div>
         <h1>Hello</h1>
      </div>
     )
  }
Enter fullscreen mode Exit fullscreen mode

This is the skeleton of a simple React component. We declared a web3Modal that will allow the select of the wallet we want to use. We need to create a provider option that will be passed to the web3Modal.

  const INFURA_ID = "your-infura-api-key";
  const providerOptions = {
  walletconnect: {
    package: WalletConnectProvider, // required
    options: {
      infuraId: INFURA_ID, // required
    },
  },
  'custom-walletlink': {
    display: {
      logo: 'https://play-lh.googleusercontent.com/PjoJoG27miSglVBXoXrxBSLveV6e3EeBPpNY55aiUUBM9Q1RCETKCOqdOkX2ZydqVf0',
      name: 'Coinbase',
      description: 'Connect to Coinbase Wallet',
    },
    options: {
      appName: 'Coinbase', // Your app name
      networkUrl: `https://mainnet.infura.io/v3/${INFURA_ID}`,
      chainId: 1,
    },
    package: WalletLink,
    connector: async (_, options) => {
      const { appName, networkUrl, chainId } = options
      const walletLink = new WalletLink({
        appName,
      })
      const provider = walletLink.makeWeb3Provider(networkUrl, chainId)
      await provider.enable()
      return provider
    },
  },
}

Enter fullscreen mode Exit fullscreen mode

The WalletLink package allows users to use your application in any desktop browser without installing an extension, and it established a secure tunnel between your app and the mobile wallet with end-to-end encryption utilizing client-generated keys and keeps all user activity private.

WalletConnect package is an open protocol that helps to communicate securely between Wallets and Dapps (Web3 Apps). The protocol establishes a remote connection between two apps and/or devices using a Bridge server to relay payloads. We need to provide an api key to connect.

let web3Modal
if (typeof window !== 'undefined') {
  web3Modal = new Web3Modal({
    network: 'mainnet', // optional
    cacheProvider: true,
    providerOptions, // required
  })
}
Enter fullscreen mode Exit fullscreen mode

We pass the providerOptions to the web3Modal instance. The App.jsx looks exactly like this now.


  import { useEffect, useState, useCallback } from "react";
  import { ethers, providers } from "ethers";
  import Web3Modal from "web3modal";
  import WalletConnectProvider from '@walletconnect/web3-provider'
  import WalletLink from 'walletlink';

const INFURA_ID = "your-infura-api-key";
  const providerOptions = {
  walletconnect: {
    package: WalletConnectProvider, // required
    options: {
      infuraId: INFURA_ID, // required
    },
  },
  'custom-walletlink': {
    display: {
      logo: 'https://play-lh.googleusercontent.com/PjoJoG27miSglVBXoXrxBSLveV6e3EeBPpNY55aiUUBM9Q1RCETKCOqdOkX2ZydqVf0',
      name: 'Coinbase',
      description: 'Connect to Coinbase Wallet',
    },
    options: {
      appName: 'Coinbase', // Your app name
      networkUrl: `https://mainnet.infura.io/v3/${INFURA_ID}`,
      chainId: 1,
    },
    package: WalletLink,
    connector: async (_, options) => {
      const { appName, networkUrl, chainId } = options
      const walletLink = new WalletLink({
        appName,
      })
      const provider = walletLink.makeWeb3Provider(networkUrl, chainId)
      await provider.enable()
      return provider
    },
  },
}

 let web3Modal
if (typeof window !== 'undefined') {
  web3Modal = new Web3Modal({
    network: 'mainnet', // optional
    cacheProvider: true,
    providerOptions, // required
  })
}


  const App = () => {
     return (
       <div>
         <h1>Hello</h1>
      </div>
     )
  }

Enter fullscreen mode Exit fullscreen mode

The provider object needs to be persistent in our application and to prevent passing props down all the component of our application. We will create a context file which we will name walletContext.js.

This file will contain the following contents:

  import * as React from "react";

  const WalletContext = React.createContext();

  const accountDetails = {
     provider: null,
     address: null,
     signer: null,
     web3Provider: null,
     network: null
}

function WalletProvider({children}){
  const [account, setAccountDetails ] = 
   React.useState(accountDetails);
  const value = { account, setAccountDetails };
  return <WalletContext.Provider value={value}>{children} 
  </WalletContext.Provider>
}

function useWallet(){
    const context = React.useContext(WalletContext);
    if (!context){
        throw new Error("useWallet must be used within a WalletProvider")
    }
    return context;
}

export {WalletProvider, useWallet }

Enter fullscreen mode Exit fullscreen mode

We created a React context and named it WalletContext. Then we created a WalletProvider function that returns the WalletContext.Provider component wrapped with any children that will be passed to it. Inside the WalletProvider function, we declared a React state that will be used to store the details of the connected account. The account state and the function to change the state setAccountDetails is passed to the value property of the WalletContext.Provider component. See application state management

We need to create a button that will be used to connect the wallet. We will define two functions which are connect and the disconnect functions. As their name suggest, they will be used to connect and disconnect and disconnect the wallet from the blockchain. Before then we need to create React state that will be used to store the value returned from the functions.

Next we created a function that will consume the created WalletContext. The useWallet hook consumes the WalletContext and returns the context for use.

  <-- previous code above -->

   const App = () => {
    const { account, setAccountDetails } = useWallet();
    const { provider,
     address,
     signer,
     web3Provider,
     network } = account;


const connect = useCallback(async function () {
  const provider = await web3Modal.connect();
  const web3Provider = new providers.Web3Provider(provider);
  const signer = web3Provider.getSigner()
  const address = await signer.getAddress()
  const network = await web3Provider.getNetwork();
  const accountDetails = {
       provider,
       web3Provider,
       signer,
       address,
       network
  }
  setAccountDetails(accountDetails);
}, []);

const disconnect = useCallback(
  async function () {
    await web3Modal.clearCachedProvider()
    if (provider?.disconnect && typeof provider.disconnect === 'function') {
      await provider.disconnect()
    }
    //reset the state here
   const accountDetails = {
      provider: null,
      web3Provider: null,
      signer: null,
      address: null,
      network: null
 }
 setAccountDetails(accountDetails);
  },
  [provider]
)

     return (
       <div>
         <h1>Hello</h1>
      </div>
     )
  }

Enter fullscreen mode Exit fullscreen mode

The connect function creates a provider variable that is set to the default web3Modal connection. This provider is then plugged into the provider object of ether.js.

const web3Provider = new providers.Web3Provider(provider)
We are able to use the web3Providerobject created by ether.js to get the signer, network, address from ether.js. This values are saved in the context using the setAccountDetails function.

The disconnect function clears the web3Modal cache , the saved state and also disconnect the provider. We optimized both functions by wrapping them in a useCallback and passing dependencies.

Auto connecting

If we have already connected to our wallet before, we will want to automatically reconnect when visiting the page or component. This we can do by calling connect in a useEffect hook.

  // Auto connect to the cached provider
  useEffect(() => {
    if (web3Modal.cachedProvider) {
      connect()
    }
  }, [connect]);
Enter fullscreen mode Exit fullscreen mode

We can also listen to events on the provider object to handle some scenarios. Like we may want to be still connected, if a user switch the connected account. The function handleAccountsChanged does that by listening for an account changed event and updates the address portion of the WalletContext.

     useEffect(() => {
    if (provider?.on) {
      const handleAccountsChanged = (accounts) => {
        console.log('accountsChanged', accounts);
        setAccountDetails({
            ...account,
            address: accounts[0],
        })
      }
      const handleChainChanged = (_hexChainId) => {
        window.location.reload()
      }

      const handleDisconnect = (error) => {
        console.log('disconnect', error)
        disconnect()
      }

      provider.on('accountsChanged', handleAccountsChanged)
      provider.on('chainChanged', handleChainChanged)
      provider.on('disconnect', handleDisconnect)

      // Subscription Cleanup
      return () => {
        if (provider.removeListener) {
          provider.removeListener('accountsChanged', handleAccountsChanged)
          provider.removeListener('chainChanged', handleChainChanged)
          provider.removeListener('disconnect', handleDisconnect)
        }
      }
    }
  }, [provider, disconnect])

Enter fullscreen mode Exit fullscreen mode

Putting it all together

//APP>JX

  import { useEffect, useState, useCallback } from "react";
import { ethers, providers } from "ethers";
import Web3Modal from "web3modal";
import WalletConnectProvider from '@walletconnect/web3-provider'
import WalletLink from 'walletlink';
import { useWallet } from './walletContext';

import './App.css';

const trimAddress = ( address ) => {
  const firstpart = address.slice(0, 4);
  const midpart = "....";
  const endpart = address.slice(address.length - 4, address.length );
  return `${firstpart}${midpart}${endpart}`
}

const INFURA_ID = '460f40a260564ac4a4f4b3fffb032dad'


const providerOptions = {
  walletconnect: {
    package: WalletConnectProvider, // required
    options: {
      infuraId: INFURA_ID, // required
    },
  },
  'custom-walletlink': {
    display: {
      logo: 'https://play-lh.googleusercontent.com/PjoJoG27miSglVBXoXrxBSLveV6e3EeBPpNY55aiUUBM9Q1RCETKCOqdOkX2ZydqVf0',
      name: 'Coinbase',
      description: 'Connect to Coinbase Wallet (not Coinbase App)',
    },
    options: {
      appName: 'Coinbase', // Your app name
      networkUrl: `https://mainnet.infura.io/v3/${INFURA_ID}`,
      chainId: 1,
    },
    package: WalletLink,
    connector: async (_, options) => {
      const { appName, networkUrl, chainId } = options
      const walletLink = new WalletLink({
        appName,
      })
      const provider = walletLink.makeWeb3Provider(networkUrl, chainId)
      await provider.enable()
      return provider
    },
  },
}

let web3Modal
if (typeof window !== 'undefined') {
  web3Modal = new Web3Modal({
    network: 'mainnet', // optional
    cacheProvider: true,
    providerOptions, // required
  })
}


function App() {
  const { account, setAccountDetails } = useWallet();
  const { provider,
    address,
    signer,
    web3Provider,
    network } = account;


const connect = useCallback(async function () {
  const provider = await web3Modal.connect();
  const web3Provider = new providers.Web3Provider(provider);
  const signer = web3Provider.getSigner()
  const address = await signer.getAddress()
  const network = await web3Provider.getNetwork();
  const accountDetails = {
       provider,
       web3Provider,
       signer,
       address,
       network
  }
  setAccountDetails(accountDetails);
}, []);



const disconnect = useCallback(
  async function () {
    await web3Modal.clearCachedProvider()
    if (provider?.disconnect && typeof provider.disconnect === 'function') {
      await provider.disconnect()
    }
    //reset the state here
    const accountDetails = {
      provider: null,
      web3Provider: null,
      signer: null,
      address: null,
      network: null
 }
 setAccountDetails(accountDetails);

},
  [provider]
)

  // Auto connect to the cached provider
  useEffect(() => {
    if (web3Modal.cachedProvider) {
      connect()
    }
  }, [connect]);


  useEffect(() => {
    if (provider?.on) {
      const handleAccountsChanged = (accounts) => {
        // eslint-disable-next-line no-console
        console.log('accountsChanged', accounts);
        setAccountDetails({
            ...account,
            address: accounts[0],
        })
      }

      const handleChainChanged = (_hexChainId) => {
        window.location.reload()
      }

      const handleDisconnect = (error) => {
        console.log('disconnect', error)
        disconnect()
      }

      provider.on('accountsChanged', handleAccountsChanged)
      provider.on('chainChanged', handleChainChanged)
      provider.on('disconnect', handleDisconnect)

      // Subscription Cleanup
      return () => {
        if (provider.removeListener) {
          provider.removeListener('accountsChanged', handleAccountsChanged)
          provider.removeListener('chainChanged', handleChainChanged)
          provider.removeListener('disconnect', handleDisconnect)
        }
      }
    }
  }, [provider, disconnect])

  return (
    <div className="App">
         {web3Provider ? (
               <button className="btn btn-danger" type="button" onClick={disconnect}>
                 {trimAddress(address)}
          </button>
        ) : (
          <button className="btn btn-success" type="button" onClick={connect}>
            Connect
          </button>
        )}
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

To make use of the WalletProvider we wrap our App component with it. This makes the WalletContext available in all the components that it will be needed.

import { WalletProvider } from "./walletContext";
ReactDOM.render(
  <React.StrictMode>
    <WalletProvider>
        <App />
    </WalletProvider>
  </React.StrictMode>,
  document.getElementById('root')
Enter fullscreen mode Exit fullscreen mode

The code is available here

Thanks for reading.

Top comments (1)

Collapse
 
athifbinu profile image
Athif binu

ok