Lemon Squeezy x Supabase x Next JS - Part 2

Rendering Lemon Squeezy Product, including the various Variants you created


The complete guide is a work in progress, this first part is completed.

This is part two of the guide, for the full guide, start here

If you are looking for a guide to help you out with setting up your very own Subscription using Next.JS, Lemon Squeezy and Supabase, you have come to the right place. In this first guide, we will look at how you can fetch products from Lemon Squeezy, so that you can populate your pricing table!

This guide is specifically created for Subscriptions with one product and multiple variants. You can see the difference in pricing strategy here.

The goal of this guide is that by the end you will be able to fetch your own products and render them using Next.js 13 App Router.

As I have mentioned in the first part of the guide, I prefer to create a component per function. If you prefer to create most in one single file, this is also possible. Remember that not all RSC can be used in an Async (Server) component, so you might have to sometimes create a client component.

Rendering the Lemon Squeezy Variants

Now that we have a working GET request (if you followed the steps carefully), we can start rendering the information. You could render the subscriptions on your homepage, or you could render it in you app in your billing section.

Wherever you may display the subscriptions, you will need to do the styling and logic yourself.

Received Lemon Squeezy Variants structure

Lets first have a look at Lemon Squeezy and their Variants file structure.

  {
    "meta": {
      "page": {
        "currentPage": 1,
        "from": 1,
        "lastPage": 1,
        "perPage": 10,
        "to": 10,
        "total": 10
      }
    },
    "jsonapi": {
      "version": "1.0"
    },
    "links": {
      "first": "https://api.lemonsqueezy.com/v1/variants?page%5Bnumber
      %5D=1&page%5Bsize%5D=10&sort=sort",
      "last": "https://api.lemonsqueezy.com/v1/variants?page%5Bnumber
      %5D=1&page%5Bsize%5D=10&sort=sort",
    },
    "data": [
      {
        "type": "variants",
        "id": "1",
        "attributes": {
          "product_id": 1,
          "name": "Example Variant",
          "slug": "46beb127-a8a9-33e6-89b5-078505657239",
          "description": "<p>Lorem ipsum...</p>",
          "price": 999,
          "is_subscription": false,
          "interval": null,
          "interval_count": null,
          "has_free_trial": false,
          "trial_interval": "day",
          "trial_interval_count": 30,
          "pay_what_you_want": false,
          "min_price": 0,
          "suggested_price": 0,
          "has_license_keys": false,
          "license_activation_limit": 5,
          "is_license_limit_unlimited": false,
          "license_length_value": 1,
          "license_length_unit": "years",
          "is_license_length_unlimited": false,
          "sort": 1,
          "status": "published",
          "status_formatted": "Published",
          "created_at": "2021-05-24T14:15:06.000000Z",
          "updated_at": "2021-06-24T14:44:38.000000Z"
        },
        "relationships": {
          "product": {
            "links": {
              "related": "https://api.lemonsqueezy.com/v1/variants/1/product",
              "self": "https://api.lemonsqueezy.com/v1/variants/1/relationships/product"
            }
          }
        },
        "links": {
          "self": "https://api.lemonsqueezy.com/v1/variants/1"
        }
      },
    ]
  }

As you can see we receive and object with an Array. This array contains the Variants information in Data. Per variant, we will have an object in the data array.

Now that we know which information we want to display, we can get to work on the pricing and pricing-display pages!

Reminder, this is what our file structure looks like:

Subscription setup
   ├── app
   │   ├── subscription
   │   │   ├── page.tsx
   │   │   ├── pricing.tsx
   │   │   ├── pricing-display.tsx
   │   │   ├── variants.tsx
   ├── and other pages

And this is how my pricing page looks for Rankingboost

rankingboost pricing plan example

I have created this using shadcn/ui, which I will use in this guide as well. If you prefer different ui framework, let us know!

Pricing-display.tsx

Let's start with our second component! We want to make sure that we have the required settings completed. For this to work, you will need to have the following set up:

  • shadcn button
  • shadcn Dialog
  • Ran the command to initialize shadcn

Look here for more information on how to set this up: shadcn ui

