@fonderie/react-admin-screens 0.1.0 → 0.3.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 CHANGED
@@ -10,12 +10,14 @@ Status: **experimental** (0.x).
10
10
  |---|---|---|
11
11
  | Today | Attention | what needs me today — empty is green |
12
12
  | System | Modules · Configuration · Doctor · Routes | what did I deploy · is it configured · is it working · what is exposed |
13
+ | People | Users | why can't this person log in — lookup, sessions, sign-ins, suspend, sign out everywhere (`authClient`) |
14
+ | Money | Catalog · Subscriber | what am I selling (configured vs stored) · what is this subscriber on, their wallet, ledger, a manual grant (`billingClient`) |
13
15
  | Settings | Config & secrets | the `@fonderie/config` admin screens, as a sub-page |
14
16
  | Messaging | Templates | the `@fonderie/courier` admin screens, as a sub-page |
15
17
  | Activity | Admin log · Access | who did what · who can be here |
16
18
 
17
19
  ```ts
18
- import { AdminClient, ConfigAdminClient, CourierAdminClient } from '@fonderie/client';
20
+ import { AdminClient, AuthAdminClient, BillingAdminClient, ConfigAdminClient, CourierAdminClient } from '@fonderie/client';
19
21
  import { AdminShell } from '@fonderie/react-admin-screens';
20
22
 
21
23
  const base = { baseUrl: 'https://api.example.com', adminToken, actor: 'louis' };
@@ -23,10 +25,12 @@ const client = new AdminClient(base);
23
25
  // prefix: the composed surface under /_admin — one token for everything.
24
26
  const configClient = new ConfigAdminClient({ ...base, prefix: '/_admin' });
25
27
  const courierClient = new CourierAdminClient({ ...base, prefix: '/_admin' });
28
+ const authClient = new AuthAdminClient(base); // @fonderie/auth ≥ 7.8
29
+ const billingClient = new BillingAdminClient(base); // @fonderie/billing ≥ 9.10
26
30
  ```
27
31
 
28
32
  ```tsx
29
- <AdminShell client={client} configClient={configClient} courierClient={courierClient} />
33
+ <AdminShell client={client} configClient={configClient} courierClient={courierClient} authClient={authClient} billingClient={billingClient} />
30
34
  ```
31
35
 
32
36
  Pass `page` and `onNavigate` to own the URL (one route per page); omit both
package/dist/index.cjs CHANGED
@@ -23,18 +23,21 @@ __export(index_exports, {
23
23
  AdminLogScreen: () => AdminLogScreen,
24
24
  AdminShell: () => AdminShell,
25
25
  AttentionScreen: () => AttentionScreen,
26
+ CatalogScreen: () => CatalogScreen,
26
27
  ConfigScreen: () => ConfigScreen,
27
28
  DoctorScreen: () => DoctorScreen,
28
29
  ModulesScreen: () => ModulesScreen,
29
30
  RoutesScreen: () => RoutesScreen,
30
- TokensScreen: () => TokensScreen
31
+ SubscriberScreen: () => SubscriberScreen,
32
+ TokensScreen: () => TokensScreen,
33
+ UsersScreen: () => UsersScreen
31
34
  });
32
35
  module.exports = __toCommonJS(index_exports);
33
36
 
34
37
  // src/screens/AdminShell.tsx
35
38
  var import_react_config_admin_screens = require("@fonderie/react-config-admin-screens");
36
39
  var import_react_courier_admin_screens = require("@fonderie/react-courier-admin-screens");
37
- var import_react = require("react");
40
+ var import_react3 = require("react");
38
41
 
39
42
  // src/styles.ts
40
43
  var styles = {
@@ -363,8 +366,396 @@ function TokensScreen({ client }) {
363
366
  ] });
364
367
  }
365
368
 
366
- // src/screens/AdminShell.tsx
369
+ // src/screens/UsersScreen.tsx
370
+ var import_react_admin8 = require("@fonderie/react-admin");
371
+ var import_react = require("react");
367
372
  var import_jsx_runtime8 = require("react/jsx-runtime");
