From a01ce471ee05c20f1d5b6c362c34b025db3f8d41 Mon Sep 17 00:00:00 2001 From: John | Elite Encoder Date: Tue, 8 Sep 2026 17:37:35 -0400 Subject: [PATCH] refactor(usage): replace wallet billing state with account usage for session management Updated PlansPanel, SidebarUsageCard, UsageView, and BillingSection components to utilize the new useAccountUsage hook and includedUsageSummaryFromBalance function. This change enhances the handling of user account usage data, improving the accuracy of billing summaries and usage displays. Adjusted related logic to ensure proper state management and error handling across components. --- components/console/PlansPanel.tsx | 21 ++++++-- components/console/SidebarUsageCard.tsx | 48 +++++++++---------- components/console/UsageView.tsx | 44 +++++++---------- .../console/settings/BillingSection.tsx | 21 ++++++-- lib/console/useAccountUsage.ts | 5 ++ lib/console/wallet-settlement-display.test.ts | 37 ++++++++++++++ lib/console/wallet-settlement-display.ts | 32 +++++++++++++ 7 files changed, 147 insertions(+), 61 deletions(-) diff --git a/components/console/PlansPanel.tsx b/components/console/PlansPanel.tsx index 8445a27..96adf71 100644 --- a/components/console/PlansPanel.tsx +++ b/components/console/PlansPanel.tsx @@ -21,10 +21,13 @@ import { } from "@/lib/console/useBillingPlans"; import { redirectToCheckout } from "@/lib/console/checkout-redirect"; import { useAuth } from "@/components/console/AuthContext"; -import { useWalletBillingState } from "@/lib/console/useOwnerWallet"; +import { + SESSION_USAGE_OPTIONS, + useAccountUsage, +} from "@/lib/console/useAccountUsage"; import { includedUsageRemainingLabel, - includedUsageSummary, + includedUsageSummaryFromBalance, } from "@/lib/console/wallet-settlement-display"; function isUsagePlan( @@ -82,10 +85,18 @@ function clearCheckoutQueryParam(): void { export default function PlansPanel() { const { isConnected } = useAuth(); const { state, reload, subscribe, changePlan } = useBillingPlans(isConnected); - const wallet = useWalletBillingState(isConnected); + const usage = useAccountUsage(isConnected, SESSION_USAGE_OPTIONS); const included = - wallet.state.status === "ready" - ? includedUsageSummary(wallet.state.wallet.billingState) + usage.status === "ready" + ? includedUsageSummaryFromBalance( + usage.data.balance, + state.status === "ready" && state.subscription + ? { + id: state.subscription.planId, + name: state.subscription.planName, + } + : undefined + ) : null; const [busyPlanId, setBusyPlanId] = useState(null); const [error, setError] = useState(null); diff --git a/components/console/SidebarUsageCard.tsx b/components/console/SidebarUsageCard.tsx index c9aad3e..206cf61 100644 --- a/components/console/SidebarUsageCard.tsx +++ b/components/console/SidebarUsageCard.tsx @@ -2,9 +2,12 @@ import Link from "next/link"; import { useAuth } from "@/components/console/AuthContext"; -import { useWalletBillingState } from "@/lib/console/useOwnerWallet"; +import { + SESSION_USAGE_OPTIONS, + useAccountUsage, +} from "@/lib/console/useAccountUsage"; import { microsToUsd } from "@/lib/console/usage-capability-display"; -import { includedUsageSummary } from "@/lib/console/wallet-settlement-display"; +import { includedUsageSummaryFromBalance } from "@/lib/console/wallet-settlement-display"; /** * Sidebar balance meter. It shows the remaining balance against the amount @@ -12,16 +15,15 @@ import { includedUsageSummary } from "@/lib/console/wallet-settlement-display"; */ export default function SidebarUsageCard() { const { isConnected } = useAuth(); - const wallet = useWalletBillingState(isConnected); + const usage = useAccountUsage(isConnected, SESSION_USAGE_OPTIONS); const included = - wallet.state.status === "ready" - ? includedUsageSummary(wallet.state.wallet.billingState) + usage.status === "ready" + ? includedUsageSummaryFromBalance(usage.data.balance) : null; - if ( - isConnected && - (wallet.state.status === "loading" || wallet.state.status === "idle") - ) { + if (!isConnected) return null; + + if (usage.status === "loading" || usage.status === "idle") { return (
0 ? Math.min(100, (remainingUsd / issuedUsd) * 100) : 0; - const canSpend = wallet.state.wallet.billingState.canSpend; + const canSpend = usage.data.balance?.hasAccess ?? remainingUsd > 0; return ( { - if (walletState.state.status !== "ready" || !included) return null; - const funding = walletState.state.wallet.billingState.funding; - const overage = funding.overage; + if (usage.status !== "ready") return null; return { - includedTotal: microsToUsd(included.totalUsdMicros), - consumed: microsToUsd(included.consumedUsdMicros), - credits: microsToUsd(walletState.state.wallet.balance?.usdMicros ?? "0"), - overage: - overage.eligible && overage.remaining - ? microsToUsd(overage.remaining.usdMicros) - : null, + includedTotal: included ? microsToUsd(included.totalUsdMicros) : 0, + consumed: included ? microsToUsd(included.consumedUsdMicros) : 0, + credits: 0, + overage: null, }; - }, [walletState.state, included]); + }, [usage.status, included]); - const loading = - walletState.state.status === "loading" || - walletState.state.status === "idle"; + const loading = usage.status === "loading" || usage.status === "idle"; return (
@@ -221,12 +216,9 @@ export default function UsageView() { />
- {walletState.state.status === "error" && ( + {usage.status === "error" && (
- +
)} diff --git a/components/console/settings/BillingSection.tsx b/components/console/settings/BillingSection.tsx index e05c738..db80366 100644 --- a/components/console/settings/BillingSection.tsx +++ b/components/console/settings/BillingSection.tsx @@ -32,9 +32,13 @@ import { startWalletTopUp, useWalletBillingState, } from "@/lib/console/useOwnerWallet"; +import { + SESSION_USAGE_OPTIONS, + useAccountUsage, +} from "@/lib/console/useAccountUsage"; import { includedUsageRemainingLabel, - includedUsageSummary, + includedUsageSummaryFromBalance, formatWalletUsd, overageLimitNote, } from "@/lib/console/wallet-settlement-display"; @@ -195,10 +199,7 @@ export default function BillingSection() { removePaymentMethod, } = useBillingAccount(isConnected); const wallet = useWalletBillingState(isConnected); - const included = - wallet.state.status === "ready" - ? includedUsageSummary(wallet.state.wallet.billingState) - : null; + const usage = useAccountUsage(isConnected, SESSION_USAGE_OPTIONS); const [busyPlanId, setBusyPlanId] = useState(null); const [pmBusy, setPmBusy] = useState(false); @@ -658,6 +659,16 @@ export default function BillingSection() { const cancelingEndsAt = resolveCancelingEffectiveAt(subscription); const cancelingEndsLabel = formatPendingCancelDate(cancelingEndsAt); + const included = + usage.status === "ready" + ? includedUsageSummaryFromBalance( + usage.data.balance, + subscription + ? { id: subscription.planId, name: subscription.planName } + : undefined + ) + : null; + const planSub = subscriptionUiState.kind === "canceling" ? `${cancelingPlanName} ends ${cancelingEndsLabel}` diff --git a/lib/console/useAccountUsage.ts b/lib/console/useAccountUsage.ts index bdcb891..44faaa3 100644 --- a/lib/console/useAccountUsage.ts +++ b/lib/console/useAccountUsage.ts @@ -43,6 +43,11 @@ type UseAccountUsageOptions = { includePrior?: boolean; }; +/** Session meters only need the live allowance, not the prior usage window. */ +export const SESSION_USAGE_OPTIONS: UseAccountUsageOptions = { + includePrior: false, +}; + function normalizeOptions( periodDaysOrOptions: number | UseAccountUsageOptions = 30 ): Required< diff --git a/lib/console/wallet-settlement-display.test.ts b/lib/console/wallet-settlement-display.test.ts index 7e588b0..d53b72f 100644 --- a/lib/console/wallet-settlement-display.test.ts +++ b/lib/console/wallet-settlement-display.test.ts @@ -8,6 +8,7 @@ import { collectionSchedule, includedUsageRemainingLabel, includedUsageSummary, + includedUsageSummaryFromBalance, overageLimitNote, spendPostureBadge, } from "./wallet-settlement-display"; @@ -257,6 +258,42 @@ describe("includedUsageSummary", () => { }); }); +describe("includedUsageSummaryFromBalance", () => { + it("returns null when the user has no granted allowance", () => { + const summary = includedUsageSummaryFromBalance({ + externalUserId: "eu_1", + balanceUsdMicros: "0", + consumedUsdMicros: "0", + lifetimeGrantedUsdMicros: "0", + hasAccess: false, + }); + assert.equal(summary, null); + }); + + it("maps remaining and granted from the user usage balance", () => { + const summary = includedUsageSummaryFromBalance( + { + externalUserId: "eu_1", + balanceUsdMicros: "4982000", + consumedUsdMicros: "18000", + lifetimeGrantedUsdMicros: "5000000", + hasAccess: true, + }, + { id: "plan_1", name: "Starter" }, + ); + assert.ok(summary); + assert.equal(summary.remainingUsd, "4.98"); + assert.equal(summary.totalUsd, "5.00"); + assert.equal(summary.consumedUsd, "0.02"); + assert.equal(summary.planName, "Starter"); + assert.equal(summary.planId, "plan_1"); + assert.equal( + includedUsageRemainingLabel(summary), + "Starter ยท $4.98 of $5.00 included left", + ); + }); +}); + describe("overageLimitNote", () => { it("shows ceiling and remaining headroom while spendable", () => { const note = overageLimitNote(makeState({})); diff --git a/lib/console/wallet-settlement-display.ts b/lib/console/wallet-settlement-display.ts index db95d08..cfeef9d 100644 --- a/lib/console/wallet-settlement-display.ts +++ b/lib/console/wallet-settlement-display.ts @@ -1,4 +1,5 @@ import type { BillingState, BillingStatus } from "@pymthouse/builder-sdk"; +import type { AccountUsageBalance } from "./account-usage"; import { microsToUsd } from "./usage-capability-display"; type IncludedUsageFunding = { @@ -191,6 +192,37 @@ export function includedUsageSummary( }; } +/** + * Session-user included allowance from `me/usage/balance`. + * Null when the user has no granted cycle (prepaid / invoice only). + */ +export function includedUsageSummaryFromBalance( + balance: AccountUsageBalance | null | undefined, + sourcePlan?: { id?: string | null; name?: string | null }, +): IncludedUsageSummary | null { + if (!balance) return null; + const remainingUsdMicros = balance.balanceUsdMicros; + const grantedUsdMicros = balance.lifetimeGrantedUsdMicros; + const consumedUsdMicros = balance.consumedUsdMicros; + const totalUsdMicros = + parseUsdMicros(grantedUsdMicros) > BigInt(0) + ? grantedUsdMicros + : remainingUsdMicros; + if (parseUsdMicros(totalUsdMicros) <= BigInt(0)) return null; + + return { + remainingUsdMicros, + totalUsdMicros, + consumedUsdMicros, + remainingUsd: formatWalletUsd(remainingUsdMicros), + totalUsd: formatWalletUsd(totalUsdMicros), + consumedUsd: formatWalletUsd(consumedUsdMicros), + planId: sourcePlan?.id?.trim() || null, + planName: sourcePlan?.name?.trim() || null, + resetsAt: null, + }; +} + export function includedUsageRemainingLabel( summary: IncludedUsageSummary, ): string {