import { useState, useEffect } from 'react'; import { Zap, Copy, Check, 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 { normalizeClinkCode, parseClinkOffer } from '@/lib/clink'; import { cn } from '@/lib/utils'; import { useTranslation } from '@/contexts/language'; 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, }: ClinkPayButtonProps) { const { t } = useTranslation(); const resolvedLabel = label ?? t('marketUi.clink.tip'); const [open, setOpen] = useState(false); const [copied, setCopied] = useState(false); const [qrDataUrl, setQrDataUrl] = useState(null); const { toast } = useToast(); const offer = parseClinkOffer(noffer); const code = offer?.raw ?? normalizeClinkCode(noffer); useEffect(() => { if (!open || !offer) return; let cancelled = false; (async () => { try { const { default: QRCode } = await import('qrcode'); // Spec: the QR payload is the RAW code — no `nostr:`/URI wrapper. const url = await QRCode.toDataURL(code, { width: 260, margin: 2, color: { dark: '#000000', light: '#ffffff' }, }); if (!cancelled) setQrDataUrl(url); } catch { if (!cancelled) setQrDataUrl(null); } })(); return () => { cancelled = true; }; }, [open, code, offer]); // Don't render anything if the code is invalid — keeps the feature opt-in. if (!offer) return null; const handleCopy = async () => { try { await navigator.clipboard.writeText(code); setCopied(true); setTimeout(() => setCopied(false), 2000); toast({ title: t('common.copied'), description: t('marketUi.clink.copiedBody') }); } catch { toast({ title: t('marketUi.clink.copyFailed'), description: t('marketUi.clink.copyFailedBody'), variant: 'destructive', }); } }; const shortCode = `${code.slice(0, 12)}…${code.slice(-6)}`; // Pricing comes from the decoded TLV (0 fixed / 1 variable / 2 spontaneous). const pricingLabel = offer.priceType === 0 ? offer.priceSats != null ? t('marketUi.clink.priceFixed', { amount: offer.priceSats.toLocaleString() }) : t('marketUi.clink.priceFixedShort') : offer.priceType === 1 ? offer.currency ? t('marketUi.clink.priceVariableCurrency', { currency: offer.currency }) : t('marketUi.clink.priceVariable') : t('marketUi.clink.priceSpontaneous'); return ( <>

{t('marketUi.clink.tooltipTitle')}

{t('marketUi.clink.tooltipBody')}

{resolvedLabel} {recipientName ? t('marketUi.clink.descRecipient', { recipientName }) : t('marketUi.clink.descGeneric')}
{qrDataUrl ? (
{t('marketUi.clink.qrAlt')}
) : (
)}
{shortCode}

{pricingLabel}

{t('marketUi.clink.footnote')}

); }