How to build a simple E-commerce web app using Alby's Lightning tools

Let your users seamlessly scan to pay for your products

How to build a simple E-commerce web app using Alby's Lightning tools

TL;DR

This project tutorial is open-source on Github and you can easily fork, customize and test as much as you like. I would appreciate a star on the Github project if you found it helpful.

Live Demo: https://bitcoin-shop.netlify.app

Frontend Repo: https://github.com/Toheeb-Ojuolape/bitcoin-shop

Backend Repo: https://github.com/Toheeb-Ojuolape/bitcommerce-api

Introduction

In this tutorial article, I will be sharing a step-by-step guide on how to build a simple e-commerce website that uses Alby's lightning tools to enable customers to make payments and sends the payment to the merchant's Alby wallet immediately.

Here's a demo of what we will be building 👇🏾:

The tech stack:

The frontend of this project is built entirely with React using Vite and the API is built with Nodejs (Typescript).

Our app will have a single view on the Home page which will hold the logic for the web app and the ui for the different products. We will also be using a simple state management dependency called Zustand to persist our state across different components, especially when adding a product to the cart.

Now, let's start hacking ✌🏼!

Backend

The backend is where we would implement the integration with Alby's js-lightning tools to generate an invoice and listen for invoice payments.

First, we will set up a Nodejs project in a folder you can call bitcoin-shop-api (or anything you like, really), then run:

npm init -y

Next, we install all the dependencies needed for the project:

npm i @getalby/lightning-tools cors dotenv express socket.io ts-node

And then dev dependencies for Typescript:

npm i @types/express @types/node typescript

Here's what each dependency does:

  1. @getalby/lightning-tools: to manage our lightning integrations and enable us to generate and listen to invoice payments.

  2. cors: to allow Cross-Origin Resource Sharing for our API.

  3. dotenv: to enable us to use environment variables in our project

  4. express: to setup an express server.

  5. socket.io: to setup real-time connection from the backend to listen to when payment has been settled and update the frontend

  6. ts-node/typescript: to run Typescript

For this tutorial, our API will have only two endpoints:

  1. GET '/products' - to fetch all the products for our project. For simplicity, we would store these products in a JSON file rather than fetching them from a database.

  2. POST '/invoice' - to generate an invoice for the purchase transaction based on the amount.

We would also be creating a helper function called listenInvoice which would enable us setup a socket connection to the frontend to push an update when the invoice has been settled.

In your App.ts, enter the code below:

require("dotenv").config()
const express = require("express");
import { Request,Response } from "express";
import router from "./src/routes/routes";
const cors = require("cors");

const port = process.env.PORT || 5001;

const app = express();

app.use(
  cors({
    origin: process.env.PROJECT_URL,
  })
);

app.use(express.json());

const server = app.listen(port, () => {
  console.log(`App listening on PORT: ${port}`);
});


app.get("/",(req:Request,res:Response)=>{
  res.send(`<h2>Bitcommerce API</h2>`)
})

const io = require("socket.io")(server, {
  cors: {
    origin: [process.env.PROJECT_URL],
  },
});


app.use(router);

export { io };

Explanation: This code helps us setup an Express server with CORS handled and routes for our different API endpoints. We also setup socket here and export it for use in our listenInvoice.ts file

/src/routes/routes.ts

import { Router } from "express";
import { fetchProducts } from "../controllers/fetchProducts";
import { generateInvoice } from "../controllers/generateInvoice";
const router = Router()


router.get("/products",fetchProducts)
router.post("/invoice",generateInvoice)


export default router

Explanation: This sets up our two API endpoints as discussed earlier and links them to their respective controllers.

For the fetchProducts controller, we have these simple lines of code that fetches and serves the data in our JSON file (you can view the sample JSON file in the codebase here) :

/src/controllers/fetchProducts.ts

import { Request, Response } from "express";
import { Product } from "../interfaces/ProductInterface";


