From 41a90016601036203023f505ebf6ec7061eb5aee Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommu Date: Wed, 20 May 2026 02:26:10 -0700 Subject: [PATCH] feat(web): redesign settings layout + split billing into its own tab (#980) --- apps/web/app/(app)/settings/page.tsx | 614 ++++++---- apps/web/components/settings/account.tsx | 1017 +++-------------- apps/web/components/settings/billing.tsx | 579 ++++++++++ .../components/settings/connections-mcp.tsx | 2 +- apps/web/components/settings/integrations.tsx | 2 +- apps/web/components/settings/support.tsx | 2 +- 6 files changed, 1082 insertions(+), 1134 deletions(-) create mode 100644 apps/web/components/settings/billing.tsx diff --git a/apps/web/app/(app)/settings/page.tsx b/apps/web/app/(app)/settings/page.tsx index ddf8a524a..f5d8c333a 100644 --- a/apps/web/app/(app)/settings/page.tsx +++ b/apps/web/app/(app)/settings/page.tsx @@ -2,12 +2,12 @@ import { Logo } from "@ui/assets/Logo" import { UserProfileMenu } from "@/components/user-profile-menu" import { useAuth } from "@lib/auth-context" -import { motion } from "motion/react" import NovaOrb from "@/components/nova/nova-orb" -import { useState, useEffect, useRef } from "react" +import { useState, useEffect, useRef, useMemo } from "react" import { cn } from "@lib/utils" import { dmSansClassName, dmSans125ClassName } from "@/lib/fonts" import Account from "@/components/settings/account" +import Billing from "@/components/settings/billing" import Integrations from "@/components/settings/integrations" import ConnectionsMCP from "@/components/settings/connections-mcp" import Support from "@/components/settings/support" @@ -16,13 +16,42 @@ import { useRouter } from "next/navigation" import { useIsMobile } from "@hooks/use-mobile" import { useLocalStorageUsername } from "@hooks/use-local-storage-username" import { analytics } from "@/lib/analytics" -import { LogOut, RotateCcw, Trash2, Sun, LoaderIcon } from "lucide-react" +import { + LogOut, + RotateCcw, + Trash2, + Sun, + LoaderIcon, + User as UserIcon, + Zap, + HelpCircle, + CreditCard, + ShieldAlert, + ChevronRight, + ChevronsUpDown, + Check, + Building2, +} from "lucide-react" import { authClient } from "@lib/auth" import { Dialog, DialogContent, DialogClose } from "@ui/components/dialog" +import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover" import { useResetOrganization } from "@/hooks/use-reset-organization" import { useDeleteUserAccount } from "@/hooks/use-account-settings" +import { useCustomer } from "autumn-js/react" +import { useOrgSummaries } from "@/hooks/use-org-summaries" +import { + PLAN_DISPLAY_NAMES, + useTokenUsage, + type PlanType, +} from "@/hooks/use-token-usage" -const TABS = ["account", "integrations", "connections", "support"] as const +const TABS = [ + "account", + "billing", + "integrations", + "connections", + "support", +] as const type SettingsTab = (typeof TABS)[number] type NavItem = { @@ -32,134 +61,39 @@ type NavItem = { icon: React.ReactNode } -type DangerItem = { - id: "logout" | "reset" | "delete" - label: string - description: string - icon: React.ReactNode - color: "neutral" | "amber" | "red" -} - const NAV_ITEMS: NavItem[] = [ { id: "account", - label: "Account & Billing", - description: "Manage your profile, plan, usage and payments", - icon: ( - - ), + label: "Account", + description: "Your profile and organization", + icon: , + }, + { + id: "billing", + label: "Billing", + description: "Plan, usage and payments", + icon: , }, { id: "integrations", label: "Integrations", - description: "Save, sync and search memories across tools", - icon: , + description: "Save, sync and search across tools", + icon: , }, { id: "connections", label: "Connections & MCP", - description: "Sync with Google Drive, Notion, OneDrive and MCP client", - icon: ( - - ), + description: "Drive, Notion, OneDrive, MCP", + icon: , }, { id: "support", label: "Support & Help", - description: "Find answers or share feedback. We're here to help.", - icon: ( - - ), - }, -] - -const DANGER_ITEMS: DangerItem[] = [ - { - id: "logout", - label: "Log out", - description: "Sign out of your account on this device", - icon: , - color: "neutral", - }, - { - id: "reset", - label: "Reset data", - description: "Erase all memories, connections and spaces", - icon: , - color: "amber", - }, - { - id: "delete", - label: "Delete account", - description: "Permanently delete your account and all data", - icon: , - color: "red", + description: "Get help or share feedback", + icon: , }, ] -const DANGER_COLORS: Record< - DangerItem["color"], - { idle: string; hover: string; icon: string } -> = { - neutral: { - idle: "text-white/50", - hover: "hover:text-white", - icon: "text-white/40", - }, - amber: { - idle: "text-[#7A6030]", - hover: "hover:text-[#C7991B]", - icon: "text-[#7A6030]", - }, - red: { - idle: "text-[#6B2A2A]", - hover: "hover:text-[#C73B1B]", - icon: "text-[#6B2A2A]", - }, -} - function parseHashToTab(hash: string): SettingsTab { const cleaned = hash.replace("#", "").toLowerCase() return TABS.includes(cleaned as SettingsTab) @@ -167,30 +101,75 @@ function parseHashToTab(hash: string): SettingsTab { : "account" } -export function UserSupermemory({ name }: { name: string }) { +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 ( - - -
-

- {name.split(" ")[0]}'s -

-

- supermemory -

+ {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" +} + +function SectionLabel({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ) +} + +function IdentityCard({ displayName }: { displayName: string }) { + const firstName = displayName?.split(" ")[0] || "" + + return ( +
+ +
+ +
+

+ {firstName ? `${firstName}'s` : "Your"} +

+

+ supermemory +

+
- +
) } export default function SettingsPage() { - const { user, org } = useAuth() + const { user, org, organizations, setActiveOrg } = useAuth() const [activeTab, setActiveTab] = useState("account") const hasInitialized = useRef(false) const router = useRouter() @@ -205,6 +184,40 @@ export default function SettingsPage() { const [deleteEmailConfirm, setDeleteEmailConfirm] = useState("") const deleteUserAccount = useDeleteUserAccount() + const [dangerMenuOpen, setDangerMenuOpen] = useState(false) + const [orgSwitcherOpen, setOrgSwitcherOpen] = useState(false) + const [switchingOrgId, setSwitchingOrgId] = useState(null) + const canSwitchOrg = (organizations?.length ?? 0) > 1 + + const autumn = useCustomer() + const { currentPlan } = useTokenUsage(autumn) + const { data: orgSummaries } = useOrgSummaries() + const planByOrgId = useMemo(() => { + const map = new Map() + for (const summary of orgSummaries ?? []) { + map.set(summary.orgId, summary.plan) + } + return map + }, [orgSummaries]) + const activeOrgPlan = org?.id + ? resolveOrgPlan(org.id, true, currentPlan, planByOrgId) + : currentPlan + + const handleOrgSwitch = async (orgSlug: string, orgId: string) => { + if (orgId === org?.id) { + setOrgSwitcherOpen(false) + return + } + setSwitchingOrgId(orgId) + try { + await setActiveOrg(orgSlug) + window.location.reload() + } catch (error) { + console.error("Failed to switch organization:", error) + setSwitchingOrgId(null) + } + } + const handleLogout = async () => { await authClient.signOut() router.push("/login") @@ -233,7 +246,6 @@ export default function SettingsPage() { setActiveTab(tab) analytics.settingsTabChanged({ tab }) - // If no hash or invalid hash, push #account if (!hash || !TABS.includes(hash.replace("#", "") as SettingsTab)) { window.history.pushState(null, "", "#account") } @@ -256,13 +268,10 @@ export default function SettingsPage() { user?.name || user?.email?.split("@")[0] || "" - const headerPossessive = headerDisplayName - ? `${headerDisplayName.split(" ")[0]}'s` - : "Your" return ( -
-
+
+
- +
+ {!isMobile && ( + { + if (canSwitchOrg) setOrgSwitcherOpen(open) + }} + > + + + + + {[...(organizations ?? [])] + .sort((a, b) => a.name.localeCompare(b.name)) + .map((organization) => { + const isCurrent = organization.id === org?.id + const isSwitching = switchingOrgId === organization.id + const plan = resolveOrgPlan( + organization.id, + isCurrent, + currentPlan, + planByOrgId, + ) + return ( + + ) + })} + + + )} + +
+
-
-
+
+ {/* Left rail */} +
-
+ + + {/* Content */} +
{activeTab === "account" && } + {activeTab === "billing" && } {activeTab === "integrations" && } {activeTab === "connections" && } {activeTab === "support" && } -
+
diff --git a/apps/web/components/settings/account.tsx b/apps/web/components/settings/account.tsx index 92fa80936..57d1264b1 100644 --- a/apps/web/components/settings/account.tsx +++ b/apps/web/components/settings/account.tsx @@ -3,10 +3,6 @@ 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 { @@ -15,26 +11,10 @@ import { 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 { Check, LoaderIcon, ChevronDown, Building2, Users } from "lucide-react" import { useMemo, useState } from "react" -import { toast } from "sonner" function SectionTitle({ children }: { children: React.ReactNode }) { return ( @@ -62,137 +42,6 @@ function SettingsCard({ children }: { children: React.ReactNode }) { ) } -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(), @@ -214,6 +63,36 @@ function OrgPlanBadge({ plan }: { plan: PlanType }) { ) } +const ROLE_LABELS: Record = { + owner: "Owner", + admin: "Admin", + member: "Member", +} + +function formatRole(role: string): string { + const r = role?.toLowerCase() ?? "" + if (ROLE_LABELS[r]) return ROLE_LABELS[r] + return r ? r.charAt(0).toUpperCase() + r.slice(1) : "Member" +} + +function RolePill({ role }: { role: string }) { + const r = role?.toLowerCase() ?? "" + const isOwner = r === "owner" + return ( + + {formatRole(role)} + + ) +} + function resolveOrgPlan( orgId: string, isCurrent: boolean, @@ -227,52 +106,13 @@ function resolveOrgPlan( } export default function Account() { - const { - user, - org, - organizations: allOrgs, - setActiveOrg, - clearActiveOrg, - } = useAuth() + const { user, org, organizations: allOrgs, setActiveOrg } = 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) @@ -285,23 +125,7 @@ export default function Account() { } } - 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 { currentPlan } = useTokenUsage(autumn) const planByOrgId = useMemo(() => { const map = new Map() @@ -332,80 +156,6 @@ export default function Account() { }) }, [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", @@ -414,7 +164,7 @@ export default function Account() { : "—" return ( -
+
Profile Details @@ -567,628 +317,117 @@ export default function Account() {
-
- Billing & Subscription +
+
+ Team members + {(org?.members?.length ?? 0) > 0 && ( + + {org?.members?.length}{" "} + {org?.members?.length === 1 ? "member" : "members"} + + )} +
-
- {hasPaidPlan ? ( - <> -
-
-

- {planDisplayNames[currentPlan]} plan -

- - ACTIVE - -
-

- Expanded memory with connections and more -

-
- - {/* Plan usage (unified) */} -
-
-

0 ? ( +

    + {[...org.members] + .sort((a, b) => { + const rolePriority = (r: string) => + r === "owner" ? 0 : r === "admin" ? 1 : 2 + const diff = + rolePriority(a.role.toLowerCase()) - + rolePriority(b.role.toLowerCase()) + if (diff !== 0) return diff + return (a.user?.name ?? "").localeCompare(b.user?.name ?? "") + }) + .map((m, idx) => { + const isYou = m.userId === user?.id + const name = m.user?.name ?? m.user?.email ?? "Unknown" + return ( +
  • 0 && "border-t border-white/[0.04]", )} > - 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 && ( - - - - - -
-
-
-

- Cancel {planDisplayNames[currentPlan]}{" "} - subscription? -

-

- You'll keep Pro features until the end of - your current billing period - {daysRemaining !== null - ? ` (${daysRemaining} day${daysRemaining !== 1 ? "s" : ""} remaining)` - : ""} - . After that, your account will switch to the - Free plan. -

-
- - - -
- -
- - - - -
+ You + + )}
-
- -
- )} -
- - ) : ( - <> -
-

- 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" : ""}` - : ""} -

-
- -
+ + + ) + })} + + ) : ( +
+
+ +
+
+ - {isUpgrading || isCheckingStatus || autumn.isLoading ? ( - <> - - Upgrading… - - ) : ( - "Upgrade to Pro - $19/month" - )} - {/* Inset blue stroke */} -
- - -
- - -
- - )} -
- -
- -
- Delete Account - -
-

- Permanently delete all your data and cancel any active - subscriptions -

- { - setIsDeleteDialogOpen(open) - if (!open) { - setEmailConfirm("") - } - }} - > - - - - -
- {/* Header */} -
-
-
-

- Delete account? -

-

- This cannot be undone. -

- {hasOwnedOrgWithTeammates && ( -

- You own at least one organization that still has - other members. Those organizations will be deleted - for everyone when you confirm. -

- )} -
- - What happens next? - - -
-

- Your account is locked immediately; data removal - runs in the background. -

-
    -
  • - Removes memories, conversations, and settings; - cancels active subscriptions. -
  • -
  • - Orgs where you're only a member: - you're removed; the org continues. -
  • -
  • Orgs you own: deleted for all members.
  • -
-
-
-
- - - -
- - {showMembershipsOverview && ( -
-

- Your organizations -

-
- {sortedMemberships.map((m) => ( -
-
-

- {m.name} -

- {m.slug ? ( -

- {m.slug} -

- ) : null} -
-
- - {formatOrgRole(m.role)} - - - {m.memberCount} member - {m.memberCount === 1 ? "" : "s"} - -
-
- ))} -
-
- )} - - {/* Confirmation input */} -
-

- Type your account email to confirm: -

-
- setEmailConfirm(e.target.value)} - placeholder={user?.email ?? "you@example.com"} - className={cn( - "w-full px-4 py-3 bg-transparent", - "text-[#FAFAFA] placeholder:text-[#737373]", - "text-[14px] tracking-[-0.14px]", - "outline-none", - dmSans125ClassName(), - )} - /> -
-
-
-
- - {/* Footer */} -
- - - - -
-
- {/* Modal inset highlight */} -
- -
-
+ Invite teammates from your organization settings. + +
+
+ )}
diff --git a/apps/web/components/settings/billing.tsx b/apps/web/components/settings/billing.tsx new file mode 100644 index 000000000..aca23307d --- /dev/null +++ b/apps/web/components/settings/billing.tsx @@ -0,0 +1,579 @@ +"use client" + +import { dmSans125ClassName } from "@/lib/fonts" +import { cn } from "@lib/utils" +import { PLAN_DISPLAY_NAMES, useTokenUsage } from "@/hooks/use-token-usage" +import { + Dialog, + DialogContent, + DialogTrigger, + DialogClose, +} from "@ui/components/dialog" +import { useCustomer } from "autumn-js/react" +import { Check, X, LoaderIcon, Settings } from "lucide-react" +import { 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} +
  • + ))} +
