@b3dotfun/sdk 0.1.69-alpha.11 → 0.1.69-alpha.13

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.
Files changed (59) hide show
  1. package/dist/cjs/anyspend/react/components/checkout/AnySpendCheckout.d.ts +3 -1
  2. package/dist/cjs/anyspend/react/components/checkout/AnySpendCheckout.js +5 -1
  3. package/dist/cjs/global-account/react/components/B3Provider/B3Provider.d.ts +4 -1
  4. package/dist/cjs/global-account/react/components/B3Provider/B3Provider.js +8 -12
  5. package/dist/cjs/global-account/react/components/B3Provider/B3Provider.native.js +6 -9
  6. package/dist/cjs/global-account/react/utils/createWagmiConfig.d.ts +4 -13
  7. package/dist/cjs/global-account/react/utils/createWagmiConfig.js +5 -7
  8. package/dist/cjs/wallet/react/components/ConnectWallet.d.ts +11 -0
  9. package/dist/cjs/wallet/react/components/ConnectWallet.js +467 -0
  10. package/dist/cjs/wallet/react/components/WalletProvider.d.ts +35 -0
  11. package/dist/cjs/wallet/react/components/WalletProvider.js +20 -0
  12. package/dist/cjs/wallet/react/hooks/useWalletDisconnect.d.ts +13 -0
  13. package/dist/cjs/wallet/react/hooks/useWalletDisconnect.js +22 -0
  14. package/dist/cjs/wallet/react/hooks/useWalletState.d.ts +31 -0
  15. package/dist/cjs/wallet/react/hooks/useWalletState.js +63 -0
  16. package/dist/cjs/wallet/react/index.d.ts +5 -0
  17. package/dist/cjs/wallet/react/index.js +16 -0
  18. package/dist/cjs/wallet/utils/createWalletConfig.d.ts +21 -0
  19. package/dist/cjs/wallet/utils/createWalletConfig.js +24 -0
  20. package/dist/esm/anyspend/react/components/checkout/AnySpendCheckout.d.ts +3 -1
  21. package/dist/esm/anyspend/react/components/checkout/AnySpendCheckout.js +5 -1
  22. package/dist/esm/global-account/react/components/B3Provider/B3Provider.d.ts +4 -1
  23. package/dist/esm/global-account/react/components/B3Provider/B3Provider.js +3 -7
  24. package/dist/esm/global-account/react/components/B3Provider/B3Provider.native.js +2 -5
  25. package/dist/esm/global-account/react/utils/createWagmiConfig.d.ts +4 -13
  26. package/dist/esm/global-account/react/utils/createWagmiConfig.js +5 -7
  27. package/dist/esm/wallet/react/components/ConnectWallet.d.ts +11 -0
  28. package/dist/esm/wallet/react/components/ConnectWallet.js +431 -0
  29. package/dist/esm/wallet/react/components/WalletProvider.d.ts +35 -0
  30. package/dist/esm/wallet/react/components/WalletProvider.js +17 -0
  31. package/dist/esm/wallet/react/hooks/useWalletDisconnect.d.ts +13 -0
  32. package/dist/esm/wallet/react/hooks/useWalletDisconnect.js +19 -0
  33. package/dist/esm/wallet/react/hooks/useWalletState.d.ts +31 -0
  34. package/dist/esm/wallet/react/hooks/useWalletState.js +60 -0
  35. package/dist/esm/wallet/react/index.d.ts +5 -0
  36. package/dist/esm/wallet/react/index.js +8 -0
  37. package/dist/esm/wallet/utils/createWalletConfig.d.ts +21 -0
  38. package/dist/esm/wallet/utils/createWalletConfig.js +21 -0
  39. package/dist/types/anyspend/react/components/checkout/AnySpendCheckout.d.ts +3 -1
  40. package/dist/types/global-account/react/components/B3Provider/B3Provider.d.ts +4 -1
  41. package/dist/types/global-account/react/utils/createWagmiConfig.d.ts +4 -13
  42. package/dist/types/wallet/react/components/ConnectWallet.d.ts +11 -0
  43. package/dist/types/wallet/react/components/WalletProvider.d.ts +35 -0
  44. package/dist/types/wallet/react/hooks/useWalletDisconnect.d.ts +13 -0
  45. package/dist/types/wallet/react/hooks/useWalletState.d.ts +31 -0
  46. package/dist/types/wallet/react/index.d.ts +5 -0
  47. package/dist/types/wallet/utils/createWalletConfig.d.ts +21 -0
  48. package/package.json +12 -1
  49. package/src/anyspend/react/components/checkout/AnySpendCheckout.tsx +6 -0
  50. package/src/global-account/react/components/B3Provider/B3Provider.native.tsx +14 -20
  51. package/src/global-account/react/components/B3Provider/B3Provider.tsx +41 -45
  52. package/src/global-account/react/utils/createWagmiConfig.tsx +6 -7
  53. package/src/wallet/__tests__/createWalletConfig.test.ts +39 -0
  54. package/src/wallet/react/components/ConnectWallet.tsx +665 -0
  55. package/src/wallet/react/components/WalletProvider.tsx +64 -0
  56. package/src/wallet/react/hooks/useWalletDisconnect.ts +22 -0
  57. package/src/wallet/react/hooks/useWalletState.ts +93 -0
  58. package/src/wallet/react/index.ts +10 -0
  59. package/src/wallet/utils/createWalletConfig.ts +39 -0
