diff --git a/actions/createCheckoutSession.ts b/actions/createCheckoutSession.ts new file mode 100644 index 0000000..5786f84 --- /dev/null +++ b/actions/createCheckoutSession.ts @@ -0,0 +1,86 @@ +"use server"; + +import { getUserData, checkUserPlanLimit } from "@/firebaseFunctions"; // Import necessary functions from firebaseFunctions.ts +import { adminDb } from "@/firebaseAdmin"; +import getBaseUrl from "@/lib/getBaseUrl"; +import stripe from "@/lib/stripe"; + +export const createCheckoutSession = async (userEmail: string, plan: string) => { + try { + // Fetch user data from Firestore + const userData = await getUserData(userEmail); + + if (!userData) { + throw new Error("User data not found."); + } + + // Check if user has reached the request limit + const isWithinLimit = await checkUserPlanLimit(userEmail); + + if (!isWithinLimit) { + throw new Error("You have reached your plan's request limit."); + } + + // First check if the user already has a stripeCustomerId + let stripeCustomerId; + + // Fetch the user document using userEmail + const userDoc = await adminDb.collection("users").doc(userEmail).get(); + const userDetails = userDoc.data(); + + if (!userDetails) { + throw new Error("User details not found."); + } + + stripeCustomerId = userDetails.stripeCustomerId; + + if (!stripeCustomerId) { + // Create a new stripe customer + const customer = await stripe.customers.create({ + email: userDetails.email, + name: userDetails.name, + metadata: { + userId: userEmail, // Using userEmail as userId + }, + }); + + // Update Firestore with the new Stripe customer ID + await adminDb.collection("users").doc(userEmail).set({ + stripeCustomerId: customer.id, + }, { merge: true }); // Use merge to avoid overwriting existing data + + stripeCustomerId = customer.id; + } + + // Stripe price IDs for plans + const priceId = plan === "pro" + ? "price_1Q0L3aSCr1Ne8DGFAI9n4GbW" // Pro Plan price ID + : plan === "enterprise" + ? "price_1Q0L3aSCr1Ne8DGF3yD1iMnd" // Enterprise Plan price ID + : null; + + if (!priceId) { + throw new Error("Invalid plan selected."); + } + + // Create a checkout session in Stripe + const session = await stripe.checkout.sessions.create({ + payment_method_types: ["card"], + mode: "subscription", + line_items: [ + { + price: priceId, + quantity: 1, + }, + ], + success_url: `${getBaseUrl()}/dashboard/upgrade/success?session_id={CHECKOUT_SESSION_ID}`, + cancel_url: `${getBaseUrl()}/dashboard/upgrade/cancel`, + }); + + // Return the session ID for the Stripe checkout session + return session.id; + } catch (error) { + console.error("Error creating checkout session:", error); + throw new Error("Failed to create checkout session."); + } +}; diff --git a/actions/createStripePortal.ts b/actions/createStripePortal.ts new file mode 100644 index 0000000..572f96c --- /dev/null +++ b/actions/createStripePortal.ts @@ -0,0 +1,32 @@ +"use server"; + +import { adminDb } from "@/firebaseAdmin"; +import getBaseUrl from "@/lib/getBaseUrl"; +import stripe from "@/lib/stripe"; +import { auth } from "@clerk/nextjs/server"; + +export async function createStripePortal() { + auth().protect(); + + const { userId } = await auth(); + + if (!userId) { + throw new Error("User not found"); + } + + // Get the Stripe customer ID from Firestore + const userDoc = await adminDb.collection("users").doc(userId).get(); + const stripeCustomerId = userDoc.data()?.stripeCustomerId; + + if (!stripeCustomerId) { + throw new Error("Stripe customer not found"); + } + + // Create a Stripe billing portal session + const session = await stripe.billingPortal.sessions.create({ + customer: stripeCustomerId, + return_url: `${getBaseUrl()}/dashboard`, // Redirect back to the dashboard after managing subscription + }); + + return session.url; // Return the URL to be used in the frontend for redirecting users +} diff --git a/app/dashboard/upgrade/page.tsx b/app/dashboard/upgrade/page.tsx new file mode 100644 index 0000000..291b7f4 --- /dev/null +++ b/app/dashboard/upgrade/page.tsx @@ -0,0 +1,77 @@ +//app/dashboard/upgrade/page.tsx +"use client"; +import React from "react"; +import getStripe from "@/lib/stripe-js"; // Ensure this utility is set up correctly +import SEO from "@/components/SEO"; // Importing SEO component + +const UpgradePage: React.FC = () => { + // Function to handle fetching Stripe session and redirecting to checkout + const getPriceFn = (plan: string) => { + fetch(`/api/checkout?plan=${plan}`) + .then((data) => data.json()) + .then(async (body) => { + const sessionId = body.sessionId; + const stripe = await getStripe(); + await stripe?.redirectToCheckout({ sessionId }); + }); + }; + + return ( +
+ {/* SEO Component */} + + +
+ {/* Free Plan */} +
+

Free Plan

+

Up to 50 requests per month.

+

Basic AI content generation.

+

Community support.

+ +
+ + {/* Pro Plan */} +
+

