From ecbb048209347f91575e6368a7a14dfee0b204c2 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommu Date: Wed, 17 Jun 2026 17:23:16 -0700 Subject: [PATCH] feat(web): company brain entitlement helpers + team-brain defaults hasCompanyBrain/getBrainMode/getSignupSource helpers, useHasCompanyBrain team fallback, space visibility field, SHARED_TEAM_BRAIN_TAG, and default-to-Team-Brain in the store. --- apps/web/hooks/use-company-brain.ts | 6 ++- apps/web/lib/billing-utils.ts | 58 ++++++++++++++++++++++++++++- apps/web/stores/index.ts | 11 ++++-- packages/lib/constants.ts | 2 + packages/lib/types.ts | 1 + packages/validation/api.ts | 4 ++ 6 files changed, 75 insertions(+), 7 deletions(-) diff --git a/apps/web/hooks/use-company-brain.ts b/apps/web/hooks/use-company-brain.ts index 1668fc8d..b85e5a53 100644 --- a/apps/web/hooks/use-company-brain.ts +++ b/apps/web/hooks/use-company-brain.ts @@ -1,7 +1,9 @@ import { useAuth } from "@lib/auth-context" -import { hasCompanyBrain } from "@/lib/billing-utils" +import { getBrainMode, hasCompanyBrain } from "@/lib/billing-utils" export function useHasCompanyBrain(): boolean { const { org } = useAuth() - return hasCompanyBrain(org?.metadata as Record | undefined) + const metadata = org?.metadata as Record | string | undefined + // Team-brain orgs use brain spaces even before the add-on webhook lands. + return hasCompanyBrain(metadata) || getBrainMode(metadata) === "team" } diff --git a/apps/web/lib/billing-utils.ts b/apps/web/lib/billing-utils.ts index 60ee3a86..577a9640 100644 --- a/apps/web/lib/billing-utils.ts +++ b/apps/web/lib/billing-utils.ts @@ -1,10 +1,21 @@ const COMPANY_BRAIN_PRODUCT_ID = "company_brain" // Add-on resolved by product presence, not tier. +// better-auth returns org.metadata as a JSON string, so accept string or object. export function hasCompanyBrain( - metadata: Record | null | undefined, + metadataRaw: Record | string | null | undefined, ): boolean { - if (!metadata) return false + if (!metadataRaw) return false + let metadata: Record + if (typeof metadataRaw === "string") { + try { + metadata = JSON.parse(metadataRaw) as Record + } catch { + return false + } + } else { + metadata = metadataRaw + } const overrides = metadata.featureOverrides as | Record | undefined @@ -16,6 +27,49 @@ export function hasCompanyBrain( return activeProducts.includes(COMPANY_BRAIN_PRODUCT_ID) } +// Origin of the org. Consumer (app.supermemory) orgs get company_brain attached, +// but the add-on lands async — signupSource is set at creation, so it's the +// reliable "this org uses brain spaces" signal in the UI. +export function getSignupSource( + metadataRaw: Record | string | null | undefined, +): string | null { + if (!metadataRaw) return null + let metadata: Record + if (typeof metadataRaw === "string") { + try { + metadata = JSON.parse(metadataRaw) as Record + } catch { + return null + } + } else { + metadata = metadataRaw + } + return typeof metadata.signupSource === "string" + ? (metadata.signupSource as string) + : null +} + +// Brain mode chosen during onboarding ("personal" | "team"). Set synchronously +// at org creation, so it's the reliable pre-webhook signal for company brain. +export function getBrainMode( + metadataRaw: Record | string | null | undefined, +): string | null { + if (!metadataRaw) return null + let metadata: Record + if (typeof metadataRaw === "string") { + try { + metadata = JSON.parse(metadataRaw) as Record + } catch { + return null + } + } else { + metadata = metadataRaw + } + return typeof metadata.brainMode === "string" + ? (metadata.brainMode as string) + : null +} + /** * Format a number with K/M suffix for display * @example formatUsageNumber(1500000) => "1.5M" diff --git a/apps/web/stores/index.ts b/apps/web/stores/index.ts index 4d754f77..82a01eaf 100644 --- a/apps/web/stores/index.ts +++ b/apps/web/stores/index.ts @@ -3,18 +3,23 @@ import { useQueryState } from "nuqs" import { projectParam } from "@/lib/search-params" import { useCallback } from "react" -import { DEFAULT_PROJECT_ID } from "@lib/constants" +import { DEFAULT_PROJECT_ID, SHARED_TEAM_BRAIN_TAG } from "@lib/constants" +import { useHasCompanyBrain } from "@/hooks/use-company-brain" export function useProject() { const [selectedProjects, _setSelectedProjects] = useQueryState( "project", projectParam, ) + const hasCompanyBrain = useHasCompanyBrain() + const defaultTag = hasCompanyBrain + ? SHARED_TEAM_BRAIN_TAG + : DEFAULT_PROJECT_ID - const selectedProject = selectedProjects[0] ?? DEFAULT_PROJECT_ID + const selectedProject = selectedProjects[0] ?? defaultTag const effectiveContainerTags = - selectedProjects.length === 0 ? [DEFAULT_PROJECT_ID] : selectedProjects + selectedProjects.length === 0 ? [defaultTag] : selectedProjects const setSelectedProjects = useCallback( (projects: string[]) => { diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index 01439d1e..7758cdb6 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -1,5 +1,6 @@ const BIG_DIMENSIONS_NEW = 1536 const DEFAULT_PROJECT_ID = "sm_project_default" +const SHARED_TEAM_BRAIN_TAG = "sm_org_shared" const SEARCH_MEMORY_SHORTCUT_URL = "https://www.icloud.com/shortcuts/b0a132cc3c0d475196bc7014aa702a5c" const ADD_MEMORY_SHORTCUT_URL = @@ -12,6 +13,7 @@ const POKE_RECIPE_URL = "https://supermemory.link/poke" export { BIG_DIMENSIONS_NEW, DEFAULT_PROJECT_ID, + SHARED_TEAM_BRAIN_TAG, SEARCH_MEMORY_SHORTCUT_URL, ADD_MEMORY_SHORTCUT_URL, RAYCAST_EXTENSION_URL, diff --git a/packages/lib/types.ts b/packages/lib/types.ts index 9afe1945..34e39c3f 100644 --- a/packages/lib/types.ts +++ b/packages/lib/types.ts @@ -6,6 +6,7 @@ export interface Project { updatedAt: string isExperimental?: boolean emoji?: string + visibility?: "public" | "private" | "unlisted" } export interface ContainerTagListType extends Project { diff --git a/packages/validation/api.ts b/packages/validation/api.ts index e1e8c8ef..23ccff2a 100644 --- a/packages/validation/api.ts +++ b/packages/validation/api.ts @@ -1496,6 +1496,10 @@ export const ContainerTagListTypeSchema = z description: "True if containerTag starts with 'sm_project_'", example: true, }), + visibility: z.enum(["public", "private", "unlisted"]).optional().openapi({ + description: "Space visibility (company brain spaces)", + example: "public", + }), }) .openapi({ description: