In the Cosmetics industry, ensuring transparency and trust is of utmost importance. However, traditional supply chains often face significant challenges such as counterfeiting and a lack of transparency.
In this tutorial, I’ll delve into the technical aspects of addressing these challenges by leveraging Solidity smart contracts on the Celo blockchain. By developing secure and transparent skincare product contracts, experience in building a decentralized ecosystem that enables seamless buying and selling of cosmetic products with an additional option for refunds.
Technology Stack:
To develop this full stack application, we will utilize the following technologies:
Smart Contract Development:
-
Solidity: We will use Solidity, a high-level programming language, to write the smart contracts that define the logic and behavior of the skincare product contracts. Solidity is specifically designed for creating decentralized applications (dApps) on the Ethereum Virtual Machine (EVM).
-
Remix: Remix is a web-based development environment that allows developers to write, test, and deploy smart contracts. We will use Remix to compile and deploy the skincare product contracts onto the Celo blockchain.
Front-End Development:
- React: React is a popular JavaScript library for building user interfaces. We will use React to develop the front-end of our application, allowing users to interact with the skincare product contracts and perform actions such as buying products and requesting refunds.
Prerequisites
To follow along with this tutorial, you should have a basic understanding of Solidity, smart contracts and React.
Also a basic understanding of web development, which should comprise of Javascript and React.
You should also have an environment set up to deploy and interact with:
-
smart contracts, such as Remix
-
Node.js and npm installed on your machine
-
A Visual Studio Code
SmartContract
Let’s get started writing out our smart contract in Remix IDE
This is the complete code.
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
interface IERC20Token {
function transfer(address, uint256) external returns (bool);
function approve(address, uint256) external returns (bool);
function transferFrom(
address,
address,
uint256
) external returns (bool);
function totalSupply() external view returns (uint256);
function balanceOf(address) external view returns (uint256);
function allowance(address, address) external view returns (uint256);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(
address indexed owner,
address indexed spender,
uint256 value
);
}
contract SkincareProduct {
uint internal productsLength = 0;
address internal cUsdTokenAddress = 0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;
struct Product {
address payable owner;
string brand;
string image;
string category;
string deliveredWithin;
uint numberOfStock;
uint amount;
uint sales;
}
struct ProductRating {
mapping(address => uint8) ratings;
}
struct ProductRefund {
mapping(address => bool) refunds;
}
mapping (uint => Product) private products;
mapping (uint => ProductRating) private productRatings;
mapping (uint => ProductRefund) private productRefunds;
event ProductOrdered (
address _from,
uint productId
);
event ProductReviewed (
uint productId,
uint8 rating
);
event ProductRefunded (
uint productId,
address refundAddress
);
function addProduct(
string calldata _brand,
string calldata _image,
string calldata _category,
string calldata _deliveredWithin,
uint _numberOfStock,
uint _amount
) public {
require(bytes(_brand).length > 0, "Empty brand");
require(bytes(_image).length > 0, "Empty image");
require(bytes(_category).length > 0, "Empty category");
require(bytes(_deliveredWithin).length > 0, "Empty delivery date");
require(_numberOfStock > 0, "Please enter a valid number of stock ");
require(_amount > 0, "Please enter a valid amount");
products[productsLength] = Product(
payable(msg.sender),
_brand,
_image,
_category,
_deliveredWithin,
_numberOfStock,
_amount,
0
);
productsLength++;
}
function getProduct(uint _index) public view returns (
address payable,
string memory,
string memory,
string memory,
string memory,
uint,
uint,
uint
) {
Product storage p = products[_index];
return (
p.owner,
p.brand,
p.image,
p.category,
p.deliveredWithin,
p.numberOfStock,
p.amount,
p.sales
);
}
function orderProduct(uint _index) public payable {
Product storage currentProduct = products[_index];
require(currentProduct.numberOfStock > 0, "Not enough products in stock to fulfill this order");
require(currentProduct.owner != msg.sender, "You can't purchase your own products");
currentProduct.numberOfStock--;
currentProduct.sales++;
require(
IERC20Token(cUsdTokenAddress).transferFrom(
msg.sender,
currentProduct.owner,
currentProduct.amount
),
"Transfer failed."
);
emit ProductOrdered(msg.sender, _index);
}
function reviewProduct(uint _index, uint8 _rating) public {
require(_rating >= 0 && _rating <= 5, "Invalid rating value");
ProductRating storage rating = productRatings[_index];
require(rating.ratings[msg.sender] == 0, "You have already reviewed this product");
rating.ratings[msg.sender] = _rating;
emit ProductReviewed(_index, _rating);
}
function refundProduct(uint _index) public {
ProductRefund storage refund = productRefunds[_index];
require(!refund.refunds[msg.sender], "You have already refunded this product");
refund.refunds[msg.sender] = true;
emit ProductRefunded(_index, msg.sender);
}
function getProductRating(uint _index, address _user) public view returns (uint8) {
ProductRating storage rating = productRatings[_index];
return rating.ratings[_user];
}
function hasRefunded(uint _index, address _user) public view returns (bool) {
ProductRefund storage refund = productRefunds[_index];
return refund.refunds[_user];
}
function getProductLength() public view returns (uint) {
return productsLength;
}
function refundBuyer(uint _index, address _buyer) public {
require(productsLength > _index, "Invalid product index");
Product storage product = products[_index];
require(msg.sender == product.owner, "Only the owner can initiate a refund");
require(productRefunds[_index].refunds[_buyer], "Buyer has not requested a refund");
product.numberOfStock++;
product.sales--;
require(
IERC20Token(cUsdTokenAddress).transferFrom(
product.owner,
_buyer,
product.amount
),
"Refund failed."
);
delete productRefunds[_index].refunds[_buyer];
}
}
The contract
declaration defines the SkincareProduct contract. It encapsulates all the variables, mappings, functions, and events defined within it.
Contract Overview
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
interface IERC20Token {
}
contract SkincareProduct {
}
Explanation
- We begin by defining the SPDX license identifier and specifying the Solidity compiler version.
- The
IERC20Token
interface represents the standard ERC20 token interface. It defines the functions required to interact with ERC20 tokens. - The
SkincareProduct
contract is our main contract that implements the skincare product marketplace. It contains variables, mappings, events, and functions related to managing products, orders, reviews, and refunds.
Contract Variables and Mappings
Next, let’s explore the variables and mappings used within the contract.
contract SkincareProduct {
uint internal productsLength = 0;
address internal cUsdTokenAddress = 0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;
struct Product {
}
struct ProductRating {
}
struct ProductRefund {
}
mapping (uint => Product) private products;
mapping (uint => ProductRating) private productRatings;
mapping (uint => ProductRefund) private productRefunds;
}
Explanation
productsLength
is an internal counter that keeps track of the number of products added to the marketplace.cUsdTokenAddress
is the Ethereum address of an ERC20 token called “cUSD,” which will be used for payment transactions within the marketplace.- The
Product
struct represents a skincare product and stores its details such as owner, brand, image, category, delivery time, stock count, price, and sales. - The
ProductRating
struct is used to store ratings given by users for each product. - The
ProductRefund
struct keeps track of product refund requests made by users. - The
products
,productRatings
, andproductRefunds
mappings associate a unique product index with its corresponding details, ratings, and refund information, respectively.
Contract Events
Now, let’s define the events emitted by the contract.
contract SkincareProduct {
// ...
event ProductOrdered (
address _from,
uint productId
);
event ProductReviewed (
uint productId,
uint8 rating
);
event ProductRefunded (
uint productId,
address refundAddress
);
}
Explanation
- The
ProductOrdered
event is emitted when a user successfully places an order for a skincare product. It contains the address of the user and the product ID. - The
ProductReviewed
event is emitted when a user submits a review for a purchased product. It includes the product ID and the rating given by the user. - The
ProductRefunded
event is emitted when a user requests a refund for a purchased product. It includes the product ID and the address of the user who made the refund request.
Adding Products
In this step, we’ll implement the function to add skincare products to the marketplace.
contract SkincareProduct {
// ...
function addProduct(
string calldata _brand,
string calldata _image,
string calldata _category,
string calldata _deliveredWithin,
uint _numberOfStock,
uint _amount
) public {
}
}
Explanation
- The
addProduct
function allows a user to add a new skincare product to the marketplace. - The function takes various parameters such as brand, image URL, category, delivery time, stock count, and price.
- Before adding the product, the function performs input validation to ensure all required information is provided.
- If the validation passes, the function creates a new
Product
struct with the provided details and adds it to theproducts
mapping.
Retrieving Product Details
Let’s implement a function to retrieve details of a specific product.
contract SkincareProduct {
// ...
function getProduct(uint _index) public view returns (
address payable,
string memory,
string memory,
string memory,
string memory,
uint,
uint,
uint
) {
}
// ...
}
Explanation
- The
getProduct
function takes an index parameter and returns the details of the product at that index. - It retrieves the corresponding
Product
struct from theproducts
mapping and returns its owner, brand, image URL, category, delivery time, stock count, price, and sales information.
Ordering a Product
Let’s implement the function for users to order a skincare product from the marketplace.
contract SkincareProduct {
// ...
function orderProduct(uint _index) public payable {
}
}
Explanation
- The
orderProduct
function allows a user to order a skincare product by specifying the product index. - Before placing the order, the function performs various validations and checks, such as ensuring sufficient stock and preventing users from purchasing their own products.
- If the checks pass, the function decreases the stock count of the product, transfers the required funds from the buyer to the seller (using the
transferFrom
function of thecUSD
token), and emits theProductOrdered
event.
Product Reviews and Refunds
We’ll now implement functions for users to review purchased products and request refunds.
contract SkincareProduct {
// ...
function reviewProduct(uint _index, uint8 _rating) public {
}
function refundProduct(uint _index) public {
}
// ...
}
Explanation
- The
reviewProduct
function allows a user to submit a review for a purchased product. It takes the product index and the rating as parameters, performs validations, adds the rating to theproductRatings
mapping, and emits theProductReviewed
event. - The
refundProduct
function enables users to request refunds for purchased products. It takes the product index as a parameter, performs validations, marks the product as refunded in theproductRefunds
mapping, and emits theProductRefunded
event.
Additional Helper Functions
Let’s implement some additional helper functions to retrieve product ratings and refund statuses.
contract SkincareProduct {
}
function hasRefunded(uint _index, address _user) public view returns (bool) {
}
function getProductLength() public view returns (uint) {
}
// ...
}
Explanation
- The
getProductRating
function retrieves the rating given by a specific user for a particular product. - The
hasRefunded
function checks whether a specific user has requested a refund for a product. - The
getProductLength
function returns the total number of products available in the marketplace.
Refund Feature
The refund feature allows the vendor to refund a product to the buyer. When a refund is initiated, the product is returned to the vendor’s inventory, and the buyer is reimbursed with the appropriate amount of cUSD tokens.
function refundProduct(uint256 _index) public {
require(_index < products.length, "Invalid product index");
Product storage product = products[_index];
require(product.owner == msg.sender, "You are not the owner of this product");
require(product.refunds > 0, "There are no refunds available for this product");
product.numberOfStock += 1;
product.refunds -= 1;
uint256 refundAmount = product.amount;
require(cUSDTContract.transfer(msg.sender, refundAmount), "Failed to transfer cUSD tokens");
emit ProductRefunded(_index, msg.sender, refundAmount);
}
Explanation
In this function:
- We check if the provided product index is valid and if the caller is the owner of the product.
- We ensure that there are available refunds for the product.
- If the conditions are met, we increase the stock of the product and decrease the number of refunds.
- The refund amount is calculated based on the original product amount.
- We transfer the refund amount in cUSD tokens to the buyer’s address using the
transfer
function of the cUSD token contract. - Finally, we emit an event
ProductRefunded
to notify the frontend or other parties about the product refund.
Frontend
App.js
Let’s get started writing out our React Code in VS Code
This is the complete code.
import "./App.css";
import { useState, useEffect, useCallback } from "react";
import Web3 from "web3";
import { newKitFromWeb3 } from "@celo/contractkit";
import BigNumber from "bignumber.js";
import SKINCARE from "./contracts/Skincare.abi.json";
import IERC from "./contracts/IERC.abi.json";
import ProductCard from "./components/productCard";
import Carousel from "./components/carousel";
import Form from "./components/form";
const ERC20_DECIMALS = 18;
const contractAddress = "0xC628cAd55cD31650014259C7B811A6B2483a8De6";
const cUSDContractAddress = "0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1";
function App() {
const [Loading, setLoading] = useState(false);
const [contract, setContract] = useState(null);
const [address, setAddress] = useState(null);
const [kit, setKit] = useState(null);
const [cUSDBalance, setcUSDBalance] = useState(0);
const [products, setProducts] = useState([]);
const [productLoading, setProductsLoading] = useState(true);
const [tab, setTab] = useState("1");
const connectToWallet = async () => {
setLoading(true);
if (window.celo) {
try {
await window.celo.enable();
const web3 = new Web3(window.celo);
let kit = newKitFromWeb3(web3);
const accounts = await kit.web3.eth.getAccounts();
const user_address = accounts[0];
kit.defaultAccount = user_address;
setAddress(user_address);
setKit(kit);
} catch (error) {
setLoading(false);
console.log(error);
}
} else {
setLoading(false);
alert("Error Occurred");
}
};
const getBalance = useCallback(async () => {
try {
const balance = await kit.getTotalBalance(address);
const USDBalance = balance.cUSD.shiftedBy(-ERC20_DECIMALS).toFixed(2);
const contract = new kit.web3.eth.Contract(SKINCARE, contractAddress);
setContract(contract);
setcUSDBalance(USDBalance);
setLoading(false);
} catch (error) {
console.log(error);
}
}, [address, kit]);
useEffect(() => {
connectToWallet();
}, []);
useEffect(() => {
if (kit && address) {
getBalance();
}
}, [kit, address, getBalance]);
const getProducts = useCallback(async () => {
const productsLength = await contract.methods.getProductLength().call();
console.log(productsLength);
const products = [];
for (let index = 0; index < productsLength; index++) {
let _products = new Promise(async (resolve, reject) => {
let product = await contract.methods.getProduct(index).call();
let rating = await contract.methods.getProductRating(index).call();
let refunds = await contract.methods.getProductRefunds(index).call();
resolve({
index: index,
owner: product[0],
brand: product[1],
image: product[2],
category: product[3],
deliveredWithin: product[4],
numberOfStock: product[5],
amount: product[6],
sales: product[7],
rating: rating,
refunds: refunds,
});
});
products.push(_products);
}
const _products = await Promise.all(products);
console.log(_products);
setProducts(_products);
setProductsLoading(false);
}, [contract]);
useEffect(() => {
if (contract) {
getProducts();
}
}, [contract, getProducts]);
const orderProduct = async (_index) => {
const cUSDContract = new kit.web3.eth.Contract(IERC, cUSDContractAddress);
try {
await cUSDContract.methods
.approve(contractAddress, products[_index].amount)
.send({ from: address });
await contract.methods.orderProduct(_index).send({ from: address });
getProducts();
getBalance();
alert(
`You have successfully ordered ${products[_index].brand}. The product will be delivered in ${products[_index].deliveredWithin}.`
);
} catch (error) {
alert(error);
}
};
const refundProduct = async (_index) => {
try {
await contract.methods.refundProduct(_index).send({ from: address });
getProducts();
getBalance();
alert(`You have successfully refunded the product.`);
} catch (error) {
alert(error);
}
};
const addProduct = async (
_brand,
_image,
_category,
_deliveredWithin,
_numberOfStock,
_amount
) => {
try {
console.log(
_brand,
_image,
_category,
_deliveredWithin,
_numberOfStock,
_amount
);
let amount = new BigNumber(_amount)
.shiftedBy(ERC20_DECIMALS)
.toString();
await contract.methods
.addProduct(
_brand,
_image,
_category,
_deliveredWithin,
_numberOfStock,
amount
)
.send({ from: address });
getProducts();
} catch (error) {
alert(error);
}
};
return (
<>
<nav className="navbar navbar-expand-sm navbar-dark space">
<div className="container">
<a className="navbar-brand">
SKIN<span className="logo">CARE</span> STORE
</a>
<button
className="navbar-toggler d-lg-none"
type="button"
data-bs-toggle="collapse"
data-bs-target="#collapsibleNavId"
aria-controls="collapsibleNavId"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse" id="collapsibleNavId">
<ul className="navbar-nav me-auto mt-2 mt-lg-0 w-100">
<li className="nav-item">
<a className="nav-link active" href="#" aria-current="page">
Home <span className="visually-hidden">(current)</span>
</a>
</li>
<li className="nav-item">
<a className="nav-link" href="#product-form">
Add Product
</a>
</li>
<li className="nav-item ms-auto">
{contract !== null && cUSDBalance !== null ? (
<div className="mt-1 text-white">
<b>{cUSDBalance} cUSD</b>
</div>
) : (
<button
className="btn btn-secondary"
onClick={() => connectToWallet()}
disabled={Loading}
>
{Loading ? "Connecting..." : "Connect Wallet"}
</button>
)}
</li>
</ul>
</div>
</div>
</nav>
<div className="container py-4">
<div className="row">
<div className="col-md-12">
<h1 className="text-center">Skin Care Store</h1>
</div>
<div className="col-md-12">
<div className="tabs">
<ul className="nav nav-pills nav-fill">
<li className="nav-item">
<a
className={`nav-link ${tab === "1" ? "active" : ""}`}
onClick={() => setTab("1")}
>
All Products
</a>
</li>
<li className="nav-item">
<a
className={`nav-link ${tab === "2" ? "active" : ""}`}
onClick={() => setTab("2")}
>
My Products
</a>
</li>
</ul>
</div>
</div>
<div className="col-md-12">
<div className="content">
{tab === "1" ? (
<>
<h2>All Products</h2>
{productLoading ? (
<p>Loading products...</p>
) : (
<>
<div className="row">
{products.map((product) => (
<div className="col-md-3" key={product.index}>
<ProductCard
product={product}
orderProduct={orderProduct}
refundProduct={refundProduct}
/>
</div>
))}
</div>
{products.length === 0 && (
<p>No products available.</p>
)}
</>
)}
</>
) : (
<>
<h2>My Products</h2>
{productLoading ? (
<p>Loading products...</p>
) : (
<>
<div className="row">
{products
.filter((product) => product.owner === address)
.map((product) => (
<div className="col-md-3" key={product.index}>
<ProductCard
product={product}
refundProduct={refundProduct}
/>
</div>
))}
</div>
{products.filter((product) => product.owner === address).length ===
0 && <p>No products available.</p>}
</>
)}
</>
)}
</div>
</div>
</div>
</div>
<div id="product-form">
<Form addProduct={addProduct} />
</div>
<Carousel />
</>
);
}
export default App;
Let’s go through the code step by step and explain its functionality.
Setting up the Environment
First, import the necessary dependencies and styles for the application.
import "./App.css";
import { useState, useEffect, useCallback } from "react";
import Web3 from "web3";
import { newKitFromWeb3 } from "@celo/contractkit";
import BigNumber from "bignumber.js";
import SKINCARE from "./contracts/Skincare.abi.json";
import IERC from "./contracts/IERC.abi.json";
import ProductCard from "./components/productCard";
import Carousel from "./components/carousel";
import Form from "./components/form";
Explanation
- The code imports required dependencies such as React, Web3, Celo ContractKit, BigNumber, and JSON files containing the ABIs (Application Binary Interface) of the smart contracts.
- It also imports custom components for displaying product cards, carousels, and a form
Initializing State Variables
Next, initialize the state variables that will store data in the application.
const ERC20_DECIMALS = 18;
const contractAddress = "0xC628cAd55cD31650014259C7B811A6B2483a8De6";
const cUSDContractAddress = "0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1";
function App() {
const [Loading, setLoading] = useState(false);
const [contract, setContract] = useState(null);
const [address, setAddress] = useState(null);
const [kit, setKit] = useState(null);
const [cUSDBalance, setcUSDBalance] = useState(0);
const [products, setProducts] = useState([]);
const [productLoading, setProductsLoading] = useState(true);
const [tab, setTab] = useState("1");
// ...
}
Explanation
- The
ERC20_DECIMALS
constant represents the decimal places for the cUSD token, which is 18 in this case. - The
contractAddress
variable holds the address of the Skincare smart contract deployed on the Celo blockchain. - The
cUSDContractAddress
variable contains the address of the cUSD token contract on the Celo blockchain. - Several state variables are initialized using the
useState
hook, includingLoading
to track loading status,contract
to store the instance of the Skincare contract,address
to hold the user’s wallet address,kit
to store the instance of the Celo ContractKit,cUSDBalance
to track the user’s cUSD balance,products
to store the skincare products fetched from the contract,productLoading
to track the loading status of products, andtab
to keep track of the currently active tab in the UI.
Connecting to the Wallet
Implement the connectToWallet
function to connect to the user’s Celo wallet.
const connectToWallet = async () => {
setLoading(true);
if (window.celo) {
try {
await window.celo.enable();
const web3 = new Web3(window.celo);
let kit = newKitFromWeb3(web3);
const accounts = await kit.web3.eth.getAccounts();
const user_address = accounts[0];
kit.defaultAccount = user_address;
setAddress(user_address);
setKit(kit);
} catch (error) {
setLoading(false);
console.log(error);
}
} else {
setLoading(false);
alert("Error Occurred");
}
};
Explanation
- The
connectToWallet
function is an asynchronous function that handles connecting to the user’s Celo wallet. - When the function is called, it first enables access to the Celo wallet using
window.celo.enable()
. - It then creates a new instance of Web3 using the Celo provider
(window.celo)
and creates a new Celo ContractKit instance from the Web3 instance. - The user’s wallet address is retrieved using
kit.web3.eth.getAccounts()
, and the default account in the ContractKit instance is set to the user’s address. - Finally, the user’s address and the ContractKit instance are set in the state variables
address
andkit
, respectively.
Retrieving User’s Balance
Implement the getBalance
function to fetch the user’s cUSD balance.
const getBalance = useCallback(async () => {
try {
const balance = await kit.getTotalBalance(address);
const USDBalance = balance.cUSD.shiftedBy(-ERC20_DECIMALS).toFixed(2);
const contract = new kit.web3.eth.Contract(SKINCARE, contractAddress);
setContract(contract);
setcUSDBalance(USDBalance);
setLoading(false);
} catch (error) {
console.log(error);
}
}, [address, kit]);
Explanation
- The
getBalance
function is defined as an asynchronous function wrapped in theuseCallback
hook to memoize its reference. - Inside the function, the
kit.getTotalBalance
method is used to fetch the total balance of the user’s wallet address, including the cUSD token. - The cUSD balance is then converted from the big number representation to a human-readable format with the decimal places shifted by
-ERC20_DECIMALS
and fixed to 2 decimal places. - A new instance of the Skincare contract is created using
new kit.web3.eth.Contract(SKINCARE, contractAddress)
. - The contract instance, cUSD balance, and loading status are updated in the state variables
contract
,cUSDBalance
, andLoading
, respectively.
Connect to Wallet and Get Balance on Component Mount
Initialize the connection to the wallet and fetch the user’s balance when the component mounts.
useEffect(() => {
connectToWallet();
}, []);
useEffect(() => {
if (kit && address) {
getBalance();
}
}, [kit, address, getBalance]);
Explanation
- The
useEffect
hook with an empty dependency array[]
is used to call theconnectToWallet
function once when the component mounts. - Another
useEffect
hook is used to fetch the user’s balance whenever thekit
,address
, orgetBalance
functions change. This ensures that the balance is updated if the wallet or address changes.
Fetching Products from the Skincare Contract
Implement the getProducts
function to retrieve skincare products from the Skincare contract.
const getProducts = useCallback(async () => {
const productsLength = await contract.methods.getProductLength().call();
console.log(productsLength);
const products = [];
for (let index = 0; index < productsLength; index++) {
let _products = new Promise(async (resolve, reject) => {
let product = await contract.methods.getProduct(index).call();
let rating = await contract.methods.getProductRating(index).call();
let refunds = await contract.methods.getProductRefunds(index).call();
resolve({
index: index,
owner: product[0],
brand: product[1],
image: product[2],
category: product[3],
deliveredWithin: product[4],
numberOfStock: product[5],
amount: product[6],
sales: product[7],
rating: rating,
refunds: refunds,
});
});
products.push(_products);
}
const _products = await Promise.all(products);
console.log(_products);
setProducts(_products);
setProductsLoading(false);
}, [contract]);
Explanation
- The
getProducts
function is defined as an asynchronous function wrapped in theuseCallback
hook to memoize its reference. - Inside the function,
productsLength
is obtained by calling thegetProductLength
method of the Skincare contract. - An empty array
products
is initialized to store the individual product promises. - A loop is executed from
index = 0
toindex < productsLength
to fetch each product’s details. - For each index, a new promise
_products
is created to asynchronously fetch the product, rating, and refunds details usingcontract.methods.getProduct(index).call()
,contract.methods.getProductRating(index).call()
, andcontract.methods.getProductRefunds(index).call()
. - The promise resolves to an object containing the product details, including the index, owner, brand, image, category, deliveredWithin, numberOfStock, amount, sales, rating, and refunds.
- The resolved promises are pushed into the
products
array. - The array of promises is then resolved using
Promise.all
to obtain an array of product objects_products
. - The
_products
array is logged to the console and set in the state variableproducts
. - The
productsLoading
state variable is set tofalse
to indicate that the products have finished loading.
Fetch Products on Contract Initialization
Use the useEffect
hook to fetch the products from the Skincare contract when the contract
variable is set.
useEffect(() => {
if (contract) {
getProducts();
}
}, [contract, getProducts]);
Explanation
- The
useEffect
hook is used to fetch the products whenever thecontract
orgetProducts
function changes. - When the
contract
variable is set, thegetProducts
function is called to retrieve the skincare products from the Skincare contract.
Order Product
Implement the orderProduct
function to handle ordering a product from the Skincare contract.
const orderProduct = async (_index) => {
const cUSDContract = new kit.web3.eth.Contract(IERC, cUSDContractAddress);
try {
await cUSDContract.methods
.approve(contractAddress, products[_index].amount)
.send({ from: address });
await contract.methods.orderProduct(_index).send({ from: address });
getProducts();
getBalance();
alert(
`You have successfully ordered ${
products[_index].brand
}. The product will be delivered in ${
products[_index].deliveredWithin
}.`
);
} catch (error) {
alert(error);
}
};
Explanation
- The
orderProduct
function takes an index_index
as a parameter to specify the product to order. - A new instance of the cUSD contract is created using
new kit.web3.eth.Contract(IERC, cUSDContractAddress)
. - The
cUSDContract
instance is used to approve the transfer of the product amount from the user’s wallet to the Skincare contract using theapprove
method. - The
approve
method is called with thecontractAddress
and the product amount (products[_index].amount
). - The transaction is sent using
.send({ from: address })
, specifying the user’s wallet address. - Once the approval is successful, the
contract.methods.orderProduct(_index).send({ from: address })
method is called to order the product from the Skincare contract. - After the order is completed, the
getProducts
andgetBalance
functions are called to update the product list and the user’s balance. - Finally, an alert is shown to notify the user about the successful order and provide information about the product and delivery time.
Render UI Components
Render the UI components to display the products, user’s balance, and a form for ordering a product.
return (
<div className="App">
<header className="App-header">
<h1>Skincare Shop</h1>
{Loading ? (
<p>Loading...</p>
) : (
<>
<p>Connected Account: {address}</p>
<p>Balance: {cUSDBalance} cUSD</p>
</>
)}
<div className="tabs">
<button className={tab === "1" ? "active" : ""} onClick={() => setTab("1")}>
Products
</button>
<button className={tab === "2" ? "active" : ""} onClick={() => setTab("2")}>
Order
</button>
</div>
{tab === "1" ? (
<>
{productLoading ? (
<p>Loading products...</p>
) : (
<Carousel>
{products.map((product, index) => (
<ProductCard
key={index}
index={product.index}
brand={product.brand}
image={product.image}
category={product.category}
deliveredWithin={product.deliveredWithin}
numberOfStock={product.numberOfStock}
amount={product.amount}
sales={product.sales}
rating={product.rating}
refunds={product.refunds}
orderProduct={orderProduct}
/>
))}
</Carousel>
)}
</>
) : (
<Form orderProduct={orderProduct} />
)}
</header>
</div>
);
Explanation
- The UI components are rendered inside the
return
statement of theApp
component. - The
header
section contains the title, connected account address, and cUSD balance. IfLoading
istrue
, a loading message is displayed; otherwise, the account address and balance are shown. - The tab buttons are rendered as part of the
tabs
div. The active tab is highlighted based on thetab
state variable. Clicking on a tab updates thetab
state. - The content inside each tab is conditionally rendered based on the
tab
state variable. Iftab
is"1"
, the products carousel is rendered. Iftab
is"2"
, the order form is rendered. - Inside the products carousel, the
ProductCard
component is mapped over theproducts
array to display each product’s details. - The
Form
component is rendered when the order tab is active, and theorderProduct
function is passed as a prop to handle the ordering process.
Refund Users
This function is responsible for initiating the refund process when invoked.
const refundProduct = async (_index) => {
try {
await contract.methods.refundProduct(_index).send({ from: address });
getProducts();
getBalance();
alert(`You have successfully refunded the product.`);
} catch (error) {
alert(error);
}
};
Explanation
- The
refundProduct
function is an asynchronous function that takes the_index
parameter, representing the index of the product to be refunded. - Inside the function, we use a try-catch block to handle any potential errors that may occur during the refund process.
- We call the
refundProduct
function of the smart contract by usingcontract.methods.refundProduct(_index).send({ from: address })
. This sends a transaction to the smart contract, requesting the refund of the product at the specified index. - After successfully initiating the refund transaction, we call the
getProducts
andgetBalance
functions. These functions are responsible for fetching the updated list of products and the current balance after the refund. - Finally, we display an alert message to notify the user that the refund process was successful.
By updating the React frontend with the refundProduct function, you enable vendors to initiate refunds for products they own directly from the user interface.
Deployment
To successfully deploy our smart contract on the Celo network, follow these steps:
- Download the Celo Extension Wallet from the Chrome Web Store. You can find it here here.
- After installing the wallet extension, fund your newly created wallet. You can use the Celo Alfojares faucet, which can be accessed here. This will provide you with Celo testnet tokens for development purposes Here.
- Once the wallet is funded, locate the plugin logo at the bottom left corner of your browser and search for the Celo plugin.
- Install the plugin and you will see the Celo logo appear in the side tab after installation.
- Connect your Celo wallet by clicking on the Celo logo in the side tab.
- Select the contract you wish to deploy.
- Finally, click on the “Deploy” button to initiate the deployment of your smart contract to the Celo network.
By following these steps, you will be able to deploy your smart contract on the Celo network using the Celo Extension Wallet.
Conclusion
In this tutorial, we’ve built a decentralized skincare product marketplace on Ethereum using Solidity and for the Frontend using React. We covered key functions such as adding products, ordering products, reviewing them, and requesting refunds. By understanding this code, you now have the foundation to expand upon this marketplace and add more advanced features to meet your specific requirements.
I hope you learned a lot from this tutorial. Here are some relevant links that would aid your learning further.
- Solidity Docs here
- Celo Docs here
- Remix IDE here
- React Components here
- You can find the source code here
The Author
Victor Ubah is a skilled Solidity developer specializing in blockchain technology, particularly in Celo. I have extensive experience in crafting secure and efficient smart contracts for decentralized applications (dApps). With a passion for blockchain development, I strive to create innovative solutions on the Celo platform.