@djangocfg/payments 2.1.541 → 2.1.542

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -61,7 +61,6 @@ directive that points Tailwind at this package's sources):
61
61
  ```css
62
62
  /* your app's globals.css */
63
63
  @import "@djangocfg/ui-core/styles/full";
64
- @import "@djangocfg/ui-tools/styles";
65
64
  @import "@djangocfg/payments/styles"; /* ← add this */
66
65
  ```
67
66
 
@@ -81,14 +80,15 @@ Next.js hosts also add the package to `transpilePackages`.
81
80
  | `CheckoutDialog` | component | close-locked dialog shell (CheckoutGuard + ui-core Dialog); wrap your checkout body in it |
82
81
  | `CheckoutForm` | component | provider-agnostic form shell (ui-core only); inject the provider field via `paymentField` |
83
82
  | `StripePaymentElement` | component | `<Elements>` + `<PaymentElement>`; render-props the live Elements to the host |
84
- | `PaymentHistory` | component | DataTable of `PaymentRecord` |
83
+ | `PaymentHistory` | component | sortable table of `PaymentRecord` (ui-core `Table`) |
85
84
  | `PaymentStatusBadge` | component | status pill on ui-core semantic tokens (success/warning/info/destructive) |
86
85
  | `toMinorUnits` / `toMajorUnits` / `formatAmount` | util | money at the boundary (amounts cross as integer minor units) |
87
86
 
88
87
  ## Conventions
89
88
 
90
89
  - Source-consumed (no bundler); `main`/`types`/`exports` → `./src/index.ts`.
91
- - ui-core flat import; ui-tools subpath-only (`@djangocfg/ui-tools/data-table`).
90
+ - ui-core flat import. No widget dependency: a package may not depend on a
91
+ widget (`widgets/CLAUDE.md`).
92
92
  - Does **not** mount its own `<UiProviders>` — the host provides it once.
93
93
  - Locale-free: user-facing labels are props/slots (host supplies i18n).
94
94
  - Verify with `pnpm -F @djangocfg/payments check` (tsc).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/payments",
3
- "version": "2.1.541",
3
+ "version": "2.1.542",
4
4
  "description": "Provider-agnostic React payments module (Stripe-first): checkout state machine, Payment Element wrapper, close-guard context, mock adapter for keyless development",
5
5
  "keywords": [
6
6
  "payments",
@@ -41,6 +41,7 @@
41
41
  "files": [
42
42
  "dist",
43
43
  "src",
44
+ "!src/**/*.stories.tsx",
44
45
  "README.md",
45
46
  "LICENSE"
46
47
  ],
@@ -53,25 +54,22 @@
53
54
  "@stripe/stripe-js": "^9.9.0"
54
55
  },
55
56
  "peerDependencies": {
56
- "@djangocfg/ui-core": "^2.1.541",
57
- "@djangocfg/ui-tools": "^2.1.541",
57
+ "@djangocfg/ui-core": "^2.1.542",
58
58
  "lucide-react": "^0.545.0",
59
- "react": "^19.2.4",
60
- "react-dom": "^19.2.4"
61
- },
62
- "peerDependenciesMeta": {
63
- "@djangocfg/ui-tools": {
64
- "optional": true
65
- }
59
+ "react": "^19.0.0",
60
+ "react-dom": "^19.0.0"
66
61
  },
67
62
  "devDependencies": {
68
- "@djangocfg/typescript-config": "^2.1.541",
69
- "@djangocfg/ui-core": "^2.1.541",
70
- "@djangocfg/ui-tools": "^2.1.541",
63
+ "@djangocfg/eslint-config": "^2.1.542",
64
+ "@djangocfg/typescript-config": "^2.1.542",
65
+ "@djangocfg/ui-core": "^2.1.542",
66
+ "@storybook/react-vite": "^10.5.0",
71
67
  "@types/node": "^25.9.5",
72
68
  "@types/react": "19.2.15",
73
69
  "@types/react-dom": "19.2.3",
70
+ "eslint": "^9.39.5",
74
71
  "lucide-react": "0.545.0",
72
+ "storybook": "^10.5.0",
75
73
  "typescript": "^5.9.3"
76
74
  },
