import { useState } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Switch } from '@/components/ui/switch'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Shield, Plus, Trash2, AlertTriangle, Info, ExternalLink, ChevronDown, ChevronUp, Star, Globe, Search, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useToast } from '@/hooks/useToast'; import { useTranslation } from '@/contexts/language'; import { isNativePlatform } from '@/lib/platform'; import { useMintDirectory } from '@/hooks/useMintDirectory'; interface Mint { url: string; name?: string; info?: unknown; } interface MintPrivacyManagerProps { mints: Mint[]; balances: { [mintUrl: string]: number }; activeMintUrl: string | null; onSetActiveMint: (url: string) => void; onAddMint: (url: string) => Promise; onRemoveMint: (mint: Mint) => void; totalBalance: number; className?: string; } // Recommended mints (no stablecoin mints). // Ordered by browser reachability — web wallets can only use mints that send // CORS headers. Minibits works great natively but does NOT allow browser // requests, so on the web it's flagged and listed last. In a native shell there // is no CORS restriction, so Minibits (the SkateSpots brand mint) leads. const MINIBITS = { url: 'https://mint.minibits.cash/Bitcoin', name: 'Minibits', }; const COINOS = { url: 'https://mint.coinos.io', name: 'Coinos', }; const LNBITS = { url: 'https://legend.lnbits.com/cashu/api/v1/AptDNABNBXv8gpuywhx6NV', name: 'LNbits Legend', description: 'walletUi.mintPrivacy.mintLnbitsDesc', }; const RECOMMENDED_MINTS_WEB = [ { ...COINOS, description: 'walletUi.mintPrivacy.mintCoinosWebDesc' }, LNBITS, { ...MINIBITS, description: 'walletUi.mintPrivacy.mintMinibitsWebDesc' }, ]; const RECOMMENDED_MINTS_NATIVE = [ { ...MINIBITS, description: 'walletUi.mintPrivacy.mintMinibitsNativeDesc' }, { ...COINOS, description: 'walletUi.mintPrivacy.mintCoinosDesc' }, LNBITS, ]; export function MintPrivacyManager({ mints, balances, activeMintUrl, onSetActiveMint, onAddMint, onRemoveMint, totalBalance, className, }: MintPrivacyManagerProps) { const { t } = useTranslation(); const RECOMMENDED_MINTS = isNativePlatform() ? RECOMMENDED_MINTS_NATIVE : RECOMMENDED_MINTS_WEB; const [showPrivacyTips, setShowPrivacyTips] = useState(false); const [showAddMint, setShowAddMint] = useState(false); const [mintUrl, setMintUrl] = useState(''); const [isAddingMint, setIsAddingMint] = useState(false); const [mintToRemove, setMintToRemove] = useState(null); const [mintQuery, setMintQuery] = useState(''); const { toast } = useToast(); // The full MintRadar community directory, so the add dialog isn't limited to a // handful of hand-picked recommendations. const { data: directory, isLoading: directoryLoading } = useMintDirectory(); const directoryMints = (directory ?? []).filter((m) => { if (!mintQuery) return true; const q = mintQuery.toLowerCase(); return (m.name?.toLowerCase().includes(q) ?? false) || m.url.toLowerCase().includes(q); }); // Calculate distribution across mints const mintDistribution = mints.map(mint => ({ mint, balance: balances[mint.url] || 0, percentage: totalBalance > 0 ? ((balances[mint.url] || 0) / totalBalance) * 100 : 0, })); // Privacy score based on distribution const getPrivacyScore = () => { if (mints.length === 0) return { score: 0, label: t('walletUi.mintPrivacy.noMints'), color: 'text-gray-500' }; if (mints.length === 1) return { score: 30, label: t('walletUi.mintPrivacy.singleMint'), color: 'text-red-400' }; // Check if funds are well distributed const maxPercentage = Math.max(...mintDistribution.map(m => m.percentage)); if (maxPercentage > 80) return { score: 50, label: t('walletUi.mintPrivacy.concentrated'), color: 'text-amber-400' }; if (maxPercentage > 60) return { score: 70, label: t('walletUi.mintPrivacy.moderate'), color: 'text-yellow-400' }; return { score: 90, label: t('walletUi.mintPrivacy.wellDistributed'), color: 'text-green-400' }; }; const privacyScore = getPrivacyScore(); const handleAddMint = async () => { if (!mintUrl.trim()) return; // Basic validation if (!mintUrl.startsWith('https://')) { toast({ variant: 'destructive', title: t('walletUi.mintPrivacy.invalidUrl'), description: t('walletUi.mintPrivacy.invalidUrlDesc') }); return; } setIsAddingMint(true); try { await onAddMint(mintUrl.trim()); setMintUrl(''); setShowAddMint(false); toast({ title: t('walletUi.mintPrivacy.mintAdded'), description: t('walletUi.mintPrivacy.mintAddedDesc') }); } catch (err) { toast({ variant: 'destructive', title: t('walletUi.mintPrivacy.failedAdd'), description: err instanceof Error ? err.message : t('walletUi.cashu.unknownError') }); } finally { setIsAddingMint(false); } }; const handleConfirmRemove = () => { if (mintToRemove) { onRemoveMint(mintToRemove); setMintToRemove(null); } }; const getCleanMintLabel = (mint: Mint) => { if (mint.name) return mint.name; try { const url = new URL(mint.url); return url.hostname; } catch { return mint.url; } }; return ( <>
{t('walletUi.mintPrivacy.title')} {privacyScore.label}
{t('walletUi.mintPrivacy.desc')}
{/* Distribution Visualization */} {mints.length > 0 && totalBalance > 0 && (
{mintDistribution.map((item, index) => (
= 4 && "bg-gray-500" )} style={{ width: `${item.percentage}%` }} title={`${getCleanMintLabel(item.mint)}: ${item.percentage.toFixed(1)}%`} /> ))}
{mintDistribution.map((item, index) => (
= 4 && "bg-gray-500" )} /> {getCleanMintLabel(item.mint)} ({item.percentage.toFixed(0)}%)
))}
)} {/* Privacy Warning */} {mints.length === 1 && ( {t('walletUi.mintPrivacy.singleWarning')} )} {/* Mint List */}
{mints.map((mint) => (
onSetActiveMint(mint.url)} >
{getCleanMintLabel(mint)} {activeMintUrl === mint.url && ( {t('walletUi.mintPrivacy.default')} )}
{(balances[mint.url] || 0).toLocaleString()} sats
))}
{/* Add Mint */} {/* Auto Distribution Toggle — not yet implemented, so it's shown as a disabled affordance with a "Soon" badge rather than a switch that flips on but does nothing. */}
{t('walletUi.mintPrivacy.soon')}

