This commit is contained in:
Turbobot
2024-03-21 13:41:16 +08:00
committed by giancarlo
commit bb58169fe9
204 changed files with 26228 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
import { createEnv } from "@t3-oss/env-nextjs";
import * as z from "zod";
export const env = createEnv({
shared: {},
server: {
NEXTJS_URL: z.preprocess(
(str) =>
process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : str,
process.env.VERCEL_URL ? z.string().min(1) : z.string().url(),
),
STRIPE_API_KEY: z.string(),
},
client: {
NEXT_PUBLIC_STRIPE_STD_PRODUCT_ID: z.string(),
NEXT_PUBLIC_STRIPE_STD_MONTHLY_PRICE_ID: z.string(),
NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID: z.string(),
NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID: z.string(),
},
// Client side variables gets destructured here due to Next.js static analysis
// Shared ones are also included here for good measure since the behavior has been inconsistent
experimental__runtimeEnv: {
NEXT_PUBLIC_STRIPE_STD_PRODUCT_ID:
process.env.NEXT_PUBLIC_STRIPE_STD_PRODUCT_ID,
NEXT_PUBLIC_STRIPE_STD_MONTHLY_PRICE_ID:
process.env.NEXT_PUBLIC_STRIPE_STD_MONTHLY_PRICE_ID,
NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID:
process.env.NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID,
NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID:
process.env.NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID,
},
skipValidation:
!!process.env.SKIP_ENV_VALIDATION ||
process.env.npm_lifecycle_event === "lint",
});

View File

@@ -0,0 +1,13 @@
import { Stripe } from "stripe";
import { env } from "./env.mjs";
export * from "./plans";
export * from "./webhooks";
export type { Stripe };
export const stripe = new Stripe(env.STRIPE_API_KEY, {
apiVersion: "2023-10-16",
typescript: true,
});

View File

@@ -0,0 +1,41 @@
import { SubscriptionPlan } from "@acme/db";
import { env } from "./env.mjs";
interface PlanInfo {
key: SubscriptionPlan;
name: string;
description: string;
preFeatures?: string;
features: string[];
priceId: string;
}
export const PLANS: Record<SubscriptionPlan, PlanInfo> = {
STANDARD: {
key: SubscriptionPlan.STANDARD,
name: "Standard",
description: "For individuals",
features: ["Invite up to 1 team member", "Lorem ipsum dolor sit amet"],
priceId: env.NEXT_PUBLIC_STRIPE_STD_MONTHLY_PRICE_ID,
},
PRO: {
key: SubscriptionPlan.PRO,
name: "Pro",
description: "For teams",
preFeatures: "Everything in standard, plus",
features: ["Invite up to 5 team members", "Unlimited projects"],
priceId: env.NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID,
},
FREE: {
key: SubscriptionPlan.FREE,
name: "Free",
description: "For individuals",
features: ["Invite up to 1 team member", "Lorem ipsum dolor sit amet"],
priceId: "no-id-necessary",
},
};
export function stripePriceToSubscriptionPlan(priceId: string | undefined) {
return Object.values(PLANS).find((plan) => plan.priceId === priceId);
}

View File

@@ -0,0 +1,153 @@
import { clerkClient } from "@clerk/nextjs";
import type Stripe from "stripe";
import { db, genId } from "@acme/db";
import { stripe } from ".";
import { stripePriceToSubscriptionPlan } from "./plans";
export async function handleEvent(event: Stripe.Event) {
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object;
if (typeof session.subscription !== "string") {
throw new Error("Missing or invalid subscription id");
}
const subscription = await stripe.subscriptions.retrieve(
session.subscription,
);
const customerId =
typeof subscription.customer === "string"
? subscription.customer
: subscription.customer.id;
const { userId, organizationName } = subscription.metadata;
if (!userId) {
throw new Error("Missing user id");
}
const customer = await db
.selectFrom("Customer")
.select("id")
.where("stripeId", "=", customerId)
.executeTakeFirst();
const subscriptionPlan = stripePriceToSubscriptionPlan(
subscription.items.data[0]?.price.id,
);
/**
* User is already subscribed, update their info
*/
if (customer) {
return await db
.updateTable("Customer")
.where("id", "=", customer.id)
.set({
stripeId: customerId,
subscriptionId: subscription.id,
paidUntil: new Date(subscription.current_period_end * 1000),
plan: subscriptionPlan?.key,
})
.execute();
}
/**
* User is not subscribed, create a new customer and org
*/
const organization = await clerkClient.organizations.createOrganization({
createdBy: userId,
name: organizationName!,
});
// TODO: SET ACTIVE ORG WHEN CLERK CAN BOTHER TO LET ME DO TAHT SERVERSIDE!!!
await db
.insertInto("Customer")
.values({
id: genId(),
clerkUserId: userId,
clerkOrganizationId: organization.id,
stripeId: customerId,
subscriptionId: subscription.id,
plan: subscriptionPlan?.key,
paidUntil: new Date(subscription.current_period_end * 1000),
endsAt: new Date(subscription.current_period_end * 1000),
})
.execute();
break;
}
case "invoice.payment_succeeded": {
const invoice = event.data.object;
if (typeof invoice.subscription !== "string") {
throw new Error("Missing or invalid subscription id");
}
const subscription = await stripe.subscriptions.retrieve(
invoice.subscription,
);
const subscriptionPlan = stripePriceToSubscriptionPlan(
subscription.items.data[0]?.price.id,
);
await db
.updateTable("Customer")
.where("subscriptionId", "=", subscription.id)
.set({
plan: subscriptionPlan?.key,
paidUntil: new Date(subscription.current_period_end * 1000),
})
.execute();
break;
}
case "invoice.payment_failed": {
// TODO: Handle failed payments
break;
}
case "customer.subscription.deleted": {
const subscription = event.data.object;
const customerId =
typeof subscription.customer === "string"
? subscription.customer
: subscription.customer.id;
await db
.updateTable("Customer")
.where("stripeId", "=", customerId)
.set({
subscriptionId: null,
plan: "FREE",
paidUntil: null,
})
.execute();
break;
}
case "customer.subscription.updated": {
const subscription = event.data.object;
const customerId =
typeof subscription.customer === "string"
? subscription.customer
: subscription.customer.id;
const subscriptionPlan = stripePriceToSubscriptionPlan(
subscription.items.data[0]?.price.id,
);
await db
.updateTable("Customer")
.where("stripeId", "=", customerId)
.set({
plan: subscriptionPlan?.key,
paidUntil: new Date(subscription.current_period_end * 1000),
})
.execute();
break;
}
default: {
console.log("🆗 Stripe Webhook Unhandled Event Type: ", event.type);
return;
}
}
console.log("✅ Stripe Webhook Processed");
}