- 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>
66 lines
2.2 KiB
TypeScript
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,
|
|
});
|
|
}
|