Skip to content

Commit 458fb1e

Browse files
committed
update: added pro payment flow
1 parent 6610dee commit 458fb1e

7 files changed

Lines changed: 590 additions & 3 deletions

File tree

package-lock.json

Lines changed: 94 additions & 2 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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
"next": "16.1.6",
2121
"react": "19.2.3",
2222
"react-dom": "19.2.3",
23+
"resend": "^6.9.3",
24+
"stripe": "^20.4.0",
2325
"tailwind-merge": "^3.5.0"
2426
},
2527
"devDependencies": {
@@ -32,4 +34,4 @@
3234
"tailwindcss": "^4",
3335
"typescript": "^5"
3436
}
35-
}
37+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { NextResponse } from "next/server";
2+
import Stripe from "stripe";
3+
4+
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+
});
8+
const { searchParams } = new URL(req.url);
9+
const sessionId = searchParams.get("session_id");
10+
11+
if (!sessionId) {
12+
return new NextResponse("Session ID is required", { status: 400 });
13+
}
14+
15+
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
24+
const accountId = process.env.VITE_KEYGEN_ACCOUNT_ID;
25+
const keygenToken = process.env.KEYGEN_API_TOKEN;
26+
27+
if (!accountId || !keygenToken) {
28+
throw new Error("Missing Keygen configuration");
29+
}
30+
31+
// Use Keygen's metadata query
32+
const response = await fetch(`https://api.keygen.sh/v1/accounts/${accountId}/licenses?metadata[stripe_session_id]=${sessionId}`, {
33+
headers: {
34+
'Accept': 'application/vnd.api+json',
35+
'Authorization': `Bearer ${keygenToken}`
36+
}
37+
});
38+
39+
const data = await response.json();
40+
41+
if (!response.ok || !data.data || data.data.length === 0) {
42+
// License might still be generating or failed.
43+
return NextResponse.json({ pending: true }, { status: 200 });
44+
}
45+
46+
// Got the license!
47+
const licenseKey = data.data[0].attributes.key;
48+
const email = data.data[0].attributes.name;
49+
50+
return NextResponse.json({
51+
pending: false,
52+
licenseKey,
53+
email
54+
});
55+
56+
} catch (e: any) {
57+
console.error("Failed to resolve session:", e);
58+
return new NextResponse("Server Error", { status: 500 });
59+
}
60+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { NextResponse } from "next/server";
2+
import Stripe from "stripe";
3+
import { Resend } from "resend";
4+
5+
6+
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+
});
10+
11+
const body = await req.text();
12+
const signature = req.headers.get("stripe-signature") as string;
13+
14+
let event: Stripe.Event;
15+
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+
}
26+
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;
32+
33+
if (!customerEmail) {
34+
console.error("No customer email found in session.");
35+
return new NextResponse("Invalid Session Data", { status: 400 });
36+
}
37+
38+
try {
39+
if (session.payment_status === "paid") {
40+
// Create a new license in Keygen
41+
await createLicenseForKeygen(customerEmail, session.id, paymentIntentId);
42+
}
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 });
48+
}
49+
}
50+
51+
// Return a 200 response to acknowledge receipt of the event
52+
return new NextResponse(JSON.stringify({ received: true }), { status: 200 });
53+
}
54+
55+
// -------------------------------------------------------------
56+
// Core Business Logic
57+
// -------------------------------------------------------------
58+
59+
async function createLicenseForKeygen(email: string, sessionId: string, paymentIntentId: string) {
60+
const accountId = process.env.VITE_KEYGEN_ACCOUNT_ID;
61+
const policyId = process.env.VITE_KEYGEN_POLICY_ID;
62+
const keygenToken = process.env.KEYGEN_API_TOKEN;
63+
64+
if (!accountId || !policyId || !keygenToken) {
65+
throw new Error("Missing Keygen configuration variables.");
66+
}
67+
68+
// Use the undocumented global fetch inside NextJS
69+
const response = await fetch(`https://api.keygen.sh/v1/accounts/${accountId}/licenses`, {
70+
method: 'POST',
71+
headers: {
72+
'Content-Type': 'application/vnd.api+json',
73+
'Accept': 'application/vnd.api+json',
74+
'Authorization': `Bearer ${keygenToken}`
75+
},
76+
body: JSON.stringify({
77+
data: {
78+
type: 'licenses',
79+
attributes: {
80+
name: email,
81+
metadata: {
82+
stripe_payment_id: paymentIntentId,
83+
stripe_session_id: sessionId
84+
}
85+
},
86+
relationships: {
87+
policy: {
88+
data: { type: 'policies', id: policyId }
89+
}
90+
}
91+
}
92+
})
93+
});
94+
95+
const data = await response.json();
96+
97+
if (!response.ok) {
98+
console.error("Keygen Error:", data);
99+
throw new Error(data.errors?.[0]?.detail || "Failed to create license on Keygen");
100+
}
101+
102+
// Safely extract the new key
103+
const licenseKey = data.data.attributes.key;
104+
105+
if (!licenseKey) {
106+
throw new Error("License key generation failed!");
107+
}
108+
109+
console.log(`Successfully generated license for ${email}`);
110+
111+
// Send the delivery email via Resend
112+
await sendDeliveryEmail(email, licenseKey);
113+
}
114+
115+
async function sendDeliveryEmail(email: string, licenseKey: string) {
116+
const resend = new Resend(process.env.RESEND_API_KEY || "dummy_key");
117+
const res = await resend.emails.send({
118+
from: 'Smruti @ Devian <smruti@devian.app>',
119+
to: email,
120+
subject: 'Welcome to Devian Pro! Here is your license key 🚀',
121+
text: `Hey!
122+
123+
Thank you so much for purchasing Devian Pro. As a solo developer, your support means the world to me and directly funds the future development of the app.
124+
125+
Here is your lifetime license key (valid for up to 3 devices):
126+
${licenseKey}
127+
128+
How to activate:
129+
1. Open Devian.
130+
2. Go to Settings -> Devian Pro.
131+
3. Paste your key and click Activate.
132+
133+
If you run into any issues, have feature requests, or just want to say hi, reply to this email or reach out to me directly at smruti@devian.app. I read every message!
134+
135+
Happy building,
136+
Smruti`,
137+
});
138+
139+
if (res.error) {
140+
console.error("Failed to send Resend email:", res.error);
141+
throw new Error("Email delivery failed");
142+
}
143+
144+
console.log(`Successfully emailed license to ${email}`);
145+
}

0 commit comments

Comments
 (0)