mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-07 08:26:15 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
parent
f1fbc95c0d
commit
b3399613e4
1 changed files with 316 additions and 312 deletions
|
|
@ -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() {
|
|||
<section className="flex flex-col gap-4">
|
||||
<SectionTitle>Credits</SectionTitle>
|
||||
|
||||
<SettingsCard>
|
||||
<div className="flex flex-col gap-5 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[11px] font-bold uppercase tracking-[0.5px] text-[#737373]">
|
||||
Usage this period
|
||||
</p>
|
||||
<div className="mt-4 flex items-baseline gap-1 text-[13px] text-[#A3A3A3]">
|
||||
<span className="font-semibold text-[#FAFAFA]">
|
||||
{planUsagePct < 1 && planUsagePct > 0
|
||||
? "< 1"
|
||||
: Math.round(planUsagePct)}
|
||||
%
|
||||
</span>
|
||||
<span>of monthly usage</span>
|
||||
<span className="text-[#737373]">
|
||||
{daysRemaining !== null
|
||||
? `· resets in ${daysRemaining} day${daysRemaining !== 1 ? "s" : ""}`
|
||||
: "· resets with your billing cycle"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 h-2 w-full overflow-hidden rounded-full bg-[#2E353D]">
|
||||
<div
|
||||
className="h-full rounded-full bg-[#4BA0FA]"
|
||||
style={{ width: `${planUsagePct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-3 text-[12px] text-[#737373]">
|
||||
{planUsagePct > 0
|
||||
? `${formatUsd(usdSpent)} used this period`
|
||||
: "No usage yet this period"}
|
||||
</p>
|
||||
<SettingsCard>
|
||||
<div className="flex flex-col gap-5 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[11px] font-bold uppercase tracking-[0.5px] text-[#737373]">
|
||||
Usage this period
|
||||
</p>
|
||||
<div className="mt-4 flex items-baseline gap-1 text-[13px] text-[#A3A3A3]">
|
||||
<span className="font-semibold text-[#FAFAFA]">
|
||||
{planUsagePct < 1 && planUsagePct > 0
|
||||
? "< 1"
|
||||
: Math.round(planUsagePct)}
|
||||
%
|
||||
</span>
|
||||
<span>of monthly usage</span>
|
||||
<span className="text-[#737373]">
|
||||
{daysRemaining !== null
|
||||
? `· resets in ${daysRemaining} day${daysRemaining !== 1 ? "s" : ""}`
|
||||
: "· resets with your billing cycle"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 h-2 w-full overflow-hidden rounded-full bg-[#2E353D]">
|
||||
<div
|
||||
className="h-full rounded-full bg-[#4BA0FA]"
|
||||
style={{ width: `${planUsagePct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-3 text-[12px] text-[#737373]">
|
||||
{planUsagePct > 0
|
||||
? `${formatUsd(usdSpent)} used this period`
|
||||
: "No usage yet this period"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 flex-col gap-2 sm:min-w-[170px]">
|
||||
<Dialog
|
||||
open={isCreditsDialogOpen}
|
||||
onOpenChange={setIsCreditsDialogOpen}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"inline-flex h-9 items-center justify-center gap-2 rounded-[9px] bg-[#0054AD] px-3 text-[13px] font-semibold text-[#FAFAFA] transition-colors hover:bg-[#0B65C9]",
|
||||
)}
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
Buy credits
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
className="w-[min(560px,calc(100vw-32px))] rounded-[18px] border border-[#1C2B3E] bg-[#0B0D12] p-6 shadow-[0px_18px_70px_rgba(0,0,0,0.72)]"
|
||||
<div className="flex shrink-0 flex-col gap-2 sm:min-w-[170px]">
|
||||
<Dialog
|
||||
open={isCreditsDialogOpen}
|
||||
onOpenChange={setIsCreditsDialogOpen}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"inline-flex h-9 items-center justify-center gap-2 rounded-[9px] bg-[#0054AD] px-3 text-[13px] font-semibold text-[#FAFAFA] transition-colors hover:bg-[#0B65C9]",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[22px] font-semibold tracking-[-0.22px] text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
Buy Credits
|
||||
</p>
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"mt-2 text-[15px] text-[#A3A3A3]",
|
||||
)}
|
||||
>
|
||||
Add USD to your balance for metered usage.
|
||||
</p>
|
||||
</div>
|
||||
<DialogClose asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex size-9 shrink-0 items-center justify-center rounded-full border border-white/10 bg-[#0D121A] text-[#737373] transition-colors hover:text-[#FAFAFA]"
|
||||
>
|
||||
<X className="size-5" />
|
||||
</button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-3">
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[16px] font-semibold text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
Choose an amount
|
||||
</p>
|
||||
<FieldSelect
|
||||
value={topUpAmount}
|
||||
values={TOP_UP_AMOUNTS}
|
||||
prefix="$"
|
||||
onChange={(value) => {
|
||||
setTopUpAmount(value)
|
||||
setCustomTopUpAmount("")
|
||||
}}
|
||||
disabled={topUpPendingAmount !== null}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-[11px] font-bold uppercase tracking-[0.5px] text-[#737373]">
|
||||
Custom amount (USD)
|
||||
</label>
|
||||
<input
|
||||
inputMode="decimal"
|
||||
min={1}
|
||||
onChange={(event) =>
|
||||
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]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-white/[0.06]" />
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[11px] font-bold uppercase tracking-[0.5px] text-[#737373]">
|
||||
Auto reload
|
||||
</p>
|
||||
<span className="text-[12px] text-[#737373]">
|
||||
{autoTopUpEnabled ? "on" : "off"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[16px] text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
Auto reload is{" "}
|
||||
{autoTopUpEnabled ? "enabled" : "disabled"}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
disabled={
|
||||
isSavingAutoTopUp ||
|
||||
!isAdmin ||
|
||||
(!hasPaymentMethod && !activeAutoTopUp?.enabled)
|
||||
}
|
||||
onClick={() =>
|
||||
handleAutoReloadToggle(!autoTopUpEnabled)
|
||||
}
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"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",
|
||||
)}
|
||||
>
|
||||
{autoTopUpEnabled ? "Disable" : "Enable"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!hasPaymentMethod && !activeAutoTopUp?.enabled ? (
|
||||
<p className="text-[13px] text-[#737373]">
|
||||
Save a card in Manage Billing to enable automatic
|
||||
reloads.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"grid gap-3 rounded-[10px] border border-white/[0.06] bg-[#0D121A] p-3 transition-[filter,opacity] sm:grid-cols-2",
|
||||
!autoTopUpEnabled &&
|
||||
"pointer-events-none select-none opacity-45 blur-[3px]",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-[11px] font-bold uppercase tracking-[0.5px] text-[#737373]">
|
||||
Threshold (USD)
|
||||
</label>
|
||||
<input
|
||||
disabled={!autoTopUpEnabled || isSavingAutoTopUp}
|
||||
inputMode="decimal"
|
||||
min={0}
|
||||
onChange={(event) => {
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-[11px] font-bold uppercase tracking-[0.5px] text-[#737373]">
|
||||
Reload amount (USD)
|
||||
</label>
|
||||
<input
|
||||
disabled={!autoTopUpEnabled || isSavingAutoTopUp}
|
||||
inputMode="decimal"
|
||||
min={0.01}
|
||||
onChange={(event) => {
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleSaveAutoTopUp()}
|
||||
disabled={isSavingAutoTopUp || !isAdmin}
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"inline-flex h-9 w-full items-center justify-center gap-2 rounded-[8px] border border-white/10 bg-[#080B10] text-[13px] font-medium text-[#FAFAFA] transition-colors hover:bg-[#121A24] disabled:cursor-not-allowed disabled:opacity-60",
|
||||
)}
|
||||
>
|
||||
{isSavingAutoTopUp ? (
|
||||
<LoaderIcon className="size-3.5 animate-spin" />
|
||||
) : null}
|
||||
Save threshold & reload amount
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleTopUp(selectedTopUpAmount)}
|
||||
disabled={
|
||||
topUpPendingAmount !== null ||
|
||||
!isAdmin ||
|
||||
selectedTopUpAmount <= 0
|
||||
}
|
||||
<Plus className="size-3.5" />
|
||||
Buy credits
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
className="w-[min(560px,calc(100vw-32px))] rounded-[18px] border border-[#1C2B3E] bg-[#0B0D12] p-6 shadow-[0px_18px_70px_rgba(0,0,0,0.72)]"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"inline-flex h-11 w-full items-center justify-center gap-2 rounded-[10px] bg-[#0054AD] text-[14px] font-bold text-[#FAFAFA] transition-colors hover:bg-[#0B65C9] disabled:cursor-not-allowed disabled:opacity-60",
|
||||
"text-[22px] font-semibold tracking-[-0.22px] text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
{topUpPendingAmount !== null ? (
|
||||
<LoaderIcon className="size-4 animate-spin" />
|
||||
) : null}
|
||||
Buy {formatUsd(selectedTopUpAmount)} in credits
|
||||
Buy Credits
|
||||
</p>
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"mt-2 text-[15px] text-[#A3A3A3]",
|
||||
)}
|
||||
>
|
||||
Add USD to your balance for metered usage.
|
||||
</p>
|
||||
</div>
|
||||
<DialogClose asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex size-9 shrink-0 items-center justify-center rounded-full border border-white/10 bg-[#0D121A] text-[#737373] transition-colors hover:text-[#FAFAFA]"
|
||||
>
|
||||
<X className="size-5" />
|
||||
</button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
|
||||
{!isAdmin ? (
|
||||
<p className="text-center text-[11px] text-[#737373]">
|
||||
Only owners/admins can purchase credits.
|
||||
<div className="mt-8 flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-3">
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[16px] font-semibold text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
Choose an amount
|
||||
</p>
|
||||
<FieldSelect
|
||||
value={topUpAmount}
|
||||
values={TOP_UP_AMOUNTS}
|
||||
prefix="$"
|
||||
onChange={(value) => {
|
||||
setTopUpAmount(value)
|
||||
setCustomTopUpAmount("")
|
||||
}}
|
||||
disabled={topUpPendingAmount !== null}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label
|
||||
htmlFor="custom-topup-amount"
|
||||
className="text-[11px] font-bold uppercase tracking-[0.5px] text-[#737373]"
|
||||
>
|
||||
Custom amount (USD)
|
||||
</label>
|
||||
<input
|
||||
id="custom-topup-amount"
|
||||
inputMode="decimal"
|
||||
min={1}
|
||||
onChange={(event) =>
|
||||
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]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-white/[0.06]" />
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[11px] font-bold uppercase tracking-[0.5px] text-[#737373]">
|
||||
Auto reload
|
||||
</p>
|
||||
<span className="text-[12px] text-[#737373]">
|
||||
{autoTopUpEnabled ? "on" : "off"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[16px] text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
Auto reload is{" "}
|
||||
{autoTopUpEnabled ? "enabled" : "disabled"}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
disabled={
|
||||
isSavingAutoTopUp ||
|
||||
!isAdmin ||
|
||||
(!hasPaymentMethod && !activeAutoTopUp?.enabled)
|
||||
}
|
||||
onClick={() =>
|
||||
handleAutoReloadToggle(!autoTopUpEnabled)
|
||||
}
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"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",
|
||||
)}
|
||||
>
|
||||
{autoTopUpEnabled ? "Disable" : "Enable"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!hasPaymentMethod && !activeAutoTopUp?.enabled ? (
|
||||
<p className="text-[13px] text-[#737373]">
|
||||
Save a card in Manage Billing to enable automatic
|
||||
reloads.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"grid gap-3 rounded-[10px] border border-white/[0.06] bg-[#0D121A] p-3 transition-[filter,opacity] sm:grid-cols-2",
|
||||
!autoTopUpEnabled &&
|
||||
"pointer-events-none select-none opacity-45 blur-[3px]",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label
|
||||
htmlFor="auto-topup-threshold"
|
||||
className="text-[11px] font-bold uppercase tracking-[0.5px] text-[#737373]"
|
||||
>
|
||||
Threshold (USD)
|
||||
</label>
|
||||
<input
|
||||
id="auto-topup-threshold"
|
||||
disabled={!autoTopUpEnabled || isSavingAutoTopUp}
|
||||
inputMode="decimal"
|
||||
min={0}
|
||||
onChange={(event) => {
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label
|
||||
htmlFor="auto-topup-amount"
|
||||
className="text-[11px] font-bold uppercase tracking-[0.5px] text-[#737373]"
|
||||
>
|
||||
Reload amount (USD)
|
||||
</label>
|
||||
<input
|
||||
id="auto-topup-amount"
|
||||
disabled={!autoTopUpEnabled || isSavingAutoTopUp}
|
||||
inputMode="decimal"
|
||||
min={0.01}
|
||||
onChange={(event) => {
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleSaveAutoTopUp()}
|
||||
disabled={isSavingAutoTopUp || !isAdmin}
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"inline-flex h-9 w-full items-center justify-center gap-2 rounded-[8px] border border-white/10 bg-[#080B10] text-[13px] font-medium text-[#FAFAFA] transition-colors hover:bg-[#121A24] disabled:cursor-not-allowed disabled:opacity-60",
|
||||
)}
|
||||
>
|
||||
{isSavingAutoTopUp ? (
|
||||
<LoaderIcon className="size-3.5 animate-spin" />
|
||||
) : null}
|
||||
Save threshold & reload amount
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCreditsDialogOpen(true)}
|
||||
disabled={!isAdmin}
|
||||
className="inline-flex h-8 items-center justify-center gap-2 rounded-[8px] text-[12px] font-medium text-[#A3A3A3] transition-colors hover:bg-white/[0.04] disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<span
|
||||
className="size-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: autoTopUpEnabled
|
||||
? "#4BA0FA"
|
||||
: "#737373",
|
||||
}}
|
||||
/>
|
||||
Auto reload: {autoTopUpEnabled ? "on" : "off"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleTopUp(selectedTopUpAmount)}
|
||||
disabled={
|
||||
topUpPendingAmount !== null ||
|
||||
!isAdmin ||
|
||||
selectedTopUpAmount <= 0
|
||||
}
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"inline-flex h-11 w-full items-center justify-center gap-2 rounded-[10px] bg-[#0054AD] text-[14px] font-bold text-[#FAFAFA] transition-colors hover:bg-[#0B65C9] disabled:cursor-not-allowed disabled:opacity-60",
|
||||
)}
|
||||
>
|
||||
{topUpPendingAmount !== null ? (
|
||||
<LoaderIcon className="size-4 animate-spin" />
|
||||
) : null}
|
||||
Buy {formatUsd(selectedTopUpAmount)} in credits
|
||||
</button>
|
||||
|
||||
{!isAdmin ? (
|
||||
<p className="text-center text-[11px] text-[#737373]">
|
||||
Only owners/admins can purchase credits.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCreditsDialogOpen(true)}
|
||||
disabled={!isAdmin}
|
||||
className="inline-flex h-8 items-center justify-center gap-2 rounded-[8px] text-[12px] font-medium text-[#A3A3A3] transition-colors hover:bg-white/[0.04] disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<span
|
||||
className="size-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: autoTopUpEnabled ? "#4BA0FA" : "#737373",
|
||||
}}
|
||||
/>
|
||||
Auto reload: {autoTopUpEnabled ? "on" : "off"}
|
||||
</button>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{hasPaidPlan ? (
|
||||
<SettingsCard className="border border-dashed border-white/10 bg-[#14161A]/70">
|
||||
|
|
@ -1041,8 +1045,8 @@ export default function Billing() {
|
|||
</button>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-4">
|
||||
<SectionTitle>Invoice history</SectionTitle>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue