Expanding the User table and session
prisma
1model User {
2 id String @id @default(cuid())
3 name String?
4 email String? @unique
5 emailVerified DateTime?
6 image String?
7 role String? @default("free user") // Add a new "role" column
8 accounts Account[]
9 sessions Session[]
10
11 stripeCustomerId String? @unique
12 subscriptionId String?
13 subscriptionStatus String? // Status of the subscription
14
15 createdAt DateTime @default(now())
16 updatedAt DateTime @updatedAt
17}
libs/auth.js
javascript
1. . .
2
3export const { handlers, signIn, signOut, auth } = NextAuth({
4 adapter: PrismaAdapter(prisma),
5 providers: [. . .],
6 pages: {. . .},
7 session: {. . .},
8 callbacks: {
9 session({ session, user }) {
10 session.user.role = user.role;
11 session.user.stripeCustomerId = user.stripeCustomerId;
12 session.user.subscriptionId = user.subscriptionId;
13 session.user.subscriptionStatus = user.subscriptionStatus;
14 return session;
15 },
16 },
17 events: {. . .},
18});
Expanding the user settings page
jsx
1import { auth } from "@/libs/auth";
2import prisma from "@/libs/db";
3import { redirect } from "next/navigation";
4import StripeButton from "@/components/stripeButton";
5
6export default async function SettingsPage() {
7 const session = await auth();
8
9 return (
10 <div className="flex min-h-[80vh] items-center justify-center bg-gray-100">
11 <div className="w-full max-w-md bg-white p-8 rounded-lg shadow-md">
12 <h1 className="text-2xl font-bold text-center mb-6">User Settings</h1>
13 <form
14 action={async (formData) => {
15 "use server";
16 await prisma.user.update({
17 where: {
18 email: session?.user?.email,
19 },
20 data: {
21 name: formData.get("name"),
22 },
23 });
24
25 redirect("/dashboard/settings");
26 }}
27 className="space-y-6">
28 <div>
29 <label
30 htmlFor="name"
31 className="block text-sm font-medium text-gray-700">
32 Name
33 </label>
34 <input
35 type="text"
36 id="name"
37 name="name"
38 defaultValue={session?.user?.name || ""}
39 className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
40 required
41 />
42 </div>
43
44 <div>
45 <label
46 htmlFor="email"
47 className="block text-sm font-medium text-gray-700">
48 Email
49 </label>
50 <input
51 type="email"
52 id="email"
53 name="email"
54 defaultValue={session?.user?.email || ""}
55 className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
56 required
57 />
58 </div>
59
60 <div>
61 <label
62 htmlFor="role"
63 className="block text-sm font-medium text-gray-700">
64 Role
65 </label>
66 <input
67 type="text"
68 id="role"
69 name="role"
70 defaultValue={session?.user?.role}
71 className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-100 cursor-not-allowed"
72 readOnly
73 />
74 </div>
75
76 {session?.user?.subscriptionStatus && (
77 <div>
78 <label
79 htmlFor="role"
80 className="block text-sm font-medium text-gray-700">
81 Subscription Status
82 </label>
83 <input
84 type="text"
85 id="role"
86 name="role"
87 defaultValue={session?.user?.subscriptionStatus}
88 className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-100 cursor-not-allowed"
89 readOnly
90 />
91 </div>
92 )}
93
94 <div>
95 <button
96 type="submit"
97 className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
98 Update Profile
99 </button>
100 </div>
101
102 {session?.user?.subscriptionStatus === "active" && (
103 <StripeButton type={"cancel"} />
104 )}
105 </form>
106 </div>
107 </div>
108 );
109}
Cancelling the subscription
src/components/stripeButton.jsx
jsx
1"use client";
2
3import Button from "@/components/ui/button";
4import { useState } from "react";
5
6export default function StripeButton({ type, plan }) {
7 const [loading, setLoading] = useState(false);
8 const [message, setMessage] = useState("");
9 const [confirmOpen, setConfirmOpen] = useState(false);
10
11 async function checkout(plan) {
12 setLoading(true);
13
14 const response = await fetch(`/api/stripe/${plan}/checkout`, {
15 method: "POST",
16 headers: {
17 "Content-Type": "application/json",
18 },
19 });
20
21 setLoading(false);
22
23 if (response.ok) {
24 const data = await response.json();
25 window.open(data.url, "_target");
26 } else {
27 setMessage("Something went wrong.");
28 }
29 }
30
31 async function cancel() {
32 setLoading(true);
33
34 const response = await fetch(`/api/stripe/cancel`, {
35 method: "POST",
36 headers: {
37 "Content-Type": "application/json",
38 },
39 });
40
41 setLoading(false);
42
43 if (response.ok) {
44 setMessage("Your subscription has been successfully canceled.");
45 } else {
46 setMessage("Something went wrong.");
47 }
48 }
49
50 return (
51 <>
52 {type === "checkout" && (
53 <Button
54 style="primary"
55 loading={loading}
56 className="w-full text-center"
57 onClick={() => checkout(plan)}>
58 Get this plan
59 </Button>
60 )}
61
62 {type === "cancel" && (
63 <>
64 <div
65 className={`fixed top-0 left-0 w-full h-full grid place-items-center ${
66 confirmOpen ? "block" : "hidden"
67 }`}>
68 <div
69 className="fixed top-0 left-0 w-full h-full bg-gray-900 dark:bg-gray-100 backdrop-blur-md z-0"
70 onClick={() => setConfirmOpen(false)}
71 />
72
73 <div className="max-w-96 bg-gray-100 shadow-md rounded-md p-8 flex flex-col gap-4 z-10">
74 <h2 className="text-3xl">We are sorry to see you leave</h2>
75 <p>Are you sure?</p>
76
77 <Button
78 style="danger"
79 loading={loading}
80 className="w-full text-center"
81 onClick={cancel}>
82 Yes, I'd like to cancel my subscription.
83 </Button>
84
85 <Button style="primary" onClick={() => setConfirmOpen(false)}>
86 Never mind.
87 </Button>
88
89 {message && <p>{message}</p>}
90 </div>
91 </div>
92 <Button
93 style="danger"
94 loading={loading}
95 className="w-full text-center"
96 onClick={() => setConfirmOpen(true)}>
97 Cancel my subscription
98 </Button>
99 </>
100 )}
101
102 <p>{message}</p>
103 </>
104 );
105}
Updating webhook
src/app/api/stripe/webhook/route.js
javascript
1import Stripe from "stripe";
2import prisma from "@/libs/db";
3import { headers } from "next/headers";
4
5export async function POST(request) {
6 const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
7
8 // Get the Stripe signature
9 const headersList = await headers();
10 const signature = headersList.get("stripe-signature");
11
12 // Getting the raw body
13 const body = await request.text();
14
15 // Constructing the event with raw body, signature, and webhook secret
16 const event = stripe.webhooks.constructEvent(
17 body,
18 signature,
19 process.env.STRIPE_WEBHOOK_SECRET
20 );
21
22 switch (event.type) {
23 // Handling subscription creation
24 case "customer.subscription.created": {
25 await prisma.user.update({
26 where: { stripeCustomerId: event.data.object.customer },
27 data: {
28 subscriptionId: event.data.object.id,
29 subscriptionStatus: event.data.object.status,
30 role:
31 event.data.object.status === "active" || "trialing"
32 ? "paid user"
33 : "free user",
34 },
35 });
36
37 break;
38 }
39
40 case "customer.subscription.deleted": {
41 await prisma.user.update({
42 where: { stripeCustomerId: event.data.object.customer },
43 data: {
44 subscriptionStatus: event.data.object.status,
45 role: "free user",
46 },
47 });
48 break;
49 }
50
51 default:
52 console.warn(`Unhandled event type: ${event.type}`);
53 }
54
55 return new Response("Stripe success", {
56 status: 200,
57 });
58}