The app uses:

Seller Product View


Prerequisites

Before we can start building, we need to make sure we created an app on Whop. If you haven’t done this yet, head to the Developer Settings and create a new app.

This app will use the following URL paths:

  • Seller Product Path: seller/$companyId/$productId
  • Customer Path: customer/$companyId

For more information on creating an app, check out the Create an app guide.


Project Setup

Before we start building we’re going to set up our database. This example uses Neon, but you can use any database you want.

Database Setup

  1. Head to the Neon Dashboard and create a new project
  2. Enter the name of your project, and select the region closest to you. We are using Postgres Version 15.
  3. Copy the connection string and save it somewhere safe. We will need it later.

We have now set up our database! We will create our tables later on


Next.js Setup

This tutorial uses the Next.js App Directory.

  1. Initialize a new project
  1. Install the dependencies
  1. Create the environment variables

Create a .env.local file in the root of your project and add the following:

.env.local
WHOP_API_KEY=
NEXT_PUBLIC_WHOP_APP_ID=
DATABASE_URL=
  • WHOP_API_KEY is your Whop API key. You can find this in your app’s settings.
  • NEXT_PUBLIC_WHOP_APP_ID is your Whop App ID. You can find this in your app’s settings.
  • DATABASE_URL is the connection string we copied earlier.

You can find out more about the App ID and API key here.

We now have set up everything we need to start building our app!
  1. Enable Server Actions

As server actions are still an experimental feature of Next.js, we have to manually enable them. To do this, head to next.config.js and add the following:

next.config.js
const nextConfig = {
  experimental: {
    serverActions: true,
  },
};

module.exports = nextConfig;
We now have set up everything we need to start building our app!

Building the App

This section will walk you through building the app.

Structuring the project

Before we start building, we need to structure our project. We will:

  • Create the path for our seller and customer page
  • Create a lib folder for our database client and server actions
  • Create a components folder for our components
  • Setup Frosted UI

Creating the paths

Create a new path inside of the app folder:

app/seller/[companyId]/[productId]/page.tsx

and

app/customer/[productId]/page.tsx

Creating the misc folders

In the root of your directory, create 2 new folders called lib and components.

Setting up Frosted UI

To use Frosted UI, we need to edit our layout.tsx and tailwind.config.ts files.

tailwind.config.ts
import type { Config } from "tailwindcss";
import preset from "@whop/frosted-ui/dist/preset";

const config = preset({
  content: [
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
});
export default config;

Now, create a folder in the app directory called layout.client.tsx. This is where we will define our client layout.

layout.client.tsx
"use client"; // this line is important
import { FC, PropsWithChildren } from "react";
import { TooltipProvider, Toaster } from "@whop/frosted-ui";

export const ClientLayout: FC<PropsWithChildren> = ({ children }) => {
  return (
    <TooltipProvider>
      {children}
      <Toaster />
    </TooltipProvider>
  );
};

Finally, we need to edit the layout.tsx file to use our new layout.

layout.tsx
import { ClientLayout } from "./layout.client";
import "./globals.css";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <ClientLayout>{children}</ClientLayout>
      </body>
    </html>
  );
}

Finally, head to globals.css and remove all the content except the 3 @tailwind imports, like so:

globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

Creating the Database Client

We will be using Prisma as our ORM. Prisma is a great tool that allows us to easily interact with our database with TypeSafety. To get started, we are going to create a new file called prisma.ts in our lib folder.

db.ts
import { PrismaClient } from "@prisma/client";

export const db = new PrismaClient();

You may get an error saying that PrismaClient is not exported. This is expected and will be resolved in the next step.


Defining our schema

To make use of our ORM, we are going to define our schema. Instead of doing this manually, we will get Prisma to generate it for us. To do this, run the following command in your terminal

This will set up Prisma and the files needed for it to work, now we need to create our table and schema. In your schema.prisma file, add these two models to the bottom of the file:

schema.prisma
model Codes {
  id          Int         @id @default(autoincrement())
  created_at  DateTime    @default(now())
  companyID   String
  lineItemID  String
  price       Float
  name        String
  description String
  code        String
  purchases   purchases[]

  @@unique([name, companyID])
}

