DEV Community

Cover image for Building a Web 3.0 E-commerce App with Hardhat and React.js
Busayo Samuel
Busayo Samuel

Posted on

Building a Web 3.0 E-commerce App with Hardhat and React.js

Introduction

The first e-commerce platform was created in 1982 by the Boston Computer Exchange (BCE). It wasn't until 13 years later that now-household names like eBay and Amazon came into the picture. When the idea of e-commerce was first introduced, it was met with a lot of skepticism. I understand—I’d be skeptical too if I were asked to trust an invisible company with not only my money but to deliver perfectly fitting shoes. The success we see in e-commerce today is largely due to the tireless efforts of companies that prioritized building trust among their users.

As the web continues to evolve, e-commerce solutions have also had to evolve with it. In a few years, we might see more companies move from using Web 2.0 to adopting Web 3.0 or even Web 5.0. At its core, Web 3.0's selling point is the ability to create an era of trustless transactions where smart contracts are the guarantors of trust. This begs the question, well actually two questions:

  • Can transactions in a Web 3.0 e-commerce platform truly remain decentralized?

  • Could a successful Web 3.0 e-commerce store drive higher adoption of Web 3.0 technologies and enhance user trust?

Let’s unpack these questions.

Can transactions in a Web 3.0 e-commerce platform truly remain decentralized?

In theory, a Web 3.0 e-commerce platform could maintain a certain degree of decentralization by automating many aspects of the transactions.

A decentralized storage solution can be used for product information and data. The best option to ensure the safety of user data is through the use of Web 5.0. To learn more about Web 5.0, you can read this article.

Basically, it can be challenging to build a completely decentralized e-commerce platform, but it is not impossible. Some level of centralization might be necessary for reasons like dispute resolution, product quality control, and shipping logistics.

Could a successful Web 3.0 e-commerce store drive higher adoption of Web 3.0 technologies and enhance user trust?

I have no doubt that a successful Web 3.0 e-commerce store will contribute to wider adoption. Here's how:

  • An e-commerce platform will provide a everyday use case for cryptocurrency technologies.
  • If a Web 3.0 platform can provide seamless transactions, it would help address skepticism surrounding Web 3.0 and make users more comfortable with making payments through it.
  • The inherent transparency of blockchain allows users to verify their transactions, so dispute resolution will be much easier.

Knowing all these, it is clear that building more fast and seamless Web 3.0 ecommerce platforms is just what we need for a wider adoption of blockchain technologies.

In this article, you will be provided with the practical knowledge to start building your own Web 3.0 e-commerce application using Solidity, Hardhat, and React.js. Let's get started.

Prerequisites

Before jumping into this tutorial, make sure you have the following:

  • A basic understanding of Solidity and JavaScript.
  • Node.js and npm installed on your system.
  • MetaMask installed in your browser.
  • A code editor or IDE like VSCode

Setting Up the Development Environment

In your terminal or file directory create a folder called Ethcommerce, and open it in your code editor.

mkdir Ethcommerce
cd Ethcommerce
code .
Enter fullscreen mode Exit fullscreen mode

Setting up a React project

In your project folder, enter the following commands to install React globally and create a new React project inside the root directory:

npm install --global create-react-app

npx create-react-app@latest ./

Enter fullscreen mode Exit fullscreen mode

Setting up Hardhat

Hardhat is an environment that developers use to test, compile, deploy, and debug dApps. Hardhat provides a local Ethereum network designed for development. You can easily deploy your contracts, run tests, and debug Solidity code without dealing with live environments.

In the root of your project, install Hardhat and related dev dependencies:

npm install --save-dev hardhat @nomicfoundation/hardhat-chai-matchers
Enter fullscreen mode Exit fullscreen mode

Then, initialize a new hardhat project. The command below will create starter files for your project:

npx hardhat init
Enter fullscreen mode Exit fullscreen mode

After you run the command above, a menu option list will appear to configure your preferences. Since you already have a .gitignore file, choose “no”, when prompted to create one. For the other options, accept the default settings.

Smart contract development

In the root of your project, navigate to the contracts directory and create a solidity file named Ethcommerce.sol. Then, add the following code to create a new smart contract.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;


contract Ethcommerce {}

Enter fullscreen mode Exit fullscreen mode

