diff --git a/apps/web/src/components/Toggle.tsx b/apps/web/src/components/Toggle.tsx
index 3efe3b5b..fbb9766f 100644
--- a/apps/web/src/components/Toggle.tsx
+++ b/apps/web/src/components/Toggle.tsx
@@ -7,15 +7,17 @@ const Toggle = ({
label,
disabled,
showLabel = true,
+ labelPosition = "before",
}: {
isChecked: boolean;
onChange: () => void;
label: string;
disabled?: boolean;
showLabel?: boolean;
+ labelPosition?: "before" | "after";
}) => (
-
- {showLabel && (
+
+ {showLabel && labelPosition === "before" && (
{label}
@@ -38,6 +40,11 @@ const Toggle = ({
)}
/>
+ {showLabel && labelPosition === "after" && (
+
+ {label}
+
+ )}
);
diff --git a/apps/web/src/pages/api/stripe/create_checkout_session.ts b/apps/web/src/pages/api/stripe/create_checkout_session.ts
index 4d0f655c..f57447e4 100644
--- a/apps/web/src/pages/api/stripe/create_checkout_session.ts
+++ b/apps/web/src/pages/api/stripe/create_checkout_session.ts
@@ -113,18 +113,23 @@ export default withRateLimit(
resolvedWorkspacePublicId = generateUID();
}
- const priceId =
- billing === "annual"
- ? (process.env.STRIPE_PRO_PLAN_ANNUAL_PRICE_ID ??
- process.env.STRIPE_PRO_PLAN_MONTHLY_PRICE_ID)
- : process.env.STRIPE_PRO_PLAN_MONTHLY_PRICE_ID;
+ const isTeam = body.plan === "team";
+ const annualPriceId = isTeam
+ ? (process.env.STRIPE_TEAM_PLAN_ANNUAL_PRICE_ID ??
+ process.env.STRIPE_TEAM_PLAN_MONTHLY_PRICE_ID)
+ : (process.env.STRIPE_PRO_PLAN_ANNUAL_PRICE_ID ??
+ process.env.STRIPE_PRO_PLAN_MONTHLY_PRICE_ID);
+ const monthlyPriceId = isTeam
+ ? process.env.STRIPE_TEAM_PLAN_MONTHLY_PRICE_ID
+ : process.env.STRIPE_PRO_PLAN_MONTHLY_PRICE_ID;
+ const priceId = billing === "annual" ? annualPriceId : monthlyPriceId;
const session = await stripe.checkout.sessions.create({
mode: "subscription",
payment_method_collection: "always",
line_items: [{ price: priceId, quantity: 1 }],
subscription_data: { trial_period_days: 14 },
- success_url: `${env("NEXT_PUBLIC_BASE_URL")}${successUrl}`,
+ success_url: `${env("NEXT_PUBLIC_BASE_URL")}${successUrl}?workspacePublicId=${resolvedWorkspacePublicId}`,
cancel_url: `${env("NEXT_PUBLIC_BASE_URL")}${cancelUrl}`,
client_reference_id: resolvedWorkspacePublicId,
customer: user.stripeCustomerId ?? undefined,
diff --git a/apps/web/src/pages/api/stripe/webhook.ts b/apps/web/src/pages/api/stripe/webhook.ts
index 515440b0..fb672fa9 100644
--- a/apps/web/src/pages/api/stripe/webhook.ts
+++ b/apps/web/src/pages/api/stripe/webhook.ts
@@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import type { Readable } from "node:stream";
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 { createLogger } from "@kan/logger";
import { createStripeClient } from "@kan/stripe";
@@ -55,34 +56,47 @@ export default async function handler(
if (!meta?.workspacePublicId) break;
+ const plan = meta.plan === "team" ? "team" : ("pro" as const);
+
if (
meta.isNewWorkspace === "true" &&
meta.workspaceName &&
meta.userId &&
meta.userEmail
) {
- const slug = meta.workspaceSlug ?? meta.workspacePublicId;
+ const existing = await workspaceRepo.getByPublicId(db, meta.workspacePublicId);
- await workspaceRepo.create(db, {
- publicId: meta.workspacePublicId,
- name: meta.workspaceName,
- slug,
- createdBy: meta.userId,
- createdByEmail: meta.userEmail,
- ...(meta.workspaceDescription && {
- description: meta.workspaceDescription,
- }),
+ if (!existing) {
+ const slug = meta.workspaceSlug ?? meta.workspacePublicId;
+
+ await workspaceRepo.create(db, {
+ publicId: meta.workspacePublicId,
+ name: meta.workspaceName,
+ slug,
+ plan,
+ createdBy: meta.userId,
+ createdByEmail: meta.userEmail,
+ ...(meta.workspaceDescription && {
+ description: meta.workspaceDescription,
+ }),
+ });
+
+ await subscriptionRepo.create(db, {
+ plan,
+ referenceId: meta.workspacePublicId,
+ userId: meta.userId,
+ stripeCustomerId: checkoutSession.customer as string,
+ status: "active",
+ });
+ }
+ } else {
+ // Existing workspace upgrade — update plan (and slug for pro)
+ await workspaceRepo.update(db, meta.workspacePublicId, {
+ plan,
+ ...(plan === "pro" && meta.workspaceSlug && { slug: meta.workspaceSlug }),
});
}
- const plan = meta.plan === "team" ? "team" : "pro";
- await workspaceRepo.update(db, meta.workspacePublicId, {
- plan,
- ...(plan === "pro" &&
- meta.workspaceSlug &&
- meta.isNewWorkspace !== "true" && { slug: meta.workspaceSlug }),
- });
-
break;
}
default:
diff --git a/apps/web/src/providers/workspace.tsx b/apps/web/src/providers/workspace.tsx
index 83e93930..907db944 100644
--- a/apps/web/src/providers/workspace.tsx
+++ b/apps/web/src/providers/workspace.tsx
@@ -17,7 +17,7 @@ interface Workspace {
description: string | null | undefined;
publicId: string;
slug: string | undefined;
- plan: "free" | "pro" | "enterprise" | undefined;
+ plan: "free" | "team" | "pro" | "enterprise" | undefined;
role: "admin" | "member" | "guest";
weekStartDay: 0 | 1 | 6;
}
@@ -27,7 +27,7 @@ const initialWorkspace: Workspace = {
description: null,
publicId: "",
slug: "",
- plan: "free",
+ plan: "free" as const,
role: "member",
weekStartDay: 1,
};
@@ -50,7 +50,15 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
const workspacePublicId = useSearchParams().get("workspacePublicId");
- const { data, isLoading } = api.workspace.all.useQuery();
+ const [pendingWorkspaceId, setPendingWorkspaceId] = useState
(
+ workspacePublicId,
+ );
+ const pollAttemptsRef = React.useRef(0);
+ const MAX_POLL_ATTEMPTS = 5;
+
+ const { data, isLoading } = api.workspace.all.useQuery(undefined, {
+ refetchInterval: pendingWorkspaceId ? 2000 : false,
+ });
const utils = api.useUtils();
const switchWorkspace = (_workspace: Workspace) => {
@@ -94,7 +102,16 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
({ workspace }) => workspace.publicId === storedWorkspaceId,
);
- if (!selectedWorkspace?.workspace) return;
+ if (!selectedWorkspace?.workspace) {
+ pollAttemptsRef.current += 1;
+ if (pollAttemptsRef.current >= MAX_POLL_ATTEMPTS) {
+ setPendingWorkspaceId(null);
+ }
+ return;
+ }
+
+ pollAttemptsRef.current = 0;
+ setPendingWorkspaceId(null);
setWorkspace({
publicId: selectedWorkspace.workspace.publicId,
diff --git a/apps/web/src/views/onboarding/workspace-details/index.tsx b/apps/web/src/views/onboarding/workspace-details/index.tsx
index 84e1d572..f1eba15a 100644
--- a/apps/web/src/views/onboarding/workspace-details/index.tsx
+++ b/apps/web/src/views/onboarding/workspace-details/index.tsx
@@ -225,6 +225,17 @@ export default function WorkspaceNameView() {
/>
+ {plan !== "pro" && (
+