- Expand AI system prompt with full feature map: Lab (coaches/filmers/ photographers/editors/builders + individual vs collective), Crews (map-anchored, how they differ from the Lab), Stash, Tours, Shops, Gear Swap, Rider Card - Document money layer interconnections: wallet rails (Cashu/NWC/WebLN), zap fallback order, nutzaps (NIP-61, receiving), CLINK/noffer, Geyser supporters - Instruct Sasquatch that Crews/Stash/Wallet/Tours/Shops have no live search tool and must be answered from knowledge (prevents bogus tool calls) - Add ~15 new STATIC_FAQ offline answers (crews, stash, builder, nutzap, clink, noffer, tours, shops, sell gear, supporters) - Add Crews page guide context + crew jokes; refresh Lab suggestions - Fix leftover "Zapped with Engorile!" default comment → SkateSpots branding Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
701 lines
23 KiB
TypeScript
701 lines
23 KiB
TypeScript
import { useState, useEffect, useRef, forwardRef } from 'react';
|
||
import { Zap, Copy, Check, ExternalLink, Sparkle, Sparkles, Star, Rocket, ArrowLeft, X, Wallet } from 'lucide-react';
|
||
import { Button } from '@/components/ui/button';
|
||
import { cn } from '@/lib/utils';
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
DialogTrigger,
|
||
} from '@/components/ui/dialog';
|
||
import {
|
||
Drawer,
|
||
DrawerContent,
|
||
DrawerDescription,
|
||
DrawerHeader,
|
||
DrawerTitle,
|
||
DrawerTrigger,
|
||
DrawerClose,
|
||
} from '@/components/ui/drawer';
|
||
import { Input } from '@/components/ui/input';
|
||
import { Label } from '@/components/ui/label';
|
||
import { Textarea } from '@/components/ui/textarea';
|
||
import { Card, CardContent } from '@/components/ui/card';
|
||
import { Separator } from '@/components/ui/separator';
|
||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||
import { useAuthor } from '@/hooks/useAuthor';
|
||
import { useToast } from '@/hooks/useToast';
|
||
import { useZaps } from '@/hooks/useZaps';
|
||
import { useWallet } from '@/hooks/useWallet';
|
||
import { useIsMobile } from '@/hooks/useIsMobile';
|
||
import { useCashu } from '@/hooks/wallet/useCashu';
|
||
import { useLightningOperations } from '@/hooks/wallet/useLightningOperations';
|
||
import { WalletModal } from '@/components/WalletModal';
|
||
import type { Event } from 'nostr-tools';
|
||
// qrcode imported lazily in async function to avoid canvas crash at module load
|
||
import type { WebLNProvider } from "@webbtc/webln-types";
|
||
|
||
interface ZapDialogProps {
|
||
target: Event;
|
||
children?: React.ReactNode;
|
||
className?: string;
|
||
}
|
||
|
||
const presetAmounts = [
|
||
{ amount: 1, icon: Sparkle },
|
||
{ amount: 50, icon: Sparkles },
|
||
{ amount: 100, icon: Zap },
|
||
{ amount: 250, icon: Star },
|
||
{ amount: 1000, icon: Rocket },
|
||
];
|
||
|
||
interface ZapContentProps {
|
||
invoice: string | null;
|
||
amount: number | string;
|
||
comment: string;
|
||
isZapping: boolean;
|
||
qrCodeUrl: string;
|
||
copied: boolean;
|
||
webln: WebLNProvider | null;
|
||
handleZap: () => void;
|
||
handleCopy: () => void;
|
||
openInWallet: () => void;
|
||
setAmount: (amount: number | string) => void;
|
||
setComment: (comment: string) => void;
|
||
inputRef: React.RefObject<HTMLInputElement>;
|
||
zap: (amount: number, comment: string) => void;
|
||
paymentMethod?: 'lightning' | 'cashu';
|
||
setPaymentMethod?: (method: 'lightning' | 'cashu') => void;
|
||
cashuBalance?: number;
|
||
hasCashu?: boolean;
|
||
}
|
||
|
||
// Wallet Setup Screen - shown when user has no wallet connected
|
||
const WalletSetupContent = forwardRef<HTMLDivElement, {
|
||
onContinue: () => void;
|
||
}>(({ onContinue }, ref) => (
|
||
<div className="px-4 py-6 space-y-6" ref={ref}>
|
||
<div className="text-center space-y-3">
|
||
<div className="mx-auto w-16 h-16 rounded-full bg-orange-500/20 flex items-center justify-center">
|
||
<Wallet className="h-8 w-8 text-orange-400" />
|
||
</div>
|
||
<div>
|
||
<h3 className="font-bold text-lg mb-2">Connect Your Wallet First</h3>
|
||
<p className="text-sm text-muted-foreground">
|
||
To send zaps (Bitcoin tips), you need to connect a Lightning wallet.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<Card className="bg-blue-950/30 border-blue-500/50">
|
||
<CardContent className="p-4 space-y-3">
|
||
<div className="flex items-start gap-3">
|
||
<div className="text-2xl">💡</div>
|
||
<div className="flex-1">
|
||
<p className="text-sm font-medium mb-1">Quick Setup (30 seconds)</p>
|
||
<p className="text-xs text-muted-foreground">
|
||
Connect your existing Lightning wallet with Nostr Wallet Connect (NWC). No extra accounts needed.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<div className="space-y-3">
|
||
<WalletModal>
|
||
<Button className="w-full gap-2 bg-orange-500 hover:bg-orange-600" size="lg">
|
||
<Wallet className="h-4 w-4" />
|
||
Connect Wallet with NWC
|
||
</Button>
|
||
</WalletModal>
|
||
|
||
<a
|
||
href="https://planb.academy/en/tutorials/wallet"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="block"
|
||
>
|
||
<Button variant="outline" className="w-full gap-2" size="lg">
|
||
<ExternalLink className="h-4 w-4" />
|
||
Don't have a wallet? Learn how
|
||
</Button>
|
||
</a>
|
||
|
||
<Button
|
||
variant="ghost"
|
||
onClick={onContinue}
|
||
className="w-full text-muted-foreground hover:text-foreground"
|
||
size="sm"
|
||
>
|
||
Skip for now (manual invoice)
|
||
</Button>
|
||
</div>
|
||
|
||
<Card className="bg-yellow-950/30 border-yellow-500/30">
|
||
<CardContent className="p-3">
|
||
<p className="text-xs text-gray-300">
|
||
<strong className="text-yellow-400">ℹ️ Note:</strong> You can still create an invoice without connecting a wallet, but you'll need to copy it manually to pay.
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
));
|
||
WalletSetupContent.displayName = 'WalletSetupContent';
|
||
|
||
// Moved ZapContent outside of ZapDialog to prevent re-renders causing focus loss
|
||
const ZapContent = forwardRef<HTMLDivElement, ZapContentProps>(({
|
||
invoice,
|
||
amount,
|
||
comment,
|
||
isZapping,
|
||
qrCodeUrl,
|
||
copied,
|
||
webln,
|
||
handleZap,
|
||
handleCopy,
|
||
openInWallet,
|
||
setAmount,
|
||
setComment,
|
||
inputRef,
|
||
zap,
|
||
paymentMethod,
|
||
setPaymentMethod,
|
||
cashuBalance,
|
||
hasCashu,
|
||
}, ref) => (
|
||
<div ref={ref}>
|
||
{invoice ? (
|
||
<div className="flex flex-col h-full min-h-0">
|
||
{/* Payment amount display */}
|
||
<div className="text-center pt-4">
|
||
<div className="text-2xl font-bold">{amount} sats</div>
|
||
</div>
|
||
|
||
<Separator className="my-4" />
|
||
|
||
<div className="flex flex-col justify-center min-h-0 flex-1 px-2">
|
||
{/* QR Code */}
|
||
<div className="flex justify-center">
|
||
<Card className="p-3 [@media(max-height:680px)]:max-w-[65vw] max-w-[95vw] mx-auto">
|
||
<CardContent className="p-0 flex justify-center">
|
||
{qrCodeUrl ? (
|
||
<img
|
||
src={qrCodeUrl}
|
||
alt="Lightning Invoice QR Code"
|
||
className="w-full h-auto aspect-square max-w-full object-contain"
|
||
/>
|
||
) : (
|
||
<div className="w-full aspect-square bg-muted animate-pulse rounded" />
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* Invoice input */}
|
||
<div className="space-y-2 mt-4">
|
||
<Label htmlFor="invoice">Lightning Invoice</Label>
|
||
<div className="flex gap-2 min-w-0">
|
||
<Input
|
||
id="invoice"
|
||
value={invoice}
|
||
readOnly
|
||
className="font-mono text-xs min-w-0 flex-1 overflow-hidden text-ellipsis"
|
||
onClick={(e) => e.currentTarget.select()}
|
||
/>
|
||
<Button
|
||
variant="outline"
|
||
size="icon"
|
||
onClick={handleCopy}
|
||
className="shrink-0"
|
||
>
|
||
{copied ? (
|
||
<Check className="h-4 w-4 text-green-600" />
|
||
) : (
|
||
<Copy className="h-4 w-4" />
|
||
)}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Payment buttons */}
|
||
<div className="space-y-3 mt-4">
|
||
{webln && (
|
||
<Button
|
||
onClick={() => {
|
||
const finalAmount = typeof amount === 'string' ? parseInt(amount, 10) : amount;
|
||
zap(finalAmount, comment);
|
||
}}
|
||
disabled={isZapping}
|
||
className="w-full"
|
||
size="lg"
|
||
>
|
||
<Zap className="h-4 w-4 mr-2" />
|
||
{isZapping ? "Processing..." : "Pay with WebLN"}
|
||
</Button>
|
||
)}
|
||
|
||
<Button
|
||
variant="outline"
|
||
onClick={openInWallet}
|
||
className="w-full"
|
||
size="lg"
|
||
>
|
||
<ExternalLink className="h-4 w-4 mr-2" />
|
||
Open in Lightning Wallet
|
||
</Button>
|
||
|
||
<div className="text-xs sm:text-[.65rem] text-muted-foreground text-center">
|
||
Scan the QR code or copy the invoice to pay with any Lightning wallet.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<div className="grid gap-3 px-4 py-4 w-full overflow-hidden">
|
||
{/* Cashu Balance Banner - Show prominently if user has Cashu funds */}
|
||
{hasCashu && cashuBalance !== undefined && cashuBalance > 0 && setPaymentMethod && (
|
||
<Card className={cn(
|
||
"border-2 transition-all cursor-pointer",
|
||
paymentMethod === 'cashu'
|
||
? "bg-orange-500/20 border-orange-500/50"
|
||
: "bg-orange-500/10 border-orange-500/30 hover:bg-orange-500/15"
|
||
)}
|
||
onClick={() => setPaymentMethod('cashu')}
|
||
>
|
||
<CardContent className="p-3">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 rounded-lg bg-orange-500/20 flex items-center justify-center">
|
||
<span className="text-xl">🥜</span>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm font-bold text-orange-400">Cashu Wallet</p>
|
||
<p className="text-xs text-muted-foreground">Pay instantly with your built-in wallet</p>
|
||
</div>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className="text-lg font-bold text-orange-400">{cashuBalance.toLocaleString()}</p>
|
||
<p className="text-[10px] text-muted-foreground uppercase">sats available</p>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Payment Method Selector - show if user has Cashu but might want Lightning too */}
|
||
{hasCashu && cashuBalance !== undefined && setPaymentMethod && (
|
||
<>
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
type="button"
|
||
variant={paymentMethod === 'cashu' ? 'default' : 'ghost'}
|
||
size="sm"
|
||
onClick={() => setPaymentMethod('cashu')}
|
||
className={cn(
|
||
"flex-1 gap-2 h-9",
|
||
paymentMethod === 'cashu' && "bg-orange-500 hover:bg-orange-600"
|
||
)}
|
||
>
|
||
<span>🥜</span>
|
||
<span className="text-xs">Cashu</span>
|
||
</Button>
|
||
<Button
|
||
type="button"
|
||
variant={paymentMethod === 'lightning' ? 'default' : 'ghost'}
|
||
size="sm"
|
||
onClick={() => setPaymentMethod('lightning')}
|
||
className="flex-1 gap-2 h-9"
|
||
>
|
||
<Zap className="h-4 w-4" />
|
||
<span className="text-xs">Lightning</span>
|
||
</Button>
|
||
</div>
|
||
<Separator />
|
||
</>
|
||
)}
|
||
|
||
<ToggleGroup
|
||
type="single"
|
||
value={String(amount)}
|
||
onValueChange={(value) => {
|
||
if (value) {
|
||
setAmount(parseInt(value, 10));
|
||
}
|
||
}}
|
||
className="grid grid-cols-5 gap-1 w-full"
|
||
>
|
||
{presetAmounts.map(({ amount: presetAmount, icon: Icon }) => (
|
||
<ToggleGroupItem
|
||
key={presetAmount}
|
||
value={String(presetAmount)}
|
||
className="flex flex-col h-auto min-w-0 text-xs px-1 py-2"
|
||
>
|
||
<Icon className="h-4 w-4 mb-1" />
|
||
<span className="truncate">{presetAmount}</span>
|
||
</ToggleGroupItem>
|
||
))}
|
||
</ToggleGroup>
|
||
<div className="flex items-center gap-2">
|
||
<div className="h-px flex-1 bg-muted" />
|
||
<span className="text-xs text-muted-foreground">OR</span>
|
||
<div className="h-px flex-1 bg-muted" />
|
||
</div>
|
||
<Input
|
||
ref={inputRef}
|
||
id="custom-amount"
|
||
type="number"
|
||
placeholder="Custom amount"
|
||
value={amount}
|
||
onChange={(e) => setAmount(e.target.value)}
|
||
className="w-full text-sm"
|
||
/>
|
||
<Textarea
|
||
id="custom-comment"
|
||
placeholder="Add a comment (optional)"
|
||
value={comment}
|
||
onChange={(e) => setComment(e.target.value)}
|
||
className="w-full resize-none text-sm"
|
||
rows={2}
|
||
/>
|
||
</div>
|
||
<div className="px-4 pb-4 space-y-2">
|
||
{/* Show insufficient balance warning for Cashu */}
|
||
{paymentMethod === 'cashu' && cashuBalance !== undefined && (
|
||
typeof amount === 'number' && amount > cashuBalance ? (
|
||
<Card className="bg-yellow-950/30 border-yellow-500/30">
|
||
<CardContent className="p-3">
|
||
<p className="text-xs text-yellow-200">
|
||
⚠️ Insufficient Cashu balance. You need {(amount - cashuBalance).toLocaleString()} more sats.
|
||
Switch to Lightning or top up your Cashu wallet.
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
) : null
|
||
)}
|
||
|
||
<Button
|
||
onClick={handleZap}
|
||
className="w-full"
|
||
disabled={
|
||
isZapping ||
|
||
(paymentMethod === 'cashu' && typeof amount === 'number' && cashuBalance !== undefined && amount > cashuBalance)
|
||
}
|
||
size="default"
|
||
>
|
||
{isZapping ? (
|
||
'Creating invoice...'
|
||
) : (
|
||
<>
|
||
{paymentMethod === 'cashu' ? '🥜' : <Zap className="h-4 w-4 mr-2" />}
|
||
Zap {amount} sats {paymentMethod === 'cashu' ? 'with Cashu' : ''}
|
||
</>
|
||
)}
|
||
</Button>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
));
|
||
ZapContent.displayName = 'ZapContent';
|
||
|
||
export function ZapDialog({ target, children, className }: ZapDialogProps) {
|
||
const [open, setOpen] = useState(false);
|
||
const { user } = useCurrentUser();
|
||
const { data: author } = useAuthor(target.pubkey);
|
||
const { toast } = useToast();
|
||
const { webln, activeNWC } = useWallet();
|
||
const { zap, isZapping, invoice, setInvoice } = useZaps(target, webln, activeNWC, () => setOpen(false));
|
||
const [amount, setAmount] = useState<number | string>(100);
|
||
const [comment, setComment] = useState<string>('');
|
||
const [copied, setCopied] = useState(false);
|
||
const [qrCodeUrl, setQrCodeUrl] = useState<string>('');
|
||
const [showWalletSetup, setShowWalletSetup] = useState(false);
|
||
const [hasShownSetup, setHasShownSetup] = useState(false);
|
||
const [paymentMethod, setPaymentMethod] = useState<'lightning' | 'cashu'>('lightning');
|
||
const inputRef = useRef<HTMLInputElement>(null);
|
||
const isMobile = useIsMobile();
|
||
|
||
// Cashu wallet integration
|
||
const { totalBalance: cashuBalance, isInitialized: hasCashu, coco, mints } = useCashu();
|
||
|
||
// Check if user has any wallet connected
|
||
const hasWallet = !!webln || !!activeNWC;
|
||
const hasAnyCashuBalance = hasCashu && cashuBalance > 0;
|
||
|
||
useEffect(() => {
|
||
if (target) {
|
||
setComment('Zapped on SkateSpots! 🛹');
|
||
}
|
||
}, [target]);
|
||
|
||
// Generate QR code
|
||
useEffect(() => {
|
||
let isCancelled = false;
|
||
|
||
const generateQR = async () => {
|
||
if (!invoice) {
|
||
setQrCodeUrl('');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const { default: QRCode } = await import('qrcode');
|
||
const url = await QRCode.toDataURL(invoice.toUpperCase(), {
|
||
width: 512,
|
||
margin: 2,
|
||
color: {
|
||
dark: '#000000',
|
||
light: '#FFFFFF',
|
||
},
|
||
});
|
||
|
||
if (!isCancelled) {
|
||
setQrCodeUrl(url);
|
||
}
|
||
} catch (err) {
|
||
if (!isCancelled) {
|
||
console.error('Failed to generate QR code:', err);
|
||
}
|
||
}
|
||
};
|
||
|
||
generateQR();
|
||
|
||
return () => {
|
||
isCancelled = true;
|
||
};
|
||
}, [invoice]);
|
||
|
||
const handleCopy = async () => {
|
||
if (invoice) {
|
||
await navigator.clipboard.writeText(invoice);
|
||
setCopied(true);
|
||
toast({
|
||
title: 'Invoice copied',
|
||
description: 'Lightning invoice copied to clipboard',
|
||
});
|
||
setTimeout(() => setCopied(false), 2000);
|
||
}
|
||
};
|
||
|
||
const openInWallet = () => {
|
||
if (invoice) {
|
||
const lightningUrl = `lightning:${invoice}`;
|
||
window.open(lightningUrl, '_blank');
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (open) {
|
||
setAmount(100);
|
||
setInvoice(null);
|
||
setCopied(false);
|
||
setQrCodeUrl('');
|
||
|
||
// Show wallet setup screen if no wallet connected and haven't shown it yet
|
||
if (!hasWallet && !hasShownSetup) {
|
||
setShowWalletSetup(true);
|
||
} else {
|
||
setShowWalletSetup(false);
|
||
}
|
||
} else {
|
||
// Clean up state when dialog closes
|
||
setAmount(100);
|
||
setInvoice(null);
|
||
setCopied(false);
|
||
setQrCodeUrl('');
|
||
setShowWalletSetup(false);
|
||
}
|
||
}, [open, setInvoice, hasWallet, hasShownSetup]);
|
||
|
||
const handleZap = async () => {
|
||
const finalAmount = typeof amount === 'string' ? parseInt(amount, 10) : amount;
|
||
|
||
// If Cashu payment method is selected and user has invoice to pay
|
||
if (paymentMethod === 'cashu' && hasCashu && coco && invoice) {
|
||
try {
|
||
// Get first active mint
|
||
const activeMint = mints.find(m => m.url);
|
||
if (!activeMint) {
|
||
throw new Error('No active mint found');
|
||
}
|
||
|
||
// Pay the Lightning invoice using Cashu (create and pay melt quote)
|
||
const meltQuote = await coco.quotes.createMeltQuote(activeMint.url, invoice);
|
||
await coco.quotes.payMeltQuote(activeMint.url, meltQuote.quote);
|
||
|
||
toast({
|
||
title: 'Zap sent!',
|
||
description: `Successfully zapped ${finalAmount} sats from your Cashu wallet`,
|
||
});
|
||
setOpen(false);
|
||
setInvoice(null);
|
||
} catch (error) {
|
||
console.error('Cashu payment error:', error);
|
||
toast({
|
||
variant: 'destructive',
|
||
title: 'Payment failed',
|
||
description: error instanceof Error ? error.message : 'Failed to pay with Cashu wallet',
|
||
});
|
||
}
|
||
} else {
|
||
// Use regular Lightning/NWC zap flow
|
||
zap(finalAmount, comment);
|
||
}
|
||
};
|
||
|
||
const handleContinueWithoutWallet = () => {
|
||
setShowWalletSetup(false);
|
||
setHasShownSetup(true);
|
||
};
|
||
|
||
const contentProps = {
|
||
invoice,
|
||
amount,
|
||
comment,
|
||
isZapping,
|
||
qrCodeUrl,
|
||
copied,
|
||
webln,
|
||
handleZap,
|
||
handleCopy,
|
||
openInWallet,
|
||
setAmount,
|
||
setComment,
|
||
inputRef,
|
||
zap,
|
||
paymentMethod,
|
||
setPaymentMethod,
|
||
cashuBalance,
|
||
hasCashu,
|
||
};
|
||
|
||
if (!user || user.pubkey === target.pubkey || !author?.metadata?.lud06 && !author?.metadata?.lud16) {
|
||
return null;
|
||
}
|
||
|
||
if (isMobile) {
|
||
// Use drawer for entire mobile flow, make it full-screen when showing invoice
|
||
return (
|
||
<Drawer
|
||
open={open}
|
||
onOpenChange={(newOpen) => {
|
||
// Reset invoice when closing
|
||
if (!newOpen) {
|
||
setInvoice(null);
|
||
setQrCodeUrl('');
|
||
}
|
||
setOpen(newOpen);
|
||
}}
|
||
dismissible={true} // Always allow dismissal via drag
|
||
snapPoints={invoice ? [0.5, 0.75, 0.98] : [0.98]}
|
||
activeSnapPoint={invoice ? 0.98 : 0.98}
|
||
modal={true}
|
||
shouldScaleBackground={false}
|
||
fadeFromIndex={0}
|
||
>
|
||
<DrawerTrigger asChild>
|
||
<div className={`cursor-pointer ${className || ''}`}>
|
||
{children}
|
||
</div>
|
||
</DrawerTrigger>
|
||
<DrawerContent
|
||
key={invoice ? 'payment' : 'form'}
|
||
className={cn(
|
||
"transition-all duration-300",
|
||
invoice ? "h-full max-h-screen" : "max-h-[98vh]"
|
||
)}
|
||
data-testid="zap-modal"
|
||
>
|
||
<DrawerHeader className="text-center relative">
|
||
{/* Back button when showing invoice */}
|
||
{invoice && (
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => {
|
||
setInvoice(null);
|
||
setQrCodeUrl('');
|
||
}}
|
||
className="absolute left-4 top-4 flex items-center gap-2"
|
||
>
|
||
<ArrowLeft className="h-4 w-4" />
|
||
</Button>
|
||
)}
|
||
|
||
{/* Close button */}
|
||
<DrawerClose asChild>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="absolute right-4 top-4"
|
||
>
|
||
<X className="h-4 w-4" />
|
||
<span className="sr-only">Close</span>
|
||
</Button>
|
||
</DrawerClose>
|
||
|
||
<DrawerTitle className="text-lg break-words pt-2">
|
||
{showWalletSetup ? 'Connect Wallet' : invoice ? 'Lightning Payment' : 'Send a Zap'}
|
||
</DrawerTitle>
|
||
<DrawerDescription className="text-sm break-words text-center">
|
||
{showWalletSetup ? (
|
||
'Connect your Lightning wallet to send zaps'
|
||
) : invoice ? (
|
||
'Pay with Bitcoin Lightning Network'
|
||
) : (
|
||
'Zaps are small Bitcoin payments that support the creator of this item. If you enjoyed this, consider sending a zap!'
|
||
)}
|
||
</DrawerDescription>
|
||
</DrawerHeader>
|
||
<div className="flex-1 overflow-y-auto px-4 pb-4">
|
||
{showWalletSetup ? (
|
||
<WalletSetupContent onContinue={handleContinueWithoutWallet} />
|
||
) : (
|
||
<ZapContent {...contentProps} />
|
||
)}
|
||
</div>
|
||
</DrawerContent>
|
||
</Drawer>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<Dialog open={open} onOpenChange={setOpen}>
|
||
<DialogTrigger asChild>
|
||
<div className={`cursor-pointer ${className || ''}`}>
|
||
{children}
|
||
</div>
|
||
</DialogTrigger>
|
||
<DialogContent className="w-[calc(100%-1rem)] sm:max-w-[425px] max-h-[90vh] max-h-[90dvh] overflow-hidden p-4 sm:p-6" data-testid="zap-modal">
|
||
<DialogHeader>
|
||
<DialogTitle className="text-base sm:text-lg break-words">
|
||
{showWalletSetup ? 'Connect Wallet' : invoice ? 'Lightning Payment' : 'Send a Zap'}
|
||
</DialogTitle>
|
||
<DialogDescription className="text-xs sm:text-sm text-center break-words">
|
||
{showWalletSetup ? (
|
||
'Connect your Lightning wallet to send zaps'
|
||
) : invoice ? (
|
||
'Pay with Bitcoin Lightning Network'
|
||
) : (
|
||
<>
|
||
Zaps are small Bitcoin payments that support creators. Consider sending a zap!
|
||
</>
|
||
)}
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="overflow-y-auto -mx-4 sm:-mx-6 px-4 sm:px-6">
|
||
{showWalletSetup ? (
|
||
<WalletSetupContent onContinue={handleContinueWithoutWallet} />
|
||
) : (
|
||
<ZapContent {...contentProps} />
|
||
)}
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
}
|