/** * VerifyCardPage — /verify/:naddr * * Anyone can open this (scan the QR on a printed card) to check a Park Access * Card or Rider Card: it fetches the event, verifies the signature, and — for a * park pass — confirms the issuer really owns the venue it names. * * It is intentionally honest about what it does and does NOT prove. */ import { useParams, Link } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; import { nip19, verifyEvent } from 'nostr-tools'; import { useSeoMeta } from '@unhead/react'; import { BadgeCheck, ShieldAlert, ShieldX, Loader2, ExternalLink, MapPin } from 'lucide-react'; import { useNostr } from '@/hooks/useNostr'; import { useAuthor } from '@/hooks/useAuthor'; import { useTranslation } from '@/contexts/language'; import { genUserName } from '@/lib/genUserName'; import { RIDER_CARD_KIND, PARK_ACCESS_CARD_KIND } from '@/lib/riderCard'; import { SKATE_SPOT_KIND } from '@/lib/types'; import type { NostrEvent } from '@nostrify/nostrify'; interface ParsedNaddr { kind: number; pubkey: string; identifier: string; relays: string[]; } function parseNaddr(value: string): ParsedNaddr | null { try { const d = nip19.decode(value); if (d.type !== 'naddr') return null; const { kind, pubkey, identifier, relays } = d.data; return { kind, pubkey, identifier, relays: relays ?? [] }; } catch { return null; } } export default function VerifyCardPage() { const { t } = useTranslation(); const { naddr = '' } = useParams<{ naddr: string }>(); const { nostr } = useNostr(); const parsed = parseNaddr(naddr); useSeoMeta({ title: 'Verify a card — SkateSpots', description: 'Verify a SkateSpots Rider Card or Park Access Card.' }); const result = useQuery({ queryKey: ['verify-card', naddr], enabled: !!parsed, staleTime: 60_000, queryFn: async (c) => { if (!parsed) return { status: 'notfound' as const }; const signal = AbortSignal.any([c.signal, AbortSignal.timeout(8000)]); const events = await nostr.query( [{ kinds: [parsed.kind], authors: [parsed.pubkey], '#d': [parsed.identifier], limit: 1 }], { signal }, ); const event = events[0] as NostrEvent | undefined; if (!event) return { status: 'notfound' as const }; if (!verifyEvent(event)) return { status: 'invalid' as const }; // Park pass — confirm the issuer owns the venue it names, when it names one. let venue: { status: 'none' | 'verified' | 'mismatch'; title?: string; coord?: string } = { status: 'none' }; if (event.kind === PARK_ACCESS_CARD_KIND) { const aTag = event.tags.find(([n]) => n === 'a')?.[1]; if (aTag) { venue = { status: 'mismatch', coord: aTag }; const [k, pk, dTag] = aTag.split(':'); if (k === String(SKATE_SPOT_KIND) && pk && dTag) { try { const spots = await nostr.query( [{ kinds: [SKATE_SPOT_KIND], authors: [pk], '#d': [dTag], limit: 1 }], { signal }, ); const spot = spots[0] as NostrEvent | undefined; if (spot) { venue = { status: spot.pubkey === event.pubkey ? 'verified' : 'mismatch', title: spot.tags.find(([n]) => n === 'title')?.[1] || spot.tags.find(([n]) => n === 'name')?.[1], coord: aTag }; } } catch { /* leave mismatch */ } } } } return { status: 'valid' as const, event, venue }; }, }); const author = useAuthor(parsed?.pubkey ?? ''); const issuerName = author.data?.metadata?.name ?? (parsed ? genUserName(parsed.pubkey) : ''); const data = result.data; const valid = data && data.status === 'valid' ? data : undefined; const event = valid?.event; const venue = valid?.venue; return (

VERIFY CARD

{!parsed && ( } title={t('cardUi.verify.notFound')} body={t('cardUi.verify.notFoundDesc')} /> )} {parsed && result.isLoading && (
{t('cardUi.verify.checking')}
)} {parsed && !result.isLoading && data?.status === 'notfound' && ( } title={t('cardUi.verify.notFound')} body={t('cardUi.verify.notFoundDesc')} /> )} {parsed && data?.status === 'invalid' && ( } title={t('cardUi.verify.invalid')} body={t('cardUi.verify.invalidDesc')} /> )} {event && ( <> } title={t('cardUi.verify.authentic')} body={t('cardUi.verify.authenticDesc')} />
{event.kind === PARK_ACCESS_CARD_KIND && ( <> n === 'access-level')?.[1] ?? '—'} /> {event.tags.find(([n]) => n === 'valid-thru')?.[1] && ( n === 'valid-thru')?.[1] ?? ''} /> )} )} {event.kind === RIDER_CARD_KIND && ( )}
{/* Venue binding (park passes) */} {event.kind === PARK_ACCESS_CARD_KIND && venue && (
{t('cardUi.verify.venue')}
{venue.status === 'verified' && (
✓ {t('cardUi.verify.venueVerified', { name: venue.title ?? '' })}
)} {venue.status === 'mismatch' && (
{t('cardUi.verify.venueMismatch')}
)} {venue.status === 'none' && (
{t('cardUi.verify.venueNone')}
)}
)}
{parsed && ( {t('cardUi.verify.viewCard')} )}

{t('cardUi.verify.disclaimer')}

)}
); } function StatusCard({ tone, icon, title, body }: { tone: 'good' | 'warn' | 'bad'; icon: React.ReactNode; title: string; body: string }) { const ring = tone === 'good' ? 'border-green-500/40 bg-green-500/5 text-green-400' : tone === 'warn' ? 'border-amber-500/40 bg-amber-500/5 text-amber-400' : 'border-red-500/40 bg-red-500/5 text-red-400'; return (
{icon}
{title}

{body}

); } function Row({ label, value }: { label: string; value: string }) { return (
{label} {value}
); }