model purchases {
  id         Int      @id @default(autoincrement())
  created_at DateTime @default(now())

  lineItemID String
  userID     String
  codeId     Int
  codes      Codes  @relation(fields: [codeId], references: [id])

  @@index([codeId])
}

Now, we need to tell Neon that we want to create these tables. To do this, run the following command in your terminal:

To make use of our schema, we need to generate a client to enable us to interact with it. This will also resolve the import error we faced earlier. To do this, run the following command in your terminal:


Developing locally

To develop locally and see our changes on the Whop site, we need to use the whop-proxy command. To do this, run the following command in your terminal:


To find out more about the whop-proxy command, check out the documentation.


Creating our Seller page

Now that we have set up our database, we can start building our seller page. This is where sellers will be able to add videos to their products.

Head to the page.tsx we made in the seller/[companyId]/[productId] folder.

In this file we will:

  • Verify that the user is allowed to access the page
  • Display a form to create a new code
page.tsx
import NewCodeForm from "@/components/CodeForm";
import { hasAccess, validateToken, authorizedUserOn } from "@whop-apps/sdk";
import { headers } from "next/headers";

export default async function SellerPage({
  params,
}: {
  params: { companyId: string; productId: string };
}) {
  const { userId } = await validateToken({ headers });

  const access = await hasAccess({
    to: authorizedUserOn(params.companyId),
    headers,
  });

  if (!access) {
    return <p>You do not have access to this page.</p>;
  }

  return (
    <div className="bg-gray-100 min-h-screen py-6 flex flex-col justify-center sm:py-12">
      <div className="relative py-3 sm:max-w-xl sm:mx-auto">
        <h1 className="text-4xl font-bold mb-4">Create a code</h1>
        <div className="bg-white p-4 sm:p-6 rounded-3xl shadow-lg">
          <NewCodeForm companyId={params.companyId} />
        </div>
      </div>
    </div>
  );
}


Creating the Seller Form

Since our form will be a client component, we are going to create it in a separate file. In the components folder we made earlier, create a new file called SellerForm.tsx.

This page will take in the following props:

  • companyId - The ID of the company
components/CodeForm.tsx
"use client";
import { Input, NumberInput, Button } from "@whop/frosted-ui";
import { createCode } from "@/lib/actions";

export function NewCodeForm({ companyId }: { companyId: string }) {
  return (
    <div>
      <form action={createCode}>
        <div className="flex flex-col space-y-4">
          <input type="hidden" name="companyId" value={companyId} />
          <Input label="Code Name" name="name" />
          <Input label="Description" name="description" />
          <Input label="Code" name="code" placeholder="Enter the code here" />
          <NumberInput
            label="Price"
            variant="price"
            name="price"
            placeholder="50"
          />
          <Button className="w-full" type="submit">
            Create
          </Button>
        </div>
      </form>
    </div>
  );
}

Creating the Server Action

Now that we have created our form, we need to create the server action that will handle the form submission. Create a new file called actions.ts in the lib folder.

In this file, we are:

  • Creating a server action that will:
    • Create a [line item] for the code, this allows us to use in-app purchases
    • Create a new entry in our database for the code
lib/actions.ts
"use server";
import { db } from "@/app/prisma";
import { WhopAPI } from "@whop-apps/sdk";

export default async function createCode(formData: FormData) {
  const companyId = formData.get("companyId") as string;
  const name = formData.get("name") as string;
  const description = formData.get("description") as string;
  const code = formData.get("code") as string;
  let priceString = formData.get("price") as string;
  const price = parseInt(priceString);

  const lineItemResponse = await WhopAPI.app().POST("/app/line_items", {
    body: {
      allow_multiple_quantity: false,
      amount: price,
      base_currency: "usd",
      company_id: companyId,
      name,
      description,
    },
  });

  if (lineItemResponse.isErr) {
    console.log(lineItemResponse);
    throw new Error("Failed to create line item");
  }

  const lineItemId = lineItemResponse.data.id;

  await db.codes.create({
    data: {
      companyID: companyId,
      name,
      description,
      price,
      lineItemID: lineItemId,
      code,
    },
  });
}

We have now created our seller page! If you head to the seller page, you should be able to add a video URL and see it in your database.


