From ab3dcd7a7334e6520f92fd09d27a7e4a16249345 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:12:18 +0000 Subject: [PATCH] feat(web): create org from settings launches onboarding (#1136) Settings 'create organization' now routes to /onboarding?new=1&name=... (team/personal, invites) instead of a bare authClient.create. Adds the forceCreate path with name prefill, clears new=1 after a successful create to prevent duplicate orgs, and fixes the Radix popover-to-dialog pointer-events lock. --- apps/web/app/(app)/onboarding/page.tsx | 27 +++++++++-- .../settings/settings-org-switcher.tsx | 47 ++++++------------- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/apps/web/app/(app)/onboarding/page.tsx b/apps/web/app/(app)/onboarding/page.tsx index 8ee25591..311791bf 100644 --- a/apps/web/app/(app)/onboarding/page.tsx +++ b/apps/web/app/(app)/onboarding/page.tsx @@ -44,6 +44,10 @@ export default function BrainOnboardingPage() { const { user, org, organizations, setActiveOrg, refetchOrganizations } = useAuth() + // `?new=1` forces creating an additional org even when the user already has one. + const forceCreate = params?.get("new") === "1" + const nameParam = params?.get("name")?.trim() || "" + const stepFromUrl = (params?.get("step") as BrainStep | null) ?? "about" const initialStep: BrainStep = BRAIN_STEPS.includes(stepFromUrl) ? stepFromUrl @@ -68,7 +72,7 @@ export default function BrainOnboardingPage() { const [about, setAbout] = useState({ name: user?.name ?? "", about: "", - workspaceName: suggestedWorkspaceName, + workspaceName: nameParam || suggestedWorkspaceName, workspaceDomain: domain ?? "", }) const [sources, setSources] = useState({ @@ -82,6 +86,7 @@ export default function BrainOnboardingPage() { }) useEffect(() => { + if (forceCreate) return try { const raw = localStorage.getItem(STORAGE_KEY) if (!raw) return @@ -96,7 +101,7 @@ export default function BrainOnboardingPage() { if (cached.sources) setSources((s) => ({ ...s, ...cached.sources })) if (cached.team) setTeam((t) => ({ ...t, ...cached.team })) } catch {} - }, []) + }, [forceCreate]) useEffect(() => { try { @@ -175,8 +180,13 @@ export default function BrainOnboardingPage() { try { localStorage.removeItem(STORAGE_KEY) } catch {} + // Extra org from settings: hard-reload so org-scoped caches don't show the previous org's data. + if (forceCreate) { + window.location.href = "/?onboarded=1" + return + } router.push("/?onboarded=1") - }, [router, mode, sources, team]) + }, [router, mode, sources, team, forceCreate]) const goNext = useCallback(() => { const idx = BRAIN_STEPS.indexOf(step) @@ -193,7 +203,7 @@ export default function BrainOnboardingPage() { const creatingOrgRef = useRef(false) const ensureOrg = useCallback(async () => { - if (organizations && organizations.length > 0) return + if (!forceCreate && organizations && organizations.length > 0) return const name = (about.workspaceName || suggestedWorkspaceName).trim() const slug = generateOrgSlug(name) const metadata: BrainMetadata & { signupSource: string } = { @@ -225,6 +235,13 @@ export default function BrainOnboardingPage() { has_about: Boolean(about.about.trim()), has_domain: Boolean(mode === "team" && (about.workspaceDomain || domain)), }) + // Drop new=1 so a reload or back+Continue reuses this org instead of creating a duplicate. + if (forceCreate) { + const url = new URL(window.location.href) + url.searchParams.delete("new") + url.searchParams.delete("name") + router.replace(url.pathname + url.search, { scroll: false }) + } }, [ organizations, about, @@ -234,6 +251,8 @@ export default function BrainOnboardingPage() { containerTag, setActiveOrg, refetchOrganizations, + forceCreate, + router, ]) const handleAboutContinue = useCallback(async () => { diff --git a/apps/web/components/settings/settings-org-switcher.tsx b/apps/web/components/settings/settings-org-switcher.tsx index 9ebcf9b5..e3e4be84 100644 --- a/apps/web/components/settings/settings-org-switcher.tsx +++ b/apps/web/components/settings/settings-org-switcher.tsx @@ -1,6 +1,7 @@ "use client" -import { useMemo, useState } from "react" +import { useEffect, useMemo, useState } from "react" +import { useRouter } from "next/navigation" import { useCustomer } from "autumn-js/react" import { toast } from "sonner" import { @@ -13,7 +14,6 @@ import { import { cn } from "@lib/utils" import { dmSansClassName, dmSans125ClassName } from "@/lib/fonts" import { useAuth } from "@lib/auth-context" -import { authClient } from "@lib/auth" import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover" import { Dialog, DialogContent, DialogTitle } from "@ui/components/dialog" import { OrgPlanBadge, resolveOrgPlan } from "@/components/org-plan-badge" @@ -23,17 +23,9 @@ import { useTokenUsage, type PlanType } from "@/hooks/use-token-usage" const SURFACE_SHADOW = "0 2.842px 14.211px 0 rgba(0,0,0,0.25), 0.711px 0.711px 0.711px 0 rgba(255,255,255,0.10) inset" -function generateOrgSlug(name: string): string { - const base = - name - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/(^-|-$)/g, "") || "org" - return `${base}-${Math.floor(100000 + Math.random() * 900000)}` -} - export function SettingsOrgSwitcher() { const { org, organizations, setActiveOrg } = useAuth() + const router = useRouter() const autumn = useCustomer() const { currentPlan } = useTokenUsage(autumn) const { data: orgSummaries } = useOrgSummaries() @@ -44,6 +36,11 @@ export function SettingsOrgSwitcher() { const [createName, setCreateName] = useState("") const [creating, setCreating] = useState(false) + // Clear a stale Radix `pointer-events: none` left on so the dialog accepts clicks. + useEffect(() => { + if (createOpen) document.body.style.pointerEvents = "" + }, [createOpen]) + const planByOrgId = useMemo(() => { const map = new Map() for (const summary of orgSummaries ?? []) { @@ -78,29 +75,14 @@ export function SettingsOrgSwitcher() { } } - const handleCreate = async () => { + const handleCreate = () => { const name = createName.trim() if (!name || creating) return setCreating(true) - try { - const result = await authClient.organization.create({ - name, - slug: generateOrgSlug(name), - metadata: { signupSource: "consumer" }, - }) - if (result.error) { - throw new Error(result.error.message ?? "Failed to create organization") - } - await setActiveOrg(result.data?.slug ?? "") - window.location.reload() - } catch (error) { - setCreating(false) - toast.error( - error instanceof Error - ? error.message - : "Failed to create organization", - ) - } + // Org creation now happens through the onboarding flow (team/personal, name, invites). + setCreateOpen(false) + setOpen(false) + router.push(`/onboarding?new=1&name=${encodeURIComponent(name)}`) } return ( @@ -179,8 +161,9 @@ export function SettingsOrgSwitcher() {