const data:Array<Product> = require("../../data.json");

export const fetchProducts = (req: Request, res: Response) => {
  try {
    res.status(200).json({
      data: data,
    });
  } catch (error) {
    res.status(400).json({
        error:"Error encountered while fetching product data"
    })
  }
};

/src/interfaces/ProductInterfaces.tsx

export interface Product {
  id: number;
  title: string;
  description: string;
  price: number;
  discountPercentage: number;
  rating: number;
  stock: number;
  brand: string;
  category: string;
  thumbnail: string;
  images: Array<string>
}

The generateInvoice controller is where we use Alby's js-lightning tools to generate an invoice on behalf of the merchant using the merchant's lightning address (e.g. mine is ) which must be defined as an environment variable.

The buyer's name, email, address and id of the selected products would be sent as a request from the frontend. We would be storing the user data and product order information passed by the user as a comment and payerdata in our invoice:

The user's purchase request would have an interface as below:

/src/interfaces/PurchaseRequest.tsx

export interface PurchaseRequest{
    name:string,
    email:string,
    address:string,
    products:Array<number>
}

We would be calculating the total amount to be paid by the user using a sumAmount function which checks our product lists using the id of each product and sums the price as below:

/src/utils/sumAmount.tsx

import { Product } from "../interfaces/ProductInterface";
const data: Array<Product> = require("../../data.json");

export const sumAmount = (products: Array<number>): number => {
  let sum = 0;
  products.forEach((product) => {
    sum += data.filter((item) => item.id == product)[0].price;
  });
  return sum;
};

We then fetch the names of each product as well and convert it into a string of names using the same logic:

/src/utils/productNames.tsx

import { Product } from "../interfaces/ProductInterface";
const data: Array<Product> = require("../../data.json");

export const getProductNames = (products: Array<number>): string => {
  let productNames = "";
  products.forEach((product) => {
    productNames += data.filter((item) => (item.id == product))[0].title + ", ";
  });
  return productNames;
};

We also write a function that would help generate the ideal comment we would like to add to our invoice:

import { PurchaseRequest } from "../interfaces/PurchaseRequest";
import { getProductNames } from "./productName";

export const getInvoiceComment = (payload: PurchaseRequest) => {
  return `"Purchase of ${getProductNames(payload.products)} by ${
    payload.name
  }, Email: ${payload.email}, Delivery address: ${payload.address}"`;
};

Then we can write our generateInvoice API to put it all together

import { Request, Response } from "express";
import { LightningAddress, Invoice } from "@getalby/lightning-tools";
import { listenInvoice } from "../helpers/listenInvoice";
import { sumAmount } from "../utils/sumAmount";
import { PurchaseRequest } from "../interfaces/PurchaseRequest";
import { getInvoiceComment } from "../utils/getInvoiceComment";

if (!process.env.MERCHANT_LN_ADDRESS) {
  throw new Error(
    "Merchant's Lightning address environment variable is not defined"
  );
}

const ln = new LightningAddress(process.env.MERCHANT_LN_ADDRESS);

export const generateInvoice = async (req: Request, res: Response) => {
  const { name, email, products } = req.body as PurchaseRequest;
  try {
    if (products.length == 0) {
      return res.status(400).json({
        error: "Please select products to purchase",
      });
    }
    await ln.fetch();
    // get the LNURL-pay data:
    const invoice: Invoice = await ln.requestInvoice({
      satoshi: sumAmount(products),
      comment: getInvoiceComment(req.body),
      payerdata: {
        name: name,
        email: email,
      },
    });

    listenInvoice(req, invoice, res);

    return res.status(200).json({
      invoice: invoice,
    });
  } catch (error) {
    res.status(400).json({
      error: "Error encountered while generating invoice",
    });
  }
};

Next, we define our listenInvoice helper function which takes in the invoice and response. The listenInvoice function also uses Alby's js-lightning tools to check if the Lightning invoice has been settled or not and then sends a real-time response to the frontend (using socket) if the invoice has been settled. We also send an email notification to both the sender and the recipient once the invoice has been settled

/src/helpers/listenInvoice.ts

import { Invoice } from "@getalby/lightning-tools";
import { Request,Response } from "express";
import { io } from "../app";
import sendNotification from "./email";
import { PurchaseRequest } from "../interfaces/PurchaseRequest";


export const listenInvoice = async (
  req:Request,
  invoice: Invoice,
  res: Response
) => {
  try {
    const intervalId = setInterval(async () => {
      const paid = await invoice.isPaid();
      if (paid) {
        clearInterval(intervalId);
        io.emit("payment-verified", {
          message: "Payment verified successfully",
        });
        sendNotification(req.body as PurchaseRequest,res)
      }
    }, 3000);
  } catch (error) {
    res.status(400).json({
      error: "Error occured while verifying payment",
    });
  }
};

/src/helpers/email.ts

import { Response } from "express";
import { sumAmount } from "../utils/sumAmount";
import { getProductNames } from "../utils/productName";
import { PurchaseRequest } from "../interfaces/PurchaseRequest";

var nodemailer = require("nodemailer");
const Handlebars = require("handlebars");
const path = require("path");
const fs = require("fs");
const templatePath = path.join(__dirname, "../templates/email.hbs");
const template = fs.readFileSync(templatePath, "utf-8");
const compiledTemplate = Handlebars.compile(template);

if (!process.env.EMAIL_ADDRESS || !process.env.EMAIL_PASSWORD) {
  throw new Error("Please set your email credentials");
}

async function sendNotification(payload: PurchaseRequest, res: Response) {
  var mail = nodemailer.createTransport({
    service: "gmail",
    port: 465,
    secure: true,
    auth: {
      user: process.env.EMAIL_ADDRESS,
      pass: process.env.EMAIL_PASSWORD,
    },
  });

  const html = compiledTemplate({
    name: payload.name,
    email: payload.email,
    amount: sumAmount(payload.products),
    address: payload.address,
    products: getProductNames(payload.products),
    support: "mailto:" + process.env.EMAIL_ADDRESS,
  });

  var mailOptions = {
    from: `"Bitcoin⚡Shop <${process.env.EMAIL_ADDRESS}>"`,
    to: `${payload.email},${process.env.EMAIL_ADDRESS}`,
    replyTo: process.env.EMAIL_ADDRESS,
    subject: "Your order has been received, " + payload.name,
    html: html,
  };

  mail.sendMail(mailOptions, function (error: any, info: any) {
    if (error) {
      return;
    } else {
      res.status(200).json({
        message: "Email sent successfully",
      });
      return;
    }
  });
}

export default sendNotification;

And that's it! Our backend logic is done. You can find all the code for the backend here

Now let's move to the frontend implementation of the project

Frontend

Let's set up our React project using Vite. To do this, run:

npm create vite@latest my-bitcoin-shop --template react

Then we install the required dependencies

npm i @webbtc/webln-types axios react-hot-toast react-qr-code react-router-dom socket.io-client webln zustand

Here's what each dependency does:

  1. axios: to handle API requests

  2. react-hot-toast: shows beautiful toast messages when we copy our lightning invoice or add a new product to the cart.

  3. react-qr-code: displays the lightning invoice in qrcode form

  4. react-router-dom: to add routing to the project (for later updates)

  5. socket.io-client: to setup socket connection from the frontend client to listen to when payment has been settled and update the frontend

  6. @webbtc/webln-types: to integrate WebLN and prompt a user's web wallet to pay an invoice

  7. zustand: for state management

To manage multiple routes in this project, your App.jsx should look like this:

import { Route,BrowserRouter, Routes } from 'react-router-dom'
import Home from './views/Home/Home'
import "./App.css"

