Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion src/app/invoice/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
12 changes: 9 additions & 3 deletions src/components/InvoiceTable.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -100,7 +100,13 @@ const STATUS_STYLES: Record<string, string> = {
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 (
<span
Expand All @@ -110,7 +116,7 @@ function StatusChip({ status }: { status: string }) {
{status}
</span>
);
}
});

// ── Sort header button ────────────────────────────────────────────────────────

Expand Down
4 changes: 4 additions & 0 deletions src/lib/blockchainCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions src/lib/stellar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,16 @@ export async function payMilestone(params: {
export async function fetchUsdcBalance(
walletAddress: string,
tokenContractId = USDC_CONTRACT_ID
): Promise<bigint> {
const cacheKey = `${walletAddress}:${tokenContractId}`;
return blockchainCache.getOrFetch("balance", cacheKey, () =>
fetchUsdcBalanceUncached(walletAddress, tokenContractId)
);
}

async function fetchUsdcBalanceUncached(
walletAddress: string,
tokenContractId: string
): Promise<bigint> {
const {
Contract,
Expand Down
Loading