Create a dynamic allowlist with Airtable and Next.js



Introduction

On this information, we’re going to create an version the place folks will have the ability to enroll their wallets to an allowlist and people wallets can later mint the NFT!

Sounds cool, proper!? Let’s get began.



Setup

I’m going to make use of the Next typescript starter template of thirdweb for this information. If you have already got a Subsequent.js app you’ll be able to merely observe these steps to get began:

  • Set up @thirdweb-dev/react and @thirdweb-dev/sdk
  • Add metamask authentication to the location. You possibly can observe this information
  • You all are set to go now!

If you’re additionally going to make use of the Next TypeScript template then observe these steps

  • Click on on the use template button within the repo
  • Clone the repo
  • Set up the dependencies:
npm set up # npm

yarn set up # yarn
Enter fullscreen mode

Exit fullscreen mode

By default the community in _app.tsx is Mainnet, we have to change it to Mumbai

import kind { AppProps } from 'subsequent/app';
import { ChainId, ThirdwebProvider } from '@thirdweb-dev/react';

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

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

export default MyApp;
Enter fullscreen mode

Exit fullscreen mode



Creating an version and NFT

We additionally must create an version and an NFT within the version to have the ability to mint them to the customers. So, go to the thirdweb dashboard and create an version. I’m doing it with an Version contract for this demo however it may be additionally achieved with certainly one of one NFTs (ERC-721).

Fill out the small print and deploy the contract!

Now, let’s create and mint a brand new NFT within it. Fill out the small print and ensure to set the preliminary provide to 0.

Mint New NFT



Making the web site



Creating an nft card

To showcase our nft, we’re going to create a easy card and we’ll get the information from the version contract itself! So, inside index.tsx add the useEdition hook like this:

const version = useEdition("EDITION_CONTRACT_ADDRESS");
Enter fullscreen mode

Exit fullscreen mode

We have to import it from the thirdweb sdk:

import { useEdition } from "@thirdweb-dev/react";
Enter fullscreen mode

Exit fullscreen mode

Now, we’re going to create a brand new state to carry the nft knowledge:

  const [nftData, setNftData] = useState<EditionMetadata["metadata"] | null>(
    null
  );
Enter fullscreen mode

Exit fullscreen mode

We additionally must import EditionMetadata and useState:

import { EditionMetadata } from "@thirdweb-dev/sdk";
import { useState } from "react";
Enter fullscreen mode

Exit fullscreen mode

Lastly, create a useEffect to get the information from the contract and replace the variable:

 useEffect(() => {
    version?.get("0").then((nft) => {
      setNftData(nft.metadata);
    });
  }, [edition]);
Enter fullscreen mode

Exit fullscreen mode

If you happen to console log the nft metadata you’re going to get one thing like this:

NFT Metadata

If the person is logged in we’ll present them this nft as a substitute of there tackle, so substitute the fragment (<>) with this:

<div>
  {nftData?.picture && (
    <Picture
      src={nftData?.picture}
      alt={nftData?.title}
      width="280"
      top="210"
      objectFit="include"
    />
  )}
  <p>
    <span>Title:</span> {nftData?.title}
  </p>
  <p>
    <span> Description:</span> {nftData?.description}
  </p>

  <button>Mint</button>
</div>
Enter fullscreen mode

Exit fullscreen mode

You’ll now get an error as a result of we’re utilizing the subsequent <Picture /> element however have not whitelisted the area, so go to subsequent.config.js and add the next:

