"use client"
import { dmSans125ClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { useAuth } from "@lib/auth-context"
import {
useAccountMemberships,
useDeleteUserAccount,
} from "@/hooks/use-account-settings"
import { useOrgSummaries } from "@/hooks/use-org-summaries"
import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"
import {
PLAN_DISPLAY_NAMES,
PLAN_RANK,
useTokenUsage,
type PlanType,
} from "@/hooks/use-token-usage"
import {
Dialog,
DialogContent,
DialogTrigger,
DialogClose,
} from "@ui/components/dialog"
import { authClient } from "@lib/auth"
import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover"
import { useCustomer } from "autumn-js/react"
import {
Check,
X,
Trash2,
LoaderIcon,
Settings,
ChevronDown,
Building2,
} from "lucide-react"
import { useMemo, useState } from "react"
import { toast } from "sonner"
function SectionTitle({ children }: { children: React.ReactNode }) {
return (
{children}
)
}
function SettingsCard({ children }: { children: React.ReactNode }) {
return (
{children}
)
}
function PlanComparisonCard({
name,
price,
period,
description,
credits,
features,
highlight,
}: {
name: string
price: string
period: string
description: string
credits: string
features: string[]
highlight: boolean
}) {
return (
{name}
{highlight && (
RECOMMENDED
)}
{price}
{period && (
{period}
)}
{description}
{credits}
of usage included
{features.map((text) => (
-
{text}
))}
)
}
function formatOrgRole(role: string): string {
const r = role.toLowerCase()
if (r === "owner") return "Owner"
if (r === "admin") return "Admin"
if (r === "member") return "Member"
return role
? role.charAt(0).toUpperCase() + role.slice(1).toLowerCase()
: "Member"
}
/** Matches ACTIVE / RECOMMENDED pills in billing settings. */
const orgPlanBadgeBase = cn(
dmSans125ClassName(),
"inline-flex h-[18px] min-w-[42px] shrink-0 items-center justify-center rounded-[3px] px-1.5 text-[10px] uppercase",
)
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]",
scale: "bg-[#0054AD] font-bold tracking-[0.36px] text-[#FAFAFA]",
enterprise: "bg-[#FAFAFA] font-bold tracking-[0.36px] text-[#0D121A]",
}
function OrgPlanBadge({ plan }: { plan: PlanType }) {
return (
{PLAN_DISPLAY_NAMES[plan]}
)
}
function resolveOrgPlan(
orgId: string,
isCurrent: boolean,
currentPlan: PlanType,
planByOrgId: Map,
): PlanType {
const fromSummary = planByOrgId.get(orgId)
if (fromSummary) return fromSummary
if (isCurrent) return currentPlan
return "free"
}
export default function Account() {
const {
user,
org,
organizations: allOrgs,
setActiveOrg,
clearActiveOrg,
} = useAuth()
const autumn = useCustomer()
const [isUpgrading, setIsUpgrading] = useState(false)
const [isCancelling, setIsCancelling] = useState(false)
const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false)
const [emailConfirm, setEmailConfirm] = useState("")
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
const [isClosingAccount, setIsClosingAccount] = useState(false)
const [switchingOrgId, setSwitchingOrgId] = useState(null)
const [orgMenuOpen, setOrgMenuOpen] = useState(false)
const canSwitchOrg = (allOrgs?.length ?? 0) > 1
const { data: memberships, isPending: membershipsPending } =
useAccountMemberships()
const { data: orgSummaries } = useOrgSummaries()
const sortedMemberships = useMemo(() => {
if (!memberships?.length) return []
return [...memberships].sort((a, b) => a.name.localeCompare(b.name))
}, [memberships])
const ownedOrgs = useMemo(
() => memberships?.filter((m) => m.role === "owner") ?? [],
[memberships],
)
const hasOwnedOrgWithTeammates = useMemo(
() => ownedOrgs.some((m) => m.memberCount > 1),
[ownedOrgs],
)
const showMembershipsOverview =
!membershipsPending &&
(sortedMemberships.length > 1 || hasOwnedOrgWithTeammates)
const deleteUserAccount = useDeleteUserAccount()
const emailMatches = user?.email
? emailConfirm.trim().toLowerCase() === user.email.trim().toLowerCase()
: false
const handleOrgSwitch = async (orgSlug: string, orgId: string) => {
if (orgId === org?.id) return
setSwitchingOrgId(orgId)
try {
await setActiveOrg(orgSlug)
window.location.reload()
} catch (error) {
console.error("Failed to switch organization:", error)
setSwitchingOrgId(null)
}
}
const {
usdIncluded,
usdSpent,
planUsagePct,
currentPlan,
hasPaidPlan,
isLoading: isCheckingStatus,
daysRemaining,
} = useTokenUsage(autumn)
const formatUsd = (n: number) =>
n.toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
const planDisplayNames = PLAN_DISPLAY_NAMES
const planByOrgId = useMemo(() => {
const map = new Map()
for (const summary of orgSummaries ?? []) {
map.set(summary.orgId, summary.plan)
}
return map
}, [orgSummaries])
const sortedOrgsForMenu = useMemo(() => {
if (!allOrgs?.length) return []
return [...allOrgs].sort((a, b) => {
const planA = resolveOrgPlan(
a.id,
a.id === org?.id,
currentPlan,
planByOrgId,
)
const planB = resolveOrgPlan(
b.id,
b.id === org?.id,
currentPlan,
planByOrgId,
)
const rankDiff = PLAN_RANK[planB] - PLAN_RANK[planA]
if (rankDiff !== 0) return rankDiff
return a.name.localeCompare(b.name)
})
}, [allOrgs, org?.id, currentPlan, planByOrgId])
// Handlers
const handleUpgrade = async () => {
setIsUpgrading(true)
try {
const result = await autumn.attach({
planId: "api_pro",
successUrl: `${window.location.origin}/settings#account`,
})
if (result?.paymentUrl) {
window.open(result.paymentUrl, "_self")
return
}
autumn.refetch?.()
} catch (error) {
console.error(error)
toast.error("Failed to start checkout. Please try again.")
} finally {
setIsUpgrading(false)
}
}
// Enterprise is contract-based — direct those users to the portal/sales.
const cancellablePlanId =
currentPlan === "pro" || currentPlan === "scale"
? (`api_${currentPlan}` as const)
: null
const handleCancelSubscription = async () => {
if (!cancellablePlanId) return
setIsCancelling(true)
try {
await autumn.updateSubscription({
planId: cancellablePlanId,
cancelAction: "cancel_end_of_cycle",
})
autumn.refetch?.()
setIsCancelDialogOpen(false)
toast.success(
`Subscription cancelled. ${planDisplayNames[currentPlan]} features remain active until the end of your billing period.`,
)
} catch (error) {
console.error(error)
toast.error("Failed to cancel subscription. Please try again.")
} finally {
setIsCancelling(false)
}
}
const handleDeleteAccount = async () => {
if (!user?.email || !emailMatches || membershipsPending) return
setIsClosingAccount(true)
try {
await deleteUserAccount.mutateAsync({
confirmation: user.email,
})
clearActiveOrg()
try {
await authClient.signOut()
} catch {
window.location.assign("/login/new")
return
}
setIsDeleteDialogOpen(false)
setEmailConfirm("")
window.location.assign("/login/new")
} catch (e) {
const msg = e instanceof Error ? e.message : "Something went wrong"
toast.error(msg)
} finally {
setIsClosingAccount(false)
}
}
// Format member since date
const memberSince = user?.createdAt
? new Date(user.createdAt).toLocaleDateString("en-US", {
month: "short",
year: "numeric",
})
: "—"
return (
Profile Details
{/* Avatar + Name/Email */}
{user?.name?.charAt(0) ?? "U"}
{user?.name ?? "—"}
{user?.email ?? "—"}
Organization
{
if (canSwitchOrg) setOrgMenuOpen(open)
}}
>
{org?.name ?? "Personal"}
{canSwitchOrg && (
)}
{canSwitchOrg && (
{sortedOrgsForMenu.map((organization) => {
const isCurrent = organization.id === org?.id
const isSwitching = switchingOrgId === organization.id
const plan = resolveOrgPlan(
organization.id,
isCurrent,
currentPlan,
planByOrgId,
)
return (
)
})}
)}
Member since
{memberSince}
Billing & Subscription
{hasPaidPlan ? (
<>
{planDisplayNames[currentPlan]} plan
ACTIVE
Expanded memory with connections and more
{/* Plan usage (unified) */}
Plan usage
{planUsagePct < 1 && planUsagePct > 0
? "< 1"
: Math.round(planUsagePct)}
% used
80
? "#ef4444"
: "linear-gradient(to right, #4BA0FA 80%, #002757 100%)",
}}
title={`$${formatUsd(usdSpent)} of $${formatUsd(usdIncluded)} used`}
/>
{daysRemaining !== null
? `Resets in ${daysRemaining} day${daysRemaining !== 1 ? "s" : ""}`
: ""}
{cancellablePlanId && (
)}
>
) : (
<>
Free Plan
You are on basic plan
{/* Plan usage (unified) */}
Plan usage
{planUsagePct < 1 && planUsagePct > 0
? "< 1"
: Math.round(planUsagePct)}
% used
80 ? "#ef4444" : "#0054AD",
}}
title={`$${formatUsd(usdSpent)} of $${formatUsd(usdIncluded)} used`}
/>
{daysRemaining !== null
? `Resets in ${daysRemaining} day${daysRemaining !== 1 ? "s" : ""}`
: ""}
>
)}
Delete Account
Permanently delete all your data and cancel any active
subscriptions
)
}