feat: allow multiple subscriptions

This commit is contained in:
Henry
2025-09-08 23:20:02 +01:00
parent 38803f6696
commit 7f496adf63
3 changed files with 65 additions and 28 deletions

View File

@@ -3,6 +3,7 @@ import { env } from "next-runtime-env";
import { z } from "zod"; import { z } from "zod";
import { createNextApiContext } from "@kan/api/trpc"; import { createNextApiContext } from "@kan/api/trpc";
import * as subscriptionRepo from "@kan/db/repository/subscription.repo";
import * as workspaceRepo from "@kan/db/repository/workspace.repo"; import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { createStripeClient } from "@kan/stripe"; import { createStripeClient } from "@kan/stripe";
@@ -40,17 +41,22 @@ export default async function handler(
const body = req.body as CheckoutSessionRequest; const body = req.body as CheckoutSessionRequest;
const { successUrl, cancelUrl, slug, workspacePublicId } = body; const { successUrl, cancelUrl, slug, workspacePublicId } = body;
if (!successUrl || !cancelUrl || !slug || !workspacePublicId) { if (!successUrl || !cancelUrl || !workspacePublicId) {
return res.status(400).json({ error: "Missing required fields" }); return res.status(400).json({ error: "Missing required fields" });
} }
const slugResult = workspaceSlugSchema.safeParse(slug); if (slug) {
const slugResult = workspaceSlugSchema.safeParse(slug);
if (!slugResult.success) { if (!slugResult.success) {
return new Response(JSON.stringify({ error: "Invalid workspace slug" }), { return new Response(
status: 400, JSON.stringify({ error: "Invalid workspace slug" }),
headers: { "Content-Type": "application/json" }, {
}); status: 400,
headers: { "Content-Type": "application/json" },
},
);
}
} }
const workspace = await workspaceRepo.getAllByUserId(db, user.id); const workspace = await workspaceRepo.getAllByUserId(db, user.id);
@@ -66,6 +72,20 @@ export default async function handler(
}); });
} }
const subscription = await subscriptionRepo.create(db, {
plan: "pro",
referenceId: workspacePublicId,
userId: user.id,
stripeCustomerId: user.stripeCustomerId ?? "",
status: "incomplete",
});
const subscriptionId = subscription?.id;
if (!subscriptionId) {
return res.status(500).json({ error: "Error creating subscription" });
}
const session = await stripe.checkout.sessions.create({ const session = await stripe.checkout.sessions.create({
mode: "subscription", mode: "subscription",
line_items: [ line_items: [
@@ -76,10 +96,13 @@ export default async function handler(
], ],
success_url: `${env("NEXT_PUBLIC_BASE_URL")}${successUrl}`, success_url: `${env("NEXT_PUBLIC_BASE_URL")}${successUrl}`,
cancel_url: `${env("NEXT_PUBLIC_BASE_URL")}${cancelUrl}`, cancel_url: `${env("NEXT_PUBLIC_BASE_URL")}${cancelUrl}`,
client_reference_id: workspacePublicId,
customer: user.stripeCustomerId ?? undefined, customer: user.stripeCustomerId ?? undefined,
metadata: { metadata: {
workspaceSlug: slug, ...(slug && { workspaceSlug: slug }),
workspacePublicId, workspacePublicId,
userId: user.id,
subscriptionId,
}, },
}); });

View File

@@ -1,14 +1,11 @@
import { t } from "@lingui/core/macro"; import { t } from "@lingui/core/macro";
import { HiBolt, HiCheckBadge } from "react-icons/hi2"; import { HiBolt, HiCheckBadge } from "react-icons/hi2";
import { authClient } from "@kan/auth/client";
import Button from "~/components/Button"; import Button from "~/components/Button";
import { useModal } from "~/providers/modal"; import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup"; import { usePopup } from "~/providers/popup";
export function UpgradeToProConfirmation({ export function UpgradeToProConfirmation({
userId,
workspacePublicId, workspacePublicId,
}: { }: {
userId: string; userId: string;
@@ -18,25 +15,28 @@ export function UpgradeToProConfirmation({
const { showPopup } = usePopup(); const { showPopup } = usePopup();
const handleUpgrade = async () => { const handleUpgrade = async () => {
const { data, error } = await authClient.subscription.upgrade({ try {
plan: "pro", const response = await fetch("/api/stripe/create_checkout_session", {
referenceId: workspacePublicId, method: "POST",
metadata: { headers: {
userId, "Content-Type": "application/json",
workspacePublicId, },
...(entityId && { workspaceSlug: entityId }), body: JSON.stringify({
}, ...(entityId && { workspaceSlug: entityId }),
successUrl: "/settings", workspacePublicId: workspacePublicId,
cancelUrl: "/settings", cancelUrl: "/settings",
returnUrl: "/settings", successUrl: "/settings",
disableRedirect: true, }),
}); });
if (data?.url) { const { url } = (await response.json()) as { url: string };
window.location.href = data.url;
} if (url) {
window.location.href = url;
}
} catch (error) {
console.error("Error creating checkout session:", error);
if (error) {
showPopup({ showPopup({
header: t`Error upgrading subscription`, header: t`Error upgrading subscription`,
message: t`Please try again later, or contact customer support.`, message: t`Please try again later, or contact customer support.`,

View File

@@ -75,3 +75,17 @@ export const getByReferenceId = async (db: dbClient, referenceId: string) => {
where: eq(subscription.referenceId, referenceId), where: eq(subscription.referenceId, referenceId),
}); });
}; };
export const create = async (
db: dbClient,
data: {
plan: string;
referenceId: string;
userId: string;
stripeCustomerId: string;
status: string;
},
) => {
const [result] = await db.insert(subscription).values(data).returning();
return result;
};