@theoo/react 0.4.1 → 0.4.3

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,9 +1,9 @@
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";
6
- import { useBalances, useEspees, useMoneyOrders, useSwap } from "./hooks.js";
6
+ import { useBalances, useEspees, useEspeesSell, useMoneyOrders, useSwap } from "./hooks.js";
7
7
  const s = styles;
8
8
  const t = styles.token;
9
9
  const card = {
@@ -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
- const names = { swap: "Swap", espees: "Buy 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 === "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 }))] }));
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(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,82 +207,156 @@ 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] }));
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
+ */
226
+ const SELL_WORDS = {
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.",
236
+ };
237
+ const when = (secs) => (secs ? new Date(secs * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "");
238
+ /**
239
+ * Selling ESPEES to the desk. The desk goes first: it locks the payout in an
240
+ * escrow on chain, naming this account, and only then is the seller shown
241
+ * where to send. Nothing leaves the account at any step.
242
+ */
243
+ function SellPanel({ chain }) {
244
+ const e = useEspeesSell(chain);
245
+ const sell = e.info?.sell;
246
+ const tokens = sell?.tokens ?? [];
247
+ const [token, setToken] = useState("");
248
+ const [amount, setAmount] = useState("");
249
+ const [from, setFrom] = useState("");
250
+ const [again, setAgain] = useState("");
251
+ const [reference, setReference] = useState("");
252
+ useEffect(() => {
253
+ if (!token && tokens.length)
254
+ setToken(tokens[0].token.address);
255
+ }, [tokens, token]);
256
+ const chosen = tokens.find((d) => d.token.address === token);
257
+ const parsed = useMemo(() => {
258
+ if (!amount.trim())
259
+ return {};
260
+ try {
261
+ const units = parseUnits(amount, 18);
262
+ if (BigInt(units) === 0n)
263
+ return { problem: "Enter an amount." };
264
+ return { units };
265
+ }
266
+ catch {
267
+ return { problem: "Invalid amount." };
268
+ }
269
+ }, [amount]);
270
+ const estimate = chosen && parsed.units ? formatUnits((BigInt(parsed.units) * 10n ** BigInt(chosen.token.decimals)) / BigInt(chosen.priceE18), chosen.token.decimals, 6) : null;
271
+ const mismatch = again.trim() !== "" && from.trim() !== again.trim();
272
+ useEffect(() => {
273
+ if (e.quote)
274
+ e.reset();
275
+ // eslint-disable-next-line react-hooks/exhaustive-deps
276
+ }, [token, amount, from, again]);
277
+ if (!e.info)
278
+ return _jsx("p", { style: s.muted, children: "Loading\u2026" });
279
+ const o = e.order;
280
+ const q = e.quote;
281
+ const error = e.error ? (_jsx("div", { style: s.error, role: "alert", children: e.error.message })) : null;
282
+ // A sale this session has seen: what the seller does next, or how it ended,
283
+ // until they choose to sell more.
284
+ if (o) {
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] }));
286
+ }
287
+ if (!sell?.open)
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" })] }))] }));
214
290
  }
215
291
  /** The newest order of a kind, with its state in words. */
216
292
  function Latest({ order, words }) {
217
293
  if (!order)
218
294
  return null;
219
- 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 })] }));
220
296
  }
221
- /** Buying with a card or a bank transfer, on MoonPay's own page. */
222
- 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 }) {
223
320
  const theoo = useTheoo();
224
- const { balances } = useBalances(chain);
225
- const { orders, reload } = useMoneyOrders(10);
226
- const stables = (balances?.balances ?? []).filter((b) => b.token.stable);
227
- const [token, setToken] = useState("");
228
- const [fiat, setFiat] = useState("ngn");
229
- const [amount, setAmount] = useState("");
230
- const [busy, setBusy] = useState(false);
321
+ const { orders, reload: reloadOrders } = useMoneyOrders(10);
322
+ const [st, setSt] = useState(null);
231
323
  const [error, setError] = useState(null);
232
- useEffect(() => {
233
- if (!token && stables.length)
234
- setToken(stables[0].token.address);
235
- }, [stables, token]);
236
- const go = async () => {
237
- setBusy(true);
238
- 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 () => {
239
334
  try {
240
- const start = await theoo.me.money.buy({ chain, token, fiatCurrency: fiat, fiatAmount: amount.trim() || undefined });
241
- window.open(start.url, "_blank", "noopener");
242
- await reload();
335
+ setSt(await theoo.me.money.bachs.status());
243
336
  }
244
337
  catch (e) {
245
338
  setError(e instanceof Error ? e.message : String(e));
246
339
  }
247
- finally {
248
- setBusy(false);
249
- }
250
- };
251
- 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." } })] }));
252
- }
253
- /** Cashing out to a Nigerian bank account through Yellow Card. */
254
- function CashoutPanel({ chain, sender, onDone }) {
255
- const theoo = useTheoo();
256
- const { balances, reload } = useBalances(chain);
257
- const { orders, reload: reloadOrders } = useMoneyOrders(10);
258
- const [banks, setBanks] = useState(null);
259
- const [bank, setBank] = useState("");
260
- const [number, setNumber] = useState("");
261
- const [name, setName] = useState(null);
262
- const [confirmed, setConfirmed] = useState(false);
263
- const [token, setToken] = useState("");
264
- const [amount, setAmount] = useState("");
265
- const [quote, setQuote] = useState(null);
266
- const [busy, setBusy] = useState(false);
267
- const [error, setError] = useState(null);
268
- const stables = (balances?.balances ?? []).filter((b) => b.token.stable);
269
- const chosen = stables.find((b) => b.token.address === token);
270
- useEffect(() => {
271
- theoo.money.banks().then(setBanks, (e) => setError(e instanceof Error ? e.message : String(e)));
272
340
  }, [theoo]);
273
341
  useEffect(() => {
274
- if (!token && stables.length)
275
- setToken(stables[0].token.address);
276
- }, [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));
277
347
  useEffect(() => {
278
- setName(null);
279
- setConfirmed(false);
280
- }, [bank, number]);
281
- 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]);
282
360
  const act = async (f) => {
283
361
  setBusy(true);
284
362
  setError(null);
@@ -292,25 +370,45 @@ function CashoutPanel({ chain, sender, onDone }) {
292
370
  setBusy(false);
293
371
  }
294
372
  };
295
- if (!sender) {
296
- 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" })] }));
297
389
  }
298
- let units = null;
299
- try {
300
- 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" })] }));
301
392
  }
302
- catch {
303
- 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] }));
304
406
  }
305
- 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 () => {
306
- setQuote(await theoo.me.money.quoteCashout({ chain, token, amount: units, bank, accountNumber: number.trim(), accountName: name, sender }));
307
- }), 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 () => {
308
- const status = await theoo.me.money.cashOut(quote);
309
- setQuote(null);
310
- onDone?.(status);
311
- await reloadOrders();
312
- [0, 2000, 5000].forEach((ms) => setTimeout(() => void reload(), ms));
313
- }), 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] }));
314
412
  }
315
413
  function Recent({ orders, explorerTx }) {
316
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) => {