Inside your contract, declare an address variable to represent the owner of the project. In the constructor, assign the value of msg.sender to this owner variable.

 address public owner;
    constructor() {
        owner = msg.sender;
    }

Enter fullscreen mode Exit fullscreen mode

Create a struct called Item in your contract. This struct will define all the properties for each individual store product.

    struct Item {
        uint256 id;
        string name;
        string category;
        string image;
        uint256 cost;
        uint256 rating;
        uint256 stock;
    }

Enter fullscreen mode Exit fullscreen mode

Then create a modifier called onlyOwner. This modifier will ensure that only the owner of the smart contract can list an item in the store.

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }


Enter fullscreen mode Exit fullscreen mode

Listing Products

In a traditional e-commerce application, the seller or admin has permission to list products in the store. The list function in this application serves precisely that purpose.

Before you create the list function, you need to create a mapping and an event. The items mapping will store the products in the smart contract. The List event is emitted once a product has been successfully listed.

mapping(uint256 => Item) public items;
event List(string name, uint256 cost, uint256 quantity);

Enter fullscreen mode Exit fullscreen mode
    function list(
        uint256 _id,
        string memory _name,
        string memory _category,
        string memory _image,
        uint256 _cost,
        uint256 _rating,
        uint256 _stock
    ) public onlyOwner {
        // Create Item
        Item memory item = Item(
            _id,
            _name,
            _category,
            _image,
            _cost,
            _rating,
            _stock
        );


        // Add Item to mapping
        items[_id] = item;


        // Emit event
        emit List(_name, _cost, _stock);
    }

Enter fullscreen mode Exit fullscreen mode

The function above takes in the properties of the struct Item, and these properties are used to create a new product item, which is added to the mapping of items.

Adding tests

Writing tests is an important step when developing smart contract code. Even a small error in the logic can result in your wallet being drained. Since a smart contract becomes immutable once deployed, developers need to write comprehensive tests that cover all aspects of the logic.

Hardhat allows us to use Mocha and Chai to write tests in Javascript. To get started, navigate to the tests folder and create a new file called ethcommerce.js.
Add the code below to your test file:

const { expect } = require("chai")
const {ethers}=require("hardhat")
require("@nomicfoundation/hardhat-chai-matchers")


const tokens = (n) => {
  return ethers.parseUnits(n.toString(), 'ether')
}


// Global constants for listing an item...
const ID = 1
const NAME = "Shoes"
const CATEGORY = "Clothing"
const IMAGE = "https://ipfs.io/ipfs/QmTYEboq8raiBs7GTUg2yLXB3PMz6HuBNgNfSZBx5Msztg/shoes.jpg"
const COST = tokens(1)
const RATING = 4
const STOCK = 5
Enter fullscreen mode Exit fullscreen mode

The tokens function in the code above uses the parseUnits method to convert the ether value passed into the function to wei (the smallest unit in Ethereum).

Add the code below to create a new test suite called Ethcommerce.

describe("Ethcommerce", () => {
    let ethcommerce
    let deployer, buyer

    beforeEach(async () => {
      // Setup accounts
      [deployer, buyer] = await ethers.getSigners()

      // Deploy contract
      const Ethcommerce= await ethers.getContractFactory("Ethcommerce")
      ethcommerce = await  Ethcommerce.deploy()
    })

     //rest of the test
})

Enter fullscreen mode Exit fullscreen mode

The Ethcommerce test suite declares three variables: ethcommerce (representing an instance of the smart contract), and deployer and buyer (representing Ethereum account signers).

The beforeEach function runs before each test in the suite. So, before each test, a new instance of the Ethcommerce contract is created and then deployed.

The code below creates tests to check the contract's deployment behaviour. The first test expects that the owner’s address matches the deployer’s address, which is the first address in the list returned from ethers.getSigners() .

