SkateSpots/src/hooks/useFeaturedVerification.ts
shakespeare.diy e48a5e6bc5 Add duration tiers + layered NIP-57 zap verification, extend featured/Pro to tours
- Add 4 duration tiers (7d/30d/90d/365d) with per-day price discrimination to usePlatformFee
- PlatformFeePayment: tier selector UI + attempt NIP-57 zap request (a/p tags), graceful fallback to plain LNURL invoice
- Add getZapProviderPubkey + getZapInvoiceFromLightningAddress helpers
- Add useFeaturedVerification hook (verified/claimed/none) querying kind 9735 zap receipts
- ProBadge: optional entityCoord to show green Verified state on detail pages
- Wire entityCoord through all 5 Become* Lab pages + CoachDetailPage
- Add featuredTour fee type; FeaturedUpgrade generalized for events + tours
- CreateTourPage: featured tag on publish + FeaturedUpgrade UI
- TourCard/TourDetailPage: render ProBadge; sort featured tours first
- Document tiers + zap-receipt verification scheme in NIP.md

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-24 16:18:15 +07:00

66 lines
2.2 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import { useNostr } from '@nostrify/react';
import { PLATFORM_OWNER_PUBKEYS } from '@/hooks/useListingRestrictions';
import { isFeaturedActive } from '@/lib/types';
export type FeaturedStatus = 'none' | 'claimed' | 'verified';
/**
* Verify a "featured"/Pro listing against NIP-57 zap receipts.
*
* Layered / best-effort:
* - `verified`: a kind-9735 zap receipt paying the platform (p = platform pubkey)
* and referencing this entity's `a` coordinate was found on relays. Tamper-proof.
* - `claimed`: the event carries an unexpired `featured` tag but no matching zap
* receipt could be found (e.g. the Lightning provider doesn't publish receipts).
* This is the older self-attested trust level — still shown, but not verified.
* - `none`: not featured (or the featured window has expired).
*
* The graceful fallback means Pro never silently breaks when the provider
* (currently minibits) doesn't support zaps.
*/
export function useFeaturedVerification(
entityCoord: string | undefined,
featuredUntil: number | undefined,
) {
const { nostr } = useNostr();
const claimed = isFeaturedActive(featuredUntil);
return useQuery<FeaturedStatus>({
queryKey: ['featured-verification', entityCoord, featuredUntil],
queryFn: async (c) => {
// Not featured at all.
if (!claimed || !entityCoord) return 'none';
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(6000)]);
try {
// Zap receipts paying the platform that reference this entity.
const receipts = await nostr.query(
[
{
kinds: [9735],
'#a': [entityCoord],
'#p': PLATFORM_OWNER_PUBKEYS,
limit: 20,
},
],
{ signal },
);
// Any matching receipt authorizes the featured status.
if (receipts.length > 0) return 'verified';
} catch {
// Relay error — fall through to claimed.
}
// Featured tag present but unverifiable → self-attested.
return 'claimed';
},
enabled: claimed && !!entityCoord,
staleTime: 5 * 60 * 1000, // 5 min
gcTime: 30 * 60 * 1000,
retry: 1,
});
}