diff --git a/apps/web/components/settings/billing.tsx b/apps/web/components/settings/billing.tsx index 8de02cf1..331a2175 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"} +

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

- 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" - /> -
-
- -
-
-
- - + + +
+
+

- {topUpPendingAmount !== null ? ( - - ) : null} - Buy {formatUsd(selectedTopUpAmount)} in credits + Buy Credits +

+

+ Add USD to your balance for metered usage. +

+
+ + + +
- {!isAdmin ? ( -

- Only owners/admins can purchase credits. +

+
+

+ 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} +
+
+ +
- +
+ {hasPaidPlan ? ( @@ -1041,8 +1045,8 @@ export default function Billing() {
- ) : null} -
+ ) : null} +
Invoice history