The next tests check the listing behaviour. It ensures that each item property is correctly assigned and that the right event is emitted at the end of the listing.

  describe("Deployment", () => {
        it("Sets the owner", async () => {
          expect(await ethcommerce.owner()).to.equal(deployer.address)
        })
      })


      describe("Listing", () => {
        let transaction

        beforeEach(async () => {
          // List a item
          transaction = await ethcommerce.connect(deployer).list(ID, NAME, CATEGORY, IMAGE, COST, RATING, STOCK)
          await transaction.wait()
        })

        it("Returns item attributes", async () => {
          const item = await ethcommerce.items(ID)

          expect(item.id).to.equal(ID)
          expect(item.name).to.equal(NAME)
          expect(item.category).to.equal(CATEGORY)
          expect(item.image).to.equal(IMAGE)
         expect(item.cost).to.equal(COST)
          expect(item.rating).to.equal(RATING)
          expect(item.stock).to.equal(STOCK)
        })

        it("Emits List event", () => {
          expect(transaction).to.emit(ethcommerce, "List")
        })
      })

Enter fullscreen mode Exit fullscreen mode

Buying Products

In this section, you will create a function in your contract that allows users to buy products listed in the contract. Let’s get into it.

Add the code below to create three variables:

  • Order: A struct for orders
  • orderCount: A mapping that keeps track of all orders
  • orders: Another mapping to store all the orders.

The mapping, orders is a nested mapping. It uses the address of the user placing an order as the key, and the corresponding value is a mapping of all the orders made by that user.

 mapping(address => mapping(uint256 => Order)) public orders;
 mapping(address => uint256) public orderCount;

 struct Order {
        uint256 time;
        Item item;
  }

Enter fullscreen mode Exit fullscreen mode

Add a Buy event in your contract. This event will be emitted every time a user buys a product.

 event Buy(address buyer, uint256 orderId, uint256 itemId);

Enter fullscreen mode Exit fullscreen mode

Add the buy function just after the list function:

  function buy(uint256 _id) public payable {
        // Fetch item
        Item memory item = items[_id];


        // Require enough ether to buy item
        require(msg.value >= item.cost);


        // Require item is in stock
        require(item.stock > 0);


        // Create order
        Order memory order = Order(block.timestamp, item);


        // Add order for user
        orderCount[msg.sender]++; // <-- Order ID
        orders[msg.sender][orderCount[msg.sender]] = order;


        // Subtract stock
        items[_id].stock = item.stock - 1;


        // Emit event
        emit Buy(msg.sender, orderCount[msg.sender], item.id);
    }

Enter fullscreen mode Exit fullscreen mode

The buy function takes in the _id of the product. This _id is used to fetch the product from the mapping of items.

If the msg.value sent along with the buy request is greater or equal to the cost of the item and the item is still in stock, a new order will be created with the timestamp and the item.

The orderCount, item’s stock (item.stock) and items mapping variables will then be updated with the appropriate values.

Finally, a Buy event will be emitted to indicate a successful purchase.

Writing tests for the buy function

The test suite for the buy function will be checking 4 scenarios:

  • Order Count Update: This test case checks whether the buyer’s order count increases by 1 after making a purchase.
  • Order Details: It checks that the order details (such as timestamp and item name) are correctly stored in the contract after the purchase.
  • Contract Balance Update: This test case confirms that the contract’s balance (in Ether) is updated by the cost of the purchased item.
  • Event Emission: Finally, the last test case checks whether the contract emits a ‘Buy’ event during the purchase.

Go to your test file and add the code below:

describe('Buying', () => {
    let transaction;

    beforeEach(async () => {
      // List a item
      transaction = await ethcommerce
        .connect(deployer)
        .list(ID, NAME, CATEGORY, IMAGE, COST, RATING, STOCK);
      await transaction.wait();


      // Buy a item
      transaction = await ethcommerce.connect(buyer).buy(ID, { value: COST });
      await transaction.wait();
    });

    it("Updates buyer's order count", async () => {
      const result = await ethcommerce.orderCount(buyer.address);
      expect(result).to.equal(1);
    });

    it('Adds the order', async () => {
      const order = await ethcommerce.orders(buyer.address, 1);


      expect(order.time).to.be.greaterThan(0);
      expect(order.item.name).to.equal(NAME);
    });


    it('Updates the contract balance', async () => {
      const result = await ethers.provider.getBalance(ethcommerce.getAddress());
      expect(result).to.equal(COST);
    });


    it('Emits Buy event', () => {
      expect(transaction).to.emit(ethcommerce, 'Buy');
    });
  });


Enter fullscreen mode Exit fullscreen mode

Withdrawing

