fix(audit P1): comment cache key, ticket verify fails closed, featured amount check, no empty-object persist, sticky sub-bars
This commit is contained in:
parent
f2bfbf4274
commit
5728c226d5
8 changed files with 34 additions and 5 deletions
|
|
@ -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]
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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<string | null> | null = null;
|
||||
|
||||
function cachedProvider(): Promise<string | null> {
|
||||
|
|
@ -45,6 +49,12 @@ export async function verifiedFeaturedCoords(
|
|||
});
|
||||
const set = new Set<string>();
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>).length === 0
|
||||
) return false;
|
||||
|
||||
const prefix = Array.isArray(query.queryKey) ? query.queryKey[0] : query.queryKey;
|
||||
if (typeof prefix !== 'string') return false;
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ export default function SkatersPage() {
|
|||
</div>
|
||||
|
||||
{/* ── Search + filters ── */}
|
||||
<div className="sticky top-0 z-20 bg-black/95 backdrop-blur-md border-b border-white/10">
|
||||
<div className="sticky top-14 md:top-16 z-20 bg-black/95 backdrop-blur-md border-b border-white/10">
|
||||
<div className="container px-4 py-3 max-w-6xl space-y-3">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500 pointer-events-none" />
|
||||
|
|
|
|||
|
|
@ -1070,7 +1070,7 @@ export default function StashPage() {
|
|||
`}</style>
|
||||
|
||||
{/* ── Sticky header ── */}
|
||||
<div className="sticky top-0 z-30 bg-black/96 backdrop-blur-md border-b border-white/8">
|
||||
<div className="sticky top-14 md:top-16 z-30 bg-black/96 backdrop-blur-md border-b border-white/8">
|
||||
<div className="container px-4 max-w-7xl">
|
||||
{/* Title row — wraps on phones so the actions get their own line */}
|
||||
<div className="flex flex-wrap items-center justify-between min-h-14 py-2 gap-x-3 gap-y-2">
|
||||
|
|
|
|||
|
|
@ -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'); }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue