The drawCardToCanvas function had an old version of the card rendering logic that leaked outside the function body (lines 505-731 were orphaned code). This code referenced local variables (ctx, p, data, qrX, qrSize, PAD, etc.) that only existed inside the function, causing ReferenceError at module load time and crashing the entire app with a blank screen. Also added missing lucide-react imports (Printer, Loader2, Share2, Download, Check, Copy, Award, Zap, Info, Palette, Shield, ChevronUp, ChevronDown) that were used in the JSX but not imported, causing the secondary Printer is not defined ReferenceError. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
321 lines
11 KiB
TypeScript
321 lines
11 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useToast } from '@/hooks/useToast';
|
|
import { useWallet } from '@/hooks/useWallet';
|
|
import { useNWC } from '@/hooks/useNWCContext';
|
|
import { getInvoiceFromLightningAddress } from '@/lib/lightningAddress';
|
|
import { LISTING_FEE_SATS, PLATFORM_LIGHTNING_ADDRESS } from '@/hooks/useListingRestrictions';
|
|
import { Zap, Loader2, CheckCircle, Copy, ExternalLink, ShieldAlert, TrendingUp, MapPin } from 'lucide-react';
|
|
// qrcode imported lazily in async function
|
|
|
|
interface SpamFeePaymentProps {
|
|
feeRecipient: string;
|
|
feeRecipientName: string;
|
|
onPaymentSuccess: () => void;
|
|
onSkip?: () => void;
|
|
}
|
|
|
|
type PaymentState = 'idle' | 'generating' | 'waiting' | 'paying' | 'success' | 'error';
|
|
|
|
export function SpamFeePayment({
|
|
feeRecipient,
|
|
feeRecipientName,
|
|
onPaymentSuccess,
|
|
}: SpamFeePaymentProps) {
|
|
const { toast } = useToast();
|
|
const { webln, activeNWC } = useWallet();
|
|
const { sendPayment } = useNWC();
|
|
const [paymentState, setPaymentState] = useState<PaymentState>('idle');
|
|
const [invoice, setInvoice] = useState<string | null>(null);
|
|
const [qrCodeUrl, setQrCodeUrl] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Auto-pay when wallet is connected and invoice is generated
|
|
useEffect(() => {
|
|
if (invoice && paymentState === 'waiting' && (webln || activeNWC)) {
|
|
handleAutoPay();
|
|
}
|
|
}, [invoice, paymentState, webln, activeNWC]);
|
|
|
|
const generateInvoice = async () => {
|
|
setPaymentState('generating');
|
|
setError(null);
|
|
|
|
try {
|
|
const bolt11 = await getInvoiceFromLightningAddress({
|
|
lightningAddress: feeRecipient,
|
|
amountSats: LISTING_FEE_SATS,
|
|
comment: 'SkateSpots listing fee',
|
|
});
|
|
|
|
setInvoice(bolt11);
|
|
|
|
// Generate QR code
|
|
const { default: QRCode } = await import('qrcode');
|
|
const qr = await QRCode.toDataURL(bolt11.toUpperCase(), {
|
|
width: 256,
|
|
margin: 2,
|
|
color: {
|
|
dark: '#000000',
|
|
light: '#ffffff',
|
|
},
|
|
});
|
|
setQrCodeUrl(qr);
|
|
|
|
setPaymentState('waiting');
|
|
|
|
// If wallet is available, try auto-pay
|
|
if (webln || activeNWC) {
|
|
await handleAutoPay();
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to generate invoice:', err);
|
|
setError(err instanceof Error ? err.message : 'Failed to generate invoice');
|
|
setPaymentState('error');
|
|
}
|
|
};
|
|
|
|
const handleAutoPay = async () => {
|
|
if (!invoice) return;
|
|
|
|
setPaymentState('paying');
|
|
|
|
try {
|
|
// Try NWC first
|
|
if (activeNWC && activeNWC.isConnected) {
|
|
await sendPayment(activeNWC, invoice);
|
|
setPaymentState('success');
|
|
toast({
|
|
title: 'Payment successful!',
|
|
description: `${LISTING_FEE_SATS} sats sent to ${feeRecipientName}`,
|
|
});
|
|
setTimeout(onPaymentSuccess, 1500);
|
|
return;
|
|
}
|
|
|
|
// Try WebLN
|
|
if (webln) {
|
|
if (webln.enable) {
|
|
await webln.enable();
|
|
}
|
|
await webln.sendPayment(invoice);
|
|
setPaymentState('success');
|
|
toast({
|
|
title: 'Payment successful!',
|
|
description: `${LISTING_FEE_SATS} sats sent to ${feeRecipientName}`,
|
|
});
|
|
setTimeout(onPaymentSuccess, 1500);
|
|
return;
|
|
}
|
|
|
|
// No auto-pay available, stay in waiting state
|
|
setPaymentState('waiting');
|
|
} catch (err) {
|
|
console.error('Auto-pay failed:', err);
|
|
// Fall back to manual payment
|
|
setPaymentState('waiting');
|
|
toast({
|
|
title: 'Auto-pay failed',
|
|
description: 'Please pay manually using the QR code or invoice',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
};
|
|
|
|
const copyInvoice = async () => {
|
|
if (!invoice) return;
|
|
try {
|
|
await navigator.clipboard.writeText(invoice);
|
|
toast({ title: 'Invoice copied!' });
|
|
} catch {
|
|
toast({ title: 'Failed to copy', variant: 'destructive' });
|
|
}
|
|
};
|
|
|
|
const openInWallet = () => {
|
|
if (!invoice) return;
|
|
window.open(`lightning:${invoice}`, '_blank');
|
|
};
|
|
|
|
// Show success state
|
|
if (paymentState === 'success') {
|
|
return (
|
|
<Card className="bg-green-950/30 border-2 border-green-500/50">
|
|
<CardContent className="py-8 text-center">
|
|
<CheckCircle className="h-12 w-12 text-green-400 mx-auto mb-4" />
|
|
<h3 className="font-bold text-green-400 text-lg mb-2">Payment Received!</h3>
|
|
<p className="text-gray-300">
|
|
Your {LISTING_FEE_SATS} sat fee has been paid. You can now create your listing.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// Show payment flow
|
|
return (
|
|
<Card className="bg-yellow-950/30 border-2 border-yellow-500/50">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-yellow-400">
|
|
<ShieldAlert className="h-5 w-5" />
|
|
One-Time Listing Fee
|
|
</CardTitle>
|
|
<CardDescription className="text-gray-300">
|
|
As a new member, a small fee is required for your first listing.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* Why this fee exists */}
|
|
<div className="bg-black/30 rounded-lg p-4 space-y-3">
|
|
<h4 className="font-bold text-white text-sm">Why do I need to pay?</h4>
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex items-start gap-2">
|
|
<ShieldAlert className="h-4 w-4 text-yellow-400 mt-0.5 flex-shrink-0" />
|
|
<p className="text-gray-300">
|
|
<span className="text-white font-medium">Spam prevention:</span> This fee stops bots and scammers from flooding the marketplace with fake listings.
|
|
</p>
|
|
</div>
|
|
<div className="flex items-start gap-2">
|
|
<TrendingUp className="h-4 w-4 text-green-400 mt-0.5 flex-shrink-0" />
|
|
<p className="text-gray-300">
|
|
<span className="text-white font-medium">Trust building:</span> New accounts need to prove commitment before listing. After a few successful listings, this fee is waived.
|
|
</p>
|
|
</div>
|
|
<div className="flex items-start gap-2">
|
|
<MapPin className="h-4 w-4 text-[#ff6600] mt-0.5 flex-shrink-0" />
|
|
<p className="text-gray-300">
|
|
<span className="text-white font-medium">Supports the community:</span> Fee goes to{' '}
|
|
<span className="font-semibold text-yellow-400">{feeRecipientName}</span>
|
|
{feeRecipient !== PLATFORM_LIGHTNING_ADDRESS && ' — the person who created this spot!'}
|
|
{feeRecipient === PLATFORM_LIGHTNING_ADDRESS && ' — helping keep SkateSpots running.'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Fee amount */}
|
|
<div className="text-center py-3 bg-yellow-500/10 rounded-lg border border-yellow-500/30">
|
|
<div className="text-3xl font-black text-yellow-400">
|
|
⚡ {LISTING_FEE_SATS.toLocaleString()} sats
|
|
</div>
|
|
<p className="text-sm text-gray-400 mt-1">~${(LISTING_FEE_SATS * 0.0006).toFixed(2)} USD • One-time fee</p>
|
|
</div>
|
|
|
|
{/* Initial state - generate invoice button */}
|
|
{paymentState === 'idle' && (
|
|
<Button
|
|
onClick={generateInvoice}
|
|
className="w-full bg-yellow-500 hover:bg-yellow-600 text-black font-bold"
|
|
>
|
|
<Zap className="h-4 w-4 mr-2" />
|
|
Pay {LISTING_FEE_SATS} sats to Continue
|
|
</Button>
|
|
)}
|
|
|
|
{/* Generating invoice */}
|
|
{paymentState === 'generating' && (
|
|
<div className="text-center py-4">
|
|
<Loader2 className="h-8 w-8 animate-spin mx-auto text-yellow-400" />
|
|
<p className="text-gray-400 mt-2">Generating invoice...</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Paying */}
|
|
{paymentState === 'paying' && (
|
|
<div className="text-center py-4">
|
|
<Loader2 className="h-8 w-8 animate-spin mx-auto text-yellow-400" />
|
|
<p className="text-gray-400 mt-2">Processing payment...</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Waiting for payment - show QR and manual options */}
|
|
{paymentState === 'waiting' && invoice && qrCodeUrl && (
|
|
<div className="space-y-4">
|
|
{/* QR Code */}
|
|
<div className="flex justify-center">
|
|
<div className="bg-white p-3 rounded-lg">
|
|
<img src={qrCodeUrl} alt="Payment QR Code" className="w-48 h-48" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action buttons */}
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<Button
|
|
variant="outline"
|
|
onClick={copyInvoice}
|
|
className="border-yellow-500/50 hover:bg-yellow-500/10"
|
|
>
|
|
<Copy className="h-4 w-4 mr-2" />
|
|
Copy Invoice
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
onClick={openInWallet}
|
|
className="border-yellow-500/50 hover:bg-yellow-500/10"
|
|
>
|
|
<ExternalLink className="h-4 w-4 mr-2" />
|
|
Open Wallet
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Try again with wallet */}
|
|
{(webln || activeNWC) && (
|
|
<Button
|
|
onClick={handleAutoPay}
|
|
className="w-full bg-yellow-500 hover:bg-yellow-600 text-black font-bold"
|
|
>
|
|
<Zap className="h-4 w-4 mr-2" />
|
|
Pay with Connected Wallet
|
|
</Button>
|
|
)}
|
|
|
|
<p className="text-xs text-gray-400 text-center">
|
|
Scan the QR code with a Lightning wallet or use the buttons above
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Error state */}
|
|
{paymentState === 'error' && error && (
|
|
<div className="space-y-4">
|
|
<div className="text-center py-2">
|
|
<p className="text-red-400">{error}</p>
|
|
</div>
|
|
<Button
|
|
onClick={generateInvoice}
|
|
variant="outline"
|
|
className="w-full border-yellow-500/50 hover:bg-yellow-500/10"
|
|
>
|
|
Try Again
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{/* How to avoid future fees */}
|
|
<div className="bg-green-500/10 rounded-lg p-3 border border-green-500/30">
|
|
<p className="text-green-400 font-semibold text-sm mb-2">
|
|
✨ How to skip this fee in the future:
|
|
</p>
|
|
<ul className="text-gray-300 text-sm space-y-1">
|
|
<li className="flex items-center gap-2">
|
|
<span className="text-green-400">→</span>
|
|
Add a skate spot to the map
|
|
</li>
|
|
<li className="flex items-center gap-2">
|
|
<span className="text-green-400">→</span>
|
|
Complete 2-3 successful listings
|
|
</li>
|
|
<li className="flex items-center gap-2">
|
|
<span className="text-green-400">→</span>
|
|
Build your community reputation
|
|
</li>
|
|
</ul>
|
|
<p className="text-xs text-gray-400 mt-2">
|
|
Trusted members get unlimited free listings!
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|