@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.
- package/README.md +64 -27
- package/client/dashboard-api.ts +286 -0
- package/client/stripe-mirror.css +272 -159
- package/client/stripe-mirror.tsx +1384 -541
- package/dist/client/dashboard-api.d.ts +107 -0
- package/dist/client/dashboard-api.js +238 -0
- package/dist/client/dashboard-api.ts +286 -0
- package/dist/client/stripe-mirror.bundle.js +236 -0
- package/dist/client/stripe-mirror.css +275 -0
- package/dist/client/stripe-mirror.d.ts +134 -0
- package/dist/client/stripe-mirror.js +823 -0
- package/dist/client/stripe-mirror.tsx +1534 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +39 -0
- package/dist/src/generated/events.gen.json +1 -0
- package/dist/src/generated/surface.gen.json +1 -0
- package/dist/src/generated/ui.gen.json +1 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +73 -0
- package/dist/src/manifest.d.ts +2 -0
- package/dist/src/manifest.js +1065 -0
- package/dist/src/screens/checkout.d.ts +31 -0
- package/dist/src/screens/checkout.js +241 -0
- package/dist/src/screens/consent-skin.d.ts +4 -0
- package/dist/src/screens/consent-skin.js +18 -0
- package/dist/src/screens/financial-connections.d.ts +5 -0
- package/dist/src/screens/financial-connections.js +90 -0
- package/dist/src/screens/identity.d.ts +5 -0
- package/dist/src/screens/identity.js +86 -0
- package/dist/src/screens/industries.d.ts +1 -0
- package/dist/src/screens/industries.js +267 -0
- package/dist/src/screens/onboarding.d.ts +13 -0
- package/dist/src/screens/onboarding.js +225 -0
- package/dist/src/screens/portal.d.ts +5 -0
- package/dist/src/screens/portal.js +214 -0
- package/dist/src/screens/public-details.d.ts +5 -0
- package/dist/src/screens/public-details.js +90 -0
- package/dist/src/semantics/after-payment.d.ts +22 -0
- package/dist/src/semantics/after-payment.js +93 -0
- package/dist/src/semantics/apps-secrets.d.ts +2 -0
- package/dist/src/semantics/apps-secrets.js +54 -0
- package/dist/src/semantics/balance.d.ts +11 -0
- package/dist/src/semantics/balance.js +195 -0
- package/dist/src/semantics/billing.d.ts +2 -0
- package/dist/src/semantics/billing.js +220 -0
- package/dist/src/semantics/charges.d.ts +28 -0
- package/dist/src/semantics/charges.js +201 -0
- package/dist/src/semantics/checkout.d.ts +15 -0
- package/dist/src/semantics/checkout.js +303 -0
- package/dist/src/semantics/connect.d.ts +5 -0
- package/dist/src/semantics/connect.js +476 -0
- package/dist/src/semantics/coupons.d.ts +6 -0
- package/dist/src/semantics/coupons.js +92 -0
- package/dist/src/semantics/credit-notes.d.ts +2 -0
- package/dist/src/semantics/credit-notes.js +172 -0
- package/dist/src/semantics/customers.d.ts +6 -0
- package/dist/src/semantics/customers.js +429 -0
- package/dist/src/semantics/disputes.d.ts +2 -0
- package/dist/src/semantics/disputes.js +51 -0
- package/dist/src/semantics/entitlements.d.ts +2 -0
- package/dist/src/semantics/entitlements.js +95 -0
- package/dist/src/semantics/ephemeral-keys.d.ts +2 -0
- package/dist/src/semantics/ephemeral-keys.js +34 -0
- package/dist/src/semantics/files.d.ts +2 -0
- package/dist/src/semantics/files.js +125 -0
- package/dist/src/semantics/invoices.d.ts +18 -0
- package/dist/src/semantics/invoices.js +541 -0
- package/dist/src/semantics/issuing.d.ts +13 -0
- package/dist/src/semantics/issuing.js +570 -0
- package/dist/src/semantics/ledger.d.ts +54 -0
- package/dist/src/semantics/ledger.js +181 -0
- package/dist/src/semantics/payment-intents.d.ts +18 -0
- package/dist/src/semantics/payment-intents.js +404 -0
- package/dist/src/semantics/payment-links.d.ts +2 -0
- package/dist/src/semantics/payment-links.js +133 -0
- package/dist/src/semantics/payment-methods.d.ts +20 -0
- package/dist/src/semantics/payment-methods.js +138 -0
- package/dist/src/semantics/plans.d.ts +5 -0
- package/dist/src/semantics/plans.js +121 -0
- package/dist/src/semantics/platform.d.ts +9 -0
- package/dist/src/semantics/platform.js +206 -0
- package/dist/src/semantics/products.d.ts +2 -0
- package/dist/src/semantics/products.js +140 -0
- package/dist/src/semantics/radar.d.ts +2 -0
- package/dist/src/semantics/radar.js +83 -0
- package/dist/src/semantics/refunds.d.ts +9 -0
- package/dist/src/semantics/refunds.js +195 -0
- package/dist/src/semantics/renewals.d.ts +47 -0
- package/dist/src/semantics/renewals.js +251 -0
- package/dist/src/semantics/setup-intents.d.ts +2 -0
- package/dist/src/semantics/setup-intents.js +84 -0
- package/dist/src/semantics/shared.d.ts +78 -0
- package/dist/src/semantics/shared.js +192 -0
- package/dist/src/semantics/subscription-schedules.d.ts +2 -0
- package/dist/src/semantics/subscription-schedules.js +119 -0
- package/dist/src/semantics/subscriptions.d.ts +11 -0
- package/dist/src/semantics/subscriptions.js +605 -0
- package/dist/src/semantics/tax.d.ts +2 -0
- package/dist/src/semantics/tax.js +197 -0
- package/dist/src/semantics/terminal.d.ts +5 -0
- package/dist/src/semantics/terminal.js +182 -0
- package/dist/src/semantics/test-clocks.d.ts +6 -0
- package/dist/src/semantics/test-clocks.js +73 -0
- package/dist/src/semantics/tokens.d.ts +4 -0
- package/dist/src/semantics/tokens.js +44 -0
- package/dist/src/semantics/transfers.d.ts +2 -0
- package/dist/src/semantics/transfers.js +154 -0
- package/dist/src/semantics/treasury.d.ts +2 -0
- package/dist/src/semantics/treasury.js +377 -0
- package/dist/src/semantics/webhook-endpoints.d.ts +3 -0
- package/dist/src/semantics/webhook-endpoints.js +85 -0
- package/dist/src/stripe-budget.d.ts +55 -0
- package/dist/src/stripe-budget.js +155 -0
- package/dist/src/stripe-capabilities.d.ts +3 -0
- package/dist/src/stripe-capabilities.js +5052 -0
- package/dist/src/stripe-conformance.d.ts +41 -0
- package/dist/src/stripe-conformance.js +96 -0
- package/dist/src/stripe-connector.d.ts +161 -0
- package/dist/src/stripe-connector.js +414 -0
- package/dist/src/stripe-emit.d.ts +2 -0
- package/dist/src/stripe-emit.js +145 -0
- package/dist/src/stripe-events.d.ts +93 -0
- package/dist/src/stripe-events.js +388 -0
- package/dist/src/stripe-js.d.ts +4 -0
- package/dist/src/stripe-js.js +70 -0
- package/dist/src/stripe-mirror-ui.d.ts +15 -0
- package/dist/src/stripe-mirror-ui.js +87 -0
- package/dist/src/stripe-params.d.ts +3 -0
- package/dist/src/stripe-params.js +43 -0
- package/dist/src/stripe-perform-harness.d.ts +9 -0
- package/dist/src/stripe-perform-harness.js +26 -0
- package/dist/src/stripe-server.d.ts +33 -0
- package/dist/src/stripe-server.js +326 -0
- package/dist/src/stripe-shared.d.ts +106 -0
- package/dist/src/stripe-shared.js +273 -0
- package/dist/src/stripe-twin.d.ts +155 -0
- package/dist/src/stripe-twin.js +1226 -0
- package/dist/src/stripe-ui-conformance.d.ts +5 -0
- package/dist/src/stripe-ui-conformance.js +79 -0
- package/dist/src/stripe-ui-structure.d.ts +3 -0
- package/dist/src/stripe-ui-structure.js +168 -0
- package/dist/src/stripe-version.d.ts +10 -0
- package/dist/src/stripe-version.js +285 -0
- package/dist/test-fixtures/stripe-known-deviations.json +105 -0
- package/dist/test-fixtures/stripe-openapi-operations.SOURCE.md +14 -0
- package/dist/test-fixtures/stripe-openapi-operations.json +4717 -0
- package/dist/test-fixtures/stripe-schemas.SOURCE.md +35 -0
- package/dist/test-fixtures/stripe-schemas.json +3740 -0
- package/package.json +18 -10
- package/src/cli.ts +7 -7
- package/src/generated/events.gen.json +1 -0
- package/src/generated/surface.gen.json +1 -0
- package/src/generated/ui.gen.json +1 -0
- package/src/index.ts +31 -9
- package/src/manifest.ts +1097 -0
- package/src/screens/checkout.tsx +252 -0
- package/src/screens/consent-skin.ts +20 -0
- package/src/screens/financial-connections.tsx +101 -0
- package/src/screens/identity.tsx +96 -0
- package/src/screens/industries.ts +267 -0
- package/src/screens/onboarding.tsx +243 -0
- package/src/screens/portal.tsx +218 -0
- package/src/screens/public-details.tsx +105 -0
- package/src/semantics/after-payment.ts +113 -0
- package/src/semantics/apps-secrets.ts +58 -0
- package/src/semantics/balance.ts +209 -0
- package/src/semantics/billing.ts +216 -0
- package/src/semantics/charges.ts +211 -0
- package/src/semantics/checkout.ts +297 -0
- package/src/semantics/connect.ts +471 -0
- package/src/semantics/coupons.ts +97 -0
- package/src/semantics/credit-notes.ts +168 -0
- package/src/semantics/customers.ts +432 -0
- package/src/semantics/disputes.ts +62 -0
- package/src/semantics/entitlements.ts +94 -0
- package/src/semantics/ephemeral-keys.ts +34 -0
- package/src/semantics/files.ts +143 -0
- package/src/semantics/invoices.ts +541 -0
- package/src/semantics/issuing.ts +585 -0
- package/src/semantics/ledger.ts +216 -0
- package/src/semantics/payment-intents.ts +420 -0
- package/src/semantics/payment-links.ts +148 -0
- package/src/semantics/payment-methods.ts +143 -0
- package/src/semantics/plans.ts +131 -0
- package/src/semantics/platform.ts +220 -0
- package/src/semantics/products.ts +154 -0
- package/src/semantics/radar.ts +85 -0
- package/src/semantics/refunds.ts +218 -0
- package/src/semantics/renewals.ts +274 -0
- package/src/semantics/setup-intents.ts +87 -0
- package/src/semantics/shared.ts +215 -0
- package/src/semantics/subscription-schedules.ts +129 -0
- package/src/semantics/subscriptions.ts +610 -0
- package/src/semantics/tax.ts +220 -0
- package/src/semantics/terminal.ts +195 -0
- package/src/semantics/test-clocks.ts +77 -0
- package/src/semantics/tokens.ts +52 -0
- package/src/semantics/transfers.ts +174 -0
- package/src/semantics/treasury.ts +383 -0
- package/src/semantics/webhook-endpoints.ts +87 -0
- package/src/stripe-budget.ts +4 -4
- package/src/stripe-capabilities.ts +1456 -222
- package/src/stripe-conformance.ts +6 -5
- package/src/stripe-connector.ts +68 -40
- package/src/stripe-emit.ts +14 -7
- package/src/stripe-events.ts +94 -36
- package/src/stripe-js.ts +70 -0
- package/src/stripe-mirror-ui.ts +28 -298
- package/src/stripe-params.ts +44 -0
- package/src/stripe-perform-harness.ts +29 -0
- package/src/stripe-server.ts +263 -38
- package/src/stripe-shared.ts +294 -0
- package/src/stripe-twin.ts +429 -5325
- package/src/stripe-ui-conformance.ts +70 -107
- package/src/stripe-ui-structure.ts +124 -348
- package/src/stripe-version.ts +278 -0
- package/test-fixtures/stripe-known-deviations.json +2 -7
- package/test-fixtures/stripe-openapi-operations.json +1188 -2855
- 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
|
+
}
|