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" && ( +
+ setIsProToggle((v) => !v)} + label={t`Upgrade to Pro ($29/month)`} + labelPosition="after" + /> +
+ )} +