Skip to content
Open
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
33 changes: 32 additions & 1 deletion src/components/TransactionStatus.patterns.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const BASE: Omit<Transaction, "status" | "message"> = {
const pending: Transaction = { ...BASE, status: "pending" };
const success: Transaction = { ...BASE, status: "success" };
const error: Transaction = { ...BASE, status: "error", message: "Insufficient funds" };
const stale: Transaction = { ...BASE, status: "stale" };

// ─── Helper ──────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -139,7 +140,27 @@ describe("TransactionStatus — pattern fills (WCAG 1.4.1)", () => {
});
});

// ─── 4. data-status attribute ──────────────────────────────────────────────

// ─── 3.5 Stale ─────────────────────────────────────────────────────────────
describe("stale status", () => {
it("3.5a. icon-bg carries the colour-tint class (dc-status-icon-bg--accent)", () => {
render(<TransactionStatus transaction={stale} onNewDraw={() => {}} />);
expect(getIconBg("stale")).toHaveClass("dc-status-icon-bg--accent");
});

it("3.5b. icon-bg carries the geometry-pattern class (dc-status-icon-bg--pattern-pending)", () => {
render(<TransactionStatus transaction={stale} onNewDraw={() => {}} />);
expect(getIconBg("stale")).toHaveClass("dc-status-icon-bg--pattern-pending");
});

it("3.5c. renders the refresh button when onRefresh is provided", () => {
const { unmount } = render(<TransactionStatus transaction={stale} onNewDraw={() => {}} onRefresh={() => {}} />);
expect(screen.getByRole("button", { name: /refresh status/i })).toBeInTheDocument();
unmount();
});
});

// ─── 4. data-status attribute ──────────────────────────────────────────────

describe("data-status attribute", () => {
it("4a. pending transaction sets data-status='pending'", () => {
Expand All @@ -156,6 +177,11 @@ describe("TransactionStatus — pattern fills (WCAG 1.4.1)", () => {
render(<TransactionStatus transaction={error} onNewDraw={() => {}} />);
expect(document.querySelector("[data-status='error']")).toBeInTheDocument();
});

it("4d. stale transaction sets data-status='stale'", () => {
render(<TransactionStatus transaction={stale} onNewDraw={() => {}} />);
expect(document.querySelector("[data-status='stale']")).toBeInTheDocument();
});
});

// ─── 5. Screen-reader / WCAG 4.1.3 status message ─────────────────────────
Expand Down Expand Up @@ -200,6 +226,11 @@ describe("TransactionStatus — pattern fills (WCAG 1.4.1)", () => {
render(<TransactionStatus transaction={errorNoMsg} onNewDraw={() => {}} />);
expect(screen.getByText(/an error occurred/i)).toBeInTheDocument();
});

it("6f. stale renders 'Status Unknown' heading", () => {
render(<TransactionStatus transaction={stale} onNewDraw={() => {}} />);
expect(screen.getByRole("heading", { name: /status unknown/i })).toBeInTheDocument();
});
});

// ─── 7. Icon aria-hidden ───────────────────────────────────────────────────
Expand Down
88 changes: 23 additions & 65 deletions src/components/TransactionStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,85 +1,48 @@
/**
* TransactionStatus
*
* Step 4 (final) of the draw-credit flow. Shows the outcome of the draw
* request: pending / success / error — with a transaction detail card and a
* "Make Another Draw" reset button.
*
* Design-token classes used (all from `src/index.css` `.dc-*` block):
* dc-spinner-wrap (reused for centred text layout),
* dc-status-icon-bg, dc-status-icon-bg--accent/success/error,
* dc-status-icon, dc-status-icon--accent/success/error,
* dc-step__title, dc-step__subtitle,
* dc-status-detail-card, dc-status-detail-row,
* dc-status-detail-row__label, dc-status-detail-row__value,
* dc-status-detail-row__value--mono, dc-status-detail-row__value--large,
* dc-success-notice,
* dc-btn, dc-btn--primary, dc-btn--full, dc-btn--icon-gap
*
* Color-blind accessibility (WCAG 2.1 SC 1.4.1):
* The icon circle uses both a color-tint class AND a pattern-fill class
* (from src/styles/patterns.css) so each status is identifiable by shape,
* not color alone:
* pending → concentric dots (dc-status-icon-bg--pattern-pending)
* success → diagonal stripes (dc-status-icon-bg--pattern-success)
* error → crosshatch (dc-status-icon-bg--pattern-error)
*
* Accessibility:
* - The outer div has role="status" + aria-live="polite" so the result is
* announced when it replaces the loading spinner.
* - Icons are aria-hidden="true"; all meaningful state info is in text.
* - forced-colors overrides in patterns.css preserve pattern geometry in
* Windows High Contrast mode.
*/

import { Transaction } from "@/types/draw-credit.types";
import { CheckCircle2, AlertCircle, Clock, RotateCcw } from "lucide-react";
import "@/styles/patterns.css";

interface TransactionStatusProps {
transaction: Transaction;
onNewDraw: () => void;
onRefresh?: () => void;
}

