Crypto Product Card

Pay-with-crypto or custom erc20 token product card

CryptocurrencyEcommerceErc20 Tokens
Best seller
Rolex Submariner Date

Rolex Submariner

120 USDT

Technologies

Installation

Install Web3 dependencies

npm install @reown/appkit @reown/appkit-adapter-wagmi wagmi viem @tanstack/react-query

Add Shadcn components

npx shadcn@latest add card toast button

Add Sepolia testnet to config

Edit the file created during the Reown Appkit installation named config/index.tsx and add the sepolia network for this example:
import { sepolia } from "@reown/appkit/networks";

export const networks = [
  //...
  sepolia // add sepolia network here
];

Add Sepolia testnet to Appkit context

Edit the second file created during the Reown Appkit installation named context/index.tsx and add the sepolia network for this example:
import { sepolia } from "@reown/appkit/networks";

const modal = createAppKit({
  //...
  networks: [
    sepolia, // add sepolia network here
  ],
  //...
});

Add Toaster component

Add the Shadcn Toaster component to the app/layout.tsx file to enable component toast notifications:
import type { Metadata } from "next";
import "./globals.css";
import { Toaster } from "@/components/ui/toaster"; // import Toaster component

import { cookies, headers } from "next/headers";
import ContextProvider from "@/context/AppKitContext";

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  const cookies = headers().get("cookie");

  return (
    <html lang="en">
      <body>
        <ContextProvider cookies={cookies}>{children}</ContextProvider>
        <Toaster /> {/* add Toaster component here */}
      </body>
    </html>
  );
}

Copy and paste source code

"use client";

