@volter/twin-stripe 0.1.2 → 2.0.0

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.
Files changed (219) hide show
  1. package/README.md +64 -27
  2. package/client/dashboard-api.ts +286 -0
  3. package/client/stripe-mirror.css +272 -159
  4. package/client/stripe-mirror.tsx +1384 -541
  5. package/dist/client/dashboard-api.d.ts +107 -0
  6. package/dist/client/dashboard-api.js +238 -0
  7. package/dist/client/dashboard-api.ts +286 -0
  8. package/dist/client/stripe-mirror.bundle.js +236 -0
  9. package/dist/client/stripe-mirror.css +275 -0
  10. package/dist/client/stripe-mirror.d.ts +134 -0
  11. package/dist/client/stripe-mirror.js +823 -0
  12. package/dist/client/stripe-mirror.tsx +1534 -0
  13. package/dist/src/cli.d.ts +2 -0
  14. package/dist/src/cli.js +39 -0
  15. package/dist/src/generated/events.gen.json +1 -0
  16. package/dist/src/generated/surface.gen.json +1 -0
  17. package/dist/src/generated/ui.gen.json +1 -0
  18. package/dist/src/index.d.ts +14 -0
  19. package/dist/src/index.js +73 -0
  20. package/dist/src/manifest.d.ts +2 -0
  21. package/dist/src/manifest.js +1065 -0
  22. package/dist/src/screens/checkout.d.ts +31 -0
  23. package/dist/src/screens/checkout.js +241 -0
  24. package/dist/src/screens/consent-skin.d.ts +4 -0
  25. package/dist/src/screens/consent-skin.js +18 -0
  26. package/dist/src/screens/financial-connections.d.ts +5 -0
  27. package/dist/src/screens/financial-connections.js +90 -0
  28. package/dist/src/screens/identity.d.ts +5 -0
  29. package/dist/src/screens/identity.js +86 -0
  30. package/dist/src/screens/industries.d.ts +1 -0
  31. package/dist/src/screens/industries.js +267 -0
  32. package/dist/src/screens/onboarding.d.ts +13 -0
  33. package/dist/src/screens/onboarding.js +225 -0
  34. package/dist/src/screens/portal.d.ts +5 -0
  35. package/dist/src/screens/portal.js +214 -0
  36. package/dist/src/screens/public-details.d.ts +5 -0
  37. package/dist/src/screens/public-details.js +90 -0
  38. package/dist/src/semantics/after-payment.d.ts +22 -0
  39. package/dist/src/semantics/after-payment.js +93 -0
  40. package/dist/src/semantics/apps-secrets.d.ts +2 -0
  41. package/dist/src/semantics/apps-secrets.js +54 -0
  42. package/dist/src/semantics/balance.d.ts +11 -0
  43. package/dist/src/semantics/balance.js +195 -0
  44. package/dist/src/semantics/billing.d.ts +2 -0
  45. package/dist/src/semantics/billing.js +220 -0
  46. package/dist/src/semantics/charges.d.ts +28 -0
  47. package/dist/src/semantics/charges.js +201 -0
  48. package/dist/src/semantics/checkout.d.ts +15 -0
  49. package/dist/src/semantics/checkout.js +303 -0
  50. package/dist/src/semantics/connect.d.ts +5 -0
  51. package/dist/src/semantics/connect.js +476 -0
  52. package/dist/src/semantics/coupons.d.ts +6 -0
  53. package/dist/src/semantics/coupons.js +92 -0
  54. package/dist/src/semantics/credit-notes.d.ts +2 -0
  55. package/dist/src/semantics/credit-notes.js +172 -0
  56. package/dist/src/semantics/customers.d.ts +6 -0
  57. package/dist/src/semantics/customers.js +429 -0
  58. package/dist/src/semantics/disputes.d.ts +2 -0
  59. package/dist/src/semantics/disputes.js +51 -0
  60. package/dist/src/semantics/entitlements.d.ts +2 -0
  61. package/dist/src/semantics/entitlements.js +95 -0
  62. package/dist/src/semantics/ephemeral-keys.d.ts +2 -0
  63. package/dist/src/semantics/ephemeral-keys.js +34 -0
  64. package/dist/src/semantics/files.d.ts +2 -0
  65. package/dist/src/semantics/files.js +125 -0
  66. package/dist/src/semantics/invoices.d.ts +18 -0
  67. package/dist/src/semantics/invoices.js +541 -0
  68. package/dist/src/semantics/issuing.d.ts +13 -0
  69. package/dist/src/semantics/issuing.js +570 -0
  70. package/dist/src/semantics/ledger.d.ts +54 -0
  71. package/dist/src/semantics/ledger.js +181 -0
  72. package/dist/src/semantics/payment-intents.d.ts +18 -0
  73. package/dist/src/semantics/payment-intents.js +404 -0
  74. package/dist/src/semantics/payment-links.d.ts +2 -0
  75. package/dist/src/semantics/payment-links.js +133 -0
  76. package/dist/src/semantics/payment-methods.d.ts +20 -0
  77. package/dist/src/semantics/payment-methods.js +138 -0
  78. package/dist/src/semantics/plans.d.ts +5 -0
  79. package/dist/src/semantics/plans.js +121 -0
  80. package/dist/src/semantics/platform.d.ts +9 -0
  81. package/dist/src/semantics/platform.js +206 -0
  82. package/dist/src/semantics/products.d.ts +2 -0
  83. package/dist/src/semantics/products.js +140 -0
  84. package/dist/src/semantics/radar.d.ts +2 -0
  85. package/dist/src/semantics/radar.js +83 -0
  86. package/dist/src/semantics/refunds.d.ts +9 -0
  87. package/dist/src/semantics/refunds.js +195 -0
  88. package/dist/src/semantics/renewals.d.ts +47 -0
  89. package/dist/src/semantics/renewals.js +251 -0
  90. package/dist/src/semantics/setup-intents.d.ts +2 -0
  91. package/dist/src/semantics/setup-intents.js +84 -0
  92. package/dist/src/semantics/shared.d.ts +78 -0
  93. package/dist/src/semantics/shared.js +192 -0
  94. package/dist/src/semantics/subscription-schedules.d.ts +2 -0
  95. package/dist/src/semantics/subscription-schedules.js +119 -0
  96. package/dist/src/semantics/subscriptions.d.ts +11 -0
  97. package/dist/src/semantics/subscriptions.js +605 -0
  98. package/dist/src/semantics/tax.d.ts +2 -0
  99. package/dist/src/semantics/tax.js +197 -0
  100. package/dist/src/semantics/terminal.d.ts +5 -0
  101. package/dist/src/semantics/terminal.js +182 -0
  102. package/dist/src/semantics/test-clocks.d.ts +6 -0
  103. package/dist/src/semantics/test-clocks.js +73 -0
  104. package/dist/src/semantics/tokens.d.ts +4 -0
  105. package/dist/src/semantics/tokens.js +44 -0
  106. package/dist/src/semantics/transfers.d.ts +2 -0
  107. package/dist/src/semantics/transfers.js +154 -0
  108. package/dist/src/semantics/treasury.d.ts +2 -0
  109. package/dist/src/semantics/treasury.js +377 -0
  110. package/dist/src/semantics/webhook-endpoints.d.ts +3 -0
  111. package/dist/src/semantics/webhook-endpoints.js +85 -0
  112. package/dist/src/stripe-budget.d.ts +55 -0
  113. package/dist/src/stripe-budget.js +155 -0
  114. package/dist/src/stripe-capabilities.d.ts +3 -0
  115. package/dist/src/stripe-capabilities.js +5052 -0
  116. package/dist/src/stripe-conformance.d.ts +41 -0
  117. package/dist/src/stripe-conformance.js +96 -0
  118. package/dist/src/stripe-connector.d.ts +161 -0
  119. package/dist/src/stripe-connector.js +414 -0
  120. package/dist/src/stripe-emit.d.ts +2 -0
  121. package/dist/src/stripe-emit.js +145 -0
  122. package/dist/src/stripe-events.d.ts +93 -0
  123. package/dist/src/stripe-events.js +388 -0
  124. package/dist/src/stripe-js.d.ts +4 -0
  125. package/dist/src/stripe-js.js +70 -0
  126. package/dist/src/stripe-mirror-ui.d.ts +15 -0
  127. package/dist/src/stripe-mirror-ui.js +87 -0
  128. package/dist/src/stripe-params.d.ts +3 -0
  129. package/dist/src/stripe-params.js +43 -0
  130. package/dist/src/stripe-perform-harness.d.ts +9 -0
  131. package/dist/src/stripe-perform-harness.js +26 -0
  132. package/dist/src/stripe-server.d.ts +33 -0
  133. package/dist/src/stripe-server.js +326 -0
  134. package/dist/src/stripe-shared.d.ts +106 -0
  135. package/dist/src/stripe-shared.js +273 -0
  136. package/dist/src/stripe-twin.d.ts +155 -0
  137. package/dist/src/stripe-twin.js +1226 -0
  138. package/dist/src/stripe-ui-conformance.d.ts +5 -0
  139. package/dist/src/stripe-ui-conformance.js +79 -0
  140. package/dist/src/stripe-ui-structure.d.ts +3 -0
  141. package/dist/src/stripe-ui-structure.js +168 -0
  142. package/dist/src/stripe-version.d.ts +10 -0
  143. package/dist/src/stripe-version.js +285 -0
  144. package/dist/test-fixtures/stripe-known-deviations.json +105 -0
  145. package/dist/test-fixtures/stripe-openapi-operations.SOURCE.md +14 -0
  146. package/dist/test-fixtures/stripe-openapi-operations.json +4717 -0
  147. package/dist/test-fixtures/stripe-schemas.SOURCE.md +35 -0
  148. package/dist/test-fixtures/stripe-schemas.json +3740 -0
  149. package/package.json +18 -10
  150. package/src/cli.ts +7 -7
  151. package/src/generated/events.gen.json +1 -0
  152. package/src/generated/surface.gen.json +1 -0
  153. package/src/generated/ui.gen.json +1 -0
  154. package/src/index.ts +31 -9
  155. package/src/manifest.ts +1097 -0
  156. package/src/screens/checkout.tsx +252 -0
  157. package/src/screens/consent-skin.ts +20 -0
  158. package/src/screens/financial-connections.tsx +101 -0
  159. package/src/screens/identity.tsx +96 -0
  160. package/src/screens/industries.ts +267 -0
  161. package/src/screens/onboarding.tsx +243 -0
  162. package/src/screens/portal.tsx +218 -0
  163. package/src/screens/public-details.tsx +105 -0
  164. package/src/semantics/after-payment.ts +113 -0
  165. package/src/semantics/apps-secrets.ts +58 -0
  166. package/src/semantics/balance.ts +209 -0
  167. package/src/semantics/billing.ts +216 -0
  168. package/src/semantics/charges.ts +211 -0
  169. package/src/semantics/checkout.ts +297 -0
  170. package/src/semantics/connect.ts +471 -0
  171. package/src/semantics/coupons.ts +97 -0
  172. package/src/semantics/credit-notes.ts +168 -0
  173. package/src/semantics/customers.ts +432 -0
  174. package/src/semantics/disputes.ts +62 -0
  175. package/src/semantics/entitlements.ts +94 -0
  176. package/src/semantics/ephemeral-keys.ts +34 -0
  177. package/src/semantics/files.ts +143 -0
  178. package/src/semantics/invoices.ts +541 -0
  179. package/src/semantics/issuing.ts +585 -0
  180. package/src/semantics/ledger.ts +216 -0
  181. package/src/semantics/payment-intents.ts +420 -0
  182. package/src/semantics/payment-links.ts +148 -0
  183. package/src/semantics/payment-methods.ts +143 -0
  184. package/src/semantics/plans.ts +131 -0
  185. package/src/semantics/platform.ts +220 -0
  186. package/src/semantics/products.ts +154 -0
  187. package/src/semantics/radar.ts +85 -0
  188. package/src/semantics/refunds.ts +218 -0
  189. package/src/semantics/renewals.ts +274 -0
  190. package/src/semantics/setup-intents.ts +87 -0
  191. package/src/semantics/shared.ts +215 -0
  192. package/src/semantics/subscription-schedules.ts +129 -0
  193. package/src/semantics/subscriptions.ts +610 -0
  194. package/src/semantics/tax.ts +220 -0
  195. package/src/semantics/terminal.ts +195 -0
  196. package/src/semantics/test-clocks.ts +77 -0
  197. package/src/semantics/tokens.ts +52 -0
  198. package/src/semantics/transfers.ts +174 -0
  199. package/src/semantics/treasury.ts +383 -0
  200. package/src/semantics/webhook-endpoints.ts +87 -0
  201. package/src/stripe-budget.ts +4 -4
  202. package/src/stripe-capabilities.ts +1456 -222
  203. package/src/stripe-conformance.ts +6 -5
  204. package/src/stripe-connector.ts +68 -40
  205. package/src/stripe-emit.ts +14 -7
  206. package/src/stripe-events.ts +94 -36
  207. package/src/stripe-js.ts +70 -0
  208. package/src/stripe-mirror-ui.ts +28 -298
  209. package/src/stripe-params.ts +44 -0
  210. package/src/stripe-perform-harness.ts +29 -0
  211. package/src/stripe-server.ts +263 -38
  212. package/src/stripe-shared.ts +294 -0
  213. package/src/stripe-twin.ts +429 -5325
  214. package/src/stripe-ui-conformance.ts +70 -107
  215. package/src/stripe-ui-structure.ts +124 -348
  216. package/src/stripe-version.ts +278 -0
  217. package/test-fixtures/stripe-known-deviations.json +2 -7
  218. package/test-fixtures/stripe-openapi-operations.json +1188 -2855
  219. package/src/stripe-form.ts +0 -35
@@ -0,0 +1,823 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // The Stripe Dashboard (manifest screen `dashboard`, dashboard.stripe.com/test/{section}): a React app
3
+ // (Bun-bundled) authored as Stripe's Dashboard in test mode, reading and writing only through Stripe's API
4
+ // (client/dashboard-api.ts). Its pages sit at Stripe's own paths: /test/dashboard (Home),
5
+ // /test/balance/overview, /test/payments[/{pi|ch}], /test/customers[/{cus}], /test/subscriptions[/{sub}],
6
+ // /test/invoices[/{in}], /test/products, /test/developers, /test/settings, and the pages under "More".
7
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
8
+ import { createRoot } from 'react-dom/client';
9
+ import { formatStripeAmount, formatRecurring, formatPaymentMethod, formatPaymentError, formatBalanceSummary, formatAccountFlags, accountCurrentlyDue, publicBusinessName, PLATFORM_DEFAULT_NAME, } from "../src/stripe-shared.js";
10
+ import { API_BASE, list, loadBalances, loadCustomer, loadCustomers, loadDevelopers, loadHome, loadInvoices, loadPayment, loadPayments, loadProducts, loadSubscription, loadSubscriptions, retrieve, createRefund, cancelSubscription, createObject, } from "./dashboard-api.js";
11
+ // ── routing: Stripe's paths, inside whatever prefix this page is served under ─────────────────────
12
+ const BASE_PATH = API_BASE;
13
+ export function routeOf(pathname) {
14
+ const inside = pathname.startsWith(BASE_PATH) ? pathname.slice(BASE_PATH.length) : pathname;
15
+ const parts = inside.split('/').filter(Boolean);
16
+ return parts[0] === 'test' ? parts.slice(1) : parts;
17
+ }
18
+ const hrefOf = (to) => `${BASE_PATH}/test/${to.replace(/^\/+/, '')}`;
19
+ // ── formatting ───────────────────────────────────────────────────────────────
20
+ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
21
+ export function formatDate(ts, withTime = true) {
22
+ const n = Number(ts);
23
+ if (!Number.isFinite(n) || n <= 0)
24
+ return '—';
25
+ const d = new Date(n * 1000);
26
+ const day = `${MONTHS[d.getMonth()]} ${d.getDate()}`;
27
+ const sameYear = d.getFullYear() === new Date().getFullYear();
28
+ if (!withTime)
29
+ return `${day}, ${d.getFullYear()}`;
30
+ const h = d.getHours() % 12 || 12;
31
+ const time = `${h}:${String(d.getMinutes()).padStart(2, '0')} ${d.getHours() < 12 ? 'AM' : 'PM'}`;
32
+ return sameYear ? `${day}, ${time}` : `${day}, ${d.getFullYear()}, ${time}`;
33
+ }
34
+ const money = (amount, currency) => formatStripeAmount(Number(amount) || 0, currency);
35
+ const ccy = (currency) => String(currency ?? 'usd').toUpperCase();
36
+ const titleCase = (s) => s.replace(/_/g, ' ').replace(/^\w/, (c) => c.toUpperCase());
37
+ const customerLabel = (c) => (c ? c.name || c.email || c.id : '—');
38
+ // ── icons ────────────────────────────────────────────────────────────────────
39
+ const ICONS = {
40
+ home: 'M8 1.5 1.5 7v7.5h4.5v-4.5h4v4.5h4.5V7L8 1.5z',
41
+ balance: 'M2 4h12v9H2zM2 7h12M5 10.5h3',
42
+ transactions: 'M2 5h10M9 2l3 3-3 3M14 11H4M7 8l-3 3 3 3',
43
+ customers: 'M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM2.5 14.5c.6-2.8 2.8-4.5 5.5-4.5s4.9 1.7 5.5 4.5',
44
+ catalog: 'M2 4.5 8 1.5l6 3v7L8 14.5l-6-3v-7zM2 4.5l6 3 6-3M8 7.5v7',
45
+ connect: 'M5.5 10.5 10.5 5.5M7 3.5l1.5-1.5a3 3 0 0 1 4.5 4.5L11.5 8M9 12.5 7.5 14a3 3 0 0 1-4.5-4.5L4.5 8',
46
+ payments: 'M1.5 3.5h13v9h-13zM1.5 6.5h13M4 10h2.5',
47
+ billing: 'M3 1.5h10v13l-2-1.3-2 1.3-2-1.3-2 1.3-2-1.3zM5.5 5h5M5.5 8h5',
48
+ reporting: 'M2 14h12M4 11V7M8 11V3M12 11V8',
49
+ more: 'M3 8h.01M8 8h.01M13 8h.01',
50
+ developers: 'M5 4 1.5 8 5 12M11 4l3.5 4-3.5 4',
51
+ search: 'M7 12.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM11 11l3.5 3.5',
52
+ bell: 'M4 11V7a4 4 0 0 1 8 0v4l1.5 1.5h-11L4 11zM6.5 14h3',
53
+ gear: 'M8 10.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zM8 1v2M8 13v2M1 8h2M13 8h2M3 3l1.4 1.4M11.6 11.6 13 13M3 13l1.4-1.4M11.6 4.4 13 3',
54
+ help: 'M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14zM6 6a2 2 0 1 1 2.5 2c-.4.1-.5.4-.5.8V10M8 12v.01',
55
+ plus: 'M8 2v12M2 8h12',
56
+ check: 'M3 8.5 6.5 12 13 4.5',
57
+ x: 'M4 4l8 8M12 4l-8 8',
58
+ refund: 'M4 6h6.5a3 3 0 0 1 0 6H6M6.5 3.5 4 6l2.5 2.5',
59
+ clock: 'M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14zM8 4v4l2.5 1.5',
60
+ chevronDown: 'M4 6l4 4 4-4',
61
+ chevronRight: 'M6 4l4 4-4 4',
62
+ dispute: 'M8 1.5 15 14H1L8 1.5zM8 6v3.5M8 11.5v.01',
63
+ arrowRight: 'M2 8h11M9 4l4 4-4 4',
64
+ card: 'M1.5 3.5h13v9h-13zM1.5 6.5h13',
65
+ download: 'M8 2v8M4.5 6.5 8 10l3.5-3.5M2.5 13.5h11',
66
+ person: 'M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM2.5 14.5c.6-2.8 2.8-4.5 5.5-4.5s4.9 1.7 5.5 4.5',
67
+ };
68
+ export function Icon({ name, size = 16, className }) {
69
+ return (_jsx("svg", { className: `icon ${className ?? ''}`, width: size, height: size, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: ICONS[name] ?? '' }) }));
70
+ }
71
+ /** The card network mark Stripe shows beside a card's last four. */
72
+ export function CardBrand({ brand }) {
73
+ const b = String(brand ?? '').toLowerCase();
74
+ if (b === 'mastercard') {
75
+ return (_jsx("span", { className: "brand brand-mastercard", title: "Mastercard", children: _jsxs("svg", { width: "20", height: "12", viewBox: "0 0 20 12", "aria-hidden": "true", children: [_jsx("circle", { cx: "7", cy: "6", r: "5", fill: "#EB001B" }), _jsx("circle", { cx: "13", cy: "6", r: "5", fill: "#F79E1B", fillOpacity: "0.9" })] }) }));
76
+ }
77
+ if (b === 'amex')
78
+ return _jsx("span", { className: "brand brand-amex", title: "American Express", children: "AMEX" });
79
+ if (b === 'visa')
80
+ return _jsx("span", { className: "brand brand-visa", title: "Visa", children: "VISA" });
81
+ return _jsx("span", { className: "brand brand-generic", title: brand ?? 'Card', children: _jsx(Icon, { name: "card", size: 12 }) });
82
+ }
83
+ export function CardLabel({ pm }) {
84
+ const card = pm?.card;
85
+ if (!card)
86
+ return _jsx("span", { className: "muted", children: pm ? formatPaymentMethod(pm) : '—' });
87
+ return _jsxs("span", { className: "card-label", children: [_jsx(CardBrand, { brand: card.brand }), _jsx("span", { className: "dots", children: "\u2022\u2022\u2022\u2022" }), " ", card.last4] });
88
+ }
89
+ const STATUS = {
90
+ succeeded: { label: 'Succeeded', tone: 'green', icon: 'check' },
91
+ refunded: { label: 'Refunded', tone: 'grey', icon: 'refund' },
92
+ partially_refunded: { label: 'Partial refund', tone: 'grey', icon: 'refund' },
93
+ failed: { label: 'Failed', tone: 'red', icon: 'x' },
94
+ incomplete: { label: 'Incomplete', tone: 'grey', icon: 'clock' },
95
+ uncaptured: { label: 'Uncaptured', tone: 'blue', icon: 'clock' },
96
+ canceled: { label: 'Canceled', tone: 'grey', icon: 'x' },
97
+ disputed: { label: 'Disputed', tone: 'orange', icon: 'dispute' },
98
+ processing: { label: 'Pending', tone: 'grey', icon: 'clock' },
99
+ active: { label: 'Active', tone: 'green' },
100
+ trialing: { label: 'Trialing', tone: 'blue' },
101
+ past_due: { label: 'Past due', tone: 'red' },
102
+ unpaid: { label: 'Unpaid', tone: 'red' },
103
+ incomplete_expired: { label: 'Expired', tone: 'grey' },
104
+ paused: { label: 'Paused', tone: 'grey' },
105
+ paid: { label: 'Paid', tone: 'green' },
106
+ open: { label: 'Open', tone: 'blue' },
107
+ draft: { label: 'Draft', tone: 'grey' },
108
+ void: { label: 'Void', tone: 'grey' },
109
+ uncollectible: { label: 'Uncollectible', tone: 'red' },
110
+ pending: { label: 'Pending', tone: 'grey' },
111
+ enabled: { label: 'Enabled', tone: 'green' },
112
+ disabled: { label: 'Disabled', tone: 'grey' },
113
+ true: { label: 'Active', tone: 'green' },
114
+ false: { label: 'Archived', tone: 'grey' },
115
+ };
116
+ export function StatusPill({ value, suffix }) {
117
+ const key = String(value ?? '');
118
+ const s = STATUS[key] ?? { label: titleCase(key || '—'), tone: 'grey' };
119
+ return (_jsxs("span", { className: `pill pill-${s.tone}`, "data-status": key, children: [s.label, suffix ?? '', s.icon ? _jsx(Icon, { name: s.icon, size: 12 }) : null] }));
120
+ }
121
+ /** A declined payment's reason, as Stripe states it above the payment. */
122
+ export function PaymentErrorBanner({ error }) {
123
+ const text = formatPaymentError(error);
124
+ if (!text)
125
+ return null;
126
+ const message = typeof error?.message === 'string' ? error.message : text;
127
+ return (_jsxs("div", { className: "pay-error", role: "alert", children: [_jsx(Icon, { name: "dispute" }), _jsxs("div", { children: [_jsx("div", { className: "pay-error-label", children: "Payment error" }), _jsxs("div", { className: "pay-error-text", children: [message, " ", _jsx("span", { className: "mono muted", children: text.split(':')[0] })] })] })] }));
128
+ }
129
+ /** The balance's per-currency buckets: incoming (pending) and available. */
130
+ export function BalanceSummary({ row }) {
131
+ const buckets = formatBalanceSummary(row);
132
+ if (buckets.length === 0)
133
+ return null;
134
+ const label = (b) => (b === 'pending' ? 'Incoming' : 'Available');
135
+ return (_jsx("div", { className: "balance-summary", children: [...buckets].reverse().map((b) => (_jsxs("div", { className: `balance-bucket ${b.bucket}`, children: [_jsx("span", { className: "balance-bucket-label", children: label(b.bucket) }), _jsx("span", { className: "balance-bucket-amount", children: b.text })] }, b.bucket))) }));
136
+ }
137
+ /** A connected account's readiness: charges, payouts, details, and what is currently due. */
138
+ export function ConnectAccountPanel({ row }) {
139
+ const flags = formatAccountFlags(row);
140
+ const due = accountCurrentlyDue(row);
141
+ if (flags.length === 0)
142
+ return null;
143
+ return (_jsxs("div", { className: "connect-panel", children: [_jsx("div", { className: "connect-flags", children: flags.map((f) => (_jsxs("div", { className: `connect-flag ${f.enabled ? 'ok' : 'warn'}`, children: [_jsx("span", { className: "connect-flag-label", children: f.label }), _jsx(StatusPill, { value: f.enabled ? 'enabled' : 'disabled' })] }, f.key))) }), due.length > 0 && (_jsxs("div", { className: "connect-requirements", role: "status", children: [_jsx("span", { className: "connect-req-label", children: "Requirements currently due" }), _jsx("ul", { className: "connect-req-list", children: due.map((d) => _jsx("li", { className: "connect-req-item mono", children: d }, d)) })] }))] }));
144
+ }
145
+ const d = (r) => formatDate(r.created);
146
+ export const SECTIONS = [
147
+ { key: 'customers', label: 'Customers', path: 'customers', title: (r) => r.name || r.email || r.id, subtitle: (r) => r.email || r.id },
148
+ { key: 'payment_intents', label: 'Payments', path: 'payments', title: (r) => formatStripeAmount(r.amount, r.currency), subtitle: (r) => r.description || r.id },
149
+ { key: 'subscriptions', label: 'Subscriptions', path: 'subscriptions', title: (r) => r.id, subtitle: (r) => r.customer ?? '' },
150
+ { key: 'invoices', label: 'Invoices', path: 'invoices', title: (r) => formatStripeAmount(r.total ?? r.amount_due, r.currency), subtitle: (r) => r.number || r.id },
151
+ { key: 'products', label: 'Products', path: 'products', title: (r) => r.name || r.id, subtitle: (r) => r.description || r.id },
152
+ { key: 'prices', label: 'Prices', path: 'prices', title: (r) => `${formatStripeAmount(r.unit_amount, r.currency)}${formatRecurring(r.recurring) ? ` ${formatRecurring(r.recurring)}` : ''}`, subtitle: (r) => r.product ?? r.id },
153
+ { key: 'charges', label: 'Charges', path: 'charges', title: (r) => formatStripeAmount(r.amount, r.currency), subtitle: (r) => r.description || r.id },
154
+ { key: 'refunds', label: 'Refunds', path: 'refunds', title: (r) => formatStripeAmount(r.amount, r.currency), subtitle: (r) => r.charge ?? r.id },
155
+ { key: 'payment_methods', label: 'Payment Methods', path: 'payment-methods', title: (r) => formatPaymentMethod(r), subtitle: (r) => r.customer ?? r.id },
156
+ { key: 'setup_intents', label: 'Setup Intents', path: 'setup-intents', title: (r) => r.id, subtitle: (r) => r.usage ?? '' },
157
+ { key: 'invoiceitems', label: 'Invoice Items', path: 'invoice-items', title: (r) => formatStripeAmount(r.amount, r.currency), subtitle: (r) => r.description || r.customer || r.id },
158
+ { key: 'credit_notes', label: 'Credit Notes', path: 'credit-notes', title: (r) => formatStripeAmount(r.amount, r.currency), subtitle: (r) => r.invoice ?? r.id },
159
+ { key: 'tax_ids', label: 'Tax IDs', path: 'tax-ids', title: (r) => `${r.value ?? r.id}`, subtitle: (r) => `${r.type ?? ''}` },
160
+ { key: 'balance', label: 'Balance', path: 'balance', title: (r) => r.id, subtitle: () => '' },
161
+ // pages the Dashboard has, reached from the navigation
162
+ { key: 'disputes', label: 'Disputes', path: 'disputes', title: (r) => formatStripeAmount(r.amount, r.currency), subtitle: (r) => `${titleCase(String(r.reason ?? ''))} · ${r.charge ?? ''}`, page: true, columns: ['Amount', 'Reason'] },
163
+ { key: 'payouts', label: 'Payouts', path: 'payouts', title: (r) => formatStripeAmount(r.amount, r.currency), subtitle: (r) => `Arrives ${formatDate(r.arrival_date, false)}`, page: true, columns: ['Amount', 'Arrival'] },
164
+ { key: 'balance_transactions', label: 'Balance Transactions', path: 'balance-transactions', title: (r) => formatStripeAmount(r.amount, r.currency), subtitle: (r) => `${titleCase(String(r.type ?? ''))} · ${r.source ?? r.id}`, page: true, columns: ['Amount', 'Type'] },
165
+ { key: 'payment_links', label: 'Payment Links', path: 'payment-links', title: (r) => r.url || r.id, subtitle: (r) => (r.active ? 'Active' : 'Inactive'), page: true },
166
+ { key: 'coupons', label: 'Coupons', path: 'coupons', title: (r) => r.name || (r.percent_off != null ? `${r.percent_off}% off` : `${formatStripeAmount(r.amount_off, r.currency)} off`), subtitle: (r) => titleCase(String(r.duration ?? '')), page: true, columns: ['Coupon', 'Duration'] },
167
+ { key: 'promotion_codes', label: 'Promotion Codes', path: 'promotion-codes', title: (r) => r.code || r.id, subtitle: (r) => (typeof r.coupon === 'object' ? r.coupon?.id : r.coupon) ?? '', page: true },
168
+ { key: 'quotes', label: 'Quotes', path: 'quotes', title: (r) => formatStripeAmount(r.amount_total, r.currency), subtitle: (r) => r.customer ?? r.id, page: true, columns: ['Amount', 'Customer'] },
169
+ { key: 'accounts', label: 'Connected Accounts', path: 'connect/accounts', title: (r) => r.business_profile?.name || r.email || r.id, subtitle: (r) => `${titleCase(String(r.type ?? 'account'))} · ${r.country ?? ''}`, page: true },
170
+ { key: 'transfers', label: 'Transfers', path: 'connect/transfers', title: (r) => formatStripeAmount(r.amount, r.currency), subtitle: (r) => `To ${r.destination ?? '—'}`, page: true, columns: ['Amount', 'Destination'] },
171
+ { key: 'tax_rates', label: 'Tax Rates', path: 'tax/rates', title: (r) => `${r.display_name ?? 'Tax'} · ${r.percentage ?? 0}%`, subtitle: (r) => `${r.inclusive ? 'Inclusive' : 'Exclusive'} · ${r.jurisdiction ?? ''}`, page: true, columns: ['Tax rate', 'Type'] },
172
+ { key: 'reviews', label: 'Radar Reviews', path: 'radar/reviews', title: (r) => r.charge || r.payment_intent || r.id, subtitle: (r) => (r.open ? 'Open' : titleCase(String(r.closed_reason ?? 'closed'))), page: true },
173
+ { key: 'radar/value_lists', label: 'Radar Value Lists', path: 'radar/lists', title: (r) => r.name || r.alias || r.id, subtitle: (r) => `${r.item_type ?? ''} · ${r.alias ?? ''}`, page: true },
174
+ { key: 'terminal/readers', label: 'Terminal Readers', path: 'terminal/readers', title: (r) => r.label || r.id, subtitle: (r) => `${titleCase(String(r.device_type ?? ''))} · ${r.status ?? ''}`, page: true },
175
+ { key: 'terminal/locations', label: 'Terminal Locations', path: 'terminal/locations', title: (r) => r.display_name || r.id, subtitle: (r) => r.address?.city ?? '', page: true },
176
+ { key: 'issuing/cards', label: 'Issuing Cards', path: 'issuing/cards', title: (r) => `•••• ${r.last4 ?? ''}`, subtitle: (r) => `${titleCase(String(r.type ?? ''))} · ${typeof r.cardholder === 'object' ? r.cardholder?.name ?? '' : r.cardholder ?? ''}`, page: true },
177
+ { key: 'issuing/cardholders', label: 'Issuing Cardholders', path: 'issuing/cardholders', title: (r) => r.name || r.id, subtitle: (r) => r.email || titleCase(String(r.type ?? '')), page: true },
178
+ { key: 'reporting/report_runs', label: 'Reports', path: 'reports', title: (r) => r.report_type || r.id, subtitle: (r) => titleCase(String(r.status ?? '')), page: true },
179
+ { key: 'events', label: 'Events', path: 'events', title: (r) => r.type || r.id, subtitle: (r) => r.data?.object?.id ?? r.id, page: true, columns: ['Event', 'Object'] },
180
+ { key: 'webhook_endpoints', label: 'Webhook Endpoints', path: 'webhooks', title: (r) => r.url || r.id, subtitle: (r) => `${(r.enabled_events ?? []).length} events`, page: true },
181
+ ];
182
+ const SECTION_BY_PATH = new Map(SECTIONS.filter((s) => s.page).map((s) => [s.path, s]));
183
+ const statusOfRow = (r) => (r.status !== undefined ? r.status : r.active !== undefined ? String(r.active) : undefined);
184
+ /** A Stripe list table over one resource's objects. */
185
+ export function ListPane({ section, rows, query, currentId, onSelect }) {
186
+ return (_jsxs("table", { className: "table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: section.columns?.[0] ?? section.label.replace(/^(Radar|Terminal|Issuing|Tax) /, '') }), _jsx("th", { children: section.columns?.[1] ?? 'Details' }), _jsx("th", { children: "Status" }), _jsx("th", { children: "ID" }), _jsx("th", { className: "right", children: "Created" })] }) }), _jsxs("tbody", { children: [rows.length === 0 && _jsx("tr", { className: "empty-row", children: _jsx("td", { colSpan: 5, children: _jsx(EmptyState, { title: query ? 'No results' : `No ${section.label.toLowerCase()}` }) }) }), rows.map((row, i) => (_jsxs("tr", { className: `list-row ${currentId === row.id ? 'current' : ''}`, onClick: () => onSelect(row.id), children: [_jsx("td", { className: "strong", children: section.title(row) }), _jsx("td", { className: "muted", children: section.subtitle(row) }), _jsx("td", { children: statusOfRow(row) !== undefined ? _jsx(StatusPill, { value: statusOfRow(row) }) : null }), _jsx("td", { className: "mono muted", children: row.id }), _jsx("td", { className: "right muted nowrap", children: d(row) })] }, row.id ?? i)))] })] }));
187
+ }
188
+ export function globalSearch(data, query, limit = 12) {
189
+ const q = query.trim().toLowerCase();
190
+ if (!q)
191
+ return [];
192
+ const hits = [];
193
+ const add = (rows, kind, route, label, sub, fields) => {
194
+ for (const r of rows ?? []) {
195
+ if (hits.length >= limit)
196
+ return;
197
+ if (fields(r).some((f) => String(f ?? '').toLowerCase().includes(q)))
198
+ hits.push({ to: `${route}/${r.id}`, kind, label: label(r), sub: sub(r) });
199
+ }
200
+ };
201
+ add(data.customers, 'Customers', 'customers', (r) => r.name || r.email || r.id, (r) => r.email || r.id, (r) => [r.id, r.name, r.email, r.description]);
202
+ add(data.payment_intents, 'Payments', 'payments', (r) => `${formatStripeAmount(r.amount, r.currency)} ${ccy(r.currency)}`, (r) => r.description || r.id, (r) => [r.id, r.description, r.latest_charge, (Number(r.amount) / 100).toFixed(2)]);
203
+ add(data.subscriptions, 'Subscriptions', 'subscriptions', (r) => r.id, (r) => titleCase(String(r.status ?? '')), (r) => [r.id]);
204
+ add(data.invoices, 'Invoices', 'invoices', (r) => r.number || r.id, (r) => `${formatStripeAmount(r.total, r.currency)} · ${titleCase(String(r.status ?? ''))}`, (r) => [r.id, r.number, r.customer_email]);
205
+ add(data.products, 'Products', 'products', (r) => r.name || r.id, (r) => r.description || r.id, (r) => [r.id, r.name, r.description]);
206
+ return hits;
207
+ }
208
+ function SearchBox({ go }) {
209
+ const [q, setQ] = useState('');
210
+ const [open, setOpen] = useState(false);
211
+ const [data, setData] = useState({});
212
+ const input = useRef(null);
213
+ const load = useCallback(async () => {
214
+ const keys = ['customers', 'payment_intents', 'subscriptions', 'invoices', 'products'];
215
+ const lists = await Promise.all(keys.map((k) => list(`/v1/${k}${k === 'subscriptions' ? '?status=all' : ''}`).catch(() => [])));
216
+ setData(Object.fromEntries(keys.map((k, i) => [k, lists[i]])));
217
+ }, []);
218
+ useEffect(() => {
219
+ const onKey = (e) => {
220
+ const typing = e.target?.closest?.('input,textarea,select');
221
+ if (!typing && (e.key === '/' || ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k'))) {
222
+ e.preventDefault();
223
+ input.current?.focus();
224
+ }
225
+ if (e.key === 'Escape') {
226
+ setOpen(false);
227
+ input.current?.blur();
228
+ }
229
+ };
230
+ window.addEventListener('keydown', onKey);
231
+ return () => window.removeEventListener('keydown', onKey);
232
+ }, []);
233
+ const hits = useMemo(() => globalSearch(data, q), [data, q]);
234
+ const pick = (h) => { go(h.to); setOpen(false); setQ(''); input.current?.blur(); };
235
+ return (_jsxs("div", { className: "search", children: [_jsx(Icon, { name: "search", className: "search-icon" }), _jsx("input", { ref: input, className: "search-input cmdk-input", placeholder: "Search", value: q, onFocus: () => { setOpen(true); void load(); }, onBlur: () => window.setTimeout(() => setOpen(false), 150), onChange: (e) => { setQ(e.target.value); setOpen(true); }, onKeyDown: (e) => { if (e.key === 'Enter' && hits[0])
236
+ pick(hits[0]); } }), open && q ? (_jsxs("div", { className: "cmdk", role: "listbox", "aria-label": "Search results", children: [hits.length === 0 && _jsxs("div", { className: "cmdk-empty", children: ["No results for \u201C", q, "\u201D"] }), hits.map((h, i) => (_jsxs(React.Fragment, { children: [i === 0 || hits[i - 1].kind !== h.kind ? _jsx("div", { className: "cmdk-group", children: h.kind }) : null, _jsxs("button", { className: "cmdk-hit", role: "option", onMouseDown: (e) => e.preventDefault(), onClick: () => pick(h), children: [_jsx("span", { className: "cmdk-hit-label", children: h.label }), _jsx("span", { className: "cmdk-hit-sub", children: h.sub })] })] }, h.to)))] })) : null] }));
237
+ }
238
+ const PRIMARY_NAV = [
239
+ { to: 'dashboard', label: 'Home', icon: 'home' },
240
+ { to: 'balance/overview', label: 'Balances', icon: 'balance', match: ['balance', 'payouts', 'balance-transactions'] },
241
+ { to: 'payments', label: 'Transactions', icon: 'transactions', match: ['payments', 'disputes'] },
242
+ { to: 'customers', label: 'Customers', icon: 'customers' },
243
+ { to: 'products', label: 'Product catalog', icon: 'catalog', match: ['products', 'coupons', 'promotion-codes'] },
244
+ ];
245
+ const PRODUCT_NAV = [
246
+ { label: 'Connect', icon: 'connect', items: [{ to: 'connect/accounts', label: 'Connected accounts' }, { to: 'connect/transfers', label: 'Transfers' }] },
247
+ { label: 'Payments', icon: 'payments', items: [{ to: 'disputes', label: 'Disputes' }, { to: 'radar/reviews', label: 'Radar' }, { to: 'payment-links', label: 'Payment Links' }, { to: 'terminal/readers', label: 'Terminal' }] },
248
+ { label: 'Billing', icon: 'billing', items: [{ to: 'subscriptions', label: 'Subscriptions' }, { to: 'invoices', label: 'Invoices' }, { to: 'quotes', label: 'Quotes' }] },
249
+ { label: 'Reporting', icon: 'reporting', items: [{ to: 'reports', label: 'Reports' }] },
250
+ { label: 'More', icon: 'more', items: [{ to: 'tax/rates', label: 'Tax' }, { to: 'issuing/cards', label: 'Issuing' }] },
251
+ ];
252
+ const isActive = (route, item) => {
253
+ const here = route.join('/');
254
+ return [item.to, ...(item.match ?? [])].some((m) => here === m || here.startsWith(`${m}/`) || route[0] === m);
255
+ };
256
+ export function SideNav({ route, go }) {
257
+ const [open, setOpen] = useState({ Billing: true });
258
+ const link = (item, sub = false) => (_jsxs("a", { href: hrefOf(item.to), className: `nav-item ${sub ? 'nav-sub' : ''} ${isActive(route, item) ? 'active' : ''}`, onClick: (e) => { e.preventDefault(); go(item.to); }, children: [item.icon ? _jsx(Icon, { name: item.icon }) : null, _jsx("span", { children: item.label })] }, item.to));
259
+ return (_jsxs("nav", { className: "side", "aria-label": "Main", children: [_jsxs("button", { className: "account-switcher", onClick: () => go('settings'), children: [_jsx("span", { className: "account-mark", children: "T" }), _jsx("span", { className: "account-name", children: "Twin Inc." }), _jsx(Icon, { name: "chevronDown", size: 12 })] }), _jsx("div", { className: "nav-group", children: PRIMARY_NAV.map((i) => link(i)) }), _jsx("div", { className: "nav-heading", children: "Products" }), _jsx("div", { className: "nav-group", children: PRODUCT_NAV.map((g) => {
260
+ const expanded = open[g.label] || g.items.some((i) => isActive(route, i));
261
+ return (_jsxs("div", { className: "nav-folder", children: [_jsxs("button", { className: `nav-item ${expanded ? 'expanded' : ''}`, "aria-expanded": expanded, onClick: () => setOpen((o) => ({ ...o, [g.label]: !expanded })), children: [_jsx(Icon, { name: g.icon }), _jsx("span", { children: g.label }), _jsx(Icon, { name: expanded ? 'chevronDown' : 'chevronRight', size: 12, className: "nav-caret" })] }), expanded ? g.items.map((i) => link(i, true)) : null] }, g.label));
262
+ }) }), _jsx("div", { className: "nav-foot", children: link({ to: 'developers', label: 'Developers', icon: 'developers', match: ['developers', 'events', 'webhooks'] }) })] }));
263
+ }
264
+ function TopBar({ go }) {
265
+ return (_jsxs("header", { className: "topbar", children: [_jsx(SearchBox, { go: go }), _jsxs("div", { className: "topbar-actions", children: [_jsx("button", { className: "icon-btn", title: "Help", "aria-label": "Help", children: _jsx(Icon, { name: "help" }) }), _jsx("button", { className: "icon-btn", title: "Notifications", "aria-label": "Notifications", children: _jsx(Icon, { name: "bell" }) }), _jsx("button", { className: "icon-btn", title: "Settings", "aria-label": "Settings", onClick: () => go('settings'), children: _jsx(Icon, { name: "gear" }) }), _jsx("button", { className: "icon-btn create", title: "Create", "aria-label": "Create", onClick: () => go('customers?create=1'), children: _jsx(Icon, { name: "plus" }) })] })] }));
266
+ }
267
+ export function TestModeBand() {
268
+ return (_jsxs("div", { className: "test-band", role: "note", children: [_jsx("span", { className: "test-band-tag", children: "Test mode" }), _jsx("span", { className: "test-band-text", children: "You\u2019re using test data. Changes you make here don\u2019t affect your live account." })] }));
269
+ }
270
+ // ── data per page ────────────────────────────────────────────────────────────
271
+ /** Load a page's data through the API, refresh it every few seconds, and after a write. */
272
+ function useApi(loader, deps) {
273
+ const [state, setState] = useState({});
274
+ const [tick, setTick] = useState(0);
275
+ useEffect(() => {
276
+ let live = true;
277
+ const run = () => loader().then((data) => { if (live)
278
+ setState({ data }); }, (e) => { if (live)
279
+ setState((s) => ({ ...s, error: String(e?.message ?? e) })); });
280
+ void run();
281
+ const timer = window.setInterval(run, 4000);
282
+ return () => { live = false; window.clearInterval(timer); };
283
+ // eslint-disable-next-line react-hooks/exhaustive-deps
284
+ }, [...deps, tick]);
285
+ return { ...state, reload: () => setTick((t) => t + 1) };
286
+ }
287
+ function EmptyState({ title, body }) {
288
+ return _jsxs("div", { className: "empty", children: [_jsx("div", { className: "empty-title", children: title }), body ? _jsx("div", { className: "empty-body", children: body }) : null] });
289
+ }
290
+ function Loading({ error }) {
291
+ return error ? _jsx("div", { className: "page-error", role: "alert", children: error }) : _jsx("div", { className: "loading", children: "Loading\u2026" });
292
+ }
293
+ function PageHead({ title, actions, kicker, children }) {
294
+ return (_jsxs("div", { className: "page-head", children: [kicker ? _jsx("div", { className: "kicker", children: kicker }) : null, _jsxs("div", { className: "page-head-row", children: [_jsx("h1", { children: title }), actions ? _jsx("div", { className: "page-actions", children: actions }) : null] }), children] }));
295
+ }
296
+ function FilterCards({ options, value, onChange }) {
297
+ return (_jsx("div", { className: "filter-cards", role: "tablist", children: options.map((o) => (_jsxs("button", { role: "tab", "aria-selected": value === o.key, className: `filter-card ${value === o.key ? 'selected' : ''}`, onClick: () => onChange(o.key), children: [_jsx("span", { className: "filter-card-label", children: o.label }), _jsx("span", { className: "filter-card-count", children: o.count })] }, o.key))) }));
298
+ }
299
+ /** One of the toolbar's filter chips: a dashed pill that opens a list of values; a chosen value fills it. */
300
+ function FilterChip({ label, value, options, onChange }) {
301
+ const [open, setOpen] = useState(false);
302
+ const chosen = options.find((o) => o.key === value);
303
+ return (_jsxs("div", { className: "chip-wrap", children: [_jsxs("button", { className: `chip ${chosen ? 'chip-set' : ''}`, "aria-expanded": open, onClick: () => setOpen((o) => !o), children: [_jsx("span", { className: "chip-plus", onClick: (e) => { if (chosen) {
304
+ e.stopPropagation();
305
+ onChange(undefined);
306
+ setOpen(false);
307
+ } }, children: _jsx(Icon, { name: chosen ? 'x' : 'plus', size: 10 }) }), label, chosen ? _jsxs(_Fragment, { children: [_jsx("span", { className: "chip-sep", children: "|" }), _jsx("span", { className: "chip-value", children: chosen.label })] }) : null] }), open ? (_jsxs("div", { className: "popover", role: "menu", "aria-label": `Filter by ${label}`, children: [_jsxs("div", { className: "popover-title", children: ["Filter by ", label.toLowerCase()] }), options.map((o) => (_jsxs("button", { role: "menuitemradio", "aria-checked": o.key === value, className: `popover-item ${o.key === value ? 'checked' : ''}`, onClick: () => { onChange(o.key); setOpen(false); }, children: [_jsx("span", { className: "popover-radio" }), o.label] }, o.key)))] })) : null] }));
308
+ }
309
+ /** Download the rows the table shows as a CSV, as the Dashboard's Export does. */
310
+ function exportCsv(name, header, rows) {
311
+ const cell = (v) => `"${String(v).replace(/"/g, '""')}"`;
312
+ const csv = [header, ...rows].map((r) => r.map(cell).join(',')).join('\n');
313
+ const a = document.createElement('a');
314
+ a.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' }));
315
+ a.download = `${name}.csv`;
316
+ a.click();
317
+ URL.revokeObjectURL(a.href);
318
+ }
319
+ function ResultCount({ n }) {
320
+ return (_jsxs("div", { className: "table-foot", children: [_jsxs("span", { children: [n, " ", n === 1 ? 'result' : 'results'] }), _jsxs("span", { className: "pager", children: [_jsx("button", { className: "btn", disabled: true, children: "Previous" }), _jsx("button", { className: "btn", disabled: true, children: "Next" })] })] }));
321
+ }
322
+ function Section({ title, actions, children, className }) {
323
+ return (_jsxs("section", { className: `section ${className ?? ''}`, children: [_jsxs("div", { className: "section-head", children: [_jsx("h2", { children: title }), actions] }), children] }));
324
+ }
325
+ function Props({ rows }) {
326
+ return (_jsx("dl", { className: "props", children: rows.map(([k, v]) => _jsxs("div", { className: "prop", children: [_jsx("dt", { children: k }), _jsx("dd", { children: v ?? '—' })] }, k)) }));
327
+ }
328
+ function Amount({ amount, currency }) {
329
+ return _jsxs("span", { className: "amount", children: [_jsx("span", { className: "amount-value", children: money(amount, currency) }), " ", _jsx("span", { className: "amount-ccy", children: ccy(currency) })] });
330
+ }
331
+ function Link({ to, go, children, className }) {
332
+ return _jsx("a", { href: hrefOf(to), className: `link ${className ?? ''}`, onClick: (e) => { e.preventDefault(); e.stopPropagation(); go(to); }, children: children });
333
+ }
334
+ // ── Home ─────────────────────────────────────────────────────────────────────
335
+ export function computeHomeMetrics(data) {
336
+ const succeeded = data.payments.filter((p) => ['succeeded', 'refunded', 'partially_refunded', 'disputed'].includes(p.status));
337
+ const currency = succeeded[0]?.currency ?? 'usd';
338
+ const grossVolume = succeeded.reduce((s, p) => s + p.amount, 0);
339
+ const refunded = succeeded.reduce((s, p) => s + p.amountRefunded, 0);
340
+ const available = data.balance ? (data.balance.available ?? []).find((b) => b.currency === currency)?.amount ?? 0 : 0;
341
+ const pending = data.balance ? (data.balance.pending ?? []).find((b) => b.currency === currency)?.amount ?? 0 : 0;
342
+ return {
343
+ currency, grossVolume, netVolume: grossVolume - refunded, available: Number(available) || 0, pending: Number(pending) || 0,
344
+ succeededPayments: data.payments.filter((p) => p.status === 'succeeded').length,
345
+ failedPayments: data.payments.filter((p) => p.status === 'failed').length,
346
+ customers: data.customers.length,
347
+ activeSubscriptions: data.subscriptions.filter((s) => s.status === 'active' || s.status === 'trialing').length,
348
+ openInvoices: data.invoices.filter((i) => i.status === 'open').length,
349
+ };
350
+ }
351
+ function Sparkline({ points, of = points.length }) {
352
+ const w = 280, h = 64;
353
+ const max = Math.max(1, ...points);
354
+ const xy = points.map((p, i) => `${(i / Math.max(1, of - 1)) * w},${h - 4 - (p / max) * (h - 12)}`);
355
+ return (_jsxs("svg", { className: "spark", viewBox: `0 0 ${w} ${h}`, preserveAspectRatio: "none", "aria-hidden": "true", children: [_jsx("polyline", { points: xy.join(' '), fill: "none", stroke: "#635BFF", strokeWidth: "2", vectorEffect: "non-scaling-stroke" }), _jsx("line", { x1: "0", x2: w, y1: h - 1, y2: h - 1, stroke: "#EBEEF1" })] }));
356
+ }
357
+ function cumulativeByHour(payments) {
358
+ const out = new Array(24).fill(0);
359
+ for (const p of payments)
360
+ if (p.status !== 'failed' && p.status !== 'incomplete')
361
+ out[new Date(p.created * 1000).getHours()] += p.amount;
362
+ for (let i = 1; i < 24; i++)
363
+ out[i] += out[i - 1];
364
+ // today's line stops at the current hour, as Stripe's does
365
+ return out.slice(0, new Date().getHours() + 1);
366
+ }
367
+ export function HomeScreen({ go }) {
368
+ const { data, error } = useApi(loadHome, []);
369
+ if (!data)
370
+ return _jsx(Loading, { error: error });
371
+ const m = computeHomeMetrics(data);
372
+ const curve = cumulativeByHour(data.payments);
373
+ const overview = [
374
+ { key: 'gross', label: 'Gross volume', value: money(m.grossVolume, m.currency) },
375
+ { key: 'net', label: 'Net volume', value: money(m.netVolume, m.currency) },
376
+ { key: 'payments', label: 'Successful payments', value: String(m.succeededPayments) },
377
+ { key: 'failed', label: 'Failed payments', value: String(m.failedPayments) },
378
+ { key: 'customers', label: 'New customers', value: String(m.customers) },
379
+ { key: 'subs', label: 'Active subscribers', value: String(m.activeSubscriptions) },
380
+ ];
381
+ return (_jsxs("div", { className: "home-screen", children: [_jsx(PageHead, { title: "Today" }), _jsxs("div", { className: "today", children: [_jsxs("div", { className: "today-chart", children: [_jsxs("div", { className: "today-label", children: ["Gross volume ", _jsx(Icon, { name: "chevronDown", size: 12 })] }), _jsx("div", { className: "today-value", children: money(m.grossVolume, m.currency) }), _jsx(Sparkline, { points: curve, of: 24 }), _jsxs("div", { className: "today-axis", children: [_jsx("span", { children: "12:00 AM" }), _jsx("span", { children: "11:59 PM" })] })] }), _jsxs("div", { className: "today-side", children: [_jsxs("div", { className: "today-side-block", children: [_jsxs("div", { className: "today-label", children: [ccy(m.currency), " balance"] }), _jsx("div", { className: "today-side-value", children: money(m.available + m.pending, m.currency) }), _jsx(Link, { to: "balance/overview", go: go, children: "View" })] }), _jsxs("div", { className: "today-side-block", children: [_jsx("div", { className: "today-label", children: "Payouts" }), _jsx("div", { className: "today-side-value", children: money(0, m.currency) }), _jsx(Link, { to: "payouts", go: go, children: "View" })] })] })] }), _jsx("h2", { className: "overview-title", children: "Your overview" }), _jsx("div", { className: "home-metrics", children: overview.map((c) => (_jsxs("div", { className: "metric-card", "data-metric": c.key, children: [_jsx("span", { className: "metric-label", children: c.label }), _jsx("span", { className: "metric-value", children: c.value })] }, c.key))) }), _jsx(Section, { title: "Recent payments", actions: _jsx(Link, { to: "payments", go: go, children: "View all" }), children: _jsx(PaymentsTable, { payments: data.payments.slice(0, 5), go: go, compact: true }) })] }));
382
+ }
383
+ const matchesFilter = (p, f) => f === 'all' || (f === 'refunded' ? p.status === 'refunded' || p.status === 'partially_refunded' : p.status === f);
384
+ export function PaymentsTable({ payments, go, compact, noCustomer }) {
385
+ const cols = (compact ? 6 : 8) - (noCustomer ? 1 : 0);
386
+ return (_jsxs("table", { className: "table payments-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Amount" }), _jsx("th", {}), _jsx("th", { children: "Payment method" }), _jsx("th", { children: "Description" }), noCustomer ? null : _jsx("th", { children: "Customer" }), _jsx("th", { children: "Date" }), compact ? null : _jsxs(_Fragment, { children: [_jsx("th", { children: "Refunded date" }), _jsx("th", { children: "Decline reason" })] })] }) }), _jsxs("tbody", { children: [payments.length === 0 && _jsx("tr", { className: "empty-row", children: _jsx("td", { colSpan: cols, children: _jsx(EmptyState, { title: "No payments", body: compact ? undefined : 'Payments you create through the API show up here.' }) }) }), payments.map((p) => (_jsxs("tr", { className: "list-row", onClick: () => go(`payments/${p.id}`), children: [_jsx("td", { className: "nowrap", children: _jsx(Amount, { amount: p.amount, currency: p.currency }) }), _jsx("td", { children: _jsx(StatusPill, { value: p.status }) }), _jsx("td", { className: "nowrap", children: _jsx(CardLabel, { pm: p.paymentMethod }) }), _jsx("td", { className: "truncate", children: p.description ?? _jsx("span", { className: "muted", children: p.id }) }), noCustomer ? null : _jsx("td", { className: "truncate", children: p.customer ? p.customer.email || p.customer.name || p.customer.id : _jsx("span", { className: "muted", children: "\u2014" }) }), _jsx("td", { className: "nowrap muted", children: formatDate(p.created) }), compact ? null : _jsxs(_Fragment, { children: [_jsx("td", { className: "nowrap muted", children: p.refundedAt ? formatDate(p.refundedAt) : '—' }), _jsx("td", { className: "nowrap muted", children: p.declineReason ?? '—' })] })] }, p.id)))] })] }));
387
+ }
388
+ const DAY = 86400;
389
+ const DATE_RANGES = [{ key: 'today', label: 'Today' }, { key: '7d', label: 'Last 7 days' }, { key: '4w', label: 'Last 4 weeks' }, { key: '12m', label: 'Last 12 months' }];
390
+ function inRange(ts, range) {
391
+ if (!range)
392
+ return true;
393
+ const now = Date.now() / 1000;
394
+ if (range === 'today') {
395
+ const d = new Date();
396
+ d.setHours(0, 0, 0, 0);
397
+ return ts >= d.getTime() / 1000;
398
+ }
399
+ return ts >= now - ({ '7d': 7, '4w': 28, '12m': 365 }[range] ?? 0) * DAY;
400
+ }
401
+ function PaymentsPage({ go }) {
402
+ const { data, error } = useApi(loadPayments, []);
403
+ const [filter, setFilter] = useState('all');
404
+ const [chips, setChips] = useState({});
405
+ const all = data ?? [];
406
+ const payments = all.filter((p) => inRange(p.created, chips.date) && (!chips.currency || p.currency === chips.currency)
407
+ && (!chips.status || p.status === chips.status) && (!chips.method || (p.paymentMethod?.card?.brand ?? p.paymentMethod?.type) === chips.method));
408
+ const count = (f) => payments.filter((p) => matchesFilter(p, f)).length;
409
+ const shown = payments.filter((p) => matchesFilter(p, filter));
410
+ const uniq = (xs) => [...new Set(xs.filter((x) => Boolean(x)))];
411
+ return (_jsxs("div", { className: "page", children: [_jsx(PageHead, { title: "Transactions", children: _jsxs("div", { className: "tabs", children: [_jsx(Link, { to: "payments", go: go, className: "tab active", children: "Payments" }), _jsx(Link, { to: "disputes", go: go, className: "tab", children: "Disputes" })] }) }), _jsx(FilterCards, { value: filter, onChange: setFilter, options: [
412
+ { key: 'all', label: 'All', count: payments.length }, { key: 'succeeded', label: 'Succeeded', count: count('succeeded') },
413
+ { key: 'refunded', label: 'Refunded', count: count('refunded') }, { key: 'disputed', label: 'Disputed', count: count('disputed') },
414
+ { key: 'failed', label: 'Failed', count: count('failed') }, { key: 'uncaptured', label: 'Uncaptured', count: count('uncaptured') },
415
+ ] }), _jsxs("div", { className: "toolbar", children: [_jsx(FilterChip, { label: "Date and time", value: chips.date, options: DATE_RANGES, onChange: (date) => setChips((c) => ({ ...c, date })) }), _jsx(FilterChip, { label: "Currency", value: chips.currency, options: uniq(all.map((p) => p.currency)).map((c) => ({ key: c, label: ccy(c) })), onChange: (currency) => setChips((c) => ({ ...c, currency })) }), _jsx(FilterChip, { label: "Status", value: chips.status, options: uniq(all.map((p) => p.status)).map((k) => ({ key: k, label: STATUS[k]?.label ?? k })), onChange: (status) => setChips((c) => ({ ...c, status })) }), _jsx(FilterChip, { label: "Payment method", value: chips.method, options: uniq(all.map((p) => p.paymentMethod?.card?.brand ?? p.paymentMethod?.type)).map((b) => ({ key: b, label: b === 'amex' ? 'American Express' : titleCase(b) })), onChange: (method) => setChips((c) => ({ ...c, method })) }), _jsx("div", { className: "toolbar-right", children: _jsxs("button", { className: "btn", onClick: () => exportCsv('payments', ['id', 'Amount', 'Currency', 'Status', 'Description', 'Customer Email', 'Created'], shown.map((p) => [p.id, (p.amount / 100).toFixed(2), p.currency, p.status, p.description ?? '', p.customer?.email ?? '', new Date(p.created * 1000).toISOString()])), children: [_jsx(Icon, { name: "download", size: 14 }), " Export"] }) })] }), data ? _jsxs(_Fragment, { children: [_jsx(PaymentsTable, { payments: shown, go: go }), _jsx(ResultCount, { n: shown.length })] }) : _jsx(Loading, { error: error })] }));
416
+ }
417
+ const EVENT_TEXT = {
418
+ 'payment_intent.created': 'Payment started',
419
+ 'payment_intent.succeeded': 'Payment succeeded',
420
+ 'payment_intent.payment_failed': 'Payment failed',
421
+ 'payment_intent.canceled': 'Payment canceled',
422
+ 'payment_intent.amount_capturable_updated': 'Payment authorized',
423
+ 'payment_intent.requires_action': 'Payment requires authentication',
424
+ 'charge.succeeded': 'Payment succeeded',
425
+ 'charge.failed': 'Payment failed',
426
+ 'charge.captured': 'Payment captured',
427
+ 'charge.dispute.created': 'Payment disputed',
428
+ 'refund.created': 'Refund initiated',
429
+ 'refund.updated': 'Refund updated',
430
+ 'customer.created': 'Customer created',
431
+ 'customer.updated': 'Customer updated',
432
+ 'customer.subscription.created': 'Subscription created',
433
+ 'customer.subscription.updated': 'Subscription updated',
434
+ 'customer.subscription.deleted': 'Subscription canceled',
435
+ 'invoice.created': 'Invoice created',
436
+ 'invoice.finalized': 'Invoice finalized',
437
+ 'invoice.paid': 'Invoice paid',
438
+ 'invoice.payment_failed': 'Invoice payment failed',
439
+ };
440
+ function eventText(e) {
441
+ const obj = e.data?.object ?? {};
442
+ if (e.type === 'charge.refunded')
443
+ return `${money(obj.amount_refunded, obj.currency)} ${ccy(obj.currency)} refunded`;
444
+ if (e.type === 'refund.created')
445
+ return `Refund of ${money(obj.amount, obj.currency)} ${ccy(obj.currency)} initiated`;
446
+ if (e.type === 'payment_intent.payment_failed')
447
+ return `Payment failed${obj.last_payment_error?.message ? `: ${obj.last_payment_error.message}` : ''}`;
448
+ return EVENT_TEXT[e.type] ?? e.type;
449
+ }
450
+ const eventTone = (type) => (/failed|dispute/.test(type) ? 'red' : /succeeded|paid|captured/.test(type) ? 'green' : /refund/.test(type) ? 'grey' : 'blue');
451
+ function Timeline({ events }) {
452
+ if (events.length === 0)
453
+ return _jsx(EmptyState, { title: "No events" });
454
+ return (_jsx("ol", { className: "timeline", children: events.map((e) => (_jsxs("li", { className: "timeline-item", children: [_jsx("span", { className: `timeline-dot dot-${eventTone(e.type)}` }), _jsxs("div", { className: "timeline-body", children: [_jsx("div", { className: "timeline-text", children: eventText(e) }), _jsx("div", { className: "timeline-time", children: formatDate(e.created) })] })] }, e.id))) }));
455
+ }
456
+ function PaymentPage({ id, go }) {
457
+ const { data, error, reload } = useApi(() => loadPayment(id), [id]);
458
+ const [refunding, setRefunding] = useState(false);
459
+ if (!data)
460
+ return _jsx(Loading, { error: error });
461
+ return (_jsxs(_Fragment, { children: [_jsx(PaymentDetailView, { p: data, go: go, onRefund: () => setRefunding(true) }), refunding ? _jsx(RefundDialog, { payment: data, onClose: () => setRefunding(false), onDone: () => { setRefunding(false); reload(); } }) : null] }));
462
+ }
463
+ /** A payment's page: amount and status, who and how, the timeline of its events, the money, its refunds. */
464
+ export function PaymentDetailView({ p, go, onRefund }) {
465
+ const refundable = Boolean(p.charge) && (p.status === 'succeeded' || p.status === 'partially_refunded') && p.amount - p.amountRefunded > 0;
466
+ const card = p.paymentMethod?.card;
467
+ const net = p.amount - p.amountRefunded;
468
+ return (_jsxs("div", { className: "page detail-page", children: [_jsx(PageHead, { kicker: _jsxs(_Fragment, { children: [_jsx(Icon, { name: "payments", size: 12 }), " Payment"] }), title: _jsxs(_Fragment, { children: [_jsx(Amount, { amount: p.amount, currency: p.currency }), " ", _jsx(StatusPill, { value: p.status })] }), actions: refundable ? _jsxs("button", { className: "btn", onClick: onRefund, children: [_jsx(Icon, { name: "refund", size: 14 }), " Refund"] }) : null, children: _jsxs("div", { className: "summary", children: [_jsxs("div", { className: "summary-item", children: [_jsx("div", { className: "summary-label", children: "Last update" }), _jsx("div", { children: formatDate(p.events[0]?.created ?? p.created) })] }), _jsxs("div", { className: "summary-item", children: [_jsx("div", { className: "summary-label", children: "Customer" }), _jsx("div", { children: p.customer ? _jsx(Link, { to: `customers/${p.customer.id}`, go: go, children: p.customer.email || customerLabel(p.customer) }) : '—' })] }), _jsxs("div", { className: "summary-item", children: [_jsx("div", { className: "summary-label", children: "Payment method" }), _jsx("div", { children: _jsx(CardLabel, { pm: p.paymentMethod }) })] }), p.declineReason ? _jsxs("div", { className: "summary-item", children: [_jsx("div", { className: "summary-label", children: "Decline reason" }), _jsx("div", { children: p.declineReason })] }) : null] }) }), p.intent?.last_payment_error ? _jsx(PaymentErrorBanner, { error: p.intent.last_payment_error }) : null, _jsxs("div", { className: "detail-columns", children: [_jsxs("div", { className: "detail-main", children: [_jsx(Section, { title: "Timeline", children: _jsx(Timeline, { events: p.events }) }), _jsx(Section, { title: "Payment breakdown", children: _jsxs("dl", { className: "breakdown", children: [_jsxs("div", { children: [_jsx("dt", { children: "Payment amount" }), _jsxs("dd", { children: [money(p.amount, p.currency), " ", ccy(p.currency)] })] }), p.amountRefunded > 0 ? _jsxs("div", { children: [_jsx("dt", { children: "Refunded" }), _jsxs("dd", { children: ["\u2212", money(p.amountRefunded, p.currency), " ", ccy(p.currency)] })] }) : null, _jsxs("div", { className: "total", children: [_jsx("dt", { children: "Net amount" }), _jsxs("dd", { children: [money(p.status === 'failed' || p.status === 'incomplete' ? 0 : net, p.currency), " ", ccy(p.currency)] })] })] }) }), p.refunds.length > 0 ? (_jsx(Section, { title: "Refunds", children: _jsxs("table", { className: "table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Amount" }), _jsx("th", {}), _jsx("th", { children: "Reason" }), _jsx("th", { children: "ID" }), _jsx("th", { className: "right", children: "Date" })] }) }), _jsx("tbody", { children: p.refunds.map((r) => (_jsxs("tr", { children: [_jsx("td", { children: _jsx(Amount, { amount: Number(r.amount) || 0, currency: r.currency }) }), _jsx("td", { children: _jsx(StatusPill, { value: r.status }) }), _jsx("td", { children: r.reason ? titleCase(r.reason) : '—' }), _jsx("td", { className: "mono muted", children: r.id }), _jsx("td", { className: "right muted nowrap", children: formatDate(r.created) })] }, r.id))) })] }) })) : null] }), _jsxs("aside", { className: "detail-side", children: [_jsx(Section, { title: "Payment details", children: _jsx(Props, { rows: [
469
+ ['Statement descriptor', p.charge?.calculated_statement_descriptor ?? p.intent?.statement_descriptor ?? '—'],
470
+ ['Description', p.description ?? '—'],
471
+ ...(p.invoice ? [['Invoice', _jsx(Link, { to: `invoices/${p.invoice.id}`, go: go, children: p.invoice.number || p.invoice.id })]] : []),
472
+ ['Payment ID', _jsx("span", { className: "mono", children: p.intent?.id ?? p.id })],
473
+ ...(p.charge ? [['Charge ID', _jsx("span", { className: "mono", children: p.charge.id })]] : []),
474
+ ] }) }), _jsx(Section, { title: "Payment method", children: p.paymentMethod ? (_jsx(Props, { rows: [
475
+ ['ID', _jsx("span", { className: "mono", children: p.paymentMethod.id })],
476
+ ['Number', card ? _jsxs("span", { className: "card-label", children: [_jsx("span", { className: "dots", children: "\u2022\u2022\u2022\u2022 \u2022\u2022\u2022\u2022 \u2022\u2022\u2022\u2022" }), " ", card.last4] }) : '—'],
477
+ ['Expires', card ? `${String(card.exp_month).padStart(2, '0')} / ${card.exp_year}` : '—'],
478
+ ['Type', card ? `${BRANDS[card.brand] ?? titleCase(String(card.brand))} ${card.funding ?? ''} card` : titleCase(String(p.paymentMethod.type))],
479
+ ['Owner', p.paymentMethod.billing_details?.name ?? p.customer?.name ?? '—'],
480
+ ['Owner email', p.paymentMethod.billing_details?.email ?? p.customer?.email ?? '—'],
481
+ ['Origin', card?.country ? COUNTRIES[card.country] ?? card.country : '—'],
482
+ ['CVC check', card?.checks?.cvc_check ? (card.checks.cvc_check === 'pass' ? _jsxs("span", { className: "check-pass", children: [_jsx(Icon, { name: "check", size: 12 }), " Passed"] }) : titleCase(card.checks.cvc_check)) : '—'],
483
+ ] })) : _jsx(EmptyState, { title: "No payment method" }) }), _jsx(Section, { title: "Metadata", children: Object.keys(p.intent?.metadata ?? p.charge?.metadata ?? {}).length ? (_jsx(Props, { rows: Object.entries(p.intent?.metadata ?? p.charge?.metadata ?? {}).map(([k, v]) => [k, String(v)]) })) : _jsx("div", { className: "muted small", children: "No metadata" }) })] })] })] }));
484
+ }
485
+ const BRANDS = { visa: 'Visa', mastercard: 'Mastercard', amex: 'American Express', discover: 'Discover', diners: 'Diners Club', jcb: 'JCB', unionpay: 'UnionPay' };
486
+ const COUNTRIES = { US: 'United States', GB: 'United Kingdom', CA: 'Canada', DE: 'Germany', FR: 'France', AU: 'Australia', JP: 'Japan', IE: 'Ireland', NL: 'Netherlands', SG: 'Singapore' };
487
+ // ── dialogs ──────────────────────────────────────────────────────────────────
488
+ function Dialog({ title, onClose, children, footer, label }) {
489
+ return (_jsx("div", { className: "overlay", onMouseDown: onClose, children: _jsxs("div", { className: "dialog create-modal", role: "dialog", "aria-modal": "true", "aria-label": label ?? title, onMouseDown: (e) => e.stopPropagation(), children: [_jsxs("div", { className: "dialog-head", children: [_jsx("h2", { children: title }), _jsx("button", { className: "icon-btn", "aria-label": "Close", onClick: onClose, children: _jsx(Icon, { name: "x" }) })] }), _jsx("div", { className: "dialog-body", children: children }), _jsx("div", { className: "dialog-foot", children: footer })] }) }));
490
+ }
491
+ function RefundDialog({ payment, onClose, onDone }) {
492
+ const remaining = payment.amount - payment.amountRefunded;
493
+ const [amount, setAmount] = useState((remaining / 100).toFixed(2));
494
+ const [reason, setReason] = useState('');
495
+ const [busy, setBusy] = useState(false);
496
+ const [err, setErr] = useState(null);
497
+ const cents = Math.round(Number(amount) * 100);
498
+ const submit = async () => {
499
+ if (!Number.isFinite(cents) || cents <= 0) {
500
+ setErr('Enter an amount greater than 0.');
501
+ return;
502
+ }
503
+ if (cents > remaining) {
504
+ setErr(`Refund cannot be more than ${money(remaining, payment.currency)}.`);
505
+ return;
506
+ }
507
+ setBusy(true);
508
+ setErr(null);
509
+ try {
510
+ await createRefund(payment, cents === remaining && payment.amountRefunded === 0 ? undefined : cents, reason && reason !== 'other' ? reason : undefined);
511
+ onDone();
512
+ }
513
+ catch (e) {
514
+ setErr(String(e.message ?? e));
515
+ }
516
+ finally {
517
+ setBusy(false);
518
+ }
519
+ };
520
+ return (_jsxs(Dialog, { title: "Refund payment", onClose: onClose, footer: _jsxs(_Fragment, { children: [_jsx("button", { className: "btn", onClick: onClose, children: "Cancel" }), _jsx("button", { className: "btn btn-primary create-submit", disabled: busy, onClick: submit, children: busy ? 'Refunding…' : 'Refund' })] }), children: [_jsx("p", { className: "dialog-note", children: "Refunds take 5\u201310 days to appear on a customer\u2019s statement. Stripe\u2019s fees for the original payment won\u2019t be returned, but there are no additional fees for the refund." }), _jsxs("label", { className: "field create-field", children: [_jsx("span", { className: "field-label", children: "Refund" }), _jsxs("span", { className: "money-input", children: [_jsx("span", { className: "money-prefix", children: "$" }), _jsx("input", { "aria-label": "Refund amount", inputMode: "decimal", value: amount, onChange: (e) => setAmount(e.target.value) }), _jsx("span", { className: "money-suffix", children: ccy(payment.currency) })] }), payment.amountRefunded > 0 ? _jsxs("span", { className: "field-hint", children: [money(payment.amountRefunded, payment.currency), " already refunded"] }) : null] }), _jsxs("label", { className: "field create-field", children: [_jsx("span", { className: "field-label", children: "Reason" }), _jsxs("select", { "aria-label": "Reason", value: reason, onChange: (e) => setReason(e.target.value), children: [_jsx("option", { value: "", children: "Select a reason" }), _jsx("option", { value: "duplicate", children: "Duplicate" }), _jsx("option", { value: "fraudulent", children: "Fraudulent" }), _jsx("option", { value: "requested_by_customer", children: "Requested by customer" }), _jsx("option", { value: "other", children: "Other" })] })] }), err ? _jsx("div", { className: "form-error", role: "alert", children: err }) : null] }));
521
+ }
522
+ function CancelSubscriptionDialog({ subscription, onClose, onDone }) {
523
+ const [when, setWhen] = useState('now');
524
+ const [busy, setBusy] = useState(false);
525
+ const [err, setErr] = useState(null);
526
+ const submit = async () => {
527
+ setBusy(true);
528
+ setErr(null);
529
+ try {
530
+ await cancelSubscription(String(subscription.id), when);
531
+ onDone();
532
+ }
533
+ catch (e) {
534
+ setErr(String(e.message ?? e));
535
+ }
536
+ finally {
537
+ setBusy(false);
538
+ }
539
+ };
540
+ return (_jsxs(Dialog, { title: "Cancel subscription", onClose: onClose, footer: _jsxs(_Fragment, { children: [_jsx("button", { className: "btn", onClick: onClose, children: "Don\u2019t cancel" }), _jsx("button", { className: "btn btn-danger create-submit", disabled: busy, onClick: submit, children: busy ? 'Canceling…' : 'Cancel subscription' })] }), children: [_jsxs("fieldset", { className: "radio-group", children: [_jsx("legend", { className: "field-label", children: "Cancel" }), _jsxs("label", { className: "radio", children: [_jsx("input", { type: "radio", name: "when", checked: when === 'now', onChange: () => setWhen('now') }), _jsxs("span", { children: ["Immediately", _jsx("span", { className: "radio-hint", children: "The subscription ends now and no further invoices are created." })] })] }), _jsxs("label", { className: "radio", children: [_jsx("input", { type: "radio", name: "when", checked: when === 'period_end', onChange: () => setWhen('period_end') }), _jsxs("span", { children: ["End of the current period", _jsx("span", { className: "radio-hint", children: formatDate(subscription.current_period_end, false) })] })] })] }), err ? _jsx("div", { className: "form-error", role: "alert", children: err }) : null] }));
541
+ }
542
+ /** The create forms the Dashboard opens from a list page: each posts the resource's create call. */
543
+ export const CREATE_FORMS = {
544
+ customers: {
545
+ title: 'Create customer', submit: 'Add customer',
546
+ fields: [{ name: 'name', label: 'Name' }, { name: 'email', label: 'Email', type: 'email' }, { name: 'description', label: 'Description' }, { name: 'phone', label: 'Phone' }],
547
+ },
548
+ products: {
549
+ title: 'Add a product', submit: 'Add product',
550
+ fields: [{ name: 'name', label: 'Name', required: true }, { name: 'description', label: 'Description' }],
551
+ },
552
+ };
553
+ function CreateDialog({ collection, onClose, onCreated }) {
554
+ const form = CREATE_FORMS[collection];
555
+ const [values, setValues] = useState({});
556
+ const [busy, setBusy] = useState(false);
557
+ const [err, setErr] = useState(null);
558
+ const submit = async () => {
559
+ setBusy(true);
560
+ setErr(null);
561
+ try {
562
+ onCreated(await createObject(collection, values));
563
+ }
564
+ catch (e) {
565
+ setErr(String(e.message ?? e));
566
+ }
567
+ finally {
568
+ setBusy(false);
569
+ }
570
+ };
571
+ return (_jsxs(Dialog, { title: form.title, onClose: onClose, footer: _jsxs(_Fragment, { children: [_jsx("button", { className: "btn", onClick: onClose, children: "Cancel" }), _jsx("button", { className: "btn btn-primary create-submit", disabled: busy, onClick: submit, children: busy ? 'Saving…' : form.submit })] }), children: [collection === 'customers' ? _jsx("div", { className: "dialog-subhead", children: "Account information" }) : null, form.fields.map((f) => (_jsxs("label", { className: "field create-field", children: [_jsxs("span", { className: "field-label", children: [f.label, f.required ? '' : _jsx("span", { className: "optional", children: " (optional)" })] }), _jsx("input", { "aria-label": f.label, type: f.type ?? 'text', placeholder: f.placeholder ?? '', value: values[f.name] ?? '', onChange: (e) => setValues((v) => ({ ...v, [f.name]: e.target.value })) })] }, f.name))), err ? _jsx("div", { className: "form-error", role: "alert", children: err }) : null] }));
572
+ }
573
+ const SEGMENTS = [
574
+ { key: 'all', label: 'All customers', test: () => true },
575
+ { key: 'first_time', label: 'First-time customers', test: (c) => c.payments === 1 },
576
+ { key: 'repeat', label: 'Repeat customers', test: (c) => c.payments > 1 },
577
+ { key: 'recent', label: 'Recent customers', test: (c) => Number(c.created) >= Date.now() / 1000 - 30 * DAY },
578
+ { key: 'refunds', label: 'High refunds', test: (c) => c.refunds > 0 },
579
+ ];
580
+ function CustomersPage({ go, create }) {
581
+ const { data, error, reload } = useApi(loadCustomers, []);
582
+ const [creating, setCreating] = useState(create);
583
+ const [segment, setSegment] = useState('all');
584
+ const [created, setCreated] = useState();
585
+ useEffect(() => { if (create)
586
+ setCreating(true); }, [create]);
587
+ const all = (data ?? []).filter((c) => inRange(Number(c.created), created));
588
+ const rows = all.filter(SEGMENTS.find((s) => s.key === segment).test);
589
+ return (_jsxs("div", { className: "page", children: [_jsx(PageHead, { title: "Customers", actions: _jsxs("button", { className: "btn btn-primary", onClick: () => setCreating(true), children: [_jsx(Icon, { name: "plus", size: 14 }), " Create customer"] }) }), _jsx(FilterCards, { value: segment, onChange: setSegment, options: SEGMENTS.map((s) => ({ key: s.key, label: s.label, count: all.filter(s.test).length })) }), _jsxs("div", { className: "toolbar", children: [_jsx(FilterChip, { label: "Created", value: created, options: DATE_RANGES, onChange: setCreated }), _jsx("div", { className: "toolbar-right", children: _jsxs("button", { className: "btn", onClick: () => exportCsv('customers', ['id', 'Name', 'Email', 'Created', 'Total Spend', 'Payment Count', 'Refunded Volume'], rows.map((c) => [c.id, c.name ?? '', c.email ?? '', new Date(Number(c.created) * 1000).toISOString(), (c.totalSpend / 100).toFixed(2), c.payments, (c.refunds / 100).toFixed(2)])), children: [_jsx(Icon, { name: "download", size: 14 }), " Export"] }) })] }), data ? (_jsxs(_Fragment, { children: [_jsxs("table", { className: "table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Name" }), _jsx("th", { children: "Email" }), _jsx("th", { children: "Default payment method" }), _jsx("th", { children: "Created" }), _jsx("th", { className: "right", children: "Total spend" }), _jsx("th", { className: "right", children: "Payments" }), _jsx("th", { className: "right", children: "Refunds" })] }) }), _jsxs("tbody", { children: [rows.length === 0 && _jsx("tr", { className: "empty-row", children: _jsx("td", { colSpan: 7, children: _jsx(EmptyState, { title: "No customers", body: "Customers you create show up here." }) }) }), rows.map((c) => (_jsxs("tr", { className: "list-row", onClick: () => go(`customers/${c.id}`), children: [_jsx("td", { className: "strong", children: c.name || _jsx("span", { className: "muted", children: "\u2014" }) }), _jsx("td", { children: c.email || _jsx("span", { className: "muted", children: "\u2014" }) }), _jsx("td", { children: _jsx(CardLabel, { pm: c.defaultCard }) }), _jsx("td", { className: "muted nowrap", children: formatDate(c.created) }), _jsx("td", { className: "right nowrap", children: money(c.totalSpend, c.currency) }), _jsx("td", { className: "right", children: c.payments }), _jsx("td", { className: "right nowrap", children: money(c.refunds, c.currency) })] }, c.id)))] })] }), _jsx(ResultCount, { n: rows.length })] })) : _jsx(Loading, { error: error }), creating ? _jsx(CreateDialog, { collection: "customers", onClose: () => { setCreating(false); if (create)
590
+ go('customers'); }, onCreated: (c) => { setCreating(false); reload(); go(`customers/${c.id}`); } }) : null] }));
591
+ }
592
+ function subscriptionProduct(sub, products) {
593
+ const items = sub.items?.data ?? [];
594
+ const names = items.map((it) => {
595
+ const productId = typeof it.price?.product === 'string' ? it.price.product : it.price?.product?.id;
596
+ const name = products.get(productId)?.name ?? it.price?.nickname ?? productId ?? 'Product';
597
+ return Number(it.quantity) > 1 ? `${name} × ${it.quantity}` : name;
598
+ });
599
+ return names.length > 1 ? `${names[0]} and ${names.length - 1} more` : names[0] ?? '—';
600
+ }
601
+ function subscriptionAmount(sub) {
602
+ const items = sub.items?.data ?? [];
603
+ const amount = items.reduce((n, it) => n + (Number(it.price?.unit_amount) || 0) * (Number(it.quantity) || 1), 0);
604
+ const interval = items[0]?.price?.recurring?.interval ?? 'month';
605
+ return { amount, currency: String(items[0]?.price?.currency ?? sub.currency ?? 'usd'), interval };
606
+ }
607
+ export function InvoicesTable({ invoices, customers, go, noCustomer }) {
608
+ return (_jsxs("table", { className: "table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Amount" }), _jsx("th", {}), _jsx("th", { children: "Invoice number" }), noCustomer ? null : _jsx("th", { children: "Customer" }), _jsx("th", { children: "Due" }), _jsx("th", { className: "right", children: "Created" })] }) }), _jsxs("tbody", { children: [invoices.length === 0 && _jsx("tr", { className: "empty-row", children: _jsx("td", { colSpan: 6, children: _jsx(EmptyState, { title: "No invoices" }) }) }), invoices.map((inv) => {
609
+ const c = customers?.get(inv.customer);
610
+ return (_jsxs("tr", { className: "list-row", onClick: () => go(`invoices/${inv.id}`), children: [_jsx("td", { className: "nowrap", children: _jsx(Amount, { amount: Number(inv.total ?? inv.amount_due) || 0, currency: inv.currency }) }), _jsx("td", { children: _jsx(StatusPill, { value: inv.status }) }), _jsx("td", { className: "mono", children: inv.number || inv.id }), noCustomer ? null : _jsx("td", { className: "truncate", children: inv.customer_email || c?.email || c?.name || inv.customer }), _jsx("td", { className: "muted nowrap", children: inv.due_date ? formatDate(inv.due_date, false) : '—' }), _jsx("td", { className: "right muted nowrap", children: formatDate(inv.created) })] }, inv.id));
611
+ })] })] }));
612
+ }
613
+ function CustomerPage({ id, go }) {
614
+ const { data, error } = useApi(() => loadCustomer(id), [id]);
615
+ return data ? _jsx(CustomerDetailView, { data: data, go: go }) : _jsx(Loading, { error: error });
616
+ }
617
+ /** A customer's page: who they are, their subscriptions, payments, saved cards, invoices and events. */
618
+ export function CustomerDetailView({ data, go }) {
619
+ const c = data.customer;
620
+ const defaultPm = c.invoice_settings?.default_payment_method;
621
+ const spend = data.payments.filter((p) => p.status !== 'failed' && p.status !== 'incomplete').reduce((n, p) => n + p.amount - p.amountRefunded, 0);
622
+ return (_jsxs("div", { className: "page detail-page", children: [_jsxs(PageHead, { kicker: _jsxs(_Fragment, { children: [_jsx(Icon, { name: "person", size: 12 }), " Customer"] }), title: c.name || c.email || c.id, children: [_jsx("div", { className: "subtitle", children: c.email ?? '' }), _jsxs("div", { className: "summary", children: [_jsxs("div", { className: "summary-item", children: [_jsx("div", { className: "summary-label", children: "Customer since" }), _jsx("div", { children: formatDate(c.created, false) })] }), _jsxs("div", { className: "summary-item", children: [_jsx("div", { className: "summary-label", children: "Total spend" }), _jsx("div", { children: money(spend, data.payments[0]?.currency ?? 'usd') })] }), _jsxs("div", { className: "summary-item", children: [_jsx("div", { className: "summary-label", children: "Payments" }), _jsx("div", { children: data.payments.filter((p) => p.status !== 'failed' && p.status !== 'incomplete').length })] })] })] }), _jsxs("div", { className: "detail-columns", children: [_jsxs("div", { className: "detail-main", children: [_jsx(Section, { title: "Subscriptions", children: data.subscriptions.length === 0 ? _jsx("div", { className: "muted small", children: "No subscriptions" }) : (_jsx("div", { className: "sub-list", children: data.subscriptions.map((s) => {
623
+ const a = subscriptionAmount(s);
624
+ return (_jsxs("a", { href: hrefOf(`subscriptions/${s.id}`), className: "sub-card list-row", onClick: (e) => { e.preventDefault(); go(`subscriptions/${s.id}`); }, children: [_jsxs("div", { className: "sub-card-main", children: [_jsx("span", { className: "strong", children: subscriptionProduct(s, data.products) }), " ", _jsx(StatusPill, { value: s.status })] }), _jsxs("div", { className: "muted small", children: [money(a.amount, a.currency), " / ", a.interval, " \u00B7 ", s.status === 'canceled' ? `Canceled ${formatDate(s.canceled_at ?? s.ended_at, false)}` : s.status === 'trialing' ? `Trial ends ${formatDate(s.trial_end, false)}` : s.cancel_at_period_end ? `Cancels ${formatDate(s.current_period_end, false)}` : `Next invoice on ${formatDate(s.current_period_end, false)}`] })] }, s.id));
625
+ }) })) }), _jsx(Section, { title: "Payments", className: "customer-payments", children: _jsx(PaymentsTable, { payments: data.payments, go: go, compact: true, noCustomer: true }) }), _jsx(Section, { title: "Payment methods", children: data.paymentMethods.length === 0 ? _jsx("div", { className: "muted small", children: "No payment methods" }) : (_jsx("table", { className: "table", children: _jsx("tbody", { children: data.paymentMethods.map((pm) => (_jsxs("tr", { children: [_jsxs("td", { children: [_jsx(CardLabel, { pm: pm }), " ", pm.id === defaultPm ? _jsx("span", { className: "pill pill-grey", children: "Default" }) : null] }), _jsx("td", { className: "muted", children: pm.card ? `Expires ${String(pm.card.exp_month).padStart(2, '0')} / ${pm.card.exp_year}` : '' }), _jsx("td", { className: "mono muted right", children: pm.id })] }, pm.id))) }) })) }), _jsx(Section, { title: "Invoices", children: _jsx(InvoicesTable, { invoices: data.invoices, go: go, noCustomer: true }) }), _jsx(Section, { title: "Events", children: _jsx(Timeline, { events: data.events.slice(0, 10) }) })] }), _jsxs("aside", { className: "detail-side", children: [_jsx(Section, { title: "Details", children: _jsx(Props, { rows: [
626
+ ['Customer ID', _jsx("span", { className: "mono", children: c.id })],
627
+ ['Customer since', formatDate(c.created, false)],
628
+ ['Email', c.email ?? '—'],
629
+ ['Description', c.description || '—'],
630
+ ['Phone', c.phone || '—'],
631
+ ['Billing details', c.address?.line1 ? `${c.address.line1}, ${c.address.city ?? ''}` : 'No billing address'],
632
+ ['Currency', c.currency ? ccy(c.currency) : '—'],
633
+ ] }) }), _jsx(Section, { title: "Metadata", children: Object.keys(c.metadata ?? {}).length ? _jsx(Props, { rows: Object.entries(c.metadata).map(([k, v]) => [k, String(v)]) }) : _jsx("div", { className: "muted small", children: "No metadata" }) })] })] })] }));
634
+ }
635
+ function SubscriptionsPage({ go }) {
636
+ const { data, error } = useApi(loadSubscriptions, []);
637
+ const [filter, setFilter] = useState('all');
638
+ const subs = data?.subscriptions ?? [];
639
+ const shown = subs.filter((s) => filter === 'all' || s.status === filter);
640
+ return (_jsxs("div", { className: "page", children: [_jsx(PageHead, { title: "Subscriptions" }), _jsx(FilterCards, { value: filter, onChange: setFilter, options: ['all', 'active', 'trialing', 'past_due', 'canceled'].map((k) => ({ key: k, label: k === 'all' ? 'All' : STATUS[k].label, count: k === 'all' ? subs.length : subs.filter((s) => s.status === k).length })) }), data ? (_jsxs(_Fragment, { children: [_jsxs("table", { className: "table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Customer" }), _jsx("th", { children: "Status" }), _jsx("th", { children: "Billing" }), _jsx("th", { children: "Product" }), _jsx("th", { className: "right", children: "Amount" }), _jsx("th", { className: "right", children: "Created" })] }) }), _jsxs("tbody", { children: [shown.length === 0 && _jsx("tr", { className: "empty-row", children: _jsx("td", { colSpan: 6, children: _jsx(EmptyState, { title: "No subscriptions" }) }) }), shown.map((s) => {
641
+ const c = data.customers.get(s.customer);
642
+ const a = subscriptionAmount(s);
643
+ return (_jsxs("tr", { className: "list-row", onClick: () => go(`subscriptions/${s.id}`), children: [_jsxs("td", { children: [_jsx("div", { className: "strong", children: c?.name || c?.email || s.customer }), c?.name && c?.email ? _jsx("div", { className: "muted small", children: c.email }) : null] }), _jsxs("td", { children: [_jsx(StatusPill, { value: s.status }), s.cancel_at_period_end && s.status !== 'canceled' ? _jsxs("span", { className: "muted small", children: [" Cancels ", formatDate(s.current_period_end, false)] }) : null] }), _jsx("td", { children: s.collection_method === 'send_invoice' ? 'Send invoice' : 'Auto' }), _jsx("td", { children: subscriptionProduct(s, data.products) }), _jsxs("td", { className: "right nowrap", children: [money(a.amount, a.currency), " / ", a.interval] }), _jsx("td", { className: "right muted nowrap", children: formatDate(s.created) })] }, s.id));
644
+ })] })] }), _jsx(ResultCount, { n: shown.length })] })) : _jsx(Loading, { error: error })] }));
645
+ }
646
+ function SubscriptionPage({ id, go }) {
647
+ const { data, error, reload } = useApi(() => loadSubscription(id), [id]);
648
+ const [canceling, setCanceling] = useState(false);
649
+ if (!data)
650
+ return _jsx(Loading, { error: error });
651
+ return (_jsxs(_Fragment, { children: [_jsx(SubscriptionDetailView, { data: data, go: go, onCancel: () => setCanceling(true) }), canceling ? _jsx(CancelSubscriptionDialog, { subscription: data.subscription, onClose: () => setCanceling(false), onDone: () => { setCanceling(false); reload(); } }) : null] }));
652
+ }
653
+ /** A subscription's page: whose, on what, its period and next invoice, its prices, invoices and events. */
654
+ export function SubscriptionDetailView({ data, go, onCancel }) {
655
+ const s = data.subscription;
656
+ const c = data.customer;
657
+ const a = subscriptionAmount(s);
658
+ const product = subscriptionProduct(s, data.products);
659
+ const live = s.status !== 'canceled' && s.status !== 'incomplete_expired';
660
+ const next = s.status === 'canceled' ? `Canceled on ${formatDate(s.canceled_at ?? s.ended_at, false)}`
661
+ : s.cancel_at_period_end ? `Cancels on ${formatDate(s.current_period_end, false)}`
662
+ : s.status === 'trialing' ? `Trial ends ${formatDate(s.trial_end, false)} · First invoice for ${money(a.amount, a.currency)}`
663
+ : `Next invoice on ${formatDate(s.current_period_end, false)} for ${money(a.amount, a.currency)}`;
664
+ return (_jsxs("div", { className: "page detail-page", children: [_jsxs(PageHead, { kicker: _jsxs(_Fragment, { children: [_jsx(Icon, { name: "billing", size: 12 }), " Subscription"] }), title: _jsxs(_Fragment, { children: [customerLabel(c), " on ", product, " ", _jsx(StatusPill, { value: s.status })] }), actions: live && !s.cancel_at_period_end ? _jsx("button", { className: "btn", onClick: onCancel, children: "Cancel subscription" }) : null, children: [_jsxs("div", { className: "subtitle", children: ["Billing ", a.interval, "ly \u00B7 ", next] }), _jsxs("div", { className: "summary", children: [_jsxs("div", { className: "summary-item", children: [_jsx("div", { className: "summary-label", children: "Started" }), _jsx("div", { children: formatDate(s.start_date ?? s.created, false) })] }), _jsxs("div", { className: "summary-item", children: [_jsx("div", { className: "summary-label", children: s.status === 'canceled' ? 'Ended' : s.cancel_at_period_end ? 'Cancels' : 'Next invoice' }), _jsx("div", { children: s.status === 'canceled' ? formatDate(s.ended_at ?? s.canceled_at, false) : s.cancel_at_period_end ? formatDate(s.current_period_end, false) : `${money(a.amount, a.currency)} on ${formatDate(s.current_period_end, false)}` })] }), _jsxs("div", { className: "summary-item", children: [_jsx("div", { className: "summary-label", children: "Customer" }), _jsx("div", { children: c ? _jsx(Link, { to: `customers/${c.id}`, go: go, children: c.email || c.name }) : s.customer })] })] })] }), _jsx(Section, { title: "Subscription details", children: _jsx(Props, { rows: [
665
+ ['Customer', c ? _jsx(Link, { to: `customers/${c.id}`, go: go, children: customerLabel(c) }) : s.customer],
666
+ ['Created', formatDate(s.created)],
667
+ ['Current period', `${formatDate(s.current_period_start, false).replace(/, \d{4}$/, '')} to ${formatDate(s.current_period_end, false)}`],
668
+ ...(s.trial_end ? [['Trial', `${formatDate(s.trial_start, false)} to ${formatDate(s.trial_end, false)}`]] : []),
669
+ ['ID', _jsx("span", { className: "mono", children: s.id })],
670
+ ['Billing method', s.collection_method === 'send_invoice' ? 'Email invoice to the customer' : 'Charge default payment method'],
671
+ ['Payment method', _jsx(CardLabel, { pm: data.paymentMethod })],
672
+ ['Discounts', (s.discounts ?? []).length ? `${s.discounts.length}` : '—'],
673
+ ] }) }), _jsx(Section, { title: "Pricing", children: _jsxs("table", { className: "table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Product" }), _jsx("th", { className: "right", children: "Quantity" }), _jsx("th", { className: "right", children: "Unit price" }), _jsx("th", { className: "right", children: "Total" })] }) }), _jsx("tbody", { children: (s.items?.data ?? []).map((it) => {
674
+ const productId = typeof it.price?.product === 'string' ? it.price.product : it.price?.product?.id;
675
+ const unit = Number(it.price?.unit_amount) || 0;
676
+ const q = Number(it.quantity) || 1;
677
+ return (_jsxs("tr", { children: [_jsxs("td", { children: [_jsx("div", { className: "strong", children: data.products.get(productId)?.name ?? productId }), _jsxs("div", { className: "muted small", children: [money(unit, it.price?.currency), " / ", it.price?.recurring?.interval ?? 'month'] })] }), _jsx("td", { className: "right", children: q }), _jsx("td", { className: "right", children: money(unit, it.price?.currency) }), _jsxs("td", { className: "right", children: [money(unit * q, it.price?.currency), " ", ccy(it.price?.currency)] })] }, it.id));
678
+ }) })] }) }), _jsx(Section, { title: "Invoices", children: _jsx(InvoicesTable, { invoices: data.invoices, go: go, noCustomer: true }) }), _jsx(Section, { title: "Events", children: _jsx(Timeline, { events: data.events }) })] }));
679
+ }
680
+ function InvoicesPage({ go }) {
681
+ const { data, error } = useApi(loadInvoices, []);
682
+ const [filter, setFilter] = useState('all');
683
+ const invoices = data?.invoices ?? [];
684
+ const shown = invoices.filter((i) => filter === 'all' || i.status === filter);
685
+ return (_jsxs("div", { className: "page", children: [_jsx(PageHead, { title: "Invoices" }), _jsx(FilterCards, { value: filter, onChange: setFilter, options: ['all', 'draft', 'open', 'paid', 'void', 'uncollectible'].map((k) => ({ key: k, label: k === 'all' ? 'All invoices' : STATUS[k].label, count: k === 'all' ? invoices.length : invoices.filter((i) => i.status === k).length })) }), data ? _jsxs(_Fragment, { children: [_jsx(InvoicesTable, { invoices: shown, customers: data.customers, go: go }), _jsx(ResultCount, { n: shown.length })] }) : _jsx(Loading, { error: error })] }));
686
+ }
687
+ function InvoicePage({ id, go }) {
688
+ const { data, error } = useApi(async () => {
689
+ const invoice = await retrieve(`/v1/invoices/${id}`);
690
+ const customer = typeof invoice.customer === 'string' ? await retrieve(`/v1/customers/${invoice.customer}`).catch(() => undefined) : undefined;
691
+ return { invoice, customer };
692
+ }, [id]);
693
+ if (!data)
694
+ return _jsx(Loading, { error: error });
695
+ const inv = data.invoice;
696
+ const lines = inv.lines?.data ?? [];
697
+ return (_jsxs("div", { className: "page detail-page", children: [_jsx(PageHead, { kicker: _jsxs(_Fragment, { children: [_jsx(Icon, { name: "billing", size: 12 }), " Invoice"] }), title: _jsxs(_Fragment, { children: [_jsx(Amount, { amount: Number(inv.total ?? inv.amount_due) || 0, currency: inv.currency }), " ", _jsx(StatusPill, { value: inv.status })] }), children: _jsxs("div", { className: "summary", children: [_jsxs("div", { className: "summary-item", children: [_jsx("div", { className: "summary-label", children: "Invoice number" }), _jsx("div", { className: "mono", children: inv.number || inv.id })] }), _jsxs("div", { className: "summary-item", children: [_jsx("div", { className: "summary-label", children: "Customer" }), _jsx("div", { children: data.customer ? _jsx(Link, { to: `customers/${data.customer.id}`, go: go, children: data.customer.email || data.customer.name }) : inv.customer })] }), _jsxs("div", { className: "summary-item", children: [_jsx("div", { className: "summary-label", children: inv.status === 'paid' ? 'Paid' : 'Due' }), _jsx("div", { children: inv.status === 'paid' ? formatDate(inv.status_transitions?.paid_at) : inv.due_date ? formatDate(inv.due_date, false) : '—' })] })] }) }), _jsx(Section, { title: "Summary", children: _jsxs("table", { className: "table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Description" }), _jsx("th", { className: "right", children: "Qty" }), _jsx("th", { className: "right", children: "Amount" })] }) }), _jsxs("tbody", { children: [lines.map((l) => _jsxs("tr", { children: [_jsx("td", { children: l.description || (l.price?.product ? `Subscription (${formatDate(l.period?.start, false)} – ${formatDate(l.period?.end, false)})` : '—') }), _jsx("td", { className: "right", children: l.quantity ?? 1 }), _jsx("td", { className: "right", children: money(l.amount, l.currency) })] }, l.id)), _jsxs("tr", { className: "table-total", children: [_jsx("td", { children: "Total" }), _jsx("td", {}), _jsx("td", { className: "right strong", children: money(inv.total, inv.currency) })] }), _jsxs("tr", { className: "table-total", children: [_jsx("td", { children: "Amount due" }), _jsx("td", {}), _jsx("td", { className: "right strong", children: money(inv.amount_remaining ?? inv.amount_due, inv.currency) })] })] })] }) }), _jsx(Section, { title: "Details", children: _jsx(Props, { rows: [
698
+ ['ID', _jsx("span", { className: "mono", children: inv.id })],
699
+ ['Billing', inv.collection_method === 'send_invoice' ? 'Send invoice' : 'Charge automatically'],
700
+ ...(inv.subscription ? [['Subscription', _jsx(Link, { to: `subscriptions/${inv.subscription}`, go: go, children: inv.subscription })]] : []),
701
+ ...(inv.payment_intent ? [['Payment', _jsx(Link, { to: `payments/${inv.payment_intent}`, go: go, children: inv.payment_intent })]] : []),
702
+ ['Created', formatDate(inv.created)],
703
+ ] }) })] }));
704
+ }
705
+ // ── Product catalog ──────────────────────────────────────────────────────────
706
+ function ProductsPage({ go }) {
707
+ const { data, error, reload } = useApi(loadProducts, []);
708
+ const [creating, setCreating] = useState(false);
709
+ return (_jsxs("div", { className: "page", children: [_jsx(PageHead, { title: "Product catalog", actions: _jsxs("button", { className: "btn btn-primary", onClick: () => setCreating(true), children: [_jsx(Icon, { name: "plus", size: 14 }), " Create product"] }), children: _jsxs("div", { className: "tabs", children: [_jsx(Link, { to: "products", go: go, className: "tab active", children: "Products" }), _jsx(Link, { to: "coupons", go: go, className: "tab", children: "Coupons" })] }) }), data ? (_jsxs(_Fragment, { children: [_jsxs("table", { className: "table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Name" }), _jsx("th", { children: "Pricing" }), _jsx("th", { children: "Created" }), _jsx("th", { className: "right", children: "Updated" })] }) }), _jsxs("tbody", { children: [data.products.length === 0 && _jsx("tr", { className: "empty-row", children: _jsx("td", { colSpan: 4, children: _jsx(EmptyState, { title: "No products" }) }) }), data.products.map((p) => {
710
+ const prices = data.prices.filter((x) => x.product === p.id);
711
+ const first = prices[prices.length - 1];
712
+ return (_jsxs("tr", { children: [_jsx("td", { children: _jsxs("div", { className: "product-cell", children: [_jsx("span", { className: "product-thumb", children: p.images?.[0] ? _jsx("img", { src: p.images[0], alt: "" }) : _jsx(Icon, { name: "catalog" }) }), _jsxs("div", { children: [_jsx("div", { className: "strong", children: p.name }), p.description ? _jsx("div", { className: "muted small", children: p.description }) : null] })] }) }), _jsx("td", { children: first ? _jsxs(_Fragment, { children: [_jsxs("span", { children: [money(first.unit_amount, first.currency), " ", ccy(first.currency), first.recurring ? ` / ${first.recurring.interval}` : ''] }), prices.length > 1 ? _jsxs("div", { className: "muted small", children: [prices.length, " prices"] }) : null] }) : _jsx("span", { className: "muted", children: "No prices" }) }), _jsx("td", { className: "muted nowrap", children: formatDate(p.created, false) }), _jsx("td", { className: "right muted nowrap", children: formatDate(p.updated ?? p.created, false) })] }, p.id));
713
+ })] })] }), _jsx(ResultCount, { n: data.products.length })] })) : _jsx(Loading, { error: error }), creating ? _jsx(CreateDialog, { collection: "products", onClose: () => setCreating(false), onCreated: () => { setCreating(false); reload(); } }) : null] }));
714
+ }
715
+ // ── Balances ─────────────────────────────────────────────────────────────────
716
+ function BalancesPage({ go }) {
717
+ const { data, error } = useApi(loadBalances, []);
718
+ if (!data)
719
+ return _jsx(Loading, { error: error });
720
+ const buckets = (k) => (data.balance[k] ?? []);
721
+ const currencies = [...new Set([...buckets('available'), ...buckets('pending')].map((b) => b.currency))];
722
+ return (_jsxs("div", { className: "page", children: [_jsx(PageHead, { title: "Balances" }), _jsxs(Section, { title: "Balance summary", children: [_jsx(BalanceSummary, { row: data.balance }), _jsxs("table", { className: "table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Payments" }), _jsx("th", { className: "right", children: "Amount" })] }) }), _jsx("tbody", { children: currencies.map((c) => {
723
+ const pending = buckets('pending').find((b) => b.currency === c)?.amount ?? 0;
724
+ const available = buckets('available').find((b) => b.currency === c)?.amount ?? 0;
725
+ return (_jsxs(React.Fragment, { children: [_jsxs("tr", { children: [_jsx("td", { children: "Incoming" }), _jsxs("td", { className: "right", children: [money(pending, c), " ", ccy(c)] })] }), _jsxs("tr", { children: [_jsx("td", { children: "Available to pay out" }), _jsxs("td", { className: "right", children: [money(available, c), " ", ccy(c)] })] }), _jsxs("tr", { className: "table-total", children: [_jsx("td", { children: "Total" }), _jsxs("td", { className: "right strong", children: [money(pending + available, c), " ", ccy(c)] })] })] }, c));
726
+ }) })] })] }), _jsx(Section, { title: "Recent activity", actions: _jsx(Link, { to: "balance-transactions", go: go, children: "View all" }), children: _jsx(ListPane, { section: SECTIONS.find((s) => s.key === 'balance_transactions'), rows: data.transactions.slice(0, 10), onSelect: () => { } }) }), _jsx(Section, { title: "Payouts", actions: _jsx(Link, { to: "payouts", go: go, children: "View all" }), children: _jsx(ListPane, { section: SECTIONS.find((s) => s.key === 'payouts'), rows: data.payouts.slice(0, 10), onSelect: () => { } }) })] }));
727
+ }
728
+ // ── Developers and Settings ──────────────────────────────────────────────────
729
+ export function payoutScheduleText(account) {
730
+ const sched = account?.settings?.payouts?.schedule;
731
+ if (!sched || typeof sched !== 'object')
732
+ return 'Daily';
733
+ const interval = String(sched.interval ?? 'daily');
734
+ if (interval === 'weekly' && sched.weekly_anchor)
735
+ return `Weekly on ${titleCase(String(sched.weekly_anchor))}`;
736
+ if (interval === 'monthly' && sched.monthly_anchor != null)
737
+ return `Monthly on day ${sched.monthly_anchor}`;
738
+ return titleCase(interval);
739
+ }
740
+ function DevelopersPage({ go }) {
741
+ const { data, error } = useApi(loadDevelopers, []);
742
+ if (!data)
743
+ return _jsx(Loading, { error: error });
744
+ return (_jsxs("div", { className: "page", children: [_jsx(PageHead, { title: "Developers" }), _jsxs("section", { className: "section settings-block", "data-settings": "api-keys", children: [_jsx("div", { className: "section-head", children: _jsx("h2", { children: "API keys" }) }), _jsxs("table", { className: "table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Name" }), _jsx("th", { children: "Token" }), _jsx("th", { className: "right", children: "Created" })] }) }), _jsxs("tbody", { children: [_jsxs("tr", { children: [_jsx("td", { className: "strong", children: "Publishable key" }), _jsx("td", { className: "mono", children: "pk_test_twin" }), _jsx("td", { className: "right muted", children: "\u2014" })] }), _jsxs("tr", { children: [_jsx("td", { className: "strong", children: "Secret key" }), _jsx("td", { className: "mono", children: "sk_test_\u2022\u2022\u2022\u2022twin" }), _jsx("td", { className: "right muted", children: "\u2014" })] })] })] })] }), _jsxs("section", { className: "section settings-block", "data-settings": "webhooks", children: [_jsx("div", { className: "section-head", children: _jsx("h2", { children: "Webhooks" }) }), _jsxs("table", { className: "table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Endpoint" }), _jsx("th", { children: "Status" }), _jsx("th", { className: "right", children: "Listening to" })] }) }), _jsxs("tbody", { children: [data.webhooks.length === 0 && _jsx("tr", { className: "empty-row", children: _jsx("td", { colSpan: 3, children: _jsx(EmptyState, { title: "No webhook endpoints" }) }) }), data.webhooks.map((w) => _jsxs("tr", { children: [_jsx("td", { className: "mono", children: w.url ?? w.id }), _jsx("td", { children: _jsx(StatusPill, { value: w.status ?? 'enabled' }) }), _jsxs("td", { className: "right muted", children: [(w.enabled_events ?? []).length, " events"] })] }, w.id))] })] })] }), _jsx(Section, { title: "Events", actions: _jsx(Link, { to: "events", go: go, children: "View all" }), children: _jsxs("table", { className: "table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Event" }), _jsx("th", { children: "Type" }), _jsx("th", { children: "Object" }), _jsx("th", { className: "right", children: "Date" })] }) }), _jsx("tbody", { children: data.events.slice(0, 15).map((e) => (_jsxs("tr", { children: [_jsx("td", { children: eventText(e) }), _jsx("td", { className: "mono muted", children: e.type }), _jsx("td", { className: "mono muted", children: e.data?.object?.id }), _jsx("td", { className: "right muted nowrap", children: formatDate(e.created) })] }, e.id))) })] }) })] }));
745
+ }
746
+ function SettingsPage() {
747
+ const { data, error } = useApi(loadDevelopers, []);
748
+ if (!data)
749
+ return _jsx(Loading, { error: error });
750
+ const account = data.account;
751
+ return (_jsxs("div", { className: "page", children: [_jsx(PageHead, { title: "Settings" }), _jsxs("section", { className: "section settings-block", "data-settings": "account", children: [_jsx("div", { className: "section-head", children: _jsx("h2", { children: "Business details" }) }), _jsx(Props, { rows: [
752
+ ['Account ID', _jsx("span", { className: "mono", children: account?.id ?? '—' })],
753
+ ['Account name', account?.settings?.dashboard?.display_name ?? PLATFORM_DEFAULT_NAME],
754
+ ['Country', account?.country ?? '—'],
755
+ ['Default currency', String(account?.default_currency ?? 'usd').toUpperCase()],
756
+ ] })] }), _jsxs("section", { className: "section settings-block", "data-settings": "public-details", children: [_jsxs("div", { className: "section-head", children: [_jsx("h2", { children: "Public details" }), _jsx("a", { className: "btn", href: `${API_BASE}/settings/public`, children: "Edit" })] }), _jsx(Props, { rows: [['Business name', _jsx("span", { className: "settings-public-name", children: publicBusinessName(account) })]] })] }), _jsxs("section", { className: "section settings-block", "data-settings": "payouts", children: [_jsx("div", { className: "section-head", children: _jsx("h2", { children: "Payouts" }) }), _jsx(Props, { rows: [['Payout schedule', _jsx("span", { className: "settings-payout-schedule", children: payoutScheduleText(account) })]] })] })] }));
757
+ }
758
+ function ResourcePage({ section, go }) {
759
+ const { data, error } = useApi(() => list(`/v1/${section.key}`), [section.key]);
760
+ const account = section.key === 'accounts' ? data?.[0] : undefined;
761
+ const tabs = section.path.startsWith('radar/') ? ['radar/reviews', 'radar/lists']
762
+ : section.path.startsWith('terminal/') ? ['terminal/readers', 'terminal/locations']
763
+ : section.path.startsWith('issuing/') ? ['issuing/cards', 'issuing/cardholders']
764
+ : section.path.startsWith('connect/') ? ['connect/accounts', 'connect/transfers']
765
+ : section.key === 'payouts' || section.key === 'balance_transactions' ? ['payouts', 'balance-transactions'] : [];
766
+ const title = section.path.startsWith('radar/') ? 'Radar' : section.path.startsWith('terminal/') ? 'Terminal' : section.path.startsWith('issuing/') ? 'Issuing'
767
+ : section.path.startsWith('connect/') ? 'Connect' : section.key === 'tax_rates' ? 'Tax' : section.label;
768
+ return (_jsxs("div", { className: "page", children: [_jsx(PageHead, { title: title, children: tabs.length ? _jsx("div", { className: "tabs", children: tabs.map((t) => _jsx(Link, { to: t, go: go, className: `tab ${t === section.path ? 'active' : ''}`, children: SECTION_BY_PATH.get(t).label.replace(/^(Radar|Terminal|Issuing) /, '') }, t)) }) : null }), account ? _jsx(ConnectAccountPanel, { row: account }) : null, data ? _jsxs(_Fragment, { children: [_jsx(ListPane, { section: section, rows: data, onSelect: () => { } }), _jsx(ResultCount, { n: data.length })] }) : _jsx(Loading, { error: error })] }));
769
+ }
770
+ // ── the app ──────────────────────────────────────────────────────────────────
771
+ export function Page({ route, search, go }) {
772
+ const [head, id] = route;
773
+ const joined = route.join('/');
774
+ if (!head || head === 'dashboard')
775
+ return _jsx(HomeScreen, { go: go });
776
+ if (head === 'payments')
777
+ return id ? _jsx(PaymentPage, { id: id, go: go }) : _jsx(PaymentsPage, { go: go });
778
+ if (head === 'customers')
779
+ return id ? _jsx(CustomerPage, { id: id, go: go }) : _jsx(CustomersPage, { go: go, create: new URLSearchParams(search).has('create') });
780
+ if (head === 'subscriptions')
781
+ return id ? _jsx(SubscriptionPage, { id: id, go: go }) : _jsx(SubscriptionsPage, { go: go });
782
+ if (head === 'invoices')
783
+ return id ? _jsx(InvoicePage, { id: id, go: go }) : _jsx(InvoicesPage, { go: go });
784
+ if (head === 'products')
785
+ return _jsx(ProductsPage, { go: go });
786
+ if (head === 'balance')
787
+ return _jsx(BalancesPage, { go: go });
788
+ if (head === 'developers')
789
+ return _jsx(DevelopersPage, { go: go });
790
+ if (head === 'settings')
791
+ return _jsx(SettingsPage, {});
792
+ const section = SECTION_BY_PATH.get(joined) ?? SECTION_BY_PATH.get(head);
793
+ if (section)
794
+ return _jsx(ResourcePage, { section: section, go: go }, section.key);
795
+ return _jsx("div", { className: "page", children: _jsx(EmptyState, { title: "Page not found", body: "This page doesn\u2019t exist in the Dashboard." }) });
796
+ }
797
+ export function App() {
798
+ const [loc, setLoc] = useState(() => ({ path: window.location.pathname, search: window.location.search }));
799
+ useEffect(() => {
800
+ const onPop = () => setLoc({ path: window.location.pathname, search: window.location.search });
801
+ window.addEventListener('popstate', onPop);
802
+ return () => window.removeEventListener('popstate', onPop);
803
+ }, []);
804
+ const go = useCallback((to) => {
805
+ const href = hrefOf(to);
806
+ window.history.pushState(null, '', href);
807
+ const u = new URL(href, window.location.origin);
808
+ setLoc({ path: u.pathname, search: u.search });
809
+ window.scrollTo(0, 0);
810
+ }, []);
811
+ const route = routeOf(loc.path);
812
+ useEffect(() => {
813
+ if (route.length === 0)
814
+ window.history.replaceState(null, '', hrefOf('dashboard'));
815
+ const [head, id] = route;
816
+ const names = { dashboard: 'Home', payments: 'Payments', customers: 'Customers', subscriptions: 'Subscriptions', invoices: 'Invoices', products: 'Product catalog', balance: 'Balances', developers: 'Developers', settings: 'Settings' };
817
+ document.title = `${id ? `${id} – ` : ''}${names[head ?? 'dashboard'] ?? SECTION_BY_PATH.get(route.join('/'))?.label ?? 'Dashboard'} – Stripe`;
818
+ }, [loc.path]);
819
+ return (_jsxs("div", { className: "dash", children: [_jsx(TestModeBand, {}), _jsxs("div", { className: "frame", children: [_jsx(SideNav, { route: route, go: go }), _jsxs("div", { className: "main", children: [_jsx(TopBar, { go: go }), _jsx("main", { className: "content", children: _jsx(Page, { route: route, search: loc.search, go: go }) })] })] })] }));
820
+ }
821
+ if (typeof document !== 'undefined' && document.getElementById('root')) {
822
+ createRoot(document.getElementById('root')).render(_jsx(App, {}));
823
+ }