Pro Plan

+

500 requests per month.

+

Advanced AI content generation.

+

Priority email support.

+ +
+ + {/* Enterprise Plan */} +
+

Enterprise Plan

+

Unlimited requests.

+

Access to all AI features.

+

24/7 premium support.

+ +
+
+
+ ); +}; + +export default UpgradePage; diff --git a/app/webhook/route.ts b/app/webhook/route.ts new file mode 100644 index 0000000..a82ac95 --- /dev/null +++ b/app/webhook/route.ts @@ -0,0 +1,95 @@ +import { adminDb } from "@/firebaseAdmin"; +import stripe from "@/lib/stripe"; +import { headers } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; +import Stripe from "stripe"; + +export async function POST(req: NextRequest) { + const headersList = headers(); + const body = await req.text(); // important: must be req.text() not req.json() + const signature = headersList.get("stripe-signature"); + + if (!signature) { + return new Response("No signature", { status: 400 }); + } + + if (!process.env.STRIPE_WEBHOOK_SECRET) { + console.log("⚠️ Stripe webhook secret is not set."); + return new NextResponse("Stripe webhook secret is not set", { + status: 400, + }); + } + + let event: Stripe.Event; + + try { + event = stripe.webhooks.constructEvent( + body, + signature, + process.env.STRIPE_WEBHOOK_SECRET + ); + } catch (err) { + console.error(`Webhook Error: ${err}`); + return new NextResponse(`Webhook Error: ${err}`, { status: 400 }); + } + + // Helper function to get user details + const getUserDetails = async (customerId: string) => { + const userDoc = await adminDb + .collection("users") + .where("stripeCustomerId", "==", customerId) + .limit(1) + .get(); + + if (!userDoc.empty) { + return userDoc.docs[0]; + } + return null; + }; + + // Handling different event types + switch (event.type) { + case "checkout.session.completed": + case "payment_intent.succeeded": { + const invoice = event.data.object as Stripe.PaymentIntent; + const customerId = invoice.customer as string; + + const userDetails = await getUserDetails(customerId); + if (!userDetails?.id) { + console.error("User not found for customerId:", customerId); + return new NextResponse("User not found", { status: 404 }); + } + + // Update the user's membership status + await adminDb.collection("users").doc(userDetails.id).update({ + hasActiveMembership: true, + plan: "pro", // Assuming they are on the Pro plan after payment + }); + + break; + } + case "customer.subscription.deleted": + case "subscription_schedule.canceled": { + const subscription = event.data.object as Stripe.Subscription; + const customerId = subscription.customer as string; + + const userDetails = await getUserDetails(customerId); + if (!userDetails?.id) { + console.error("User not found for customerId:", customerId); + return new NextResponse("User not found", { status: 404 }); + } + + // Update the user's membership status to false + await adminDb.collection("users").doc(userDetails.id).update({ + hasActiveMembership: false, + plan: "free", // Downgrade to free plan if subscription is canceled + }); + break; + } + + default: + console.log(`Unhandled event type: ${event.type}`); + } + + return NextResponse.json({ message: "Webhook received and processed" }); +} diff --git a/lib/stripe-js.ts b/lib/stripe-js.ts index 2207124..c5e9277 100644 --- a/lib/stripe-js.ts +++ b/lib/stripe-js.ts @@ -1,14 +1,16 @@ +// lib/stripe-js.ts + import { loadStripe, Stripe } from "@stripe/stripe-js"; let stripePromise: Promise; -if (process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY === undefined) { - throw new Error("NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is not set"); -} - const getStripe = (): Promise => { if (!stripePromise) { - stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!); + if (process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY === undefined) { + throw new Error("NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is not set"); + } + + stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY); } return stripePromise; diff --git a/lib/stripe.ts b/lib/stripe.ts index f8c9239..fd77f49 100644 --- a/lib/stripe.ts +++ b/lib/stripe.ts @@ -1,3 +1,5 @@ +// lib/stripe.ts + import Stripe from "stripe"; const stripeSecretKey = process.env.STRIPE_SECRET_KEY; @@ -6,6 +8,8 @@ if (!stripeSecretKey) { throw new Error("STRIPE_SECRET_KEY is not set"); } -const stripe = new Stripe(stripeSecretKey); +const stripe = new Stripe(stripeSecretKey, { + apiVersion: "2024-06-20", +}); export default stripe; diff --git a/package.json b/package.json index ed05e61..4076454 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "inspiregem", - "version": "0.1.0", + "version": "1.0.0", "private": true, "scripts": { "dev": "next dev", @@ -12,6 +12,7 @@ "axios": "^1.7.7", "bcryptjs": "^2.4.3", "classnames": "^2.5.1", + "@clerk/nextjs": "^5.2.6", "dotenv": "^16.4.5", "firebase": "^10.12.4", "firebase-admin": "^11.4.1", @@ -34,6 +35,7 @@ "@stripe/stripe-js": "^4.4.0", "next-auth": "^4.24.7", "react-toastify": "^10.0.5", + "micro": "^10.0.1", "stripe": "^16.10.0", "tailwindcss": "^3.4.10" },