Introduction
I got referred by someone to create a frontend react app for a web3 project. I have never developed anything in the web3 space but I liked the challenge so I took it up.
My task was to connect MetaMask and Solana Phantom wallets to a React app.
I had to do a lot of research to get this working and I will be dropping links to resources that were helpful at the end of this tutorial.
Side note: This article was supposed to contain MetaMask and Phantom wallet connection to a React app. However, after writing about MetaMask connection I realized the article was getting too long so I decided to break it up and write about Phantom wallet connection in another article.
What we are building
We will be building a simple button that prompts a modal with MetaMask and Phantom wallet options so users can select which wallet to connect to.
The user's wallet address is also going to be displayed on the Navbar after they connect.
Let's get started!
Getting started
To get started, we have to create a react app called web3-wallet
I'm assuming you are already familiar with React setup so I won't go into that. If you aren't you can read about setting up your react app here
We delete everything in src
except index.js
, app.js
, and index.css
Now, let's build out a basic React app that the wallets will connect to.
Creating a basic React app
We will create a navbar with a connect
button for users to click on.
I'll be using tailwindCSS for the styling because I love it π
Tailwind setup
To install tailwindCSS, run the following command on your terminal
yarn add -D tailwindcss postcss autoprefixer
Create a tailwind.config.js
file on your root folder and paste this
module.exports = {
content: ["./src/**/*.{html,js}"],
theme: {
extend: {},
},
plugins: [],
}
Also, create postcss.config.js
file on your root folder and paste this
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}
Lastly, add the following to your index.css
file
@tailwind base;
@tailwind components;
@tailwind utilities;
Navbar component
In src
, create a folder called components
that will host all the components used on this app.
In the components
folder, create a component called Navbar.js
The Navbar will have a logo, and a button and also display the wallet address when a user connects to a wallet.
import React from 'react';
const Navbar = () => {
return (
<nav className='lg:px-52 px-6 py-8 bg-[#8b5cf6] relative'>
<div className='flex justify-between transition-all duration-500
ease-in rounded'>
<div className='text-white font-bold text-3xl'>LOGO</div>
<h3 className='text-white font-semibold mr-4'>Wallet address:</h3>
<button
type='button'
className='my-6 lg:my-0 bg-white text-primary-200 font-semibold
py-2 px-6 items-center rounded-lg flex justify-center >
Connect
</button>
</div>
</nav>
);
};
export default Navbar;
In App.js
, let's clear the boilerplate code and import Navbar
import React from 'react';
import Navbar from './components/Navbar';
const App = () => {
return (
<>
<Navbar />
</>
);
};
export default App;
On the terminal run yarn start
to open the app on http://localhost:3000/
We can see our Navbar with the connect button on it.
We want the button to open up a modal and we'll use useState
hook to keep track of the state of the modal.
Since this state
is going to be accessed by more than one component, it is best practice to keep it in a common parent component, which in this case is App.js
In App.js
, import useState
and create a state called modalOpen
setting the initial value to false
.
To open the Modal, modalOpen
needs to be true
, so let's pass this as a props
in the Navbar
component.
import React, { useState } from 'react';
import Navbar from './components/Navbar';
const App = () => {
const [modalOpen, setModalOpen] = useState(false);
return (
<>
<Navbar openModal={() => setModalOpen(true)} />
</>
);
};
export default App;
In the Navbar.js
file, import openModal
and pass it to the onClick
event of the connect
button.
const Navbar = ({ openModal }) => {
return (
<nav className='lg:px-52 px-6 py-8 bg-[#8b5cf6] relative'>
<div className='flex justify-between transition-all duration-500
ease-in rounded'>
<div className='text-white font-bold text-3xl'>LOGO</div>
<h3 className='text-white font-semibold mr-4'>Wallet address:</h3>
<button
type='button'
className='my-6 lg:my-0 bg-white text-primary-200 font-semibold py-2
px-6 items-center rounded-lg flex justify-center '
onClick={openModal}
>
Connect
</button>
</div>
</nav>
);
};
export default Navbar;
To check if this works, add console.log(modalOpen)
to the App.js
file.
When we inspect the app, we can see false
logged on the console and when the connect
button is clicked true
gets logged on the console. So it works!
Now, we can build the Modal
component.
Modal component
In the components
folder, create a file called Modal.js
.
We also need to pass the modalOpen
state to the Modal
component. Let's import this component to App.js
so that we can pass the state
as props
.
return (
<>
<Navbar openModal={() => setModalOpen(true)} />
<Modal isOpen = {modalOpen}/>
</>
);
In Modal.js
, import the isOpen
props
Remember modalOpen
needs to be true to render the Modal
component, so let's write a condition to this effect.
import React from 'react';
const Modal = ({ isOpen }) => {
return <>{isOpen ? <h3>I'm open</h3> : null}</>;
};
export default Modal;
This condition renders the Modal
only when the state is true, and the state becomes true when the connect
button is clicked.
We can see I'm open
text displayed on the screen when the connect
button is clicked
To close the Modal, we have to set modalOpen
to false.
Back in App.js
, set modalOpen
to false
and pass it as props to the Modal
component.
<Modal isOpen={modalOpen} onClose={()=>setModalOpen(false)} />
Let's import onClose
in the Modal
component and style the component.
import React from 'react';
const Modal = ({ isOpen, onClose }) => {
return (
<>
{isOpen ? (
<div className='bg-[rgba(0,0,0,0.5)] top-0 left-0 w-full h-screen absolute'>
<div className=' right-0 left-0 z-50 lg:w-4/12 absolute bg-white p-4 top-20
h-full mx-auto'>
<div className=' py-4 flex px-6 justify-end'>
<button
className='border border-current rounded px-4 py-1 text-xl font-semibold
cursor-pointer'
onClick={onClose}
>
Close
</button>
</div>
<h3 className='text-center text-2xl font-semibold'>
Choose Wallet to continue
</h3>
</div>
</div>
) : null}
</>
);
};
export default Modal;
Now, we have a toggle between the connect
button on the Navbar and the close
button on the Modal.
Wallets component
Let's create a Wallets
component to hold wallet details for MetaMask and Phantom.
In the Wallets.js
file, create an array of objects. This array can also be saved in another file and imported here if you like.
const walletList = [
{
name: 'Phantom',
icon: <img src={PhantomLogo} alt='phantom logo' className='w-[30px]' />,
button: (
<button className='bg-[#512da8] hover:bg-[#1a1f2e] text-white
py-3 px-5 rounded-[4px]'>
Connect
</button>
),
},
{
name: 'MetaMask',
icon: <Icon icon='logos:metamask-icon' className='text-2xl' />,
button: (
<button className='bg-[#512da8] hover:bg-[#1a1f2e] text-white
py-3 px-5 rounded-[4px]'>
Connect
</button>
),
},
];
For the icons, I downloaded an image for Phantom wallet and saved it in the assets
folder inside src
. For MetaMask, I used a MetaMask icon from iconify
. You can install iconify
by running yarn add -D @iconify/react
We will map
through walletList
array to render wallet details.
{walletList.map((wallet) => (
<div
key={wallet.name}
className='flex items-center justify-between mb-5 px-4'
>
<div className='flex'>
<span className='mr-3'>{wallet.icon}</span>
<p> {wallet.name}</p>
</div>
<div> {wallet.button}</div>
</div>
))}
Now, we can import Wallets
component inside the Modal
component
import React from 'react';
import Wallets from './Wallets';
const Modal = ({ isOpen, onClose }) => {
return (
<>
{isOpen ? (
<div className='bg-[rgba(0,0,0,0.5)] top-0 left-0 w-full h-screen absolute'>
<div className=' right-0 left-0 z-50 lg:w-4/12 absolute bg-white p-4
top-20 h-full mx-auto'>
<div className=' py-4 flex px-6 justify-end'>
<button
className='border border-current rounded px-4 py-1 text-xl
font-semibold cursor-pointer'
onClick={onClose}
>
Close
</button>
</div>
<h3 className='text-center text-2xl font-semibold'>
Choose Wallet to continue
</h3>
<Wallets />
</div>
</div>
) : null}
</>
);
};
export default Modal;
We have built a basic React app and can now connect MetaMask and Phantom wallets to it.
Connecting MetaMask
I found MetaMask pretty straightforward and easy to connect, so I'll start with it.
First off, we add MetaMask extension to our browser.
If you are using chrome browser, click here to add MetaMask to browser, then create a MetaMask account if you don't already have one.
Now we install some dependencies to aid with the connectivity. I'll attach resources to explain what these dependencies do.
yarn add web3 @web3-react/core @web3-react/injected-connector
In App.js
we import Web3
and Web3ReactProvider
from the dependencies we installed.
We create getLibrary
function and pass the function as props to the Web3ReactProvider
, then we wrap our components in App.js
with the Web3ReactProvider
.
import React, { useState } from 'react';
import Modal from './components/Modal';
import Navbar from './components/Navbar';
import Web3 from 'web3';
import { Web3ReactProvider } from '@web3-react/core';
const App = () => {
const [modalOpen, setModalOpen] = useState(false);
const getLibrary = (provider) => {
return new Web3(provider);
};
return (
<>
<Web3ReactProvider getLibrary={getLibrary}>
<Navbar openModal={() => setModalOpen(true)} />
<Modal isOpen={modalOpen} onClose={() => setModalOpen(false)} />
</Web3ReactProvider>
</>
);
};
export default App;
Create a folder called helper
in src
and a file called helper.js
in the folder.
In helper.js
, import InjectedConnector
from @web3-react/injected-connector
import { InjectedConnector } from '@web3-react/injected-connector';
export const injected = new InjectedConnector({
supportedChainIds: [1, 3, 4, 5, 42],
});
When a user is connected to MetaMask, we want to be able to get the active status, account information, and some other details and we want to use these details in several components on the app. To avoid props drilling, let's use Context API
to pass these props around.
Create a folder in src
called context
, Let's call the context MetaMaskWalletContext.js
Here we can import useWeb3React
and gain access to MetaMask wallet details
import React, { createContext } from 'react';
import { useWeb3React } from '@web3-react/core';
export const MetaMaskWalletContext = createContext();
export const MetaMaskWalletProvider = ({ children }) => {
const { active, activate, deactivate, account } = useWeb3React();
return (
<MetaMaskWalletContext.Provider
value={{ active, activate, deactivate, account }}
>
{children}
</MetaMaskWalletContext.Provider>
);
};
export default MetaMaskWalletProvider;
If we need to extract more details from useWeb3React
we can just add them later on.
In App.js
, let's import MetaMaskWalletProvider
and wrap our components with it.
import React, { useState } from 'react';
import Modal from './components/Modal';
import Navbar from './components/Navbar';
import Web3 from 'web3';
import { Web3ReactProvider } from '@web3-react/core';
import { MetaMaskWalletProvider } from './context/MetaMaskWalletContext';
const App = () => {
const [modalOpen, setModalOpen] = useState(false);
const getWeb3Library = (provider) => {
return new Web3(provider);
};
return (
<>
<Web3ReactProvider getWeb3Library={getWeb3Library}>
<MetaMaskWalletProvider>
<Navbar openModal={() => setModalOpen(true)} />
<Modal isOpen={modalOpen} onClose={() => setModalOpen(false)} />
</MetaMaskWalletProvider>
</Web3ReactProvider>
</>
);
};
export default App;
Back in Wallets.js
, we can use MetaMaskWalletContext
and access the details we require.
Let's write a function for the connect
button onClick
event.
We import injected
from the helper
file and pass it into activate
import { MetaMaskWalletContext } from '../context/MetaMaskWalletContext';
import { injected } from '../helper/helper';
const { active, activate } = useContext(MetaMaskWalletContext);
async function connect() {
try {
await activate(injected);
} catch (ex) {
console.log(ex);
}
}
button: (
<button
className='bg-[#512da8] hover:bg-[#1a1f2e] text-white
py-3 px-5 rounded-[4px]'
onClick={connect}
>
Connect
</button>
Now when we click on connect
, MetaMask opens up on the browser and asks for password to login. Cool!
Let's make the modal close when a user is connected to MetaMask.
We already wrote an onClose
function for the modal close
button so we can just pass it down as props to the Wallets
component.
<Wallets onClose={onClose} />
In Wallets.js
, let's write a logic to close the modal when the connection is active. We check if a connection is active and trigger onClose
if (active) {
onClose();
}
The next thing we need to do is to display the Wallet address on the navbar.
In Navbar.js
, import the MetaMaskWalletContext
so we can access account
and active
status
import { MetaMaskWalletContext } from '../context/MetaMaskWalletContext';
const { account, active } = useContext(MetaMaskWalletContext);
<h3 className='text-white font-semibold mr-4'>
{active ? `Wallet address: ${account}` : ''}
</h3>
Now when we connect, we can see the wallet address clearly on the dashboard.
Let's also change the connect button to disconnect and also write a function to disconnect a user from MetaMask.
In Navbar.js
import { injected } from '../helper/helper';
async function disconnect() {
try {
await deactivate(injected);
} catch (ex) {
console.log(ex);
}
}
const handleConnection = () => {
if (active) {
disconnect();
} else {
openModal();
}
};
<button
type='button'
className='my-6 lg:my-0 bg-white text-primary-200 font-semibold py-2
px-6 items-center rounded-lg flex justify-center '
onClick={handleConnection}
>
{active ? 'Disconnect' : 'Connect'}
</button>
Tada! there we have itπ
We can connect to MetaMask wallet, display the wallet address, and disconnect when we want.
I hope this article has been helpful. Tell me what you think by dropping a comment.π
Github repo : https://github.com/Netacci/metamask-connect
Metamask resources : https://www.youtube.com/watch?v=DCA53Go5ON8&t=622s&ab_channel=ShmojiCodes