Creating the Customer page

This page will be what the users see when they click on the app. It will display a list of all available codes with a button to purchase. If a code has been purchased, the option to purchase it will be disabled and the code will be displayed.

This will be done in the customer/[productId]/page.tsx file.

In this file we will:

  • Fetch all the codes from the database attached to the company
  • Render a grid of cards with the code information
page.tsx
import { db } from "@/app/prisma";
import { validateToken } from "@whop-apps/sdk";
import { headers } from "next/headers";
import CodeCard from "@/components/CodeCard";

export default async function CustomerPage({
  params,
}: {
  params: { companyId: string };
}) {
  const { userId } = await validateToken({ headers });

  const codes = await db.codes.findMany({
    where: {
      companyID: params.companyId,
    },
    include: {
      purchases: {
        where: {
          userID: userId,
        },
      },
    },
  });

  if (codes.length === 0) {
    return <p>This company does not have any codes yet.</p>;
  }

  return (
    <div className="grid grid-cols-1 p-5 md:grid-cols-2 gap-4">
      {codes.map((code) => {
        // Check if there's a purchase for this riddle by the current user
        const purchasedByUser = code.purchases.some(
          (purchase) => purchase.userID === userId
        );

        return (
          <CodeCard key={code.id} code={code} purchased={purchasedByUser} />
        );
      })}
    </div>
  );
}

Creating the Code Card

This component will be used to display the code information, such as price and description. There will also be a button that will allow the user to purchase the code, and open the in-app purchase iFrame.

components/CodeCard.tsx
"use client";
import { Button, Tag } from "@whop/frosted-ui";
import { WhopApp } from "@/lib/iframe";
import { useCallback, useTransition } from "react";
import { createPurchase } from "@/lib/actions";
import "@/lib/iframe";

interface CodeCardProps {
  name: string;
  description: string;
  price: number;
  lineItemID: string;
  code: string;
  id: number;
}

export default function CodeCard({
  code,
  purchased,
  userID,
}: {
  code: CodeCardProps;
  purchased: boolean;
  userID: string;
}) {
  let [isPending, startTransition] = useTransition();

  const handlePurchaseButton = useCallback(async () => {
    try {
      const result = await WhopApp.inAppPurchase({
        line_item_id: code.lineItemID,
      });
      if (result.status === "ok") {
        startTransition(() => {
          createPurchase({
            codeId: code.id,
            lineItemID: code.lineItemID,
            userID,
          });
        });
      }
    } catch (e) {
      console.error(e);
    }
  }, [code.lineItemID, code.id, userID]);

  return (
    <div className="border rounded-xl p-4 shadow-md hover:shadow-lg transition-shadow duration-300">
      <h2 className="text-2xl font-semibold mb-2">{code.name}</h2>
      <p className="text-gray-600 mb-4">{code.description}</p>
      {purchased ? (
        <div>
          <p className="text-gray-600 mb-4">Code: {code.code}</p>
          <div className="flex justify-between items-center">
            <span className="text-xl font-bold line-through">
              ${code.price.toFixed(2)} <Tag text="Purchased" className="ml-3" />
            </span>
            <Button disabled={true}>Purchase</Button>
          </div>
        </div>
      ) : (
        <div className="flex justify-between items-center">
          <span className="text-xl font-bold">${code.price.toFixed(2)} </span>
          <Button onClick={handlePurchaseButton}>Purchase</Button>
        </div>
      )}
    </div>
  );
}

Server action

You will notice that we imported another server action from our actions.ts file. This will create a new entry in our database for the purchase.

Add this to the actions.ts file:

lib/actions.ts
export async function createPurchase({
  lineItemID,
  userID,
  codeId,
}: {
  lineItemID: string;
  userID: string;
  codeId: number;
}) {
  await db.purchases.create({
    data: {
      lineItemID,
      userID,
      codeId: codeId,
    },
  });
}
We have now created our customer page!

Next steps

Now that we have created the app, we need to host it so anyone can use it. We recommend using Vercel, but you can use any hosting provider you want.

Make sure to keep your environment variables secret and don’t commit them to your repository. Your company must also have Whop payments enabled to support in-app purchases.

Was this page helpful?