From 3d5988afd0b197f03afc49d271d1678a0e9e2968 Mon Sep 17 00:00:00 2001 From: Nora Date: Fri, 26 Jun 2026 06:42:08 -0600 Subject: [PATCH] =?UTF-8?q?fix:=20react-doctor=20async-defer-await=2010?= =?UTF-8?q?=E2=86=920=20(move=20await=20below=20guards),=20rerender-memo-w?= =?UTF-8?q?ith-default-value=2013=E2=86=920=20(module-scope=20EMPTY=5F*=20?= =?UTF-8?q?defaults)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/actions/auth-actions.ts | 6 +++--- src/actions/billing/stripe-checkout.ts | 3 ++- src/actions/checkout.ts | 6 ++++-- src/actions/integrations/credentials.ts | 6 ++++-- src/actions/stops.ts | 3 ++- src/actions/tax.ts | 6 ++++-- src/actions/water-log/field.ts | 3 ++- src/app/admin/import/ImportCenterClient.tsx | 4 +++- src/app/admin/wholesale/WholesaleClient.tsx | 7 ++++++- src/components/admin/AdminOrdersPanel.tsx | 12 +++++++++++- src/components/admin/BrandSettingsForm.tsx | 4 +++- src/components/admin/CommunicationsPage.tsx | 10 +++++++--- src/components/admin/NewProductForm.tsx | 4 +++- src/components/admin/PaymentSettingsForm.tsx | 4 +++- src/components/admin/ProductFormModal.tsx | 4 +++- src/components/admin/ProductsClient.tsx | 4 +++- src/components/admin/ShippingSettingsForm.tsx | 4 +++- src/components/route-trace/LotDetailPanel.tsx | 4 +++- 18 files changed, 69 insertions(+), 25 deletions(-) diff --git a/src/actions/auth-actions.ts b/src/actions/auth-actions.ts index 4cbc50d..5a547b6 100644 --- a/src/actions/auth-actions.ts +++ b/src/actions/auth-actions.ts @@ -83,12 +83,12 @@ export async function signInWithGoogleAction(input: { * into a fake session. */ export async function devLoginAction(role: string): Promise<{ success: boolean }> { - // `getSession()` is recognized by the server-auth-actions rule and - // returns null gracefully — dev logins don't have a session yet. - await getSession(); if (process.env.NODE_ENV === "production") { return { success: false }; } + // `getSession()` is recognized by the server-auth-actions rule and + // returns null gracefully — dev logins don't have a session yet. + await getSession(); const cookieStore = await cookies(); cookieStore.set("dev_session", role, { httpOnly: true, diff --git a/src/actions/billing/stripe-checkout.ts b/src/actions/billing/stripe-checkout.ts index 8494dc0..735f033 100644 --- a/src/actions/billing/stripe-checkout.ts +++ b/src/actions/billing/stripe-checkout.ts @@ -88,9 +88,10 @@ export async function createPlanUpgradeCheckout( billingPeriod?: "monthly" | "annual" ): Promise<{ success: boolean; url?: string; error?: string }> { -await getSession(); if (!["starter", "farm", "enterprise"].includes(planTier)) { + if (!["starter", "farm", "enterprise"].includes(planTier)) { return { success: false, error: "Invalid plan tier" }; } + await getSession(); const annual = billingPeriod === "annual"; return createStripeCheckoutSession( brandId, diff --git a/src/actions/checkout.ts b/src/actions/checkout.ts index 8f510e6..87fad4b 100644 --- a/src/actions/checkout.ts +++ b/src/actions/checkout.ts @@ -182,7 +182,8 @@ export async function mergeLocalCart( userId: string ): Promise<{ merged: CartItem[] }> { -await getSession(); if (!localCart || localCart.length === 0) return { merged: [] }; + if (!localCart || localCart.length === 0) return { merged: [] }; + await getSession(); // Fetch server cart let serverCart: CartItem[] = []; @@ -282,7 +283,8 @@ export async function checkStopProductAvailability( productIds: string[] ): Promise { -await getSession(); if (!productIds || productIds.length === 0) return []; + if (!productIds || productIds.length === 0) return []; + await getSession(); const { rows } = await pool.query<{ check_stop_product_availability: ProductAvailability[] | null }>( `SELECT check_stop_product_availability($1, $2::uuid[]) AS "check_stop_product_availability"`, [stopId, productIds], diff --git a/src/actions/integrations/credentials.ts b/src/actions/integrations/credentials.ts index 10654cd..e7318e7 100644 --- a/src/actions/integrations/credentials.ts +++ b/src/actions/integrations/credentials.ts @@ -155,9 +155,10 @@ export async function testResendConnection(apiKey: string): Promise<{ message: string; }> { -await getSession(); if (!apiKey?.trim()) { + if (!apiKey?.trim()) { return { ok: false, message: "API key is required" }; } + await getSession(); try { const response = await fetch("https://api.resend.com/domains", { @@ -184,9 +185,10 @@ export async function testTwilioConnection( authToken: string ): Promise<{ ok: boolean; message: string }> { -await getSession(); if (!accountSid?.trim() || !authToken?.trim()) { + if (!accountSid?.trim() || !authToken?.trim()) { return { ok: false, message: "Account SID and Auth Token are required" }; } + await getSession(); try { const credentials = Buffer.from(`${accountSid}:${authToken}`).toString("base64"); diff --git a/src/actions/stops.ts b/src/actions/stops.ts index 5b0fa2e..2f68582 100644 --- a/src/actions/stops.ts +++ b/src/actions/stops.ts @@ -192,7 +192,8 @@ export async function getPublicStopsForBrand( brandSlug: string ): Promise { -await getSession(); if (!brandSlug) return []; + if (!brandSlug) return []; + await getSession(); // Wrapped in try/catch so a build-time DB outage (ECONNREFUSED) doesn't // crash the prerender — the page just renders with no stops and diff --git a/src/actions/tax.ts b/src/actions/tax.ts index 71bdb8e..18958db 100644 --- a/src/actions/tax.ts +++ b/src/actions/tax.ts @@ -203,7 +203,8 @@ export async function getTaxSummaryAction(params: { endDate: string; }): Promise { -await getSession(); if (!params.brandId) return { success: false, error: "Brand required" }; + if (!params.brandId) return { success: false, error: "Brand required" }; + await getSession(); try { const { rows } = await pool.query<{ total_tax_collected: number | string; @@ -239,7 +240,8 @@ export async function getTaxableOrdersAction(params: { endDate: string; }): Promise { -await getSession(); if (!params.brandId) return { success: false, error: "Brand required" }; + if (!params.brandId) return { success: false, error: "Brand required" }; + await getSession(); try { const { rows } = await pool.query<{ order_id: string; diff --git a/src/actions/water-log/field.ts b/src/actions/water-log/field.ts index 67c707c..30ad0b4 100644 --- a/src/actions/water-log/field.ts +++ b/src/actions/water-log/field.ts @@ -435,7 +435,8 @@ await getSession(); const cookieStore = await cookies(); export async function setWaterLang(lang: string): Promise { -await getSession(); if (!["en", "es"].includes(lang)) return; + if (!["en", "es"].includes(lang)) return; + await getSession(); const cookieStore = await cookies(); cookieStore.set("wl_lang", lang, { httpOnly: false, diff --git a/src/app/admin/import/ImportCenterClient.tsx b/src/app/admin/import/ImportCenterClient.tsx index d0241da..24f6974 100644 --- a/src/app/admin/import/ImportCenterClient.tsx +++ b/src/app/admin/import/ImportCenterClient.tsx @@ -15,6 +15,8 @@ const ENTITY_LABELS: Record = { const analysisLabels = ["Reading your file...", "AI is mapping columns...", "Cleaning and normalizing data...", "Finalizing preview..."]; +const EMPTY_BRANDS: { id: string; name: string }[] = []; + const ALL_FIELDS = ["ignore", "product_name", "price", "description", "product_type", "active", "image_url", "customer_name", "customer_email", "customer_phone", "stop_id", "quantity", "fulfillment", "product_id", "first_name", "last_name", "full_name", "tags", "email_opt_in", "sms_opt_in", "external_id", @@ -29,7 +31,7 @@ type Props = { isPlatformAdmin?: boolean; }; -export default function ImportCenterClient({ brandId: initialBrandId, brandName: initialBrandName, brands = [], isPlatformAdmin = false }: Props) { +export default function ImportCenterClient({ brandId: initialBrandId, brandName: initialBrandName, brands = EMPTY_BRANDS, isPlatformAdmin = false }: Props) { const [activeBrandId, setActiveBrandId] = useState(initialBrandId); // Brand name is derived from the brands list — no separate state, so // updates to it don't trigger an extra render of the whole component. diff --git a/src/app/admin/wholesale/WholesaleClient.tsx b/src/app/admin/wholesale/WholesaleClient.tsx index ff6adc0..21d6bfd 100644 --- a/src/app/admin/wholesale/WholesaleClient.tsx +++ b/src/app/admin/wholesale/WholesaleClient.tsx @@ -357,7 +357,12 @@ function DashboardTab({ stats, recentOrders, brandId, onMsg, webhookActivity }: // ── Customers Tab ──────────────────────────────────────────────────────────── -function CustomersTab({ customers, products, brandId, onMsg, registrations = [], onRefresh }: { +const EMPTY_REGISTRATIONS: Array<{ + id: string; company_name: string | null; contact_name: string | null; + email: string; phone: string | null; account_status: string; created_at: string; +}> = []; + +function CustomersTab({ customers, products, brandId, onMsg, registrations = EMPTY_REGISTRATIONS, onRefresh }: { customers: WholesaleCustomer[]; products: WholesaleProduct[]; brandId: string; diff --git a/src/components/admin/AdminOrdersPanel.tsx b/src/components/admin/AdminOrdersPanel.tsx index 9caf85f..7760597 100644 --- a/src/components/admin/AdminOrdersPanel.tsx +++ b/src/components/admin/AdminOrdersPanel.tsx @@ -56,6 +56,14 @@ type Order = { order_items?: OrderItem[]; }; +type Product = { + id: string; + name: string; + price: number; + type?: string | null; + active?: boolean; +}; + type Stop = { id: string; city: string; @@ -91,10 +99,12 @@ const ChevronDownIcon = () => ; const ChevronRightIcon = () => ; +const EMPTY_PRODUCTS: Product[] = []; + export default function AdminOrdersPanel({ initialOrders, initialStops, - initialProducts = [], + initialProducts = EMPTY_PRODUCTS, brandId, }: AdminOrdersPanelProps) { const { success: showSuccess, error: showError } = useToast(); diff --git a/src/components/admin/BrandSettingsForm.tsx b/src/components/admin/BrandSettingsForm.tsx index 8235aa1..9fdcfb8 100644 --- a/src/components/admin/BrandSettingsForm.tsx +++ b/src/components/admin/BrandSettingsForm.tsx @@ -20,6 +20,8 @@ type Props = { isPlatformAdmin?: boolean; }; +const EMPTY_BRANDS: { id: string; name: string }[] = []; + export default function BrandSettingsForm(props: Props) { // Non-platform-admin case: the brand never changes, so we can pass props // straight through (key forces the body to remount if the outer ever @@ -41,7 +43,7 @@ function PlatformAdminBrandSettingsForm({ settings: _initialSettings, brandId: initialBrandId, brandName: initialBrandName, - brands = [], + brands = EMPTY_BRANDS, isPlatformAdmin: _isPlatformAdmin, }: Props) { // Use lazy initializers so the lint's static "useState(prop)" check diff --git a/src/components/admin/CommunicationsPage.tsx b/src/components/admin/CommunicationsPage.tsx index 5a33a89..f4fa2d7 100644 --- a/src/components/admin/CommunicationsPage.tsx +++ b/src/components/admin/CommunicationsPage.tsx @@ -86,6 +86,10 @@ const ChartIcon = () => ( ); +const EMPTY_CONTACTS: Contact[] = []; +const EMPTY_SEGMENTS: Segment[] = []; +const EMPTY_ANALYTICS: CampaignAnalytics[] = []; + export default function CommunicationsPage({ campaigns, templates, @@ -93,10 +97,10 @@ export default function CommunicationsPage({ editCampaign, editMode, editTemplate, - initialContacts = [], + initialContacts = EMPTY_CONTACTS, initialContactTotal = 0, - initialSegments = [], - initialAnalytics = [], + initialSegments = EMPTY_SEGMENTS, + initialAnalytics = EMPTY_ANALYTICS, editCampaignId, initialTab, }: { diff --git a/src/components/admin/NewProductForm.tsx b/src/components/admin/NewProductForm.tsx index ebc4703..a4d2c1e 100644 --- a/src/components/admin/NewProductForm.tsx +++ b/src/components/admin/NewProductForm.tsx @@ -41,7 +41,9 @@ type Props = { lockBrand?: boolean; }; -export default function NewProductForm({ defaultBrandId = "", brands = [], lockBrand = false }: Props) { +const EMPTY_BRANDS: { id: string; name: string }[] = []; + +export default function NewProductForm({ defaultBrandId = "", brands = EMPTY_BRANDS, lockBrand = false }: Props) { const router = useRouter(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); diff --git a/src/components/admin/PaymentSettingsForm.tsx b/src/components/admin/PaymentSettingsForm.tsx index ec3bacd..53acd9c 100644 --- a/src/components/admin/PaymentSettingsForm.tsx +++ b/src/components/admin/PaymentSettingsForm.tsx @@ -33,7 +33,9 @@ interface ValidationErrors { squareLocationId?: string; } -export default function PaymentSettingsForm({ settings, brandId, brands = [], isPlatformAdmin = false }: Props) { +const EMPTY_BRANDS: { id: string; name: string }[] = []; + +export default function PaymentSettingsForm({ settings, brandId, brands = EMPTY_BRANDS, isPlatformAdmin = false }: Props) { // Use lazy initializers so the lint's static "useState(prop)" check does // not fire; the values are computed on first render and then mutated // locally as the user edits. When the parent passes a new prop, the diff --git a/src/components/admin/ProductFormModal.tsx b/src/components/admin/ProductFormModal.tsx index de540dc..cfaebf3 100644 --- a/src/components/admin/ProductFormModal.tsx +++ b/src/components/admin/ProductFormModal.tsx @@ -79,13 +79,15 @@ const TYPE_OPTIONS: Array<{ }, ]; +const EMPTY_BRANDS: { id: string; name: string }[] = []; + export default function ProductFormModal({ open, mode, onClose, onSubmit, initial, - brands = [], + brands = EMPTY_BRANDS, lockBrand = false, lockedBrandId = "", initialImageUrl = null, diff --git a/src/components/admin/ProductsClient.tsx b/src/components/admin/ProductsClient.tsx index 99de814..f76cf20 100644 --- a/src/components/admin/ProductsClient.tsx +++ b/src/components/admin/ProductsClient.tsx @@ -64,10 +64,12 @@ async function resizeImage(file: File, maxWidth: number): Promise { }); } +const EMPTY_BRANDS: { id: string; name: string }[] = []; + export default function ProductsClient({ products, brandId, - brands = [], + brands = EMPTY_BRANDS, isPlatformAdmin = false, }: { products: Product[]; diff --git a/src/components/admin/ShippingSettingsForm.tsx b/src/components/admin/ShippingSettingsForm.tsx index 1b2c763..e977a22 100644 --- a/src/components/admin/ShippingSettingsForm.tsx +++ b/src/components/admin/ShippingSettingsForm.tsx @@ -31,6 +31,8 @@ interface ValidationErrors { fedexApiSecret?: string; } +const EMPTY_BRANDS: { id: string; name: string }[] = []; + export default function ShippingSettingsForm(props: Props) { // Non-platform-admin case: brand is fixed, just pass props through. // The body's `key` forces a remount if the brand id ever changes. @@ -49,7 +51,7 @@ export default function ShippingSettingsForm(props: Props) { function PlatformAdminShippingSettingsForm({ settings: _initialSettings, brandId: initialBrandId, - brands = [], + brands = EMPTY_BRANDS, isPlatformAdmin: _isPlatformAdmin, }: Props) { // Lazy initializers so the lint's static "useState(prop)" check does not fire. diff --git a/src/components/route-trace/LotDetailPanel.tsx b/src/components/route-trace/LotDetailPanel.tsx index bcdb227..3c01553 100644 --- a/src/components/route-trace/LotDetailPanel.tsx +++ b/src/components/route-trace/LotDetailPanel.tsx @@ -211,10 +211,12 @@ function CheckIcon({ className }: { className?: string }) { ); } +const EMPTY_ORDERS: LotOrder[] = []; + export default function LotDetailPanel({ lot, brandId, - orders = [], + orders = EMPTY_ORDERS, }: { lot: LotDetail; brandId: string;