The last article taught you how to write, compile, test, and deploy smart contracts using Solidity, Javascript, dRPC endpoint, and API key.
Here, you'll learn how to build the user interface(UI) for the Coffee Payment using, React.js, Typescript, and Web3.js.
You know how to use CSS frameworks, React.js, and typescript.
Thirdweb is a web3 development platform that provides developers with SDKs, tools, and resources to simplify the process. For this article, you will be using the ConnectWallet SDK.
Before you get your hands dirty, it's important to understand why this process is crucial. A wallet must be connected before a user can interact with any dApp because the wallet serves as the user's identity and holds the necessary funds for transactions. This connection ensures that the user can securely and seamlessly use the smart contract features provided by the dApp.
Remember, you used the private key from your wallet to deploy the smart contract.
The role of thirdweb here is to simplify the writing of a long bunch of code; all you need is just to import some functions and a few lines of code.
npm i thirdweb
main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import { ThirdwebProvider } from "thirdweb/react"; //import ThirdwebProvider
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ThirdwebProvider>
<App />
</ThirdwebProvider>
</StrictMode>
)
connectWallet.tsx
under src
folder.connectWallet.tsx
import { createThirdwebClient } from "thirdweb";
import { ConnectButton } from "thirdweb/react";
const client = createThirdwebClient({ clientId: import.meta.env.VITE_CLIENT_ID});
export default function ConnectBtn() {
return (
<ConnectButton client={client} />
);
}
You will notice I use a client ID stored in
.env
file, the is because you need an API key to use the connectWallet SDK.
connectBtn
function to the App.tsx
file to view it
You will be able to connect any wallet of choice to any dApp.
Congratulations, you’ve successfully created a Connect Wallet button. Learn more on Thirdweb.
Here, you will be creating the state variable needed and functions to load web3 etc.
For now all your action will be carry out inside the
App.tsx
, you should have clear out the file by now.
if you have been following through from the deployment article, you’ve already installed web3.js, check your
package.json
for confirmation
import { useState, useEffect } from "react";
import Web3 from "web3";
import ABI from "../artifacts/contracts/coffee.sol/Coffee.json";
import ConnectBtn from "./connectWallet";
// State variables for amount, totalCoffeesSold, totalEtherReceived, coffeePrice and ethToUsdRate
const [amount, setAmount] = useState(0);
const [totalCoffeesSold, setTotalCoffeesSold] = useState<number | null>(null);
const [totalEtherReceived, setTotalEtherReceived] = useState<number | null>(null);
const [coffeePrice, setCoffeePrice] = useState(0);
const [ethToUsdRate, setEthToUsdRate] = useState(0);
const [accountBalance, setAccountBalance] = useState(0);
amount
: used to store the number of coffees being purchased.totalCoffeesSold
: Stores the total number of coffees sold.totalEtherReceived
: Stores the total amount of Ether received from coffee sales.coffeePrice
: Stores the price of the coffee in Ether.ethToUsdRate
: Stores the current exchange rate from Ether to USD.accountBalance
: Stores the user's account balance in Ether.// Function to load web3 and contract
const RPC = new Web3(`https://lb.drpc.org/ogrpc?network=sepolia&dkey=${import.meta.env.VITE_dRPC_API_KEY}`);
const web3 = new Web3(window.ethereum)
const contractAddress = "0xC8644fA354D7c2209cB6a9DFd9c6d18e899B8D97";
const contract = new web3.eth.Contract(ABI.abi, contractAddress);
Breakdown:
const RPC
= creates a new Web3 instance using a dPRC endpoint which contains URL and API key.const web3
= creates another Web3 instance using metamask's provider.window.ethereum
: This allows the application to interact with the user's wallet.const contractAddress
= the Ethereum address where the smart contract is deployed.const contract
= creates a new contract instance to interact with the smart contract.You will notice that you used two Web3 instance
RPC
andweb3
. This is because RPC Provider can only be used to make calls while “Window.ethereum” can be used for both calls and make transaction.
// Function to pay coffee from buyer account
const buyCoffee = async () => {
const accounts = await web3.eth.getAccounts();
await contract.methods.buyCoffee(amount).send({ from: accounts[0] });
};
This asynchronous function allows the user to pay for the number of coffees bought.
const accounts
Retrieved all the Ethereum accounts available in the user's wallet.await contract.methods.buyCoffee(amount).send({ from: accounts[0] })
: calls the buyCoffee
method of the smart contract to purchase coffee.contract.methods.buyCoffee(amount)
: Accesses the buyCoffee
function of the smart contract set it argument to the value of the input field where the user will enter the number of coffee bought..send({ from: accounts[0] })
: Sends a transaction from the first account in the accounts
array to execute the buyCoffee
method on the smart contract.The HTML syntax will look like this:
<input
type="number"
value={amount}
onChange={(e) => setAmount(parseInt(e.target.value))}
className="border bg-transparent rounded p-2 mb-4 w-full outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
/>
<button
onClick={buyCoffee}
className="bg-yellow-900 bg-opacity-35 text-white font-bold py-2 px-4 rounded w-full hover:bg-yellow-700 transition duration-300"
>
Buy Coffee
</button>
// Function to fetch total coffees sold
useEffect(() => {
const fetchTotalCoffeesSold = async () => {
try {
const total = (await contract.methods.getTotalCoffeesSold().call()) as number;
setTotalCoffeesSold(Number(total));
} catch (error) {
console.error('Error fetching total coffees sold:', error);
}
};
fetchTotalCoffeesSold();
}, []);
// Function to fetch total ether received
useEffect(() => {
const getTotalEther = async () => {
try {
const total = (await contract.methods.getTotalEtherReceived().call()) as number;
setTotalEtherReceived(Number(web3.utils.fromWei(total, 'ether')));
} catch (error) {
console.error('Error fetching total ether received:', error);
}
}; getTotalEther();
})
// Function to fetch coffee price
useEffect(() => {
const fetchCoffeePrice = async () => {
try {
const price = await contract.methods.coffeePrice().call();
const priceInEther = web3.utils.fromWei(Number(price), 'ether');
setCoffeePrice(Number(priceInEther));
} catch (error) {
console.error('Error fetching coffee price:', error);
}
};
fetchCoffeePrice();
}, []);
Each function here is calling the right function from the contract using the contract.methods.FunctionName().call()
and converting the return value price
to ether
using the web3.utils.fromWei(Number(price), ‘ether’
.
The HTML will look like this:
<p className="text-lg mb-2 flex justify-between">Amount of coffees sold: <span className="font-semibold">{totalCoffeesSold}</span></p>
<p className="text-lg mb-2 flex justify-between">Total ether received: <span className="font-semibold">{totalEtherReceived} Eth </span></p>
<p className="text-lg mb-4 flex justify-between">Coffee price: <span className="font-semibold">{coffeePrice} Eth </span></p>
To get the user Account balance, you will be using the dRPC provider.
Can you remember why? if you know the answer, drop it in the comment section
useEffect(() => {
const getAccountBalance = async () => {
const accounts = await web3.eth.getAccounts();
const balance = await RPC.eth.getBalance(accounts[0]);
setAccountBalance(Number(Number(web3.utils.fromWei(balance, 'ether')).toFixed(4)));
};
getAccountBalance();
})
This approach leverages web3
to interact with the Account for account retrieval and RPC
for querying the balance because you need the web3
to query the account connected.
The HTML looks like this:
<p className="text-lg mb-2 flex justify-between">Your balance: <span className="font-semibold">{accountBalance} Eth </span></p>
I decided to add two extra features here which are:
// Function to fetch the current price of ETH in USD using Coingecko API
useEffect(() => {
const fetchEthToUsdRate = async () => {
try {
const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd');
const data = await response.json();
setEthToUsdRate(data.ethereum.usd);
} catch (error) {
console.error('Error fetching ETH to USD rate:', error);
}
};
fetchEthToUsdRate();
}, []);
// variable` to display coffee price in USD
const coffeePriceInUsd = (coffeePrice * ethToUsdRate).toFixed(2);
// variable to display account balance in USD
const accountBalanceInUsd = (accountBalance * ethToUsdRate).toFixed(2);
// variable to display total ether received in USD
const totalEtherReceivedInUsd = ((totalEtherReceived ?? 0) * Number(ethToUsdRate)).toFixed(2);
const checkNetwork = async () => {
const networkId = await web3.eth.net.getId();
if (BigInt(networkId) !== BigInt(11155111)) { // Sepolia network ID
alert('Please switch to the Sepolia network');
}
};
useEffect(() => {
checkNetwork();
}, []);
By doing so, the User will know that they need to connect their wallet to the Sepolia network before interacting with the dApp.
import { useState, useEffect } from "react";
import Web3 from "web3";
import ABI from "../artifacts/contracts/coffee.sol/Coffee.json";
import ConnectBtn from "./connectWallet";
const App = () => {
// State variables for amount, totalCoffeesSold, totalEtherReceived, coffeePrice and ethToUsdRate
const [amount, setAmount] = useState(0);
const [totalCoffeesSold, setTotalCoffeesSold] = useState<number | null>(null);
const [totalEtherReceived, setTotalEtherReceived] = useState<number | null>(null);
const [coffeePrice, setCoffeePrice] = useState(0);
const [ethToUsdRate, setEthToUsdRate] = useState(0);
const [accountBalance, setAccountBalance] = useState(0);
// Function to load web3 and contract
const RPC = new Web3(`https://lb.drpc.org/ogrpc?network=sepolia&dkey=${import.meta.env.VITE_dRPC_API_KEY}`);
const web3 = new Web3(window.ethereum)
const contractAddress = "0xC8644fA354D7c2209cB6a9DFd9c6d18e899B8D97";
const contract = new web3.eth.Contract(ABI.abi, contractAddress);
// Function to check if connected to Sepolia network
const checkNetwork = async () => {
const networkId = await web3.eth.net.getId();
if (BigInt(networkId) !== BigInt(11155111)) { // Sepolia network ID
alert('Please switch to the Sepolia network');
}
};
useEffect(() => {
checkNetwork();
}, []);
// Function to fetch the current price of ETH in USD using Coingecko API
useEffect(() => {
const fetchEthToUsdRate = async () => {
try {
const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd');
const data = await response.json();
setEthToUsdRate(data.ethereum.usd);
} catch (error) {
console.error('Error fetching ETH to USD rate:', error);
}
};
fetchEthToUsdRate();
}, []);
//Function to show user account balance
useEffect(() => {
const getAccountBalance = async () => {
const accounts = await web3.eth.getAccounts();
const balance = await RPC.eth.getBalance(accounts[0]);
setAccountBalance(Number(Number(web3.utils.fromWei(balance, 'ether')).toFixed(4)));
};
getAccountBalance();
})
// Function to fetch total coffees sold
useEffect(() => {
const fetchTotalCoffeesSold = async () => {
try {
const total = (await contract.methods.getTotalCoffeesSold().call()) as number;
setTotalCoffeesSold(Number(total));
} catch (error) {
console.error('Error fetching total coffees sold:', error);
}
};
fetchTotalCoffeesSold();
}, []);
// Function to fetch total ether received
useEffect(() => {
const getTotalEther = async () => {
try {
const total = (await contract.methods.getTotalEtherReceived().call()) as number;
setTotalEtherReceived(Number(web3.utils.fromWei(total, 'ether')));
} catch (error) {
console.error('Error fetching total ether received:', error);
}
}; getTotalEther();
})
// Function to fetch coffee price
useEffect(() => {
const fetchCoffeePrice = async () => {
try {
const price = await contract.methods.coffeePrice().call();
const priceInEther = web3.utils.fromWei(Number(price), 'ether');
setCoffeePrice(Number(priceInEther));
} catch (error) {
console.error('Error fetching coffee price:', error);
}
};
fetchCoffeePrice();
}, []);
// Function to pay coffee from buyer account
const buyCoffee = async () => {
const accounts = await web3.eth.getAccounts();
await contract.methods.buyCoffee(amount).send({ from: accounts[0] });
};
// variable` to display coffee price in USD
const coffeePriceInUsd = (coffeePrice * ethToUsdRate).toFixed(2);
// variable to display account balance in USD
const accountBalanceInUsd = (accountBalance * ethToUsdRate).toFixed(2);
// variable to display total ether received in USD
const totalEtherReceivedInUsd = ((totalEtherReceived ?? 0) * Number(ethToUsdRate)).toFixed(2);
return (
<div
className="min-h-screen flex flex-col"
style={{
backgroundImage: "url('https://thumbs.dreamstime.com/b/coffee-background-space-text-85121087.jpg')",
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
>
<header className=" text-white py-4 shadow-md flex justify-between items-center px-8">
<h1 className="text-3xl font-bold">Coffee Store</h1>
<ConnectBtn />
</header>
<main className="flex-grow flex items-center justify-center p-4">
<div className="bg-white bg-opacity-50 shadow-lg rounded-lg p-8 max-w-md w-full transform transition duration-500 hover:scale-105">
<div className="text-center mb-6">
<h2 className="text-2xl font-semibold mb-2">Welcome to the Coffee Store</h2>
<p className="text-lg">Pay for your favorite coffee with Ether</p>
</div>
<div className="mb-4">
<p className="text-lg mb-2 flex justify-between">Your balance: <span className="font-semibold">{accountBalance} Eth (${accountBalanceInUsd}) </span></p>
<p className="text-lg mb-2 flex justify-between">Amount of coffees sold: <span className="font-semibold">{totalCoffeesSold}</span></p>
<p className="text-lg mb-2 flex justify-between">Total ether received: <span className="font-semibold">{totalEtherReceived} Eth (${totalEtherReceivedInUsd})</span></p>
<p className="text-lg mb-4 flex justify-between">Coffee price: <span className="font-semibold">{coffeePrice} Eth (${coffeePriceInUsd})</span></p>
</div>
<input
type="number"
value={amount}
onChange={(e) => setAmount(parseInt(e.target.value))}
className="border bg-transparent rounded p-2 mb-4 w-full outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
/>
<button
onClick={buyCoffee}
className="bg-yellow-900 bg-opacity-35 text-white font-bold py-2 px-4 rounded w-full hover:bg-yellow-700 transition duration-300"
>
Buy Coffee
</button>
</div>
</main>
</div>
);
}
export default App;
Your UI should look like this
If you have been following from the first article to this, congratulations 🥳and well done 👍🏽.
From the previous articles, you have been able to understand the core tech stack for web3 development, how to deploy a smart contract and create a UI for it, so feel free to call yourself a WEB3 DEVELOPER.
You can access the live site here
Live demo - https://www.youtube.com/watch?v=ZZNIbLWckDY