function App() {

  return (
    <>
     <BrowserRouter>
     <Routes>
      <Route path='/' element={<Home />}/>
     </Routes>
     </BrowserRouter>
    </>
  )
}

export default App

For state management using zustand, we setup a simple store that enables us to add products to the cart, remove products and fetch all the products in our cart:

/src/store/store.jsx

import { create } from "zustand";

const useProductStore = create((set) => ({
  products: [],
  addProducts: (newProducts) =>
    set((state) => ({ products: [...state.products, newProducts] })),
  removeAllProducts: () => set({ products: [] }),
  removeItem: (products) => set({ products: products }),
}));

export default useProductStore;

To separate our API logic from our UI logic, let's create an Ajax file where we would perform all our API calls to the backend

/src/ajax/ajax.jsx

import axios from "axios";
import handleError from "../utils/handleError";

export default {
  async fetchProducts() {
    try {
      const response = await axios({
        method: "GET",
        url: import.meta.env.VITE_API_URL + "/products",
        headers: {
          "Content-Type": "application/json",
        },
      });
      return response.data;
    } catch (error) {
      handleError(error.response.data.error)
      return error
    }
  },

  async generateInvoice(buyer, products) {
    try {
      const response = await axios({
        method: "POST",
        url: import.meta.env.VITE_API_URL + "/invoice",
        headers: {
          "Content-Type": "application/json",
        },
        data: {
          name: buyer.name,
          email: buyer.email,
          address: buyer.address,
          products: products,
        },
      });
      return response.data;
    } catch (error) {
      handleError(error.response.data.error)
    }
  },
};

/src/helpers/handleError.js

import { toast } from "react-hot-toast";

export default function handleError(message) {
  toast.error(message);
}

Now let's define our Home.jsx view.

Create a views folder, create a Home folder and then a Home.jsx file with the following lines of code:

/src/views/Home/Home.jsx

import React, { useState, useEffect } from "react";
import Products from "../../components/Products/Products";
import ajax from "../../ajax/ajax";
import Navbar from "../../components/NavBar/Navbar";
import useProductStore from "../../store/store";
import Checkout from "../../components/Checkout/Checkout";
import { toast } from "react-hot-toast";
import Cart from "../../components/Elements/Cart/Cart";
import handleError from "../../utils/handleError";

function Home() {
  const [products, setProducts] = useState([]);
  const [checkout, setShowCheckout] = useState(false);
  const [productsincart, setProductsInCart] = useState(
    useProductStore.getState().products
  );
  const [loading, setLoading] = useState(true);
  useEffect(() => {
    const fetchAllProducts = async () => {
      try {
        const fetchProducts = await ajax.fetchProducts();
        setProducts(fetchProducts.data);
        setLoading(false);
      } catch (error) {
        handleError(error.response.data.error);
        setLoading(false);
      }
    };
    fetchAllProducts();
  }, [setLoading]);

  const handleAddProduct = (product) => {
    const id = toast.loading("loading");
    useProductStore.getState().addProducts(product);
    setProductsInCart(useProductStore.getState().products);
    toast.dismiss(id);
    toast.success("Product added to cart");
  };

  const removeItem = (index) => {
    const updatedProducts = productsincart.slice();
    updatedProducts.splice(index, 1);
    useProductStore.getState().removeItem(updatedProducts);
    setProductsInCart(updatedProducts);
  };

  const removeAllProducts = () => {
    useProductStore.getState().removeAllProducts();
    setProductsInCart([]);
  };

  const showCheckout = () => {
    setShowCheckout(!checkout);
    window.scroll(0, 0);
  };
  return (
    <div className="relative">
      <Navbar products={productsincart} showCheckout={showCheckout} />
      <Products
        products={products}
        addProduct={handleAddProduct}
        loading={loading}
      />

      <div
        className={
          productsincart.length >= 1
            ? "bounce-animation floating-cart"
            : "floating-cart"
        }
      >
        <Cart products={productsincart} showCheckout={showCheckout} />
      </div>
      {checkout && (
        <Checkout
          products={productsincart}
          closeCheckout={() => setShowCheckout(!checkout)}
          removeItem={removeItem}
          removeAllProducts={removeAllProducts}
        />
      )}
    </div>
  );
}

