bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
130 lines (129 loc) • 12.8 kB
JavaScript
"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"] })] })] }) })] }) })] }));
}