How to Build a Web3 "Buy Me a Coffee" dApp with thirdweb

How to Build a Web3 "Buy Me a Coffee" dApp with thirdweb

In this guide, we'll walk through how to create a decentralized application (dapp) that allows users to connect their wallet, send tips, and leave messages.

By the end of this guide, you will have a fully functioning web3 "Buy Me a Coffee" clone built with Next.js, thirdweb, and Solidity. Users will be able to:

  • Connect their wallet using various web3 providers
  • Send ETH tips and leave messages
  • View total tips received and recent messages

You can check out a video version of this tutorial here:

Checkout the GitHub repo:

GitHub - thirdweb-example/youtube-buy-me-a-coffee
Contribute to thirdweb-example/youtube-buy-me-a-coffee development by creating an account on GitHub.

Prerequisites

Before we begin, make sure you have the following:

  • thirdweb account
  • A wallet like MetaMask to connect to thirdweb
  • Some test funds on a testnet of choice

Step 1: Create the Smart Contract

First, let's create the smart contract that will handle the on-chain logic for our app. We'll be creating this contract from scratch with Solidity.

  1. Create a new contract by running npx thirdweb create contract in your command line and select Hardhat or Forge as your framework.
  2. Go to the Solidity file created.
💡
If using Hardhat this will be located in thecontractsfolder. If using Forge this will be located in thesrcfolder.
  1. Add the following code:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

contract BuyMeACoffee {
    struct Coffee {
        address sender;
        string message;
        uint256 timestamp;
    }

    uint256 totalCoffee;
    address payable owner;

    event NewCoffee(address indexed sender, uint256 timestamp, string message);

    constructor() {
        owner = payable(msg.sender);
    }

    function buyMeACoffee (
        string memory _message
    ) payable public {
        require(msg.value > 0 ether, "You need to send some ether");

        totalCoffee += 1;

        payable(owner).transfer(msg.value);
        
        emit NewCoffee(msg.sender, block.timestamp, _message);
    }

    function getTotalCoffee() public view returns (uint256) {
        return totalCoffee;
    }
}

Now that we have our smart contract created to handle our onchain functionality of our accountability app we can deploy it to any EVM blockchain.

Step 2: Deploy the Smart Contract

Next, we need to deploy the smart contract that will power our accountability app.

  1. Run the command npx thirdweb deploy to deploy your smart contract
  2. Select the chain you want to deploy your contract to and click 'Deploy Now'

Once deployed, save the contract address somewhere as we'll need it later.

Step 3: Set Up the Next.js Project and install Connect SDK

Next, let's set up our Next.js project:

  1. Open a terminal and run: npx thirdweb create app
  2. Select Next.js for the framework
  3. Open project in code editor
  4. Edit the .env file and add you clientID
  5. Define your chain. In this example we'll be using Base Sepolia Testnet
import { defineChain } from "thirdweb";
import { baseSepolia } from "thirdweb/chains";

export const chain = defineChain( baseSepolia );

Step 4: Add a way to connect a wallet to app

Next, let's create a way for a user to connect a web3 wallet to our accountability app.

  1. In page.tsx we can add a ConnectEmbed component from Connect SDK
<div>
  <ConnectEmbed 
    client={client}
    chain={chain}
  />
</div>
  1. We can then check to see if a wallet is connected and if there is we can show a connected ConnectButton
const account = useActiveAccount();

if(account) {
  return (
    <div>
    	<ConnectButton
          client={client}
          chain={chain}
         />
    </div>
  )
}

Now we have a way for our user to connect and disconnect a wallet from our accountability app.

Step 5: Build accountability app

Get Contract

Now, lets get some data from our smart contract and create our accountability app to interact with.

  1. Create a new file contract.ts in a utils folder
  2. Get our accountability smart contract using getContract
const contractAddress = "<Contract_Address>";

export const contract = getContract({
    client: client,
    chain: chain,
    address: contractAddress,
    abi: contractABI
});
  1. Create a new file for the accountability smart contract ABI
💡
You can find your smart contract ABI in the contract dashboard in the "source" tab under the ABI dropdown
export const contractABI = ["Contract_ABI"] as const;

First, create a new file called _app.tsx in the pages directory and add the following code:

import type { AppProps } from 'next/app';
import { ChainId, ThirdwebProvider } from '@thirdweb-dev/react';
import '../styles/globals.css';

