DEV Community

Discussion on: How to add User Accounts and Paid Subscriptions to your Next.js Website

Collapse
 
andriusmv profile image
Andres Moreno Vasquez

Hey Andrew, great article! Just one thing: how do you add components to conditionally render parts of your application? I mean, how do you accomplish something like this:
membershipType == Gold? :
Thank you!

Collapse
 
sethmckilla profile image
Seth

Hey I've actually done just that in my application. I'm going to attempt to summarize as succintly as possible. To start, instead of a boolean value for isActive in the schema, I switched this to the subId from stripe (i.e. price_...):

// prisma/schema.prisma

model User {
  id               String    @id @default(cuid())
  name             String?
  email            String?   @unique
  emailVerified    DateTime?
  image            String?
  stripeCustomerId String?   @unique
  stripeSubId      String?
  accounts         Account[]
  sessions         Session[]
}
Enter fullscreen mode Exit fullscreen mode

Then I added a config file where I store info about the tiers and prices (which I've created on Stripe the same way as Andrew did above):

// config/index.ts

export const CURRENCY = "usd";
export const YEARLY_DISCOUNT = 0.8;
export const MONTHLY_PRICES = {
  MONTHLY_SINGLE: 10.0,
  MONTHLY_MULTI: 25.0,
  MONTHLY_UNLIMITED: 50.0,
};
export const MONTHLY_PRICING = {
  ...MONTHLY_PRICES,
  YEARLY_SINGLE: MONTHLY_PRICES["MONTHLY_SINGLE"] * YEARLY_DISCOUNT,
  YEARLY_MULTI: MONTHLY_PRICES["MONTHLY_MULTI"] * YEARLY_DISCOUNT,
  YEARLY_UNLIMITED: MONTHLY_PRICES["MONTHLY_UNLIMITED"] * YEARLY_DISCOUNT,
};
export const TRIAL_PERIOD_DAYS = 14;
export const SUBSCRIPTION_IDS = {
  STRIPE_YEARLY_UNLIMITED_SUB_ID: process.env.STRIPE_YEARLY_UNLIMITED_SUB_ID,
  STRIPE_YEARLY_MULTI_SUB_ID: process.env.STRIPE_YEARLY_MULTI_SUB_ID,
  STRIPE_YEARLY_SINGLE_SUB_ID: process.env.STRIPE_YEARLY_SINGLE_SUB_ID,
  STRIPE_MONTHLY_UNLIMITED_SUB_ID: process.env.STRIPE_MONTHLY_UNLIMITED_SUB_ID,
  STRIPE_MONTHLY_MULTI_SUB_ID: process.env.STRIPE_MONTHLY_MULTI_SUB_ID,
  STRIPE_MONTHLY_SINGLE_SUB_ID: process.env.STRIPE_MONTHLY_SINGLE_SUB_ID,
};
Enter fullscreen mode Exit fullscreen mode

Note I've also got a yearly discount too, I like having all this in a config file so I can easily change these values in one spot and the entire app updates based on the values. Then I use the subType the user selects on the frontend to determine which subId to use in the checkout session:

// pages/api/stripe/checkout-sessions.ts
...
const subType: string = req.body.subType;

const checkoutSession = await stripe.checkout.sessions.create({
  mode: "subscription",
  customer: session.user.stripeCustomerId,
  metadata: {
    payingUserId: session.user.id,
  },
  subscription_data: {
    trial_period_days,
  },
  line_items: [
    {
      price:
        SUBSCRIPTION_IDS[
          `STRIPE_${subType}_SUB_ID` as keyof typeof SUBSCRIPTION_IDS
        ],
      quantity: 1,
    },
  ],
  success_url: `${req.headers.origin}/profile/setup?session_id={CHECKOUT_SESSION_ID}`,
  cancel_url: `${req.headers.origin}/profile/signup?cancelledPayment=true`,
});

if (!checkoutSession.url) throw new Error("No checkout session url!");

return res.status(200).json(checkoutSession);
...
Enter fullscreen mode Exit fullscreen mode

Then I created a helper function to determine the pricing tier from the subId that I store in the database:

// utils/stripe.ts

export const getStripeSubTier = (subId: string) => {
  const subTier = Object.keys(SUBSCRIPTION_IDS).find(
    (key) => SUBSCRIPTION_IDS[key as keyof typeof SUBSCRIPTION_IDS] === subId
  );
  return subTier?.split("_")[2];
};
Enter fullscreen mode Exit fullscreen mode

The webhook also needs to be updated:

// api/webhooks/stripe.ts
...
case "customer.subscription.created": {
  const subscription = event.data.object as Stripe.Subscription;

  await prisma.user.update({
    where: {
      stripeCustomerId: subscription.customer as string,
    },
    data: {
      stripeSubId: subscription.items.data[0]?.price.id,
    },
  });
  break;
}
...
Enter fullscreen mode Exit fullscreen mode

Then finally add that to the user session in the next-auth session callback:

// pages/api/auth/[...nextauth].ts
...
  callbacks: {
    async session({ session, user }) {
      const stripeSubTier = getStripeSubTier(user.stripeSubId as string);

      session!.user!.id = user.id;
      session!.user!.stripeCustomerId = user.stripeCustomerId as string;
      session!.user!.stripeSubTier = stripeSubTier as string;

      return session;
    },
  },
...
Enter fullscreen mode Exit fullscreen mode

Hopefully this makes sense, let me know if you have any questions!