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 ; } return ; } function CashuWalletBrowserNotice() { return ( {/* Ambient glow */}
Built-in Ecash Wallet App only Self-custodial Cashu ecash, powered by Bitcoin Lightning
{/* Native-only explainer — framed as a premium capability, not a disabled feature */}

Unlock the full wallet in the SkateSpots app

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.

{/* What works right here — the actual money flow */}

Zap & tip — works in your browser now

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.

{['Nostr Wallet Connect', 'Alby / WebLN', 'Lightning address'].map((label) => ( {label} ))}
); } 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 ( Cashu Wallet Initializing wallet... ); } // Show wallet error state if (walletError) { return ( Wallet Error {walletError.message} ); } return ( <>
{/* Main Wallet Card */} Cashu Wallet Built-in ecash wallet — no external setup required {/* Balance Display — compact on mobile since BalanceHero already shows it */}
Balance
{totalBalance.toLocaleString()} sats
{usdValue !== null && (
≈ ${usdValue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
)}
{/* Active Mint Selector */} {mints.length > 0 && (
)} {/* Main Actions */} Receive Send {/* Receive Tab */} {/* Lightning Invoice */}
setQuoteAmount(e.target.value)} className="h-11 min-w-0" />
Or
{/* Cashu Token */}
setTokenInput(e.target.value)} className="font-mono text-xs h-11" />
{/* Send Tab */} {/* Send Token */}
setSendAmount(e.target.value)} className="min-w-0 h-11" />
Or
{/* Pay Lightning Invoice */}
setLightningInvoice(e.target.value)} className="font-mono text-xs h-11" />
Or
{/* Pay Lightning Address */}
setLightningAddress(e.target.value)} className="h-11" /> setLightningAddressAmount(e.target.value)} className="h-11" />
{/* History */} {history.length > 0 && (
{history.slice(0, 5).map((entry) => (
{entry.type === 'mint' || entry.type === 'receive' ? ( ) : ( )}
{entry.type === 'mint' && 'Received ⚡ Lightning'} {entry.type === 'melt' && 'Sent ⚡ Lightning'} {entry.type === 'send' && 'Sent 🥜 Token'} {entry.type === 'receive' && 'Received 🥜 Token'}
{new Date(entry.timestamp).toLocaleDateString()}
{entry.type === 'mint' || entry.type === 'receive' ? '+' : '-'} {entry.amount.toLocaleString()}
sats
))}
)}
{/* What is Cashu Info Card */} What is Cashu?

Cashu is digital cash backed by Bitcoin Lightning. Think of it as Bitcoin tokens you hold in your browser.

{[ 'Works offline at skate spots', 'Instant peer-to-peer payments', "Privacy-focused (mints can't track you)", 'Always redeemable for Lightning sats', ].map((item) => (
✅ {item}
))}
Find mints at mintradar.pedani.eu
{/* Mint Privacy Management */}
{/* Backup Warning */} {mnemonic && ( Backup Your Wallet

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.

This is like your Nostr nsec — keep it secret and backed up!

)} {/* Invoice Modal */} Receive {currentInvoice?.amount} sats Pay this invoice from any Lightning wallet {currentInvoice && (
Invoice QR Code

⏳ Waiting for payment...

)}
{/* Send Token Modal */} !open && setSentToken(null)}> Token Ready to Send Share this token with the recipient {sentToken && (
{sentTokenQRCode && (
Token QR Code
)}
Anyone with this token can claim it - treat it like cash!
)}
{/* 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 && ( )} {/* QR Scanner Modal */} setShowQRScanner(false)} onResult={handleQRScanResult} /> ); }