Navigate back to Ethcommerce.sol and add a withdraw function. This function sends the balance of the contract to the owner.


    function withdraw() public onlyOwner {
        (bool success, ) = owner.call{value: address(this).balance}("");
        require(success);
    }

Enter fullscreen mode Exit fullscreen mode

Writing tests for withdrawing

Copy the code below into your test files to check for withdrawal behaviour.

These tests ensure that only the owner of the contract can withdraw and that the contract balance and owner balance are correctly updated when a withdrawal is triggered.

     describe("Withdrawing", () => {
    let balanceBefore

    beforeEach(async () => {
      // List a item
      let transaction = await ethcommerce.connect(deployer).list(ID, NAME, CATEGORY, IMAGE, COST, RATING, STOCK)
      await transaction.wait()

      // Buy a item
      transaction = await ethcommerce.connect(buyer).buy(ID, { value: COST })
      await transaction.wait()

      // Get Deployer balance before
      balanceBefore = await ethers.provider.getBalance(deployer.address)

      // Withdraw
      transaction = await ethcommerce.connect(deployer).withdraw()
      await transaction.wait()
    })

    it('Updates the owner balance', async () => {
      const balanceAfter = await ethers.provider.getBalance(deployer.address)
      expect(balanceAfter).to.be.greaterThan(balanceBefore)
    })

    it('Updates the contract balance', async () => {
      const result = await ethers.provider.getBalance(ethcommerce.getAddress())
      expect(result).to.equal(0)
    })
  })

Enter fullscreen mode Exit fullscreen mode

Enter the command below in your terminal to run your tests:

npx hardhat test
Enter fullscreen mode Exit fullscreen mode

To start the hardhat node, copy the command below:

npx hardhat node
Enter fullscreen mode Exit fullscreen mode

Deploying the contract

Usually, in an e-commerce application, the listing of products is usually done in a dashboard. However, for the purpose of this tutorial, you will be seeding the list of product items from a JSON file.

You can copy the JSON file from my GitHub repository.

In the src directory, create an items.json file and paste the copied content into this file.

Hardhat allows users to create a script file containing deployment instructions. For this project you will create a deploy.js script file. In this file, the products listed in items.json will be seeded into the contract after deployment.

In the root of your project, create a scripts folder, then create a file called deploy.js. In this file, add the following code:

const hre = require("hardhat")

const { items } = require("../src/items.json")

const tokens = (n) => {
  return ethers.parseUnits(n.toString(), 'ether')
}

