SkateSpots/src/components/SpamFeePayment.tsx
shakespeare.diy ebbf2d6326 fix: remove orphaned canvas code from RiderCardPage causing ctx/Printer is not defined crash
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>
2026-06-16 17:02:59 +07:00

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>
);
}