Skip to content

Commit

Permalink
Merge pull request #75 from ArhanAnsari/ArhanAnsari-patch-1
Browse files Browse the repository at this point in the history
Upgrade Plans
  • Loading branch information
ArhanAnsari authored Oct 6, 2024
2 parents 77bdfa3 + bc28aa5 commit 6617297
Show file tree
Hide file tree
Showing 7 changed files with 305 additions and 7 deletions.
86 changes: 86 additions & 0 deletions actions/createCheckoutSession.ts
Original file line number Diff line number Diff line change
@@ -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.");
}
};
32 changes: 32 additions & 0 deletions actions/createStripePortal.ts
Original file line number Diff line number Diff line change
@@ -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
}
77 changes: 77 additions & 0 deletions app/dashboard/upgrade/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="min-h-screen bg-gray-50 py-10 px-4">
{/* SEO Component */}
<SEO
title="Upgrade Your Plan - InspireGem"
description="Upgrade to a higher plan on InspireGem and unlock advanced AI content generation features."
/>

<div className="max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-8">
{/* Free Plan */}
<div className="bg-white rounded-lg shadow-lg p-6 hover:shadow-xl transition duration-300 transform hover:scale-105">
<h2 className="text-3xl font-bold text-blue-600 mb-4">Free Plan</h2>
<p className="text-gray-600 mb-4">Up to 50 requests per month.</p>
<p className="text-gray-600 mb-4">Basic AI content generation.</p>
<p className="text-gray-600 mb-6">Community support.</p>
<button
type="button"
className="w-full text-center text-white bg-blue-500 hover:bg-blue-600 font-bold py-3 rounded-lg transition duration-300 transform hover:scale-105"
onClick={() => getPriceFn("free")}
>
Continue with Free Plan
</button>
</div>

{/* Pro Plan */}
<div className="bg-white rounded-lg shadow-lg p-6 hover:shadow-xl transition duration-300 transform hover:scale-105">
<h2 className="text-3xl font-bold text-green-600 mb-4">Pro Plan</h2>
<p className="text-gray-600 mb-4">500 requests per month.</p>
<p className="text-gray-600 mb-4">Advanced AI content generation.</p>
<p className="text-gray-600 mb-6">Priority email support.</p>
<button
type="button"
className="w-full text-center text-white bg-green-500 hover:bg-green-600 font-bold py-3 rounded-lg transition duration-300 transform hover:scale-105"
onClick={() => getPriceFn("pro")}
>
Upgrade to Pro - ₹499/month
</button>
</div>

{/* Enterprise Plan */}
<div className="bg-white rounded-lg shadow-lg p-6 hover:shadow-xl transition duration-300 transform hover:scale-105">
<h2 className="text-3xl font-bold text-red-600 mb-4">Enterprise Plan</h2>
<p className="text-gray-600 mb-4">Unlimited requests.</p>
<p className="text-gray-600 mb-4">Access to all AI features.</p>
<p className="text-gray-600 mb-6">24/7 premium support.</p>
<button
type="button"
className="w-full text-center text-white bg-red-500 hover:bg-red-600 font-bold py-3 rounded-lg transition duration-300 transform hover:scale-105"
onClick={() => getPriceFn("enterprise")}
>
Upgrade to Enterprise - ₹1,999/month
</button>
</div>
</div>
</div>
);
};

export default UpgradePage;
95 changes: 95 additions & 0 deletions app/webhook/route.ts
Original file line number Diff line number Diff line change
@@ -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" });
}
12 changes: 7 additions & 5 deletions lib/stripe-js.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
// lib/stripe-js.ts

import { loadStripe, Stripe } from "@stripe/stripe-js";

let stripePromise: Promise<Stripe | null>;

if (process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY === undefined) {
throw new Error("NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is not set");
}

const getStripe = (): Promise<Stripe | null> => {
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;
Expand Down
6 changes: 5 additions & 1 deletion lib/stripe.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// lib/stripe.ts

import Stripe from "stripe";

const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
Expand All @@ -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;
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "inspiregem",
"version": "0.1.0",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
Expand All @@ -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",
Expand All @@ -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"
},
Expand Down

0 comments on commit 6617297

Please sign in to comment.