export default Home;

The Home screen contains our Navbar component, Products component and Checkout component.

/src/components/Navbar/Navbar.jsx

import React from "react";
import "./Navbar.css";
import Cart from "../Elements/Cart/Cart";
import logo from "../../assets/bitcoinlogo.png"

function Navbar({ showCheckout,products }) {
  return (
    <div className="navbar">
      <h1>My ฿itcoin Shop <img width="30px" src={logo}/></h1>
      <Cart products={products} showCheckout={showCheckout} />
    </div>
  );
}

export default Navbar;

/src/components/Products/Products.jsx

import React from "react";
import ProductCategory from "./ProductCategory";

function Products({ products, addProduct }) {
  return (
    <div>
      <ProductCategory
        products={products}
        title={"All Products"}
        addProduct={addProduct}
      />

      {/* Devices products */}
      <ProductCategory
        products={products.filter((product)=>product.category==='device')}
        title={"Devices"}
        addProduct={addProduct}
      />

      <ProductCategory
        products={products.filter((product)=>product.category==='accessories')}
        title={"Accessories"}
        addProduct={addProduct}
      />

    </div>
  );
}

export default Products;

/src/components/Products/ProductCategory.jsx

import React from "react";
import Product from "./Product";

function ProductCategory({ title, addProduct, products }) {
  return (
    <div className="product-category">
      <h2>{title}</h2>
      <div className="products">
        {products &&
          products.map((product, i) => (
            <Product product={product} key={i} addProduct={addProduct} />
          ))}
      </div>
    </div>
  );
}

export default ProductCategory;

/src/components/Products/Product.jsx

import React from "react";
import { PrimaryButton } from "../Elements/Buttons/Buttons";
import "./Products.css";
import { standardAmountFormat } from "../../utils/amountFormatter";

function Product({ product, addProduct }) {
  return (
    <div className="product">
      <img width={"250px"} src={product.thumbnail} />
      <div>
        <div className="product-title">{product.title}</div>
        <p>{standardAmountFormat(product.price)}</p>
      </div>
      <PrimaryButton onClick={() => addProduct(product)}>
        Add to cart
      </PrimaryButton>
    </div>
  );
}

export default Product;

The standardAmount function is used to format the price of our products properly:

src/utils/amountFormatter.js

export function standardAmountFormat(amount){
    return parseFloat(parseFloat(amount && amount).toFixed(2)).toLocaleString('en') + " sats"
  }

Next, let's implement our Checkout components to manage the products added to our cart.

The main Checkout component houses multiple components and also holds the socket connection to our backend. At the point of payment, we generate an invoice and check if the user has a WebLN-supported wallet in their browser. If they do, the WebLN sendPayment() function is called to initiate payment. If the user does not have WebLN installed, then an Invoice QR code and string is displayed.

/src/utils/checkWebln.js

export const checkWebln = async () => {
  if (window.webln) {
    await window.webln.enable()
    return true
  } else {
    return false;
  }
};

Once an invoice has been settled, the socket connection changes the paymentStatus in the component from false to true, clears the products in the cart and then displays the SuccessComponent to the user.

/src/components/Checkout/Checkout.jsx

