From 9bbe436fabc400ced52d049bb894d27c87685f16 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Tue, 21 Jul 2026 21:34:25 +0000 Subject: [PATCH] docs(brain): note trial attach after Slack OAuth (#1313) ## Summary - Documents that mono attaches Scale 14d trial + Company Brain after Slack OAuth from `/brain`. Stacks on #1310. Billing implementation is in mono. --- apps/web/app/(app)/brain/page.tsx | 3 +- apps/web/components/settings/billing.tsx | 477 +++++++++++++++++------ apps/web/hooks/use-token-usage.ts | 23 +- apps/web/lib/billing-utils.ts | 72 ++++ packages/lib/queries.ts | 8 +- 5 files changed, 453 insertions(+), 130 deletions(-) diff --git a/apps/web/app/(app)/brain/page.tsx b/apps/web/app/(app)/brain/page.tsx index b27898e91..955412812 100644 --- a/apps/web/app/(app)/brain/page.tsx +++ b/apps/web/app/(app)/brain/page.tsx @@ -21,7 +21,8 @@ import { const BACKEND = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" -// No forms: sign up → org auto-created → Slack install. The bot asks the rest. +// No forms: sign up → org auto-created → Slack install. +// After OAuth, mono attaches api_scale (14d trial) + company_brain (200 credits). export default function BrainEntryPage() { const router = useRouter() const { user, org, organizations, setActiveOrg, refetchOrganizations } = diff --git a/apps/web/components/settings/billing.tsx b/apps/web/components/settings/billing.tsx index 93909fc2e..e94ef416d 100644 --- a/apps/web/components/settings/billing.tsx +++ b/apps/web/components/settings/billing.tsx @@ -2,6 +2,8 @@ import { dmSans125ClassName } from "@/lib/fonts" import { PLAN_DISPLAY_NAMES, useTokenUsage } from "@/hooks/use-token-usage" +import { useHasCompanyBrain } from "@/hooks/use-company-brain" +import { getBrainTrialInfo } from "@/lib/billing-utils" import { cn } from "@lib/utils" import { useAuth } from "@lib/auth-context" import { getCanceledSubscription } from "@lib/queries" @@ -201,6 +203,42 @@ const ADVANCED_PLAN_CARDS: PlanCardDefinition[] = [ }, ] +// Company Brain workspaces only sell Scale / Enterprise (no Free, Pro, Max). +const COMPANY_BRAIN_PLAN_CARDS: PlanCardDefinition[] = [ + { + id: "scale", + name: "Scale", + price: "$399", + period: "/mo", + credits: "$600", + productId: "api_scale", + description: "Company Brain for your team, with production usage", + mostPopular: true, + features: [ + "Company Brain Slack agent & shared memory", + "$600 monthly usage credits when paid", + "Auto top-up & spend caps", + "Team connectors & dedicated support", + ], + }, + { + id: "enterprise", + name: "Enterprise", + price: "Custom", + period: "", + credits: "Unlimited", + productId: "api_enterprise", + description: "Custom deployments with dedicated engineering", + includesFrom: "Scale", + features: [ + "Custom metering & billing", + "Custom integrations & SSO", + "Forward-deployed engineer", + ], + isContactSales: true, + }, +] + const PLAN_RANK: Record = { free: 0, pro: 1, @@ -478,6 +516,11 @@ export default function Billing() { const { user, org } = useAuth() const autumn = useCustomer() const posthog = usePostHog() + const isCompanyBrain = useHasCompanyBrain() + const brainTrial = useMemo( + () => getBrainTrialInfo(org?.metadata as Record | string), + [org?.metadata], + ) const [isUpgrading, setIsUpgrading] = useState(false) const [isCancelling, setIsCancelling] = useState(false) const [isResuming, setIsResuming] = useState(false) @@ -511,20 +554,57 @@ export default function Billing() { planUsagePct, currentPlan, hasPaidPlan, + isTrialing: autumnTrialing, + trialEndsAtMs: autumnTrialEndsAtMs, isLoading: isCheckingStatus, daysRemaining, } = useTokenUsage(autumn) + const brainTrialStillOpen = + brainTrial.status === "active" && + (brainTrial.endsAtMs == null || brainTrial.endsAtMs > Date.now()) + const isBrainTrialEnded = + isCompanyBrain && + (brainTrial.status === "expired" || + brainTrial.status === "exhausted" || + (brainTrial.status === "active" && + brainTrial.endsAtMs != null && + brainTrial.endsAtMs <= Date.now())) + const isOnTrial = + !isBrainTrialEnded && + (autumnTrialing || (isCompanyBrain && brainTrialStillOpen)) + const trialEndsAtMs = brainTrial.endsAtMs ?? autumnTrialEndsAtMs ?? null + const trialDaysLeft = + brainTrial.daysRemaining ?? + (trialEndsAtMs != null + ? Math.max( + 0, + Math.ceil((trialEndsAtMs - Date.now()) / (1000 * 60 * 60 * 24)), + ) + : null) + const trialEndsLabel = + trialEndsAtMs != null + ? new Date(trialEndsAtMs).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }) + : null + const trialCredits = brainTrial.credits ?? (isOnTrial ? 200 : null) + const showPlanUsage = hasPaidPlan || isOnTrial || isCompanyBrain + // Open the carousel to the page holding the current plan (Max/Scale/Enterprise live on page 2). + // Company Brain orgs only list Scale + Enterprise — no carousel. const didAutoOpenPlanPage = useRef(false) useEffect(() => { + if (isCompanyBrain) return if (didAutoOpenPlanPage.current || isCheckingStatus) return didAutoOpenPlanPage.current = true if (ADVANCED_PLAN_CARDS.some((p) => p.id === currentPlan)) { setIsPlanCarouselActive(true) setPlanPage(1) } - }, [isCheckingStatus, currentPlan]) + }, [isCheckingStatus, currentPlan, isCompanyBrain]) const balance = autumn.data?.balances?.[CREDIT_FEATURE_ID] const creditRemaining = @@ -834,6 +914,25 @@ export default function Billing() { ) } + // Trial Scale: primary CTA is activate paid Scale (not a dead "current" state). + if (plan.id === "scale" && (isOnTrial || isBrainTrialEnded)) { + return ( + + ) + } + if (isCurrentPlan) { return ( ) } @@ -914,31 +1015,43 @@ export default function Billing() {
-
+

- {hasPaidPlan - ? `${planDisplayNames[currentPlan]} plan` - : "Free plan"} + {isOnTrial || + isBrainTrialEnded || + (isCompanyBrain && currentPlan === "scale") + ? "Scale plan" + : hasPaidPlan + ? `${planDisplayNames[currentPlan]} plan` + : "Free plan"}

{isPlanCanceling ? "Cancelling" - : hasPaidPlan - ? "Active" - : "Free"} + : isBrainTrialEnded + ? brainTrial.status === "exhausted" + ? "Credits used up" + : "Trial ended" + : isOnTrial + ? "Free trial" + : hasPaidPlan + ? "Active" + : "Free"}

