DEV Community

Cover image for Building a Payment Flow in Next.js 13 Using Stripe, Mailsender, and Webhooks
Levi Schouten
Levi Schouten

Posted on

Building a Payment Flow in Next.js 13 Using Stripe, Mailsender, and Webhooks

Introduction:
In my latest project, WanderKit.net, users can effortlessly generate personalised itineraries for their upcoming trips using AI. To enhance the user experience and provide a seamless journey, I decided to implement a payment flow feature. This would allow users to purchase the generated itineraries and have them securely stored in our database, enabling easy access and integration with their calendars. In this blog, I'll walk you through the process of setting up the payment flow using Stripe, integrating with Mailsender for confirmation emails, and handling webhooks for successful payments.

Requirements:
Before starting, ensure you have the following API keys from Stripe, obtained after signing up for their services. Use the test keys during development:

NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=""
STRIPE_SECRET_KEY=""
STRIPE_WEBHOOK_SECRET=""
Enter fullscreen mode Exit fullscreen mode

Creating the Checkout Endpoint:
The first step was to create a checkout endpoint on the server-side. This endpoint creates a checkout session and returns it to the client.

// src/app/api/checkout/route.ts

import Stripe from "stripe";
import { NextResponse } from "next/server";

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

export async function POST(req: Request) {
  try {
    const body = await req.json();

    if (!body.itineraryId) {
      throw new Error("Missing itinerary_id");
    }

    const params: Stripe.Checkout.SessionCreateParams = {
      submit_type: "pay",
      payment_method_types: ["card"],
      mode: "payment",
      line_items: [
        {
          price_data: {
            currency: 'usd',
            product_data: {
              name: "Itinerary",
            },
            unit_amount: 100,
          },
          quantity: 1,
        },
      ],
      metadata: {
        itinerary_id: body.itineraryId,
      },
      success_url: `${process.env.URL}/success?itineraryId=${body.itineraryId}`,
      cancel_url: `${process.env.URL}/`,
    };

    const checkoutSession: Stripe.Checkout.Session =
      await stripe.checkout.sessions.create(params);

    return NextResponse.json({ result: checkoutSession, ok: true });
  } catch (error) {
    console.error(error);
    return NextResponse.json(
      { message: "something went wrong", ok: false },
      { status: 500 }
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

Client-Side Payment Handling:
On the client-side, I implemented a handle payment function connected to a button. This function calls the previously created endpoint with the necessary data (in this case, the itinerary). Upon response, it redirects the user to the checkout page, handling any potential errors.

export default function Home() {
  // ...

  const handlePayment = async () => {
    try {
      if (!itinerary) {
        throw new Error("Something went wrong");
      }

      const response = await fetch("api/checkout/", {
        method: "post",
        body: JSON.stringify({ itineraryId: itinerary.id }),
      });

      const json = await response.json();

      if (!json.ok) {
        throw new Error("Something went wrong");
      }

      const stripe = await getStripe();

      if (!stripe) {
        throw new Error("Something went wrong");
      }

      await stripe.redirectToCheckout({ sessionId: json.result.id });
    } catch (error) {
      toast("Failed to start transaction", "Please try again.");
    }
  };

  return (
    // ...
    <Button onClick={handlePayment}>Purchase</Button>  
    // ...
  )

}
Enter fullscreen mode Exit fullscreen mode

Setting Up Webhooks:
Webhooks are essential for receiving real-time updates from Stripe once the payment is successful. It ensures we process the payment confirmation securely. Stripe verifies its signature by parsing the raw string rather than the request body. In the webhook, I checked if the event type was checkout.session.completed, and if so, I updated the database accordingly and sent the user a verification email using Mailsender.

// src/app/api/webhooks/checkout/route.ts

import Cors from "micro-cors";
import { headers } from "next/headers";
import { NextResponse } from "next/server";

const stripe = require("stripe")(process.env.STRIPE_PRIVATE);

const cors = Cors({
  allowMethods: ["POST", "HEAD"],
});

const secret = process.env.STRIPE_WEBHOOK_SECRET || "";

export async function POST(req: Request) {
  try {
    const body = await req.text();

    const signature = headers().get("stripe-signature");

    const event = stripe.webhooks.constructEvent(body, signature, secret);

    if (event.type === "checkout.session.completed") {
      if (!event.data.object.customer_details.email) {
        throw new Error(`missing user email, ${event.id}`);
      }

      if (!event.data.object.metadata.itinerary_id) {
        throw new Error(`missing itinerary_id on metadata, ${event.id}`);
      }

      updateDatabase(event.data.object.metadata.itinerary_id);
      sendEmail(event.data.object.customer_details.email);

    }

    return NextResponse.json({ result: event, ok: true });
  } catch (error) {
    console.error(error);
    return NextResponse.json(
      {
        message: "something went wrong",
        ok: false,
      },
      { status: 500 }
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

Testing Locally:
To test the payment flow locally, I installed the Stripe CLI https://stripe.com/docs/stripe-cli and added a script to the package.json file. Running "stripe:listen" ensures that webhooks are directed to our local project while developing. I could then click through the app, proceed with a test payment (in test mode using a provided credit card number), and verify that the webhook was triggered.

"stripe:listen": "stripe listen --forward-to localhost:3000/api/webhooks/checkout"
Enter fullscreen mode Exit fullscreen mode

Manually trigger a webhook with the following command, adding whatever parameters you need for testing.

stripe trigger checkout.session.completed --add checkout_session:metadata.itinerary_id=123
Enter fullscreen mode Exit fullscreen mode

Setting Up Webhooks for Production:
After successful testing, I configured a new webhook in the Stripe developer dashboard for the production environment, ensuring the smooth operation of the payment flow in the live system. https://dashboard.stripe.com/webhooks

Conclusion:
By integrating Stripe, Mailsender, and webhooks into my Next.js 13 app, WanderKit.net now offers users a convenient payment flow to purchase their AI-generated itineraries. The entire process is secure and efficient, providing travelers with an unforgettable journey planning experience!

Checkout the live product at wanderkit.net, read my other blogs on levischouten.net

Top comments (0)