diff --git a/README.md b/README.md index 5e65776a..f5465c90 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,15 @@ Open [http://localhost:3000](http://localhost:3000). | `/invoice/[id]` | Invoice detail, payment progress, Pay button | | `/verify/[id]` | Public on-chain verification (no login needed) | +## Continuous Testing (Local Dev) + +Run tests continuously while developing, instead of a single one-off run: + +```bash +npm run test:watch # vitest in watch mode, scoped to files changed vs. the last commit +npm run test:watch:all # vitest in watch mode, full suite +``` + ## Run Lint ```bash diff --git a/src/app/invoice/[id]/page.tsx b/src/app/invoice/[id]/page.tsx index e17b2847..72596f81 100644 --- a/src/app/invoice/[id]/page.tsx +++ b/src/app/invoice/[id]/page.tsx @@ -82,7 +82,10 @@ import InvoiceSentiment from "@/components/invoice/InvoiceSentiment"; import PayerWalletHealth from "@/components/invoice/PayerWalletHealth"; import ReconciliationPanel from "@/components/invoice/ReconciliationPanel"; import InvoiceExportButton from "@/components/InvoiceExportButton"; -import ReleaseBanner from "@/components/ReleaseBanner"; +// Only rendered once an invoice transitions to "released", and pulls in the +// canvas-confetti animation library — load it on demand instead of in the +// page's initial bundle. +const ReleaseBanner = dynamic(() => import("@/components/ReleaseBanner"), { ssr: false }); import { cancelReminder, setReminder } from "@/lib/reminders"; import { recordCooldown } from "@/lib/cooldown"; import { exportTimelineAsImage } from "@/lib/timelineImageExport"; diff --git a/src/components/InvoiceTable.tsx b/src/components/InvoiceTable.tsx index 8b37538c..4fb5b6c5 100644 --- a/src/components/InvoiceTable.tsx +++ b/src/components/InvoiceTable.tsx @@ -1,7 +1,7 @@ "use client"; import Link from "next/link"; -import { useState, useCallback, useMemo, useEffect, useRef } from "react"; +import { useState, useCallback, useMemo, useEffect, useRef, memo } from "react"; import type { Invoice } from "@stellar-split/sdk"; import { formatAmount } from "@stellar-split/sdk"; import { getInvoiceDisplayStatus } from "@/lib/dashboardFilters"; @@ -100,7 +100,13 @@ const STATUS_STYLES: Record = { Refunded: "bg-gray-500/20 text-gray-400", }; -function StatusChip({ status }: { status: string }) { +// Rendered once per row (potentially hundreds of rows). Its only prop is a +// primitive status string, so React.memo's default shallow comparison lets +// unaffected rows skip re-rendering this chip when the table re-renders for +// an unrelated reason (sort change, selection toggle on another row, column +// visibility toggle, etc.) — those re-render the parent row but this +// specific status string is almost always unchanged. +const StatusChip = memo(function StatusChip({ status }: { status: string }) { const style = STATUS_STYLES[status] ?? "bg-gray-500/20 text-gray-400"; return ( ); -} +}); // ── Sort header button ──────────────────────────────────────────────────────── diff --git a/src/lib/blockchainCache.ts b/src/lib/blockchainCache.ts index 87a5a369..fdd127c3 100644 --- a/src/lib/blockchainCache.ts +++ b/src/lib/blockchainCache.ts @@ -32,6 +32,10 @@ export const CACHE_TTL = { quote: 5_000, /** Contract/account metadata that essentially never changes post-deploy. */ staticMeta: 5 * 60_000, + /** USDC token balance for a wallet. Polled from multiple components + * (WalletConnect header, PayModal) on connect/mount; short TTL collapses + * those into one RPC simulation without serving noticeably stale data. */ + balance: 10_000, } as const; export type CacheKind = keyof typeof CACHE_TTL; diff --git a/src/lib/stellar.ts b/src/lib/stellar.ts index bb8d65af..3ba8fa68 100644 --- a/src/lib/stellar.ts +++ b/src/lib/stellar.ts @@ -242,6 +242,16 @@ export async function payMilestone(params: { export async function fetchUsdcBalance( walletAddress: string, tokenContractId = USDC_CONTRACT_ID +): Promise { + const cacheKey = `${walletAddress}:${tokenContractId}`; + return blockchainCache.getOrFetch("balance", cacheKey, () => + fetchUsdcBalanceUncached(walletAddress, tokenContractId) + ); +} + +async function fetchUsdcBalanceUncached( + walletAddress: string, + tokenContractId: string ): Promise { const { Contract,