Skip to content

Commit 187fbeb

Browse files
Merge pull request #20 from ARYPROGRAMMER/develop/home
last feature phase 1: pro subscription added
2 parents 662cf38 + a85142d commit 187fbeb

File tree

13 files changed

+402
-6
lines changed

13 files changed

+402
-6
lines changed

convex/_generated/api.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
} from "convex/server";
1616
import type * as codeExecutions from "../codeExecutions.js";
1717
import type * as http from "../http.js";
18+
import type * as lemonSqueezy from "../lemonSqueezy.js";
1819
import type * as snippets from "../snippets.js";
1920
import type * as users from "../users.js";
2021

@@ -29,6 +30,7 @@ import type * as users from "../users.js";
2930
declare const fullApi: ApiFromModules<{
3031
codeExecutions: typeof codeExecutions;
3132
http: typeof http;
33+
lemonSqueezy: typeof lemonSqueezy;
3234
snippets: typeof snippets;
3335
users: typeof users;
3436
}>;

convex/http.ts

+45-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,51 @@ import { WebhookEvent } from "@clerk/nextjs/server";
55
import { api, internal } from "./_generated/api";
66
const http = httpRouter();
77

8+
http.route({
9+
path: "/lemon-squeezy-webhook",
10+
method: "POST",
11+
handler: httpAction(async (ctx, req) => {
12+
const payloadString = await req.json();
13+
const signature = req.headers.get("X-Signature");
14+
if (!signature) {
15+
return new Response("Missing signature", { status: 400 });
16+
}
17+
try {
18+
const payload = await ctx.runAction(internal.lemonSqueezy.verifyWebhook, {
19+
payload: payloadString,
20+
signature,
21+
});
22+
23+
if (payload.meta.event_name === "order.created") {
24+
const { data } = payload;
25+
const {success} = await ctx.runMutation(api.users.upgradePro,{
26+
email: data.attributes.use_email,
27+
lemonSqueezyCustomerId: data.attributes.customer_id.toString(),
28+
lemonSqueezyOrderId: data.id,
29+
amount: data.attributes.total,
30+
});
31+
32+
if (success) {
33+
34+
35+
36+
37+
38+
39+
40+
41+
}
42+
}
43+
44+
return new Response("Webhook processed successfully", { status: 200 });
45+
46+
} catch (e) {
47+
console.log("Webhook error:", e);
48+
return new Response("Error processing webhook", { status: 500 });
49+
}
50+
}),
51+
});
52+
853
http.route({
954
path: "/clerk-webhook",
1055
method: "POST",
@@ -59,9 +104,7 @@ http.route({
59104
}
60105
}
61106
return new Response("Webhook processed successfully", { status: 200 });
62-
63107
}),
64108
});
65109

66-
67110
export default http;

convex/lemonSqueezy.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"use node";
2+
import { v } from "convex/values";
3+
import { internalAction } from "./_generated/server";
4+
import { createHmac } from "crypto";
5+
6+
const webhookSecret = process.env.NEXT_PUBLIC_CLERK_WEBHOOK_SECRET!;
7+
8+
function verifySignature(payload: string, signature: string) {
9+
const hmac = createHmac("sha256", webhookSecret);
10+
const computedSignature = hmac.update(payload).digest("hex");
11+
return computedSignature === signature;
12+
}
13+
14+
export const verifyWebhook = internalAction({
15+
args: {
16+
payload: v.string(),
17+
signature: v.string(),
18+
},
19+
handler: async (ctx, args) => {
20+
const isValid = verifySignature(args.payload, args.signature);
21+
22+
if (!isValid) {
23+
throw new Error("Invalid signature");
24+
}
25+
26+
return JSON.parse(args.payload);
27+
},
28+
});

convex/users.ts

+28-1
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,31 @@ export const getUser = query({
3939
if (!user)return null;
4040
return user;
4141
}
42-
})
42+
})
43+
44+
45+
export const upgradePro = mutation({
46+
args: {
47+
email: v.string(),
48+
lemonSqueezyCustomerId: v.string(),
49+
lemonSqueezyOrderId: v.string(),
50+
amount: v.number(),
51+
},
52+
handler: async (ctx, args) => {
53+
const user = await ctx.db
54+
.query("users")
55+
.filter((q) => q.eq(q.field("email"), args.email))
56+
.first();
57+
58+
if (!user) throw new Error("User not found");
59+
60+
await ctx.db.patch(user._id, {
61+
isPro: true,
62+
proSince: Date.now(),
63+
lemonSqueezyCustomerId: args.lemonSqueezyCustomerId,
64+
lemonSqueezyOrderId: args.lemonSqueezyOrderId,
65+
});
66+
67+
return { success: true };
68+
},
69+
});

src/app/(home)/_components/HeaderProfileBtn.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
2-
import { SignedOut, SignInButton, UserButton } from "@clerk/nextjs";
2+
import LoginButton from "@/components/ui/LoginButton";
3+
import { SignedOut, UserButton } from "@clerk/nextjs";
34
import { User } from "lucide-react";
45