import React, { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { erc20Abi, zeroAddress, parseEther } from "viem";
import {
  useAccount,
  useChainId,
  useSwitchChain,
  useWriteContract,
  useWaitForTransactionReceipt,
  useSendTransaction,
} from "wagmi";
import { useAppKit } from "@reown/appkit/react";
import { Card, CardContent } from "@/components/ui/card";
import { useToast } from "@/hooks/use-toast";
import Image from "next/image";

interface Props {
  currency?: `0x${string}`;
  price: number;
  paymentReceiverAddress: `0x${string}`;
  chainId: number;
}

const CryptoProductCard = ({
  currency = zeroAddress,
  price,
  paymentReceiverAddress,
  chainId,
}: Props) => {
  const [isPaymentProcessing, setPaymentProcessing] = useState<boolean>(false);
  const [isMounted, setMounted] = useState<boolean>(false);
  const { open } = useAppKit();
  const { address, isConnected, isConnecting, isReconnecting } = useAccount();
  const { switchChain } = useSwitchChain();
  const currentChainId = useChainId();
  const { toast } = useToast();

  const activeChainId: number = chainId;

  const currencySymbol: string = "USDT";

  const { data: nativePaymentHash, sendTransaction } = useSendTransaction({
    mutation: {
      onSuccess: () => {
        setPaymentProcessing(true);
      },
      onError: () => {
        toast({
          title: "Execution reverted",
          description:
            "Insufficient balance or transaction reverted. Please try again.",
          variant: "destructive",
        });
      },
    },
  });

  const { isSuccess: isNativePaymentSuccess } = useWaitForTransactionReceipt({
    hash: nativePaymentHash,
  });

  const { data: ercPaymentHash, writeContract } = useWriteContract({
    mutation: {
      onSuccess: () => {
        setPaymentProcessing(true);
      },
      onError: (error) => {
        console.log(error);
        toast({
          title: "Execution reverted",
          description:
            "Insufficient balance or transaction reverted. Please try again.",
        });
      },
    },
  });

  const { isSuccess: isErcPaymentSuccess } = useWaitForTransactionReceipt({
    hash: ercPaymentHash,
  });

  const handlePayment = () => {
    try {
      if ((currency as `0x${string}`) === zeroAddress) {
        sendTransaction({
          to: paymentReceiverAddress,
          value: parseEther(price.toString()),
        });
      } else {
        writeContract({
          abi: erc20Abi,
          address: currency,
          functionName: "transfer",
          args: [paymentReceiverAddress, parseEther(price.toString())],
        });
      }
    } catch (error) {
      console.log(error);
    }
  };

  const handleChainSwitch = () => {
    if (currentChainId === activeChainId) {
      return;
    }

    switchChain({ chainId: activeChainId });
  };

  useEffect(() => {
    if (isErcPaymentSuccess || isNativePaymentSuccess) {
      toast({
        title: "Payment successful",
        description:
          "Your payment is complete. Thank you for shopping with us.",
      });
    }
    setPaymentProcessing(false);
  }, [isErcPaymentSuccess, isNativePaymentSuccess]);

  useEffect(() => {
    setMounted(true);
  }, []);

  return (
    <div>
      {isMounted && isConnected && address && (
        <div className="absolute top-4 right-10">
          <w3m-account-button />
        </div>
      )}

      <Card className="w-[300px] h-[472px] shadow">
        <CardContent className="p-3 relative">
          <div className="text-indigo-500 border border-indigo-500 rounded-full p-1 font-bold text-xs w-[80px] absolute top-3 left-2 flex justify-center items-center">
            Best seller
          </div>
          <div className="h-[345px]">
            <Image
              src="https://purepng.com/public/uploads/thumbnail//rolex-submariner-date-au9.png"
              alt="Rolex Submariner Date"
              width={0}
              height={0}
              className="w-full px-6 h-[345px]"
              unoptimized
            />
          </div>

          <div className="mb-3 text-center flex flex-col gap-1">
            <p className="">Rolex Submariner</p>
            <p className="font-bold">
              {price} {currencySymbol}
            </p>
          </div>

          {isMounted ? (
            <div>
              {isConnecting || isReconnecting ? (
                <Button
                  disabled
                  className="bg-indigo-500 w-full flex items-center gap-2 text-white font-medium rounded-[8px]"
                >
                  <div className="w-3.5 h-3.5 border-2 border-gray-400 border-b-transparent rounded-full inline-block box-border animate-spin" />
                  Loading
                </Button>
              ) : !isConnected || !address ? (
                <Button onClick={() => open()} className="bg-indigo-500 w-full">
                  Connect
                </Button>
              ) : !isConnecting &&
                !isReconnecting &&
                currentChainId !== activeChainId ? (
                <Button
                  onClick={handleChainSwitch}
                  className="bg-red-500 text-white hover:text-black w-full rounded-[8px]"
                >
                  Switch to sepolia
                </Button>
              ) : (
                <Button
                  disabled={isPaymentProcessing}
                  onClick={handlePayment}
                  className="bg-indigo-500 w-full disabled:opacity-80 font-medium text-white hover:text-black rounded-[8px]"
                >
                  {isPaymentProcessing ? (
                    <div className="flex items-center gap-2">
                      <div className="w-3.5 h-3.5 border-2 border-gray-400 border-b-transparent rounded-full inline-block box-border animate-spin" />
                      Processing
                    </div>
                  ) : (
                    "Buy now"
                  )}
                </Button>
              )}
            </div>
          ) : (
            <Button
              disabled
              className="bg-indigo-500 w-full flex items-center gap-2 text-white font-medium rounded-[8px]"
            >
              <div className="w-3.5 h-3.5 border-2 border-gray-400 border-b-transparent rounded-full inline-block box-border animate-spin" />
              Loading
            </Button>
          )}
        </CardContent>
      </Card>
    </div>
  );
};

export default CryptoProductCard;

Props

PropTypeDescriptionDefault value
currency`0x${string}`Optional address of erc20 token to be used for paymentzeroAddress
pricenumberPrice of the product in Ether valueundefined
paymentReceiverAddress`0x${string}`Address of the receiver of the paymentundefined
chainIdnumberChain ID of the network to be used (must match token network if erc20 option is used)undefined

See something you like?

Take your project further with our advanced custom development.

"One of the only full-stack Web3 component libraries i've seen in the space so far. Ten out of ten recommended. Saved me a ton of time. Can't wait to see what templates they release next."

Samy

Side projects builder