import { useCallback, useEffect, useRef, useState } from "react"; import QRCode from "qrcode"; import { SimplePool, generateSecretKey, getPublicKey } from "nostr-tools"; import { BunkerSigner, createNostrConnectURI } from "nostr-tools/nip46"; import { useToast } from "../contexts/ToastContext"; import type { Nip98Signer } from "../api"; const DEFAULT_RELAYS = [ "wss://relay.damus.io", "wss://relay.nostr.band", ]; const WAIT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes function randomHex(bytes: number): string { const arr = new Uint8Array(bytes); crypto.getRandomValues(arr); return Array.from(arr) .map((b) => b.toString(16).padStart(2, "0")) .join(""); } interface RemoteSignerQRProps { onConnect: (sign: Nip98Signer) => Promise; onError: (message: string | null) => void; disabled?: boolean; /** When true, show "Signing in…" but keep mounted so the signer pool stays alive. */ signingIn?: boolean; } export function RemoteSignerQR({ onConnect, onError, disabled, signingIn }: RemoteSignerQRProps) { const { showToast } = useToast(); const [connectionUri, setConnectionUri] = useState(null); const [qrDataUrl, setQrDataUrl] = useState(null); const [copyLabel, setCopyLabel] = useState("Copy"); const [waiting, setWaiting] = useState(false); const poolRef = useRef(null); const abortRef = useRef(null); const timeoutRef = useRef | null>(null); const clearTimeoutRef = useCallback(() => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } }, []); const generateUri = useCallback(() => { const clientSecret = generateSecretKey(); const clientPubkey = getPublicKey(clientSecret); const secret = randomHex(32); const uri = createNostrConnectURI({ clientPubkey, relays: DEFAULT_RELAYS, secret, name: "Sats Faucet", url: typeof window !== "undefined" ? window.location.origin : "", }); return { uri, clientSecret }; }, []); const startWaiting = useCallback( (uri: string, clientSecret: Uint8Array) => { clearTimeoutRef(); const pool = new SimplePool(); poolRef.current = pool; const abort = new AbortController(); abortRef.current = abort; setWaiting(true); timeoutRef.current = setTimeout(() => { timeoutRef.current = null; if (abortRef.current?.signal.aborted) return; abortRef.current?.abort(); onError("Connection timed out. Try regenerating the QR code."); setWaiting(false); }, WAIT_TIMEOUT_MS); BunkerSigner.fromURI(clientSecret, uri, { pool }, abort.signal) .then(async (signer) => { if (abortRef.current?.signal.aborted) return; clearTimeoutRef(); setWaiting(false); const sign: Nip98Signer = (e) => signer.signEvent(e); try { await onConnect(sign); } catch (err) { const msg = err instanceof Error ? err.message : String(err); onError(msg); } }) .catch((err) => { if (abortRef.current?.signal.aborted) return; clearTimeoutRef(); setWaiting(false); const msg = err instanceof Error ? err.message : String(err); onError(msg); }); }, [onConnect, onError, clearTimeoutRef] ); const regenerate = useCallback(() => { clearTimeoutRef(); abortRef.current?.abort(); abortRef.current = null; if (poolRef.current) { poolRef.current.destroy(); poolRef.current = null; } onError(null); const { uri, clientSecret } = generateUri(); setConnectionUri(uri); QRCode.toDataURL(uri, { width: 220, margin: 1 }).then(setQrDataUrl).catch(() => setQrDataUrl(null)); startWaiting(uri, clientSecret); }, [generateUri, startWaiting, onError, clearTimeoutRef]); useEffect(() => { if (disabled) return; const { uri, clientSecret } = generateUri(); setConnectionUri(uri); setQrDataUrl(null); QRCode.toDataURL(uri, { width: 220, margin: 1 }).then(setQrDataUrl).catch(() => setQrDataUrl(null)); startWaiting(uri, clientSecret); return () => { clearTimeoutRef(); abortRef.current?.abort(); if (poolRef.current) { poolRef.current.destroy(); poolRef.current = null; } }; }, [disabled, generateUri, startWaiting, clearTimeoutRef]); const handleCopy = useCallback(() => { if (!connectionUri) return; navigator.clipboard.writeText(connectionUri).then(() => { setCopyLabel("Copied"); showToast("Copied"); setTimeout(() => setCopyLabel("Copy"), 1500); }); }, [connectionUri, showToast]); if (disabled) return null; return (
{qrDataUrl ? ( Scan to connect with your signer ) : ( Generating QR… )}
{signingIn ? (

Signing in…

) : waiting ? (

Waiting for signer approval…

) : null}
); }