{isPlanCanceling - ? `Cancels on ${cancelEndsLabel}${cancelEndsDays !== null ? ` · ${cancelEndsDays} day${cancelEndsDays !== 1 ? "s" : ""} left` : ""}. You'll move to Free after that.` - : hasPaidPlan - ? "Expanded memory, connections, and usage for this workspace." - : "Upgrade when you need more workspace usage and integrations."} + ? `Cancels on ${cancelEndsLabel}${cancelEndsDays !== null ? ` · ${cancelEndsDays} day${cancelEndsDays !== 1 ? "s" : ""} left` : ""}.${isCompanyBrain ? "" : " You'll move to Free after that."}` + : isOnTrial + ? [ + trialEndsLabel + ? `Ends ${trialEndsLabel}${trialDaysLeft != null ? ` · ${trialDaysLeft} day${trialDaysLeft !== 1 ? "s" : ""} left` : ""}` + : null, + trialCredits != null + ? `$${trialCredits} trial credits` + : null, + ] + .filter(Boolean) + .join(" · ") + : isBrainTrialEnded + ? "Trial ended. Activate Scale to restore access." + : hasPaidPlan + ? "Expanded memory, connections, and usage for this workspace." + : "Upgrade when you need more workspace usage and integrations."}

+ {isBrainTrialEnded ? ( +
+ +
+ ) : null}
@@ -1216,51 +1362,64 @@ export default function Billing() {
-
-
-

- Plan usage -

+ {showPlanUsage ? ( +
+
+

+ {isOnTrial ? "Trial credit usage" : "Plan usage"} +

+

+ {formatUsd(usdSpent)} + {usdIncluded > 0 ? ( + + {" "} + / {formatUsd(usdIncluded)} + + ) : null} + + {planUsagePct < 1 && planUsagePct > 0 + ? "< 1" + : Math.round(planUsagePct)} + % used + +

+
+
+
80 + ? "#C73B1B" + : "linear-gradient(90deg, #2368D2 0%, #4BA0FA 100%)", + }} + /> +

- {planUsagePct < 1 && planUsagePct > 0 - ? "< 1" - : Math.round(planUsagePct)} - % used + {isOnTrial + ? "Credits apply for the trial period" + : daysRemaining !== null + ? `Resets in ${daysRemaining} day${daysRemaining !== 1 ? "s" : ""}` + : "Usage resets with your billing cycle"}

-
-
80 - ? "#C73B1B" - : "linear-gradient(90deg, #2368D2 0%, #4BA0FA 100%)", - }} - /> -
-

