Skip to content

Commit

Permalink
feat(payments): Add webhook endpoint. Add payment intent
Browse files Browse the repository at this point in the history
  • Loading branch information
phukon committed Mar 21, 2024
1 parent eb188b8 commit fdbe179
Show file tree
Hide file tree
Showing 12 changed files with 316 additions and 74 deletions.
12 changes: 12 additions & 0 deletions prisma/migrations/20240321115301_payment_intents/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
Warnings:
- A unique constraint covering the columns `[stripe_payment_intent_id]` on the table `User` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "User" ADD COLUMN "stripePaymentDate" TIMESTAMP(3),
ADD COLUMN "stripe_payment_intent_id" TEXT;

-- CreateIndex
CREATE UNIQUE INDEX "User_stripe_payment_intent_id_key" ON "User"("stripe_payment_intent_id");
12 changes: 9 additions & 3 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,17 @@ model User {
notes Note[]
wordCountRef Int?
// Payments
stripeCustomerId String? @unique @map(name: "stripe_customer_id")
// Stripe
stripeCustomerId String? @unique @map(name: "stripe_customer_id")
stripePriceId String? @map(name: "stripe_price_id")
// Subs
stripeSubscriptionId String? @unique @map(name: "stripe_subscription_id")
stripePriceId String? @map(name: "stripe_price_id")
stripeCurrentPeriodEnd DateTime? @map(name: "stripe_current_period_end")
// Payment intents
stripePaymentIntentId String? @unique @map(name: "stripe_payment_intent_id")
stripePaymentDate DateTime?
}

enum NoteType {
Expand Down
70 changes: 70 additions & 0 deletions src/actions/createStripePayIntentSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"use server";

import { PLANS } from "@/config/stripePlans";
import { getUserById } from "@/data/user";
import { currentUser } from "@/lib/auth";
import { stripe, getUserPaymentStatus } from "@/lib/stripe";
import { absoluteUrl } from "@/lib/utils";

type Tperiod = {
period: "onetime" | "monthly" | "yearly";
};

export const createStripePayIntentSession = async ({ period }: Tperiod) => {
try {
const user = await currentUser();
if (!user) throw new Error("Unauthorized");
if (!user.id) throw new Error("Invalid user ID");

const dbUser = await getUserById(user.id);
if (!dbUser) throw new Error("Unauthorized");

const billingUrl = absoluteUrl("/dash/billing");
const isPaidUser = await getUserPaymentStatus();

if (isPaidUser) {
const stripeSession = await stripe.billingPortal.sessions.create({
customer: dbUser.stripeCustomerId!,
return_url: billingUrl,
});
return { url: stripeSession.url };
}

const customer = await stripe.customers.create({
name: dbUser.name!,
email: dbUser.email!,
metadata: {
userId: dbUser.id,
},
});

const stripeSession = await stripe.checkout.sessions.create({
customer: customer.id,
success_url: billingUrl,
cancel_url: billingUrl,
payment_method_types: ["card"],
mode: "payment",
billing_address_collection: "auto",
line_items: [
{
//
price: PLANS.find((plan) => plan.name === "Pro")?.price[
period === "onetime" ? "onetime" : period
].priceIds.test, // will fix later
quantity: 1,
},
],
metadata: {
userId: dbUser.id,
},
payment_intent_data: {
metadata: {
userId: dbUser.id,
},
},
});
return { url: stripeSession.url };
} catch (error) {
return { error: JSON.stringify(error) };
}
};
8 changes: 4 additions & 4 deletions src/actions/createStripeSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { absoluteUrl } from "@/lib/utils";
*/

type TcreateStripeSession = {
period: "monthly" | "yearly";
period: "onetime" | "monthly" | "yearly";
};

export const createStripeSession = async ({ period }: TcreateStripeSession) => {
Expand Down Expand Up @@ -44,13 +44,13 @@ export const createStripeSession = async ({ period }: TcreateStripeSession) => {
success_url: billingUrl,
cancel_url: billingUrl,
payment_method_types: ["card"],
mode: "subscription",
mode: "payment",
billing_address_collection: "auto",
line_items: [
{
//
price: PLANS.find((plan) => plan.name === "Pro")?.price[period === "yearly" ? "yearly" : "monthly"].priceIds
.test,
price: PLANS.find((plan) => plan.name === "Pro")?.price[period === "onetime" ? "onetime" : period].priceIds // will fix later
.production,
quantity: 1,
},
],
Expand Down
56 changes: 56 additions & 0 deletions src/app/api/webhooks/stripePaymentIntent/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { db } from "@/lib/db";
import { stripe } from "@/lib/stripe";
import { headers } from "next/headers";
import type Stripe from "stripe";

export async function POST(request: Request) {
const body = await request.text();
const signature = headers().get("Stripe-Signature") ?? "";

let event: Stripe.Event;

try {
event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
} catch (err) {
return new Response(`Webhook Error: ${err instanceof Error ? err.message : "Unknown Error"}`, {
status: 400,
});
}

const session = event.data.object as Stripe.Checkout.Session;

if (!session?.metadata?.userId) {
return new Response(null, {
status: 500,
});
}

if (event.type === "payment_intent.succeeded") {
// console.log(session)
const paymentIntentId = session.id as string;
// console.log("Payment Intent ID:", paymentIntentId);

const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);
// console.log("Payment Intent:", paymentIntent);

const userId = session.metadata.userId;
// console.log("User ID:", userId)

const paymentDate = new Date(paymentIntent.created * 1000); // milliseconds since the Unix epoch
// console.log("Payment Date:", paymentDate);

await db.user.update({
where: {
id: userId,
},
data: {
stripeCustomerId: session.customer as string,
stripePriceId: session.line_items?.data[0].price?.id,
stripePaymentIntentId: paymentIntentId,
stripePaymentDate: paymentDate,
},
});
}

return new Response(null, { status: 200 });
}
9 changes: 6 additions & 3 deletions src/app/dash/billing/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import BillingForm from "@/components/billing/Billing";
import { getUserSubscriptionPlan } from "@/lib/stripe";
import { getUserPaymentStatus } from "@/lib/stripe";

const Page = async () => {
const subscriptionPlan = await getUserSubscriptionPlan();
// const subscriptionPlan = await getUserSubscriptionPlan();

return <BillingForm subscriptionPlan={subscriptionPlan} />;
// return <BillingForm subscriptionPlan={subscriptionPlan} />;
const isPaidUser = await getUserPaymentStatus();

return <BillingForm isPaidUser={isPaidUser} />;
};

export default Page;
33 changes: 23 additions & 10 deletions src/components/Gallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,21 @@ type PhotoProps = {
children?: ReactNode;
};

function Photo({ src, alt, filename, width, height, rotate, left, index, flipDirection, meta, children }: PhotoProps) {
const fileName = filename || (typeof src !== "string" && `${src.src.split("/").at(-1)?.split(".")[0]}.jpg`);
function Photo({
src,
alt,
filename,
width,
height,
rotate,
left,
index,
flipDirection,
meta,
children,
}: PhotoProps) {
const fileName =
filename || (typeof src !== "string" && `${src.src.split("/").at(-1)?.split(".")[0]}.jpg`);
const shared = "absolute h-full w-full rounded-2xl overflow-hidden border-2 border-gray-800";
return (
<motion.div
Expand Down Expand Up @@ -98,7 +111,7 @@ function Photo({ src, alt, filename, width, height, rotate, left, index, flipDir
}}
>
<Halo strength={50} className="flex items-center">
<span className="absolute h-[1500px] w-[1500px] bg-gray-950 bg-[length:280px]" />
<span className="absolute h-[1500px] w-[1500px] bg-gray-950 bg-[length:280px]" />
<div className="z-[1] px-6">
<div className={clsx(ticketingFont.className, "flex flex-col gap-1 uppercase")}>
<p className="text-sm text-white">{fileName}</p>
Expand All @@ -119,15 +132,15 @@ export default function Gallery() {
<Photo
src={ps2}
filename="Neat and clean UI"
alt="Track your progress"
alt="ps2"
width={300}
height={580}
rotate={0}
left={-36}
index={1}
/>
<Photo
filename="Multiple heatmap themes to choose from!"
filename="Multiple heatmap themes to choose from!"
src={ps3}
alt="ps2"
width={300}
Expand All @@ -138,7 +151,7 @@ export default function Gallery() {
flipDirection="left"
/>
<Photo
filename="Everything at a glance"
filename="Everything at a glance"
src={ps1}
alt="ps3"
width={300}
Expand All @@ -158,8 +171,8 @@ export function GallerySmall() {
<section className="absolute left-0 lg:left-48 xl:left-80 2xl:left-96 flex h-[268px] gap-4">
<Photo
src={ps2}
meta="Track your progress"
alt="Track your progress"
filename="Neat and clean UI"
alt="ps2"
width={200}
height={400}
rotate={0}
Expand All @@ -168,7 +181,7 @@ export function GallerySmall() {
/>
<Photo
src={ps3}
meta="2024-03-07"
filename="Multiple heatmap themes to choose from!"
alt="ps2"
width={200}
height={400}
Expand All @@ -179,7 +192,7 @@ export function GallerySmall() {
/>
<Photo
src={ps1}
meta="2024-03-20"
filename="Everything at a glance"
alt="ps3"
width={200}
height={400}
Expand Down
Loading

0 comments on commit fdbe179

Please sign in to comment.