From c829fd3811829165a90dd0a9c0e913852fec2668 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommu Date: Sun, 14 Jun 2026 11:06:34 -0700 Subject: [PATCH] feat(web): company brain entitlement helper and hook Add hasCompanyBrain helper + useHasCompanyBrain hook reading the company_brain add-on from org metadata to gate Company Brain UI. --- apps/web/hooks/use-company-brain.ts | 7 ++++++ apps/web/lib/billing-utils.ts | 18 ++++++++++++++++ packages/lib/auth-context.tsx | 33 +++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 apps/web/hooks/use-company-brain.ts diff --git a/apps/web/hooks/use-company-brain.ts b/apps/web/hooks/use-company-brain.ts new file mode 100644 index 00000000..1668fc8d --- /dev/null +++ b/apps/web/hooks/use-company-brain.ts @@ -0,0 +1,7 @@ +import { useAuth } from "@lib/auth-context" +import { hasCompanyBrain } from "@/lib/billing-utils" + +export function useHasCompanyBrain(): boolean { + const { org } = useAuth() + return hasCompanyBrain(org?.metadata as Record | undefined) +} diff --git a/apps/web/lib/billing-utils.ts b/apps/web/lib/billing-utils.ts index b07e5c07..60ee3a86 100644 --- a/apps/web/lib/billing-utils.ts +++ b/apps/web/lib/billing-utils.ts @@ -1,3 +1,21 @@ +const COMPANY_BRAIN_PRODUCT_ID = "company_brain" + +// Add-on resolved by product presence, not tier. +export function hasCompanyBrain( + metadata: Record | null | undefined, +): boolean { + if (!metadata) return false + const overrides = metadata.featureOverrides as + | Record + | undefined + const override = overrides?.[COMPANY_BRAIN_PRODUCT_ID] + if (override) return Boolean(override.allow) + const activeProducts = Array.isArray(metadata.activeProducts) + ? (metadata.activeProducts as string[]) + : [] + return activeProducts.includes(COMPANY_BRAIN_PRODUCT_ID) +} + /** * Format a number with K/M suffix for display * @example formatUsageNumber(1500000) => "1.5M" diff --git a/packages/lib/auth-context.tsx b/packages/lib/auth-context.tsx index d3d0bedb..acd15e88 100644 --- a/packages/lib/auth-context.tsx +++ b/packages/lib/auth-context.tsx @@ -18,6 +18,23 @@ type OrganizationListItem = NonNullable< const STORAGE_KEY = "supermemory-consumer-last-org-slug" +// Reads ?org= from the URL once and removes it, so a deep link that +// selects an org doesn't re-fire on refresh or back-navigation. +function consumeRequestedOrgSlug(): string | null { + if (typeof window === "undefined") return null + const params = new URLSearchParams(window.location.search) + const slug = params.get("org") + if (!slug) return null + params.delete("org") + const qs = params.toString() + window.history.replaceState( + null, + "", + `${window.location.pathname}${qs ? `?${qs}` : ""}${window.location.hash}`, + ) + return slug +} + interface AuthContextType { session: SessionData["session"] | null user: SessionData["user"] | null @@ -123,6 +140,22 @@ export function AuthProvider({ children }: { children: ReactNode }) { const activeOrgId = session.session.activeOrganizationId + // Deep link (?org=) takes priority — used when arriving from + // the console. Strip the param so refresh/back doesn't re-trigger. + const requestedSlug = consumeRequestedOrgSlug() + if (requestedSlug) { + const match = orgs.find((o) => o.slug === requestedSlug) + if (match) { + if (activeOrgId === match.id) { + const full = await authClient.organization.getFullOrganization() + if (!cancelled) setOrg(full?.data ?? null) + } else { + await setActiveOrg(requestedSlug) + } + return + } + } + if (orgs.length === 1) { const one = orgs[0] if (!one) return