56
function HeaderProfileBtn() {
@@ -16,7 +17,7 @@ function HeaderProfileBtn() {
1617
</UserButton>
1718

1819
<SignedOut>
19-
<SignInButton />
20+
<LoginButton />
2021
</SignedOut>
2122
</>
2223
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import NavigationHeader from "@/components/ui/NavigationHeader";
2+
import { ArrowRight, Command, Star } from "lucide-react";
3+
import Link from "next/link";
4+
5+
function BeingProPlan() {
6+
return (
7+
<div className=" bg-[#0a0a0f]">
8+
<NavigationHeader />
9+
<div className="relative px-4 h-[80vh] flex items-center justify-center">
10+
<div className="relative max-w-xl mx-auto text-center">
11+
<div className="absolute inset-x-0 -top-px h-px bg-gradient-to-r from-transparent via-blue-500/50 to-transparent" />
12+
<div className="absolute inset-x-0 -bottom-px h-px bg-gradient-to-r from-transparent via-purple-500/50 to-transparent" />
13+
<div className="absolute -inset-0.5 bg-gradient-to-r from-blue-500/30 to-purple-500/30 blur-2xl opacity-10" />
14+
15+
<div className="relative bg-[#12121a]/90 border border-gray-800/50 backdrop-blur-2xl rounded-2xl p-12">
16+
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/[0.05] to-purple-500/[0.05] rounded-2xl" />
17+
18+
<div className="relative">
19+
<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">
20+
<Star className="w-8 h-8 text-purple-400" />
21+
</div>
22+
23+
<h1 className="text-3xl font-semibold text-white mb-3">
24+
Pro Plan Active
25+
</h1>
26+
<p className="text-gray-400 mb-8 text-lg">
27+
Experience the full power of professional development
28+
</p>
29+
30+
<Link
31+
href="/"
32+
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"
33+
>
34+
<Command className="w-5 h-5 text-blue-400" />
35+
<span>Open Editor</span>
36+
<ArrowRight className="w-5 h-5 text-purple-400 group-hover:translate-x-0.5 transition-transform" />
37+
</Link>
38+
</div>
39+
</div>
40+
</div>
41+
</div>
42+
</div>
43+
);
44+
}
45+
export default BeingProPlan;
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const FeatureCategory = ({ children, label }: { children: React.ReactNode; label: string }) => (
2+
<div className="space-y-4">
3+
<h3 className="text-sm font-medium text-gray-400 uppercase tracking-wider">{label}</h3>
4+
<div className="space-y-3">{children}</div>
5+
</div>
6+
);
7+
8+
export default FeatureCategory;

src/app/pricing/_components/Item.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Check } from "lucide-react";
2+
3+
const FeatureItem = ({ children }: { children: React.ReactNode }) => (
4+
<div className="flex items-start gap-3 group">
5+
<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">
6+
<Check className="w-3 h-3 text-blue-400" />
7+
</div>
8+
<span className="text-gray-400 group-hover:text-gray-300 transition-colors">{children}</span>
9+
</div>
10+
);
11+
12+
export default FeatureItem;
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Zap } from "lucide-react";
2+
import Link from "next/link";
3+
4+
export default function UpgradeButton() {
5+
const CHEKOUT_URL =
6+
"https://arya-opensource.lemonsqueezy.com/buy/c4c9a31d-3c39-4678-aa64-70fc57205d60";
7+
8+
return (
9+
<Link
10+
href={CHEKOUT_URL}
11+
className="inline-flex items-center justify-center gap-2 px-8 py-4 text-white
12+
bg-gradient-to-r from-blue-500 to-blue-600 rounded-lg
13+
hover:from-blue-600 hover:to-blue-700 transition-all"
14+
>
15+
<Zap className="w-5 h-5" />
16+
Upgrade to Pro
17+
</Link>
18+
);
19+
}

src/app/pricing/_constants/index.ts

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Boxes, Globe, RefreshCcw, Shield } from "lucide-react";
2+
3+
export const ENTERPRISE_FEATURES = [
4+
{
5+
icon: Globe,
6+
label: "Global Infrastructure",
7+
desc: "Lightning-fast execution across worldwide edge nodes",
8+
},
9+
{
10+
icon: Shield,
11+
label: "Enterprise Security",
12+
desc: "Bank-grade encryption and security protocols",
13+
},
14+
{
15+
icon: RefreshCcw,
16+
label: "Real-time Sync",
17+
desc: "Instant synchronization across all devices",
18+
},
19+
{
20+
icon: Boxes,
21+
label: "Unlimited Storage",
22+
desc: "Store unlimited snippets and projects",
23+
},
24+
];
25+
26+
export const FEATURES = {
27+
development: [
28+
"Advanced AI",
29+
"Custom theme builder",
30+
"Integrated debugging tools",
31+
"Multi-language support",
32+
],
33+
collaboration: [
34+
"Real-time pair programming",
35+
"Team workspaces",
36+
"Version control integration",
37+
"Code review tools",
38+
],
39+
deployment: [
40+
"One-click deployment",
41+
"CI/CD integration",
42+
"Container support",
43+
"Custom domain mapping",
44+
],
45+
};

0 commit comments

Comments
 (0)