UNPKG

bigblocks

Version:

Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React

130 lines (129 loc) 12.8 kB
"use client"; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { CheckIcon, ChevronDownIcon, CopyIcon, GearIcon, HamburgerMenuIcon, LockClosedIcon, LockOpen1Icon, PersonIcon, PlusIcon, } from "@radix-ui/react-icons"; import { useEffect, useState } from "react"; import { useBitcoinAuth } from "../../hooks/useBitcoinAuth.js"; import { AuthButton } from "../authentication/AuthButton.js"; import { BitcoinAvatar } from "../ui-components/BitcoinAvatar.js"; import { LoadingButton } from "../ui-components/LoadingButton.js"; import { PasswordInput } from "../ui-components/PasswordInput.js"; import { Badge } from "../ui/badge.js"; import { Button } from "../ui/button.js"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "../ui/dialog.js"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "../ui/dropdown-menu.js"; import { CompactWalletOverview } from "../wallet/components/WalletOverview.js"; export function ProfileDropdownMenu({ showWalletBalance = true, walletBalance, exchangeRate = 50, currency = "USD", useMenuIcon = false, trigger, showThemeSwitch = true, onViewProfile, onSettings, onWalletUnlocked, onCreateAccount, onSignIn, additionalItems, align = "end", side = "bottom", signOutRedirectTo = "/", showProfileDetails = true, }) { const { user, isAuthenticated, profiles, activeProfile, hasWalletCapabilities, walletExtension, switchProfile, validatePassword, hasLocalBackup, signIn, } = useBitcoinAuth(); const [isUnlockDialogOpen, setIsUnlockDialogOpen] = useState(false); const [unlockPassword, setUnlockPassword] = useState(""); const [isUnlocking, setIsUnlocking] = useState(false); const [unlockError, setUnlockError] = useState(null); const [copiedAddress, setCopiedAddress] = useState(null); // Reset copied state after 2 seconds useEffect(() => { if (copiedAddress) { const timer = setTimeout(() => { setCopiedAddress(null); }, 2000); return () => clearTimeout(timer); } return undefined; }, [copiedAddress]); // Handle copy with toast notification const handleCopy = async (address) => { try { await navigator.clipboard.writeText(address); setCopiedAddress(address); // Note: Toast notifications should be handled by the consuming app // We'll use the copiedAddress state to show feedback in the UI return true; } catch (err) { console.error("Failed to copy:", err); return false; } }; // Check if wallet is locked const isWalletLocked = hasWalletCapabilities && !walletExtension; // Get user avatar and name const getAvatarData = () => { if (!isAuthenticated || !user) { return { src: "", fallback: "?" }; } const profile = activeProfile || profiles[0]; const name = profile?.name || "User"; const avatar = profile?.image || ""; const initials = name .split(" ") .map((word) => word[0]) .join("") .toUpperCase() .slice(0, 2); return { src: avatar, fallback: initials, name }; }; const avatarData = getAvatarData(); // Handle unlock - works for both initial unlock and wallet re-unlock const handleUnlock = async () => { if (!unlockPassword) { setUnlockError("Password is required"); return; } setIsUnlocking(true); setUnlockError(null); try { if (!isAuthenticated && hasLocalBackup) { // Initial unlock - sign in with password await signIn(unlockPassword); setIsUnlockDialogOpen(false); setUnlockPassword(""); } else if (isAuthenticated) { // Re-unlock wallet features const isValid = await validatePassword(unlockPassword); if (isValid) { setIsUnlockDialogOpen(false); setUnlockPassword(""); onWalletUnlocked?.(); } else { setUnlockError("Invalid password"); } } } catch (error) { const errorMessage = error instanceof Error ? error.message : "Failed to unlock"; setUnlockError(errorMessage); } finally { setIsUnlocking(false); } }; // Create trigger element based on auth state const getTriggerElement = () => { if (trigger) return trigger; if (isAuthenticated) { // Authenticated - show avatar return (_jsx(Button, { variant: "ghost", size: "icon", className: "rounded-full", children: _jsx(BitcoinAvatar, { src: avatarData.src, alt: avatarData.name || "User", fallback: avatarData.fallback, size: "default" }) })); } // Not authenticated if (hasLocalBackup) { // Has backup - show unlock button return (_jsxs(Button, { variant: "secondary", size: "lg", children: [_jsx(LockClosedIcon, { className: "h-4 w-4 mr-2" }), "Unlock"] })); } // No backup - show sign up button return (_jsxs(Button, { variant: "secondary", size: "lg", children: [useMenuIcon ? (_jsx(HamburgerMenuIcon, { className: "h-4 w-4" })) : (_jsx(PersonIcon, { className: "h-4 w-4 mr-2" })), !useMenuIcon && "Sign Up"] })); }; const triggerElement = getTriggerElement(); return (_jsxs(_Fragment, { children: [_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: triggerElement }), _jsx(DropdownMenuContent, { align: align, side: side, className: "min-w-[240px]", children: isAuthenticated && user ? (_jsxs(_Fragment, { children: [showWalletBalance && hasWalletCapabilities && walletBalance && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuItem, { disabled: true, children: _jsx(CompactWalletOverview, { balance: walletBalance, showValues: !isWalletLocked, currency: currency, exchangeRate: exchangeRate }) }), _jsx(DropdownMenuSeparator, {})] })), showProfileDetails && activeProfile && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuItem, { disabled: true, children: _jsx("div", { className: "flex flex-col gap-2 w-full", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(BitcoinAvatar, { src: activeProfile.image, alt: activeProfile.name || "Profile", fallback: avatarData.fallback, size: "lg" }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("p", { className: "text-sm font-medium", children: activeProfile.name }), _jsxs("p", { className: "text-xs text-muted-foreground", children: [user.address.slice(0, 8), "...", user.address.slice(-6)] })] })] }) }) }), _jsx(DropdownMenuSeparator, {})] })), onViewProfile && (_jsxs(DropdownMenuItem, { onClick: onViewProfile, children: [_jsx(PersonIcon, { className: "mr-2 h-4 w-4" }), "View Profile"] })), user && (_jsxs(DropdownMenuSub, { children: [_jsxs(DropdownMenuSubTrigger, { children: [_jsx(CopyIcon, { className: "mr-2 h-4 w-4" }), "Copy Address"] }), _jsxs(DropdownMenuSubContent, { children: [_jsx(DropdownMenuItem, { onClick: () => handleCopy(user.address), children: _jsxs("div", { className: "flex items-center gap-1 w-full", children: [_jsx("span", { className: "text-sm", children: "Payment Address" }), copiedAddress === user.address && (_jsx(CheckIcon, { className: "ml-auto h-4 w-4 text-green-600" }))] }) }), user.ordinalsAddress && user.ordinalsAddress !== user.address && (_jsx(DropdownMenuItem, { onClick: () => user.ordinalsAddress && handleCopy(user.ordinalsAddress), children: _jsxs("div", { className: "flex items-center gap-1 w-full", children: [_jsx("span", { className: "text-sm", children: "Ordinals Address" }), copiedAddress === user.ordinalsAddress && (_jsx(CheckIcon, { className: "ml-auto h-4 w-4 text-green-600" }))] }) })), user.identityAddress && (_jsx(DropdownMenuItem, { onClick: () => user.identityAddress && handleCopy(user.identityAddress), children: _jsxs("div", { className: "flex items-center gap-1 w-full", children: [_jsx("span", { className: "text-sm", children: "Identity Address" }), copiedAddress === user.identityAddress && (_jsx(CheckIcon, { className: "ml-auto h-4 w-4 text-green-600" }))] }) }))] })] })), profiles.length > 1 && (_jsxs(DropdownMenuSub, { children: [_jsxs(DropdownMenuSubTrigger, { children: [_jsx(ChevronDownIcon, { className: "mr-2 h-4 w-4" }), "Switch Profile"] }), _jsxs(DropdownMenuSubContent, { children: [profiles.map((profile) => (_jsx(DropdownMenuItem, { onClick: () => switchProfile(profile.id), children: _jsxs("div", { className: "flex items-center gap-1 w-full", children: [_jsx(BitcoinAvatar, { src: profile.image, alt: profile.name || "Profile", fallback: profile.name?.[0] || "?", size: "sm" }), _jsx("span", { className: "text-sm", children: profile.name }), profile.id === activeProfile?.id && (_jsx(CheckIcon, { className: "ml-auto h-4 w-4" }))] }) }, profile.id))), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { children: [_jsx(PlusIcon, { className: "mr-2 h-4 w-4" }), "Create New Profile"] })] })] })), hasWalletCapabilities && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), isWalletLocked ? (_jsxs(DropdownMenuItem, { onClick: () => setIsUnlockDialogOpen(true), children: [_jsx(LockClosedIcon, { className: "mr-2 h-4 w-4" }), "Unlock Wallet"] })) : (_jsxs(DropdownMenuItem, { children: [_jsx(LockOpen1Icon, { className: "mr-2 h-4 w-4" }), _jsxs("div", { className: "flex items-center gap-1 w-full", children: [_jsx("span", { children: "Wallet Unlocked" }), _jsx(Badge, { variant: "secondary", className: "ml-auto", children: "Active" })] })] }))] })), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: onSettings, children: [_jsx(GearIcon, { className: "mr-2 h-4 w-4" }), "Settings"] }), additionalItems && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), additionalItems] })), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { className: "text-destructive", children: _jsx(AuthButton, { mode: "signout", variant: "ghost", size: "default", redirectTo: signOutRedirectTo }) })] })) : (_jsxs(_Fragment, { children: [hasLocalBackup ? ( // Has backup - show unlock option _jsxs(DropdownMenuItem, { onClick: () => setIsUnlockDialogOpen(true), children: [_jsx(LockClosedIcon, { className: "mr-2 h-4 w-4" }), "Unlock"] })) : ( // No backup - show sign up / sign in options _jsxs(_Fragment, { children: [_jsxs(DropdownMenuItem, { onClick: onCreateAccount, children: [_jsx(PersonIcon, { className: "mr-2 h-4 w-4" }), "Create Account"] }), _jsxs(DropdownMenuItem, { onClick: onSignIn, children: [_jsx(LockOpen1Icon, { className: "mr-2 h-4 w-4" }), "Sign In"] })] })), onSettings && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: onSettings, children: [_jsx(GearIcon, { className: "mr-2 h-4 w-4" }), "Settings"] })] })), additionalItems && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), additionalItems] }))] })) })] }), _jsx(Dialog, { open: isUnlockDialogOpen, onOpenChange: setIsUnlockDialogOpen, children: _jsxs(DialogContent, { className: "max-w-[400px]", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Unlock" }), _jsx("p", { className: "text-sm text-muted-foreground mb-4", children: "Enter your password to unlock" })] }), _jsx("form", { onSubmit: (e) => { e.preventDefault(); handleUnlock(); }, children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(PasswordInput, { value: unlockPassword, onChange: (value) => setUnlockPassword(value), placeholder: "Enter password" }), unlockError && (_jsx("p", { className: "text-sm text-destructive", children: unlockError })), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { variant: "secondary", type: "button", onClick: () => setIsUnlockDialogOpen(false), children: "Cancel" }), _jsxs(LoadingButton, { type: "submit", loading: isUnlocking, disabled: !unlockPassword, children: [_jsx(LockOpen1Icon, { className: "mr-2 h-4 w-4" }), "Unlock"] })] })] }) })] }) })] })); }