Skip to content

Commit

Permalink
last feature phase 1: pro subscription added
Browse files Browse the repository at this point in the history
Signed-off-by: ARYPROGRAMMER <arya.2023ug1104@iiitranchi.ac.in>
  • Loading branch information
ARYPROGRAMMER committed Dec 22, 2024
1 parent 467a87b commit a85142d
Show file tree
Hide file tree
Showing 13 changed files with 402 additions and 6 deletions.
2 changes: 2 additions & 0 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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;
}>;
Expand Down
47 changes: 45 additions & 2 deletions convex/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -59,9 +104,7 @@ http.route({
}
}
return new Response("Webhook processed successfully", { status: 200 });

}),
});


export default http;
28 changes: 28 additions & 0 deletions convex/lemonSqueezy.ts
Original file line number Diff line number Diff line change
@@ -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);
},
});
29 changes: 28 additions & 1 deletion convex/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,31 @@ export const getUser = query({
if (!user)return null;
return user;
}
})
})


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 };
},
});
5 changes: 3 additions & 2 deletions src/app/(home)/_components/HeaderProfileBtn.tsx
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -16,7 +17,7 @@ function HeaderProfileBtn() {
</UserButton>

<SignedOut>
<SignInButton />
<LoginButton />
</SignedOut>
</>
);
Expand Down
45 changes: 45 additions & 0 deletions src/app/pricing/_components/BeingProPlan.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className=" bg-[#0a0a0f]">
<NavigationHeader />
<div className="relative px-4 h-[80vh] flex items-center justify-center">
<div className="relative max-w-xl mx-auto text-center">
<div className="absolute inset-x-0 -top-px h-px bg-gradient-to-r from-transparent via-blue-500/50 to-transparent" />
<div className="absolute inset-x-0 -bottom-px h-px bg-gradient-to-r from-transparent via-purple-500/50 to-transparent" />
<div className="absolute -inset-0.5 bg-gradient-to-r from-blue-500/30 to-purple-500/30 blur-2xl opacity-10" />

<div className="relative bg-[#12121a]/90 border border-gray-800/50 backdrop-blur-2xl rounded-2xl p-12">
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/[0.05] to-purple-500/[0.05] rounded-2xl" />

<div className="relative">
<div className="inline-flex p-4 rounded-2xl bg-gradient-to-br from-purple-500/10 to-blue-500/10 mb-6 ring-1 ring-gray-800/60">
<Star className="w-8 h-8 text-purple-400" />
</div>

<h1 className="text-3xl font-semibold text-white mb-3">
Pro Plan Active
</h1>
<p className="text-gray-400 mb-8 text-lg">
Experience the full power of professional development
</p>

<Link
href="/"
className="inline-flex items-center justify-center gap-2 w-full px-8 py-4 bg-gradient-to-r from-blue-500/10 to-purple-500/10 hover:from-blue-500/20 hover:to-purple-500/20 text-white rounded-xl transition-all duration-200 border border-gray-800 hover:border-blue-500/50 group"
>
<Command className="w-5 h-5 text-blue-400" />
<span>Open Editor</span>
<ArrowRight className="w-5 h-5 text-purple-400 group-hover:translate-x-0.5 transition-transform" />
</Link>
</div>
</div>
</div>
</div>
</div>
);
}
export default BeingProPlan;
8 changes: 8 additions & 0 deletions src/app/pricing/_components/Category.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const FeatureCategory = ({ children, label }: { children: React.ReactNode; label: string }) => (
<div className="space-y-4">
<h3 className="text-sm font-medium text-gray-400 uppercase tracking-wider">{label}</h3>
<div className="space-y-3">{children}</div>
</div>
);

export default FeatureCategory;
12 changes: 12 additions & 0 deletions src/app/pricing/_components/Item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Check } from "lucide-react";

const FeatureItem = ({ children }: { children: React.ReactNode }) => (
<div className="flex items-start gap-3 group">
<div className="mt-1 flex-shrink-0 w-5 h-5 rounded-full bg-blue-500/10 flex items-center justify-center border border-blue-500/20 group-hover:border-blue-500/40 group-hover:bg-blue-500/20 transition-colors">
<Check className="w-3 h-3 text-blue-400" />
</div>
<span className="text-gray-400 group-hover:text-gray-300 transition-colors">{children}</span>
</div>
);

export default FeatureItem;
19 changes: 19 additions & 0 deletions src/app/pricing/_components/Upgrade.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Link
href={CHEKOUT_URL}
className="inline-flex items-center justify-center gap-2 px-8 py-4 text-white
bg-gradient-to-r from-blue-500 to-blue-600 rounded-lg
hover:from-blue-600 hover:to-blue-700 transition-all"
>
<Zap className="w-5 h-5" />
Upgrade to Pro
</Link>
);
}
45 changes: 45 additions & 0 deletions src/app/pricing/_constants/index.ts
Original file line number Diff line number Diff line change
@@ -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",
],
};
Loading

0 comments on commit a85142d

Please sign in to comment.