How To Implement Stripe Subscription In Next Js 14 Application Without Build Fail

How To Implement Stripe Subscription In Next Js 14 Application Without Build Fail

Stripe Subscription Implementation in Next Js 14

·

4 min read

In this article I will share how to imeplement stripe subscription in next js applications. Stripe is the most used payment gateway. Nowadays SAAS softwares are very popular. And most of the SAAS is using stripe subscription feature.

I am Sk Shoyeb and I was making our SAAS product Clocknotes. Here I have to struggle the proper method to implement stripe subscription method. There was many resources but most of them are outdated or has build errors.

Create Checkout Action

To redirect to the stripe checkout page you need to create a checkout action. So make a server action file and write the code for create checkout action.

export const createCheckoutSession = async () => {
  const user = await getSession();
  const session = await stripe.checkout.sessions.create({
    line_items: [
      {
        price: "price_1OzJxMSEFisHGGgezP8ysUES",
        quantity: 1,
      },
    ],
    customer: user?.stripeCustomerId || "",
    subscription_data: {
      metadata: {
        id: user?.stripeCustomerId || "",
      },
    },
    mode: "subscription",
    success_url: `${process.env.APP_URL}/payment/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.APP_URL}/payment?canceled=true`,
  });
  redirect(session.url || "");
};

Here, you need to add a feild in your user table named stripeCustomerId to save your customer ID. Here I am sending it manualy because when a new checkout happens stripe generate new customer Id which will lose your previous customer data. If you send it manually it will automatically fetch all your customer data and the customer can perform checkout quickly.

Make the Success route

Create a success folder under payment folder. Next add route.ts under success folder.

export async function GET(req: NextRequest) {
  const user = await getSession();
  const session_id = req.nextUrl.searchParams.get("session_id");
  if (!session_id) {
    redirect("/payment");
  }
  const checkoutSession = await stripe.checkout.sessions.retrieve(session_id);
  const customerId = checkoutSession.customer as string;

  await db.user.update({
    where: {
      id: user.id,
    },
    data: {
      stripeCustomerId: customerId,
      expirationDate: null,
    },
  });
  redirect("/payment");
}

After success I am saving the customer id in database and make a expirationDate so that if the customer cancel the subscription the expirationDate will be added in the database.

Page.tsx

This is your payment page.

import ProPlan from "@/app/(main)/billing/_components/ProPlan";

import {
  Card,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { FileSliders } from "lucide-react";
import {
  createPortalSession,
createCheckoutSession
} from "@/app/(main)/billing/_components/actions/billing.actions";


const Billing = async () => {
  const subs = await hasSubscription();
  return (
    <div>
      {subs ? (
        <>
          <Card>
            <CardHeader>
              <CardTitle>Pro Plan Activated</CardTitle>
              <CardDescription>
                You are currently on a pro plan. In the pro plan your all
                features are unlocked. You can manage your subscription clicking
                on the manage subscription buttion
              </CardDescription>
            </CardHeader>

            <CardFooter>
              <form action={createPortalSession}>
                <Button type="submit">
                  <FileSliders className="mr-2 h-4 w-4" /> Manage Subscription
                </Button>
              </form>
            </CardFooter>
          </Card>
        </>
      ) : (
        <div className="flex gap-12 mt-4">
        <Card className="w-[380px]">
      <CardHeader>
        <CardTitle className="flex justify-between">
          <span>Pro Plan</span> <span>$7.99/Mo</span>
        </CardTitle>
        <CardDescription>
          Pro plan allows you access all the pro features. This is the monthly
          plan
        </CardDescription>
      </CardHeader>
      <CardContent className="grid gap-4">
        {proPlan?.map((item, index) => (
          <div
            className="mb-4 grid grid-cols-[25px_1fr] items-center gap-4 pb-1 last:mb-0 last:pb-0"
            key={index}
          >
            <CheckCheck className="text-sky-500" />
            <div className="space-y-1">
              <p className="text-sm font-medium leading-none">{item}</p>
            </div>
          </div>
        ))}
      </CardContent>
      <CardFooter>
        <form action={createCheckoutSession}>
          <input type="hidden" name="lookup_key" value="pro-monthly" />
          <Button className="w-full" type="submit">
            <CreditCard className="mr-2 h-4 w-4" /> Upgrade
          </Button>
        </form>
      </CardFooter>
    </Card>
        </div>
      )}
    </div>
  );
};
export default Billing;

After that we need a function for getting is the user subscription active or not. If active then it will show the manage subscription button and if it not active it will show the payment card.

Create PortalSession

We need to make a server action function for redirect the user to the manage stripe portal.

export const createPortalSession = async () => {
  const user = await getSession();
  const userData = await db.tenant.findUnique({
    where: { id: user.id},
  });
  if (!userData ) throw new Error("No tenant found");
  if (!userData .stripeCustomerId) throw new Error("No stripe customer id");

  const portalSession = await stripe.billingPortal.sessions.create({
    customer: userData.stripeCustomerId,
    return_url: `${process.env.APP_URL}/payment`,
  });
  redirect(portalSession.url);
};

Check If User Is Subscribed

Make a stripe.ts file under lib folder.

export const stripe = new Stripe(String(process.env.STRIPE_SECRET_KEY), {
  apiVersion: "2023-10-16",
});

export const hasSubscription = async () => {
  const user = await getSession();
  if (user) {
    const userData = await prisma.user.findUnique({
      where: {
        id: user.id,
      },
    });
    if (!userData?.stripeCustomerId) {
      return false;
    }
    const subscriptions = await stripe.subscriptions.list({
      customer: String(userData?.stripeCustomerId),
    });
    return subscriptions.data.length > 0;
  }
  return false;
};

You need to make this file first because the stripe variable may use in other functions.

Webhooks

Webhooks check the event between your application and the stripe server. Like if you want to send email in payment successful then you need to use webhooks. Here we will add expriationDate on user cancel a subscription.

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

export async function POST(req: NextRequest) {
  const payload = await req.text();
  const res = JSON.parse(payload);

  const sig = req.headers.get("Stripe-Signature");

  try {
    let event = stripe.webhooks.constructEvent(payload, sig!, ENDPOINT_SECRET!);

    switch (event.type) {
      case `customer.subscription.updated`:
        const subscription = event.data.object as Stripe.Subscription;
        await prisma.user.update({
          where: {
            id: subscription.metadata.userId,
          },
          data: {
            expirationDate: subscription.cancel_at
              ? new Date(subscription.cancel_at * 1000)
              : null,
          },
        });
    }

    return NextResponse.json({
      status: "success",
      event: event.type,
      response: res,
    });
  } catch (error) {
    return NextResponse.json({ status: "Failed", error });
  }
}

This stripe implementation will help you in making a build error free application. This was the method of stripe subscription implementation in next js 14.