@tollbooth-dpyc/web 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/wallet.js ADDED
@@ -0,0 +1,153 @@
1
+ /**
2
+ * The wallet's logic without a framework: the top-up state machine, the reading
3
+ * of a check_payment answer, and the words a coupon row needs. Pure, so it is
4
+ * tested without React or a server.
5
+ *
6
+ * A top-up goes idle → creating → awaiting (an invoice is out) → settled, or
7
+ * back to idle on cancel, or to failed when the invoice expires, is invalid or
8
+ * the service refuses. While awaiting, check_payment may be polled: it is free,
9
+ * idempotent and safe to repeat, unlike receive_npub_proof.
10
+ */
11
+ export const TOP_UP_IDLE = { phase: "idle", message: null };
12
+ export function paymentPhase(status) {
13
+ switch (status) {
14
+ case "Settled":
15
+ return "settled";
16
+ case "Expired":
17
+ return "expired";
18
+ case "Invalid":
19
+ return "invalid";
20
+ default:
21
+ return "waiting"; // New, Processing, Pending, or a word we do not know yet
22
+ }
23
+ }
24
+ /** The invoice in a purchase_credits answer, or null when there is none to pay. */
25
+ export function invoiceFrom(result, asked) {
26
+ if (result.error || result.success === false || !result.invoice_id)
27
+ return null;
28
+ const bolt11 = result.lightning_invoice || result.payment_request || null;
29
+ return {
30
+ id: result.invoice_id,
31
+ checkoutLink: result.checkout_link || null,
32
+ bolt11,
33
+ sats: typeof result.amount_sats === "number" ? result.amount_sats : asked,
34
+ };
35
+ }
36
+ /** A whole number of sats worth invoicing, from what the patron typed; else null. */
37
+ export function parseSats(input) {
38
+ const n = typeof input === "number" ? input : Number(String(input).replace(/[\s,_]/g, ""));
39
+ return Number.isSafeInteger(n) && n > 0 ? n : null;
40
+ }
41
+ export function topUpReducer(state, event) {
42
+ switch (event.type) {
43
+ case "create":
44
+ if (state.phase === "creating" || state.phase === "awaiting")
45
+ return state;
46
+ return parseSats(event.sats) === null
47
+ ? { phase: "idle", message: "Choose a whole number of sats above zero." }
48
+ : { phase: "creating", sats: event.sats };
49
+ case "created": {
50
+ if (state.phase !== "creating")
51
+ return state;
52
+ const invoice = invoiceFrom(event.result, state.sats);
53
+ if (!invoice) {
54
+ return {
55
+ phase: "failed",
56
+ invoice: null,
57
+ message: event.result.error ?? "The service did not return an invoice. Try again.",
58
+ };
59
+ }
60
+ return { phase: "awaiting", invoice, checking: false, status: null, checks: 0 };
61
+ }
62
+ case "check":
63
+ if (state.phase !== "awaiting" || state.checking)
64
+ return state;
65
+ return { ...state, checking: true };
66
+ case "checked": {
67
+ if (state.phase !== "awaiting")
68
+ return state;
69
+ const r = event.result;
70
+ if (r.error)
71
+ return { ...state, checking: false, status: r.error, checks: state.checks + 1 };
72
+ switch (paymentPhase(r.status)) {
73
+ case "settled":
74
+ return {
75
+ phase: "settled",
76
+ invoice: state.invoice,
77
+ credited: typeof r.credits_granted === "number" ? r.credits_granted : state.invoice.sats,
78
+ };
79
+ case "expired":
80
+ return { phase: "failed", invoice: state.invoice, message: "The invoice expired. Make a new one." };
81
+ case "invalid":
82
+ return { phase: "failed", invoice: state.invoice, message: "The payment was invalid." };
83
+ default:
84
+ return {
85
+ ...state,
86
+ checking: false,
87
+ status: r.message ?? r.status ?? null,
88
+ checks: state.checks + 1,
89
+ };
90
+ }
91
+ }
92
+ case "error":
93
+ if (state.phase === "creating")
94
+ return { phase: "failed", invoice: null, message: event.message };
95
+ if (state.phase === "awaiting") {
96
+ return { ...state, checking: false, status: event.message, checks: state.checks + 1 };
97
+ }
98
+ return state;
99
+ case "cancel":
100
+ case "reset":
101
+ return TOP_UP_IDLE;
102
+ }
103
+ }
104
+ /** Whether a poll should run now: an invoice is out, nothing is in flight, and the budget is not spent. */
105
+ export function shouldPoll(state, maxChecks) {
106
+ return state.phase === "awaiting" && !state.checking && state.checks < maxChecks;
107
+ }
108
+ /** Sats as the fleet writes them. Null is unknown, and unknown is a dash, never 0. */
109
+ export function formatSats(n) {
110
+ return typeof n === "number" && Number.isFinite(n) ? n.toLocaleString("en-US") : "—";
111
+ }
112
+ // ── Coupons ────────────────────────────────────────────────────────────────
113
+ /** The wheel's coupon status, in words a patron reads. */
114
+ export function couponStatusLabel(status) {
115
+ switch (status) {
116
+ case "active":
117
+ return "Active";
118
+ case "window_closed":
119
+ return "Expired";
120
+ case "window_not_started":
121
+ return "Not yet active";
122
+ case "patron_limit":
123
+ return "All uses claimed";
124
+ case "total_limit":
125
+ return "Fully claimed";
126
+ default:
127
+ return status;
128
+ }
129
+ }
130
+ export function couponUsesText(c) {
131
+ if (c.uses_per_patron == null)
132
+ return "∞ uses";
133
+ const n = c.uses_per_patron;
134
+ return `${c.uses_remaining ?? 0} of ${n} use${n === 1 ? "" : "s"} left`;
135
+ }
136
+ /** Whole days until the window closes, never below zero; null when the date is unreadable. */
137
+ export function couponDaysLeft(validUntil, now = Date.now()) {
138
+ const end = new Date(validUntil).getTime();
139
+ if (!Number.isFinite(end))
140
+ return null;
141
+ return Math.max(0, Math.ceil((end - now) / 86_400_000));
142
+ }
143
+ export function couponExpiryText(validUntil, now = Date.now()) {
144
+ const days = couponDaysLeft(validUntil, now);
145
+ if (days === null)
146
+ return null;
147
+ return days === 0 ? "expires today" : `expires in ${days} day${days === 1 ? "" : "s"}`;
148
+ }
149
+ /** What a successful redeem_coupon says back. */
150
+ export function redeemedText(r) {
151
+ const left = r.uses_remaining != null ? ` — ${r.uses_remaining} use${r.uses_remaining === 1 ? "" : "s"} left` : "";
152
+ return `${r.name ?? "Coupon"}: ${r.discount_percent ?? 0}% off${left}.`;
153
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tollbooth-dpyc/web",
3
- "version": "1.0.0",
3
+ "version": "1.1.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",
package/theme.css CHANGED
@@ -51,3 +51,25 @@
51
51
  --tb-notice-ink: #d8b4fe;
52
52
  }
53
53
  }
54
+
55
+ /* An explicit dark pick (bootstrapTheme / useTheme set data-theme) wins over
56
+ a light OS, just as data-theme="light" wins over a dark one above. */
57
+ :root[data-theme="dark"] {
58
+ --tb-accent: #7aa7ff;
59
+ --tb-on-accent: #0b1220;
60
+ --tb-surface: #151a21;
61
+ --tb-surface-2: #1d232c;
62
+ --tb-ink: #e6ebf2;
63
+ --tb-muted: #9aa5b4;
64
+ --tb-line: #2b333e;
65
+ --tb-ok: #5fd39a;
66
+ --tb-warn-bg: #2a2110;
67
+ --tb-warn-line: #5c4717;
68
+ --tb-warn-ink: #f2cf7a;
69
+ --tb-err-bg: #2c1515;
70
+ --tb-err-line: #5e2525;
71
+ --tb-err-ink: #f4a3a3;
72
+ --tb-notice-bg: #241532;
73
+ --tb-notice-line: #4d2d6e;
74
+ --tb-notice-ink: #d8b4fe;
75
+ }