'use client' /** * SignConfirmSheet — elegant pre-signing confirmation UI. * * Usage (imperative, no context needed): * * import { confirmSign } from '@/components/wallet/SignConfirmSheet' * * const ok = await confirmSign({ * label: 'Enable Auto-Trade', * description: 'Bot will open live trades on Hyperliquid when signals fire.', * danger: true, * }) * if (!ok) return // user cancelled * const env = await signRequest(...) // only NOW triggers MetaMask */ import { createRoot } from 'react-dom/client' export interface SignConfirmOptions { /** Short action title, e.g. "Enable Auto-Trade" */ label: string /** One-sentence explanation shown to the user */ description: string /** If true, renders a red/warning accent — for irreversible / live-money actions */ danger?: boolean } // Track the currently-mounted sheet so a second confirmSign() call can resolve // the first one with `false` instead of leaving its promise dangling forever. let _activeCancel: (() => void) | null = null /** Renders a confirmation sheet and resolves true (confirm) or false (cancel). */ export function confirmSign(opts: SignConfirmOptions): Promise { return new Promise((resolve) => { // If another sheet is already mounted, cancel it first so its awaiting // caller resolves with `false` rather than hanging indefinitely. if (_activeCancel) { try { _activeCancel() } catch { /* ignore */ } } const existing = document.getElementById('sign-confirm-sheet-root') if (existing?.parentNode) { existing.parentNode.removeChild(existing) } const container = document.createElement('div') container.id = 'sign-confirm-sheet-root' document.body.appendChild(container) const root = createRoot(container) let settled = false function cleanup(result: boolean) { if (settled) return settled = true // Clear the active-cancel slot only if it still points to *this* sheet — // a later confirmSign() may have already replaced it. if (_activeCancel === cancelSelf) _activeCancel = null root.unmount() if (container.parentNode) { container.parentNode.removeChild(container) } resolve(result) } // Stable reference used both as the active-cancel handle and inside cleanup. const cancelSelf = () => cleanup(false) _activeCancel = cancelSelf root.render( cleanup(true)} onCancel={cancelSelf} />) }) } /* ─── Internal sheet component ─────────────────────────────────────────────── */ function Sheet({ label, description, danger, onConfirm, onCancel, }: SignConfirmOptions & { onConfirm: () => void; onCancel: () => void }) { const accent = danger ? '#ef4444' : '#f5a524' const accentSoft = danger ? 'rgba(239,68,68,0.1)' : 'rgba(245,165,36,0.1)' // Dismiss on backdrop click function handleBackdrop(e: React.MouseEvent) { if (e.target === e.currentTarget) onCancel() } // Dismiss on Escape function handleKey(e: React.KeyboardEvent) { if (e.key === 'Escape') onCancel() } return (
{/* 拖拽条 */}
{/* 图标 + 标题 */}
🔏
Wallet signature required
{label}
{/* 说明文字 */}
{description}
{/* 说明条 */}
Signing is used only to verify your identity. It will NOT authorize any on-chain transfer and will NOT cost any gas. The MetaMask popup will appear after you click Confirm.
{/* 按钮组 */}
) }