@recoilpay/intent-react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,354 @@
1
+ // src/RecoilIntent.tsx
2
+ import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
3
+
4
+ // src/icons.tsx
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ var base = {
7
+ width: 16,
8
+ height: 16,
9
+ viewBox: "0 0 16 16",
10
+ fill: "none",
11
+ stroke: "currentColor",
12
+ strokeWidth: 1.5,
13
+ strokeLinecap: "round",
14
+ strokeLinejoin: "round",
15
+ "aria-hidden": true
16
+ };
17
+ var ArrowRight = () => /* @__PURE__ */ jsx("svg", { ...base, children: /* @__PURE__ */ jsx("path", { d: "M3 8h10M9 4l4 4-4 4" }) });
18
+ var Check = () => /* @__PURE__ */ jsx("svg", { ...base, children: /* @__PURE__ */ jsx("path", { d: "M3.5 8.5l3 3 6-7" }) });
19
+ var Cross = () => /* @__PURE__ */ jsx("svg", { ...base, children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" }) });
20
+ var Alert = () => /* @__PURE__ */ jsxs("svg", { ...base, children: [
21
+ /* @__PURE__ */ jsx("circle", { cx: "8", cy: "8", r: "6" }),
22
+ /* @__PURE__ */ jsx("path", { d: "M8 5v3.5M8 11h.01" })
23
+ ] });
24
+ var Spinner = () => /* @__PURE__ */ jsx("svg", { ...base, className: "rp-spin", children: /* @__PURE__ */ jsx("path", { d: "M8 2a6 6 0 1 0 6 6" }) });
25
+ var Route = () => /* @__PURE__ */ jsxs("svg", { ...base, children: [
26
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "12", r: "1.5" }),
27
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "4", r: "1.5" }),
28
+ /* @__PURE__ */ jsx("path", { d: "M5.5 12H10a2 2 0 0 0 0-4H6a2 2 0 0 1 0-4h4.5" })
29
+ ] });
30
+ var External = () => /* @__PURE__ */ jsx("svg", { ...base, width: 12, height: 12, children: /* @__PURE__ */ jsx("path", { d: "M6 3H3v10h10v-3M9 3h4v4M13 3L7 9" }) });
31
+
32
+ // src/useRecoilIntent.ts
33
+ import { createIntentSession } from "@recoilpay/intent-core";
34
+ import { useEffect, useState, useSyncExternalStore } from "react";
35
+ function useRecoilIntent(options = {}) {
36
+ const [session] = useState(() => options.session ?? createIntentSession({ apiUrl: options.apiUrl, hfAccessToken: options.hfAccessToken }));
37
+ const state = useSyncExternalStore(session.subscribe, session.getState, session.getState);
38
+ const wallet = options.wallet ?? null;
39
+ useEffect(() => session.setWallet(wallet), [session, wallet]);
40
+ const hfAccessToken = options.hfAccessToken;
41
+ useEffect(() => {
42
+ if (hfAccessToken !== void 0) session.setHfAccessToken(hfAccessToken);
43
+ }, [session, hfAccessToken]);
44
+ return { ...state, run: session.run, confirm: session.confirm, reset: session.reset, session };
45
+ }
46
+
47
+ // src/RecoilIntent.tsx
48
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
49
+ var DEFAULT_EXAMPLES = [
50
+ { label: "Swap 1 USDC from OP Sepolia to Base Sepolia", sentence: "swap 1 USDC on op sepolia for USDC on base sepolia" },
51
+ { label: "Swap 5 USDC from Base Sepolia to OP Sepolia", sentence: "swap 5 USDC on base sepolia for USDC on op sepolia" }
52
+ ];
53
+ var BUSY = ["switchingChain", "approving", "signing", "submitting"];
54
+ var CARD_PHASES = ["quoted", "needsWallet", ...BUSY];
55
+ function themeVars(theme = {}) {
56
+ const vars = {};
57
+ if (theme.accent) vars["--rp-accent"] = theme.accent;
58
+ if (theme.accentText) vars["--rp-accent-text"] = theme.accentText;
59
+ if (theme.radius !== void 0) vars["--rp-radius"] = `${theme.radius}px`;
60
+ if (theme.fontFamily) vars["--rp-font"] = theme.fontFamily;
61
+ return vars;
62
+ }
63
+ function RecoilIntent(props) {
64
+ const intent = useRecoilIntent({ apiUrl: props.apiUrl, session: props.session, wallet: props.wallet, hfAccessToken: props.hfAccessToken });
65
+ const [value, setValue] = useState2(props.defaultValue ?? "");
66
+ const examples = props.examples === false ? [] : props.examples ?? DEFAULT_EXAMPLES;
67
+ useLifecycleCallbacks(intent, props);
68
+ const submit = (sentence = value) => {
69
+ const text = sentence.trim();
70
+ if (text && intent.ready) void intent.run(text);
71
+ };
72
+ const startOver = () => {
73
+ intent.reset();
74
+ setValue("");
75
+ };
76
+ const connect = () => props.onConnectWallet?.();
77
+ const showCard = intent.preview && (CARD_PHASES.includes(intent.phase) || intent.phase === "error" && intent.route);
78
+ return /* @__PURE__ */ jsxs2("div", { className: ["rp-root", props.className].filter(Boolean).join(" "), "data-mode": props.theme?.mode ?? "dark", style: themeVars(props.theme), children: [
79
+ /* @__PURE__ */ jsxs2("div", { className: "rp-composer", children: [
80
+ /* @__PURE__ */ jsxs2("div", { className: "rp-composer-head", children: [
81
+ /* @__PURE__ */ jsxs2("span", { className: "rp-eyebrow", children: [
82
+ /* @__PURE__ */ jsx2("span", { "aria-hidden": "true", children: "/" }),
83
+ "intent"
84
+ ] }),
85
+ /* @__PURE__ */ jsx2("span", { className: "rp-powered", children: "RecoilPay" })
86
+ ] }),
87
+ /* @__PURE__ */ jsxs2(
88
+ "form",
89
+ {
90
+ className: "rp-composer-row",
91
+ onSubmit: (e) => {
92
+ e.preventDefault();
93
+ submit();
94
+ },
95
+ children: [
96
+ /* @__PURE__ */ jsx2(
97
+ "input",
98
+ {
99
+ className: "rp-input",
100
+ value,
101
+ onChange: (e) => setValue(e.target.value),
102
+ placeholder: props.placeholder ?? "swap 1 USDC on OP Sepolia for USDC on Base Sepolia",
103
+ "aria-label": "Describe a swap or send",
104
+ autoComplete: "off",
105
+ spellCheck: false,
106
+ maxLength: 500
107
+ }
108
+ ),
109
+ /* @__PURE__ */ jsx2("button", { type: "submit", className: "rp-run", "aria-label": "Run intent", disabled: !intent.ready || !value.trim(), children: /* @__PURE__ */ jsx2(ArrowRight, {}) })
110
+ ]
111
+ }
112
+ ),
113
+ intent.phase === "idle" && examples.length > 0 && /* @__PURE__ */ jsxs2("div", { className: "rp-examples", children: [
114
+ /* @__PURE__ */ jsx2("span", { className: "rp-examples-label", children: "try" }),
115
+ /* @__PURE__ */ jsx2("div", { className: "rp-example-list", children: examples.map((ex) => /* @__PURE__ */ jsx2(
116
+ "button",
117
+ {
118
+ type: "button",
119
+ className: "rp-example",
120
+ disabled: !intent.ready,
121
+ onClick: () => {
122
+ setValue(ex.sentence);
123
+ submit(ex.sentence);
124
+ },
125
+ children: ex.label
126
+ },
127
+ ex.sentence
128
+ )) })
129
+ ] })
130
+ ] }),
131
+ /* @__PURE__ */ jsxs2("div", { className: "rp-results", "aria-live": "polite", children: [
132
+ intent.assetsError && intent.phase === "idle" && /* @__PURE__ */ jsxs2(Note, { icon: /* @__PURE__ */ jsx2(Alert, {}), children: [
133
+ "Could not reach the solver network.",
134
+ " ",
135
+ /* @__PURE__ */ jsx2("button", { type: "button", className: "rp-link", onClick: () => void intent.session.refreshAssets(), children: "Retry" })
136
+ ] }),
137
+ intent.phase === "parsing" && /* @__PURE__ */ jsx2(Note, { icon: /* @__PURE__ */ jsx2(Spinner, {}), children: "Reading your intent\u2026" }),
138
+ intent.phase === "offTemplate" && intent.hint && /* @__PURE__ */ jsx2(Note, { icon: /* @__PURE__ */ jsx2(Alert, {}), children: intent.hint }),
139
+ intent.phase === "invalid" && /* @__PURE__ */ jsx2("ul", { className: "rp-issues", children: intent.issues.map((issue, i) => /* @__PURE__ */ jsxs2("li", { children: [
140
+ /* @__PURE__ */ jsx2(Alert, {}),
141
+ /* @__PURE__ */ jsx2("span", { children: issue.message })
142
+ ] }, `${issue.field}-${i}`)) }),
143
+ intent.phase === "quoting" && /* @__PURE__ */ jsx2(Note, { icon: /* @__PURE__ */ jsx2(Route, {}), tone: "accent", children: intent.queueTotal > 1 ? `Intent ${intent.queueIndex + 1} of ${intent.queueTotal}: asking solvers for a quote\u2026` : "Asking solvers for a quote\u2026" }),
144
+ intent.phase === "needsWallet" && !intent.preview && /* @__PURE__ */ jsxs2("div", { className: "rp-connect", children: [
145
+ /* @__PURE__ */ jsx2("p", { children: "Connect a wallet to get a quote." }),
146
+ props.onConnectWallet && /* @__PURE__ */ jsx2("button", { type: "button", className: "rp-button", onClick: connect, children: "Connect wallet" })
147
+ ] }),
148
+ intent.queueTotal > 1 && !["idle", "parsing", "offTemplate", "invalid"].includes(intent.phase) && /* @__PURE__ */ jsxs2("div", { className: "rp-queue", children: [
149
+ /* @__PURE__ */ jsxs2("span", { children: [
150
+ "Intent ",
151
+ intent.queueIndex + 1,
152
+ " of ",
153
+ intent.queueTotal
154
+ ] }),
155
+ /* @__PURE__ */ jsx2("div", { className: "rp-queue-track", children: /* @__PURE__ */ jsx2("div", { style: { width: `${(intent.queueIndex + (intent.phase === "done" ? 1 : 0)) / intent.queueTotal * 100}%` } }) })
156
+ ] }),
157
+ showCard && intent.preview && /* @__PURE__ */ jsx2(
158
+ ConfirmCard,
159
+ {
160
+ preview: intent.preview,
161
+ phase: intent.phase,
162
+ needsApproval: intent.needsApproval,
163
+ isConnected: intent.isConnected,
164
+ direct: intent.route === "direct",
165
+ error: intent.phase === "error" ? intent.error : null,
166
+ onConfirm: () => void intent.confirm(),
167
+ onConnect: props.onConnectWallet ? connect : void 0,
168
+ onCancel: startOver
169
+ }
170
+ ),
171
+ (intent.phase === "tracking" || intent.phase === "done") && intent.orderId && /* @__PURE__ */ jsx2(OrderProgress, { status: intent.status, direct: intent.route === "direct", explorerUrl: intent.explorerUrl, onNew: startOver }),
172
+ intent.phase === "error" && !intent.route && /* @__PURE__ */ jsxs2("div", { className: "rp-error", children: [
173
+ /* @__PURE__ */ jsx2("p", { children: intent.error }),
174
+ /* @__PURE__ */ jsx2("button", { type: "button", className: "rp-button rp-button-quiet", onClick: startOver, children: "Dismiss" })
175
+ ] })
176
+ ] })
177
+ ] });
178
+ }
179
+ function useLifecycleCallbacks(intent, props) {
180
+ const prev = useRef(intent);
181
+ const cbs = useRef(props);
182
+ cbs.current = props;
183
+ useEffect2(() => {
184
+ const before = prev.current;
185
+ prev.current = intent;
186
+ if (intent.orderId && intent.orderId !== before.orderId) cbs.current.onOrderSubmitted?.(intent.orderId);
187
+ if (intent.phase === "done" && before.phase !== "done") cbs.current.onComplete?.(intent);
188
+ if (intent.phase === "error" && before.phase !== "error" && intent.error) cbs.current.onError?.(intent.error);
189
+ }, [intent]);
190
+ }
191
+ function Note({ icon, tone = "muted", children }) {
192
+ return /* @__PURE__ */ jsxs2("p", { className: "rp-note", "data-tone": tone, children: [
193
+ /* @__PURE__ */ jsx2("span", { className: "rp-note-icon", children: icon }),
194
+ /* @__PURE__ */ jsx2("span", { children })
195
+ ] });
196
+ }
197
+ var shortAddr = (a) => a.length > 12 ? `${a.slice(0, 6)}\u2026${a.slice(-4)}` : a;
198
+ function actionLabel(phase, isConnected, needsApproval, symbol, direct) {
199
+ if (!isConnected || phase === "needsWallet") return "Connect wallet";
200
+ switch (phase) {
201
+ case "switchingChain":
202
+ return "Switching network\u2026";
203
+ case "approving":
204
+ return `Approving ${symbol}\u2026`;
205
+ case "signing":
206
+ return "Confirm in your wallet\u2026";
207
+ case "submitting":
208
+ return "Submitting\u2026";
209
+ case "error":
210
+ return "Try again";
211
+ default:
212
+ if (direct) return `Send ${symbol}`;
213
+ return needsApproval ? `Approve ${symbol} and swap` : "Confirm and sign";
214
+ }
215
+ }
216
+ function ConfirmCard(p) {
217
+ const busy = BUSY.includes(p.phase);
218
+ const send = p.preview.action === "send";
219
+ const wantsWallet = !p.isConnected || p.phase === "needsWallet";
220
+ const { solverCount, solversQueried } = p.preview;
221
+ return /* @__PURE__ */ jsxs2("div", { className: "rp-card", children: [
222
+ /* @__PURE__ */ jsxs2("div", { className: "rp-card-head", children: [
223
+ /* @__PURE__ */ jsxs2("div", { children: [
224
+ /* @__PURE__ */ jsx2("div", { className: "rp-overline", children: "Review" }),
225
+ /* @__PURE__ */ jsx2("div", { className: "rp-card-title", children: p.direct ? "Direct transfer" : "Best solver route" })
226
+ ] }),
227
+ /* @__PURE__ */ jsxs2("span", { className: "rp-badge", children: [
228
+ /* @__PURE__ */ jsx2(Check, {}),
229
+ " Quoted"
230
+ ] })
231
+ ] }),
232
+ /* @__PURE__ */ jsxs2("div", { className: "rp-route", children: [
233
+ /* @__PURE__ */ jsx2(Amount, { label: send ? "You send" : "You pay", amount: p.preview.payAmount, symbol: p.preview.paySymbol, chain: p.preview.srcChainName }),
234
+ /* @__PURE__ */ jsx2("span", { className: "rp-route-arrow", children: /* @__PURE__ */ jsx2(ArrowRight, {}) }),
235
+ /* @__PURE__ */ jsx2(
236
+ Amount,
237
+ {
238
+ align: "end",
239
+ label: send ? "They receive" : "You receive",
240
+ amount: p.direct ? p.preview.receiveAmount : `~${p.preview.receiveAmount}`,
241
+ symbol: p.preview.receiveSymbol,
242
+ chain: p.preview.dstChainName
243
+ }
244
+ )
245
+ ] }),
246
+ /* @__PURE__ */ jsxs2("dl", { className: "rp-meta", children: [
247
+ /* @__PURE__ */ jsxs2("div", { children: [
248
+ /* @__PURE__ */ jsx2("dt", { children: "Settles in" }),
249
+ /* @__PURE__ */ jsx2("dd", { children: p.preview.etaSeconds ? `~${p.preview.etaSeconds}s` : "Fast" })
250
+ ] }),
251
+ /* @__PURE__ */ jsxs2("div", { children: [
252
+ /* @__PURE__ */ jsx2("dt", { children: p.direct ? "Route" : "Solvers" }),
253
+ /* @__PURE__ */ jsx2("dd", { "data-accent": true, children: p.direct ? "Your wallet" : solversQueried && solversQueried > solverCount ? `${solverCount} of ${solversQueried} quoted` : `${solverCount} quoted` })
254
+ ] }),
255
+ send && p.preview.recipient && /* @__PURE__ */ jsxs2("div", { children: [
256
+ /* @__PURE__ */ jsx2("dt", { children: "Recipient" }),
257
+ /* @__PURE__ */ jsx2("dd", { className: "rp-mono", children: shortAddr(p.preview.recipient) })
258
+ ] })
259
+ ] }),
260
+ p.needsApproval && !busy && !wantsWallet && /* @__PURE__ */ jsxs2("p", { className: "rp-fineprint", children: [
261
+ "First time with ",
262
+ p.preview.paySymbol,
263
+ ": your wallet will ask for a one-time approval, then a signature."
264
+ ] }),
265
+ p.error && /* @__PURE__ */ jsx2("p", { className: "rp-card-error", children: p.error }),
266
+ /* @__PURE__ */ jsxs2("div", { className: "rp-card-actions", children: [
267
+ /* @__PURE__ */ jsxs2(
268
+ "button",
269
+ {
270
+ type: "button",
271
+ className: "rp-button",
272
+ disabled: busy || wantsWallet && !p.onConnect,
273
+ onClick: wantsWallet ? p.onConnect : p.onConfirm,
274
+ children: [
275
+ busy && /* @__PURE__ */ jsx2(Spinner, {}),
276
+ actionLabel(p.phase, p.isConnected, p.needsApproval, p.preview.paySymbol, p.direct)
277
+ ]
278
+ }
279
+ ),
280
+ !busy && /* @__PURE__ */ jsx2("button", { type: "button", className: "rp-link", onClick: p.onCancel, children: "Cancel" })
281
+ ] })
282
+ ] });
283
+ }
284
+ function Amount({ label, amount, symbol, chain, align = "start" }) {
285
+ return /* @__PURE__ */ jsxs2("div", { className: "rp-amount", "data-align": align, children: [
286
+ /* @__PURE__ */ jsx2("div", { className: "rp-overline", children: label }),
287
+ /* @__PURE__ */ jsxs2("div", { className: "rp-amount-value", children: [
288
+ amount,
289
+ " ",
290
+ symbol
291
+ ] }),
292
+ /* @__PURE__ */ jsxs2("div", { className: "rp-amount-chain", children: [
293
+ "on ",
294
+ chain
295
+ ] })
296
+ ] });
297
+ }
298
+ var STEPS = ["Submitted", "Filling", "Filled", "Settling", "Done"];
299
+ function stepOf(status) {
300
+ switch (status) {
301
+ case "executing":
302
+ return 1;
303
+ case "executed":
304
+ return 2;
305
+ case "settled":
306
+ case "settling":
307
+ return 3;
308
+ case "finalized":
309
+ return 4;
310
+ default:
311
+ return 0;
312
+ }
313
+ }
314
+ function OrderProgress({ status, direct, explorerUrl, onNew }) {
315
+ const failed = !!status && typeof status === "object" && "failed" in status;
316
+ const refunded = status === "refunded";
317
+ const finished = status === "finalized";
318
+ const active = stepOf(status);
319
+ const title = failed ? "Order failed" : refunded ? "Refunded" : finished ? direct ? "Sent" : "Settled" : "Settling\u2026";
320
+ return /* @__PURE__ */ jsxs2("div", { className: "rp-card", "data-state": failed || refunded ? "bad" : finished ? "good" : "pending", children: [
321
+ /* @__PURE__ */ jsxs2("div", { className: "rp-card-head", children: [
322
+ /* @__PURE__ */ jsx2("div", { className: "rp-card-title", children: title }),
323
+ explorerUrl && /* @__PURE__ */ jsxs2("a", { className: "rp-link", href: explorerUrl, target: "_blank", rel: "noopener noreferrer", children: [
324
+ "View on explorer ",
325
+ /* @__PURE__ */ jsx2(External, {})
326
+ ] })
327
+ ] }),
328
+ !direct && /* @__PURE__ */ jsx2("ol", { className: "rp-steps", children: STEPS.map((label, i) => {
329
+ const state = failed || refunded ? "bad" : i < active || finished ? "done" : i === active ? "current" : "todo";
330
+ return /* @__PURE__ */ jsxs2("li", { "data-state": state, children: [
331
+ /* @__PURE__ */ jsx2("span", { className: "rp-step-dot", children: state === "bad" ? /* @__PURE__ */ jsx2(Cross, {}) : state === "done" ? /* @__PURE__ */ jsx2(Check, {}) : state === "current" ? /* @__PURE__ */ jsx2(Spinner, {}) : null }),
332
+ /* @__PURE__ */ jsx2("span", { className: "rp-step-label", children: label })
333
+ ] }, label);
334
+ }) }),
335
+ failed && /* @__PURE__ */ jsxs2("p", { className: "rp-card-error", children: [
336
+ status.failed[0],
337
+ ": ",
338
+ status.failed[1]
339
+ ] }),
340
+ (finished || failed || refunded) && /* @__PURE__ */ jsx2("div", { className: "rp-card-actions", children: /* @__PURE__ */ jsx2("button", { type: "button", className: "rp-button rp-button-quiet", onClick: onNew, children: "New intent" }) })
341
+ ] });
342
+ }
343
+
344
+ // src/index.ts
345
+ import {
346
+ createIntentSession as createIntentSession2,
347
+ viemWallet
348
+ } from "@recoilpay/intent-core";
349
+ export {
350
+ RecoilIntent,
351
+ createIntentSession2 as createIntentSession,
352
+ useRecoilIntent,
353
+ viemWallet
354
+ };