{t('walletUi.mintPrivacy.autoDesc')}

{/* Privacy Tips Toggle */} {showPrivacyTips && (
• {t('walletUi.mintPrivacy.tip1')}
• {t('walletUi.mintPrivacy.tip2')}
• {t('walletUi.mintPrivacy.tip3')}
• {t('walletUi.mintPrivacy.tip4')}

{t('walletUi.mintPrivacy.future')}

)} {/* External Link */} {t('walletUi.mintPrivacy.findMore')} {/* Add Mint Dialog */} {t('walletUi.mintPrivacy.addMint')} {t('walletUi.mintPrivacy.addMintDesc')}
{/* Manual Entry */}
setMintUrl(e.target.value)} className="font-mono text-xs" />
{t('walletUi.mintPrivacy.orChoose')}
{/* Recommended Mints */}
{RECOMMENDED_MINTS.map((mint) => { const isAdded = mints.some(m => m.url.toLowerCase() === mint.url.toLowerCase()); return (
{mint.name}

{t(mint.description)}

{isAdded ? ( {t('walletUi.mintPrivacy.added')} ) : ( )}
); })}
{/* Full directory (MintRadar) — the recommended list above is just a shortlist; the whole community directory is searchable here. */}
setMintQuery(e.target.value)} placeholder={t('walletUi.mintPrivacy.searchMints')} className="pl-8 text-xs" />
{directoryLoading ? (

{t('common.loading')}

) : directoryMints.length === 0 ? (

{t('walletUi.mintPrivacy.noDirectoryMints')}

) : ( directoryMints.map((mint) => { const isAdded = mints.some((m) => m.url.toLowerCase() === mint.url.toLowerCase()); return (

{mint.name || getCleanMintLabel({ url: mint.url })}

{mint.url.replace(/^https?:\/\//, '')}

{isAdded ? ( {t('walletUi.mintPrivacy.added')} ) : ( )}
); }) )}

{t('walletUi.mintPrivacy.directoryHint')}

{/* Trust Warning */} {t('walletUi.mintPrivacy.trustWarning')} {t('walletUi.mintPrivacy.trustWarningDesc')}
{/* Remove Mint Confirmation */} !open && setMintToRemove(null)}> {t('walletUi.mintPrivacy.removeTitle')} {t('walletUi.mintPrivacy.removeDesc', { name: mintToRemove ? getCleanMintLabel(mintToRemove) : '' })}
{mintToRemove && (balances[mintToRemove.url] || 0) > 0 && ( {t('walletUi.mintPrivacy.warning')} {t('walletUi.mintPrivacy.withdrawWarning', { amount: (balances[mintToRemove.url] || 0).toLocaleString() })} )}
); }