From 7faecfff36d605a539767ba501c7b2fdb92e4fa7 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Sat, 23 May 2026 13:47:15 +0530 Subject: [PATCH 1/8] Implemented the billing additions --- apps/web/components/settings/billing.tsx | 1136 ++++++++++++++-------- apps/web/lib/analytics.ts | 2 +- 2 files changed, 718 insertions(+), 420 deletions(-) diff --git a/apps/web/components/settings/billing.tsx b/apps/web/components/settings/billing.tsx index aca23307d..fcf450bc1 100644 --- a/apps/web/components/settings/billing.tsx +++ b/apps/web/components/settings/billing.tsx @@ -1,38 +1,97 @@ "use client" import { dmSans125ClassName } from "@/lib/fonts" -import { cn } from "@lib/utils" +import { calculateUsagePercent } from "@/lib/billing-utils" import { PLAN_DISPLAY_NAMES, useTokenUsage } from "@/hooks/use-token-usage" +import { cn } from "@lib/utils" import { Dialog, + DialogClose, 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 { useQueryClient } from "@tanstack/react-query" +import { useCustomer, useListPlans } from "autumn-js/react" +import { + Check, + CreditCard, + ExternalLink, + LoaderIcon, + ReceiptText, + Settings, + X, + Zap, +} from "lucide-react" +import { useEffect, useMemo, useState } from "react" import { toast } from "sonner" -function SectionTitle({ children }: { children: React.ReactNode }) { +const API_BASE = + process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" + +const CREDIT_FEATURE_ID = "usd_credits" +const FALLBACK_TOP_UP_PLAN_ID = "api_topup" +const TOP_UP_AMOUNTS = [10, 25, 50] as const +const AUTO_TOP_UP_THRESHOLDS = [2, 5, 10] as const +const AUTO_TOP_UP_AMOUNTS = [10, 25, 50] as const + +type BillingInvoice = { + planIds?: string[] + stripeId: string + status: string + total: number + currency: string + createdAt: number + hostedInvoiceUrl?: string | null +} + +type BillingAutoTopup = { + featureId: string + enabled: boolean + threshold: number + quantity: number + invoiceMode?: boolean + purchaseLimit?: { + interval: "hour" | "day" | "week" | "month" + intervalCount?: number + limit: number + } +} + +function SectionTitle({ + children, + aside, +}: { + children: React.ReactNode + aside?: React.ReactNode +}) { return ( -

- {children} -

+
+

+ {children} +

+ {aside} +
) } -function SettingsCard({ children }: { children: React.ReactNode }) { +function SettingsCard({ + children, + className, +}: { + children: React.ReactNode + className?: string +}) { return (
{children} @@ -40,132 +99,147 @@ function SettingsCard({ children }: { children: React.ReactNode }) { ) } -function PlanComparisonCard({ - name, - price, - period, - description, - credits, - features, - highlight, +function Pill({ + children, + tone = "muted", }: { - name: string - price: string - period: string - description: string - credits: string - features: string[] - highlight: boolean + children: React.ReactNode + tone?: "active" | "muted" | "warning" }) { return ( -
-
-

- {name} -

- {highlight && ( - - RECOMMENDED - - )} -
+ {children} + + ) +} -
- void + disabled?: boolean +}) { + return ( +
+ {values.map((item) => ( +
- -

- {description} -

- -
-
-

- {credits} -

-

- of usage included -

-
-
- -
    - {features.map((text) => ( -
  • - - {text} -
  • - ))} -
+ {prefix} + {item} + + ))}
) } +function formatUsd(value: number) { + return value.toLocaleString(undefined, { + style: "currency", + currency: "USD", + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }) +} + +function formatInvoiceAmount(total: number, currency: string) { + const normalizedTotal = + Number.isInteger(total) && total > 100 ? total / 100 : total + return normalizedTotal.toLocaleString(undefined, { + style: "currency", + currency: currency?.toUpperCase() || "USD", + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }) +} + +function formatDate(timestamp: number) { + return new Intl.DateTimeFormat(undefined, { + month: "short", + day: "numeric", + year: "numeric", + }).format(new Date(timestamp)) +} + +function normalizeTimestamp(timestamp: number) { + return timestamp < 10_000_000_000 ? timestamp * 1000 : timestamp +} + +function getStatusTone(status: string): "active" | "muted" | "warning" { + const normalized = status.toLowerCase() + if (normalized === "paid" || normalized === "succeeded") return "active" + if (normalized === "open" || normalized === "draft") return "muted" + return "warning" +} + +function findTopUpPlanId( + plans: Array<{ + id: string + name?: string + description?: string | null + addOn?: boolean + }>, +) { + const knownPlan = [ + "api_topup", + "api_top_up", + "api_credit_topup", + "api_credits_topup", + "api_usage_topup", + ].find((id) => plans.some((plan) => plan.id === id)) + + if (knownPlan) return knownPlan + + const discovered = plans.find((plan) => { + const label = `${plan.id} ${plan.name ?? ""} ${plan.description ?? ""}` + return plan.addOn && /top.?up|credit|usage/i.test(label) + }) + + return discovered?.id ?? FALLBACK_TOP_UP_PLAN_ID +} + export default function Billing() { - const autumn = useCustomer() + const queryClient = useQueryClient() + const autumn = useCustomer({ expand: ["invoices", "payment_method"] }) + const plansQuery = useListPlans({ + queryOptions: { staleTime: 5 * 60 * 1000 }, + }) const [isUpgrading, setIsUpgrading] = useState(false) const [isCancelling, setIsCancelling] = useState(false) const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false) + const [topUpAmount, setTopUpAmount] = useState(25) + const [topUpPendingAmount, setTopUpPendingAmount] = useState( + null, + ) + const [autoTopUpEnabled, setAutoTopUpEnabled] = useState(false) + const [autoTopUpThreshold, setAutoTopUpThreshold] = useState(5) + const [autoTopUpAmount, setAutoTopUpAmount] = useState(25) + const [isSavingAutoTopUp, setIsSavingAutoTopUp] = useState(false) const { usdIncluded, @@ -177,25 +251,49 @@ export default function Billing() { daysRemaining, } = useTokenUsage(autumn) - const formatUsd = (n: number) => - n.toLocaleString(undefined, { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }) + const balance = autumn.data?.balances?.[CREDIT_FEATURE_ID] + const creditRemaining = + balance?.remaining ?? Math.max(usdIncluded - usdSpent, 0) + const creditGranted = balance?.granted ?? usdIncluded + const creditUsagePct = creditGranted + ? calculateUsagePercent(creditGranted - creditRemaining, creditGranted) + : planUsagePct + + const invoices = useMemo(() => { + return ([...(autumn.data?.invoices ?? [])] as BillingInvoice[]) + .sort((a, b) => b.createdAt - a.createdAt) + .slice(0, 8) + }, [autumn.data?.invoices]) + + const topUpPlanId = useMemo( + () => findTopUpPlanId(plansQuery.data ?? []), + [plansQuery.data], + ) + + const activeAutoTopUp = useMemo(() => { + const autoTopups = (autumn.data?.billingControls?.autoTopups ?? + []) as BillingAutoTopup[] + return autoTopups.find( + (item: BillingAutoTopup) => item.featureId === CREDIT_FEATURE_ID, + ) + }, [autumn.data?.billingControls?.autoTopups]) + + useEffect(() => { + if (!activeAutoTopUp) return + setAutoTopUpEnabled(activeAutoTopUp.enabled) + setAutoTopUpThreshold(activeAutoTopUp.threshold) + setAutoTopUpAmount(activeAutoTopUp.quantity) + }, [activeAutoTopUp]) const planDisplayNames = PLAN_DISPLAY_NAMES const handleUpgrade = async () => { setIsUpgrading(true) try { - const result = await autumn.attach({ + 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) @@ -231,347 +329,547 @@ export default function Billing() { } } + const handleTopUp = async (amount: number) => { + setTopUpPendingAmount(amount) + try { + await autumn.attach({ + planId: topUpPlanId, + featureQuantities: [{ featureId: CREDIT_FEATURE_ID, quantity: amount }], + successUrl: `${window.location.origin}/settings#billing`, + metadata: { + source: "nova_billing_topup", + amount: String(amount), + }, + }) + autumn.refetch?.() + toast.success(`${formatUsd(amount)} credit top-up added.`) + } catch (error) { + console.error(error) + toast.error("Failed to start top-up checkout. Please try again.") + } finally { + setTopUpPendingAmount(null) + } + } + + const handleSaveAutoTopUp = async () => { + setIsSavingAutoTopUp(true) + const existingAutoTopups = (autumn.data?.billingControls?.autoTopups ?? + []) as BillingAutoTopup[] + const nextAutoTopups = [ + ...existingAutoTopups.filter( + (item: BillingAutoTopup) => item.featureId !== CREDIT_FEATURE_ID, + ), + { + featureId: CREDIT_FEATURE_ID, + enabled: autoTopUpEnabled, + threshold: autoTopUpThreshold, + quantity: autoTopUpAmount, + purchaseLimit: { + interval: "month", + intervalCount: 1, + limit: 10, + }, + }, + ] + + try { + const response = await fetch(`${API_BASE}/api/autumn/updateCustomer`, { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-App-Source": "nova", + }, + body: JSON.stringify({ + billingControls: { + ...autumn.data?.billingControls, + autoTopups: nextAutoTopups, + }, + }), + }) + + if (!response.ok) { + const body = (await response.json().catch(() => ({}))) as { + message?: string + } + throw new Error(body.message ?? "Failed to update auto top-up") + } + + await queryClient.invalidateQueries({ queryKey: ["autumn"] }) + autumn.refetch?.() + toast.success( + autoTopUpEnabled ? "Auto top-up updated." : "Auto top-up disabled.", + ) + } catch (error) { + console.error(error) + toast.error( + error instanceof Error + ? error.message + : "Failed to update auto top-up.", + ) + } finally { + setIsSavingAutoTopUp(false) + } + } + + const handleManageBilling = () => { + autumn.openCustomerPortal?.({ + returnUrl: `${window.location.origin}/settings#billing`, + }) + } + 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" : ""}` - : ""} + {hasPaidPlan + ? `${planDisplayNames[currentPlan]} plan` + : "Free plan"}

+ + {hasPaidPlan ? "Active" : "Free"} +
+

+ {hasPaidPlan + ? "Expanded memory, connections, and usage for this workspace." + : "Upgrade when you need more workspace usage and integrations."} +

+
-
- + {cancellablePlanId ? ( + - - Manage billing -
- - {cancellablePlanId && ( - + + + - +
+
+

+ Cancel {planDisplayNames[currentPlan]}? +

+

+ You keep paid features until the current billing + period ends + {daysRemaining !== null + ? ` (${daysRemaining} day${daysRemaining !== 1 ? "s" : ""} remaining)` + : ""} + . +

+
+ + + +
+
+ + + - - -
-
-
-

- 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. -

-
- - - -
- -
- - - - -
-
-
- -
+
+ +
+ ) : null} +
+
+ +
+
+

- + > + Plan usage +

+

+ {planUsagePct < 1 && planUsagePct > 0 + ? "< 1" + : Math.round(planUsagePct)} + % used +

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

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

+
+ + {!hasPaidPlan ? ( + + ) : null} +
+ +
+ +
+ Auto top-up on ) : ( - <> -
+ Auto top-up off + ) + } + > + Credits + +
+ +
+
+

- Free Plan + Available balance

- You are on basic plan + {formatUsd(creditRemaining)}

+
+ +
+
-
-
-

- Plan usage -

-

- {planUsagePct < 1 && planUsagePct > 0 - ? "< 1" - : Math.round(planUsagePct)} - % used -

-
-
-
80 ? "#ef4444" : "#0054AD", - }} - title={`$${formatUsd(usdSpent)} of $${formatUsd(usdIncluded)} used`} - /> -
+
+
+ Credits used + + {Math.round(creditUsagePct)}% + +
+
+
+
+
+ +
+ + +
+
+ + + +
+
+
+

+ Auto top-up +

- {daysRemaining !== null - ? `Resets in ${daysRemaining} day${daysRemaining !== 1 ? "s" : ""}` - : ""} + Add credits automatically when the workspace balance gets + low.

- +
-
- +
+

+ Trigger below +

+ - +
+

+ Add each time +

+
- - )} -
+
+ +
+
+ +

+ Limit: up to 10 automatic top-ups per month. +

+
+ +
+
+
+
+
+ +
+ Invoice history + + {autumn.isLoading ? ( +
+ + + Loading invoices + +
+ ) : invoices.length === 0 ? ( +
+ +

+ No invoices yet +

+
+ ) : ( +
+ {invoices.map((invoice) => { + const date = formatDate(normalizeTimestamp(invoice.createdAt)) + return ( +
+
+

+ {invoice.planIds?.length + ? invoice.planIds.join(", ") + : "Billing invoice"} +

+

+ {invoice.stripeId} +

+
+

+ {date} +

+

+ {formatInvoiceAmount(invoice.total, invoice.currency)} +

+
+ + {invoice.status} + + {invoice.hostedInvoiceUrl ? ( + + + + ) : null} +
+
+ ) + })} +
+ )}
diff --git a/apps/web/lib/analytics.ts b/apps/web/lib/analytics.ts index eef87c146..d762654e5 100644 --- a/apps/web/lib/analytics.ts +++ b/apps/web/lib/analytics.ts @@ -151,7 +151,7 @@ export const analytics = { // settings / spaces / docs analytics settingsTabChanged: (props: { - tab: "account" | "integrations" | "connections" | "support" + tab: "account" | "billing" | "integrations" | "connections" | "support" }) => safeCapture("settings_tab_changed", props), spaceCreated: () => safeCapture("space_created"), From f1fbc95c0de998eded77d1b179eb16727bc749a0 Mon Sep 17 00:00:00 2001 From: ved015 Date: Sun, 24 May 2026 15:30:42 +0530 Subject: [PATCH 2/8] add top-up and credits in settings --- apps/web/components/settings/billing.tsx | 744 +++++++++++++++-------- apps/web/package.json | 6 +- bun.lock | 3 + packages/ui/components/dialog.tsx | 2 +- 4 files changed, 506 insertions(+), 249 deletions(-) diff --git a/apps/web/components/settings/billing.tsx b/apps/web/components/settings/billing.tsx index fcf450bc1..8de02cf1e 100644 --- a/apps/web/components/settings/billing.tsx +++ b/apps/web/components/settings/billing.tsx @@ -4,35 +4,41 @@ import { dmSans125ClassName } from "@/lib/fonts" import { calculateUsagePercent } from "@/lib/billing-utils" import { PLAN_DISPLAY_NAMES, useTokenUsage } from "@/hooks/use-token-usage" import { cn } from "@lib/utils" +import { useAuth } from "@lib/auth-context" import { Dialog, DialogClose, DialogContent, DialogTrigger, } from "@ui/components/dialog" -import { useQueryClient } from "@tanstack/react-query" -import { useCustomer, useListPlans } from "autumn-js/react" +import { Coins as PhosphorCoins } from "@phosphor-icons/react" +import { useQuery, useQueryClient } from "@tanstack/react-query" +import { useCustomer } from "autumn-js/react" import { - Check, CreditCard, ExternalLink, LoaderIcon, + Plus, ReceiptText, Settings, X, Zap, } from "lucide-react" -import { useEffect, useMemo, useState } from "react" +import { type ComponentType, useEffect, useMemo, useState } from "react" import { toast } from "sonner" const API_BASE = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" const CREDIT_FEATURE_ID = "usd_credits" -const FALLBACK_TOP_UP_PLAN_ID = "api_topup" -const TOP_UP_AMOUNTS = [10, 25, 50] as const +const TOP_UP_PLAN_ID = "credits_topup" +const TOP_UP_AMOUNTS = [10, 25, 50, 100] as const const AUTO_TOP_UP_THRESHOLDS = [2, 5, 10] as const const AUTO_TOP_UP_AMOUNTS = [10, 25, 50] as const +const ConsoleCoinsIcon = PhosphorCoins as unknown as ComponentType<{ + className?: string + weight?: "light" +}> type BillingInvoice = { planIds?: string[] @@ -44,19 +50,26 @@ type BillingInvoice = { hostedInvoiceUrl?: string | null } -type BillingAutoTopup = { +type AutoTopupConfig = { featureId: string enabled: boolean threshold: number quantity: number - invoiceMode?: boolean - purchaseLimit?: { + purchaseLimit: { interval: "hour" | "day" | "week" | "month" - intervalCount?: number + intervalCount: number limit: number - } + } | null } +type AutoTopupsResponse = + | { + ok: true + hasPaymentMethod: boolean + autoTopup: AutoTopupConfig | null + } + | { ok: false; reason: string; message?: string } + function SectionTitle({ children, aside, @@ -133,8 +146,14 @@ function FieldSelect({ onChange: (value: number) => void disabled?: boolean }) { + const cols = values.length <= 3 ? "grid-cols-3" : "grid-cols-4" return ( -
+
{values.map((item) => ( + + +
+
+

+ Buy Credits +

+

+ Add USD to your balance for metered usage. +

+
+ + + +
+ +
+
+

+ Choose an amount +

+ { + setTopUpAmount(value) + setCustomTopUpAmount("") + }} + disabled={topUpPendingAmount !== null} + /> +
+ + + setCustomTopUpAmount(event.target.value) + } + placeholder="e.g. 75" + type="number" + value={customTopUpAmount} + className="h-11 rounded-[10px] border border-white/10 bg-[#080B10] px-3 text-[14px] text-[#FAFAFA] outline-none placeholder:text-[#737373] focus:border-[#0054AD]" + /> +
+
+ +
+ +
+
+

+ Auto reload +

+ + {autoTopUpEnabled ? "on" : "off"} + +
+ +
+

+ Auto reload is{" "} + {autoTopUpEnabled ? "enabled" : "disabled"} +

+ +
+ + {!hasPaymentMethod && !activeAutoTopUp?.enabled ? ( +

+ Save a card in Manage Billing to enable automatic + reloads. +

+ ) : null} + +
+
+ + { + const value = Number.parseFloat( + event.target.value, + ) + setAutoTopUpThreshold( + Number.isFinite(value) ? value : 0, + ) + }} + type="number" + value={ + Number.isFinite(autoTopUpThreshold) + ? autoTopUpThreshold + : "" + } + className="h-10 rounded-[8px] border border-white/10 bg-[#080B10] px-3 text-[13px] text-[#FAFAFA] outline-none focus:border-[#0054AD] disabled:opacity-60" + /> +
+
+ + { + const value = Number.parseFloat( + event.target.value, + ) + setAutoTopUpAmount( + Number.isFinite(value) ? value : 0, + ) + }} + type="number" + value={ + Number.isFinite(autoTopUpAmount) + ? autoTopUpAmount + : "" + } + className="h-10 rounded-[8px] border border-white/10 bg-[#080B10] px-3 text-[13px] text-[#FAFAFA] outline-none focus:border-[#0054AD] disabled:opacity-60" + /> +
+
+ +
+
+
+ + + + {!isAdmin ? ( +

+ Only owners/admins can purchase credits. +

+ ) : null} +
+ +
- -
-
-
-

- Auto top-up + {hasPaidPlan ? ( + +

+
+ +
+

+ Top-up credits{" "} + + (optional) +

- Add credits automatically when the workspace balance gets - low. + {creditRemaining > 0 + ? `${formatUsd(creditRemaining)} available` + : "No top-up credits yet"}

-
- -
- -
-
-

- Trigger below +

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

-
-
-

- Add each time -

- -
-
- -
-
- -

- Limit: up to 10 automatic top-ups per month. -

-
-
+
-
- + ) : null} +
Invoice history - {autumn.isLoading ? ( + {invoicesQuery.isLoading ? (
@@ -823,6 +1070,11 @@ export default function Billing() {
{invoices.map((invoice) => { const date = formatDate(normalizeTimestamp(invoice.createdAt)) + const label = invoice.planIds?.length + ? invoice.planIds + .map((id) => getInvoiceProductLabel(id)) + .join(", ") + : "Billing invoice" return (
- {invoice.planIds?.length - ? invoice.planIds.join(", ") - : "Billing invoice"} + {label}

{invoice.stripeId} diff --git a/apps/web/package.json b/apps/web/package.json index 3535d9a43..847912a04 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -2,7 +2,10 @@ "name": "@repo/web", "version": "0.1.0", "private": true, - "portless": { "name": "app.dev.supermemory", "script": "dev:app" }, + "portless": { + "name": "app.dev.supermemory", + "script": "dev:app" + }, "scripts": { "dev": "portless", "dev:app": "next dev --port ${PORT:-3000}", @@ -29,6 +32,7 @@ "@floating-ui/react": "^0.27.0", "@lobbyside/react": "0.2.0", "@opennextjs/cloudflare": "^1.12.0", + "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-avatar": "^1.1.10", diff --git a/bun.lock b/bun.lock index f1d890ca0..ee980381c 100644 --- a/bun.lock +++ b/bun.lock @@ -141,6 +141,7 @@ "@floating-ui/react": "^0.27.0", "@lobbyside/react": "0.2.0", "@opennextjs/cloudflare": "^1.12.0", + "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-avatar": "^1.1.10", @@ -1276,6 +1277,8 @@ "@peculiar/x509": ["@peculiar/x509@1.14.3", "", { "dependencies": { "@peculiar/asn1-cms": "^2.6.0", "@peculiar/asn1-csr": "^2.6.0", "@peculiar/asn1-ecc": "^2.6.0", "@peculiar/asn1-pkcs9": "^2.6.0", "@peculiar/asn1-rsa": "^2.6.0", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA=="], + "@phosphor-icons/react": ["@phosphor-icons/react@2.1.10", "", { "peerDependencies": { "react": ">= 16.8", "react-dom": ">= 16.8" } }, "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA=="], + "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], "@pixi/colord": ["@pixi/colord@2.9.6", "", {}, "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA=="], diff --git a/packages/ui/components/dialog.tsx b/packages/ui/components/dialog.tsx index b40fabba6..430b23b6b 100644 --- a/packages/ui/components/dialog.tsx +++ b/packages/ui/components/dialog.tsx @@ -36,7 +36,7 @@ function DialogOverlay({ return ( Date: Sun, 24 May 2026 10:05:48 +0000 Subject: [PATCH 3/8] Fix Biome lint errors in billing.tsx - Remove unused imports: CreditCard, Zap, calculateUsagePercent - Remove unused variables: AUTO_TOP_UP_THRESHOLDS, AUTO_TOP_UP_AMOUNTS, creditUsagePct - Fix a11y: associate labels with inputs using htmlFor/id attributes - Fix formatting issues Co-Authored-By: Claude Opus 4.5 --- apps/web/components/settings/billing.tsx | 578 ++++++++++++----------- 1 file changed, 291 insertions(+), 287 deletions(-) diff --git a/apps/web/components/settings/billing.tsx b/apps/web/components/settings/billing.tsx index 8de02cf1e..331a21751 100644 --- a/apps/web/components/settings/billing.tsx +++ b/apps/web/components/settings/billing.tsx @@ -1,7 +1,6 @@ "use client" import { dmSans125ClassName } from "@/lib/fonts" -import { calculateUsagePercent } from "@/lib/billing-utils" import { PLAN_DISPLAY_NAMES, useTokenUsage } from "@/hooks/use-token-usage" import { cn } from "@lib/utils" import { useAuth } from "@lib/auth-context" @@ -15,14 +14,12 @@ import { Coins as PhosphorCoins } from "@phosphor-icons/react" import { useQuery, useQueryClient } from "@tanstack/react-query" import { useCustomer } from "autumn-js/react" import { - CreditCard, ExternalLink, LoaderIcon, Plus, ReceiptText, Settings, X, - Zap, } from "lucide-react" import { type ComponentType, useEffect, useMemo, useState } from "react" import { toast } from "sonner" @@ -33,8 +30,6 @@ const API_BASE = const CREDIT_FEATURE_ID = "usd_credits" const TOP_UP_PLAN_ID = "credits_topup" const TOP_UP_AMOUNTS = [10, 25, 50, 100] as const -const AUTO_TOP_UP_THRESHOLDS = [2, 5, 10] as const -const AUTO_TOP_UP_AMOUNTS = [10, 25, 50] as const const ConsoleCoinsIcon = PhosphorCoins as unknown as ComponentType<{ className?: string weight?: "light" @@ -274,10 +269,6 @@ export default function Billing() { const balance = autumn.data?.balances?.[CREDIT_FEATURE_ID] const creditRemaining = balance?.remaining ?? Math.max(usdIncluded - usdSpent, 0) - const creditGranted = balance?.granted ?? usdIncluded - const creditUsagePct = creditGranted - ? calculateUsagePercent(creditGranted - creditRemaining, creditGranted) - : planUsagePct // --- Invoices via dedicated billing API (matches console) --- const invoicesQuery = useQuery({ @@ -295,7 +286,9 @@ export default function Billing() { staleTime: 60_000, }) const invoices = useMemo(() => { - return [...(invoicesQuery.data ?? [])].sort((a, b) => b.createdAt - a.createdAt) + return [...(invoicesQuery.data ?? [])].sort( + (a, b) => b.createdAt - a.createdAt, + ) }, [invoicesQuery.data]) // --- Auto top-ups via dedicated billing API (matches console) --- @@ -314,7 +307,9 @@ export default function Billing() { }) const autoTopupData = - autoTopupsQuery.data && "ok" in autoTopupsQuery.data && autoTopupsQuery.data.ok + autoTopupsQuery.data && + "ok" in autoTopupsQuery.data && + autoTopupsQuery.data.ok ? autoTopupsQuery.data : null const hasPaymentMethod = Boolean(autoTopupData?.hasPaymentMethod) @@ -445,27 +440,24 @@ export default function Billing() { } setIsSavingAutoTopUp(true) try { - const response = await fetch( - `${API_BASE}/v3/auth/billing/auto-topups`, - { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "X-App-Source": "nova", - }, - body: JSON.stringify({ - enabled: autoTopUpEnabled, - threshold: autoTopUpThreshold, - quantity: autoTopUpAmount, - purchaseLimit: { - interval: "month" as const, - intervalCount: 1, - limit: 10, - }, - }), + const response = await fetch(`${API_BASE}/v3/auth/billing/auto-topups`, { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-App-Source": "nova", }, - ) + body: JSON.stringify({ + enabled: autoTopUpEnabled, + threshold: autoTopUpThreshold, + quantity: autoTopUpAmount, + purchaseLimit: { + interval: "month" as const, + intervalCount: 1, + limit: 10, + }, + }), + }) if (!response.ok) { const body = (await response.json().catch(() => ({}))) as { @@ -480,7 +472,9 @@ export default function Billing() { await queryClient.invalidateQueries({ queryKey: ["autumn"] }) autumn.refetch?.() toast.success( - autoTopUpEnabled ? "Auto top-up settings saved." : "Auto top-up disabled.", + autoTopUpEnabled + ? "Auto top-up settings saved." + : "Auto top-up disabled.", ) } catch (error) { console.error(error) @@ -702,297 +696,307 @@ export default function Billing() {

Credits - -
-
-

- Usage this period -

-
- - {planUsagePct < 1 && planUsagePct > 0 - ? "< 1" - : Math.round(planUsagePct)} - % - - of monthly usage - - {daysRemaining !== null - ? `· resets in ${daysRemaining} day${daysRemaining !== 1 ? "s" : ""}` - : "· resets with your billing cycle"} - -
-
-
-
-

- {planUsagePct > 0 - ? `${formatUsd(usdSpent)} used this period` - : "No usage yet this period"} -

+ +
+
+

+ Usage this period +

+
+ + {planUsagePct < 1 && planUsagePct > 0 + ? "< 1" + : Math.round(planUsagePct)} + % + + of monthly usage + + {daysRemaining !== null + ? `· resets in ${daysRemaining} day${daysRemaining !== 1 ? "s" : ""}` + : "· resets with your billing cycle"} +
+
+
+
+

+ {planUsagePct > 0 + ? `${formatUsd(usdSpent)} used this period` + : "No usage yet this period"} +

+
-
- - - - - + + + + + +
+
+

+ Buy Credits +

+

+ Add USD to your balance for metered usage. +

+
+ + + +
+ +
+
+

+ Choose an amount +

+ { + setTopUpAmount(value) + setCustomTopUpAmount("") + }} + disabled={topUpPendingAmount !== null} + /> +
+ + + setCustomTopUpAmount(event.target.value) + } + placeholder="e.g. 75" + type="number" + value={customTopUpAmount} + className="h-11 rounded-[10px] border border-white/10 bg-[#080B10] px-3 text-[14px] text-[#FAFAFA] outline-none placeholder:text-[#737373] focus:border-[#0054AD]" + /> +
+
+ +
+ +
+
+

+ Auto reload

+ + {autoTopUpEnabled ? "on" : "off"} + +
+ +

- Add USD to your balance for metered usage. + Auto reload is{" "} + {autoTopUpEnabled ? "enabled" : "disabled"}

-
- - -
- -
-
-

+ handleAutoReloadToggle(!autoTopUpEnabled) + } className={cn( dmSans125ClassName(), - "text-[16px] font-semibold text-[#FAFAFA]", + "inline-flex h-9 min-w-[96px] items-center justify-center rounded-[9px] border border-white/10 bg-[#0D121A] px-3 text-[13px] font-medium text-[#FAFAFA] transition-colors hover:bg-[#121A24] disabled:cursor-not-allowed disabled:opacity-45", )} > - Choose an amount + {autoTopUpEnabled ? "Disable" : "Enable"} + +

+ + {!hasPaymentMethod && !activeAutoTopUp?.enabled ? ( +

+ Save a card in Manage Billing to enable automatic + reloads.

- { - setTopUpAmount(value) - setCustomTopUpAmount("") - }} - disabled={topUpPendingAmount !== null} - /> + ) : null} + +
-
-
- -
- -
-
-

- Auto reload -

- - {autoTopUpEnabled ? "on" : "off"} - -
- -
-

+

+ Reload amount (USD) + + { + const value = Number.parseFloat( + event.target.value, + ) + setAutoTopUpAmount( + Number.isFinite(value) ? value : 0, + ) + }} + type="number" + value={ + Number.isFinite(autoTopUpAmount) + ? autoTopUpAmount + : "" + } + className="h-10 rounded-[8px] border border-white/10 bg-[#080B10] px-3 text-[13px] text-[#FAFAFA] outline-none focus:border-[#0054AD] disabled:opacity-60" + /> +
+
- - {!hasPaymentMethod && !activeAutoTopUp?.enabled ? ( -

- Save a card in Manage Billing to enable automatic - reloads. -

- ) : null} - -
-
- - { - const value = Number.parseFloat( - event.target.value, - ) - setAutoTopUpThreshold( - Number.isFinite(value) ? value : 0, - ) - }} - type="number" - value={ - Number.isFinite(autoTopUpThreshold) - ? autoTopUpThreshold - : "" - } - className="h-10 rounded-[8px] border border-white/10 bg-[#080B10] px-3 text-[13px] text-[#FAFAFA] outline-none focus:border-[#0054AD] disabled:opacity-60" - /> -
-
- - { - const value = Number.parseFloat( - event.target.value, - ) - setAutoTopUpAmount( - Number.isFinite(value) ? value : 0, - ) - }} - type="number" - value={ - Number.isFinite(autoTopUpAmount) - ? autoTopUpAmount - : "" - } - className="h-10 rounded-[8px] border border-white/10 bg-[#080B10] px-3 text-[13px] text-[#FAFAFA] outline-none focus:border-[#0054AD] disabled:opacity-60" - /> -
-
- -
-
+
- - - {!isAdmin ? ( -

- Only owners/admins can purchase credits. -

+
- -
- -
+ Buy {formatUsd(selectedTopUpAmount)} in credits + + + {!isAdmin ? ( +

+ Only owners/admins can purchase credits. +

+ ) : null} +
+ + +
-
+
+ {hasPaidPlan ? ( @@ -1041,8 +1045,8 @@ export default function Billing() {
- ) : null} -
+ ) : null} +
Invoice history From c1f6b8803c0390c475c9d1a9f57c966271532398 Mon Sep 17 00:00:00 2001 From: ved015 Date: Mon, 25 May 2026 10:32:45 +0530 Subject: [PATCH 4/8] reuse existing package --- apps/web/components/settings/billing.tsx | 15 ++++----------- apps/web/package.json | 1 - bun.lock | 3 --- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/apps/web/components/settings/billing.tsx b/apps/web/components/settings/billing.tsx index 331a21751..697a33f5c 100644 --- a/apps/web/components/settings/billing.tsx +++ b/apps/web/components/settings/billing.tsx @@ -10,10 +10,10 @@ import { DialogContent, DialogTrigger, } from "@ui/components/dialog" -import { Coins as PhosphorCoins } from "@phosphor-icons/react" import { useQuery, useQueryClient } from "@tanstack/react-query" import { useCustomer } from "autumn-js/react" import { + Coins, ExternalLink, LoaderIcon, Plus, @@ -21,7 +21,7 @@ import { Settings, X, } from "lucide-react" -import { type ComponentType, useEffect, useMemo, useState } from "react" +import { useEffect, useMemo, useState } from "react" import { toast } from "sonner" const API_BASE = @@ -30,10 +30,6 @@ const API_BASE = const CREDIT_FEATURE_ID = "usd_credits" const TOP_UP_PLAN_ID = "credits_topup" const TOP_UP_AMOUNTS = [10, 25, 50, 100] as const -const ConsoleCoinsIcon = PhosphorCoins as unknown as ComponentType<{ - className?: string - weight?: "light" -}> type BillingInvoice = { planIds?: string[] @@ -998,14 +994,11 @@ export default function Billing() {
- {hasPaidPlan ? ( + {true || hasPaidPlan ? (
- +

Top-up credits{" "} diff --git a/apps/web/package.json b/apps/web/package.json index 847912a04..427c233f2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -32,7 +32,6 @@ "@floating-ui/react": "^0.27.0", "@lobbyside/react": "0.2.0", "@opennextjs/cloudflare": "^1.12.0", - "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-avatar": "^1.1.10", diff --git a/bun.lock b/bun.lock index ee980381c..f1d890ca0 100644 --- a/bun.lock +++ b/bun.lock @@ -141,7 +141,6 @@ "@floating-ui/react": "^0.27.0", "@lobbyside/react": "0.2.0", "@opennextjs/cloudflare": "^1.12.0", - "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-avatar": "^1.1.10", @@ -1277,8 +1276,6 @@ "@peculiar/x509": ["@peculiar/x509@1.14.3", "", { "dependencies": { "@peculiar/asn1-cms": "^2.6.0", "@peculiar/asn1-csr": "^2.6.0", "@peculiar/asn1-ecc": "^2.6.0", "@peculiar/asn1-pkcs9": "^2.6.0", "@peculiar/asn1-rsa": "^2.6.0", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA=="], - "@phosphor-icons/react": ["@phosphor-icons/react@2.1.10", "", { "peerDependencies": { "react": ">= 16.8", "react-dom": ">= 16.8" } }, "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA=="], - "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], "@pixi/colord": ["@pixi/colord@2.9.6", "", {}, "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA=="], From 31b2dc4a9bfb6ffaf4227405821bde56c281753e Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 05:07:57 +0000 Subject: [PATCH 5/8] Fix noConstantCondition lint error in billing.tsx Remove debug `true ||` from conditional that was causing Biome lint error. Co-Authored-By: Claude Opus 4.5 --- apps/web/components/settings/billing.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/components/settings/billing.tsx b/apps/web/components/settings/billing.tsx index 697a33f5c..07d0fdb1f 100644 --- a/apps/web/components/settings/billing.tsx +++ b/apps/web/components/settings/billing.tsx @@ -994,7 +994,7 @@ export default function Billing() {

- {true || hasPaidPlan ? ( + {hasPaidPlan ? (
From c68007bc96cacffa91fdbf5cc053c1e62a4ae70a Mon Sep 17 00:00:00 2001 From: Vedant Mahajan Date: Mon, 25 May 2026 10:43:29 +0530 Subject: [PATCH 6/8] Update apps/web/components/settings/billing.tsx Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- apps/web/components/settings/billing.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/components/settings/billing.tsx b/apps/web/components/settings/billing.tsx index 07d0fdb1f..b20935c3b 100644 --- a/apps/web/components/settings/billing.tsx +++ b/apps/web/components/settings/billing.tsx @@ -994,7 +994,7 @@ export default function Billing() {
- {hasPaidPlan ? ( + {hasPaidPlan ? (
From 030165f5cbf91f4391a079af9b554bfed48aca11 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 05:15:23 +0000 Subject: [PATCH 7/8] Fix Biome formatting in billing.tsx Co-Authored-By: Claude Opus 4.5 --- apps/web/components/settings/billing.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/components/settings/billing.tsx b/apps/web/components/settings/billing.tsx index b20935c3b..07d0fdb1f 100644 --- a/apps/web/components/settings/billing.tsx +++ b/apps/web/components/settings/billing.tsx @@ -994,7 +994,7 @@ export default function Billing() {
- {hasPaidPlan ? ( + {hasPaidPlan ? (
From c7cf2f59b062287ea0f98ff1dda27363c33307d8 Mon Sep 17 00:00:00 2001 From: Vedant Mahajan Date: Mon, 25 May 2026 11:04:44 +0530 Subject: [PATCH 8/8] Remove multiline portless configuration in package.json --- apps/web/package.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 427c233f2..3535d9a43 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -2,10 +2,7 @@ "name": "@repo/web", "version": "0.1.0", "private": true, - "portless": { - "name": "app.dev.supermemory", - "script": "dev:app" - }, + "portless": { "name": "app.dev.supermemory", "script": "dev:app" }, "scripts": { "dev": "portless", "dev:app": "next dev --port ${PORT:-3000}",