+
+ ) +} + +export default function Billing() { + const autumn = useCustomer() + const [isUpgrading, setIsUpgrading] = useState(false) + const [isCancelling, setIsCancelling] = useState(false) + const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false) + + 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 handleUpgrade = async () => { + setIsUpgrading(true) + try { + const result = await autumn.attach({ + planId: "api_pro", + successUrl: `${window.location.origin}/settings#billing`, + }) + 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) + } + } + + 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) + } + } + + return ( +
+
+ Billing & Subscription + +
+ {hasPaidPlan ? ( + <> +
+
+

+ {planDisplayNames[currentPlan]} plan +

+ + ACTIVE + +
+

+ Expanded memory with connections and more +

+
+ +
+
+

+ 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 && ( + + + + + +
+
+
+

+ Cancel {planDisplayNames[currentPlan]}{" "} + subscription? +

+

+ You'll keep Pro features until the end of + your current billing period + {daysRemaining !== null + ? ` (${daysRemaining} day${daysRemaining !== 1 ? "s" : ""} remaining)` + : ""} + . After that, your account will switch to the + Free plan. +

+
+ + + +
+ +
+ + + + +
+
+
+ +
+ )} +
+ + ) : ( + <> +
+

+ Free Plan +

+

+ You are on basic plan +

+
+ +
+
+

+ 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" : ""}` + : ""} +

+
+ + + +
+ + +
+ + )} +
+ +
+
+ ) +} diff --git a/apps/web/components/settings/connections-mcp.tsx b/apps/web/components/settings/connections-mcp.tsx index 06603e9d6..3f1497534 100644 --- a/apps/web/components/settings/connections-mcp.tsx +++ b/apps/web/components/settings/connections-mcp.tsx @@ -559,7 +559,7 @@ export default function ConnectionsMCP() { const isLoading = autumn.isLoading return ( -
+
{/* Supermemory Connections Section */}
}> diff --git a/apps/web/components/settings/integrations.tsx b/apps/web/components/settings/integrations.tsx index de72ad162..125cec90a 100644 --- a/apps/web/components/settings/integrations.tsx +++ b/apps/web/components/settings/integrations.tsx @@ -259,7 +259,7 @@ export default function Integrations() { } return ( -
+
Integrations diff --git a/apps/web/components/settings/support.tsx b/apps/web/components/settings/support.tsx index f5f7508d3..921759ef4 100644 --- a/apps/web/components/settings/support.tsx +++ b/apps/web/components/settings/support.tsx @@ -103,7 +103,7 @@ export default function Support() { } return ( -
+
{/* Support & Help Section */}
Support & Help