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
215 changes: 215 additions & 0 deletions docs/BillingHistoryPreview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
# BillingHistory Preview Card

**Campaign:** GrantFox FWC26
**Issue:** UI/UX — hover-triggered preview card on BillingHistory; keyboard-accessible alternative
**Route:** `/billing/history`
**Files changed:**
- `src/components/PreviewCard.tsx` — extended with billing-specific fields
- `src/pages/BillingHistory.tsx` — new page component
- `src/App.tsx` — route + navigation entries added

---

## Overview

The Billing History page displays a paginated table of USDC transactions. Each row's description cell is wrapped in a **PreviewCard** trigger. Hovering or focusing the trigger opens a compact floating panel that reveals on-chain details — transaction hash, network, confirmation count, direction badge, formatted amount, and timestamp — without requiring the user to navigate to a separate page.

---

## User flows

### Mouse user

1. Visit `/billing/history`.
2. Hover the description text of any transaction row.
3. A tooltip-style card appears below the cell with:
- Transaction type + credit/debit badge
- Formatted USDC amount (e.g. `+100.00 USDC`)
- Network name (`Stellar Mainnet`)
- Confirmation count
- Truncated tx hash (full hash available in the `title` attribute)
- Formatted timestamp
4. Moving the cursor off the row closes the card.

### Keyboard user

1. Tab to a transaction row's description trigger.
2. The preview card opens automatically on focus.
3. Read the on-chain details via the `aria-describedby` association or by listening to the `role="tooltip"` announcement.
4. Press **Escape** to dismiss the card and return focus to the trigger.
5. Tab again to move to the next row (card closes automatically on blur).

---

## Filtering

The page exposes four filter controls above the table:

| Control | Values |
|---------|--------|
| Search | Free text (description or tx hash prefix) |
| Type | All / Deposit / API Call / Refund / Settlement / Fee |
| Status | All / success / pending / error / warning |
| Direction | All / Credit / Debit |

Filters are combinable. A live region (`role="status" aria-live="polite"`) announces the active filter state to screen readers after each change.

When no transactions match, a clearly labelled empty-state message replaces the table.

---

## API — PreviewCard

`PreviewCard` is a shared component (`src/components/PreviewCard.tsx`). It accepts a `PreviewCardData` object, a trigger as `children`, an optional `position` prop, and an optional `className`.

### `PreviewCardData` — complete type

```ts
export interface PreviewCardData {
id: string;
title: string;

// — Shared (dashboard overview + billing history)
subtitle?: string;
category?: string;
status?: StatusVariant; // See StatusBadge
description?: string;
metrics?: PreviewMetric[]; // { label, value }[]
tags?: string[];
price?: string | number;
lastActive?: string;
details?: Record<string, string | number>;

// — Billing-specific (GrantFox FWC26)
txHash?: string; // Full 64-char Stellar hash
network?: string; // e.g. "Stellar Mainnet"
confirmations?: number; // Ledger confirmation count
type?: string; // "Deposit" | "API Call" | etc.
timestamp?: string; // ISO 8601
amount?: number; // USDC value (positive)
direction?: 'credit' | 'debit';
}
```

**Mode detection:** if any of `txHash`, `network`, `confirmations`, `type`, or `amount` is present, the card renders the billing-specific section and suppresses the generic `metrics` grid and `price/lastActive` footer. Existing callers (DashboardOverview) are unaffected.

### `PreviewCardProps`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `data` | `PreviewCardData` | required | Card content |
| `children` | `ReactNode` | required | Hover/focus trigger element |
| `position` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'bottom'` | Floating panel placement |
| `className` | `string` | `''` | Extra class on the wrapper `div` |

### Usage example — billing row

```tsx
import PreviewCard, { type PreviewCardData } from '../components/PreviewCard';

const data: PreviewCardData = {
id: 'tx-001',
title: 'USDC vault deposit',
status: 'success',
type: 'Deposit',
direction: 'credit',
amount: 100.00,
txHash: 'A3F9B2C1D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6C7D8E9F0A1',
network: 'Stellar Mainnet',
confirmations: 120,
timestamp: '2026-07-25T14:32:00Z',
};