// This is the chainId your dApp will work on.
const activeChainId = ChainId.Mumbai;

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ThirdwebProvider desiredChainId={activeChainId}>
      <Component {...pageProps} />
    </ThirdwebProvider>
  );
}

export default MyApp;
  

Creating the Buy Me a Coffee Component

Now that we have our contract set up, let's build the main component for our dapp - the "Buy Me a Coffee" form.

  1. Create state variables for the input fields a user will have to fill out for the tip amount and the message they want to leave
const [buyAmount, setBuyAmount] = useState(0);
const [message, setMessage] = useState("");
  1. Create input for user to specify the tip amount and the message they want to leave
<div>
    <label>Tip amount</label>
    <p>*Must be greater than 0.</p>
    <input 
        type="number" 
        value={buyAmount}
        onChange={(e) => setBuyAmount(Number(e.target.value))}
        step={0.01}
    />
    <label>Message</label>
    <input 
        type="text" 
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        placeholder="Enter a message..."
    />
</div>
  1. Create a TransactionButton only if a user has entered a tip amount greater than 0 and a message. After the transaction is complete it should reset the values for the tip amount and message and refetch the information from the contract.
{message && buyAmount > 0 && (
    <TransactionButton
        transaction={() => (
            prepareContractCall({
                contract: contract,
                method: "buyMeACoffee",
                params: [message],
                value: BigInt(toWei(buyAmount.toString())),
            })
        )}
        onTransactionConfirmed={() => {
            alert("Thank you for the coffee!")
            setBuyAmount(0);
            setMessage("");
            refetchTotalCoffees();
            refetchContractEvents();
        }}
    >Buy Coffee</TransactionButton>
)}
  1. Lets display the total coffees (tips) that have been left. To do this we can call the getTotalCoffee function from the smart contract using the useReadContract hook
const { 
    data: totalCoffees, 
    refetch: refetchTotalCoffees 
} = useReadContract({
    contract: contract,
    method: "getTotalCoffee",
});
  1. Next, lets get the the events from our smart contract. Out events should show to most recent tips and messages left. We can then display this data on our app.
const { 
    data: contractEvents, 
    refetch: refetchContractEvents 
} = useContractEvents({ 
    contract: contract,
});
  1. We can also add some additional functions to truncate our wallet addresses and convert the block timestamp to a readable date.
const truncateWalletAddress = (address: string) => {
    return `${address.slice(0, 6)}...${address.slice(-4)}`;
}
const convertDate = (timestamp: bigint) => {
    const timestampNumber = Number(timestamp);
    return new Date(timestampNumber * 1000).toLocaleString();
};
  1. Finally, we'll display our total coffees and we can display the contract events of recent coffees in our app. When mapping though the array of events you can reverse() the array to display the most recent event at the top of your list.
<div>
    <h3>Total Coffees: {totalCoffees?.toString()}</h3>
    <p>Recent Coffees:</p>
    {contractEvents && contractEvents.length > 0 && (
        [...contractEvents].reverse().map((event, index) => (
            <div key={index}>
                <div>
                    <p>
                        {truncateWalletAddress(event.args.sender)}
                    </p>
                    <p>
                        {convertDate(event.args.timestamp)}
                    </p>
                </div>
                <p>
                    {event.args.message}
                </p>
            </div>
        ))
    )}
</div>

Testing it Out

Start your Next.js app with yarn dev and open http://localhost:3000 in your browser.

Connect your wallet using the ConnectEmbed modal. You should then see the BuyMeACoffee component with the form to send a tip and message.

Enter a tip amount and message, then click "Buy a Coffee". Approve the transaction in your wallet. Once confirmed, you should see an alert and the recent coffees list and total should update.

Try sending a few more tips from different wallets to see the recent coffees list grow.

And that's it! You've now built a fully functional web3 "Buy Me a Coffee" dapp using thirdweb. Users can connect their wallet, send tips with messages, and view all the recent tips.

Conclusion

And there you have it! We've built a web3 accountability app that allows users to:

  1. Connect their wallet
  2. Tip an amount and leave a message
  3. Display total coffees (tips) left
  4. Display the most recent coffees (tips) left

By using thirdweb's Connect SDK, we were able to easily interact with our smart contract to read data and make transactions.

You can build an app like this on any EVM-compatible blockchain, including popular L2s like Optimism, Base, Arbitrum, Avalanche and more.

The thirdweb Connect SDK provides a simple and powerful way to build web3 apps with a great developer experience.

I hope you enjoyed this tutorial and found it valuable!