@@ -0,0 +1,665 @@
1
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
2
+ import {
3
+ WalletCoinbase,
4
+ WalletMetamask,
5
+ WalletPhantom,
6
+ WalletRabby,
7
+ WalletRainbow,
8
+ WalletWalletConnect,
9
+ } from "@web3icons/react";
10
+ import { truncateAddress } from "@b3dotfun/sdk/shared/utils";
11
+ import { AlertCircle, Check, ChevronDown, Copy, Loader2, LogOut, Wallet, X } from "lucide-react";
12
+ import { useCallback, useEffect, useRef, useState } from "react";
13
+ import { useWalletDisconnect } from "../hooks/useWalletDisconnect";
14
+ import { useWalletState, type WalletConnector } from "../hooks/useWalletState";
15
+
16
+ /** Injects the spinner keyframe once into the document head (idempotent via DOM id). */
17
+ function SpinnerKeyframes() {
18
+ useEffect(() => {
19
+ if (document.getElementById("b3-spin-keyframes")) return;
20
+ const style = document.createElement("style");
21
+ style.id = "b3-spin-keyframes";
22
+ style.textContent = "@keyframes b3-spin { to { transform: rotate(360deg) } }";
23
+ document.head.appendChild(style);
24
+ }, []);
25
+ return null;
26
+ }
27
+
28
+ export interface ConnectWalletProps {
29
+ /** Theme for the modal and button. Defaults to "light". */
30
+ theme?: "light" | "dark";
31
+ /** Label for the connect button. Defaults to "Connect Wallet". */
32
+ buttonLabel?: string;
33
+ }
34
+
35
+ const WALLET_DOWNLOAD_URLS: Record<string, string> = {
36
+ metamask: "https://metamask.io/download/",
37
+ "coinbase wallet": "https://www.coinbase.com/wallet/downloads",
38
+ phantom: "https://phantom.app/download",
39
+ rabby: "https://rabby.io/",
40
+ rainbow: "https://rainbow.me/download",
41
+ walletconnect: "https://walletconnect.com/",
42
+ };
43
+
44
+ function getWalletIcon(name: string, size = 24) {
45
+ switch (name.toLowerCase()) {
46
+ case "metamask":
47
+ return <WalletMetamask width={size} height={size} />;
48
+ case "coinbase wallet":
49
+ return <WalletCoinbase width={size} height={size} />;
50
+ case "phantom":
51
+ return <WalletPhantom width={size} height={size} />;
52
+ case "walletconnect":
53
+ return <WalletWalletConnect width={size} height={size} />;
54
+ case "rabby wallet":
55
+ case "rabby":
56
+ return <WalletRabby width={size} height={size} />;
57
+ case "rainbow":
58
+ return <WalletRainbow width={size} height={size} />;
59
+ default:
60
+ return <Wallet width={size} height={size} />;
61
+ }
62
+ }
63
+
64
+ // B3 brand colors
65
+ const B3_BLUE = "#3368ef";
66
+ const B3_BLUE_HOVER = "#2554d4";
67
+
68
+ const theme = {
69
+ light: {
70
+ bg: "#ffffff",
71
+ cardBg: "#ffffff",
72
+ text: "#1a1a2e",
73
+ textSecondary: "#64748b",
74
+ textTertiary: "#94a3b8",
75
+ border: "#D1D1D6",
76
+ borderLight: "#e2e8f0",
77
+ hoverBg: "#f1f5f9",
78
+ iconBg: "#f1f5f9",
79
+ shadow:
80
+ "0 20px 24px -4px rgba(10,13,18,0.08), 0 8px 8px -4px rgba(10,13,18,0.03), 0 3px 3px -1.5px rgba(10,13,18,0.04)",
81
+ popoverShadow: "0 10px 30px -5px rgba(10,13,18,0.1), 0 4px 6px -4px rgba(10,13,18,0.05)",
82
+ overlayBg: "rgba(0,0,0,0.8)",
83
+ badgeInstalledBg: "#ecfdf5",
84
+ badgeInstalledText: "#059669",
85
+ badgeInstallBg: "#f1f5f9",
86
+ badgeInstallText: "#64748b",
87
+ connectedDot: "#10b981",
88
+ disconnectBorder: "#fca5a5",
89
+ disconnectText: "#dc2626",
90
+ disconnectHoverBg: "#fef2f2",
91
+ copyHoverBg: "#f1f5f9",
92
+ },
93
+ dark: {
94
+ bg: "#0f0f14",
95
+ cardBg: "#1a1a24",
96
+ text: "#f1f5f9",
97
+ textSecondary: "#94a3b8",
98
+ textTertiary: "#64748b",
99
+ border: "#2a2a3a",
100
+ borderLight: "#2a2a3a",
101
+ hoverBg: "#22222e",
102
+ iconBg: "#22222e",
103
+ shadow: "0 20px 24px -4px rgba(0,0,0,0.3), 0 8px 8px -4px rgba(0,0,0,0.2)",
104
+ popoverShadow: "0 10px 30px -5px rgba(0,0,0,0.4)",
105
+ overlayBg: "rgba(0,0,0,0.85)",
106
+ badgeInstalledBg: "#052e16",
107
+ badgeInstalledText: "#34d399",
108
+ badgeInstallBg: "#22222e",
109
+ badgeInstallText: "#64748b",
110
+ connectedDot: "#34d399",
111
+ disconnectBorder: "#7f1d1d",
112
+ disconnectText: "#f87171",
113
+ disconnectHoverBg: "#1c1017",
114
+ copyHoverBg: "#22222e",
115
+ },
116
+ };
117
+
118
+ /**
119
+ * Drop-in wallet connection component styled to match the B3 design system.
120
+ * Uses inline styles — works in any app without tailwind configuration.
121
+ */
122
+ export function ConnectWallet({ theme: themeProp = "light", buttonLabel = "Connect Wallet" }: ConnectWalletProps) {
123
+ const { address, isConnected, chain, connectors } = useWalletState();
124
+ const { disconnect } = useWalletDisconnect();
125
+ const [modalOpen, setModalOpen] = useState(false);
126
+ const [popoverOpen, setPopoverOpen] = useState(false);
127
+ const [copied, setCopied] = useState(false);
128
+ const [hoveredUid, setHoveredUid] = useState<string | null>(null);
129
+ const [btnHover, setBtnHover] = useState(false);
130
+ const [disconnectHover, setDisconnectHover] = useState(false);
131
+ const [copyHover, setCopyHover] = useState(false);
132
+ const [connectingUid, setConnectingUid] = useState<string | null>(null);
133
+ const [connectError, setConnectError] = useState<string | null>(null);
134
+ const [connectSuccess, setConnectSuccess] = useState(false);
135
+
136
+ const t = theme[themeProp];
137
+
138
+ const successTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
139
+
140
+ const resetConnectState = useCallback(() => {
141
+ setConnectingUid(null);
142
+ setConnectError(null);
143
+ setConnectSuccess(false);
144
+ if (successTimerRef.current) {
145
+ clearTimeout(successTimerRef.current);
146
+ successTimerRef.current = null;
147
+ }
148
+ }, []);
149
+
150
+ // Cleanup success timer on unmount
151
+ useEffect(() => {
152
+ return () => {
153
+ if (successTimerRef.current) clearTimeout(successTimerRef.current);
154
+ };
155
+ }, []);
156
+
157
+ const handleConnect = useCallback(
158
+ async (connector: WalletConnector) => {
159
+ if (!connector.isInstalled) {
160
+ const url = WALLET_DOWNLOAD_URLS[connector.name.toLowerCase()];
161
+ if (url) window.open(url, "_blank", "noopener,noreferrer");
162
+ return;
163
+ }
164
+ setConnectingUid(connector.uid);
165
+ setConnectError(null);
166
+ setConnectSuccess(false);
167
+ try {
168
+ await connector.connect();
169
+ setConnectSuccess(true);
170
+ successTimerRef.current = setTimeout(() => {
171
+ setModalOpen(false);
172
+ resetConnectState();
173
+ }, 800);
174
+ } catch (err: unknown) {
175
+ const message = err instanceof Error ? err.message : "Connection failed";
176
+ if (message.includes("rejected") || message.includes("denied") || message.includes("User rejected")) {
177
+ setConnectError("Connection rejected");
178
+ } else {
179
+ setConnectError(message.length > 60 ? message.slice(0, 60) + "..." : message);
180
+ }
181
+ setConnectingUid(null);
182
+ }
183
+ },
184
+ [resetConnectState],
185
+ );
186
+
187
+ const handleDisconnect = useCallback(() => {
188
+ disconnect();
189
+ setPopoverOpen(false);
190
+ }, [disconnect]);
191
+
192
+ const handleCopyAddress = useCallback(async () => {
193
+ if (address) {
194
+ try {
195
+ await navigator.clipboard.writeText(address);
196
+ setCopied(true);
197
+ } catch {
198
+ // Clipboard API may be denied
199
+ }
200
+ }
201
+ }, [address]);
202
+
203
+ useEffect(() => {
204
+ if (!copied) return;
205
+ const timer = setTimeout(() => setCopied(false), 2000);
206
+ return () => clearTimeout(timer);
207
+ }, [copied]);
208
+
209
+ // Close popover on Escape key
210
+ useEffect(() => {
211
+ if (!popoverOpen) return;
212
+ const handleKeyDown = (e: globalThis.KeyboardEvent) => {
213
+ if (e.key === "Escape") setPopoverOpen(false);
214
+ };
215
+ document.addEventListener("keydown", handleKeyDown);
216
+ return () => document.removeEventListener("keydown", handleKeyDown);
217
+ }, [popoverOpen]);
218
+
219
+ // --- Connected state ---
220
+ if (isConnected && address) {
221
+ return (
222
+ <div
223
+ style={{
224
+ position: "relative",
225
+ display: "inline-block",
226
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
227
+ }}
228
+ >
229
+ <button
230
+ onClick={() => setPopoverOpen(!popoverOpen)}
231
+ aria-expanded={popoverOpen}
232
+ aria-haspopup="true"
233
+ style={{
234
+ display: "flex",
235
+ alignItems: "center",
236
+ gap: 10,
237
+ padding: "10px 16px",
238
+ borderRadius: 12,
239
+ border: `1.5px solid ${t.border}`,
240
+ background: t.cardBg,
241
+ color: t.text,
242
+ fontSize: 14,
243
+ fontWeight: 600,
244
+ cursor: "pointer",
245
+ letterSpacing: "-0.01em",
246
+ transition: "all 150ms ease",
247
+ }}
248
+ >
249
+ <span
250
+ style={{
251
+ width: 8,
252
+ height: 8,
253
+ borderRadius: "50%",
254
+ background: t.connectedDot,
255
+ display: "inline-block",
256
+ boxShadow: `0 0 6px ${t.connectedDot}`,
257
+ }}
258
+ />
259
+ <span style={{ fontFamily: "'SF Mono', 'Fira Code', 'Cascadia Code', monospace", fontSize: 13 }}>
260
+ {truncateAddress(address)}
261
+ </span>
262
+ <ChevronDown
263
+ size={14}
264
+ color={t.textTertiary}
265
+ style={{ transition: "transform 200ms", transform: popoverOpen ? "rotate(180deg)" : "none" }}
266
+ />
267
+ </button>
268
+
269
+ {popoverOpen && (
270
+ <>
271
+ <div style={{ position: "fixed", inset: 0, zIndex: 40 }} onClick={() => setPopoverOpen(false)} />
272
+ <div
273
+ role="dialog"
274
+ aria-label="Wallet details"
275
+ onClick={e => e.stopPropagation()}
276
+ style={{
277
+ position: "absolute",
278
+ right: 0,
279
+ top: "100%",
280
+ marginTop: 8,
281
+ width: 320,
282
+ borderRadius: 16,
283
+ border: `1.5px solid ${t.border}`,
284
+ background: t.cardBg,
285
+ boxShadow: t.popoverShadow,
286
+ zIndex: 50,
287
+ overflow: "hidden",
288
+ }}
289
+ >
290
+ {/* Header */}
291
+ <div
292
+ style={{
293
+ padding: "16px 20px 14px",
294
+ borderBottom: `1px solid ${t.borderLight}`,
295
+ display: "flex",
296
+ alignItems: "center",
297
+ justifyContent: "space-between",
298
+ }}
299
+ >
300
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
301
+ <span
302
+ style={{
303
+ width: 8,
304
+ height: 8,
305
+ borderRadius: "50%",
306
+ background: t.connectedDot,
307
+ display: "inline-block",
308
+ boxShadow: `0 0 6px ${t.connectedDot}`,
309
+ }}
310
+ />
311
+ <span style={{ fontSize: 13, fontWeight: 600, color: t.text, letterSpacing: "-0.01em" }}>
312
+ Connected
313
+ </span>
314
+ </div>
315
+ {chain && (
316
+ <span
317
+ style={{
318
+ fontSize: 11,
319
+ fontWeight: 600,
320
+ padding: "3px 10px",
321
+ borderRadius: 99,
322
+ background: t.iconBg,
323
+ color: t.textSecondary,
324
+ letterSpacing: "0.02em",
325
+ textTransform: "uppercase",
326
+ }}
327
+ >
328
+ {chain.name}
329
+ </span>
330
+ )}
331
+ </div>
332
+
333
+ <div style={{ padding: "12px 12px 16px" }}>
334
+ {/* Address */}
335
+ <button
336
+ onClick={handleCopyAddress}
337
+ onMouseEnter={() => setCopyHover(true)}
338
+ onMouseLeave={() => setCopyHover(false)}
339
+ style={{
340
+ display: "flex",
341
+ alignItems: "center",
342
+ gap: 10,
343
+ width: "100%",
344
+ padding: "10px 12px",
345
+ borderRadius: 10,
346
+ border: "none",
347
+ background: copyHover ? t.copyHoverBg : "transparent",
348
+ textAlign: "left",
349
+ fontFamily: "'SF Mono', 'Fira Code', 'Cascadia Code', monospace",
350
+ fontSize: 12,
351
+ color: t.text,
352
+ cursor: "pointer",
353
+ marginBottom: 8,
354
+ wordBreak: "break-all",
355
+ lineHeight: 1.5,
356
+ transition: "background 150ms ease",
357
+ }}
358
+ >
359
+ <span style={{ flex: 1 }}>{address}</span>
360
+ {copied ? (
361
+ <Check size={14} color={t.connectedDot} style={{ flexShrink: 0 }} />
362
+ ) : (
363
+ <Copy size={14} color={t.textTertiary} style={{ flexShrink: 0 }} />
364
+ )}
365
+ </button>
366
+
367
+ {/* Disconnect */}
368
+ <button
369
+ onClick={handleDisconnect}
370
+ onMouseEnter={() => setDisconnectHover(true)}
371
+ onMouseLeave={() => setDisconnectHover(false)}
372
+ style={{
373
+ display: "flex",
374
+ alignItems: "center",
375
+ justifyContent: "center",
376
+ gap: 8,
377
+ width: "100%",
378
+ padding: "10px 16px",
379
+ borderRadius: 10,
380
+ border: `1.5px solid ${t.disconnectBorder}`,
381
+ background: disconnectHover ? t.disconnectHoverBg : "transparent",
382
+ color: t.disconnectText,
383
+ fontSize: 13,
384
+ fontWeight: 600,
385
+ cursor: "pointer",
386
+ transition: "all 150ms ease",
387
+ }}
388
+ >
389
+ <LogOut size={15} />
390
+ Disconnect
391
+ </button>
392
+ </div>
393
+ </div>
394
+ </>
395
+ )}
396
+ </div>
397
+ );
398
+ }
399
+
400
+ // --- Disconnected state ---
401
+ return (
402
+ <DialogPrimitive.Root
403
+ open={modalOpen}
404
+ onOpenChange={open => {
405
+ setModalOpen(open);
406
+ if (!open) resetConnectState();
407
+ }}
408
+ >
409
+ <DialogPrimitive.Trigger asChild>
410
+ <button
411
+ onMouseEnter={() => setBtnHover(true)}
412
+ onMouseLeave={() => setBtnHover(false)}
413
+ style={{
414
+ padding: "12px 24px",
415
+ borderRadius: 12,
416
+ border: "none",
417
+ background: btnHover ? B3_BLUE_HOVER : B3_BLUE,
418
+ color: "#ffffff",
419
+ fontSize: 14,
420
+ fontWeight: 600,
421
+ cursor: "pointer",
422
+ letterSpacing: "-0.01em",
423
+ transition: "all 150ms ease",
424
+ boxShadow: btnHover ? `0 4px 12px ${B3_BLUE}40, 0 1px 3px rgba(0,0,0,0.1)` : `0 2px 8px ${B3_BLUE}30`,
425
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
426
+ }}
427
+ >
428
+ {buttonLabel}
429
+ </button>
430
+ </DialogPrimitive.Trigger>
431
+
432
+ <SpinnerKeyframes />
433
+ <DialogPrimitive.Portal>
434
+ <DialogPrimitive.Overlay
435
+ style={{
436
+ position: "fixed",
437
+ inset: 0,
438
+ zIndex: 50,
439
+ background: t.overlayBg,
440
+ backdropFilter: "blur(20px)",
441
+ WebkitBackdropFilter: "blur(20px)",
442
+ }}
443
+ />
444
+ <DialogPrimitive.Content
445
+ style={{
446
+ position: "fixed",
447
+ left: "50%",
448
+ top: "50%",
449
+ transform: "translate(-50%, -50%)",
450
+ zIndex: 50,
451
+ width: "calc(100% - 32px)",
452
+ maxWidth: 400,
453
+ borderRadius: 20,
454
+ border: `1.5px solid ${t.border}`,
455
+ background: t.cardBg,
456
+ color: t.text,
457
+ boxShadow: t.shadow,
458
+ overflow: "hidden",
459
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
460
+ }}
461
+ >
462
+ {/* Modal header */}
463
+ <div
464
+ style={{
465
+ display: "flex",
466
+ alignItems: "center",
467
+ justifyContent: "space-between",
468
+ padding: "20px 24px 16px",
469
+ }}
470
+ >
471
+ <div>
472
+ <DialogPrimitive.Title
473
+ style={{
474
+ fontSize: 18,
475
+ fontWeight: 700,
476
+ margin: 0,
477
+ letterSpacing: "-0.02em",
478
+ color: t.text,
479
+ }}
480
+ >
481
+ Connect Wallet
482
+ </DialogPrimitive.Title>
483
+ <p style={{ fontSize: 13, color: t.textSecondary, margin: "4px 0 0", letterSpacing: "-0.01em" }}>
484
+ Choose your preferred wallet
485
+ </p>
486
+ </div>
487
+ <DialogPrimitive.Close
488
+ style={{
489
+ background: t.iconBg,
490
+ border: "none",
491
+ cursor: "pointer",
492
+ padding: 8,
493
+ borderRadius: 10,
494
+ color: t.textSecondary,
495
+ display: "flex",
496
+ alignItems: "center",
497
+ justifyContent: "center",
498
+ transition: "all 150ms ease",
499
+ }}
500
+ >
501
+ <X size={16} />
502
+ </DialogPrimitive.Close>
503
+ </div>
504
+
505
+ {/* Error banner */}
506
+ {connectError && (
507
+ <div
508
+ style={{
509
+ margin: "0 12px 8px",
510
+ padding: "10px 14px",
511
+ borderRadius: 10,
512
+ background: themeProp === "dark" ? "#1c1017" : "#fef2f2",
513
+ border: `1px solid ${t.disconnectBorder}`,
514
+ display: "flex",
515
+ alignItems: "center",
516
+ gap: 8,
517
+ fontSize: 13,
518
+ color: t.disconnectText,
519
+ }}
520
+ >
521
+ <AlertCircle size={16} style={{ flexShrink: 0 }} />
522
+ <span style={{ flex: 1 }}>{connectError}</span>
523
+ <button
524
+ onClick={() => setConnectError(null)}
525
+ style={{ background: "none", border: "none", cursor: "pointer", color: t.disconnectText, padding: 2 }}
526
+ >
527
+ <X size={14} />
528
+ </button>
529
+ </div>
530
+ )}
531
+
532
+ {/* Connector list */}
533
+ <div style={{ padding: "0 12px 16px" }}>
534
+ {connectors.length === 0 ? (
535
+ <p style={{ padding: "40px 0", textAlign: "center", fontSize: 14, color: t.textSecondary }}>
536
+ No wallets detected
537
+ </p>
538
+ ) : (
539
+ <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
540
+ {connectors.map(connector => {
541
+ const isLoading = connectingUid === connector.uid;
542
+ const isSuccess = connectSuccess && connectingUid === connector.uid;
543
+ const isDisabled = connectingUid !== null && connectingUid !== connector.uid;
544
+
545
+ return (
546
+ <button
547
+ key={connector.uid}
548
+ onClick={() => !connectingUid && handleConnect(connector)}
549
+ onMouseEnter={() => setHoveredUid(connector.uid)}
550
+ onMouseLeave={() => setHoveredUid(null)}
551
+ style={{
552
+ display: "flex",
553
+ alignItems: "center",
554
+ gap: 14,
555
+ width: "100%",
556
+ padding: "14px 16px",
557
+ borderRadius: 14,
558
+ border: isLoading
559
+ ? `1.5px solid ${B3_BLUE}`
560
+ : isSuccess
561
+ ? `1.5px solid ${t.connectedDot}`
562
+ : hoveredUid === connector.uid && !isDisabled
563
+ ? `1.5px solid ${B3_BLUE}40`
564
+ : "1.5px solid transparent",
565
+ background: isLoading
566
+ ? themeProp === "dark"
567
+ ? "#1a1a30"
568
+ : "#eff6ff"
569
+ : isSuccess
570
+ ? themeProp === "dark"
571
+ ? "#0a1f15"
572
+ : "#ecfdf5"
573
+ : hoveredUid === connector.uid && !isDisabled
574
+ ? t.hoverBg
575
+ : "transparent",
576
+ textAlign: "left",
577
+ cursor: isDisabled ? "default" : "pointer",
578
+ color: t.text,
579
+ opacity: isDisabled ? 0.4 : 1,
580
+ transition: "all 150ms ease",
581
+ }}
582
+ >
583
+ <div
584
+ style={{
585
+ width: 44,
586
+ height: 44,
587
+ borderRadius: 12,
588
+ background: t.iconBg,
589
+ display: "flex",
590
+ alignItems: "center",
591
+ justifyContent: "center",
592
+ flexShrink: 0,
593
+ border: `1px solid ${t.borderLight}`,
594
+ }}
595
+ >
596
+ {getWalletIcon(connector.name, 24)}
597
+ </div>
598
+ <div style={{ flex: 1, minWidth: 0 }}>
599
+ <div style={{ fontSize: 14, fontWeight: 600, letterSpacing: "-0.01em" }}>{connector.name}</div>
600
+ {isLoading && (
601
+ <div style={{ fontSize: 12, color: B3_BLUE, marginTop: 2 }}>Waiting for approval...</div>
602
+ )}
603
+ {isSuccess && (
604
+ <div style={{ fontSize: 12, color: t.connectedDot, marginTop: 2 }}>Connected</div>
605
+ )}
606
+ </div>
607
+ {isLoading && (
608
+ <Loader2
609
+ size={18}
610
+ color={B3_BLUE}
611
+ style={{ flexShrink: 0, animation: "b3-spin 1s linear infinite" }}
612
+ />
613
+ )}
614
+ {isSuccess && <Check size={18} color={t.connectedDot} style={{ flexShrink: 0 }} />}
615
+ {!isLoading && !isSuccess && !connector.isInstalled && (
616
+ <span
617
+ style={{
618
+ fontSize: 11,
619
+ fontWeight: 600,
620
+ padding: "4px 10px",
621
+ borderRadius: 99,
622
+ background: t.badgeInstallBg,
623
+ color: t.badgeInstallText,
624
+ letterSpacing: "0.01em",
625
+ }}
626
+ >
627
+ Install
628
+ </span>
629
+ )}
630
+ </button>
631
+ );
632
+ })}
633
+ </div>
634
+ )}
635
+ </div>
636
+
637
+ {/* Footer */}
638
+ <div
639
+ style={{
640
+ padding: "12px 24px 16px",
641
+ borderTop: `1px solid ${t.borderLight}`,
642
+ display: "flex",
643
+ alignItems: "center",
644
+ justifyContent: "center",
645
+ gap: 6,
646
+ }}
647
+ >
648
+ <img src="https://cdn.b3.fun/b3_logo.svg" alt="B3" width={16} height={16} />
649
+ <span
650
+ style={{
651
+ fontSize: 11,
652
+ fontWeight: 700,
653
+ color: B3_BLUE,
654
+ letterSpacing: "0.06em",
655
+ textTransform: "uppercase",
656
+ }}
657
+ >
658
+ Powered by B3
659
+ </span>
660
+ </div>
661
+ </DialogPrimitive.Content>
662
+ </DialogPrimitive.Portal>
663
+ </DialogPrimitive.Root>
664
+ );
665
+ }