feat: create workspace on successful activation
This commit is contained in:
@@ -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";
|
||||
}) => (
|
||||
<div className="mr-4 flex items-center justify-end">
|
||||
{showLabel && (
|
||||
<div className="flex items-center">
|
||||
{showLabel && labelPosition === "before" && (
|
||||
<span className="mr-2 text-xs text-light-900 dark:text-dark-900">
|
||||
{label}
|
||||
</span>
|
||||
@@ -38,6 +40,11 @@ const Toggle = ({
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
{showLabel && labelPosition === "after" && (
|
||||
<span className="ml-2 text-xs text-light-900 dark:text-dark-900">
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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<string | null>(
|
||||
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,
|
||||
|
||||
@@ -225,6 +225,17 @@ export default function WorkspaceNameView() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{plan !== "pro" && (
|
||||
<div className="pb-2">
|
||||
<Toggle
|
||||
isChecked={isProToggle}
|
||||
onChange={() => setIsProToggle((v) => !v)}
|
||||
label={t`Upgrade to Pro ($29/month)`}
|
||||
labelPosition="after"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<textarea
|
||||
value={description}
|
||||
@@ -242,15 +253,15 @@ export default function WorkspaceNameView() {
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-wrap items-center justify-between gap-3">
|
||||
{plan !== "pro" && (
|
||||
<Toggle
|
||||
isChecked={isProToggle}
|
||||
onChange={() => setIsProToggle((v) => !v)}
|
||||
label={t`Upgrade to Pro ($29/month)`}
|
||||
/>
|
||||
)}
|
||||
<div className="ml-auto flex gap-2">
|
||||
<Button variant="ghost" onClick={() => router.back()}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/onboarding/select-plan?plan=${plan}&billing=${billing}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
{t`Back`}
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -64,6 +64,8 @@ export const create = async (
|
||||
slug: string;
|
||||
createdBy: string;
|
||||
createdByEmail: string;
|
||||
description?: string;
|
||||
plan?: "free" | "team" | "pro" | "enterprise";
|
||||
},
|
||||
) => {
|
||||
const [workspace] = await db
|
||||
@@ -73,6 +75,8 @@ export const create = async (
|
||||
name: workspaceInput.name,
|
||||
slug: workspaceInput.slug,
|
||||
createdBy: workspaceInput.createdBy,
|
||||
...(workspaceInput.description && { description: workspaceInput.description }),
|
||||
...(workspaceInput.plan && { plan: workspaceInput.plan }),
|
||||
})
|
||||
.returning({
|
||||
id: workspaces.id,
|
||||
|
||||
Reference in New Issue
Block a user