@bufinance/web3-signin 0.1.1 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bufinance/web3-signin",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Headless cross-app Web3 wallet sign-in for BUFI: EIP-6963 wallet discovery + Supabase signInWithWeb3 (EIP-4361) + Turnstile captcha. Shared by desk-v1 and defi-web-app. Source of truth lives here; both apps consume it.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -1,4 +1,4 @@
1
- import { type ReactNode, useMemo, useState } from "react";
1
+ import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
2
2
  import type { TokenBalance, WalletAccount, WalletScope } from "../types";
3
3
 
4
4
  /**
@@ -32,8 +32,29 @@ export interface WalletDropdownProps {
32
32
  scope?: WalletScope | "all";
33
33
  /** Currency formatter for the USD total; defaults to en-US $. */
34
34
  formatUsd?: (n: number) => string;
35
- /** Extra content inside the open panel — desk injects gateway/hinkal/multisig. */
35
+ /**
36
+ * Custom trigger (pill) renderer. When provided, replaces the built-in
37
+ * text-only pill so a host can render its own rich trigger (icon, animated
38
+ * total, chevron) while the shell still owns open-state, the auth gate, and
39
+ * click-outside. The host renders its own button and calls `toggle` on click.
40
+ * Omit for the default pill (desk).
41
+ */
42
+ trigger?: (state: {
43
+ open: boolean;
44
+ total: number;
45
+ toggle: () => void;
46
+ formatUsd: (n: number) => string;
47
+ }) => ReactNode;
48
+ /** Extra content inside the open panel — desk injects gateway/hinkal/multisig;
49
+ * defi-web injects its full per-chain balance body (net toggle, ghost, APY). */
36
50
  children?: ReactNode;
51
+ /**
52
+ * Render the built-in flat balance rows for the active account. Default true
53
+ * (desk). Set false when the host renders its own richer balance body via
54
+ * `children` (defi-web's per-chain grouped list) — the `accounts` balances
55
+ * are then used only for the pill total, not drawn as rows.
56
+ */
57
+ renderBalances?: boolean;
37
58
  /** Shown when authed but no accounts yet (provisioning). */
38
59
  emptyState?: ReactNode;
39
60
  onSelectAccount?: (account: WalletAccount) => void;
@@ -53,7 +74,9 @@ export function WalletDropdown(props: WalletDropdownProps): ReactNode {
53
74
  accounts,
54
75
  scope = "all",
55
76
  formatUsd = defaultFormatUsd,
77
+ trigger,
56
78
  children,
79
+ renderBalances = true,
57
80
  emptyState,
58
81
  onSelectAccount,
59
82
  classNames = {},
@@ -66,6 +89,27 @@ export function WalletDropdown(props: WalletDropdownProps): ReactNode {
66
89
 
67
90
  const [open, setOpen] = useState(false);
68
91
  const [activeId, setActiveId] = useState<string | null>(null);
92
+ const rootRef = useRef<HTMLDivElement>(null);
93
+
94
+ // Dependency-light popover ergonomics (no radix): click-outside + Escape close.
95
+ // Both consuming apps get this for free, so neither has to re-implement it.
96
+ useEffect(() => {
97
+ if (!open) return;
98
+ const onPointerDown = (e: MouseEvent): void => {
99
+ if (rootRef.current && !rootRef.current.contains(e.target as Node)) {
100
+ setOpen(false);
101
+ }
102
+ };
103
+ const onKeyDown = (e: KeyboardEvent): void => {
104
+ if (e.key === "Escape") setOpen(false);
105
+ };
106
+ document.addEventListener("mousedown", onPointerDown);
107
+ document.addEventListener("keydown", onKeyDown);
108
+ return () => {
109
+ document.removeEventListener("mousedown", onPointerDown);
110
+ document.removeEventListener("keydown", onKeyDown);
111
+ };
112
+ }, [open]);
69
113
 
70
114
  // Auth gate — the whole surface is hidden until sign-in passes.
71
115
  if (!authed) return null;
@@ -73,17 +117,23 @@ export function WalletDropdown(props: WalletDropdownProps): ReactNode {
73
117
  const active = visible.find((a) => a.id === activeId) ?? visible[0];
74
118
  const pillTotal = active ? totalUsd(active.balances) : 0;
75
119
 
120
+ const toggle = (): void => setOpen((v) => !v);
121
+
76
122
  return (
77
- <div className={classNames.root} data-bufi-wallet>
78
- <button
79
- type="button"
80
- aria-label="Open Stablecoin FX Wallet"
81
- aria-expanded={open}
82
- className={classNames.pill ?? "acct-mini"}
83
- onClick={() => setOpen((v) => !v)}
84
- >
85
- {formatUsd(pillTotal)}
86
- </button>
123
+ <div ref={rootRef} className={classNames.root} data-bufi-wallet>
124
+ {trigger ? (
125
+ trigger({ open, total: pillTotal, toggle, formatUsd })
126
+ ) : (
127
+ <button
128
+ type="button"
129
+ aria-label="Open Stablecoin FX Wallet"
130
+ aria-expanded={open}
131
+ className={classNames.pill ?? "acct-mini"}
132
+ onClick={toggle}
133
+ >
134
+ {formatUsd(pillTotal)}
135
+ </button>
136
+ )}
87
137
 
88
138
  {open && (
89
139
  <div role="menu" className={classNames.panel} data-bufi-wallet-panel>
@@ -114,7 +164,7 @@ export function WalletDropdown(props: WalletDropdownProps): ReactNode {
114
164
  </div>
115
165
  )}
116
166
 
117
- {active && (
167
+ {renderBalances && active && (
118
168
  <div className={classNames.accountRow}>
119
169
  {active.balances.length === 0
120
170
  ? (emptyState ?? null)