import React, { useState, useEffect } from "react";
import "./Checkout.css";
import CheckoutItem from "./CheckoutItem";
import closeicon from "../../assets/icons/closeicon.svg";
import { sumAmount } from "../../utils/sumAmount";
import { standardAmountFormat } from "../../utils/amountFormatter";
import { OutlinedButton } from "../Elements/Buttons/Buttons";
import ajax from "../../ajax/ajax";
import Invoice from "../Invoice/invoice";
import DetailsForm from "../Elements/Forms/DetailsForm";
import SuccessComponent from "./SuccessComponent";
import io from "socket.io-client";
import { productList } from "../../utils/productList";
import handleError from "../../utils/handleError";
import { checkWebln } from "../../utils/checkWebln";

function Checkout({ products, closeCheckout, removeItem, removeAllProducts }) {
  const [loading, setLoading] = useState(false);
  const [invoice, setInvoice] = useState("");
  const [ispayment, setPayment] = useState(false);
  const [disabled, setDisabled] = useState(true);
  const [paymentStatus, setPaymentStatus] = useState(false);
  const [buyer, setBuyer] = useState({});
  const socket = io(import.meta.env.VITE_API_URL);

  useEffect(() => {
    socket.on("payment-verified", () => {
      setPaymentStatus(true);
      removeAllProducts();
    });
  }, []);

  const generateInvoice = async () => {
    try {
      setLoading(true);
      const response = await ajax.generateInvoice(buyer, productList(products));
      const weblnStatus = await checkWebln()
      if (weblnStatus) {
        setLoading(false);
        await window.webln.sendPayment(response.invoice.paymentRequest);
      } else {
        setLoading(false);
        setInvoice(response.invoice);
        setPayment(true);
      }
    } catch (error) {
      handleError(error.message);
      setLoading(false);
      setPayment(false);
    }
  };

  const setBuyerInfo = (payload) => {
    setBuyer(payload);
  };

  const goBack = () => {
    setPayment(false);
  };

  return (
    <div className="checkout">
      <div className="flex justify-between">
        <h3>Checkout</h3>
        <div onClick={closeCheckout}>
          <img width={"25px"} src={closeicon} alt="closeicon" />
        </div>
      </div>
      {products.map((product, index) => (
        <div key={index}>
          <CheckoutItem
            product={product}
            index={index}
            removeItem={removeItem}
          />
        </div>
      ))}

      {products.length != 0 && (
        <div>
          <div className="total-checkout">
            <div>Total:</div>{" "}
            <div>
              <strong>{standardAmountFormat(sumAmount(products))}</strong>
            </div>
          </div>

          {!ispayment && (
            <div>
              <DetailsForm
                setDisabled={setDisabled}
                setBuyerInfo={setBuyerInfo}
              />
              {!disabled && (
                <OutlinedButton
                  width={"100%"}
                  loading={loading}
                  onClick={() => generateInvoice()}
                >
                  Pay Now
                </OutlinedButton>
              )}
            </div>
          )}

          {ispayment && !paymentStatus && invoice && (
            <Invoice goBack={goBack} invoice={invoice} />
          )}
        </div>
      )}

      {paymentStatus && <SuccessComponent />}
    </div>
  );
}

export default Checkout;

Now let's sum the price of each product to display to the user on the frontend :

/src/utils/sumAmount.js

export const sumAmount = (products)=>{
    return products.reduce((arr,cur) => arr+cur.price,0)
}

/src/components/Checkout/CheckoutItem.jsx

import React from "react";
import { standardAmountFormat } from "../../utils/amountFormatter";
import closeicon from "../../assets/icons/closeicon.svg";

function CheckoutItem({ product, index,removeItem }) {
  return (
    <div className="checkout-item">
      <div className="flex">
        <img
          className="checkout-image"
          src={product.thumbnail}
          alt="checkout-image"
        />
        <div className="checkout-item-title">{product.title}</div>
      </div>
      <div className="flex justify-between">
        <div className="checkout-product-price">
          {standardAmountFormat(product.price)}
        </div>
        <img
          onClick={() => removeItem(index)}
          width={"21px"}
          src={closeicon}
          alt="closeicon"
        />
      </div>
    </div>
  );
}

