diff --git a/apps/web/app/(app)/settings/page.tsx b/apps/web/app/(app)/settings/page.tsx
index 4d6719774..51745b80a 100644
--- a/apps/web/app/(app)/settings/page.tsx
+++ b/apps/web/app/(app)/settings/page.tsx
@@ -104,6 +104,7 @@ function parseHashToTab(hash: string): SettingsTab {
const ORG_PLAN_BADGE_STYLES: Record = {
free: "bg-[#2E353D] font-mono font-medium tracking-[0.12em] text-[#A3A3A3]",
pro: "bg-[#4BA0FA] font-bold tracking-[0.36px] text-[#00171A]",
+ max: "bg-[#1E7FE0] font-bold tracking-[0.36px] text-[#00171A]",
scale: "bg-[#0054AD] font-bold tracking-[0.36px] text-[#FAFAFA]",
enterprise: "bg-[#FAFAFA] font-bold tracking-[0.36px] text-[#0D121A]",
}
diff --git a/apps/web/components/org-plan-badge.tsx b/apps/web/components/org-plan-badge.tsx
index e5775e652..12fa1d199 100644
--- a/apps/web/components/org-plan-badge.tsx
+++ b/apps/web/components/org-plan-badge.tsx
@@ -10,6 +10,7 @@ const orgPlanBadgeBase = cn(
const ORG_PLAN_BADGE_STYLES: Record = {
free: "bg-[#2E353D] font-mono font-medium tracking-[0.12em] text-[#A3A3A3]",
pro: "bg-[#4BA0FA] font-bold tracking-[0.36px] text-[#00171A]",
+ max: "bg-[#1E7FE0] font-bold tracking-[0.36px] text-[#00171A]",
scale: "bg-[#0054AD] font-bold tracking-[0.36px] text-[#FAFAFA]",
enterprise: "bg-[#FAFAFA] font-bold tracking-[0.36px] text-[#0D121A]",
}
diff --git a/apps/web/components/settings/billing.tsx b/apps/web/components/settings/billing.tsx
index 6fe59994d..556874f87 100644
--- a/apps/web/components/settings/billing.tsx
+++ b/apps/web/components/settings/billing.tsx
@@ -4,6 +4,7 @@ import { dmSans125ClassName } from "@/lib/fonts"
import { PLAN_DISPLAY_NAMES, useTokenUsage } from "@/hooks/use-token-usage"
import { cn } from "@lib/utils"
import { useAuth } from "@lib/auth-context"
+import { getCanceledSubscription } from "@lib/queries"
import {
Dialog,
DialogClose,
@@ -24,7 +25,7 @@ import {
Settings,
X,
} from "lucide-react"
-import { useEffect, useMemo, useState } from "react"
+import { useEffect, useMemo, useRef, useState } from "react"
import { toast } from "sonner"
const API_BASE =
@@ -70,16 +71,17 @@ type AutoTopupsResponse =
| { ok: false; reason: string; message?: string }
type PlanCardDefinition = {
- id: "free" | "pro" | "scale" | "enterprise"
+ id: "free" | "pro" | "max" | "scale" | "enterprise"
name: string
price: string
period: string
credits: string
- productId: "api_free" | "api_pro" | "api_scale" | "api_enterprise"
+ productId: "api_free" | "api_pro" | "api_max" | "api_scale" | "api_enterprise"
description: string
includesFrom?: string
features: string[]
isContactSales?: boolean
+ mostPopular?: boolean
}
const PLAN_CARDS: PlanCardDefinition[] = [
@@ -114,6 +116,18 @@ const PLAN_CARDS: PlanCardDefinition[] = [
]
const ADVANCED_PLAN_CARDS: PlanCardDefinition[] = [
+ {
+ id: "max",
+ name: "Max",
+ price: "$100",
+ period: "/mo",
+ credits: "$130",
+ productId: "api_max",
+ description: "For power users who outgrow Pro",
+ includesFrom: "Pro",
+ mostPopular: true,
+ features: ["6× the credits of Pro", "Gmail connector", "Priority support"],
+ },
{
id: "scale",
name: "Scale",
@@ -122,10 +136,10 @@ const ADVANCED_PLAN_CARDS: PlanCardDefinition[] = [
credits: "$600",
productId: "api_scale",
description: "For teams and production workloads",
- includesFrom: "Pro",
+ includesFrom: "Max",
features: [
"Auto top-up & spend caps",
- "Gmail, S3 & Web Crawler connectors",
+ "S3 & Web Crawler connectors",
"Dedicated support",
],
},
@@ -150,8 +164,9 @@ const ADVANCED_PLAN_CARDS: PlanCardDefinition[] = [
const PLAN_RANK: Record = {
free: 0,
pro: 1,
- scale: 2,
- enterprise: 3,
+ max: 2,
+ scale: 3,
+ enterprise: 4,
}
function SectionTitle({
@@ -208,9 +223,16 @@ function PlanCard({
className={cn(
"relative flex min-h-[416px] flex-col overflow-hidden rounded-[14px] border p-5",
"shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]",
- "border-white/[0.08] bg-[#14161A]",
+ plan.mostPopular
+ ? "border-[#4BA0FA]/40 bg-[#14161A]"
+ : "border-white/[0.08] bg-[#14161A]",
)}
>
+ {plan.mostPopular ? (
+
+ Most popular
+
+ ) : null}
{plan.name}
@@ -396,6 +418,7 @@ function getInvoiceProductLabel(productId: string | undefined): string {
const planMap: Record = {
api_free: "Free",
api_pro: "Pro",
+ api_max: "Max",
api_scale: "Scale",
api_enterprise: "Enterprise",
memory_free: "Free",
@@ -416,7 +439,9 @@ export default function Billing() {
const autumn = useCustomer({ expand: ["payment_method"] })
const [isUpgrading, setIsUpgrading] = useState(false)
const [isCancelling, setIsCancelling] = useState(false)
+ const [isResuming, setIsResuming] = useState(false)
const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false)
+ const [cancelConfirmText, setCancelConfirmText] = useState("")
const [isCreditsDialogOpen, setIsCreditsDialogOpen] = useState(false)
const [isPlanCarouselActive, setIsPlanCarouselActive] = useState(false)
const [planPage, setPlanPage] = useState<0 | 1>(0)
@@ -446,6 +471,17 @@ export default function Billing() {
daysRemaining,
} = useTokenUsage(autumn)
+ // Open the carousel to the page holding the current plan (Max/Scale/Enterprise live on page 2).
+ const didAutoOpenPlanPage = useRef(false)
+ useEffect(() => {
+ if (didAutoOpenPlanPage.current || isCheckingStatus) return
+ didAutoOpenPlanPage.current = true
+ if (ADVANCED_PLAN_CARDS.some((p) => p.id === currentPlan)) {
+ setIsPlanCarouselActive(true)
+ setPlanPage(1)
+ }
+ }, [isCheckingStatus, currentPlan])
+
const balance = autumn.data?.balances?.[CREDIT_FEATURE_ID]
const creditRemaining =
balance?.remaining ?? Math.max(usdIncluded - usdSpent, 0)
@@ -517,7 +553,7 @@ export default function Billing() {
const planDisplayNames = PLAN_DISPLAY_NAMES
- const handleUpgrade = async (planId: "api_pro" | "api_scale") => {
+ const handleUpgrade = async (planId: "api_pro" | "api_max" | "api_scale") => {
setIsUpgrading(true)
try {
const result = await autumn.attach({
@@ -538,10 +574,59 @@ export default function Billing() {
}
const cancellablePlanId =
- currentPlan === "pro" || currentPlan === "scale"
+ currentPlan === "pro" || currentPlan === "max" || currentPlan === "scale"
? (`api_${currentPlan}` as const)
: null
+ const currentPlanCard = [...PLAN_CARDS, ...ADVANCED_PLAN_CARDS].find(
+ (p) => p.id === currentPlan,
+ )
+ const cancelLossItems = currentPlanCard
+ ? [
+ `${currentPlanCard.credits}/mo included credits`,
+ ...currentPlanCard.features.filter((f) => !/credit/i.test(f)),
+ ]
+ : []
+ const canConfirmCancel = cancelConfirmText.trim().toUpperCase() === "CANCEL"
+
+ const canceledSub = getCanceledSubscription(autumn.data?.subscriptions)
+ const isPlanCanceling = canceledSub != null
+ const cancelEndsAt =
+ canceledSub?.endsAt != null ? normalizeTimestamp(canceledSub.endsAt) : null
+ const cancelEndsLabel =
+ cancelEndsAt != null
+ ? new Date(cancelEndsAt).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ })
+ : "the end of your billing period"
+ const cancelEndsDays =
+ cancelEndsAt != null
+ ? Math.max(
+ 0,
+ Math.ceil((cancelEndsAt - Date.now()) / (1000 * 60 * 60 * 24)),
+ )
+ : null
+
+ const handleResumeSubscription = async () => {
+ if (!canceledSub || isResuming) return
+ setIsResuming(true)
+ try {
+ await autumn.updateSubscription({
+ planId: canceledSub.planId,
+ cancelAction: "uncancel",
+ })
+ autumn.refetch?.()
+ toast.success(`${planDisplayNames[currentPlan]} subscription resumed.`)
+ } catch (error) {
+ console.error(error)
+ toast.error("Failed to resume subscription. Please try again.")
+ } finally {
+ setIsResuming(false)
+ }
+ }
+
const handleCancelSubscription = async () => {
if (!cancellablePlanId) return
setIsCancelling(true)
@@ -552,6 +637,7 @@ export default function Billing() {
})
autumn.refetch?.()
setIsCancelDialogOpen(false)
+ setCancelConfirmText("")
toast.success(
`Subscription cancelled. ${planDisplayNames[currentPlan]} features remain active until the end of your billing period.`,
)
@@ -743,7 +829,9 @@ export default function Billing() {
}
const checkoutPlanId =
- plan.productId === "api_pro" || plan.productId === "api_scale"
+ plan.productId === "api_pro" ||
+ plan.productId === "api_max" ||
+ plan.productId === "api_scale"
? plan.productId
: null
if (!checkoutPlanId) return null
@@ -784,8 +872,20 @@ export default function Billing() {
? `${planDisplayNames[currentPlan]} plan`
: "Free plan"}
-
- {hasPaidPlan ? "Active" : "Free"}
+
+ {isPlanCanceling
+ ? "Cancelling"
+ : hasPaidPlan
+ ? "Active"
+ : "Free"}
- {hasPaidPlan
- ? "Expanded memory, connections, and usage for this workspace."
- : "Upgrade when you need more workspace usage and integrations."}
+ {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."}
@@ -812,10 +914,28 @@ export default function Billing() {
Manage
- {cancellablePlanId ? (
+ {isPlanCanceling ? (
+ void handleResumeSubscription()}
+ disabled={isResuming}
+ className={cn(
+ dmSans125ClassName(),
+ "inline-flex h-9 items-center gap-2 rounded-[9px] bg-[#0054AD] px-3 text-[13px] font-medium text-[#FAFAFA] transition-colors hover:bg-[#0B65C9] disabled:cursor-not-allowed disabled:opacity-50",
+ )}
+ >
+ {isResuming ? (
+
+ ) : null}
+ Resume plan
+
+ ) : cancellablePlanId ? (
{
+ setIsCancelDialogOpen(open)
+ if (!open) setCancelConfirmText("")
+ }}
>
+ {cancelLossItems.length > 0 ? (
+
+
+ You'll lose
+
+
+ {cancelLossItems.map((item) => (
+
+
+ {item}
+
+ ))}
+
+
+ ) : null}
+
+
+ Type{" "}
+
+ CANCEL
+ {" "}
+ to confirm
+
+ setCancelConfirmText(e.target.value)}
+ placeholder="CANCEL"
+ type="text"
+ value={cancelConfirmText}
+ />
+
void handleCancelSubscription()}
- disabled={isCancelling}
+ disabled={isCancelling || !canConfirmCancel}
className={cn(
dmSans125ClassName(),
"inline-flex h-9 items-center gap-2 rounded-[9px] bg-[#290F0A] px-3 text-[13px] font-medium text-[#C73B1B] transition-opacity disabled:cursor-not-allowed disabled:opacity-50",
@@ -966,7 +1128,7 @@ export default function Billing() {
onClick={() => setPlanPage(1)}
disabled={planPage === 1}
className="flex size-8 items-center justify-center rounded-full border border-white/[0.08] bg-white/[0.02] text-[#A3A3A3] transition-colors hover:bg-white/[0.05] hover:text-[#FAFAFA] disabled:cursor-not-allowed disabled:opacity-35"
- aria-label="Show Scale and Enterprise plans"
+ aria-label="Show Max, Scale and Enterprise plans"
>
@@ -993,7 +1155,7 @@ export default function Billing() {
/>
))}
-
+
{ADVANCED_PLAN_CARDS.map((plan) => (
{
resetOrgOnboarded()
diff --git a/apps/web/hooks/use-token-usage.ts b/apps/web/hooks/use-token-usage.ts
index 898d89ac5..4dc628110 100644
--- a/apps/web/hooks/use-token-usage.ts
+++ b/apps/web/hooks/use-token-usage.ts
@@ -2,11 +2,12 @@ import { getSubscriptionStatus, isAllowedFrom } from "@lib/queries"
import type { useCustomer } from "autumn-js/react"
import { calculateUsagePercent, getDaysRemaining } from "@/lib/billing-utils"
-export type PlanType = "free" | "pro" | "scale" | "enterprise"
+export type PlanType = "free" | "pro" | "max" | "scale" | "enterprise"
export const PLAN_DISPLAY_NAMES: Record = {
free: "Free",
pro: "Pro",
+ max: "Max",
scale: "Scale",
enterprise: "Enterprise",
}
@@ -15,8 +16,9 @@ export const PLAN_DISPLAY_NAMES: Record = {
export const PLAN_RANK: Record = {
free: 0,
pro: 1,
- scale: 2,
- enterprise: 3,
+ max: 2,
+ scale: 3,
+ enterprise: 4,
}
export function normalizePlanType(raw: unknown): PlanType {
@@ -24,6 +26,7 @@ export function normalizePlanType(raw: unknown): PlanType {
const normalized = raw.toLowerCase().replace(/^api_/, "")
if (normalized === "enterprise") return "enterprise"
if (normalized === "scale") return "scale"
+ if (normalized === "max") return "max"
if (normalized === "pro") return "pro"
return "free"
}
@@ -43,6 +46,8 @@ export function useTokenUsage(autumn: ReturnType) {
currentPlan = "enterprise"
} else if (isAllowedFrom(status, "api_scale")) {
currentPlan = "scale"
+ } else if (isAllowedFrom(status, "api_max")) {
+ currentPlan = "max"
} else if (isAllowedFrom(status, "api_pro")) {
currentPlan = "pro"
}
diff --git a/packages/lib/queries.ts b/packages/lib/queries.ts
index eb50d0f11..7879a6edc 100644
--- a/packages/lib/queries.ts
+++ b/packages/lib/queries.ts
@@ -7,7 +7,12 @@ import { $fetch } from "./api"
type DocumentsResponse = z.infer
type DocumentWithMemories = DocumentsResponse["documents"][0]
-export const PLAN_TIERS = ["api_pro", "api_scale", "api_enterprise"] as const
+export const PLAN_TIERS = [
+ "api_pro",
+ "api_max",
+ "api_scale",
+ "api_enterprise",
+] as const
export type PlanTier = (typeof PLAN_TIERS)[number]
export type SubscriptionStatusMap = Record<
@@ -17,6 +22,7 @@ export type SubscriptionStatusMap = Record<
const DEFAULT_SUBSCRIPTION_STATUS: SubscriptionStatusMap = {
api_pro: { allowed: false, status: null },
+ api_max: { allowed: false, status: null },
api_scale: { allowed: false, status: null },
api_enterprise: { allowed: false, status: null },
}
@@ -57,6 +63,33 @@ export function hasActivePlan(
return isAllowedFrom(getSubscriptionStatus(subscriptions), minimumTier)
}
+export type CanceledSubscription = { planId: string; endsAt: number | null }
+
+// A subscription scheduled to cancel at period end: still active, but canceledAt is set.
+export function getCanceledSubscription(
+ subscriptions:
+ | Array<{
+ planId: string
+ status?: string
+ canceledAt?: number | null
+ currentPeriodEnd?: number | null
+ expiresAt?: number | null
+ }>
+ | undefined,
+): CanceledSubscription | null {
+ const sub = subscriptions?.find(
+ (s) =>
+ s.status === "active" &&
+ s.canceledAt != null &&
+ (PLAN_TIERS as readonly string[]).includes(s.planId),
+ )
+ if (!sub) return null
+ return {
+ planId: sub.planId,
+ endsAt: sub.currentPeriodEnd ?? sub.expiresAt ?? null,
+ }
+}
+
export const useDeleteDocument = (selectedProject: string) => {
const queryClient = useQueryClient()