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'; 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 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: t('common.copied'), description: t('marketUi.clink.copiedBody') }); } catch { toast({ title: t('marketUi.clink.copyFailed'), description: t('marketUi.clink.copyFailedBody'), variant: 'destructive', }); } }; const shortCode = `${noffer.slice(0, 12)}…${noffer.slice(-6)}`; 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}

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

); }