export default CheckoutItem;

/src/Components/Forms/DetailsForms.jsx

import React, { useState, useEffect } from "react";
import Input from "./Input";

function DetailsForm({ setDisabled,setBuyerInfo }) {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [address, setAddress] = useState("");
  useEffect(() => {
    if (name && email && address) {
      setDisabled(false);
      setBuyerInfo({
        name:name,
        email:email,
        address:address
      })
    } else {
      setDisabled(true);
    }
  }, [name, email, address]);

  return (
    <form className="details-form">
      <div className="form-row">
        <div className="form-colum">
          <Input
            setValue={setName}
            label={"Name"}
            placeholder={"First & last name"}
            type="text"
          />
        </div>
        <div className="form-colum">
          <Input
            setValue={setEmail}
            label={"Email"}
            placeholder={"Email address"}
            type="email"
            value={name}
          />
        </div>
      </div>

      <div className="form-row">
        <Input
          setValue={setAddress}
          width={"95%"}
          label={"Address"}
          placeholder={"Delivery Address"}
          type="text"
        />
      </div>
    </form>
  );
}

export default DetailsForm;

/src/components/Checkout/SuccessComponent.jsx

import React from "react";
import success from "../../assets/icons/success.gif";

function SuccessComponent() {
  return (
    <div className="success-icon">
      <div>
        <img width="200px" src={success} alt="" />
        <h2>Payment made successfully</h2>
        <p>
          Your payment has been confirmed successfully  <br/>and your products are on
          the way
        </p>
      </div>
    </div>
  );
}

export default SuccessComponent;

Now, let's write the code for the Invoice component to display a Lightning invoice for payment:

/src/components/Invoice/Invoice.jsx

import React from "react";
import QRCode from "react-qr-code";
import "./invoice.css";
import InvoiceInput from "../Elements/Forms/InvoiceInput";
import PaymentLoading from "./PaymentLoading";
import backIcon from "../../assets/icons/back-icon.svg";
import handleError from "../../utils/handleError";
import { checkWebln } from "../../utils/checkWebln";

function invoice({ invoice, goBack }) {
  const payInvoice = async () => {
    const weblnStatus = await checkWebln();
    if (weblnStatus) {
      await window.webln.sendPayment(invoice.paymentRequest);
    } else {
      handleError("WebLN is not available");
    }
  };
  return (
    <div>
      <img onClick={goBack} src={backIcon} width={"20px"}></img>
      <div onClick={payInvoice} className="invoice">
        <div>
          <QRCode
            size={100}
            style={{ height: "auto", maxWidth: "150px", width: "150px" }}
            value={invoice.paymentRequest}
            viewBox={`0 0 256 256`}
          />
        </div>
      </div>
      <p className="invoice-text">Click the invoice to pay</p>
      <div>
        <InvoiceInput invoice={invoice.paymentRequest} />
      </div>

      <PaymentLoading invoice={invoice} />
    </div>
  );
}

export default invoice;

NOTE: With the payInvoice function, we use the WebLN dependency to prompt the user's Lightning wallet to pay for the products. This wallet could be a Chrome extension like the Alby Browser Extension that works in the browser.

This is a holistic overview of the frontend of the Bitcoin-shop project. You can visit the repo here to checkout the code I used to add styling and other custom elements I created for the frontend.

Conclusion

In this tutorial, we successfully created an e-commerce shop that uses the Lightning Network as its payment provider to seamlessly enable users to pay for products. We've barely scratched the surface of what is possible with these sophisticated tools provided by Alby. To learn more about all the other amazing things you can do with the Alby JS lightning tools, make sure to check out its documentation here: https://github.com/getAlby/js-lightning-tools

Happy hacking ✌🏼

Reference Documentations:

  1. Alby's js-lightning tools: https://github.com/getAlby/js-lightning-tools

  2. WebLN Guide: https://www.webln.guide/