@fonderie/react-admin-screens 0.2.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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/screens/AdminShell.tsx
2
2
  import { ConfigEditorScreen, ConfigListScreen } from "@fonderie/react-config-admin-screens";
3
3
  import { TemplateEditorScreen, TemplateListScreen } from "@fonderie/react-courier-admin-screens";
4
- import { useState as useState2 } from "react";
4
+ import { useState as useState3 } from "react";
5
5
 
6
6
  // src/styles.ts
7
7
  var styles = {
@@ -475,8 +475,251 @@ function UsersScreen({ client }) {
475
475
  ] });
476
476
  }
477
477
 
478
- // src/screens/AdminShell.tsx
478
+ // src/screens/CatalogScreen.tsx
479
+ import { useAdminCatalog } from "@fonderie/react-admin";
479
480
  import { Fragment as Fragment6, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
481
+ function CatalogScreen({ client }) {
482
+ const { catalog, isLoading, error, refresh, deletePlan } = useAdminCatalog(client);
483
+ const money = (v) => v == null ? "\u2014" : (v / 100).toFixed(2);
484
+ return /* @__PURE__ */ jsxs9("div", { style: styles.container, children: [
485
+ /* @__PURE__ */ jsxs9("div", { style: styles.toolbar, children: [
486
+ /* @__PURE__ */ jsx9("h1", { style: { ...styles.title, marginBottom: 0 }, children: "Catalog" }),
487
+ /* @__PURE__ */ jsx9(
488
+ "button",
489
+ {
490
+ type: "button",
491
+ style: styles.button,
492
+ onClick: () => void refresh(),
493
+ disabled: isLoading,
494
+ children: "Refresh"
495
+ }
496
+ )
497
+ ] }),
498
+ error ? /* @__PURE__ */ jsx9("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
499
+ isLoading && !catalog ? /* @__PURE__ */ jsx9("p", { style: styles.status, children: "Loading\u2026" }) : null,
500
+ catalog ? /* @__PURE__ */ jsxs9(Fragment6, { children: [
501
+ /* @__PURE__ */ jsx9("h2", { style: styles.subtitle, children: "Configured (code)" }),
502
+ /* @__PURE__ */ jsx9("pre", { style: { ...styles.mono, background: "#f9fafb", padding: 12, overflowX: "auto" }, children: JSON.stringify(catalog.configured, null, 2) }),
503
+ /* @__PURE__ */ jsx9("h2", { style: styles.subtitle, children: "Stored (database)" }),
504
+ catalog.stored.length === 0 ? /* @__PURE__ */ jsx9("p", { style: styles.muted, children: "No stored plans." }) : /* @__PURE__ */ jsxs9("table", { style: styles.table, children: [
505
+ /* @__PURE__ */ jsx9("thead", { children: /* @__PURE__ */ jsxs9("tr", { children: [
506
+ /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Plan" }),
507
+ /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Tier" }),
508
+ /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Seats" }),
509
+ /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Monthly" }),
510
+ /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Yearly" }),
511
+ /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Trial" }),
512
+ /* @__PURE__ */ jsx9("th", { style: styles.th })
513
+ ] }) }),
514
+ /* @__PURE__ */ jsx9("tbody", { children: catalog.stored.map((p) => /* @__PURE__ */ jsxs9("tr", { children: [
515
+ /* @__PURE__ */ jsxs9("td", { style: styles.td, children: [
516
+ /* @__PURE__ */ jsx9("strong", { children: p.name }),
517
+ p.description ? /* @__PURE__ */ jsx9("div", { style: styles.muted, children: p.description }) : null
518
+ ] }),
519
+ /* @__PURE__ */ jsx9("td", { style: styles.td, children: p.tier }),
520
+ /* @__PURE__ */ jsx9("td", { style: styles.td, children: p.seats ?? "\u221E" }),
521
+ /* @__PURE__ */ jsxs9("td", { style: styles.td, children: [
522
+ money(p.pricing?.monthly),
523
+ " ",
524
+ p.pricing?.currency ?? ""
525
+ ] }),
526
+ /* @__PURE__ */ jsx9("td", { style: styles.td, children: money(p.pricing?.yearly) }),
527
+ /* @__PURE__ */ jsx9("td", { style: styles.td, children: p.trialDays ? `${p.trialDays} d` : "\u2014" }),
528
+ /* @__PURE__ */ jsx9("td", { style: styles.td, children: /* @__PURE__ */ jsx9(
529
+ "button",
530
+ {
531
+ type: "button",
532
+ style: styles.button,
533
+ onClick: () => {
534
+ if (window.confirm(`Delete stored plan "${p.name}"?`))
535
+ void deletePlan(p.id).catch(() => {
536
+ });
537
+ },
538
+ children: "Delete"
539
+ }
540
+ ) })
541
+ ] }, p.id)) })
542
+ ] })
543
+ ] }) : null
544
+ ] });
545
+ }
546
+
547
+ // src/screens/SubscriberScreen.tsx
548
+ import { useAdminSubscriber } from "@fonderie/react-admin";
549
+ import { useState as useState2 } from "react";
550
+ import { Fragment as Fragment7, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
551
+ function SubscriberScreen({ client }) {
552
+ const [type, setType] = useState2("user");
553
+ const [idInput, setIdInput] = useState2("");
554
+ const [subscriber, setSubscriber] = useState2(null);
555
+ const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = useAdminSubscriber(client, subscriber, { limit: 20 });
556
+ const [amount, setAmount] = useState2("");
557
+ const [note, setNote] = useState2("");
558
+ const [granted, setGranted] = useState2(null);
559
+ return /* @__PURE__ */ jsxs10("div", { style: styles.container, children: [
560
+ /* @__PURE__ */ jsx10("h1", { style: styles.title, children: "Subscriber" }),
561
+ /* @__PURE__ */ jsxs10(
562
+ "form",
563
+ {
564
+ style: styles.toolbar,
565
+ onSubmit: (e) => {
566
+ e.preventDefault();
567
+ setGranted(null);
568
+ setSubscriber(idInput.trim() ? { type, id: idInput.trim() } : null);
569
+ },
570
+ children: [
571
+ /* @__PURE__ */ jsxs10(
572
+ "select",
573
+ {
574
+ value: type,
575
+ onChange: (e) => setType(e.target.value),
576
+ style: styles.button,
577
+ "aria-label": "Type",
578
+ children: [
579
+ /* @__PURE__ */ jsx10("option", { value: "user", children: "user" }),
580
+ /* @__PURE__ */ jsx10("option", { value: "workspace", children: "workspace" })
581
+ ]
582
+ }
583
+ ),
584
+ /* @__PURE__ */ jsx10(
585
+ "input",
586
+ {
587
+ value: idInput,
588
+ onChange: (e) => setIdInput(e.target.value),
589
+ placeholder: "subscriber id",
590
+ style: { ...styles.button, cursor: "text", minWidth: 280 },
591
+ "aria-label": "Subscriber id"
592
+ }
593
+ ),
594
+ /* @__PURE__ */ jsx10("button", { type: "submit", style: styles.button, disabled: isLoading, children: "Look up" })
595
+ ]
596
+ }
597
+ ),
598
+ error ? /* @__PURE__ */ jsx10("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
599
+ subscriber && !isLoading ? /* @__PURE__ */ jsxs10(Fragment7, { children: [
600
+ /* @__PURE__ */ jsx10("h2", { style: styles.subtitle, children: "Subscription" }),
601
+ subscription ? /* @__PURE__ */ jsx10("table", { style: styles.table, children: /* @__PURE__ */ jsxs10("tbody", { children: [
602
+ /* @__PURE__ */ jsxs10("tr", { children: [
603
+ /* @__PURE__ */ jsx10("td", { style: styles.td, children: "plan" }),
604
+ /* @__PURE__ */ jsxs10("td", { style: styles.td, children: [
605
+ /* @__PURE__ */ jsx10("strong", { children: subscription.plan }),
606
+ " \xB7 ",
607
+ subscription.interval
608
+ ] })
609
+ ] }),
610
+ /* @__PURE__ */ jsxs10("tr", { children: [
611
+ /* @__PURE__ */ jsx10("td", { style: styles.td, children: "status" }),
612
+ /* @__PURE__ */ jsxs10("td", { style: styles.td, children: [
613
+ subscription.status,
614
+ subscription.cancelAtPeriodEnd ? " \xB7 cancels at period end" : ""
615
+ ] })
616
+ ] }),
617
+ /* @__PURE__ */ jsxs10("tr", { children: [
618
+ /* @__PURE__ */ jsx10("td", { style: styles.td, children: "period" }),
619
+ /* @__PURE__ */ jsxs10("td", { style: styles.td, children: [
620
+ subscription.currentPeriodStart ? new Date(subscription.currentPeriodStart).toLocaleDateString() : "\u2014",
621
+ " ",
622
+ "\u2192",
623
+ " ",
624
+ subscription.currentPeriodEnd ? new Date(subscription.currentPeriodEnd).toLocaleDateString() : "\u2014"
625
+ ] })
626
+ ] }),
627
+ /* @__PURE__ */ jsxs10("tr", { children: [
628
+ /* @__PURE__ */ jsx10("td", { style: styles.td, children: "provider" }),
629
+ /* @__PURE__ */ jsx10("td", { style: { ...styles.td, ...styles.mono }, children: subscription.providerSubscriptionId ?? "\u2014" })
630
+ ] })
631
+ ] }) }) : /* @__PURE__ */ jsx10("p", { style: styles.muted, children: "No subscription." }),
632
+ /* @__PURE__ */ jsx10("h2", { style: styles.subtitle, children: "Wallet" }),
633
+ wallet ? /* @__PURE__ */ jsxs10(Fragment7, { children: [
634
+ /* @__PURE__ */ jsxs10("p", { children: [
635
+ /* @__PURE__ */ jsx10("strong", { children: wallet.balance }),
636
+ " ",
637
+ wallet.currency,
638
+ " ",
639
+ /* @__PURE__ */ jsxs10("span", { style: styles.muted, children: [
640
+ "(granted ",
641
+ wallet.granted ?? "0",
642
+ " \xB7 purchased ",
643
+ wallet.purchased ?? "0",
644
+ ") \xB7 minor units, precision ",
645
+ wallet.precision
646
+ ] })
647
+ ] }),
648
+ /* @__PURE__ */ jsxs10(
649
+ "form",
650
+ {
651
+ style: styles.toolbar,
652
+ onSubmit: (e) => {
653
+ e.preventDefault();
654
+ const key = `admin-grant-${subscriber.type}-${subscriber.id}-${Date.now()}`;
655
+ void grant({
656
+ amount: amount.trim(),
657
+ ...note ? { description: note } : {},
658
+ idempotencyKey: key
659
+ }).then(() => {
660
+ setGranted(`Granted ${amount.trim()} ${wallet.currency}.`);
661
+ setAmount("");
662
+ setNote("");
663
+ }).catch(() => {
664
+ });
665
+ },
666
+ children: [
667
+ /* @__PURE__ */ jsx10(
668
+ "input",
669
+ {
670
+ value: amount,
671
+ onChange: (e) => setAmount(e.target.value),
672
+ placeholder: "amount (minor units)",
673
+ style: { ...styles.button, cursor: "text" },
674
+ "aria-label": "Amount"
675
+ }
676
+ ),
677
+ /* @__PURE__ */ jsx10(
678
+ "input",
679
+ {
680
+ value: note,
681
+ onChange: (e) => setNote(e.target.value),
682
+ placeholder: "reason (optional)",
683
+ style: { ...styles.button, cursor: "text", minWidth: 220 },
684
+ "aria-label": "Reason"
685
+ }
686
+ ),
687
+ /* @__PURE__ */ jsx10("button", { type: "submit", style: styles.button, disabled: !/^\d+$/.test(amount.trim()), children: "Grant" }),
688
+ granted ? /* @__PURE__ */ jsx10("span", { style: styles.ok, children: granted }) : null
689
+ ]
690
+ }
691
+ ),
692
+ /* @__PURE__ */ jsx10("h2", { style: styles.subtitle, children: "Ledger" }),
693
+ ledger.length === 0 ? /* @__PURE__ */ jsx10("p", { style: styles.muted, children: "Nothing moved yet." }) : /* @__PURE__ */ jsxs10("table", { style: styles.table, children: [
694
+ /* @__PURE__ */ jsx10("thead", { children: /* @__PURE__ */ jsxs10("tr", { children: [
695
+ /* @__PURE__ */ jsx10("th", { style: styles.th, children: "When" }),
696
+ /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Kind" }),
697
+ /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Amount" }),
698
+ /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Note" })
699
+ ] }) }),
700
+ /* @__PURE__ */ jsx10("tbody", { children: ledger.map((t) => /* @__PURE__ */ jsxs10("tr", { children: [
701
+ /* @__PURE__ */ jsx10("td", { style: { ...styles.td, ...styles.muted }, children: new Date(t.createdAt).toLocaleString() }),
702
+ /* @__PURE__ */ jsx10("td", { style: styles.td, children: t.type }),
703
+ /* @__PURE__ */ jsx10("td", { style: { ...styles.td, ...styles.mono }, children: t.amount }),
704
+ /* @__PURE__ */ jsx10("td", { style: { ...styles.td, ...styles.muted }, children: t.description ?? "" })
705
+ ] }, t.id)) })
706
+ ] }),
707
+ hasMoreLedger ? /* @__PURE__ */ jsx10(
708
+ "button",
709
+ {
710
+ type: "button",
711
+ style: { ...styles.button, marginTop: 8 },
712
+ onClick: () => void loadMoreLedger(),
713
+ children: "Load more"
714
+ }
715
+ ) : null
716
+ ] }) : /* @__PURE__ */ jsx10("p", { style: styles.muted, children: "No wallet \u2014 billing has no wallet configuration." })
717
+ ] }) : null
718
+ ] });
719
+ }
720
+
721
+ // src/screens/AdminShell.tsx
722
+ import { Fragment as Fragment8, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
480
723
  var NAV = [
481
724
  { group: "Today", items: [{ page: "attention", label: "Attention" }] },
482
725
  {
@@ -489,6 +732,13 @@ var NAV = [
489
732
  ]
490
733
  },
491
734
  { group: "People", items: [{ page: "users", label: "Users", needs: "auth" }] },
735
+ {
736
+ group: "Money",
737
+ items: [
738
+ { page: "catalog", label: "Catalog", needs: "billing" },
739
+ { page: "subscriber", label: "Subscriber", needs: "billing" }
740
+ ]
741
+ },
492
742
  { group: "Settings", items: [{ page: "settings", label: "Config & secrets", needs: "config" }] },
493
743
  { group: "Messaging", items: [{ page: "templates", label: "Templates", needs: "courier" }] },
494
744
  {
@@ -504,52 +754,60 @@ function AdminShell({
504
754
  configClient,
505
755
  courierClient,
506
756
  authClient,
757
+ billingClient,
507
758
  environment,
508
759
  page,
509
760
  onNavigate
510
761
  }) {
511
- const [own, setOwn] = useState2("attention");
762
+ const [own, setOwn] = useState3("attention");
512
763
  const current = page ?? own;
513
764
  const go = (p) => {
514
765
  if (onNavigate) onNavigate(p);
515
766
  if (page === void 0) setOwn(p);
516
767
  };
517
- const [editing, setEditing] = useState2(null);
768
+ const [editing, setEditing] = useState3(null);
518
769
  const has = {
519
770
  config: Boolean(configClient),
520
771
  courier: Boolean(courierClient),
521
- auth: Boolean(authClient)
772
+ auth: Boolean(authClient),
773
+ billing: Boolean(billingClient)
522
774
  };
523
775
  let body;
524
776
  switch (current) {
525
777
  case "attention":
526
- body = /* @__PURE__ */ jsx9(AttentionScreen, { client });
778
+ body = /* @__PURE__ */ jsx11(AttentionScreen, { client });
527
779
  break;
528
780
  case "modules":
529
- body = /* @__PURE__ */ jsx9(ModulesScreen, { client });
781
+ body = /* @__PURE__ */ jsx11(ModulesScreen, { client });
530
782
  break;
531
783
  case "doctor":
532
- body = /* @__PURE__ */ jsx9(DoctorScreen, { client });
784
+ body = /* @__PURE__ */ jsx11(DoctorScreen, { client });
533
785
  break;
534
786
  case "config":
535
- body = /* @__PURE__ */ jsx9(ConfigScreen, { client });
787
+ body = /* @__PURE__ */ jsx11(ConfigScreen, { client });
536
788
  break;
537
789
  case "routes":
538
- body = /* @__PURE__ */ jsx9(RoutesScreen, { client });
790
+ body = /* @__PURE__ */ jsx11(RoutesScreen, { client });
539
791
  break;
540
792
  case "tokens":
541
- body = /* @__PURE__ */ jsx9(TokensScreen, { client });
793
+ body = /* @__PURE__ */ jsx11(TokensScreen, { client });
542
794
  break;
543
795
  case "log":
544
- body = /* @__PURE__ */ jsx9(AdminLogScreen, { client });
796
+ body = /* @__PURE__ */ jsx11(AdminLogScreen, { client });
797
+ break;
798
+ case "catalog":
799
+ body = billingClient ? /* @__PURE__ */ jsx11(CatalogScreen, { client: billingClient }) : /* @__PURE__ */ jsx11("p", { style: styles.status, children: "Pass a BillingAdminClient to see the catalog here." });
800
+ break;
801
+ case "subscriber":
802
+ body = billingClient ? /* @__PURE__ */ jsx11(SubscriberScreen, { client: billingClient }) : /* @__PURE__ */ jsx11("p", { style: styles.status, children: "Pass a BillingAdminClient to look up subscribers here." });
545
803
  break;
546
804
  case "users":
547
- body = authClient ? /* @__PURE__ */ jsx9(UsersScreen, { client: authClient }) : /* @__PURE__ */ jsx9("p", { style: styles.status, children: "Pass an AuthAdminClient to look up users here." });
805
+ body = authClient ? /* @__PURE__ */ jsx11(UsersScreen, { client: authClient }) : /* @__PURE__ */ jsx11("p", { style: styles.status, children: "Pass an AuthAdminClient to look up users here." });
548
806
  break;
549
807
  case "settings":
550
- body = !configClient ? /* @__PURE__ */ jsx9("p", { style: styles.status, children: "Pass a ConfigAdminClient to manage config and secrets here." }) : editing && editing.kind !== "template" ? /* @__PURE__ */ jsxs9(Fragment6, { children: [
551
- /* @__PURE__ */ jsx9("button", { type: "button", style: styles.button, onClick: () => setEditing(null), children: "\u2190 Back" }),
552
- /* @__PURE__ */ jsx9(
808
+ body = !configClient ? /* @__PURE__ */ jsx11("p", { style: styles.status, children: "Pass a ConfigAdminClient to manage config and secrets here." }) : editing && editing.kind !== "template" ? /* @__PURE__ */ jsxs11(Fragment8, { children: [
809
+ /* @__PURE__ */ jsx11("button", { type: "button", style: styles.button, onClick: () => setEditing(null), children: "\u2190 Back" }),
810
+ /* @__PURE__ */ jsx11(
553
811
  ConfigEditorScreen,
554
812
  {
555
813
  client: configClient,
@@ -559,7 +817,7 @@ function AdminShell({
559
817
  onSaved: () => setEditing(null)
560
818
  }
561
819
  )
562
- ] }) : /* @__PURE__ */ jsx9(
820
+ ] }) : /* @__PURE__ */ jsx11(
563
821
  ConfigListScreen,
564
822
  {
565
823
  client: configClient,
@@ -570,9 +828,9 @@ function AdminShell({
570
828
  );
571
829
  break;
572
830
  case "templates":
573
- body = !courierClient ? /* @__PURE__ */ jsx9("p", { style: styles.status, children: "Pass a CourierAdminClient to manage templates here." }) : editing?.kind === "template" ? /* @__PURE__ */ jsxs9(Fragment6, { children: [
574
- /* @__PURE__ */ jsx9("button", { type: "button", style: styles.button, onClick: () => setEditing(null), children: "\u2190 Back" }),
575
- /* @__PURE__ */ jsx9(
831
+ body = !courierClient ? /* @__PURE__ */ jsx11("p", { style: styles.status, children: "Pass a CourierAdminClient to manage templates here." }) : editing?.kind === "template" ? /* @__PURE__ */ jsxs11(Fragment8, { children: [
832
+ /* @__PURE__ */ jsx11("button", { type: "button", style: styles.button, onClick: () => setEditing(null), children: "\u2190 Back" }),
833
+ /* @__PURE__ */ jsx11(
576
834
  TemplateEditorScreen,
577
835
  {
578
836
  client: courierClient,
@@ -580,7 +838,7 @@ function AdminShell({
580
838
  onSaved: () => setEditing(null)
581
839
  }
582
840
  )
583
- ] }) : /* @__PURE__ */ jsx9(
841
+ ] }) : /* @__PURE__ */ jsx11(
584
842
  TemplateListScreen,
585
843
  {
586
844
  client: courierClient,
@@ -589,13 +847,13 @@ function AdminShell({
589
847
  );
590
848
  break;
591
849
  }
592
- return /* @__PURE__ */ jsxs9("div", { style: styles.shell, children: [
593
- /* @__PURE__ */ jsx9("nav", { style: styles.nav, "aria-label": "Admin", children: NAV.map((g) => {
850
+ return /* @__PURE__ */ jsxs11("div", { style: styles.shell, children: [
851
+ /* @__PURE__ */ jsx11("nav", { style: styles.nav, "aria-label": "Admin", children: NAV.map((g) => {
594
852
  const items = g.items.filter((i) => !i.needs || has[i.needs]);
595
853
  if (items.length === 0) return null;
596
- return /* @__PURE__ */ jsxs9("div", { children: [
597
- /* @__PURE__ */ jsx9("div", { style: styles.navGroup, children: g.group }),
598
- items.map((i) => /* @__PURE__ */ jsx9(
854
+ return /* @__PURE__ */ jsxs11("div", { children: [
855
+ /* @__PURE__ */ jsx11("div", { style: styles.navGroup, children: g.group }),
856
+ items.map((i) => /* @__PURE__ */ jsx11(
599
857
  "button",
600
858
  {
601
859
  type: "button",
@@ -611,17 +869,19 @@ function AdminShell({
611
869
  ))
612
870
  ] }, g.group);
613
871
  }) }),
614
- /* @__PURE__ */ jsx9("main", { style: styles.main, children: body })
872
+ /* @__PURE__ */ jsx11("main", { style: styles.main, children: body })
615
873
  ] });
616
874
  }
617
875
  export {
618
876
  AdminLogScreen,
619
877
  AdminShell,
620
878
  AttentionScreen,
879
+ CatalogScreen,
621
880
  ConfigScreen,
622
881
  DoctorScreen,
623
882
  ModulesScreen,
624
883
  RoutesScreen,
884
+ SubscriberScreen,
625
885
  TokensScreen,
626
886
  UsersScreen
627
887
  };