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, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useToast } from '@/hooks/useToast'; import { isNativePlatform } from '@/lib/platform'; 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: 'Community mint on LNbits', }; const RECOMMENDED_MINTS_WEB = [ { ...COINOS, description: 'Web-friendly mint (recommended for browsers)' }, LNBITS, { ...MINIBITS, description: '⚠ Native app only — not reachable from web browsers' }, ]; const RECOMMENDED_MINTS_NATIVE = [ { ...MINIBITS, description: 'SkateSpots mint — recommended' }, { ...COINOS, description: 'Web-friendly mint' }, LNBITS, ]; export function MintPrivacyManager({ mints, balances, activeMintUrl, onSetActiveMint, onAddMint, onRemoveMint, totalBalance, className, }: MintPrivacyManagerProps) { 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 { toast } = useToast(); // 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: 'No Mints', color: 'text-gray-500' }; if (mints.length === 1) return { score: 30, label: 'Single Mint', 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: 'Concentrated', color: 'text-amber-400' }; if (maxPercentage > 60) return { score: 70, label: 'Moderate', color: 'text-yellow-400' }; return { score: 90, label: 'Well Distributed', color: 'text-green-400' }; }; const privacyScore = getPrivacyScore(); const handleAddMint = async () => { if (!mintUrl.trim()) return; // Basic validation if (!mintUrl.startsWith('https://')) { toast({ variant: 'destructive', title: 'Invalid URL', description: 'Mint URL must start with https://' }); return; } setIsAddingMint(true); try { await onAddMint(mintUrl.trim()); setMintUrl(''); setShowAddMint(false); toast({ title: 'Mint Added', description: 'New mint added successfully' }); } catch (err) { toast({ variant: 'destructive', title: 'Failed to add mint', description: err instanceof Error ? err.message : 'Unknown error' }); } 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 ( <>
Mint Privacy {privacyScore.label}
Using multiple mints improves privacy by preventing any single mint from seeing all your transactions
{/* 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 && ( Using a single mint means that mint can see all your transaction timing and amounts. Consider adding more mints for better privacy. )} {/* Mint List */}
{mints.map((mint) => (
onSetActiveMint(mint.url)} >
{getCleanMintLabel(mint)} {activeMintUrl === mint.url && ( 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. */}
Soon

Spread received funds across multiple mints

{/* Privacy Tips Toggle */} {showPrivacyTips && (
• Use 2-3 different mints minimum for better privacy
• Don't keep all funds in one mint - spread them out
• Consider mint jurisdiction (different countries = different legal access)
• Mints see timing of transactions but NOT who you pay (blind signatures)

Future features: Mint rotation and privacy scoring - stay tuned!

)} {/* External Link */} Find more mints at mintradar.pedani.eu {/* Add Mint Dialog */} Add Mint Add a new Cashu mint to your wallet
{/* Manual Entry */}
setMintUrl(e.target.value)} className="font-mono text-xs" />
Or choose from list
{/* Recommended Mints */}
{RECOMMENDED_MINTS.map((mint) => { const isAdded = mints.some(m => m.url.toLowerCase() === mint.url.toLowerCase()); return (
{mint.name}

{mint.description}

{isAdded ? ( Added ) : ( )}
); })}
{/* Trust Warning */} Trust Warning: Mints are custodial - they hold your funds. Only use mints you trust. If a mint disappears, your funds may be lost. Spreading funds across multiple mints reduces risk.
{/* Remove Mint Confirmation */} !open && setMintToRemove(null)}> Remove Mint? Are you sure you want to remove {mintToRemove && getCleanMintLabel(mintToRemove)}?
{mintToRemove && (balances[mintToRemove.url] || 0) > 0 && ( Warning: This mint has {(balances[mintToRemove.url] || 0).toLocaleString()} sats. Withdraw your funds before removing! )}
); }