./pricing-display.tsx
    "use client";
 
    import { Button } from "@/components/ui/button";
    import {
        Dialog,
        DialogContent,
        DialogDescription,
        DialogHeader,
        DialogTitle,
        DialogTrigger,
    } from "@/components/ui/dialog"
    import Link from "next/link";
    import parse from 'html-react-parser';
 
 
    export default function PricingContent({
      productVariants,
      company_id,
      user,
      store_id,
    }: {
      productVariants: any;
      company_id: any;
      user: any;
      store_id: any;
    }) {
      function createCheckoutLink({ variantId }: { variantId: string }): string {
        const baseUrl = new URL(
          `https://${store_id}.lemonsqueezy.com/checkout/buy/${variantId}`
        );
 
        const email = user.user.email;
 
        const url = new URL(baseUrl);
        url.searchParams.append("checkout[custom][company_id]", company_id);
        if (email) url.searchParams.append("checkout[email]", email);
 
        return url.toString();
      }
 
      return (
      <Dialog>
        <DialogTrigger className=`inline-flex items-center justify-center
        rounded-md text-sm font-medium ring-offset-white transition-colors
        focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400
        focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50
        dark:ring-offset-zinc-950 dark:focus-visible:ring-zinc-800 h-10 px-4 py-2
        border border-zinc-200 bg-white hover:bg-zinc-100 hover:text-zinc-900
        dark:border-zinc-800 dark:bg-zinc-950 dark:hover:bg-zinc-800
        dark:hover:text-zinc-50`>
          Add subscription
        </DialogTrigger>
        <DialogContent className="max-w-3xl">
          <DialogHeader>
            <DialogTitle className="mb-4">Select your plan</DialogTitle>
            <DialogDescription>
              <div className="flex gap-x-2 w-full">
                {productVariants.data.map((variant: any) => (
                  <div
                    key={variant.id}
                    className="border rounded-lg p-4 w-full justify-between"
                  >
                    <h3 className="text-xl text-muted-foreground">
                      {variant.attributes.name}
                    </h3>
                    <p className="text-3xl font-bold text-pink-500 mb-6">
                      €{variant.attributes.price / 100}
                    </p>
                    <p className=`prose dark:prose-invert prose-p:text-foreground prose-p:pb-2
                    prose-li:-my-6 prose-li:leading-5 prose-ul:h-40`>
                      {parse(variant.attributes.description)}
                    </p>
                    <Link
                      className="w-full flex justify-center"
                      href={createCheckoutLink({
                        variantId: variant.attributes.slug,
                      })}
                    >
                      <Button className=`mt-2 bg-pink-500 text-white dark:bg-pink-500
                      dark:text-white w-full hover:bg-pink-400 hover:dark:bg-pink-400`>
                        Buy Now
                      </Button>
                    </Link>
                  </div>
                ))}
              </div>
            </DialogDescription>
          </DialogHeader>
        </DialogContent>
      </Dialog>
    );
  }

"use client"

As the dialog component from shadcn can't be used with server components, we have to create them as client components. This is why I have created a seperate component for this part. The use client function makes it only available in the client, luckily we can sent it data ourselves, so that we have the information available and only need to render it.

Imports

The imports are pretty straightforward. We need the button and the dialog, to be able to render the content of the Get Request. On top of that, we want to send the users to the correct URL after they click on Buy now. That is where we use Link / 'next/link'. The parse from html-react-parser is used to display the html content of our description created in Lemon Squeezy.

  import { Button } from "@/components/ui/button";
  import {
      Dialog,
      DialogContent,
      DialogDescription,
      DialogHeader,
      DialogTitle,
      DialogTrigger,
  } from "@/components/ui/dialog"
  import Link from "next/link";
  import parse from 'html-react-parser';

PricingContent function

Parameters

parameters
  export default function PricingContent({
    productVariants,
    company_id,
    user,
    store_id,
  }: {
    productVariants: any;
    company_id: any;
    user: any;
    store_id: any;
  }) {
    // rest of the content
  }

Most of it comes from the logic I have implemented before and we will only see much later. However, lets look in more detail:

  • productVariants - map the cards for your pricing (one variant, one card -- three variants, three cards)
  • company_id - later we can populate our Supabase database
  • user - we can get the users email this way, so they don't have to prefill it
  • store_id - so we can link to the right Lemon Squeezy store

Create the Checkout function

With this information, we are able to create a function which can create the checkout link. For each variant, we are able to create a unique link.

createLink
  function createCheckoutLink({ variantId }: { variantId: string }): string {
    const baseUrl = new URL(
      `https://${store_id}.lemonsqueezy.com/checkout/buy/${variantId}`
    );
    const email = user.user.email;
 
    const url = new URL(baseUrl);
    url.searchParams.append("checkout[custom][company_id]", company_id);
    if (email) url.searchParams.append("checkout[email]", email);
 
    return url.toString();
  }

The baseUrl variable, contains a 'new' URL, which we will use to generate the checkout link. The variantId parameter comes from the mapped data from Lemon Squeezy. The parameter value will be populated based on the button the end user clicks eventually.

