feat(supporters): add silent-payments donation card (BIP-353 name + QR)

This commit is contained in:
SkateSpots 2026-10-05 19:40:38 +07:00
parent 34cf930657
commit 1691d49930
3 changed files with 80 additions and 2 deletions

View file

@ -6500,6 +6500,11 @@ walletUi: {
sourceAngor: 'Angor',
supportersTotals: '{{count}} supporters · {{angor}} sats via Angor · {{zap}} sats zapped',
quietNote: 'Plus everyone who gave quietly — thank you. ❤️',
silentTitle: 'Private on-chain (silent payments)',
silentDesc: 'One name, a fresh address for every payment — on-chain via silent payments, or Lightning from the same record. Nothing to reuse, nothing to link.',
silentCopyName: 'Copy name',
silentCopied: 'Copied',
silentWallets: 'Needs a wallet that supports payment names / silent payments: Sparrow, Cake, Dana, Phoenix, BlueWallet, Wasabi, Nunchuk or BitBox02.',
tiersNote: 'Tiers: Supporter under 100k sats · Patron 100k–1M · Legend 1M+',
tierEmpty: 'No one yet — be the first.',
joinSupporters: 'Join {{count}} supporters',

View file

@ -6500,6 +6500,11 @@ walletUi: {
sourceAngor: 'Angor',
supportersTotals: '{{count}} patrocinadores · {{angor}} sats vía Angor · {{zap}} sats zapeados',
quietNote: 'Y a todos los que aportaron en silencio — gracias. ❤️',
silentTitle: 'On-chain privado (silent payments)',
silentDesc: 'Un nombre, una dirección nueva en cada pago — on-chain con silent payments, o Lightning desde el mismo registro. Sin reutilizar direcciones, nada que enlazar.',
silentCopyName: 'Copiar nombre',
silentCopied: 'Copiado',
silentWallets: 'Requiere una cartera compatible con nombres de pago / silent payments: Sparrow, Cake, Dana, Phoenix, BlueWallet, Wasabi, Nunchuk o BitBox02.',
tiersNote: 'Niveles: Colaborador menos de 100k sats · Patrón 100k–1M · Leyenda 1M+',
tierEmpty: 'Nadie todavía — sé el primero.',
joinSupporters: 'Únete a {{count}} patrocinadores',

View file

@ -1,8 +1,9 @@
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useSeoMeta } from '@unhead/react';
import { Link } from 'react-router-dom';
import { nip19 } from 'nostr-tools';
import { ExternalLink, Zap, Heart, Users, Handshake, Building2, Star, ShieldCheck, Server, Globe, Bot, Code2, Briefcase, Megaphone, TrendingUp } from 'lucide-react';
import QRCode from 'qrcode';
import { ExternalLink, Zap, Heart, Users, Handshake, Building2, Star, ShieldCheck, Server, Globe, Bot, Code2, Briefcase, Megaphone, TrendingUp, Check, Copy } from 'lucide-react';
import { SupportSkateSpots } from '@/components/SupportSkateSpots';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { BACKERS, BACKERS_TOTAL_SATS, tierOf } from '@/lib/backers';
@ -15,6 +16,13 @@ import { useTranslation } from '@/contexts/language';
const ANGOR_PROJECT_URL = 'https://angor.io/project/angor1qtunna00uqyx52rr0nvqa059z2gknhrmn7urd6z';
// Silent-payments "payment name" (BIP-353) → BIP-321 URI with a silent payment
// address (sp) + a BOLT 12 Lightning offer (lno). Resolved from
// skatespots.user._bitcoin-payment.silentpayments.net (2026-10-05). If the name
// is re-pointed, update SILENT_URI from the DNS TXT record.
const SILENT_NAME = 'skatespots@silentpayments.net';
const SILENT_URI = 'bitcoin:?sp=sp1qq2xlxmvh5tpkjw460cfjf5ndz87fc45k0fm3glyalqxq4m436u8nyqa745dyh55pz4vchhjzam5tmlvvlp6whhkqmdk3e7xvfp4y80jhvq40lyl3&lno=lno1pgqppmsrse80qf0aara4slvcjxrvu6j2rp5ftmjy4yntlsmsutpkvkt6878s8y79jwe96rpahf567zyt0lhe26sxpsace8n9u5l359n254e639mqqgpas2cxvhc6ftcr6wrh6nv36mz53qhsfvwrxvgf8mnvq4tsg0z2plqqxvv8geddvc60pys4ja0ajvhhhka2zzfdsnmkvygu872fa0awyqvv8rrpg6k0kg8zpxh8hy3hm9pr35hfa73s92mk38xfqfgfz98v0w34c4z8un6ljxlfjcu69m3de0nqarvh8k8kqqey5kgfmvyfhrg3wltewtj8xn35aevy96dws40hygx7xpfu2k0ty8rm4mv262df8f9q3z2strn4cmh8svss';
// Only count support received from this point (the project's public launch era).
// Zaps to the platform before this are ignored. NOTE: as of 2026-10-05 every
// platform zap is already ≥ 2026-01, so this doesn't currently trim the list —
@ -117,6 +125,63 @@ const PAYS_FOR = [
{ icon: Megaphone, key: 'walletUi.supporters.payMarketing', desc: 'walletUi.supporters.payMarketingDesc' },
] as const;
// ─── Silent-payments donation card (BIP-353 name → BIP-321 URI) ──────────────
function SilentPaymentsCard({ className }: { className?: string }) {
const { t } = useTranslation();
const [qr, setQr] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
useEffect(() => {
let active = true;
QRCode.toDataURL(SILENT_URI, { width: 200, margin: 1 })
.then((url) => { if (active) setQr(url); })
.catch(() => { if (active) setQr(null); });
return () => { active = false; };
}, []);
const copy = async () => {
try {
await navigator.clipboard.writeText(SILENT_NAME);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch { /* clipboard blocked */ }
};
return (
<div className={`rounded-2xl border-2 border-white/15 bg-black/60 p-6 ${className ?? ''}`}>
<div className="flex items-center gap-2.5">
<span className="flex h-9 w-9 items-center justify-center rounded-xl bg-white/5 text-lg">🕵️</span>
<div>
<h3 className="text-lg font-black uppercase leading-tight text-white">
{t('walletUi.supporters.silentTitle')}
</h3>
<p className="text-xs text-gray-400">{t('walletUi.supporters.silentDesc')}</p>
</div>
</div>
<div className="mt-4 flex flex-col items-center gap-4 sm:flex-row sm:items-start">
{qr && (
<img src={qr} alt={SILENT_NAME} className="h-[200px] w-[200px] shrink-0 rounded-xl bg-white p-2" />
)}
<div className="min-w-0 text-center sm:text-left">
<p className="break-all font-mono text-sm text-white">{SILENT_NAME}</p>
<button
onClick={copy}
className="mt-2 inline-flex min-h-0 min-w-0 items-center gap-1.5 rounded-full border border-white/20 px-3 py-1.5 text-xs font-bold text-gray-200 transition-colors hover:bg-white/5"
>
{copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
{copied ? t('walletUi.supporters.silentCopied') : t('walletUi.supporters.silentCopyName')}
</button>
<p className="mt-3 text-[11px] leading-relaxed text-gray-500">
{t('walletUi.supporters.silentWallets')}
</p>
</div>
</div>
</div>
);
}
// ─── Section header ───────────────────────────────────────────────────────────
function SectionHeader({
@ -577,6 +642,9 @@ export default function SupportersPage() {
{/* Lightning rail — the in-app way to chip in (zap publicly or pay quietly). */}
<SupportSkateSpots className="mt-4" />
{/* Silent payments — private on-chain (and Lightning) via a BIP-353 name. */}
<SilentPaymentsCard className="mt-4" />
</section>
{/* ── 2. What your support pays for (words only) ── */}