@djangocfg/payments 2.1.540 → 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 +3 -3
- package/package.json +11 -13
- package/src/components/PaymentHistory.tsx +106 -45
- package/src/transport/mock-adapter.ts +1 -1
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 |
|
|
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
|
|
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.
|
|
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.
|
|
57
|
-
"@djangocfg/ui-tools": "^2.1.540",
|
|
57
|
+
"@djangocfg/ui-core": "^2.1.542",
|
|
58
58
|
"lucide-react": "^0.545.0",
|
|
59
|
-
"react": "^19.
|
|
60
|
-
"react-dom": "^19.
|
|
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/
|
|
69
|
-
"@djangocfg/
|
|
70
|
-
"@djangocfg/ui-
|
|
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
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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 {
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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(
|
|
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
|