@tollbooth-dpyc/web 1.2.0 → 1.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
@@ -87,7 +87,7 @@ bootstrapTheme(); // main.tsx, after configureTollbooth: <prefix>:the
87
87
  formatDateTime={(iso) => formatInZone(iso, zone)}
88
88
  before={<FundingStatus />}
89
89
  coupons={{ classNames: { root: "card", chip: "chip" } }}
90
- classNames={{ section: "card p-5", figure: "text-3xl", chip: "chip", chipActive: "chip-on" }}
90
+ classNames={{ section: "card p-5", figure: "text-3xl", chip: "chip", chipActive: "chip-on", primary: "chip-accent" }}
91
91
  />
92
92
 
93
93
  <ThemeToggle themes={["dark", "light", "system"]} classNames={{ chip: "chip", active: "chip-on" }} />
@@ -98,8 +98,18 @@ bootstrapTheme(); // main.tsx, after configureTollbooth: <prefix>:the
98
98
  onClear={clearAll}
99
99
  classNames={{ root: "flex gap-2", input: "field", chip: "chip" }}
100
100
  />
101
+
102
+ <SortHeader as="div" label="Date" col="date" activeCol={col} dir={dir} onSort={sortBy} /> // a div grid
101
103
  ```
102
104
 
105
+ A preset amount in `WalletPage` / `WalletCard` only fills the amount; the
106
+ invoice is made by the explicit "Create invoice". `classNames.primary` styles
107
+ the action that moves a top-up forward (Redeem in `CouponsPanel`); every other
108
+ action stays a chip. `TableFilter`'s panel slides itself back on screen on a
109
+ phone; `clearPlacement="panel"` puts Clear inside it. Every standard-tool
110
+ wrapper takes the same optional `CallOptions` as `callTool`, last
111
+ (`listCanonicalIdentities({ bestEffort: true })`).
112
+
103
113
  `useTopUp` is the wallet's top-up on its own (purchase_credits → invoice →
104
114
  check_payment, polled while the tab is visible) for a site drawing its own.
105
115
 
package/dist/client.d.ts CHANGED
@@ -28,14 +28,6 @@ export interface CallOptions {
28
28
  * patron out, whatever it gets back.
29
29
  */
30
30
  bestEffort?: boolean;
31
- /**
32
- * A background read — a badge, a poll — that must not send the patron to
33
- * the sign-in gate. A proof bounce still throws `ProofRequiredError`, but
34
- * the session is left as it is and `onProofExpired` is not told; the next
35
- * call the patron makes themselves meets the lapse and re-arms sign-in.
36
- * Opt-in: by default a bounce signs the patron out and re-arms the gate.
37
- */
38
- quietProof?: boolean;
39
31
  timeoutMs?: number;
40
32
  }
41
33
  export interface ToolAnswer<T> {
package/dist/client.js CHANGED
@@ -166,8 +166,6 @@ export async function callToolWithContent(tool, args = {}, opts = {}) {
166
166
  if (!opts.bestEffort) {
167
167
  const bounced = proofBounceMessage(payload);
168
168
  if (bounced !== null) {
169
- if (opts.quietProof)
170
- throw new ProofRequiredError(bounced);
171
169
  // The token just refused is the same one the recent-login shortcut
172
170
  // would replay, so it goes too, or a returning patron re-bounces.
173
171
  const npub = getStoredNpub();
@@ -5,7 +5,8 @@
5
5
  * Mechanics only — the calls (list_my_coupons, redeem_coupon, forget_coupon),
6
6
  * the states and the words. Every visual choice is the site's, through
7
7
  * `classNames`; with none, the panel is plain markup inheriting from the page.
8
- * Actions (Redeem, Remove) are chips: `classNames.chip`.
8
+ * Actions (Redeem, Remove) are chips: `classNames.chip`; Redeem, the primary
9
+ * action, takes `classNames.primary` instead when a site gives it its accent.
9
10
  */
10
11
  import { type ReactNode } from "react";
11
12
  import { type PatronCoupon } from "../standardTools.ts";
@@ -18,6 +19,10 @@ export interface CouponsPanelClassNames {
18
19
  input?: string;
19
20
  /** Every action: Redeem, Remove. */
20
21
  chip?: string;
22
+ /** Redeem, in place of `chip`, for a site that gives it its accent. Default: `chip`. */
23
+ primary?: string;
24
+ /** The sub-labels `formHeading` and `listHeading`. */
25
+ subheading?: string;
21
26
  /** Added to the message after a redeem, with `ok` or `error`. */
22
27
  message?: string;
23
28
  ok?: string;
@@ -38,6 +43,10 @@ export interface CouponsPanelProps {
38
43
  intro?: ReactNode;
39
44
  /** Shown when the patron holds no coupons. */
40
45
  empty?: ReactNode;
46
+ /** A sub-label above the code box, e.g. "Redeem a code". Default none. */
47
+ formHeading?: ReactNode;
48
+ /** A sub-label above the held coupons, e.g. "Active". Shown only when there are some. Default none. */
49
+ listHeading?: ReactNode;
41
50
  placeholder?: string;
42
51
  /** Content of the Redeem chip. */
43
52
  redeemLabel?: ReactNode;
@@ -49,4 +58,4 @@ export interface CouponsPanelProps {
49
58
  /** Draw a coupon's row yourself; `forget` removes it (after confirmForget). */
50
59
  renderCoupon?: (coupon: PatronCoupon, forget: () => void) => ReactNode;
51
60
  }
52
- export default function CouponsPanel({ heading, intro, empty, placeholder, redeemLabel, forgetLabel, confirmForget, classNames: c, renderCoupon, }: CouponsPanelProps): import("react").JSX.Element;
61
+ export default function CouponsPanel({ heading, intro, empty, formHeading, listHeading, placeholder, redeemLabel, forgetLabel, confirmForget, classNames: c, renderCoupon, }: CouponsPanelProps): import("react").JSX.Element;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * The patron's coupons: redeem an operator's code once, see what is held, and
4
4
  * take one off the list.
@@ -6,7 +6,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  * Mechanics only — the calls (list_my_coupons, redeem_coupon, forget_coupon),
7
7
  * the states and the words. Every visual choice is the site's, through
8
8
  * `classNames`; with none, the panel is plain markup inheriting from the page.
9
- * Actions (Redeem, Remove) are chips: `classNames.chip`.
9
+ * Actions (Redeem, Remove) are chips: `classNames.chip`; Redeem, the primary
10
+ * action, takes `classNames.primary` instead when a site gives it its accent.
10
11
  */
11
12
  import { useCallback, useEffect, useState } from "react";
12
13
  import { forgetCoupon, listMyCoupons, redeemCoupon } from "../standardTools.js";
@@ -21,7 +22,7 @@ function cx(...parts) {
21
22
  const s = parts.filter(Boolean).join(" ");
22
23
  return s || undefined;
23
24
  }
24
- export default function CouponsPanel({ heading = "My coupons", intro = DEFAULT_INTRO, empty = DEFAULT_EMPTY, placeholder = "Coupon code", redeemLabel = "Redeem", forgetLabel = "Remove", confirmForget = defaultConfirm, classNames: c = {}, renderCoupon, }) {
25
+ export default function CouponsPanel({ heading = "My coupons", intro = DEFAULT_INTRO, empty = DEFAULT_EMPTY, formHeading, listHeading, placeholder = "Coupon code", redeemLabel = "Redeem", forgetLabel = "Remove", confirmForget = defaultConfirm, classNames: c = {}, renderCoupon, }) {
25
26
  const [coupons, setCoupons] = useState([]);
26
27
  const [loading, setLoading] = useState(true);
27
28
  const [loadError, setLoadError] = useState(null);
@@ -86,10 +87,10 @@ export default function CouponsPanel({ heading = "My coupons", intro = DEFAULT_I
86
87
  setMsg({ ok: false, text: e.message });
87
88
  }
88
89
  }
89
- return (_jsxs("section", { className: c.root, children: [heading && _jsx("div", { className: c.heading, children: heading }), intro && _jsx("p", { className: c.intro, children: intro }), _jsxs("div", { className: c.form, children: [_jsx("input", { type: "text", value: code, onChange: (e) => setCode(e.target.value.toUpperCase()), onKeyDown: (e) => {
90
+ return (_jsxs("section", { className: c.root, children: [heading && _jsx("div", { className: c.heading, children: heading }), intro && _jsx("p", { className: c.intro, children: intro }), formHeading && _jsx("div", { className: c.subheading, children: formHeading }), _jsxs("div", { className: c.form, children: [_jsx("input", { type: "text", value: code, onChange: (e) => setCode(e.target.value.toUpperCase()), onKeyDown: (e) => {
90
91
  if (e.key === "Enter")
91
92
  void redeem();
92
- }, placeholder: placeholder, "aria-label": "Coupon code", autoCapitalize: "characters", autoCorrect: "off", spellCheck: false, disabled: redeeming, className: c.input }), _jsx("button", { type: "button", onClick: () => void redeem(), disabled: redeeming || !code.trim(), className: c.chip, children: redeeming ? "Redeeming…" : redeemLabel })] }), msg && (_jsx("p", { role: "status", className: cx(c.message, msg.ok ? c.ok : c.error), children: msg.text })), loading ? (_jsx("p", { className: c.loading, children: "Loading\u2026" })) : loadError ? (_jsx("p", { className: cx(c.message, c.error), children: loadError })) : coupons.length === 0 ? (empty && _jsx("p", { className: c.empty, children: empty })) : (_jsx("ul", { className: c.list, children: coupons.map((coupon) => renderCoupon ? (_jsx("li", { className: c.row, children: renderCoupon(coupon, () => void forget(coupon)) }, coupon.coupon_id)) : (_jsx(CouponRow, { coupon: coupon, c: c, forgetLabel: forgetLabel, onForget: () => void forget(coupon) }, coupon.coupon_id))) }))] }));
93
+ }, placeholder: placeholder, "aria-label": "Coupon code", autoCapitalize: "characters", autoCorrect: "off", spellCheck: false, disabled: redeeming, className: c.input }), _jsx("button", { type: "button", onClick: () => void redeem(), disabled: redeeming || !code.trim(), className: c.primary ?? c.chip, children: redeeming ? "Redeeming…" : redeemLabel })] }), msg && (_jsx("p", { role: "status", className: cx(c.message, msg.ok ? c.ok : c.error), children: msg.text })), loading ? (_jsx("p", { className: c.loading, children: "Loading\u2026" })) : loadError ? (_jsx("p", { className: cx(c.message, c.error), children: loadError })) : coupons.length === 0 ? (empty && _jsx("p", { className: c.empty, children: empty })) : (_jsxs(_Fragment, { children: [listHeading && _jsx("div", { className: c.subheading, children: listHeading }), _jsx("ul", { className: c.list, children: coupons.map((coupon) => renderCoupon ? (_jsx("li", { className: c.row, children: renderCoupon(coupon, () => void forget(coupon)) }, coupon.coupon_id)) : (_jsx(CouponRow, { coupon: coupon, c: c, forgetLabel: forgetLabel, onForget: () => void forget(coupon) }, coupon.coupon_id))) })] }))] }));
93
94
  }
94
95
  function CouponRow({ coupon, c, forgetLabel, onForget, }) {
95
96
  const active = coupon.status === "active";
@@ -10,7 +10,7 @@
10
10
  import type { ReactNode } from "react";
11
11
  import { type SortDir } from "../table.ts";
12
12
  export interface SortHeaderClassNames {
13
- /** The <th>. */
13
+ /** The header cell: the <th>, or the <div> when `as="div"`. */
14
14
  cell?: string;
15
15
  /** The button inside a sortable header. */
16
16
  button?: string;
@@ -28,10 +28,15 @@ export interface SortHeaderProps {
28
28
  onSort: (col: string, dir: SortDir) => void;
29
29
  /** The direction a newly chosen column starts in. Default "desc". */
30
30
  initialDir?: SortDir;
31
+ /**
32
+ * The element: a <th> in a table (default), or a <div role="columnheader">
33
+ * for a grid of divs — the sort, the button and aria-sort are the same.
34
+ */
35
+ as?: "th" | "div";
31
36
  classNames?: SortHeaderClassNames;
32
37
  }
33
38
  /** A header cell. Tapping the sorted column flips it; another column starts at `initialDir`. */
34
- export declare function SortHeader({ label, col, activeCol, dir, onSort, initialDir, classNames: c }: SortHeaderProps): import("react").JSX.Element;
39
+ export declare function SortHeader({ label, col, activeCol, dir, onSort, initialDir, as, classNames: c, }: SortHeaderProps): import("react").JSX.Element;
35
40
  export interface PageControlsClassNames {
36
41
  root?: string;
37
42
  /** Every control. */
@@ -1,15 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { clampPage, lastPage, nextSort } from "../table.js";
2
+ import { ariaSort, clampPage, lastPage, nextSort } from "../table.js";
3
3
  function cx(...parts) {
4
4
  const s = parts.filter(Boolean).join(" ");
5
5
  return s || undefined;
6
6
  }
7
7
  /** A header cell. Tapping the sorted column flips it; another column starts at `initialDir`. */
8
- export function SortHeader({ label, col, activeCol, dir, onSort, initialDir = "desc", classNames: c = {} }) {
9
- if (!col)
10
- return _jsx("th", { className: c.cell, children: label });
8
+ export function SortHeader({ label, col, activeCol, dir, onSort, initialDir = "desc", as = "th", classNames: c = {}, }) {
9
+ const Cell = as;
10
+ const role = as === "div" ? "columnheader" : undefined;
11
+ if (!col) {
12
+ return (_jsx(Cell, { role: role, className: c.cell, children: label }));
13
+ }
11
14
  const active = col === activeCol;
12
- return (_jsx("th", { className: c.cell, "aria-sort": active ? (dir === "desc" ? "descending" : "ascending") : undefined, children: _jsxs("button", { type: "button", onClick: () => {
15
+ return (_jsx(Cell, { role: role, className: c.cell, "aria-sort": ariaSort(col, activeCol, dir), children: _jsxs("button", { type: "button", onClick: () => {
13
16
  const next = nextSort(col, activeCol, dir, initialDir);
14
17
  onSort(next.col, next.dir);
15
18
  }, className: cx(c.button, active && c.active), children: [label, active && (_jsx("span", { "aria-hidden": true, className: c.arrow, children: dir === "desc" ? " ▾" : " ▴" }))] }) }));
@@ -10,7 +10,9 @@
10
10
  * the query (server-side, or `filterRows` in the browser), so paging reflects
11
11
  * the filtered set. The search is applied on Enter or its chip, never on each
12
12
  * keystroke. Mechanics only: every visual choice is the site's, through
13
- * `classNames`; the actions are chips.
13
+ * `classNames`; the actions are chips. The panel is kept on screen: once open,
14
+ * it is slid sideways (`nudgeIntoView`) so no part of it hangs past either edge
15
+ * of a phone.
14
16
  */
15
17
  import { type ReactNode } from "react";
16
18
  export interface DateFieldOption {
@@ -86,8 +88,14 @@ export interface TableFilterProps<F extends object> {
86
88
  questions?: TableFilterQuestions<F>;
87
89
  /** Turn everything off. */
88
90
  onClear: () => void;
91
+ /**
92
+ * Where the Clear chip goes: beside the other controls (default), or inside
93
+ * the questions panel. "panel" needs a `questions` group; without one it is
94
+ * beside.
95
+ */
96
+ clearPlacement?: "beside" | "panel";
89
97
  searchIcon?: ReactNode;
90
98
  clearLabel?: ReactNode;
91
99
  classNames?: TableFilterClassNames;
92
100
  }
93
- export default function TableFilter<F extends object>({ search, dates, questions, onClear, searchIcon, clearLabel, classNames: c, }: TableFilterProps<F>): import("react").JSX.Element;
101
+ export default function TableFilter<F extends object>({ search, dates, questions, onClear, clearPlacement, searchIcon, clearLabel, classNames: c, }: TableFilterProps<F>): import("react").JSX.Element;
@@ -11,18 +11,24 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
11
11
  * the query (server-side, or `filterRows` in the browser), so paging reflects
12
12
  * the filtered set. The search is applied on Enter or its chip, never on each
13
13
  * keystroke. Mechanics only: every visual choice is the site's, through
14
- * `classNames`; the actions are chips.
14
+ * `classNames`; the actions are chips. The panel is kept on screen: once open,
15
+ * it is slid sideways (`nudgeIntoView`) so no part of it hangs past either edge
16
+ * of a phone.
15
17
  */
16
- import { useEffect, useRef, useState } from "react";
18
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
17
19
  import { Search, SlidersHorizontal, X } from "lucide-react";
18
- import { filterActive } from "../table.js";
20
+ import { filterActive, nudgeIntoView } from "../table.js";
21
+ /** The panel keeps this many px from each edge of the viewport. */
22
+ const EDGE = 8;
19
23
  function cx(...parts) {
20
24
  const s = parts.filter(Boolean).join(" ");
21
25
  return s || undefined;
22
26
  }
23
- export default function TableFilter({ search, dates, questions, onClear, searchIcon = _jsx(Search, { size: "1em", "aria-hidden": true }), clearLabel = (_jsxs(_Fragment, { children: [_jsx(X, { size: "1em", "aria-hidden": true }), " Clear"] })), classNames: c = {}, }) {
27
+ export default function TableFilter({ search, dates, questions, onClear, clearPlacement = "beside", searchIcon = _jsx(Search, { size: "1em", "aria-hidden": true }), clearLabel = (_jsxs(_Fragment, { children: [_jsx(X, { size: "1em", "aria-hidden": true }), " Clear"] })), classNames: c = {}, }) {
24
28
  const active = filterActive({ search: search?.value, dateFrom: dates?.from, dateTo: dates?.to }, questions?.summary ?? "");
25
- return (_jsxs("div", { className: c.root, children: [search && _jsx(SearchBox, { search: search, icon: searchIcon, c: c }), dates && _jsx(DateRange, { dates: dates, c: c }), questions && _jsx(QuestionPanel, { q: questions, c: c }), active && (_jsx("button", { type: "button", onClick: onClear, className: c.chip, children: clearLabel }))] }));
29
+ const clear = active ? (_jsx("button", { type: "button", onClick: onClear, className: c.chip, children: clearLabel })) : null;
30
+ const inPanel = clearPlacement === "panel" && !!questions;
31
+ return (_jsxs("div", { className: c.root, children: [search && _jsx(SearchBox, { search: search, icon: searchIcon, c: c }), dates && _jsx(DateRange, { dates: dates, c: c }), questions && _jsx(QuestionPanel, { q: questions, clear: inPanel ? clear : null, c: c }), !inPanel && clear] }));
26
32
  }
27
33
  function SearchBox({ search, icon, c }) {
28
34
  // Edited freely, applied on Enter or the chip; follows the applied value
@@ -37,9 +43,31 @@ function SearchBox({ search, icon, c }) {
37
43
  function DateRange({ dates, c }) {
38
44
  return (_jsxs("div", { className: c.dates, children: [dates.fields && dates.fields.length > 0 && dates.onField && (_jsx("select", { value: dates.field, onChange: (e) => dates.onField(e.target.value), title: "Which date the range filters", "aria-label": "Date to filter", className: c.select, children: dates.fields.map((o) => (_jsx("option", { value: o.value, children: o.label }, o.value))) })), _jsx("input", { type: "date", value: dates.from, max: dates.to || undefined, onChange: (e) => dates.onFrom(e.target.value), title: "From (inclusive)", "aria-label": "From date", className: c.date }), _jsx("span", { className: c.separator, "aria-hidden": true, children: "\u2013" }), _jsx("input", { type: "date", value: dates.to, min: dates.from || undefined, onChange: (e) => dates.onTo(e.target.value), title: "To (inclusive)", "aria-label": "To date", className: c.date })] }));
39
45
  }
40
- function QuestionPanel({ q, c }) {
46
+ function QuestionPanel({ q, clear, c, }) {
41
47
  const [open, setOpen] = useState(false);
42
48
  const box = useRef(null);
49
+ const panel = useRef(null);
50
+ // Kept on screen: measured where the site's CSS puts it, then slid by the
51
+ // `translate` property (a site's own `transform` is left alone). Only a panel
52
+ // wider than the viewport gets a max-width.
53
+ useLayoutEffect(() => {
54
+ const el = panel.current;
55
+ if (!open || !el)
56
+ return;
57
+ const place = () => {
58
+ el.style.translate = "";
59
+ el.style.maxWidth = "";
60
+ const room = document.documentElement.clientWidth;
61
+ if (el.getBoundingClientRect().width > room - 2 * EDGE)
62
+ el.style.maxWidth = `${room - 2 * EDGE}px`;
63
+ const r = el.getBoundingClientRect();
64
+ const dx = nudgeIntoView(r.left, r.width, room, EDGE);
65
+ el.style.translate = dx ? `${dx}px 0` : "";
66
+ };
67
+ place();
68
+ window.addEventListener("resize", place);
69
+ return () => window.removeEventListener("resize", place);
70
+ }, [open]);
43
71
  // Dismissed by a tap outside or Escape.
44
72
  useEffect(() => {
45
73
  if (!open)
@@ -61,8 +89,8 @@ function QuestionPanel({ q, c }) {
61
89
  }, [open]);
62
90
  const on = q.summary !== "";
63
91
  const set = (key, v) => q.onChange({ ...q.value, [key]: v });
64
- return (_jsxs("div", { className: c.questions, ref: box, children: [_jsxs("button", { type: "button", onClick: () => setOpen((o) => !o), "aria-expanded": open, title: on ? `Filtered: ${q.summary}` : "Filter", "aria-label": on ? `Filtered: ${q.summary}` : "Filter", className: cx(c.chip, on && c.chipActive), children: [q.icon ?? _jsx(SlidersHorizontal, { size: "1em", "aria-hidden": true }), on && _jsx("span", { className: c.summary, children: q.summary })] }), open && (_jsx("div", { className: c.panel, role: "group", "aria-label": "Filter", children: q.questions.map((question) => {
65
- const id = `tf-${String(question.key)}`;
66
- return question.kind === "toggle" ? (_jsxs("label", { className: c.question, children: [_jsx("input", { type: "checkbox", checked: !!q.value[question.key], onChange: (e) => set(question.key, e.target.checked), className: c.checkbox }), " ", question.label] }, id)) : (_jsxs("div", { className: c.question, children: [_jsx("label", { htmlFor: id, children: question.label }), " ", _jsx("input", { id: id, inputMode: "numeric", value: String(q.value[question.key] ?? ""), onChange: (e) => set(question.key, e.target.value), className: c.number }), " ", _jsx("span", { className: c.unit, children: question.unit })] }, id));
67
- }) }))] }));
92
+ return (_jsxs("div", { className: c.questions, ref: box, children: [_jsxs("button", { type: "button", onClick: () => setOpen((o) => !o), "aria-expanded": open, title: on ? `Filtered: ${q.summary}` : "Filter", "aria-label": on ? `Filtered: ${q.summary}` : "Filter", className: cx(c.chip, on && c.chipActive), children: [q.icon ?? _jsx(SlidersHorizontal, { size: "1em", "aria-hidden": true }), on && _jsx("span", { className: c.summary, children: q.summary })] }), open && (_jsxs("div", { className: c.panel, ref: panel, role: "group", "aria-label": "Filter", children: [q.questions.map((question) => {
93
+ const id = `tf-${String(question.key)}`;
94
+ return question.kind === "toggle" ? (_jsxs("label", { className: c.question, children: [_jsx("input", { type: "checkbox", checked: !!q.value[question.key], onChange: (e) => set(question.key, e.target.checked), className: c.checkbox }), " ", question.label] }, id)) : (_jsxs("div", { className: c.question, children: [_jsx("label", { htmlFor: id, children: question.label }), " ", _jsx("input", { id: id, inputMode: "numeric", value: String(q.value[question.key] ?? ""), onChange: (e) => set(question.key, e.target.value), className: c.number }), " ", _jsx("span", { className: c.unit, children: question.unit })] }, id));
95
+ }), clear] }))] }));
68
96
  }
@@ -4,11 +4,23 @@
4
4
  * Every figure is the service's answer or a dash. A balance nobody has read is
5
5
  * unknown, and unknown is not zero: showing 0 to someone with funds would send
6
6
  * them to buy credits they already own. The card offers round numbers to pay
7
- * in and never guesses what anything costs.
7
+ * in and never guesses what anything costs. Tapping one only chooses it; the
8
+ * invoice is made by "Create invoice".
8
9
  *
9
10
  * The compact card. `WalletPage` is the full account view; both run the same
10
11
  * top-up (`useTopUp`), which checks an open invoice on its own until it settles.
11
12
  */
12
- export default function WalletCard({ topUps }: {
13
+ export interface WalletCardClassNames {
14
+ /** Each preset amount, and the invoice's check and cancel. Default: the package's outlined chip. */
15
+ chip?: string;
16
+ /** Added to the chosen preset. Default: an accent ring. */
17
+ chipActive?: string;
18
+ /** The action that moves a top-up forward — "Create invoice", then "Pay". Default: the accent button. */
19
+ primary?: string;
20
+ }
21
+ export interface WalletCardProps {
22
+ /** Preset amounts, in sats. A tap fills the amount; it does not invoice. */
13
23
  topUps?: number[];
14
- }): import("react").JSX.Element;
24
+ classNames?: WalletCardClassNames;
25
+ }
26
+ export default function WalletCard({ topUps, classNames }: WalletCardProps): import("react").JSX.Element;
@@ -5,20 +5,23 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  * Every figure is the service's answer or a dash. A balance nobody has read is
6
6
  * unknown, and unknown is not zero: showing 0 to someone with funds would send
7
7
  * them to buy credits they already own. The card offers round numbers to pay
8
- * in and never guesses what anything costs.
8
+ * in and never guesses what anything costs. Tapping one only chooses it; the
9
+ * invoice is made by "Create invoice".
9
10
  *
10
11
  * The compact card. `WalletPage` is the full account view; both run the same
11
12
  * top-up (`useTopUp`), which checks an open invoice on its own until it settles.
12
13
  */
13
- import { useCallback, useEffect, useState } from "react";
14
+ import { useCallback, useEffect, useReducer, useState } from "react";
14
15
  import { RefreshCw, Zap } from "lucide-react";
15
16
  import { tollboothConfig } from "../config.js";
16
17
  import { checkBalance } from "../standardTools.js";
17
- import { formatSats } from "../wallet.js";
18
+ import { amountReducer, formatSats, parseSats, presetChosen } from "../wallet.js";
18
19
  import { card, ghost, iconButton, muted, primary } from "./ui.js";
19
20
  import { useTopUp } from "./useTopUp.js";
20
21
  const DEFAULT_TOP_UPS = [1_000, 5_000, 20_000];
21
- export default function WalletCard({ topUps = DEFAULT_TOP_UPS }) {
22
+ const cx = (...parts) => parts.filter(Boolean).join(" ");
23
+ export default function WalletCard({ topUps = DEFAULT_TOP_UPS, classNames = {} }) {
24
+ const c = { chip: ghost, chipActive: "ring-2 ring-[var(--tb-accent)]", primary, ...classNames };
22
25
  const { appName } = tollboothConfig();
23
26
  const [balance, setBalance] = useState(null);
24
27
  const [reachable, setReachable] = useState(true);
@@ -33,6 +36,8 @@ export default function WalletCard({ topUps = DEFAULT_TOP_UPS }) {
33
36
  }, []);
34
37
  useEffect(load, [load]);
35
38
  const { state, create, check, cancel } = useTopUp({ onSettled: load });
39
+ const [amount, pick] = useReducer(amountReducer, "");
40
+ const sats = parseSats(amount);
36
41
  const busy = state.phase === "creating" || (state.phase === "awaiting" && state.checking);
37
42
  const invoice = state.phase === "awaiting" ? state.invoice : null;
38
43
  const msg = state.phase === "settled"
@@ -44,5 +49,5 @@ export default function WalletCard({ topUps = DEFAULT_TOP_UPS }) {
44
49
  : state.phase === "idle"
45
50
  ? (state.message ?? "")
46
51
  : "";
47
- return (_jsxs("section", { className: `${card} p-4`, children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: `text-sm ${muted}`, children: "Balance" }), _jsx("button", { type: "button", onClick: load, title: "Refresh", "aria-label": "Refresh balance", className: iconButton, children: _jsx(RefreshCw, { size: 14 }) })] }), _jsxs("div", { className: "mt-1 flex items-baseline gap-2", children: [_jsx("span", { className: "text-2xl font-semibold tabular-nums", children: formatSats(reachable ? balance : null) }), _jsx("span", { className: `text-sm ${muted}`, children: "sats" })] }), !reachable && _jsxs("p", { className: `mt-2 text-xs ${muted}`, children: [appName, " did not answer, so no balance is shown."] }), !invoice ? (_jsx("div", { className: "mt-4 flex gap-2", children: topUps.map((n) => (_jsxs("button", { type: "button", disabled: busy, onClick: () => create(n), className: ghost, children: ["+", n.toLocaleString("en-US")] }, n))) })) : (_jsxs("div", { className: "mt-4 space-y-2", children: [_jsxs("a", { href: invoice.checkoutLink ?? (invoice.bolt11 ? `lightning:${invoice.bolt11}` : "#"), target: "_blank", rel: "noreferrer", className: `${primary} flex items-center justify-center gap-2`, children: [_jsx(Zap, { size: 16 }), " Pay ", invoice.sats.toLocaleString("en-US"), " sats"] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { type: "button", disabled: busy, onClick: check, className: ghost, children: "I've paid \u2014 check" }), _jsx("button", { type: "button", onClick: cancel, className: `${ghost} w-auto px-4`, children: "Cancel" })] })] })), msg && _jsx("p", { className: `mt-3 text-xs ${muted}`, children: msg })] }));
52
+ return (_jsxs("section", { className: `${card} p-4`, children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: `text-sm ${muted}`, children: "Balance" }), _jsx("button", { type: "button", onClick: load, title: "Refresh", "aria-label": "Refresh balance", className: iconButton, children: _jsx(RefreshCw, { size: 14 }) })] }), _jsxs("div", { className: "mt-1 flex items-baseline gap-2", children: [_jsx("span", { className: "text-2xl font-semibold tabular-nums", children: formatSats(reachable ? balance : null) }), _jsx("span", { className: `text-sm ${muted}`, children: "sats" })] }), !reachable && _jsxs("p", { className: `mt-2 text-xs ${muted}`, children: [appName, " did not answer, so no balance is shown."] }), !invoice ? (_jsxs("div", { className: "mt-4 space-y-2", children: [_jsx("div", { className: "flex gap-2", children: topUps.map((n) => (_jsxs("button", { type: "button", disabled: busy, "aria-pressed": presetChosen(amount, n), onClick: () => pick({ type: "preset", sats: n }), className: cx(c.chip, presetChosen(amount, n) && c.chipActive), children: ["+", n.toLocaleString("en-US")] }, n))) }), _jsx("button", { type: "button", disabled: busy || !sats, onClick: () => sats && create(sats), className: c.primary, children: "Create invoice" })] })) : (_jsxs("div", { className: "mt-4 space-y-2", children: [_jsxs("a", { href: invoice.checkoutLink ?? (invoice.bolt11 ? `lightning:${invoice.bolt11}` : "#"), target: "_blank", rel: "noreferrer", className: cx(c.primary, "flex items-center justify-center gap-2"), children: [_jsx(Zap, { size: 16 }), " Pay ", invoice.sats.toLocaleString("en-US"), " sats"] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { type: "button", disabled: busy, onClick: check, className: c.chip, children: "I've paid \u2014 check" }), _jsx("button", { type: "button", onClick: cancel, className: cx(c.chip, "w-auto px-4"), children: "Cancel" })] })] })), msg && _jsx("p", { className: `mt-3 text-xs ${muted}`, children: msg })] }));
48
53
  }
@@ -6,7 +6,9 @@
6
6
  * purchase_credits → check_payment, account_statement, the coupon tools),
7
7
  * holds the states and writes the words; every visual choice belongs to the
8
8
  * site, through `classNames` — with none it is plain markup that inherits from
9
- * the page. Actions are chips (`classNames.chip`). Anything only one site has
9
+ * the page. Actions are chips (`classNames.chip`); the one that moves a top-up
10
+ * forward may keep an accent (`classNames.primary`). A preset amount only fills
11
+ * the amount — the invoice is made by "Create invoice". Anything only one site has
10
12
  * (a funding-health panel, an operator view) goes in `before` or `children`.
11
13
  *
12
14
  * A balance nobody has read is a dash, never 0: showing 0 to someone with
@@ -32,7 +34,13 @@ export interface WalletPageClassNames {
32
34
  error?: string;
33
35
  /** Every action. */
34
36
  chip?: string;
35
- /** Added to a chip that is the current choice. */
37
+ /**
38
+ * The action that moves a top-up forward — "Create invoice", then "Open
39
+ * checkout" — in place of `chip`, for a site that gives it its accent.
40
+ * Default: `chip`.
41
+ */
42
+ primary?: string;
43
+ /** Added to a chip that is the current choice (the chosen preset). */
36
44
  chipActive?: string;
37
45
  /** A row of chips. */
38
46
  chips?: string;
@@ -48,7 +56,7 @@ export interface WalletPageClassNames {
48
56
  export interface WalletPageProps {
49
57
  /** The page heading. Default "Wallet"; null for none. */
50
58
  heading?: ReactNode;
51
- /** Preset amounts, in sats, offered as chips. */
59
+ /** Preset amounts, in sats, offered as chips. A tap fills the amount; it does not invoice. */
52
60
  topUps?: number[];
53
61
  /** Offer a box for any amount. Default true. */
54
62
  customAmount?: boolean;
@@ -7,15 +7,17 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
7
7
  * purchase_credits → check_payment, account_statement, the coupon tools),
8
8
  * holds the states and writes the words; every visual choice belongs to the
9
9
  * site, through `classNames` — with none it is plain markup that inherits from
10
- * the page. Actions are chips (`classNames.chip`). Anything only one site has
10
+ * the page. Actions are chips (`classNames.chip`); the one that moves a top-up
11
+ * forward may keep an accent (`classNames.primary`). A preset amount only fills
12
+ * the amount — the invoice is made by "Create invoice". Anything only one site has
11
13
  * (a funding-health panel, an operator view) goes in `before` or `children`.
12
14
  *
13
15
  * A balance nobody has read is a dash, never 0: showing 0 to someone with
14
16
  * funds sends them to buy credits they already own.
15
17
  */
16
- import { useCallback, useEffect, useState } from "react";
18
+ import { useCallback, useEffect, useReducer, useState } from "react";
17
19
  import { checkBalance, getAccountStatement, } from "../standardTools.js";
18
- import { formatSats, parseSats } from "../wallet.js";
20
+ import { amountReducer, formatSats, parseSats, presetChosen } from "../wallet.js";
19
21
  import CouponsPanel, {} from "./CouponsPanel.js";
20
22
  import { useTopUp } from "./useTopUp.js";
21
23
  const DEFAULT_TOP_UPS = [1_000, 5_000, 25_000];
@@ -55,10 +57,15 @@ function Balance({ bal, error, refresh, formatDateTime, c, }) {
55
57
  }
56
58
  function TopUpSection({ topUps, customAmount, poll, onSettled, c, }) {
57
59
  const { state, create, check, cancel, reset } = useTopUp({ ...poll, onSettled });
58
- const [custom, setCustom] = useState("");
60
+ const [amount, pick] = useReducer(amountReducer, "");
59
61
  const [copied, setCopied] = useState(false);
60
- const customSats = parseSats(custom);
62
+ const sats = parseSats(amount);
61
63
  const creating = state.phase === "creating";
64
+ const primary = c.primary ?? c.chip;
65
+ function confirm() {
66
+ if (sats && !creating)
67
+ create(sats);
68
+ }
62
69
  async function copy(text) {
63
70
  try {
64
71
  await navigator.clipboard.writeText(text);
@@ -69,10 +76,13 @@ function TopUpSection({ topUps, customAmount, poll, onSettled, c, }) {
69
76
  /* the string is on screen and selectable */
70
77
  }
71
78
  }
72
- return (_jsxs("section", { className: c.section, children: [_jsx("div", { className: c.sectionTitle, children: "Top up with Lightning" }), (state.phase === "idle" || state.phase === "creating") && (_jsxs(_Fragment, { children: [_jsx("div", { className: c.chips, children: topUps.map((n) => (_jsxs("button", { type: "button", disabled: creating, onClick: () => create(n), className: cx(c.chip, creating && state.sats === n && c.chipActive), children: [n.toLocaleString("en-US"), " sats"] }, n))) }), customAmount && (_jsxs("div", { className: c.chips, children: [_jsx("input", { inputMode: "numeric", value: custom, onChange: (e) => setCustom(e.target.value.replace(/[^\d]/g, "")), onKeyDown: (e) => {
73
- if (e.key === "Enter" && customSats)
74
- create(customSats);
75
- }, placeholder: "Any amount", "aria-label": "Amount in sats", className: c.input }), _jsx("button", { type: "button", disabled: creating || !customSats, onClick: () => customSats && create(customSats), className: c.chip, children: "Create invoice" })] })), creating && _jsx("p", { className: c.status, children: "Making a Lightning invoice\u2026" }), state.phase === "idle" && state.message && _jsx("p", { className: c.error, children: state.message })] })), state.phase === "awaiting" && (_jsxs("div", { className: c.invoice, children: [_jsxs("p", { className: c.status, children: ["Invoice for ", formatSats(state.invoice.sats), " sats"] }), state.invoice.bolt11 && _jsx("div", { className: c.bolt11, children: state.invoice.bolt11 }), _jsxs("div", { className: c.chips, children: [state.invoice.checkoutLink && (_jsx("a", { href: state.invoice.checkoutLink, target: "_blank", rel: "noreferrer", className: c.chip, children: "Open checkout" })), state.invoice.bolt11 && (_jsx("button", { type: "button", onClick: () => void copy(state.invoice.bolt11), className: c.chip, children: copied ? "Copied" : "Copy invoice" })), _jsx("button", { type: "button", disabled: state.checking, onClick: check, className: c.chip, children: state.checking ? "Checking…" : "I've paid — check" }), _jsx("button", { type: "button", onClick: cancel, className: c.chip, children: "Cancel" })] }), _jsx("p", { className: c.status, "aria-live": "polite", children: state.status ?? "Waiting for payment." })] })), state.phase === "settled" && (_jsxs(_Fragment, { children: [_jsxs("p", { className: c.status, "aria-live": "polite", children: ["Paid. ", formatSats(state.credited), " sats added."] }), _jsx("div", { className: c.chips, children: _jsx("button", { type: "button", onClick: reset, className: c.chip, children: "Done" }) })] })), state.phase === "failed" && (_jsxs(_Fragment, { children: [_jsx("p", { className: c.error, children: state.message }), _jsx("div", { className: c.chips, children: _jsx("button", { type: "button", onClick: reset, className: c.chip, children: "Try again" }) })] }))] }));
79
+ return (_jsxs("section", { className: c.section, children: [_jsx("div", { className: c.sectionTitle, children: "Top up with Lightning" }), (state.phase === "idle" || state.phase === "creating") && (_jsxs(_Fragment, { children: [_jsx("div", { className: c.chips, children: topUps.map((n) => (_jsxs("button", { type: "button", disabled: creating, "aria-pressed": presetChosen(amount, n), onClick: () => pick({ type: "preset", sats: n }), className: cx(c.chip, presetChosen(amount, n) && c.chipActive), children: [n.toLocaleString("en-US"), " sats"] }, n))) }), _jsxs("div", { className: c.chips, children: [customAmount && (_jsx("input", { inputMode: "numeric", value: amount, disabled: creating, onChange: (e) => pick({ type: "typed", text: e.target.value }), onKeyDown: (e) => {
80
+ if (e.key === "Enter")
81
+ confirm();
82
+ }, placeholder: "Any amount", "aria-label": "Amount in sats", className: c.input })), _jsx("button", { type: "button", disabled: creating || !sats, onClick: confirm, className: primary, children: "Create invoice" })] }), creating && _jsx("p", { className: c.status, children: "Making a Lightning invoice\u2026" }), state.phase === "idle" && state.message && _jsx("p", { className: c.error, children: state.message })] })), state.phase === "awaiting" && (_jsxs("div", { className: c.invoice, children: [_jsxs("p", { className: c.status, children: ["Invoice for ", formatSats(state.invoice.sats), " sats"] }), state.invoice.bolt11 && _jsx("div", { className: c.bolt11, children: state.invoice.bolt11 }), _jsxs("div", { className: c.chips, children: [state.invoice.checkoutLink && (_jsx("a", { href: state.invoice.checkoutLink, target: "_blank", rel: "noreferrer", className: primary, children: "Open checkout" })), state.invoice.bolt11 && (_jsx("button", { type: "button", onClick: () => void copy(state.invoice.bolt11), className: c.chip, children: copied ? "Copied" : "Copy invoice" })), _jsx("button", { type: "button", disabled: state.checking, onClick: check, className: c.chip, children: state.checking ? "Checking…" : "I've paid — check" }), _jsx("button", { type: "button", onClick: cancel, className: c.chip, children: "Cancel" })] }), _jsx("p", { className: c.status, "aria-live": "polite", children: state.status ?? "Waiting for payment." })] })), state.phase === "settled" && (_jsxs(_Fragment, { children: [_jsxs("p", { className: c.status, "aria-live": "polite", children: ["Paid. ", formatSats(state.credited), " sats added."] }), _jsx("div", { className: c.chips, children: _jsx("button", { type: "button", onClick: () => {
83
+ pick({ type: "clear" });
84
+ reset();
85
+ }, className: c.chip, children: "Done" }) })] })), state.phase === "failed" && (_jsxs(_Fragment, { children: [_jsx("p", { className: c.error, children: state.message }), _jsx("div", { className: c.chips, children: _jsx("button", { type: "button", onClick: reset, className: c.chip, children: "Try again" }) })] }))] }));
76
86
  }
77
87
  function Tranches({ tranches, formatDate, c, }) {
78
88
  return (_jsxs("section", { className: c.section, children: [_jsx("div", { className: c.sectionTitle, children: "Credit tranches" }), _jsx("ul", { className: c.list, children: tranches.map((t) => (_jsxs("li", { className: c.row, children: [_jsxs("span", { children: [formatSats(t.remaining_sats), " / ", formatSats(t.amount_sats), " sats"] }), " ", _jsx("span", { children: t.expires_at ? `expires ${formatDate(t.expires_at)}` : "no expiry" })] }, t.id))) })] }));
@@ -10,7 +10,7 @@ export { default as QuoteScroller, type QuoteScrollerProps, type QuoteScrollerCl
10
10
  export { default as SessionKeyClaim } from "./SessionKeyClaim.tsx";
11
11
  export { default as TableFilter, type DateFieldOption, type FilterQuestion, type TableFilterProps, type TableFilterClassNames, type TableFilterDates, type TableFilterQuestions, type TableFilterSearch, } from "./TableFilter.tsx";
12
12
  export { default as ThemeToggle, type ThemeToggleProps, type ThemeToggleClassNames } from "./ThemeToggle.tsx";
13
- export { default as WalletCard } from "./WalletCard.tsx";
13
+ export { default as WalletCard, type WalletCardProps, type WalletCardClassNames } from "./WalletCard.tsx";
14
14
  export { default as WalletPage, type WalletPageProps, type WalletPageClassNames } from "./WalletPage.tsx";
15
15
  export { useSession, type Session } from "./useSession.ts";
16
16
  export { useDebugLog } from "./useDebugLog.ts";
@@ -1,7 +1,12 @@
1
1
  /**
2
2
  * Typed wrappers for the tools every operator gets from
3
3
  * `register_standard_tools` in the tollbooth-dpyc wheel.
4
+ *
5
+ * Every wrapper takes the same optional `CallOptions` as `callTool`, last.
6
+ * A wrapper that is best-effort by default stays so unless the caller says
7
+ * otherwise; what the caller passes wins.
4
8
  */
9
+ import { type CallOptions } from "./client.ts";
5
10
  /** What a patron must supply beyond the npub proof, per the wheel's `patron_auth_block`. */
6
11
  export interface PatronAuth {
7
12
  mode: "oauth" | "secure_courier" | "none";
@@ -42,7 +47,7 @@ export interface ServiceStatus {
42
47
  lifecycle?: string;
43
48
  message?: string;
44
49
  }
45
- export declare function serviceStatus(): Promise<ServiceStatus>;
50
+ export declare function serviceStatus(opts?: CallOptions): Promise<ServiceStatus>;
46
51
  export interface NpubProofResult {
47
52
  success?: boolean;
48
53
  proven_npub?: string;
@@ -62,12 +67,12 @@ export interface NpubProofResult {
62
67
  * in the DM is the one on their own screen; an impostor firing the same tool
63
68
  * from elsewhere cannot make the patron's open tab show the attacker's code.
64
69
  */
65
- export declare function requestNpubProof(patronNpub: string, verifyAt?: string, reason?: string): Promise<NpubProofResult>;
70
+ export declare function requestNpubProof(patronNpub: string, verifyAt?: string, reason?: string, opts?: CallOptions): Promise<NpubProofResult>;
66
71
  /**
67
72
  * Step 2: drain DMs for the signed reply to step 1. Call only after the human
68
73
  * says they replied — it is destructive, so never poll it.
69
74
  */
70
- export declare function receiveNpubProof(patronNpub: string, dpopToken: string): Promise<NpubProofResult>;
75
+ export declare function receiveNpubProof(patronNpub: string, dpopToken: string, opts?: CallOptions): Promise<NpubProofResult>;
71
76
  export interface ProofStatusResult {
72
77
  success?: boolean;
73
78
  status?: "valid" | "expired" | "unknown" | string;
@@ -75,7 +80,7 @@ export interface ProofStatusResult {
75
80
  error?: string;
76
81
  error_code?: string;
77
82
  }
78
- export declare function checkProofStatus(patronNpub: string, dpopToken: string): Promise<ProofStatusResult>;
83
+ export declare function checkProofStatus(patronNpub: string, dpopToken: string, opts?: CallOptions): Promise<ProofStatusResult>;
79
84
  /** One credit tranche: a purchase (or grant) and what is left of it. */
80
85
  export interface CreditTranche {
81
86
  id: string;
@@ -116,7 +121,7 @@ export interface CheckBalanceResult {
116
121
  error?: string;
117
122
  error_code?: string;
118
123
  }
119
- export declare function checkBalance(): Promise<CheckBalanceResult>;
124
+ export declare function checkBalance(opts?: CallOptions): Promise<CheckBalanceResult>;
120
125
  export interface ConstraintEffect {
121
126
  type: string;
122
127
  message?: string;
@@ -148,7 +153,7 @@ export interface CheckPriceResult {
148
153
  * them (ad valorem, or a multiplier on a categorical argument). Null when
149
154
  * unreadable: show the answer without a price rather than invent one.
150
155
  */
151
- export declare function checkPrice(toolId: string, toolKwargs?: Record<string, unknown>): Promise<number | null>;
156
+ export declare function checkPrice(toolId: string, toolKwargs?: Record<string, unknown>, opts?: CallOptions): Promise<number | null>;
152
157
  export interface PurchaseCreditsResult {
153
158
  success?: boolean;
154
159
  invoice_id?: string;
@@ -160,7 +165,7 @@ export interface PurchaseCreditsResult {
160
165
  error?: string;
161
166
  error_code?: string;
162
167
  }
163
- export declare function purchaseCredits(sats: number): Promise<PurchaseCreditsResult>;
168
+ export declare function purchaseCredits(sats: number, opts?: CallOptions): Promise<PurchaseCreditsResult>;
164
169
  export interface CheckPaymentResult {
165
170
  success?: boolean;
166
171
  status?: "New" | "Processing" | "Settled" | "Expired" | "Invalid" | string;
@@ -175,7 +180,7 @@ export interface CheckPaymentResult {
175
180
  error?: string;
176
181
  error_code?: string;
177
182
  }
178
- export declare function checkPayment(invoiceId: string): Promise<CheckPaymentResult>;
183
+ export declare function checkPayment(invoiceId: string, opts?: CallOptions): Promise<CheckPaymentResult>;
179
184
  export interface StatementInvoice {
180
185
  invoice_id: string;
181
186
  status: string;
@@ -224,7 +229,7 @@ export interface AccountStatementResult {
224
229
  error?: string;
225
230
  error_code?: string;
226
231
  }
227
- export declare function getAccountStatement(days?: number): Promise<AccountStatementResult>;
232
+ export declare function getAccountStatement(days?: number, opts?: CallOptions): Promise<AccountStatementResult>;
228
233
  export interface PatronCoupon {
229
234
  coupon_id: string;
230
235
  name: string;
@@ -264,11 +269,11 @@ export interface ForgetCouponResult {
264
269
  error_code?: string;
265
270
  }
266
271
  /** The coupons this patron has redeemed here. Free. */
267
- export declare function listMyCoupons(): Promise<ListMyCouponsResult>;
272
+ export declare function listMyCoupons(opts?: CallOptions): Promise<ListMyCouponsResult>;
268
273
  /** Redeem an operator's code once; the discount then applies on its own. Free. */
269
- export declare function redeemCoupon(code: string): Promise<RedeemCouponResult>;
274
+ export declare function redeemCoupon(code: string, opts?: CallOptions): Promise<RedeemCouponResult>;
270
275
  /** Take a coupon off the patron's list. The code stays re-redeemable while its window allows. */
271
- export declare function forgetCoupon(couponId: string): Promise<ForgetCouponResult>;
276
+ export declare function forgetCoupon(couponId: string, opts?: CallOptions): Promise<ForgetCouponResult>;
272
277
  export interface Kind0 {
273
278
  name?: string;
274
279
  display_name?: string;
@@ -285,7 +290,7 @@ export interface GetNostrProfileResult {
285
290
  profile?: Kind0;
286
291
  error?: string;
287
292
  }
288
- export declare function getNostrProfile(npub: string): Promise<GetNostrProfileResult>;
293
+ export declare function getNostrProfile(npub: string, opts?: CallOptions): Promise<GetNostrProfileResult>;
289
294
  export interface PublishNostrProfileResult {
290
295
  success: boolean;
291
296
  ok?: number;
@@ -294,7 +299,7 @@ export interface PublishNostrProfileResult {
294
299
  error?: string;
295
300
  }
296
301
  /** Relay a kind-0 the BROWSER signed. The wheel verifies it and fans it out. */
297
- export declare function publishNostrProfile(npub: string, signedEvent: string): Promise<PublishNostrProfileResult>;
302
+ export declare function publishNostrProfile(npub: string, signedEvent: string, opts?: CallOptions): Promise<PublishNostrProfileResult>;
298
303
  /** Every lifecycle `session_status` reports. */
299
304
  export type SessionLifecycle = "ready" | "warming_up" | "misconfigured" | "quota_exceeded" | "not_registered" | "no_identity";
300
305
  /** The patron's stored upstream OAuth token, when the operator has a provider. */
@@ -325,7 +330,7 @@ export interface SessionStatusResult {
325
330
  * Is the operator ready to serve? Free, no proof. With `patronNpub`, also the
326
331
  * patron's upstream OAuth token expiry, so a page can refresh ahead of time.
327
332
  */
328
- export declare function sessionStatus(patronNpub?: string): Promise<SessionStatusResult>;
333
+ export declare function sessionStatus(patronNpub?: string, opts?: CallOptions): Promise<SessionStatusResult>;
329
334
  export interface OnboardingItem {
330
335
  field: string;
331
336
  /** "identity" | "authority" | "secret" */
@@ -353,12 +358,12 @@ export interface OperatorOnboardingStatus {
353
358
  error?: string;
354
359
  error_code?: string;
355
360
  }
356
- export declare function getOperatorOnboardingStatus(): Promise<OperatorOnboardingStatus>;
361
+ export declare function getOperatorOnboardingStatus(opts?: CallOptions): Promise<OperatorOnboardingStatus>;
357
362
  /**
358
363
  * The operator's own balance at its Authority — what certifies patron top-ups.
359
364
  * The Authority's ledger, in the same shape as a patron's `check_balance`.
360
365
  */
361
- export declare function checkAuthorityBalance(): Promise<CheckBalanceResult>;
366
+ export declare function checkAuthorityBalance(opts?: CallOptions): Promise<CheckBalanceResult>;
362
367
  export interface PricingStep {
363
368
  id: string;
364
369
  type: string;
@@ -395,7 +400,7 @@ export interface PricingModelResult {
395
400
  error?: string;
396
401
  }
397
402
  /** The operator's active pricing model. Free, no proof. */
398
- export declare function getPricingModel(): Promise<PricingModelResult>;
403
+ export declare function getPricingModel(opts?: CallOptions): Promise<PricingModelResult>;
399
404
  export interface CanonicalIdentity {
400
405
  tool_id: string;
401
406
  mcp_name: string;
@@ -419,7 +424,7 @@ export interface CanonicalIdentitiesResult {
419
424
  }>;
420
425
  error?: string;
421
426
  }
422
- export declare function listCanonicalIdentities(): Promise<CanonicalIdentitiesResult>;
427
+ export declare function listCanonicalIdentities(opts?: CallOptions): Promise<CanonicalIdentitiesResult>;
423
428
  export interface PatronCredentialFieldsResult {
424
429
  success?: boolean;
425
430
  fields?: string[];
@@ -436,8 +441,8 @@ export interface PatronCredentialWriteResult {
436
441
  error_code?: string;
437
442
  }
438
443
  /** The names of the patron's stored credential fields — never their values. */
439
- export declare function getPatronCredentialFields(): Promise<PatronCredentialFieldsResult>;
444
+ export declare function getPatronCredentialFields(opts?: CallOptions): Promise<PatronCredentialFieldsResult>;
440
445
  /** Set one credential field, leaving the others as they are. */
441
- export declare function updatePatronCredential(field: string, value: string): Promise<PatronCredentialWriteResult>;
446
+ export declare function updatePatronCredential(field: string, value: string, opts?: CallOptions): Promise<PatronCredentialWriteResult>;
442
447
  /** Remove one credential field, leaving the others as they are. */
443
- export declare function deletePatronCredential(field: string): Promise<PatronCredentialWriteResult>;
448
+ export declare function deletePatronCredential(field: string, opts?: CallOptions): Promise<PatronCredentialWriteResult>;
@@ -1,10 +1,14 @@
1
1
  /**
2
2
  * Typed wrappers for the tools every operator gets from
3
3
  * `register_standard_tools` in the tollbooth-dpyc wheel.
4
+ *
5
+ * Every wrapper takes the same optional `CallOptions` as `callTool`, last.
6
+ * A wrapper that is best-effort by default stays so unless the caller says
7
+ * otherwise; what the caller passes wins.
4
8
  */
5
9
  import { callTool } from "./client.js";
6
- export function serviceStatus() {
7
- return callTool("service_status", {}, { bestEffort: true });
10
+ export function serviceStatus(opts) {
11
+ return callTool("service_status", {}, { bestEffort: true, ...opts });
8
12
  }
9
13
  /**
10
14
  * Step 1 of DM sign-in: a Secure Courier challenge DM to the npub. Free.
@@ -14,28 +18,25 @@ export function serviceStatus() {
14
18
  * in the DM is the one on their own screen; an impostor firing the same tool
15
19
  * from elsewhere cannot make the patron's open tab show the attacker's code.
16
20
  */
17
- export function requestNpubProof(patronNpub, verifyAt, reason) {
21
+ export function requestNpubProof(patronNpub, verifyAt, reason, opts) {
18
22
  return callTool("request_npub_proof", {
19
23
  patron_npub: patronNpub,
20
24
  ...(verifyAt ? { verify_at: verifyAt } : {}),
21
25
  ...(reason ? { reason } : {}),
22
- });
26
+ }, opts);
23
27
  }
24
28
  /**
25
29
  * Step 2: drain DMs for the signed reply to step 1. Call only after the human
26
30
  * says they replied — it is destructive, so never poll it.
27
31
  */
28
- export function receiveNpubProof(patronNpub, dpopToken) {
29
- return callTool("receive_npub_proof", {
30
- patron_npub: patronNpub,
31
- dpop_token: dpopToken,
32
- });
32
+ export function receiveNpubProof(patronNpub, dpopToken, opts) {
33
+ return callTool("receive_npub_proof", { patron_npub: patronNpub, dpop_token: dpopToken }, opts);
33
34
  }
34
- export function checkProofStatus(patronNpub, dpopToken) {
35
- return callTool("check_proof_status", { patron_npub: patronNpub, dpop_token: dpopToken }, { bestEffort: true });
35
+ export function checkProofStatus(patronNpub, dpopToken, opts) {
36
+ return callTool("check_proof_status", { patron_npub: patronNpub, dpop_token: dpopToken }, { bestEffort: true, ...opts });
36
37
  }
37
- export function checkBalance() {
38
- return callTool("check_balance", {});
38
+ export function checkBalance(opts) {
39
+ return callTool("check_balance", {}, opts);
39
40
  }
40
41
  /**
41
42
  * The fare for one tool in api_sats, as the live pricing model resolves it,
@@ -44,79 +45,73 @@ export function checkBalance() {
44
45
  * them (ad valorem, or a multiplier on a categorical argument). Null when
45
46
  * unreadable: show the answer without a price rather than invent one.
46
47
  */
47
- export async function checkPrice(toolId, toolKwargs) {
48
- const r = await callTool("check_price", {
49
- tool_id: toolId,
50
- ...(toolKwargs ? { tool_kwargs: JSON.stringify(toolKwargs) } : {}),
51
- });
48
+ export async function checkPrice(toolId, toolKwargs, opts) {
49
+ const r = await callTool("check_price", { tool_id: toolId, ...(toolKwargs ? { tool_kwargs: JSON.stringify(toolKwargs) } : {}) }, opts);
52
50
  const v = r.effective_cost_api_sats ?? r.base_cost_api_sats;
53
51
  return typeof v === "number" ? v : null;
54
52
  }
55
- export function purchaseCredits(sats) {
56
- return callTool("purchase_credits", { amount_sats: sats });
53
+ export function purchaseCredits(sats, opts) {
54
+ return callTool("purchase_credits", { amount_sats: sats }, opts);
57
55
  }
58
- export function checkPayment(invoiceId) {
59
- return callTool("check_payment", { invoice_id: invoiceId });
56
+ export function checkPayment(invoiceId, opts) {
57
+ return callTool("check_payment", { invoice_id: invoiceId }, opts);
60
58
  }
61
- export function getAccountStatement(days = 30) {
62
- return callTool("account_statement", { days });
59
+ export function getAccountStatement(days = 30, opts) {
60
+ return callTool("account_statement", { days }, opts);
63
61
  }
64
62
  /** The coupons this patron has redeemed here. Free. */
65
- export function listMyCoupons() {
66
- return callTool("list_my_coupons", {});
63
+ export function listMyCoupons(opts) {
64
+ return callTool("list_my_coupons", {}, opts);
67
65
  }
68
66
  /** Redeem an operator's code once; the discount then applies on its own. Free. */
69
- export function redeemCoupon(code) {
70
- return callTool("redeem_coupon", { code });
67
+ export function redeemCoupon(code, opts) {
68
+ return callTool("redeem_coupon", { code }, opts);
71
69
  }
72
70
  /** Take a coupon off the patron's list. The code stays re-redeemable while its window allows. */
73
- export function forgetCoupon(couponId) {
74
- return callTool("forget_coupon", { coupon_id: couponId });
71
+ export function forgetCoupon(couponId, opts) {
72
+ return callTool("forget_coupon", { coupon_id: couponId }, opts);
75
73
  }
76
- export function getNostrProfile(npub) {
77
- return callTool("get_nostr_profile", { npub });
74
+ export function getNostrProfile(npub, opts) {
75
+ return callTool("get_nostr_profile", { npub }, opts);
78
76
  }
79
77
  /** Relay a kind-0 the BROWSER signed. The wheel verifies it and fans it out. */
80
- export function publishNostrProfile(npub, signedEvent) {
81
- return callTool("publish_nostr_profile", {
82
- npub,
83
- signed_event: signedEvent,
84
- });
78
+ export function publishNostrProfile(npub, signedEvent, opts) {
79
+ return callTool("publish_nostr_profile", { npub, signed_event: signedEvent }, opts);
85
80
  }
86
81
  /**
87
82
  * Is the operator ready to serve? Free, no proof. With `patronNpub`, also the
88
83
  * patron's upstream OAuth token expiry, so a page can refresh ahead of time.
89
84
  */
90
- export function sessionStatus(patronNpub) {
91
- return callTool("session_status", patronNpub ? { patron_npub: patronNpub } : {}, { bestEffort: true });
85
+ export function sessionStatus(patronNpub, opts) {
86
+ return callTool("session_status", patronNpub ? { patron_npub: patronNpub } : {}, { bestEffort: true, ...opts });
92
87
  }
93
- export function getOperatorOnboardingStatus() {
94
- return callTool("get_operator_onboarding_status", {}, { bestEffort: true });
88
+ export function getOperatorOnboardingStatus(opts) {
89
+ return callTool("get_operator_onboarding_status", {}, { bestEffort: true, ...opts });
95
90
  }
96
91
  /**
97
92
  * The operator's own balance at its Authority — what certifies patron top-ups.
98
93
  * The Authority's ledger, in the same shape as a patron's `check_balance`.
99
94
  */
100
- export function checkAuthorityBalance() {
101
- return callTool("check_authority_balance", {}, { bestEffort: true });
95
+ export function checkAuthorityBalance(opts) {
96
+ return callTool("check_authority_balance", {}, { bestEffort: true, ...opts });
102
97
  }
103
98
  /** The operator's active pricing model. Free, no proof. */
104
- export function getPricingModel() {
105
- return callTool("get_pricing_model", {});
99
+ export function getPricingModel(opts) {
100
+ return callTool("get_pricing_model", {}, opts);
106
101
  }
107
- export function listCanonicalIdentities() {
108
- return callTool("list_canonical_identities", {});
102
+ export function listCanonicalIdentities(opts) {
103
+ return callTool("list_canonical_identities", {}, opts);
109
104
  }
110
105
  /** The names of the patron's stored credential fields — never their values. */
111
- export function getPatronCredentialFields() {
112
- return callTool("get_patron_credential_fields", {});
106
+ export function getPatronCredentialFields(opts) {
107
+ return callTool("get_patron_credential_fields", {}, opts);
113
108
  }
114
109
  /** Set one credential field, leaving the others as they are. */
115
- export function updatePatronCredential(field, value) {
110
+ export function updatePatronCredential(field, value, opts) {
116
111
  // `field` first: the log's scrubber keys on it to hide `value`.
117
- return callTool("update_patron_credential", { field, value });
112
+ return callTool("update_patron_credential", { field, value }, opts);
118
113
  }
119
114
  /** Remove one credential field, leaving the others as they are. */
120
- export function deletePatronCredential(field) {
121
- return callTool("delete_patron_credential", { field });
115
+ export function deletePatronCredential(field, opts) {
116
+ return callTool("delete_patron_credential", { field }, opts);
122
117
  }
package/dist/table.d.ts CHANGED
@@ -21,6 +21,16 @@ export declare function nextSort(col: string, activeCol: string, dir: SortDir, i
21
21
  col: string;
22
22
  dir: SortDir;
23
23
  };
24
+ /** The `aria-sort` of a column header: set on the sorted column only. */
25
+ export declare function ariaSort(col: string, activeCol: string, dir: SortDir): "ascending" | "descending" | undefined;
26
+ /**
27
+ * How far to slide a popover sideways so it stays inside the viewport: the
28
+ * horizontal offset (px, negative is left) to add to where it would sit.
29
+ * `left` and `width` are its box as laid out before any slide. It keeps
30
+ * `margin` px from each edge; one wider than the room left is pinned to the
31
+ * left margin (give it a max-width to fit). Zero when it already fits.
32
+ */
33
+ export declare function nudgeIntoView(left: number, width: number, viewportWidth: number, margin?: number): number;
24
34
  /**
25
35
  * A case-insensitive test for `pattern`. It is read as a regular expression
26
36
  * when it is one, and as plain text when it is not, so a stray "(" in a search
package/dist/table.js CHANGED
@@ -35,6 +35,29 @@ export function nextSort(col, activeCol, dir, initial = "desc") {
35
35
  return { col, dir: dir === "desc" ? "asc" : "desc" };
36
36
  return { col, dir: initial };
37
37
  }
38
+ /** The `aria-sort` of a column header: set on the sorted column only. */
39
+ export function ariaSort(col, activeCol, dir) {
40
+ if (col !== activeCol)
41
+ return undefined;
42
+ return dir === "desc" ? "descending" : "ascending";
43
+ }
44
+ // ── A popover kept on screen ───────────────────────────────────────────────
45
+ /**
46
+ * How far to slide a popover sideways so it stays inside the viewport: the
47
+ * horizontal offset (px, negative is left) to add to where it would sit.
48
+ * `left` and `width` are its box as laid out before any slide. It keeps
49
+ * `margin` px from each edge; one wider than the room left is pinned to the
50
+ * left margin (give it a max-width to fit). Zero when it already fits.
51
+ */
52
+ export function nudgeIntoView(left, width, viewportWidth, margin = 8) {
53
+ const minLeft = margin;
54
+ const maxLeft = viewportWidth - margin - width;
55
+ if (maxLeft < minLeft || left < minLeft)
56
+ return minLeft - left;
57
+ if (left > maxLeft)
58
+ return maxLeft - left;
59
+ return 0;
60
+ }
38
61
  // ── Filtering ──────────────────────────────────────────────────────────────
39
62
  /**
40
63
  * A case-insensitive test for `pattern`. It is read as a regular expression
package/dist/wallet.d.ts CHANGED
@@ -66,6 +66,24 @@ export declare function paymentPhase(status: string | undefined | null): Payment
66
66
  export declare function invoiceFrom(result: PurchaseCreditsResult, asked: number): Invoice | null;
67
67
  /** A whole number of sats worth invoicing, from what the patron typed; else null. */
68
68
  export declare function parseSats(input: string | number): number | null;
69
+ /**
70
+ * The amount box. Tapping a preset only fills it; an invoice is made only by
71
+ * the explicit "Create invoice" action, which reads `parseSats(text)`. A tap is
72
+ * a choice, not a purchase — a patron who taps the wrong chip changes it
73
+ * before anything is asked of the service.
74
+ */
75
+ export type AmountEvent = {
76
+ type: "preset";
77
+ sats: number;
78
+ } | {
79
+ type: "typed";
80
+ text: string;
81
+ } | {
82
+ type: "clear";
83
+ };
84
+ export declare function amountReducer(text: string, event: AmountEvent): string;
85
+ /** Whether preset `sats` is what the amount box holds — the chip to mark as chosen. */
86
+ export declare function presetChosen(text: string, sats: number): boolean;
69
87
  export declare function topUpReducer(state: TopUpState, event: TopUpEvent): TopUpState;
70
88
  /** Whether a poll should run now: an invoice is out, nothing is in flight, and the budget is not spent. */
71
89
  export declare function shouldPoll(state: TopUpState, maxChecks: number): boolean;
package/dist/wallet.js CHANGED
@@ -38,6 +38,20 @@ export function parseSats(input) {
38
38
  const n = typeof input === "number" ? input : Number(String(input).replace(/[\s,_]/g, ""));
39
39
  return Number.isSafeInteger(n) && n > 0 ? n : null;
40
40
  }
41
+ export function amountReducer(text, event) {
42
+ switch (event.type) {
43
+ case "preset":
44
+ return parseSats(event.sats) === null ? text : String(event.sats);
45
+ case "typed":
46
+ return event.text.replace(/[^\d]/g, "");
47
+ case "clear":
48
+ return "";
49
+ }
50
+ }
51
+ /** Whether preset `sats` is what the amount box holds — the chip to mark as chosen. */
52
+ export function presetChosen(text, sats) {
53
+ return parseSats(text) === sats;
54
+ }
41
55
  export function topUpReducer(state, event) {
42
56
  switch (event.type) {
43
57
  case "create":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tollbooth-dpyc/web",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Shared browser SDK for Tollbooth DPYC operator front ends: npub sign-in, proofs, MCP client, profile and wallet",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Lonnie VanZandt",