How to create a seamless NFT Checkout experience

How to create a seamless NFT Checkout experience  - thirdweb Guides

This guide will show you how to create a seamless NFT checkout experience for your users using Magic and Stripe.

We'll walk through the following:

  1. Creating an edition contract for the NFT
  2. Creating a Stripe account for accepting payments
  3. Creating a full-stack Next.js application

Let's get started!

Deploying an ERC1155 contract

I will use an edition contract for the NFTs, but you can also use ERC721 to create unique ones!

To deploy the contract, head to the Contracts page in your thirdweb Dashboard and hit "Deploy new contract":

deploy new contract

You will be taken to our Explore page — where you can browse smart contracts built by the top protocols in web3 and deploy them in just a few clicks!

Note: You can also use the thirdweb CLI to set up a smart contract environment by running the below command from your terminal:

npx thirdweb create contract

This will take you through an easy-to-follow flow of steps for you to create your contract. Learn more about this in our CLI guide.

Otherwise, let's get back to Explore:

thirdweb explore page


In here, select your smart contract of choice. For this guide, we're going to use the Edition contract to create our NFT collection. Click Deploy Now to start the deployment flow:

thirdweb's Edition Contract

Set up your smart contract with an image, name, description, etc., and configure which wallet address will receive the funds from primary and secondary sales:

You can select any network you like; for this guide, I am choosing Mumbai. Learn more about the different networks we have available below:

Which Blockchain & Network Should I Use?
Learn the typical workflow for deploying smart contracts on to a blockchain, with an overview of the different options available to you on thirdweb.

Once the contract is deployed, go to the NFT tab and click on mint. A drawer will open where you need to add the metadata of your NFT.

Add metadata for the NFT you want to mint

Once you have entered the metadata, click on Mint NFT and approve the transaction!

Creating a Stripe account

Go to the Stripe dashboard and create a new account:

Create a new stripe account

Once the account is created we will see the API keys:

Copy the API keys for interacting with the account

Save these keys because we are going to need them later!

Go to the Magic.link dashboard and create a new Magic Auth app like this:

Create new magic.link app

Creating the fullstack next.js app

Before we begin, you can access the complete source code for this template on GitHub.

Using the thirdweb CLI, create a new Next.js & TypeScript project with the React SDK preconfigured for you using the following command:

npx thirdweb create app --next --ts

By default, the network is Mainnet; you'll need to change it to the network you deployed your smart contracts to inside the _app.tsx file.

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

Now, add the env variables in a new .env.local file like this:

NEXT_PUBLIC_MAGIC_LINK_API_KEY=<your-magic-link-api-key>
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=<your-stripe-publishable-key>
STRIPE_SECRET_KEY=<your-stripe-secret-key>
PRIVATE_KEY=<your-private-key>
NEXT_PUBLIC_DOMAIN=<your-domain>

IMPORTANT: Using private keys as an env variable is vulnerable to attacks and is not the best practice. We are doing it in this guide for brevity, but we strongly recommend using a secret manager to store your private key.

IMPORTANT: Private Keys.
If you are using environment variables to store your private key (not recommended), never commit your private key to Git.

- Create a .gitignore file.
- Add .env to your .gitignore file

If you push your private key to GitHub, your wallet will be drained!

Adding Magic Auth

In _app.tsx add magicConnectors like this:

const magicLinkConnector = new MagicConnector({
  options: {
    apiKey: process.env.NEXT_PUBLIC_MAGIC_LINK_API_KEY as string,
    rpcUrls: {
      [ChainId.Mumbai]: "https://rpc-mumbai.maticvigil.com",
    },
  },
});

// Array of wallet connectors you want to use for your dApp.
const connectors = [magicLinkConnector];

And, pass it in the ThirdwebProvider like this:

  <ThirdwebProvider
      desiredChainId={activeChainId}
      walletConnectors={connectors}
      chainRpc={{
        [ChainId.Mumbai]: "https://rpc-mumbai.maticvigil.com",
      }}
    >
      <Component {...pageProps} />
    </ThirdwebProvider>

Now, to add sign-in with email we need to take the email from the user and connect with magic using this:

<>
  <h2 style={{ fontSize: "1.3rem" }}>Login With Email</h2>
  <form
    onSubmit={(e) => {
      e.preventDefault();
      connectWithMagic({ email });
    }}
    style={{
      width: 500,
      maxWidth: "90vw",
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      flexDirection: "row",
      gap: 16,
    }}
  >
    <input
      type="email"
      placeholder="Your Email Address"
      style={{ width: "90%", marginBottom: 0 }}
      onChange={(e) => setEmail(e.target.value)}
    />
    <button>Login</button>
  </form>
</>

We will be going to need these hooks as well:

const connectWithMagic = useMagic();
const [email, setEmail] = useState<string>("");
const address = useAddress();

Now that the user can sign in we will show the user You are signed in as: walletAddress if the user is there like this:

