SkateSpots/src/components/wallet/CashuWalletCard.tsx
SkateSpots 6def78fffe chore(lint): clear all no-unused-vars warnings (position-precise)
Removed/renamed the remaining 39 unused locals/dead code, file-by-file:
- unused destructured bindings/params (remove the binding, keep the call)
- unused single-line pure consts and dead functions/interfaces removed
- deleted the dead _FeedItem component and its now-unused imports
- amber _readChunks self-call fixed; SpotMap popup call site updated

Warnings: 182 -> 7 (only react-refresh/only-export-components remain).
no-explicit-any=0, exhaustive-deps=0, no-unused-vars=0.
Gate: 101 tests, 0 errors, boot smoke green. AGENTS.md baseline + guidance updated.
2026-09-26 22:20:21 +07:00

837 lines
34 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback } from 'react';
import { useCashu } from '@/hooks/wallet/useCashu';
import { useMintManager } from '@/hooks/wallet/useMintManager';
import { useLightningOperations } from '@/hooks/wallet/useLightningOperations';
import { useTokenOperations } from '@/hooks/wallet/useTokenOperations';
import { useHistoryManager } from '@/hooks/wallet/useHistoryManager';
import { useSatsToUsd } from '@/hooks/useExchangeRate';
import { useLightningAddress } from '@/hooks/useLightningAddress';
import { generateQRCode } from '@/lib/wallet/qrCodeGenerator';
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 { Skeleton } from '@/components/ui/skeleton';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Wallet,
ArrowDownCircle,
ArrowUpCircle,
History,
Copy,
ExternalLink,
AlertCircle,
Shield,
ScanLine,
Zap,
Nut,
} from 'lucide-react';
import { useToast } from '@/hooks/useToast';
import { QRScannerModal } from '@/components/wallet/QRScannerModal';
import { MintPrivacyManager } from '@/components/wallet/MintPrivacyManager';
import { RecoveryPhraseDialog } from '@/components/wallet/RecoveryPhraseDialog';
import { isNativePlatform } from '@/lib/platform';
import { Smartphone } from 'lucide-react';
/**
* The built-in Cashu (ecash) mint wallet only works where there's no browser
* CORS jail — i.e. in the native app. In a plain web browser most mints block
* requests (no Access-Control-Allow-Origin), so minting/melting fails. Rather
* than ship a browser feature that unreliably errors, we gate the full wallet
* to native and, in the browser, explain how to transact (zaps via a connected
* Lightning wallet, or use the native app).
*
* Zaps/tips — the app's actual money flow — keep working in the browser via
* NWC / WebLN / Lightning address, none of which touch a Cashu mint.
*/
export function CashuWalletCard() {
if (!isNativePlatform()) {
return <CashuWalletBrowserNotice />;
}
return <CashuWalletCardNative />;
}
function CashuWalletBrowserNotice() {
return (
<Card className="relative overflow-hidden border-orange-500/30 bg-gradient-to-br from-orange-500/[0.07] via-background to-yellow-500/[0.05]">
{/* Ambient glow */}
<div className="pointer-events-none absolute -top-16 -right-16 h-40 w-40 rounded-full bg-orange-500/10 blur-3xl" />
<div className="pointer-events-none absolute -bottom-20 -left-10 h-40 w-40 rounded-full bg-yellow-500/[0.07] blur-3xl" />
<CardHeader className="relative p-5 pb-3">
<div className="flex items-center gap-3">
<div className="relative flex h-11 w-11 items-center justify-center rounded-2xl bg-gradient-to-br from-orange-500 to-amber-500 shadow-lg shadow-orange-500/25">
<Nut className="h-5 w-5 text-black" />
</div>
<div className="min-w-0">
<CardTitle className="text-base font-black tracking-tight flex items-center gap-2">
Built-in Ecash Wallet
<Badge className="border-0 bg-orange-500/15 text-[10px] font-bold uppercase tracking-wider text-orange-400">
App only
</Badge>
</CardTitle>
<CardDescription className="text-xs mt-0.5">
Self-custodial Cashu ecash, powered by Bitcoin Lightning
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="relative space-y-4 p-5 pt-1">
{/* Native-only explainer — framed as a premium capability, not a disabled feature */}
<div className="rounded-2xl border border-orange-500/20 bg-gradient-to-br from-orange-500/[0.08] to-transparent p-4">
<div className="flex items-start gap-3">
<div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-xl bg-orange-500/15">
<Smartphone className="h-4 w-4 text-orange-400" />
</div>
<div className="space-y-1.5">
<p className="text-sm font-bold leading-tight">Unlock the full wallet in the SkateSpots app</p>
<p className="text-xs text-muted-foreground leading-relaxed">
The built-in ecash wallet lets you mint, send and redeem Cashu tokens right at the
spot. It lives in the native app, where it can talk to mints directly — browsers
sandbox those connections for your security.
</p>
</div>
</div>
</div>
{/* What works right here — the actual money flow */}
<div className="rounded-2xl border bg-muted/30 p-4">
<p className="text-sm font-bold flex items-center gap-2">
<span className="flex h-6 w-6 items-center justify-center rounded-lg bg-amber-500/15">
<Zap className="h-3.5 w-3.5 text-amber-500" />
</span>
Zap &amp; tip — works in your browser now
</p>
<p className="mt-2 text-xs text-muted-foreground leading-relaxed">
Sending sats to spots, events, coaches and crews works right here. Just connect a
Lightning wallet below — Nostr Wallet Connect or a browser extension like Alby. No
Cashu mint needed.
</p>
<div className="mt-3 flex flex-wrap gap-1.5">
{['Nostr Wallet Connect', 'Alby / WebLN', 'Lightning address'].map((label) => (
<span
key={label}
className="inline-flex items-center gap-1 rounded-full border border-amber-500/25 bg-amber-500/10 px-2.5 py-1 text-[11px] font-semibold text-amber-500/90"
>
<span className="h-1.5 w-1.5 rounded-full bg-green-500" />
{label}
</span>
))}
</div>
</div>
</CardContent>
</Card>
);
}
function CashuWalletCardNative() {
const {
coco,
repositories,
balances,
mints,
totalBalance,
isLoading: walletLoading,
error: walletError,
mnemonic,
needsBackup,
confirmBackup,
getEncodedToken,
refreshMints,
} = useCashu();
const usdValue = useSatsToUsd(totalBalance);
const { toast } = useToast();
const { getInvoice: getLightningInvoice, isLoading: isResolvingAddress } = useLightningAddress();
const {
activeMintUrl,
setActiveMintUrl,
handleAddMint,
handleRemoveMintDirect,
getCleanMintLabel,
} = useMintManager({ coco, mints, repositories, refreshMints });
const {
quoteAmount,
setQuoteAmount,
showInvoiceModal,
setShowInvoiceModal,
currentInvoice,
handleLightningReceive,
isGeneratingInvoice,
lightningInvoice,
setLightningInvoice,
isProcessingPayment,
handleLightningSend,
} = useLightningOperations({ coco, activeMintUrl, generateQRCode });
const {
tokenInput,
setTokenInput,
handleTokenReceive,
isReceivingToken,
sendAmount,
setSendAmount,
handleTokenSend,
isCreatingToken,
sentToken,
setSentToken,
sentTokenQRCode,
} = useTokenOperations({ coco, activeMintUrl, generateQRCode, getEncodedToken });
const { history } = useHistoryManager({
coco,
repositories,
mints,
});
// QR Scanner state
const [showQRScanner, setShowQRScanner] = useState(false);
// Recovery-phrase reveal dialog
const [showRecoveryPhrase, setShowRecoveryPhrase] = useState(false);
// Lightning address state. Its amount is kept SEPARATE from the token-send
// amount (`sendAmount`) so switching between "Create Token" and "Pay Address"
// never carries a stale value between two unrelated intents.
const [lightningAddress, setLightningAddress] = useState('');
const [lightningAddressAmount, setLightningAddressAmount] = useState('');
// Set first mint as active if none selected
useEffect(() => {
if (mints.length > 0 && !activeMintUrl) {
setActiveMintUrl(mints[0].url);
}
}, [mints, activeMintUrl, setActiveMintUrl]);
// Handle QR scan result
const handleQRScanResult = useCallback(async (result: { type: string; value: string }) => {
setShowQRScanner(false);
switch (result.type) {
case 'cashu_token':
setTokenInput(result.value);
// Auto-receive
if (coco) {
try {
await coco.wallet.receive(result.value);
toast({ title: 'Token received', description: 'Cashu token redeemed successfully!' });
} catch (err) {
toast({
variant: 'destructive',
title: 'Receive failed',
description: err instanceof Error ? err.message : 'Unknown error'
});
}
}
break;
case 'lightning_invoice':
setLightningInvoice(result.value);
break;
case 'lightning_address':
setLightningAddress(result.value);
break;
case 'bitcoin_address':
// Recognized, but this is an ecash/Lightning wallet — no on-chain send.
toast({
variant: 'destructive',
title: 'On-chain not supported here',
description: 'That’s a Bitcoin address. This wallet only sends Cashu tokens and Lightning payments.',
});
break;
case 'npub':
toast({
variant: 'destructive',
title: 'That’s a Nostr profile',
description: 'Scan a Cashu token, Lightning invoice, or Lightning address to pay.',
});
break;
case 'clink_offer':
case 'clink_debit':
toast({
variant: 'destructive',
title: 'Payment code not supported yet',
description: 'This wallet can’t pay CLINK codes yet. Use a Lightning invoice or address instead.',
});
break;
default:
toast({
variant: 'destructive',
title: 'Unrecognized QR code',
description: 'Scan a Cashu token, Lightning invoice, or Lightning address.',
});
}
}, [coco, setTokenInput, setLightningInvoice, toast]);
// Handle paying a Lightning address
const [isPayingAddress, setIsPayingAddress] = useState(false);
const handlePayLightningAddress = useCallback(async () => {
if (!lightningAddress || !lightningAddressAmount || !coco || !activeMintUrl) return;
try {
const amount = parseInt(lightningAddressAmount);
if (isNaN(amount) || amount <= 0) {
toast({ variant: 'destructive', title: 'Invalid amount', description: 'Please enter a valid amount' });
return;
}
setIsPayingAddress(true);
// First resolve the Lightning address to get an invoice
const invoice = await getLightningInvoice({ lightningAddress, amountSats: amount });
// Then pay the invoice using Cashu
const meltQuote = await coco.quotes.createMeltQuote(activeMintUrl, invoice);
await coco.quotes.payMeltQuote(activeMintUrl, meltQuote.quote);
toast({ title: 'Payment sent', description: `Sent ${amount} sats to ${lightningAddress}` });
setLightningAddress('');
setLightningAddressAmount('');
} catch (err) {
toast({
variant: 'destructive',
title: 'Payment failed',
description: err instanceof Error ? err.message : 'Unknown error'
});
} finally {
setIsPayingAddress(false);
}
}, [lightningAddress, lightningAddressAmount, coco, activeMintUrl, getLightningInvoice, toast]);
// Copy to clipboard helper
const copyToClipboard = async (text: string, label: string) => {
try {
await navigator.clipboard.writeText(text);
toast({ title: 'Copied', description: `${label} copied to clipboard` });
} catch (err) {
console.error('Failed to copy:', err);
}
};
// Show wallet loading state
if (walletLoading) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Wallet className="h-5 w-5 text-orange-500" />
Cashu Wallet
</CardTitle>
<CardDescription>Initializing wallet...</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Skeleton className="h-20 w-full" />
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
</CardContent>
</Card>
);
}
// Show wallet error state
if (walletError) {
return (
<Card className="border-destructive/50">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-destructive">
<AlertCircle className="h-5 w-5" />
Wallet Error
</CardTitle>
</CardHeader>
<CardContent>
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{walletError.message}</AlertDescription>
</Alert>
</CardContent>
</Card>
);
}
return (
<>
<div className="space-y-4">
{/* Main Wallet Card */}
<Card className="border-orange-500/30 bg-gradient-to-br from-orange-500/5 to-yellow-500/5">
<CardHeader className="p-4">
<CardTitle className="text-base font-bold flex items-center gap-2">
<Wallet className="h-5 w-5 text-orange-500" />
Cashu Wallet
</CardTitle>
<CardDescription className="text-xs">
Built-in ecash wallet — no external setup required
</CardDescription>
</CardHeader>
<CardContent className="space-y-4 p-4 pt-0">
{/* Balance Display — compact on mobile since BalanceHero already shows it */}
<div className="flex items-center justify-between py-3 px-4 rounded-xl bg-orange-500/10 border border-orange-500/20">
<div>
<div className="text-[10px] text-orange-400/80 uppercase tracking-widest font-bold mb-0.5">Balance</div>
<div
className="text-2xl font-black tabular-nums leading-none"
aria-live="polite"
aria-label={`Wallet balance ${totalBalance.toLocaleString()} sats`}
>
{totalBalance.toLocaleString()}
<span className="text-sm font-semibold text-muted-foreground ml-1.5">sats</span>
</div>
{usdValue !== null && (
<div className="text-xs text-muted-foreground mt-0.5">
≈ ${usdValue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div>
)}
</div>
<Nut className="h-8 w-8 text-orange-500/30 flex-shrink-0" />
</div>
{/* Active Mint Selector */}
{mints.length > 0 && (
<div className="space-y-1.5">
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">Active Mint</Label>
<Select value={activeMintUrl || undefined} onValueChange={setActiveMintUrl}>
<SelectTrigger className="h-11">
<SelectValue placeholder="Select a mint" />
</SelectTrigger>
<SelectContent>
{mints.map((mint) => (
<SelectItem key={mint.url} value={mint.url}>
<div className="flex items-center justify-between gap-3">
<span>{getCleanMintLabel(mint)}</span>
<span className="text-xs text-muted-foreground">
{balances[mint.url] || 0} sats
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{/* Main Actions */}
<Tabs defaultValue="receive" className="w-full">
<TabsList className="grid w-full grid-cols-2 h-11 rounded-xl">
<TabsTrigger value="receive" className="gap-2 rounded-lg h-full font-bold text-sm data-[state=active]:bg-green-600 data-[state=active]:text-white">
<ArrowDownCircle className="h-4 w-4" />
Receive
</TabsTrigger>
<TabsTrigger value="send" className="gap-2 rounded-lg h-full font-bold text-sm data-[state=active]:bg-red-600 data-[state=active]:text-white">
<ArrowUpCircle className="h-4 w-4" />
Send
</TabsTrigger>
</TabsList>
{/* Receive Tab */}
<TabsContent value="receive" className="space-y-4 mt-4">
{/* Lightning Invoice */}
<div className="space-y-3">
<Label className="text-sm font-semibold">⚡ Receive via Lightning</Label>
<div className="flex flex-col gap-2">
<Input
type="number"
inputMode="numeric"
placeholder="Amount in sats"
value={quoteAmount}
onChange={(e) => setQuoteAmount(e.target.value)}
className="h-11 min-w-0"
/>
<Button
onClick={handleLightningReceive}
disabled={!activeMintUrl || !quoteAmount || isGeneratingInvoice}
className="h-11 bg-orange-500 hover:bg-orange-600 font-bold"
>
{isGeneratingInvoice ? 'Generating…' : 'Generate Invoice'}
</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">Or</span>
</div>
</div>
{/* Cashu Token */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-sm font-semibold">🥜 Receive Cashu Token</Label>
<Button
variant="outline"
size="sm"
className="h-9 gap-1.5 px-3"
onClick={() => setShowQRScanner(true)}
>
<ScanLine className="h-4 w-4" />
Scan QR
</Button>
</div>
<div className="space-y-2">
<Input
placeholder="Paste Cashu token (cashu...)"
value={tokenInput}
onChange={(e) => setTokenInput(e.target.value)}
className="font-mono text-xs h-11"
/>
<Button
onClick={handleTokenReceive}
disabled={!tokenInput || isReceivingToken}
className="w-full h-11 bg-green-600 hover:bg-green-700 font-bold"
>
{isReceivingToken ? 'Receiving…' : 'Receive Token'}
</Button>
</div>
</div>
</TabsContent>
{/* Send Tab */}
<TabsContent value="send" className="space-y-4 mt-4">
{/* Send Token */}
<div className="space-y-3">
<Label className="text-sm font-semibold">🥜 Create Cashu Token</Label>
<div className="flex flex-col gap-2">
<Input
type="number"
inputMode="numeric"
placeholder="Amount in sats"
value={sendAmount}
onChange={(e) => setSendAmount(e.target.value)}
className="min-w-0 h-11"
/>
<Button
onClick={handleTokenSend}
disabled={!activeMintUrl || !sendAmount || isCreatingToken}
className="h-11 bg-orange-500 hover:bg-orange-600 font-bold"
>
{isCreatingToken ? 'Creating…' : 'Create Token'}
</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">Or</span>
</div>
</div>
{/* Pay Lightning Invoice */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-sm font-semibold">⚡ Pay Lightning Invoice</Label>
<Button
variant="outline"
size="sm"
className="h-9 gap-1.5 px-3"
onClick={() => setShowQRScanner(true)}
>
<ScanLine className="h-4 w-4" />
Scan QR
</Button>
</div>
<div className="space-y-2">
<Input
placeholder="Paste Lightning invoice (lnbc...)"
value={lightningInvoice}
onChange={(e) => setLightningInvoice(e.target.value)}
className="font-mono text-xs h-11"
/>
<Button
onClick={handleLightningSend}
disabled={!lightningInvoice || isProcessingPayment}
className="w-full h-11 bg-blue-600 hover:bg-blue-700 font-bold"
>
<Zap className="h-4 w-4 mr-2" />
{isProcessingPayment ? 'Processing...' : 'Pay Invoice'}
</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">Or</span>
</div>
</div>
{/* Pay Lightning Address */}
<div className="space-y-3">
<Label className="text-sm font-semibold">📧 Pay Lightning Address</Label>
<div className="space-y-2">
<Input
placeholder="user@domain.com"
value={lightningAddress}
onChange={(e) => setLightningAddress(e.target.value)}
className="h-11"
/>
<Input
type="number"
inputMode="numeric"
placeholder="Amount in sats"
value={lightningAddressAmount}
onChange={(e) => setLightningAddressAmount(e.target.value)}
className="h-11"
/>
<Button
onClick={handlePayLightningAddress}
disabled={!lightningAddress || !lightningAddressAmount || isResolvingAddress || isPayingAddress}
className="w-full h-11 bg-purple-600 hover:bg-purple-700 font-bold"
>
{isResolvingAddress || isPayingAddress ? 'Sending…' : 'Send to Address'}
</Button>
</div>
</div>
</TabsContent>
</Tabs>
{/* History */}
{history.length > 0 && (
<div className="space-y-3">
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wide flex items-center gap-2">
<History className="h-3.5 w-3.5" />
Recent Activity
</Label>
<div className="space-y-2 max-h-52 overflow-y-auto scroll-container">
{history.slice(0, 5).map((entry) => (
<div
key={entry.id}
className="flex items-center justify-between p-3 rounded-xl bg-muted/50 border min-h-[52px]"
>
<div className="flex items-center gap-2.5 min-w-0">
{entry.type === 'mint' || entry.type === 'receive' ? (
<ArrowDownCircle className="h-5 w-5 text-green-500 flex-shrink-0" />
) : (
<ArrowUpCircle className="h-5 w-5 text-red-500 flex-shrink-0" />
)}
<div className="min-w-0">
<div className="text-sm font-semibold leading-tight">
{entry.type === 'mint' && 'Received ⚡ Lightning'}
{entry.type === 'melt' && 'Sent ⚡ Lightning'}
{entry.type === 'send' && 'Sent 🥜 Token'}
{entry.type === 'receive' && 'Received 🥜 Token'}
</div>
<div className="text-xs text-muted-foreground">
{new Date(entry.timestamp).toLocaleDateString()}
</div>
</div>
</div>
<div className="text-right flex-shrink-0 ml-2">
<div className={`text-sm font-mono font-bold ${entry.type === 'mint' || entry.type === 'receive' ? 'text-green-400' : 'text-red-400'}`}>
{entry.type === 'mint' || entry.type === 'receive' ? '+' : '-'}
{entry.amount.toLocaleString()}
</div>
<div className="text-xs text-muted-foreground">sats</div>
</div>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
{/* What is Cashu Info Card */}
<Card className="border-gray-700">
<CardHeader className="px-4 pt-4 pb-3">
<CardTitle className="flex items-center gap-2 text-base">
<Zap className="h-5 w-5 text-amber-500" />
What is Cashu?
</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-4 space-y-3">
<p className="text-muted-foreground text-sm leading-relaxed">
Cashu is digital cash backed by Bitcoin Lightning. Think of it as Bitcoin tokens you hold in your browser.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm">
{[
'Works offline at skate spots',
'Instant peer-to-peer payments',
"Privacy-focused (mints can't track you)",
'Always redeemable for Lightning sats',
].map((item) => (
<div key={item} className="flex items-start gap-2 p-2.5 bg-muted/40 rounded-lg">
<span className="text-green-500 flex-shrink-0 mt-0.5">✅</span>
<span className="text-muted-foreground text-xs leading-snug">{item}</span>
</div>
))}
</div>
<a
href="https://mintradar.pedani.eu/"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-sm text-primary hover:underline font-medium"
>
<ExternalLink className="h-4 w-4" />
Find mints at mintradar.pedani.eu
</a>
</CardContent>
</Card>
{/* Mint Privacy Management */}
<MintPrivacyManager
mints={mints}
balances={balances}
activeMintUrl={activeMintUrl}
onSetActiveMint={setActiveMintUrl}
onAddMint={handleAddMint}
onRemoveMint={handleRemoveMintDirect}
totalBalance={totalBalance}
/>
</div>
{/* Backup Warning */}
{mnemonic && (
<Card className="border-amber-500/50 bg-amber-500/5">
<CardHeader className="px-4 pt-4 pb-3">
<CardTitle className="flex items-center gap-2 text-amber-600 text-base">
<Shield className="h-5 w-5" />
Backup Your Wallet
</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-4 space-y-3">
<p className="text-muted-foreground text-sm leading-relaxed">
Your wallet is secured with a 12-word recovery phrase. Save it safely — you'll need it to restore your balance if you clear browser data.
</p>
<Button
onClick={() => setShowRecoveryPhrase(true)}
variant="outline"
className="w-full h-11 border-amber-500/50 text-amber-600 hover:bg-amber-500/10 font-bold"
>
View Recovery Phrase
</Button>
<p className="text-xs text-muted-foreground italic">
This is like your Nostr nsec — keep it secret and backed up!
</p>
</CardContent>
</Card>
)}
{/* Invoice Modal */}
<Dialog open={showInvoiceModal} onOpenChange={setShowInvoiceModal}>
<DialogContent className="w-[calc(100%-1rem)] sm:max-w-md p-4 sm:p-6">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg">Receive {currentInvoice?.amount} sats</DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
Pay this invoice from any Lightning wallet
</DialogDescription>
</DialogHeader>
{currentInvoice && (
<div className="space-y-3 sm:space-y-4">
<div className="flex justify-center p-2 sm:p-4 bg-white rounded-lg">
<img src={currentInvoice.qrCodeUrl} alt="Invoice QR Code" className="max-w-full max-h-[40vh] h-auto object-contain" />
</div>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Lightning Invoice</Label>
<div className="flex gap-2">
<Input
value={currentInvoice.invoice}
readOnly
className="font-mono text-[10px] sm:text-xs"
/>
<Button
size="sm"
variant="outline"
onClick={() => copyToClipboard(currentInvoice.invoice, 'Invoice')}
>
<Copy className="h-4 w-4" />
</Button>
</div>
</div>
<p className="text-xs sm:text-sm text-amber-600">
⏳ Waiting for payment...
</p>
</div>
)}
</DialogContent>
</Dialog>
{/* Send Token Modal */}
<Dialog open={!!sentToken} onOpenChange={(open) => !open && setSentToken(null)}>
<DialogContent className="w-[calc(100%-1rem)] sm:max-w-md p-4 sm:p-6">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg">Token Ready to Send</DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
Share this token with the recipient
</DialogDescription>
</DialogHeader>
{sentToken && (
<div className="space-y-3 sm:space-y-4">
{sentTokenQRCode && (
<div className="flex justify-center p-2 sm:p-4 bg-white rounded-lg">
<img src={sentTokenQRCode} alt="Token QR Code" className="max-w-full max-h-[40vh] h-auto object-contain" />
</div>
)}
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Cashu Token</Label>
<div className="space-y-2">
<Input
value={sentToken}
readOnly
className="font-mono text-[10px] sm:text-xs"
/>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => copyToClipboard(sentToken, 'Token')}
className="flex-1"
>
<Copy className="h-4 w-4 mr-2" />
Copy Token
</Button>
</div>
</div>
</div>
<Alert className="border-amber-500/50 bg-amber-500/5">
<AlertCircle className="h-4 w-4 text-amber-600" />
<AlertDescription className="text-xs sm:text-sm">
Anyone with this token can claim it - treat it like cash!
</AlertDescription>
</Alert>
</div>
)}
</DialogContent>
</Dialog>
{/* Recovery Phrase Dialog — replaces the old raw alert() with an elegant,
hidden-by-default reveal.
When needsBackup is set (a freshly auto-generated seed), it becomes a
MANDATORY, non-dismissable backup step: the user must reveal the words
and confirm before they can start using the wallet. */}
{mnemonic && (
<RecoveryPhraseDialog
open={showRecoveryPhrase || needsBackup}
onOpenChange={setShowRecoveryPhrase}
mnemonic={mnemonic}
requireConfirm={needsBackup}
onConfirm={confirmBackup}
/>
)}
{/* QR Scanner Modal */}
<QRScannerModal
isOpen={showQRScanner}
onClose={() => setShowQRScanner(false)}
onResult={handleQRScanResult}
/>
</>
);
}