feat(web): take a card before the Company Brain trial starts (#1459)

Onboarding now opens a trial step that collects a card through Stripe checkout before the brain is enabled, with a timeline showing today's $0, the day-12 reminder, and the day-14 charge.

- Only leaves the card step once the API confirms the trial is live
- Brain home shows a setup banner and dims what the trial unlocks
- Recovers orgs that abandoned checkout instead of stranding them
- Adds the organization ID to account settings, copyable from the label
This commit is contained in:
MaheshtheDev 2026-08-13 06:58:47 +00:00
parent c70c142fc7
commit 0695ca421b
11 changed files with 451 additions and 34 deletions

View file

@ -8,6 +8,8 @@ import { ArrowRight, Check, FileText, Loader2, UserPlus } from "lucide-react"
import { useQueryState } from "nuqs"
import { useSettingsModal } from "@/components/settings/settings-modal"
import { useBrainTrial } from "@/hooks/use-brain-trial"
import { TrialSetupBanner } from "@/components/trial-setup-banner"
import { useTrialStatus } from "@/hooks/use-trial-status"
import { dmSans125ClassName } from "@/lib/fonts"
import { useViewMode } from "@/lib/view-mode-context"
import {
@ -170,6 +172,7 @@ export function BrainHomeView() {
const o = useBrainOverview()
const trial = useBrainTrial()
const board = useConnectionsBoard()
const { needsSetup } = useTrialStatus()
// Rows with no reported state (older orgs, pre-Slack) don't count or render.
const milestones = [
...(o.researchStatus != null ? [o.researchStatus === "done"] : []),
@ -186,6 +189,7 @@ export function BrainHomeView() {
return (
<div className="mx-auto max-w-[1080px] space-y-6">
<TrialSetupBanner />
<StatsRow
memories={o.memoriesCount}
connected={o.connectedCount}
@ -195,7 +199,7 @@ export function BrainHomeView() {
setupTotal={milestonesTotal}
lastUpdatedAt={o.lastUpdatedAt}
/>
{board.slack && !board.slack.connected && <SlackBanner />}
{board.slack && !board.slack.connected && !needsSetup && <SlackBanner />}
<div className="grid items-start gap-6 lg:grid-cols-[minmax(0,1fr)_340px]">
<div className="min-w-0 space-y-6">
{board.showBoard && <ConnectToolsCard board={board} />}
@ -486,7 +490,7 @@ function BrainTimeline({
canInvite: boolean
toolsCardVisible: boolean
}) {
const trial = useBrainTrial()
const { needsSetup } = useTrialStatus()
const { openSettings } = useSettingsModal()
const { setViewMode } = useViewMode()
const [, setInvite] = useQueryState("invite")
@ -532,12 +536,13 @@ function BrainTimeline({
title: slackConnected ? "Slack connected" : "Connect Slack",
hint: slackConnected
? undefined
: trial.state === "trialing"
? "Ask your brain from any channel."
: "Starts your 14-day free trial. No credit card needed.",
action: slackConnected
? undefined
: { label: "Add", href: `${BACKEND}/brain/slack/oauth/install` },
: needsSetup
? "Starts with your trial."
: "Ask your brain from any channel.",
action:
slackConnected || needsSetup
? undefined
: { label: "Add", href: `${BACKEND}/brain/slack/oauth/install` },
},
...(rollout != null
? [

View file

@ -4,6 +4,7 @@ import { cn } from "@lib/utils"
import { ArrowRight, Loader2 } from "lucide-react"
import { useCallback, useEffect, useState } from "react"
import { toast } from "sonner"
import { useTrialStatus } from "@/hooks/use-trial-status"
import { dmSans125ClassName } from "@/lib/fonts"
import { useViewMode } from "@/lib/view-mode-context"
import { brainConnectorIcon, SlackMark } from "../brain-connector-icons"
@ -192,6 +193,7 @@ export const CONNECT_TOOLS_CARD_ID = "connect-tools"
export function ConnectToolsCard({ board }: { board: ConnectionsBoardState }) {
const { setViewMode } = useViewMode()
const { loading, featured, overflow, busy, isConnected, connect } = board
const { needsSetup } = useTrialStatus()
return (
<section
@ -209,11 +211,19 @@ export function ConnectToolsCard({ board }: { board: ConnectionsBoardState }) {
Connect your tools
</p>
<p className="mt-0.5 text-[12px] font-medium text-[#737373]">
Give your Slack agent live access to the apps your team already uses.
{needsSetup
? "Starts with your trial."
: "Give your Slack agent live access to the apps your team already uses."}
</p>
</div>
<div className="overflow-hidden rounded-[12px] bg-[#14161A]">
<div
className={cn(
"overflow-hidden rounded-[12px] bg-[#14161A]",
needsSetup && "pointer-events-none opacity-40 select-none",
)}
aria-disabled={needsSetup || undefined}
>
{loading ? (
Array.from({ length: 3 }).map((_, i) => (
<TileSkeleton key={i} showDivider={i < 2} />
@ -248,6 +258,7 @@ export function ConnectToolsCard({ board }: { board: ConnectionsBoardState }) {
export function AskInSlackCard({ board }: { board: ConnectionsBoardState }) {
const { previewApps, isConnected, connectedCount } = board
const { needsSetup } = useTrialStatus()
const prompts = previewApps
.filter((a) => AGENT_PROMPTS[a.slug])
.slice(0, 6)
@ -275,12 +286,19 @@ export function AskInSlackCard({ board }: { board: ConnectionsBoardState }) {
</p>
</div>
<p className="text-[12px] font-medium leading-[1.5] text-[#737373]">
{connectedCount > 0
? "Things your agent can answer now:"
: "Connect a tool and your agent can answer:"}
{needsSetup
? "Starts with your trial."
: connectedCount > 0
? "Things your agent can answer now:"
: "Connect a tool and your agent can answer:"}
</p>
<div className="overflow-hidden rounded-[12px] bg-[#14161A]">
<div
className={cn(
"overflow-hidden rounded-[12px] bg-[#14161A]",
needsSetup && "opacity-40 select-none",
)}
>
{prompts.map((p, i) => (
<div
key={p.slug}
@ -426,6 +444,7 @@ function TileSkeleton({ showDivider = false }: { showDivider?: boolean }) {
}
export function SlackBanner() {
const { needsSetup } = useTrialStatus()
return (
<section
className="relative overflow-hidden rounded-[18px] bg-[#1B1F24] p-3.5 sm:p-5"
@ -472,12 +491,20 @@ export function SlackBanner() {
</div>
<a
href={`${BACKEND}/brain/slack/oauth/install`}
href={
needsSetup ? "/onboarding" : `${BACKEND}/brain/slack/oauth/install`
}
className="inline-flex shrink-0 items-center justify-center rounded-lg bg-white px-3 py-1.5 text-[13px] font-semibold text-[#1D1C1D] transition-transform hover:scale-[1.02] sm:gap-2 sm:px-4 sm:py-2.5 sm:text-[14px]"
>
<SlackMark className="hidden sm:block sm:size-[18px]" />
<span className="sm:hidden">Add</span>
<span className="hidden sm:inline">Add to Slack</span>
{needsSetup ? (
<span>Start trial</span>
) : (
<>
<SlackMark className="hidden sm:block sm:size-[18px]" />
<span className="sm:hidden">Add</span>
<span className="hidden sm:inline">Add to Slack</span>
</>
)}
</a>
</div>
</section>

View file

@ -32,6 +32,7 @@ import { StaticGraphPreview } from "@/components/memory-graph/graph-card"
import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/components/tooltip"
import { ChromeIcon, RaycastIcon } from "@/components/integration-icons"
import { SlackConnectCard } from "@/components/slack-connect-card"
import { TrialSetupBanner } from "@/components/trial-setup-banner"
import { GoogleDrive, Notion, MCPIcon } from "@ui/assets/icons"
import { analytics } from "@/lib/analytics"
import type { IntegrationParamValue } from "@/lib/search-params"
@ -1344,6 +1345,7 @@ export function DashboardView({
)}
>
<div className="mx-auto w-full max-w-4xl space-y-4 md:space-y-5">
<TrialSetupBanner />
<SlackConnectCard />
{headerNotice ? <div className="space-y-2">{headerNotice}</div> : null}

View file

@ -30,6 +30,9 @@ import {
UserAvatar,
} from "./step-about"
import { ResearchActionRail } from "./research-action-rail"
import { CHECKOUT_RETURN_PARAM, StepTrial } from "./step-trial"
import { useTrialStatus } from "@/hooks/use-trial-status"
import { analytics } from "@/lib/analytics"
import {
type CompanyBrainConfirmResult,
type CompanyBrainOrganizationChoice,
@ -52,7 +55,7 @@ interface CompanyBrainOnboardingProps {
const BACKEND =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
type Phase = "confirm" | "research"
type Phase = "confirm" | "trial" | "research"
function normalizeDomain(input: string): string {
const host = input
@ -82,6 +85,23 @@ export function CompanyBrainOnboarding({
onUsePersonal,
}: CompanyBrainOnboardingProps) {
const [phase, setPhase] = useState<Phase>("confirm")
const { needsSetup } = useTrialStatus()
const resumedRef = useRef(false)
useEffect(() => {
if (resumedRef.current) return
const url = new URL(window.location.href)
if (url.searchParams.get(CHECKOUT_RETURN_PARAM) !== "complete") return
resumedRef.current = true
url.searchParams.delete(CHECKOUT_RETURN_PARAM)
window.history.replaceState({}, "", `${url.pathname}${url.search}`)
setPhase("research")
}, [])
useEffect(() => {
if (resumedRef.current || !needsSetup || phase !== "confirm") return
resumedRef.current = true
setPhase("trial")
analytics.brainTrialCardViewed()
}, [needsSetup, phase])
const [domain, setDomain] = useState(initialDomain)
const [organizationChoices, setOrganizationChoices] = useState<
CompanyBrainOrganizationChoice[] | null
@ -107,7 +127,8 @@ export function CompanyBrainOnboarding({
}
setOrganizationChoices(null)
setServerSchedulesResearch(result.serverSchedulesResearch)
setPhase("research")
setPhase("trial")
analytics.brainTrialCardViewed()
}
// New-org signup schedules research after provisioning; if that hook is slow
@ -203,9 +224,9 @@ export function CompanyBrainOnboarding({
<main
className={cn(
"relative z-10 flex-1 flex flex-col min-h-0",
phase === "confirm"
? "justify-center items-center px-4 md:px-10"
: "justify-start items-stretch pt-2 px-4 md:px-8 xl:px-14",
phase === "research"
? "justify-start items-stretch pt-2 px-4 md:px-8 xl:px-14"
: "justify-center items-center px-4 md:px-10",
)}
>
{/* Persistent card: full confirm card, then morphs into a slim docked header. */}
@ -215,13 +236,25 @@ export function CompanyBrainOnboarding({
style={cardSurfaceStyle}
className={cn(
"w-full mx-auto rounded-[22px] bg-[#1B1F24]",
phase === "confirm"
? "max-w-xl p-6 md:p-8"
: "max-w-7xl px-5 py-3 xl:max-w-[1360px]",
phase === "research"
? "max-w-7xl px-5 py-3 xl:max-w-[1360px]"
: phase === "trial"
? "max-w-4xl p-6 md:p-7"
: "max-w-xl p-6 md:p-8",
)}
>
<AnimatePresence mode="wait" initial={false}>
{phase === "confirm" ? (
{phase === "trial" ? (
<motion.div
key="trial"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
>
<StepTrial onActive={() => setPhase("research")} />
</motion.div>
) : phase === "confirm" ? (
<motion.div
key="confirm"
initial={{ opacity: 0 }}

View file

@ -536,7 +536,7 @@ function SlackStepBody({
Add to Slack
</a>
<p className="mt-2 text-center text-[11px] font-medium leading-[1.5] text-[#525D6E]">
Starts your 14-day free trial. No credit card needed.
Included in your 14-day trial.
</p>
</div>
)

View file

@ -0,0 +1,239 @@
"use client"
import { Gmail, GoogleDrive, Granola, MCPIcon, Notion } from "@ui/assets/icons"
import { GradientLogo } from "@ui/assets/Logo"
import { Button } from "@ui/components/button"
import { cn } from "@lib/utils"
import { ArrowRight, Loader2, ShieldCheck } from "lucide-react"
import { useState } from "react"
import { toast } from "sonner"
import { SlackMark } from "@/components/brain-connector-icons"
import { analytics } from "@/lib/analytics"
import { dmSans125ClassName } from "@/lib/fonts"
const BACKEND =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
export const CHECKOUT_RETURN_PARAM = "brainTrial"
const TRIAL_DAYS = 14
/** The only reminder that lands before the charge; 15 and 17 are post-trial. */
const REMINDER_DAY = 12
const MONTHLY_PRICE = "$100"
function checkoutReturnUrl(): string {
const url = new URL(window.location.href)
url.searchParams.set(CHECKOUT_RETURN_PARAM, "complete")
return url.toString()
}
function dayOffset(days: number): string {
const at = new Date(Date.now() + days * 24 * 60 * 60 * 1000)
return at.toLocaleDateString(undefined, { month: "short", day: "numeric" })
}
const ORBIT = [
{ key: "slack", r: 74, deg: 0, node: <SlackMark className="size-4" /> },
{ key: "gmail", r: 74, deg: 128, node: <Gmail className="size-4" /> },
{ key: "notion", r: 74, deg: 236, node: <Notion className="size-4" /> },
{ key: "drive", r: 112, deg: 58, node: <GoogleDrive className="size-4" /> },
{ key: "granola", r: 112, deg: 172, node: <Granola className="size-4" /> },
{ key: "mcp", r: 112, deg: 296, node: <MCPIcon className="size-4" /> },
]
const SPIN = "motion-safe:animate-[spin_44s_linear_infinite]"
const SPIN_BACK = "motion-safe:animate-[spin_44s_linear_infinite_reverse]"
function BrainPanel() {
return (
<div className="relative hidden aspect-[3/2] w-[56%] shrink-0 items-center justify-center overflow-hidden rounded-xl bg-[#0B0E13] ring-1 ring-white/[0.06] md:flex">
<span
aria-hidden="true"
className="pointer-events-none absolute size-44 rounded-full bg-[#4BA0FA]/15 blur-3xl"
/>
<div aria-hidden="true" className="relative size-[248px]">
<span className="absolute left-1/2 top-1/2 size-[148px] -translate-x-1/2 -translate-y-1/2 rounded-full border border-white/[0.07]" />
<span className="absolute left-1/2 top-1/2 size-[224px] -translate-x-1/2 -translate-y-1/2 rounded-full border border-white/[0.05]" />
<div className={cn("absolute inset-0", SPIN)}>
{ORBIT.map(({ key, r, deg, node }) => (
<span
key={key}
style={{
transform: `translate(-50%, -50%) rotate(${deg}deg) translateY(-${r}px)`,
}}
className="absolute left-1/2 top-1/2 flex size-8 items-center justify-center rounded-full bg-[#161B22] ring-1 ring-white/10"
>
<span
className={cn("flex", SPIN_BACK)}
style={{ rotate: `${-deg}deg` }}
>
{node}
</span>
</span>
))}
</div>
<GradientLogo className="absolute left-1/2 top-1/2 h-auto w-[68px] -translate-x-1/2 -translate-y-1/2" />
</div>
<p className="absolute inset-x-0 bottom-5 text-center text-[13px] font-medium text-[#8b8b8b]">
Meet <span className="text-[#4BA0FA]">@supermemory</span>
</p>
</div>
)
}
function TimelineRow({
date,
title,
value,
current,
}: {
date: string
title: string
value?: string
current?: boolean
}) {
return (
<li className="relative flex items-start gap-3 pl-[18px]">
<span
aria-hidden="true"
className={cn(
"absolute left-0 top-[5px] size-[7px] rounded-full",
current
? "bg-[#fafafa] ring-4 ring-[#fafafa]/10"
: "bg-[#2b3138] ring-1 ring-white/15",
)}
/>
<div className="flex min-w-0 flex-1 items-baseline justify-between gap-3">
<div className="flex min-w-0 flex-col gap-0.5">
<span className="text-[13px] font-medium text-[#fafafa]">{date}</span>
<span className="text-[12px] leading-snug text-[#8b8b8b]">
{title}
</span>
</div>
{value ? (
<span className="shrink-0 text-[14px] font-medium text-[#fafafa] tabular-nums">
{value}
</span>
) : null}
</div>
</li>
)
}
export function StepTrial({ onActive }: { onActive: () => void }) {
const [starting, setStarting] = useState(false)
const start = async () => {
if (starting) return
setStarting(true)
analytics.brainTrialCheckoutStarted()
try {
const res = await fetch(`${BACKEND}/brain/trial/start`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ successUrl: checkoutReturnUrl() }),
})
const data = (await res.json()) as {
checkoutUrl?: string | null
status?: string
error?: string
}
if (res.status === 409 || data.error === "trial_unavailable") {
throw new Error(
"This workspace has already used its free trial. Upgrade from billing to continue.",
)
}
if (!res.ok) throw new Error(data.error ?? "Couldn't start the trial.")
if (data.checkoutUrl) {
window.location.href = data.checkoutUrl
return
}
if (data.status === "already_active" || data.status === "attached") {
onActive()
return
}
throw new Error("Couldn't start the trial.")
} catch (error) {
console.error("Failed to start trial:", error)
toast.error(
error instanceof Error ? error.message : "Couldn't start the trial.",
)
setStarting(false)
}
}
return (
<div className="flex gap-6">
<div className="flex min-w-0 flex-1 flex-col gap-5">
<div className="flex flex-col gap-1.5">
<h2
className={cn(
dmSans125ClassName(),
"text-[22px] leading-tight font-medium text-[#fafafa]",
)}
>
Start your {TRIAL_DAYS}-day trial
</h2>
<p className="text-[13px] leading-relaxed text-[#8b8b8b]">
We take a card now so Company Brain keeps working when the trial
ends. Cancel any time before then and you won't be charged.
</p>
</div>
<ol className="relative flex flex-col gap-5 py-1">
<span
aria-hidden="true"
className="absolute left-[3px] top-2.5 bottom-[22px] w-px bg-white/10"
/>
<TimelineRow
date="Today"
title="Full access to Company Brain"
value="$0"
current
/>
<TimelineRow
date={dayOffset(REMINDER_DAY)}
title="We email you before the charge"
/>
<TimelineRow
date={dayOffset(TRIAL_DAYS)}
title="Trial ends"
value={`${MONTHLY_PRICE}/mo`}
/>
</ol>
<div className="flex flex-col items-center gap-3">
<Button
variant="insideOut"
onClick={start}
disabled={starting}
className="w-full justify-center rounded-full px-5 py-[11px] text-[13px] font-medium text-[#fafafa]"
>
{starting ? (
<>
Opening checkout
<Loader2 className="size-3.5 animate-spin" />
</>
) : (
<>
Add card and start trial
<ArrowRight className="size-3.5" />
</>
)}
</Button>
<p className="flex items-center gap-1.5 text-[12px] text-[#737373]">
<ShieldCheck className="size-3.5" />
Secured by Stripe · Cancel in one click
</p>
</div>
</div>
<BrainPanel />
</div>
)
}

View file

@ -23,6 +23,7 @@ import { Dialog, DialogContent, DialogTitle } from "@ui/components/dialog"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { useMutation, useQuery } from "@tanstack/react-query"
import {
Copy,
LoaderIcon,
ChevronDown,
Users,
@ -458,10 +459,26 @@ export default function Account({
<span
className={cn(
dmSans125ClassName(),
"text-[12px] tracking-[-0.12px] text-[#737373]",
"flex items-center gap-1 text-[12px] tracking-[-0.12px] text-[#737373]",
)}
>
Organization
{org?.id ? (
<button
type="button"
aria-label="Copy organization ID"
title={org.id}
onClick={() => {
navigator.clipboard
.writeText(org.id)
.then(() => toast.success("Organization ID copied"))
.catch(() => toast.error("Couldn't copy"))
}}
className="inline-flex size-4 shrink-0 items-center justify-center rounded transition-colors hover:text-[#FAFAFA]"
>
<Copy className="size-2.5" />
</button>
) : null}
</span>
{isEditingOrgName ? (
<form

View file

@ -51,6 +51,7 @@ function SlackMark({ className }: { className?: string }) {
export function SlackConnectCard() {
const isCompanyBrain = useHasCompanyBrain()
const [status, setStatus] = useState<SlackStatus | null>(null)
const [trialActive, setTrialActive] = useState(true)
const [loading, setLoading] = useState(true)
useEffect(() => {
@ -58,10 +59,16 @@ export function SlackConnectCard() {
let active = true
;(async () => {
try {
const res = await fetch(`${BACKEND}/brain/slack/status`, {
credentials: "include",
})
if (active && res.ok) setStatus((await res.json()) as SlackStatus)
const [slackRes, trialRes] = await Promise.all([
fetch(`${BACKEND}/brain/slack/status`, { credentials: "include" }),
fetch(`${BACKEND}/brain/trial/status`, { credentials: "include" }),
])
if (!active) return
if (slackRes.ok) setStatus((await slackRes.json()) as SlackStatus)
if (trialRes.ok) {
const trial = (await trialRes.json()) as { active?: boolean }
setTrialActive(Boolean(trial.active))
}
} finally {
if (active) setLoading(false)
}
@ -92,7 +99,7 @@ export function SlackConnectCard() {
<span className="size-1.5 rounded-full bg-[#2EB67D]" />
Connected
</span>
) : (
) : trialActive ? (
<a
href={`${BACKEND}/brain/slack/oauth/install`}
className="inline-flex shrink-0 items-center gap-2 rounded-lg bg-white px-3.5 py-2 text-[13px] font-semibold text-[#1D1C1D] transition-transform hover:scale-[1.02]"
@ -100,6 +107,13 @@ export function SlackConnectCard() {
<SlackMark className="size-4" />
Add to Slack
</a>
) : (
<a
href="/onboarding"
className="inline-flex shrink-0 items-center gap-2 rounded-lg bg-white/10 px-3.5 py-2 text-[13px] font-semibold text-fg-primary ring-1 ring-surface-border transition-colors hover:bg-white/15"
>
Finish setting up
</a>
)}
</div>
)

View file

@ -0,0 +1,41 @@
"use client"
import { ArrowRight, CreditCard } from "lucide-react"
import Link from "next/link"
import { useTrialStatus } from "@/hooks/use-trial-status"
export function TrialSetupBanner() {
const { needsSetup, data } = useTrialStatus()
if (!needsSetup) return null
const endedTrial = data?.reason === "trial_ended"
return (
<div className="flex items-center justify-between gap-4 rounded-[14px] bg-[#191D24] px-4 py-3 ring-1 ring-[#4BA0FA]/20 sm:px-5">
<div className="flex min-w-0 items-center gap-3">
<span className="flex size-8 shrink-0 items-center justify-center rounded-full bg-[#4BA0FA]/12">
<CreditCard className="size-4 text-[#4BA0FA]" />
</span>
<div className="min-w-0">
<p className="text-sm font-semibold text-fg-primary">
{endedTrial
? "Your Company Brain trial has ended"
: "Finish setting up Company Brain"}
</p>
<p className="mt-0.5 truncate text-[12px] text-fg-muted">
{endedTrial
? "Move to Max or Scale to switch the brain back on."
: "Add a card to start your 14-day trial. $0 today."}
</p>
</div>
</div>
<Link
href={endedTrial ? "/?settings=billing" : "/onboarding"}
className="inline-flex shrink-0 items-center gap-1.5 rounded-lg bg-white px-3.5 py-2 text-[13px] font-semibold text-[#1D1C1D] transition-transform hover:scale-[1.02]"
>
{endedTrial ? "Upgrade" : "Add card"}
<ArrowRight className="size-3.5" />
</Link>
</div>
)
}

View file

@ -0,0 +1,34 @@
import { useQuery } from "@tanstack/react-query"
import { useHasCompanyBrain } from "@/hooks/use-company-brain"
const BACKEND =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
export type TrialStatus = {
active: boolean
reason: string | null
}
/** Distinguishes a named Company Brain org from one whose trial is actually live. */
export function useTrialStatus() {
const isCompanyBrain = useHasCompanyBrain()
const query = useQuery({
queryKey: ["brain", "trial-status"],
queryFn: async (): Promise<TrialStatus> => {
const res = await fetch(`${BACKEND}/brain/trial/status`, {
credentials: "include",
})
if (!res.ok) throw new Error("Failed to load trial status")
const data = (await res.json()) as { active?: boolean; reason?: string }
return { active: Boolean(data.active), reason: data.reason ?? null }
},
enabled: isCompanyBrain,
staleTime: 30 * 1000,
})
return {
...query,
needsSetup: isCompanyBrain && query.data ? !query.data.active : false,
}
}

View file

@ -271,4 +271,9 @@ export const analytics = {
}) => safeCapture("company_brain_promo_clicked", props),
companyBrainPromoDismissed: () =>
safeCapture("company_brain_promo_dismissed"),
brainTrialCardViewed: () => safeCapture("brain_trial_card_viewed"),
brainTrialCheckoutStarted: () => safeCapture("brain_trial_checkout_started"),
brainTrialCheckoutAbandoned: () =>
safeCapture("brain_trial_checkout_abandoned"),
}