/** * 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, SKATE_CREW_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 (
{t('cardUi.verify.disclaimer')}
{body}