77
75
  "publishConfig": {
@@ -3,16 +3,36 @@
3
3
  // ============================================================================
4
4
  // @djangocfg/payments — PaymentHistory
5
5
  // ============================================================================
6
- // A DataTable of PaymentRecord rows. Uses the ui-tools DataTable (subpath
7
- // import, per the in-repo rule) and the PaymentStatusBadge. Pure
8
- // presentational — the host feeds `records` (e.g. from usePaymentHistory()).
6
+ // A sortable table of PaymentRecord rows, built on the ui-core Table
7
+ // primitives. Pure presentational — the host feeds `records` (e.g. from
8
+ // usePaymentHistory()).
9
+ //
10
+ // This deliberately does NOT use a generic data-grid. A package may not depend
11
+ // on a widget (see widgets/CLAUDE.md), and the two sortable columns here are
12
+ // the whole requirement; a grid would buy filtering, pagination and selection
13
+ // that this surface never shows.
9
14
 
10
- import React, { useMemo } from 'react';
11
- import { DataTable, type DataTableColumn } from '@djangocfg/ui-tools/data-table';
15
+ import React, { useCallback, useMemo, useState } from 'react';
16
+ import {
17
+ Table,
18
+ TableBody,
19
+ TableCell,
20
+ TableHead,
21
+ TableHeader,
22
+ TableRow,
23
+ } from '@djangocfg/ui-core';
12
24
  import { PaymentStatusBadge } from './PaymentStatusBadge';
13
25
  import { formatAmount } from '../domain/money';
14
26
  import type { PaymentRecord } from '../domain/types';
15
27
 
28
+ type SortKey = 'createdAt' | 'amount';
29
+ type SortDirection = 'asc' | 'desc';
30
+
31
+ interface SortState {
32
+ key: SortKey;
33
+ direction: SortDirection;
34
+ }
35
+
16
36
  export interface PaymentHistoryProps {
17
37
  records: PaymentRecord[];
18
38
  loading?: boolean;
@@ -22,6 +42,8 @@ export interface PaymentHistoryProps {
22
42
  className?: string;
23
43
  }
24
44
 
45
+ const SORT_INDICATOR: Record<SortDirection, string> = { asc: '↑', desc: '↓' };
46
+
25
47
  export function PaymentHistory({
26
48
  records,
27
49
  loading,
@@ -30,50 +52,89 @@ export function PaymentHistory({
30
52
  locale,
31
53
  className,
32
54
  }: PaymentHistoryProps) {
33
- const columns = useMemo<DataTableColumn<PaymentRecord>[]>(
34
- () => [
35
- {
36
- key: 'createdAt',
37
- header: 'Date',
38
- sortable: true,
39
- cell: (p) => new Date(p.createdAt).toLocaleDateString(locale),
40
- },
41
- {
42
- key: 'reference',
43
- header: 'For',
44
- cell: (p) => `${p.reference.kind} · ${p.reference.id}`,
45
- },
46
- {
47
- key: 'amount',
48
- header: 'Amount',
49
- sortable: true,
50
- cell: (p) => formatAmount(p.amount, p.currency, locale),
51
- },
52
- {
53
- key: 'provider',
54
- header: 'Provider',
55
- cell: (p) => p.provider,
56
- },
57
- {
58
- key: 'status',
59
- header: 'Status',
60
- cell: (p) => <PaymentStatusBadge status={p.status} />,
61
- },
62
- ],
63
- [locale],
55
+ const [sort, setSort] = useState<SortState>({ key: 'createdAt', direction: 'desc' });
56
+
57
+ const toggleSort = useCallback((key: SortKey) => {
58
+ setSort((current) =>
59
+ current.key === key
60
+ ? { key, direction: current.direction === 'asc' ? 'desc' : 'asc' }
61
+ : { key, direction: 'desc' },
62
+ );
63
+ }, []);
64
+
65
+ const sortedRecords = useMemo(() => {
66
+ const factor = sort.direction === 'asc' ? 1 : -1;
67
+ // Sort a copy: `records` belongs to the host and may be memoized upstream.
68
+ return [...records].sort((a, b) => {
69
+ if (sort.key === 'amount') return (a.amount - b.amount) * factor;
70
+ return (Date.parse(a.createdAt) - Date.parse(b.createdAt)) * factor;
71
+ });
72
+ }, [records, sort]);
73
+
74
+ const rows = useMemo(
75
+ () =>
76
+ sortedRecords.map((record) => ({
77
+ record,
78
+ id: record.id,
79
+ date: new Date(record.createdAt).toLocaleDateString(locale),
80
+ reference: `${record.reference.kind} · ${record.reference.id}`,
81
+ amount: formatAmount(record.amount, record.currency, locale),
82
+ })),
83
+ [sortedRecords, locale],
84
+ );
85
+
86
+ const renderSortableHead = (key: SortKey, label: string) => (
87
+ <TableHead>
88
+ <button
89
+ type="button"
90
+ onClick={() => toggleSort(key)}
91
+ className="inline-flex items-center gap-1 font-medium hover:text-foreground"
92
+ aria-sort={sort.key === key ? (sort.direction === 'asc' ? 'ascending' : 'descending') : 'none'}
93
+ >
94
+ {label}
95
+ {sort.key === key ? <span aria-hidden>{SORT_INDICATOR[sort.direction]}</span> : null}
96
+ </button>
97
+ </TableHead>
64
98
  );
65
99
 
66
100
  return (
67
101
  <div className={className}>
68
- <DataTable
69
- data={records}
70
- columns={columns}
71
- getRowId={(p) => p.id}
72
- loading={loading}
73
- emptyMessage={emptyMessage}
74
- onRowClick={onRowClick}
75
- getRowClassName={onRowClick ? () => 'cursor-pointer' : undefined}
76
- />
102
+ <Table>
103
+ <TableHeader>
104
+ <TableRow>
105
+ {renderSortableHead('createdAt', 'Date')}
106
+ <TableHead>For</TableHead>
107
+ {renderSortableHead('amount', 'Amount')}
108
+ <TableHead>Provider</TableHead>
109
+ <TableHead>Status</TableHead>
110
+ </TableRow>
111
+ </TableHeader>
112
+ <TableBody>
113
+ {loading || rows.length === 0 ? (
114
+ <TableRow>
115
+ <TableCell colSpan={5} className="py-8 text-center text-muted-foreground">
116
+ {loading ? 'Loading…' : emptyMessage}
117
+ </TableCell>
118
+ </TableRow>
119
+ ) : (
120
+ rows.map((row) => (
121
+ <TableRow
122
+ key={row.id}
123
+ onClick={onRowClick ? () => onRowClick(row.record) : undefined}
124
+ className={onRowClick ? 'cursor-pointer' : undefined}
125
+ >
126
+ <TableCell>{row.date}</TableCell>
127
+ <TableCell>{row.reference}</TableCell>
128
+ <TableCell>{row.amount}</TableCell>
129
+ <TableCell>{row.record.provider}</TableCell>
130
+ <TableCell>
131
+ <PaymentStatusBadge status={row.record.status} />
132
+ </TableCell>
133
+ </TableRow>
134
+ ))
135
+ )}
136
+ </TableBody>
137
+ </Table>
77
138
  </div>
78
139
  );
79
140
  }
@@ -100,7 +100,7 @@ export function createMockPaymentAdapter(
100
100
  return { status: 'succeeded', intentId };
101
101
  },
102
102
 
103
- async createSubscription(input: StartSubscriptionInput): Promise<SubscriptionIntent> {
103
+ async createSubscription(_input: StartSubscriptionInput): Promise<SubscriptionIntent> {
104
104
  await delay(latencyMs);
105
105
  const subId = nextId('sub');
106
106
  // Mock the deferred flow: a paid first invoice → confirmPayment path, with