async function main() {
  // Setup accounts
  const [deployer] = await ethers.getSigners()

  // Deploy Ethcommerce
  const Ethcommerce = await hre.ethers.getContractFactory("Ethcommerce")
  const ethcommerce = await Ethcommerce.deploy()
  await ethcommerce.waitForDeployment()
  const address= await ethcommerce.getAddress()
  console.log(`Deployed Ethcommerce Contract at: ${address}\n`)

  // Listing items...
  for (let i = 0; i < items.length; i++) {
    const transaction = await ethcommerce.connect(deployer).list(
      items[i].id,
      items[i].name,
      items[i].category,
      items[i].image,
      tokens(items[i].price),
      items[i].rating,
      items[i].stock,
    )

    await transaction.wait()

    console.log(`Listed item ${items[i].id}: ${items[i].name}`)
  }
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
Enter fullscreen mode Exit fullscreen mode

The script above begins by deploying Ethcommerce. Once the contract has been successfully deployed, we proceed to loop through the items from the JSON file. Using the list function, each product item is seeded into the contract.

Run the script with the command:

npx hardhat run ./scripts/deploy.js --network localhost
Enter fullscreen mode Exit fullscreen mode

Make sure your hardhat node is running in another terminal when you run this command.

This will initiate the deployment process for your contract on the local network.

If you check your console, you should see a message that prints out your contract address.

Proceed to navigate to your src folder and create a config.json file. Add the following to the json file:

{
    "31337": {
        "ethcommerce": {
            "address": "*********************”
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Replace the address's placeholder text (****) with the contract address printed in your console.

Connecting to the front-end

Since this tutorial is focused on smart contracts, I won’t be dwelling much on the styling. You can find the stylesheet for index.css on Github.

Adding the abi (Application Binary Interface) file

When we create a smart contract using languages like Solidity, it compiles into bytecode, which is a low-level, machine-readable format. It’s hard for web applications or even other smart contracts to interact with the bytecode. The ABI serves as a bridge that enables communication between off-chain applications and on-chain smart contracts.

To add the ABI file to your front-end, navigate to artifacts/contracts/Ethcommerce.sol/Ethcommerce.json. This path is where the ABI is stored in your project. Inside the ABI file, locate the array with the key abi and copy it.

In the src directory, create another directory called abis. Inside the abis directory, create an Ethcommerce.json file. Paste the abi array into your newly created JSON file.

Adding Hardhat to Metamask

If you are working with hardhat for the first time, you need to add it as a network to your metamask in order to use the test accounts provided out of the box.

To get started, open your Metamask browser extension and click on Settings. In the list of settings, click on Networks, then click on the Add Network Button at the bottom of the popup to navigate to the networks page. Scroll to the bottom of the page and click on the Add Network Manually text to reveal a form. In the input field for Network name, type "Hardhat".

To get the RPC URL, run this command in your terminal:

npx hardhat node
Enter fullscreen mode Exit fullscreen mode

This command should print out hardhat’s RPC URL, 10 test account addresses and their private keys.

Enter the RPC URL in the provided input field, then use "31337" as the chain ID and finally, add "ETH" as the currency symbol.

Adding Hardhat's test accounts

  • Click on your account address at the top of Metamask’s extension pop- up.
  • Next, click on the + Add Account or hardware wallet button.
  • Select Import Account from the list of options that comes up.
  • Return to your terminal and copy the private key of the first account from the list of test accounts.
  • Paste the private key into the provided input field.
  • Click the import button.

Your new account should now appear in your wallet, funded with a certain amount of ETH.
Repeat the process for 2 to 3 accounts. This gives you the option to switch between accounts and test your application.

Loading products from the smart contract

You’ll be using the abi file, Ethcommerce.json and the config.json file to load the product list into the state in React.

Copy the code below into your App.js file:

import { useEffect, useState } from 'react';
import { ethers } from 'ethers';

// ABIs
import Ethcommerce from './abis/Ethcommerce.json';


// Config
import config from './config.json';


function App() {
  const [provider, setProvider] = useState(null);
  const [ethcommerce, setEthcommerce] = useState(null);

  const [electronics, setElectronics] = useState(null);
  const [clothing, setClothing] = useState(null);
  const [toys, setToys] = useState(null);
  const [account, setAccount] = useState(null);

  const loadBlockchainData = async () => {
    const provider = new ethers.BrowserProvider(window.ethereum);
    setProvider(provider);
    const network = await provider.getNetwork();

    const loadedEthcommerce = new ethers.Contract(
      config[network.chainId].ethcommerce.address,
      Ethcommerce,
      provider
    );
    console.log(await loadedEthcommerce.items(1));
    setEthcommerce(loadedEthcommerce);

    const items = [];

    for (var i = 0; i < 9; i++) {
      const item = await ethcommerce.items(i + 1);
      items.push(item);
    }
    console.log(items, loadedEthcommerce, 'hi');
    const electronics = items.filter((item) => item.category === 'electronics');
    const clothing = items.filter((item) => item.category === 'clothing');
    const toys = items.filter((item) => item.category === 'toys');

    setElectronics(electronics);
    setClothing(clothing);
    setToys(toys);
  };

  useEffect(() => {
    loadBlockchainData();
  }, []);

  return (
    <div>
      <h2>Ethcommerce Best Sellers</h2>
    </div>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

The loadBlockchainData function initializes an Ethereum provider using the BrowserProvider method. It then loads the Ethcommerce smart contract using the contract address in the config.json file and the abi file. The function goes on to fetch item data, and filter them into 3 categories.

Connecting Metamask to the front-end

In this section, you will create the Navigation component. This component will include a button that enables users to connect their wallet to the application. Let's get into it.

In the src directory, create a folder and name it components. Inside the components folder, create a Navigation.js file.

Copy the code below into the file:

import { ethers } from 'ethers';


const Navigation = ({ account, setAccount }) => {
  const connectHandler = async () => {
    try {
      const accounts = await window.ethereum.request({
        method: 'eth_requestAccounts'
      });
      const account = ethers.getAddress(accounts[0]);
      setAccount(account);

    } catch (error) {
      if (error.code === 4001) {
        console.error('User denied connection.');
      }
    }
  };

  return (
    <nav>
      <div className='nav__brand'>
        <h1>Ethcommerce</h1>
      </div>

      <input type='text' className='nav__search' />

      {account ? (
        <button type='button' className='nav__connect'>
          {account.slice(0, 6) + '...' + account.slice(38, 42)}
        </button>
      ) : (
        <button type='button' className='nav__connect' onClick={connectHandler}>
          Connect
        </button>
      )}

      <ul className='nav__links'>
        <li>
          <a href='#Clothing & Jewelry'>Clothing & Jewelry</a>
        </li>
        <li>
          <a href='#Electronics & Gadgets'>Electronics & Gadgets</a>
        </li>
        <li>
          <a href='#Toys & Gaming'>Toys & Gaming</a>
        </li>
      </ul>
    </nav>
  );
};

export default Navigation;
Enter fullscreen mode Exit fullscreen mode

The connectHandler function will attempt to connect to the user’s Ethereum wallet by requesting account access.

If successful, it retrieves the first account from the returned accounts array, converts it to an Ethereum address using ethers.getAddress(accounts[0]), and sets it as the account state variable.

If an account has already been created, a button showing part of the user's address will be displayed. If not, the connect button is displayed.

Add the Navigation component to App.js. Add the code below between the div wrappers:

      <Navigation account={account} setAccount={setAccount} />
      <h2>Dappazon Best Sellers</h2>
Enter fullscreen mode Exit fullscreen mode

Then import the Navigation component at the top of your code:

// Components
import Navigation from './components/Navigation'

Enter fullscreen mode Exit fullscreen mode

Check your page in the browser, and it should look like the image below:

Image description

Listing products

In your components directory, create a Section.js file and add the code below into the file:

import { ethers } from 'ethers'
const Section = ({ title, items, togglePop }) => {
    return (
        <div className='cards__section'>
            <h3 id={title}>{title}</h3>
            <hr />
            <div className='cards'>
                {items.map((item, index) => (
                    <div className='card' key={index} onClick={() => 
                     togglePop(item)}>
                        <div className='card__image'>
                            <img src={item.image} alt="Item" />
                        </div>
                        <div className='card__info'>
                            <h4>{item.name}</h4>                            <p>{ether.formatUnits(item.cost.toString(), 'ether')} ETH</p>
                        </div>
                    </div>
                ))}
            </div>
        </div>
    );
}

export default Section;
Enter fullscreen mode Exit fullscreen mode

This file will display the list of products for each product category. Copy the code below into App.js:

import Section from './components/Section'

Enter fullscreen mode Exit fullscreen mode

This should go right after the h2 tag.

      {electronics && clothing && toys && (
        <>
          <Section title={"Clothing & Jewelry"} items={clothing} togglePop={togglePop} />
          <Section title={"Electronics & Gadgets"} items={electronics} togglePop={togglePop} />
          <Section title={"Toys & Gaming"} items={toys} togglePop={togglePop} />
        </>
      )}

Enter fullscreen mode Exit fullscreen mode

The code above checks to see if all 3 categories have been successfully fetched from the smart contract, before displaying the sections.

Your page should look like this:

Image description

Create a toggleProp function to toggle the visibility of a specific product when clicked. So, when a product card is clicked, the product object is saved into the item state and then displayed as a modal on the page.

 const [toggle, setToggle] = useState(false)
 const [item, setItem] = useState({})

  const togglePop = (item) => {
    setItem(item)
    toggle ? setToggle(false) : setToggle(true)
  }
Enter fullscreen mode Exit fullscreen mode

Creating products modal

In your components folder, create a Product.js file. Copy the code below into the file:

import { useEffect, useState } from 'react'
import { ethers } from 'ethers'

import close from '../assets/close.svg'

const Product = ({ item, provider, account, ethcommerce, togglePop }) => {
  const [order, setOrder] = useState(null)
  const [hasBought, setHasBought] = useState(false)

  const fetchDetails = async () => {
    const events = await ethcommerce.queryFilter("Buy")
    const orders = events.filter(
      (event) => event.args.buyer === account && event.args.itemId.toString() === item.id.toString()
    )

    if (orders.length === 0) return

    const order = await ethcommerce.orders(account, orders[0].args.orderId)
    setOrder(order)
  }

  return (
    <div className="product">

    </div >
  );
}

export default Product;
Enter fullscreen mode Exit fullscreen mode

This component takes in a few props including item, which is an object that has the product details.

In the fetchDetails function, the item.id is used to check if the user has bought the product before, so the order details can be saved in the state.

Using useEffect, fetchDetails is immediately called after the component mounts.

  useEffect(() => {
    fetchDetails()
  }, [hasBought])

Enter fullscreen mode Exit fullscreen mode

close.svg is currently not in the project. Go to github to download the file, and then add it to src/assets.

Next, you’re going to create a buyHandler function. This function will access the smart contract and pass the product’s id and cost to the smart contract's buy function.

 const buyHandler = async () => {
    const signer = await provider.getSigner()

    // Buy item...
    let transaction = await ethcommerce.connect(signer).buy(item.id, { value: item.cost })
    await transaction.wait()

    setHasBought(true)
  }
Enter fullscreen mode Exit fullscreen mode

To display the product details add the code below between the div wrapper:

 <div className='product__details'>
        <div className='product__image'>
          <img src={item.image} alt='Product' />
        </div>
        <div className='product__overview'>
          <h1>{item.name}</h1>
          <hr />
          <p>{item.address}</p>

          <h2>{ethers.formatUnits(item.cost.toString(), 'ether')} ETH</h2>
          <hr />
          <h2>Overview</h2>
          <p>
            {item.description}
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Minima rem,
            iusto, consectetur inventore quod soluta quos qui assumenda aperiam,
            eveniet doloribus commodi error modi eaque! Iure repudiandae
            temporibus ex? Optio!
          </p>
        </div>
      </div>
Enter fullscreen mode Exit fullscreen mode

Just underneath the product__overview div, add the code to display the order details and the button to trigger the buyHandler.

   <div className="product__order">
          <h1>{ethers.formatUnits(item.cost.toString(), 'ether')} ETH</h1>
          <p>
            FREE delivery <br />
            <strong>
              {new Date(Date.now() + 345600000).toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' })}
            </strong>
          </p>
          {item.stock > 0 ? (
            <p>In Stock.</p>
          ) : (
            <p>Out of Stock.</p>
          )}
          <button className='product__buy' onClick={buyHandler}>
            Buy Now
          </button>
          <p><small>Ships from</small> Ethcommerce</p>
          <p><small>Sold by</small> Ethcommerce</p>
          {order && (
            <div className='product__bought'>
              Item bought on <br />
              <strong>
                {new Date(Number(order.time.toString() + '000')).toLocaleDateString(
                  undefined,
                  {
                    weekday: 'long',
                    hour: 'numeric',
                    minute: 'numeric',
                    second: 'numeric'
                  })}
              </strong>
            </div>
          )}
        </div>
        <button onClick={togglePop} className="product__close">
          <img src={close} alt="Close" />
        </button>

Enter fullscreen mode Exit fullscreen mode

Navigate to App.js and add Product to the jsx.

import Product from './components/Product'
Enter fullscreen mode Exit fullscreen mode

The toggle state determines the visibility of the Product modal, so when a product card is clicked, the toggle is set to true.

      {toggle && (
        <Product item={item} provider={provider} account={account} ethcommerce={ethcommerce} togglePop={togglePop} />
      )}
Enter fullscreen mode Exit fullscreen mode

And with that, we have come to the end of this tutorial.

Conclusion

Whew! That was quite a ride. In this tutorial, we explored the true meaning of decentralization for a Web3 e-commerce application and emphasized the importance of building a seamless and fast platform to promote global Web3 adoption. We walked through using Hardhat and React.js to create an application with basic e-commerce functionality. You now have an overview of what it takes to build e-commerce solutions using Web3 technology. However, there's still more ground to cover, including adding email notifications, chat support, and logistics to this application. Feel free to experiment and incorporate these features into the platform. Have fun!

Top comments (1)

Collapse
 
obinna_robin_b3be1a80a40a profile image
Obinna Robin

Thanks! Bookmarked👌