@tollbooth-dpyc/web 1.1.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 +11 -1
- package/dist/client.js +1 -0
- package/dist/debugLog.js +5 -2
- package/dist/inlineProof.d.ts +8 -0
- package/dist/inlineProof.js +15 -1
- package/dist/react/CouponsPanel.d.ts +11 -2
- package/dist/react/CouponsPanel.js +6 -5
- package/dist/react/PagedTable.d.ts +7 -2
- package/dist/react/PagedTable.js +8 -5
- package/dist/react/TableFilter.d.ts +10 -2
- package/dist/react/TableFilter.js +38 -10
- package/dist/react/WalletCard.d.ts +15 -3
- package/dist/react/WalletCard.js +10 -5
- package/dist/react/WalletPage.d.ts +11 -3
- package/dist/react/WalletPage.js +19 -9
- package/dist/react/index.d.ts +1 -1
- package/dist/react/useSession.d.ts +2 -0
- package/dist/react/useSession.js +2 -1
- package/dist/standardTools.d.ts +233 -22
- package/dist/standardTools.js +73 -36
- package/dist/table.d.ts +10 -0
- package/dist/table.js +23 -0
- package/dist/wallet.d.ts +18 -0
- package/dist/wallet.js +14 -0
- package/package.json +1 -1
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.js
CHANGED
package/dist/debugLog.js
CHANGED
|
@@ -28,9 +28,12 @@ const REDACTED = "[redacted]";
|
|
|
28
28
|
const SECRET_FIELD = "(?:nsec|ncryptsec|dpop_token|proof|poison|password|passphrase|secret|token|authorization|api_?key|private_?key|client_secret|[a-z0-9_]*_(?:nsec|token|secret|password|passphrase|api_?key|private_?key))";
|
|
29
29
|
// `"dpop_token":"…"`, `\"dpop_token\":\"…\"` (JSON inside a JSON string),
|
|
30
30
|
// `dpop_token=…` (a query string) and `dpop_token: '…'` (a Python repr). A
|
|
31
|
-
// quoted value is taken whole
|
|
31
|
+
// quoted value is taken whole — spaces, escaped quotes and the other kind of
|
|
32
|
+
// quote included — up to its own closing quote or the end of a truncated line;
|
|
33
|
+
// a bare one up to a delimiter. Stopping at the first quote of either kind let
|
|
34
|
+
// the tail of a secret holding `'` or `\"` through.
|
|
32
35
|
function fieldValue(names) {
|
|
33
|
-
return new RegExp(`((?:\\\\*["'])?\\b${names}(?:\\\\*["'])?\\s*[:=]\\s*)(?:(\\\\*["'])[
|
|
36
|
+
return new RegExp(`((?:\\\\*["'])?\\b${names}(?:\\\\*["'])?\\s*[:=]\\s*)(?:(\\\\*["'])(?:\\\\.|\\\\$|(?!\\2)[^\\\\])*?(?=\\2|$)|[^"'\\\\,}\\]&\\s]+)`, "gi");
|
|
34
37
|
}
|
|
35
38
|
const FIELD_VALUE = fieldValue(SECRET_FIELD);
|
|
36
39
|
// update_patron_credential / update_operator_credential carry the secret as
|
package/dist/inlineProof.d.ts
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
* runtime tool name, e.g. "chart_snapshot_display"), and `created_at` (within
|
|
6
6
|
* about 60 seconds of server time). The wheel verifies the Schnorr signature
|
|
7
7
|
* inline — no relay round trip, no cached phrase.
|
|
8
|
+
*
|
|
9
|
+
* Every proof also carries a random `nonce` tag, exactly as the wheel's own
|
|
10
|
+
* `create_proof` does. Without it, two calls to the same tool in the same
|
|
11
|
+
* wall-clock second sign byte-identical events, their ids collide, and the
|
|
12
|
+
* verifier's replay guard refuses the second. The nonce is signed but inert:
|
|
13
|
+
* `verify_proof` reads only the `u` tag.
|
|
8
14
|
*/
|
|
15
|
+
/** 16 random bytes as 32 lowercase hex chars — Python's `secrets.token_hex(16)`. */
|
|
16
|
+
export declare function proofNonce(): string;
|
|
9
17
|
/** The JSON-stringified signed event, exactly what `verify_proof` expects. */
|
|
10
18
|
export declare function signInlineProof(runtimeToolName: string, secretKey?: Uint8Array): string;
|
package/dist/inlineProof.js
CHANGED
|
@@ -5,16 +5,30 @@
|
|
|
5
5
|
* runtime tool name, e.g. "chart_snapshot_display"), and `created_at` (within
|
|
6
6
|
* about 60 seconds of server time). The wheel verifies the Schnorr signature
|
|
7
7
|
* inline — no relay round trip, no cached phrase.
|
|
8
|
+
*
|
|
9
|
+
* Every proof also carries a random `nonce` tag, exactly as the wheel's own
|
|
10
|
+
* `create_proof` does. Without it, two calls to the same tool in the same
|
|
11
|
+
* wall-clock second sign byte-identical events, their ids collide, and the
|
|
12
|
+
* verifier's replay guard refuses the second. The nonce is signed but inert:
|
|
13
|
+
* `verify_proof` reads only the `u` tag.
|
|
8
14
|
*/
|
|
9
15
|
import { finalizeEvent } from "nostr-tools";
|
|
10
16
|
import { getSessionNsecBytes } from "./sessionNsec.js";
|
|
11
17
|
const PROOF_KIND = 27235;
|
|
18
|
+
/** 16 random bytes as 32 lowercase hex chars — Python's `secrets.token_hex(16)`. */
|
|
19
|
+
export function proofNonce() {
|
|
20
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
21
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
22
|
+
}
|
|
12
23
|
/** The JSON-stringified signed event, exactly what `verify_proof` expects. */
|
|
13
24
|
export function signInlineProof(runtimeToolName, secretKey) {
|
|
14
25
|
const signed = finalizeEvent({
|
|
15
26
|
kind: PROOF_KIND,
|
|
16
27
|
created_at: Math.floor(Date.now() / 1000),
|
|
17
|
-
tags: [
|
|
28
|
+
tags: [
|
|
29
|
+
["u", runtimeToolName],
|
|
30
|
+
["nonce", proofNonce()],
|
|
31
|
+
],
|
|
18
32
|
content: "",
|
|
19
33
|
}, secretKey ?? getSessionNsecBytes());
|
|
20
34
|
return JSON.stringify(signed);
|
|
@@ -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. */
|
package/dist/react/PagedTable.js
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
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(
|
|
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
|
-
|
|
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 && (
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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
|
-
|
|
24
|
+
classNames?: WalletCardClassNames;
|
|
25
|
+
}
|
|
26
|
+
export default function WalletCard({ topUps, classNames }: WalletCardProps): import("react").JSX.Element;
|
package/dist/react/WalletCard.js
CHANGED
|
@@ -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
|
-
|
|
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 ? (
|
|
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`)
|
|
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
|
-
/**
|
|
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;
|
package/dist/react/WalletPage.js
CHANGED
|
@@ -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`)
|
|
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 [
|
|
60
|
+
const [amount, pick] = useReducer(amountReducer, "");
|
|
59
61
|
const [copied, setCopied] = useState(false);
|
|
60
|
-
const
|
|
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: () =>
|
|
73
|
-
if (e.key === "Enter"
|
|
74
|
-
|
|
75
|
-
}, placeholder: "Any amount", "aria-label": "Amount in sats", className: c.input }), _jsx("button", { type: "button", disabled: creating || !
|
|
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))) })] }));
|
package/dist/react/index.d.ts
CHANGED
|
@@ -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";
|
|
@@ -12,6 +12,8 @@ export interface Session {
|
|
|
12
12
|
canSign: boolean;
|
|
13
13
|
/** Set when a cached proof lapses: a calm prompt to sign in again. */
|
|
14
14
|
notice: string;
|
|
15
|
+
/** Clear the notice without signing in — the patron read it and moved on. */
|
|
16
|
+
dismissNotice: () => void;
|
|
15
17
|
/** Call after a successful sign-in so the whole app notices. */
|
|
16
18
|
refresh: () => void;
|
|
17
19
|
signOut: () => void;
|
package/dist/react/useSession.js
CHANGED
|
@@ -28,6 +28,7 @@ export function useSession() {
|
|
|
28
28
|
setState(read());
|
|
29
29
|
setNotice("");
|
|
30
30
|
}, []);
|
|
31
|
+
const dismissNotice = useCallback(() => setNotice(""), []);
|
|
31
32
|
useEffect(() => onProofExpired(() => {
|
|
32
33
|
// A tab that can sign for THIS npub lost nothing.
|
|
33
34
|
if (canSignFor(currentClaim()))
|
|
@@ -35,5 +36,5 @@ export function useSession() {
|
|
|
35
36
|
setNotice("Your session lapsed while you were away. Sign in again to carry on.");
|
|
36
37
|
setState(read());
|
|
37
38
|
}), []);
|
|
38
|
-
return { ...state, notice, refresh, signOut };
|
|
39
|
+
return { ...state, notice, dismissNotice, refresh, signOut };
|
|
39
40
|
}
|
package/dist/standardTools.d.ts
CHANGED
|
@@ -1,17 +1,53 @@
|
|
|
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";
|
|
10
|
+
/** What a patron must supply beyond the npub proof, per the wheel's `patron_auth_block`. */
|
|
11
|
+
export interface PatronAuth {
|
|
12
|
+
mode: "oauth" | "secure_courier" | "none";
|
|
13
|
+
patron_credentials_required: boolean;
|
|
14
|
+
/** mode "oauth": the provider's credential service. */
|
|
15
|
+
oauth_service?: string;
|
|
16
|
+
/** mode "secure_courier": the service a patron delivers secrets to. */
|
|
17
|
+
credential_service?: string;
|
|
18
|
+
}
|
|
19
|
+
/** The wheel's `service_status`: health and build facts, free and unauthenticated. */
|
|
5
20
|
export interface ServiceStatus {
|
|
21
|
+
success?: boolean;
|
|
22
|
+
service?: string;
|
|
23
|
+
slug?: string;
|
|
24
|
+
version?: string;
|
|
25
|
+
tollbooth_dpyc_version?: string;
|
|
26
|
+
vault_configured?: boolean;
|
|
27
|
+
courier_has_vault?: boolean;
|
|
6
28
|
operator_npub_hash?: string;
|
|
29
|
+
process_id?: number;
|
|
30
|
+
/** Deploy metadata from the environment (commit, build time, …), lower-cased keys. */
|
|
31
|
+
build_info?: Record<string, string> | null;
|
|
32
|
+
patron_auth?: PatronAuth;
|
|
33
|
+
/** Only on a server that runs background jobs. */
|
|
34
|
+
async_jobs?: {
|
|
35
|
+
docket_url_set: boolean;
|
|
36
|
+
backend: string;
|
|
37
|
+
durable_across_recycles: boolean;
|
|
38
|
+
};
|
|
39
|
+
durable_jobs?: {
|
|
40
|
+
modal_app: string | null;
|
|
41
|
+
detached_executor_active: boolean;
|
|
42
|
+
detached_executor_resolved: boolean;
|
|
43
|
+
detached_executor_error: string | null;
|
|
44
|
+
last_dispatch_error: string | null;
|
|
45
|
+
dispatching: boolean;
|
|
46
|
+
};
|
|
7
47
|
lifecycle?: string;
|
|
8
48
|
message?: string;
|
|
9
|
-
version?: string;
|
|
10
|
-
tollbooth_dpyc_version?: string;
|
|
11
|
-
service?: string;
|
|
12
|
-
slug?: string;
|
|
13
49
|
}
|
|
14
|
-
export declare function serviceStatus(): Promise<ServiceStatus>;
|
|
50
|
+
export declare function serviceStatus(opts?: CallOptions): Promise<ServiceStatus>;
|
|
15
51
|
export interface NpubProofResult {
|
|
16
52
|
success?: boolean;
|
|
17
53
|
proven_npub?: string;
|
|
@@ -31,12 +67,12 @@ export interface NpubProofResult {
|
|
|
31
67
|
* in the DM is the one on their own screen; an impostor firing the same tool
|
|
32
68
|
* from elsewhere cannot make the patron's open tab show the attacker's code.
|
|
33
69
|
*/
|
|
34
|
-
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>;
|
|
35
71
|
/**
|
|
36
72
|
* Step 2: drain DMs for the signed reply to step 1. Call only after the human
|
|
37
73
|
* says they replied — it is destructive, so never poll it.
|
|
38
74
|
*/
|
|
39
|
-
export declare function receiveNpubProof(patronNpub: string, dpopToken: string): Promise<NpubProofResult>;
|
|
75
|
+
export declare function receiveNpubProof(patronNpub: string, dpopToken: string, opts?: CallOptions): Promise<NpubProofResult>;
|
|
40
76
|
export interface ProofStatusResult {
|
|
41
77
|
success?: boolean;
|
|
42
78
|
status?: "valid" | "expired" | "unknown" | string;
|
|
@@ -44,7 +80,7 @@ export interface ProofStatusResult {
|
|
|
44
80
|
error?: string;
|
|
45
81
|
error_code?: string;
|
|
46
82
|
}
|
|
47
|
-
export declare function checkProofStatus(patronNpub: string, dpopToken: string): Promise<ProofStatusResult>;
|
|
83
|
+
export declare function checkProofStatus(patronNpub: string, dpopToken: string, opts?: CallOptions): Promise<ProofStatusResult>;
|
|
48
84
|
/** One credit tranche: a purchase (or grant) and what is left of it. */
|
|
49
85
|
export interface CreditTranche {
|
|
50
86
|
id: string;
|
|
@@ -75,24 +111,49 @@ export interface CheckBalanceResult {
|
|
|
75
111
|
calls: number;
|
|
76
112
|
api_sats: number;
|
|
77
113
|
}>;
|
|
114
|
+
invoice_summary?: {
|
|
115
|
+
total_invoices: number;
|
|
116
|
+
settled_count: number;
|
|
117
|
+
pending_count: number;
|
|
118
|
+
total_real_sats: number;
|
|
119
|
+
total_api_sats_credited: number;
|
|
120
|
+
};
|
|
78
121
|
error?: string;
|
|
79
122
|
error_code?: string;
|
|
80
123
|
}
|
|
81
|
-
export declare function checkBalance(): Promise<CheckBalanceResult>;
|
|
124
|
+
export declare function checkBalance(opts?: CallOptions): Promise<CheckBalanceResult>;
|
|
125
|
+
export interface ConstraintEffect {
|
|
126
|
+
type: string;
|
|
127
|
+
message?: string;
|
|
128
|
+
[key: string]: unknown;
|
|
129
|
+
}
|
|
130
|
+
/** The wheel's `check_price` preview. */
|
|
82
131
|
export interface CheckPriceResult {
|
|
83
132
|
success: boolean;
|
|
84
133
|
tool_id?: string;
|
|
85
|
-
|
|
86
|
-
|
|
134
|
+
tool_name?: string;
|
|
135
|
+
/** "flat" | "flat+multipliers" | "percent" */
|
|
136
|
+
pricing_type?: string;
|
|
137
|
+
base_cost_api_sats?: number | null;
|
|
138
|
+
effective_cost_api_sats?: number | null;
|
|
139
|
+
rate_percent?: number;
|
|
140
|
+
rate_param?: string;
|
|
141
|
+
min_cost_sats?: number;
|
|
142
|
+
multipliers?: Record<string, unknown>;
|
|
143
|
+
constraints_enabled?: boolean;
|
|
144
|
+
constraint_effects?: ConstraintEffect[];
|
|
145
|
+
hint?: string;
|
|
87
146
|
error?: string;
|
|
88
147
|
error_code?: string;
|
|
89
148
|
}
|
|
90
149
|
/**
|
|
91
150
|
* The fare for one tool in api_sats, as the live pricing model resolves it,
|
|
92
|
-
* constraints included — keyed by the tool's frozen UUID
|
|
93
|
-
*
|
|
151
|
+
* constraints included — keyed by the tool's frozen UUID (or its capability
|
|
152
|
+
* name). `toolKwargs` are the call's own parameters, for a tool priced by
|
|
153
|
+
* them (ad valorem, or a multiplier on a categorical argument). Null when
|
|
154
|
+
* unreadable: show the answer without a price rather than invent one.
|
|
94
155
|
*/
|
|
95
|
-
export declare function checkPrice(toolId: string): Promise<number | null>;
|
|
156
|
+
export declare function checkPrice(toolId: string, toolKwargs?: Record<string, unknown>, opts?: CallOptions): Promise<number | null>;
|
|
96
157
|
export interface PurchaseCreditsResult {
|
|
97
158
|
success?: boolean;
|
|
98
159
|
invoice_id?: string;
|
|
@@ -104,7 +165,7 @@ export interface PurchaseCreditsResult {
|
|
|
104
165
|
error?: string;
|
|
105
166
|
error_code?: string;
|
|
106
167
|
}
|
|
107
|
-
export declare function purchaseCredits(sats: number): Promise<PurchaseCreditsResult>;
|
|
168
|
+
export declare function purchaseCredits(sats: number, opts?: CallOptions): Promise<PurchaseCreditsResult>;
|
|
108
169
|
export interface CheckPaymentResult {
|
|
109
170
|
success?: boolean;
|
|
110
171
|
status?: "New" | "Processing" | "Settled" | "Expired" | "Invalid" | string;
|
|
@@ -112,10 +173,14 @@ export interface CheckPaymentResult {
|
|
|
112
173
|
invoice_id?: string;
|
|
113
174
|
credits_granted?: number;
|
|
114
175
|
balance_api_sats?: number;
|
|
176
|
+
amount_sats?: number;
|
|
177
|
+
/** false: the credit could not be written; nothing was granted. */
|
|
178
|
+
persisted?: boolean;
|
|
179
|
+
source?: string;
|
|
115
180
|
error?: string;
|
|
116
181
|
error_code?: string;
|
|
117
182
|
}
|
|
118
|
-
export declare function checkPayment(invoiceId: string): Promise<CheckPaymentResult>;
|
|
183
|
+
export declare function checkPayment(invoiceId: string, opts?: CallOptions): Promise<CheckPaymentResult>;
|
|
119
184
|
export interface StatementInvoice {
|
|
120
185
|
invoice_id: string;
|
|
121
186
|
status: string;
|
|
@@ -164,7 +229,7 @@ export interface AccountStatementResult {
|
|
|
164
229
|
error?: string;
|
|
165
230
|
error_code?: string;
|
|
166
231
|
}
|
|
167
|
-
export declare function getAccountStatement(days?: number): Promise<AccountStatementResult>;
|
|
232
|
+
export declare function getAccountStatement(days?: number, opts?: CallOptions): Promise<AccountStatementResult>;
|
|
168
233
|
export interface PatronCoupon {
|
|
169
234
|
coupon_id: string;
|
|
170
235
|
name: string;
|
|
@@ -204,11 +269,11 @@ export interface ForgetCouponResult {
|
|
|
204
269
|
error_code?: string;
|
|
205
270
|
}
|
|
206
271
|
/** The coupons this patron has redeemed here. Free. */
|
|
207
|
-
export declare function listMyCoupons(): Promise<ListMyCouponsResult>;
|
|
272
|
+
export declare function listMyCoupons(opts?: CallOptions): Promise<ListMyCouponsResult>;
|
|
208
273
|
/** Redeem an operator's code once; the discount then applies on its own. Free. */
|
|
209
|
-
export declare function redeemCoupon(code: string): Promise<RedeemCouponResult>;
|
|
274
|
+
export declare function redeemCoupon(code: string, opts?: CallOptions): Promise<RedeemCouponResult>;
|
|
210
275
|
/** Take a coupon off the patron's list. The code stays re-redeemable while its window allows. */
|
|
211
|
-
export declare function forgetCoupon(couponId: string): Promise<ForgetCouponResult>;
|
|
276
|
+
export declare function forgetCoupon(couponId: string, opts?: CallOptions): Promise<ForgetCouponResult>;
|
|
212
277
|
export interface Kind0 {
|
|
213
278
|
name?: string;
|
|
214
279
|
display_name?: string;
|
|
@@ -225,7 +290,7 @@ export interface GetNostrProfileResult {
|
|
|
225
290
|
profile?: Kind0;
|
|
226
291
|
error?: string;
|
|
227
292
|
}
|
|
228
|
-
export declare function getNostrProfile(npub: string): Promise<GetNostrProfileResult>;
|
|
293
|
+
export declare function getNostrProfile(npub: string, opts?: CallOptions): Promise<GetNostrProfileResult>;
|
|
229
294
|
export interface PublishNostrProfileResult {
|
|
230
295
|
success: boolean;
|
|
231
296
|
ok?: number;
|
|
@@ -234,4 +299,150 @@ export interface PublishNostrProfileResult {
|
|
|
234
299
|
error?: string;
|
|
235
300
|
}
|
|
236
301
|
/** Relay a kind-0 the BROWSER signed. The wheel verifies it and fans it out. */
|
|
237
|
-
export declare function publishNostrProfile(npub: string, signedEvent: string): Promise<PublishNostrProfileResult>;
|
|
302
|
+
export declare function publishNostrProfile(npub: string, signedEvent: string, opts?: CallOptions): Promise<PublishNostrProfileResult>;
|
|
303
|
+
/** Every lifecycle `session_status` reports. */
|
|
304
|
+
export type SessionLifecycle = "ready" | "warming_up" | "misconfigured" | "quota_exceeded" | "not_registered" | "no_identity";
|
|
305
|
+
/** The patron's stored upstream OAuth token, when the operator has a provider. */
|
|
306
|
+
export interface UpstreamOAuth {
|
|
307
|
+
service: string;
|
|
308
|
+
has_access_token: boolean;
|
|
309
|
+
has_refresh_token: boolean;
|
|
310
|
+
refresh_enabled: boolean;
|
|
311
|
+
access_token_expires_at?: number;
|
|
312
|
+
access_token_expires_in_seconds?: number;
|
|
313
|
+
}
|
|
314
|
+
export interface SessionStatusResult {
|
|
315
|
+
success?: boolean;
|
|
316
|
+
lifecycle?: SessionLifecycle;
|
|
317
|
+
message?: string;
|
|
318
|
+
operator_npub?: string;
|
|
319
|
+
/** Why it is not ready, from the failing layer. */
|
|
320
|
+
detail?: string;
|
|
321
|
+
operator_credential_service?: string;
|
|
322
|
+
patron_credential_service?: string;
|
|
323
|
+
upstream_oauth?: UpstreamOAuth;
|
|
324
|
+
/** The vault could not answer — not the same as "never authorized". */
|
|
325
|
+
upstream_oauth_unreadable?: string;
|
|
326
|
+
error?: string;
|
|
327
|
+
error_code?: string;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Is the operator ready to serve? Free, no proof. With `patronNpub`, also the
|
|
331
|
+
* patron's upstream OAuth token expiry, so a page can refresh ahead of time.
|
|
332
|
+
*/
|
|
333
|
+
export declare function sessionStatus(patronNpub?: string, opts?: CallOptions): Promise<SessionStatusResult>;
|
|
334
|
+
export interface OnboardingItem {
|
|
335
|
+
field: string;
|
|
336
|
+
/** "identity" | "authority" | "secret" */
|
|
337
|
+
category: string;
|
|
338
|
+
/** "configured" | "missing" | "unknown" */
|
|
339
|
+
status: string;
|
|
340
|
+
how?: string;
|
|
341
|
+
lifecycle?: string;
|
|
342
|
+
delivered_at?: string | null;
|
|
343
|
+
}
|
|
344
|
+
/** The wheel's `get_operator_onboarding_status`: what the operator still has to set up. */
|
|
345
|
+
export interface OperatorOnboardingStatus {
|
|
346
|
+
ready?: boolean;
|
|
347
|
+
configured?: OnboardingItem[];
|
|
348
|
+
missing?: OnboardingItem[];
|
|
349
|
+
optional_missing?: OnboardingItem[];
|
|
350
|
+
summary?: string;
|
|
351
|
+
bootstrap_error?: string;
|
|
352
|
+
vault_ok?: boolean;
|
|
353
|
+
/** Set when the credential vault could not be read: nothing is known either way. */
|
|
354
|
+
vault_situation?: string;
|
|
355
|
+
credential_greeting?: string | null;
|
|
356
|
+
credential_service?: string | null;
|
|
357
|
+
operator_name?: string;
|
|
358
|
+
error?: string;
|
|
359
|
+
error_code?: string;
|
|
360
|
+
}
|
|
361
|
+
export declare function getOperatorOnboardingStatus(opts?: CallOptions): Promise<OperatorOnboardingStatus>;
|
|
362
|
+
/**
|
|
363
|
+
* The operator's own balance at its Authority — what certifies patron top-ups.
|
|
364
|
+
* The Authority's ledger, in the same shape as a patron's `check_balance`.
|
|
365
|
+
*/
|
|
366
|
+
export declare function checkAuthorityBalance(opts?: CallOptions): Promise<CheckBalanceResult>;
|
|
367
|
+
export interface PricingStep {
|
|
368
|
+
id: string;
|
|
369
|
+
type: string;
|
|
370
|
+
params?: Record<string, unknown>;
|
|
371
|
+
patron_npubs?: string[];
|
|
372
|
+
}
|
|
373
|
+
export interface ToolPrice {
|
|
374
|
+
tool_id: string;
|
|
375
|
+
tool_name: string;
|
|
376
|
+
price_sats: number;
|
|
377
|
+
category: string;
|
|
378
|
+
intent: string;
|
|
379
|
+
priced: boolean;
|
|
380
|
+
price_type?: string;
|
|
381
|
+
price_formula?: unknown;
|
|
382
|
+
min_cost?: number;
|
|
383
|
+
max_cost?: number;
|
|
384
|
+
multipliers?: Record<string, unknown>;
|
|
385
|
+
chain?: PricingStep[];
|
|
386
|
+
}
|
|
387
|
+
/** The wheel's `get_pricing_model`. `status` is "ok" or "error". */
|
|
388
|
+
export interface PricingModelResult {
|
|
389
|
+
status?: string;
|
|
390
|
+
model_id?: string | null;
|
|
391
|
+
name?: string | null;
|
|
392
|
+
is_active?: boolean | null;
|
|
393
|
+
tools?: ToolPrice[] | null;
|
|
394
|
+
tranche_lifetime?: {
|
|
395
|
+
target_usage_pct: number;
|
|
396
|
+
ttl_days?: number;
|
|
397
|
+
min_days?: number;
|
|
398
|
+
max_days?: number;
|
|
399
|
+
};
|
|
400
|
+
error?: string;
|
|
401
|
+
}
|
|
402
|
+
/** The operator's active pricing model. Free, no proof. */
|
|
403
|
+
export declare function getPricingModel(opts?: CallOptions): Promise<PricingModelResult>;
|
|
404
|
+
export interface CanonicalIdentity {
|
|
405
|
+
tool_id: string;
|
|
406
|
+
mcp_name: string;
|
|
407
|
+
category: string;
|
|
408
|
+
intent: string;
|
|
409
|
+
capability: string;
|
|
410
|
+
registered: boolean;
|
|
411
|
+
reason?: string;
|
|
412
|
+
}
|
|
413
|
+
/** The wheel's `list_canonical_identities`: every tool's frozen UUID and wire name. */
|
|
414
|
+
export interface CanonicalIdentitiesResult {
|
|
415
|
+
success?: boolean;
|
|
416
|
+
operator_npub?: string;
|
|
417
|
+
count?: number;
|
|
418
|
+
unregistered_count?: number;
|
|
419
|
+
tools?: CanonicalIdentity[];
|
|
420
|
+
unregistered?: Array<{
|
|
421
|
+
mcp_name: string;
|
|
422
|
+
reason: string;
|
|
423
|
+
[key: string]: unknown;
|
|
424
|
+
}>;
|
|
425
|
+
error?: string;
|
|
426
|
+
}
|
|
427
|
+
export declare function listCanonicalIdentities(opts?: CallOptions): Promise<CanonicalIdentitiesResult>;
|
|
428
|
+
export interface PatronCredentialFieldsResult {
|
|
429
|
+
success?: boolean;
|
|
430
|
+
fields?: string[];
|
|
431
|
+
/** Per field, when it was delivered; null for secrets vaulted before stamps. */
|
|
432
|
+
delivered_at?: Record<string, string | null>;
|
|
433
|
+
count?: number;
|
|
434
|
+
error?: string;
|
|
435
|
+
error_code?: string;
|
|
436
|
+
}
|
|
437
|
+
export interface PatronCredentialWriteResult {
|
|
438
|
+
success?: boolean;
|
|
439
|
+
message?: string;
|
|
440
|
+
error?: string;
|
|
441
|
+
error_code?: string;
|
|
442
|
+
}
|
|
443
|
+
/** The names of the patron's stored credential fields — never their values. */
|
|
444
|
+
export declare function getPatronCredentialFields(opts?: CallOptions): Promise<PatronCredentialFieldsResult>;
|
|
445
|
+
/** Set one credential field, leaving the others as they are. */
|
|
446
|
+
export declare function updatePatronCredential(field: string, value: string, opts?: CallOptions): Promise<PatronCredentialWriteResult>;
|
|
447
|
+
/** Remove one credential field, leaving the others as they are. */
|
|
448
|
+
export declare function deletePatronCredential(field: string, opts?: CallOptions): Promise<PatronCredentialWriteResult>;
|
package/dist/standardTools.js
CHANGED
|
@@ -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,67 +18,100 @@ 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,
|
|
42
|
-
* constraints included — keyed by the tool's frozen UUID
|
|
43
|
-
*
|
|
43
|
+
* constraints included — keyed by the tool's frozen UUID (or its capability
|
|
44
|
+
* name). `toolKwargs` are the call's own parameters, for a tool priced by
|
|
45
|
+
* them (ad valorem, or a multiplier on a categorical argument). Null when
|
|
46
|
+
* unreadable: show the answer without a price rather than invent one.
|
|
44
47
|
*/
|
|
45
|
-
export async function checkPrice(toolId) {
|
|
46
|
-
const r = await callTool("check_price", { tool_id: toolId });
|
|
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);
|
|
47
50
|
const v = r.effective_cost_api_sats ?? r.base_cost_api_sats;
|
|
48
51
|
return typeof v === "number" ? v : null;
|
|
49
52
|
}
|
|
50
|
-
export function purchaseCredits(sats) {
|
|
51
|
-
return callTool("purchase_credits", { amount_sats: sats });
|
|
53
|
+
export function purchaseCredits(sats, opts) {
|
|
54
|
+
return callTool("purchase_credits", { amount_sats: sats }, opts);
|
|
52
55
|
}
|
|
53
|
-
export function checkPayment(invoiceId) {
|
|
54
|
-
return callTool("check_payment", { invoice_id: invoiceId });
|
|
56
|
+
export function checkPayment(invoiceId, opts) {
|
|
57
|
+
return callTool("check_payment", { invoice_id: invoiceId }, opts);
|
|
55
58
|
}
|
|
56
|
-
export function getAccountStatement(days = 30) {
|
|
57
|
-
return callTool("account_statement", { days });
|
|
59
|
+
export function getAccountStatement(days = 30, opts) {
|
|
60
|
+
return callTool("account_statement", { days }, opts);
|
|
58
61
|
}
|
|
59
62
|
/** The coupons this patron has redeemed here. Free. */
|
|
60
|
-
export function listMyCoupons() {
|
|
61
|
-
return callTool("list_my_coupons", {});
|
|
63
|
+
export function listMyCoupons(opts) {
|
|
64
|
+
return callTool("list_my_coupons", {}, opts);
|
|
62
65
|
}
|
|
63
66
|
/** Redeem an operator's code once; the discount then applies on its own. Free. */
|
|
64
|
-
export function redeemCoupon(code) {
|
|
65
|
-
return callTool("redeem_coupon", { code });
|
|
67
|
+
export function redeemCoupon(code, opts) {
|
|
68
|
+
return callTool("redeem_coupon", { code }, opts);
|
|
66
69
|
}
|
|
67
70
|
/** Take a coupon off the patron's list. The code stays re-redeemable while its window allows. */
|
|
68
|
-
export function forgetCoupon(couponId) {
|
|
69
|
-
return callTool("forget_coupon", { coupon_id: couponId });
|
|
71
|
+
export function forgetCoupon(couponId, opts) {
|
|
72
|
+
return callTool("forget_coupon", { coupon_id: couponId }, opts);
|
|
70
73
|
}
|
|
71
|
-
export function getNostrProfile(npub) {
|
|
72
|
-
return callTool("get_nostr_profile", { npub });
|
|
74
|
+
export function getNostrProfile(npub, opts) {
|
|
75
|
+
return callTool("get_nostr_profile", { npub }, opts);
|
|
73
76
|
}
|
|
74
77
|
/** Relay a kind-0 the BROWSER signed. The wheel verifies it and fans it out. */
|
|
75
|
-
export function publishNostrProfile(npub, signedEvent) {
|
|
76
|
-
return callTool("publish_nostr_profile", {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
export function publishNostrProfile(npub, signedEvent, opts) {
|
|
79
|
+
return callTool("publish_nostr_profile", { npub, signed_event: signedEvent }, opts);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Is the operator ready to serve? Free, no proof. With `patronNpub`, also the
|
|
83
|
+
* patron's upstream OAuth token expiry, so a page can refresh ahead of time.
|
|
84
|
+
*/
|
|
85
|
+
export function sessionStatus(patronNpub, opts) {
|
|
86
|
+
return callTool("session_status", patronNpub ? { patron_npub: patronNpub } : {}, { bestEffort: true, ...opts });
|
|
87
|
+
}
|
|
88
|
+
export function getOperatorOnboardingStatus(opts) {
|
|
89
|
+
return callTool("get_operator_onboarding_status", {}, { bestEffort: true, ...opts });
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The operator's own balance at its Authority — what certifies patron top-ups.
|
|
93
|
+
* The Authority's ledger, in the same shape as a patron's `check_balance`.
|
|
94
|
+
*/
|
|
95
|
+
export function checkAuthorityBalance(opts) {
|
|
96
|
+
return callTool("check_authority_balance", {}, { bestEffort: true, ...opts });
|
|
97
|
+
}
|
|
98
|
+
/** The operator's active pricing model. Free, no proof. */
|
|
99
|
+
export function getPricingModel(opts) {
|
|
100
|
+
return callTool("get_pricing_model", {}, opts);
|
|
101
|
+
}
|
|
102
|
+
export function listCanonicalIdentities(opts) {
|
|
103
|
+
return callTool("list_canonical_identities", {}, opts);
|
|
104
|
+
}
|
|
105
|
+
/** The names of the patron's stored credential fields — never their values. */
|
|
106
|
+
export function getPatronCredentialFields(opts) {
|
|
107
|
+
return callTool("get_patron_credential_fields", {}, opts);
|
|
108
|
+
}
|
|
109
|
+
/** Set one credential field, leaving the others as they are. */
|
|
110
|
+
export function updatePatronCredential(field, value, opts) {
|
|
111
|
+
// `field` first: the log's scrubber keys on it to hide `value`.
|
|
112
|
+
return callTool("update_patron_credential", { field, value }, opts);
|
|
113
|
+
}
|
|
114
|
+
/** Remove one credential field, leaving the others as they are. */
|
|
115
|
+
export function deletePatronCredential(field, opts) {
|
|
116
|
+
return callTool("delete_patron_credential", { field }, opts);
|
|
80
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.
|
|
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",
|