/**
* Maps a transaction status to the CSS modifier and icon for that outcome.
* All colour references go through the dc-status-icon-bg--* and
* dc-status-icon--* token classes (var(--accent/success/error)).
*
* patternMod maps to the dc-status-icon-bg--pattern-* classes in
* src/styles/patterns.css, providing a geometry cue beyond colour alone
* (WCAG 2.1 SC 1.4.1 – Use of Color).
*/
const STATUS_CONFIG = {
pending: {
Icon: Clock,
title: "Processing",
colorMod: "accent",
/** Concentric dots — conveys cyclical "in progress" motion. */
patternMod: "pending",
message: "Your draw request is being processed.",
},
success: {
Icon: CheckCircle2,
title: "Draw Successful",
colorMod: "success",
/** Diagonal stripes (45°, upward sweep) — positive direction cue. */
patternMod: "success",
message: "Funds have been disbursed to your account.",
},
error: {
Icon: AlertCircle,
title: "Draw Failed",
colorMod: "error",
/** Crosshatch (45° + 135°) — dense warning texture, distinct from success. */
patternMod: "error",
message: null, // filled from transaction.message at render time
message: null,
},
stale: {
Icon: Clock,
title: "Status Unknown",
colorMod: "accent",
patternMod: "pending",
message: "The transaction status is unclear. Please refresh to check again.",
},
} as const;

export function TransactionStatus({
transaction,
onNewDraw,
onRefresh,
}: TransactionStatusProps) {
const config = STATUS_CONFIG[transaction.status];
const { Icon, title, colorMod, patternMod } = config;
Expand All @@ -90,16 +53,11 @@ export function TransactionStatus({
: config.message;

return (
/*
* role="status" + aria-live="polite" ensures the result is announced when
* this replaces the loading spinner (which was removed from the DOM).
*/
<div
className="dc-spinner-wrap"
role="status"
aria-live="polite"
>
{/* Status icon circle — colour tint + pattern fill for color-blind accessibility */}
<div className="dc-status-icon-wrap" style={{ display: "flex", justifyContent: "center" }}>
<div
className={`dc-status-icon-bg dc-status-icon-bg--${colorMod} dc-status-icon-bg--pattern-${patternMod}`}
Expand All @@ -112,31 +70,24 @@ export function TransactionStatus({
</div>
</div>

{/* Title + message */}
<div>
<h2 className="dc-step__title">{title}</h2>
<p className="dc-step__subtitle">{message}</p>
</div>

{/* Transaction detail card */}
<div className="dc-status-detail-card">
{/* Transaction ID */}
<div className="dc-status-detail-row">
<p className="dc-status-detail-row__label">Transaction ID</p>
<p className="dc-status-detail-row__value dc-status-detail-row__value--mono">
{transaction.id}
</p>
</div>

{/* Amount drawn */}
<div className="dc-status-detail-row">
<p className="dc-status-detail-row__label">Amount Drawn</p>
<p className="dc-status-detail-row__value dc-status-detail-row__value--large tabular-nums">
${transaction.amount.toLocaleString()}
</p>
</div>

{/* Timestamp (optional) */}
{transaction.timestamp && (
<div className="dc-status-detail-row">
<p className="dc-status-detail-row__label">Time</p>
Expand All @@ -147,19 +98,26 @@ export function TransactionStatus({
)}
</div>

{/* Success notice — uses var(--success-tint/border) via dc-success-notice */}
{transaction.status === "success" && (
<div className="dc-success-notice">
<p>
Funds will be deposited to your account within 1-2 business days.
</p>
<p>Funds will be deposited to your account within 1-2 business days.</p>
</div>
)}

{/* Reset CTA */}
{transaction.status === "stale" && onRefresh && (
<button
onClick={onRefresh}
className="dc-btn dc-btn--primary dc-btn--full dc-btn--icon-gap"
style={{ marginBottom: '12px' }}
>
<RotateCcw width={20} height={20} aria-hidden="true" />
Refresh Status
</button>
)}

<button
onClick={onNewDraw}
className="dc-btn dc-btn--primary dc-btn--full dc-btn--icon-gap"
className={`dc-btn dc-btn--full dc-btn--icon-gap ${transaction.status === "stale" ? "" : "dc-btn--primary"}`}
>
<RotateCcw width={20} height={20} aria-hidden="true" />
Make Another Draw
Expand Down
12 changes: 12 additions & 0 deletions src/pages/DrawCreditPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,18 @@ export default function DrawCreditPage() {
<TransactionStatus
transaction={transaction}
onNewDraw={handleNewDraw}
onRefresh={async () => {
setIsLoading(true);
try {
// Simulate network ledger status check
await new Promise(r => setTimeout(r, 1500));
setTransaction(prev =>
prev ? { ...prev, status: Math.random() > 0.5 ? 'success' : 'error' } : null
);
} finally {
setIsLoading(false);
}
}}
/>
)}
</>
Expand Down
2 changes: 1 addition & 1 deletion src/types/creditLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export type UtilizationLevel = 'low' | 'medium' | 'high';
export type TransactionType = 'Draw' | 'Repay' | 'Fee' | 'Interest' | 'StatusChange';

/** Settlement state of an on-chain transaction surfaced to the UI. */
export type TransactionStatus = 'Completed' | 'Pending' | 'Failed';
export type TransactionStatus = 'Completed' | 'Pending' | 'Failed' | 'Stale';

/**
* Canonical ledger entry shape. Mirrors what the backend indexer returns;
Expand Down
2 changes: 1 addition & 1 deletion src/types/draw-credit.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface Transaction {
id: string;
creditLineId: string;
amount: number;
status: "pending" | "success" | "error";
status: "pending" | "success" | "error" | "stale";
message?: string;
timestamp?: Date;
}
Expand Down