373
+ function UsersScreen({ client }) {
374
+ const [input, setInput] = (0, import_react.useState)("");
375
+ const [email, setEmail] = (0, import_react.useState)(void 0);
376
+ const { user, isLoading, error, suspend, unsuspend, revokeSessions } = (0, import_react_admin8.useAdminUser)(
377
+ client,
378
+ email ? { email } : {}
379
+ );
380
+ const userId = user?.id ?? null;
381
+ const sessions = (0, import_react_admin8.useAdminUserSessions)(client, userId);
382
+ const history = (0, import_react_admin8.useAdminLoginHistory)(client, userId, { limit: 20 });
383
+ const yesNo = (v) => v ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: styles.ok, children: "yes" }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: styles.muted, children: "no" });
384
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: styles.container, children: [
385
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h1", { style: styles.title, children: "Users" }),
386
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
387
+ "form",
388
+ {
389
+ style: styles.toolbar,
390
+ onSubmit: (e) => {
391
+ e.preventDefault();
392
+ setEmail(input.trim() || void 0);
393
+ },
394
+ children: [
395
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
396
+ "input",
397
+ {
398
+ type: "email",
399
+ value: input,
400
+ onChange: (e) => setInput(e.target.value),
401
+ placeholder: "email address",
402
+ style: { ...styles.button, cursor: "text", minWidth: 280 },
403
+ "aria-label": "Email"
404
+ }
405
+ ),
406
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "submit", style: styles.button, disabled: isLoading, children: "Look up" })
407
+ ]
408
+ }
409
+ ),
410
+ error ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { style: styles.error, role: "alert", children: error.status === 404 ? "No user with that email." : error.explanation }) : null,
411
+ user ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
412
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("h2", { style: styles.subtitle, children: [
413
+ user.firstName || user.lastName ? `${user.firstName} ${user.lastName}`.trim() : user.email,
414
+ " ",
415
+ user.suspended ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: styles.badge, children: "suspended" }) : null,
416
+ user.deletedAt ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: styles.badge, children: "deleted" }) : null
417
+ ] }),
418
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("table", { style: styles.table, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("tbody", { children: [
419
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("tr", { children: [
420
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { style: styles.td, children: "id" }),
421
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { style: { ...styles.td, ...styles.mono }, children: user.id })
422
+ ] }),
423
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("tr", { children: [
424
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { style: styles.td, children: "email" }),
425
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("td", { style: styles.td, children: [
426
+ user.email,
427
+ " \xB7 verified ",
428
+ yesNo(user.isEmailVerified)
429
+ ] })
430
+ ] }),
431
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("tr", { children: [
432
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { style: styles.td, children: "MFA" }),
433
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { style: styles.td, children: yesNo(user.mfaEnabled) })
434
+ ] }),
435
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("tr", { children: [
436
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { style: styles.td, children: "provider" }),
437
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("td", { style: styles.td, children: [
438
+ user.provider || "password",
439
+ user.hasPassword ? "" : " \xB7 no password set"
440
+ ] })
441
+ ] }),
442
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("tr", { children: [
443
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { style: styles.td, children: "last login" }),
444
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { style: styles.td, children: user.lastLogin ? new Date(user.lastLogin).toLocaleString() : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: styles.muted, children: "never" }) })
445
+ ] }),
446
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("tr", { children: [
447
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { style: styles.td, children: "created" }),
448
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { style: styles.td, children: new Date(user.createdAt).toLocaleString() })
449
+ ] })
450
+ ] }) }),
451
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { ...styles.toolbar, marginTop: 12 }, children: [
452
+ user.suspended ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
453
+ "button",
454
+ {
455
+ type: "button",
456
+ style: styles.button,
457
+ onClick: () => void unsuspend(),
458
+ disabled: isLoading,
459
+ children: "Unsuspend"
460
+ }
461
+ ) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
462
+ "button",
463
+ {
464
+ type: "button",
465
+ style: styles.button,
466
+ onClick: () => void suspend(),
467
+ disabled: isLoading,
468
+ children: "Suspend"
469
+ }
470
+ ),
471
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
472
+ "button",
473
+ {
474
+ type: "button",
475
+ style: styles.button,
476
+ onClick: () => void revokeSessions().then(() => sessions.refresh()),
477
+ disabled: isLoading,
478
+ children: "Sign out everywhere"
479
+ }
480
+ )
481
+ ] }),
482
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h2", { style: styles.subtitle, children: "Live sessions" }),
483
+ sessions.sessions.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { style: styles.muted, children: "None." }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("ul", { style: styles.list, children: sessions.sessions.map((s) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("li", { style: styles.row, children: [
484
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: styles.mono, children: s.ipAddress ?? "\u2014" }),
485
+ " ",
486
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: styles.muted, children: s.userAgent ?? "" }),
487
+ " ",
488
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { style: styles.muted, children: [
489
+ "since ",
490
+ new Date(s.createdAt).toLocaleString()
491
+ ] })
492
+ ] }, s.id)) }),
493
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h2", { style: styles.subtitle, children: "Recent sign-ins" }),
494
+ history.events.length === 0 && !history.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { style: styles.muted, children: "None recorded." }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("ul", { style: styles.list, children: history.events.map((e) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("li", { style: styles.row, children: [
495
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: e.outcome === "success" ? styles.ok : styles.bad, children: e.outcome }),
496
+ " ",
497
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: styles.muted, children: e.method }),
498
+ " ",
499
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: styles.muted, children: new Date(e.createdAt).toLocaleString() })
500
+ ] }, e.id)) }),
501
+ history.hasMore && !history.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
502
+ "button",
503
+ {
504
+ type: "button",
505
+ style: { ...styles.button, marginTop: 8 },
506
+ onClick: () => void history.loadMore(),
507
+ children: "Load more"
508
+ }
509
+ ) : null
510
+ ] }) : null
511
+ ] });
512
+ }
513
+
514
+ // src/screens/CatalogScreen.tsx
515
+ var import_react_admin9 = require("@fonderie/react-admin");
516
+ var import_jsx_runtime9 = require("react/jsx-runtime");
517
+ function CatalogScreen({ client }) {
518
+ const { catalog, isLoading, error, refresh, deletePlan } = (0, import_react_admin9.useAdminCatalog)(client);
519
+ const money = (v) => v == null ? "\u2014" : (v / 100).toFixed(2);
520
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: styles.container, children: [
521
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: styles.toolbar, children: [
522
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h1", { style: { ...styles.title, marginBottom: 0 }, children: "Catalog" }),
523
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
524
+ "button",
525
+ {
526
+ type: "button",
527
+ style: styles.button,
528
+ onClick: () => void refresh(),
529
+ disabled: isLoading,
530
+ children: "Refresh"
531
+ }
532
+ )
533
+ ] }),
534
+ error ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
535
+ isLoading && !catalog ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { style: styles.status, children: "Loading\u2026" }) : null,
536
+ catalog ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
537
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h2", { style: styles.subtitle, children: "Configured (code)" }),
538
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("pre", { style: { ...styles.mono, background: "#f9fafb", padding: 12, overflowX: "auto" }, children: JSON.stringify(catalog.configured, null, 2) }),
539
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h2", { style: styles.subtitle, children: "Stored (database)" }),
540
+ catalog.stored.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { style: styles.muted, children: "No stored plans." }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("table", { style: styles.table, children: [
541
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("tr", { children: [
542
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("th", { style: styles.th, children: "Plan" }),
543
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("th", { style: styles.th, children: "Tier" }),
544
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("th", { style: styles.th, children: "Seats" }),
545
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("th", { style: styles.th, children: "Monthly" }),
546
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("th", { style: styles.th, children: "Yearly" }),
547
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("th", { style: styles.th, children: "Trial" }),
548
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("th", { style: styles.th })
549
+ ] }) }),
550
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("tbody", { children: catalog.stored.map((p) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("tr", { children: [
551
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("td", { style: styles.td, children: [
552
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("strong", { children: p.name }),
553
+ p.description ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: styles.muted, children: p.description }) : null
554
+ ] }),
555
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { style: styles.td, children: p.tier }),
556
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { style: styles.td, children: p.seats ?? "\u221E" }),
557
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("td", { style: styles.td, children: [
558
+ money(p.pricing?.monthly),
559
+ " ",
560
+ p.pricing?.currency ?? ""
561
+ ] }),
562
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { style: styles.td, children: money(p.pricing?.yearly) }),
563
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { style: styles.td, children: p.trialDays ? `${p.trialDays} d` : "\u2014" }),
564
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { style: styles.td, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
565
+ "button",
566
+ {
567
+ type: "button",
568
+ style: styles.button,
569
+ onClick: () => {
570
+ if (window.confirm(`Delete stored plan "${p.name}"?`))
571
+ void deletePlan(p.id).catch(() => {
572
+ });
573
+ },
574
+ children: "Delete"
575
+ }
576
+ ) })
577
+ ] }, p.id)) })
578
+ ] })
579
+ ] }) : null
580
+ ] });
581
+ }
582
+
583
+ // src/screens/SubscriberScreen.tsx
584
+ var import_react_admin10 = require("@fonderie/react-admin");
585
+ var import_react2 = require("react");
586
+ var import_jsx_runtime10 = require("react/jsx-runtime");
587
+ function SubscriberScreen({ client }) {
588
+ const [type, setType] = (0, import_react2.useState)("user");
589
+ const [idInput, setIdInput] = (0, import_react2.useState)("");
590
+ const [subscriber, setSubscriber] = (0, import_react2.useState)(null);
591
+ const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = (0, import_react_admin10.useAdminSubscriber)(client, subscriber, { limit: 20 });
592
+ const [amount, setAmount] = (0, import_react2.useState)("");
593
+ const [note, setNote] = (0, import_react2.useState)("");
594
+ const [granted, setGranted] = (0, import_react2.useState)(null);
595
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: styles.container, children: [
596
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h1", { style: styles.title, children: "Subscriber" }),
597
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
598
+ "form",
599
+ {
600
+ style: styles.toolbar,
601
+ onSubmit: (e) => {
602
+ e.preventDefault();
603
+ setGranted(null);
604
+ setSubscriber(idInput.trim() ? { type, id: idInput.trim() } : null);
605
+ },
606
+ children: [
607
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
608
+ "select",
609
+ {
610
+ value: type,
611
+ onChange: (e) => setType(e.target.value),
612
+ style: styles.button,
613
+ "aria-label": "Type",
614
+ children: [
615
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("option", { value: "user", children: "user" }),
616
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("option", { value: "workspace", children: "workspace" })
617
+ ]
618
+ }
619
+ ),
620
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
621
+ "input",
622
+ {
623
+ value: idInput,
624
+ onChange: (e) => setIdInput(e.target.value),
625
+ placeholder: "subscriber id",
626
+ style: { ...styles.button, cursor: "text", minWidth: 280 },
627
+ "aria-label": "Subscriber id"
628
+ }
629
+ ),
630
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "submit", style: styles.button, disabled: isLoading, children: "Look up" })
631
+ ]
632
+ }
633
+ ),
634
+ error ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
635
+ subscriber && !isLoading ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
636
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h2", { style: styles.subtitle, children: "Subscription" }),
637
+ subscription ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("table", { style: styles.table, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("tbody", { children: [
638
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("tr", { children: [
639
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("td", { style: styles.td, children: "plan" }),
640
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("td", { style: styles.td, children: [
641
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("strong", { children: subscription.plan }),
642
+ " \xB7 ",
643
+ subscription.interval
644
+ ] })
645
+ ] }),
646
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("tr", { children: [
647
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("td", { style: styles.td, children: "status" }),
648
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("td", { style: styles.td, children: [
649
+ subscription.status,
650
+ subscription.cancelAtPeriodEnd ? " \xB7 cancels at period end" : ""
651
+ ] })
652
+ ] }),
653
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("tr", { children: [
654
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("td", { style: styles.td, children: "period" }),
655
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("td", { style: styles.td, children: [
656
+ subscription.currentPeriodStart ? new Date(subscription.currentPeriodStart).toLocaleDateString() : "\u2014",
657
+ " ",
658
+ "\u2192",
659
+ " ",
660
+ subscription.currentPeriodEnd ? new Date(subscription.currentPeriodEnd).toLocaleDateString() : "\u2014"
661
+ ] })
662
+ ] }),
663
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("tr", { children: [
664
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("td", { style: styles.td, children: "provider" }),
665
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("td", { style: { ...styles.td, ...styles.mono }, children: subscription.providerSubscriptionId ?? "\u2014" })
666
+ ] })
667
+ ] }) }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { style: styles.muted, children: "No subscription." }),
668
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h2", { style: styles.subtitle, children: "Wallet" }),
669
+ wallet ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
670
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("p", { children: [
671
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("strong", { children: wallet.balance }),
672
+ " ",
673
+ wallet.currency,
674
+ " ",
675
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { style: styles.muted, children: [
676
+ "(granted ",
677
+ wallet.granted ?? "0",
678
+ " \xB7 purchased ",
679
+ wallet.purchased ?? "0",
680
+ ") \xB7 minor units, precision ",
681
+ wallet.precision
682
+ ] })
683
+ ] }),
684
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
685
+ "form",
686
+ {
687
+ style: styles.toolbar,
688
+ onSubmit: (e) => {
689
+ e.preventDefault();
690
+ const key = `admin-grant-${subscriber.type}-${subscriber.id}-${Date.now()}`;
691
+ void grant({
692
+ amount: amount.trim(),
693
+ ...note ? { description: note } : {},
694
+ idempotencyKey: key
695
+ }).then(() => {
696
+ setGranted(`Granted ${amount.trim()} ${wallet.currency}.`);
697
+ setAmount("");
698
+ setNote("");
699
+ }).catch(() => {
700
+ });
701
+ },
702
+ children: [
703
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
704
+ "input",
705
+ {
706
+ value: amount,
707
+ onChange: (e) => setAmount(e.target.value),
708
+ placeholder: "amount (minor units)",
709
+ style: { ...styles.button, cursor: "text" },
710
+ "aria-label": "Amount"
711
+ }
712
+ ),
713
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
714
+ "input",
715
+ {
716
+ value: note,
717
+ onChange: (e) => setNote(e.target.value),
718
+ placeholder: "reason (optional)",
719
+ style: { ...styles.button, cursor: "text", minWidth: 220 },
720
+ "aria-label": "Reason"
721
+ }
722
+ ),
723
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "submit", style: styles.button, disabled: !/^\d+$/.test(amount.trim()), children: "Grant" }),
724
+ granted ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: styles.ok, children: granted }) : null
725
+ ]
726
+ }
727
+ ),
728
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h2", { style: styles.subtitle, children: "Ledger" }),
729
+ ledger.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { style: styles.muted, children: "Nothing moved yet." }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("table", { style: styles.table, children: [
730
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("tr", { children: [
731
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("th", { style: styles.th, children: "When" }),
732
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("th", { style: styles.th, children: "Kind" }),
733
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("th", { style: styles.th, children: "Amount" }),
734
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("th", { style: styles.th, children: "Note" })
735
+ ] }) }),
736
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("tbody", { children: ledger.map((t) => /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("tr", { children: [
737
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("td", { style: { ...styles.td, ...styles.muted }, children: new Date(t.createdAt).toLocaleString() }),
738
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("td", { style: styles.td, children: t.type }),
739
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("td", { style: { ...styles.td, ...styles.mono }, children: t.amount }),
740
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("td", { style: { ...styles.td, ...styles.muted }, children: t.description ?? "" })
741
+ ] }, t.id)) })
742
+ ] }),
743
+ hasMoreLedger ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
744
+ "button",
745
+ {
746
+ type: "button",
747
+ style: { ...styles.button, marginTop: 8 },
748
+ onClick: () => void loadMoreLedger(),
749
+ children: "Load more"
750
+ }
751
+ ) : null
752
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { style: styles.muted, children: "No wallet \u2014 billing has no wallet configuration." })
753
+ ] }) : null
754
+ ] });
755
+ }
756
+
757
+ // src/screens/AdminShell.tsx
758
+ var import_jsx_runtime11 = require("react/jsx-runtime");
368
759
  var NAV = [
369
760
  { group: "Today", items: [{ page: "attention", label: "Attention" }] },
370
761
  {
@@ -376,6 +767,14 @@ var NAV = [
376
767
  { page: "routes", label: "Routes" }
377
768
  ]
378
769
  },
770
+ { group: "People", items: [{ page: "users", label: "Users", needs: "auth" }] },
771
+ {
772
+ group: "Money",
773
+ items: [
774
+ { page: "catalog", label: "Catalog", needs: "billing" },
775
+ { page: "subscriber", label: "Subscriber", needs: "billing" }
776
+ ]
777
+ },
379
778
  { group: "Settings", items: [{ page: "settings", label: "Config & secrets", needs: "config" }] },
380
779
  { group: "Messaging", items: [{ page: "templates", label: "Templates", needs: "courier" }] },
381
780
  {
@@ -390,45 +789,61 @@ function AdminShell({
390
789
  client,
391
790
  configClient,
392
791
  courierClient,
792
+ authClient,
793
+ billingClient,
393
794
  environment,
394
795
  page,
395
796
  onNavigate
396
797
  }) {
397
- const [own, setOwn] = (0, import_react.useState)("attention");
798
+ const [own, setOwn] = (0, import_react3.useState)("attention");
398
799
  const current = page ?? own;
399
800
  const go = (p) => {
400
801
  if (onNavigate) onNavigate(p);
401
802
  if (page === void 0) setOwn(p);
402
803
  };
403
- const [editing, setEditing] = (0, import_react.useState)(null);
404
- const has = { config: Boolean(configClient), courier: Boolean(courierClient) };
804
+ const [editing, setEditing] = (0, import_react3.useState)(null);
805
+ const has = {
806
+ config: Boolean(configClient),
807
+ courier: Boolean(courierClient),
808
+ auth: Boolean(authClient),
809
+ billing: Boolean(billingClient)
810
+ };
405
811
  let body;
406
812
  switch (current) {
407
813
  case "attention":
408
- body = /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(AttentionScreen, { client });
814
+ body = /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(AttentionScreen, { client });
409
815
  break;
410
816
  case "modules":
411
- body = /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ModulesScreen, { client });
817
+ body = /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ModulesScreen, { client });
412
818
  break;
413
819
  case "doctor":
414
- body = /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(DoctorScreen, { client });
820
+ body = /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DoctorScreen, { client });
415
821
  break;
416
822
  case "config":
417
- body = /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ConfigScreen, { client });
823
+ body = /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ConfigScreen, { client });
418
824
  break;
