diff --git a/app/(dashboard)/(routes)/settings/constants.ts b/app/(dashboard)/(routes)/settings/constants.ts
new file mode 100644
index 0000000..d037863
--- /dev/null
+++ b/app/(dashboard)/(routes)/settings/constants.ts
@@ -0,0 +1,7 @@
+import * as z from "zod";
+
+export const formSchema = z.object({
+ prompt: z.string().min(1, {
+ message: "Prompt is required."
+ }),
+});
diff --git a/app/(dashboard)/(routes)/settings/page.tsx b/app/(dashboard)/(routes)/settings/page.tsx
new file mode 100644
index 0000000..d972a6d
--- /dev/null
+++ b/app/(dashboard)/(routes)/settings/page.tsx
@@ -0,0 +1,31 @@
+import { Settings } from "lucide-react";
+
+import { Heading } from "@/components/heading";
+import { SubscriptionButton } from "@/components/subscription-button";
+import { checkSubscription } from "@/lib/subscription";
+
+const SettingsPage = async () => {
+ const isPro = await checkSubscription();
+
+ return (
+
+
+
+
+ {isPro ? "You are currently on a Pro plan." : "You are currently on a free plan."}
+
+
+
+
+ );
+}
+
+export default SettingsPage;
+
diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx
index c5ed8eb..2ec9ef1 100644
--- a/app/(dashboard)/layout.tsx
+++ b/app/(dashboard)/layout.tsx
@@ -1,14 +1,16 @@
import Navbar from "@/components/navbar";
import { Sidebar } from "@/components/sidebar";
import { getApiLimitCount } from "@/lib/api-limit";
+import { checkSubscription } from "@/lib/subscription";
const DashboardLayout = async ({ children }: { children: React.ReactNode }) => {
const apiLimitCount = await getApiLimitCount();
+ const isPro = await checkSubscription()
return (
-
+
diff --git a/app/api/code/route.ts b/app/api/code/route.ts
index 1d09195..da87681 100644
--- a/app/api/code/route.ts
+++ b/app/api/code/route.ts
@@ -2,7 +2,7 @@ import { auth } from "@clerk/nextjs";
import { NextResponse } from "next/server";
import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from "openai";
-// import { checkSubscription } from "@/lib/subscription";
+import { checkSubscription } from "@/lib/subscription";
import { incrementApiLimit, checkApiLimit } from "@/lib/api-limit";
const configuration = new Configuration({
@@ -35,9 +35,9 @@ export async function POST(req: Request) {
}
const freeTrial = await checkApiLimit();
- // const isPro = await checkSubscription();
+ const isPro = await checkSubscription();
- if (!freeTrial) {
+ if (!freeTrial && !isPro) {
return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 });
}
@@ -46,9 +46,9 @@ export async function POST(req: Request) {
messages: [instructionMessage, ...messages]
});
- // if (!isPro) {
- await incrementApiLimit();
- // }
+ if (!isPro) {
+ await incrementApiLimit();
+ }
return NextResponse.json(response.data.choices[0].message);
} catch (error) {
diff --git a/app/api/conversation/route.ts b/app/api/conversation/route.ts
index 5c58f4b..a83a896 100644
--- a/app/api/conversation/route.ts
+++ b/app/api/conversation/route.ts
@@ -2,7 +2,7 @@ import { auth } from "@clerk/nextjs";
import { NextResponse } from "next/server";
import { Configuration, OpenAIApi } from "openai";
-// import { checkSubscription } from "@/lib/subscription";
+import { checkSubscription } from "@/lib/subscription";
import { incrementApiLimit, checkApiLimit } from "@/lib/api-limit";
const configuration = new Configuration({
@@ -31,10 +31,9 @@ export async function POST(req: Request) {
}
const freeTrial = await checkApiLimit();
- // const isPro = await checkSubscription();
+ const isPro = await checkSubscription();
- // if (!freeTrial && !isPro) {
- if (!freeTrial) {
+ if (!freeTrial && !isPro) {
return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 });
}
@@ -43,9 +42,9 @@ export async function POST(req: Request) {
messages
});
- // if (!isPro) {
- await incrementApiLimit();
- // }
+ if (!isPro) {
+ await incrementApiLimit();
+ }
return NextResponse.json(response.data.choices[0].message);
} catch (error) {
diff --git a/app/api/image/route.ts b/app/api/image/route.ts
index 520f272..1c42879 100644
--- a/app/api/image/route.ts
+++ b/app/api/image/route.ts
@@ -2,7 +2,7 @@ import { auth } from "@clerk/nextjs";
import { NextResponse } from "next/server";
import { Configuration, OpenAIApi } from "openai";
-// import { checkSubscription } from "@/lib/subscription";
+import { checkSubscription } from "@/lib/subscription";
import { incrementApiLimit, checkApiLimit } from "@/lib/api-limit";
const configuration = new Configuration({
@@ -38,9 +38,9 @@ export async function POST(req: Request) {
}
const freeTrial = await checkApiLimit();
- // const isPro = await checkSubscription();
+ const isPro = await checkSubscription();
- if (!freeTrial) {
+ if (!freeTrial && !isPro) {
return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 });
}
@@ -50,9 +50,9 @@ export async function POST(req: Request) {
size: resolution,
});
- // if (!isPro) {
- await incrementApiLimit();
- // }
+ if (!isPro) {
+ await incrementApiLimit();
+ }
return NextResponse.json(response.data.data);
} catch (error) {
diff --git a/app/api/music/route.ts b/app/api/music/route.ts
index 0f986ef..ea1e35c 100644
--- a/app/api/music/route.ts
+++ b/app/api/music/route.ts
@@ -2,7 +2,7 @@ import Replicate from "replicate";
import { auth } from "@clerk/nextjs";
import { NextResponse } from "next/server";
-// import { checkSubscription } from "@/lib/subscription";
+import { checkSubscription } from "@/lib/subscription";
import { incrementApiLimit, checkApiLimit } from "@/lib/api-limit";
const replicate = new Replicate({
@@ -24,9 +24,9 @@ export async function POST(req: Request) {
}
const freeTrial = await checkApiLimit();
- // const isPro = await checkSubscription();
+ const isPro = await checkSubscription();
- if (!freeTrial) {
+ if (!freeTrial && !isPro) {
return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 });
}
@@ -39,9 +39,9 @@ export async function POST(req: Request) {
}
);
- // if (!isPro) {
- await incrementApiLimit();
- // }
+ if (!isPro) {
+ await incrementApiLimit();
+ }
return NextResponse.json(response);
} catch (error) {
diff --git a/app/api/stripe/route.ts b/app/api/stripe/route.ts
new file mode 100644
index 0000000..85d4e1f
--- /dev/null
+++ b/app/api/stripe/route.ts
@@ -0,0 +1,67 @@
+import { auth, currentUser } from "@clerk/nextjs";
+import { NextResponse } from "next/server";
+
+import prismadb from "@/lib/prismadb";
+import { stripe } from "@/lib/stripe";
+import { absoluteUrl } from "@/lib/utils";
+
+const settingsUrl = absoluteUrl("/settings");
+
+export async function GET() {
+ try {
+ const { userId } = auth();
+ const user = await currentUser();
+
+ if (!userId || !user) {
+ return new NextResponse("Unauthorized", { status: 401 });
+ }
+
+ const userSubscription = await prismadb.userSubscription.findUnique({
+ where: {
+ userId
+ }
+ })
+
+ if (userSubscription && userSubscription.stripeCustomerId) {
+ const stripeSession = await stripe.billingPortal.sessions.create({
+ customer: userSubscription.stripeCustomerId,
+ return_url: settingsUrl,
+ })
+
+ return new NextResponse(JSON.stringify({ url: stripeSession.url }))
+ }
+
+ const stripeSession = await stripe.checkout.sessions.create({
+ success_url: settingsUrl,
+ cancel_url: settingsUrl,
+ payment_method_types: ["card"],
+ mode: "subscription",
+ billing_address_collection: "auto",
+ customer_email: user.emailAddresses[0].emailAddress,
+ line_items: [
+ {
+ price_data: {
+ currency: "INR",
+ product_data: {
+ name: "Genius Pro",
+ description: "Unlimited AI Generations"
+ },
+ unit_amount: 200000,
+ recurring: {
+ interval: "month"
+ }
+ },
+ quantity: 1,
+ },
+ ],
+ metadata: {
+ userId,
+ },
+ })
+
+ return new NextResponse(JSON.stringify({ url: stripeSession.url }))
+ } catch (error) {
+ console.log("[STRIPE_ERROR]", error);
+ return new NextResponse("Internal Error", { status: 500 });
+ }
+};
diff --git a/app/api/video/route.ts b/app/api/video/route.ts
index 24afdfc..f24f097 100644
--- a/app/api/video/route.ts
+++ b/app/api/video/route.ts
@@ -3,7 +3,7 @@ import { auth } from "@clerk/nextjs";
import { NextResponse } from "next/server";
import { incrementApiLimit, checkApiLimit } from "@/lib/api-limit";
-// import { checkSubscription } from "@/lib/subscription";
+import { checkSubscription } from "@/lib/subscription";
const replicate = new Replicate({
auth: process.env.REPLICATE_API_TOKEN!,
@@ -24,9 +24,9 @@ export async function POST(req: Request) {
}
const freeTrial = await checkApiLimit();
- // const isPro = await checkSubscription();
+ const isPro = await checkSubscription();
- if (!freeTrial) {
+ if (!freeTrial && !isPro) {
return new NextResponse("Free trial has expired. Please upgrade to pro.", { status: 403 });
}
@@ -39,9 +39,9 @@ export async function POST(req: Request) {
}
);
- // if (!isPro) {
- await incrementApiLimit();
- // }
+ if (!isPro) {
+ await incrementApiLimit();
+ }
return NextResponse.json(response);
} catch (error) {
diff --git a/app/api/webhook/route.ts b/app/api/webhook/route.ts
new file mode 100644
index 0000000..bc1423d
--- /dev/null
+++ b/app/api/webhook/route.ts
@@ -0,0 +1,67 @@
+import Stripe from "stripe"
+import { headers } from "next/headers"
+import { NextResponse } from "next/server"
+
+import prismadb from "@/lib/prismadb"
+import { stripe } from "@/lib/stripe"
+
+export async function POST(req: Request) {
+ const body = await req.text()
+ const signature = headers().get("Stripe-Signature") as string
+
+ let event: Stripe.Event
+
+ try {
+ event = stripe.webhooks.constructEvent(
+ body,
+ signature,
+ process.env.STRIPE_WEBHOOK_SECRET!
+ )
+ } catch (error: any) {
+ return new NextResponse(`Webhook Error: ${error.message}`, { status: 400 })
+ }
+
+ const session = event.data.object as Stripe.Checkout.Session
+
+ if (event.type === "checkout.session.completed") {
+ const subscription = await stripe.subscriptions.retrieve(
+ session.subscription as string
+ )
+
+ if (!session?.metadata?.userId) {
+ return new NextResponse("User id is required", { status: 400 });
+ }
+
+ await prismadb.userSubscription.create({
+ data: {
+ userId: session?.metadata?.userId,
+ stripeSubscriptionId: subscription.id,
+ stripeCustomerId: subscription.customer as string,
+ stripePriceId: subscription.items.data[0].price.id,
+ stripeCurrentPeriodEnd: new Date(
+ subscription.current_period_end * 1000
+ ),
+ },
+ })
+ }
+
+ if (event.type === "invoice.payment_succeeded") {
+ const subscription = await stripe.subscriptions.retrieve(
+ session.subscription as string
+ )
+
+ await prismadb.userSubscription.update({
+ where: {
+ stripeSubscriptionId: subscription.id,
+ },
+ data: {
+ stripePriceId: subscription.items.data[0].price.id,
+ stripeCurrentPeriodEnd: new Date(
+ subscription.current_period_end * 1000
+ ),
+ },
+ })
+ }
+
+ return new NextResponse(null, { status: 200 })
+};
diff --git a/components/free-counter.tsx b/components/free-counter.tsx
index 84a9e35..1fb75a4 100644
--- a/components/free-counter.tsx
+++ b/components/free-counter.tsx
@@ -10,7 +10,7 @@ import { MAX_FREE_COUNTS } from "@/constants";
import { useProModal } from "@/hooks/use-pro-modal";
interface FreeCounterProps {
- isPro?: boolean;
+ isPro: boolean;
apiLimitCount: number;
}
diff --git a/components/mobile-sidebar.tsx b/components/mobile-sidebar.tsx
index 8e162f2..95a2dd2 100644
--- a/components/mobile-sidebar.tsx
+++ b/components/mobile-sidebar.tsx
@@ -1,17 +1,18 @@
"use client";
-import { useEffect, useState } from "react";
import { Menu } from "lucide-react";
+import { useEffect, useState } from "react";
+import { Sidebar } from "@/components/sidebar";
import { Button } from "@/components/ui/button";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
-import { Sidebar } from "@/components/sidebar";
interface MobileSidebarProps {
apiLimitCount: number
+ isPro: boolean
}
-export const MobileSidebar: React.FC = ({ apiLimitCount }) => {
+export const MobileSidebar: React.FC = ({ apiLimitCount = 0, isPro = false }) => {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => setIsMounted(true), []);
@@ -26,7 +27,7 @@ export const MobileSidebar: React.FC = ({ apiLimitCount }) =
-
+
);
diff --git a/components/navbar.tsx b/components/navbar.tsx
index 1427536..dcd2106 100644
--- a/components/navbar.tsx
+++ b/components/navbar.tsx
@@ -2,13 +2,15 @@ import { UserButton } from "@clerk/nextjs";
import { MobileSidebar } from "@/components/mobile-sidebar";
import { getApiLimitCount } from "@/lib/api-limit";
+import { checkSubscription } from "@/lib/subscription";
const Navbar: React.FC = async () => {
const apiLimitCount = await getApiLimitCount();
+ const isPro = await checkSubscription()
return (
-
+
diff --git a/components/sidebar.tsx b/components/sidebar.tsx
index 4466310..ef6bc9b 100644
--- a/components/sidebar.tsx
+++ b/components/sidebar.tsx
@@ -58,9 +58,10 @@ const routes = [
interface SidebarProps {
apiLimitCount: number;
+ isPro: boolean;
}
-export const Sidebar: React.FC
= ({ apiLimitCount = 0 }) => {
+export const Sidebar: React.FC = ({ apiLimitCount = 0, isPro = false }) => {
const pathname = usePathname();
return (
@@ -95,7 +96,7 @@ export const Sidebar: React.FC = ({ apiLimitCount = 0 }) => {
);
diff --git a/components/subscription-button.tsx b/components/subscription-button.tsx
new file mode 100644
index 0000000..3e4acb0
--- /dev/null
+++ b/components/subscription-button.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+import axios from "axios";
+import { useState } from "react";
+import { Zap } from "lucide-react";
+import { toast } from "react-hot-toast";
+
+import { Button } from "@/components/ui/button";
+
+export const SubscriptionButton = ({ isPro = false }: { isPro: boolean }) => {
+ const [loading, setLoading] = useState(false);
+
+ const onClick = async () => {
+ try {
+ setLoading(true);
+
+ const response = await axios.get("/api/stripe");
+
+ window.location.href = response.data.url;
+ } catch (error) {
+ toast.error("Something went wrong");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ )
+};
diff --git a/lib/stripe.ts b/lib/stripe.ts
new file mode 100644
index 0000000..a297572
--- /dev/null
+++ b/lib/stripe.ts
@@ -0,0 +1,6 @@
+import Stripe from "stripe"
+
+export const stripe = new Stripe(process.env.STRIPE_API_KEY!, {
+ apiVersion: "2022-11-15",
+ typescript: true,
+});
diff --git a/lib/subscription.ts b/lib/subscription.ts
new file mode 100644
index 0000000..47d9c83
--- /dev/null
+++ b/lib/subscription.ts
@@ -0,0 +1,31 @@
+import { auth } from "@clerk/nextjs";
+
+import prismadb from "@/lib/prismadb";
+
+const DAY_IN_MS = 86_400_000;
+
+export const checkSubscription = async () => {
+ const { userId } = auth();
+
+ if (!userId) return false
+
+ const userSubscription = await prismadb.userSubscription.findUnique({
+ where: {
+ userId: userId,
+ },
+ select: {
+ stripeSubscriptionId: true,
+ stripeCurrentPeriodEnd: true,
+ stripeCustomerId: true,
+ stripePriceId: true,
+ },
+ })
+
+ if (!userSubscription) return false;
+
+ const isValid =
+ userSubscription.stripePriceId &&
+ userSubscription.stripeCurrentPeriodEnd?.getTime()! + DAY_IN_MS > Date.now()
+
+ return !!isValid;
+};
diff --git a/lib/utils.ts b/lib/utils.ts
index ec79801..66894a5 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -1,6 +1,10 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
-
+
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+
+export function absoluteUrl(path: string) {
+ return `${process.env.NEXT_PUBLIC_APP_URL}${path}`
+}
\ No newline at end of file
diff --git a/middleware.ts b/middleware.ts
index 73ad107..519729f 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -4,7 +4,7 @@ import { authMiddleware } from "@clerk/nextjs";
// Please edit this to allow other routes to be public as needed.
// See https://clerk.com/docs/nextjs/middleware for more information about configuring your middleware
export default authMiddleware({
- publicRoutes: ['/']
+ publicRoutes: ['/', '/api/webhook']
});
export const config = {
diff --git a/package.json b/package.json
index b9bf0e7..5b94635 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
"react-hot-toast": "^2.4.1",
"react-markdown": "^8.0.7",
"replicate": "^0.14.0",
+ "stripe": "^12.16.0",
"tailwind-merge": "^1.14.0",
"tailwindcss": "3.3.3",
"tailwindcss-animate": "^1.0.6",
diff --git a/yarn.lock b/yarn.lock
index adc85eb..85b0a3c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -756,7 +756,7 @@
"@types/node" "*"
form-data "^3.0.0"
-"@types/node@*", "@types/node@20.4.5":
+"@types/node@*", "@types/node@20.4.5", "@types/node@>=8.1.0":
version "20.4.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.5.tgz#9dc0a5cb1ccce4f7a731660935ab70b9c00a5d69"
integrity sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg==
@@ -3150,6 +3150,13 @@ pvutils@^1.1.3:
resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.3.tgz#f35fc1d27e7cd3dfbd39c0826d173e806a03f5a3"
integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==
+qs@^6.11.0:
+ version "6.11.2"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9"
+ integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==
+ dependencies:
+ side-channel "^1.0.4"
+
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
@@ -3557,6 +3564,14 @@ strip-json-comments@^3.1.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
+stripe@^12.16.0:
+ version "12.16.0"
+ resolved "https://registry.yarnpkg.com/stripe/-/stripe-12.16.0.tgz#a52f3ab9480b10435dd1e9792820c65bfd406506"
+ integrity sha512-iwVkyZsS9KsWmEVK5qnbPeX/m94+Z3fXIRU7Q4iNBS2Zuj1spGIjLKMN3tejUs/ZZ2o7dCYFJvVdC2aZMYo8GA==
+ dependencies:
+ "@types/node" ">=8.1.0"
+ qs "^6.11.0"
+
style-to-object@^0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.4.1.tgz#53cf856f7cf7f172d72939d9679556469ba5de37"