@theoo/react 0.4.2 → 0.4.4

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/money.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useState } from "react";
2
+ import { useCallback, useEffect, useMemo, useState } from "react";
3
3
  import { formatUnits, parseUnits } from "@theoo/sdk";
4
4
  import { useTheoo } from "./context.js";
5
5
  import { styles } from "./components.js";
@@ -27,13 +27,13 @@ function find(balances, wanted) {
27
27
  return balances.find((b) => b.token.symbol.toLowerCase() === w || b.token.address.toLowerCase() === w);
28
28
  }
29
29
  const STATE_WORDS = {
30
- awaiting_signature: "Waiting for your passkey",
31
- processing: "On its way",
30
+ awaiting_signature: "Awaiting confirmation",
31
+ processing: "Processing",
32
32
  completed: "Done",
33
33
  failed: "Failed",
34
34
  expired: "Expired",
35
35
  open: "Open",
36
- awaiting_delivery: "Waiting for the desk",
36
+ awaiting_delivery: "Pending",
37
37
  delivered: "Delivered",
38
38
  disputed: "Disputed",
39
39
  refunded: "Refunded",
@@ -47,10 +47,10 @@ const STATE_WORDS = {
47
47
  * the passkey signs is one batch that approves the exact amount, swaps, and
48
48
  * resets the approval to zero. Nothing is left behind for anyone to spend.
49
49
  */
50
- export function TheooMoney({ chain, flows = ["swap"], defaultSell, defaultBuy, onDone, explorerTx, className, cashoutSender }) {
50
+ export function TheooMoney({ chain, flows = ["swap"], defaultSell, defaultBuy, onDone, explorerTx, className }) {
51
51
  const [tab, setTab] = useState(flows[0] ?? "swap");
52
52
  const names = { swap: "Swap", espees: "Buy ESPEES", sell: "Sell ESPEES", buy: "Add money", cashout: "Cash out" };
53
- return (_jsxs("div", { className: className, style: card, "data-theoo-money": "", children: [flows.length > 1 ? (_jsx("div", { role: "tablist", style: { display: "flex", gap: 6, overflowX: "auto", scrollbarWidth: "none", margin: "0 -4px", padding: "0 4px" }, children: flows.map((f) => (_jsx("button", { type: "button", role: "tab", "aria-selected": tab === f, onClick: () => setTab(f), style: { ...(tab === f ? s.button : s.secondary), padding: "6px 12px", fontSize: 13.5, whiteSpace: "nowrap", flex: "none", width: "auto" }, children: names[f] }, f))) })) : null, tab === "espees" ? (_jsx(EspeesPanel, { chain: chain, onDone: onDone, explorerTx: explorerTx })) : tab === "sell" ? (_jsx(SellPanel, { chain: chain })) : tab === "buy" ? (_jsx(BuyPanel, { chain: chain })) : tab === "cashout" ? (_jsx(CashoutPanel, { chain: chain, sender: cashoutSender, onDone: onDone })) : (_jsx(SwapPanel, { chain: chain, defaultSell: defaultSell, defaultBuy: defaultBuy, onDone: onDone, explorerTx: explorerTx }))] }));
53
+ return (_jsxs("div", { className: className, style: card, "data-theoo-money": "", children: [flows.length > 1 ? (_jsx("div", { role: "tablist", style: { display: "flex", gap: 6, overflowX: "auto", scrollbarWidth: "none", margin: "0 -4px", padding: "0 4px" }, children: flows.map((f) => (_jsx("button", { type: "button", role: "tab", "aria-selected": tab === f, onClick: () => setTab(f), style: { ...(tab === f ? s.button : s.secondary), padding: "6px 12px", fontSize: 13.5, whiteSpace: "nowrap", flex: "none", width: "auto" }, children: names[f] }, f))) })) : null, tab === "espees" ? (_jsx(EspeesPanel, { chain: chain, onDone: onDone, explorerTx: explorerTx })) : tab === "sell" ? (_jsx(SellPanel, { chain: chain })) : tab === "buy" ? (_jsx(BachsPanel, { mode: "buy" })) : tab === "cashout" ? (_jsx(BachsPanel, { mode: "cashout" })) : (_jsx(SwapPanel, { chain: chain, defaultSell: defaultSell, defaultBuy: defaultBuy, onDone: onDone, explorerTx: explorerTx }))] }));
54
54
  }
55
55
  function SwapPanel({ chain, defaultSell, defaultBuy, onDone, explorerTx }) {
56
56
  const { balances, error: balanceError, reload } = useBalances(chain);
@@ -82,17 +82,15 @@ function SwapPanel({ chain, defaultSell, defaultBuy, onDone, explorerTx }) {
82
82
  try {
83
83
  const units = parseUnits(amount, sell.token.decimals);
84
84
  if (BigInt(units) === 0n)
85
- return { problem: "Enter an amount above zero." };
85
+ return { problem: "Enter an amount." };
86
86
  if (BigInt(units) > BigInt(sell.amount))
87
- return { problem: `You have ${sell.display} ${sell.token.symbol}.` };
87
+ return { problem: "Not enough balance." };
88
88
  return { units };
89
89
  }
90
90
  catch (e) {
91
91
  const tooPrecise = e instanceof Error && /decimal places/.test(e.message);
92
92
  return {
93
- problem: tooPrecise
94
- ? `${sell.token.symbol} has at most ${sell.token.decimals} decimal places.`
95
- : "Enter an amount like 12.5.",
93
+ problem: tooPrecise ? "Too many decimals." : "Invalid amount.",
96
94
  };
97
95
  }
98
96
  }, [amount, sell]);
@@ -123,17 +121,23 @@ function SwapPanel({ chain, defaultSell, defaultBuy, onDone, explorerTx }) {
123
121
  const q = swap.quote;
124
122
  const done = swap.status;
125
123
  const txLink = done?.txHash && explorerTx ? explorerTx(done.txHash, done.chain) : null;
126
- return (_jsxs("div", { style: { display: "grid", gap: 12 }, children: [_jsxs("div", { style: { display: "flex", flexWrap: "wrap", justifyContent: "space-between", alignItems: "baseline", gap: "4px 10px" }, children: [_jsx("strong", { style: { fontSize: 15.5, letterSpacing: "-0.01em", whiteSpace: "nowrap" }, children: "Swap" }), balances ? _jsx("span", { style: s.tiny, children: "Gas is on us \u00B7 you sign with your passkey" }) : null] }), balanceError ? (_jsx("div", { style: s.error, role: "alert", children: balanceError.message })) : null, !balances && !balanceError ? _jsx("p", { style: s.muted, children: "Loading balances\u2026" }) : null, balances && list.length < 2 ? _jsx("p", { style: s.muted, children: "Swaps aren't available on this chain yet." }) : null, balances && list.length >= 2 ? (_jsxs(_Fragment, { children: [_jsxs("div", { style: { display: "grid", gap: 6 }, children: [_jsxs("div", { style: { display: "flex", justifyContent: "space-between" }, children: [_jsx("label", { htmlFor: "theoo-money-amount", style: label, children: "You pay" }), sell ? (_jsxs("span", { style: s.tiny, children: ["Balance ", sell.display, " ", sell.token.symbol, " ", _jsx("button", { type: "button", style: link, onClick: () => setAmount(formatUnits(sell.amount, sell.token.decimals, sell.token.decimals)), children: "Max" })] })) : null] }), _jsxs("div", { style: row, children: [_jsx("input", { id: "theoo-money-amount", inputMode: "decimal", autoComplete: "off", placeholder: "0.00", value: amount, onChange: (e) => setAmount(e.target.value), style: { ...s.input, fontFamily: t.mono, fontSize: 17 }, "aria-invalid": parsed.problem ? true : undefined }), _jsx("select", { id: "theoo-money-sell", "aria-label": "Token to pay with", value: sellAddr, onChange: (e) => setSellAddr(e.target.value), style: select, children: list.map((b) => (_jsx("option", { value: b.token.address, disabled: b.token.address === buyAddr, children: b.token.symbol }, b.token.address))) })] }), parsed.problem ? _jsx("p", { style: { ...s.tiny, color: t.alert }, children: parsed.problem }) : null] }), _jsx("div", { style: { display: "flex", justifyContent: "center" }, children: _jsx("button", { type: "button", onClick: flip, "aria-label": "Swap direction", style: { ...s.secondary, padding: "4px 12px", fontSize: 15 }, children: "\u21C5" }) }), _jsxs("div", { style: { display: "grid", gap: 6 }, children: [_jsx("label", { htmlFor: "theoo-money-buy", style: label, children: "You get" }), _jsxs("div", { style: row, children: [_jsx("div", { style: { ...s.input, fontFamily: t.mono, fontSize: 17, color: q ? t.ink : t.ink3 }, "aria-live": "polite", children: q ? q.buy.display : "—" }), _jsx("select", { id: "theoo-money-buy", value: buyAddr, onChange: (e) => setBuyAddr(e.target.value), style: select, children: list.map((b) => (_jsx("option", { value: b.token.address, disabled: b.token.address === sellAddr, children: b.token.symbol }, b.token.address))) })] })] }), q && !swap.expired ? (_jsxs("div", { style: { display: "grid", gap: 6, padding: 12, borderRadius: t.radius, background: t.accentSoft, fontSize: 13.5, lineHeight: 1.5 }, "data-theoo-quote": "", children: [_jsx("span", { children: q.summary }), q.warning ? _jsx("span", { style: { color: t.alert }, children: q.warning }) : null, _jsxs("span", { style: s.tiny, children: ["The swap can't deliver less than ", q.minBuy.display, " ", q.minBuy.symbol, ": the chain enforces it. Quote good for ", swap.secondsLeft, "s."] })] })) : null, q && swap.expired ? _jsx("p", { style: s.muted, children: "That quote expired. Ask for a fresh one." }) : null, swap.error ? (_jsx("div", { style: s.error, role: "alert", children: swap.error.message })) : null, !q || swap.expired ? (_jsx("button", { type: "button", style: { ...s.button, opacity: !parsed.units || swap.busy ? 0.6 : 1 }, disabled: !parsed.units || swap.busy || !sell || !buy, onClick: () => sell && buy && parsed.units && swap.ask({ sellToken: sell.token.address, buyToken: buy.token.address, sellAmount: parsed.units }), children: swap.busy ? "Checking the best price…" : q ? "Get a fresh quote" : "Get a quote" })) : (_jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr auto", gap: 8 }, children: [_jsx("button", { type: "button", style: { ...s.button, opacity: swap.busy ? 0.6 : 1 }, disabled: swap.busy, onClick: () => void swap.confirm(), children: swap.busy ? "Confirming…" : "Confirm with passkey" }), _jsx("button", { type: "button", style: s.secondary, disabled: swap.busy, onClick: swap.reset, children: "Cancel" })] })), done ? (_jsxs("p", { style: { ...s.muted, color: done.state === "failed" ? t.alert : t.ink2 }, role: "status", children: [done.state === "included" ? "Swapped. " : done.state === "failed" ? `The swap failed on chain${done.error ? `: ${done.error}` : ""}. ` : "Sent; it's on its way. ", txLink ? (_jsx("a", { href: txLink, target: "_blank", rel: "noopener noreferrer", style: { color: t.accent }, children: "View transaction" })) : null] })) : null] })) : null, orders && orders.length ? _jsx(Recent, { orders: orders, explorerTx: explorerTx }) : null] }));
124
+ return (_jsxs("div", { style: { display: "grid", gap: 12 }, children: [_jsx("div", { style: { display: "flex", flexWrap: "wrap", justifyContent: "space-between", alignItems: "baseline", gap: "4px 10px" }, children: _jsx("strong", { style: { fontSize: 15.5, letterSpacing: "-0.01em", whiteSpace: "nowrap" }, children: "Swap" }) }), balanceError ? (_jsx("div", { style: s.error, role: "alert", children: balanceError.message })) : null, !balances && !balanceError ? _jsx("p", { style: s.muted, children: "Loading\u2026" }) : null, balances && list.length < 2 ? _jsx("p", { style: s.muted, children: "Not available yet." }) : null, balances && list.length >= 2 ? (_jsxs(_Fragment, { children: [_jsxs("div", { style: { display: "grid", gap: 6 }, children: [_jsxs("div", { style: { display: "flex", justifyContent: "space-between" }, children: [_jsx("label", { htmlFor: "theoo-money-amount", style: label, children: "You pay" }), sell ? (_jsxs("span", { style: s.tiny, children: ["Balance ", sell.display, " ", sell.token.symbol, " ", _jsx("button", { type: "button", style: link, onClick: () => setAmount(formatUnits(sell.amount, sell.token.decimals, sell.token.decimals)), children: "Max" })] })) : null] }), _jsxs("div", { style: row, children: [_jsx("input", { id: "theoo-money-amount", inputMode: "decimal", autoComplete: "off", placeholder: "0.00", value: amount, onChange: (e) => setAmount(e.target.value), style: { ...s.input, fontFamily: t.mono, fontSize: 17 }, "aria-invalid": parsed.problem ? true : undefined }), _jsx("select", { id: "theoo-money-sell", "aria-label": "Token to pay with", value: sellAddr, onChange: (e) => setSellAddr(e.target.value), style: select, children: list.map((b) => (_jsx("option", { value: b.token.address, disabled: b.token.address === buyAddr, children: b.token.symbol }, b.token.address))) })] }), parsed.problem ? _jsx("p", { style: { ...s.tiny, color: t.alert }, children: parsed.problem }) : null] }), _jsx("div", { style: { display: "flex", justifyContent: "center" }, children: _jsx("button", { type: "button", onClick: flip, "aria-label": "Swap direction", style: { ...s.secondary, padding: "4px 12px", fontSize: 15 }, children: "\u21C5" }) }), _jsxs("div", { style: { display: "grid", gap: 6 }, children: [_jsx("label", { htmlFor: "theoo-money-buy", style: label, children: "You get" }), _jsxs("div", { style: row, children: [_jsx("div", { style: { ...s.input, fontFamily: t.mono, fontSize: 17, color: q ? t.ink : t.ink3 }, "aria-live": "polite", children: q ? q.buy.display : "—" }), _jsx("select", { id: "theoo-money-buy", value: buyAddr, onChange: (e) => setBuyAddr(e.target.value), style: select, children: list.map((b) => (_jsx("option", { value: b.token.address, disabled: b.token.address === sellAddr, children: b.token.symbol }, b.token.address))) })] })] }), q && !swap.expired ? (_jsxs("div", { style: { display: "grid", gap: 6, padding: 12, borderRadius: t.radius, background: t.accentSoft, fontSize: 13.5, lineHeight: 1.5 }, "data-theoo-quote": "", children: [_jsx("span", { children: q.summary }), q.warning ? _jsx("span", { style: { color: t.alert }, children: q.warning }) : null, _jsxs("span", { style: s.tiny, children: [swap.secondsLeft, "s"] })] })) : null, q && swap.expired ? _jsx("p", { style: s.muted, children: "Quote expired." }) : null, swap.error ? (_jsx("div", { style: s.error, role: "alert", children: swap.error.message })) : null, !q || swap.expired ? (_jsx("button", { type: "button", style: { ...s.button, opacity: !parsed.units || swap.busy ? 0.6 : 1 }, disabled: !parsed.units || swap.busy || !sell || !buy, onClick: () => sell && buy && parsed.units && swap.ask({ sellToken: sell.token.address, buyToken: buy.token.address, sellAmount: parsed.units }), children: swap.busy ? "Getting a quote…" : q ? "Refresh quote" : "Get a quote" })) : (_jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr auto", gap: 8 }, children: [_jsx("button", { type: "button", style: { ...s.button, opacity: swap.busy ? 0.6 : 1 }, disabled: swap.busy, onClick: () => void swap.confirm(), children: swap.busy ? "Confirming…" : "Confirm" }), _jsx("button", { type: "button", style: s.secondary, disabled: swap.busy, onClick: swap.reset, children: "Cancel" })] })), done ? (_jsxs("p", { style: { ...s.muted, color: done.state === "failed" ? t.alert : t.ink2 }, role: "status", children: [done.state === "included" ? "Swapped. " : done.state === "failed" ? "Swap failed. " : "Processing. ", txLink ? (_jsx("a", { href: txLink, target: "_blank", rel: "noopener noreferrer", style: { color: t.accent }, children: "View transaction" })) : null] })) : null] })) : null, orders && orders.length ? _jsx(Recent, { orders: orders, explorerTx: explorerTx }) : null] }));
127
125
  }
126
+ /**
127
+ * A buy, in a few words. Behind them: the payment sits in an escrow on chain,
128
+ * not with the desk. The desk sends ESPEES by hand; the buyer confirms it
129
+ * arrived or disputes it, and a dispute goes to the arbiter. If the desk
130
+ * delivers nothing by the deadline, the escrow refunds the buyer on its own.
131
+ */
128
132
  const ESPEES_WORDS = {
129
- awaiting_signature: "Waiting for your passkey.",
130
- awaiting_delivery: "Paid into the escrow, not to the desk. The desk sends your ESPEES by hand, usually within a couple of minutes. If nothing arrives in time, your payment comes back on its own.",
131
- delivered: "The desk says your ESPEES is sent. Did it arrive?",
132
- disputed: "You reported a problem. The arbiter will decide between you and the desk.",
133
- completed: "Done: the desk has been paid.",
134
- refunded: "Refunded to your account.",
135
- failed: "The payment failed on chain; nothing was taken.",
136
- expired: "Not signed in time; nothing was taken.",
133
+ awaiting_signature: "Awaiting confirmation.",
134
+ awaiting_delivery: "Paid. Your ESPEES is on its way.",
135
+ delivered: "Sent. Did it arrive?",
136
+ disputed: "Under review.",
137
+ completed: "Done.",
138
+ refunded: "Refunded.",
139
+ failed: "Failed. Nothing was charged.",
140
+ expired: "Expired. Nothing was charged.",
137
141
  };
138
142
  /**
139
143
  * ESPEES from the desk. The payment goes into an escrow on chain, the desk
@@ -160,13 +164,13 @@ function EspeesPanel({ chain, onDone, explorerTx }) {
160
164
  try {
161
165
  const units = parseUnits(amount, chosen.token.decimals);
162
166
  if (BigInt(units) === 0n)
163
- return { problem: "Enter an amount above zero." };
167
+ return { problem: "Enter an amount." };
164
168
  if (held && BigInt(units) > BigInt(held.amount))
165
- return { problem: `You have ${held.display} ${chosen.token.symbol}.` };
169
+ return { problem: "Not enough balance." };
166
170
  return { units };
167
171
  }
168
172
  catch {
169
- return { problem: "Enter an amount like 20." };
173
+ return { problem: "Invalid amount." };
170
174
  }
171
175
  }, [amount, chosen, held]);
172
176
  const estimate = chosen && parsed.units
@@ -203,25 +207,32 @@ function EspeesPanel({ chain, onDone, explorerTx }) {
203
207
  await reloadOrders();
204
208
  };
205
209
  if (!e.info)
206
- return _jsx("p", { style: s.muted, children: "Loading the desk\u2026" });
210
+ return _jsx("p", { style: s.muted, children: "Loading\u2026" });
207
211
  if (!e.info.open)
208
- return _jsx("p", { style: s.muted, children: "The ESPEES desk isn't selling on this chain right now." });
212
+ return _jsx("p", { style: s.muted, children: "Not available right now." });
209
213
  const q = e.quote;
210
214
  const minutes = Math.round(e.info.deliverWithinSecs / 60);
211
- return (_jsxs("div", { style: { display: "grid", gap: 12 }, "data-theoo-espees": "", children: [_jsxs("div", { style: { display: "flex", flexWrap: "wrap", justifyContent: "space-between", alignItems: "baseline", gap: "4px 10px" }, children: [_jsx("strong", { style: { fontSize: 15.5, letterSpacing: "-0.01em", whiteSpace: "nowrap" }, children: "Buy ESPEES" }), _jsxs("span", { style: s.tiny, children: ["Delivered within ", minutes, " minutes, or your money comes back"] })] }), _jsxs("div", { style: { display: "grid", gap: 6 }, children: [_jsxs("div", { style: { display: "flex", justifyContent: "space-between" }, children: [_jsx("label", { htmlFor: "theoo-espees-amount", style: label, children: "You pay" }), held && chosen ? (_jsxs("span", { style: s.tiny, children: ["Balance ", held.display, " ", chosen.token.symbol] })) : null] }), _jsxs("div", { style: row, children: [_jsx("input", { id: "theoo-espees-amount", inputMode: "decimal", autoComplete: "off", placeholder: "0.00", value: amount, onChange: (ev) => setAmount(ev.target.value), style: { ...s.input, fontFamily: t.mono, fontSize: 17 }, "aria-invalid": parsed.problem ? true : undefined }), _jsx("select", { "aria-label": "Pay ESPEES with", value: token, onChange: (ev) => setToken(ev.target.value), style: select, children: tokens.map((d) => (_jsx("option", { value: d.token.address, children: d.token.symbol }, d.token.address))) })] }), parsed.problem ? _jsx("p", { style: { ...s.tiny, color: t.alert }, children: parsed.problem }) : null, chosen ? (_jsxs("p", { style: s.tiny, children: [estimate ? `You get about ${estimate} ESPEES · ` : "", chosen.price, " ESPEES per ", chosen.token.symbol] })) : null] }), _jsxs("div", { style: { display: "grid", gap: 6 }, children: [_jsx("label", { htmlFor: "theoo-espees-address", style: label, children: "Your Espees wallet address" }), _jsx("input", { id: "theoo-espees-address", autoComplete: "off", value: address, onChange: (ev) => setAddress(ev.target.value), style: s.input }), _jsx("label", { htmlFor: "theoo-espees-again", style: label, children: "The same address, again" }), _jsx("input", { id: "theoo-espees-again", autoComplete: "off", value: again, onChange: (ev) => setAgain(ev.target.value), style: s.input, "aria-invalid": mismatch ? true : undefined }), mismatch ? _jsx("p", { style: { ...s.tiny, color: t.alert }, children: "The two addresses don't match." }) : null] }), q && !e.expired ? (_jsxs("div", { style: { display: "grid", gap: 6, padding: 12, borderRadius: t.radius, background: t.accentSoft, fontSize: 13.5, lineHeight: 1.5 }, "data-theoo-quote": "", children: [_jsx("span", { children: q.summary }), _jsxs("span", { style: s.tiny, children: ["Price held for ", e.secondsLeft, "s."] })] })) : null, q && e.expired ? _jsx("p", { style: s.muted, children: "That price expired. Ask for a fresh one." }) : null, e.error ? (_jsx("div", { style: s.error, role: "alert", children: e.error.message })) : null, !q || e.expired ? (_jsx("button", { type: "button", style: { ...s.button, opacity: !parsed.units || e.busy ? 0.6 : 1 }, disabled: !parsed.units || e.busy || !chosen || !address.trim() || address.trim() !== again.trim(), onClick: () => chosen &&
215
+ return (_jsxs("div", { style: { display: "grid", gap: 12 }, "data-theoo-espees": "", children: [_jsxs("div", { style: { display: "flex", flexWrap: "wrap", justifyContent: "space-between", alignItems: "baseline", gap: "4px 10px" }, children: [_jsx("strong", { style: { fontSize: 15.5, letterSpacing: "-0.01em", whiteSpace: "nowrap" }, children: "Buy ESPEES" }), _jsxs("span", { style: s.tiny, children: ["Arrives within ", minutes, " min"] })] }), _jsxs("div", { style: { display: "grid", gap: 6 }, children: [_jsxs("div", { style: { display: "flex", justifyContent: "space-between" }, children: [_jsx("label", { htmlFor: "theoo-espees-amount", style: label, children: "You pay" }), held && chosen ? (_jsxs("span", { style: s.tiny, children: ["Balance ", held.display, " ", chosen.token.symbol] })) : null] }), _jsxs("div", { style: row, children: [_jsx("input", { id: "theoo-espees-amount", inputMode: "decimal", autoComplete: "off", placeholder: "0.00", value: amount, onChange: (ev) => setAmount(ev.target.value), style: { ...s.input, fontFamily: t.mono, fontSize: 17 }, "aria-invalid": parsed.problem ? true : undefined }), _jsx("select", { "aria-label": "Pay ESPEES with", value: token, onChange: (ev) => setToken(ev.target.value), style: select, children: tokens.map((d) => (_jsx("option", { value: d.token.address, children: d.token.symbol }, d.token.address))) })] }), parsed.problem ? _jsx("p", { style: { ...s.tiny, color: t.alert }, children: parsed.problem }) : null, chosen ? (_jsxs("p", { style: s.tiny, children: [estimate ? `You get about ${estimate} ESPEES · ` : "", chosen.price, " ESPEES per ", chosen.token.symbol] })) : null] }), _jsxs("div", { style: { display: "grid", gap: 6 }, children: [_jsx("label", { htmlFor: "theoo-espees-address", style: label, children: "Espees address" }), _jsx("input", { id: "theoo-espees-address", autoComplete: "off", value: address, onChange: (ev) => setAddress(ev.target.value), style: s.input }), _jsx("label", { htmlFor: "theoo-espees-again", style: label, children: "Confirm address" }), _jsx("input", { id: "theoo-espees-again", autoComplete: "off", value: again, onChange: (ev) => setAgain(ev.target.value), style: s.input, "aria-invalid": mismatch ? true : undefined }), mismatch ? _jsx("p", { style: { ...s.tiny, color: t.alert }, children: "The two addresses don't match." }) : null] }), q && !e.expired ? (_jsxs("div", { style: { display: "grid", gap: 6, padding: 12, borderRadius: t.radius, background: t.accentSoft, fontSize: 13.5, lineHeight: 1.5 }, "data-theoo-quote": "", children: [_jsx("span", { children: q.summary }), _jsxs("span", { style: s.tiny, children: [e.secondsLeft, "s"] })] })) : null, q && e.expired ? _jsx("p", { style: s.muted, children: "Price expired." }) : null, e.error ? (_jsx("div", { style: s.error, role: "alert", children: e.error.message })) : null, !q || e.expired ? (_jsx("button", { type: "button", style: { ...s.button, opacity: !parsed.units || e.busy ? 0.6 : 1 }, disabled: !parsed.units || e.busy || !chosen || !address.trim() || address.trim() !== again.trim(), onClick: () => chosen &&
212
216
  parsed.units &&
213
- e.ask({ payToken: chosen.token.address, payAmount: parsed.units, espeesAddress: address, espeesAddressConfirm: again }), children: e.busy ? "Asking the desk…" : q ? "Get a fresh price" : "Get a price" })) : (_jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr auto", gap: 8 }, children: [_jsx("button", { type: "button", style: { ...s.button, opacity: e.busy ? 0.6 : 1 }, disabled: e.busy, onClick: () => void e.confirm(), children: e.busy ? "Paying…" : "Pay with passkey" }), _jsx("button", { type: "button", style: s.secondary, disabled: e.busy, onClick: e.reset, children: "Cancel" })] })), latest ? (_jsxs("div", { style: { display: "grid", gap: 8, borderTop: `1px solid ${t.line}`, paddingTop: 10 }, "data-espees-order": latest.state, children: [_jsxs("span", { style: label, children: ["Your latest order: ", latest.pay ? `${latest.pay.display} ${latest.pay.symbol}` : "", " \u2192 ", latest.get ? `${latest.get.display} ESPEES` : ""] }), _jsx("span", { style: { fontSize: 13.5 }, children: ESPEES_WORDS[latest.state] ?? latest.state }), latest.state === "delivered" ? (_jsxs("div", { style: { display: "flex", gap: 8 }, children: [_jsx("button", { type: "button", style: s.button, disabled: e.busy, onClick: () => void answer(e.release), children: "It arrived" }), _jsx("button", { type: "button", style: s.secondary, disabled: e.busy, onClick: () => void answer(e.dispute), children: "Report a problem" })] })) : null, latest.txHash && explorerTx && latest.chain ? (_jsx("a", { href: explorerTx(latest.txHash, latest.chain) ?? undefined, target: "_blank", rel: "noopener noreferrer", style: { color: t.accent, fontSize: 12.5 }, children: "View the payment" })) : null] })) : null] }));
217
+ e.ask({ payToken: chosen.token.address, payAmount: parsed.units, espeesAddress: address, espeesAddressConfirm: again }), children: e.busy ? "Getting a price…" : q ? "Refresh price" : "Get a price" })) : (_jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr auto", gap: 8 }, children: [_jsx("button", { type: "button", style: { ...s.button, opacity: e.busy ? 0.6 : 1 }, disabled: e.busy, onClick: () => void e.confirm(), children: e.busy ? "Paying…" : "Pay" }), _jsx("button", { type: "button", style: s.secondary, disabled: e.busy, onClick: e.reset, children: "Cancel" })] })), latest ? (_jsxs("div", { style: { display: "grid", gap: 8, borderTop: `1px solid ${t.line}`, paddingTop: 10 }, "data-espees-order": latest.state, children: [_jsxs("span", { style: label, children: ["Latest: ", latest.pay ? `${latest.pay.display} ${latest.pay.symbol}` : "", " \u2192 ", latest.get ? `${latest.get.display} ESPEES` : ""] }), _jsx("span", { style: { fontSize: 13.5 }, children: ESPEES_WORDS[latest.state] ?? latest.state }), latest.state === "delivered" ? (_jsxs("div", { style: { display: "flex", gap: 8 }, children: [_jsx("button", { type: "button", style: s.button, disabled: e.busy, onClick: () => void answer(e.release), children: "It arrived" }), _jsx("button", { type: "button", style: s.secondary, disabled: e.busy, onClick: () => void answer(e.dispute), children: "Report a problem" })] })) : null, latest.txHash && explorerTx && latest.chain ? (_jsx("a", { href: explorerTx(latest.txHash, latest.chain) ?? undefined, target: "_blank", rel: "noopener noreferrer", style: { color: t.accent, fontSize: 12.5 }, children: "View transaction" })) : null] })) : null] }));
214
218
  }
219
+ /**
220
+ * A sale, in a few words. Behind them: the desk first locks the payout in an
221
+ * escrow on chain, naming this account; only then is the seller told where
222
+ * to send. If the seller sends nothing in time the desk takes its payout
223
+ * back; if the desk says nothing arrived, the arbiter decides. A desk that
224
+ * never locks a payout lets the order lapse, and nothing moves.
225
+ */
215
226
  const SELL_WORDS = {
216
- awaiting_desk: "Waiting for the desk to lock your payout. Don't send anything yet.",
217
- awaiting_espees: "Your payout is locked. Send the ESPEES now.",
218
- espees_sent: "The desk is checking the ESPEES arrived.",
219
- disputed: "The desk says the ESPEES didn't arrive. The arbiter will decide.",
220
- completed: "Paid. It's in your account.",
221
- expired: "Nothing was sent in time, so the desk took its payout back.",
222
- rejected: "The arbiter found the ESPEES didn't arrive, so the desk kept its payout.",
223
- cancelled: "Called off. Nothing moved.",
224
- lapsed: "The desk didn't lock a payout in time. Nothing moved.",
227
+ awaiting_desk: "Pending. Don't send yet.",
228
+ awaiting_espees: "Ready. Send the ESPEES now.",
229
+ espees_sent: "Confirming receipt.",
230
+ disputed: "Under review.",
231
+ completed: "Paid.",
232
+ expired: "Expired.",
233
+ rejected: "Closed. The ESPEES didn't arrive.",
234
+ cancelled: "Cancelled.",
235
+ lapsed: "Expired.",
225
236
  };
226
237
  const when = (secs) => (secs ? new Date(secs * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "");
227
238
  /**
@@ -249,11 +260,11 @@ function SellPanel({ chain }) {
249
260
  try {
250
261
  const units = parseUnits(amount, 18);
251
262
  if (BigInt(units) === 0n)
252
- return { problem: "Enter an amount above zero." };
263
+ return { problem: "Enter an amount." };
253
264
  return { units };
254
265
  }
255
266
  catch {
256
- return { problem: "Enter an amount like 25." };
267
+ return { problem: "Invalid amount." };
257
268
  }
258
269
  }, [amount]);
259
270
  const estimate = chosen && parsed.units ? formatUnits((BigInt(parsed.units) * 10n ** BigInt(chosen.token.decimals)) / BigInt(chosen.priceE18), chosen.token.decimals, 6) : null;
@@ -264,86 +275,88 @@ function SellPanel({ chain }) {
264
275
  // eslint-disable-next-line react-hooks/exhaustive-deps
265
276
  }, [token, amount, from, again]);
266
277
  if (!e.info)
267
- return _jsx("p", { style: s.muted, children: "Loading the desk\u2026" });
278
+ return _jsx("p", { style: s.muted, children: "Loading\u2026" });
268
279
  const o = e.order;
269
280
  const q = e.quote;
270
281
  const error = e.error ? (_jsx("div", { style: s.error, role: "alert", children: e.error.message })) : null;
271
282
  // A sale this session has seen: what the seller does next, or how it ended,
272
283
  // until they choose to sell more.
273
284
  if (o) {
274
- return (_jsxs("div", { style: { display: "grid", gap: 10 }, "data-theoo-sell": o.state, children: [_jsxs("strong", { style: { fontSize: 15.5, letterSpacing: "-0.01em" }, children: ["Selling ", o.espees.display, " ESPEES for ", o.receive.display, " ", o.receive.symbol] }), _jsx("span", { style: { fontSize: 13.5, lineHeight: 1.5 }, children: SELL_WORDS[o.state] ?? o.state }), o.state === "awaiting_espees" && o.sendTo ? (_jsxs("div", { style: { display: "grid", gap: 8, padding: 12, borderRadius: t.radius, background: t.accentSoft, fontSize: 13.5, lineHeight: 1.6 }, children: [_jsxs("span", { children: ["Send ", _jsxs("strong", { children: [o.espees.display, " ESPEES"] }), " from ", _jsx("strong", { children: o.espeesFrom }), " to ", _jsx("strong", { "data-sell-send-to": "", children: o.sendTo }), ", with the note ", _jsx("strong", { style: { fontFamily: t.mono }, "data-sell-memo": "", children: o.memo }), o.deadline ? `, by ${when(o.deadline)}` : "", "."] }), _jsx("label", { htmlFor: "theoo-sell-reference", style: label, children: "The Espees transaction reference" }), _jsx("input", { id: "theoo-sell-reference", autoComplete: "off", value: reference, onChange: (ev) => setReference(ev.target.value), style: s.input }), _jsx("button", { type: "button", style: { ...s.button, opacity: e.busy || reference.trim().length < 3 ? 0.6 : 1 }, disabled: e.busy || reference.trim().length < 3, onClick: () => void e.sent(reference.trim()), children: e.busy ? "Signing…" : "I've sent it" })] })) : null, o.state === "espees_sent" && o.disputeUntil ? (_jsxs("span", { style: s.tiny, children: ["Unless the desk says it didn't arrive, you're paid by ", when(o.disputeUntil), "."] })) : null, o.state === "awaiting_desk" ? (_jsx("button", { type: "button", style: s.secondary, disabled: e.busy, onClick: () => void e.cancel(), children: "Call it off" })) : null, !e.live ? (_jsx("button", { type: "button", style: s.secondary, onClick: e.dismiss, children: "Sell more" })) : null, error] }));
285
+ return (_jsxs("div", { style: { display: "grid", gap: 10 }, "data-theoo-sell": o.state, children: [_jsxs("strong", { style: { fontSize: 15.5, letterSpacing: "-0.01em" }, children: ["Selling ", o.espees.display, " ESPEES for ", o.receive.display, " ", o.receive.symbol] }), _jsx("span", { style: { fontSize: 13.5, lineHeight: 1.5 }, children: SELL_WORDS[o.state] ?? o.state }), o.state === "awaiting_espees" && o.sendTo ? (_jsxs("div", { style: { display: "grid", gap: 8, padding: 12, borderRadius: t.radius, background: t.accentSoft, fontSize: 13.5, lineHeight: 1.6 }, children: [_jsxs("span", { children: ["Send ", _jsxs("strong", { children: [o.espees.display, " ESPEES"] }), " from ", _jsx("strong", { children: o.espeesFrom }), " to ", _jsx("strong", { "data-sell-send-to": "", children: o.sendTo }), " ", "with note ", _jsx("strong", { style: { fontFamily: t.mono }, "data-sell-memo": "", children: o.memo }), o.deadline ? ` by ${when(o.deadline)}` : "", "."] }), _jsx("label", { htmlFor: "theoo-sell-reference", style: label, children: "Transaction reference" }), _jsx("input", { id: "theoo-sell-reference", autoComplete: "off", value: reference, onChange: (ev) => setReference(ev.target.value), style: s.input }), _jsx("button", { type: "button", style: { ...s.button, opacity: e.busy || reference.trim().length < 3 ? 0.6 : 1 }, disabled: e.busy || reference.trim().length < 3, onClick: () => void e.sent(reference.trim()), children: e.busy ? "Confirming…" : "I've sent it" })] })) : null, o.state === "espees_sent" && o.disputeUntil ? (_jsxs("span", { style: s.tiny, children: ["Payout by ", when(o.disputeUntil)] })) : null, o.state === "awaiting_desk" ? (_jsx("button", { type: "button", style: s.secondary, disabled: e.busy, onClick: () => void e.cancel(), children: "Cancel" })) : null, !e.live ? (_jsx("button", { type: "button", style: s.secondary, onClick: e.dismiss, children: "Sell more" })) : null, error] }));
275
286
  }
276
287
  if (!sell?.open)
277
- return _jsx("p", { style: s.muted, children: "The desk isn't buying ESPEES on this chain right now." });
278
- return (_jsxs("div", { style: { display: "grid", gap: 12 }, "data-theoo-sell": "", children: [_jsxs("div", { style: { display: "flex", flexWrap: "wrap", justifyContent: "space-between", alignItems: "baseline", gap: "4px 10px" }, children: [_jsx("strong", { style: { fontSize: 15.5, letterSpacing: "-0.01em", whiteSpace: "nowrap" }, children: "Sell ESPEES" }), _jsx("span", { style: s.tiny, children: "The desk locks your payout before you send anything" })] }), _jsxs("div", { style: { display: "grid", gap: 6 }, children: [_jsx("label", { htmlFor: "theoo-sell-amount", style: label, children: "You sell" }), _jsxs("div", { style: row, children: [_jsx("input", { id: "theoo-sell-amount", inputMode: "decimal", autoComplete: "off", placeholder: "0.00", value: amount, onChange: (ev) => setAmount(ev.target.value), style: { ...s.input, fontFamily: t.mono, fontSize: 17 }, "aria-invalid": parsed.problem ? true : undefined }), _jsx("select", { "aria-label": "Be paid in", value: token, onChange: (ev) => setToken(ev.target.value), style: select, children: tokens.map((d) => (_jsx("option", { value: d.token.address, children: d.token.symbol }, d.token.address))) })] }), parsed.problem ? _jsx("p", { style: { ...s.tiny, color: t.alert }, children: parsed.problem }) : null, chosen ? (_jsxs("p", { style: s.tiny, children: [estimate ? `You get about ${estimate} ${chosen.token.symbol} · ` : "", chosen.price, " ESPEES per ", chosen.token.symbol] })) : null] }), _jsxs("div", { style: { display: "grid", gap: 6 }, children: [_jsx("label", { htmlFor: "theoo-sell-from", style: label, children: "The Espees wallet you'll send from" }), _jsx("input", { id: "theoo-sell-from", autoComplete: "off", value: from, onChange: (ev) => setFrom(ev.target.value), style: s.input }), _jsx("label", { htmlFor: "theoo-sell-again", style: label, children: "The same address, again" }), _jsx("input", { id: "theoo-sell-again", autoComplete: "off", value: again, onChange: (ev) => setAgain(ev.target.value), style: s.input, "aria-invalid": mismatch ? true : undefined }), mismatch ? _jsx("p", { style: { ...s.tiny, color: t.alert }, children: "The two addresses don't match." }) : null] }), q && !e.expired ? (_jsxs("div", { style: { display: "grid", gap: 6, padding: 12, borderRadius: t.radius, background: t.accentSoft, fontSize: 13.5, lineHeight: 1.5 }, "data-theoo-quote": "", children: [_jsx("span", { children: q.summary }), _jsxs("span", { style: s.tiny, children: ["Price held for ", e.secondsLeft, "s."] })] })) : null, q && e.expired ? _jsx("p", { style: s.muted, children: "That price expired. Ask for a fresh one." }) : null, error, !q || e.expired ? (_jsx("button", { type: "button", style: { ...s.button, opacity: !parsed.units || e.busy ? 0.6 : 1 }, disabled: !parsed.units || e.busy || !chosen || !from.trim() || from.trim() !== again.trim(), onClick: () => chosen && parsed.units && e.ask({ receiveToken: chosen.token.address, espeesAmount: parsed.units, espeesFrom: from, espeesFromConfirm: again }), children: e.busy ? "Asking the desk…" : q ? "Get a fresh price" : "Get a price" })) : (_jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr auto", gap: 8 }, children: [_jsx("button", { type: "button", style: { ...s.button, opacity: e.busy ? 0.6 : 1 }, disabled: e.busy, onClick: () => void e.place(), children: e.busy ? "Placing…" : "Place the order" }), _jsx("button", { type: "button", style: s.secondary, disabled: e.busy, onClick: e.reset, children: "Cancel" })] }))] }));
288
+ return _jsx("p", { style: s.muted, children: "Not available right now." });
289
+ return (_jsxs("div", { style: { display: "grid", gap: 12 }, "data-theoo-sell": "", children: [_jsx("div", { style: { display: "flex", flexWrap: "wrap", justifyContent: "space-between", alignItems: "baseline", gap: "4px 10px" }, children: _jsx("strong", { style: { fontSize: 15.5, letterSpacing: "-0.01em", whiteSpace: "nowrap" }, children: "Sell ESPEES" }) }), _jsxs("div", { style: { display: "grid", gap: 6 }, children: [_jsx("label", { htmlFor: "theoo-sell-amount", style: label, children: "You sell" }), _jsxs("div", { style: row, children: [_jsx("input", { id: "theoo-sell-amount", inputMode: "decimal", autoComplete: "off", placeholder: "0.00", value: amount, onChange: (ev) => setAmount(ev.target.value), style: { ...s.input, fontFamily: t.mono, fontSize: 17 }, "aria-invalid": parsed.problem ? true : undefined }), _jsx("select", { "aria-label": "Be paid in", value: token, onChange: (ev) => setToken(ev.target.value), style: select, children: tokens.map((d) => (_jsx("option", { value: d.token.address, children: d.token.symbol }, d.token.address))) })] }), parsed.problem ? _jsx("p", { style: { ...s.tiny, color: t.alert }, children: parsed.problem }) : null, chosen ? (_jsxs("p", { style: s.tiny, children: [estimate ? `You get about ${estimate} ${chosen.token.symbol} · ` : "", chosen.price, " ESPEES per ", chosen.token.symbol] })) : null] }), _jsxs("div", { style: { display: "grid", gap: 6 }, children: [_jsx("label", { htmlFor: "theoo-sell-from", style: label, children: "Espees address" }), _jsx("input", { id: "theoo-sell-from", autoComplete: "off", value: from, onChange: (ev) => setFrom(ev.target.value), style: s.input }), _jsx("label", { htmlFor: "theoo-sell-again", style: label, children: "Confirm address" }), _jsx("input", { id: "theoo-sell-again", autoComplete: "off", value: again, onChange: (ev) => setAgain(ev.target.value), style: s.input, "aria-invalid": mismatch ? true : undefined }), mismatch ? _jsx("p", { style: { ...s.tiny, color: t.alert }, children: "The two addresses don't match." }) : null] }), q && !e.expired ? (_jsxs("div", { style: { display: "grid", gap: 6, padding: 12, borderRadius: t.radius, background: t.accentSoft, fontSize: 13.5, lineHeight: 1.5 }, "data-theoo-quote": "", children: [_jsx("span", { children: q.summary }), _jsxs("span", { style: s.tiny, children: [e.secondsLeft, "s"] })] })) : null, q && e.expired ? _jsx("p", { style: s.muted, children: "Price expired." }) : null, error, !q || e.expired ? (_jsx("button", { type: "button", style: { ...s.button, opacity: !parsed.units || e.busy ? 0.6 : 1 }, disabled: !parsed.units || e.busy || !chosen || !from.trim() || from.trim() !== again.trim(), onClick: () => chosen && parsed.units && e.ask({ receiveToken: chosen.token.address, espeesAmount: parsed.units, espeesFrom: from, espeesFromConfirm: again }), children: e.busy ? "Getting a price…" : q ? "Refresh price" : "Get a price" })) : (_jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr auto", gap: 8 }, children: [_jsx("button", { type: "button", style: { ...s.button, opacity: e.busy ? 0.6 : 1 }, disabled: e.busy, onClick: () => void e.place(), children: e.busy ? "Placing…" : "Place order" }), _jsx("button", { type: "button", style: s.secondary, disabled: e.busy, onClick: e.reset, children: "Cancel" })] }))] }));
279
290
  }
280
291
  /** The newest order of a kind, with its state in words. */
281
292
  function Latest({ order, words }) {
282
293
  if (!order)
283
294
  return null;
284
- return (_jsxs("div", { style: { display: "grid", gap: 4, borderTop: `1px solid ${t.line}`, paddingTop: 10 }, "data-money-latest": order.state, children: [_jsx("span", { style: label, children: "Your latest" }), _jsx("span", { style: { fontSize: 13.5 }, children: words[order.state] ?? STATE_WORDS[order.state] ?? order.state })] }));
295
+ return (_jsxs("div", { style: { display: "grid", gap: 4, borderTop: `1px solid ${t.line}`, paddingTop: 10 }, "data-money-latest": order.state, children: [_jsx("span", { style: label, children: "Latest" }), _jsx("span", { style: { fontSize: 13.5 }, children: words[order.state] ?? STATE_WORDS[order.state] ?? order.state })] }));
285
296
  }
286
- /** Buying with a card or a bank transfer, on MoonPay's own page. */
287
- function BuyPanel({ chain }) {
297
+ const CHAIN_NAMES = { 1: "Ethereum", 56: "BNB Chain", 97: "BNB testnet", 8453: "Base", 84532: "Base Sepolia", 11155111: "Sepolia" };
298
+ const chainLabel = (c) => CHAIN_NAMES[c] ?? `chain ${c}`;
299
+ // `received`: the naira (or dollars) reached the user's Bachs balance.
300
+ // `processing`: Bachs is paying it on. `awaiting_payout`: Bachs refused the
301
+ // payout for now (a limit, a balance); the settler tries again.
302
+ const BACHS_WORDS = {
303
+ received: "Received",
304
+ processing: "Processing",
305
+ awaiting_payout: "Pending",
306
+ completed: "Completed",
307
+ failed: "Failed",
308
+ };
309
+ /**
310
+ * Naira in and out through the user's own Bachs account. Bachs does the KYC
311
+ * on its own page and holds the naira in the user's own balance there; Theoo
312
+ * only passes it on, to this user's own account or their own bank.
313
+ *
314
+ * None of that is said on screen, Bachs's name included: the user sees an
315
+ * identity check, then an account number. Bachs's fees (1% of a deposit, at
316
+ * most ₦300, and 1% with a minimum of 1 to pay it on as a stablecoin) show
317
+ * up only as "Fees apply"; the project's own terms carry the detail.
318
+ */
319
+ function BachsPanel({ mode }) {
288
320
  const theoo = useTheoo();
289
- const { balances } = useBalances(chain);
290
- const { orders, reload } = useMoneyOrders(10);
291
- const stables = (balances?.balances ?? []).filter((b) => b.token.stable);
292
- const [token, setToken] = useState("");
293
- const [fiat, setFiat] = useState("ngn");
294
- const [amount, setAmount] = useState("");
295
- const [busy, setBusy] = useState(false);
321
+ const { orders, reload: reloadOrders } = useMoneyOrders(10);
322
+ const [st, setSt] = useState(null);
296
323
  const [error, setError] = useState(null);
297
- useEffect(() => {
298
- if (!token && stables.length)
299
- setToken(stables[0].token.address);
300
- }, [stables, token]);
301
- const go = async () => {
302
- setBusy(true);
303
- setError(null);
324
+ const [busy, setBusy] = useState(false);
325
+ const [email, setEmail] = useState("");
326
+ const [first, setFirst] = useState("");
327
+ const [last, setLast] = useState("");
328
+ const [choice, setChoice] = useState("");
329
+ const [banks, setBanks] = useState([]);
330
+ const [bankCode, setBankCode] = useState("");
331
+ const [accountNumber, setAccountNumber] = useState("");
332
+ const [changing, setChanging] = useState(false);
333
+ const load = useCallback(async () => {
304
334
  try {
305
- const start = await theoo.me.money.buy({ chain, token, fiatCurrency: fiat, fiatAmount: amount.trim() || undefined });
306
- window.open(start.url, "_blank", "noopener");
307
- await reload();
335
+ setSt(await theoo.me.money.bachs.status());
308
336
  }
309
337
  catch (e) {
310
338
  setError(e instanceof Error ? e.message : String(e));
311
339
  }
312
- finally {
313
- setBusy(false);
314
- }
315
- };
316
- return (_jsxs("div", { style: { display: "grid", gap: 12 }, "data-theoo-buy": "", children: [_jsx("strong", { style: { fontSize: 15.5, letterSpacing: "-0.01em", whiteSpace: "nowrap" }, children: "Add money" }), _jsxs("div", { style: row, children: [_jsx("input", { "aria-label": "Amount to pay", inputMode: "decimal", placeholder: "20000", value: amount, onChange: (e) => setAmount(e.target.value), style: { ...s.input, fontFamily: t.mono, fontSize: 17 } }), _jsxs("select", { "aria-label": "Pay in", value: fiat, onChange: (e) => setFiat(e.target.value), style: select, children: [_jsx("option", { value: "ngn", children: "NGN" }), _jsx("option", { value: "usd", children: "USD" })] })] }), _jsx("select", { "aria-label": "Token to receive", value: token, onChange: (e) => setToken(e.target.value), style: { ...select, width: "100%" }, children: stables.map((b) => (_jsxs("option", { value: b.token.address, children: ["Receive ", b.token.symbol] }, b.token.address))) }), _jsx("p", { style: s.tiny, children: "MoonPay checks your identity and takes the payment, by card or bank transfer. The tokens arrive in this account: its address is fixed in the link, so nobody can change it." }), error ? (_jsx("div", { style: s.error, role: "alert", children: error })) : null, _jsx("button", { type: "button", style: { ...s.button, opacity: busy || !token ? 0.6 : 1 }, disabled: busy || !token, onClick: () => void go(), children: busy ? "Opening MoonPay…" : "Continue to MoonPay" }), _jsx(Latest, { order: (orders ?? []).find((o) => o.kind === "onramp"), words: { awaiting_payment: "Waiting for your payment on MoonPay.", processing: "Paid. MoonPay is sending your tokens.", completed: "Arrived in your account.", failed: "MoonPay couldn't complete it; nothing was taken from this account." } })] }));
317
- }
318
- /** Cashing out to a Nigerian bank account through Yellow Card. */
319
- function CashoutPanel({ chain, sender, onDone }) {
320
- const theoo = useTheoo();
321
- const { balances, reload } = useBalances(chain);
322
- const { orders, reload: reloadOrders } = useMoneyOrders(10);
323
- const [banks, setBanks] = useState(null);
324
- const [bank, setBank] = useState("");
325
- const [number, setNumber] = useState("");
326
- const [name, setName] = useState(null);
327
- const [confirmed, setConfirmed] = useState(false);
328
- const [token, setToken] = useState("");
329
- const [amount, setAmount] = useState("");
330
- const [quote, setQuote] = useState(null);
331
- const [busy, setBusy] = useState(false);
332
- const [error, setError] = useState(null);
333
- const stables = (balances?.balances ?? []).filter((b) => b.token.stable);
334
- const chosen = stables.find((b) => b.token.address === token);
335
- useEffect(() => {
336
- theoo.money.banks().then(setBanks, (e) => setError(e instanceof Error ? e.message : String(e)));
337
340
  }, [theoo]);
338
341
  useEffect(() => {
339
- if (!token && stables.length)
340
- setToken(stables[0].token.address);
341
- }, [stables, token]);
342
+ void load();
343
+ }, [load]);
344
+ // While Bachs is still deciding, or money is moving, keep it current.
345
+ const waiting = !!st?.linked && (mode === "buy" ? !st.depositsActive : !st.payoutsActive);
346
+ const moving = (orders ?? []).some((o) => (o.kind === "onramp" || o.kind === "offramp") && ["received", "processing", "awaiting_payout"].includes(o.state));
342
347
  useEffect(() => {
343
- setName(null);
344
- setConfirmed(false);
345
- }, [bank, number]);
346
- useEffect(() => setQuote(null), [bank, number, token, amount, confirmed]);
348
+ if (!waiting && !moving)
349
+ return;
350
+ const id = setInterval(() => {
351
+ void load();
352
+ void reloadOrders();
353
+ }, 8000);
354
+ return () => clearInterval(id);
355
+ }, [waiting, moving, load, reloadOrders]);
356
+ useEffect(() => {
357
+ if (mode === "cashout" && st?.payoutsActive && banks.length === 0)
358
+ theoo.money.bachsBanks().then(setBanks, () => undefined);
359
+ }, [mode, st?.payoutsActive, banks.length, theoo]);
347
360
  const act = async (f) => {
348
361
  setBusy(true);
349
362
  setError(null);
@@ -357,25 +370,45 @@ function CashoutPanel({ chain, sender, onDone }) {
357
370
  setBusy(false);
358
371
  }
359
372
  };
360
- if (!sender) {
361
- return _jsx("p", { style: s.muted, children: "Cashing out needs the account holder's details (name, NIN and BVN), which this app hasn't supplied." });
373
+ const verify = () => act(async () => {
374
+ const { url } = await theoo.me.money.bachs.onboard({ email, firstName: first || undefined, lastName: last || undefined, returnUrl: window.location.href.split("#")[0] });
375
+ window.open(url, "_blank", "noopener");
376
+ await load();
377
+ });
378
+ const errorBox = error ? (_jsx("div", { style: s.error, role: "alert", children: error })) : null;
379
+ if (!st)
380
+ return _jsx("p", { style: s.muted, children: "Loading\u2026" });
381
+ // Either the server has no Bachs key, or this project hasn't turned the flow on.
382
+ if (!st.available || (mode === "buy" ? !st.buyEnabled : !st.cashoutEnabled))
383
+ return _jsx("p", { style: s.muted, children: "Not available yet." });
384
+ const head = _jsx("strong", { style: { fontSize: 15.5, letterSpacing: "-0.01em" }, children: mode === "buy" ? "Add money" : "Cash out" });
385
+ if (!st.linked) {
386
+ return (
387
+ // Bachs opens an account in the user's name and checks their ID and BVN on its own page.
388
+ _jsxs("div", { style: { display: "grid", gap: 10 }, "data-theoo-bachs": "unlinked", children: [head, _jsx("label", { htmlFor: "theoo-bachs-email", style: label, children: "Email" }), _jsx("input", { id: "theoo-bachs-email", type: "email", autoComplete: "email", value: email, onChange: (e) => setEmail(e.target.value), style: s.input }), _jsxs("div", { style: row, children: [_jsx("input", { "aria-label": "First name", placeholder: "First name", autoComplete: "given-name", value: first, onChange: (e) => setFirst(e.target.value), style: s.input }), _jsx("input", { "aria-label": "Last name", placeholder: "Last name", autoComplete: "family-name", value: last, onChange: (e) => setLast(e.target.value), style: s.input })] }), errorBox, _jsx("button", { type: "button", style: { ...s.button, opacity: busy || !email.includes("@") ? 0.6 : 1 }, disabled: busy || !email.includes("@"), onClick: () => void verify(), children: busy ? "Opening…" : "Verify identity" })] }));
362
389
  }
363
- let units = null;
364
- try {
365
- units = chosen && amount.trim() ? parseUnits(amount, chosen.token.decimals) : null;
390
+ if (waiting) {
391
+ return (_jsxs("div", { style: { display: "grid", gap: 10 }, "data-theoo-bachs": "verifying", children: [head, _jsx("p", { style: s.muted, children: "Verification in progress." }), errorBox, _jsx("button", { type: "button", style: s.secondary, disabled: busy, onClick: () => void verify(), children: "Continue verification" })] }));
366
392
  }
367
- catch {
368
- units = null;
393
+ const mine = (orders ?? []).filter((o) => o.kind === (mode === "buy" ? "onramp" : "offramp")).slice(0, 4);
394
+ const history = mine.length ? (_jsx("div", { style: { display: "grid", gap: 6, borderTop: `1px solid ${t.line}`, paddingTop: 10 }, children: mine.map((o) => (_jsxs("div", { style: { display: "flex", justifyContent: "space-between", gap: 8, fontSize: 13 }, "data-bachs-order": o.state, children: [_jsxs("span", { children: [o.pay ? `${o.pay.display} ${o.pay.symbol}` : "", o.get ? ` → ${o.get.display} ${o.get.symbol}` : ""] }), _jsx("span", { style: s.tiny, children: BACHS_WORDS[o.state] ?? o.state })] }, o.id))) })) : null;
395
+ if (mode === "buy") {
396
+ if (!st.receive || changing) {
397
+ const options = st.receiveOptions;
398
+ const selected = choice || (options[0] ? `${options[0].chain}:${options[0].asset}` : "");
399
+ return (_jsxs("div", { style: { display: "grid", gap: 10 }, "data-theoo-bachs": "choose", children: [head, _jsx("label", { htmlFor: "theoo-bachs-receive", style: label, children: "Receive as" }), options.length === 0 ? (_jsx("p", { style: s.muted, children: "Not available yet." })) : (_jsx("select", { id: "theoo-bachs-receive", value: selected, onChange: (e) => setChoice(e.target.value), style: { ...select, width: "100%" }, children: options.map((o) => (_jsxs("option", { value: `${o.chain}:${o.asset}`, children: [o.asset, " on ", chainLabel(o.chain)] }, `${o.chain}:${o.asset}`))) })), errorBox, _jsx("button", { type: "button", style: { ...s.button, opacity: busy || !selected ? 0.6 : 1 }, disabled: busy || !selected, onClick: () => void act(async () => {
400
+ const [c, a] = selected.split(":");
401
+ setSt(await theoo.me.money.bachs.setReceive(Number(c), a));
402
+ setChanging(false);
403
+ }), children: busy ? "Saving…" : "Continue" })] }));
404
+ }
405
+ return (_jsxs("div", { style: { display: "grid", gap: 10 }, "data-theoo-bachs": "ready", children: [head, st.nairaAccount ? (_jsxs("div", { style: { display: "grid", gap: 4, padding: 12, borderRadius: t.radius, background: t.accentSoft }, children: [_jsx("span", { style: label, children: "Send naira to" }), _jsx("strong", { style: { fontFamily: t.mono, fontSize: 20, letterSpacing: "0.04em" }, "data-bachs-account-number": "", children: st.nairaAccount.accountNumber }), _jsx("span", { style: { fontSize: 13.5 }, children: st.nairaAccount.bankName })] })) : (_jsx("p", { style: s.muted, children: "Setting up your account number\u2026" })), _jsxs("p", { style: s.tiny, children: ["Arrives as ", st.receive.asset, " on ", chainLabel(st.receive.chain), ". Fees apply.", " ", _jsx("button", { type: "button", style: link, onClick: () => setChanging(true), children: "Change" })] }), errorBox, history] }));
369
406
  }
370
- return (_jsxs("div", { style: { display: "grid", gap: 12 }, "data-theoo-cashout": "", children: [_jsx("strong", { style: { fontSize: 15.5, letterSpacing: "-0.01em", whiteSpace: "nowrap" }, children: "Cash out to a bank" }), _jsxs("select", { "aria-label": "Bank", value: bank, onChange: (e) => setBank(e.target.value), style: { ...select, width: "100%" }, children: [_jsx("option", { value: "", children: banks ? "Choose a bank" : "Loading banks…" }), (banks ?? []).map((b) => (_jsx("option", { value: b.id, children: b.name }, b.id)))] }), _jsxs("div", { style: row, children: [_jsx("input", { "aria-label": "Account number", inputMode: "numeric", placeholder: "10 digits", value: number, onChange: (e) => setNumber(e.target.value), style: s.input }), _jsx("button", { type: "button", style: s.secondary, disabled: busy || !bank || number.trim().length !== 10, onClick: () => void act(async () => setName((await theoo.me.money.resolveAccount(bank, number)).accountName)), children: "Check" })] }), name ? (_jsxs("label", { style: { display: "flex", gap: 8, fontSize: 13.5 }, children: [_jsx("input", { type: "checkbox", checked: confirmed, onChange: (e) => setConfirmed(e.target.checked) }), "Pay ", name] })) : null, _jsxs("div", { style: row, children: [_jsx("input", { "aria-label": "Amount to cash out", inputMode: "decimal", placeholder: "0.00", value: amount, onChange: (e) => setAmount(e.target.value), style: { ...s.input, fontFamily: t.mono, fontSize: 17 } }), _jsx("select", { "aria-label": "Cash out from", value: token, onChange: (e) => setToken(e.target.value), style: select, children: stables.map((b) => (_jsx("option", { value: b.token.address, children: b.token.symbol }, b.token.address))) })] }), quote ? (_jsx("div", { style: { display: "grid", gap: 6, padding: 12, borderRadius: t.radius, background: t.accentSoft, fontSize: 13.5 }, "data-theoo-quote": "", children: quote.summary })) : null, error ? (_jsx("div", { style: s.error, role: "alert", children: error })) : null, !quote ? (_jsx("button", { type: "button", style: { ...s.button, opacity: busy ? 0.6 : 1 }, disabled: busy || !confirmed || !units || !name, onClick: () => void act(async () => {
371
- setQuote(await theoo.me.money.quoteCashout({ chain, token, amount: units, bank, accountNumber: number.trim(), accountName: name, sender }));
372
- }), children: busy ? "Getting a quote…" : "Get a quote" })) : (_jsx("button", { type: "button", style: { ...s.button, opacity: busy ? 0.6 : 1 }, disabled: busy, onClick: () => void act(async () => {
373
- const status = await theoo.me.money.cashOut(quote);
374
- setQuote(null);
375
- onDone?.(status);
376
- await reloadOrders();
377
- [0, 2000, 5000].forEach((ms) => setTimeout(() => void reload(), ms));
378
- }), children: busy ? "Sending…" : "Send with passkey" })), _jsx(Latest, { order: (orders ?? []).find((o) => o.kind === "offramp"), words: { awaiting_signature: "Waiting for your passkey.", processing: "Sent. Yellow Card is paying the bank.", completed: "Paid into the bank account.", refunded: "The payout failed; your tokens came back to this account.", failed: "The transfer failed on chain; nothing was taken." } })] }));
407
+ // Cashing out.
408
+ return (_jsxs("div", { style: { display: "grid", gap: 10 }, "data-theoo-bachs": "cashout", children: [head, st.bank && !changing ? (_jsxs("div", { style: { display: "grid", gap: 4, padding: 12, borderRadius: t.radius, background: t.accentSoft }, children: [_jsx("span", { style: label, children: "Paid to" }), _jsx("strong", { style: { fontSize: 15 }, "data-bachs-account-name": "", children: st.bank.accountName }), _jsxs("span", { style: { fontSize: 13.5 }, children: [st.bank.bankName, " \u2022\u2022", st.bank.last4] }), _jsx("button", { type: "button", style: { ...link, justifySelf: "start" }, onClick: () => setChanging(true), children: "Change" })] })) : (_jsxs(_Fragment, { children: [_jsx("label", { htmlFor: "theoo-bachs-bank", style: label, children: "Your bank" }), _jsxs("select", { id: "theoo-bachs-bank", value: bankCode, onChange: (e) => setBankCode(e.target.value), style: { ...select, width: "100%" }, children: [_jsx("option", { value: "", children: "Choose your bank" }), banks.map((b) => (_jsx("option", { value: b.code, children: b.name }, b.code)))] }), _jsx("label", { htmlFor: "theoo-bachs-account", style: label, children: "Account number" }), _jsx("input", { id: "theoo-bachs-account", inputMode: "numeric", autoComplete: "off", maxLength: 10, value: accountNumber, onChange: (e) => setAccountNumber(e.target.value.replace(/\D/g, "")), style: { ...s.input, fontFamily: t.mono } }), _jsx("p", { style: s.tiny, children: "Must be in your name." }), _jsx("button", { type: "button", style: { ...s.button, opacity: busy || !bankCode || accountNumber.length !== 10 ? 0.6 : 1 }, disabled: busy || !bankCode || accountNumber.length !== 10, onClick: () => void act(async () => {
409
+ setSt(await theoo.me.money.bachs.setBank(bankCode, accountNumber));
410
+ setChanging(false);
411
+ }), children: busy ? "Checking…" : "Save" })] })), !st.cashoutPayInReady ? _jsx("p", { style: s.muted, children: "Coming soon." }) : null, errorBox, history] }));
379
412
  }
380
413
  function Recent({ orders, explorerTx }) {
381
414
  return (_jsxs("div", { style: { display: "grid", gap: 6, borderTop: `1px solid ${t.line}`, paddingTop: 10 }, children: [_jsx("span", { style: label, children: "Recent" }), orders.map((o) => {