Skip to content

Commit 1e5a2b0

Browse files
committed
update: moved to razorpay
1 parent 4683132 commit 1e5a2b0

9 files changed

Lines changed: 545 additions & 124 deletions

File tree

package-lock.json

Lines changed: 125 additions & 37 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "devian-web",
3-
"version": "0.2.2",
3+
"version": "1.0.0",
44
"private": true,
55
"scripts": {
66
"dev": "next dev",
@@ -18,10 +18,11 @@
1818
"framer-motion": "^12.34.3",
1919
"lucide-react": "^0.575.0",
2020
"next": "16.1.6",
21+
"razorpay": "^2.9.6",
2122
"react": "19.2.3",
2223
"react-dom": "19.2.3",
24+
"react-razorpay": "^3.0.1",
2325
"resend": "^6.9.3",
24-
"stripe": "^20.4.0",
2526
"tailwind-merge": "^3.5.0"
2627
},
2728
"devDependencies": {

src/app/api/license/generate/route.ts

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,15 @@
11
import { NextResponse } from "next/server";
2-
import Stripe from "stripe";
32

43
export async function GET(req: Request) {
5-
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "dummy_key", {
6-
apiVersion: "2026-02-25.clover",
7-
});
84
const { searchParams } = new URL(req.url);
9-
const sessionId = searchParams.get("session_id");
5+
const orderId = searchParams.get("order_id");
106

11-
if (!sessionId) {
12-
return new NextResponse("Session ID is required", { status: 400 });
7+
if (!orderId) {
8+
return new NextResponse("Order ID is required", { status: 400 });
139
}
1410

1511
try {
16-
// Verify the session against Stripe
17-
const session = await stripe.checkout.sessions.retrieve(sessionId);
18-
19-
if (session.payment_status !== "paid") {
20-
return new NextResponse("Payment not complete", { status: 400 });
21-
}
22-
23-
// Search Keygen for the license matching this Stripe Session ID
12+
// Search Keygen for the license matching this Razorpay Order ID
2413
const accountId = process.env.VITE_KEYGEN_ACCOUNT_ID;
2514
const keygenToken = process.env.KEYGEN_API_TOKEN;
2615

@@ -29,7 +18,7 @@ export async function GET(req: Request) {
2918
}
3019

3120
// Use Keygen's metadata query
32-
const response = await fetch(`https://api.keygen.sh/v1/accounts/${accountId}/licenses?metadata[stripe_session_id]=${sessionId}`, {
21+
const response = await fetch(`https://api.keygen.sh/v1/accounts/${accountId}/licenses?metadata[razorpay_order_id]=${orderId}`, {
3322
headers: {
3423
'Accept': 'application/vnd.api+json',
3524
'Authorization': `Bearer ${keygenToken}`
@@ -54,7 +43,7 @@ export async function GET(req: Request) {
5443
});
5544

5645
} catch (e: any) {
57-
console.error("Failed to resolve session:", e);
46+
console.error("Failed to resolve order:", e);
5847
return new NextResponse("Server Error", { status: 500 });
5948
}
6049
}

src/app/api/pricing/route.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { NextResponse } from 'next/server';
2+
3+
export const dynamic = 'force-dynamic';
4+
5+
export async function GET(req: Request) {
6+
const country = req.headers.get('x-vercel-ip-country') || process.env.TEST_COUNTRY_CODE || 'US';
7+
8+
let amount = 2000;
9+
let currency = 'USD';
10+
let symbol = '$';
11+
let displayAmount = '20';
12+
13+
if (country === 'IN') {
14+
amount = 120000;
15+
currency = 'INR';
16+
symbol = '₹';
17+
displayAmount = '1,200';
18+
}
19+
20+
return NextResponse.json({
21+
amount,
22+
currency,
23+
symbol,
24+
displayAmount,
25+
country
26+
});
27+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { NextResponse } from 'next/server';
2+
import Razorpay from 'razorpay';
3+
4+
export async function POST(req: Request) {
5+
try {
6+
const razorpay = new Razorpay({
7+
key_id: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID as string,
8+
key_secret: process.env.RAZORPAY_KEY_SECRET as string,
9+
});
10+
11+
const country = req.headers.get('x-vercel-ip-country') || process.env.TEST_COUNTRY_CODE || 'US';
12+
13+
// Pricing logic
14+
let amount = 2000; // $20.00 USD (in cents)
15+
let currency = 'USD';
16+
17+
if (country === 'IN') {
18+
amount = 120000; // ₹1200.00 INR (in paise)
19+
currency = 'INR';
20+
}
21+
22+
const options = {
23+
amount,
24+
currency,
25+
receipt: `receipt_${Date.now()}`,
26+
payment_capture: 1 // Auto capture
27+
};
28+
29+
const order = await razorpay.orders.create(options);
30+
31+
return NextResponse.json({
32+
orderId: order.id,
33+
amount: order.amount,
34+
currency: order.currency
35+
});
36+
37+
} catch (error: any) {
38+
console.error("Razorpay Order Creation Error:", error);
39+
return new NextResponse(`Error: ${error.message}`, { status: 500 });
40+
}
41+
}
Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,62 @@
11
import { NextResponse } from "next/server";
2-
import Stripe from "stripe";
2+
import crypto from "crypto";
33
import { Resend } from "resend";
44

5-
65
export async function POST(req: Request) {
7-
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "dummy_key", {
8-
apiVersion: "2026-02-25.clover", // Adjust to the version you are using
9-
});
6+
try {
7+
const body = await req.text();
8+
const signature = req.headers.get("x-razorpay-signature") as string;
9+
const webhookSecret = process.env.RAZORPAY_WEBHOOK_SECRET as string;
1010

11-
const body = await req.text();
12-
const signature = req.headers.get("stripe-signature") as string;
11+
if (!signature || !webhookSecret) {
12+
console.error("Missing webhook signature or secret");
13+
return new NextResponse("Invalid request", { status: 400 });
14+
}
1315

14-
let event: Stripe.Event;
16+
// Verify the signature
17+
const expectedSignature = crypto
18+
.createHmac("sha256", webhookSecret)
19+
.update(body)
20+
.digest("hex");
1521

16-
try {
17-
event = stripe.webhooks.constructEvent(
18-
body,
19-
signature,
20-
process.env.STRIPE_WEBHOOK_SECRET as string
21-
);
22-
} catch (error: any) {
23-
console.error(`Webhook signature verification failed: ${error.message}`);
24-
return new NextResponse(`Webhook Error: ${error.message}`, { status: 400 });
25-
}
22+
if (expectedSignature !== signature) {
23+
console.error("Webhook signature verification failed");
24+
return new NextResponse("Invalid signature", { status: 400 });
25+
}
2626

27-
// Handle the checkout session completion
28-
if (event.type === "checkout.session.completed") {
29-
const session = event.data.object as Stripe.Checkout.Session;
30-
const customerEmail = session.customer_details?.email;
31-
const paymentIntentId = session.payment_intent as string;
27+
// Parse event
28+
const event = JSON.parse(body);
3229

33-
if (!customerEmail) {
34-
console.error("No customer email found in session.");
35-
return new NextResponse("Invalid Session Data", { status: 400 });
36-
}
30+
// Handle the order.paid event
31+
if (event.event === "order.paid") {
32+
const paymentEntity = event.payload.payment.entity;
33+
const orderEntity = event.payload.order.entity;
3734

38-
try {
39-
if (session.payment_status === "paid") {
40-
// Create a new license in Keygen
41-
await createLicenseForKeygen(customerEmail, session.id, paymentIntentId);
35+
const customerEmail = paymentEntity.email;
36+
const paymentId = paymentEntity.id;
37+
const orderId = orderEntity.id;
38+
39+
if (!customerEmail) {
40+
console.error("No customer email found in Razorpay payment data.");
41+
return new NextResponse("Invalid Session Data", { status: 400 });
4242
}
43-
} catch (e: any) {
44-
console.error("Failed to process order:", e);
45-
// Stripe will retry the webhook if we fail.
46-
// In production, we'd log this, potentially to Sentry or PostHog
47-
return new NextResponse("Internal Server Error", { status: 500 });
43+
44+
// Create a new license in Keygen
45+
await createLicenseForKeygen(customerEmail, orderId, paymentId);
4846
}
49-
}
5047

51-
// Return a 200 response to acknowledge receipt of the event
52-
return new NextResponse(JSON.stringify({ received: true }), { status: 200 });
48+
return new NextResponse(JSON.stringify({ received: true }), { status: 200 });
49+
} catch (error: any) {
50+
console.error(`Webhook Error: ${error.message}`);
51+
return new NextResponse(`Webhook Error: ${error.message}`, { status: 500 });
52+
}
5353
}
5454

5555
// -------------------------------------------------------------
5656
// Core Business Logic
5757
// -------------------------------------------------------------
5858

59-
async function createLicenseForKeygen(email: string, sessionId: string, paymentIntentId: string) {
59+
async function createLicenseForKeygen(email: string, orderId: string, paymentId: string) {
6060
const accountId = process.env.VITE_KEYGEN_ACCOUNT_ID;
6161
const policyId = process.env.VITE_KEYGEN_POLICY_ID;
6262
const keygenToken = process.env.KEYGEN_API_TOKEN;
@@ -79,8 +79,8 @@ async function createLicenseForKeygen(email: string, sessionId: string, paymentI
7979
attributes: {
8080
name: email,
8181
metadata: {
82-
stripe_payment_id: paymentIntentId,
83-
stripe_session_id: sessionId
82+
razorpay_payment_id: paymentId,
83+
razorpay_order_id: orderId
8484
}
8585
},
8686
relationships: {

src/app/pricing/page.tsx

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { CheckCircle2, ShieldAlert, Check, X, ArrowRight, Zap, Crown, Bot, Database, FolderGit2 } from "lucide-react";
1+
"use client";
2+
3+
import { CheckCircle2, ShieldAlert, Check, X, ArrowRight, Zap, Crown, Bot, Database, FolderGit2, Loader2 } from "lucide-react";
24
import Link from "next/link";
35
import { Activity } from "lucide-react";
6+
import { RazorpayButton } from "@/components/PricingSection";
7+
import { useState, useEffect } from "react";
48

5-
export const metadata = {
6-
title: "Devian | Features & Pricing",
7-
description: "Compare Devian Community and Devian Pro plans. Upgrade to unlock powerful local AI capabilities and unrestricted docker management.",
8-
};
9+
// Moved metadata to layout.tsx or a separate server component if needed since this is now a client component.
10+
// Or we can just remove it for now if next.js complains, but usually it must be in a server component.
911

1012
const FEATURES = [
1113
{
@@ -55,6 +57,20 @@ const FEATURES = [
5557
];
5658

5759
export default function FeaturesPage() {
60+
const [priceData, setPriceData] = useState<{ symbol: string; displayAmount: string } | null>(null);
61+
62+
useEffect(() => {
63+
fetch('/api/pricing')
64+
.then(res => res.json())
65+
.then(data => {
66+
setPriceData(data);
67+
})
68+
.catch(err => {
69+
// Fallback to USD on error
70+
setPriceData({ symbol: '$', displayAmount: '20' });
71+
});
72+
}, []);
73+
5874
return (
5975
<div className="min-h-screen bg-[#121212] text-white selection:bg-primary/30">
6076
{/* Navigation */}
@@ -69,9 +85,7 @@ export default function FeaturesPage() {
6985
</div>
7086
<div className="hidden md:flex items-center gap-8 text-sm font-medium text-white/70 tracking-wide">
7187
<Link href="/" className="hover:text-white transition-colors">Home</Link>
72-
<a href={process.env.NEXT_PUBLIC_STRIPE_PAYMENT_LINK || "#"} className="bg-primary/10 text-primary border border-primary/20 px-5 py-2 rounded-full hover:bg-primary hover:text-white transition-all shadow-[0_0_10px_rgba(101,140,194,0.1)]">
73-
Buy Devian Pro
74-
</a>
88+
<RazorpayButton className="bg-primary/10 text-primary border border-primary/20 px-5 py-2 rounded-full hover:bg-primary hover:text-white transition-all shadow-[0_0_10px_rgba(101,140,194,0.1)] text-sm" />
7589
</div>
7690
</nav>
7791

@@ -89,7 +103,7 @@ export default function FeaturesPage() {
89103
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto mb-24">
90104
<div className="bg-zinc-900/50 border border-zinc-800 rounded-2xl p-8 flex flex-col">
91105
<h3 className="text-2xl font-bold mb-2">Community</h3>
92-
<div className="text-4xl font-bold mb-6">$0<span className="text-lg text-zinc-500 font-normal">/forever</span></div>
106+
<div className="text-4xl font-bold mb-6">{priceData?.symbol || "$"}0<span className="text-lg text-zinc-500 font-normal">/forever</span></div>
93107
<p className="text-zinc-400 mb-8 flex-1">
94108
Perfect for students and hobbyists who need a better way to view their local stack.
95109
</p>
@@ -109,16 +123,22 @@ export default function FeaturesPage() {
109123
<h3 className="text-2xl font-bold mb-2 flex items-center gap-2">
110124
Devian Pro <Zap className="w-5 h-5 text-primary" />
111125
</h3>
112-
<div className="text-4xl font-bold mb-6">$29<span className="text-lg text-zinc-500 font-normal">/lifetime</span></div>
126+
<div className="text-4xl font-bold mb-6 flex items-baseline gap-2">
127+
{priceData ? (
128+
<>
129+
{priceData.symbol}{priceData.displayAmount}
130+
<span className="text-lg text-zinc-500 font-normal">/lifetime</span>
131+
</>
132+
) : (
133+
<div className="h-10 w-24 bg-zinc-800 rounded animate-pulse" />
134+
)}
135+
</div>
113136
<p className="text-zinc-400 mb-8 flex-1 relative z-10">
114137
For professional developers who want absolute control, intelligent insights, and AI automation.
115138
</p>
116-
<a
117-
href={process.env.NEXT_PUBLIC_STRIPE_PAYMENT_LINK || "#"}
139+
<RazorpayButton
118140
className="w-full text-center py-3 bg-primary hover:bg-primary/90 text-white rounded-lg font-medium transition-colors shadow-[0_0_30px_-5px_var(--tw-shadow-color)] shadow-primary/30 relative z-10"
119-
>
120-
Buy Devian Pro
121-
</a>
141+
/>
122142
<p className="text-center text-xs text-zinc-500 mt-3 relative z-10">Valid for up to 3 personal devices</p>
123143
</div>
124144
</div>
@@ -171,12 +191,11 @@ export default function FeaturesPage() {
171191
<p className="text-zinc-400 mb-8">
172192
Join other professional developers building faster with Devian Pro. One-time payment, lifetime updates.
173193
</p>
174-
<a
175-
href={process.env.NEXT_PUBLIC_STRIPE_PAYMENT_LINK || "#"}
194+
<RazorpayButton
176195
className="inline-flex items-center justify-center px-8 py-4 bg-primary hover:bg-primary/90 text-white rounded-full font-medium transition-all hover:scale-105 shadow-[0_0_40px_-10px_var(--tw-shadow-color)] shadow-primary"
177196
>
178197
Get Devian Pro Today
179-
</a>
198+
</RazorpayButton>
180199
</div>
181200
</main>
182201
</div>

0 commit comments

Comments
 (0)