<div>
  {address ? (
    <>
      <p>You are signed in as: {address}</p>
    </>
  ) : (
    <>
      <h2 style={{ fontSize: "1.3rem" }}>Login With Email</h2>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          connectWithMagic({ email });
        }}
        style={{
          width: 500,
          maxWidth: "90vw",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          flexDirection: "row",
          gap: 16,
        }}
      >
        <input
          type="email"
          placeholder="Your Email Address"
          style={{ width: "90%", marginBottom: 0 }}
          onChange={(e) => setEmail(e.target.value)}
        />
        <button>Login</button>
      </form>
    </>
  )}
</div>

We will also show the user which NFT they are buying when they are signed in so, we will need to get the NFT details from the contract like this:

const { contract } = useContract("YOUR_CONTRACT_ADDRESS", "edition");
const { data: nft } = useNFT(contract, 0);

And below the you are signed in as line add the following:

<div>
  {nft?.metadata && (
    <ThirdwebNftMedia
      metadata={nft?.metadata}
      style={{ width: 200, height: 200 }}
    />
  )}
  <h2>{nft?.metadata?.name}</h2>
  <p>{nft?.metadata?.description}</p>
  <p>Price: 100$</p>
</div>

Handling payments using Stripe

Now, we need to set up some APIs and logic for getting the payments. So, firstly we will install some packages:

npm i stripe micro @stripe/stripe-js @stripe/react-stripe-js # npm

yarn add stripe micro @stripe/stripe-js @stripe/react-stripe-js # yarn

Now, we will create an api in the pages/api folder called stripe_intent.ts and add the following:

import { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2022-11-15",
});

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  const { address } = req.body;

  const amount = 10000;

  try {
    const payment_intent = await stripe.paymentIntents.create({
      amount: amount,
      currency: "usd",
      description: "Payment description",
      automatic_payment_methods: {
        enabled: true,
      },
      metadata: { address },
    });

    return res.status(200).json(payment_intent);
  } catch (err) {
    const errorMessage =
      err instanceof Error ? err.message : "Internal server error";
    return res.status(500).json({ statusCode: 500, message: errorMessage });
  }
};

export default handler;
💡
Remember to update the amount and currency. The amount will be in the subdivision of a currency so for USD it will be in cents. To get that we simply need to multiply the USD amount by 100.

Now, we will make an API request on the frontend to get and store the clientSecret in a state like this:

  const [clientSecret, setClientSecret] = useState("");

  useEffect(() => {
    if (address) {
      fetch("/api/stripe_intent", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          address,
        }),
      })
        .then((res) => res.json())
        .then((data) => {
          setClientSecret(data.client_secret);
        });
    }
  }, [address]);

Since we need to send the address as metadata we won't send a request unless the address is there.

Now, we also need to show the card elements where people will enter the card details, so for that let's prepare our options that Stripe needs like this:

const stripe = loadStripe(
  process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY as string
);

const appearance: Appearance = {
  theme: "night",
  labels: "above",
};

const options: StripeElementsOptions = {
  clientSecret,
  appearance,
};

Feel free to mess around with the appearance based on your need!

Now, just before the NFT card, I will display the form inside a Stripe element wrapper like this:

{clientSecret && (
    <Elements options={options} stripe={stripe}>
      <Form />
    </Elements>
  )}

We need to import all this like this:

import { Elements } from "@stripe/react-stripe-js";
import {
  Appearance,
  loadStripe,
  StripeElementsOptions,
} from "@stripe/stripe-js";

You may be wondering where we get this Form from; we are going to create this now!
To accomplish this, create a new components folder and a new file Form.tsx inside it. Once the file gets created, add the following:

import {
  PaymentElement,
  useElements,
  useStripe,
} from "@stripe/react-stripe-js";
import React, { useState } from "react";

const Form = () => {
  const [isLoading, setIsLoading] = useState(false);
  const stripe = useStripe();
  const elements = useElements();
  const URL = process.env.NEXT_PUBLIC_DOMAIN || "http://localhost:3000";

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    if (!stripe || !elements) {
      return console.error("not loaded");
    }

    setIsLoading(true);

    await stripe.confirmPayment({
      elements,
      confirmParams: {
        return_url: URL,
      },
    });

    setIsLoading(false);
  };

  return (
    <form onSubmit={handleSubmit}>
      <PaymentElement />
      <button disabled={isLoading || !stripe || !elements}>
        <span>{isLoading ? "Loading..." : "Pay now"}</span>
      </button>
    </form>
  );
};

export default Form;

This code will add payment elements where users can input their details and a payment button.

Sending the NFT on a successful payment

We can now accept payments, but where do we send the NFT? To get updates from Stripe, we are going to use a webhook. To create a webhook, create a webhook.ts file in pages/api and add the following:

import Stripe from "stripe";
import { buffer } from "micro";
import { NextApiRequest, NextApiResponse } from "next";
import { ThirdwebSDK } from "@thirdweb-dev/sdk";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, {
  apiVersion: "2022-11-15",
});

const webhookSecret = process.env.WEBHOOK_SECRET_KEY as string;