<PreviewCard data={data} position="bottom">
<span>USDC vault deposit</span>
</PreviewCard>
```

---

## API — BillingHistory page

`BillingHistory` is a default-exported React component at `src/pages/BillingHistory.tsx`. It requires no props — all data comes from the internal `MOCK_TRANSACTIONS` constant.

### Exports

| Name | Kind | Description |
|------|------|-------------|
| `BillingHistory` | named + default export | Page component |
| `MOCK_TRANSACTIONS` | named export | Array of `BillingTransaction` for use in tests and Storybook |
| `BillingTransaction` | type | Shape of a single transaction record |
| `TxType` | type | `'Deposit' \| 'API Call' \| 'Refund' \| 'Settlement' \| 'Fee'` |
| `TxDirection` | type | `'credit' \| 'debit'` |
| `TxStatus` | type | Subset of `StatusVariant` used for transactions |

---

## Accessibility notes (WCAG 2.1 AA)

| Criterion | Implementation |
|-----------|----------------|
| **1.1.1 Non-text content** | Direction arrows are `aria-hidden`; information is also in the `direction` badge text. |
| **1.3.1 Info and Relationships** | `<table>` with `<thead>` / `<th scope="col">` / `<tbody>` exposes structure. |
| **1.3.3 Sensory Characteristics** | Status is never conveyed by colour alone — `StatusBadge` adds a texture pattern. |
| **1.4.1 Use of Color** | Credit/debit is labelled with text ("↑ credit" / "↓ debit") in addition to colour. |
| **1.4.3 Contrast** | All colours reference design tokens; both dark and light themes pass 4.5:1 minimum. |
| **2.1.1 Keyboard** | All PreviewCard triggers are `tabIndex={0}` with `role="button"`. Escape dismisses the preview. |
| **2.4.3 Focus Order** | Escape restores focus to the trigger; `suppressNextFocus` guard prevents immediate re-open. |
| **2.4.6 Headings and Labels** | Page heading (`<h1>`), filter group (`role="group" aria-label`), and table all have labels. |
| **4.1.2 Name, Role, Value** | Trigger: `role="button"`, `aria-label`, `aria-describedby`. Panel: `role="tooltip"`, `aria-label`. |
| **4.1.3 Status Messages** | Filter changes announced via `role="status" aria-live="polite" aria-atomic="true"`. |

### Skip-link support

The page lives under `<main id="main-content">` which the app-level skip link already targets — no additional work needed.

---

## Design-token usage

All colours in `PreviewCard` and `BillingHistory` use CSS custom properties so both light and dark themes are covered automatically.

| Token | Used for |
|-------|----------|
| `--surface` | Preview card background |
| `--line` / `--border-color` | Preview card and table borders |
| `--shadow` | Preview card drop shadow |
| `--text-primary` / `--text` | Primary text |
| `--text-secondary` / `--muted` | Secondary / muted text |
| `--accent` | Tx hash, confirmation count, accent values |
| `--success` | Credit amounts, high-confirmation count |
| `--danger` | Debit amounts, error states |
| `--bg-chip` | Billing details section background |
| `--sb-success-bg/fg` | Credit direction badge |
| `--sb-error-bg/fg` | Debit direction badge |

---

## Responsive behaviour

- The table container uses `overflow-x: auto` so horizontal scrolling is available on small viewports.
- The preview card has `maxWidth: 90vw` — it never overflows the viewport on narrow screens.
- Filter controls use `flexWrap: wrap` so they stack on small screens without clipping.
- The page heading uses `clamp(1.35rem, 3vw, 1.75rem)` for fluid sizing.

---

## Running the tests

```bash
# New tests only
npm test -- --run src/components/PreviewCard.test.tsx src/pages/BillingHistory.test.tsx