419
825
  case "routes":
420
- body = /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(RoutesScreen, { client });
826
+ body = /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(RoutesScreen, { client });
421
827
  break;
422
828
  case "tokens":
423
- body = /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(TokensScreen, { client });
829
+ body = /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(TokensScreen, { client });
424
830
  break;
425
831
  case "log":
426
- body = /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(AdminLogScreen, { client });
832
+ body = /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(AdminLogScreen, { client });
833
+ break;
834
+ case "catalog":
835
+ body = billingClient ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(CatalogScreen, { client: billingClient }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { style: styles.status, children: "Pass a BillingAdminClient to see the catalog here." });
836
+ break;
837
+ case "subscriber":
838
+ body = billingClient ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SubscriberScreen, { client: billingClient }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { style: styles.status, children: "Pass a BillingAdminClient to look up subscribers here." });
839
+ break;
840
+ case "users":
841
+ body = authClient ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(UsersScreen, { client: authClient }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { style: styles.status, children: "Pass an AuthAdminClient to look up users here." });
427
842
  break;
428
843
  case "settings":
429
- body = !configClient ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { style: styles.status, children: "Pass a ConfigAdminClient to manage config and secrets here." }) : editing && editing.kind !== "template" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
430
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", style: styles.button, onClick: () => setEditing(null), children: "\u2190 Back" }),
431
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
844
+ body = !configClient ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { style: styles.status, children: "Pass a ConfigAdminClient to manage config and secrets here." }) : editing && editing.kind !== "template" ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
845
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", style: styles.button, onClick: () => setEditing(null), children: "\u2190 Back" }),
846
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
432
847
  import_react_config_admin_screens.ConfigEditorScreen,
