From 5728c226d5dec596f5db4a1a46c16133e57837ae Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Mon, 5 Oct 2026 21:07:07 +0700 Subject: [PATCH] fix(audit P1): comment cache key, ticket verify fails closed, featured amount check, no empty-object persist, sticky sub-bars --- src/hooks/usePostComment.ts | 2 +- src/i18n/en.ts | 2 ++ src/i18n/es.ts | 2 ++ src/lib/featuredVerification.ts | 10 ++++++++++ src/lib/queryPersister.ts | 8 ++++++++ src/pages/SkatersPage.tsx | 2 +- src/pages/StashPage.tsx | 2 +- src/pages/VerifyCardPage.tsx | 11 +++++++++-- 8 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/hooks/usePostComment.ts b/src/hooks/usePostComment.ts index 30fe3eb..84a2405 100644 --- a/src/hooks/usePostComment.ts +++ b/src/hooks/usePostComment.ts @@ -85,7 +85,7 @@ export function usePostComment() { onSuccess: (_, { root }) => { // Invalidate and refetch comments queryClient.invalidateQueries({ - queryKey: ['comments', root instanceof URL ? root.toString() : root.id] + queryKey: ['nostr', 'comments', root instanceof URL ? root.toString() : root.id] }); }, }); diff --git a/src/i18n/en.ts b/src/i18n/en.ts index ed4a544..c3a2d99 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -6801,6 +6801,8 @@ walletUi: { noRsvpDesc: 'The holder has no RSVP on record for this event.', admitted: 'Admitted', admittedDesc: 'Organiser’s event + the holder RSVP’d.', + noHolder: 'No holder in the link', + noHolderDesc: 'This QR has no ticket holder (`?h=`), so the holder can’t be checked.', }, passes: { tab: 'My passes', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 61bedc3..f62c438 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -6801,6 +6801,8 @@ walletUi: { noRsvpDesc: 'El titular no tiene confirmación registrada para este evento.', admitted: 'Admitido', admittedDesc: 'Evento del organizador + el titular confirmó.', + noHolder: 'Sin titular en el enlace', + noHolderDesc: 'Este QR no incluye titular (`?h=`), así que no se puede comprobar.', }, passes: { tab: 'Mis pases', diff --git a/src/lib/featuredVerification.ts b/src/lib/featuredVerification.ts index 6a75864..b77866d 100644 --- a/src/lib/featuredVerification.ts +++ b/src/lib/featuredVerification.ts @@ -12,9 +12,13 @@ */ import { PRO_LIGHTNING_ADDRESS } from '@/lib/sasquatchPro'; import { getZapProviderPubkey } from '@/lib/lightningAddress'; +import { getSatoshisFromBolt11 } from '@/lib/bolt11'; import { PLATFORM_OWNER_PUBKEYS } from '@/hooks/useListingRestrictions'; import type { NostrEvent, NostrFilter } from '@nostrify/nostrify'; +/** Minimum paid amount for a "featured" receipt to count (closes the 1-sat self-zap hole). */ +const MIN_FEATURED_SATS = 1000; + let providerPromise: Promise | null = null; function cachedProvider(): Promise { @@ -45,6 +49,12 @@ export async function verifiedFeaturedCoords( }); const set = new Set(); for (const r of receipts) { + // Require a real payment — a receipt's mere presence let anyone self-zap + // 1 sat and buy a "Verified" badge. (The exact tier also depends on the + // expiry; a floor is enough to close the hole.) + const invoice = r.tags.find(([n]) => n === 'bolt11')?.[1]; + const sats = invoice ? getSatoshisFromBolt11(invoice) : 0; + if (!Number.isFinite(sats) || sats < MIN_FEATURED_SATS) continue; for (const [n, v] of r.tags) if (n === 'a' && v) set.add(v); } return set; diff --git a/src/lib/queryPersister.ts b/src/lib/queryPersister.ts index b3fc13b..98a5268 100644 --- a/src/lib/queryPersister.ts +++ b/src/lib/queryPersister.ts @@ -103,6 +103,14 @@ export function shouldDehydrateQuery(query: Query): boolean { // return 0 results, and caching that would wipe the last-known map on the next // cold start — the exact opposite of what persistence is for. if (Array.isArray(query.state.data) && query.state.data.length === 0) return false; + // Never persist an empty plain object either — e.g. `useAuthor` returns `{}` + // when every relay failed, and persisting that would overwrite a good cached + // profile on the next cold start. + if ( + typeof query.state.data === 'object' && + !Array.isArray(query.state.data) && + Object.keys(query.state.data as Record).length === 0 + ) return false; const prefix = Array.isArray(query.queryKey) ? query.queryKey[0] : query.queryKey; if (typeof prefix !== 'string') return false; diff --git a/src/pages/SkatersPage.tsx b/src/pages/SkatersPage.tsx index 8c5ff06..c9fc516 100644 --- a/src/pages/SkatersPage.tsx +++ b/src/pages/SkatersPage.tsx @@ -228,7 +228,7 @@ export default function SkatersPage() { {/* ── Search + filters ── */} -
+
diff --git a/src/pages/StashPage.tsx b/src/pages/StashPage.tsx index 3a2c25b..baea5d1 100644 --- a/src/pages/StashPage.tsx +++ b/src/pages/StashPage.tsx @@ -1070,7 +1070,7 @@ export default function StashPage() { `} {/* ── Sticky header ── */} -
+
{/* Title row — wraps on phones so the actions get their own line */}
diff --git a/src/pages/VerifyCardPage.tsx b/src/pages/VerifyCardPage.tsx index d2988ca..e491cc5 100644 --- a/src/pages/VerifyCardPage.tsx +++ b/src/pages/VerifyCardPage.tsx @@ -162,8 +162,15 @@ export default function VerifyCardPage() { else if (isPark && expiry === 'expired') { tone = 'bad'; label = t('cardUi.v.expired'); sub = t('cardUi.v.expiredDesc'); } else if (isPark && venue?.status === 'mismatch') { tone = 'warn'; label = t('cardUi.v.unverifiedIssuer'); sub = t('cardUi.v.unverifiedIssuerDesc'); } else if (isPark && venue?.status === 'none') { tone = 'warn'; label = t('cardUi.v.notBound'); sub = t('cardUi.v.notBoundDesc'); } - else if (isTicket && holderHex && ticket.data?.status === 'none') { tone = 'warn'; label = t('cardUi.v.noRsvp'); sub = t('cardUi.v.noRsvpDesc'); } - else if (isTicket) { tone = 'good'; label = t('cardUi.v.admitted'); sub = t('cardUi.v.admittedDesc'); } + else if (isTicket) { + // Fail CLOSED: only green when a holder is bound AND their RSVP is positive. + // Missing `?h=`, a loading/errored RSVP, or "none" must not read as admitted. + const rs = ticket.data?.status; + const confirmed = !!holderHex && (rs === 'accepted' || rs === 'going' || rs === 'tentative' || rs === 'interested'); + if (confirmed) { tone = 'good'; label = t('cardUi.v.admitted'); sub = t('cardUi.v.admittedDesc'); } + else if (!holderHex) { tone = 'warn'; label = t('cardUi.v.noHolder'); sub = t('cardUi.v.noHolderDesc'); } + else { tone = 'warn'; label = t('cardUi.v.noRsvp'); sub = t('cardUi.v.noRsvpDesc'); } + } else if (isPark) { tone = 'good'; label = t('cardUi.v.valid'); sub = t('cardUi.v.validDesc'); } else { tone = 'good'; label = t('cardUi.verify.authentic'); sub = t('cardUi.verify.authenticDesc'); } }