export const config = {
  api: {
    bodyParser: false,
  },
};

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  const buf = await buffer(req);
  const sig = req.headers["stripe-signature"];

  const sdk = ThirdwebSDK.fromPrivateKey(
    process.env.PRIVATE_KEY as string,
    "mumbai"
  );

  const nftCollection = await sdk.getContract(
    "YOUR_EDITION_ADDRESS",
    "edition"
  );

  let event;

  if (buf && sig) {
    try {
      event = stripe.webhooks.constructEvent(buf, sig, webhookSecret);
    } catch (err) {
      return res.status(400).send(`Webhook Error: ${(err as Error).message}`);
    }

    const data = JSON.parse(String(buf));

    if (event.type === "payment_intent.succeeded") {
      const paymentMethod = event.data.object as any;
      const address = paymentMethod.metadata.address;

      const tx = await nftCollection.erc1155.mintAdditionalSupplyTo(
        address,
        0,
        1
      );

      console.log(tx);

      console.log(
        `PaymentIntent was successfull for: ${data.data.object.amount}`
      );
    }
  }
  return res.json({ received: true });
};

export default handler;
💡
Remember to update your contract address

We have set up our webhook, but we need to get updates on it as well; for testing on localhost, we need to test to install the Stripe cli. Once you have installed the Stripe CLI, run stripe login to log into your account. Now, finally, run this command to listen to the webhooks in real-time:

stripe listen --forward-to localhost:3000/api/webhook

Once you run this command it will show a signing secret key:

Add this in your .env.local file like this:

WEBHOOK_SECRET_KEY=secret_key_here

If you try out the app now with any test card that Stripe Provides, you will see that the payment will go through on the dashboard, and the supply of the NFT will increase by one 🥳.

Payment succeeded on stripe

But nothing happens once the user has completed the payment. So to let them know that the payment was successful or if it has errored out, we will show a message in the form as follows:

const [message, setMessage] = useState<null | string | undefined>(null);
const [isSuccess, setIsSuccess] = useState(false);
const stripe = useStripe();

useEffect(() => {
  if (!stripe) {
    return;
  }

  const clientSecret = new URLSearchParams(window.location.search).get(
    "payment_intent_client_secret"
  );

  if (!clientSecret) {
    return;
  }

  stripe.retrievePaymentIntent(clientSecret).then(({ paymentIntent }) => {
    switch (paymentIntent?.status) {
      case "succeeded":
        setIsSuccess(true);
        setMessage("Your payment was successfull!");
        break;
      case "processing":
        setMessage("Your payment is processing.");
        break;
      case "requires_payment_method":
        setMessage("Your payment was not successful, please try again.");
        break;
      default:
        setMessage("Something went wrong.");
        break;
    }
  });
}, [stripe]);
return (
  <>
    {message ? (
      <>
        {isSuccess && (
          <a
            href="https://testnets.opensea.io/assets/mumbai/YOUR_EDITION_ADDRESS/0"
            target="_blank"
            rel="noreferrer"
          >
            Check out your NFT
          </a>
        )}
        <h1>{message}</h1>
      </>
    ) : (
      <form onSubmit={handleSubmit}>
        <PaymentElement />
        <button disabled={isLoading || !stripe || !elements}>
          <span>{isLoading ? "Loading..." : "Pay now"}</span>
        </button>
      </form>
    )}
  </>
);

Now if the user returns, there will be an id in the URL, which we can check using stripe and show a message based on that. We also show a button to the nft; here, you need to update the address. If we now test it out, everything works perfectly! 🎉

Deployment

To get this app to production, you need to do some deployment steps.

Stripe

Go to your stripe dashboard and activate payments. You need to provide some details to activate your account:

Verify your stripe account to accept real money

Upon completion, go to the developer's section and click on the api keys tab. You will be able to see your production api keys. Store it safely somewhere because we need it when we deploy our app.

Finally, we need to add a webhook endpoint to Stripe. Go to the webhooks section under developers and click on add an endpoint. Then add your details,

  • Endpoint URL would be https://<your_domain>/api/webhook
  • Enter anything as a description if you want to
  • Update the version to the latest as we are using that
  • Select payment_intent.succeeded as an event to listen.
Add webhook of live URL to stripe

You can use any hosting service; I will use Vercel. So, create a GitHub repository and push the code you wrote. Go to Vercel and create a new project. You will need to select the GitHub repository you created and click import. Now, add all the updated env vars:

NEXT_PUBLIC_MAGIC_LINK_API_KEY=<your-magic-link-api-key>
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=<your-stripe-publishable-key>
STRIPE_SECRET_KEY=<your-stripe-secret-key>
WEBHOOK_SECRET_KEY=<your-stripe-webhook-secret-key>
PRIVATE_KEY=<your-private-key>
NEXT_PUBLIC_DOMAIN=<your-domain>

Make sure to update the stripe keys to the ones that we just got!

Conclusion

This guide taught us how to create a seamless NFT checkout experience for your users by allowing signup by Magic.link and accepting credit card payments using Stripe!

You learned a lot, now pat yourself on the back and share your amazing apps with us! If you want to look at the code, check out the GitHub Repository.