@bufinance/web3-signin 0.1.0 → 0.1.2
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/README.md +1 -4
- package/package.json +4 -2
- package/src/react/turnstile.tsx +21 -14
- package/src/react/wallet-dropdown.tsx +34 -4
package/README.md
CHANGED
|
@@ -8,12 +8,9 @@ It wraps three things:
|
|
|
8
8
|
2. **Supabase Web3 auth** — `signInWithWeb3` (EIP-4361 "Sign in with Ethereum"). Supabase builds the SIWE message, the wallet signs, GoTrue verifies server-side and mints the session. No custom SIWE backend.
|
|
9
9
|
3. **Turnstile CAPTCHA** (wallet logins only) — renders the Cloudflare Turnstile widget and hands the token to `signInWithWeb3` as `captchaToken`. **No siteverify Worker** — Supabase verifies the token with the secret set in its dashboard.
|
|
10
10
|
|
|
11
|
-
## Install (
|
|
11
|
+
## Install (public npm)
|
|
12
12
|
|
|
13
13
|
```
|
|
14
|
-
# .npmrc
|
|
15
|
-
@bufinance:registry=https://npm.pkg.github.com
|
|
16
|
-
|
|
17
14
|
bun add @bufinance/web3-signin
|
|
18
15
|
```
|
|
19
16
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bufinance/web3-signin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
".": "./src/index.ts",
|
|
10
10
|
"./react": "./src/react/index.ts"
|
|
11
11
|
},
|
|
12
|
-
"files": [
|
|
12
|
+
"files": [
|
|
13
|
+
"src"
|
|
14
|
+
],
|
|
13
15
|
"scripts": {
|
|
14
16
|
"typecheck": "tsc --noEmit",
|
|
15
17
|
"test": "bun test"
|
package/src/react/turnstile.tsx
CHANGED
|
@@ -9,14 +9,18 @@ import { useEffect, useRef } from "react";
|
|
|
9
9
|
* token up via `onToken`, which the caller passes as `captchaToken` to
|
|
10
10
|
* `signInWithWeb3`. Reset it after each auth attempt (tokens are single-use).
|
|
11
11
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
interface TurnstileApi {
|
|
13
|
+
render: (el: HTMLElement, opts: TurnstileRenderOpts) => string;
|
|
14
|
+
reset: (id?: string) => void;
|
|
15
|
+
remove: (id?: string) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Read window.turnstile without `declare global` — augmenting the global
|
|
19
|
+
* Window collides with consumers that already declare their own turnstile
|
|
20
|
+
* type. A local intersection cast keeps this package self-contained. */
|
|
21
|
+
function getTurnstile(): TurnstileApi | undefined {
|
|
22
|
+
if (typeof window === "undefined") return undefined;
|
|
23
|
+
return (window as Window & { turnstile?: TurnstileApi }).turnstile;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
interface TurnstileRenderOpts {
|
|
@@ -35,7 +39,7 @@ let scriptPromise: Promise<void> | null = null;
|
|
|
35
39
|
|
|
36
40
|
function loadScript(): Promise<void> {
|
|
37
41
|
if (typeof window === "undefined") return Promise.resolve();
|
|
38
|
-
if (
|
|
42
|
+
if (getTurnstile()) return Promise.resolve();
|
|
39
43
|
if (scriptPromise) return scriptPromise;
|
|
40
44
|
scriptPromise = new Promise<void>((resolve, reject) => {
|
|
41
45
|
const s = document.createElement("script");
|
|
@@ -79,8 +83,10 @@ export function Turnstile({
|
|
|
79
83
|
if (!sitekey) return;
|
|
80
84
|
loadScript()
|
|
81
85
|
.then(() => {
|
|
82
|
-
if (cancelled || !ref.current
|
|
83
|
-
|
|
86
|
+
if (cancelled || !ref.current) return;
|
|
87
|
+
const api = getTurnstile();
|
|
88
|
+
if (!api) return;
|
|
89
|
+
widgetId.current = api.render(ref.current, {
|
|
84
90
|
sitekey,
|
|
85
91
|
action: "bufi-wallet-login",
|
|
86
92
|
theme,
|
|
@@ -93,9 +99,10 @@ export function Turnstile({
|
|
|
93
99
|
.catch(() => cb.current.onError?.());
|
|
94
100
|
return () => {
|
|
95
101
|
cancelled = true;
|
|
96
|
-
|
|
102
|
+
const api = getTurnstile();
|
|
103
|
+
if (widgetId.current && api) {
|
|
97
104
|
try {
|
|
98
|
-
|
|
105
|
+
api.remove(widgetId.current);
|
|
99
106
|
} catch {
|
|
100
107
|
/* widget already gone */
|
|
101
108
|
}
|
|
@@ -110,5 +117,5 @@ export function Turnstile({
|
|
|
110
117
|
/** Imperatively reset the rendered widget (tokens are single-use; reset after
|
|
111
118
|
* each auth attempt). Pass the same sitekey instance's container. */
|
|
112
119
|
export function resetTurnstile(): void {
|
|
113
|
-
|
|
120
|
+
getTurnstile()?.reset();
|
|
114
121
|
}
|
|
@@ -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,16 @@ 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
|
+
/** Extra content inside the open panel — desk injects gateway/hinkal/multisig;
|
|
36
|
+
* defi-web injects its full per-chain balance body (net toggle, ghost, APY). */
|
|
36
37
|
children?: ReactNode;
|
|
38
|
+
/**
|
|
39
|
+
* Render the built-in flat balance rows for the active account. Default true
|
|
40
|
+
* (desk). Set false when the host renders its own richer balance body via
|
|
41
|
+
* `children` (defi-web's per-chain grouped list) — the `accounts` balances
|
|
42
|
+
* are then used only for the pill total, not drawn as rows.
|
|
43
|
+
*/
|
|
44
|
+
renderBalances?: boolean;
|
|
37
45
|
/** Shown when authed but no accounts yet (provisioning). */
|
|
38
46
|
emptyState?: ReactNode;
|
|
39
47
|
onSelectAccount?: (account: WalletAccount) => void;
|
|
@@ -54,6 +62,7 @@ export function WalletDropdown(props: WalletDropdownProps): ReactNode {
|
|
|
54
62
|
scope = "all",
|
|
55
63
|
formatUsd = defaultFormatUsd,
|
|
56
64
|
children,
|
|
65
|
+
renderBalances = true,
|
|
57
66
|
emptyState,
|
|
58
67
|
onSelectAccount,
|
|
59
68
|
classNames = {},
|
|
@@ -66,6 +75,27 @@ export function WalletDropdown(props: WalletDropdownProps): ReactNode {
|
|
|
66
75
|
|
|
67
76
|
const [open, setOpen] = useState(false);
|
|
68
77
|
const [activeId, setActiveId] = useState<string | null>(null);
|
|
78
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
79
|
+
|
|
80
|
+
// Dependency-light popover ergonomics (no radix): click-outside + Escape close.
|
|
81
|
+
// Both consuming apps get this for free, so neither has to re-implement it.
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!open) return;
|
|
84
|
+
const onPointerDown = (e: MouseEvent): void => {
|
|
85
|
+
if (rootRef.current && !rootRef.current.contains(e.target as Node)) {
|
|
86
|
+
setOpen(false);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const onKeyDown = (e: KeyboardEvent): void => {
|
|
90
|
+
if (e.key === "Escape") setOpen(false);
|
|
91
|
+
};
|
|
92
|
+
document.addEventListener("mousedown", onPointerDown);
|
|
93
|
+
document.addEventListener("keydown", onKeyDown);
|
|
94
|
+
return () => {
|
|
95
|
+
document.removeEventListener("mousedown", onPointerDown);
|
|
96
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
97
|
+
};
|
|
98
|
+
}, [open]);
|
|
69
99
|
|
|
70
100
|
// Auth gate — the whole surface is hidden until sign-in passes.
|
|
71
101
|
if (!authed) return null;
|
|
@@ -74,7 +104,7 @@ export function WalletDropdown(props: WalletDropdownProps): ReactNode {
|
|
|
74
104
|
const pillTotal = active ? totalUsd(active.balances) : 0;
|
|
75
105
|
|
|
76
106
|
return (
|
|
77
|
-
<div className={classNames.root} data-bufi-wallet>
|
|
107
|
+
<div ref={rootRef} className={classNames.root} data-bufi-wallet>
|
|
78
108
|
<button
|
|
79
109
|
type="button"
|
|
80
110
|
aria-label="Open Stablecoin FX Wallet"
|
|
@@ -114,7 +144,7 @@ export function WalletDropdown(props: WalletDropdownProps): ReactNode {
|
|
|
114
144
|
</div>
|
|
115
145
|
)}
|
|
116
146
|
|
|
117
|
-
{active && (
|
|
147
|
+
{renderBalances && active && (
|
|
118
148
|
<div className={classNames.accountRow}>
|
|
119
149
|
{active.balances.length === 0
|
|
120
150
|
? (emptyState ?? null)
|