# Full suite
npm test -- --run
```

Expected result: **56 new tests pass** (31 BillingHistory + 25 PreviewCard).

---

## Related docs

- [UI Design System](./UI-Design-System.md) — design tokens and component guidelines
- [ResponseDiff](./ResponseDiff.md) — similar hover/focus pattern on `CallHistoryRow`
- `src/components/EndpointPreview.tsx` — the original hover preview pattern this feature follows
10 changes: 10 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import PublishApi from "./pages/PublishApi";
import { startRouteLoading, stopRouteLoading } from "./hooks/useRouteLoading";
import { formatUsdc, formatUsdShortcut } from "./utils/format";
import DepositPreview from "./components/DepositPreview";

Check failure on line 15 in src/App.tsx

View workflow job for this annotation

GitHub Actions / TypeScript check & production build

'DepositPreview' is declared but its value is never read.
import { EXPLORER_BASE_URL, MIN_DEPOSIT, NETWORK_FEE, PRESET_AMOUNTS } from "./config/constants";
import CompareDrawer from "./components/CompareDrawer";
import CompareTray from "./components/CompareTray";
Expand All @@ -25,6 +25,7 @@
import { ShortcutsModal } from "./components/ShortcutsModal";
import { ToastProvider } from "./components/Toast";
import { InvoiceCard } from "./pages/InvoiceCard";
import BillingHistory from "./pages/BillingHistory";

type DepositStage = "input" | "approving" | "pending" | "confirmed" | "failed";
type DemoOutcome = "confirmed" | "failed";
Expand Down Expand Up @@ -117,6 +118,7 @@
planBadge: "/apis/plan-badge",
apiUsage: "/api-usage",
billing: "/billing",
billingHistory: "/billing/history",
documentation: "/documentation",
status: "/status",
themePlayground: "/theme-playground",
Expand Down Expand Up @@ -277,17 +279,19 @@
[APP_ROUTES.dashboard]: "Dashboard – Callora",
[APP_ROUTES.myApis]: "My APIs – Callora",
[APP_ROUTES.billing]: "Billing – Callora",
[APP_ROUTES.billingHistory]: "Billing History – Callora",
"/api-usage": "API Usage – Callora",
[APP_ROUTES.landing]: "Callora",
[APP_ROUTES.endpointSummary]: "Endpoint Summary – Callora",

Check failure on line 285 in src/App.tsx

View workflow job for this annotation

GitHub Actions / TypeScript check & production build

Property 'endpointSummary' does not exist on type '{ readonly landing: "/"; readonly dashboard: "/dashboard"; readonly marketplace: "/marketplace"; readonly publish: "/publish"; readonly myApis: "/apis/my-apis"; readonly planBadge: "/apis/plan-badge"; ... 8 more ...; readonly rateLimitCard: "/rate-limit"; }'.
};
const routeDescriptionMap: Record<string, string> = {
[APP_ROUTES.marketplace]: "Explore APIs on the Callora marketplace, discover and integrate APIs for your applications.",
[APP_ROUTES.dashboard]: "Your Callora dashboard showing balances, recent activity and quick actions.",
[APP_ROUTES.billing]: "Manage your USDC vault, deposit funds, and view transaction status.",
[APP_ROUTES.billingHistory]: "View your full USDC transaction history with on-chain details, filters, and hover previews.",
"/api-usage": "Monitor API usage, request stats, and view call history.",
[APP_ROUTES.landing]: "Callora - Programmable API Access, pay-per-call billing, and on-chain settlement.",
[APP_ROUTES.endpointSummary]: "Quick reference list of all API endpoints on Callora.",

Check failure on line 294 in src/App.tsx

View workflow job for this annotation

GitHub Actions / TypeScript check & production build

Property 'endpointSummary' does not exist on type '{ readonly landing: "/"; readonly dashboard: "/dashboard"; readonly marketplace: "/marketplace"; readonly publish: "/publish"; readonly myApis: "/apis/my-apis"; readonly planBadge: "/apis/plan-badge"; ... 8 more ...; readonly rateLimitCard: "/rate-limit"; }'.
};
const currentTitle = routeTitleMap[location.pathname] ?? "Callora";
const currentDescription = routeDescriptionMap[location.pathname];
Expand All @@ -300,7 +304,7 @@
const [amountInput, setAmountInput] = useState("50");
const [selectedPreset, setSelectedPreset] = useState<number | "custom">(50);

const [isTouchDevice, setIsTouchDevice] = useState(false);

Check failure on line 307 in src/App.tsx

View workflow job for this annotation

GitHub Actions / TypeScript check & production build

'isTouchDevice' is declared but its value is never read.
useEffect(() => {
setIsTouchDevice('ontouchstart' in window || navigator.maxTouchPoints > 0);
}, []);
Expand Down Expand Up @@ -554,6 +558,9 @@
<NavLink to={APP_ROUTES.billing} className={({ isActive }) => (isActive ? "link-nav active" : "link-nav")}>
Billing
</NavLink>
<NavLink to={APP_ROUTES.billingHistory} className={({ isActive }) => (isActive ? "link-nav active" : "link-nav")}>
Billing History
</NavLink>
<NavLink to={APP_ROUTES.themePlayground} className={({ isActive }) => (isActive ? "link-nav active" : "link-nav")}>
Theme Playground
</NavLink>
Expand Down Expand Up @@ -595,7 +602,7 @@
<p className="eyebrow">Deposit USDC to Vault</p>
<h1>Review every number before you approve.</h1>
</div>
<button className="primary-button no-print" onClick={openDeposit}>

Check failure on line 605 in src/App.tsx

View workflow job for this annotation

GitHub Actions / TypeScript check & production build

Type '(presetAmount?: number) => void' is not assignable to type 'MouseEventHandler<HTMLButtonElement>'.
Open deposit modal
</button>
</div>
Expand Down Expand Up @@ -685,6 +692,9 @@

<Route path={APP_ROUTES.rateLimitCard} element={<RateLimitCard />} />

{/* ── Billing History (FWC26) ──────────────────────────────── */}
<Route path={APP_ROUTES.billingHistory} element={<BillingHistory />} />

<Route path="*" element={<NotFound onGoHome={() => navigate(APP_ROUTES.dashboard)} />} />
</Routes>
</main>
Expand Down
Loading
Loading