Sasquatch (the look regressed): I over-corrected last pass by shrinking the bubble on the map and bolting an always-visible ✕ over the avatar on mobile. Restored the full-size bubble + glow on every route (the map only loses the pulse), moved Hide to a desktop-hover ✕ and an EyeOff "Hide assistant" button in the panel header (Settings → Appearance still re-enables it). Mints: the add-mint dialog only offered a hand-picked "Recommended" shortlist. It now also lists the full searchable MintRadar directory (useMintDirectory) below the recommendations. Events: CreateEventPage could only link a skate spot. Added an optional "Link to a Shop" picker (useSkateShops, search, location autofill) that writes ['a','30017:…'] + ['venue-name', …].
565 lines
23 KiB
TypeScript
565 lines
23 KiB
TypeScript
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<void>;
|
|
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<Mint | null>(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 (
|
|
<>
|
|
<Card className={cn("border-purple-500/30", className)}>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<Shield className="h-5 w-5 text-purple-500" />
|
|
{t('walletUi.mintPrivacy.title')}
|
|
</CardTitle>
|
|
<Badge
|
|
variant="outline"
|
|
className={cn("text-xs", privacyScore.color)}
|
|
>
|
|
{privacyScore.label}
|
|
</Badge>
|
|
</div>
|
|
<CardDescription className="text-xs">
|
|
{t('walletUi.mintPrivacy.desc')}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
|
|
<CardContent className="space-y-4">
|
|
{/* Distribution Visualization */}
|
|
{mints.length > 0 && totalBalance > 0 && (
|
|
<div className="space-y-2">
|
|
<Label className="text-xs text-muted-foreground">{t('walletUi.mintPrivacy.distribution')}</Label>
|
|
<div className="h-3 rounded-full bg-gray-800 overflow-hidden flex">
|
|
{mintDistribution.map((item, index) => (
|
|
<div
|
|
key={item.mint.url}
|
|
className={cn(
|
|
"h-full transition-all",
|
|
index === 0 && "bg-purple-500",
|
|
index === 1 && "bg-blue-500",
|
|
index === 2 && "bg-green-500",
|
|
index === 3 && "bg-amber-500",
|
|
index >= 4 && "bg-gray-500"
|
|
)}
|
|
style={{ width: `${item.percentage}%` }}
|
|
title={`${getCleanMintLabel(item.mint)}: ${item.percentage.toFixed(1)}%`}
|
|
/>
|
|
))}
|
|
</div>
|
|
<div className="flex flex-wrap gap-2 text-xs">
|
|
{mintDistribution.map((item, index) => (
|
|
<div key={item.mint.url} className="flex items-center gap-1">
|
|
<div className={cn(
|
|
"w-2 h-2 rounded-full",
|
|
index === 0 && "bg-purple-500",
|
|
index === 1 && "bg-blue-500",
|
|
index === 2 && "bg-green-500",
|
|
index === 3 && "bg-amber-500",
|
|
index >= 4 && "bg-gray-500"
|
|
)} />
|
|
<span className="text-gray-400">{getCleanMintLabel(item.mint)}</span>
|
|
<span className="text-gray-500">({item.percentage.toFixed(0)}%)</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Privacy Warning */}
|
|
{mints.length === 1 && (
|
|
<Alert className="border-amber-500/30 bg-amber-500/5">
|
|
<AlertTriangle className="h-4 w-4 text-amber-500" />
|
|
<AlertDescription className="text-xs">
|
|
{t('walletUi.mintPrivacy.singleWarning')}
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{/* Mint List */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium">{t('walletUi.mintPrivacy.yourMints', { count: mints.length })}</Label>
|
|
<div className="space-y-2 max-h-48 overflow-y-auto">
|
|
{mints.map((mint) => (
|
|
<div
|
|
key={mint.url}
|
|
className={cn(
|
|
"flex items-center justify-between p-3 rounded-lg border transition-colors",
|
|
activeMintUrl === mint.url
|
|
? "bg-purple-500/10 border-purple-500/30"
|
|
: "bg-muted/50 border-muted"
|
|
)}
|
|
>
|
|
<div
|
|
className="flex-1 min-w-0 cursor-pointer"
|
|
onClick={() => onSetActiveMint(mint.url)}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-medium text-sm truncate">
|
|
{getCleanMintLabel(mint)}
|
|
</span>
|
|
{activeMintUrl === mint.url && (
|
|
<Badge variant="secondary" className="text-[10px]">
|
|
<Star className="h-2.5 w-2.5 mr-0.5" />
|
|
{t('walletUi.mintPrivacy.default')}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{(balances[mint.url] || 0).toLocaleString()} sats
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setMintToRemove(mint)}
|
|
className="text-destructive hover:text-destructive h-8 w-8 p-0"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Add Mint */}
|
|
<Button
|
|
variant="outline"
|
|
className="w-full gap-2"
|
|
onClick={() => setShowAddMint(true)}
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
{t('walletUi.mintPrivacy.addMint')}
|
|
</Button>
|
|
|
|
{/* 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. */}
|
|
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/50 opacity-70">
|
|
<div className="space-y-0.5">
|
|
<div className="flex items-center gap-2">
|
|
<Label className="text-sm font-medium">{t('walletUi.mintPrivacy.autoDistribute')}</Label>
|
|
<Badge variant="outline" className="text-[10px] text-amber-400 border-amber-500/30">
|
|
{t('walletUi.mintPrivacy.soon')}
|
|
</Badge>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t('walletUi.mintPrivacy.autoDesc')}
|
|
</p>
|
|
</div>
|
|
<Switch checked={false} disabled aria-label={t('walletUi.mintPrivacy.autoAria')} />
|
|
</div>
|
|
|
|
{/* Privacy Tips Toggle */}
|
|
<Button
|
|
variant="ghost"
|
|
className="w-full justify-between text-sm"
|
|
onClick={() => setShowPrivacyTips(!showPrivacyTips)}
|
|
>
|
|
<span className="flex items-center gap-2">
|
|
<Info className="h-4 w-4 text-blue-500" />
|
|
{t('walletUi.privacy.title')}
|
|
</span>
|
|
{showPrivacyTips ? (
|
|
<ChevronUp className="h-4 w-4" />
|
|
) : (
|
|
<ChevronDown className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
|
|
{showPrivacyTips && (
|
|
<div className="space-y-2 text-xs p-3 rounded-lg bg-blue-500/5 border border-blue-500/20">
|
|
<div className="flex items-start gap-2">
|
|
<span className="text-blue-400 font-bold">•</span>
|
|
<span className="text-gray-300">{t('walletUi.mintPrivacy.tip1')}</span>
|
|
</div>
|
|
<div className="flex items-start gap-2">
|
|
<span className="text-blue-400 font-bold">•</span>
|
|
<span className="text-gray-300">{t('walletUi.mintPrivacy.tip2')}</span>
|
|
</div>
|
|
<div className="flex items-start gap-2">
|
|
<span className="text-blue-400 font-bold">•</span>
|
|
<span className="text-gray-300">{t('walletUi.mintPrivacy.tip3')}</span>
|
|
</div>
|
|
<div className="flex items-start gap-2">
|
|
<span className="text-blue-400 font-bold">•</span>
|
|
<span className="text-gray-300">{t('walletUi.mintPrivacy.tip4')}</span>
|
|
</div>
|
|
<div className="mt-2 pt-2 border-t border-blue-500/20">
|
|
<p className="text-gray-500 italic">
|
|
{t('walletUi.mintPrivacy.future')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* External Link */}
|
|
<a
|
|
href="https://mintradar.pedani.eu/"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground"
|
|
>
|
|
<ExternalLink className="h-3 w-3" />
|
|
{t('walletUi.mintPrivacy.findMore')}
|
|
</a>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Add Mint Dialog */}
|
|
<Dialog open={showAddMint} onOpenChange={setShowAddMint}>
|
|
<DialogContent className="w-[calc(100%-1rem)] sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>{t('walletUi.mintPrivacy.addMint')}</DialogTitle>
|
|
<DialogDescription>
|
|
{t('walletUi.mintPrivacy.addMintDesc')}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
{/* Manual Entry */}
|
|
<div className="space-y-2">
|
|
<Label className="text-sm">{t('walletUi.mintPrivacy.mintUrl')}</Label>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
placeholder="https://mint.example.com"
|
|
value={mintUrl}
|
|
onChange={(e) => setMintUrl(e.target.value)}
|
|
className="font-mono text-xs"
|
|
/>
|
|
<Button
|
|
onClick={handleAddMint}
|
|
disabled={!mintUrl || isAddingMint}
|
|
>
|
|
{isAddingMint ? t('walletUi.mintPrivacy.adding') : t('walletUi.mintPrivacy.add')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative">
|
|
<div className="absolute inset-0 flex items-center">
|
|
<span className="w-full border-t" />
|
|
</div>
|
|
<div className="relative flex justify-center text-xs uppercase">
|
|
<span className="bg-background px-2 text-muted-foreground">{t('walletUi.mintPrivacy.orChoose')}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recommended Mints */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs text-muted-foreground">{t('walletUi.mintPrivacy.recommended')}</Label>
|
|
{RECOMMENDED_MINTS.map((mint) => {
|
|
const isAdded = mints.some(m => m.url.toLowerCase() === mint.url.toLowerCase());
|
|
return (
|
|
<div
|
|
key={mint.url}
|
|
className={cn(
|
|
"flex items-center justify-between p-3 rounded-lg border",
|
|
isAdded ? "bg-green-500/5 border-green-500/20" : "bg-muted/50"
|
|
)}
|
|
>
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
<Globe className="h-4 w-4 text-muted-foreground" />
|
|
<span className="font-medium text-sm">{mint.name}</span>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground ml-6">{t(mint.description)}</p>
|
|
</div>
|
|
{isAdded ? (
|
|
<Badge variant="secondary" className="text-xs">{t('walletUi.mintPrivacy.added')}</Badge>
|
|
) : (
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={async () => {
|
|
setIsAddingMint(true);
|
|
try {
|
|
await onAddMint(mint.url);
|
|
toast({ title: t('walletUi.mintPrivacy.mintAdded'), description: t('walletUi.mintPrivacy.mintAddedNamed', { name: mint.name }) });
|
|
} catch (err) {
|
|
toast({
|
|
variant: 'destructive',
|
|
title: t('walletUi.mintPrivacy.failed'),
|
|
description: err instanceof Error ? err.message : t('walletUi.cashu.unknownError')
|
|
});
|
|
} finally {
|
|
setIsAddingMint(false);
|
|
}
|
|
}}
|
|
disabled={isAddingMint}
|
|
>
|
|
Add
|
|
</Button>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Full directory (MintRadar) — the recommended list above is just a
|
|
shortlist; the whole community directory is searchable here. */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs text-muted-foreground">{t('walletUi.mintPrivacy.allMints')}</Label>
|
|
<div className="relative">
|
|
<Search className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
|
<Input
|
|
value={mintQuery}
|
|
onChange={(e) => setMintQuery(e.target.value)}
|
|
placeholder={t('walletUi.mintPrivacy.searchMints')}
|
|
className="pl-8 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="max-h-56 space-y-1 overflow-y-auto rounded-lg border p-1.5">
|
|
{directoryLoading ? (
|
|
<p className="py-4 text-center text-xs text-muted-foreground">{t('common.loading')}</p>
|
|
) : directoryMints.length === 0 ? (
|
|
<p className="py-4 text-center text-xs text-muted-foreground">{t('walletUi.mintPrivacy.noDirectoryMints')}</p>
|
|
) : (
|
|
directoryMints.map((mint) => {
|
|
const isAdded = mints.some((m) => m.url.toLowerCase() === mint.url.toLowerCase());
|
|
return (
|
|
<div
|
|
key={mint.url}
|
|
className={cn('flex items-center justify-between gap-2 rounded-lg px-2 py-1.5', isAdded ? 'bg-green-500/5' : 'bg-muted/40')}
|
|
>
|
|
<div className="min-w-0">
|
|
<p className="truncate text-xs font-medium">{mint.name || getCleanMintLabel({ url: mint.url })}</p>
|
|
<p className="truncate font-mono text-[10px] text-muted-foreground">{mint.url.replace(/^https?:\/\//, '')}</p>
|
|
</div>
|
|
{isAdded ? (
|
|
<Badge variant="secondary" className="shrink-0 text-[10px]">{t('walletUi.mintPrivacy.added')}</Badge>
|
|
) : (
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="shrink-0"
|
|
disabled={isAddingMint}
|
|
onClick={async () => {
|
|
setIsAddingMint(true);
|
|
try {
|
|
await onAddMint(mint.url);
|
|
toast({ title: t('walletUi.mintPrivacy.mintAdded'), description: t('walletUi.mintPrivacy.mintAddedNamed', { name: mint.name || mint.url }) });
|
|
} catch (err) {
|
|
toast({ variant: 'destructive', title: t('walletUi.mintPrivacy.failed'), description: err instanceof Error ? err.message : t('walletUi.cashu.unknownError') });
|
|
} finally {
|
|
setIsAddingMint(false);
|
|
}
|
|
}}
|
|
>
|
|
{t('walletUi.mintPrivacy.add')}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
<p className="text-[10px] text-muted-foreground">{t('walletUi.mintPrivacy.directoryHint')}</p>
|
|
</div>
|
|
|
|
{/* Trust Warning */}
|
|
<Alert className="border-amber-500/30 bg-amber-500/5">
|
|
<AlertTriangle className="h-4 w-4 text-amber-500" />
|
|
<AlertDescription className="text-xs">
|
|
<span className="font-bold text-amber-400">{t('walletUi.mintPrivacy.trustWarning')}</span> {t('walletUi.mintPrivacy.trustWarningDesc')}
|
|
</AlertDescription>
|
|
</Alert>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Remove Mint Confirmation */}
|
|
<Dialog open={!!mintToRemove} onOpenChange={(open) => !open && setMintToRemove(null)}>
|
|
<DialogContent className="w-[calc(100%-1rem)] sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>{t('walletUi.mintPrivacy.removeTitle')}</DialogTitle>
|
|
<DialogDescription>
|
|
{t('walletUi.mintPrivacy.removeDesc', { name: mintToRemove ? getCleanMintLabel(mintToRemove) : '' })}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
{mintToRemove && (balances[mintToRemove.url] || 0) > 0 && (
|
|
<Alert className="border-red-500/30 bg-red-500/5">
|
|
<AlertTriangle className="h-4 w-4 text-red-500" />
|
|
<AlertDescription className="text-xs">
|
|
<span className="font-bold text-red-400">{t('walletUi.mintPrivacy.warning')}</span> {t('walletUi.mintPrivacy.withdrawWarning', { amount: (balances[mintToRemove.url] || 0).toLocaleString() })}
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
<div className="flex gap-2 justify-end">
|
|
<Button variant="outline" onClick={() => setMintToRemove(null)}>
|
|
{t('common.cancel')}
|
|
</Button>
|
|
<Button variant="destructive" onClick={handleConfirmRemove}>
|
|
{t('walletUi.mintPrivacy.removeMint')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
}
|