Merge pull request #1118 from useautumn/clean-up-dashboard-cache-top-up

Clean up dashboard: remove org cache and auto top-up from balance sheet
This commit is contained in:
Ayush
2026-03-27 18:22:32 +00:00
committed by GitHub
12 changed files with 6 additions and 312 deletions

View File

@@ -4,23 +4,12 @@ import { useEffect } from "react";
import { authClient, useListOrganizations } from "@/lib/auth-client";
import { useAxiosInstance } from "@/services/useAxiosInstance";
const ORG_STORAGE_KEY = "autumn_org";
let lastSwitchedOrgId: string | null = null;
export const setLastSwitchedOrgId = (id: string) => {
lastSwitchedOrgId = id;
};
export const getLastSwitchedOrgId = () => lastSwitchedOrgId;
/** Clears all org-related localStorage cache entries. Call before reload on session changes (impersonation start/stop). */
export const clearOrgCache = () => {
for (const key of Object.keys(localStorage)) {
if (key.startsWith(ORG_STORAGE_KEY)) {
localStorage.removeItem(key);
}
}
};
export const useOrg = (params?: { env?: AppEnv }) => {
const axiosInstance = useAxiosInstance({ env: params?.env });
const { data: orgList } = useListOrganizations();
@@ -28,32 +17,12 @@ export const useOrg = (params?: { env?: AppEnv }) => {
const fetcher = async () => {
try {
const { data } = await axiosInstance.get("/organization");
if (data) {
const storageKey = params?.env
? `${ORG_STORAGE_KEY}_${params.env}`
: ORG_STORAGE_KEY;
localStorage.setItem(storageKey, JSON.stringify(data));
}
return data;
} catch {
return null;
}
};
const getInitialData = () => {
try {
const storageKey = params?.env
? `${ORG_STORAGE_KEY}_${params.env}`
: ORG_STORAGE_KEY;
const stored = localStorage.getItem(storageKey);
return stored ? JSON.parse(stored) : undefined;
} catch {
return undefined;
}
};
const initialDataValue = getInitialData();
const {
data: org,
isLoading,
@@ -62,7 +31,6 @@ export const useOrg = (params?: { env?: AppEnv }) => {
} = useQuery({
queryKey: params?.env ? ["org", params.env] : ["org"],
queryFn: fetcher,
initialData: initialDataValue,
placeholderData: keepPreviousData,
refetchOnWindowFocus: true,
staleTime: 30_000,
@@ -78,10 +46,10 @@ export const useOrg = (params?: { env?: AppEnv }) => {
} else {
console.log("No org to set active, signing out");
await authClient.signOut();
window.location.href = "/sign-in";
}
};
// 1. If no org...
if (!org && !isLoading) {
handleNoActiveOrg();
}

View File

@@ -2,7 +2,6 @@ import { Globe } from "@phosphor-icons/react";
import { useNavigate } from "react-router";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { clearOrgCache } from "@/hooks/common/useOrg";
import { authClient } from "@/lib/auth-client";
import { AdminOrgTable } from "@/views/admin/AdminOrgTable";
import { AdminUserTable } from "@/views/admin/AdminUserTable";
@@ -35,7 +34,6 @@ export const AdminView = () => {
return;
}
clearOrgCache();
window.location.reload();
};

View File

@@ -2,7 +2,6 @@ import { AlertCircle, Loader2, ShieldCheck } from "lucide-react";
import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router";
import { Button } from "@/components/v2/buttons/Button";
import { clearOrgCache } from "@/hooks/common/useOrg";
import { authClient } from "@/lib/auth-client";
import { useAxiosInstance } from "../../services/useAxiosInstance";
import { useAdmin } from "./hooks/useAdmin";
@@ -65,9 +64,6 @@ export function ImpersonateRedirect() {
// Step 5: Navigate to the redirect path
setStatus("Redirecting...");
// Clear stale org cache before navigating so the new session loads fresh data
clearOrgCache();
window.location.href = redirect;
} catch (err: unknown) {
const errorMessage =

View File

@@ -10,7 +10,6 @@ import type {
Rollover,
} from "@autumn/shared";
import { toast } from "sonner";
import { clearOrgCache } from "@/hooks/common/useOrg";
import { authClient } from "@/lib/auth-client";
import { formatUnixToDate } from "../../utils/formatUtils/formatDateUtils";
@@ -66,7 +65,6 @@ export const impersonateUser = async ({
await authClient.organization.setActive({ organizationId });
}
clearOrgCache();
window.location.reload();
};

View File

@@ -1,5 +1,4 @@
import { type GroupedPermission, groupAndFormatScopes } from "@autumn/shared";
import { clearOrgCache } from "@/hooks/common/useOrg";
import {
Check,
ChevronDown,
@@ -147,7 +146,6 @@ export const Consent = () => {
await authClient.organization.setActive({
organizationId: orgId,
});
clearOrgCache();
window.location.reload();
} catch (_) {
toast.error("Failed to switch organization");

View File

@@ -1,121 +0,0 @@
import { PurchaseLimitInterval } from "@autumn/shared";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/v2/selects/Select";
import type { BalanceEditFormInstance } from "./useBalanceEditForm";
const RATE_LIMIT_INTERVALS = [
{ value: PurchaseLimitInterval.Hour, label: "Hour" },
{ value: PurchaseLimitInterval.Day, label: "Day" },
{ value: PurchaseLimitInterval.Week, label: "Week" },
{ value: PurchaseLimitInterval.Month, label: "Month" },
];
export function AutoTopUpSection({ form }: { form: BalanceEditFormInstance }) {
return (
<div className="flex flex-col gap-3">
<form.AppField name="autoTopUp.enabled">
{(field) => (
<field.AreaCheckboxField
title="Auto Top-Up"
description="Automatically purchase more credits when balance drops below a threshold."
/>
)}
</form.AppField>
<form.Field name="autoTopUp.enabled">
{(enabledField) =>
enabledField.state.value && (
<>
<div className="grid grid-cols-2 gap-3">
<form.AppField name="autoTopUp.threshold">
{(field) => (
<field.NumberField
label="Threshold"
description="Balance level that triggers a top-up"
placeholder="e.g. 10"
min={0}
float
/>
)}
</form.AppField>
<form.AppField name="autoTopUp.quantity">
{(field) => (
<field.NumberField
label="Quantity"
description="Credits added per top-up"
placeholder="e.g. 100"
min={1}
float
/>
)}
</form.AppField>
</div>
<form.AppField name="autoTopUp.maxPurchasesEnabled">
{(field) => (
<field.AreaCheckboxField
title="Rate Limit"
description="Limit how many auto top-ups can occur in a given interval."
/>
)}
</form.AppField>
<form.Field name="autoTopUp.maxPurchasesEnabled">
{(maxField) =>
maxField.state.value && (
<div className="grid grid-cols-2 gap-3">
<form.Field name="autoTopUp.interval">
{(field) => (
<div>
<div className="text-form-label block mb-1">
Interval
</div>
<p className="text-t3 text-xs mb-1">
Rate limit reset period
</p>
<Select
value={field.state.value}
onValueChange={(v) =>
field.handleChange(v as PurchaseLimitInterval)
}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{RATE_LIMIT_INTERVALS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</form.Field>
<form.AppField name="autoTopUp.maxPurchases">
{(field) => (
<field.NumberField
label="Max Purchases"
description="Top-ups allowed per interval"
placeholder="e.g. 5"
min={1}
/>
)}
</form.AppField>
</div>
)
}
</form.Field>
</>
)
}
</form.Field>
</div>
);
}

View File

@@ -1,12 +1,9 @@
import {
type AutoTopup,
computeGrantedBalanceInput,
type Entity,
type FullCusProduct,
type FullCustomerEntitlement,
type FullCustomerPrice,
isOneOffPrice,
isPrepaidPrice,
isUnlimitedCusEnt,
numberWithCommas,
} from "@autumn/shared";
@@ -23,14 +20,12 @@ import { LabelInput } from "@/components/v2/inputs/LabelInput";
import { SheetHeader, SheetSection } from "@/components/v2/sheets/InlineSheet";
import { useCustomerBalanceSheetStore } from "@/hooks/stores/useCustomerBalanceSheetStore";
import { useSheetStore } from "@/hooks/stores/useSheetStore";
import { CusService } from "@/services/customers/CusService";
import { useAxiosInstance } from "@/services/useAxiosInstance";
import { formatUnixToDateTime } from "@/utils/formatUtils/formatDateUtils";
import { getBackendErr, notNullish } from "@/utils/genUtils";
import { useCusQuery } from "@/views/customers/customer/hooks/useCusQuery";
import { InfoBox } from "@/views/onboarding2/integrate/components/InfoBox";
import { useCustomerContext } from "../../customer/CustomerContext";
import { AutoTopUpSection } from "./AutoTopUpSection";
import { BalanceEditPreviews } from "./BalanceEditPreviews";
import { GrantedBalancePopover } from "./GrantedBalancePopover";
import {
@@ -80,19 +75,6 @@ export function BalanceEditSheet() {
cp.price.entitlement_id === selectedCusEnt.entitlement.id,
);
const hasOneOffPrepaidPrice = cusPrice
? isOneOffPrice(cusPrice.price) && isPrepaidPrice(cusPrice.price)
: false;
const hasExistingAutoTopUp = customer?.auto_topups?.some(
(c: AutoTopup) => c.feature_id === featureId,
);
const isEligibleForAutoTopUp =
hasOneOffPrepaidPrice || !!hasExistingAutoTopUp;
const existingAutoTopUp =
customer?.auto_topups?.find((c: AutoTopup) => c.feature_id === featureId) ??
null;
return (
<div className="flex flex-col h-full">
<SheetHeader
@@ -119,8 +101,6 @@ export function BalanceEditSheet() {
cusProduct={cusProduct}
cusPrice={cusPrice}
featureId={featureId}
existingAutoTopUp={existingAutoTopUp}
isEligibleForAutoTopUp={isEligibleForAutoTopUp}
/>
)}
</div>
@@ -161,8 +141,6 @@ function BalanceEditForm({
cusProduct,
cusPrice,
featureId,
existingAutoTopUp,
isEligibleForAutoTopUp,
}: {
selectedCusEnt: FullCustomerEntitlement;
entityId: string | null;
@@ -170,13 +148,10 @@ function BalanceEditForm({
cusProduct: FullCusProduct | undefined;
cusPrice: FullCustomerPrice | undefined;
featureId: string;
existingAutoTopUp: AutoTopup | null;
isEligibleForAutoTopUp: boolean;
}) {
const form = useBalanceEditForm({
selectedCusEnt,
entityId,
existingAutoTopUp,
});
return (
@@ -190,7 +165,7 @@ function BalanceEditForm({
/>
</SheetSection>
<SheetSection withSeparator={isEligibleForAutoTopUp}>
<SheetSection withSeparator={false}>
<BalanceFields
form={form}
selectedCusEnt={selectedCusEnt}
@@ -198,12 +173,6 @@ function BalanceEditForm({
/>
</SheetSection>
{isEligibleForAutoTopUp && (
<SheetSection withSeparator={false}>
<AutoTopUpSection form={form} />
</SheetSection>
)}
<SubmitButton
form={form}
customer={customer}
@@ -550,40 +519,6 @@ function SubmitButton({
}
}
// Queue auto top-up update
if (hasAutoTopUpChanges({ form })) {
const autoTopUp = values.autoTopUp;
const newConfig: AutoTopup = {
feature_id: featureId,
enabled: autoTopUp.enabled,
threshold: autoTopUp.threshold ?? 0,
quantity: autoTopUp.quantity ?? 1,
...(autoTopUp.enabled &&
autoTopUp.maxPurchasesEnabled && {
purchase_limit: {
interval: autoTopUp.interval,
limit: autoTopUp.maxPurchases ?? 1,
},
}),
};
const otherConfigs = (customer.auto_topups ?? []).filter(
(c: AutoTopup) => c.feature_id !== featureId,
);
promises.push(
CusService.updateCustomer({
axios: axiosInstance,
customer_id: customer.id || customer.internal_id,
data: {
billing_controls: {
auto_topups: [...otherConfigs, newConfig],
},
},
}),
);
}
await Promise.all(promises);
toast.success("Updated successfully");
handleClose();
@@ -630,20 +565,3 @@ function hasBalanceChanges({
);
}
function hasAutoTopUpChanges({
form,
}: {
form: BalanceEditFormInstance;
}): boolean {
const meta = form.state.fieldMeta;
return (
meta["autoTopUp.enabled"]?.isDirty ||
meta["autoTopUp.threshold"]?.isDirty ||
meta["autoTopUp.quantity"]?.isDirty ||
meta["autoTopUp.maxPurchasesEnabled"]?.isDirty ||
meta["autoTopUp.interval"]?.isDirty ||
meta["autoTopUp.maxPurchases"]?.isDirty ||
false
);
}

View File

@@ -1,4 +1,3 @@
import { PurchaseLimitInterval } from "@autumn/shared";
import { z } from "zod/v4";
export const BalanceEditFormSchema = z
@@ -8,17 +7,9 @@ export const BalanceEditFormSchema = z
grantedAndPurchasedBalance: z.number().nullable(),
nextResetAt: z.number().nullable(),
addValue: z.number().nullable(),
autoTopUp: z.object({
enabled: z.boolean(),
threshold: z.number().min(0).nullable(),
quantity: z.number().min(1).nullable(),
maxPurchasesEnabled: z.boolean(),
interval: z.enum(PurchaseLimitInterval),
maxPurchases: z.number().min(1).nullable(),
}),
})
.check((ctx) => {
const { mode, balance, addValue, autoTopUp } = ctx.value;
const { mode, balance, addValue } = ctx.value;
if (mode === "set" && balance === null) {
ctx.issues.push({
@@ -37,35 +28,6 @@ export const BalanceEditFormSchema = z
input: addValue,
});
}
if (autoTopUp.enabled) {
if (autoTopUp.threshold === null || autoTopUp.threshold < 0) {
ctx.issues.push({
code: "custom",
message: "Threshold must be 0 or above",
path: ["autoTopUp", "threshold"],
input: autoTopUp.threshold,
});
}
if (autoTopUp.quantity === null || autoTopUp.quantity < 1) {
ctx.issues.push({
code: "custom",
message: "Quantity must be 1 or above",
path: ["autoTopUp", "quantity"],
input: autoTopUp.quantity,
});
}
if (autoTopUp.maxPurchasesEnabled) {
if (autoTopUp.maxPurchases === null || autoTopUp.maxPurchases < 1) {
ctx.issues.push({
code: "custom",
message: "Max purchases must be 1 or above",
path: ["autoTopUp", "maxPurchases"],
input: autoTopUp.maxPurchases,
});
}
}
}
});
export type BalanceEditForm = z.infer<typeof BalanceEditFormSchema>;

View File

@@ -1,11 +1,9 @@
import {
type AutoTopup,
cusEntsToBalance,
cusEntsToGrantedBalance,
cusEntsToPrepaidQuantity,
type FullCusEntWithFullCusProduct,
nullish,
PurchaseLimitInterval,
} from "@autumn/shared";
import { useAppForm } from "@/hooks/form/form";
import {
@@ -16,11 +14,9 @@ import {
export function useBalanceEditForm({
selectedCusEnt,
entityId,
existingAutoTopUp,
}: {
selectedCusEnt: FullCusEntWithFullCusProduct;
entityId: string | null;
existingAutoTopUp: AutoTopup | null;
}) {
const prepaidAllowance = cusEntsToPrepaidQuantity({
cusEnts: [selectedCusEnt],
@@ -47,16 +43,6 @@ export function useBalanceEditForm({
grantedAndPurchasedBalance: grantedAndPurchasedBalance ?? null,
nextResetAt: selectedCusEnt.next_reset_at ?? null,
addValue: null,
autoTopUp: {
enabled: existingAutoTopUp?.enabled ?? false,
threshold: existingAutoTopUp?.threshold ?? null,
quantity: existingAutoTopUp?.quantity ?? null,
maxPurchasesEnabled: !!existingAutoTopUp?.purchase_limit,
interval:
existingAutoTopUp?.purchase_limit?.interval ??
PurchaseLimitInterval.Month,
maxPurchases: existingAutoTopUp?.purchase_limit?.limit ?? null,
},
} as BalanceEditForm,
validators: {
onChange: BalanceEditFormSchema,

View File

@@ -5,7 +5,6 @@ import {
DropdownMenuItem,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { clearOrgCache } from "@/hooks/common/useOrg";
import { authClient, useSession } from "@/lib/auth-client";
import { getBackendErr, notNullish } from "@/utils/genUtils";
import { AdminOnly } from "@/views/admin/components/AdminOnly";
@@ -27,7 +26,6 @@ export const AdminDropdownItems = () => {
setStopImpersonatingLoading(true);
try {
await authClient.admin.stopImpersonating();
clearOrgCache();
window.location.reload();
} catch (error) {
toast.error(getBackendErr(error, "Failed to stop impersonation"));

View File

@@ -1,7 +1,6 @@
import { LogOut } from "lucide-react";
import React from "react";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { clearOrgCache } from "@/hooks/common/useOrg";
import { authClient } from "@/lib/auth-client";
export const LogOutItem = () => {
@@ -11,10 +10,10 @@ export const LogOutItem = () => {
onClick={async () => {
try {
await authClient.signOut();
clearOrgCache();
window.location.href = "/sign-in";
} catch (error) {
console.error("Error signing out:", error);
} finally {
window.location.href = "/sign-in";
}
}}
>

View File

@@ -27,11 +27,7 @@ import {
} from "@/components/ui/dropdown-menu";
import { Skeleton } from "@/components/ui/skeleton";
import { useTheme } from "@/contexts/ThemeProvider";
import {
clearOrgCache,
setLastSwitchedOrgId,
useOrg,
} from "@/hooks/common/useOrg";
import { setLastSwitchedOrgId, useOrg } from "@/hooks/common/useOrg";
import {
authClient,
useListOrganizations,
@@ -257,8 +253,6 @@ export const useOrgSwitch = () => {
organizationId: orgId,
});
clearOrgCache();
const { data: newOrg } = await axiosInstance.get("/organization");
if (newOrg?.id) setLastSwitchedOrgId(newOrg.id);