From ac2d6759345176d6ca319e13f70299c00e63f6c1 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 3 Apr 2026 00:00:18 +0100 Subject: [PATCH] refactor: tweak onboarding steps UX --- apps/web/src/components/NewWorkspaceForm.tsx | 2 +- .../views/onboarding/select-plan/index.tsx | 36 +++++-- .../onboarding/workspace-details/index.tsx | 100 +++++++++++------- 3 files changed, 91 insertions(+), 47 deletions(-) diff --git a/apps/web/src/components/NewWorkspaceForm.tsx b/apps/web/src/components/NewWorkspaceForm.tsx index acc3c44e..70d501cf 100644 --- a/apps/web/src/components/NewWorkspaceForm.tsx +++ b/apps/web/src/components/NewWorkspaceForm.tsx @@ -127,7 +127,7 @@ export function NewWorkspaceForm() { body: JSON.stringify({ slug: slug || undefined, workspacePublicId: values.publicId, - cancelUrl: "/settings/workspace?upgrade=pro", + cancelUrl: window.location.pathname + window.location.search, successUrl: "/boards", }), }, diff --git a/apps/web/src/views/onboarding/select-plan/index.tsx b/apps/web/src/views/onboarding/select-plan/index.tsx index 44a3e997..fd4fcab7 100644 --- a/apps/web/src/views/onboarding/select-plan/index.tsx +++ b/apps/web/src/views/onboarding/select-plan/index.tsx @@ -1,4 +1,4 @@ -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { t } from "@lingui/core/macro"; import { Radio, RadioGroup } from "@headlessui/react"; import { AnimatePresence, motion } from "framer-motion"; @@ -40,8 +40,13 @@ const ORBIT_USERS: Record< export default function SelectPlanView() { const router = useRouter(); - const [selected, setSelected] = useState("solo"); - const [billing, setBilling] = useState("annual"); + const searchParams = useSearchParams(); + const [selected, setSelected] = useState( + (searchParams.get("plan") as PlanId | null) ?? "solo", + ); + const [billing, setBilling] = useState( + (searchParams.get("billing") as Billing | null) ?? "annual", + ); const { data: workspaces } = api.workspace.all.useQuery(); const { data: session } = authClient.useSession(); const { data: user } = api.user.getUser.useQuery(undefined, { @@ -90,6 +95,16 @@ export default function SelectPlanView() { }, ]; + const handleSelectPlan = (plan: PlanId) => { + setSelected(plan); + router.replace(`/onboarding/select-plan?plan=${plan}&billing=${billing}`); + }; + + const handleSetBilling = (b: Billing) => { + setBilling(b); + router.replace(`/onboarding/select-plan?plan=${selected}&billing=${b}`); + }; + const handleContinue = () => router.push(`/onboarding/workspace?plan=${selected}&billing=${billing}`); @@ -102,11 +117,11 @@ export default function SelectPlanView() { }; return ( -
+
{/* Left panel */} -
+

{t`Choose a plan`} @@ -119,7 +134,7 @@ export default function SelectPlanView() {
{FREQUENCIES.map((f) => ( @@ -147,7 +162,7 @@ export default function SelectPlanView() {

-
+
{hasExistingWorkspace && (
+ {!hasExistingWorkspace && ( + + )}
); } diff --git a/apps/web/src/views/onboarding/workspace-details/index.tsx b/apps/web/src/views/onboarding/workspace-details/index.tsx index dad9cb40..84e1d572 100644 --- a/apps/web/src/views/onboarding/workspace-details/index.tsx +++ b/apps/web/src/views/onboarding/workspace-details/index.tsx @@ -1,7 +1,7 @@ import { useRouter, useSearchParams } from "next/navigation"; import { t } from "@lingui/core/macro"; -import { AnimatePresence, motion } from "framer-motion"; -import { useEffect, useMemo, useState } from "react"; +import { motion } from "framer-motion"; +import { useEffect, useState } from "react"; import { HiArrowLeft, HiArrowPath, @@ -13,7 +13,6 @@ import { } from "react-icons/hi2"; import { authClient } from "@kan/auth/client"; -import { generateUID } from "@kan/shared/utils"; import Button from "~/components/Button"; import Input from "~/components/Input"; @@ -46,8 +45,6 @@ export default function WorkspaceNameView() { const [slug, setSlug] = useState(""); const [description, setDescription] = useState(""); - const generatedUid = useMemo(() => generateUID(), []); - const [debouncedSlug] = useDebounce(slug, 500); const isTyping = slug !== debouncedSlug; @@ -73,6 +70,8 @@ export default function WorkspaceNameView() { const { data: user } = api.user.getUser.useQuery(undefined, { enabled: !!session?.user, }); + const { data: workspaces } = api.workspace.all.useQuery(); + const hasExistingWorkspace = !!workspaces?.length; const utils = api.useUtils(); @@ -83,34 +82,8 @@ export default function WorkspaceNameView() { const createWorkspace = api.workspace.create.useMutation({ onSuccess: async (workspace) => { if (!workspace.publicId) return; - localStorage.setItem("workspacePublicId", workspace.publicId); - localStorage.removeItem("onboardingWorkspaceSlug"); void utils.workspace.all.invalidate(); - - if (plan !== "solo") { - try { - const response = await fetch("/api/stripe/create_checkout_session", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - workspacePublicId: workspace.publicId, - cancelUrl: "/boards", - successUrl: "/boards", - billing, - }), - }); - const data = await response.json(); - const url = (data as { url: string }).url; - if (url) { - window.location.href = url; - return; - } - } catch { - // fall through - } - } - router.push("/boards"); }, onError: () => { @@ -122,9 +95,52 @@ export default function WorkspaceNameView() { }, }); - const handleContinue = () => { + const [isRedirectingToCheckout, setIsRedirectingToCheckout] = useState(false); + + const handleContinue = async () => { if (!name.trim()) return; - createWorkspace.mutate({ name: name.trim() }); + + if (plan === "solo") { + createWorkspace.mutate({ + name: name.trim(), + ...(description.trim() && { description: description.trim() }), + }); + return; + } + + // team/pro: redirect to Stripe — workspace created on checkout_success + setIsRedirectingToCheckout(true); + try { + const response = await fetch("/api/stripe/create_checkout_session", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + workspaceName: name.trim(), + ...(description.trim() && { + workspaceDescription: description.trim(), + }), + ...(plan === "pro" && slug ? { workspaceSlug: slug } : {}), + cancelUrl: window.location.pathname + window.location.search, + successUrl: "/boards", + billing, + plan, + }), + }); + const data = await response.json(); + const url = (data as { url: string }).url; + if (url) { + window.location.href = url; + return; + } + } catch { + // fall through + } + setIsRedirectingToCheckout(false); + showPopup({ + header: t`Unable to start checkout`, + message: t`Please try again later, or contact customer support.`, + icon: "error", + }); }; useEffect(() => { @@ -144,11 +160,11 @@ export default function WorkspaceNameView() { }, []); return ( -
+
{/* Left panel */} -
+

{t`Set up your workspace`} @@ -225,7 +241,7 @@ export default function WorkspaceNameView() {

-
+
{plan !== "pro" && ( @@ -317,6 +336,11 @@ export default function WorkspaceNameView() {
+ {!hasExistingWorkspace && ( + + )}
); }