- {daysRemaining !== null - ? `Resets in ${daysRemaining} day${daysRemaining !== 1 ? "s" : ""}` - : "Usage resets with your billing cycle"} -

-
+ ) : null}
@@ -1268,7 +1427,7 @@ export default function Billing() {
+ {isCompanyBrain ? ( +
+ {COMPANY_BRAIN_PLAN_CARDS.map((plan) => ( + + ))}
+ ) : ( + <> +
+
+
+ {PLAN_CARDS.map((plan) => ( + + ))} +
+
+ {ADVANCED_PLAN_CARDS.map((plan) => ( + + ))} +
+
+
+ {isPlanCarouselActive ? null : ( +
+ +
+ )} + )}
@@ -1567,16 +1752,16 @@ export default function Billing() { - {hasPaidPlan ? ( + {hasPaidPlan || isOnTrial ? (
Credits - -
+ {isOnTrial ? ( +

- Top-up credits + Trial credits

- {creditRemaining > 0 - ? `${formatUsd(creditRemaining)} available` - : "No top-up credits yet"} + {formatUsd(creditRemaining)} remaining + {usdIncluded > 0 ? ( + + {" "} + of {formatUsd(usdIncluded)} + + ) : trialCredits != null ? ( + + {" "} + of {formatUsd(trialCredits)} + + ) : null}

- Optional add-on that{" "} + Company Brain trials include{" "} - rolls over + ${trialCredits ?? 200} {" "} - month-to-month, separate from your monthly usage above. + in usage credits. Paid Scale includes $600/mo. Top-ups are + available after you activate.

- -
-
+ + ) : ( + +
+
+ +
+

+ Top-up credits +

+

+ {creditRemaining > 0 + ? `${formatUsd(creditRemaining)} available` + : "No top-up credits yet"} +

+

+ Optional add-on that{" "} + + rolls over + {" "} + month-to-month, separate from your monthly usage above. +

+
+
+ +
+
+ )}
) : null} diff --git a/apps/web/hooks/use-token-usage.ts b/apps/web/hooks/use-token-usage.ts index 4dc628110..31b4e9623 100644 --- a/apps/web/hooks/use-token-usage.ts +++ b/apps/web/hooks/use-token-usage.ts @@ -39,7 +39,8 @@ const TOKEN_METER_IDS = [ ] as const export function useTokenUsage(autumn: ReturnType) { - const status = getSubscriptionStatus(autumn.data?.subscriptions) + const subscriptions = autumn.data?.subscriptions + const status = getSubscriptionStatus(subscriptions) let currentPlan: PlanType = "free" if (isAllowedFrom(status, "api_enterprise")) { @@ -54,6 +55,24 @@ export function useTokenUsage(autumn: ReturnType) { const hasPaidPlan = currentPlan !== "free" + const planProductId = + currentPlan === "free" ? null : (`api_${currentPlan}` as const) + const currentSub = planProductId + ? subscriptions?.find((s) => s.planId === planProductId) + : undefined + const isTrialing = currentSub?.status === "trialing" + const trialEndsAtMs = (() => { + if (!currentSub) return null + const sub = currentSub as { + trialEndsAt?: number | null + currentPeriodEnd?: number | null + expiresAt?: number | null + } + const raw = sub.trialEndsAt ?? sub.currentPeriodEnd ?? sub.expiresAt + if (raw == null) return null + return raw < 10_000_000_000 ? raw * 1000 : raw + })() + const balances = autumn.data?.balances ?? {} const tokensUsed = TOKEN_METER_IDS.reduce((sum, id) => { @@ -88,6 +107,8 @@ export function useTokenUsage(autumn: ReturnType) { planUsagePct, currentPlan, hasPaidPlan, + isTrialing, + trialEndsAtMs, isLoading, daysRemaining, } diff --git a/apps/web/lib/billing-utils.ts b/apps/web/lib/billing-utils.ts index cc652768e..3360e9c41 100644 --- a/apps/web/lib/billing-utils.ts +++ b/apps/web/lib/billing-utils.ts @@ -112,6 +112,78 @@ export function getBrainMode( : null } +export type BrainTrialStatus = + | "active" + | "exhausted" + | "expired" + | "converted" + | "skipped" + +export type BrainTrialInfo = { + status: BrainTrialStatus | null + startedAtMs: number | null + endsAtMs: number | null + credits: number | null + daysRemaining: number | null +} + +function parseOrgMetadata( + metadataRaw: Record | string | null | undefined, +): Record | null { + if (!metadataRaw) return null + if (typeof metadataRaw === "string") { + try { + return JSON.parse(metadataRaw) as Record + } catch { + return null + } + } + return metadataRaw +} + +/** Company Brain Slack trial fields written by mono after OAuth attach. */ +export function getBrainTrialInfo( + metadataRaw: Record | string | null | undefined, +): BrainTrialInfo { + const metadata = parseOrgMetadata(metadataRaw) + const rawStatus = metadata?.brainTrialStatus + const status = + rawStatus === "active" || + rawStatus === "exhausted" || + rawStatus === "expired" || + rawStatus === "converted" || + rawStatus === "skipped" + ? rawStatus + : null + + const startedAtMs = + typeof metadata?.brainTrialStartedAt === "string" + ? Date.parse(metadata.brainTrialStartedAt) + : Number.NaN + const endsAtMs = + typeof metadata?.brainTrialEndsAt === "string" + ? Date.parse(metadata.brainTrialEndsAt) + : Number.NaN + const credits = + typeof metadata?.brainTrialCredits === "number" + ? metadata.brainTrialCredits + : null + + const safeEnds = Number.isFinite(endsAtMs) ? endsAtMs : null + const daysRemaining = + safeEnds != null + ? Math.max(0, Math.ceil((safeEnds - Date.now()) / (1000 * 60 * 60 * 24))) + : null + + return { + status, + startedAtMs: Number.isFinite(startedAtMs) ? startedAtMs : null, + endsAtMs: safeEnds, + credits, + daysRemaining, + } +} + /** * Format a number with K/M suffix for display * @example formatUsageNumber(1500000) => "1.5M" diff --git a/packages/lib/queries.ts b/packages/lib/queries.ts index 7879a6edc..fc234b0af 100644 --- a/packages/lib/queries.ts +++ b/packages/lib/queries.ts @@ -27,6 +27,10 @@ const DEFAULT_SUBSCRIPTION_STATUS: SubscriptionStatusMap = { api_enterprise: { allowed: false, status: null }, } +function isLiveSubscriptionStatus(status: string | null | undefined): boolean { + return status === "active" || status === "trialing" +} + export function isAllowedFrom( status: SubscriptionStatusMap, minimumTier: PlanTier, @@ -34,7 +38,7 @@ export function isAllowedFrom( const minIndex = PLAN_TIERS.indexOf(minimumTier) return PLAN_TIERS.slice(minIndex).some((tier) => { const s = status[tier] - return s?.status === "active" + return isLiveSubscriptionStatus(s?.status) }) } @@ -49,7 +53,7 @@ export function getSubscriptionStatus( for (const tier of PLAN_TIERS) { const sub = subMap.get(tier) statusMap[tier] = { - allowed: sub?.status === "active", + allowed: isLiveSubscriptionStatus(sub?.status), status: sub?.status ?? null, } }