/** @kind {import('subsequent').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  photographs: {
    domains: ["gateway.ipfscdn.io"],
  },
};

module.exports = nextConfig;
Enter fullscreen mode

Exit fullscreen mode

Since we’re altering the subsequent.js config we have to restart the server. When you restart the server you will note one thing like this

NFT Card display



Creating the mint api

We’re going to use the signature minting on our backend to make sure that the tackle is current within the airtable, however first let’s get the signature minting working.

Create a brand new folder api within the pages folder and generate-mint-sig.ts inside it.

We’ll now construct a fundamental api that can output “gm wagmi”

import kind { NextApiRequest, NextApiResponse } from "subsequent";

const generateMintSignature = async (
  req: NextApiRequest,
  res: NextApiResponse
) => {
  res.ship("gm wagmi");
};

export default generateMintSignature;
Enter fullscreen mode

Exit fullscreen mode

This creates a fundamental api for us, in case you now go to the api/generate-mint-sig endpoint you’re going to get a response of Gm wagmi.

Api response

Let’s now initialize the thirdweb sdk!

const sdk = new ThirdwebSDK(
    new ethers.Pockets(
      course of.env.PRIVATE_KEY as string,
      ethers.getDefaultProvider(course of.env.ALCHEMY_API_URL)
    )
  );
Enter fullscreen mode

Exit fullscreen mode

as you’ll be able to see we’re utilizing 2 surroundings variables to initialize the sdk. The primary is the PRIVATE_KEY which is the personal key of the pockets. The second is the ALCHEMY_API_URL which is the url of the Alchemy api. Create a brand new file .env.native and add the 2 variables.

PRIVATE_KEY=<private_key>
ALCHEMY_API_URL=<alchemy_api_url>
Enter fullscreen mode

Exit fullscreen mode

Let’s examine methods to get these two variables.

Getting the pockets personal key

In your metamask pockets, click on on the three dots, then click on account particulars. You will note an choice to export personal key there. Export your personal key and paste it into the PRIVATE_KEY variable.

Export private key

It will give full entry to your pockets to ensure to maintain it secret!

Getting the alchemy api key

Go to Alchemy and join an account. You must create a brand new app, so click on on create app. Fill out the small print, and hit submit. I’m utilizing Mumbai community for this demo however it is advisable use the community that you’re utilizing!

Create Alchemy app

Lastly, paste this within the .env.native file too. Since we’ve got modified the env variables we have to restart the server. So, lower the terminal and run yarn dev once more.

We additionally must import ethers and ThirdwebSDK:

import { ThirdwebSDK } from "@thirdweb-dev/sdk";
import { ethers } from "ethers";
Enter fullscreen mode

Exit fullscreen mode

Producing the siganture mint

Within the api we’re going to use the sdk to get entry to the version contract and generate a mint signature:

 const version = sdk.getEdition("EDITION_CONTRACT_ADDRESS");
  attempt {
    const signedPayload = await version.signature.generate({
      tokenId: 0,
      amount: "1",
      to: tackle,
    });

    res.standing(200).json({
      signedPayload: signedPayload,
    });
  } catch (err) {
    res.standing(500).json({
      error: err,
    });
  }
Enter fullscreen mode

Exit fullscreen mode

We’ll get the tackle from the frontend so, add this:

const { tackle } = JSON.parse(req.physique);
Enter fullscreen mode

Exit fullscreen mode

Let’s now mint the api on the frontend by calling this api:

  const mintWithSignature = async () => {
    const signedPayloadReq = await fetch(`/api/generate-mint-sig`, {
      technique: "POST",
      physique: JSON.stringify({ tackle }),
    });

    const signedPayload = await signedPayloadReq.json();

    attempt {
      const nft = await version?.signature.mint(signedPayload.signedPayload);
      return nft;
    } catch (err) {
      console.error(err);
      return null;
    }
  };
Enter fullscreen mode

Exit fullscreen mode

This operate will request the api that we simply constructed with the tackle. It should give us a signed payload that we will use to mint the nft through our version contract. So, let’s connect it to the button:

<button onClick={() => mintWithSignature()}>Mint</button>
Enter fullscreen mode

Exit fullscreen mode

Now our mint works! However wait, everybody can mint now so let’s create an airtable database to retailer the addresses that may mint.



Creating an allowlist with airtable

Go to Airtable and create a brand new base.

After you create a brand new base, give a reputation to your base and add two columns: Deal with and Minted like this:

Airtable base

Getting airtable api keys and id

We now must get some api key’s and id’s to work together with the bottom. So, go to your Airtable account and generate an api key

Generate api key

Retailer this api key someplace secure as we’re going to want it.

Now to get the bottom id go to the Airtable API and click on on the bottom that you simply simply created. While you open the web page, on the high itself you’ll see “Your base id is app……”.

Inside .env.native add three new variables:

AIRTABLE_API_KEY=
AIRTABLE_BASE_ID=
AIRTABLE_TABLE_NAME=
Enter fullscreen mode

Exit fullscreen mode



Making a utility operate for accessing the desk

To maintain our code clear we’re going to create a file the place we initialize the airtable with the api key, title, and id. So, create a brand new folder utils and Airtable.ts inside it. Now, add within the following in Airtable.ts:

import Airtable from "airtable";

// Authenticate
Airtable.configure({
  apiKey: course of.env.AIRTABLE_API_KEY,
});

// Initialize a base
const base = Airtable.base(course of.env.AIRTABLE_BASE_ID!);

// Reference a desk
const desk = base(course of.env.AIRTABLE_TABLE_NAME!);

export { desk };
Enter fullscreen mode

Exit fullscreen mode

As you’ll be able to see we’re going to want to put in a brand new package deal referred to as airtable:

npm i airtable # npm

yarn add airtable # yarn
Enter fullscreen mode

Exit fullscreen mode

Now, we have to replace the api to first verify if the pockets is current within the desk. So, in api/generate-mint-sig.ts add the next:

  const document = await desk
    .choose({
      fields: ["Addresses", "minted"],
      filterByFormula: `NOT({Addresses} != '${tackle}')`,
    })
    .all();

  if (document.size === 0) {
    res.standing(404).json({
      error: "Person is not in allowlist",
    });
  }
Enter fullscreen mode

Exit fullscreen mode

So, this queries the airtable to get the data of the tackle current within the addresses column and we’re checking if the size document is 0. Whether it is 0, then we’re not permitting the person to mint and ship an error. We may also wrap the half the place we ship the response in an else block. The api ought to now look just like this:

import kind { NextApiRequest, NextApiResponse } from "subsequent";
import { ThirdwebSDK } from "@thirdweb-dev/sdk";
import { ethers } from "ethers";
import { desk } from "../../utils/Airtable";

const generateMintSignature = async (
  req: NextApiRequest,
  res: NextApiResponse
) => {
  const { tackle } = JSON.parse(req.physique);

  const document = await desk
    .choose({
      fields: ["Addresses", "minted"],
      filterByFormula: `NOT({Addresses} != '${tackle}')`,
    })
    .all();

  if (document.size === 0) {
    res.standing(404).json({
      error: "Person is not in allowlist",
    });
  } else {
    const sdk = new ThirdwebSDK(
      new ethers.Pockets(
        course of.env.PRIVATE_KEY as string,
        ethers.getDefaultProvider(course of.env.ALCHEMY_API_URL)
      )
    );

    const version = sdk.getEdition(
      "0x62C84CC051544c43d05a5Ff0E8Da596fBdB15032"
    );
    attempt {
      const signedPayload = await version.signature.generate({
        tokenId: 0,
        amount: "1",
        to: tackle,
      });

      res.standing(200).json({
        signedPayload: signedPayload,
      });
    } catch (err) {
      res.standing(500).json({
        error: err,
      });
    }
  }
};

export default generateMintSignature;
Enter fullscreen mode

Exit fullscreen mode

Within the mintWithSignature operate we’ll now add a easy alert to point out the error if the person shouldn’t be within the allowlist.

 const mintWithSignature = async () => {
    const signedPayloadReq = await fetch(`/api/generate-mint-sig`, {
      technique: "POST",
      physique: JSON.stringify({ tackle }),
    });

    const signedPayload = await signedPayloadReq.json();

    if (signedPayload.error) {
      alert(signedPayload.error);
      return;
    }

    attempt {
      const nft = await version?.signature.mint(signedPayload.signedPayload);
      return nft;
    } catch (err) {
      console.error(err);
      return null;
    }
  };
Enter fullscreen mode

Exit fullscreen mode

Now in case you attempt minting you must now see an error that you’re not within the allowlist. However in case you add the tackle you might be utilizing to mint to the tackle column, it might assist you to mint!



Setting the minted to true when the tackle mints

We’ll create one other api to not expose the api keys, so within the api folder create a brand new file set-minted.ts and add the next:

import kind { NextApiRequest, NextApiResponse } from "subsequent";
import { desk } from "../../utils/Airtable";

const generateMintSignature = async (
  req: NextApiRequest,
  res: NextApiResponse
) => {
  const { tackle } = JSON.parse(req.physique);

  const document = await desk
    .choose({
      fields: ["Addresses", "minted"],
      filterByFormula: `NOT({Addresses} != '${tackle}')`,
    })
    .all();

  attempt {
    document[0].updateFields({
      minted: "true",
    });
    res.standing(200).json({
      success: true,
    });
  } catch (err) {
    res.standing(500).json({
      error: err,
    });
  }
};

export default generateMintSignature;
Enter fullscreen mode

Exit fullscreen mode

Principally what this code block is doing is getting the document of the tackle after which updating the minted column to true. And for fundamental error dealing with if there’s an error we’ll return the error. Now, within the mintSignature operate I’ll add the fetch request to the set-minted api if the nft was minted profitable like this:

if (nft) {
  await fetch(`/api/set-minted`, {
    technique: "POST",
    physique: JSON.stringify({ tackle }),
  });
}
Enter fullscreen mode

Exit fullscreen mode

If you happen to attempt minting once more and verify the airtable it might now set the minted column to true! If you happen to get an error just like the column title not discovered then guarantee that the names are identical.



Including customers to allowlist

I’m going so as to add a easy button for including customers to allowlist. You possibly can even create an early entry kind or one thing comparable. So, create a button in index.tsx:

<button>Add pockets to allowlist</button>
Enter fullscreen mode

Exit fullscreen mode

Now, let’s create an api so as to add the pockets to the allowlist. So, create a brand new file add-to-allowlist.ts and add the next:

import kind { NextApiRequest, NextApiResponse } from "subsequent";
import { desk } from "../../utils/Airtable";

const addToAllowlist = async (req: NextApiRequest, res: NextApiResponse) => {
  const { tackle } = JSON.parse(req.physique);

  const document = await desk
    .choose({
      fields: ["Addresses", "minted"],
      filterByFormula: `NOT({Addresses} != '${tackle}')`,
    })
    .all();

  if (document.size > 0) {
    res.standing(400).json({
      success: false,
      error: "Person is already in allowlist",
    });
  }

  if (document.size === 0) {
    attempt {
      await desk.create([
        {
          fields: {
            Addresses: address,
          },
        },
      ]);
      res.standing(200).json({
        success: true,
        message: "Person added to allowlist",
      });
    } catch (err) {
      res.standing(500).json({
        success: false,
        error: err,
      });
    }
  }
};

export default addToAllowlist;
Enter fullscreen mode

Exit fullscreen mode

Right here, we’re first checking if the person already exists and if it exists we’ll return an error saying that the person is already within the allowlist. If the person would not exist we’ll create the person within the allowlist. Now, let’s head again to the frontend code and create a operate to name this api:

  const addWallet = async () => {
    const payload = await fetch(`/api/add-to-allowlist`, {
      technique: "POST",
      physique: JSON.stringify({ tackle }),
    });

    const payloadJson = await payload.json();
    console.log(payloadJson);

    if (payloadJson.success) {
      alert(payloadJson.message);
    } else {
      alert(payloadJson.error);
    }
  };
Enter fullscreen mode

Exit fullscreen mode

Lastly, add an onClick occasion to the button:

<button onClick={addWallet}>Add pockets to allowlist</button>
Enter fullscreen mode

Exit fullscreen mode

If you happen to attempt switching the related pockets or take away the pockets from the database you will note that the person’s pockets tackle is being added to the bottom!



Minor enhancements (Optionally available)

Let’s simply add some minor enhancements to our app!



Styling

At the moment the location appears fairly boring, so let’s add some easy styling. Create a brand new file globals.css within the types folder and add the next to reset the types:

html,
physique {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
    Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}

a {
  colour: inherit;
  text-decoration: none;
}

* {
  box-sizing: border-box;
}
Enter fullscreen mode

Exit fullscreen mode

Import it within the _app.tsx file:

import "../types/globals.css";
Enter fullscreen mode

Exit fullscreen mode

Now, create a brand new file Dwelling.module.css within the types folder and add these easy stylings:

.container {
  show: flex;
  flex-direction: column;
  align-items: middle;
  justify-content: middle;
  top: 100vh;
  background-color: #c0ffee;
}

.container > button,
.btn {
  background: #1ce;
  colour: #fff;
  font-size: 1rem;
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 0.5rem;
  cursor: pointer;
}

.btn {
  margin-top: 10px;
}

.NFT {
  show: flex;
  flex-direction: column;
  justify-content: middle;
}

.nftDesc {
  margin: 10px 0px;
  font-size: 0.8rem;
  colour: #000;
}

.nftDesc > span {
  font-weight: daring;
}
Enter fullscreen mode

Exit fullscreen mode

Now let’s implement these stylings in index.tsx by including the classNames:

 <div className={types.container}>
      {tackle ? (
        <div className={types.NFT}>
          {nftData?.picture && (
            <Picture
              src={nftData?.picture}
              alt={nftData?.title}
              width="280"
              top="210"
              objectFit="include"
            />
          )}
          <p className={types.nftDesc}>
            <span>Title:</span> {nftData?.title}
          </p>
          <p className={types.nftDesc}>
            <span> Description:</span> {nftData?.description}
          </p>

          <button className={types.btn} onClick={addWallet}>
            Add pockets to allowlist
          </button>

          <button className={types.btn} onClick={() => mintWithSignature()}>
            Mint
          </button>
        </div>
      ) : (
        <button onClick={connectWithMetamask}>Join with Metamask</button>
      )}
    </div>
Enter fullscreen mode

Exit fullscreen mode

Our web site now appears a lot better!

Deploy edition contract



Loading

At the moment in case you click on the mint button or add to allowlist button nothing occurs for some time, so let’s add a loading textual content to inform the person that one thing is going on. So, create 2 states within the Dwelling.tsx file:

const [addWalletLoading, setAddWalletLoading] = useState(false);
const [mintLoading, setMintLoading] = useState(false);
Enter fullscreen mode

Exit fullscreen mode

Add this ternary operator for altering the textual content of the buttons and disable the button whether it is loading:

 <button
   className={types.btn}
   disabled={addWalletLoading}
   onClick={addWallet}
>
  {addWalletLoading ? "loading..." : "Add pockets to allowlist"}
</button>

 <button
   className={types.btn}
   disabled={mintLoading}
   onClick={() => mintWithSignature()}
>
  {mintLoading ? "loading..." : "Mint"}
</button>
Enter fullscreen mode

Exit fullscreen mode

Nothing occurs but as a result of we aren’t altering the states so, within the capabilities we have to change the loading states:

  const mintWithSignature = async () => {
    setMintLoading(true);
    const signedPayloadReq = await fetch(`/api/generate-mint-sig`, {
      technique: "POST",
      physique: JSON.stringify({ tackle }),
    });

    const signedPayload = await signedPayloadReq.json();

    if (signedPayload.error) {
      alert(signedPayload.error);
      return;
    }

    attempt {
      const nft = await version?.signature.mint(signedPayload.signedPayload);
      if (nft) {
        await fetch(`/api/set-minted`, {
          technique: "POST",
          physique: JSON.stringify({ tackle }),
        });
      }
      return nft;
    } catch (err) {
      console.error(err);
      return null;
    } lastly {
      setMintLoading(false);
    }
  };

  const addWallet = async () => {
    setAddWalletLoading(true);
    const payload = await fetch(`/api/add-to-allowlist`, {
      technique: "POST",
      physique: JSON.stringify({ tackle }),
    });

    const payloadJson = await payload.json();
    setAddWalletLoading(false);

    if (payloadJson.success) {
      alert(payloadJson.message);
    } else {
      alert(payloadJson.error);
    }
  };
Enter fullscreen mode

Exit fullscreen mode

If you happen to now attempt to click on any of the buttons you’ll see the loading textual content for a second.



Error dealing with

At the moment, the person might be on the mistaken community and get bizarre errors, so we’ll disable the button if the person shouldn’t be on the proper community.

Get entry to the community through the use of the useNetwork hook:

const community = useNetwork();
Enter fullscreen mode

Exit fullscreen mode

Will probably be imported from the thirdweb sdk:

import {
  useAddress,
  useEdition,
  useMetamask,
  useNetwork,
} from "@thirdweb-dev/react";
Enter fullscreen mode

Exit fullscreen mode

Now, within the mint button add the checks-

<button
  className={types.btn}
  disabled= community[0]?.knowledge?.chain?.id !== ChainId.Mumbai
  onClick={() => mintWithSignature()}
>
  {community[0]?.knowledge?.chain?.id === ChainId.Mumbai
    ? mintLoading
      ? "loading..."
      : "Mint"
    : "Swap to Mumbai"}
</button>
Enter fullscreen mode

Exit fullscreen mode

I constructed this Dapp on the Mumbai community so I’m checking for the Mumbai community however it is advisable do that for the community you constructed upon.



Conclusion

This was rather a lot, now give your self a pat on the again and share your wonderful apps with us! If you wish to take a look on the code, take a look at the GitHub Repository.

Add a Comment

Your email address will not be published. Required fields are marked *