Other information, is the email. Which comes from the user parameter that we have. If there is an email, we will incorporate this value also within the URL, so the users don't have to fill this themselves.

The url is then created based on the baseUrl and appended url.searchParams.append by a custom field, company_id. This field will eventually make it possible for us to update our database. We will get back to that once we reach this point of the guide. Then if there is an email available, this email will be added to the url.

In the end the function returns the url.toString() which makes it possible to use the Link function later with the appended url.

Returning the Dialog

Return statement
  return (
    <Dialog>
      <DialogTrigger className=`inline-flex items-center justify-center
      rounded-md text-sm font-medium ring-offset-white transition-colors
      focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400
      focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50
      dark:ring-offset-zinc-950 dark:focus-visible:ring-zinc-800 h-10 px-4 py-2
      border border-zinc-200 bg-white hover:bg-zinc-100 hover:text-zinc-900
      dark:border-zinc-800 dark:bg-zinc-950 dark:hover:bg-zinc-800
      dark:hover:text-zinc-50`>
        Add subscription
      </DialogTrigger>
      <DialogContent className="max-w-3xl">
        <DialogHeader>
          <DialogTitle className="mb-4">Select your plan</DialogTitle>
          <DialogDescription>
            <div className="flex gap-x-2 w-full">
              {productVariants.data.map((variant: any) => (
                <div
                  key={variant.id}
                  className="border rounded-lg p-4 w-full justify-between"
                >
                  <h3 className="text-xl text-muted-foreground">
                    {variant.attributes.name}
                  </h3>
                  <p className="text-3xl font-bold text-pink-500 mb-6">
                    €{variant.attributes.price / 100}
                  </p>
                  <p className=`prose dark:prose-invert prose-p:text-foreground prose-p:pb-2
                  prose-li:-my-6 prose-li:leading-5 prose-ul:h-40`>
                    {parse(variant.attributes.description)}
                  </p>
                  <Link
                    className="w-full flex justify-center"
                    href={createCheckoutLink({
                      variantId: variant.attributes.slug,
                    })}
                  >
                    <Button className=`mt-2 bg-pink-500 text-white dark:bg-pink-500
                    dark:text-white w-full hover:bg-pink-400 hover:dark:bg-pink-400`>
                      Buy Now
                    </Button>
                  </Link>
                </div>
              ))}
            </div>
          </DialogDescription>
        </DialogHeader>
      </DialogContent>
    </Dialog>
  );

As you can see from the return statement, we are mapping the data based on the productVariants.data source. This means that from the complete JSON file we receive from Lemon Squeezy, we will only use the data that is within .data.

Let's focus on the main content: productVariants.

We receive the JSON, including the Data object from getProductVariants, as we have seen in the previous step in the guide. In this section we are generating the overview of all variants that are available within a specific product.

When we look at the anatomy of the rendered variants, we can see that each variant renders their own data. See comments what each ink does.

  {productVariants.data.map((variant: any) => (
    // Div for the variant container (key for the map)
    <div key={variant.id} className="border rounded-lg p-4 w-full justify-between"> 
      // Title of the variant
      <h3 className="text-xl text-muted-foreground">{variant.attributes.name}</h3>
      // Price per month
      <p className="text-3xl font-bold text-pink-500 mb-6">€{variant.attributes.price / 100}</p>
      // Description that is created in Lemon Squeezy
      <p className=`prose dark:prose-invert prose-p:text-foreground prose-p:pb-2
      prose-li:-my-6 prose-li:leading-5 prose-ul:h-40`>{parse(variant.attributes.description)}</p>
      //  Link to create checkout for this variant
      <Link className="w-full flex justify-center" 
        href={createCheckoutLink({variantId: variant.attributes.slug })}>
        // Styling the button like others
        <Button className=`mt-2 bg-pink-500 text-white dark:bg-pink-500 dark:text-white 
        w-full hover:bg-pink-400 hover:dark:bg-pink-400`>Buy Now</Button>
      </Link>
    </div>
  ))}

Note: This guide was created before shadcn had created Theming. I will update the guide once this functionality has matured.

With this setup, we are able to generate the dialog screen, which pops over your page. But so far, we haven't been able to generate this yet.

The reason is we need a server component to send the data to the client component. The next part of the guide will focus on the pricing.tsx page.

Like what you see? Do you prefer to have a ready made app instead of building the integration yourself?

No worries, we have got you covered.

With Supaboost, you will be able to skip at least 30 days development time, by having these features readily available:

  • Auth
  • Lemon Squeezy integration
  • Safe development with Typescript
  • Next.js App Router
  • Supabase SQL scripts
  • And much more.

Get Supaboost

View

Next step

Click here to continue to the Server Component

View