How to implement subscription model using LemonSqueezy in Next.js (13.4 stable app router)
Tags: nextjs, saas, react, beginners
Published: 2023-05-27 06:00:00

## Table of contents

- [Introduction](#introduction)
- [Getting Started](#getting-started)
- [Setup Next.js app](#setup-nextjs-app)
- [User Registration](#user-registration)
- [Setup LemonSqueezy account](#setup-lemonsqueezy-account)
- [How subscription works](#how-subscription-works)
- [Implementing Subscription](#implementing-subscription)
  - [Creating a checkout](#creating-a-checkout)
  - [Synchronization with Webhook](#synchronization-with-webhook)
  - [Check the subscription status](#check-the-subscription-status)
  - [Manage subscription](#manage-subscription)
- [Deployment](#deployment)
- [Conclusion](#conclusion)
- [Connect with me](#connect-with-me)

## Introduction

Hey guys! In this blog post, we will implement a subscription model using LemonSqueezy in our Next.js app with the latest App router. If you're interested in learning about it, this is the post for you.

## Getting Started

Let me tell you what I am going to cover in this blog post. First, we are going to bootstrap our Next.js project. Then we will set up our LemonSqueezy account. After that, we will learn how the subscription process works and how we are going to integrate LemonSqueezy subscriptions into our Next.js app. Then we will write code and finally deploy our app on Vercel. So we will cover everything from start to finish. This blog post expects you to have familiarity with React and Next.js (App router). If you know the basics you are good to go.

What we will be building today? We are going to build a simple app that will let the user create an account and subscribe to our product. Later he can cancel it anytime, change credit card credentials, and all that. So that’s the overview of what we are going to build.

## Setup Next.js app

For this tutorial, I have created a [GitHub repo](https://github.com/developeratul/lemonsqueezy-nextjs) that has a branch called `starter` You will have to follow a simple GitHub workflow and checkout to this branch to get the starter code to follow along with this tutorial. The final code will be in the `main` branch.

## User Registration

Let’s create a very simple authentication where users can register and log in. No session management and all that other authorization stuff since that’s not the main purpose of this tutorial.

Let’s make some edits in the `app/sign-up/page.tsx`

```tsx
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import React from "react";
import { toast } from "react-hot-toast";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { axios } from "~/lib/axios";
import { useAuth } from "~/providers/auth";
import { SignUpResponse } from "../api/auth/sign-up/route";

export default function SignUp() {
  const [input, setInput] = React.useState({ email: "", password: "" });
  const { setUser, user } = useAuth();
  const router = useRouter();
  const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = ({
    target: { value, name },
  }) => {
    setInput((pre) => ({ ...pre, [name]: value }));
  };
  const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (e) => {
    try {
      e.preventDefault();

      const { email, password } = input;

      if (!email || !password) return;

      const { user, message } = await axios.post<any, SignUpResponse>("/api/auth/sign-up", {
        email,
        password,
      });

      setUser(user);
      router.push("/");
      toast.success(message);
    } catch (err) {
      //
    }
  };
  return (
    <div className="flex flex-col justify-center items-center gap-8 w-full">
      <div className="w-full max-w-md bg-white/5 p-8 rounded-lg border border-gray-900">
        <form onSubmit={handleSubmit} className="flex flex-col justify-center items-center gap-4">
          <h2 className="text-2xl">Sign up</h2>
          <Input
            required
            type="email"
            onChange={handleInputChange}
            name="email"
            placeholder="Enter email"
          />
          <Input
            required
            type="password"
            placeholder="Enter password"
            onChange={handleInputChange}
            name="password"
          />
          <Button type="submit" className="w-full">
            Sign up
          </Button>
        </form>
      </div>
      <div>
        <p>
          Already have an account?{" "}
          <Link className="text-blue-300 hover:underline underline-offset-4" href="/login">
            Login
          </Link>
        </p>
      </div>
    </div>
  );
}
```

Let create the API route. `app/api/sign-up/route.ts`

```tsx
import bcrypt from "bcryptjs";
import { NextResponse } from "next/server";
import { prisma } from "~/prisma/db";
import { User } from "~/providers/auth";

export type SignUpResponse = {
  user: User;
  message: string;
};

export async function POST(req: Request) {
  try {
    const { email, password } = await req.json();

    const prevUser = await prisma.user.findUnique({
      where: { email },
    });

    if (prevUser) return NextResponse.json({ message: "Email already in use" }, { status: 409 });

    const hashedPassword = await bcrypt.hash(password, 12);

    const user = await prisma.user.create({
      data: { email, password: hashedPassword },
      select: { id: true, email: true },
    });

    return NextResponse.json({
      user,
      message: "Your account has been created!",
    } satisfies SignUpResponse);
  } catch (err) {
    if (err instanceof Error) {
      return NextResponse.json({ message: err.message }, { status: 500 });
    }
  }
}
```

We are done with sign-up. Now let’s implement login.

`app/login/page.tsx`

```tsx
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import React from "react";
import { toast } from "react-hot-toast";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { axios } from "~/lib/axios";
import { useAuth } from "~/providers/auth";
import { SignUpResponse } from "../api/auth/sign-up/route";

export default function Login() {
  const [input, setInput] = React.useState({ email: "", password: "" });
  const { setUser, user } = useAuth();
  const router = useRouter();
  const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = ({
    target: { value, name },
  }) => {
    setInput((pre) => ({ ...pre, [name]: value }));
  };
  const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (e) => {
    try {
      e.preventDefault();

      const { email, password } = input;

      if (!email || !password) return;

      const { user, message } = await axios.post<any, SignUpResponse>("/api/auth/login", {
        email,
        password,
      });

      setUser(user);
      router.push("/");
      toast.success(message);
    } catch (err) {
      //
    }
  };
  return (
    <div className="flex flex-col justify-center items-center gap-8 w-full">
      <div className="w-full max-w-md bg-white/5 p-8 rounded-lg border border-gray-900">
        <form onSubmit={handleSubmit} className="flex flex-col justify-center items-center gap-4">
          <h2 className="text-2xl">Login</h2>
          <Input
            required
            type="email"
            placeholder="Enter email"
            name="email"
            value={input.email}
            onChange={handleInputChange}
          />
          <Input
            required
            type="password"
            placeholder="Enter password"
            name="password"
            value={input.password}
            onChange={handleInputChange}
          />
          <Button type="submit" className="w-full">
            Login
          </Button>
        </form>
      </div>
      <div>
        <p>
          Don&apos;t have an account?{" "}
          <Link className="text-blue-300 hover:underline underline-offset-4" href="/sign-up">
            Sign up
          </Link>
        </p>
      </div>
    </div>
  );
}
```

`app/api/auth/login/route.ts`

```tsx
import bcrypt from "bcryptjs";
import { NextResponse } from "next/server";
import { prisma } from "~/prisma/db";

export async function POST(req: Request) {
  const { email: emailInput, password: passwordInput } = await req.json();

  const user = await prisma.user.findUnique({
    where: { email: emailInput },
    select: { id: true, email: true, password: true },
  });

  if (!user) return NextResponse.json({ message: "Your account does not exist" }, { status: 404 });

  const { id, password, email } = user;

  const passwordMatched = await bcrypt.compare(passwordInput, password);

  if (!passwordMatched)
    return NextResponse.json({ message: "Invalid credentials" }, { status: 403 });

  return NextResponse.json({ user: { id, email }, message: "Welcome back!" });
}
```

That’s all about user registration.

## Setup LemonSqueezy account

Before I start discussing how subscription works, let’s get our LemonSqueezy account ready. For that, I would highly suggest following their [guide](https://docs.lemonsqueezy.com/guides/getting-started).

## How subscription works

A subscription is a signed agreement between a supplier and customer that the customer will receive and provide payment for regular products or services. Subscription payments are collected in different intervals like Month, Week, Year, etc.

Before the user pays, he must subscribe to our product. Once he subscribes, he will automatically get charged after the specified interval from his credit card until he cancels the subscription which he can do anytime.

Once the user has subscribed and paid for our product, we use a webhook that will send a request to our API endpoint with a payload. We will make changes in our database according to that payload and the customer will get access to the premium plan or whatever.

So that’s basically how subscription works. Now let’s start implementing it in our code.

## Implementing Subscription

If you have got your LemonSqueezy account ready, go to this [page](https://app.lemonsqueezy.com/settings/api) and generate a new API key. Also, create a test product. Here’s a [guide](https://scribehow.com/shared/Lemonsqueezy_Workflow__Lcy8ZDW4ROCPFwEsys_94A) on that. Now add this line in our `.env` file:

```
LEMONSQUEEZY_API_KEY="[YOUR API KEY]"
```

We need those credentials to proceed further:

- Store id
- Product id

You can get this through the API they provide. Make an API request to this endpoint [`https://api.lemonsqueezy.com/v1/products`](https://api.lemonsqueezy.com/v1/products) while having the authorization header present in your request.

![Postman request](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rfomzi5adllil7d8amle.png)

The first marked credential is the `Product id` and the second one is the `Store id` Now let’s add them to our `.env` file:

```
LEMON_SQUEEZY_STORE_ID="[YOUR STORE ID]"
LEMONS_SQUEEZY_PRODUCT_ID="[YOUR PRODUCT ID]"
```

### Creating a checkout

Now that we have the required credentials, let create a checkout option in our app. First, we will add a button on the `home` page.

`app/page.tsx`

```tsx
"use client";
import { useRouter } from "next/navigation";
import React from "react";
import { Button } from "~/components/ui/button";
import { axios } from "~/lib/axios";
import { useAuth } from "~/providers/auth";
import { CreateCheckoutResponse } from "./api/payment/subscribe/route";

export default function Home() {
  const { isAuthenticated, user } = useAuth();
  const router = useRouter();

  React.useEffect(() => {
    if (!isAuthenticated) {
      router.push("/login");
    }
  }, [isAuthenticated, router]);

  if (!isAuthenticated || !user) return <></>;

  const handleClick = async () => {
    try {
      const { checkoutURL } = await axios.post<any, CreateCheckoutResponse>(
        "/api/payment/subscribe",
        { userId: user.id }
      );
      window.location.href = checkoutURL;
    } catch (err) {
      //
    }
  };

  return (
    <div className="w-full max-w-md bg-white/5 border border-gray-900 p-8 rounded-lg">
      <div className="flex flex-col gap-4">
        <h2 className="text-2xl">Your profile</h2>
        <Button onClick={handleClick}>Subscribe</Button>
      </div>
    </div>
  );
}
```

![App preview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t9izi2a3cjlwpolfm9ij.png)

Before proceeding furture, we need to install a package:

```bash
yarn add lemonsqueezy.ts
```

This is an unofficial wrapper around the official API. Lets initialize it.

`lib/lemons.ts`

```tsx
import { LemonsqueezyClient } from "lemonsqueezy.ts";

export const client = new LemonsqueezyClient(process.env.LEMONSQUEEZY_API_KEY as string);
```

Let’s create the API route for creating a checkout.

`app/api/payment/subscribe/route.ts`

```tsx
import type { CreateCheckoutResult } from "lemonsqueezy.ts/dist/types";
import { NextResponse } from "next/server";
import { axios } from "~/lib/axios";
import { client } from "~/lib/lemons";
import { prisma } from "~/prisma/db";

export type CreateCheckoutResponse = {
  checkoutURL: string;
};

export async function POST(request: Request) {
  try {
    const { userId } = await request.json();

    const user = await prisma.user.findUnique({
      where: { id: userId },
      select: { id: true, email: true },
    });

    if (!user) return NextResponse.json({ message: "Your account was not found" }, { status: 404 });

    const variant = (
      await client.listAllVariants({ productId: process.env.LEMONS_SQUEEZY_PRODUCT_ID })
    ).data[0];

    const checkout = (await axios.post(
      "https://api.lemonsqueezy.com/v1/checkouts",
      {
        data: {
          type: "checkouts",
          attributes: { checkout_data: { email: user.email, custom: [user.id] } },
          relationships: {
            store: { data: { type: "stores", id: process.env.LEMON_SQUEEZY_STORE_ID } },
            variant: { data: { type: "variants", id: variant.id } },
          },
        },
      },
      { headers: { Authorization: `Bearer ${process.env.LEMONSQUEEZY_API_KEY}` } }
    )) as CreateCheckoutResult;

    return NextResponse.json({ checkoutURL: checkout.data.attributes.url }, { status: 201 });
  } catch (err: any) {
    return NextResponse.json({ message: err.message || err }, { status: 500 });
  }
}
```

Let me explain this code to you. First, we are checking if the user exists or not. Secondly, we are listing all the variants of our product. In _Stripe_, we call it **Price** but in _LemonSqueezy_, we call it a **Variant**. A **variant id** is required for creating a checkout. If you have not created any variants for your product, LemonSqueezy automatically created a default variant for your product. In that case, that is what we are retrieving. Thirdly, We are creating a checkout through the API. I have passed the user email and the Id as the payload. This will be needed when we will deal with webhooks. I have also passed some required fields in the body and a auth header. Finally, send the checkout URL within the response. The client will redirect the user to this URL.

### Synchronization with Webhook

Now that we are done with checkout where users can pay and subscribe to our product, it’s time to keep in sync with them. To create and test webhooks locally, we can use a service like ngrok. I have a [Twitter thread](https://twitter.com/developeratul/status/1645260200906043404) on how it’s done. So not going into much detail.

<!-- This embed syntax works on DEV.to -->

{% twitter 1645260200906043404 %}

Run the following command once you have got ngrok installed on your computer.

```bash
ngrok http 3000 # or wherever your app is running
```

![Ngrok preview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c9eif0fasmrdqiggnlfx.png)

Copy this URL. Go to this [page](https://app.lemonsqueezy.com/settings/webhooks) on your LemonSqueezy dashboard, create a new webhook, and add this as a callback URL which should look like this `http://[YOUR URL]/payment/webhook`. We still have to create this route. Then, add a signing secret. We will listen to those events:

- subscription_created
- subscription_updated

Now we have to update our `.env` file.

```
LEMONS_SQUEEZY_SIGNATURE_SECRET="[YOUR SECRET]"
```

Whenever a user checks out and subscribes to our product, LemonSqueezy is going to send a post request to the endpoint specified in the webhook with a payload. So now let’s create that route. Before that, run the following command to install a package that will be used to validate the webhook signature.

```bash
yarn add raw-body
```

Now lets prepare our route for handling webhook requests.

`api/api/payment/webhook/route.ts`

```tsx
import { Buffer } from "buffer";
import crypto from "crypto";
import { headers } from "next/headers";
import { NextResponse } from "next/server";
import rawBody from "raw-body";
import { Readable } from "stream";
import { client } from "~/lib/lemons";
import { prisma } from "~/prisma/db";

export async function POST(request: Request) {
  const body = await rawBody(Readable.from(Buffer.from(await request.text())));
  const headersList = headers();
  const payload = JSON.parse(body.toString());
  const sigString = headersList.get("x-signature");
  const secret = process.env.LEMONS_SQUEEZY_SIGNATURE_SECRET as string;
  const hmac = crypto.createHmac("sha256", secret);
  const digest = Buffer.from(hmac.update(body).digest("hex"), "utf8");
  const signature = Buffer.from(
    Array.isArray(sigString) ? sigString.join("") : sigString || "",
    "utf8"
  );

  // Check if the webhook event was for this product or not
  if (
    parseInt(payload.data.attributes.product_id) !==
    parseInt(process.env.LEMONS_SQUEEZY_PRODUCT_ID as string)
  ) {
    return NextResponse.json({ message: "Invalid product" }, { status: 403 });
  }

  // validate signature
  if (!crypto.timingSafeEqual(digest, signature)) {
    return NextResponse.json({ message: "Invalid signature" }, { status: 403 });
  }

  const userId = payload.meta.custom_data[0];

  // Check if custom defined data i.e. the `userId` is there or not
  if (!userId) {
    return NextResponse.json({ message: "No userId provided" }, { status: 403 });
  }

  switch (payload.meta.event_name) {
    case "subscription_created": {
      const subscription = await client.retrieveSubscription({ id: payload.data.id });

      await prisma.user.update({
        where: { id: userId },
        data: {
          subscriptionId: `${subscription.data.id}`,
          customerId: `${payload.data.attributes.customer_id}`,
          variantId: subscription.data.attributes.variant_id,
          currentPeriodEnd: subscription.data.attributes.renews_at,
        },
      });
    }

    case "subscription_updated": {
      const subscription = await client.retrieveSubscription({ id: payload.data.id });

      const user = await prisma.user.findUnique({
        where: { subscriptionId: `${subscription.data.id}` },
        select: { subscriptionId: true },
      });

      if (!user || !user.subscriptionId) return;

      await prisma.user.update({
        where: { subscriptionId: user.subscriptionId },
        data: {
          variantId: subscription.data.attributes.variant_id,
          currentPeriodEnd: subscription.data.attributes.renews_at,
        },
      });
    }

    default: {
      return;
    }
  }
}
```

Let me explain this code to you. First, we are validating the webhook signature for security concerns. Secondly, we are checking if the webhook was for the specific product which we are integrating into this app. Then we are checking if the payload contains our custom-defined credential in that case, it’s the `userId`. After that, we have two switch cases. One for the `subscription_created` and the other for the `subscription_updated` event.

1. `subscription_created` Whenever a user subscribes and pays for our product, this event is called. When this event is called, we wanna capture the **subcriptionId**, **customerId**, **variantId,** and the **currentPeriodEnd** in our database**.**
2. `subscription_updated` Whenever the subscription gets updated, this event is called. Updating a subscription refers to Paying at the period’s end, canceling the subscription, etc. When this event is called, we wanna update our **variantId** (just in case, the user picks a different pricing/variant) and the **currentPeriodEnd.**

> I will explain the purpose of storing **currentPeriodEnd** later in this blog when will check the user’s subscription status.

### Check the subscription status

Now that, we are done with checkout and synchronization, it’s time to fetch the user’s subscription status and limit his account accordingly.

`lib/subscription.ts`

```tsx
import { prisma } from "~/prisma/db";
import { client } from "./lemons";

export async function getUserSubscriptionPlan(userId: string) {
  const user = await prisma.user.findUnique({
    where: { id: userId },
    select: {
      subscriptionId: true,
      currentPeriodEnd: true,
      customerId: true,
      variantId: true,
    },
  });

  if (!user) throw new Error("User not found");

  // Check if user is on a pro plan.
  const isPro =
    user.variantId &&
    user.currentPeriodEnd &&
    user.currentPeriodEnd.getTime() + 86_400_000 > Date.now();

  const subscription = await client.retrieveSubscription({ id: user.subscriptionId });

  // If user has a pro plan, check cancel status on Stripe.
  let isCanceled = false;

  if (isPro && user.subscriptionId) {
    isCanceled = subscription.data.attributes.cancelled;
  }

  return {
    ...user,
    currentPeriodEnd: user.currentPeriodEnd?.getTime(),
    isCanceled,
    isPro,
    updatePaymentMethodURL: subscription.data.attributes.urls.update_payment_method,
  };
}
```

In this code above, we are first checking if the user exists or not. Then we are checking if the user is on the pro plan or not and store it in the `isPro` var. This was the purpose of storing the `currentPeriodEnd` Then if the user is on the pro plan, check if he has canceled the plan or not. Finally, return all of these within an object. We will use this function to retrieve if the user is on the pro plan or not and limit his account accordingly.

Now we will be making some updates to our home page. Until now, the home page was being rendered as a client component. Now we will render it as a server component. We have to make make our authentication persistent as well. So there will be a bit more addition to it.

`app/page.tsx`

```tsx
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getUserSubscriptionPlan } from "~/lib/subscription";
import SubscribeButton from "./SubscribeButton";

export default async function Home() {
  const cookiesList = cookies();
  const userId = cookiesList.get("userId")?.value || "";

  if (!userId) return redirect("/login");

  const { isPro } = await getUserSubscriptionPlan(userId);
  return (
    <div className="w-full max-w-md bg-white/5 border border-gray-900 p-8 rounded-lg">
      <div className="flex flex-col gap-4">
        <h2 className="text-2xl">Your profile</h2>
        {isPro ? <p>You&apos;re subscribed</p> : <SubscribeButton />}
      </div>
    </div>
  );
}
```

Fetching the subscription status and hide the subscribe button accordingly.

`app/SubscribeButton.tsx`

```tsx
"use client";
import { useRouter } from "next/navigation";
import React from "react";
import { Button } from "~/components/ui/button";
import { axios } from "~/lib/axios";
import { useAuth } from "~/providers/auth";
import { CreateCheckoutResponse } from "./api/payment/subscribe/route";

export default function SubscribeButton() {
  const { isAuthenticated, user } = useAuth();
  const router = useRouter();

  React.useEffect(() => {
    if (!isAuthenticated) {
      router.push("/login");
    }
  }, [isAuthenticated, router]);

  if (!isAuthenticated || !user) return <></>;

  const handleClick = async () => {
    try {
      const { checkoutURL } = await axios.post<any, CreateCheckoutResponse>(
        "/api/payment/subscribe",
        { userId: user.id }
      );
      window.location.href = checkoutURL;
    } catch (err) {
      //
    }
  };

  return <Button onClick={handleClick}>Subscribe</Button>;
}
```

Since the home page is a RSC now, we had to separate this button to a client component.

`app/login/page.tsx` One Small addition (Storing cookie)

```tsx
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import React from "react";
import { toast } from "react-hot-toast";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { axios } from "~/lib/axios";
import { useAuth } from "~/providers/auth";
import { SignUpResponse } from "../api/auth/sign-up/route";

export default function Login() {
  const [input, setInput] = React.useState({ email: "", password: "" });
  const { setUser, user } = useAuth();
  const router = useRouter();
  const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = ({
    target: { value, name },
  }) => {
    setInput((pre) => ({ ...pre, [name]: value }));
  };
  const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (e) => {
    try {
      e.preventDefault();

      const { email, password } = input;

      if (!email || !password) return;

      const { user, message } = await axios.post<any, SignUpResponse>("/api/auth/login", {
        email,
        password,
      });

      setUser(user);
      // ADDITION: Store the user id in a cookie 👇
      document.cookie = `userId=${user?.id}`;
      router.push("/");
      toast.success(message);
    } catch (err) {
      //
    }
  };
  return (
    <div className="flex flex-col justify-center items-center gap-8 w-full">
      <div className="w-full max-w-md bg-white/5 p-8 rounded-lg border border-gray-900">
        <form onSubmit={handleSubmit} className="flex flex-col justify-center items-center gap-4">
          <h2 className="text-2xl">Login</h2>
          <Input
            required
            type="email"
            placeholder="Enter email"
            name="email"
            value={input.email}
            onChange={handleInputChange}
          />
          <Input
            required
            type="password"
            placeholder="Enter password"
            name="password"
            value={input.password}
            onChange={handleInputChange}
          />
          <Button type="submit" className="w-full">
            Login
          </Button>
        </form>
      </div>
      <div>
        <p>
          Don&apos;t have an account?{" "}
          <Link className="text-blue-300 hover:underline underline-offset-4" href="/sign-up">
            Sign up
          </Link>
        </p>
      </div>
    </div>
  );
}
```

`app/sign-up/page.tsx` One Small addition (same as login)

```tsx
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import React from "react";
import { toast } from "react-hot-toast";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { axios } from "~/lib/axios";
import { useAuth } from "~/providers/auth";
import { SignUpResponse } from "../api/auth/sign-up/route";

export default function SignUp() {
  const [input, setInput] = React.useState({ email: "", password: "" });
  const { setUser, user } = useAuth();
  const router = useRouter();
  const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = ({
    target: { value, name },
  }) => {
    setInput((pre) => ({ ...pre, [name]: value }));
  };
  const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (e) => {
    try {
      e.preventDefault();

      const { email, password } = input;

      if (!email || !password) return;

      const { user, message } = await axios.post<any, SignUpResponse>("/api/auth/sign-up", {
        email,
        password,
      });

      setUser(user);
      // ADDITION: Store the user id in a cookie 👇
      document.cookie = `userId=${user?.id}`;
      router.push("/");
      toast.success(message);
    } catch (err) {
      //
    }
  };
  return (
    <div className="flex flex-col justify-center items-center gap-8 w-full">
      <div className="w-full max-w-md bg-white/5 p-8 rounded-lg border border-gray-900">
        <form onSubmit={handleSubmit} className="flex flex-col justify-center items-center gap-4">
          <h2 className="text-2xl">Sign up</h2>
          <Input
            required
            type="email"
            onChange={handleInputChange}
            name="email"
            placeholder="Enter email"
          />
          <Input
            required
            type="password"
            placeholder="Enter password"
            onChange={handleInputChange}
            name="password"
          />
          <Button type="submit" className="w-full">
            Sign up
          </Button>
        </form>
      </div>
      <div>
        <p>
          Already have an account?{" "}
          <Link className="text-blue-300 hover:underline underline-offset-4" href="/login">
            Login
          </Link>
        </p>
      </div>
    </div>
  );
}
```

Now those changes are made, this feature should be functional.

### Manage subscription

Now that the user has subscribed, he should be able to cancel it anytime he wants so he will not get changed anymore also resume the subscription if he wants to continue again. We will create another component for that in the home page.

Create the `ManageSubscription` component.

`app\ManageSubscription.tsx`

```tsx
"use client";
import { useRouter } from "next/navigation";
import { toast } from "react-hot-toast";
import { Button } from "~/components/ui/button";
import { axios } from "~/lib/axios";

export default function ManageSubscription(props: {
  userId: string;
  isCanceled: boolean;
  currentPeriodEnd?: number;
}) {
  const { userId, isCanceled, currentPeriodEnd } = props;
  const router = useRouter();

  // If the subscription is cancelled, let the user resume his plan
  if (isCanceled && currentPeriodEnd) {
    const handleResumeSubscription = async () => {
      try {
        const { message } = await axios.post<any, { message: string }>(
          "/api/payment/resume-subscription",
          { userId }
        );
        router.refresh();
        toast.success(message);
      } catch (err) {
        //
      }
    };

    return (
      <div className="flex flex-col justify-between items-center gap-4">
        <p>
          You have cancelled the subscription but you still have access to our service until{" "}
          {new Date(currentPeriodEnd).toDateString()}
        </p>
        <Button className="bg-blue-300 hover:bg-blue-500 w-full" onClick={handleResumeSubscription}>
          Resume plan
        </Button>
      </div>
    );
  }

  // If the user is subscribed, let him cancel his plan
  const handleCancelSubscription = async () => {
    try {
      const { message } = await axios.post<any, { message: string }>(
        "/api/payment/cancel-subscription",
        { userId }
      );
      router.refresh();
      toast.success(message);
    } catch (err) {
      //
    }
  };

  return (
    <div className="flex flex-col justify-between items-center gap-4">
      <p>You are subscribed to our product. Congratulations</p>
      <Button className="bg-red-300 hover:bg-red-500 w-full" onClick={handleCancelSubscription}>
        Cancel
      </Button>
    </div>
  );
}
```

Let’s add this component to our home page:

`app/page.tsx`

```tsx
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getUserSubscriptionPlan } from "~/lib/subscription";
import ManageSubscription from "./ManageSubscription";
import SubscribeButton from "./SubscribeButton";

export default async function Home() {
  const cookiesList = cookies();
  const userId = cookiesList.get("userId")?.value || "";

  if (!userId) return redirect("/login");

  const { isPro, isCanceled, currentPeriodEnd } = await getUserSubscriptionPlan(userId);
  return (
    <div className="w-full max-w-md bg-white/5 border border-gray-900 p-8 rounded-lg">
      <div className="flex flex-col gap-4">
        <h2 className="text-2xl">Your profile</h2>
        {isPro ? (
          <ManageSubscription
            userId={userId}
            isCanceled={isCanceled}
            currentPeriodEnd={currentPeriodEnd}
          />
        ) : (
          <SubscribeButton />
        )}
      </div>
    </div>
  );
}
```

Now we have to create the API routes which are being used in the `ManageSubscription` component. Let’s do that.

`app\api\payment\cancel-subscription\route.ts`

```tsx
import { NextResponse } from "next/server";
import { axios } from "~/lib/axios";
import { getUserSubscriptionPlan } from "~/lib/subscription";
import { prisma } from "~/prisma/db";

export async function POST(request: Request) {
  try {
    const { userId } = await request.json();

    const user = await prisma.user.findUnique({
      where: { id: userId },
      select: {
        id: true,
        email: true,
        subscriptionId: true,
        variantId: true,
        currentPeriodEnd: true,
      },
    });

    if (!user) return NextResponse.json({ message: "User not found" }, { status: 404 });

    const { isPro } = await getUserSubscriptionPlan(user.id);

    if (!isPro) return NextResponse.json({ message: "You are not subscribed" }, { status: 402 });

    await axios.patch(
      `https://api.lemonsqueezy.com/v1/subscriptions/${user.subscriptionId}`,
      {
        data: {
          type: "subscriptions",
          id: user.subscriptionId,
          attributes: {
            cancelled: true, // <- ALl the line of code just for this
          },
        },
      },
      {
        headers: {
          Accept: "application/vnd.api+json",
          "Content-Type": "application/vnd.api+json",
          Authorization: `Bearer ${process.env.LEMONSQUEEZY_API_KEY}`,
        },
      }
    );

    const endsAt = user.currentPeriodEnd?.toLocaleString();

    return NextResponse.json({
      message: `Your subscription has been cancelled. You will still have access to our product until '${endsAt}'`,
    });
  } catch (err) {
    console.log({ err });
    if (err instanceof Error) {
      return NextResponse.json({ message: err.message }, { status: 500 });
    }
  }
}
```

This route lets the user cancel his plan. Once his plan is canceled, the subscription object will get updated which belongs to this user and a value of `true` will be assigned to the `cancelled` property. Remember we used this property while checking if the user has canceled his plan or not?

```tsx
// lib\subscription.ts
isCanceled = subscription.data.attributes.cancelled;
```

`app\api\payment\resume-subscription\route.ts`

```tsx
import { NextResponse } from "next/server";
import { axios } from "~/lib/axios";
import { getUserSubscriptionPlan } from "~/lib/subscription";
import { prisma } from "~/prisma/db";

export async function POST(request: Request) {
  try {
    const { userId } = await request.json();

    const user = await prisma.user.findUnique({
      where: { id: userId },
      select: {
        id: true,
        email: true,
        subscriptionId: true,
        variantId: true,
        currentPeriodEnd: true,
      },
    });

    if (!user) return NextResponse.json({ message: "User not found" }, { status: 404 });

    const { isPro } = await getUserSubscriptionPlan(user.id);

    if (!isPro) return NextResponse.json({ message: "You are not subscribed" }, { status: 402 });

    await axios.patch(
      `https://api.lemonsqueezy.com/v1/subscriptions/${user.subscriptionId}`,
      {
        data: {
          type: "subscriptions",
          id: user.subscriptionId,
          attributes: {
            cancelled: false, // <- Cancel
          },
        },
      },
      {
        headers: {
          Accept: "application/vnd.api+json",
          "Content-Type": "application/vnd.api+json",
          Authorization: `Bearer ${process.env.LEMONSQUEEZY_API_KEY}`,
        },
      }
    );

    return NextResponse.json({
      message: `Your subscription has been resumed.`,
    });
  } catch (err) {
    console.log({ err });
    if (err instanceof Error) {
      return NextResponse.json({ message: err.message }, { status: 500 });
    }
  }
}
```

This will do the opposite of the previous route.

We’re done. The subscription management component should now be functional.

In addition, the user should be able to change his credit card and other credentials so lets add this option.

`app\ManageSubscription.tsx`

```tsx
"use client";
import { useRouter } from "next/navigation";
import { toast } from "react-hot-toast";
import { Button } from "~/components/ui/button";
import { axios } from "~/lib/axios";

export default function ManageSubscription(props: {
  userId: string;
  isCanceled: boolean;
  currentPeriodEnd?: number;
  updatePaymentMethodURL: string;
}) {
  const { userId, isCanceled, currentPeriodEnd, updatePaymentMethodURL } = props;
  const router = useRouter();

  // If the subscription is cancelled, let the user resume his plan
  if (isCanceled && currentPeriodEnd) {
    const handleResumeSubscription = async () => {
      try {
        const { message } = await axios.post<any, { message: string }>(
          "/api/payment/resume-subscription",
          { userId }
        );
        router.refresh();
        toast.success(message);
      } catch (err) {
        //
      }
    };

    return (
      <div className="flex flex-col justify-between items-center gap-4">
        <p>
          You have cancelled the subscription but you still have access to our service until{" "}
          {new Date(currentPeriodEnd).toDateString()}
        </p>
        <Button className="w-full" onClick={handleResumeSubscription}>
          Resume plan
        </Button>
        <a
          href={updatePaymentMethodURL}
          className="w-full"
          target="_blank"
          rel="noopener noreferrer"
        >
          <Button className="w-full">Update payment method</Button>
        </a>
      </div>
    );
  }

  // If the user is subscribed, let him cancel his plan
  const handleCancelSubscription = async () => {
    try {
      const { message } = await axios.post<any, { message: string }>(
        "/api/payment/cancel-subscription",
        { userId }
      );
      router.refresh();
      toast.success(message);
    } catch (err) {
      //
    }
  };

  return (
    <div className="flex flex-col justify-between items-center gap-4">
      <p>You are subscribed to our product. Congratulations</p>
      <Button className="bg-red-300 hover:bg-red-500 w-full" onClick={handleCancelSubscription}>
        Cancel
      </Button>
      <a href={updatePaymentMethodURL} className="w-full" target="_blank" rel="noopener noreferrer">
        <Button className="w-full">Update payment method</Button>
      </a>
    </div>
  );
}
```

Added two buttons that will redirect the user to the page where he can perform that action. Note that, this component now accepts an additional prop so make sure you are passing it from the parent component. In that case it’s the home page.

```tsx
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getUserSubscriptionPlan } from "~/lib/subscription";
import ManageSubscription from "./ManageSubscription";
import SubscribeButton from "./SubscribeButton";

export default async function Home() {
  const cookiesList = cookies();
  const userId = cookiesList.get("userId")?.value || "";

  if (!userId) return redirect("/login");

  const { isPro, isCanceled, currentPeriodEnd, updatePaymentMethodURL } =
    await getUserSubscriptionPlan(userId);

  return (
    <div className="w-full max-w-md bg-white/5 border border-gray-900 p-8 rounded-lg">
      <div className="flex flex-col gap-4">
        <h2 className="text-2xl">Your profile</h2>
        {isPro ? (
          <ManageSubscription
            userId={userId}
            isCanceled={isCanceled}
            currentPeriodEnd={currentPeriodEnd}
            updatePaymentMethodURL={updatePaymentMethodURL}
          />
        ) : (
          <SubscribeButton />
        )}
      </div>
    </div>
  );
}
```

That’s all about managing subscriptions.

## Deployment

We are done developing this app and it is ready to be shipped. You can use Vercel, Render, or any other platform for the deployment. It’s a straightforward process. Import your repo > Enter env variables and deploy.

## Conclusion

That’s all folks. Hope that article was able to add value and you have found a way to receive payments for your SaaS. I wish you all the best in implementing that feature. The full source code can be found on my [Github](https://github.com/developeratul/lemonsqueezy-nextjs/tree/main). If you think there is any bug in my code, please feel free to let me know or you can send a PR on GitHub. Thanks for reading this article until the end and I will see you in the next one ✌️

## Connect with me

### 👤 Minhazur Rahman Ratul

- Twitter ([@developeratul](https://twitter.com/developeratul))
- Github ([@developeratul](http://github.com/developeratul))
- [Linkedin](https://www.linkedin.com/in/minhazur-rahman-ratul-407352211/)

> It took me hours to write that article, but you need only a few to support it. You can support me by liking it, sharing it, and [buying me a coffee](https://www.buymeacoffee.com/azammmgolZ).