diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 1f35d6b..8f885ce 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -15,6 +15,7 @@ import type { } from "convex/server"; import type * as codeExecutions from "../codeExecutions.js"; import type * as http from "../http.js"; +import type * as lemonSqueezy from "../lemonSqueezy.js"; import type * as snippets from "../snippets.js"; import type * as users from "../users.js"; @@ -29,6 +30,7 @@ import type * as users from "../users.js"; declare const fullApi: ApiFromModules<{ codeExecutions: typeof codeExecutions; http: typeof http; + lemonSqueezy: typeof lemonSqueezy; snippets: typeof snippets; users: typeof users; }>; diff --git a/convex/http.ts b/convex/http.ts index 3ee81ca..36d759d 100644 --- a/convex/http.ts +++ b/convex/http.ts @@ -5,6 +5,51 @@ import { WebhookEvent } from "@clerk/nextjs/server"; import { api, internal } from "./_generated/api"; const http = httpRouter(); +http.route({ + path: "/lemon-squeezy-webhook", + method: "POST", + handler: httpAction(async (ctx, req) => { + const payloadString = await req.json(); + const signature = req.headers.get("X-Signature"); + if (!signature) { + return new Response("Missing signature", { status: 400 }); + } + try { + const payload = await ctx.runAction(internal.lemonSqueezy.verifyWebhook, { + payload: payloadString, + signature, + }); + + if (payload.meta.event_name === "order.created") { + const { data } = payload; + const {success} = await ctx.runMutation(api.users.upgradePro,{ + email: data.attributes.use_email, + lemonSqueezyCustomerId: data.attributes.customer_id.toString(), + lemonSqueezyOrderId: data.id, + amount: data.attributes.total, + }); + + if (success) { + + + + + + + + + } + } + + return new Response("Webhook processed successfully", { status: 200 }); + + } catch (e) { + console.log("Webhook error:", e); + return new Response("Error processing webhook", { status: 500 }); + } + }), +}); + http.route({ path: "/clerk-webhook", method: "POST", @@ -59,9 +104,7 @@ http.route({ } } return new Response("Webhook processed successfully", { status: 200 }); - }), }); - export default http; diff --git a/convex/lemonSqueezy.ts b/convex/lemonSqueezy.ts new file mode 100644 index 0000000..f1337d7 --- /dev/null +++ b/convex/lemonSqueezy.ts @@ -0,0 +1,28 @@ +"use node"; +import { v } from "convex/values"; +import { internalAction } from "./_generated/server"; +import { createHmac } from "crypto"; + +const webhookSecret = process.env.NEXT_PUBLIC_CLERK_WEBHOOK_SECRET!; + +function verifySignature(payload: string, signature: string) { + const hmac = createHmac("sha256", webhookSecret); + const computedSignature = hmac.update(payload).digest("hex"); + return computedSignature === signature; +} + +export const verifyWebhook = internalAction({ + args: { + payload: v.string(), + signature: v.string(), + }, + handler: async (ctx, args) => { + const isValid = verifySignature(args.payload, args.signature); + + if (!isValid) { + throw new Error("Invalid signature"); + } + + return JSON.parse(args.payload); + }, +}); diff --git a/convex/users.ts b/convex/users.ts index 90f0881..af1e914 100644 --- a/convex/users.ts +++ b/convex/users.ts @@ -39,4 +39,31 @@ export const getUser = query({ if (!user)return null; return user; } -}) \ No newline at end of file +}) + + +export const upgradePro = mutation({ + args: { + email: v.string(), + lemonSqueezyCustomerId: v.string(), + lemonSqueezyOrderId: v.string(), + amount: v.number(), + }, + handler: async (ctx, args) => { + const user = await ctx.db + .query("users") + .filter((q) => q.eq(q.field("email"), args.email)) + .first(); + + if (!user) throw new Error("User not found"); + + await ctx.db.patch(user._id, { + isPro: true, + proSince: Date.now(), + lemonSqueezyCustomerId: args.lemonSqueezyCustomerId, + lemonSqueezyOrderId: args.lemonSqueezyOrderId, + }); + + return { success: true }; + }, + }); \ No newline at end of file diff --git a/src/app/(home)/_components/HeaderProfileBtn.tsx b/src/app/(home)/_components/HeaderProfileBtn.tsx index f0efc71..61d6221 100644 --- a/src/app/(home)/_components/HeaderProfileBtn.tsx +++ b/src/app/(home)/_components/HeaderProfileBtn.tsx @@ -1,5 +1,6 @@ "use client"; -import { SignedOut, SignInButton, UserButton } from "@clerk/nextjs"; +import LoginButton from "@/components/ui/LoginButton"; +import { SignedOut, UserButton } from "@clerk/nextjs"; import { User } from "lucide-react"; function HeaderProfileBtn() { @@ -16,7 +17,7 @@ function HeaderProfileBtn() { - + ); diff --git a/src/app/pricing/_components/BeingProPlan.tsx b/src/app/pricing/_components/BeingProPlan.tsx new file mode 100644 index 0000000..ae3b491 --- /dev/null +++ b/src/app/pricing/_components/BeingProPlan.tsx @@ -0,0 +1,45 @@ +import NavigationHeader from "@/components/ui/NavigationHeader"; +import { ArrowRight, Command, Star } from "lucide-react"; +import Link from "next/link"; + +function BeingProPlan() { + return ( +
+ +
+
+
+
+
+ +
+
+ +
+
+ +
+ +

+ Pro Plan Active +

+

+ Experience the full power of professional development +

+ + + + Open Editor + + +
+
+
+
+
+ ); +} +export default BeingProPlan; diff --git a/src/app/pricing/_components/Category.tsx b/src/app/pricing/_components/Category.tsx new file mode 100644 index 0000000..a383f3a --- /dev/null +++ b/src/app/pricing/_components/Category.tsx @@ -0,0 +1,8 @@ +const FeatureCategory = ({ children, label }: { children: React.ReactNode; label: string }) => ( +
+

{label}

+
{children}
+
+ ); + + export default FeatureCategory; \ No newline at end of file diff --git a/src/app/pricing/_components/Item.tsx b/src/app/pricing/_components/Item.tsx new file mode 100644 index 0000000..a05795b --- /dev/null +++ b/src/app/pricing/_components/Item.tsx @@ -0,0 +1,12 @@ +import { Check } from "lucide-react"; + +const FeatureItem = ({ children }: { children: React.ReactNode }) => ( +
+
+ +
+ {children} +
+); + +export default FeatureItem; \ No newline at end of file diff --git a/src/app/pricing/_components/Upgrade.tsx b/src/app/pricing/_components/Upgrade.tsx new file mode 100644 index 0000000..b0f8332 --- /dev/null +++ b/src/app/pricing/_components/Upgrade.tsx @@ -0,0 +1,19 @@ +import { Zap } from "lucide-react"; +import Link from "next/link"; + +export default function UpgradeButton() { + const CHEKOUT_URL = + "https://arya-opensource.lemonsqueezy.com/buy/c4c9a31d-3c39-4678-aa64-70fc57205d60"; + + return ( + + + Upgrade to Pro + + ); +} \ No newline at end of file diff --git a/src/app/pricing/_constants/index.ts b/src/app/pricing/_constants/index.ts new file mode 100644 index 0000000..2e98552 --- /dev/null +++ b/src/app/pricing/_constants/index.ts @@ -0,0 +1,45 @@ +import { Boxes, Globe, RefreshCcw, Shield } from "lucide-react"; + +export const ENTERPRISE_FEATURES = [ + { + icon: Globe, + label: "Global Infrastructure", + desc: "Lightning-fast execution across worldwide edge nodes", + }, + { + icon: Shield, + label: "Enterprise Security", + desc: "Bank-grade encryption and security protocols", + }, + { + icon: RefreshCcw, + label: "Real-time Sync", + desc: "Instant synchronization across all devices", + }, + { + icon: Boxes, + label: "Unlimited Storage", + desc: "Store unlimited snippets and projects", + }, +]; + +export const FEATURES = { + development: [ + "Advanced AI", + "Custom theme builder", + "Integrated debugging tools", + "Multi-language support", + ], + collaboration: [ + "Real-time pair programming", + "Team workspaces", + "Version control integration", + "Code review tools", + ], + deployment: [ + "One-click deployment", + "CI/CD integration", + "Container support", + "Custom domain mapping", + ], +}; \ No newline at end of file diff --git a/src/app/pricing/page.tsx b/src/app/pricing/page.tsx new file mode 100644 index 0000000..06cf7ae --- /dev/null +++ b/src/app/pricing/page.tsx @@ -0,0 +1,150 @@ +import { currentUser } from "@clerk/nextjs/server"; +import { ConvexHttpClient } from "convex/browser"; +import React from "react"; +import { api } from "../../../convex/_generated/api"; +import BeingProPlan from "./_components/BeingProPlan"; +import { Star } from "lucide-react"; +import { ENTERPRISE_FEATURES, FEATURES } from "./_constants"; +import NavigationHeader from "@/components/ui/NavigationHeader"; +import { SignedIn, SignedOut } from "@clerk/nextjs"; +import UpgradeButton from "./_components/Upgrade"; +import FeatureItem from "./_components/Item"; +import FeatureCategory from "./_components/Category"; +import LoginButton from "@/components/ui/LoginButton"; + +async function PricingPage() { + const user = await currentUser(); + const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!); + + const convexUser = await convex.query(api.users.getUser, { + userId: user?.id || "", + }); + + if (convexUser?.isPro) return ; + + return ( + +
+ + + {/* main content */} + +
+
+ {/* Hero */} +
+
+
+

+ Elevate Your
+ Development Experience +

+
+

+ Join the next generation of developers with our professional suite of tools +

+
+ + {/* Enterprise Features */} +
+ {ENTERPRISE_FEATURES.map((feature) => ( +
+
+
+ +
+ +

{feature.label}

+

{feature.desc}

+
+
+ ))} +
+ + {/* Pricing Card */} + +
+
+
+
+
+ +
+ {/* header */} +
+
+ +
+

Lifetime Pro Access

+
+ + + 799 + + one-time +
+

Unlock the full potential of CodeX

+
+ + {/* Features grid */} +
+ + {FEATURES.development.map((feature, idx) => ( + {feature} + ))} + + + + {FEATURES.collaboration.map((feature, idx) => ( + {feature} + ))} + + + + {FEATURES.deployment.map((feature, idx) => ( + {feature} + ))} + +
+ + {/* CTA */} +
+ + + + + + + +
+
+
+
+
+
+
+ + + + ) +} + +export default PricingPage; diff --git a/src/app/snippets/page.tsx b/src/app/snippets/page.tsx index 5f237c3..a76a0fb 100644 --- a/src/app/snippets/page.tsx +++ b/src/app/snippets/page.tsx @@ -182,7 +182,6 @@ function SnippetsPage() { - {/* edge case: empty state */} {filteredSnippets.length === 0 && ( + + + ); +} +export default LoginButton; \ No newline at end of file