433
848
  {
434
849
  client: configClient,
@@ -438,7 +853,7 @@ function AdminShell({
438
853
  onSaved: () => setEditing(null)
439
854
  }
440
855
  )
441
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
856
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
442
857
  import_react_config_admin_screens.ConfigListScreen,
443
858
  {
444
859
  client: configClient,
@@ -449,9 +864,9 @@ function AdminShell({
449
864
  );
450
865
  break;
451
866
  case "templates":
452
- body = !courierClient ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { style: styles.status, children: "Pass a CourierAdminClient to manage templates here." }) : editing?.kind === "template" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
453
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", style: styles.button, onClick: () => setEditing(null), children: "\u2190 Back" }),
454
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
867
+ body = !courierClient ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { style: styles.status, children: "Pass a CourierAdminClient to manage templates here." }) : editing?.kind === "template" ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
868
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", style: styles.button, onClick: () => setEditing(null), children: "\u2190 Back" }),
869
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
455
870
  import_react_courier_admin_screens.TemplateEditorScreen,
456
871
  {
457
872
  client: courierClient,
@@ -459,7 +874,7 @@ function AdminShell({
459
874
  onSaved: () => setEditing(null)
460
875
  }
461
876
  )
462
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
877
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
463
878
  import_react_courier_admin_screens.TemplateListScreen,
464
879
  {
465
880
  client: courierClient,
@@ -468,13 +883,13 @@ function AdminShell({
468
883
  );
469
884
  break;
470
885
  }
471
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: styles.shell, children: [
472
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("nav", { style: styles.nav, "aria-label": "Admin", children: NAV.map((g) => {
886
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { style: styles.shell, children: [
887
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("nav", { style: styles.nav, "aria-label": "Admin", children: NAV.map((g) => {
473
888
  const items = g.items.filter((i) => !i.needs || has[i.needs]);
474
889
  if (items.length === 0) return null;
475
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
476
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: styles.navGroup, children: g.group }),
477
- items.map((i) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
890
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { children: [
891
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { style: styles.navGroup, children: g.group }),
892
+ items.map((i) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
478
893
  "button",
479
894
  {
480
895
  type: "button",
@@ -490,7 +905,7 @@ function AdminShell({
490
905
  ))
491
906
  ] }, g.group);
492
907
  }) }),
493
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("main", { style: styles.main, children: body })
908
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("main", { style: styles.main, children: body })
494
909
  ] });
495
910
  }
496
911
  // Annotate the CommonJS export names for ESM import in node:
@@ -498,10 +913,13 @@ function AdminShell({
498
913
  AdminLogScreen,
499
914
  AdminShell,
500
915
  AttentionScreen,
916
+ CatalogScreen,
501
917
  ConfigScreen,
502
918
  DoctorScreen,
503
919
  ModulesScreen,
504
920
  RoutesScreen,
505
- TokensScreen
921
+ SubscriberScreen,
922
+ TokensScreen,
923
+ UsersScreen
506
924
  });
507
925
  //# sourceMappingURL=index.cjs.map