import { useState, useEffect } from 'react'; import { Zap, Copy, Check, ExternalLink, ShieldCheck, HelpCircle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'; import { useToast } from '@/hooks/useToast'; import { isValidNoffer } from '@/lib/types'; import { cn } from '@/lib/utils'; interface ClinkPayButtonProps { /** The CLINK Offer code (noffer1...) */ noffer: string; /** Display name of the recipient, e.g. the provider or shop name */ recipientName?: string; className?: string; /** Button variant — defaults to a subtle outline so it stays secondary */ variant?: 'default' | 'outline' | 'secondary'; size?: 'default' | 'sm' | 'lg'; /** * Friendlier button label. Defaults to "Tip in Bitcoin" — CLINK is jargon, * so we lead with what it does and explain the tech in the tooltip/dialog. */ label?: string; } /** * A self-custodial "Pay via CLINK" button. * * CLINK Offers (noffer1...) are Nostr-native, self-custodial static payment * codes (see https://clinkme.dev). This component does NOT transact — it hands * the code off to a CLINK-compatible wallet via QR, copy, or a nostr: deep link. * It only renders when a valid noffer is present, so it is purely additive and * never replaces the existing Lightning Address / zap flow. */ export function ClinkPayButton({ noffer, recipientName, className, variant = 'outline', size = 'default', label = 'Tip in Bitcoin', }: ClinkPayButtonProps) { const [open, setOpen] = useState(false); const [copied, setCopied] = useState(false); const [qrDataUrl, setQrDataUrl] = useState(null); const { toast } = useToast(); const valid = isValidNoffer(noffer); const nostrUri = `nostr:${noffer}`; useEffect(() => { if (!open || !valid) return; let cancelled = false; (async () => { try { const { default: QRCode } = await import('qrcode'); const url = await QRCode.toDataURL(noffer.toUpperCase(), { width: 260, margin: 2, color: { dark: '#000000', light: '#ffffff' }, }); if (!cancelled) setQrDataUrl(url); } catch { if (!cancelled) setQrDataUrl(null); } })(); return () => { cancelled = true; }; }, [open, noffer, valid]); // Don't render anything if the code is invalid — keeps the feature opt-in. if (!valid) return null; const handleCopy = async () => { try { await navigator.clipboard.writeText(noffer); setCopied(true); setTimeout(() => setCopied(false), 2000); toast({ title: 'Copied', description: 'CLINK code copied to clipboard.' }); } catch { toast({ title: 'Copy failed', description: 'Could not access the clipboard.', variant: 'destructive', }); } }; const shortCode = `${noffer.slice(0, 12)}…${noffer.slice(-6)}`; return ( <>

Pay directly with Bitcoin over Nostr

CLINK is a self-custodial payment code — no app account, no middleman. Scan it with a compatible Lightning wallet and the tip goes straight to them.

{label} {recipientName ? `Send a self-custodial Bitcoin (Lightning) payment to ${recipientName} — no middleman.` : 'Send a self-custodial Bitcoin (Lightning) payment over Nostr — no middleman.'}
{qrDataUrl ? (
CLINK Offer QR code
) : (
)}
{shortCode}

Scan with a CLINK-compatible wallet (e.g. ShockWallet). No custodian, no web server — the invoice is requested directly over Nostr.

); }