-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #75 from ArhanAnsari/ArhanAnsari-patch-1
Upgrade Plans
- Loading branch information
Showing
7 changed files
with
305 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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."); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" }); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters